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:
@@ -1,9 +1,9 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from strategies.base import Strategy
|
||||
from strategies.base import StockStrategy, mask_scores
|
||||
|
||||
|
||||
class MultiFactorStrategy(Strategy):
|
||||
class MultiFactorStrategy(StockStrategy):
|
||||
"""
|
||||
Multi-factor strategy combining momentum and value signals with a market-timing filter.
|
||||
|
||||
@@ -33,7 +33,7 @@ class MultiFactorStrategy(Strategy):
|
||||
self.value_period = value_period
|
||||
self.top_n = top_n
|
||||
|
||||
def generate_signals(self, data: pd.DataFrame) -> pd.DataFrame:
|
||||
def generate_signals(self, data: pd.DataFrame, eligible: pd.DataFrame | None = None) -> pd.DataFrame:
|
||||
stock = data[self.tickers]
|
||||
|
||||
# --- Market timing filter ---
|
||||
@@ -42,7 +42,7 @@ class MultiFactorStrategy(Strategy):
|
||||
|
||||
# --- Momentum factor ---
|
||||
# Return from T-(momentum_period+skip) to T-skip, avoiding the last month
|
||||
momentum = stock.shift(self.momentum_skip).pct_change(self.momentum_period)
|
||||
momentum = stock.shift(self.momentum_skip).pct_change(self.momentum_period, fill_method=None)
|
||||
|
||||
# --- Value factor ---
|
||||
# min_price_over_period / current_price (higher = more "undervalued" vs recent range)
|
||||
@@ -51,16 +51,17 @@ class MultiFactorStrategy(Strategy):
|
||||
# --- Cross-sectional ranking (each row ranked across assets) ---
|
||||
# na_option="keep" so NaN stocks (pre-IPO / delisted / masked) stay
|
||||
# NaN in the composite score instead of being assigned pct=1.0.
|
||||
mom_rank = momentum.rank(axis=1, pct=True, na_option="keep")
|
||||
val_rank = value.rank(axis=1, pct=True, na_option="keep")
|
||||
mom_rank = mask_scores(momentum, eligible).rank(axis=1, pct=True, na_option="keep")
|
||||
val_rank = mask_scores(value, eligible).rank(axis=1, pct=True, na_option="keep")
|
||||
scores = mom_rank + val_rank # combined score, higher = better
|
||||
|
||||
# --- Select top_n assets per row ---
|
||||
# Only allocate rows that have enough valid scores
|
||||
scores = mask_scores(scores, eligible)
|
||||
n_valid = scores.notna().sum(axis=1)
|
||||
enough_data = n_valid >= self.top_n
|
||||
|
||||
score_rank = scores.rank(axis=1, ascending=False, na_option="bottom")
|
||||
score_rank = mask_scores(scores, eligible).rank(axis=1, ascending=False, na_option="bottom")
|
||||
top_mask = (score_rank <= self.top_n) & enough_data.values.reshape(-1, 1)
|
||||
|
||||
# Equal-weight allocation among selected assets
|
||||
|
||||
Reference in New Issue
Block a user