def _check_for_external_axes(config):
    '''
    Check that all `fig` and `ax` kwargs are either ALL None, 
    or ALL are valid instances of Figures/Axes:
 
    An external Axes object can be passed in three places:
        - mpf.plot() `ax=` kwarg
        - mpf.plot() `volume=` kwarg
        - mpf.make_addplot() `ax=` kwarg
    ALL three places MUST be an Axes object, OR
    ALL three places MUST be None.  But it may not be mixed.
    '''
    ap_axlist = []
    addplot = config['addplot']
    if addplot is not None:
        if isinstance(addplot,dict):
            addplot = [addplot,]   # make list of dict to be consistent
        elif not _list_of_dict(addplot):
            raise TypeError('addplot must be `dict`, or `list of dict`, NOT '+str(type(addplot)))
        for apd in addplot:
            ap_axlist.append(apd['ax'])
 
    if len(ap_axlist) > 0:
        if config['ax'] is None:
            if not all([ax is None for ax in ap_axlist]):
                raise ValueError('make_addplot() `ax` kwarg NOT all None, while plot() `ax` kwarg IS None')
        else: # config['ax'] is NOT None:
            if not isinstance(config['ax'],mpl.axes.Axes):
                raise ValueError('plot() ax kwarg must be of type `matplotlib.axis.Axes`')
            if not all([isinstance(ax,mpl.axes.Axes) for ax in ap_axlist]):
                raise ValueError('make_addplot() `ax` kwargs must all be of type `matplotlib.axis.Axes`')

    # At this point, if we have not raised an exception, then plot(ax=) and make_addplot(ax=)
    # are in sync: either they are all None, or they are all of type `matplotlib.axes.Axes`.
    # Therefore we only need plot(ax=), i.e. config['ax'], as we check `volume`: ### and `fig`:

    if config['ax'] is None:
        if isinstance(config['volume'],mpl.axes.Axes):
            raise ValueError('`volume` set to external Axes requires all other Axes be external.')
        #if config['fig'] is not None:
        #    raise ValueError('`fig` kwarg must be None if `ax` kwarg is None.')
    else:
        if not isinstance(config['volume'],mpl.axes.Axes) and config['volume'] != False:
            raise ValueError('`volume` must be of type `matplotlib.axis.Axes`')
        #if not isinstance(config['fig'],mpl.figure.Figure):
        #    raise ValueError('`fig` kwarg must be of type `matplotlib.figure.Figure`')
    
    external_axes_mode = True if isinstance(config['ax'],mpl.axes.Axes) else False
    return external_axes_mode
