接上回:观潮淬火记——82 条信号回填后,14 行代码砍掉 0% 胜率的死亡地带。
那篇讲的是用数据磨刃。这篇讲的是:刀刃底下藏着两只鬼。
和而泰的孤独
晚上 8 点打开 Dashboard,持仓区一眼就不对。
三只持仓——和而泰、威海广泰、赛力斯——本该并排站着的三张卡片,只剩了一张。和而泰孤零零地立在那里,浮盈 -2.3%,绿色的数字在深色背景上格外刺眼。
自选股和热门板块倒是正常。只有持仓区,两只股票凭空消失了。
第一反应是数据库出了问题。SSH 上去一查——三只都在。数据没丢。
那问题出在哪?
5/32
答案在日志里。
baostock——观潮用来拉 A 股 K 线的数据源——在下午 3 点,A 股收盘的那一分钟,崩了。不是偶尔丢一两只,而是连锁式地全线断连。后台定时扫描正好在跑,32 只股票只有 5 只拿到了数据。
然后这份残缺的数据被写入缓存,持久化到磁盘。
从下午 3 点到我晚上打开 Dashboard,这份 5/32 的扫描结果一直被当成真货端给前端。和而泰恰好是那 5 只幸存者之一,所以它还在。威海广泰和赛力斯没那么幸运。
翻代码一看,扫描失败时的处理方式是——pass。静默吞掉异常,接着扫下一只。讽刺的是,另一个兄弟函数里已经有级联失败保护了:连续 3 只失败就自动重连。但定时扫描是后来写的,没移植这套逻辑。
同一个仓库里,一个函数有护栏,旁边另一个没有。防线的薄弱环节不在最危险的地方,在最容易被忽视的地方。
三堵墙
Polly 先修了前两层:连续失败自动重连,扫完再检查关键持仓是否到齐,漏了就单独补。
小龙虾 Review 时补了第三刀:
"前两层都在修'怎么扫得更全'。但如果 baostock 真的宕了两小时呢?重连也救不回来。这时候你宁可端旧数据,也不该端残缺数据。"
说得对。加一个成功率门槛——不到 50%?这轮扫描结果直接扔掉,继续用上一轮的旧缓存。旧数据可能是半小时前的,但至少是完整的。
三层防护各管各的,互不依赖:
| 防护层 | 逻辑 | 应对场景 |
|---|---|---|
| 级联失败检测 | 连续 3 只失败 → 强制重连 | 瞬时断连 |
| 末尾重试 | 持仓/自选缺失 → 补扫 | 部分漏扫 |
| 残缺缓存保护 | 成功率 <50% → 拒绝覆盖 | 数据源彻底宕机 |
小龙虾打了 9/10。扣的 1 分:"0.5 这个阈值写死了,股票池缩小时可能误触发。"加了个 TODO。合理。
重启服务:32/32 全部命中。 三只持仓整整齐齐回到 Dashboard。
第一只鬼——消失的持仓——捉住了。
176776
bug 修完,照例翻一遍日志确认没有副作用。然后三行诡异的记录跳了出来:
[09:00:31] 📊 开始分析: 176776
[09:00:32] 📊 开始分析: 364107
[09:00:33] 📊 开始分析: 558017
三个 6 位纯数字,格式上完全合法——A 股代码就是 6 位数字。但沪市 60 开头、深市 00/30 开头,17、36、55 打头的代码根本不存在。
baostock 温柔地拒绝了它们——查无此股,返回空数据,程序跳过。没有报错,没有脏数据入库。单看结果,完全无害。
但它们每天都在跑。北京时间 17:00,准时出现。那是 cron 定时任务触发的时间。
微秒碎片
cron 脚本里,取持仓列表的逻辑是一行正则:re.findall(r"\b(\d{6})\b", text)——在整个 JSON 响应里盲扫所有 6 位数字。
持仓数据长这样:
{"symbol": "002402", "added_at": "2026-03-13T09:29:43.176776"}
{"symbol": "601349", "added_at": "2026-03-18T14:22:51.364107"}
{"symbol": "002111", "added_at": "2026-03-25T16:40:09.558017"}
看到了吗?
176776、364107、558017——不是股票代码,是 added_at 时间戳里的微秒精度后缀。正则不区分语义,只认位数。6 位数字就匹配,管你是股票代码还是时间碎片。
每只持仓的入库时间都贡献了一个幽灵代码。三只持仓,三只鬼。
它们从 cron 脚本上线第一天就开始跑,至今每天准时报到。因为 baostock 对无效代码只是静默返回空——不报错、不入库——所以从没有人注意到。每天多跑 3 次无意义的 analyze_stock,浪费大约 15 秒,仅此而已。
安静、合规、无害。一只完美的幽灵。
创可贴和水管
Polly 先在 analyze_stock 入口加了格式校验——A 股必须 6 位纯数字,美股必须 1-5 位字母。幽灵代码到门口就会被拦回去。
小龙虾看完给了 8 分:
"你挡住了。但这只是下游防御。上游那个 regex 才是 root cause——等于给漏水管贴了创可贴。"
真正该修的是那行正则。把盲扫换成 JSON 解析,从结构化数据里精准提取 symbol 字段。不管时间戳精度是微秒还是纳秒,不管 JSON 里将来加什么数字字段,都不会再匹配到幽灵。
上下游都修完,9 分收了。
第二只鬼——无形的幽灵——也捉住了。
看得见的和看不见的
第一只鬼让人紧张。打开 Dashboard 发现持仓少了,第一反应是"数据库被清了"。但查下来只是缓存被残缺数据覆盖,三层防护搞定。
第二只鬼更值得回味。它不造成任何可见后果——不报错、不留脏数据、不影响任何功能。它只是安静地每天多跑三次无意义的分析。如果不是修第一只 bug 时顺手翻了整段日志,可能永远不会发现它。
可见的 bug 让人紧张,但不可见的幽灵才值得警惕。 它合规,它安静,它无害——直到有人在日志的河流里,注意到了那个不该出现在这里的数字。
观潮系列
| 日期 | 标题 |
|---|---|
| 03-11 | 观潮诞生记 |
| 03-12 | 观潮进化记 |
| 03-13 | 观潮实战记 |
| 03-19 | 观潮验证记 |
| 03-20 | 观潮出海记 |
| 03-21 | 观潮锐评记 |
| 03-23 | 观潮自省记 |
| 03-29 | 观潮托管记 |
| 03-30 | 观潮淬火记 |
| 04-02 | 观潮捉鬼记 ← 本篇 |

