Files
myquant/finance/agents/research_agent.py
T
simonandClaude Opus 4.7 271a9343a5 Initial commit: cc-cursor 全链路量化研究平台
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>
2026-06-07 15:59:05 +08:00

146 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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),
}