7 Sprints 全部完成: Sprint 0: 基础设施 (DataManager + MariaDB) Sprint 1: 因子引擎 (34因子/12分类) Sprint 2: VectorBT 回测 (5策略+截面) Sprint 3: Optuna 优化 (+Walk-Forward) Sprint 4: ML 模型 (LightGBM+CatBoost) Sprint 5: Qwen 情绪因子 (三源新闻+日期对齐) Sprint 6: Agent 系统 (4Agent+日报.md/.html) 生产加固 (15项): Tushare双源fallback, SSH自动恢复, pool_pre_ping, save_daily先删后插, load_dotenv绝对路径, 日报5d/20d修复, RiskAgent改上证指数, 昨日对比+数据截止, mac_report utf8mb4, CLAUDE-*.md 9条已知Bug, demo全参数化, djapi数据源归一化, indexDatas API修正 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
153 lines
5.6 KiB
Python
153 lines
5.6 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] # 生成日报
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
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
|
|
from factors.registry import get_factor
|
|
|
|
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 main():
|
|
if len(sys.argv) < 2:
|
|
print("用法: agent_cli.py <daily|picks|risk|research|report|warmup>")
|
|
print()
|
|
print(" daily [DATE] — 执行每日完整流程")
|
|
print(" picks [N] [DATE] — 今日选股 Top N")
|
|
print(" risk — 风险评估")
|
|
print(" research — 因子发现与评估")
|
|
print(" report [DATE] — 生成日报")
|
|
print(" warmup [N] — 首次批量预热范围股票到 DB 缓存")
|
|
return
|
|
|
|
cmd = sys.argv[1]
|
|
engines = init_engines()
|
|
|
|
from agents.orchestrator import AgentOrchestrator
|
|
orch = AgentOrchestrator(**engines)
|
|
orch.setup()
|
|
|
|
if cmd == "daily":
|
|
date = sys.argv[2] if len(sys.argv) > 2 else None
|
|
results = orch.run_daily(date=date)
|
|
# 打印日报内容
|
|
report_md = results.get("report", {}).get("report_markdown", "")
|
|
if report_md:
|
|
print(report_md)
|
|
|
|
elif cmd == "picks":
|
|
n = int(sys.argv[2]) if len(sys.argv) > 2 else 15
|
|
date = sys.argv[3] if len(sys.argv) > 3 else None
|
|
result = orch.picks(date=date, top_n=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}")
|
|
|
|
elif 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}")
|
|
|
|
elif cmd == "research":
|
|
result = orch.run_research_cycle()
|
|
top = result.get("research", {}).get("top_factors", [])
|
|
print(f"\n因子评估结果:")
|
|
if not top:
|
|
print(" (无结果)")
|
|
return
|
|
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}")
|
|
|
|
elif cmd == "warmup":
|
|
batch_n = int(sys.argv[2]) if len(sys.argv) > 2 else 50
|
|
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)))
|
|
|
|
elif cmd == "report":
|
|
date = sys.argv[2] if len(sys.argv) > 2 else None
|
|
result = orch.generate_report(date=date)
|
|
print("\n日报已生成: {}".format(result.get("report_path", "?")))
|
|
# 存入 DB
|
|
md = result.get("report_markdown", "")
|
|
if md:
|
|
from reports.storage import save_report
|
|
save_report(md, "量化日报", report_date=date or datetime.now().strftime("%Y%m%d"),
|
|
subject_type="daily", subject_code="")
|
|
print(" 已存入 DB")
|
|
if md:
|
|
print(md)
|
|
|
|
else:
|
|
print(f"未知命令: {cmd}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|