fix: IC chart cumulative mean + forward return horizon matching + multi-horizon eval
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+32
-13
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""End-to-end pipeline: HS300 universe -> momentum signal -> cross-sectional IC
|
||||
"""End-to-end pipeline: HS300 universe -> reversal signal -> cross-sectional IC
|
||||
-> multi-stock backtest (AlphaStrategy + RankEqualWeightBuilder) -> reports."""
|
||||
import logging
|
||||
|
||||
@@ -12,14 +12,24 @@ from data.downloader import download_batch
|
||||
from data.universe import SYMBOLS
|
||||
from eval.metrics import evaluate_cross_sectional
|
||||
from portfolio.builder import RankEqualWeightBuilder
|
||||
from signals.momentum import MomentumSignal
|
||||
from signals.reversal import ReversalSignal
|
||||
from strategies.alpha_strategy import AlphaStrategy
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main():
|
||||
def _forward_returns(data: dict[str, pd.DataFrame], horizon: int) -> pd.DataFrame:
|
||||
"""Build a date-indexed DataFrame of ``horizon``-day forward returns per stock."""
|
||||
forward_returns: dict[str, pd.Series] = {}
|
||||
for sym, df in data.items():
|
||||
fwd = df["close"].pct_change(horizon).shift(-horizon)
|
||||
fwd.index = pd.to_datetime(df["date"])
|
||||
forward_returns[sym] = fwd
|
||||
return pd.DataFrame(forward_returns)
|
||||
|
||||
|
||||
def main(forward_horizon: int = 5):
|
||||
symbols = SYMBOLS[:30]
|
||||
start, end = "2023-01-01", "2024-12-31"
|
||||
initial_cash = 1_000_000
|
||||
@@ -29,24 +39,25 @@ def main():
|
||||
data = {s: df for s, df in data.items() if df is not None and not df.empty}
|
||||
logger.info(f"Downloaded {len(data)}/{len(symbols)} symbols")
|
||||
|
||||
# 3. Compute the momentum signal per stock.
|
||||
signal = MomentumSignal(lookback=5)
|
||||
# 3. Compute the reversal signal per stock.
|
||||
signal = ReversalSignal(lookback=5)
|
||||
signal_series: dict[str, pd.Series] = {}
|
||||
forward_returns: dict[str, pd.Series] = {}
|
||||
for sym, df in data.items():
|
||||
idx = pd.to_datetime(df["date"])
|
||||
sig = signal.compute(df)
|
||||
sig.index = idx
|
||||
sig.index = pd.to_datetime(df["date"])
|
||||
signal_series[sym] = sig
|
||||
fwd = df["close"].pct_change().shift(-1) # next-day return
|
||||
fwd.index = idx
|
||||
forward_returns[sym] = fwd
|
||||
|
||||
# 4. Cross-sectional IC evaluation.
|
||||
# 4. Cross-sectional IC at the matching forward horizon.
|
||||
signals_df = pd.DataFrame(signal_series)
|
||||
returns_df = pd.DataFrame(forward_returns)
|
||||
returns_df = _forward_returns(data, forward_horizon)
|
||||
signal_eval = evaluate_cross_sectional(signals_df, returns_df)
|
||||
|
||||
# 4b. Multi-horizon IC to show which horizon the signal works at.
|
||||
horizon_evals = {
|
||||
h: evaluate_cross_sectional(signals_df, _forward_returns(data, h))
|
||||
for h in (1, 5, 20)
|
||||
}
|
||||
|
||||
# 5. Attach the signal column to each DataFrame and build feeds.
|
||||
config = BacktestConfig(
|
||||
symbols=list(data.keys()),
|
||||
@@ -79,6 +90,14 @@ def main():
|
||||
f"{signal_eval['rank_ic_std']:.4f} / {signal_eval['rank_ir']:.4f}")
|
||||
print(f"Hit rate: {signal_eval['hit_rate']:.2%}")
|
||||
print(f"Periods: {signal_eval['n_periods']}")
|
||||
|
||||
print("\nMULTI-HORIZON IC")
|
||||
print("=" * 50)
|
||||
print(f"{'Horizon':>8} {'Rank IC':>9} {'Rank IR':>9} {'Hit rate':>9} {'Periods':>8}")
|
||||
for h, ev in horizon_evals.items():
|
||||
print(f"{f'{h}d':>8} {ev['rank_ic_mean']:>9.4f} {ev['rank_ir']:>9.4f} "
|
||||
f"{ev['hit_rate']:>8.2%} {ev['n_periods']:>8}")
|
||||
|
||||
print(f"\nReports written to: {artifacts}")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user