def plot( data, **kwargs ):
    """
    Given a Pandas DataFrame containing columns Open,High,Low,Close and optionally Volume
    with a DatetimeIndex, plot the data.
    Available plots include ohlc bars, candlestick, and line plots.
    Also provide visually analysis in the form of common technical studies, such as:
    moving averages, renko, etc.
    Also provide ability to plot trading signals, and/or addtional user-defined data.
    """

    config = _process_kwargs(kwargs, _valid_plot_kwargs())

    # translate alias types:
    config['type'] = _get_valid_plot_types(config['type'])
    
    dates,opens,highs,lows,closes,volumes = _check_and_prepare_data(data, config)

    if config['type'] in VALID_PMOVE_TYPES and config['addplot'] is not None:
        err = "`addplot` is not supported for `type='" + config['type'] +"'`"
        raise ValueError(err)

    external_axes_mode = _check_for_external_axes(config)

    if external_axes_mode:
        if config['figscale'] is not None:
            warnings.warn('\n\n ================================================================= '+
                          '\n\n   WARNING: `figscale` has NO effect in External Axes Mode.'+
                          '\n\n ================================================================ ',
                          category=UserWarning)
        if config['figratio'] is not None:
            warnings.warn('\n\n ================================================================= '+
                          '\n\n   WARNING: `figratio` has NO effect in External Axes Mode.'+
                          '\n\n ================================================================ ',
                          category=UserWarning)
        if config['figsize'] is not None:
            warnings.warn('\n\n ================================================================= '+
                          '\n\n   WARNING: `figsize` has NO effect in External Axes Mode.'+
                          '\n\n ================================================================ ',
                          category=UserWarning)
    else:
        if config['figscale'] is None: config['figscale'] = 1.0
        if config['figratio'] is None: config['figratio'] = DEFAULT_FIGRATIO

    style = config['style']

    if external_axes_mode and hasattr(config['ax'],'mpfstyle') and style is None:
        style = config['ax'].mpfstyle
    elif style is None:
        style = 'default'

    if isinstance(style,str):
        style = _styles._get_mpfstyle(style)

    config['style'] = style

    if isinstance(style,dict):
        if not external_axes_mode: _styles._apply_mpfstyle(style)
    else:
        raise TypeError('style should be a `dict`; why is it not?')

    # ----------------------------------------------------------------------
    # TODO:  Add some warnings, or raise an exception, if external_axes_mode
    #        and user is trying to figscale, figratio, or figsize.
    # ----------------------------------------------------------------------

    if not external_axes_mode:
        fig = plt.figure()
        _adjust_figsize(fig,config)
    else:
        fig = None

    if config['volume'] and volumes is None:
        raise ValueError('Request for volume, but NO volume data.')

    if external_axes_mode:
        panels     = None
        if config['volume']:
            volumeAxes = config['volume']
            volumeAxes.set_axisbelow(config['saxbelow'])
    else:
        panels = _build_panels(fig, config)
        volumeAxes = panels.at[config['volume_panel'],'axes'][0] if config['volume'] is True else None

    fmtstring = _determine_format_string( dates, config['datetime_format'] )

    ptype = config['type'] 

    if config['show_nontrading']:
        formatter = mdates.DateFormatter(fmtstring)
        xdates = dates
    else:
        formatter = IntegerIndexDateTimeFormatter(dates, fmtstring)
        xdates = np.arange(len(dates))

    if external_axes_mode:
        axA1 = config['ax']
        axA1.set_axisbelow(config['saxbelow'])
    else:
        axA1 = panels.at[config['main_panel'],'axes'][0]

    # Will have to handle widths config separately for PMOVE types ??
    config['_width_config'] = _determine_width_config(xdates, config)


    rwc = config['return_width_config']
    if isinstance(rwc,dict) and len(rwc)==0:
        config['return_width_config'].update(config['_width_config'])
 

    collections = None
    if ptype == 'line':
        lw = config['_width_config']['line_width']
        axA1.plot(xdates, closes, color=config['linecolor'], linewidth=lw)
    else:
        collections =_construct_mpf_collections(ptype,dates,xdates,opens,highs,lows,closes,volumes,config,style)

    if ptype in VALID_PMOVE_TYPES:
        collections, new_dates, volumes, brick_values, size = collections
        formatter = IntegerIndexDateTimeFormatter(new_dates, fmtstring)
        xdates = np.arange(len(new_dates))

    if collections is not None:
        for collection in collections:
            axA1.add_collection(collection)

    if ptype in VALID_PMOVE_TYPES:
        mavprices = _plot_mav(axA1,config,xdates,brick_values)
    else:
        mavprices = _plot_mav(axA1,config,xdates,closes)

    avg_dist_between_points = (xdates[-1] - xdates[0]) / float(len(xdates))
    if not config['tight_layout']:
        minx = xdates[0]  - avg_dist_between_points
        maxx = xdates[-1] + avg_dist_between_points
    else:
        minx = xdates[0]  - (0.45 * avg_dist_between_points)
        maxx = xdates[-1] + (0.45 * avg_dist_between_points)

    if len(xdates) == 1:  # kludge special case
        minx = minx - 0.75
        maxx = maxx + 0.75
    if ptype not in VALID_PMOVE_TYPES:
        _lows  = lows
        _highs = highs
    else:
        _lows  = brick_values
        _highs = [brick+size for brick in brick_values]

    miny = np.nanmin(_lows)
    maxy = np.nanmax(_highs)

    if config['ylim'] is not None:
        axA1.set_ylim(config['ylim'][0], config['ylim'][1])
    elif config['tight_layout']:
        ydelta = 0.01 * (maxy-miny)
        axA1.set_ylim(miny-ydelta,maxy+ydelta)

    if config['xlim'] is not None:
        axA1.set_xlim(config['xlim'][0], config['xlim'][1])
    elif config['tight_layout']:
        axA1.set_xlim(minx,maxx)

    if (config['ylim'] is None and
        config['xlim'] is None and
        not config['tight_layout']):
        corners = (minx, miny), (maxx, maxy)
        axA1.update_datalim(corners)

    if config['return_calculated_values'] is not None:
        retdict = config['return_calculated_values']
        if ptype in VALID_PMOVE_TYPES:
            prekey = ptype
            retdict[prekey+'_bricks'] = brick_values
            retdict[prekey+'_dates'] = mdates.num2date(new_dates)
            retdict[prekey+'_size'] = size
            if config['volume']:
                retdict[prekey+'_volumes'] = volumes
        if config['mav'] is not None:
            mav = config['mav']
            if len(mav) != len(mavprices):
                warnings.warn('len(mav)='+str(len(mav))+' BUT len(mavprices)='+str(len(mavprices)))
            else:
                for jj in range(0,len(mav)):     
                    retdict['mav' + str(mav[jj])] = mavprices[jj]
        retdict['minx'] = minx
        retdict['maxx'] = maxx
        retdict['miny'] = miny
        retdict['maxy'] = maxy

    # Note: these are NOT mutually exclusive, so the order of this
    #       if/elif is important: VALID_PMOVE_TYPES must be first.
    if ptype in VALID_PMOVE_TYPES:
        dtix = pd.DatetimeIndex([dt for dt in mdates.num2date(new_dates)])
    elif not config['show_nontrading']:
        dtix = data.index
    else:
        dtix = None

    line_collections = []
    line_collections.append(_construct_aline_collections(config['alines'], dtix))
    line_collections.append(_construct_hline_collections(config['hlines'], minx, maxx))
    line_collections.append(_construct_vline_collections(config['vlines'], dtix, miny, maxy))
    tlines = config['tlines']
    if isinstance(tlines,(list,tuple)) and all([isinstance(item,dict) for item in tlines]):
        pass
    else:
        tlines = [tlines,]
    for tline_item in tlines:
        line_collections.append(_construct_tline_collections(tline_item, dtix, dates, opens, highs, lows, closes))
     
    for collection in line_collections:
        if collection is not None:
            axA1.add_collection(collection)

    datalen = len(xdates)
    if config['volume']:
        vup,vdown = style['marketcolors']['volume'].values()
        #-- print('vup,vdown=',vup,vdown)
        vcolors = _updown_colors(vup, vdown, opens, closes, use_prev_close=style['marketcolors']['vcdopcod'])
        #-- print('len(vcolors),len(opens),len(closes)=',len(vcolors),len(opens),len(closes))
        #-- print('vcolors=',vcolors)

        w  = config['_width_config']['volume_width']
        lw = config['_width_config']['volume_linewidth']

        adjc =  _adjust_color_brightness(vcolors,0.90)
        volumeAxes.bar(xdates,volumes,width=w,linewidth=lw,color=vcolors,ec=adjc)
        vymin = 0.3 * np.nanmin(volumes)
        vymax = 1.1 * np.nanmax(volumes)
        volumeAxes.set_ylim(vymin,vymax)

    xrotation = config['xrotation']
    if not external_axes_mode:
        _set_ticks_on_bottom_panel_only(panels,formatter,rotation=xrotation)
    else:
        axA1.tick_params(axis='x',rotation=xrotation)
        axA1.xaxis.set_major_formatter(formatter)

    addplot = config['addplot']
    if addplot is not None and ptype not in VALID_PMOVE_TYPES:
        # NOTE: If in external_axes_mode, then all code relating
        #       to panels and secondary_y becomes irrrelevant.
        #       If the user wants something on a secondary_y then user should
        #       determine that externally, and pass in the appropriate axes.

        if not external_axes_mode:
            # Calculate the Order of Magnitude Range ('mag')
            # If addplot['secondary_y'] == 'auto', then: If the addplot['data']
            # is out of the Order of Magnitude Range, then use secondary_y.

            lo = math.log(max(math.fabs(np.nanmin(lows)),1e-7),10) - 0.5
            hi = math.log(max(math.fabs(np.nanmax(highs)),1e-7),10) + 0.5

            panels['mag'] = [None]*len(panels)  # create 'mag'nitude column

            panels.at[config['main_panel'],'mag'] = {'lo':lo,'hi':hi} # update main panel magnitude range

            if config['volume']:
                lo = math.log(max(math.fabs(np.nanmin(volumes)),1e-7),10) - 0.5
                hi = math.log(max(math.fabs(np.nanmax(volumes)),1e-7),10) + 0.5
                panels.at[config['volume_panel'],'mag'] = {'lo':lo,'hi':hi}

        if isinstance(addplot,dict):
            addplot = [addplot,]   # make list of dict to be consistent

        elif not _list_of_dict(addplot):
            raise TypeError('addplot must be `dict`, or `list of dict`, NOT '+str(type(addplot)))

        for apdict in addplot:

            panid = apdict['panel'] 
            if not external_axes_mode:
                if   panid == 'main' : panid = 0  # for backwards compatibility
                elif panid == 'lower': panid = 1  # for backwards compatibility
                if apdict['y_on_right'] is not None:
                    panels.at[panid,'y_on_right'] = apdict['y_on_right']

            aptype = apdict['type']
            if aptype == 'ohlc' or aptype == 'candle':
                ax = _addplot_collections(panid,panels,apdict,xdates,config)
                _addplot_apply_supplements(ax,apdict)
            else:         
                apdata = apdict['data']
                if isinstance(apdata,list) and not isinstance(apdata[0],(float,int)):
                    raise TypeError('apdata is list but NOT of float or int')
                if isinstance(apdata,pd.DataFrame): 
                    havedf = True
                else:
                    havedf = False      # must be a single series or array
                    apdata = [apdata,]  # make it iterable
                for column in apdata:
                    ydata = apdata.loc[:,column] if havedf else column
                    ax = _addplot_columns(panid,panels,ydata,apdict,xdates,config)
                    _addplot_apply_supplements(ax,apdict)

    # fill_between is NOT supported for external_axes_mode
    # (caller can easily call ax.fill_between() themselves).
    if config['fill_between'] is not None and not external_axes_mode:
        fb    = config['fill_between']
        panid = config['main_panel']
        if isinstance(fb,dict):
            if 'x' in fb:
                raise ValueError('fill_between dict may not contain `x`')
            if 'panel' in fb:
                panid = fb['panel']
                del fb['panel']
        else:
            fb = dict(y1=fb)
        fb['x'] = xdates
        ax = panels.at[panid,'axes'][0]
        ax.fill_between(**fb)
            
    # put the primary axis on one side,
    # and the twinx() on the "other" side:
    if not external_axes_mode:
        for panid,row in panels.iterrows():
            ax = row['axes']
            y_on_right = style['y_on_right'] if row['y_on_right'] is None else row['y_on_right']
            _set_ylabels_side(ax[0],ax[1],y_on_right)
    else:
        y_on_right = style['y_on_right']
        _set_ylabels_side(axA1,None,y_on_right)

    # TODO: ================================================================
    # TODO:  Investigate:
    # TODO:  ===========
    # TODO:  It appears to me that there may be some or significant overlap
    # TODO:  between what the following functions actually do:
    # TODO:  At the very least, all four of them appear to communicate 
    # TODO:  to matplotlib that the xaxis should be treated as dates:
    # TODO:   ->  'ax.autoscale_view()'
    # TODO:   ->  'ax.xaxis_dates()'
    # TODO:   ->  'plt.autofmt_xdates()'
    # TODO:   ->  'fig.autofmt_xdate()'
    # TODO: ================================================================
    

    #if config['autofmt_xdate']:
        #print('CALLING fig.autofmt_xdate()')
        #fig.autofmt_xdate()

    axA1.autoscale_view()  # Is this really necessary??
                           # It appears to me, based on experience coding types 'ohlc' and 'candle'
                           # for `addplot`, that this IS necessary when the only thing done to the
                           # the axes is .add_collection().  (However, if ax.plot() .scatter() or
                           # .bar() was called, then possibly this is not necessary; not entirely
                           # sure, but it definitely was necessary to get 'ohlc' and 'candle' 
                           # working in `addplot`).

    axA1.set_ylabel(config['ylabel'])

    if config['volume']:
        if external_axes_mode:
            volumeAxes.tick_params(axis='x',rotation=xrotation)
            volumeAxes.xaxis.set_major_formatter(formatter)
        vxp = config['volume_exponent']
        if vxp == 'legacy':
            volumeAxes.figure.canvas.draw()  # This is needed to calculate offset
            offset = volumeAxes.yaxis.get_major_formatter().get_offset()
            if len(offset) > 0:
                offset = (' x '+offset)
        elif isinstance(vxp,int) and vxp > 0:
            volumeAxes.ticklabel_format(useOffset=False,scilimits=(vxp,vxp),axis='y')
            offset = '  $10^{'+str(vxp)+'}$'
        elif isinstance(vxp,int) and vxp == 0:
            volumeAxes.ticklabel_format(useOffset=False,style='plain',axis='y')
            offset = ''
        else:
            offset = ''
            scilims = plt.rcParams['axes.formatter.limits']
            if scilims[0] < scilims[1]:
                for power in (5,4,3,2,1):
                    xp = scilims[1]*power
                    if vymax >= 10.**xp:
                        volumeAxes.ticklabel_format(useOffset=False,scilimits=(xp,xp),axis='y')
                        offset = '  $10^{'+str(xp)+'}$'
                        break
            elif scilims[0] == scilims[1] and scilims[1] != 0:
                volumeAxes.ticklabel_format(useOffset=False,scilimits=scilims,axis='y')
                offset = ' $10^'+str(scilims[1])+'$'
        volumeAxes.yaxis.offsetText.set_visible(False)
        if config['ylabel_lower'] is None:
            vol_label = 'Volume'+offset
        else:
            if len(offset) > 0:
                offset = '\n'+offset
            vol_label = config['ylabel_lower'] + offset
        volumeAxes.set_ylabel(vol_label)
    
    if config['title'] is not None:
        if config['tight_layout']:
            # IMPORTANT: 0.89 is based on the top of the top panel
            #            being at 0.18+0.7 = 0.88.  See _panels.py
            # If the value changes there, then it needs to change here.
            title_kwargs = dict(size='x-large',weight='semibold', va='bottom', y=0.89)
        else:
            title_kwargs = dict(size='x-large',weight='semibold', va='center')
        if isinstance(config['title'],dict):
            title_dict = config['title']
            if 'title' not in title_dict:
                raise ValueError('Must have "title" entry in title dict')
            else:
                title = title_dict['title']
                del title_dict['title']
            title_kwargs.update(title_dict)  # allows override default values set by mplfinance above
        else:
            title = config['title']      # config['title'] is a string
        fig.suptitle(title,**title_kwargs)
    
    
    if config['axtitle'] is not None:
        axA1.set_title(config['axtitle'])

    if not external_axes_mode:
        for panid,row in panels.iterrows():
            if not row['used2nd']:
                row['axes'][1].set_visible(False)

    if external_axes_mode:
        return None

    # Should we create a new kwarg to return a flattened axes list
    # versus a list of tuples of primary and secondary axes?
    # For now, for backwards compatibility, we flatten axes list:
    axlist = [ax for axes in panels['axes'] for ax in axes]

    if config['axisoff']:
        for ax in axlist:
            ax.set_axis_off()

    if config['savefig'] is not None:
        save = config['savefig']
        if isinstance(save,dict):
            if config['tight_layout'] and 'bbox_inches' not in save:
                plt.savefig(**save,bbox_inches='tight')
            else:
                plt.savefig(**save)
        else:
            if config['tight_layout']:
                plt.savefig(save,bbox_inches='tight')
            else:
                plt.savefig(save)
        if config['closefig']: # True or 'auto'
            plt.close(fig)
    elif not config['returnfig']:
        plt.show(block=config['block']) # https://stackoverflow.com/a/13361748/1639359 
        if config['closefig'] == True or (config['block'] and config['closefig']):
            plt.close(fig)
    
    if config['returnfig']:
        if config['closefig'] == True: plt.close(fig)
        return (fig, axlist)
