Skip to content

Commit ad4c48c

Browse files
committed
REF: Remove useless Backtest.plot(omit_missing=) parameter
1 parent 3058dbe commit ad4c48c

File tree

4 files changed

+64
-97
lines changed

4 files changed

+64
-97
lines changed

backtesting/_plotting.py

+60-88
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import warnings
55
from itertools import cycle, combinations
66
from functools import partial
7+
from typing import List
78

89
import numpy as np
910
import pandas as pd
@@ -31,8 +32,7 @@
3132
from bokeh.palettes import Category10
3233
from bokeh.transform import factor_cmap
3334

34-
from backtesting._util import _data_period, _as_list
35-
35+
from backtesting._util import _data_period, _as_list, _Indicator
3636

3737
with open(os.path.join(os.path.dirname(__file__), 'autoscale_cb.js'),
3838
encoding='utf-8') as _f:
@@ -85,10 +85,13 @@ def lightness(color, lightness=.94):
8585
return color.to_rgb()
8686

8787

88-
def plot(*, results, df, indicators, filename='', plot_width=None,
88+
def plot(*, results: pd.Series,
89+
df: pd.DataFrame,
90+
indicators: List[_Indicator],
91+
filename='', plot_width=None,
8992
plot_equity=True, plot_pl=True,
9093
plot_volume=True, plot_drawdown=False,
91-
smooth_equity=False, relative_equity=True, omit_missing=True,
94+
smooth_equity=False, relative_equity=True,
9295
superimpose=True, show_legend=True, open_browser=True):
9396
"""
9497
Like much of GUI code everywhere, this is a mess.
@@ -101,41 +104,29 @@ def plot(*, results, df, indicators, filename='', plot_width=None,
101104
_bokeh_reset(filename)
102105

103106
COLORS = [BEAR_COLOR, BULL_COLOR]
107+
BAR_WIDTH = .8
104108

105-
equity_data = results['_equity_curve'].copy(False)
109+
assert df.index.equals(results['_equity_curve'].index)
110+
equity_data = results['_equity_curve'].copy(deep=False)
106111
trades = results['_trades']
107112

108-
orig_df = df = df.copy(False)
109-
df.index.name = None # Provides source name @index
110-
index = df.index
111-
assert df.index.equals(equity_data.index)
112-
time_resolution = getattr(index, 'resolution', None)
113-
is_datetime_index = index.is_all_dates
114-
115-
# If all Volume is NaN, don't plot volume
116113
plot_volume = plot_volume and not df.Volume.isnull().all()
114+
time_resolution = getattr(df.index, 'resolution', None)
115+
is_datetime_index = df.index.is_all_dates
117116

118-
# OHLC vbar width in msec.
119-
# +1 will work in case of non-datetime index where vbar width should just be =1
120-
bar_width = 1 + dict(day=86400,
121-
hour=3600,
122-
minute=60,
123-
second=1).get(time_resolution, 0) * 1000 * .85
124-
125-
if is_datetime_index:
126-
# Add index as a separate data source column because true .index is offset to align vbars
127-
df['datetime'] = index
128-
df.index = df.index + pd.Timedelta(bar_width / 2, unit='ms')
117+
from .lib import OHLCV_AGG
118+
# ohlc df may contain many columns. We're only interested in, and pass on to Bokeh, these
119+
df = df[list(OHLCV_AGG.keys())].copy(deep=False)
120+
df.index.name = None # Provides source name @index
121+
df['datetime'] = df.index # Save original, maybe datetime index
129122

130-
if omit_missing:
131-
bar_width = .8
132-
df = df.reset_index(drop=True)
133-
equity_data = equity_data.reset_index(drop=True)
134-
index = df.index
123+
df = df.reset_index(drop=True)
124+
equity_data = equity_data.reset_index(drop=True)
125+
index = df.index
135126

136127
new_bokeh_figure = partial(
137128
_figure,
138-
x_axis_type='datetime' if is_datetime_index and not omit_missing else 'linear',
129+
x_axis_type='linear',
139130
plot_width=plot_width,
140131
plot_height=400,
141132
tools="xpan,xwheel_zoom,box_zoom,undo,redo,reset,crosshair,save",
@@ -152,12 +143,9 @@ def plot(*, results, df, indicators, filename='', plot_width=None,
152143

153144
source = ColumnDataSource(df)
154145
source.add((df.Close >= df.Open).values.astype(np.uint8).astype(str), 'inc')
155-
trades_index = trades['ExitBar']
156-
if not omit_missing:
157-
trades_index = index[trades_index.astype(int)]
158146

159147
trade_source = ColumnDataSource(dict(
160-
index=trades_index,
148+
index=trades['ExitBar'],
161149
datetime=trades['ExitTime'],
162150
exit_price=trades['ExitPrice'],
163151
size=trades['Size'],
@@ -170,7 +158,7 @@ def plot(*, results, df, indicators, filename='', plot_width=None,
170158
lightness(BULL_COLOR, .35)]
171159
trades_cmap = factor_cmap('returns_positive', colors_darker, ['0', '1'])
172160

173-
if is_datetime_index and omit_missing:
161+
if is_datetime_index:
174162
fig_ohlc.xaxis.formatter = FuncTickFormatter(
175163
args=dict(axis=fig_ohlc.xaxis[0],
176164
formatter=DatetimeTickFormatter(days=['%d %b', '%a %d'],
@@ -184,7 +172,7 @@ def plot(*, results, df, indicators, filename='', plot_width=None,
184172
''')
185173

186174
NBSP = ' ' * 4
187-
ohlc_extreme_values = df[['High', 'Low']].copy(False)
175+
ohlc_extreme_values = df[['High', 'Low']].copy(deep=False)
188176
ohlc_tooltips = [
189177
('x, y', NBSP.join(('$index',
190178
'$y{0,0.0[0000]}'))),
@@ -222,39 +210,33 @@ def set_tooltips(fig, tooltips=(), vline=True, renderers=(), show_arrow=True):
222210
def _plot_equity_section():
223211
"""Equity section"""
224212
# Max DD Dur. line
225-
equity = equity_data['Equity'].reset_index(drop=True)
226-
argmax = equity_data['DrawdownDuration'].reset_index(drop=True).idxmax()
227-
try:
228-
dd_start = equity[:argmax].idxmax()
229-
except Exception: # ValueError: attempt to get argmax of an empty sequence
213+
equity = equity_data['Equity'].copy()
214+
dd_end = equity_data['DrawdownDuration'].idxmax()
215+
if np.isnan(dd_end):
230216
dd_start = dd_end = equity.index[0]
231-
timedelta = 0
232217
else:
233-
dd_end = argmax
234-
if is_datetime_index and omit_missing:
235-
# "Calendar" duration
236-
timedelta = df.datetime.iloc[dd_end] - df.datetime.iloc[dd_start]
237-
else:
238-
timedelta = dd_end - dd_start
239-
# Get point intersection
218+
dd_start = equity[:dd_end].idxmax()
219+
# If DD not extending into the future, get exact point of intersection with equity
240220
if dd_end != equity.index[-1]:
241-
x1, x2 = dd_end - 1, dd_end
242-
y, y1, y2 = equity[dd_start], equity[x1], equity[x2]
243-
dd_end -= (1 - (y - y1) / (y2 - y1)) * (dd_end - x1) # y = a x + b
221+
dd_end = np.interp(equity[dd_start],
222+
(equity[dd_end - 1], equity[dd_end]),
223+
(dd_end - 1, dd_end))
244224

245225
if smooth_equity:
246-
select = (pd.Index(trades['ExitBar']) |
247-
# Include beginning and end
248-
equity.index[:1] | equity.index[-1:] |
249-
# Include peak equity and peak DD
250-
pd.Index([equity.idxmax(), argmax]) |
251-
# Include max dd end points. Otherwise the MaxDD line looks amiss.
252-
pd.Index([dd_start, int(dd_end), min(equity.size - 1, int(dd_end + 1))]))
226+
interest_points = pd.Index([
227+
# Beginning and end
228+
equity.index[0], equity.index[-1],
229+
# Peak equity and peak DD
230+
equity.idxmax(), equity_data['DrawdownPct'].idxmax(),
231+
# Include max dd end points. Otherwise the MaxDD line looks amiss.
232+
dd_start, int(dd_end), min(int(dd_end + 1), equity.size - 1),
233+
])
234+
select = pd.Index(trades['ExitBar']) | interest_points
253235
select = select.unique().dropna()
254236
equity = equity.iloc[select].reindex(equity.index)
255237
equity.interpolate(inplace=True)
256238

257-
equity.index = equity_data.index
239+
assert equity.index.equals(equity_data.index)
258240

259241
if relative_equity:
260242
equity /= equity.iloc[0]
@@ -302,9 +284,10 @@ def _plot_equity_section():
302284
fig.scatter(argmax, equity[argmax],
303285
legend_label='Max Drawdown (-{:.1f}%)'.format(100 * drawdown[argmax]),
304286
color='red', size=8)
305-
fig.line([index[dd_start], index[int(dd_end)]], equity.iloc[dd_start],
287+
dd_timedelta_label = df['datetime'].iloc[int(round(dd_end))] - df['datetime'].iloc[dd_start]
288+
fig.line([dd_start, dd_end], equity.iloc[dd_start],
306289
line_color='red', line_width=2,
307-
legend_label='Max Dd Dur. ({})'.format(timedelta)
290+
legend_label='Max Dd Dur. ({})'.format(dd_timedelta_label)
308291
.replace(' 00:00:00', '')
309292
.replace('(0 days ', '('))
310293

@@ -354,7 +337,7 @@ def _plot_volume_section():
354337
fig.xaxis.formatter = fig_ohlc.xaxis[0].formatter
355338
fig.xaxis.visible = True
356339
fig_ohlc.xaxis.visible = False # Show only Volume's xaxis
357-
r = fig.vbar('index', bar_width, 'Volume', source=source, color=inc_cmap)
340+
r = fig.vbar('index', BAR_WIDTH, 'Volume', source=source, color=inc_cmap)
358341
set_tooltips(fig, [('Volume', '@Volume{0.00 a}')], renderers=[r])
359342
fig.yaxis.formatter = NumeralTickFormatter(format="0 a")
360343
return fig
@@ -374,48 +357,37 @@ def _plot_superimposed_ohlc():
374357
stacklevel=4)
375358
return
376359

377-
orig_df['_width'] = 1
378-
from .lib import OHLCV_AGG
379-
df2 = orig_df.resample(resample_rule, label='left').agg(dict(OHLCV_AGG, _width='count'))
360+
df2 = (df.assign(_width=1).set_index('datetime')
361+
.resample(resample_rule, label='left')
362+
.agg(dict(OHLCV_AGG, _width='count')))
380363

381364
# Check if resampling was downsampling; error on upsampling
382-
orig_freq = _data_period(orig_df)
383-
resample_freq = _data_period(df2)
365+
orig_freq = _data_period(df['datetime'])
366+
resample_freq = _data_period(df2.index)
384367
if resample_freq < orig_freq:
385368
raise ValueError('Invalid value for `superimpose`: Upsampling not supported.')
386369
if resample_freq == orig_freq:
387370
warnings.warn('Superimposed OHLC plot matches the original plot. Skipping.',
388371
stacklevel=4)
389372
return
390373

391-
if omit_missing:
392-
width2 = '_width'
393-
df2.index = df2['_width'].cumsum().shift(1).fillna(0)
394-
df2.index += df2['_width'] / 2 - .5
395-
df2['_width'] -= .1 # Candles don't touch
396-
else:
397-
del df['_width']
398-
width2 = dict(day=86400 * 5,
399-
hour=86400,
400-
minute=3600,
401-
second=60)[time_resolution] * 1000
402-
df2.index += pd.Timedelta(
403-
width2 / 2 +
404-
(width2 / 5 if resample_rule == 'W' else 0), # Sunday week start
405-
unit='ms')
406-
df2['inc'] = (df2.Close >= df2.Open).astype(np.uint8).astype(str)
374+
df2.index = df2['_width'].cumsum().shift(1).fillna(0)
375+
df2.index += df2['_width'] / 2 - .5
376+
df2['_width'] -= .1 # Candles don't touch
377+
378+
df2['inc'] = (df2.Close >= df2.Open).astype(int).astype(str)
407379
df2.index.name = None
408380
source2 = ColumnDataSource(df2)
409381
fig_ohlc.segment('index', 'High', 'index', 'Low', source=source2, color='#bbbbbb')
410382
colors_lighter = [lightness(BEAR_COLOR, .92),
411383
lightness(BULL_COLOR, .92)]
412-
fig_ohlc.vbar('index', width2, 'Open', 'Close', source=source2, line_color=None,
384+
fig_ohlc.vbar('index', '_width', 'Open', 'Close', source=source2, line_color=None,
413385
fill_color=factor_cmap('inc', colors_lighter, ['0', '1']))
414386

415387
def _plot_ohlc():
416388
"""Main OHLC bars"""
417389
fig_ohlc.segment('index', 'High', 'index', 'Low', source=source, color="black")
418-
r = fig_ohlc.vbar('index', bar_width, 'Open', 'Close', source=source,
390+
r = fig_ohlc.vbar('index', BAR_WIDTH, 'Open', 'Close', source=source,
419391
line_color="black", fill_color=inc_cmap)
420392
return r
421393

@@ -484,7 +456,7 @@ def __eq__(self, other):
484456
'index', source_name, source=source,
485457
legend_label=legend_label, color=color,
486458
line_color='black', fill_alpha=.8,
487-
marker='circle', radius=bar_width / 2 * 1.5)
459+
marker='circle', radius=BAR_WIDTH / 2 * 1.5)
488460
else:
489461
fig.line(
490462
'index', source_name, source=source,
@@ -495,7 +467,7 @@ def __eq__(self, other):
495467
r = fig.scatter(
496468
'index', source_name, source=source,
497469
legend_label=LegendStr(legend_label), color=color,
498-
marker='circle', radius=bar_width / 2 * .9)
470+
marker='circle', radius=BAR_WIDTH / 2 * .9)
499471
else:
500472
r = fig.line(
501473
'index', source_name, source=source,

backtesting/_util.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ def _as_list(value):
3434
return [value]
3535

3636

37-
def _data_period(df):
37+
def _data_period(index):
3838
"""Return data index period as pd.Timedelta"""
39-
values = df.index[-100:].to_series()
39+
values = pd.Series(index[-100:])
4040
return values.diff().median()
4141

4242

backtesting/backtesting.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -1227,7 +1227,7 @@ def _compute_stats(self, broker: _Broker, strategy: Strategy) -> pd.Series:
12271227
returns = trades_df['ReturnPct']
12281228
durations = trades_df['Duration']
12291229

1230-
def _round_timedelta(value, _period=_data_period(equity_df)):
1230+
def _round_timedelta(value, _period=_data_period(index)):
12311231
if not isinstance(value, pd.Timedelta):
12321232
return value
12331233
resolution = getattr(_period, 'resolution_string', None) or _period.resolution
@@ -1284,7 +1284,7 @@ def plot(self, *, results: pd.Series = None, filename=None, plot_width=None,
12841284
plot_equity=True, plot_pl=True,
12851285
plot_volume=True, plot_drawdown=False,
12861286
smooth_equity=False, relative_equity=True,
1287-
omit_missing=True, superimpose: Union[bool, str] = True,
1287+
superimpose: Union[bool, str] = True,
12881288
show_legend=True, open_browser=True):
12891289
"""
12901290
Plot the progression of the last backtest run.
@@ -1322,9 +1322,6 @@ def plot(self, *, results: pd.Series = None, filename=None, plot_width=None,
13221322
If `relative_equity` is `True`, scale and label equity graph axis
13231323
with return percent, not absolute cash-equivalent values.
13241324
1325-
If `omit_missing` is `True`, skip missing candlestick bars on the
1326-
datetime axis.
1327-
13281325
If `superimpose` is `True`, superimpose downsampled candlesticks
13291326
over the original candlestick chart. Default downsampling is:
13301327
weekly for daily data, daily for hourly data, hourly for minute data,
@@ -1356,7 +1353,6 @@ def plot(self, *, results: pd.Series = None, filename=None, plot_width=None,
13561353
plot_equity=plot_equity,
13571354
plot_pl=plot_pl,
13581355
plot_volume=plot_volume,
1359-
omit_missing=omit_missing,
13601356
plot_drawdown=plot_drawdown,
13611357
smooth_equity=smooth_equity,
13621358
relative_equity=relative_equity,

backtesting/test/_test.py

-1
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,6 @@ def test_params(self):
422422
plot_pl=False,
423423
plot_drawdown=True,
424424
superimpose=False,
425-
omit_missing=False,
426425
smooth_equity=False,
427426
relative_equity=False,
428427
show_legend=False).items():

0 commit comments

Comments
 (0)