Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…into kernc#382
  • Loading branch information
zlpatel committed Jun 16, 2021
2 parents 31bc463 + e8b9dbc commit 18474ce
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 7 deletions.
26 changes: 22 additions & 4 deletions backtesting/_plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,7 @@ def __eq__(self, other):

is_overlay = value._opts['overlay']
is_scatter = value._opts['scatter']
is_histogram = value._opts['histogram']
if is_overlay:
fig = fig_ohlc
else:
Expand All @@ -526,6 +527,10 @@ def __eq__(self, other):
legends = legends and cycle(_as_list(legends))
indicator_name = value.name
legend_label = LegendStr(indicator_name)
indicator_max = value.df.max(axis='columns')
indicator_min = value.df.min(axis='columns')
source.add(indicator_max, f'indicator_{i}_range_max')
source.add(indicator_min, f'indicator_{i}_range_min')
for j, arr in enumerate(value, 1):
color = next(colors)
legend_label = next(legends) if legends is not None else legend_label
Expand All @@ -536,7 +541,10 @@ def __eq__(self, other):
tooltips.append(f'@{{{source_name}}}{{0,0.0[0000]}}')
if is_overlay:
ohlc_extreme_values[source_name] = arr
if is_scatter:
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,
Expand All @@ -548,7 +556,10 @@ def __eq__(self, other):
legend_label=legend_label, line_color=color,
line_width=1.3)
else:
if is_scatter:
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,
Expand Down Expand Up @@ -589,7 +600,8 @@ def __eq__(self, other):
figs_above_ohlc.append(_plot_drawdown_section())

if plot_pl:
figs_above_ohlc.append(_plot_pl_section())
fig_pl = _plot_pl_section()
figs_above_ohlc.append(fig_pl)

if plot_volume:
fig_volume = _plot_volume_section()
Expand All @@ -612,9 +624,15 @@ def __eq__(self, other):

custom_js_args = dict(ohlc_range=fig_ohlc.y_range,
source=source)
if plot_pl:
custom_js_args.update(pl_range=fig_pl.y_range)
if plot_volume:
custom_js_args.update(volume_range=fig_volume.y_range)

indicator_ranges = {}
for idx, indicator in enumerate(indicator_figs):
indicator_range_key = f'indicator_{idx}_range'
indicator_ranges.update({indicator_range_key: indicator.y_range})
custom_js_args.update({'indicator_ranges': indicator_ranges})
fig_ohlc.x_range.js_on_change('end', CustomJS(args=custom_js_args,
code=_AUTOSCALE_JS_CALLBACK))

Expand Down
13 changes: 13 additions & 0 deletions backtesting/autoscale_cb.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,18 @@ window._bt_autoscale_timeout = setTimeout(function () {
max = Math.max.apply(null, source.data['Volume'].slice(i, j));
_bt_scale_range(volume_range, 0, max * 1.03, false);
}

if(indicator_ranges){
let keys = Object.keys(indicator_ranges);
for(var count=0;count<keys.length;count++){
if(keys[count]){
max = Math.max.apply(null, source.data[keys[count]+'_max'].slice(i, j));
min = Math.min.apply(null, source.data[keys[count]+'_min'].slice(i, j));
if(min && max){
_bt_scale_range(indicator_ranges[keys[count]], min, max, true);
}
}
}
}

}, 50);
10 changes: 7 additions & 3 deletions backtesting/backtesting.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def _check_params(self, params):

def I(self, # noqa: E741, E743
func: Callable, *args,
name=None, plot=True, overlay=None, color=None, scatter=False,
name=None, plot=True, overlay=None, color=None, scatter=False, histogram=False,
legends=None, **kwargs) -> np.ndarray:
"""
Declare indicator. An indicator is just an array of values,
Expand Down Expand Up @@ -109,6 +109,10 @@ def I(self, # noqa: E741, E743
legends on your indicator chart. By default it's set to None,
and `name` is used as legends.
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.
Expand Down Expand Up @@ -155,9 +159,9 @@ def init():
overlay = ((x < 1.4) & (x > .6)).mean() > .6

value = _Indicator(value, name=name, plot=plot, overlay=overlay,
color=color, scatter=scatter, legends=legends,
color=color, scatter=scatter, histogram=histogram,
# _Indicator.s Series accessor uses this:
index=self.data.index)
legends=legends, index=self.data.index)
self._indicators.append(value)
return value

Expand Down
37 changes: 37 additions & 0 deletions backtesting/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,43 @@ def next(self):
self.data.Close[index] + self.__atr[index] * self.__n_atr)


class PercentageTrailingStrategy(Strategy):
"""
A strategy with automatic trailing stop-loss, trailing the current
price at distance of some percentage. Call
`PercentageTrailingStrategy.set_trailing_sl()` to set said percentage
(`5` by default). See [tutorials] for usage examples.
[tutorials]: index.html#tutorials
Remember to call `super().init()` and `super().next()` in your
overridden methods.
"""
_sl_percent = 5.

def init(self):
super().init()

def set_trailing_sl(self, percentage: float = 5):
assert percentage > 0, "percentage must be greater than 0"
"""
Sets the future trailing stop-loss as some (`percentage`)
percentage away from the current price.
"""
self._sl_percent = percentage

def next(self):
super().next()
index = len(self.data)-1
for trade in self.trades:
if trade.is_long:
trade.sl = max(trade.sl or -np.inf,
self.data.Close[index]*(1-(self._sl_percent/100)))
else:
trade.sl = min(trade.sl or np.inf,
self.data.Close[index]*(1+(self._sl_percent/100)))


# Prevent pdoc3 documenting __init__ signature of Strategy subclasses
for cls in list(globals().values()):
if isinstance(cls, type) and issubclass(cls, Strategy):
Expand Down
34 changes: 34 additions & 0 deletions backtesting/test/_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
quantile,
SignalStrategy,
TrailingStrategy,
PercentageTrailingStrategy,
resample_apply,
plot_heatmaps,
random_ohlc_data,
Expand Down Expand Up @@ -789,6 +790,24 @@ def next(self):
# Give browser time to open before tempfile is removed
time.sleep(1)

def test_indicator_histogram(self):
class S(Strategy):
def init(self):
self.I(SMA, self.data.Close, 5, overlay=True, scatter=False, histogram=True)
self.I(SMA, self.data.Close, 10, overlay=False, scatter=False, histogram=True)

def next(self):
pass

bt = Backtest(GOOG, S)
bt.run()
with _tempfile() as f:
bt.plot(filename=f,
plot_drawdown=False, plot_equity=False, plot_pl=False, plot_volume=False,
open_browser=True)
# Give browser time to open before tempfile is removed
time.sleep(1)


class TestLib(TestCase):
def test_barssince(self):
Expand Down Expand Up @@ -880,6 +899,21 @@ def next(self):
stats = Backtest(GOOG, S).run()
self.assertEqual(stats['# Trades'], 57)

def test_PercentageTrailingStrategy(self):
class S(PercentageTrailingStrategy):
def init(self):
super().init()
self.set_trailing_sl(5)
self.sma = self.I(lambda: self.data.Close.s.rolling(10).mean())

def next(self):
super().next()
if not self.position and self.data.Close > self.sma:
self.buy()

stats = Backtest(GOOG, S).run()
self.assertEqual(stats['# Trades'], 91)


class TestUtil(TestCase):
def test_as_str(self):
Expand Down

0 comments on commit 18474ce

Please sign in to comment.