Пример #3
0
def plot(data, **kwargs):
    """
    Given a Pandas DataFrame containing columns Open,High,Low,Close and optionally Volume
    with a DatetimeIndex, plot the data.
    Available plots include ohlc bars, candlestick, and line plots.
    Also provide visually analysis in the form of common technical studies, such as:
    moving averages, renko, etc.
    Also provide ability to plot trading signals, and/or addtional user-defined data.
    """

    config = _process_kwargs(kwargs, _valid_plot_kwargs())

    dates, opens, highs, lows, closes, volumes = _check_and_prepare_data(
        data, config)

    if config['type'] in VALID_PMOVE_TYPES and config['addplot'] is not None:
        err = "`addplot` is not supported for `type='" + config['type'] + "'`"
        raise ValueError(err)

    style = config['style']
    if isinstance(style, str):
        style = _styles._get_mpfstyle(style)

    if isinstance(style, dict):
        _styles._apply_mpfstyle(style)

    if config['figsize'] is None:
        w, h = config['figratio']
        r = float(w) / float(h)
        if r < 0.20 or r > 5.0:
            raise ValueError(
                '"figratio" (aspect ratio)  must be between 0.20 and 5.0 (but is '
                + str(r) + ')')
        default_scale = DEFAULT_FIGRATIO[1] / h
        h *= default_scale
        w *= default_scale
        base = (w, h)
        figscale = config['figscale']
        fsize = [d * figscale for d in base]
    else:
        fsize = config['figsize']

    fig = plt.figure()
    fig.set_size_inches(fsize)

    if config['volume'] and volumes is None:
        raise ValueError('Request for volume, but NO volume data.')

    panels = _build_panels(fig, config)

    volumeAxes = panels.at[config['volume_panel'],
                           'axes'][0] if config['volume'] is True else None

    fmtstring = _determine_format_string(dates, config['datetime_format'])

    ptype = config['type']

    if config['show_nontrading']:
        formatter = mdates.DateFormatter(fmtstring)
        xdates = dates
    else:
        formatter = IntegerIndexDateTimeFormatter(dates, fmtstring)
        xdates = np.arange(len(dates))

    axA1 = panels.at[config['main_panel'], 'axes'][0]

    # Will have to handle widths config separately for PMOVE types ??
    config['_width_config'] = _determine_width_config(xdates, config)

    rwc = config['return_width_config']
    if isinstance(rwc, dict) and len(rwc) == 0:
        config['return_width_config'].update(config['_width_config'])

    collections = None
    if ptype == 'line':
        lw = config['_width_config']['line_width']
        axA1.plot(xdates, closes, color=config['linecolor'], linewidth=lw)
    else:
        collections = _construct_mpf_collections(ptype, dates, xdates, opens,
                                                 highs, lows, closes, volumes,
                                                 config, style)

    if ptype in VALID_PMOVE_TYPES:
        collections, new_dates, volumes, brick_values, size = collections
        formatter = IntegerIndexDateTimeFormatter(new_dates, fmtstring)
        xdates = np.arange(len(new_dates))

    if collections is not None:
        for collection in collections:
            axA1.add_collection(collection)

    mavgs = config['mav']
    if mavgs is not None:
        if isinstance(mavgs, int):
            mavgs = mavgs,  # convert to tuple
        if len(mavgs) > 7:
            mavgs = mavgs[0:7]  # take at most 7

        if style['mavcolors'] is not None:
            mavc = cycle(style['mavcolors'])
        else:
            mavc = None

        # Get rcParams['lines.linewidth'] and scale it
        # according to the deinsity of data??

        for mav in mavgs:
            if ptype in VALID_PMOVE_TYPES:
                mavprices = pd.Series(brick_values).rolling(mav).mean().values
            else:
                mavprices = pd.Series(closes).rolling(mav).mean().values

            lw = config['_width_config']['line_width']
            if mavc:
                axA1.plot(xdates, mavprices, linewidth=lw, color=next(mavc))
            else:
                axA1.plot(xdates, mavprices, linewidth=lw)

    avg_dist_between_points = (xdates[-1] - xdates[0]) / float(len(xdates))
    if not config['tight_layout']:
        #print('plot: xdates[-1]=',xdates[-1])
        #print('plot: xdates[ 0]=',xdates[ 0])
        #print('plot: len(xdates)=',len(xdates))
        #print('plot: avg_dist_between_points =',avg_dist_between_points)
        minx = xdates[0] - avg_dist_between_points
        maxx = xdates[-1] + avg_dist_between_points
    else:
        minx = xdates[0] - (0.45 * avg_dist_between_points)
        maxx = xdates[-1] + (0.45 * avg_dist_between_points)

    if len(xdates) == 1:  # kludge special case
        minx = minx - 0.75
        maxx = maxx + 0.75
    if ptype not in VALID_PMOVE_TYPES:
        _lows = lows
        _highs = highs
    else:
        _lows = brick_values
        _highs = [brick + size for brick in brick_values]

    miny = np.nanmin(_lows)
    maxy = np.nanmax(_highs)
    #if len(xdates) > 1:
    #   stdy = (stat.stdev(_lows) + stat.stdev(_highs)) / 2.0
    #else:  # kludge special case
    #   stdy = 0.02 * math.fabs(maxy - miny)
    # print('minx,miny,maxx,maxy,stdy=',minx,miny,maxx,maxy,stdy)

    if config['set_ylim'] is not None:
        axA1.set_ylim(config['set_ylim'][0], config['set_ylim'][1])
    elif config['tight_layout']:
        axA1.set_xlim(minx, maxx)
        ydelta = 0.01 * (maxy - miny)
        axA1.set_ylim(miny - ydelta, maxy + ydelta)
    else:
        corners = (minx, miny), (maxx, maxy)
        axA1.update_datalim(corners)

    if config['return_calculated_values'] is not None:
        retdict = config['return_calculated_values']
        if ptype in VALID_PMOVE_TYPES:
            prekey = ptype
            retdict[prekey + '_bricks'] = brick_values
            retdict[prekey + '_dates'] = mdates.num2date(new_dates)
            retdict[prekey + '_size'] = size
            if config['volume']:
                retdict[prekey + '_volumes'] = volumes
        if mavgs is not None:
            for i in range(0, len(mavgs)):
                retdict['mav' + str(mavgs[i])] = mavprices
        retdict['minx'] = minx
        retdict['maxx'] = maxx
        retdict['miny'] = miny
        retdict['maxy'] = maxy

    # Note: these are NOT mutually exclusive, so the order of this
    #       if/elif is important: VALID_PMOVE_TYPES must be first.
    if ptype in VALID_PMOVE_TYPES:
        dtix = pd.DatetimeIndex([dt for dt in mdates.num2date(new_dates)])
    elif not config['show_nontrading']:
        dtix = data.index
    else:
        dtix = None

    line_collections = []
    line_collections.append(
        _construct_aline_collections(config['alines'], dtix))
    line_collections.append(
        _construct_hline_collections(config['hlines'], minx, maxx))
    line_collections.append(
        _construct_vline_collections(config['vlines'], dtix, miny, maxy))
    tlines = config['tlines']
    if isinstance(tlines, (list, tuple)) and all(
        [isinstance(item, dict) for item in tlines]):
        pass
    else:
        tlines = [
            tlines,
        ]
    for tline_item in tlines:
        line_collections.append(
            _construct_tline_collections(tline_item, dtix, dates, opens, highs,
                                         lows, closes))

    for collection in line_collections:
        if collection is not None:
            axA1.add_collection(collection)

    datalen = len(xdates)
    if config['volume']:
        vup, vdown = style['marketcolors']['volume'].values()
        #-- print('vup,vdown=',vup,vdown)
        vcolors = _updown_colors(
            vup,
            vdown,
            opens,
            closes,
            use_prev_close=style['marketcolors']['vcdopcod'])
        #-- print('len(vcolors),len(opens),len(closes)=',len(vcolors),len(opens),len(closes))
        #-- print('vcolors=',vcolors)

        w = config['_width_config']['volume_width']
        lw = config['_width_config']['volume_linewidth']

        adjc = _adjust_color_brightness(vcolors, 0.90)
        volumeAxes.bar(xdates,
                       volumes,
                       width=w,
                       linewidth=lw,
                       color=vcolors,
                       ec=adjc)
        miny = 0.3 * np.nanmin(volumes)
        maxy = 1.1 * np.nanmax(volumes)
        volumeAxes.set_ylim(miny, maxy)

    xrotation = config['xrotation']
    _set_ticks_on_bottom_panel_only(panels, formatter, rotation=xrotation)

    addplot = config['addplot']
    if addplot is not None and ptype not in VALID_PMOVE_TYPES:
        # Calculate the Order of Magnitude Range ('mag')
        # If addplot['secondary_y'] == 'auto', then: If the addplot['data']
        # is out of the Order of Magnitude Range, then use secondary_y.
        # Calculate omrange for Main panel, and for Lower (volume) panel:
        lo = math.log(max(math.fabs(np.nanmin(lows)), 1e-7), 10) - 0.5
        hi = math.log(max(math.fabs(np.nanmax(highs)), 1e-7), 10) + 0.5

        panels['mag'] = [None] * len(panels)  # create 'mag' column

        panels.at[config['main_panel'], 'mag'] = {
            'lo': lo,
            'hi': hi
        }  # update main panel magnitude range

        if config['volume']:
            lo = math.log(max(math.fabs(np.nanmin(volumes)), 1e-7), 10) - 0.5
            hi = math.log(max(math.fabs(np.nanmax(volumes)), 1e-7), 10) + 0.5
            panels.at[config['volume_panel'], 'mag'] = {'lo': lo, 'hi': hi}

        if isinstance(addplot, dict):
            addplot = [
                addplot,
            ]  # make list of dict to be consistent

        elif not _list_of_dict(addplot):
            raise TypeError('addplot must be `dict`, or `list of dict`, NOT ' +
                            str(type(addplot)))

        for apdict in addplot:
            apdata = apdict['data']
            if isinstance(apdata,
                          list) and not isinstance(apdata[0], (float, int)):
                raise TypeError('apdata is list but NOT of float or int')
            if isinstance(apdata, pd.DataFrame):
                havedf = True
            else:
                havedf = False  # must be a single series or array
                apdata = [
                    apdata,
                ]  # make it iterable

            for column in apdata:
                if havedf:
                    ydata = apdata.loc[:, column]
                else:
                    ydata = column
                yd = [y for y in ydata if not math.isnan(y)]
                ymhi = math.log(max(math.fabs(np.nanmax(yd)), 1e-7), 10)
                ymlo = math.log(max(math.fabs(np.nanmin(yd)), 1e-7), 10)
                secondary_y = False
                panid = apdict['panel']
                if panid == 'main': panid = 0  # for backwards compatibility
                elif panid == 'lower': panid = 1  # for backwards compatibility
                if apdict['secondary_y'] == 'auto':
                    # If mag(nitude) for this panel is not yet set, then set it
                    # here, as this is the first ydata to be plotted on this panel:
                    # i.e. consider this to be the 'primary' axis for this panel.
                    p = panid, 'mag'
                    if panels.at[p] is None:
                        panels.at[p] = {'lo': ymlo, 'hi': ymhi}
                    elif ymlo < panels.at[p]['lo'] or ymhi > panels.at[p]['hi']:
                        secondary_y = True
                    #if secondary_y:
                    #    print('auto says USE secondary_y ... for panel',panid)
                    #else:
                    #    print('auto says do NOT use secondary_y ... for panel',panid)
                else:
                    secondary_y = apdict['secondary_y']
                    #print("apdict['secondary_y'] says secondary_y is",secondary_y)

                if secondary_y:
                    ax = panels.at[panid, 'axes'][1]
                    panels.at[panid, 'used2nd'] = True
                else:
                    ax = panels.at[panid, 'axes'][0]

                if (apdict["ylabel"] is not None):
                    ax.set_ylabel(apdict["ylabel"])

                aptype = apdict['type']
                if aptype == 'scatter':
                    size = apdict['markersize']
                    mark = apdict['marker']
                    color = apdict['color']
                    if isinstance(mark, (list, tuple, np.ndarray)):
                        _mscatter(xdates,
                                  ydata,
                                  ax=ax,
                                  m=mark,
                                  s=size,
                                  color=color)
                    else:
                        ax.scatter(xdates,
                                   ydata,
                                   s=size,
                                   marker=mark,
                                   color=color)
                elif aptype == 'bar':
                    width = apdict['width']
                    bottom = apdict['bottom']
                    color = apdict['color']
                    alpha = apdict['alpha']
                    ax.bar(xdates,
                           ydata,
                           width=width,
                           bottom=bottom,
                           color=color,
                           alpha=alpha)
                elif aptype == 'line':
                    ls = apdict['linestyle']
                    color = apdict['color']
                    ax.plot(xdates, ydata, linestyle=ls, color=color)
                #elif aptype == 'ohlc' or aptype == 'candle':
                # This won't work as is, because here we are looping through one column at a time
                # and mpf_collections needs ohlc columns:
                #    collections =_construct_mpf_collections(aptype,dates,xdates,opens,highs,lows,closes,volumes,config,style)
                #    if len(collections) == 1: collections = [collections]
                #    for collection in collections:
                #        ax.add_collection(collection)
                else:
                    raise ValueError('addplot type "' + str(aptype) +
                                     '" NOT yet supported.')

    if config['fill_between'] is not None:
        fb = config['fill_between']
        panid = config['main_panel']
        if isinstance(fb, dict):
            if 'x' in fb:
                raise ValueError('fill_between dict may not contain `x`')
            if 'panel' in fb:
                panid = fb['panel']
                del fb['panel']
        else:
            fb = dict(y1=fb)
        fb['x'] = xdates
        ax = panels.at[panid, 'axes'][0]
        ax.fill_between(**fb)

    if config['set_ylim_panelB'] is not None:
        miny = config['set_ylim_panelB'][0]
        maxy = config['set_ylim_panelB'][1]
        panels.at[1, 'axes'][0].set_ylim(miny, maxy)

    # put the twinx() on the "other" side:
    if style['y_on_right']:
        for ax in panels['axes'].values:
            ax[0].yaxis.set_label_position('right')
            ax[0].yaxis.tick_right()
            ax[1].yaxis.set_label_position('left')
            ax[1].yaxis.tick_left()
    else:
        for ax in panels['axes'].values:
            ax[0].yaxis.set_label_position('left')
            ax[0].yaxis.tick_left()
            ax[1].yaxis.set_label_position('right')
            ax[1].yaxis.tick_right()

    # TODO: ================================================================
    # TODO:  Investigate:
    # TODO:  ===========
    # TODO:  It appears to me that there may be some or significant overlap
    # TODO:  between what the following functions actually do:
    # TODO:  At the very least, all four of them appear to communicate
    # TODO:  to matplotlib that the xaxis should be treated as dates:
    # TODO:   ->  'ax.autoscale_view()'
    # TODO:   ->  'ax.xaxis_dates()'
    # TODO:   ->  'plt.autofmt_xdates()'
    # TODO:   ->  'fig.autofmt_xdate()'
    # TODO: ================================================================

    #if config['autofmt_xdate']:
    #print('CALLING fig.autofmt_xdate()')
    #fig.autofmt_xdate()

    axA1.autoscale_view()  # Is this really necessary??

    axA1.set_ylabel(config['ylabel'])

    if config['volume']:
        volumeAxes.figure.canvas.draw()  # This is needed to calculate offset
        offset = volumeAxes.yaxis.get_major_formatter().get_offset()
        volumeAxes.yaxis.offsetText.set_visible(False)
        if len(offset) > 0:
            offset = (' x ' + offset)
        if config['ylabel_lower'] is None:
            vol_label = 'Volume' + offset
        else:
            if len(offset) > 0:
                offset = '\n' + offset
            vol_label = config['ylabel_lower'] + offset
        volumeAxes.set_ylabel(vol_label)

    if config['title'] is not None:
        if config['tight_layout']:
            # IMPORTANT: 0.89 is based on the top of the top panel
            #            being at 0.18+0.7 = 0.88.  See _panels.py
            # If the value changes there, then it needs to change here.
            fig.suptitle(config['title'],
                         size='x-large',
                         weight='semibold',
                         va='bottom',
                         y=0.89)
        else:
            fig.suptitle(config['title'],
                         size='x-large',
                         weight='semibold',
                         va='center')

    for panid, row in panels.iterrows():
        if not row['used2nd']:
            row['axes'][1].set_visible(False)

    # Should we create a new kwarg to return a flattened axes list
    # versus a list of tuples of primary and secondary axes?
    # For now, for backwards compatibility, we flatten axes list:
    axlist = [ax for axes in panels['axes'] for ax in axes]

    if config['axisoff']:
        for ax in axlist:
            ax.set_xlim(xdates[0], xdates[-1])
            ax.set_axis_off()

    if config['savefig'] is not None:
        save = config['savefig']
        if isinstance(save, dict):
            # Expand to fill chart if axisoff
            if config['axisoff'] and 'bbox_inches' not in save:
                plt.savefig(**save, bbox_inches='tight')
            else:
                plt.savefig(**save)
        else:
            if config['axisoff']:
                plt.savefig(save, bbox_inches='tight')
            else:
                plt.savefig(save)
        if config['closefig']:  # True or 'auto'
            plt.close(fig)
    elif not config['returnfig']:
        plt.show(block=config['block']
                 )  # https://stackoverflow.com/a/13361748/1639359
        if config['closefig'] == True or (config['block']
                                          and config['closefig']):
            plt.close(fig)

    if config['returnfig']:
        if config['closefig'] == True: plt.close(fig)
        return (fig, axlist)
