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)
Example #2
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)
Example #3
0
def plot(data, **kwargs):
    """
    Given open,high,low,close,volume data for a financial instrument (such as a stock, index,
    currency, future, option, etc.) 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, macd, trading envelope, 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.')

    # -------------------------------------------------------------
    # For now (06-Feb-2020) to keep the code somewhat simpler for
    # implementing `secondary_y` we are going to ALWAYS create
    # secondary (twinx) axes, whether we need them or not, and
    # then they will be available to use later when we are plotting:
    # -------------------------------------------------------------

    need_lower_panel = False
    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 apdict in addplot:
            if apdict['panel'] == 'lower':
                need_lower_panel = True
                break

    #  fig.add_axes( [left, bottom, width, height] ) ... numbers are fraction of fig
    if need_lower_panel or config['volume']:
        ax1 = fig.add_axes([0.15, 0.38, 0.70, 0.50])
        ax2 = fig.add_axes([0.15, 0.18, 0.70, 0.20], sharex=ax1)
        plt.xticks(rotation=45)  # must do this after creation of axis, and
        # after `sharex`, but must be BEFORE any 'twinx()'
        ax2.set_axisbelow(True)  # so grid does not show through volume bars.
        ax4 = ax2.twinx()
        ax4.grid(False)
    else:
        ax1 = fig.add_axes([0.15, 0.18, 0.70, 0.70])
        plt.xticks(
            rotation=45
        )  # must do this after creation of axis, but before any 'twinx()'
        ax2 = None
        ax4 = None
    ax3 = ax1.twinx()
    ax3.grid(False)

    avg_days_between_points = (dates[-1] - dates[0]) / float(len(dates))

    # avgerage of 3 or more data points per day we will call intraday data:
    if avg_days_between_points < 0.33:  # intraday
        if mdates.num2date(dates[-1]).date() != mdates.num2date(
                dates[0]).date():
            # intraday data for more than one day:
            fmtstring = '%b %d, %H:%M'
        else:  # intraday data for a single day
            fmtstring = '%H:%M'
    else:  # 'daily' data (or could be weekly, etc.)
        if mdates.num2date(dates[-1]).date().year != mdates.num2date(
                dates[0]).date().year:
            fmtstring = '%Y-%b-%d'
        else:
            fmtstring = '%b %d'

    ptype = config['type']

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

    collections = None
    if ptype == 'candle' or ptype == 'candlestick':
        collections = _construct_candlestick_collections(
            xdates,
            opens,
            highs,
            lows,
            closes,
            marketcolors=style['marketcolors'])
    elif ptype == 'ohlc' or ptype == 'bars' or ptype == 'ohlc_bars':
        collections = _construct_ohlc_collections(
            xdates,
            opens,
            highs,
            lows,
            closes,
            marketcolors=style['marketcolors'])
    elif ptype == 'renko':
        collections, new_dates, volumes, brick_values, size = _construct_renko_collections(
            dates,
            highs,
            lows,
            volumes,
            config['renko_params'],
            closes,
            marketcolors=style['marketcolors'])

    elif ptype == 'pnf':
        collections, new_dates, volumes, brick_values, size = _construct_pointnfig_collections(
            dates,
            highs,
            lows,
            volumes,
            config['pnf_params'],
            closes,
            marketcolors=style['marketcolors'])

    elif ptype == 'line':
        ax1.plot(xdates, closes, color=config['linecolor'])

    else:
        raise ValueError('Unrecognized plot type = "' + ptype + '"')

    if ptype in VALID_PMOVE_TYPES:
        formatter = IntegerIndexDateTimeFormatter(new_dates, fmtstring)
        xdates = np.arange(len(new_dates))
        ax1.xaxis.set_major_formatter(formatter)

    if collections is not None:
        for collection in collections:
            ax1.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:
                ax1.plot(xdates, mavprices, color=next(mavc))
            else:
                ax1.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 = [low for low in lows if low != -1]
        _highs = [high for high in highs if high != -1]
    else:
        _lows = [brick for brick in brick_values]
        _highs = [brick + size for brick in brick_values]

    miny = min(_lows)
    maxy = max(_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:
        ax1.set_ylim(config['set_ylim'][0], config['set_ylim'][1])
    else:
        corners = (minx, miny), (maxx, maxy)
        ax1.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:
            ax1.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
        ax2.bar(xdates, volumes, width=width, color=vcolors)
        if config['set_ylim_panelB'] is None:
            miny = 0.3 * min(volumes)
            maxy = 1.1 * max(volumes)
            ax2.set_ylim(miny, maxy)
        else:
            miny = config['set_ylim_panelB'][0]
            maxy = config['set_ylim_panelB'][1]
            ax2.set_ylim(miny, maxy)
        ax2.xaxis.set_major_formatter(formatter)

    used_ax3 = False
    used_ax4 = 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(min(lows)), 1e-7), 10) - 0.5
        hi = math.log(max(math.fabs(max(highs)), 1e-7), 10) + 0.5
        omrange = {'main': {'lo': lo, 'hi': hi}, 'lower': None}
        if config['volume']:
            lo = math.log(max(math.fabs(min(volumes)), 1e-7), 10) - 0.5
            hi = math.log(max(math.fabs(max(volumes)), 1e-7), 10) + 0.5
            omrange.update(lower={'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(max(yd)), 1e-7), 10)
                ymlo = math.log(max(math.fabs(min(yd)), 1e-7), 10)
                secondary_y = False
                if apdict['secondary_y'] == 'auto':
                    if apdict['panel'] == 'lower':
                        # 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['lower'] is None:
                            omrange.update(lower={'lo': ymlo, 'hi': ymhi})
                        elif ymlo < omrange['lower']['lo'] or ymhi > omrange[
                                'lower']['hi']:
                            secondary_y = True
                    elif ymlo < omrange['main']['lo'] or ymhi > omrange[
                            'main']['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':
                    ax = ax4 if secondary_y else ax2
                else:
                    ax = ax3 if secondary_y else ax1

                if ax == ax3:
                    used_ax3 = True
                if ax == ax4:
                    used_ax4 = True

                if apdict['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)
                else:
                    ls = apdict['linestyle']
                    color = apdict['color']
                    ax.plot(xdates, ydata, linestyle=ls, color=color)

    # put the twinx() on the "other" side:
    if style['y_on_right']:
        ax1.yaxis.set_label_position('right')
        ax1.yaxis.tick_right()
        ax3.yaxis.set_label_position('left')
        ax3.yaxis.tick_left()
        if ax2 and ax4:
            ax2.yaxis.set_label_position('right')
            ax2.yaxis.tick_right()
            if ax4 != ax2:
                ax4.yaxis.set_label_position('left')
                ax4.yaxis.tick_left()
    else:
        ax1.yaxis.set_label_position('left')
        ax1.yaxis.tick_left()
        ax3.yaxis.set_label_position('right')
        ax3.yaxis.tick_right()
        if ax2 and ax4:
            ax2.yaxis.set_label_position('left')
            ax2.yaxis.tick_left()
            if ax4 != ax2:
                ax4.yaxis.set_label_position('right')
                ax4.yaxis.tick_right()

    if need_lower_panel or config['volume']:
        ax1.spines['bottom'].set_linewidth(0.25)
        ax2.spines['top'].set_linewidth(0.25)
        plt.setp(ax1.get_xticklabels(), visible=False)

    # 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()

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

    ax1.set_ylabel(config['ylabel'])

    if config['volume']:
        ax2.figure.canvas.draw()  # This is needed to calculate offset
        offset = ax2.yaxis.get_major_formatter().get_offset()
        ax2.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
        ax2.set_ylabel(vol_label)

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

    if not used_ax3 and ax3 is not None:
        ax3.get_yaxis().set_visible(False)

    if not used_ax4 and ax4 is not None:
        ax4.get_yaxis().set_visible(False)

    if config['returnfig']:
        axlist = [ax1, ax3]
        if ax2: axlist.append(ax2)
        if ax4: axlist.append(ax4)

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

    if config['returnfig']:
        return (fig, axlist)
Example #4
0
        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']:
Example #5
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)