交易日志系统设计 — 让每笔交易可学习
为什么必须有交易日志、字段设计、归因分析方法、心理偏差识别
日期: 2026-06-05 方向: 风控 / 流程 阶段: Phase 1: 基础与工具链 标签: #TradeJournal #DecisionJournal #Attribution #ExPostRationalization #HindsightBias #Schema
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | 为什么必须有交易日志、字段设计、归因分析方法、心理偏差识别 |
| 实操 | 定 schema + 写 trade_log.py + 跑通一条端到端记录 + 给 Day 1-26 的 paper 单补录 |
| 产出 | TR-DAY27 笔记 + Trade 模板 + 周报/月报脚本 + decision journal 框架 |
零、Week 4 风控章定位
Week 4 之前我们干了什么:
- Week 1 工具链(IBKR / ib_insync / 数据 / 回测引擎)
- Week 2 第一组因子(动量 / mean reversion / 波动率)
- Week 3 期权基础与 Wheel paper trading
Day 21-26 已经把仓位上限、止损、Kelly、相关性矩阵、压力测试、流动性风险都过了一遍。但这些都是「事前 / 事中」风控——它们决定你能不能开仓、要不要砍仓。
Day 27 要解决的是 「事后」环节:没有交易日志,前面 26 天的所有功夫都会随时间漂移成自我安慰。这是个人量化里最被低估、却最决定 3 年后水平的一件事。
一、为什么必须有交易日志
1.1 大脑记忆是骗子
人类大脑不是「事实存储设备」,是「叙事构造设备」。每次回忆一笔交易,大脑会重新合成一个剧情,让它和你当前的情绪/结论自洽。结果:
| 偏差 | 表现 | 真实代价 |
|---|---|---|
| Survivor bias(幸存者偏差) | 只记得赚的几笔;亏的「都是 noise / 不算」 | 高估自己策略 Sharpe |
| Hindsight bias(事后诸葛) | 看到 SPY 已经涨完才说「我早就看好啊」 | 学不到真正的教训 |
| Ex-post rationalization | 亏完才说「这笔本来就不该开」 | 真正的进场理由永远不被审视 |
| Anchor on outcome | 赚钱的 = 好决策;亏钱的 = 坏决策 | 把运气当能力 |
| Recency bias | 最近 5 笔决定你对整个策略的信心 | 频繁切换策略 |
我做了 10 年金融零售产品,最深刻的一个观察:优秀的人和平庸的人的差距,不在「记忆力强」而在「不信任记忆」。前者建系统,后者凭感觉。
1.2 "If it's not measured, it's not managed"
德鲁克这句被引用到滥的话,对个人量化几乎是宪法级别真理:
没有日志
→ 没有数据
→ 无法做归因(attribution)
→ 不知道赚的是 alpha / beta / 还是 luck
→ 无法分辨「策略对了」vs「市场救了你」
→ 无法迭代
→ 第 90 天和第 1 天水平一样
反过来,有日志 + 能归因 = 复利学习速度。每笔交易都是一个「带标签的数据点」,500 笔之后你对自己的策略边界比任何回测都清楚。
1.3 这是产品经理思维迁移最直接的地方
我做 PM 时坚持每个重要决策(功能上下线 / 招聘 / 架构选择)写 Decision Journal——
- 决策时的 thesis 是什么
- 我当时知道哪些信息
- 我假设了什么
- 决策的 expected outcome
- 决策的 worst case
3 个月后回看,90% 的「我早就觉得 X」都是骗自己。交易日志是同一回事,只不过反馈周期更短(几天到几周)、信号更干净(P&L 是硬数字)。
二、交易日志该记录什么 — 四阶段模型
把一笔交易切成 4 个阶段,每个阶段记不同的东西。
2.1 下单前(Pre-trade)
这是最关键的一段,因为它在「outcome 还没出来」时记录,没有任何 hindsight 污染。
| 字段 | 为什么记 |
|---|---|
| strategy_name | 这笔属于哪个策略(momentum / wheel / earnings_drift / discretionary) |
| thesis | 50-200 字白话解释「我凭什么觉得这能赚钱」 |
| entry_signal | 触发进场的具体条件(如 "RSI < 30 + 大盘 above 200MA") |
| stop_loss | 哪个价位 / 哪个事件下止损 |
| take_profit | 目标价 / 目标收益率 / 时间窗口 |
| max_holding_days | 持仓上限(很多策略 alpha 衰减在 5-20 天) |
| position_size | 美元金额 + 占组合 % + 仓位依据(Kelly / 固定 % / 波动率倒数) |
| expected_R | 预期赔率(赚的目标 / 止损距离),<1 通常不该做 |
| confidence | 0-1 的主观置信度(不是科学但能用来校准) |
实操规则:下单前 thesis 必须打出来再发单。如果敲不出 thesis,说明你不知道为什么做这笔——直接 abort。
2.2 执行中(Execution)
记录订单从发出到成交的过程,主要为了量化「执行损耗」:
| 字段 | 为什么记 |
|---|---|
| order_type | Market / Limit / Bracket / TWAP |
| limit_price | 你挂的价 |
| actual_fill_price | 实际成交价 |
| slippage_bps | (fill - limit) / limit × 10000 |
| fill_time_ms | 从发单到成交时间 |
| partial_fills | 是否分批成交 |
| commission | 手续费 |
为什么这段重要:60% 的散户策略「回测年化 30%,实盘 5%」的差距,主要在 slippage + commission。你必须知道自己平均每笔被吃了多少 bps。
2.3 持仓中(During hold)
持仓期间的关键事件,特别是「让你想砍仓 / 加仓」的瞬间:
| 字段 | 为什么记 |
|---|---|
| mid_pnl_snapshots | 每日收盘 mark-to-market P&L |
| drawdown_during | 持仓期间最大浮亏 |
| greeks_changes(期权) | Delta / Theta / Vega 关键变化点 |
| news_events | 财报 / FOMC / 个股新闻 |
| thesis_still_valid | 每隔几天问自己:原始 thesis 还成立吗?yes/no/partial |
| emotions | 「我现在想砍仓」「我现在想加仓」 |
最关键字段是 thesis_still_valid。砍仓的唯一合理理由是 thesis 失效,不是亏了 10%。
2.4 平仓后(Post-trade / Post-mortem)
| 字段 | 为什么记 |
|---|---|
| actual_exit_time | |
| actual_exit_price | |
| actual_pnl | 美元 + R 倍数 + % |
| planned_pnl | 进场时预期 |
| pnl_attribution | alpha / beta / event / luck(见第六节) |
| holding_days | |
| exit_reason | stop / target / time / discretionary / thesis_broke |
| plan_vs_actual_delta | 哪里偏离了计划 |
| lessons_learned | 一句话教训 |
| bias_tags | 这笔有没有触发 hindsight / sunk cost / fomo / revenge_trade |
三、关键字段 Schema 设计
把上面 4 段合并,落成可机器读的 schema:
# trade_schema.py
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional, List
from enum import Enum
import uuid
class Strategy(Enum):
MOMENTUM = "momentum"
MEAN_REVERSION = "mean_reversion"
WHEEL = "wheel"
EARNINGS_DRIFT = "earnings_drift"
PAIRS = "pairs"
DISCRETIONARY = "discretionary"
class ExitReason(Enum):
STOP_LOSS = "stop_loss"
TAKE_PROFIT = "take_profit"
TIME_STOP = "time_stop"
THESIS_BROKE = "thesis_broke"
REBALANCE = "rebalance"
DISCRETIONARY = "discretionary"
class BiasTag(Enum):
HINDSIGHT = "hindsight_bias"
EX_POST_RAT = "ex_post_rationalization"
SUNK_COST = "sunk_cost"
FOMO = "fomo"
REVENGE = "revenge_trade"
ANCHORING = "anchoring"
@dataclass
class Trade:
# ============ identity ============
trade_id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
strategy: Strategy = Strategy.DISCRETIONARY
symbol: str = ""
direction: str = "long" # long / short
instrument: str = "stock" # stock / option / future
# ============ pre-trade ============
entry_time: Optional[datetime] = None
entry_price: float = 0.0
position_size_usd: float = 0.0
position_pct_portfolio: float = 0.0
sizing_method: str = "" # kelly_half / fixed_pct / vol_target
thesis: str = "" # 50-200 字
entry_signal: str = ""
stop_loss: Optional[float] = None
take_profit: Optional[float] = None
max_holding_days: Optional[int] = None
expected_R: Optional[float] = None # reward / risk
confidence: float = 0.5 # 0-1
# ============ execution ============
order_type: str = "limit"
limit_price: Optional[float] = None
actual_fill_price: Optional[float] = None
slippage_bps: Optional[float] = None
fill_time_ms: Optional[int] = None
commission: float = 0.0
# ============ during hold ============
max_drawdown_during: float = 0.0
max_gain_during: float = 0.0
thesis_revalidations: List[str] = field(default_factory=list)
notable_events: List[str] = field(default_factory=list)
# ============ post-trade ============
exit_time: Optional[datetime] = None
exit_price: Optional[float] = None
actual_pnl_usd: Optional[float] = None
actual_pnl_pct: Optional[float] = None
actual_R: Optional[float] = None # 实际赔率
planned_pnl_usd: Optional[float] = None
holding_days: Optional[float] = None
exit_reason: Optional[ExitReason] = None
plan_vs_actual: str = ""
lessons_learned: str = ""
bias_tags: List[BiasTag] = field(default_factory=list)
# ============ attribution(每月跑) ============
attrib_beta_pnl: Optional[float] = None
attrib_alpha_pnl: Optional[float] = None
attrib_event_pnl: Optional[float] = None
attrib_luck_pnl: Optional[float] = None
为什么这么设计:
- 所有数值字段
Optional— 进场时还没有 exit_price,避免被迫填假数据 - 枚举 strategy / exit_reason / bias_tag — 防止「自由文本」让后期统计变难
- thesis / entry_signal / lessons_learned 用 free text — 这部分必须保留人话
- attribution 字段单独 — 每月批量跑,不阻塞日常记录
四、存储方案选择 — 从轻到重
很多人一上来就 PostgreSQL + Grafana,3 天后放弃。正确做法是配匹配交易频率的存储。
| 方案 | 适合 | 优点 | 缺点 |
|---|---|---|---|
| Markdown 单文件每笔 | < 20 笔/月 | 写起来像写日记,git 友好 | 难做统计 |
| Markdown + 每周汇总 CSV | 20-80 笔/月 | 兼顾「人话」+「机器读」 | 双写要纪律 |
| SQLite + Markdown 双轨 | 80-300 笔/月 | 查询快,本地无依赖 | 改 schema 麻烦 |
| PostgreSQL + Streamlit | > 300 笔/月或多人 | 真正产品级 | 维护成本高 |
Phase 1 推荐方案:Markdown 单文件 + 每周汇总 CSV
理由:
- 你这阶段交易频率 5-30 笔/月,Markdown 完全够
- Markdown 强迫你写 thesis(人话),SQLite 容易让你只填数字
- git commit 自带版本历史 — 这是 audit trail
- 周末再用脚本把 markdown 抽成 CSV,跑统计
目录结构建议:
trades/
├── 2026/
│ ├── 06/
│ │ ├── 2026-06-05_SPY_momentum_001.md
│ │ ├── 2026-06-05_AAPL_wheel_002.md
│ │ └── 2026-06-08_TSLA_earnings_003.md
│ └── weekly/
│ ├── 2026-W23-summary.md
│ └── 2026-W23-trades.csv
└── monthly/
└── 2026-06-attribution.md
五、完整 Markdown 模板(可直接复制)
# Trade [trade_id]: [SYMBOL] [strategy]
> **trade_id**: 2026-06-05-SPY-001
> **strategy**: momentum
> **symbol**: SPY
> **direction**: long
> **instrument**: stock
---
## A. Pre-trade(下单前必填)
- **entry_time**: 2026-06-05 09:35 ET
- **entry_signal**: 20-day momentum > 75th percentile, RSI(14) 在 50-70 中性区间, SPY 在 200MA 之上
- **thesis**(必须 50-200 字):
> 大盘动量延续。Fed 6 月 FOMC 偏鸽,VIX < 14,无重大事件窗口。
> 这笔是策略动量信号触发,不是 discretionary。
> 如果 SPY 跌破 20MA 或 VIX 跳到 18+,thesis 失效。
- **position_size_usd**: $1,000
- **position_pct**: 20% of $5k 组合
- **sizing_method**: fixed_pct (Kelly half 在小账户下风险过高)
- **stop_loss**: $480 (entry $500,止损 4%)
- **take_profit**: $520 (4% 目标) 或 持仓满 10 个交易日
- **max_holding_days**: 10
- **expected_R**: 1.0 (4% / 4%)
- **confidence**: 0.55
## B. Execution(成交后填)
- **order_type**: limit
- **limit_price**: $500.00
- **actual_fill_price**: $500.12
- **slippage_bps**: +2.4 bps(不利方向)
- **commission**: $0.35
## C. During hold(每日盘后简记)
- 2026-06-06: 收 $502.30,浮盈 0.4%,thesis valid。
- 2026-06-09: 收 $498.10,浮亏 0.4%,thesis valid。
- 2026-06-10: SPY 突然跌到 $493,VIX 跳到 17,**thesis 半失效**。
- emotions: 6-10 当晚有「想 average down」冲动,已抑制(不在计划内)。
## D. Post-trade(平仓后必填)
- **exit_time**: 2026-06-15 15:50 ET(time stop,max_holding 到)
- **exit_price**: $505.40
- **exit_reason**: time_stop
- **actual_pnl_usd**: +$10.56(扣手续费)
- **actual_pnl_pct**: +1.06%
- **actual_R**: +0.26
- **plan_vs_actual**: 计划 ±4%,实际 +1.06%,时间到强制平仓。市场没给到目标。
- **lessons_learned**: 动量在低波环境下回报压缩,需考虑 vol-adjusted 止盈(目标 = 1×ATR 而不是固定 4%)。
- **bias_tags**: [](这笔没触发偏差,纪律执行)
模板使用 3 条铁律:
- A 段不填完,不准发单(git pre-commit hook 都可以加)
- D 段必须当天平仓后 1 小时内填,不要拖(情绪记忆衰减极快)
- D 段 lessons_learned 必须是「下一笔能用上」的具体动作,不是「我要更谨慎」这种废话
六、归因分析(Attribution)— 区分技能与运气
这是日志系统的精华。没有归因,日志只是日记。
6.1 四层归因
把一笔交易的 P&L 拆成 4 部分:
total_pnl = beta_pnl + alpha_pnl + event_pnl + luck_pnl
| 来源 | 含义 | 怎么算 |
|---|---|---|
| beta_pnl | 大盘暴露贡献 | β × (SPY return 同期) × position_size |
| alpha_pnl | 相对因子模型的超额 | OLS 残差(用 Fama-French 3/5 因子) |
| event_pnl | 财报 / news / FOMC 等事件驱动 | 事件窗口内的 abnormal return |
| luck_pnl | 剩余 noise | total - beta - alpha - event |
6.2 实操:每月用 statsmodels 跑一次
# attribution.py
import pandas as pd
import statsmodels.api as sm
def fama_french_attribution(trade_returns: pd.Series, ff_factors: pd.DataFrame) -> dict:
"""
trade_returns: index=date, values=daily portfolio return
ff_factors: columns=[Mkt-RF, SMB, HML, RMW, CMA, RF]
"""
y = trade_returns - ff_factors['RF']
X = ff_factors[['Mkt-RF', 'SMB', 'HML', 'RMW', 'CMA']]
X = sm.add_constant(X)
model = sm.OLS(y, X).fit()
alpha_annual = model.params['const'] * 252
return {
'alpha_annualized': alpha_annual,
'alpha_tstat': model.tvalues['const'],
'beta_market': model.params['Mkt-RF'],
'beta_size': model.params['SMB'],
'beta_value': model.params['HML'],
'r_squared': model.rsquared,
}
判定规则:
- alpha t-stat > 2 → 真有 alpha
- alpha t-stat 0-2 → 可能是 luck,再观察 3 个月
- alpha t-stat < 0 → 不如直接买 SPY,停止该策略
6.3 一个直觉示例
你这个月赚了 8%,SPY 同期涨 5%,组合 β=1.2。
beta_pnl = 1.2 × 5% = 6%
alpha_pnl (假设回归出来) = 1.5%
event_pnl (一笔财报赚了) = 0.5%
luck_pnl = 8% - 6% - 1.5% - 0.5% = 0%
看上去赚 8% 好厉害,真正能力贡献只有 1.5%(alpha)+ 0.5%(事件)= 2%。剩下 6% 是 beta(买个 SPY 用 1.2 杠杆就能拿到,不算技能)。
这就是为什么没归因的人会高估自己:把 beta 当 alpha,把 luck 当能力。
七、八大心理偏差 — 在日志里识别它们
7.1 Ex-post rationalization(事后合理化)
- 症状:亏完才说「我早就觉得 thesis 有问题」
- 检测:对比 A 段(pre-trade thesis)和 D 段(post-trade lessons)
- 如果 A 段写「VIX 低我看好」,D 段写「VIX 低风险其实大」,这就是 ex-post
- 代价:你的真实 entry framework 永远不被审视,永远在错的地方下单
7.2 Hindsight bias(事后诸葛)
- 症状:「我应该 hold longer / 我应该早点跑」
- 检测:看 A 段 stop / target,如果你按计划执行了,就没有应该不应该
- 正确反应:thesis 错了改 thesis,执行错了改执行,不要混淆这两个
7.3 Sunk cost fallacy(沉没成本)
- 症状:「都亏 20% 了再扛扛吧」「平均下来成本更低」
- 检测:D 段 exit_reason 是
discretionary且 actual_pnl 是大负数 - 正确反应:当前价位是否还会让你新开这笔仓?如果不会,就是沉没成本绑架
7.4 Anchoring on entry price(锚定进场价)
- 症状:盯着「成本价」决定砍不砍
- 正确反应:每天问自己「假设现在没仓位,看到这个图我会进场吗?」
- 不会 → 应该平仓,无论是赚是亏
7.5 FOMO
- 症状:日志里突然出现一笔「strategy=discretionary, thesis=「太强了不能错过」」
- 检测:strategy 字段不是预设的几个之一
- 应对:FOMO 单子要么不做,要么严格 0.5% 仓位
7.6 Revenge trade(报复交易)
- 症状:刚亏完立刻开同向更大仓位
- 检测:本笔 entry_time 距离上一笔 exit_time < 30 分钟,且 size 显著放大
- 应对:止损后强制 24 小时冷静期
7.7 Recency bias
- 症状:最近 3 笔赢了就 size 翻倍
- 检测:sizing_method 突然偏离原 framework
- 应对:sizing 由 Kelly / vol target 算出,不是由「最近运气」算出
7.8 Confirmation bias
- 症状:进场后只读支持你 thesis 的新闻
- 应对:日志的 thesis_revalidations 字段要求强制写一句反向证据
八、每周 / 每月汇总
8.1 周报模板
# Week 23 Summary (2026-06-01 to 2026-06-07)
## 交易统计
- 交易笔数:8 (5 closed, 3 open)
- 胜率:3/5 = 60%
- avg win:+1.8%
- avg loss:-1.2%
- payoff ratio:1.5
- 总 P&L:+$45 (+0.9% on $5k)
## 执行质量
- avg slippage:3.2 bps
- max slippage:12 bps(SPY market order,下次改 limit)
- 计划外平仓:1/5 = 20%(目标 <10%)
## 心态偏差
- ex_post_rationalization:1 次(trade 003)
- revenge_trade:0
- fomo:1 次(trade 006,事后看不该做)
## 本周教训 3 条
1. 财报前一天的动量信号不可靠 — 加 filter
2. 期权 wheel 在 IV < 20 时收益太薄 — 加 IV 门槛
3. SPY market order 早盘 9:30-9:40 滑点大 — 改 9:45 后
## 下周计划调整
- 动量策略加「earnings_in_next_5d = False」filter
- wheel 加「IV_rank > 30」filter
8.2 月报模板
月报多两块:
- 归因报告:跑 Fama-French,给每个策略算 alpha
- 策略 P&L 分布直方图:看是不是「胖尾」(少数大赢家驱动 vs 均匀分布)
- 改进项 backlog:本月发现的问题,下月优先级排序
九、代码:完整 trade_log.py 框架
# trade_log.py
import json
import pathlib
import pandas as pd
from datetime import datetime
from dataclasses import asdict
from trade_schema import Trade, Strategy, ExitReason, BiasTag
TRADES_DIR = pathlib.Path("trades")
class TradeLog:
def __init__(self, base_dir: pathlib.Path = TRADES_DIR):
self.base = base_dir
self.base.mkdir(parents=True, exist_ok=True)
# ---------- write ----------
def add_trade(self, trade: Trade) -> pathlib.Path:
"""Pre-trade record. Requires thesis & stop_loss."""
assert trade.thesis and len(trade.thesis) >= 30, \
"thesis must be >= 30 chars — what's your reason?"
assert trade.stop_loss is not None, "stop_loss required"
assert trade.position_size_usd > 0, "size required"
if trade.entry_time is None:
trade.entry_time = datetime.now()
date = trade.entry_time.strftime("%Y-%m-%d")
year_month = trade.entry_time.strftime("%Y/%m")
folder = self.base / year_month
folder.mkdir(parents=True, exist_ok=True)
fname = f"{date}_{trade.symbol}_{trade.strategy.value}_{trade.trade_id}.md"
path = folder / fname
path.write_text(self._render_markdown(trade), encoding="utf-8")
return path
def close_trade(self, trade_id: str, exit_price: float,
exit_reason: ExitReason, lessons: str,
bias_tags: list = None) -> pathlib.Path:
"""Post-trade update."""
path = self._find(trade_id)
trade = self._parse_markdown(path)
trade.exit_time = datetime.now()
trade.exit_price = exit_price
trade.exit_reason = exit_reason
trade.lessons_learned = lessons
trade.bias_tags = bias_tags or []
# compute pnl
sign = 1 if trade.direction == "long" else -1
trade.actual_pnl_pct = sign * (exit_price - trade.actual_fill_price) / trade.actual_fill_price
trade.actual_pnl_usd = trade.actual_pnl_pct * trade.position_size_usd - trade.commission
if trade.stop_loss:
risk_pct = abs(trade.actual_fill_price - trade.stop_loss) / trade.actual_fill_price
trade.actual_R = trade.actual_pnl_pct / risk_pct if risk_pct else 0
trade.holding_days = (trade.exit_time - trade.entry_time).total_seconds() / 86400
path.write_text(self._render_markdown(trade), encoding="utf-8")
return path
# ---------- read / aggregate ----------
def to_dataframe(self) -> pd.DataFrame:
rows = []
for p in self.base.rglob("*.md"):
if "summary" in p.name or "attribution" in p.name:
continue
try:
t = self._parse_markdown(p)
rows.append(asdict(t))
except Exception as e:
print(f"skip {p.name}: {e}")
return pd.DataFrame(rows)
def weekly_summary(self, year: int, week: int) -> str:
df = self.to_dataframe()
df['entry_week'] = pd.to_datetime(df['entry_time']).dt.isocalendar().week
df['entry_year'] = pd.to_datetime(df['entry_time']).dt.year
wk = df[(df['entry_year'] == year) & (df['entry_week'] == week)]
closed = wk[wk['exit_time'].notna()]
wins = closed[closed['actual_pnl_usd'] > 0]
losses = closed[closed['actual_pnl_usd'] < 0]
return f"""# Week {week} Summary ({year})
- Trades: {len(wk)} ({len(closed)} closed, {len(wk)-len(closed)} open)
- Win rate: {len(wins)}/{len(closed)} = {len(wins)/max(len(closed),1):.0%}
- Avg win: {wins['actual_pnl_pct'].mean():.2%}
- Avg loss: {losses['actual_pnl_pct'].mean():.2%}
- Total P&L: ${closed['actual_pnl_usd'].sum():.2f}
- Avg slippage: {closed['slippage_bps'].mean():.1f} bps
- Bias tags hit: {sum(len(b) for b in closed['bias_tags'])}
"""
# ---------- helpers (markdown <-> dataclass) ----------
def _render_markdown(self, trade: Trade) -> str:
# 略:把 Trade 渲染成上面 §5 的 markdown 模板
return f"# Trade {trade.trade_id}: {trade.symbol} {trade.strategy.value}\n\n" \
f"```json\n{json.dumps(asdict(trade), default=str, indent=2)}\n```\n"
def _parse_markdown(self, path: pathlib.Path) -> Trade:
# 略:从 markdown 解析回 Trade(简单粗暴:用嵌入的 JSON)
text = path.read_text(encoding="utf-8")
start = text.find("```json") + 7
end = text.find("```", start)
data = json.loads(text[start:end])
# 反序列化枚举略
return Trade(**{k: v for k, v in data.items() if k in Trade.__dataclass_fields__})
def _find(self, trade_id: str) -> pathlib.Path:
for p in self.base.rglob(f"*_{trade_id}.md"):
return p
raise FileNotFoundError(trade_id)
生产化 todo(不今天做):
- IBKR FlexQuery 拉每日 trade tape → 自动 reconcile 到 trade log
- 用 Claude API 读 lessons_learned 字段,月底自动生成「重复出现的教训」聚类
- pre-commit hook:A 段 thesis < 30 字直接拒绝 commit
十、PM 视角:交易日志 = Decision Journal
Annie Duke 在 Thinking in Bets 里讲过一个核心概念:结果质量 ≠ 决策质量。
- 好决策 + 坏运气 = 亏钱(但下次还该这么做)
- 坏决策 + 好运气 = 赚钱(但下次会爆炸)
Decision Journal 的作用是把「决策质量」和「结果质量」分开评估。
10.1 把它迁移到 PM 工作
我做过太多次「事后说 X 这功能本来就该砍」的复盘会。如果有 Decision Journal:
## Decision: 上线 X 功能 (2024-08-15)
### Thesis
- 用户调研 N=12 都说想要
- 竞品 A, B 都有
- 预期 +5% retention
### Assumptions
- 用户「说想要」 = 「会用」 — risky
- A/B 的 retention 提升来自这功能 — unverified
### Bet
- 投入 2 工程师月
- Expected: +5% retention 30天后
- Worst case: -2% retention(push notif 扰民)
- Confidence: 0.4
3 个月后回看,可以诚实地说「我的 confidence 0.4 + 上线后没提升 = 模型没问题,是 confidence 该更低」。而不是「我早就觉得 X 不行」。
10.2 通用规则
任何reversible cost > 1 周工作量的决策都该有 Decision Journal:
- 产品 roadmap 排序
- 招聘 senior 决策
- 架构选型(数据库、框架)
- 个人重大决策(offer / 房子 / 学习计划)
格式比内容重要:强制结构化的 thesis + assumption + expected / worst-case + confidence。3 年后你会感谢自己。
十一、Day 27 实际执行 Checklist
- (1) 定 schema:
trade_schema.py落地(直接用本文 §3 代码) - (2) 建目录:
trades/2026/06/+trades/weekly/+trades/monthly/ - (3) 模板:把 §5 的模板存成
trades/_template.md - (4) 写 trade_log.py:先实现 add_trade / close_trade / to_dataframe 三个方法
- (5) 补录 Day 21-26 的 paper 交易:哪怕只是粗略 thesis 也要补,否则前 26 天全 lost
- (6) 跑一遍 weekly_summary:拿到第一个统计输出
- (7) 在 IBKR 上下一笔 paper 单,端到端走完 A → B → C → D
- (8) git commit:
trades/目录纳入版本控制(建独立 repo 或 private) - (9) 设 pre-commit hook:拒绝 thesis 长度 < 30 的文件
- (10) 更新 TR_PROGRESS.md:Day 27 标 ✅
十二、明日预告
Day 28: Phase 1 综合 — 双因子组合(动量 × 低波动率)实盘 paper 部署
- 把 Week 1-4 的工具链 / 因子 / 风控 / 日志合并成一个「最小可运行系统」
- 双因子打分:60% momentum_20d + 40% low_vol_60d
- 10 只持仓,每周 rebalance 一次
- 每笔自动生成 trade log A 段(pre-trade)
- 跑 4 周 paper,下周末做 Phase 1 全阶段复盘
- 这是 Phase 1 的「期末考」
实际执行记录
启动一项填一项,时间戳 + 卡点。
- [hh:mm] trade_schema.py 落地 — ...
- [hh:mm] trades/ 目录 + 模板 — ...
- [hh:mm] trade_log.py add_trade 跑通 — ...
- [hh:mm] 补录 Day 21-26 paper 单(至少 thesis) — ...
- [hh:mm] 第一笔端到端 trade(A→B→C→D) — ...
- [hh:mm] weekly_summary 第一次输出 — ...
- 卡点 / 学到的:
今日核心一句话:你的策略不是写在代码里,是写在日志里的 thesis + 复盘中。代码每年会换,日志才是 3 年后真正让你比别人强的资产。
今日完成度:理论 ✓ / 代码 ✓ / 实操(你自己执行)/ 笔记 ✓