Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added beta & alpha #1221

Merged
merged 14 commits into from
Mar 11, 2025
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -68,6 +68,8 @@ CAGR [%] 16.80
Sharpe Ratio 0.66
Sortino Ratio 1.30
Calmar Ratio 0.77
Alpha [%] 450.62
Beta 0.02
Comment on lines +71 to +72
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For given values of s.loc['Return [%]'] and s.loc['Buy & Hold Return [%]'], the previous computation of

s.loc['Alpha [%]'] = s.loc['Return [%]'] - s.loc['Buy & Hold Return [%]']  # == -144

seemed much more reasonable! Can you explain the discrepancy?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LLM suggests, among the less applicable options, that the values are not computed on the same time scale—beta is computed on daily returns and alpha on the whole duration ... 🤔

Copy link
Contributor Author

@jensnesten jensnesten Mar 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the 'simple' alpha we first had is absolute alpha, where we simply calculate the absolute difference of our strategy relative to the underlying benchmark. The new calc is risk-adjusted alpha, which is the official definition of alpha, but here we might end up in situations where alpha is positive, despite underperforming the benchmark in terms of return. Especially in beta neutral strategies where beta approaches 0, you'd have situations where the right-most term will go to 0 - essentially signaling very little risk:

α = ( R r ) β ( R b r )
when β 0 :
α = ( R r )

I personally like the simple alpha calc in this context, because its simple and intuitive. But the alpha we have now is the correct alpha definition.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I added a source code comment so that we remain aware of this counter-intuitive issue. Hope not too many users get confused by it, or that we eventually get to improve on it.

Max. Drawdown [%] -33.08
Avg. Drawdown [%] -5.58
Max. Drawdown Duration 688 days 00:00:00
7 changes: 7 additions & 0 deletions backtesting/_stats.py
Original file line number Diff line number Diff line change
@@ -152,6 +152,13 @@ def _round_timedelta(value, _period=_data_period(index)):
s.loc['Sortino Ratio'] = (annualized_return - risk_free_rate) / (np.sqrt(np.mean(day_returns.clip(-np.inf, 0)**2)) * np.sqrt(annual_trading_days)) # noqa: E501
max_dd = -np.nan_to_num(dd.max())
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
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())
2 changes: 2 additions & 0 deletions backtesting/backtesting.py
Original file line number Diff line number Diff line change
@@ -1262,6 +1262,8 @@ def run(self, **kwargs) -> pd.Series:
Sharpe Ratio 0.58038
Sortino Ratio 1.08479
Calmar Ratio 0.44144
Alpha [%] 394.37391
Beta 0.03803
Max. Drawdown [%] -47.98013
Avg. Drawdown [%] -5.92585
Max. Drawdown Duration 584 days 00:00:00
2 changes: 2 additions & 0 deletions backtesting/test/_test.py
Original file line number Diff line number Diff line change
@@ -317,6 +317,8 @@ def test_compute_stats(self):
'Start': pd.Timestamp('2004-08-19 00:00:00'),
'Win Rate [%]': 46.96969696969697,
'Worst Trade [%]': -18.39887353835481,
'Alpha [%]': 394.37391142027462,
'Beta': 0.03803390709192,
})

def almost_equal(a, b):