Files
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

196 lines
6.5 KiB
Python
Raw Permalink 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.
"""
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