Skip to content

Commit 1541283

Browse files
committed
REF: Remove useless Backtest.plot(omit_missing=) parameter
1 parent 6d5e993 commit 1541283

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,11 +143,8 @@ 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
trade_source = ColumnDataSource(dict(
159-
index=trades_index,
147+
index=trades['ExitBar'],
160148
datetime=trades['ExitTime'],
161149
exit_price=trades['ExitPrice'],
162150
returns_positive=(trades['ReturnPct'] > 0).astype(int).astype(str),
@@ -168,7 +156,7 @@ def plot(*, results, df, indicators, filename='', plot_width=None,
168156
lightness(BULL_COLOR, .35)]
169157
trades_cmap = factor_cmap('returns_positive', colors_darker, ['0', '1'])
170158

171-
if is_datetime_index and omit_missing:
159+
if is_datetime_index:
172160
fig_ohlc.xaxis.formatter = FuncTickFormatter(
173161
args=dict(axis=fig_ohlc.xaxis[0],
174162
formatter=DatetimeTickFormatter(days=['%d %b', '%a %d'],
@@ -182,7 +170,7 @@ def plot(*, results, df, indicators, filename='', plot_width=None,
182170
''')
183171

184172
NBSP = ' ' * 4
185-
ohlc_extreme_values = df[['High', 'Low']].copy(False)
173+
ohlc_extreme_values = df[['High', 'Low']].copy(deep=False)
186174
ohlc_tooltips = [
187175
('x, y', NBSP.join(('$index',
188176
'$y{0,0.0[0000]}'))),
@@ -220,39 +208,36 @@ def set_tooltips(fig, tooltips=(), vline=True, renderers=(), show_arrow=True):
220208
def _plot_equity_section():
221209
"""Equity section"""
222210
# Max DD Dur. line
223-
equity = equity_data['Equity'].reset_index(drop=True)
224-
argmax = equity_data['DrawdownDuration'].reset_index(drop=True).idxmax()
225-
try:
226-
dd_start = equity[:argmax].idxmax()
227-
except Exception: # ValueError: attempt to get argmax of an empty sequence
211+
equity = equity_data['Equity'].copy()
212+
max_dd_loc = equity_data['DrawdownPct'].idxmax()
213+
dd_end = equity_data['DrawdownDuration'].idxmax()
214+
if np.isnan(dd_end):
228215
dd_start = dd_end = equity.index[0]
229-
timedelta = 0
216+
dd_timedelta_label = 0
230217
else:
231-
dd_end = argmax
232-
if is_datetime_index and omit_missing:
233-
# "Calendar" duration
234-
timedelta = df.datetime.iloc[dd_end] - df.datetime.iloc[dd_start]
235-
else:
236-
timedelta = dd_end - dd_start
218+
dd_start = equity[:dd_end].idxmax()
219+
dd_timedelta_label = df['datetime'].iloc[dd_end] - df['datetime'].iloc[dd_start]
237220
# Get point intersection
238221
if dd_end != equity.index[-1]:
239222
x1, x2 = dd_end - 1, dd_end
240223
y, y1, y2 = equity[dd_start], equity[x1], equity[x2]
241224
dd_end -= (1 - (y - y1) / (y2 - y1)) * (dd_end - x1) # y = a x + b
242225

243226
if smooth_equity:
244-
select = (pd.Index(trades['ExitBar']) |
245-
# Include beginning and end
246-
equity.index[:1] | equity.index[-1:] |
247-
# Include peak equity and peak DD
248-
pd.Index([equity.idxmax(), argmax]) |
249-
# Include max dd end points. Otherwise the MaxDD line looks amiss.
250-
pd.Index([dd_start, int(dd_end), min(equity.size - 1, int(dd_end + 1))]))
227+
interest_points = pd.Index([
228+
# Beginning and end
229+
equity.index[0], equity.index[-1],
230+
# Peak equity and peak DD
231+
equity.idxmax(), max_dd_loc,
232+
# Include max dd end points. Otherwise the MaxDD line looks amiss.
233+
dd_start, int(dd_end), min(int(dd_end + 1), equity.size - 1),
234+
])
235+
select = pd.Index(trades['ExitBar']) | interest_points
251236
select = select.unique().dropna()
252237
equity = equity.iloc[select].reindex(equity.index)
253238
equity.interpolate(inplace=True)
254239

255-
equity.index = equity_data.index
240+
assert equity.index.equals(equity_data.index)
256241

257242
if relative_equity:
258243
equity /= equity.iloc[0]
@@ -302,7 +287,7 @@ def _plot_equity_section():
302287
color='red', size=8)
303288
fig.line([index[dd_start], index[int(dd_end)]], equity.iloc[dd_start],
304289
line_color='red', line_width=2,
305-
legend_label='Max Dd Dur. ({})'.format(timedelta)
290+
legend_label='Max Dd Dur. ({})'.format(dd_timedelta_label)
306291
.replace(' 00:00:00', '')
307292
.replace('(0 days ', '('))
308293

@@ -347,7 +332,7 @@ def _plot_volume_section():
347332
fig.xaxis.formatter = fig_ohlc.xaxis[0].formatter
348333
fig.xaxis.visible = True
349334
fig_ohlc.xaxis.visible = False # Show only Volume's xaxis
350-
r = fig.vbar('index', bar_width, 'Volume', source=source, color=inc_cmap)
335+
r = fig.vbar('index', BAR_WIDTH, 'Volume', source=source, color=inc_cmap)
351336
set_tooltips(fig, [('Volume', '@Volume{0.00 a}')], renderers=[r])
352337
fig.yaxis.formatter = NumeralTickFormatter(format="0 a")
353338
return fig
@@ -367,48 +352,37 @@ def _plot_superimposed_ohlc():
367352
stacklevel=4)
368353
return
369354

370-
orig_df['_width'] = 1
371-
from .lib import OHLCV_AGG
372-
df2 = orig_df.resample(resample_rule, label='left').agg(dict(OHLCV_AGG, _width='count'))
355+
df2 = (df.assign(_width=1).set_index('datetime')
356+
.resample(resample_rule, label='left')
357+
.agg(dict(OHLCV_AGG, _width='count')))
373358

374359
# Check if resampling was downsampling; error on upsampling
375-
orig_freq = _data_period(orig_df)
376-
resample_freq = _data_period(df2)
360+
orig_freq = _data_period(df['datetime'])
361+
resample_freq = _data_period(df2.index)
377362
if resample_freq < orig_freq:
378363
raise ValueError('Invalid value for `superimpose`: Upsampling not supported.')
379364
if resample_freq == orig_freq:
380365
warnings.warn('Superimposed OHLC plot matches the original plot. Skipping.',
381366
stacklevel=4)
382367
return
383368

384-
if omit_missing:
385-
width2 = '_width'
386-
df2.index = df2['_width'].cumsum().shift(1).fillna(0)
387-
df2.index += df2['_width'] / 2 - .5
388-
df2['_width'] -= .1 # Candles don't touch
389-
else:
390-
del df['_width']
391-
width2 = dict(day=86400 * 5,
392-
hour=86400,
393-
minute=3600,
394-
second=60)[time_resolution] * 1000
395-
df2.index += pd.Timedelta(
396-
width2 / 2 +
397-
(width2 / 5 if resample_rule == 'W' else 0), # Sunday week start
398-
unit='ms')
399-
df2['inc'] = (df2.Close >= df2.Open).astype(np.uint8).astype(str)
369+
df2.index = df2['_width'].cumsum().shift(1).fillna(0)
370+
df2.index += df2['_width'] / 2 - .5
371+
df2['_width'] -= .1 # Candles don't touch
372+
373+
df2['inc'] = (df2.Close >= df2.Open).astype(int).astype(str)
400374
df2.index.name = None
401375
source2 = ColumnDataSource(df2)
402376
fig_ohlc.segment('index', 'High', 'index', 'Low', source=source2, color='#bbbbbb')
403377
colors_lighter = [lightness(BEAR_COLOR, .92),
404378
lightness(BULL_COLOR, .92)]
405-
fig_ohlc.vbar('index', width2, 'Open', 'Close', source=source2, line_color=None,
379+
fig_ohlc.vbar('index', '_width', 'Open', 'Close', source=source2, line_color=None,
406380
fill_color=factor_cmap('inc', colors_lighter, ['0', '1']))
407381

408382
def _plot_ohlc():
409383
"""Main OHLC bars"""
410384
fig_ohlc.segment('index', 'High', 'index', 'Low', source=source, color="black")
411-
r = fig_ohlc.vbar('index', bar_width, 'Open', 'Close', source=source,
385+
r = fig_ohlc.vbar('index', BAR_WIDTH, 'Open', 'Close', source=source,
412386
line_color="black", fill_color=inc_cmap)
413387
return r
414388

@@ -477,7 +451,7 @@ def __eq__(self, other):
477451
'index', source_name, source=source,
478452
legend_label=legend_label, color=color,
479453
line_color='black', fill_alpha=.8,
480-
marker='circle', radius=bar_width / 2 * 1.5)
454+
marker='circle', radius=BAR_WIDTH / 2 * 1.5)
481455
else:
482456
fig.line(
483457
'index', source_name, source=source,
@@ -488,7 +462,7 @@ def __eq__(self, other):
488462
r = fig.scatter(
489463
'index', source_name, source=source,
490464
legend_label=LegendStr(legend_label), color=color,
491-
marker='circle', radius=bar_width / 2 * .9)
465+
marker='circle', radius=BAR_WIDTH / 2 * .9)
492466
else:
493467
r = fig.line(
494468
'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)