Пример #4
0
        lo = math.log(max(math.fabs(np.nanmin(lows)),1e-7),10) - 0.5
        hi = math.log(max(math.fabs(np.nanmax(highs)),1e-7),10) + 0.5

        panels['mag'] = [None]*len(panels)  # create 'mag'nitude column

        panels.at[config['main_panel'],'mag'] = {'lo':lo,'hi':hi} # update main panel magnitude range

        if config['volume']:
            lo = math.log(max(math.fabs(np.nanmin(volumes)),1e-7),10) - 0.5
            hi = math.log(max(math.fabs(np.nanmax(volumes)),1e-7),10) + 0.5
            panels.at[config['volume_panel'],'mag'] = {'lo':lo,'hi':hi}

        if isinstance(addplot,dict):
            addplot = [addplot,]   # make list of dict to be consistent

        elif not _list_of_dict(addplot):
            raise TypeError('addplot must be `dict`, or `list of dict`, NOT '+str(type(addplot)))

        for apdict in addplot:

            panid = apdict['panel']
            if   panid == 'main' : panid = 0  # for backwards compatibility
            elif panid == 'lower': panid = 1  # for backwards compatibility

            if apdict['y_on_right'] is not None:
                panels.at[panid,'y_on_right'] = apdict['y_on_right']

            aptype = apdict['type']
            if aptype == 'ohlc' or aptype == 'candle':
                ax = _addplot_collections(panid,panels,apdict,xdates,config)
                if (apdict['ylabel'] is not None):
