fix: unify backtest execution and add forward validation candidates

Share point-in-time signals and a cash-funded order ledger across backtests and trading. Correct eligibility, missing-price fills, quality factors, regime inputs, warmup windows, and cash-injection accounting. Add reproducible registered-strategy comparisons and paired forward candidates with a 20% resize band.

Validation: 125 Python tests, 23 web tests, TypeScript/Vite build, 87 strategy causality checks, and real-data target/order/equity parity for 10 strategies.
This commit is contained in:
2026-10-01 02:24:01 -07:00
parent 761725d2c8
commit 722cade974
45 changed files with 1561 additions and 1021 deletions

View File

@@ -15,10 +15,10 @@ With:
import numpy as np
import pandas as pd
from strategies.base import Strategy
from strategies.base import StockStrategy, mask_scores
class CompositeAlphaStrategy(Strategy):
class CompositeAlphaStrategy(StockStrategy):
"""
Multi-factor alpha composite with regime gating.
"""
@@ -49,7 +49,9 @@ class CompositeAlphaStrategy(Strategy):
self.regime_gate = regime_gate
self.regime_ma = regime_ma
def generate_signals(self, data: pd.DataFrame) -> pd.DataFrame:
def generate_signals(self, data: pd.DataFrame, eligible: pd.DataFrame | None = None) -> pd.DataFrame:
if self.regime_gate and self.benchmark not in data.columns:
raise ValueError(f"Missing regime benchmark: {self.benchmark}")
# Separate benchmark from stocks if needed
if self.tickers is not None:
stock_data = data[[t for t in self.tickers if t in data.columns]]
@@ -63,17 +65,17 @@ class CompositeAlphaStrategy(Strategy):
).min() - 1
# --- Factor 2: Intermediate momentum (7-month, skip 1 month) ---
int_mom = stock_data.shift(self.skip).pct_change(self.intermediate_period - self.skip)
int_mom = stock_data.shift(self.skip).pct_change(self.intermediate_period - self.skip, fill_method=None)
# --- Factor 3: Quality composite ---
# Consistency: fraction of positive 21-day returns
monthly_ret = stock_data.pct_change(21)
monthly_ret = stock_data.pct_change(21, fill_method=None)
consistency = (monthly_ret > 0).astype(float).rolling(
self.quality_window, min_periods=self.quality_window // 2
).mean()
# Up-volume proxy: sum of positive daily returns over 20 days
daily_ret = stock_data.pct_change()
daily_ret = stock_data.pct_change(fill_method=None)
up_vol_proxy = daily_ret.where(daily_ret > 0, 0).rolling(20, min_periods=15).sum()
# --- Factor 4: Above MA200 (per-stock trend filter) ---
@@ -81,10 +83,10 @@ class CompositeAlphaStrategy(Strategy):
above_ma = (stock_data > ma200)
# --- Cross-sectional ranks ---
rec_rank = recovery.rank(axis=1, pct=True, na_option="keep")
mom_rank = int_mom.rank(axis=1, pct=True, na_option="keep")
con_rank = consistency.rank(axis=1, pct=True, na_option="keep")
upv_rank = up_vol_proxy.rank(axis=1, pct=True, na_option="keep")
rec_rank = mask_scores(recovery, eligible).rank(axis=1, pct=True, na_option="keep")
mom_rank = mask_scores(int_mom, eligible).rank(axis=1, pct=True, na_option="keep")
con_rank = mask_scores(consistency, eligible).rank(axis=1, pct=True, na_option="keep")
upv_rank = mask_scores(up_vol_proxy, eligible).rank(axis=1, pct=True, na_option="keep")
# Composite: recovery 30%, int_momentum 30%, consistency 20%, up_volume 20%
composite = (0.30 * rec_rank + 0.30 * mom_rank +
@@ -94,7 +96,8 @@ class CompositeAlphaStrategy(Strategy):
composite = composite.where(above_ma, np.nan)
# --- Select top_n ---
rank = composite.rank(axis=1, ascending=False, na_option="bottom")
rank = mask_scores(composite, eligible).rank(axis=1, ascending=False, na_option="bottom")
composite = mask_scores(composite, eligible)
n_valid = composite.notna().sum(axis=1)
enough = n_valid >= min(self.top_n, 5)
top_mask = (rank <= self.top_n) & enough.values.reshape(-1, 1)