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 扩充配置项
This commit is contained in:
+117
-72
@@ -4,6 +4,7 @@ AgentOrchestrator — Agent 编排器。
|
||||
管理所有 Agent 的生命周期、执行顺序、结果传递。
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from agents.research_agent import ResearchAgent
|
||||
@@ -11,6 +12,10 @@ 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:
|
||||
"""
|
||||
@@ -27,112 +32,153 @@ class AgentOrchestrator:
|
||||
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)
|
||||
print(f"[Orchestrator] 已注册 {len(self.agents)} 个 Agent: {list(self.agents)}")
|
||||
logger.info("[Orchestrator] 已注册 %s 个 Agent: %s",
|
||||
len(self.agents), list(self.agents))
|
||||
|
||||
# ── 每日流程 ──────────────────────────────────────────
|
||||
|
||||
def run_daily(self, date: str | None = None) -> dict:
|
||||
"""
|
||||
每日任务流:
|
||||
每日任务流(5 步,任一步失败不中断整体):
|
||||
|
||||
1. 同步数据
|
||||
1. 同步数据(增量同步已缓存股票)
|
||||
2. 风险评估
|
||||
3. 股票打分
|
||||
4. 生成日报
|
||||
4. 情绪因子
|
||||
5. 生成日报
|
||||
"""
|
||||
date = date or datetime.now().strftime("%Y%m%d")
|
||||
print(f"\n{'='*60}")
|
||||
print(f"[Orchestrator] 每日流程 — {date}")
|
||||
logger.info("[Orchestrator] 每日流程 — %s", date)
|
||||
print(f"{'='*60}")
|
||||
|
||||
results = {"date": date}
|
||||
results: dict = {"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)))
|
||||
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))
|
||||
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)))
|
||||
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}
|
||||
|
||||
if uncached > 0:
|
||||
print(" 提示: {} 只股票未缓存,运行 'agent_cli.py warmup' 首次批量预热".format(uncached))
|
||||
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: 风险评估
|
||||
print("\n[Step 2/4] 风险评估...")
|
||||
risk = self.agents["risk"].execute()
|
||||
results["risk"] = risk
|
||||
print(f" 风险: {risk['risk_level']}, 仓位: {risk['target_exposure']:.0%}")
|
||||
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: 选股打分
|
||||
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])))
|
||||
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: 情绪因子
|
||||
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
|
||||
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: 生成日报
|
||||
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']}")
|
||||
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(f"[Orchestrator] 每日流程完成")
|
||||
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
|
||||
|
||||
@@ -140,16 +186,15 @@ class AgentOrchestrator:
|
||||
|
||||
def run_research_cycle(self, ts_codes: list[str] | None = None) -> dict:
|
||||
"""
|
||||
研究周期:
|
||||
研究周期(1 步):
|
||||
|
||||
1. 因子发现与评估
|
||||
2. 更新 IC 权重
|
||||
1. 因子发现与评估(IC / IC_IR / 分层收益)
|
||||
"""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"[Orchestrator] 研究周期")
|
||||
logger.info("[Orchestrator] 研究周期")
|
||||
print(f"{'='*60}")
|
||||
|
||||
print("\n[Step 1/2] 因子发现...")
|
||||
print("\n[Step 1/1] 因子发现...")
|
||||
research = self.agents["research"].execute(ts_codes=ts_codes)
|
||||
results = {"research": research}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user