Skip to content

Commit 7117123

Browse files
committed
Update Sharpe, Sortino, and Calmar to use per-bar returns.
1 parent d3e9c64 commit 7117123

File tree

2 files changed

+30
-28
lines changed

2 files changed

+30
-28
lines changed

src/pybroker/eval.py

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -178,44 +178,44 @@ def log_profit_factor(changes: NDArray[np.float64]) -> np.floating:
178178

179179
@njit
180180
def sharpe_ratio(
181-
changes: NDArray[np.float64],
181+
returns: NDArray[np.float64],
182182
obs: Optional[int] = None,
183183
downside_only: bool = False,
184184
) -> np.floating:
185185
"""Computes the
186186
`Sharpe Ratio <https://en.wikipedia.org/wiki/Sharpe_ratio>`_.
187187
188188
Args:
189-
changes: Array of differences between each bar and the previous bar.
189+
returns: Array of returns centered at 0.
190190
obs: Number of observations used to annualize the Sharpe Ratio. For
191191
example, a value of ``252`` would be used to annualize daily
192192
returns.
193193
"""
194-
std_changes = changes[changes < 0] if downside_only else changes
194+
std_changes = returns[returns < 0] if downside_only else returns
195195
if not len(std_changes):
196196
return np.float64(0)
197197
std = np.std(std_changes)
198198
if std == 0:
199199
return np.float64(0)
200-
sr = np.mean(changes) / std
200+
sr = np.mean(returns) / std
201201
if obs is not None:
202202
sr *= np.sqrt(obs)
203203
return sr
204204

205205

206206
def sortino_ratio(
207-
changes: NDArray[np.float64], obs: Optional[int] = None
207+
returns: NDArray[np.float64], obs: Optional[int] = None
208208
) -> float:
209209
"""Computes the
210210
`Sortino Ratio <https://en.wikipedia.org/wiki/Sortino_ratio>`_.
211211
212212
Args:
213-
changes: Array of differences between each bar and the previous bar.
213+
returns: Array of returns centered at 0.
214214
obs: Number of observations used to annualize the Sortino Ratio. For
215215
example, a value of ``252`` would be used to annualize daily
216216
returns.
217217
"""
218-
return float(sharpe_ratio(changes, obs, downside_only=True))
218+
return float(sharpe_ratio(returns, obs, downside_only=True))
219219

220220

221221
def conf_profit_factor(
@@ -275,19 +275,19 @@ def max_drawdown(changes: NDArray[np.float64]) -> float:
275275
return -dd
276276

277277

278-
def calmar_ratio(changes: NDArray[np.float64], bars_per_year: int) -> float:
278+
def calmar_ratio(returns: NDArray[np.float64], bars_per_year: int) -> float:
279279
"""Computes the Calmar Ratio.
280280
281281
Args:
282-
changes: Array of differences between each bar and the previous bar.
282+
returns: Array of returns centered at 0.
283283
bars_per_year: Number of bars per annum.
284284
"""
285-
if not len(changes):
285+
if not len(returns):
286286
return 0
287-
max_dd = np.abs(max_drawdown(changes))
287+
max_dd = np.abs(max_drawdown(returns))
288288
if max_dd == 0:
289289
return 0
290-
return np.mean(changes) * bars_per_year / max_dd
290+
return np.mean(returns) * bars_per_year / max_dd
291291

292292

293293
@njit
@@ -923,16 +923,17 @@ def evaluate(
923923
samples=bootstrap_samples, sample_size=bootstrap_sample_size
924924
)
925925
confs_result = self._calc_conf_intervals(
926-
bar_changes,
927-
bootstrap_sample_size,
928-
bootstrap_samples,
929-
bars_per_year,
926+
changes=bar_changes,
927+
returns=bar_returns,
928+
sample_size=bootstrap_sample_size,
929+
samples=bootstrap_samples,
930+
bars_per_year=bars_per_year,
930931
)
931932
dd_result = self._calc_drawdown_conf(
932-
bar_changes,
933-
bar_returns,
934-
bootstrap_sample_size,
935-
bootstrap_samples,
933+
changes=bar_changes,
934+
returns=bar_returns,
935+
sample_size=bootstrap_sample_size,
936+
samples=bootstrap_samples,
936937
)
937938
bootstrap = BootstrapResult(
938939
conf_intervals=confs_result.df,
@@ -979,8 +980,8 @@ def _calc_eval_metrics(
979980
if max_dd_index
980981
else None
981982
)
982-
sharpe = sharpe_ratio(bar_changes, bars_per_year)
983-
sortino = sortino_ratio(bar_changes, bars_per_year)
983+
sharpe = sharpe_ratio(bar_returns, bars_per_year)
984+
sortino = sortino_ratio(bar_returns, bars_per_year)
984985
pf = profit_factor(bar_changes)
985986
r2 = r_squared(market_values)
986987
ui = ulcer_index(market_values)
@@ -1046,7 +1047,7 @@ def _calc_eval_metrics(
10461047
annual_volatility_pct = float(
10471048
np.std(bar_returns * 100) * np.sqrt(bars_per_year)
10481049
)
1049-
calmar = calmar_ratio(bar_changes, bars_per_year)
1050+
calmar = calmar_ratio(bar_returns, bars_per_year)
10501051
return EvalMetrics(
10511052
trade_count=len(pnls),
10521053
initial_market_value=market_values[0],
@@ -1097,14 +1098,15 @@ def _calc_eval_metrics(
10971098
def _calc_conf_intervals(
10981099
self,
10991100
changes: NDArray[np.float64],
1101+
returns: NDArray[np.float64],
11001102
sample_size: int,
11011103
samples: int,
11021104
bars_per_year: Optional[int],
11031105
) -> _ConfsResult:
11041106
pf_intervals = conf_profit_factor(changes, sample_size, samples)
11051107
pf_conf = self._to_conf_intervals("Profit Factor", pf_intervals)
11061108
sr_intervals = conf_sharpe_ratio(
1107-
changes, sample_size, samples, bars_per_year
1109+
returns, sample_size, samples, bars_per_year
11081110
)
11091111
sharpe_conf = self._to_conf_intervals("Sharpe Ratio", sr_intervals)
11101112
df = pd.DataFrame.from_records(

tests/test_eval.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -484,11 +484,11 @@ class TestEvaluateMixin:
484484
@pytest.mark.parametrize(
485485
"bars_per_year, expected_sharpe, expected_sortino",
486486
[
487-
(None, 0.01710828175162464, 0.01714937872464358),
487+
(None, 0.026013464180574847, 0.02727734785007549),
488488
(
489489
252,
490-
0.01710828175162464 * np.sqrt(252),
491-
0.01714937872464358 * np.sqrt(252),
490+
0.026013464180574847 * np.sqrt(252),
491+
0.02727734785007549 * np.sqrt(252),
492492
),
493493
],
494494
)
@@ -583,7 +583,7 @@ def test_evaluate(
583583
assert metrics.std_error == 69646.36129687089
584584
assert metrics.total_fees == 0
585585
if bars_per_year is not None:
586-
assert metrics.calmar == 0.6819937522980625
586+
assert metrics.calmar == 1.1557170701224246
587587
assert truncate(metrics.annual_return_pct, 6) == truncate(
588588
5.897743691129764, 6
589589
)

0 commit comments

Comments
 (0)