@@ -1344,7 +1344,7 @@ def _compute_stats(self, broker: _Broker, strategy: Strategy) -> pd.Series:
1344
1344
1345
1345
equity = pd .Series (broker ._equity ).bfill ().fillna (broker ._cash ).values
1346
1346
dd = 1 - equity / np .maximum .accumulate (equity )
1347
- dd_dur , dd_peaks = self ._compute_drawdown_duration_peaks (pd .Series (dd , index = data . index ))
1347
+ dd_dur , dd_peaks = self ._compute_drawdown_duration_peaks (pd .Series (dd , index = index ))
1348
1348
1349
1349
equity_df = pd .DataFrame ({
1350
1350
'Equity' : equity ,
@@ -1391,25 +1391,51 @@ def _round_timedelta(value, _period=_data_period(index)):
1391
1391
s .loc ['Return [%]' ] = (equity [- 1 ] - equity [0 ]) / equity [0 ] * 100
1392
1392
c = data .Close .values
1393
1393
s .loc ['Buy & Hold Return [%]' ] = (c [- 1 ] - c [0 ]) / c [0 ] * 100 # long-only return
1394
- s .loc ['Max. Drawdown [%]' ] = max_dd = - np .nan_to_num (dd .max ()) * 100
1394
+
1395
+ def geometric_mean (x ):
1396
+ return np .exp (np .log (1 + x ).sum () / (len (x ) or np .nan )) - 1
1397
+
1398
+ day_returns = gmean_day_return = annual_trading_days = np .array (np .nan )
1399
+ if index .is_all_dates :
1400
+ day_returns = equity_df ['Equity' ].resample ('D' ).last ().dropna ().pct_change ()
1401
+ gmean_day_return = geometric_mean (day_returns )
1402
+ annual_trading_days = (
1403
+ 365 if index .dayofweek .to_series ().between (5 , 6 ).mean () > 2 / 7 * .6 else
1404
+ 252 )
1405
+
1406
+ # Annualized return and risk metrics are computed based on the (mostly correct)
1407
+ # assumption that the returns are compounded. See: https://dx.doi.org/10.2139/ssrn.3054517
1408
+ # Our annualized return matches `empyrical.annual_return(day_returns)` whereas
1409
+ # our risk doesn't; they use the simpler approach below.
1410
+ annualized_return = (1 + gmean_day_return )** annual_trading_days - 1
1411
+ s .loc ['Return (Ann.) [%]' ] = annualized_return * 100
1412
+ s .loc ['Volatility (Ann.) [%]' ] = np .sqrt ((day_returns .var (ddof = 1 ) + (1 + gmean_day_return )** 2 )** annual_trading_days - (1 + gmean_day_return )** (2 * annual_trading_days )) * 100 # noqa: E501
1413
+ # s.loc['Return (Ann.) [%]'] = gmean_day_return * annual_trading_days * 100
1414
+ # s.loc['Risk (Ann.) [%]'] = day_returns.std(ddof=1) * np.sqrt(annual_trading_days) * 100
1415
+
1416
+ # Our Sharpe mismatches `empyrical.sharpe_ratio()` because they use arithmetic mean return
1417
+ # and simple standard deviation
1418
+ s .loc ['Sharpe Ratio' ] = np .clip (s .loc ['Return (Ann.) [%]' ] / (s .loc ['Volatility (Ann.) [%]' ] or np .nan ), 0 , np .inf ) # noqa: E501
1419
+ # Our Sortino mismatches `empyrical.sortino_ratio()` because they use arithmetic mean return
1420
+ s .loc ['Sortino Ratio' ] = np .clip (annualized_return / (np .sqrt (np .mean (day_returns .clip (- np .inf , 0 )** 2 )) * np .sqrt (annual_trading_days )), 0 , np .inf ) # noqa: E501
1421
+ max_dd = - np .nan_to_num (dd .max ())
1422
+ s .loc ['Calmar Ratio' ] = np .clip (annualized_return / (- max_dd or np .nan ), 0 , np .inf )
1423
+ s .loc ['Max. Drawdown [%]' ] = max_dd * 100
1395
1424
s .loc ['Avg. Drawdown [%]' ] = - dd_peaks .mean () * 100
1396
1425
s .loc ['Max. Drawdown Duration' ] = _round_timedelta (dd_dur .max ())
1397
1426
s .loc ['Avg. Drawdown Duration' ] = _round_timedelta (dd_dur .mean ())
1398
1427
s .loc ['# Trades' ] = n_trades = len (trades )
1399
1428
s .loc ['Win Rate [%]' ] = win_rate = np .nan if not n_trades else (pl > 0 ).sum () / n_trades * 100 # noqa: E501
1400
1429
s .loc ['Best Trade [%]' ] = returns .max () * 100
1401
1430
s .loc ['Worst Trade [%]' ] = returns .min () * 100
1402
- mean_return = np . exp ( np . log ( 1 + returns ). sum () / ( len ( returns ) or np . nan )) - 1
1431
+ mean_return = geometric_mean ( returns )
1403
1432
s .loc ['Avg. Trade [%]' ] = mean_return * 100
1404
1433
s .loc ['Max. Trade Duration' ] = _round_timedelta (durations .max ())
1405
1434
s .loc ['Avg. Trade Duration' ] = _round_timedelta (durations .mean ())
1406
1435
s .loc ['Profit Factor' ] = returns [returns > 0 ].sum () / (abs (returns [returns < 0 ].sum ()) or np .nan ) # noqa: E501
1407
1436
s .loc ['Expectancy [%]' ] = ((returns [returns > 0 ].mean () * win_rate -
1408
1437
returns [returns < 0 ].mean () * (100 - win_rate )))
1409
1438
s .loc ['SQN' ] = np .sqrt (n_trades ) * pl .mean () / (pl .std () or np .nan )
1410
- s .loc ['Sharpe Ratio' ] = mean_return / (returns .std () or np .nan )
1411
- s .loc ['Sortino Ratio' ] = mean_return / (returns [returns < 0 ].std () or np .nan )
1412
- s .loc ['Calmar Ratio' ] = mean_return / ((- max_dd / 100 ) or np .nan )
1413
1439
1414
1440
s .loc ['_strategy' ] = strategy
1415
1441
s .loc ['_equity_curve' ] = equity_df
0 commit comments