def _addplot_columns(panid,panels,ydata,apdict,xdates,config): external_axes_mode = apdict['ax'] is not None if not external_axes_mode: secondary_y = False if apdict['secondary_y'] == 'auto': 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 = _auto_secondary_y( panels, panid, ymlo, ymhi ) 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] else: ax = apdict['ax'] aptype = apdict['type'] if aptype == 'scatter': size = apdict['markersize'] mark = apdict['marker'] color = apdict['color'] alpha = apdict['alpha'] if isinstance(mark,(list,tuple,np.ndarray)): _mscatter(xdates,ydata,ax=ax,m=mark,s=size,color=color,alpha=alpha) else: ax.scatter(xdates,ydata,s=size,marker=mark,color=color,alpha=alpha) elif aptype == 'bar': width = 0.8 if apdict['width'] is None else 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'] width = apdict['width'] if apdict['width'] is not None else 1.6*config['_width_config']['line_width'] alpha = apdict['alpha'] ax.plot(xdates,ydata,linestyle=ls,color=color,linewidth=width,alpha=alpha) elif aptype == 'step': stepwhere = apdict['stepwhere'] ls = apdict['linestyle'] color = apdict['color'] width = apdict['width'] if apdict['width'] is not None else 1.6*config['_width_config']['line_width'] alpha = apdict['alpha'] ax.step(xdates,ydata,where = stepwhere,linestyle=ls,color=color,linewidth=width,alpha=alpha) else: raise ValueError('addplot type "'+str(aptype)+'" NOT yet supported.') if apdict['mav'] is not None: apmavprices = _plot_mav(ax,config,xdates,ydata,apdict['mav']) return ax
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)