"""Single-factor IC analysis and strategy backtest.""" from __future__ import annotations import json from pathlib import Path from typing import Any import pandas as pd from config.settings import load_settings from factor_engine.factor_evaluation import daily_rank_ic, evaluate_factor_panel, ic_summary, quantile_spread from factor_engine.formula_registry import ( compute_factor, factor_series_to_panel, get_factor_spec, list_factor_specs, load_label_panel, registry_output_dir, ) from strategies.runner import run_strategy_backtest, save_backtest_result def build_factor_label_panel( factor_name: str, segment: str = "test", start_time: str | None = None, end_time: str | None = None, ) -> pd.DataFrame: settings = load_settings() if start_time is None or end_time is None: seg = settings.segments.get(segment, settings.segments["test"]) start_time = start_time or seg[0] end_time = end_time or seg[1] spec = get_factor_spec(factor_name) factor_s = compute_factor(factor_name, start_time=start_time, end_time=end_time, cache=True) factor_panel = factor_series_to_panel(factor_s, factor_name) label_panel = load_label_panel(start_time=start_time, end_time=end_time, label_expr=spec.label_expr) merged = factor_panel.merge(label_panel, on=["date", "symbol"], how="inner") merged = merged.rename(columns={factor_name: "factor"}) merged = merged.dropna(subset=["factor", "label"]) return merged def analyze_single_factor( factor_name: str, segment: str = "test", start_time: str | None = None, end_time: str | None = None, ) -> dict[str, Any]: panel = build_factor_label_panel(factor_name, segment=segment, start_time=start_time, end_time=end_time) metrics = evaluate_factor_panel(panel) ic_series = daily_rank_ic(panel) spread = quantile_spread(panel) spec = get_factor_spec(factor_name) return { "factor_name": factor_name, "expression": spec.expression, "description": spec.description, "segment": segment, "n_obs": len(panel), "metrics": metrics, "ic_series": ic_series, "quantile_spread": spread, } def backtest_single_factor( factor_name: str, strategy_name: str = "topk_dropout", strategy_kwargs: dict[str, Any] | None = None, segment: str = "test", start_time: str | None = None, end_time: str | None = None, output_dir: Path | None = None, ) -> dict[str, Any]: settings = load_settings() if start_time is None or end_time is None: seg = settings.segments.get(segment, settings.segments["test"]) start_time = start_time or seg[0] end_time = end_time or seg[1] analysis = analyze_single_factor(factor_name, segment=segment, start_time=start_time, end_time=end_time) signal_source = { "type": "factor_registry", "name": factor_name, "start_time": start_time, "end_time": end_time, } bt_result = run_strategy_backtest( strategy_name=strategy_name, signal_source=signal_source, strategy_kwargs=strategy_kwargs, start_time=start_time, end_time=end_time, ) out_dir = output_dir or settings.output_root / "factors" / "single_backtest" / factor_name save_backtest_result(bt_result, out_dir) ic_path = registry_output_dir() / "ic_summary.csv" ic_row = { "factor_name": factor_name, "expression": analysis["expression"], **analysis["metrics"], "strategy": strategy_name, "segment": segment, } if ic_path.exists(): summary_df = pd.read_csv(ic_path) summary_df = summary_df[summary_df["factor_name"] != factor_name] summary_df = pd.concat([summary_df, pd.DataFrame([ic_row])], ignore_index=True) else: summary_df = pd.DataFrame([ic_row]) summary_df.to_csv(ic_path, index=False) result = { "factor_name": factor_name, "analysis": {k: v for k, v in analysis.items() if k not in ("ic_series", "quantile_spread")}, "backtest": { "strategy": strategy_name, "signal_stats": bt_result.signal_stats, "risk": bt_result.risk.to_dict() if not bt_result.risk.empty else {}, "output_dir": str(out_dir), }, } with open(out_dir / "single_factor_report.json", "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False, indent=2, default=str) return result def run_all_single_factor_backtests( strategy_name: str = "topk_dropout", segment: str = "test", enabled_only: bool = True, ) -> pd.DataFrame: rows = [] for spec in list_factor_specs(enabled_only=enabled_only): print(f"\n=== Single factor backtest: {spec.name} ===") try: res = backtest_single_factor(spec.name, strategy_name=strategy_name, segment=segment) row = {"factor_name": spec.name, **res["analysis"]["metrics"]} if res["backtest"]["risk"]: row["ann_return"] = res["backtest"]["risk"].get("annualized_return") row["max_drawdown"] = res["backtest"]["risk"].get("max_drawdown") rows.append(row) print(f" ICIR={row.get('icir', 'NA'):.4f}") except Exception as exc: print(f" FAILED: {exc}") rows.append({"factor_name": spec.name, "error": str(exc)}) df = pd.DataFrame(rows) out = registry_output_dir() / "all_single_factor_summary.csv" df.to_csv(out, index=False) print(f"\nSummary saved: {out}") return df