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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user