Files
myquant/finance/cli/agent_cli.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

174 lines
6.5 KiB
Python

#!/usr/bin/env python
"""
Agent 命令行入口。
用法:
python cli/agent_cli.py daily # 执行每日流程
python cli/agent_cli.py picks [N] # 今日选股 Top N
python cli/agent_cli.py risk # 风险评估
python cli/agent_cli.py research # 因子研究
python cli/agent_cli.py report [DATE] # 生成日报
python cli/agent_cli.py warmup [N] # 首次批量预热
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import argparse
import logging
from datetime import datetime
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
def init_engines():
"""初始化所有引擎。"""
from data.data_manager import DataManager
from factors.engine import FactorEngine
from backtest.vectorbt.engine import VectorBTEngine
from optimizer.engine import OptunaEngine
from factors.sentiment.sentiment_engine import SentimentEngine
from factors.sentiment.news_source import NewsSource
from factors.sentiment.qwen_client import QwenClient
dm = DataManager()
dm.init_db()
fe = FactorEngine(dm)
bt = VectorBTEngine()
opt = OptunaEngine(bt)
sent = SentimentEngine(dm, qwen_client=QwenClient(), news_source=NewsSource())
fe._sentiment_engine = sent
return {
"dm": dm,
"fe": fe,
"bt": bt,
"opt": opt,
"sent": sent,
}
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(prog="agent_cli", description="cc-cursor 量化 Agent CLI")
sub = p.add_subparsers(dest="cmd", required=True)
d = sub.add_parser("daily", help="执行每日完整流程")
d.add_argument("date", nargs="?", default=None, help="日期 YYYYMMDD(默认今天)")
pk = sub.add_parser("picks", help="今日选股 Top N")
pk.add_argument("top_n", nargs="?", type=int, default=15, help="返回 Top N")
pk.add_argument("date", nargs="?", default=None, help="日期 YYYYMMDD")
sub.add_parser("risk", help="风险评估")
sub.add_parser("research", help="因子发现与评估")
rp = sub.add_parser("report", help="生成日报")
rp.add_argument("date", nargs="?", default=None, help="日期 YYYYMMDD")
wm = sub.add_parser("warmup", help="首次批量预热范围股票到 DB 缓存")
wm.add_argument("batch_n", nargs="?", type=int, default=50, help="每批股票数")
return p
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
engines = init_engines()
from agents.orchestrator import AgentOrchestrator
orch = AgentOrchestrator(**engines)
orch.setup()
today = datetime.now().strftime("%Y%m%d")
if args.cmd == "daily":
results = orch.run_daily(date=args.date)
report_md = results.get("report", {}).get("report_markdown", "")
if report_md:
print(report_md)
# 任一步骤失败 → 非零退出码,便于调度/CI 感知
failed = [k for k, v in results.items()
if isinstance(v, dict) and "error" in v]
if failed:
print("每日流程有步骤未完成: {}".format(", ".join(failed)), file=sys.stderr)
return 1
return 0
elif args.cmd == "picks":
result = orch.picks(date=args.date, top_n=args.top_n)
print(f"\n选股结果 ({result.get('date', '?')}):")
for p in result.get("top_picks", []):
print(f" {p['ts_code']:12s} {p.get('name', ''):10s} {p['score']:.4f}")
return 0
elif args.cmd == "risk":
result = orch.risk_check()
print(f"\n风险评估:")
print(f" 等级: {result['risk_level']}")
print(f" 建议仓位: {result['target_exposure']:.0%}")
print(f" 止损线: {result['stop_loss']:.0%}")
print(f" 单票上限: {result['max_single_position']:.0%}")
indicators = result.get("indicators", {})
if indicators:
print(f" 波动率: {indicators.get('market_volatility', 0):.1f}%")
print(f" 回撤: {indicators.get('current_drawdown', 0):.1f}%")
for a in result.get("alerts", []):
print(f" ⚠️ {a}")
return 0
elif args.cmd == "research":
result = orch.run_research_cycle()
top = result.get("research", {}).get("top_factors", [])
print(f"\n因子评估结果:")
if not top:
print(" (无结果)")
return 0
print(f" {'因子':20s} {'IC':>8s} {'IC_IR':>8s} {'多头':>8s} {'空头':>8s} {'得分':>8s}")
print(f" {'─'*60}")
for f in top:
print(f" {f['name']:20s} {f['ic_mean']:>+8.4f} {f['icir']:>8.3f} "
f"{f['long_ret']:>+7.1f}% {f['short_ret']:>+7.1f}% {f['score']:>8.4f}")
return 0
elif args.cmd == "warmup":
batch_n = args.batch_n
print("首次批量预热: 每次 {} 只股票,分批执行...".format(batch_n))
sent = engines.get("sent")
dm = engines.get("dm")
scope = sent.get_scope_stocks() if sent else list(dm.get_stock_list().index[:100])
from database.dao import get_latest_trade_date
uncached = [c for c in scope if not get_latest_trade_date(c)]
print("范围: {} 只, 未缓存: {} 只".format(len(scope), len(uncached)))
total_synced = 0
for i in range(0, len(uncached), batch_n):
batch = uncached[i:i + batch_n]
print("[warmup] 批次 {}/{} ({}~{})".format(i // batch_n + 1, (len(uncached) - 1) // batch_n + 1, i, i + len(batch)))
for ts_code in batch:
try:
n = dm.sync_daily(ts_code)
total_synced += n
except Exception as e:
print(" {} 失败: {}".format(ts_code, e))
print(" 累计同步: {} 条".format(total_synced))
print("预热完成: {} 条数据, {} 只新股票已缓存".format(total_synced, len(uncached)))
return 0
elif args.cmd == "report":
date = args.date or today
result = orch.generate_report(date=date)
print("\n日报已生成: {}".format(result.get("report_path", "?")))
md = result.get("report_markdown", "")
# 注意: ReportAgent.execute 内部已保存到 DB,这里不再重复入库
if md:
print(md)
return 0
return 0 # 防御:未匹配(应不会到达)
if __name__ == "__main__":
sys.exit(main())