def build_color_lines(df: pd.DataFrame, scheme, col_open: str = 'open', col_close: str = 'close', col_prefix: str = '') -> pd.DataFrame: # build color strings from scheme colorup = convert_color(scheme.barup) colordown = convert_color(scheme.bardown) colorup_wick = convert_color(scheme.barup_wick) colordown_wick = convert_color(scheme.bardown_wick) colorup_outline = convert_color(scheme.barup_outline) colordown_outline = convert_color(scheme.bardown_outline) volup = convert_color(scheme.volup) voldown = convert_color(scheme.voldown) # build binary series determining if up or down bar is_up: pd.DataFrame = df[col_close] >= df[col_open] # we use the open-line as a indicator for NaN values nan_ref = df[col_open] # TODO: we want to have NaN values in the color lines if the corresponding data is also NaN # find better way with less isnan usage color_df = pd.DataFrame(index=df.index) color_df[col_prefix + 'colors_bars'] = [np.nan if np.isnan(n) else colorup if x else colordown for x, n in zip(is_up, nan_ref)] color_df[col_prefix + 'colors_wicks'] = [np.nan if np.isnan(n) else colorup_wick if x else colordown_wick for x, n in zip(is_up, nan_ref)] color_df[col_prefix + 'colors_outline'] = [np.nan if np.isnan(n) else colorup_outline if x else colordown_outline for x, n in zip(is_up, nan_ref)] color_df[col_prefix + 'colors_volume'] = [np.nan if np.isnan(n) else volup if x else voldown for x, n in zip(is_up, nan_ref)] # convert to object since we want to hold str and NaN for c in color_df.columns: color_df[c] = color_df[c].astype(object) return color_df
def plot_data(self, data: bt.AbstractDataBase): source_id = FigureEnvelope._source_id(data) # title = sanitize_source_name(label_resolver.datatarget2label([data])) title = "" legend_label = "OHLC" # append to title self._figure_append_title(title) self._add_columns([(source_id + x, object) for x in ['open', 'high', 'low', 'close']]) self._add_columns([(source_id + x, str) for x in ['colors_bars', 'colors_wicks', 'colors_outline']]) if self._scheme.style == 'line': if data.plotinfo.plotmaster is None: color = convert_color(self._scheme.loc) else: self._nextcolor(data.plotinfo.plotmaster) color = convert_color(self._color(data.plotinfo.plotmaster)) renderer = self.figure.line('index', source_id + 'close', source=self._cds, line_color=color, legend_label=legend_label) self._set_single_hover_renderer(renderer) self._hoverc.add_hovertip("Close", f"@{source_id}close", data) elif self._scheme.style == 'bar': self.figure.segment('index', source_id + 'high', 'index', source_id + 'low', source=self._cds, color=source_id + 'colors_wicks', legend_label=legend_label) renderer = self.figure.vbar('index', get_bar_width(), source_id + 'open', source_id + 'close', source=self._cds, fill_color=source_id + 'colors_bars', line_color=source_id + 'colors_outline', legend_label=legend_label, ) self._set_single_hover_renderer(renderer) self._hoverc.add_hovertip("Open", f"@{source_id}open{{{self._scheme.number_format}}}", data) self._hoverc.add_hovertip("High", f"@{source_id}high{{{self._scheme.number_format}}}", data) self._hoverc.add_hovertip("Low", f"@{source_id}low{{{self._scheme.number_format}}}", data) self._hoverc.add_hovertip("Close", f"@{source_id}close{{{self._scheme.number_format}}}", data) else: raise Exception(f"Unsupported style '{self._scheme.style}'") # make sure the regular y-axis only scales to the normal data on 1st axis (not to e.g. volume data on 2nd axis) self.figure.y_range.renderers.append(renderer) if self._scheme.volume and self._scheme.voloverlay: self.plot_volume(data, self._scheme.voltrans, True)
def plot_volume(self, data: bt.AbstractDataBase, alpha=1.0, extra_axis=False): """extra_axis displays a second axis (for overlay on data plotting)""" source_id = FigureEnvelope._source_id(data) self._add_columns([(source_id + 'volume', np.float64), (source_id + 'colors_volume', np.object)]) kwargs = { 'fill_alpha': alpha, 'line_alpha': alpha, 'name': 'Volume', 'legend_label': 'Volume' } ax_formatter = NumeralTickFormatter(format=self._scheme.number_format) if extra_axis: source_data_axis = 'axvol' self.figure.extra_y_ranges = { source_data_axis: DataRange1d( range_padding=1.0 / self._scheme.volscaling, start=0, ) } # use colorup ax_color = convert_color(self._scheme.volup) ax = LinearAxis(y_range_name=source_data_axis, formatter=ax_formatter, axis_label_text_color=ax_color, axis_line_color=ax_color, major_label_text_color=ax_color, major_tick_line_color=ax_color, minor_tick_line_color=ax_color) self.figure.add_layout(ax, 'left') kwargs['y_range_name'] = source_data_axis else: self.figure.yaxis.formatter = ax_formatter vbars = self.figure.vbar('index', get_bar_width(), f'{source_id}volume', 0, source=self._cds, fill_color=f'{source_id}colors_volume', line_color="black", **kwargs) # make sure the new axis only auto-scales to the volume data if extra_axis: self.figure.extra_y_ranges['axvol'].renderers = [vbars] self._hoverc.add_hovertip( "Volume", f"@{source_id}volume{{({self._scheme.number_format})}}", data)
def _plot_hlines(self, obj): hlines = obj.plotinfo._get('plothlines', []) if not hlines: hlines = obj.plotinfo._get('plotyhlines', []) # Horizontal Lines hline_color = convert_color(self._scheme.hlinescolor) for hline in hlines: span = Span(location=hline, dimension='width', line_color=hline_color, line_dash=convert_linestyle(self._scheme.hlinesstyle), line_width=self._scheme.hlineswidth) self.figure.renderers.append(span)
def _plot_indicator_observer(self, obj: Union[bt.Indicator, bt.Observer], master, strat_clk: array = None): pl = plotobj2label(obj) self._figure_append_title(pl) indlabel = obj.plotlabel() plotinfo = obj.plotinfo for lineidx in range(obj.size()): line = obj.lines[lineidx] source_id = Figure._source_id(line) linealias = obj.lines._getlinealias(lineidx) lineplotinfo = getattr(obj.plotlines, '_%d' % lineidx, None) if not lineplotinfo: lineplotinfo = getattr(obj.plotlines, linealias, None) if not lineplotinfo: lineplotinfo = bt.AutoInfoClass() if lineplotinfo._get('_plotskip', False): continue marker = lineplotinfo._get("marker", None) method = lineplotinfo._get('_method', "line") color = getattr(lineplotinfo, "color", None) if color is None: if not lineplotinfo._get('_samecolor', False): self._nextcolor() color = self._color() color = convert_color(color) kwglyphs = {'name': linealias} dataline = line.plotrange(self._start, self._end) line_clk = get_data_obj(obj).lines.datetime.plotrange( self._start, self._end) dataline = resample_line(dataline, line_clk, strat_clk) self._add_to_cds(dataline, source_id) label = None if master is None or lineidx == 0 or plotinfo.plotlinelabels: label = indlabel if master is None or plotinfo.plotlinelabels: label += " " + (lineplotinfo._get("_name", "") or linealias) kwglyphs['legend_label'] = label if marker is not None: kwglyphs['size'] = lineplotinfo.markersize * 1.2 kwglyphs['color'] = color kwglyphs['y'] = source_id mrk_fncs = { '^': self.figure.triangle, 'v': self.figure.inverted_triangle, 'o': self.figure.circle, '<': self.figure.circle_cross, '>': self.figure.circle_x, '1': self.figure.diamond, '2': self.figure.diamond_cross, '3': self.figure.hex, '4': self.figure.square, '8': self.figure.square_cross, 's': self.figure.square_x, 'p': self.figure.triangle, '*': self.figure.asterisk, 'h': self.figure.hex, 'H': self.figure.hex, '+': self.figure.asterisk, 'x': self.figure.x, 'D': self.figure.diamond_cross, 'd': self.figure.diamond, } if marker not in mrk_fncs: raise Exception( f"Sorry, unsupported marker: '{marker}'. Please report to GitHub." ) glyph_fnc = mrk_fncs[marker] elif method == "bar": kwglyphs['bottom'] = 0 kwglyphs['line_color'] = 'black' kwglyphs['fill_color'] = color kwglyphs['width'] = get_bar_width() kwglyphs['top'] = source_id glyph_fnc = self.figure.vbar elif method == "line": kwglyphs['line_width'] = 1 kwglyphs['color'] = color kwglyphs['y'] = source_id linestyle = getattr(lineplotinfo, "ls", None) if linestyle is not None: kwglyphs['line_dash'] = convert_linestyle(linestyle) glyph_fnc = self.figure.line else: raise Exception(f"Unknown plotting method '{method}'") renderer = glyph_fnc("index", source=self._cds, **kwglyphs) # for markers add additional renderer so hover pops up for all of them if marker is None: self._set_single_hover_renderer(renderer) else: self._add_hover_renderer(renderer) hover_label_suffix = f" - {linealias}" if obj.size( ) > 1 else "" # we need no suffix if there is just one line in the indicator anyway hover_label = indlabel + hover_label_suffix hover_data = f"@{source_id}{{{self._scheme.number_format}}}" self._hoverc.add_hovertip(hover_label, hover_data, obj) # adapt y-axis if needed if master is None or getattr(master.plotinfo, 'plotylimited', False) is False: adapt_yranges(self.figure.y_range, dataline) self._set_yticks(obj) self._plot_hlines(obj)
def plot_volume(self, data: bt.AbstractDataBase, strat_clk: array, alpha, extra_axis=False): """extra_axis displays a second axis (for overlay on data plotting)""" source_id = Figure._source_id(data) df = convert_to_pandas(strat_clk, data, self._start, self._end) if len(nanfilt(df.volume)) == 0: return colorup = convert_color(self._scheme.volup) colordown = convert_color(self._scheme.voldown) is_up = df.close > df.open colors = [colorup if x else colordown for x in is_up] self._add_to_cds(df.volume, f'{source_id}volume') self._add_to_cds(colors, f'{source_id}volume_colors') kwargs = { 'fill_alpha': alpha, 'line_alpha': alpha, 'name': 'Volume', 'legend_label': 'Volume' } ax_formatter = NumeralTickFormatter(format=self._scheme.number_format) if extra_axis: self.figure.extra_y_ranges = {'axvol': DataRange1d()} adapt_yranges(self.figure.extra_y_ranges['axvol'], df.volume) self.figure.extra_y_ranges['axvol'].end /= self._scheme.volscaling ax_color = colorup ax = LinearAxis(y_range_name="axvol", formatter=ax_formatter, axis_label_text_color=ax_color, axis_line_color=ax_color, major_label_text_color=ax_color, major_tick_line_color=ax_color, minor_tick_line_color=ax_color) self.figure.add_layout(ax, 'left') kwargs['y_range_name'] = "axvol" else: self.figure.yaxis.formatter = ax_formatter adapt_yranges(self.figure.y_range, df.volume) self.figure.y_range.end /= self._scheme.volscaling self.figure.vbar('index', get_bar_width(), f'{source_id}volume', 0, source=self._cds, fill_color=f'{source_id}volume_colors', line_color="black", **kwargs) self._hoverc.add_hovertip( "Volume", f"@{source_id}volume{{({self._scheme.number_format})}}", data)
def plot_data(self, data: bt.AbstractDataBase, strat_clk: array = None): source_id = Figure._source_id(data) title = sanitize_source_name(label_resolver.datatarget2label([data])) # append to title self._figure_append_title(title) df = convert_to_pandas(strat_clk, data, self._start, self._end) # configure colors colorup = convert_color(self._scheme.barup) colordown = convert_color(self._scheme.bardown) colorup_wick = convert_color(self._scheme.barup_wick) colordown_wick = convert_color(self._scheme.bardown_wick) colorup_outline = convert_color(self._scheme.barup_outline) colordown_outline = convert_color(self._scheme.bardown_outline) is_up = df.close > df.open self._add_to_cds(df.open, source_id + 'open') self._add_to_cds(df.high, source_id + 'high') self._add_to_cds(df.low, source_id + 'low') self._add_to_cds(df.close, source_id + 'close') self._add_to_cds([colorup if x else colordown for x in is_up], source_id + 'colors_bars') self._add_to_cds( [colorup_wick if x else colordown_wick for x in is_up], source_id + 'colors_wicks') self._add_to_cds( [colorup_outline if x else colordown_outline for x in is_up], source_id + 'colors_outline') if self._scheme.style == 'line': if data.plotinfo.plotmaster is None: color = convert_color(self._scheme.loc) else: self._nextcolor(data.plotinfo.plotmaster) color = convert_color(self._color(data.plotinfo.plotmaster)) renderer = self.figure.line('index', source_id + 'close', source=self._cds, line_color=color, legend=title) self._set_single_hover_renderer(renderer) self._hoverc.add_hovertip("Close", f"@{source_id}close", data) elif self._scheme.style == 'bar': self.figure.segment('index', source_id + 'high', 'index', source_id + 'low', source=self._cds, color=source_id + 'colors_wicks', legend_label=title) renderer = self.figure.vbar( 'index', get_bar_width(), source_id + 'open', source_id + 'close', source=self._cds, fill_color=source_id + 'colors_bars', line_color=source_id + 'colors_outline', legend_label=title, ) self._set_single_hover_renderer(renderer) self._hoverc.add_hovertip( "Open", f"@{source_id}open{{{self._scheme.number_format}}}", data) self._hoverc.add_hovertip( "High", f"@{source_id}high{{{self._scheme.number_format}}}", data) self._hoverc.add_hovertip( "Low", f"@{source_id}low{{{self._scheme.number_format}}}", data) self._hoverc.add_hovertip( "Close", f"@{source_id}close{{{self._scheme.number_format}}}", data) else: raise Exception(f"Unsupported style '{self._scheme.style}'") adapt_yranges(self.figure.y_range, df.low, df.high) # check if we have to plot volume overlay if self._scheme.volume and self._scheme.voloverlay: self.plot_volume(data, strat_clk, self._scheme.voltrans, True)
def _init_figure(self): # plot height will be set later f = figure(tools=Figure._tools, x_axis_type='linear', aspect_ratio=self._scheme.plot_aspect_ratio) # TODO: backend webgl (output_backend="webgl") removed due to this bug: # https://github.com/bokeh/bokeh/issues/7568 f.border_fill_color = convert_color(self._scheme.border_fill) f.xaxis.axis_line_color = convert_color(self._scheme.axis_line_color) f.yaxis.axis_line_color = convert_color(self._scheme.axis_line_color) f.xaxis.minor_tick_line_color = convert_color( self._scheme.tick_line_color) f.yaxis.minor_tick_line_color = convert_color( self._scheme.tick_line_color) f.xaxis.major_tick_line_color = convert_color( self._scheme.tick_line_color) f.yaxis.major_tick_line_color = convert_color( self._scheme.tick_line_color) f.xaxis.major_label_text_color = convert_color( self._scheme.axis_label_text_color) f.yaxis.major_label_text_color = convert_color( self._scheme.axis_label_text_color) f.xgrid.grid_line_color = convert_color(self._scheme.grid_line_color) f.ygrid.grid_line_color = convert_color(self._scheme.grid_line_color) f.title.text_color = convert_color(self._scheme.plot_title_text_color) f.left[0].formatter.use_scientific = False f.background_fill_color = convert_color(self._scheme.background_fill) # mechanism for proper date axis without gaps, thanks! # https://groups.google.com/a/continuum.io/forum/#!topic/bokeh/t3HkalO4TGA f.xaxis.formatter = FuncTickFormatter(args=dict( axis=f.xaxis[0], formatter=DatetimeTickFormatter( minutes=[self._scheme.axis_tickformat_minutes], hourmin=[self._scheme.axis_tickformat_hourmin], hours=[self._scheme.axis_tickformat_hours], days=[self._scheme.axis_tickformat_days], months=[self._scheme.axis_tickformat_months], years=[self._scheme.axis_tickformat_years], ), source=self._cds, ), code=""" // We override this axis' formatter's `doFormat` method // with one that maps index ticks to dates. Some of those dates // are undefined (e.g. those whose ticks fall out of defined data // range) and we must filter out and account for those, otherwise // the formatter computes invalid visible span and returns some // labels as 'ERR'. // Note, after this assignment statement, on next plot redrawing, // our override `doFormat` will be called directly // -- FunctionTickFormatter.doFormat(), i.e. _this_ code, no longer // executes. axis.formatter.doFormat = function (ticks) { const dates = ticks.map(i => source.data.datetime[i]), valid = t => t !== undefined, labels = formatter.doFormat(dates.filter(valid)); let i = 0; return dates.map(t => valid(t) ? labels[i++] : ''); }; // we do this manually only for the first time we are called const labels = axis.formatter.doFormat(ticks); return labels[index]; """) ch = CrosshairTool(line_color=self._scheme.crosshair_line_color) f.tools.append(ch) h = HoverTool(tooltips=[ ('Time', f'@datetime{{{self._scheme.hovertool_timeformat}}}') ], mode="vline", formatters={'datetime': 'datetime'}) f.tools.append(h) self._hover = h self.figure = f
def _color(self, key: object = None): return convert_color(self._scheme.color(self._coloridx[key]))
def _plot_indicator_observer(self, obj: Union[bt.Indicator, bt.Observer]): pl = labelizer.label(obj) self._figure_append_title(pl) indlabel = obj.plotlabel() plotinfo = obj.plotinfo is_multiline = obj.size() > 1 for lineidx in range(obj.size()): line = obj.lines[lineidx] source_id = get_source_id(line) linealias = obj.lines._getlinealias(lineidx) lineplotinfo = get_plotlineinfo(obj, lineidx) if lineplotinfo._get('_plotskip', False): continue method = lineplotinfo._get('_method', 'line') color = getattr(lineplotinfo, 'color', None) if color is None: if not lineplotinfo._get('_samecolor', False): self._nextcolor() color = self._color() color = convert_color(color) kwglyphs = {'name': linealias} self._add_column(source_id, np.float64) # either all individual lines of are displayed in the legend or only the ind/obs as a whole label = indlabel if is_multiline and plotinfo.plotlinelabels: label += " " + (lineplotinfo._get("_name", "") or linealias) kwglyphs['legend_label'] = label marker = lineplotinfo._get('marker', None) if marker is not None: fnc_name, extra_kwglyphs = build_marker_call(marker, self.bfigure, source_id, color, lineplotinfo.markersize) kwglyphs.update(extra_kwglyphs) glyph_fnc = getattr(self.bfigure, fnc_name) elif method == "bar": kwglyphs['bottom'] = 0 kwglyphs['line_color'] = 'black' kwglyphs['fill_color'] = color kwglyphs['width'] = get_bar_width() kwglyphs['top'] = source_id glyph_fnc = self.bfigure.vbar elif method == "line": kwglyphs['line_width'] = 1 kwglyphs['color'] = color kwglyphs['y'] = source_id linestyle = getattr(lineplotinfo, "ls", None) if linestyle is not None: kwglyphs['line_dash'] = convert_linestyle(linestyle) linewidth = getattr(lineplotinfo, "lw", None) if linewidth is not None: kwglyphs['line_width'] = linewidth glyph_fnc = self.bfigure.line else: raise Exception(f"Unknown plotting method '{method}'") renderer = glyph_fnc("index", source=self._cds, **kwglyphs) # iterate again to generate area plot data for fattr, y1, y2, fcol, falpha, fop in get_ind_areas(obj, lineidx): self._add_column(y1, np.float64) falpha = falpha or self._scheme.fillalpha fcol = convert_color(fcol) self.bfigure.varea('index', source=self._cds, y1=y1, y2=y2, fill_color=fcol, fill_alpha=falpha) # make sure the regular y-axis only scales to the normal data (data + ind/obs) on 1st axis (not to e.g. volume data on 2nd axis) self.bfigure.y_range.renderers.append(renderer) # for markers add additional renderer so hover pops up for all of them if marker is None: self._set_single_hover_renderer(renderer) else: self._add_hover_renderer(renderer) hover_label_suffix = f" - {linealias}" if obj.size() > 1 else "" # we need no suffix if there is just one line in the indicator anyway hover_label = indlabel + hover_label_suffix hover_data = f"@{source_id}{{{self._scheme.number_format}}}" self._hoverc.add_hovertip(hover_label, hover_data, obj) self._set_yticks(obj) self._plot_hlines(obj)
def _plot_indicator_observer(self, obj: Union[bt.Indicator, bt.Observer], master): # pl = plotobj2label(obj) pl = "" self._figure_append_title(pl) indlabel = obj.plotlabel() plotinfo = obj.plotinfo is_multiline = obj.size() > 1 for lineidx in range(obj.size()): line = obj.lines[lineidx] source_id = FigureEnvelope._source_id(line) linealias = obj.lines._getlinealias(lineidx) lineplotinfo = getattr(obj.plotlines, '_%d' % lineidx, None) if not lineplotinfo: lineplotinfo = getattr(obj.plotlines, linealias, None) if not lineplotinfo: lineplotinfo = bt.AutoInfoClass() if lineplotinfo._get('_plotskip', False): continue marker = lineplotinfo._get("marker", None) method = lineplotinfo._get('_method', "line") color = getattr(lineplotinfo, "color", None) if color is None: if not lineplotinfo._get('_samecolor', False): self._nextcolor() color = self._color() color = convert_color(color) kwglyphs = {'name': linealias} self._add_column(source_id, np.float64) # either all individual lines of are displayed in the legend or only the ind/obs as a whole label = indlabel if is_multiline and plotinfo.plotlinelabels: label += " " + (lineplotinfo._get("_name", "") or linealias) kwglyphs['legend_label'] = label if marker is not None: kwglyphs['size'] = lineplotinfo.markersize * 1.2 kwglyphs['color'] = color kwglyphs['y'] = source_id if marker not in FigureEnvelope._mrk_fncs: raise Exception(f"Sorry, unsupported marker: '{marker}'. Please report to GitHub.") glyph_fnc_name = FigureEnvelope._mrk_fncs[marker] glyph_fnc = getattr(self.figure, glyph_fnc_name) elif method == "bar": kwglyphs['bottom'] = 0 kwglyphs['line_color'] = 'black' kwglyphs['fill_color'] = color kwglyphs['width'] = get_bar_width() kwglyphs['top'] = source_id glyph_fnc = self.figure.vbar elif method == "line": kwglyphs['line_width'] = 1 kwglyphs['color'] = color kwglyphs['y'] = source_id linestyle = getattr(lineplotinfo, "ls", None) if linestyle is not None: kwglyphs['line_dash'] = convert_linestyle(linestyle) glyph_fnc = self.figure.line else: raise Exception(f"Unknown plotting method '{method}'") renderer = glyph_fnc("index", source=self._cds, **kwglyphs) # make sure the regular y-axis only scales to the normal data (data + ind/obs) on 1st axis (not to e.g. volume data on 2nd axis) self.figure.y_range.renderers.append(renderer) # for markers add additional renderer so hover pops up for all of them if marker is None: self._set_single_hover_renderer(renderer) else: self._add_hover_renderer(renderer) hover_label_suffix = f" - {linealias}" if obj.size() > 1 else "" # we need no suffix if there is just one line in the indicator anyway hover_label = indlabel + hover_label_suffix hover_data = f"@{source_id}{{{self._scheme.number_format}}}" # self._hoverc.add_hovertip(hover_label, hover_data, obj) self._set_yticks(obj) self._plot_hlines(obj)