- 新增 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 扩充配置项
257 lines
9.7 KiB
Python
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
|