#!/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())