Пример #5
0
def _build_panels( figure, config ):
    """
    Create and return a DataFrame containing panel information
    and Axes objects for each panel, etc.

    We allow up to 10 panels, identified by their panel id (panid)
    which is an integer 0 through 9.  

    Parameters
    ----------
    figure       : pyplot.Figure
        figure on which to create the Axes for the panels

    config       : dict
        config dict from `mplfinance.plot()`
        
    Config
    ------
    The following items are used from `config`:

    num_panels   : integer (0-9) or None
        number of panels to create

    addplot      : dict or None
        value for the `addplot=` kwarg passed into `mplfinance.plot()`

    volume_panel : integer (0-9) or None
        panel id (0-number_of_panels)

    main_panel   : integer (0-9) or None
        panel id (0-number_of_panels)

    panel_ratios : sequence or None
        sequence of relative sizes for the panels;

        NOTE: If len(panel_ratios) == number of panels (regardless
        of whether number of panels was specified or inferred),
        then panel ratios are the relative sizes of each panel,
        in panel id order, 0 through N (where N = number of panels).

        If len(panel_ratios) != number of panels, then len(panel_ratios)
        must equal 2, and panel_ratios[0] is the relative size for the 'main'
        panel, and panel_ratios[1] is the relative size for all other panels.

        If the number of panels == 1, the panel_ratios is ignored.

    
Returns
    ----------
    panels  : pandas.DataFrame
        dataframe indexed by panel id (panid) and having the following columns:
          axes           : tuple of matplotlib.Axes (primary and secondary) for each column.
          used secondary : bool indicating whether or not the seconday Axes is in use.
          relative size  : height of panel as proportion of sum of all relative sizes

    """

    num_panels   = config['num_panels']
    addplot      = config['addplot']
    volume       = config['volume']
    volume_panel = config['volume_panel']
    num_panels   = config['num_panels']
    main_panel   = config['main_panel']
    panel_ratios = config['panel_ratios']

    if not _valid_panel_id(main_panel):
        raise ValueError('main_panel id must be integer 0 to 9, but is '+str(main_panel))

    if num_panels is None:  # then infer the number of panels:
        pset = {0} # start with a set including only panel zero
        if addplot is not None:
            if isinstance(addplot,dict):
                addplot = [addplot,]   # make list of dict to be consistent
            elif not _list_of_dict(addplot):
                raise TypeError('addplot must be `dict`, or `list of dict`, NOT '+str(type(addplot)))

            backwards_panel_compatibility = {'main':0,'lower':1,'A':0,'B':1,'C':2}

            for apdict in addplot:
                panel = apdict['panel']
                if panel in backwards_panel_compatibility:
                    panel = backwards_panel_compatibility[panel]
                if not _valid_panel_id(panel):
                    raise ValueError('addplot panel must be integer 0 to 9, but is "'+str(panel)+'"')
                pset.add(panel)

        if volume is True:
            if not _valid_panel_id(volume_panel):
                raise ValueError('volume_panel must be integer 0 to 9, but is "'+str(volume_panel)+'"')
            pset.add(volume_panel)

        pset.add(main_panel)

        pset = sorted(pset)
        missing = [m for m in range(len(pset)) if m not in pset]
        if len(missing) != 0:
            raise ValueError('inferred panel list is missing panels: '+str(missing))

    else:
        if not isinstance(num_panels,int) or num_panels < 1 or num_panels > 10:
            raise ValueError('num_panels must be integer 1 to 10, but is "'+str(volume_panel)+'"')
        pset = range(0,num_panels)

    _nones = [None]*len(pset)
    panels = pd.DataFrame(dict(axes=_nones,
                               relsize=_nones,
                               lift=_nones,
                               height=_nones,
                               used2nd=[False]*len(pset),
                               title=_nones,
                               ylabel=_nones),
                          index=pset)
    panels.index.name = 'panid'

    # Now determine the height for each panel:
    # ( figure, num_panels='infer', addplot=None, volume_panel=None, main_panel=0, panel_ratios=None ):

    if panel_ratios is not None:
        if not isinstance(panel_ratios,(list,tuple)):
            raise TypeError('panel_ratios must be a list or tuple')
        if len(panel_ratios) != len(panels) and not (len(panel_ratios)==2 and len(panels) > 2):
            err  = 'len(panel_ratios) must be 2, or must be same as number of panels'
            err += '\nlen(panel_ratios)='+str(len(panel_ratios))+'  num panels='+str(len(panels))
            raise ValueError(err)
        if len(panel_ratios) == 2 and len(panels) > 2:
            pratios = [panel_ratios[1]]*len(panels)
            pratios[main_panel] = panel_ratios[0]
        else:
            pratios = panel_ratios
    else:
        pratios = [2]*len(panels)
        pratios[main_panel] = 5

    panels['relsize'] = pratios
    #print('len(panels)=',len(panels))
    #print('len(pratios)=',len(pratios))

    #print('pratios=')
    #print(pratios)

    #print('panels=')
    #print(panels)
        
    psum = sum(pratios)
    for panid,size in enumerate(pratios):
        panels.at[panid,'height'] = 0.7 * size / psum

    # Now create the Axes:

    for panid,row in panels.iterrows():
        height = row.height
        lift   = panels['height'].loc[panid+1:].sum()
        panels.at[panid,'lift'] = lift
        if panid == 0:
            # rect = [left, bottom, width, height] 
            ax0 = figure.add_axes( [0.15, 0.18+lift, 0.70, height] )
        else:
            ax0 = figure.add_axes( [0.15, 0.18+lift, 0.70, height], sharex=panels.at[0,'axes'][0] )
        ax1 = ax0.twinx()
        ax1.grid(False)
        if config['saxbelow']:      # issue#115 issuecomment-639446764
            ax0.set_axisbelow(True) # so grid does not show through plot data on any panel.
        elif panid == volume_panel:
            ax0.set_axisbelow(True) # so grid does not show through volume bars.
        panels.at[panid,'axes'] = (ax0,ax1)

    return panels
