quant_test / factor_engine /single_factor_backtest.py
lucky-loster's picture
Upload folder using huggingface_hub
590a501 verified
Raw
History Blame Contribute Delete
5.64 kB
"""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