feat: add residual momentum to signal-edge backtest
Adds a research-only 12-1 residual momentum signal to the cross-sectional signal-evaluation harness. The signal estimates benchmark beta over the 12-1 formation window and ranks cumulative stock return minus beta-adjusted benchmark return; it only appears when benchmark closes are available. No production qualification behavior changes. The Backtest signal table labels the new row as 12-1 residual momentum. Tests cover benchmark-gated emission and beta removal while keeping stock-specific drift. Verification: 453 backend tests pass, ruff check app/ clean, frontend npm run build clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -62,6 +62,44 @@ def _bar(high: float, low: float, close: float, open_: float | None = None) -> S
|
||||
)
|
||||
|
||||
|
||||
def _signal_test_series(extra_return: float = 0.0) -> tuple[list[date], list[float], list[float], dict[date, float]]:
|
||||
base = date(2024, 1, 1)
|
||||
dates = [base + timedelta(days=i) for i in range(280)]
|
||||
benchmark = [100.0]
|
||||
closes = [100.0]
|
||||
for i in range(1, len(dates)):
|
||||
market_ret = 0.0004 + 0.002 * math.sin(i / 9.0)
|
||||
benchmark.append(benchmark[-1] * (1.0 + market_ret))
|
||||
# Same market beta for both test stocks; only ``extra_return`` is
|
||||
# idiosyncratic drift, which residual momentum should keep.
|
||||
stock_ret = 1.4 * market_ret + extra_return
|
||||
closes.append(closes[-1] * (1.0 + stock_ret))
|
||||
highs = [c * 1.01 for c in closes]
|
||||
benchmark_closes = dict(zip(dates, benchmark))
|
||||
return dates, closes, highs, benchmark_closes
|
||||
|
||||
|
||||
def test_signal_values_emit_residual_momentum_only_with_benchmark():
|
||||
dates, closes, highs, benchmark = _signal_test_series(extra_return=0.0008)
|
||||
no_benchmark = bt._signal_values(dates, closes, highs, 260)
|
||||
with_benchmark = bt._signal_values(dates, closes, highs, 260, benchmark)
|
||||
|
||||
assert "mom_12_1" in no_benchmark
|
||||
assert "mom_12_1_resid" not in no_benchmark
|
||||
assert "mom_12_1_resid" in with_benchmark
|
||||
|
||||
|
||||
def test_residual_momentum_removes_market_beta_but_keeps_specific_drift():
|
||||
dates, pure_beta, highs, benchmark = _signal_test_series(extra_return=0.0)
|
||||
_, drift_stock, drift_highs, _ = _signal_test_series(extra_return=0.0008)
|
||||
|
||||
pure = bt._signal_values(dates, pure_beta, highs, 260, benchmark)
|
||||
drift = bt._signal_values(dates, drift_stock, drift_highs, 260, benchmark)
|
||||
|
||||
assert pure["mom_12_1_resid"] == pytest.approx(0.0, abs=0.03)
|
||||
assert drift["mom_12_1_resid"] > pure["mom_12_1_resid"] + 0.12
|
||||
|
||||
|
||||
class TestStopFillR:
|
||||
def test_intraday_fill_at_stop(self):
|
||||
assert bt._stop_fill_r("long", 100.0, 95.0, _bar(101, 94, 96)) == pytest.approx(-1.0)
|
||||
|
||||
Reference in New Issue
Block a user