@@ -1344,7 +1344,7 @@ def _compute_stats(self, broker: _Broker, strategy: Strategy) -> pd.Series:
13441344
13451345 equity = pd .Series (broker ._equity ).bfill ().fillna (broker ._cash ).values
13461346 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 ))
13481348
13491349 equity_df = pd .DataFrame ({
13501350 'Equity' : equity ,
@@ -1391,25 +1391,51 @@ def _round_timedelta(value, _period=_data_period(index)):
13911391 s .loc ['Return [%]' ] = (equity [- 1 ] - equity [0 ]) / equity [0 ] * 100
13921392 c = data .Close .values
13931393 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
13951424 s .loc ['Avg. Drawdown [%]' ] = - dd_peaks .mean () * 100
13961425 s .loc ['Max. Drawdown Duration' ] = _round_timedelta (dd_dur .max ())
13971426 s .loc ['Avg. Drawdown Duration' ] = _round_timedelta (dd_dur .mean ())
13981427 s .loc ['# Trades' ] = n_trades = len (trades )
13991428 s .loc ['Win Rate [%]' ] = win_rate = np .nan if not n_trades else (pl > 0 ).sum () / n_trades * 100 # noqa: E501
14001429 s .loc ['Best Trade [%]' ] = returns .max () * 100
14011430 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 )
14031432 s .loc ['Avg. Trade [%]' ] = mean_return * 100
14041433 s .loc ['Max. Trade Duration' ] = _round_timedelta (durations .max ())
14051434 s .loc ['Avg. Trade Duration' ] = _round_timedelta (durations .mean ())
14061435 s .loc ['Profit Factor' ] = returns [returns > 0 ].sum () / (abs (returns [returns < 0 ].sum ()) or np .nan ) # noqa: E501
14071436 s .loc ['Expectancy [%]' ] = ((returns [returns > 0 ].mean () * win_rate -
14081437 returns [returns < 0 ].mean () * (100 - win_rate )))
14091438 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 )
14131439
14141440 s .loc ['_strategy' ] = strategy
14151441 s .loc ['_equity_curve' ] = equity_df
0 commit comments