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>
212 lines
7.5 KiB
Python
212 lines
7.5 KiB
Python
"""
|
|
AgentOrchestrator — Agent 编排器。
|
|
|
|
管理所有 Agent 的生命周期、执行顺序、结果传递。
|
|
"""
|
|
|
|
from datetime import datetime
|
|
|
|
from agents.research_agent import ResearchAgent
|
|
from agents.selection_agent import SelectionAgent
|
|
from agents.risk_agent import RiskAgent
|
|
from agents.report_agent import ReportAgent
|
|
|
|
|
|
class AgentOrchestrator:
|
|
"""
|
|
Agent 编排器。
|
|
|
|
用法:
|
|
orch = AgentOrchestrator(dm=dm, fe=engine_fe, bt=engine_bt, ...)
|
|
orch.setup() # 注册所有 Agent
|
|
orch.run_daily() # 执行每日流程
|
|
"""
|
|
|
|
def __init__(self, **engines):
|
|
self.engines = engines
|
|
self.agents: dict = {}
|
|
self._last_results: dict = {}
|
|
|
|
def setup(self):
|
|
"""注册所有 Agent。"""
|
|
self.agents["research"] = ResearchAgent(**self.engines)
|
|
self.agents["selection"] = SelectionAgent(**self.engines)
|
|
self.agents["risk"] = RiskAgent(**self.engines)
|
|
self.agents["report"] = ReportAgent(**self.engines)
|
|
print(f"[Orchestrator] 已注册 {len(self.agents)} 个 Agent: {list(self.agents)}")
|
|
|
|
# ── 每日流程 ──────────────────────────────────────────
|
|
|
|
def run_daily(self, date: str | None = None) -> dict:
|
|
"""
|
|
每日任务流:
|
|
|
|
1. 同步数据
|
|
2. 风险评估
|
|
3. 股票打分
|
|
4. 生成日报
|
|
"""
|
|
date = date or datetime.now().strftime("%Y%m%d")
|
|
print(f"\n{'='*60}")
|
|
print(f"[Orchestrator] 每日流程 — {date}")
|
|
print(f"{'='*60}")
|
|
|
|
results = {"date": date}
|
|
|
|
# Step 1: 增量同步已缓存股票的最新行情
|
|
print("\n[Step 1/4] 同步行情...")
|
|
dm = self.engines.get("dm")
|
|
sent = self.engines.get("sent")
|
|
from database.dao import get_latest_trade_date
|
|
|
|
scope_stocks = []
|
|
if sent:
|
|
scope_stocks = sent.get_scope_stocks()
|
|
if not scope_stocks and dm:
|
|
scope_stocks = list(dm.get_stock_list().index[:100])
|
|
print(" 范围: {} 只股票".format(len(scope_stocks)))
|
|
|
|
# 分类:已缓存(增量更新),未缓存(统计跳过)
|
|
cached = [c for c in scope_stocks if get_latest_trade_date(c)]
|
|
uncached = len(scope_stocks) - len(cached)
|
|
print(" 已缓存: {} 只 (增量更新), 未缓存: {} 只 (跳过,需首次批量预热)".format(len(cached), uncached))
|
|
|
|
synced = 0
|
|
for i, ts_code in enumerate(cached):
|
|
try:
|
|
n = dm.sync_daily(ts_code)
|
|
synced += n
|
|
except Exception:
|
|
continue
|
|
if (i + 1) % 100 == 0:
|
|
print(" [同步] 进度: {}/{}".format(i + 1, len(cached)))
|
|
print(" 同步完成: {} 条新数据 (已缓存{}/全量{})".format(synced, len(cached), len(scope_stocks)))
|
|
|
|
if uncached > 0:
|
|
print(" 提示: {} 只股票未缓存,运行 'agent_cli.py warmup' 首次批量预热".format(uncached))
|
|
|
|
# Step 2: 风险评估
|
|
print("\n[Step 2/4] 风险评估...")
|
|
risk = self.agents["risk"].execute()
|
|
results["risk"] = risk
|
|
print(f" 风险: {risk['risk_level']}, 仓位: {risk['target_exposure']:.0%}")
|
|
|
|
# Step 3: 选股打分
|
|
print("\n[Step 3/4] 股票打分...")
|
|
selection = self.agents["selection"].execute(date=date, top_n=15)
|
|
results["selection"] = selection
|
|
top = selection.get("top_picks", [])
|
|
if top:
|
|
print(" Top 5: {}".format(", ".join(p['ts_code'] for p in top[:5])))
|
|
|
|
# Step 4: 情绪因子
|
|
print("\n[Step 4/5] 情绪因子...")
|
|
sentiment_df = None
|
|
sent_eng = self.engines.get("sent")
|
|
if sent_eng:
|
|
try:
|
|
# 用范围内第一只有缓存的股票计算情绪因子
|
|
ref_code = cached[0] if cached else "000001.SZ"
|
|
sentiment_df = sent_eng.compute(ref_code, max_news=30)
|
|
if sentiment_df is not None and not sentiment_df.empty:
|
|
valid = sentiment_df.dropna(how="all")
|
|
print(" {}: {} 个因子, {} 个有效交易日".format(ref_code, sentiment_df.shape[1], len(valid)))
|
|
else:
|
|
print(" (无有效情绪数据)")
|
|
except Exception as e:
|
|
print(" [SKIP] 情绪因子计算失败: {}".format(e))
|
|
else:
|
|
print(" (SentimentEngine 未配置)")
|
|
results["sentiment"] = sentiment_df
|
|
|
|
# Step 5: 生成日报
|
|
print("\n[Step 5/5] 生成日报...")
|
|
report = self.agents["report"].execute(
|
|
date=date,
|
|
selection_result=selection,
|
|
risk_result=risk,
|
|
sentiment_result=sentiment_df,
|
|
)
|
|
results["report"] = report
|
|
print(f" 日报: {report['report_path']}")
|
|
|
|
self._last_results = results
|
|
print(f"\n{'='*60}")
|
|
print(f"[Orchestrator] 每日流程完成")
|
|
print(f"{'='*60}\n")
|
|
return results
|
|
|
|
# ── 研究流程(每周一次) ────────────────────────────────
|
|
|
|
def run_research_cycle(self, ts_codes: list[str] | None = None) -> dict:
|
|
"""
|
|
研究周期:
|
|
|
|
1. 因子发现与评估
|
|
2. 更新 IC 权重
|
|
"""
|
|
print(f"\n{'='*60}")
|
|
print(f"[Orchestrator] 研究周期")
|
|
print(f"{'='*60}")
|
|
|
|
print("\n[Step 1/2] 因子发现...")
|
|
research = self.agents["research"].execute(ts_codes=ts_codes)
|
|
results = {"research": research}
|
|
|
|
top = research.get("top_factors", [])
|
|
if top:
|
|
print(f" Top 5 因子:")
|
|
for f in top[:5]:
|
|
print(f" {f['name']:20s} IC={f['ic_mean']:+.4f} ICIR={f['icir']:.3f}")
|
|
|
|
return results
|
|
|
|
# ── 便捷方法 ──────────────────────────────────────────
|
|
|
|
def picks(self, date: str | None = None, top_n: int = 15) -> dict:
|
|
"""快速选股。"""
|
|
return self.agents["selection"].execute(date=date, top_n=top_n)
|
|
|
|
def risk_check(self) -> dict:
|
|
"""快速风险评估。"""
|
|
return self.agents["risk"].execute()
|
|
|
|
def generate_report(self, date: str | None = None) -> dict:
|
|
"""快速生成日报。"""
|
|
date = date or datetime.now().strftime("%Y%m%d")
|
|
|
|
# 尝试拉取最新数据(Tushare 优先,几秒即可完成)
|
|
dm = self.engines.get("dm")
|
|
if dm:
|
|
try:
|
|
dm.sync_daily("000001.SZ")
|
|
except Exception:
|
|
pass
|
|
|
|
# 查 DB 最新交易日
|
|
data_freshness = None
|
|
try:
|
|
from database.dao import get_latest_trade_date
|
|
data_freshness = get_latest_trade_date("000001.SZ")
|
|
except Exception:
|
|
pass
|
|
|
|
sel = self.picks(date)
|
|
risk = self.risk_check()
|
|
# 尝试取情绪因子
|
|
sentiment_df = None
|
|
sent_eng = self.engines.get("sent")
|
|
if sent_eng:
|
|
try:
|
|
sentiment_df = sent_eng.compute("000001.SZ", max_news=30)
|
|
except Exception:
|
|
pass
|
|
return self.agents["report"].execute(
|
|
date=date, selection_result=sel, risk_result=risk,
|
|
sentiment_result=sentiment_df, data_freshness=data_freshness,
|
|
)
|
|
|
|
@property
|
|
def last_results(self) -> dict:
|
|
return self._last_results
|