def _determine_width_config( xdates, config ): ''' Given x-axis xdates, and `mpf.plot()` kwargs config, determine the widths and linewidths for candles, volume bars, ohlc bars, etc. ''' datalen = len(xdates) avg_dist_between_points = (xdates[-1] - xdates[0]) / float(datalen) tweak = 1.06 if datalen > 100 else 1.03 adjust = tweak*avg_dist_between_points if config['show_nontrading'] else 1.0 width_config = {} if config['width_adjuster_version'] == 'v0': # Behave like original version of code: width_config['volume_width' ] = 0.5*avg_dist_between_points width_config['volume_linewidth'] = None width_config['ohlc_ticksize' ] = avg_dist_between_points / 2.5 width_config['ohlc_linewidth' ] = None width_config['candle_width' ] = avg_dist_between_points / 2.0 width_config['candle_linewidth'] = None width_config['line_width' ] = None else: # config['width_adjuster_version'] == 'v1' width_config['volume_width' ] = _dfinterpolate(_widths,datalen,'vw' ) * adjust width_config['volume_linewidth'] = _dfinterpolate(_widths,datalen,'vlw') width_config['ohlc_ticksize' ] = _dfinterpolate(_widths,datalen,'ow' ) * adjust width_config['ohlc_linewidth' ] = _dfinterpolate(_widths,datalen,'olw') width_config['candle_width' ] = _dfinterpolate(_widths,datalen,'cw' ) * adjust width_config['candle_linewidth'] = _dfinterpolate(_widths,datalen,'clw') width_config['line_width' ] = _dfinterpolate(_widths,datalen,'lw') if config['scale_width_adjustment'] is not None: scale = _process_kwargs(config['scale_width_adjustment'],_valid_scale_width_kwargs()) if scale['volume'] is not None: width_config['volume_width'] *= scale['volume'] if scale['ohlc'] is not None: width_config['ohlc_ticksize'] *= scale['ohlc'] if scale['candle'] is not None: width_config['candle_width'] *= scale['candle'] if scale['lines'] is not None: width_config['line_width'] *= scale['lines'] if scale['volume_linewidth'] is not None: width_config['volume_linewidth'] *= scale['volume_linewidth'] if scale['ohlc_linewidth'] is not None: width_config['ohlc_linewidth' ] *= scale['ohlc_linewidth'] if scale['candle_linewidth'] is not None: width_config['candle_linewidth'] *= scale['candle_linewidth'] if config['update_width_config'] is not None: update = _process_kwargs(config['update_width_config'],_valid_update_width_kwargs()) uplist = [ (k,v) for k,v in update.items() if v is not None ] width_config.update(uplist) return width_config
def _construct_aline_collections(alines, dtix=None): """construct arbitrary line collections Parameters ---------- alines : sequence sequences of segments, which are sequences of lines, which are sequences of two or more points ( date[time], price ) or (x,y) date[time] may be (a) pandas.to_datetime parseable string, (b) pandas Timestamp, or (c) python datetime.datetime or datetime.date alines may also be a dict, containing the following keys: 'alines' : the same as defined above: sequence of price, or dates, or segments 'colors' : colors for the above alines 'linestyle' : line types for the above alines 'linewidths' : line types for the above alines dtix: date index for the x-axis, used for converting the dates when x-values are 'evenly spaced integers' (as when skipping non-trading days) Returns ------- ret : list lines collections """ if alines is None: return None if isinstance(alines, dict): aconfig = _process_kwargs(alines, _valid_lines_kwargs()) alines = aconfig['alines'] else: aconfig = _process_kwargs({}, _valid_lines_kwargs()) #print('aconfig=',aconfig) #print('alines=',alines) alines = _alines_validator(alines, returnStandardizedValue=True) if alines is None: raise ValueError('Unable to standardize alines value: ' + str(alines)) alines = _convert_segment_dates(alines, dtix) lw = aconfig['linewidths'] co = aconfig['colors'] ls = aconfig['linestyle'] al = aconfig['alpha'] lcollection = LineCollection(alines, colors=co, linewidths=lw, linestyles=ls, antialiaseds=(0, ), alpha=al) return lcollection
def _construct_hline_collections(hlines,minx,maxx): """Construct horizontal lines collection Parameters ---------- hlines : sequence sequence of [price] values at which to draw horizontal lines hlines may also be a dict, containing the following keys: 'hlines' : the same as defined above: sequence of price, or dates, or segments 'colors' : colors for the above hlines 'linestyle' : line types for the above hlines 'linewidths' : line types for the above hlines minx : the minimum value for x for the horizontal line, already converted to `xdates` format maxx : the maximum value for x for the horizontal line, already converted to `xdates` format Returns ------- ret : list lines collections """ if hlines is None: return None #print('_construct_hline_collections() called:', # '\nhlines=',hlines,'\nminx,maxx=',minx,maxx) # hlines do NOT require converting segment dates, because the dates # are not user-specified, but are from already converted minxdt,maxxdt if isinstance(hlines,dict): hconfig = _process_kwargs(hlines, _valid_lines_kwargs()) hlines = hconfig['hlines'] else: hconfig = _process_kwargs({}, _valid_lines_kwargs()) #print('hconfig=',hconfig) #print('hlines=',hlines) lines = [] if not isinstance(hlines,(list,tuple)): hlines = [hlines,] # may be a single price value for val in hlines: lines.append( [(minx,val),(maxx,val)] ) lw = hconfig['linewidths'] co = hconfig['colors'] ls = hconfig['linestyle'] al = hconfig['alpha'] lcollection = LineCollection(lines,colors=co,linewidths=lw,linestyles=ls,antialiaseds=(0,),alpha=al) return lcollection
def make_addplot(data, **kwargs): ''' Take data (pd.Series, pd.DataFrame, np.ndarray of floats, list of floats), and kwargs (see valid_addplot_kwargs_table) and construct a correctly structured dict to be passed into plot() using kwarg `addplot`. NOTE WELL: len(data) here must match the len(data) passed into plot() ''' if not isinstance(data, (pd.Series, pd.DataFrame, np.ndarray, list)): raise TypeError('Wrong type for data, in make_addplot()') config = _process_kwargs(kwargs, _valid_addplot_kwargs()) return dict(data=data, **config)
def make_mpf_style(**kwargs): config = _process_kwargs(kwargs, _valid_make_mpf_style_kwargs()) if config['base_mpf_style'] is not None: style = _get_mpfstyle(config['base_mpf_style']) update = [(k, v) for k, v in config.items() if v is not None] style.update(update) else: style = config if style['marketcolors'] is None: style['marketcolors'] = _styles['default']['marketcolors'] return style
def make_mpf_style( **kwargs ): config = _process_kwargs(kwargs, _valid_make_mpf_style_kwargs()) if config['rc'] is not None and config['legacy_rc'] is not None: raise ValueError('kwargs `rc` and `legacy_rc` may NOT be used together!') # ----------- # March 2021: Found bug that if caller used `base_mpf_style` and `rc` at # the same time, then the caller's `rc` will completely replace the `rc` # of `base_mpf_style`. That was never the intention! Rather it should be # that the caller's `rc` merely adds to and/or modifies the `rc` of the # `base_mpf_style`. In order to provide a path to "backwards compatibility" # for users who may have depended on the bug behavior (callers `rc` replaces # `rc` of `base_mpf_style`) we provide a new kwarg `legacy_rc` which will # now behave the way that `rc` used to behave. # ----------- if config['base_mpf_style'] is not None: style = _get_mpfstyle(config['base_mpf_style']) # Have to handle 'rc' separately, so we don't wipe # out the 'rc' params in the `base_mpf_style` that # are not specified in the `make_mpf_style` config: if config['rc'] is not None: rc = config['rc'] del config['rc'] if isinstance(style['rc'],list): style['rc'] = dict(style['rc']) if style['rc'] is None: style['rc'] = {} style['rc'].update(rc) elif config['legacy_rc'] is not None: config['rc'] = config['legacy_rc'] del config['legacy_rc'] update = [ (k,v) for k,v in config.items() if v is not None ] style.update(update) else: style = config if style['marketcolors'] is None: style['marketcolors'] = _styles['default']['marketcolors'] return style
def _construct_pointnfig_collections(dates, highs, lows, volumes, config_pointnfig_params, closes, marketcolors=None): """Represent the price change with Xs and Os Parameters ---------- dates : sequence sequence of dates highs : sequence sequence of high values lows : sequence sequence of low values config_pointnfig_params : kwargs table (dictionary) box_size : size of each box atr_length : length of time used for calculating atr closes : sequence sequence of closing values marketcolors : dict of colors: up, down, edge, wick, alpha Returns ------- ret : tuple rectCollection """ pointnfig_params = _process_kwargs(config_pointnfig_params, _valid_pointnfig_kwargs()) if marketcolors is None: marketcolors = _get_mpfstyle('classic')['marketcolors'] print('default market colors:', marketcolors) box_size = pointnfig_params['box_size'] atr_length = pointnfig_params['atr_length'] if box_size == 'atr': box_size = _calculate_atr(atr_length, highs, lows, closes) else: # is an integer or float total_atr = _calculate_atr(len(closes) - 1, highs, lows, closes) upper_limit = 5 * total_atr lower_limit = 0.01 * total_atr if box_size > upper_limit: raise ValueError( "Specified box_size may not be larger than (1.5* the Average True Value of the dataset) which has value: " + str(upper_limit)) elif box_size < lower_limit: raise ValueError( "Specified box_size may not be smaller than (0.01* the Average True Value of the dataset) which has value: " + str(lower_limit)) alpha = marketcolors['alpha'] uc = mcolors.to_rgba(marketcolors['candle']['up'], alpha) dc = mcolors.to_rgba(marketcolors['candle']['down'], alpha) tfc = mcolors.to_rgba(marketcolors['edge']['down'], 0) # transparent face color cdiff = [] prev_close_box = closes[0] new_volumes = [ ] # holds the volumes corresponding with the index. If more than one index for the same day then they all have the same volume. new_dates = [] # holds the dates corresponding with the index volume_cache = 0 # holds the volumes for the dates that were skipped prev_sign = 0 current_cdiff_index = -1 for i in range(len(closes) - 1): box_diff = int((closes[i + 1] - prev_close_box) / box_size) if box_diff == 0: if volumes is not None: volume_cache += volumes[i] continue sign = box_diff / abs(box_diff) if sign == prev_sign: cdiff[current_cdiff_index] += box_diff if volumes is not None: new_volumes[current_cdiff_index] += volumes[i] + volume_cache volume_cache = 0 else: cdiff.append(box_diff) if volumes is not None: new_volumes.append(volumes[i] + volume_cache) volume_cache = 0 new_dates.append(dates[i]) prev_sign = sign current_cdiff_index += 1 prev_close_box += box_diff * box_size curr_price = closes[0] box_values = [] # y values for the boxes circle_patches = [ ] # list of circle patches to be used to create the cirCollection line_seg = [] # line segments that make up the Xs for index, difference in enumerate(cdiff): diff = abs(difference) sign = (difference / abs(difference)) # -1 or 1 start_iteration = 0 if sign > 0 else 1 x = [index] * (diff) y = [ curr_price + (i * box_size * sign) for i in range(start_iteration, diff + start_iteration) ] curr_price += (box_size * sign * (diff)) box_values.append(sum(y) / len(y)) for i in range(len(x)): # x and y have the same length height = box_size * 0.85 width = (50 / box_size) / len(new_dates) if height < 0.5: width = height padding = (box_size * 0.075) if sign == 1: # X line_seg.append([(x[i] - width / 2, y[i] + padding), (x[i] + width / 2, y[i] + height + padding) ]) # create / part of the X line_seg.append([(x[i] - width / 2, y[i] + height + padding), (x[i] + width / 2, y[i] + padding) ]) # create \ part of the X else: # O circle_patches.append( Ellipse((x[i], y[i] - (height / 2) - padding), width, height)) useAA = 0, # use tuple here lw = 0.5 cirCollection = PatchCollection(circle_patches) cirCollection.set_facecolor([tfc] * len(circle_patches)) cirCollection.set_edgecolor([dc] * len(circle_patches)) xCollection = LineCollection(line_seg, colors=[uc] * len(line_seg), linewidths=lw, antialiaseds=useAA) return (cirCollection, xCollection), new_dates, new_volumes, box_values, box_size
def _construct_renko_collections(dates, highs, lows, volumes, config_renko_params, closes, marketcolors=None): """Represent the price change with bricks Parameters ---------- dates : sequence sequence of dates highs : sequence sequence of high values lows : sequence sequence of low values config_renko_params : kwargs table (dictionary) brick_size : size of each brick atr_length : length of time used for calculating atr closes : sequence sequence of closing values marketcolors : dict of colors: up, down, edge, wick, alpha Returns ------- ret : tuple rectCollection """ renko_params = _process_kwargs(config_renko_params, _valid_renko_kwargs()) if marketcolors is None: marketcolors = _get_mpfstyle('classic')['marketcolors'] print('default market colors:', marketcolors) brick_size = renko_params['brick_size'] atr_length = renko_params['atr_length'] if brick_size == 'atr': brick_size = _calculate_atr(atr_length, highs, lows, closes) else: # is an integer or float total_atr = _calculate_atr(len(closes) - 1, highs, lows, closes) upper_limit = 1.5 * total_atr lower_limit = 0.01 * total_atr if brick_size > upper_limit: raise ValueError( "Specified brick_size may not be larger than (1.5* the Average True Value of the dataset) which has value: " + str(upper_limit)) elif brick_size < lower_limit: raise ValueError( "Specified brick_size may not be smaller than (0.01* the Average True Value of the dataset) which has value: " + str(lower_limit)) alpha = marketcolors['alpha'] uc = mcolors.to_rgba(marketcolors['candle']['up'], alpha) dc = mcolors.to_rgba(marketcolors['candle']['down'], alpha) euc = mcolors.to_rgba(marketcolors['edge']['up'], 1.0) edc = mcolors.to_rgba(marketcolors['edge']['down'], 1.0) cdiff = [] prev_close_brick = closes[0] for i in range(len(closes) - 1): brick_diff = int((closes[i + 1] - prev_close_brick) / brick_size) cdiff.append(brick_diff) prev_close_brick += brick_diff * brick_size bricks = [] # holds bricks, 1 for down bricks, -1 for up bricks new_dates = [] # holds the dates corresponding with the index new_volumes = [ ] # holds the volumes corresponding with the index. If more than one index for the same day then they all have the same volume. start_price = closes[0] volume_cache = 0 # holds the volumes for the dates that were skipped last_diff_sign = 0 # direction the bricks were last going in -1 -> down, 1 -> up for i in range(len(cdiff)): num_bricks = abs(cdiff[i]) curr_diff_sign = cdiff[i] / abs(cdiff[i]) if cdiff[i] != 0 else 0 if last_diff_sign != 0 and num_bricks > 0 and curr_diff_sign != last_diff_sign: num_bricks -= 1 last_diff_sign = curr_diff_sign if num_bricks != 0: new_dates.extend([dates[i]] * num_bricks) if volumes is not None: # only adds volumes if there are volume values when volume=True if num_bricks != 0: new_volumes.extend([volumes[i] + volume_cache] * num_bricks) volume_cache = 0 else: volume_cache += volumes[i] if cdiff[i] > 0: bricks.extend([1] * num_bricks) else: bricks.extend([-1] * num_bricks) verts = [] colors = [] edge_colors = [] brick_values = [] prev_num = -1 if bricks[0] > 0 else 0 for index, number in enumerate(bricks): if number == 1: # up brick colors.append(uc) edge_colors.append(euc) else: # down brick colors.append(dc) edge_colors.append(edc) prev_num += number brick_y = start_price + (prev_num * brick_size) brick_values.append(brick_y) x, y = index, brick_y verts.append( ((x, y), (x, y + brick_size), (x + 1, y + brick_size), (x + 1, y))) useAA = 0, # use tuple here lw = None rectCollection = PolyCollection(verts, facecolors=colors, antialiaseds=useAA, edgecolors=edge_colors, linewidths=lw) return (rectCollection, ), new_dates, new_volumes, brick_values, brick_size
def plot(data, is_not_set_visible=False, **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" or ptype == "p&f" or ptype == "pointnfigure": ( collections, new_dates, volumes, brick_values, size, ) = _construct_pointnfig_collections( dates, highs, lows, volumes, config["pointnfig_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 = data["Close"].rolling(mav).mean().values if mavc: ax1.plot(xdates, mavprices, color=next(mavc)) else: ax1.plot(xdates, mavprices) if config["return_calculated_values"] is not None: retdict = config["return_calculated_values"] if ptype == "renko": retdict["renko_bricks"] = brick_values retdict["renko_dates"] = mdates.num2date(new_dates) if config["volume"]: retdict["renko_volumes"] = volumes if mavgs is not None: for i in range(0, len(mavgs)): retdict["mav" + str(mavgs[i])] = 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 ptype not in VALID_PMOVE_TYPES: miny = min([low for low in lows if low != -1]) maxy = max([high for high in highs if high != -1]) else: miny = min([brick for brick in brick_values]) maxy = max([brick + size for brick in brick_values]) corners = (minx, miny), (maxx, maxy) ax1.update_datalim(corners) 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) miny = 0.3 * min(volumes) maxy = 1.1 * max(volumes) 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"] 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 is_not_set_visible: # 枠線を消す + x,y軸の数値(目盛り)消す try: ax1.spines["right"].set_color("none") # 右消し ax1.spines["left"].set_color("none") # 左消し ax1.spines["top"].set_color("none") # 上消し ax1.spines["bottom"].set_color("none") # 下消し ax1.axes.xaxis.set_visible(False) ax1.axes.yaxis.set_visible(False) except: pass try: ax2.spines["right"].set_color("none") # 右消し ax2.spines["left"].set_color("none") # 左消し ax2.spines["top"].set_color("none") # 上消し ax2.spines["bottom"].set_color("none") # 下消し ax2.axes.xaxis.set_visible(False) ax2.axes.yaxis.set_visible(False) except: pass try: ax3.spines["right"].set_color("none") # 右消し ax3.spines["left"].set_color("none") # 左消し ax3.spines["top"].set_color("none") # 上消し ax3.spines["bottom"].set_color("none") # 下消し ax3.axes.xaxis.set_visible(False) ax3.axes.yaxis.set_visible(False) except: pass try: ax4.spines["right"].set_color("none") # 右消し ax4.spines["left"].set_color("none") # 左消し ax4.spines["top"].set_color("none") # 上消し ax4.spines["bottom"].set_color("none") # 下消し ax4.axes.xaxis.set_visible(False) ax4.axes.yaxis.set_visible(False) except: pass if config["savefig"] is not None: save = config["savefig"] if isinstance(save, dict): plt.savefig(**save) else: plt.savefig(save, bbox_inches="tight", pad_inches=0) # 余白削除オプション付けた # 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) # メモリ解放 forで回してるとどんどんメモリ食われるので # https://qiita.com/Masahiro_T/items/bdd0482a8efd84cdd270 plt.clf() plt.close()
def _construct_pointnfig_collections(dates, highs, lows, volumes, config_pointnfig_params, closes, marketcolors=None): """Represent the price change with Xs and Os NOTE: this code assumes if any value open, low, high, close is missing they all are missing Algorithm Explanation --------------------- In the first part of the algorithm, we populate the boxes array along with adjusting the dates and volumes arrays into the new_dates and new_volumes arrays. A single date includes a range from no boxes to many boxes, if a date has no boxes it shall not be included in new_dates, and if it has n boxes then it will be included n times. Volumes use a volume cache to save volume amounts for dates that do not have any boxes before adding the cache to the next date that has at least one box. We populate the boxes array with each close values difference from the previously created brick divided by the box size. The second part of the algorithm has a series of step. First we combine the adjacent like signed values in the boxes array (ex. [-1, -2, 3, -4] -> [-3, 3, -4]). Next we subtract 1 from the absolute value of each element in boxes except the first to ensure every time there is a trend change (ex. previous box is an X, current brick is a O) we draw one less box to account for the price having to move the previous box's amount before creating a box in the opposite direction. Next we adjust volume and dates to combine volume into non 0 box indexes and to only use dates from non 0 box indexes. We then remove all 0s from the boxes array and once again combine adjacent similarly signed differences in boxes. Lastly, we enumerate through the boxes to populate the line_seg and circle_patches arrays. line_seg holds the / and \ line segments that make up an X and circle_patches holds matplotlib.patches Ellipse objects for each O. We start by filling an x and y array each iteration which contain the x and y coordinates for each box in the column. Then for each coordinate pair in x, y we add to either the line_seg array or the circle_patches array depending on the value of sign for the current column (1 indicates line_seg, -1 indicates circle_patches). The height of the boxes take into account padding which separates each box by a small margin in order to increase readability. Useful sources: https://stackoverflow.com/questions/8750648/point-and-figure-chart-with-matplotlib https://www.investopedia.com/articles/technical/03/081303.asp Parameters ---------- dates : sequence sequence of dates highs : sequence sequence of high values lows : sequence sequence of low values config_pointnfig_params : kwargs table (dictionary) box_size : size of each box atr_length : length of time used for calculating atr closes : sequence sequence of closing values marketcolors : dict of colors: up, down, edge, wick, alpha Returns ------- ret : tuple rectCollection """ pointnfig_params = _process_kwargs(config_pointnfig_params, _valid_pnf_kwargs()) if marketcolors is None: marketcolors = _get_mpfstyle('classic')['marketcolors'] #print('default market colors:',marketcolors) box_size = pointnfig_params['box_size'] atr_length = pointnfig_params['atr_length'] if box_size == 'atr': if atr_length == 'total': box_size = _calculate_atr(len(closes) - 1, highs, lows, closes) else: box_size = _calculate_atr(atr_length, highs, lows, closes) else: # is an integer or float upper_limit = (max(closes) - min(closes)) / 2 lower_limit = 0.01 * _calculate_atr( len(closes) - 1, highs, lows, closes) if box_size > upper_limit: raise ValueError( "Specified box_size may not be larger than (50% of the close price range of the dataset) which has value: " + str(upper_limit)) elif box_size < lower_limit: raise ValueError( "Specified box_size may not be smaller than (0.01* the Average True Value of the dataset) which has value: " + str(lower_limit)) alpha = marketcolors['alpha'] uc = mcolors.to_rgba(marketcolors['ohlc']['up'], alpha) dc = mcolors.to_rgba(marketcolors['ohlc']['down'], alpha) tfc = mcolors.to_rgba(marketcolors['edge']['down'], 0) # transparent face color boxes = [ ] # each element in an integer representing the number of boxes to be drawn on that indexes column (negative numbers -> Os, positive numbers -> Xs) prev_close_box = closes[ 0] # represents the value of the last box in the previous column volume_cache = 0 # holds the volumes for the dates that were skipped temp_volumes, temp_dates = [], [ ] # holds the temp adjusted volumes and dates respectively for i in range(len(closes) - 1): box_diff = int((closes[i + 1] - prev_close_box) / box_size) if box_diff == 0: if volumes is not None: volume_cache += volumes[i] continue boxes.append(box_diff) if volumes is not None: temp_volumes.append(volumes[i] + volume_cache) volume_cache = 0 temp_dates.append(dates[i]) prev_close_box += box_diff * box_size # combine adjacent similarly signed differences boxes, indexes = combine_adjacent(boxes) new_volumes, new_dates = coalesce_volume_dates(temp_volumes, temp_dates, indexes) #subtract 1 from the abs of each diff except the first to account for the first box using the last box in the opposite direction first_elem = boxes[0] boxes = [ boxes[i] - int((boxes[i] / abs(boxes[i]))) for i in range(1, len(boxes)) ] boxes.insert(0, first_elem) # adjust volume and dates to make sure volume is combined into non 0 box indexes and only use dates from non 0 box indexes temp_volumes, temp_dates = [], [] for i in range(len(boxes)): if boxes[i] == 0: volume_cache += new_volumes[i] else: temp_volumes.append(new_volumes[i] + volume_cache) volume_cache = 0 temp_dates.append(new_dates[i]) #remove 0s from boxes boxes = list(filter(lambda diff: diff != 0, boxes)) # combine adjacent similarly signed differences again after 0s removed boxes, indexes = combine_adjacent(boxes) new_volumes, new_dates = coalesce_volume_dates(temp_volumes, temp_dates, indexes) curr_price = closes[0] box_values = [] # y values for the boxes circle_patches = [ ] # list of circle patches to be used to create the cirCollection line_seg = [] # line segments that make up the Xs for index, difference in enumerate(boxes): diff = abs(difference) sign = (difference / abs(difference)) # -1 or 1 start_iteration = 0 if sign > 0 else 1 x = [index] * (diff) y = [ curr_price + (i * box_size * sign) for i in range(start_iteration, diff + start_iteration) ] curr_price += (box_size * sign * (diff)) box_values.append(sum(y) / len(y)) for i in range(len(x)): # x and y have the same length height = box_size * 0.85 width = 0.6 if height < 0.5: width = height padding = (box_size * 0.075) if sign == 1: # X line_seg.append([(x[i] - width / 2, y[i] + padding), (x[i] + width / 2, y[i] + height + padding) ]) # create / part of the X line_seg.append([(x[i] - width / 2, y[i] + height + padding), (x[i] + width / 2, y[i] + padding) ]) # create \ part of the X else: # O circle_patches.append( Ellipse((x[i], y[i] - (height / 2) - padding), width, height)) useAA = 0, # use tuple here lw = 0.5 cirCollection = PatchCollection(circle_patches) cirCollection.set_facecolor([tfc] * len(circle_patches)) cirCollection.set_edgecolor([dc] * len(circle_patches)) xCollection = LineCollection(line_seg, colors=[uc] * len(line_seg), linewidths=lw, antialiaseds=useAA) return [cirCollection, xCollection], new_dates, new_volumes, box_values, box_size
def _construct_renko_collections(dates, highs, lows, volumes, config_renko_params, closes, marketcolors=None): """Represent the price change with bricks NOTE: this code assumes if any value open, low, high, close is missing they all are missing Algorithm Explanation --------------------- In the first part of the algorithm, we populate the cdiff array along with adjusting the dates and volumes arrays into the new_dates and new_volumes arrays. A single date includes a range from no bricks to many bricks, if a date has no bricks it shall not be included in new_dates, and if it has n bricks then it will be included n times. Volumes use a volume cache to save volume amounts for dates that do not have any bricks before adding the cache to the next date that has at least one brick. We populate the cdiff array with each close values difference from the previously created brick divided by the brick size. In the second part of the algorithm, we iterate through the values in cdiff and add 1s or -1s to the bricks array depending on whether the value is positive or negative. Every time there is a trend change (ex. previous brick is an upbrick, current brick is a down brick) we draw one less brick to account for the price having to move the previous bricks amount before creating a brick in the opposite direction. In the final part of the algorithm, we enumerate through the bricks array and assign up-colors or down-colors to the associated index in the color array and populate the verts list with each bricks vertice to be used to create the matplotlib PolyCollection. Useful sources: https://avilpage.com/2018/01/how-to-plot-renko-charts-with-python.html https://school.stockcharts.com/doku.php?id=chart_analysis:renko Parameters ---------- dates : sequence sequence of dates highs : sequence sequence of high values lows : sequence sequence of low values config_renko_params : kwargs table (dictionary) brick_size : size of each brick atr_length : length of time used for calculating atr closes : sequence sequence of closing values marketcolors : dict of colors: up, down, edge, wick, alpha Returns ------- ret : list rectCollection """ renko_params = _process_kwargs(config_renko_params, _valid_renko_kwargs()) if marketcolors is None: marketcolors = _get_mpfstyle('classic')['marketcolors'] #print('default market colors:',marketcolors) brick_size = renko_params['brick_size'] atr_length = renko_params['atr_length'] if brick_size == 'atr': if atr_length == 'total': brick_size = _calculate_atr(len(closes) - 1, highs, lows, closes) else: brick_size = _calculate_atr(atr_length, highs, lows, closes) else: # is an integer or float upper_limit = (max(closes) - min(closes)) / 2 lower_limit = 0.01 * _calculate_atr( len(closes) - 1, highs, lows, closes) if brick_size > upper_limit: raise ValueError( "Specified brick_size may not be larger than (50% of the close price range of the dataset) which has value: " + str(upper_limit)) elif brick_size < lower_limit: raise ValueError( "Specified brick_size may not be smaller than (0.01* the Average True Value of the dataset) which has value: " + str(lower_limit)) alpha = marketcolors['alpha'] uc = mcolors.to_rgba(marketcolors['candle']['up'], alpha) dc = mcolors.to_rgba(marketcolors['candle']['down'], alpha) euc = mcolors.to_rgba(marketcolors['edge']['up'], 1.0) edc = mcolors.to_rgba(marketcolors['edge']['down'], 1.0) cdiff = [ ] # holds the differences between each close and the previously created brick / the brick size prev_close_brick = closes[0] volume_cache = 0 # holds the volumes for the dates that were skipped new_dates = [] # holds the dates corresponding with the index new_volumes = [ ] # holds the volumes corresponding with the index. If more than one index for the same day then they all have the same volume. for i in range(len(closes) - 1): brick_diff = int((closes[i + 1] - prev_close_brick) / brick_size) if brick_diff == 0: if volumes is not None: volume_cache += volumes[i] continue cdiff.extend([int(brick_diff / abs(brick_diff))] * abs(brick_diff)) if volumes is not None: new_volumes.extend([volumes[i] + volume_cache] * abs(brick_diff)) volume_cache = 0 new_dates.extend([dates[i]] * abs(brick_diff)) prev_close_brick += brick_diff * brick_size bricks = [] # holds bricks, -1 for down bricks, 1 for up bricks curr_price = closes[0] last_diff_sign = 0 # direction the bricks were last going in -1 -> down, 1 -> up dates_volumes_index = 0 # keeps track of the index of the current date/volume for diff in cdiff: curr_diff_sign = diff / abs(diff) if last_diff_sign != 0 and curr_diff_sign != last_diff_sign: last_diff_sign = curr_diff_sign new_dates.pop(dates_volumes_index) if volumes is not None: if dates_volumes_index == len(new_volumes) - 1: new_volumes[dates_volumes_index - 1] += new_volumes[dates_volumes_index] else: new_volumes[dates_volumes_index + 1] += new_volumes[dates_volumes_index] new_volumes.pop(dates_volumes_index) continue last_diff_sign = curr_diff_sign if diff > 0: bricks.extend([1] * abs(diff)) else: bricks.extend([-1] * abs(diff)) dates_volumes_index += 1 verts = [] # holds the brick vertices colors = [] # holds the facecolors for each brick edge_colors = [] # holds the edgecolors for each brick brick_values = [] # holds the brick values for each brick for index, number in enumerate(bricks): if number == 1: # up brick colors.append(uc) edge_colors.append(euc) else: # down brick colors.append(dc) edge_colors.append(edc) curr_price += (brick_size * number) brick_values.append(curr_price) x, y = index, curr_price verts.append( ((x, y), (x, y + brick_size), (x + 1, y + brick_size), (x + 1, y))) useAA = 0, # use tuple here lw = None rectCollection = PolyCollection(verts, facecolors=colors, antialiaseds=useAA, edgecolors=edge_colors, linewidths=lw) return [ rectCollection, ], new_dates, new_volumes, brick_values, brick_size
def _construct_tline_collections(tlines, dtix, dates, opens, highs, lows, closes): """construct trend line collections Parameters ---------- tlines : sequence sequences of pairs of date[time]s date[time] may be (a) pandas.to_datetime parseable string, (b) pandas Timestamp, or (c) python datetime.datetime or datetime.date tlines may also be a dict, containing the following keys: 'tlines' : the same as defined above: sequence of pairs of date[time]s 'colors' : colors for the above tlines 'linestyle' : line types for the above tlines 'linewidths' : line types for the above tlines dtix: date index for the x-axis, used for converting the dates when x-values are 'evenly spaced integers' (as when skipping non-trading days) Returns ------- ret : list lines collections """ if tlines is None: return None if isinstance(tlines, dict): tconfig = _process_kwargs(tlines, _valid_lines_kwargs()) tlines = tconfig['tlines'] else: tconfig = _process_kwargs({}, _valid_lines_kwargs()) tline_use = tconfig['tline_use'] tline_method = tconfig['tline_method'] #print('tconfig=',tconfig) #print('tlines=',tlines) # reconstruct the data frame: df = pd.DataFrame( { 'open': opens, 'high': highs, 'low': lows, 'close': closes }, index=pd.DatetimeIndex(mdates.num2date(dates))) # possible `tvalue`s : close,open,high,low,oc_avg,hl_avg,ohlc_avg,hilo # 'hilo' means high on the up trend, low on the down trend. # possible `tmethod`s: point-to-point, leastsquares def _tline_point_to_point(dfslice, tline_use): p1 = dfslice.iloc[0] p2 = dfslice.iloc[-1] x1 = p1.name y1 = p1[tline_use].mean() x2 = p2.name y2 = p2[tline_use].mean() return ((x1, y1), (x2, y2)) def _tline_lsq(dfslice, tline_use): ''' This closed-form linear least squares algorithm was taken from: https://mmas.github.io/least-squares-fitting-numpy-scipy ''' s = dfslice[tline_use].mean(axis=1) xs = mdates.date2num(s.index.to_pydatetime()) ys = s.values a = np.vstack([xs, np.ones(len(xs))]).T m, b = np.dot(np.linalg.inv(np.dot(a.T, a)), np.dot(a.T, ys)) x1, x2 = xs[0], xs[-1] y1 = m * x1 + b y2 = m * x2 + b x1, x2 = mdates.num2date(x1), mdates.num2date(x2) return ((x1, y1), (x2, y2)) if isinstance(tline_use, str): tline_use = [ tline_use, ] tline_use = [u.lower() for u in tline_use] alines = [] for d1, d2 in tlines: dfslice = df.loc[d1:d2] if len(dfslice) < 2: dfdr = '\ndf date range: [' + str(df.index[0]) + ' , ' + str( df.index[-1]) + ']' raise ValueError('\ntlines date pair (' + str(d1) + ',' + str(d2) + ') too close, or wrong order, or out of range!' + dfdr) if tline_method == 'least squares' or tline_method == 'least-squares': p1, p2 = _tline_lsq(dfslice, tline_use) elif tline_method == 'point-to-point': p1, p2 = _tline_point_to_point(dfslice, tline_use) else: raise ValueError('\nUnrecognized value for `tline_method` = "' + str(tline_method) + '"') alines.append((p1, p2)) del tconfig['alines'] alines = dict(alines=alines, **tconfig) alines['tlines'] = None return _construct_aline_collections(alines, dtix)
def _construct_vline_collections(vlines, dtix, miny, maxy): """Construct vertical lines collection Parameters ---------- vlines : sequence sequence of dates or datetimes at which to draw vertical lines dates/datetimes may be (a) pandas.to_datetime parseable string, (b) pandas Timestamp (c) python datetime.datetime or datetime.date vlines may also be a dict, containing the following keys: 'vlines' : the same as defined above: sequence of dates/datetimes 'colors' : colors for the above vlines 'linestyle' : line types for the above vlines 'linewidths' : line types for the above vlines dtix: date index for the x-axis, used for converting the dates when x-values are 'evenly spaced integers' (as when skipping non-trading days) miny : minimum y-value for the vertical line maxy : maximum y-value for the vertical line Returns ------- ret : list lines collections """ if vlines is None: return None #print('_construct_vline_collections() called:', # '\nvlines=',vlines, # '\ndtix=',dtix) #print('miny,maxy=',miny,maxy) if isinstance(vlines, dict): vconfig = _process_kwargs(vlines, _valid_lines_kwargs()) vlines = vconfig['vlines'] else: vconfig = _process_kwargs({}, _valid_lines_kwargs()) #print('vconfig=',vconfig) #print('vlines=',vlines) if not isinstance(vlines, (list, tuple)): vlines = [ vlines, ] lines = [] for val in vlines: lines.append([(val, miny), (val, maxy)]) lines = _convert_segment_dates(lines, dtix) lw = vconfig['linewidths'] co = vconfig['colors'] ls = vconfig['linestyle'] al = vconfig['alpha'] lcollection = LineCollection(lines, colors=co, linewidths=lw, linestyles=ls, antialiaseds=(0, ), alpha=al) return lcollection
def make_marketcolors(**kwargs): ''' Create a 'marketcolors' dict that is structured as expected by mplfinance._styles code: up = color for close >= open down = color for close < open edge = color for edge of candlestick; if "inherit" then edge color will be same as up or down. wick = color for wick of candlestick; if "inherit" then wick color will be same as up or down. alpha = opacity, 0.0 to 1.0, of candlestick face. ohlc = color of ohlc bars when all the same color; if ohlc == "inherit" then use up/down colors. volume = color of volume bars when all the same color; if volume == "inherit" then use up/down colors. ''' config = _process_kwargs(kwargs, _valid_make_marketcolors_kwargs()) if config['base_mpf_style'] is not None: style = _get_mpfstyle(config['base_mpf_style']) else: style = _get_mpfstyle('default') marketcolors = style['marketcolors'] up = config['up'] down = config['down'] if up is not None and down is not None: marketcolors.update(candle=dict(up=up,down=down)) elif up is not None: candle = marketcolors['candle'] candle.update(up=up) marketcolors.update(candle=candle) elif down is not None: candle = marketcolors['candle'] candle.update(down=down) marketcolors.update(down=down) def _check_and_set_mktcolor(candle,**kwarg): if len(kwarg) != 1: raise ValueError('Expect only ONE kwarg') key,value = kwarg.popitem() if isinstance(value,(dict)): colors = value elif isinstance(value,str) and value == 'inherit'[0:len(value)]: colors = candle else: colors = dict(up=value, down=value) for updown in ['up','down']: if not _mpf_is_color_like(colors[updown]): err = f'NOT is_color_like() for {key}[\'{updown}\'] = {colors[updown]}' raise ValueError(err) return colors candle = marketcolors['candle'] for kw in ['edge','volume','ohlc','wick']: # `inherit=True` takes precedence: if config[kw] is not None or config['inherit'] == True: if config['inherit'] == True: kwa = {kw:'i'} else: kwa = {kw:config[kw]} c = _check_and_set_mktcolor(candle,**kwa) marketcolors.update([(kw,c)]) if config['hollow'] is not None: marketcolors.update({'hollow':config['hollow']}) if config['alpha'] is not None: marketcolors.update({'alpha':config['alpha']}) if config['vcdopcod'] is not None: marketcolors.update({'vcdopcod':config['vcdopcod']}) return marketcolors
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)
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. """ dates, opens, highs, lows, closes, volumes = _check_and_prepare_data(data) config = _process_kwargs(kwargs, _valid_plot_kwargs()) 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' 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) ptype = config['type'] 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 == 'line': ax1.plot(xdates, closes, color=config['linecolor']) else: raise ValueError('Unrecognized plot type = "' + ptype + '"') 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: mavprices = data['Close'].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 miny = min([low for low in lows if low != -1]) maxy = max([high for high in highs if high != -1]) corners = (minx, miny), (maxx, maxy) ax1.update_datalim(corners) 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) miny = 0.3 * min(volumes) maxy = 1.1 * max(volumes) 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: # 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'] 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['savefig'] is not None: save = config['savefig'] if isinstance(save, dict): plt.savefig(**save) else: plt.savefig(save) else: # https://stackoverflow.com/a/13361748/1639359 suggests plt.show(block=False) plt.show(block=config['block'])
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 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 = config['style'] = _styles._get_mpfstyle(style) if isinstance(style,dict): _styles._apply_mpfstyle(style) else: raise TypeError('style should be a `dict`; why is it not?') 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) mav = config['mav'] mavwidth = config['_width_config']['line_width'] mavcolors = style['mavcolors'] if ptype in VALID_PMOVE_TYPES: mavprices = _plot_mav(axA1, mav, xdates, brick_values, mavwidth, mavcolors) else: mavprices = _plot_mav(axA1, mav, xdates, closes, mavwidth, mavcolors)