Skip to content

Commit 2223b22

Browse files
committed
REF: Remove useless Backtest.plot(omit_missing=) parameter
1 parent ffd6409 commit 2223b22

File tree

4 files changed

+61
-92
lines changed

4 files changed

+61
-92
lines changed

backtesting/_plotting.py

+57-83
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,36 @@ 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+
max_dd_loc = equity_data['DrawdownPct'].idxmax()
215+
dd_end = equity_data['DrawdownDuration'].idxmax()
216+
if np.isnan(dd_end):
230217
dd_start = dd_end = equity.index[0]
231-
timedelta = 0
218+
dd_timedelta_label = 0
232219
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
220+
dd_start = equity[:dd_end].idxmax()
221+
dd_timedelta_label = df['datetime'].iloc[dd_end] - df['datetime'].iloc[dd_start]
239222
# Get point intersection
240223
if dd_end != equity.index[-1]:
241224
x1, x2 = dd_end - 1, dd_end
242225
y, y1, y2 = equity[dd_start], equity[x1], equity[x2]
243226
dd_end -= (1 - (y - y1) / (y2 - y1)) * (dd_end - x1) # y = a x + b
244227

245228
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))]))
229+
interest_points = pd.Index([
230+
# Beginning and end
231+
equity.index[0], equity.index[-1],
232+
# Peak equity and peak DD
233+
equity.idxmax(), max_dd_loc,
234+
# Include max dd end points. Otherwise the MaxDD line looks amiss.
235+
dd_start, int(dd_end), min(int(dd_end + 1), equity.size - 1),
236+
])
237+
select = pd.Index(trades['ExitBar']) | interest_points
253238
select = select.unique().dropna()
254239
equity = equity.iloc[select].reindex(equity.index)
255240
equity.interpolate(inplace=True)
256241

257-
equity.index = equity_data.index
242+
assert equity.index.equals(equity_data.index)
258243

259244
if relative_equity:
260245
equity /= equity.iloc[0]
@@ -304,7 +289,7 @@ def _plot_equity_section():
304289
color='red', size=8)
305290
fig.line([index[dd_start], index[int(dd_end)]], equity.iloc[dd_start],
306291
line_color='red', line_width=2,
307-
legend_label='Max Dd Dur. ({})'.format(timedelta)
292+
legend_label='Max Dd Dur. ({})'.format(dd_timedelta_label)
308293
.replace(' 00:00:00', '')
309294
.replace('(0 days ', '('))
310295

@@ -354,7 +339,7 @@ def _plot_volume_section():
354339
fig.xaxis.formatter = fig_ohlc.xaxis[0].formatter
355340
fig.xaxis.visible = True
356341
fig_ohlc.xaxis.visible = False # Show only Volume's xaxis
357-
r = fig.vbar('index', bar_width, 'Volume', source=source, color=inc_cmap)
342+
r = fig.vbar('index', BAR_WIDTH, 'Volume', source=source, color=inc_cmap)
358343
set_tooltips(fig, [('Volume', '@Volume{0.00 a}')], renderers=[r])
359344
fig.yaxis.formatter = NumeralTickFormatter(format="0 a")
360345
return fig
@@ -374,48 +359,37 @@ def _plot_superimposed_ohlc():
374359
stacklevel=4)
375360
return
376361

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'))
362+
df2 = (df.assign(_width=1).set_index('datetime')
363+
.resample(resample_rule, label='left')
364+
.agg(dict(OHLCV_AGG, _width='count')))
380365

