fix(stats): use each window's means for rolling_greeks alpha - #554
WatchTree-19 wants to merge 1 commit into
Conversation
rolling_greeks() computed beta over the rolling window but alpha as returns.mean() - beta * benchmark.mean() over the whole sample, so every window shared the same baseline and rolling alpha only moved when beta did. It now uses the window's own means, so each row matches the intercept of a regression on that window. Rolling beta is unchanged, and the plots only read beta.
, #556) avg_return(), avg_win() and avg_loss() masked the unselected cells to NaN and then called .dropna(), which drops whole rows on a DataFrame. Each column's average was computed only over rows where every other column also qualified, so a strategy's average win was measured only on the days the benchmark also rose. reports.metrics() with a benchmark therefore reported affected Average Win/Loss, Payoff Ratio, Win/Loss Ratio, CPC Index and Kelly Criterion for DataFrame input. .mean() already skips NaN column-wise (#556). rolling_greeks() computed alpha from the full-sample means instead of each window's own, so every row shared one baseline and rolling alpha only moved when rolling beta did. Rolling beta is unchanged (#554). _generate_cache_key() formatted a time-varying rf into the key with its repr, which pandas truncates to the first and last few rows: two rate series of the same length differing only in between shared an entry, and the second caller got the first one's excess returns back. The answer depended on call order (#555). Both rolling_greeks and the cache key were reported with fixes by @WatchTree-19; the contamination was found from the data @none2003 attached to #556. Bumps version to 0.0.86.
|
Confirmed and shipped in v0.0.86. Your diagnosis was exact. Reproduced your snippet before changing anything: The strategy demonstrably gains 10bp a day from day 252 and rolling alpha moves by one unit in the fifth decimal. Beta was correct throughout, which is what made this easy to miss: the rolling beta plots read only The point I'd underline for anyone reading later is the one in your title — every window shared a single baseline, so rolling alpha was structurally incapable of showing a change in skill. It could only move when beta moved. That is worse than a numerical error, because the series looked plausible and stable. After the fix both rows match I implemented it as your patch describes rather than merging the branch, because this went out alongside two other fixes and I wanted one coherent diff. Your Unlike #553 this one was not my regression — Closing as shipped. Thank you. |
Keeping 'em coming, like you asked on #553.
rolling_greeks()computes beta over the rolling window, but alpha uses the full-sample means:So every window shares one baseline, and rolling alpha only moves when rolling beta does. A strategy that picks up 10 bp a day halfway through shows the same alpha before and after:
The fix takes the window's own means, so each row is the intercept of a regression on that window. With it both rows above match the regression (0.00035 and 0.00198). Rolling beta is unchanged, and the rolling beta plots only read beta, so nothing visual moves.
Tests:
TestRollingGreeksAlphaintests/test_regressions.pychecks beta and alpha againstnp.polyfiton three windows. It fails on main and passes with the fix. Full suite: 214 passed (the onetest_normalize_tz_awareerror is environmental and happens on main too).ruff checkandruff format --checkare clean. I added a line under a 0.0.86 heading in the changelog.