backtesting.py
backtesting.py copied to clipboard
Histogram indicator plot style
I am using the method I
to plot MACD, but get the wrong style with histogram.
Maybe need more style settings with the method.
All parameters how to plot one indicator should be just one style parameter as list of dictionaries, one for each element of the indicator. Then you can specify for MACD to have 2 line elements and one histogram with the corresponding colors and names.
I did some experiments and added a complex style parameter where the user can select even size, transparency, type, color, name etc. of each element of one indicator but it needs much more work before proposing this option. Something like this:
@AGG2017 how did you specify the legends for each of the lines in 1 indicator graph? Could you share the code for this? I'm trying to draw MACD signal like this too. Thanks.
@wakandan check his commits, he change the core of library, to get such results, you should by yourself change 3 times original code. 1)Add histogram feature 2)Add legend feature 3)Add color feature
@wakandan
def MACD_Backtesting_FAST(values, fast):
"""
Return exponential moving average of `values`, at
each step taking into account `n` previous values.
"""
close = pd.Series(values)
return close.ewm(span=fast, adjust=False,min_periods=fast).mean()
def MACD_Backtesting_SLOW(values, slow):
"""
Return exponential moving average of `values`, at
each step taking into account `n` previous values.
"""
close = pd.Series(values)
return close.ewm(span=slow, adjust=False,min_periods=slow).mean()
def MACD_Backtesting_MACD(fast, slow):
"""
Return exponential moving average of `values`, at
each step taking into account `n` previous values.
"""
return fast - slow
def MACD_Backtesting_SIGNAL(values, signal):
"""
Return exponential moving average of `values`, at
each step taking into account `n` previous values.
"""
macd_ = pd.Series(values)
return macd_.ewm(span=signal, adjust=False,min_periods=signal).mean()
def MACD_Backtesting_HISTOGRAMM(macd_, signal):
"""
Return exponential moving average of `values`, at
each step taking into account `n` previous values.
"""
return macd_ - signal
class MACDStrategy(Strategy):
# Define the two EMA lags as *class variables*
# for later optimization
fast = 12
slow = 26
signal = 9
def init(self):
self.fast_macd = self.I(MACD_Backtesting_FAST, self.data.Close, self.fast, overlay=True, name='FAST')
self.slow_macd = self.I(MACD_Backtesting_SLOW, self.data.Close, self.slow, overlay=True, name='SLOW')
self.macd_macd_ = MACD_Backtesting_MACD(self.fast_macd, self.slow_macd)
self.signal_macd_ = MACD_Backtesting_SIGNAL(self.macd_macd_, self.signal)
self.histogram_macd_ = MACD_Backtesting_HISTOGRAMM(self.macd_macd_, self.signal_macd_)
self.macd_macd, self.signal_macd, self.histogram_macd = self.I(
lambda: (self.macd_macd_, self.signal_macd_, self.histogram_macd_)
, overlay=False,legends=['MACD', 'signal', 'histogramm'], histogramms=[False, False, True], scatter=False, name='MACD',)
In backtesting.py
def I(self, # noqa: E741, E743
func: Callable, *args,
name=None, plot=True, overlay=None, color=None, scatter=False, histogram=False,legends=None,histogramms=None,
**kwargs) -> np.ndarray:
"""
Declare indicator. An indicator is just an array of values,
but one that is revealed gradually in
`backtesting.backtesting.Strategy.next` much like
`backtesting.backtesting.Strategy.data` is.
Returns `np.ndarray` of indicator values.
`func` is a function that returns the indicator array(s) of
same length as `backtesting.backtesting.Strategy.data`.
In the plot legend, the indicator is labeled with
function name, unless `name` overrides it.
If `plot` is `True`, the indicator is plotted on the resulting
`backtesting.backtesting.Backtest.plot`.
If `overlay` is `True`, the indicator is plotted overlaying the
price candlestick chart (suitable e.g. for moving averages).
If `False`, the indicator is plotted standalone below the
candlestick chart. By default, a heuristic is used which decides
correctly most of the time.
`color` can be string hex RGB triplet or X11 color name.
By default, the next available color is assigned.
If `scatter` is `True`, the plotted indicator marker will be a
circle instead of a connected line segment (default).
If `histogram` is `True`, the indicator values will be plotted
as a histogram instead of line or circle. When `histogram` is
`True`, 'scatter' value will be ignored even if it's set.
Additional `*args` and `**kwargs` are passed to `func` and can
be used for parameters.
For example, using simple moving average function from TA-Lib:
def init():
self.sma = self.I(ta.SMA, self.data.Close, self.n_sma)
`legends` can be list or array of string values to represent
legends on your indicator chart. By default it's set to None,
and `name` is used as legends.
"""
if name is None:
params = ','.join(filter(None, map(_as_str, chain(args, kwargs.values()))))
func_name = _as_str(func)
name = (f'{func_name}({params})' if params else f'{func_name}')
else:
name = name.format(*map(_as_str, args),
**dict(zip(kwargs.keys(), map(_as_str, kwargs.values()))))
try:
value = func(*args, **kwargs)
except Exception as e:
raise RuntimeError(f'Indicator "{name}" errored with exception: {e}')
if isinstance(value, pd.DataFrame):
value = value.values.T
if value is not None:
value = try_(lambda: np.asarray(value, order='C'), None)
is_arraylike = value is not None
# Optionally flip the array if the user returned e.g. `df.values`
if is_arraylike and np.argmax(value.shape) == 0:
value = value.T
if not is_arraylike or not 1 <= value.ndim <= 2 or value.shape[-1] != len(self._data.Close):
raise ValueError(
'Indicators must return (optionally a tuple of) numpy.arrays of same '
f'length as `data` (data shape: {self._data.Close.shape}; indicator "{name}"'
f'shape: {getattr(value, "shape" , "")}, returned value: {value})')
if plot and overlay is None and np.issubdtype(value.dtype, np.number):
x = value / self._data.Close
# By default, overlay if strong majority of indicator values
# is within 30% of Close
with np.errstate(invalid='ignore'):
overlay = ((x < 1.4) & (x > .6)).mean() > .6
value = _Indicator(value, name=name, plot=plot, overlay=overlay,
color=color, scatter=scatter, legends=legends, histogram=histogram, histogramms=histogramms,
# _Indicator.s Series accessor uses this:
index=self.data.index)
self._indicators.append(value)
return value
_plotting.py
def _plot_indicators():
"""Strategy indicators"""
def _too_many_dims(value):
assert value.ndim >= 2
if value.ndim > 2:
warnings.warn(f"Can't plot indicators with >2D ('{value.name}')",
stacklevel=5)
return True
return False
class LegendStr(str):
# The legend string is such a string that only matches
# itself if it's the exact same object. This ensures
# legend items are listed separately even when they have the
# same string contents. Otherwise, Bokeh would always consider
# equal strings as one and the same legend item.
def __eq__(self, other):
return self is other
ohlc_colors = colorgen()
indicator_figs = []
for i, value in enumerate(indicators):
value = np.atleast_2d(value)
# Use .get()! A user might have assigned a Strategy.data-evolved
# _Array without Strategy.I()
if not value._opts.get('plot') or _too_many_dims(value):
continue
is_overlay = value._opts['overlay']
is_scatter = value._opts['scatter']
is_histogram = value._opts['histogram']
if is_overlay:
fig = fig_ohlc
else:
fig = new_indicator_figure()
indicator_figs.append(fig)
tooltips = []
colors = value._opts['color']
colors = colors and cycle(_as_list(colors)) or (
cycle([next(ohlc_colors)]) if is_overlay else colorgen())
legends = value._opts['legends']
legends = legends and cycle(_as_list(legends))
indicator_name = value.name
legend_label = LegendStr(indicator_name)
#Histogramm
histogramms = value._opts['histogramms']
histogramms = histogramms and cycle(_as_list(histogramms))
for j, arr in enumerate(value, 1):
color = next(colors)
legend_label = next(legends) if legends is not None else legend_label
is_histogram = next(histogramms) if histogramms is not None else is_histogram
source_name = f'{indicator_name}_{i}_{j}'
if arr.dtype == bool:
arr = arr.astype(int)
source.add(arr, source_name)
tooltips.append(f'@{{{source_name}}}{{0,0.0[0000]}}')
if is_overlay:
ohlc_extreme_values[source_name] = arr
if is_histogram:
fig.vbar('index', BAR_WIDTH, source_name, source=source,
legend_label=legend_label, color=color)
elif is_scatter:
fig.scatter(
'index', source_name, source=source,
legend_label=legend_label, color=color,
line_color='black', fill_alpha=.8,
marker='circle', radius=BAR_WIDTH / 2 * 1.5)
else:
fig.line(
'index', source_name, source=source,
legend_label=legend_label, line_color=color,
line_width=1.3)
else:
if is_histogram:
r = fig.vbar('index', BAR_WIDTH, source_name, source=source,
legend_label=LegendStr(legend_label), color=color)
elif is_scatter:
r = fig.scatter(
'index', source_name, source=source,
legend_label=LegendStr(legend_label), color=color,
marker='circle', radius=BAR_WIDTH / 2 * .9)
else:
r = fig.line(
'index', source_name, source=source,
legend_label=LegendStr(legend_label), line_color=color,
line_width=1.3)
# Add dashed centerline just because
mean = float(pd.Series(arr).mean())
if not np.isnan(mean) and (abs(mean) < .1 or
round(abs(mean), 1) == .5 or
round(abs(mean), -1) in (50, 100, 200)):
fig.add_layout(Span(location=float(mean), dimension='width',
line_color='#666666', line_dash='dashed',
line_width=.5))
if is_overlay:
ohlc_tooltips.append((indicator_name, NBSP.join(tooltips)))
else:
set_tooltips(fig, [(indicator_name, NBSP.join(tooltips))],
vline=True, renderers=[r])
# If the sole indicator line on this figure,
# have the legend only contain text without the glyph
if len(value) == 1:
fig.legend.glyph_width = 0
return indicator_figs
I ended up implement everything my own. Thanks @vladimircape
Hi @vladimircape,
Your tutorial is great. I did it but still cannot generate a histogram with different colors for positive and negative ones.
For now, the histogram has only one color:
Do you know how to generate a histogram with multiple colors as @AGG2017 did? Thank you.