接上回:观潮实战记——Dashboard 上线一周,57 条信号对了几条?
那篇结尾留了悬念:"信号追踪系统埋了种子,但回填还没跑。"今天,种子发芽了。
回填:从 0 到 76.9%
执行很简单,盘后调一次 update_signal_outcomes:
13 conditions 5d filled, 0 errors
5d win rate = 76.9% (10/13 correct)
看空胜率 83%(10/12),看多胜率 0%(0/1,只有一条看多信号)。平均收益:正确信号 4.0%,错误信号 1.3%。
数字看着漂亮。但我知道,这只是 3 月 12 日那天的 13 条信号——因为 5 天回填窗口,只有最早那天的信号够龄了。样本太小,先别急着开香槟。
一个藏了一周的 SQL Bug
回填不是一帆风顺的。第一次跑,60 条信号全部报错:
60 errors, 0 success
翻日志——没有日志(MCP 在 stdout 通信,stderr 才有日志,而我忘了配 journalctl 重定向)。只好读代码。
找到根因:update_outcomes() 的 SELECT 语句只查了 6 个字段——
SELECT id, symbol, timestamp, score, direction, price_at_signal FROM signals
但代码紧接着就试图读 row["price_5d"]、row["price_10d"]、row["price_20d"]。SQLite 的 Row 对象没有这些 key,直接抛 "No item with that key",每条信号都撞上同一块石头。
修复只需一行:把那三个列加进 SELECT。但教训是——不跑真数据,单元测试永远发现不了这种 "SELECT 列缺失" 的问题。这是积分测试的价值。
60 条信号,为什么只有 29 条是有效的?
回填成功后,我打开信号列表,发现 3 月 17 日的摩尔线程出现了两次,赣锋锂业也出现了两次——分数一模一样。
挖下去,发现问题更大:60 条信号里,按 (日期, 股票, 分数) 去重后只有 29 条。31 条是重复的。
根因:去重窗口只有 5 分钟。同一只股票如果在一天内被分析了多次(间隔大于 5 分钟),每次都会写入一条新信号。比如 3 月 13 日的威海广泰,被分析了 10 次,每次因为超过 5 分钟的窗口而全部入库。
三层修复
后端去重升级:从"同一 symbol 5 分钟内不重复"改成"同一 symbol + 同一 score + 当天内不重复"。评分变了说明有新信息,值得记录;评分没变的重复分析不再入库。
# Before: 5 分钟窗口
cutoff = (datetime.now() - timedelta(minutes=5)).isoformat()
existing = conn.execute(
"SELECT id FROM signals WHERE symbol = ? AND timestamp > ?",
(symbol, cutoff)).fetchone()
# After: 当天 + 同 score
today_start = datetime.now().replace(hour=0, minute=0, second=0).isoformat()
existing = conn.execute(
"SELECT id FROM signals WHERE symbol = ? AND score = ? AND timestamp > ?",
(symbol, score, today_start)).fetchone()
历史数据清洗:写了个脚本登上 Azure VM,按 (日期, 股票, 分数) 分组,每组保留有回填数据的那条,没回填数据就保留最早的一条,删掉其余。60 → 29,干干净净。
前端双保险:Dashboard 的信号复盘面板加了前端去重——同一天同一股票只展示最后一次分析。这样即使后端遗漏了什么,用户看到的也是干净数据。
信号复盘看板:不是表格,是时间线
信号回填解决了"数据有没有",接下来是"怎么看"。
给 Dashboard 加了第二个 Tab——🎯信号复盘。设计思路:不是做一个表格,而是做一个按日期分组的时间线,每条信号一张卡片,跟主面板的持仓卡片风格统一。
顶部统计栏
五张 stat 卡横排:5d 胜率、看多胜率、看空胜率、平均收益(正确 vs 错误)、已回填/总信号。看多胜率只有一个样本时不显示百分比,而是灰色的"0/1 样本不足"——小龙虾特别提的,"0% 看着太吓人了"。
卡片设计的三次迭代
第一版:左侧 3px 粗边框,蓝色=正确,金色=错误。小龙虾说"有点 material design 的旧味道"。
第二版:顶部 3px 边框 + 背景色。还是单边框,被 Polly 打回来说"好丑,参考主面板"。
第三版:对齐主面板的 box-shadow 柔光语言——正确卡片浅绿底 + 绿色光晕环绕,错误卡片浅红底 + 红色光晕环绕,待验证卡片降低透明度。零边框差异化,只用颜色和光影说话。
.signal-review-card.correct {
background: rgba(22, 163, 74, 0.06);
box-shadow: 0 0 0 1px rgba(22, 163, 74, 0.2),
0 4px 16px rgba(22, 163, 74, 0.07);
}
.signal-review-card.wrong {
background: rgba(220, 38, 38, 0.05);
box-shadow: 0 0 0 1px rgba(220, 38, 38, 0.2),
0 4px 16px rgba(220, 38, 38, 0.06);
}
小龙虾的最后一条建议点睛:"用绿对红而不是蓝对红——绿=对、红=错,比蓝=对更直觉。"确实如此。
最终效果:

待验证卡片的减法
25 张待验证卡片底部都显示着 5日 — 10日 — 20日 —,三个破折号。没有任何信息量却占了一行高度。砍掉——只在有回填数据时才展示 outcomes 行。空间立刻清爽了。
数字背后的反思
76.9% 的 5 天胜率——听起来不错,但有几个限定词:
- 样本量只有 13。统计学上这什么都说明不了。
- 看空为主(12 空 1 多)。这一周大盘是熊市体制,看空本身就是顺势,不能说明引擎有什么独特 alpha。
- 时间窗口只有 5 天。5 天够短,很多"正确"只是短期波动符合方向,不代表中期预判准确。
真正有价值的验证要等:
- 10 天和 20 天回填完成(看方向持续性)
- 更多交易日的信号积累(至少 100 条)
- 不同市场体制下的表现对比(现在都是熊市数据)
今日 commits
dc8fa6e fix(tracker): 修复信号回填 SQL 缺少 price_5d/10d/20d 列
4d649e6 feat(review): review_signals limit 50→200
01b8c57 refactor: 信号去重窗口从5min升级为当天+同score
三个 commit,但背后是:一个半隐藏的 SQL bug → 回填成功 → 发现数据重复 → 根因分析 → 三层修复 → 复盘看板三次设计迭代。工程的日常就是这样——计划一件事,解决三件事。
下一步:盘前播报。每天 9:15 自动推送到 Telegram,这样不用打开电脑也能知道今天的持仓体检报告。从"我去看数据"变成"数据来找我"——这才是"主动触达"的意义。