Пример #6
0
def plot(data, **kwargs):
    """
    Given a Pandas DataFrame containing columns Open,High,Low,Close and optionally Volume
    with a DatetimeIndex, plot the data.
    Available plots include ohlc bars, candlestick, and line plots.
    Also provide visually analysis in the form of common technical studies, such as:
    moving averages, renko, etc.
    Also provide ability to plot trading signals, and/or addtional user-defined data.
    """

    config = _process_kwargs(kwargs, _valid_plot_kwargs())

    dates, opens, highs, lows, closes, volumes = _check_and_prepare_data(
        data, config)

    if config['type'] in VALID_PMOVE_TYPES and config['addplot'] is not None:
        err = "`addplot` is not supported for `type='" + config['type'] + "'`"
        raise ValueError(err)

    style = config['style']
    if isinstance(style, str):
        style = _styles._get_mpfstyle(style)

    if isinstance(style, dict):
        _styles._apply_mpfstyle(style)

    w, h = config['figratio']
    r = float(w) / float(h)
    if r < 0.25 or r > 4.0:
        raise ValueError(
            '"figratio" (aspect ratio)  must be between 0.25 and 4.0 (but is '
            + str(r) + ')')
    base = (w, h)
    figscale = config['figscale']
    fsize = [d * figscale for d in base]

    fig = plt.figure()
    fig.set_size_inches(fsize)

    if config['volume'] and volumes is None:
        raise ValueError('Request for volume, but NO volume data.')

    if config['volume']:
        if config['volume'] not in ['B', 'C']: config['volume'] = 'B'

    ha, hb, hc = _determine_relative_panel_heights(config['addplot'],
                                                   config['volume'],
                                                   config['panel_ratio'])

    axA1, axA2, axB1, axB2, axC1, axC2, actual_order = _create_panel_axes(
        fig, ha, hb, hc, config['panel_order'])

    internalAxes = dict(A=(axA1, axA2), B=(axB1, axB2), C=(axC1, axC2))

    volumeAxes = internalAxes[
        config['volume']][0] if config['volume'] else None

    fmtstring = _determine_format_string(dates, config['datetime_format'])

    ptype = config['type']

    if config['show_nontrading']:
        formatter = mdates.DateFormatter(fmtstring)
        xdates = dates
    else:
        formatter = IntegerIndexDateTimeFormatter(dates, fmtstring)
        xdates = np.arange(len(dates))
    axA1.xaxis.set_major_formatter(formatter)

    collections = None
    if ptype == 'line':
        axA1.plot(xdates, closes, color=config['linecolor'])
    else:
        collections = _construct_mpf_collections(ptype, dates, xdates, opens,
                                                 highs, lows, closes, volumes,
                                                 config, style)

    if ptype in VALID_PMOVE_TYPES:
        collections, new_dates, volumes, brick_values, size = collections
        formatter = IntegerIndexDateTimeFormatter(new_dates, fmtstring)
        xdates = np.arange(len(new_dates))
        axA1.xaxis.set_major_formatter(formatter)

    if collections is not None:
        for collection in collections:
            axA1.add_collection(collection)

    mavgs = config['mav']
    if mavgs is not None:
        if isinstance(mavgs, int):
            mavgs = mavgs,  # convert to tuple
        if len(mavgs) > 7:
            mavgs = mavgs[0:7]  # take at most 7

        if style['mavcolors'] is not None:
            mavc = cycle(style['mavcolors'])
        else:
            mavc = None

        for mav in mavgs:
            if ptype in VALID_PMOVE_TYPES:
                mavprices = pd.Series(brick_values).rolling(mav).mean().values
            else:
                mavprices = pd.Series(closes).rolling(mav).mean().values
            if mavc:
                axA1.plot(xdates, mavprices, color=next(mavc))
            else:
                axA1.plot(xdates, mavprices)

    avg_dist_between_points = (xdates[-1] - xdates[0]) / float(len(xdates))
    minx = xdates[0] - avg_dist_between_points
    maxx = xdates[-1] + avg_dist_between_points
    if len(xdates) == 1:  # kludge special case
        minx = minx - 0.75
        maxx = maxx + 0.75
    if ptype not in VALID_PMOVE_TYPES:
        _lows = lows
        _highs = highs
    else:
        _lows = brick_values
        _highs = [brick + size for brick in brick_values]

    miny = np.nanmin(_lows)
    maxy = np.nanmax(_highs)
    #if len(xdates) > 1:
    #   stdy = (stat.stdev(_lows) + stat.stdev(_highs)) / 2.0
    #else:  # kludge special case
    #   stdy = 0.02 * math.fabs(maxy - miny)
    # print('minx,miny,maxx,maxy,stdy=',minx,miny,maxx,maxy,stdy)

    if config['set_ylim'] is not None:
        axA1.set_ylim(config['set_ylim'][0], config['set_ylim'][1])
    else:
        corners = (minx, miny), (maxx, maxy)
        axA1.update_datalim(corners)

    if config['return_calculated_values'] is not None:
        retdict = config['return_calculated_values']
        if ptype in VALID_PMOVE_TYPES:
            prekey = ptype
            retdict[prekey + '_bricks'] = brick_values
            retdict[prekey + '_dates'] = mdates.num2date(new_dates)
            retdict[prekey + '_size'] = size
            if config['volume']:
                retdict[prekey + '_volumes'] = volumes
        if mavgs is not None:
            for i in range(0, len(mavgs)):
                retdict['mav' + str(mavgs[i])] = mavprices
        retdict['minx'] = minx
        retdict['maxx'] = maxx
        retdict['miny'] = miny
        retdict['maxy'] = maxy

    # Note: these are NOT mutually exclusive, so the order of this
    #       if/elif is important: VALID_PMOVE_TYPES must be first.
    if ptype in VALID_PMOVE_TYPES:
        dtix = pd.DatetimeIndex([dt for dt in mdates.num2date(new_dates)])
    elif not config['show_nontrading']:
        dtix = data.index
    else:
        dtix = None

    line_collections = []
    line_collections.append(
        _construct_aline_collections(config['alines'], dtix))
    line_collections.append(
        _construct_hline_collections(config['hlines'], minx, maxx))
    line_collections.append(
        _construct_vline_collections(config['vlines'], dtix, miny, maxy))
    tlines = config['tlines']
    if isinstance(tlines, (list, tuple)) and all(
        [isinstance(item, dict) for item in tlines]):
        pass
    else:
        tlines = [
            tlines,
        ]
    for tline_item in tlines:
        line_collections.append(
            _construct_tline_collections(tline_item, dtix, dates, opens, highs,
                                         lows, closes))

    for collection in line_collections:
        if collection is not None:
            axA1.add_collection(collection)

    if config['volume']:
        vup, vdown = style['marketcolors']['volume'].values()
        #-- print('vup,vdown=',vup,vdown)
        vcolors = _updown_colors(
            vup,
            vdown,
            opens,
            closes,
            use_prev_close=style['marketcolors']['vcdopcod'])
        #-- print('len(vcolors),len(opens),len(closes)=',len(vcolors),len(opens),len(closes))
        #-- print('vcolors=',vcolors)
        width = 0.5 * avg_dist_between_points
        volumeAxes.bar(xdates, volumes, width=width, color=vcolors)
        miny = 0.3 * np.nanmin(volumes)
        maxy = 1.1 * np.nanmax(volumes)
        volumeAxes.set_ylim(miny, maxy)

    xrotation = config['xrotation']
    _adjust_ticklabels_per_bottom_panel(axA1, axB1, axC1, actual_order, hb, hc,
                                        formatter, xrotation)

    used_axA2 = False
    used_axB2 = False
    used_axC2 = False
    addplot = config['addplot']
    if addplot is not None and ptype not in VALID_PMOVE_TYPES:
        # Calculate the Order of Magnitude Range
        # If addplot['secondary_y'] == 'auto', then: If the addplot['data']
        # is out of the Order of Magnitude Range, then use secondary_y.
        # Calculate omrange for Main panel, and for Lower (volume) panel:
        lo = math.log(max(math.fabs(np.nanmin(lows)), 1e-7), 10) - 0.5
        hi = math.log(max(math.fabs(np.nanmax(highs)), 1e-7), 10) + 0.5

        # May 2020: Main panel is now called 'A', and Lower is called 'B'
        omrange = {'A': {'lo': lo, 'hi': hi}, 'B': None, 'C': None}
        if config['volume']:
            lo = math.log(max(math.fabs(np.nanmin(volumes)), 1e-7), 10) - 0.5
            hi = math.log(max(math.fabs(np.nanmax(volumes)), 1e-7), 10) + 0.5
            omrange.update(B={'lo': lo, 'hi': hi})

        if isinstance(addplot, dict):
            addplot = [
                addplot,
            ]  # make list of dict to be consistent

        elif not _list_of_dict(addplot):
            raise TypeError('addplot must be `dict`, or `list of dict`, NOT ' +
                            str(type(addplot)))

        for apdict in addplot:
            apdata = apdict['data']
            if isinstance(apdata,
                          list) and not isinstance(apdata[0], (float, int)):
                raise TypeError('apdata is list but NOT of float or int')
            if isinstance(apdata, pd.DataFrame):
                havedf = True
            else:
                havedf = False  # must be a single series or array
                apdata = [
                    apdata,
                ]  # make it iterable

            for column in apdata:
                if havedf:
                    ydata = apdata.loc[:, column]
                else:
                    ydata = column
                yd = [y for y in ydata if not math.isnan(y)]
                ymhi = math.log(max(math.fabs(np.nanmax(yd)), 1e-7), 10)
                ymlo = math.log(max(math.fabs(np.nanmin(yd)), 1e-7), 10)
                secondary_y = False
                if apdict['secondary_y'] == 'auto':
                    if apdict['panel'] == 'lower' or apdict['panel'] == 'B':
                        # If omrange['lower'] is not yet set by volume,
                        # then set it here as this is the first ydata
                        # to be plotted on the lower panel, so consider
                        # it to be the 'primary' lower panel axis.
                        if omrange['B'] is None:
                            omrange.update(B={'lo': ymlo, 'hi': ymhi})
                        elif ymlo < omrange['B']['lo'] or ymhi > omrange['B'][
                                'hi']:
                            secondary_y = True
                    elif apdict['panel'] == 'C':
                        if omrange['C'] is None:
                            omrange.update(B={'lo': ymlo, 'hi': ymhi})
                        elif ymlo < omrange['C']['lo'] or ymhi > omrange['C'][
                                'hi']:
                            secondary_y = True
                    elif ymlo < omrange['A']['lo'] or ymhi > omrange['A']['hi']:
                        secondary_y = True
                    #   if secondary_y:
                    #       print('auto says USE secondary_y')
                    #   else:
                    #       print('auto says do NOT use secondary_y')
                else:
                    secondary_y = apdict['secondary_y']
                    #   print("apdict['secondary_y'] says secondary_y is",secondary_y)

                if apdict['panel'] == 'lower' or apdict['panel'] == 'B':
                    ax = axB2 if secondary_y else axB1
                elif apdict['panel'] == 'C':
                    ax = axC2 if secondary_y else axC1
                else:
                    ax = axA2 if secondary_y else axA1

                if ax == axA2:
                    used_axA2 = True
                if ax == axB2:
                    used_axB2 = True
                if ax == axC2:
                    used_axC2 = True

                aptype = apdict['type']
                if aptype == 'scatter':
                    size = apdict['markersize']
                    mark = apdict['marker']
                    color = apdict['color']
                    # -------------------------------------------------------- #
                    # This fixes Issue#77, but breaks other stuff:
                    # ax.set_ylim(ymin=(miny - 0.4*stdy),ymax=(maxy + 0.4*stdy))
                    # -------------------------------------------------------- #
                    ax.scatter(xdates, ydata, s=size, marker=mark, color=color)
                elif aptype == 'bar':
                    width = apdict['width']
                    bottom = apdict['bottom']
                    color = apdict['color']
                    alpha = apdict['alpha']
                    ax.bar(xdates,
                           ydata,
                           width=width,
                           bottom=bottom,
                           color=color,
                           alpha=alpha)
                elif aptype == 'line':
                    ls = apdict['linestyle']
                    color = apdict['color']
                    ax.plot(xdates, ydata, linestyle=ls, color=color)
                #elif aptype == 'ohlc' or aptype == 'candle':
                # This won't work as is, because here we are looping through one column at a time
                # and mpf_collections needs ohlc columns:
                #    collections =_construct_mpf_collections(aptype,dates,xdates,opens,highs,lows,closes,volumes,config,style)
                #    if len(collections) == 1: collections = [collections]
                #    for collection in collections:
                #        ax.add_collection(collection)
                else:
                    raise ValueError('addplot type "' + str(aptype) +
                                     '" NOT yet supported.')

    if config['set_ylim_panelB'] is not None:
        miny = config['set_ylim_panelB'][0]
        maxy = config['set_ylim_panelB'][1]
        axB1.set_ylim(miny, maxy)

    if config['set_ylim_panelC'] is not None:
        miny = config['set_ylim_panelC'][0]
        maxy = config['set_ylim_panelC'][1]
        axC1.set_ylim(miny, maxy)

    if config['yscale'] is not None:
        yscale = config['yscale']
        panel = 'A'
        kwargs = None
        if isinstance(yscale, dict):
            if 'panel' in yscale: panel = yscale['panel']
            if 'kwargs' in yscale: kwargs = yscale['kwargs']
            yscale = yscale['yscale']
        ax = internalAxes[panel][0]
        if kwargs is not None:
            ax.set_yscale(yscale, **kwargs)
        else:
            ax.set_yscale(yscale)

    # put the twinx() on the "other" side:
    if style['y_on_right']:
        axA1.yaxis.set_label_position('right')
        axA1.yaxis.tick_right()
        axA2.yaxis.set_label_position('left')
        axA2.yaxis.tick_left()
        if axB1 and axB2:
            axB1.yaxis.set_label_position('right')
            axB1.yaxis.tick_right()
            if axB2 != axB1:
                axB2.yaxis.set_label_position('left')
                axB2.yaxis.tick_left()
    else:
        axA1.yaxis.set_label_position('left')
        axA1.yaxis.tick_left()
        axA2.yaxis.set_label_position('right')
        axA2.yaxis.tick_right()
        if axB1 and axB2:
            axB1.yaxis.set_label_position('left')
            axB1.yaxis.tick_left()
            if axB2 != axB1:
                axB2.yaxis.set_label_position('right')
                axB2.yaxis.tick_right()

    # TODO: ================================================================
    # TODO:  Investigate:
    # TODO:  ===========
    # TODO:  It appears to me that there may be some or significant overlap
    # TODO:  between what the following functions actually do:
    # TODO:  At the very least, all four of them appear to communicate
    # TODO:  to matplotlib that the xaxis should be treated as dates:
    # TODO:   ->  'ax.autoscale_view()'
    # TODO:   ->  'ax.xaxis_dates()'
    # TODO:   ->  'plt.autofmt_xdates()'
    # TODO:   ->  'fig.autofmt_xdate()'
    # TODO: ================================================================

    #if config['autofmt_xdate']:
    #print('CALLING fig.autofmt_xdate()')
    #fig.autofmt_xdate()

    axA1.autoscale_view()  # Is this really necessary??

    axA1.set_ylabel(config['ylabel'])

    if config['volume']:
        volumeAxes.figure.canvas.draw()  # This is needed to calculate offset
        offset = volumeAxes.yaxis.get_major_formatter().get_offset()
        volumeAxes.yaxis.offsetText.set_visible(False)
        if len(offset) > 0:
            offset = (' x ' + offset)
        if config['ylabel_lower'] is None:
            vol_label = 'Volume' + offset
        else:
            if len(offset) > 0:
                offset = '\n' + offset
            vol_label = config['ylabel_lower'] + offset
        volumeAxes.set_ylabel(vol_label)

    if config['title'] is not None:
        fig.suptitle(config['title'], size='x-large', weight='semibold')

    if not used_axA2 and axA2 is not None:
        axA2.get_yaxis().set_visible(False)

    if not used_axB2 and axB2 is not None:
        axB2.get_yaxis().set_visible(False)

    if not used_axC2 and axC2 is not None:
        axC2.get_yaxis().set_visible(False)

    axlist = [axA1, axA2]
    if axB1: axlist.append(axB1)
    if axB2: axlist.append(axB2)
    if axC1: axlist.append(axC1)
    if axC2: axlist.append(axC2)

    if config['axesoffdark']: fig.patch.set_facecolor('black')
    if config['axesoff']: fig.patch.set_visible(False)
    if config['axesoffdark'] or config['axesoff']:
        for ax in axlist:
            ax.set_xlim(xdates[0], xdates[-1])
            ax.set_axis_off()

    if config['savefig'] is not None:
        save = config['savefig']
        if isinstance(save, dict):
            plt.savefig(**save)
        else:
            plt.savefig(save)
        if config['closefig']:
            plt.close(fig)
    elif not config['returnfig']:
        plt.show(block=config['block']
                 )  # https://stackoverflow.com/a/13361748/1639359
        if config['block']:
            plt.close(fig)

    if config['returnfig']:
        return (fig, axlist)