第 4 章 · 记忆日志、Alpha 反思与断点续跑
本章讲框架怎么复盘自己的决策、怎么把教训喂回未来、以及崩了怎么续跑。这是 TradingAgents 区别于“一次性问答”的关键——它有时间维度。
4.1 核心思路:决策的复盘要“等行情”
交易决策对不对,当下不知道——得等几天真实行情出来才知道赚没赚、跑没跑赢大盘。所以框架把记忆做成两阶段、延迟反思:
第 N 次跑 TICKER:
Phase A(run 结束): 把决策记为 [date | TICKER | 评级 | pending],不调 LLM
第 N+1 次跑同一个 TICKER(propagate 开头):
Phase B: 找出该 TICKER 所有 pending 条 目
→ 用 yfinance 拉那段时间的真实收益 + alpha
→ 调 LLM 写 2-4 句反思
→ 原子批量回写,pending 变成带收益的已结算条目
入口在 propagate 开头调 _resolve_pending_entries(trading_graph.py:296),结束时调 memory_log.store_decision(trading_graph.py:469)。
4.2 Phase A:记决策(纯文本,无 LLM)
记忆日志是一个追加式 markdown 文件(TradingMemoryLog, memory.py)。每条决策用一行 tag 起头,用 HTML 注释 <!-- ENTRY_END --> 作硬分隔(因为它不可能出现在 LLM 散文里,memory.py:12-13):
[2024-05-10 | NVDA | Buy | pending]
DECISION:
<组合经理的完整最终决策文字>
<!-- ENTRY_END -->
store_decision(memory.py:30)带幂等保护:写前先快速扫原文,若已有同 (date, ticker) 的 pending 就不重复写。评级用 parse_rating 从决策里抽(见第 2 章)。
4.3 Phase B:结算——算 alpha、写反思
下次同标的运行时,_resolve_pending_entries(trading_graph.py:296)干三件事:
① 拉真实收益。 _fetch_returns(trading_graph.py:251)用 yfinance 拉决策日起 holding_days(默认 5 个交易日,加 7 天缓冲跨周末)的价格,算原始收益和相对基准的超额收益(alpha):
raw = (标的收盘[持有末] - 标的收盘[0]) / 标的收盘[0]
alpha = raw - (基准收盘[持有末] - 基准收盘[0]) / 基准收盘[0]
价格拿不到(太近/退市/网络错)就返回 (None,None,None),跳过、下次再试(trading_graph.py:316-317)——不阻塞。
② 选对基准。 _resolve_benchmark(trading_graph.py:230)按 ticker 的交易所后缀选基准指数:.T→日经、.HK→恒生、.L→FTSE…,美股无后缀默认 SPY(default_config.py:152-163 的 benchmark_map)。这样非美股也能有合理的 alpha 基准。
③ 写反思。 Reflector.reflect_on_final_decision(reflection.py:31)调 quick LLM,用一个刻意精简的 prompt(reflection.py:20-29):只要 2-4 句纯散文,按顺序讲“方向对没对(引 alpha)、论点哪部分成立/失败、下次一条具体教训”。为什么这么抠字数——因为反思会被再注入未来 agent 的 prompt,太长会撑爆上下文。
所有结算在一次 batch_update_with_outcomes(memory.py:164)里批量原子写,避免多次 I/O。
4.4 原子写:崩了也不毁日志
回写用 temp 文件 + os.replace() 模式(memory.py:160-162、memory.py:214-216):先写 .tmp,再原子替换。这样即使写到一半崩溃,也不会留下半损坏的 日志。pending tag [date | TICKER | 评级 | pending] 被替换成带收益的 [date | TICKER | 评级 | +3.2% | +1.1% | 5d],并追加 REFLECTION: 段。
可选的 memory_log_max_entries 会轮转掉最老的已结算条目,但pending 条目永不删(它们是未处理的活,_apply_rotation, memory.py:220)。
4.5 教训怎么喂回未来
运行开始时 get_past_context(memory.py:70)拼一段历史注入 state.past_context:
- 同标的最近 5 条完整决策+反思。
- 跨标的最近 3 条“只要反思”的教训(
_format_reflection_only)。
这段 context 被组合经理读取(portfolio_manager.py:35-40):prompt 里有历史教训就让它参考,没有就只靠当前分析。于是框架形成一个跨运行的学习回路。
4.6 断点续跑:per-ticker checkpoint
它要解决的小问题: 一次完整分析要调几十次 LLM,中途崩了(限流、断网)不该从头再来。
思路: LangGraph 的 SqliteSaver 在每个节点后存 state,崩溃后能从最后一个成功节点续跑。设计要点(graph/checkpointer.py):
- 每个 ticker 一个独立 SQLite 库(
_db_path,checkpointer.py:19),并发不同标的不争锁。 - thread_id 折入“图形状签名”(
thread_id,checkpointer.py:28):sha256(TICKER:date:signature)[:16],signature 包含分析师选择、辩论/风险轮数、资产类型(trading_graph.py:348的_run_signature)。这样换了图形状的 resume 不会误用旧 checkpoint(issue #1089)——否则会拿着 3 分析师的存档去续 4 分析师的图,静默跑歪。 - 成功跑完就
clear_checkpoint清存档,避免陈旧状态(trading_graph.py:476)。
启用方式:config["checkpoint_enabled"]=True,propagate 会用 checkpointer 重编译图(trading_graph.py:378-383)。
4.7 端到端时序(把本章串起来)
propagate("NVDA", "2024-05-20")
│
├─ Phase B: 结算上次 5-10 的 pending → 拉真实收益 → 算 alpha vs SPY → 写反思 → 原子回 写
├─ get_past_context("NVDA") → 注入历史决策+跨标的教训
├─ (可选)加载 checkpoint,从上次成功节点续
├─ 跑完整个 agent 图 → final_trade_decision
├─ Phase A: store_decision → [2024-05-20 | NVDA | ... | pending]
└─ 清 checkpoint
4.8 代码地图(本章)
| 主题 | 文件 | 符号 |
|---|---|---|
| 记忆日志 | tradingagents/agents/utils/memory.py | TradingMemoryLog.store_decision、get_past_context、batch_update_with_outcomes、_apply_rotation |
| 收益/alpha | tradingagents/graph/trading_graph.py | _fetch_returns、_resolve_benchmark、_resolve_pending_entries |
| 反思 | tradingagents/graph/reflection.py | Reflector.reflect_on_final_decision |
| checkpoint | tradingagents/graph/checkpointer.py | thread_id、get_checkpointer、checkpoint_step、clear_checkpoint |
| 图形状签名 | tradingagents/graph/trading_graph.py | _run_signature |