From a05394c311b02ff859902c74919263f8b71663cb Mon Sep 17 00:00:00 2001 From: jensnesten Date: Sun, 23 Mar 2025 10:22:17 +0100 Subject: [PATCH 1/2] Fix Issue 1251: covariance calc for insufficient data points --- backtesting/_stats.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/backtesting/_stats.py b/backtesting/_stats.py index 045bd7ca..4a68c18c 100644 --- a/backtesting/_stats.py +++ b/backtesting/_stats.py @@ -154,11 +154,15 @@ def _round_timedelta(value, _period=_data_period(index)): s.loc['Calmar Ratio'] = annualized_return / (-max_dd or np.nan) equity_log_returns = np.log(equity[1:] / equity[:-1]) market_log_returns = np.log(c[1:] / c[:-1]) - cov_matrix = np.cov(equity_log_returns, market_log_returns) - beta = cov_matrix[0, 1] / cov_matrix[1, 1] - # Jensen CAPM Alpha: can be strongly positive when beta is negative and B&H Return is large - s.loc['Alpha [%]'] = s.loc['Return [%]'] - risk_free_rate * 100 - beta * (s.loc['Buy & Hold Return [%]'] - risk_free_rate * 100) # noqa: E501 - s.loc['Beta'] = beta + if len(equity_log_returns) > 1 and len(market_log_returns) > 1: + cov_matrix = np.cov(equity_log_returns, market_log_returns) + beta = cov_matrix[0, 1] / cov_matrix[1, 1] + # Jensen CAPM Alpha: can be strongly positive when beta is negative and B&H Return is large + s.loc['Alpha [%]'] = s.loc['Return [%]'] - risk_free_rate * 100 - beta * (s.loc['Buy & Hold Return [%]'] - risk_free_rate * 100) # noqa: E501 + s.loc['Beta'] = beta + else: + s.loc['Alpha [%]'] = np.nan + s.loc['Beta'] = np.nan s.loc['Max. Drawdown [%]'] = max_dd * 100 s.loc['Avg. Drawdown [%]'] = -dd_peaks.mean() * 100 s.loc['Max. Drawdown Duration'] = _round_timedelta(dd_dur.max()) From 18af117a5d1d5ccad8befbc8c170d1dd0e73b8ad Mon Sep 17 00:00:00 2001 From: kernc Date: Wed, 26 Mar 2025 01:56:26 +0100 Subject: [PATCH 2/2] Minor update --- backtesting/_stats.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/backtesting/_stats.py b/backtesting/_stats.py index 4a68c18c..6ab44ef5 100644 --- a/backtesting/_stats.py +++ b/backtesting/_stats.py @@ -154,15 +154,14 @@ def _round_timedelta(value, _period=_data_period(index)): s.loc['Calmar Ratio'] = annualized_return / (-max_dd or np.nan) equity_log_returns = np.log(equity[1:] / equity[:-1]) market_log_returns = np.log(c[1:] / c[:-1]) + beta = np.nan if len(equity_log_returns) > 1 and len(market_log_returns) > 1: + # len == 0 on dummy call `stats_keys = compute_stats(...)` pre optimization cov_matrix = np.cov(equity_log_returns, market_log_returns) beta = cov_matrix[0, 1] / cov_matrix[1, 1] - # Jensen CAPM Alpha: can be strongly positive when beta is negative and B&H Return is large - s.loc['Alpha [%]'] = s.loc['Return [%]'] - risk_free_rate * 100 - beta * (s.loc['Buy & Hold Return [%]'] - risk_free_rate * 100) # noqa: E501 - s.loc['Beta'] = beta - else: - s.loc['Alpha [%]'] = np.nan - s.loc['Beta'] = np.nan + # Jensen CAPM Alpha: can be strongly positive when beta is negative and B&H Return is large + s.loc['Alpha [%]'] = s.loc['Return [%]'] - risk_free_rate * 100 - beta * (s.loc['Buy & Hold Return [%]'] - risk_free_rate * 100) # noqa: E501 + s.loc['Beta'] = beta s.loc['Max. Drawdown [%]'] = max_dd * 100 s.loc['Avg. Drawdown [%]'] = -dd_peaks.mean() * 100 s.loc['Max. Drawdown Duration'] = _round_timedelta(dd_dur.max())