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>
This commit is contained in:
2026-06-07 15:59:05 +08:00
co-authored by Claude Opus 4.7
commit 271a9343a5
293 changed files with 59598 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
"""
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),
}