Files
myquant/finance/agents/orchestrator.py
T
Simon 73d191b43a feat: 量化引擎加固 — 新增测试 + 数据/因子/回测层优化
- 新增 finance/tests/ 6 个测试套件(agents/backtest/dao_upsert/factors/features/fundamental_lookahead)
- 数据层: data_manager / dao 优化,新增 upsert 逻辑
- 因子层: 基本面因子抽象定位 _mapping、ROE/PE/PB 重构
- 回测层: vectorbt/engine 大改动(251 行),report 增强
- ML 层: features/backtest_integration 特征工程与回测优化
- CLI: agent_cli 重构
- config/settings 扩充配置项
2026-08-31 14:01:06 +08:00

257 lines
9.7 KiB
Python

"""
AgentOrchestrator — Agent 编排器。
管理所有 Agent 的生命周期、执行顺序、结果传递。
"""
import logging
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
logger = logging.getLogger(__name__)
TOTAL_STEPS = 5
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 _log_step(self, current: int, title: str) -> None:
"""统一步骤日志:分母固定为 TOTAL_STEPS。"""
print("\n[Step {}/{}] {}...".format(current, TOTAL_STEPS, title))
def _run_step(self, step_no, key, title, fn, results, on_ok=None):
"""执行单个步骤并隔离异常。
key: 结果字典的键(英文,向前兼容如 risk/selection/report),
title: 步骤展示名(中文,仅用于日志)。
任一子步骤抛错都会被捕获:记录 results[key]={"error": ...},
让失败不中断整体流程;on_ok 用于成功后的副作用(打印等)。
"""
self._log_step(step_no, title)
try:
value = fn()
results[key] = value
if on_ok:
on_ok(value)
return value
except Exception as e:
logger.exception("[Orchestrator] 步骤 %s 失败: %s", title, e)
results[key] = {"error": str(e)}
print(" [SKIP] 步骤 {} 失败,继续后续步骤: {}".format(title, e))
return None
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)
logger.info("[Orchestrator] 已注册 %s 个 Agent: %s",
len(self.agents), list(self.agents))
# ── 每日流程 ──────────────────────────────────────────
def run_daily(self, date: str | None = None) -> dict:
"""
每日任务流(5 步,任一步失败不中断整体):
1. 同步数据(增量同步已缓存股票)
2. 风险评估
3. 股票打分
4. 情绪因子
5. 生成日报
"""
date = date or datetime.now().strftime("%Y%m%d")
print(f"\n{'='*60}")
logger.info("[Orchestrator] 每日流程 — %s", date)
print(f"{'='*60}")
results: dict = {"date": date}
# Step 1: 增量同步已缓存股票的最新行情
dm = self.engines.get("dm")
sent = self.engines.get("sent")
from database.dao import get_latest_trade_date
def _step_sync():
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 as e:
logger.debug("同步 %s 失败: %s", ts_code, e)
continue
if (i + 1) % 100 == 0:
logger.info("[同步] 进度: %s/%s", i + 1, len(cached))
if uncached > 0:
print(" 提示: {} 只股票未缓存,运行 'agent_cli.py warmup' 首次批量预热".format(uncached))
return {"synced": synced, "cached": len(cached), "uncached": uncached,
"universe_size": len(scope_stocks), "scope_stocks": scope_stocks}
sync_info = self._run_step(1, "sync", "同步行情", _step_sync, results, on_ok=lambda r: print(
" 同步完成: {} 条新数据 (已缓存{}/全量{})".format(
r["synced"], r["cached"], r["universe_size"])))
# Step 2: 风险评估
def _risk():
risk = self.agents["risk"].execute()
risk_level = risk.get("risk_level", "?")
exposure = risk.get("target_exposure", 0)
print(" 风险: {}, 仓位: {:.0%}".format(risk_level, exposure))
return risk
risk = self._run_step(2, "risk", "风险评估", _risk, results)
# Step 3: 选股打分
def _selection():
selection = self.agents["selection"].execute(date=date, top_n=15)
top = selection.get("top_picks", [])
if top:
print(" Top 5: {}".format(", ".join(p['ts_code'] for p in top[:5])))
return selection
selection = self._run_step(3, "selection", "股票打分", _selection, results)
# Step 4: 情绪因子
def _sentiment():
sentiment_df = None
sent_eng = self.engines.get("sent")
if not sent_eng:
print(" (SentimentEngine 未配置)")
return None
# 用范围内第一只有缓存的股票计算情绪因子;无缓存信息时回退沪市指数代码
scope = (sync_info or {}).get("scope_stocks") or []
ref_code = scope[0] if scope 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(" (无有效情绪数据)")
return sentiment_df
sentiment_df = self._run_step(4, "sentiment", "情绪因子", _sentiment, results)
# Step 5: 生成日报
def _report():
rep = self.agents["report"].execute(
date=date,
selection_result=selection,
risk_result=risk,
sentiment_result=sentiment_df,
)
print(" 日报: {}".format(rep.get("report_path", "?")))
return rep
report = self._run_step(5, "report", "生成日报", _report, results)
self._last_results = results
print(f"\n{'='*60}")
print("[Orchestrator] 每日流程完成")
failed = [k for k, v in results.items() if isinstance(v, dict) and "error" in v]
if failed:
print("[Orchestrator] 有步骤未完成: {}".format(", ".join(failed)))
else:
print("[Orchestrator] 所有步骤均成功")
print(f"{'='*60}\n")
return results
# ── 研究流程(每周一次) ────────────────────────────────
def run_research_cycle(self, ts_codes: list[str] | None = None) -> dict:
"""
研究周期(1 步):
1. 因子发现与评估(IC / IC_IR / 分层收益)
"""
print(f"\n{'='*60}")
logger.info("[Orchestrator] 研究周期")
print(f"{'='*60}")
print("\n[Step 1/1] 因子发现...")
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