Skip to content

Commit 953c9e8

Browse files
committed
ENH: New Order/Trade/Position API
1 parent 41b1ddf commit 953c9e8

File tree

5 files changed

+662
-339
lines changed

5 files changed

+662
-339
lines changed

backtesting/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,6 @@
3939
except ImportError:
4040
pass # Package not installed
4141

42-
from .backtesting import Backtest, Strategy, Orders, Position # noqa: F401
42+
from .backtesting import Backtest, Strategy # noqa: F401
4343
from . import lib # noqa: F401
4444
from ._plotting import set_bokeh_output # noqa: F401

backtesting/_plotting.py

+49-41
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,13 @@ def plot(*, results, df, indicators, filename='', plot_width=None,
102102

103103
COLORS = [BEAR_COLOR, BULL_COLOR]
104104

105-
orig_trade_data = trade_data = results._trade_data.copy(False)
105+
equity_data = results['_equity_curve'].copy(False)
106+
trades = results['_trades']
106107

107108
orig_df = df = df.copy(False)
108109
df.index.name = None # Provides source name @index
109110
index = df.index
111+
assert df.index.equals(equity_data.index)
110112
time_resolution = getattr(index, 'resolution', None)
111113
is_datetime_index = index.is_all_dates
112114

@@ -128,7 +130,7 @@ def plot(*, results, df, indicators, filename='', plot_width=None,
128130
if omit_missing:
129131
bar_width = .8
130132
df = df.reset_index(drop=True)
131-
trade_data = trade_data.reset_index(drop=True)
133+
equity_data = equity_data.reset_index(drop=True)
132134
index = df.index
133135

134136
new_bokeh_figure = partial(
@@ -150,19 +152,23 @@ def plot(*, results, df, indicators, filename='', plot_width=None,
150152

151153
source = ColumnDataSource(df)
152154
source.add((df.Close >= df.Open).values.astype(np.uint8).astype(str), 'inc')
153-
returns = trade_data['Returns'].dropna()
155+
trades_index = trades['ExitBar']
156+
if not omit_missing:
157+
trades_index = index[trades_index.astype(int)]
158+
154159
trade_source = ColumnDataSource(dict(
155-
index=returns.index,
156-
datetime=orig_trade_data['Returns'].dropna().index,
157-
exit_price=trade_data['Exit Price'].dropna(),
158-
returns_pos=(returns > 0).astype(np.int8).astype(str),
160+
index=trades_index,
161+
datetime=trades['ExitTime'],
162+
exit_price=trades['ExitPrice'],
163+
size=trades['Size'],
164+
returns_positive=(trades['ReturnPct'] > 0).astype(int).astype(str),
159165
))
160166

161167
inc_cmap = factor_cmap('inc', COLORS, ['0', '1'])
162-
cmap = factor_cmap('returns_pos', COLORS, ['0', '1'])
168+
cmap = factor_cmap('returns_positive', COLORS, ['0', '1'])
163169
colors_darker = [lightness(BEAR_COLOR, .35),
164170
lightness(BULL_COLOR, .35)]
165-
trades_cmap = factor_cmap('returns_pos', colors_darker, ['0', '1'])
171+
trades_cmap = factor_cmap('returns_positive', colors_darker, ['0', '1'])
166172

167173
if is_datetime_index and omit_missing:
168174
fig_ohlc.xaxis.formatter = FuncTickFormatter(
@@ -216,8 +222,8 @@ def set_tooltips(fig, tooltips=(), vline=True, renderers=(), show_arrow=True):
216222
def _plot_equity_section():
217223
"""Equity section"""
218224
# Max DD Dur. line
219-
equity = trade_data['Equity']
220-
argmax = trade_data['Drawdown Duration'].idxmax()
225+
equity = equity_data['Equity'].reset_index(drop=True)
226+
argmax = equity_data['DrawdownDuration'].reset_index(drop=True).idxmax()
221227
try:
222228
dd_start = equity[:argmax].idxmax()
223229
except Exception: # ValueError: attempt to get argmax of an empty sequence
@@ -231,21 +237,25 @@ def _plot_equity_section():
231237
else:
232238
timedelta = dd_end - dd_start
233239
# Get point intersection
234-
if dd_end != index[-1]:
235-
x1, x2 = index.get_loc(dd_end) - 1, index.get_loc(dd_end)
240+
if dd_end != equity.index[-1]:
241+
x1, x2 = dd_end - 1, dd_end
236242
y, y1, y2 = equity[dd_start], equity[x1], equity[x2]
237-
dd_end -= (1 - (y - y1) / (y2 - y1)) * (dd_end - index[x1]) # y = a x + b
243+
dd_end -= (1 - (y - y1) / (y2 - y1)) * (dd_end - x1) # y = a x + b
238244

239245
if smooth_equity:
240-
select = (trade_data[['Entry Price',
241-
'Exit Price']].dropna(how='all').index |
242-
# Include beginning
243-
equity.index[:1] |
244-
# Include max dd end points. Otherwise, the MaxDD line looks amiss.
245-
pd.Index([dd_start, dd_end]))
246-
equity = equity[select].reindex(equity.index)
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))]))
253+
select = select.unique().dropna()
254+
equity = equity.iloc[select].reindex(equity.index)
247255
equity.interpolate(inplace=True)
248256

