7 Sprints 全部完成: Sprint 0: 基础设施 (DataManager + MariaDB) Sprint 1: 因子引擎 (34因子/12分类) Sprint 2: VectorBT 回测 (5策略+截面) Sprint 3: Optuna 优化 (+Walk-Forward) Sprint 4: ML 模型 (LightGBM+CatBoost) Sprint 5: Qwen 情绪因子 (三源新闻+日期对齐) Sprint 6: Agent 系统 (4Agent+日报.md/.html) 生产加固 (15项): Tushare双源fallback, SSH自动恢复, pool_pre_ping, save_daily先删后插, load_dotenv绝对路径, 日报5d/20d修复, RiskAgent改上证指数, 昨日对比+数据截止, mac_report utf8mb4, CLAUDE-*.md 9条已知Bug, demo全参数化, djapi数据源归一化, indexDatas API修正 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
146 lines
4.7 KiB
Python
146 lines
4.7 KiB
Python
"""
|
||
ResearchAgent — 因子发现与评估。
|
||
|
||
遍历注册因子,计算 IC/IC_IR/分层收益,输出 Top 因子。
|
||
"""
|
||
|
||
import numpy as np
|
||
import pandas as pd
|
||
|
||
from agents.base import BaseAgent
|
||
|
||
|
||
class ResearchAgent(BaseAgent):
|
||
"""因子发现 Agent。"""
|
||
|
||
name = "Research"
|
||
description = "因子发现与评估"
|
||
|
||
def execute(
|
||
self,
|
||
factor_names: list[str] | None = None,
|
||
ts_codes: list[str] | None = None,
|
||
lookahead: int = 5,
|
||
top_n: int = 10,
|
||
) -> dict:
|
||
"""
|
||
遍历因子,计算评估指标。
|
||
|
||
参数:
|
||
factor_names: 待评估因子列表,None=全部注册因子
|
||
ts_codes: 股票列表,None=DataManager 全部
|
||
lookahead: 前向收益窗口
|
||
top_n: 返回 Top N 因子
|
||
|
||
返回:
|
||
{"top_factors": [...], "all_results": DataFrame, "evaluated": int}
|
||
"""
|
||
from factors.registry import list_factors, get_factor
|
||
|
||
if factor_names is None:
|
||
# 只评估技术+基本面因子(情绪因子需要额外数据)
|
||
categories = ["动量", "RSI", "MACD", "量价", "布林", "ATR", "均线", "波动率", "换手率", "振幅", "基本面"]
|
||
factor_names = []
|
||
for cat in categories:
|
||
factor_names.extend(list_factors(cat))
|
||
|
||
if ts_codes is None:
|
||
stocks = self.dm.get_stock_list()
|
||
# 默认选 50 只代表股(前50只 + 避免API过载)
|
||
ts_codes = list(stocks.index[:50]) if self.dm else ["000001.SZ", "600519.SH"]
|
||
|
||
self.log(f"评估 {len(factor_names)} 个因子 × {len(ts_codes)} 只股票")
|
||
|
||
results = []
|
||
for fn in factor_names:
|
||
metrics = self._evaluate_factor(fn, ts_codes, lookahead)
|
||
if metrics:
|
||
results.append(metrics)
|
||
self.log(f" {fn}: IC={metrics.get('ic_mean', 0):.4f}" if metrics else f" {fn}: SKIP")
|
||
|
||
if not results:
|
||
return {"top_factors": [], "all_results": pd.DataFrame(), "evaluated": 0}
|
||
|
||
df = pd.DataFrame(results).sort_values("score", ascending=False)
|
||
top = df.head(top_n)
|
||
|
||
return {
|
||
"top_factors": top.to_dict("records"),
|
||
"all_results": df,
|
||
"evaluated": len(results),
|
||
}
|
||
|
||
def _evaluate_factor(
|
||
self, factor_name: str, ts_codes: list[str], lookahead: int
|
||
) -> dict | None:
|
||
"""对单个因子计算 IC/IC_IR。"""
|
||
from factors.registry import get_factor
|
||
from models.features import FeatureEngine
|
||
|
||
try:
|
||
factor = get_factor(factor_name)
|
||
except KeyError:
|
||
return None
|
||
|
||
fe = FeatureEngine(lookahead=lookahead, label_type="regression")
|
||
|
||
ics = []
|
||
long_rets = []
|
||
short_rets = []
|
||
success = 0
|
||
|
||
for ts_code in ts_codes:
|
||
try:
|
||
daily = self.dm.get_daily(ts_code)
|
||
if daily is None or daily.empty:
|
||
continue
|
||
daily = daily.set_index("trade_date").sort_index()
|
||
|
||
factor_df = self.fe.compute(ts_code, [factor])
|
||
if factor_df is None or factor_df.empty:
|
||
continue
|
||
|
||
X, y = fe.build(factor_df, daily, fit=True)
|
||
if X.empty or y.empty or factor_name not in X.columns:
|
||
continue
|
||
|
||
fv = X[factor_name].dropna()
|
||
yv = y.loc[fv.index]
|
||
if len(fv) < 30:
|
||
continue
|
||
|
||
ic = fv.corr(yv, method="spearman")
|
||
ics.append(ic)
|
||
|
||
# 分层收益
|
||
top_idx = fv.nlargest(int(len(fv) * 0.2)).index
|
||
bot_idx = fv.nsmallest(int(len(fv) * 0.2)).index
|
||
long_rets.append(yv.loc[top_idx.intersection(yv.index)].mean())
|
||
short_rets.append(yv.loc[bot_idx.intersection(yv.index)].mean())
|
||
|
||
success += 1
|
||
except Exception:
|
||
continue
|
||
|
||
if success < 3:
|
||
return None
|
||
|
||
ic_series = pd.Series(ics)
|
||
ic_mean = ic_series.mean()
|
||
ic_std = ic_series.std()
|
||
icir = ic_mean / ic_std if ic_std > 0 else 0
|
||
|
||
# 综合得分 = IC × IC_IR 加权
|
||
score = abs(ic_mean) * max(icir, 0)
|
||
|
||
return {
|
||
"name": factor_name,
|
||
"ic_mean": round(float(ic_mean), 4),
|
||
"ic_std": round(float(ic_std), 4),
|
||
"icir": round(float(icir), 4),
|
||
"long_ret": round(float(np.mean(long_rets)), 2) if long_rets else 0,
|
||
"short_ret": round(float(np.mean(short_rets)), 2) if short_rets else 0,
|
||
"stocks_evaluated": success,
|
||
"score": round(float(score), 4),
|
||
}
|