记录一次单腿的确认(2026-08-15)
记录一次单腿的确认(2026-08-15)
数据库里说开着一笔空单,链上却是多单——那一刻要确认的,是这一条腿到底是不是真的。
08-15 例行对账时,发现 hl_testnet.db 里躺着一条很老的 OPEN 记录。对账这件事平时看起来很枯燥,但它偶尔会拽出一笔"幽灵仓"——数据库以为还开着的仓位,链上其实早就不存在了。
一条 25 天的 SOL 空单
最先跳出来的是这一条:
hl_testnet.db有 1 个 OPEN 仓:SOL SHORT @ $77.246,开仓时间 2026-07-22,距今约 25 天。
25 天没动的仓位,本身就不正常。这极可能就是 07-30 那一波"半平仓" 遗留下来的——当时 BTC / ETH / SOL 处理过一轮,通过数据库看,只有 SOL 这一笔没清掉。
DB 和链上,一张一张对
判断它是不是幽灵仓,做法只有一个:把数据库里的每一个 OPEN,和链上的实际仓位逐一对照。
DB OPEN(sm_positions) |
链上真实 | 一致? |
|---|---|---|
| ETH LONG @1891.3, $80 | ETH szi=0.0423, entry=1891.3 | ✅ |
| AVAX SHORT @6.7176, $80 | AVAX szi=-11.91, entry=6.7176 | ✅ |
| SOL LONG @75.2198, $80 | SOL szi=1.06, entry=75.2198 | ✅ |
| BTC LONG @63150.5, $80 | BTC szi=0.00127, entry=63150.5 | ✅ |
sm_positions 这一侧,四条全部吻合。
但另一张表 hl_positions 里那条对不上:
DB OPEN(hl_positions) |
链上真实 | 一致? |
|---|---|---|
| SOL SHORT @77.246,07-22 开 | 链上 SOL 是 LONG,不是 SHORT | ❌ |
这就是幽灵仓:hl_positions 里开着一笔 SOL SHORT,可链上现在是 LONG(来自 sm_positions 的 8-15 入场)。而且 entry price 77.246 和 75.2198 也不一样——根本不是同一个仓。
写 hl_reconcile.py:先只读
事实清楚之后,那天写了 hl_reconcile.py 来做这件事,设计上沿用了 07-31 的模板,外加 Hummingbot 的 processed_coins 模式。几个关键的取舍:
- DB vs 链上 diff,两个数据库都覆盖
- 30 秒传播延迟窗口:OPEN 仓
opened_at距今 < 30s 的不当幽灵仓(这是 07-31 的教训) - 默认只读,
--apply才写库 - **幽灵仓 →
status='CLOSED'**,按 mid 现价结算 + 返还size_usd(沿用 07-31 公式) - 链上有、DB 无 → 只告警,不自动开仓(开仓是策略的活)
--loop --interval 60每 60s 一轮,交给 watchdog 拉起
"默认只读"这一条是刻意的:一个会写库的对账脚本,第一次跑必须是 dry-run。
dry-run 里被自己骗了一次
第一次 dry-run,报告说"幽灵仓:0"。这和事实完全相反。
查下去,问题出在判断逻辑上:我把"DB OPEN 但不在 hl_positions 里"当成了幽灵仓;可 hl_positions 里其实有 SOL——只不过那条被 sm_positions 的 SOL LONG 占着。于是那笔 07-22 的 SOL SHORT,被误判成了"链上有"。
正确的判定,必须用 (symbol + side) 组合,而不是只看 symbol。改完之后又顺手修了两处:函数签名漏传 side_normalizer;以及 orphan(链上有、DB 无)逻辑——它本来就是单 DB 视角,跨 DB 的仓在单库看是孤儿、跨库看是正常,这不是 bug。
报告出来:1 个幽灵,3 个孤儿
修对之后,报告对了:
- 幽灵仓 1 个:
hl_testnet.dbid=231,SOL SHORT @77.246,开了 24.3 天,链上对应方向是 LONG(被sm_positions的 SOL LONG 占着)。正是 07-30 那波遗留。 - 孤儿 3 个:BTC / ETH / AVAX 链上有、但
hl_testnet.db没记录——正常,因为这 3 个仓在hl_smart_money.db里记账。 hl_smart_money.db干净:4 个 OPEN 全部在链上对应方向有仓。
脚本的退出码 2 表示"有 ghost 且未 apply"——这是设计好的行为。
拍板与核验
那天把三项裁决一次性定了:
| 项 | 决定 | 结果 |
|---|---|---|
| ① 幽灵仓 id=231 | 批准 --apply |
CLOSED @ 75.5115,pnl=+$9.89,回写 +$450.36,current_usdc 881.48→1331.84 |
| ② 孤儿 3 个 | 只告警不修 | BTC / ETH / AVAX 全部命中 sm_positions(86/79/84),跨 DB 正常 |
| ③ loop 模式 | 本轮不进 WATCHED | 未改 watchdog,未重启任何进程 |
执行完做了独立核验:
- 账户等式
cur + open_notional − realized恒为 1361.93(不变)——不破坏不变量、未改现金趋势。 - 实测
pnl_pct=+2.22%($9.89)。我原本担心"PnL 抽走现金",但在 SOL 现价 75.5 贴近 entry 77.2 的情况下,这个担心不成立。 hl_smart_money.db未触碰,生产进程零影响。
最后
那天那条"单腿"确认下来,收获得分三层。
最表面的是修掉了一个幽灵仓——id=231 从 OPEN 变成 CLOSED,账本回到自洽。
中间一层是那条判定规则:对账要用 (symbol + side),不能只用 symbol。 因为两个数据库共享同一个 HL testnet 钱包,跨库的仓位会在单库里表现为"方向 / 价格对不上的腿"——只看 symbol,就会把一条真实的腿判成存在,把一条幽灵判成正常。
最里面一层,是对"运气"的警惕。 那次 --apply 之所以安全,纯属 SOL 现价贴近 entry 的运气——如果底层价格已经大幅偏离开仓价,用 mid 现价结算就会扭曲现金。所以那天把一条注意点钉进了长期记忆:**--apply 之前,必须先验 mid ≈ entry。** 一条脚本能不能长期用,不取决于它跑通的那一次,而取决于它跑的那个价格不巧的时候,会怎样。