""" 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