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)
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)
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)
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']:
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)