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