Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ LLM multi-agent 美股量化交易系统,通过 Alpaca 执行交易(默认 p
```bash
pytest tests/ -v # 全量测试
python main.py --mode morning|midday|evening|live # 手动跑
python main.py --mode daily # P&L 历史 CSV → Telegram(无 LLM 无交易)
```

**Prompt/CoT 改动验证(决策重放 harness,2026-06-07 加)**:prompt 改动这里没法回测,所以容易"听起来对就上"。`scripts/replay_decision.py` 把 `agent_logs` 里某次真实调用的 `input_message` 喂回**当前** prompt+model,结构化 diff 新旧决策(PM 比 per-symbol target 权重/conviction 的增删改),把"我觉得这版更好"变成"它在 N 个真实输入上具体怎么改了决策"。`--no-llm` 只列待重放、不烧 token;要 LLM 时需 `.env` key。核心逻辑在 `src/replay.py`(load/replay/diff,有单测),靠 `base.py:BaseAgent._execute(user_message)` 这个 seam(= `run()` 去掉 build_user_message 的部分)。**这是"先验证再改"的工具——P1-P5/#1-#2 这类软层改动应该用它在历史决策点上看效果,而不是只靠论证**。outcome-aware 评分(对比次日/5日真实走势判好坏)是它之上的下一层,尚未建。
Expand All @@ -19,12 +20,12 @@ python main.py --mode morning|midday|evening|live # 手动跑

- 8 个日常 LLM agent:tech / news / macro / earnings / portfolio_manager / risk_manager / position_reviewer / evening_analyst。额外一个 **meta_reflector** 每季度末跑一次,负责对 6 个可编辑 agent(tech / news / macro / earnings / PM / evening)**自我画像 + prompt 审计 + 自动修订**。risk_manager 和 position_reviewer 被 **schema + prompt_editor 双层保护**,不允许被 auto-evolved 改动(硬纪律不能被稀释)
- 双层风控:硬规则引擎(cash_only / 仓位 / 暴露 / 日损 / 板块 / 相关性 / earnings-queued) + LLM RiskManager 审核 + `_force_delever()` 硬兜底
- **6 个 session**(ET Mon-Fri):earnings_preprocess 08:00-09:15(唯一跑 earnings LLM)、morning 09:30-12:00(full team)、intra_check 09:30-16:00 每 30min tick(熔断器,零 LLM)、midday 13:00-14:30(position_reviewer patient)、close 15:30-16:00(position_reviewer act-on-trigger;窗口 ≥ launchd 30min tick 保证任何 phase 都能打中)、evening 20:00-22:00(report + outlook)。**季度末额外一次 meta**:`--mode meta` / `run_quarterly_meta_reflection`,跑 `quarterly_digest` 聚合 90 天事实 + `meta_reflector` LLM 7 步 CoT + `prompt_editor` 4 道保险 apply
- **6 个 session**(ET Mon-Fri):earnings_preprocess 08:00-09:15(唯一跑 earnings LLM)、morning 09:30-12:00(full team)、intra_check 09:30-16:00 每 30min tick(熔断器,零 LLM)、midday 13:00-14:30(position_reviewer patient)、close 15:30-16:00(position_reviewer act-on-trigger;窗口 ≥ launchd 30min tick 保证任何 phase 都能打中)、evening 20:00-22:00(report + outlook)。**季度末额外一次 meta**:`--mode meta` / `run_quarterly_meta_reflection`,跑 `quarterly_digest` 聚合 90 天事实 + `meta_reflector` LLM 7 步 CoT + `prompt_editor` 4 道保险 apply。**另有 daily CSV 导出**(2026-06-10 PR #99):`--mode daily` 把全量 NAV/SPY/drawdown 历史发成 Telegram 文档(取代 evening 推送里的 P&L 文本表);纯数据导出(零 LLM 零交易),**不走** run_if_et_window.sh(wrapper 会拒掉 daily 模式),由独立 timer `quant-agent-daily.timer`(Mon-Fri 09:00 ET)经 `scripts/run_daily_export.sh` 触发,units 在 `scripts/systemd/`
- 数据源:yfinance、FRED、RSS、SEC EDGAR
- 配置:`config/settings.yaml` + `.env`;按 agent 独立选 OpenAI / Anthropic / DeepSeek 模型。**2026-06-04 起所有 9 个 agent 用 OpenAI `gpt-5.5`**(5-11 曾切 claude-opus-4-7 应对 OpenAI quota,6-04 又切回 OpenAI 并升到 5.5;切 provider 一条 `sed` 命令)。Provider 路由按 model name 前缀判断:`deepseek-` 走 DeepSeek,`gpt-` / `o1-` / `o3-` / `o4-` 走 OpenAI,其它走 Anthropic(`src/agents/base.py` 的 `_DEEPSEEK_PREFIXES` / `_OPENAI_PREFIXES`)
- **DeepSeek(OpenAI-compatible,2026-06-05 加)**:走 openai SDK + `base_url=https://api.deepseek.com` + DeepSeek key(`_call_deepseek`)。三个坑都已处理(研究自 api-docs.deepseek.com):(1) DeepSeek 只认 **`max_tokens`** 不认 OpenAI 的 `max_completion_tokens`(发错会被静默丢弃 → 回落 ~4096 默认截断);(2) DeepSeek **拒绝**(不裁剪)超 ceiling 的 max_tokens,所以按 `_DEEPSEEK_MAX_OUTPUT` per-model 客户端 clamp(v4-flash/pro/chat/reasoner=384K,未知 deepseek-* 保守 8192);(3) 402「Insufficient Balance」= 不可重试 → 触发 failover,`insufficient_system_resource` finish_reason 记为 truncated。**`deepseek-chat`/`deepseek-reasoner` 2026-07-24 弃用**(现已 alias `deepseek-v4-flash`),新配置直接用 `deepseek-v4-flash`。cost 用**官方** $0.14/$0.28(LiteLLM 的 $0.28/$0.42 是 V4 前旧值,已 **pin** 在 `cost_table._PRICING_PINNED` 防 cache 刷新覆盖)
- **跨 provider 自动 failover**:当**非-Anthropic 主**(OpenAI 或 DeepSeek)调用重试耗尽 / 非可重试错误(quota、DeepSeek 402、死 key、宕机)后,`base.py:run()` 会**自动用 Anthropic 的 `_FALLBACK_MODEL`(=`claude-opus-4-7`)单发一次**(无重试,避免吃穿 session 窗口),成功就用它的结果继续(`AgentResult.model` 记实际用的模型,cost 按实际模型算)、失败就抛出原始错误。只在「主=OpenAI/DeepSeek 且 `.env` 有 ANTHROPIC_API_KEY」时触发;主已是 Claude 则 no-op(同 provider 无意义,且构造时不会因此报错)。截断(max_tokens)不触发 failover。pipeline 给 9 个 agent 都传 `fallback_api_key=config.api_keys.anthropic`。`src/config.py` 的 LLMConfig 默认(settings.yaml 漏配时的兜底)也已从过时的 `*-4-6` 更到 `claude-opus-4-7`
- **Telegram 推送**:开/关由 `.env` 控制,缺 `TELEGRAM_BOT_TOKEN` 或 `TELEGRAM_CHAT_ID` 时 notifier 静默 no-op,trading 不受影响。每个 session 在 `main.py` finally 块里调一次 `notifier.send(format_session_result(...))`;噪声策略:morning/midday/close/evening 总推;earnings_preprocess 只在真分析了 filing 时推;intra_check 只在 emergency 触发时推(14 次/天 OK tick 静默);meta 只在真季末跑时推;**任何 session 抛异常都强制推**绕过噪声策略。文档见 `src/notifier.py` docstring 和 README "Optional env vars" 段
- **Telegram 推送**:开/关由 `.env` 控制,缺 `TELEGRAM_BOT_TOKEN` 或 `TELEGRAM_CHAT_ID` 时 notifier 静默 no-op,trading 不受影响。每个 session 在 `main.py` finally 块里调一次 `notifier.send(format_session_result(...))`;噪声策略:morning/midday/close/evening 总推;earnings_preprocess 只在真分析了 filing 时推;intra_check 只在 emergency 触发时推(14 次/天 OK tick 静默);meta 只在真季末跑时推;daily 的 `sent` 静默(CSV 文档本身就是送达确认,status text 只在 error/skipped 时推且带原因);**任何 session 抛异常都强制推**绕过噪声策略。文档见 `src/notifier.py` docstring 和 README "Optional env vars" 段

### Agent CoT 结构(schema-enforced 必填字段数;违反 → ValidationError)
| Agent | CoT 步数 | 备注 |
Expand Down Expand Up @@ -78,7 +79,7 @@ python main.py --mode morning|midday|evening|live # 手动跑
- `src/notifier.py:TelegramNotifier`,在 `main.py` finally 块里调用 `format_session_result(mode, result, elapsed, error=...)` 推送。**Hook 必须在 finally 里**,不能在 try 内部——否则 session 抛异常时收不到 FAILED 推送(这是日志之外操作员唯一的实时信号)
- 错误必须 swallow:`notifier.send()` 内部 `except Exception` 兜住所有 HTTP / 网络 / Telegram-端报错;main.py finally 块再包一层 try/except 防 notifier 自己挂。**`notify` 失败永远不能让 session 失败**,这条比"得到通知"重要
- 噪声策略由 `format_session_result` 返回 `None` 实现(caller 看 `None` 就 skip send)。policy 见模块 docstring;改这条策略前先想清楚"这个 silence 是不是把真信号也吞了"——典型反例:earnings_preprocess 当时把 `analysis_error` 也 silence 过,结果 OpenAI quota 耗尽那天 13 个 filing 全 retry 烧 token 但没人收到通知(2026-05-11 修,`analysis_error` 现在推)
- **确定性升级告警**:evening 推送的 🚨 banner 不只看 LLM 的 `risk_rating`——`_append_evening_body` 还独立算"今日亏损 ≥ 80% 日损熔断线"(用 `result["max_daily_loss_pct"]`)触发 `DETERMINISTIC ALERT`,与 LLM 判断 OR。理由:自评风险时 LLM 最容易**低估**,而这正是最该被抓住的情况——镜像交易路径"硬规则 + LLM"两层哲学。`suggested_actions` 已挪到 P&L 历史表**之前**渲染,避免 4000 字尾部截断在高风险日吃掉最该读的行
- **确定性升级告警**:evening 推送的 🚨 banner 不只看 LLM 的 `risk_rating`——`_append_evening_body` 还独立算"今日亏损 ≥ 80% 日损熔断线"(用 `result["max_daily_loss_pct"]`)触发 `DETERMINISTIC ALERT`,与 LLM 判断 OR。理由:自评风险时 LLM 最容易**低估**,而这正是最该被抓住的情况——镜像交易路径"硬规则 + LLM"两层哲学。`suggested_actions` 渲染在 headline P&L 之后的高位,避免 4000 字尾部截断在高风险日吃掉最该读的行(其后原有的 P&L 历史文本表 2026-06-10 起移除,改由 `--mode daily` 的 CSV 导出承担)
- **内部 dead-man's check**:evening(已 gated 在交易日)调 `_expected_sessions_missing_today()`,查 `agent_logs` 今日 ET 是否有 morning(`run-`)/midday/close 的 run_id 前缀;缺了就在推送顶部 🔴(morning)/⚠️(midday·close)。catch "某 session 静默没跑"(timer 挂 / lock 卡 / 半日盘窗口算错)——push-on-completion 观测唯一看不见的失败模式。**不覆盖主机宕机 / evening 本身没跑**——那需要外部 dead-man's switch(healthchecks.io 式,wrapper 成功就 ping、缺席就外部报警),建议补上
- Telegram bot token 等同密码:写 `.env` 用 `chmod 600`,**不要**贴 git / issue / 公开 chat。token 万一外泄(推送的截图 / 误贴 ssh log)马上去 BotFather 发 `/revoke` 生新的

Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,9 @@ python main.py --mode midday # Position review + trailing stops
python main.py --mode evening # PnL report + insights for tomorrow
python main.py --mode live # APScheduler in-process (dev/legacy; production
# uses systemd/launchd timers, not this)
python main.py --mode daily # P&L history CSV -> Telegram document
# (no LLM, no trading; full NAV/SPY/drawdown
# history from Alpaca portfolio_history)
```

**Automated scheduling**: the production path is a 30-min OS-level timer (systemd `quant-agent@.timer` on Linux, launchd plist on macOS) that calls `scripts/run_if_et_window.sh <mode>` for each session. The wrapper checks the current **US/Eastern** wall clock against the target window, applies the cross-mode session lock (one heavy LLM session at a time, except `intra_check` which is exempt), and skips if the mode already ran today. Runs the right session at the right ET moment regardless of the host's timezone — handy when traveling. Windows (Mon-Fri ET, authoritative Python table at `src/trading_calendar.py` `SESSION_WINDOWS`, locked to the bash wrapper by `test_trading_calendar.py`):
Expand All @@ -290,6 +293,8 @@ python main.py --mode live # APScheduler in-process (dev/legacy; productio
- `close` 15:30-16:00 ET — position review (act-on-trigger; window ≥ 30-min OS-timer tick so it never misses)
- `evening` 20:00-22:00 ET — daily P&L + insights for next morning

The **daily P&L CSV export** (`--mode daily`) is scheduled separately — it is a pure data export (no LLM, no orders), so it skips the window/lock wrapper entirely. A fixed-time systemd timer fires it Mon-Fri 09:00 ET via `scripts/run_daily_export.sh` (sources `.env`, 300s timeout). Units are tracked at `scripts/systemd/quant-agent-daily.{service,timer}`; install with `cp scripts/systemd/quant-agent-daily.* ~/.config/systemd/user/ && systemctl --user daemon-reload && systemctl --user enable --now quant-agent-daily.timer`. The CSV replaced the P&L history text table that the evening push used to embed.

## Trading Universe

97 symbols (source of truth: `config/settings.yaml:trading.universe`):
Expand Down
Loading
Loading