257+
equity.index = equity_data.index
258+
249259
if relative_equity:
250260
equity /= equity.iloc[0]
251261

@@ -287,12 +297,12 @@ def _plot_equity_section():
287297
color='blue', size=8)
288298

289299
if not plot_drawdown:
290-
drawdown = trade_data['Drawdown']
300+
drawdown = equity_data['DrawdownPct']
291301
argmax = drawdown.idxmax()
292302
fig.scatter(argmax, equity[argmax],
293303
legend_label='Max Drawdown (-{:.1f}%)'.format(100 * drawdown[argmax]),
294304
color='red', size=8)
295-
fig.line([dd_start, dd_end], equity[dd_start],
305+
fig.line([index[dd_start], index[int(dd_end)]], equity.iloc[dd_start],
296306
line_color='red', line_width=2,
297307
legend_label='Max Dd Dur. ({})'.format(timedelta)
298308
.replace(' 00:00:00', '')
@@ -303,7 +313,7 @@ def _plot_equity_section():
303313
def _plot_drawdown_section():
304314
"""Drawdown section"""
305315
fig = new_indicator_figure(y_axis_label="Drawdown")
306-
drawdown = trade_data['Drawdown']
316+
drawdown = equity_data['DrawdownPct']
307317
argmax = drawdown.idxmax()
308318
source.add(drawdown, 'drawdown')
309319
r = fig.line('index', 'drawdown', source=source, line_width=1.3)
@@ -319,20 +329,22 @@ def _plot_pl_section():
319329
fig = new_indicator_figure(y_axis_label="Profit / Loss")
320330
fig.add_layout(Span(location=0, dimension='width', line_color='#666666',
321331
line_dash='dashed', line_width=1))
322-
position = trade_data['Exit Position'].dropna()
323-
returns_long = returns.copy()
324-
returns_short = returns.copy()
325-
returns_long[position < 0] = np.nan
326-
returns_short[position > 0] = np.nan
332+
returns_long = np.where(trades['Size'] > 0, trades['ReturnPct'], np.nan)
333+
returns_short = np.where(trades['Size'] < 0, trades['ReturnPct'], np.nan)
334+
size = trades['Size'].abs()
335+
size = np.interp(size, (size.min(), size.max()), (10, 20))
327336
trade_source.add(returns_long, 'returns_long')
328337
trade_source.add(returns_short, 'returns_short')
329-
MARKER_SIZE = 13
338+
trade_source.add(size, 'marker_size')
330339
r1 = fig.scatter('index', 'returns_long', source=trade_source, fill_color=cmap,
331-
marker='triangle', line_color='black', size=MARKER_SIZE)
340+
marker='triangle', line_color='black', size='marker_size')
332341
r2 = fig.scatter('index', 'returns_short', source=trade_source, fill_color=cmap,
333-
marker='inverted_triangle', line_color='black', size=MARKER_SIZE)
334-
set_tooltips(fig, [("P/L", "@returns_long{+0.[000]%}")], vline=False, renderers=[r1])
335-
set_tooltips(fig, [("P/L", "@returns_short{+0.[000]%}")], vline=False, renderers=[r2])
342+
marker='inverted_triangle', line_color='black', size='marker_size')
343+
tooltips = [("Size", "@size{0,0}")]
344+
set_tooltips(fig, tooltips + [("P/L", "@returns_long{+0.[000]%}")],
345+
vline=False, renderers=[r1])
346+
set_tooltips(fig, tooltips + [("P/L", "@returns_short{+0.[000]%}")],
347+
vline=False, renderers=[r2])
336348
fig.yaxis.formatter = NumeralTickFormatter(format="0.[00]%")
337349
return fig
338350

@@ -409,15 +421,11 @@ def _plot_ohlc():
409421

410422
def _plot_ohlc_trades():
411423
"""Trade entry / exit markers on OHLC plot"""
412-
exit_price = trade_data['Exit Price'].dropna()
413-
entry_price = trade_data['Entry Price'].dropna().iloc[:exit_price.size] # entry can be one more at the end # noqa: E501
414-
trade_source.add(np.column_stack((entry_price.index, exit_price.index)).tolist(),
415-
'position_lines_xs')
416-
trade_source.add(np.column_stack((entry_price, exit_price)).tolist(),
417-
'position_lines_ys')
424+
trade_source.add(trades[['EntryBar', 'ExitBar']].values.tolist(), 'position_lines_xs')
425+
trade_source.add(trades[['EntryPrice', 'ExitPrice']].values.tolist(), 'position_lines_ys')
418426
fig_ohlc.multi_line(xs='position_lines_xs', ys='position_lines_ys',
419427
source=trade_source, line_color=trades_cmap,
420-
legend_label='Trades ({})'.format(len(trade_data)),
428+
legend_label='Trades ({})'.format(len(trades)),
421429
line_width=8, line_alpha=1, line_dash='dotted')
422430

423431
def _plot_indicators():

0 commit comments

Comments
 (0)