Skip to content

Commit d4aeaee

Browse files
committed
Modified Tearsheet to handle strategies that produce no closed positions (i.e. buy and hold).
1 parent d79ffb8 commit d4aeaee

File tree

1 file changed

+62
-34
lines changed

1 file changed

+62
-34
lines changed

qstrader/statistics/tearsheet.py

Lines changed: 62 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@
1818

1919
class TearsheetStatistics(AbstractStatistics):
2020
"""
21+
Displays a Matplotlib-generated 'one-pager' as often
22+
found in institutional strategy performance reports.
23+
24+
Includes an equity curve, drawdown curve, monthly
25+
returns heatmap, yearly returns summary, strategy-
26+
level statistics and trade-level statistics.
27+
28+
Also includes an optional annualised rolling Sharpe
29+
ratio chart.
2130
"""
2231
def __init__(
2332
self, config, portfolio_handler,
@@ -88,7 +97,10 @@ def get_results(self):
8897
statistics["returns"] = returns_s
8998
statistics["rolling_sharpe"] = rolling_sharpe_s
9099
statistics["cum_returns"] = cum_returns_s
91-
statistics["positions"] = self._get_positions()
100+
101+
positions = self._get_positions()
102+
if positions is not None:
103+
statistics["positions"] = positions
92104

93105
# Benchmark statistics if benchmark ticker specified
94106
if self.benchmark is not None:
@@ -123,28 +135,28 @@ def x(p):
123135
a = []
124136
for p in pos:
125137
a.append(p.__dict__)
126-
127-
df = pd.DataFrame(a)
128-
129-
df['avg_bot'] = df['avg_bot'].apply(x)
130-
df['avg_price'] = df['avg_price'].apply(x)
131-
df['avg_sld'] = df['avg_sld'].apply(x)
132-
df['cost_basis'] = df['cost_basis'].apply(x)
133-
df['init_commission'] = df['init_commission'].apply(x)
134-
df['init_price'] = df['init_price'].apply(x)
135-
df['market_value'] = df['market_value'].apply(x)
136-
df['net'] = df['net'].apply(x)
137-
df['net_incl_comm'] = df['net_incl_comm'].apply(x)
138-
df['net_total'] = df['net_total'].apply(x)
139-
df['realised_pnl'] = df['realised_pnl'].apply(x)
140-
df['total_bot'] = df['total_bot'].apply(x)
141-
df['total_commission'] = df['total_commission'].apply(x)
142-
df['total_sld'] = df['total_sld'].apply(x)
143-
df['unrealised_pnl'] = df['unrealised_pnl'].apply(x)
144-
145-
df['trade_pct'] = (df['avg_sld'] / df['avg_bot'] - 1.0)
146-
147-
return df
138+
if len(a) == 0:
139+
# There are no closed positions
140+
return None
141+
else:
142+
df = pd.DataFrame(a)
143+
df['avg_bot'] = df['avg_bot'].apply(x)
144+
df['avg_price'] = df['avg_price'].apply(x)
145+
df['avg_sld'] = df['avg_sld'].apply(x)
146+
df['cost_basis'] = df['cost_basis'].apply(x)
147+
df['init_commission'] = df['init_commission'].apply(x)
148+
df['init_price'] = df['init_price'].apply(x)
149+
df['market_value'] = df['market_value'].apply(x)
150+
df['net'] = df['net'].apply(x)
151+
df['net_incl_comm'] = df['net_incl_comm'].apply(x)
152+
df['net_total'] = df['net_total'].apply(x)
153+
df['realised_pnl'] = df['realised_pnl'].apply(x)
154+
df['total_bot'] = df['total_bot'].apply(x)
155+
df['total_commission'] = df['total_commission'].apply(x)
156+
df['total_sld'] = df['total_sld'].apply(x)
157+
df['unrealised_pnl'] = df['unrealised_pnl'].apply(x)
158+
df['trade_pct'] = (df['avg_sld'] / df['avg_bot'] - 1.0)
159+
return df
148160

149161
def _plot_equity(self, stats, ax=None, **kwargs):
150162
"""
@@ -323,7 +335,14 @@ def format_perc(x, pos):
323335

324336
returns = stats["returns"]
325337
cum_returns = stats['cum_returns']
326-
positions = stats['positions']
338+
339+
if not 'positions' in stats:
340+
trd_yr = 0
341+
else:
342+
positions = stats['positions']
343+
trd_yr = positions.shape[0] / (
344+
(returns.index[-1] - returns.index[0]).days / 365.0
345+
)
327346

328347
if ax is None:
329348
ax = plt.gca()
@@ -337,7 +356,6 @@ def format_perc(x, pos):
337356
sortino = perf.create_sortino_ratio(returns, self.periods)
338357
rsq = perf.rsquared(range(cum_returns.shape[0]), cum_returns)
339358
dd, dd_max, dd_dur = perf.create_drawdowns(cum_returns)
340-
trd_yr = positions.shape[0] / ((returns.index[-1] - returns.index[0]).days / 365.0)
341359

342360
ax.text(0.25, 8.9, 'Total Return', fontsize=8)
343361
ax.text(7.50, 8.9, '{:.0%}'.format(tot_ret), fontweight='bold', horizontalalignment='right', fontsize=8)
@@ -411,19 +429,29 @@ def format_perc(x, pos):
411429
if ax is None:
412430
ax = plt.gca()
413431

414-
pos = stats['positions']
432+
if not 'positions' in stats:
433+
num_trades = 0
434+
win_pct = "N/A"
435+
win_pct_str = "N/A"
436+
avg_trd_pct = "N/A"
437+
avg_win_pct = "N/A"
438+
avg_loss_pct = "N/A"
439+
max_win_pct = "N/A"
440+
max_loss_pct = "N/A"
441+
else:
442+
pos = stats['positions']
443+
num_trades = pos.shape[0]
444+
win_pct = pos[pos["trade_pct"] > 0].shape[0] / float(num_trades)
445+
win_pct_str = '{:.0%}'.format(win_pct)
446+
avg_trd_pct = '{:.2%}'.format(np.mean(pos["trade_pct"]))
447+
avg_win_pct = '{:.2%}'.format(np.mean(pos[pos["trade_pct"] > 0]["trade_pct"]))
448+
avg_loss_pct = '{:.2%}'.format(np.mean(pos[pos["trade_pct"] <= 0]["trade_pct"]))
449+
max_win_pct = '{:.2%}'.format(np.max(pos["trade_pct"]))
450+
max_loss_pct = '{:.2%}'.format(np.min(pos["trade_pct"]))
415451

416452
y_axis_formatter = FuncFormatter(format_perc)
417453
ax.yaxis.set_major_formatter(FuncFormatter(y_axis_formatter))
418454

419-
num_trades = pos.shape[0]
420-
win_pct = pos[pos["trade_pct"] > 0].shape[0] / float(num_trades)
421-
win_pct_str = '{:.0%}'.format(win_pct)
422-
avg_trd_pct = '{:.2%}'.format(np.mean(pos["trade_pct"]))
423-
avg_win_pct = '{:.2%}'.format(np.mean(pos[pos["trade_pct"] > 0]["trade_pct"]))
424-
avg_loss_pct = '{:.2%}'.format(np.mean(pos[pos["trade_pct"] <= 0]["trade_pct"]))
425-
max_win_pct = '{:.2%}'.format(np.max(pos["trade_pct"]))
426-
max_loss_pct = '{:.2%}'.format(np.min(pos["trade_pct"]))
427455
# TODO: Position class needs entry date
428456
max_loss_dt = 'TBD' # pos[pos["trade_pct"] == np.min(pos["trade_pct"])].entry_date.values[0]
429457
avg_dit = '0.0' # = '{:.2f}'.format(np.mean(pos.time_in_pos))

0 commit comments

Comments
 (0)