Skip to content

Commit 53fe7e0

Browse files
authored
Merge pull request mhallsmoore#163 from mhallsmoore/rollingsharpe
Added 'Rolling Sharpe Ratio' calculation and graph plot to the Tearsh…
2 parents 5daffa2 + 827bfe8 commit 53fe7e0

File tree

2 files changed

+71
-12
lines changed

2 files changed

+71
-12
lines changed

.travis.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ sudo: false
33
language: python
44

55
env:
6-
- PYTHON=2.7 PANDAS=0.17.1
7-
- PYTHON=3.4 PANDAS=0.17.1
8-
- PYTHON=3.5 PANDAS=0.17.1
6+
- PYTHON=2.7 PANDAS=0.18.0
7+
- PYTHON=3.4 PANDAS=0.18.0
8+
- PYTHON=3.5 PANDAS=0.18.0
99

1010
#matrix:
1111
# allow_failures:

qstrader/statistics/tearsheet.py

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ class TearsheetStatistics(AbstractStatistics):
2121
"""
2222
def __init__(
2323
self, config, portfolio_handler,
24-
title=None, benchmark=None, periods=252
24+
title=None, benchmark=None, periods=252,
25+
rolling_sharpe=False
2526
):
2627
"""
2728
Takes in a portfolio handler.
@@ -32,6 +33,7 @@ def __init__(
3233
self.title = '\n'.join(title)
3334
self.benchmark = benchmark
3435
self.periods = periods
36+
self.rolling_sharpe = rolling_sharpe
3537
self.equity = {}
3638
self.equity_benchmark = {}
3739
self.log_scale = False
@@ -59,6 +61,12 @@ def get_results(self):
5961
# Returns
6062
returns_s = equity_s.pct_change().fillna(0.0)
6163

64+
# Rolling Annualised Sharpe
65+
rolling = returns_s.rolling(window=self.periods)
66+
rolling_sharpe_s = np.sqrt(self.periods) * (
67+
rolling.mean() / rolling.std()
68+
)
69+
6270
# Cummulative Returns
6371
cum_returns_s = np.exp(np.log(1 + returns_s).cumsum())
6472

@@ -78,13 +86,18 @@ def get_results(self):
7886
statistics["max_drawdown_duration"] = dd_dur
7987
statistics["equity"] = equity_s
8088
statistics["returns"] = returns_s
89+
statistics["rolling_sharpe"] = rolling_sharpe_s
8190
statistics["cum_returns"] = cum_returns_s
8291
statistics["positions"] = self._get_positions()
8392

8493
# Benchmark statistics if benchmark ticker specified
8594
if self.benchmark is not None:
8695
equity_b = pd.Series(self.equity_benchmark).sort_index()
8796
returns_b = equity_b.pct_change().fillna(0.0)
97+
rolling_b = returns_b.rolling(window=self.periods)
98+
rolling_sharpe_b = np.sqrt(self.periods) * (
99+
rolling_b.mean() / rolling_b.std()
100+
)
88101
cum_returns_b = np.exp(np.log(1 + returns_b).cumsum())
89102
dd_b, max_dd_b, dd_dur_b = perf.create_drawdowns(cum_returns_b)
90103
statistics["sharpe_b"] = perf.create_sharpe_ratio(returns_b)
@@ -93,6 +106,7 @@ def get_results(self):
93106
statistics["max_drawdown_duration_b"] = dd_dur_b
94107
statistics["equity_b"] = equity_b
95108
statistics["returns_b"] = returns_b
109+
statistics["rolling_sharpe_b"] = rolling_sharpe_b
96110
statistics["cum_returns_b"] = cum_returns_b
97111

98112
return statistics
@@ -173,6 +187,44 @@ def format_two_dec(x, pos):
173187

174188
return ax
175189

190+
def _plot_rolling_sharpe(self, stats, ax=None, **kwargs):
191+
"""
192+
Plots the curve of rolling Sharpe ratio.
193+
"""
194+
def format_two_dec(x, pos):
195+
return '%.2f' % x
196+
197+
sharpe = stats['rolling_sharpe']
198+
199+
if ax is None:
200+
ax = plt.gca()
201+
202+
y_axis_formatter = FuncFormatter(format_two_dec)
203+
ax.yaxis.set_major_formatter(FuncFormatter(y_axis_formatter))
204+
ax.xaxis.set_tick_params(reset=True)
205+
ax.yaxis.grid(linestyle=':')
206+
ax.xaxis.set_major_locator(mdates.YearLocator(1))
207+
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
208+
ax.xaxis.grid(linestyle=':')
209+
210+
if self.benchmark is not None:
211+
benchmark = stats['rolling_sharpe_b']
212+
benchmark.plot(
213+
lw=2, color='gray', label=self.benchmark, alpha=0.60,
214+
ax=ax, **kwargs
215+
)
216+
217+
sharpe.plot(lw=2, color='green', alpha=0.6, x_compat=False,
218+
label='Backtest', ax=ax, **kwargs)
219+
220+
ax.axvline(sharpe.index[252], linestyle="dashed", c="gray", lw=2)
221+
ax.set_ylabel('Rolling Annualised Sharpe')
222+
ax.legend(loc='best')
223+
ax.set_xlabel('')
224+
plt.setp(ax.get_xticklabels(), visible=True, rotation=0, ha='center')
225+
226+
return ax
227+
176228
def _plot_drawdown(self, stats, ax=None, **kwargs):
177229
"""
178230
Plots the underwater curve
@@ -523,22 +575,29 @@ def plot_results(self, filename=None):
523575
sns.set_style("whitegrid")
524576
sns.set_palette("deep", desat=.6)
525577

526-
vertical_sections = 5
578+
if self.rolling_sharpe:
579+
offset_index = 1
580+
else:
581+
offset_index = 0
582+
vertical_sections = 5 + offset_index
527583
fig = plt.figure(figsize=(10, vertical_sections * 3.5))
528584
fig.suptitle(self.title, y=0.94, weight='bold')
529585
gs = gridspec.GridSpec(vertical_sections, 3, wspace=0.25, hspace=0.5)
530586

531587
stats = self.get_results()
532-
533588
ax_equity = plt.subplot(gs[:2, :])
534-
ax_drawdown = plt.subplot(gs[2, :])
535-
ax_monthly_returns = plt.subplot(gs[3, :2])
536-
ax_yearly_returns = plt.subplot(gs[3, 2])
537-
ax_txt_curve = plt.subplot(gs[4, 0])
538-
ax_txt_trade = plt.subplot(gs[4, 1])
539-
ax_txt_time = plt.subplot(gs[4, 2])
589+
if self.rolling_sharpe:
590+
ax_sharpe = plt.subplot(gs[2, :])
591+
ax_drawdown = plt.subplot(gs[2 + offset_index, :])
592+
ax_monthly_returns = plt.subplot(gs[3 + offset_index, :2])
593+
ax_yearly_returns = plt.subplot(gs[3 + offset_index, 2])
594+
ax_txt_curve = plt.subplot(gs[4 + offset_index, 0])
595+
ax_txt_trade = plt.subplot(gs[4 + offset_index, 1])
596+
ax_txt_time = plt.subplot(gs[4 + offset_index, 2])
540597

541598
self._plot_equity(stats, ax=ax_equity)
599+
if self.rolling_sharpe:
600+
self._plot_rolling_sharpe(stats, ax=ax_sharpe)
542601
self._plot_drawdown(stats, ax=ax_drawdown)
543602
self._plot_monthly_returns(stats, ax=ax_monthly_returns)
544603
self._plot_yearly_returns(stats, ax=ax_yearly_returns)

0 commit comments

Comments
 (0)