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
+195
View File
@@ -0,0 +1,195 @@
"""
SelectionAgent — 多因子股票打分。
综合技术因子 + 基本面因子 + ML 预测值 → 股票综合得分排序。
"""
import numpy as np
import pandas as pd
from agents.base import BaseAgent
class SelectionAgent(BaseAgent):
"""股票打分 Agent。"""
name = "Selection"
description = "多因子股票打分与推荐"
def execute(
self,
date: str | None = None,
ts_codes: list[str] | None = None,
top_n: int = 15,
weighting: str = "equal",
) -> dict:
"""
对股票池打分排序。
参数:
date: 目标日期,None=最新
ts_codes: 股票池,None=指数成分股
top_n: 返回 Top N
weighting: 'equal' | 'ic_weighted' | 'ml'
返回:
{"date": ..., "top_picks": [...], "score_df": DataFrame}
"""
from factors.registry import get_factor, list_factors
# 确定股票池
if ts_codes is None:
if self.sent:
ts_codes = self.sent.get_scope_stocks()
else:
stocks = self.dm.get_stock_list()
ts_codes = list(stocks.index[:100])
if not ts_codes:
return {"date": date or self._today(), "top_picks": [], "score_df": pd.DataFrame()}
# 选择核心因子(覆盖多个维度,减少计算量)
core_factors = [
"momentum_20", "momentum_60",
"rsi_14", "volatility_20",
"vol_ratio_5", "ma_dev_20",
"turnover_5", "amplitude_5",
]
factor_objects = [get_factor(n) for n in core_factors]
self.log("打分 {} 只股票 (权重={})".format(len(ts_codes), weighting))
# 1. 筛选有 DB 缓存的股票(Orchestrator 已在 Step1 同步了全量范围)
from database.dao import get_latest_trade_date
available = []
for ts_code in sorted(ts_codes):
if get_latest_trade_date(ts_code):
available.append(ts_code)
self.log("缓存命中: {}/{} ({:.1f}%)".format(
len(available), len(ts_codes),
len(available) / len(ts_codes) * 100 if ts_codes else 0))
# 2. 打分(限制上限防止单次太慢)
score_limit = min(len(available), 300)
scores = {}
valid_count = 0
for i, ts_code in enumerate(available[:score_limit]):
try:
score = self._score_stock(ts_code, factor_objects, date, weighting)
if score is not None:
scores[ts_code] = score
valid_count += 1
except Exception:
continue
if (i + 1) % 50 == 0:
self.log(" 进度: {}/{}".format(i + 1, score_limit))
self.log("有效评分: {}/{}".format(len(scores), score_limit))
if not scores:
return {"date": date or self._today(), "top_picks": [], "score_df": pd.DataFrame()}
# 排序
sorted_scores = sorted(scores.items(), key=lambda x: x[1], reverse=True)
top = sorted_scores[:top_n]
# 取股票名称(ts_code 是 index
stock_list = self.dm.get_stock_list() if self.dm else pd.DataFrame()
name_map = {}
if not stock_list.empty and "name" in stock_list.columns:
name_map = dict(zip(stock_list.index, stock_list["name"]))
top_picks = []
for ts_code, score in top:
# 尝试多种格式匹配名称
code_no_suffix = ts_code.replace(".SZ", "").replace(".SH", "").replace(".BJ", "")
name = name_map.get(ts_code, name_map.get(code_no_suffix, ts_code))
top_picks.append({
"ts_code": ts_code,
"name": name,
"score": round(score, 4),
})
score_df = pd.DataFrame(
{"ts_code": list(scores.keys()), "score": list(scores.values())}
).sort_values("score", ascending=False).reset_index(drop=True)
return {
"date": date or self._today(),
"top_picks": top_picks,
"score_df": score_df,
"universe_size": len(ts_codes),
"valid_scores": valid_count,
}
def _filter_cached_stocks(self, ts_codes: list[str], limit: int = 100) -> list[str]:
"""筛选有 DB 日线缓存的股票,避免逐个调用 AkShare。"""
from database.dao import get_latest_trade_date
cached = []
for ts_code in ts_codes[:limit]:
latest = get_latest_trade_date(ts_code)
if latest:
cached.append(ts_code)
return cached
def _score_stock(
self,
ts_code: str,
factors: list,
date: str | None,
weighting: str,
) -> float | None:
"""对单只股票打分。"""
daily = self.dm.get_daily(ts_code)
if daily is None or daily.empty:
return None
daily = daily.set_index("trade_date").sort_index()
factor_df = self.fe.compute(ts_code, factors)
if factor_df is None or factor_df.empty:
return None
if date and date in factor_df.index:
row = factor_df.loc[date]
else:
row = factor_df.iloc[-1] # 最新一天
if row.isna().all():
return None
if weighting == "ml" and self.ml_models:
return self._score_ml(ts_code, factor_df, date)
# 等权打分:标准化因子值后求和
row_clean = row.dropna()
if len(row_clean) < 3:
return None
# z-score 标准化
z = (row_clean - factor_df[row_clean.index].mean()) / factor_df[row_clean.index].std().replace(0, 1)
return float(z.mean())
def _score_ml(self, ts_code: str, factor_df: pd.DataFrame, date: str | None) -> float | None:
"""ML 模型打分。"""
from models.features import FeatureEngine
fe = FeatureEngine(lookahead=5)
daily = self.dm.get_daily(ts_code)
if daily is None or daily.empty:
return None
daily = daily.set_index("trade_date")
try:
X, _ = fe.build(factor_df, daily, fit=False)
if X.empty:
return None
if date and date in X.index:
X = X.loc[[date]]
else:
X = X.iloc[[-1]]
model = self.ml_models.get("lightgbm") or list(self.ml_models.values())[0]
pred = model.predict(X)
return float(pred.iloc[0]) if len(pred) > 0 else None
except Exception:
return None