381366
# Check if resampling was downsampling; error on upsampling
382-
orig_freq = _data_period(orig_df)
383-
resample_freq = _data_period(df2)
367+
orig_freq = _data_period(df['datetime'])
368+
resample_freq = _data_period(df2.index)
384369
if resample_freq < orig_freq:
385370
raise ValueError('Invalid value for `superimpose`: Upsampling not supported.')
386371
if resample_freq == orig_freq:
387372
warnings.warn('Superimposed OHLC plot matches the original plot. Skipping.',
388373
stacklevel=4)
389374
return
390375

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)
376+
df2.index = df2['_width'].cumsum().shift(1).fillna(0)
377+
df2.index += df2['_width'] / 2 - .5
378+
df2['_width'] -= .1 # Candles don't touch
379+
380+
df2['inc'] = (df2.Close >= df2.Open).astype(int).astype(str)
407381
df2.index.name = None
408382
source2 = ColumnDataSource(df2)
409383
fig_ohlc.segment('index', 'High', 'index', 'Low', source=source2, color='#bbbbbb')
410384
colors_lighter = [lightness(BEAR_COLOR, .92),
411385
lightness(BULL_COLOR, .92)]
412-
fig_ohlc.vbar('index', width2, 'Open', 'Close', source=source2, line_color=None,
386+
fig_ohlc.vbar('index', '_width', 'Open', 'Close', source=source2, line_color=None,
413387
fill_color=factor_cmap('inc', colors_lighter, ['0', '1']))
414388

415389
def _plot_ohlc():
416390
"""Main OHLC bars"""
417391
fig_ohlc.segment('index', 'High', 'index', 'Low', source=source, color="black")
418-
r = fig_ohlc.vbar('index', bar_width, 'Open', 'Close', source=source,
392+
r = fig_ohlc.vbar('index', BAR_WIDTH, 'Open', 'Close', source=source,
419393
line_color="black", fill_color=inc_cmap)
420394
return r
421395

@@ -484,7 +458,7 @@ def __eq__(self, other):
484458
'index', source_name, source=source,
485459
legend_label=legend_label, color=color,
486460
line_color='black', fill_alpha=.8,
487-
marker='circle', radius=bar_width / 2 * 1.5)
461+
marker='circle', radius=BAR_WIDTH / 2 * 1.5)
488462
else:
489463
fig.line(
490464
'index', source_name, source=source,
@@ -495,7 +469,7 @@ def __eq__(self, other):
495469
r = fig.scatter(
496470
'index', source_name, source=source,
497471
legend_label=LegendStr(legend_label), color=color,
498-
marker='circle', radius=bar_width / 2 * .9)
472+
marker='circle', radius=BAR_WIDTH / 2 * .9)
499473
else:
500474
r = fig.line(
501475
'index', source_name, source=source,

backtesting/_util.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ def _as_list(value):
3232
return [value]
3333

3434

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

4040

backtesting/backtesting.py

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

1195-
def _round_timedelta(value, _period=_data_period(equity_df)):
1195+
def _round_timedelta(value, _period=_data_period(index)):
11961196
if not isinstance(value, pd.Timedelta):
11971197
return value
11981198
resolution = getattr(_period, 'resolution_string', None) or _period.resolution
@@ -1249,7 +1249,7 @@ def plot(self, *, results: pd.Series = None, filename=None, plot_width=None,
12491249
plot_equity=True, plot_pl=True,
12501250
plot_volume=True, plot_drawdown=False,
12511251
smooth_equity=False, relative_equity=True,
1252-
omit_missing=True, superimpose: Union[bool, str] = True,
1252+
superimpose: Union[bool, str] = True,
12531253
show_legend=True, open_browser=True):
12541254
"""
12551255
Plot the progression of the last backtest run.
@@ -1287,9 +1287,6 @@ def plot(self, *, results: pd.Series = None, filename=None, plot_width=None,
12871287
If `relative_equity` is `True`, scale and label equity graph axis
12881288
with return percent, not absolute cash-equivalent values.
12891289
1290-
If `omit_missing` is `True`, skip missing candlestick bars on the
1291-
datetime axis.
1292-
12931290
If `superimpose` is `True`, superimpose downsampled candlesticks
12941291
over the original candlestick chart. Default downsampling is:
12951292
weekly for daily data, daily for hourly data, hourly for minute data,
@@ -1321,7 +1318,6 @@ def plot(self, *, results: pd.Series = None, filename=None, plot_width=None,
13211318
plot_equity=plot_equity,
13221319
plot_pl=plot_pl,
13231320
plot_volume=plot_volume,
1324-
omit_missing=omit_missing,
13251321
plot_drawdown=plot_drawdown,
13261322
smooth_equity=smooth_equity,
13271323
relative_equity=relative_equity,

backtesting/test/_test.py

-1
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,6 @@ def test_params(self):
418418
plot_pl=False,
419419
plot_drawdown=True,
420420
superimpose=False,
421-
omit_missing=False,
422421
smooth_equity=False,
423422
relative_equity=False,
424423
show_legend=False).items():

0 commit comments

Comments
 (0)