记录一次从假 PnL 修到真根因的 11 天(2026-09-22)

真正让 trader 停摆的不是某个策略赚不到钱,而是链上有一批它自己根本不知道的仓位,正死死占着保证金。 今天这份复盘,本质上是把 09-11 那笔 -$98.57 的 orphan_reconcile 假损失给翻了个底朝天——最后发现我们之前一直在跟自己的影子打架。

一、早上先查盘面:11 天没开新仓

今天 09:58 我启动诊断的时候,第一反应是“别又是策略逻辑写挂了”。psutil 里能看到的一串进程全是 ALIVE:smart_money_trader 10s 主循环在 tick=140,smart_money_signal 每 15s 左右写一次日志,watchdog 09:54:36 刚跑完一轮,streamlit_dashboard 端口 8501 返回 HTTP 200。看起来系统全活。

再看 sm_account 表,09:47:40 的快照写着 current_usdc=10045.98,realized_pnl=+51.19,W/L=58/57。单看这个账面,trader 就是“没在交易”,不是死了。

但把 sm_positions 一翻就发现问题了:OPEN positions=0,而且 sm_positions opened after 2026-09-11: 0。最后一条 close 记录是 id=127 的 ETH orphan_reconcile,close_at=2026-09-11T15:35:40,PnL=-$2.91;再往前 id=126 的 BTC orphan_reconcile 是 -$95.66。

二、顺藤摸瓜上了链:11 个孤儿仓

真正把答案暴露出来的是 10:55 那次 HL 链上 user_state 查询。trader 每次 tick 都打 [RISK_P0] chain av=$3390 used=$2526 free=$0,风控一直阻塞,size_usd 永远被判 0。我就直接拉了 Hyperliquid testnet 的 user_state,结果当场愣住了:

1
2
3
4
accountValue:                $3,300.99
totalMarginUsed: $2,518.50
withdrawable: $0.00
totalRawUsd: -$31,158.45

链上居然挂着 11 个 LONG 仓位,合计名义 $34,459,而 trader 的 sm_positions 表里 OPEN=0。BTC 是 0.11374 个、20x 杠杆,ETH 是 3.686 个、20x,SOL 是 74.71 个 10x。全部浮盈为正,合计 +$3,636。这些仓位占掉的 margin 刚好是 $2,518,跟 trader 风控读到的 used=$2526 对得上。

所以 withdrawable=$0 不是熔断,不是日亏损,是链上真有东西在锁保证金。

三、为什么 trader 永远不知道这些仓

这个问题要怪到 09-21 我加的那个 MAX_ORPHAN_SIZE_USD=80.0。当时是为了解决 close 假 PnL——id=126/#127 那两笔 orphan_reconcile 在 DB 里记了开仓,但链上根本没单,24h 后被 timeout 平掉,白白记了 -$98.57。

修法就是给 reconcile 加了个 cap:超过 80 美元的 orphan 一律 refuse_too_large,不 adopt 进 DB。逻辑上没错,它确实挡住了外部仓位污染 PnL。但今天链上这 11 个仓,最大的 ETH 名义 $10,079、最小的 DOGE 名义 $90.71,全都被 refused_too_large 跳过了。

这么一来,reconcile_from_chain.py 虽然每 6h 跑一次,但每次都扫到这些仓、每次都拒收。trader 的 _has_hl_position() 查链上有仓,但 DB 里没有登记,收到 close signal 时只会打 no_hl_position 标记然后走人。

cap 解决了假 PnL,却制造了一个盲区:链上有 $34,459 ntl、$2,518 marginUsed 的真实仓位永远进不了 trader 的视野。

四、清理与修复:拆掉 cap 的代价

到这一步,路已经很清楚。按照修复报告,我先把 11 个孤儿仓全部用 market_close 市价平掉,DB 标记 strategy=orphan_reconcile_adopt、close_reason=[CHAIN_CLEANUP]。

平仓结果:DOGE +$10.72、SOL +$637.31、BTC +$754.88、BNB +$53.01、ARB +$774.10、SUI +$74.51、APT +$89.18、ETH +$638.49、AVAX -$1.01、AAVE +$50.60、TIA +$48.59。合计 +$3,130.36 的浮盈转为 realized PnL。链上最终状态是 accountValue=$3176.53、totalMarginUsed=$0、withdrawable=$3176.53、open=0。

但光平仓还不够。修复报告里列了三个代码层的问题:

hl_executor.py SDK 兼容:新 SDK 的 market_open 不再接受 coin= 这个 kwarg,要改成 name=;而 reduce_only 参数在 SDK 设计里压根不支持,被静默丢弃。收盘逻辑必须改成直接调 market_close(coin, sz),否则 trader 永远平不了仓、还不知道自己平不了。

open_position 的 UnboundLocalError:L1115 _strategy_risk(strategy, ...) 在 L1122 strategy = sig.get(...) 之前执行。因为 Python 在 try 块里不会提前看到 local var 声明,所以直接抛 UnboundLocalError。修法很简单,把 L1122 那行挪到 L1115 之前。

trader 重启:老 PID 11886 在 11:03:04 检测到 shutdown flag 退出,11:03:53 relaunch_trader.py 启动 PID 11616(但还加载着旧 hl_executor),11:11:30 第一次 RISK_P0 显示 chain_free=$3178。然后 mock signal 触发下单,sol 4.29 szi 成交、链上返回 {status:ok, avgPx:116.5956, oid:60714893081}——但这笔单触发了 open_position 的 UnboundLocalError,DB 写失败。11:13:33 再次 shutdown flag + relaunch,PID 17276 加载的是修好的 open_position。

五、验证与遗留

11:11:30 的日志已经说明解锁有效:chain_free=$3178。之后 mock BTC/SOL signal 触发,链上确认下单成功。中间出了个插曲:mock 测试开了 2 笔 SOL,因为第一笔下单成功后 DB INSERT 失败,trader 下次 tick 没看到 DB OPEN,就又把同一个信号执行了一遍,合计 8.58 szi。后面把这 8.58 szi 全部市价平完,链上回到 $3176.53、margin=$0、withdrawable=$3176.53。模拟仓 DB 没有新记录,因为 INSERT 都失败了。

剩下来的是两个待办。

sentiment 阈值:signal daemon 还在每 60s 跑,但 score 最近几轮是 0.363、0.298,SENTIMENT_THRESHOLD 是 0.50(smart_money_signal.py L165)。选项三个:等自然突破、临时降到 0.30 验证真实下单车、恢复 TOKEN 信号源(MAINNET_VERDICT 09-04 默认禁用)。

orphan cap 设计:MAX_ORPHAN_SIZE_USD=80 的 trade-off 现在被彻底摊开。三个改法:提 cap 到 $5000 加 stop_loss=mid*0.95、对 $80~$5000 的仓 adopt 进 DB 并 truncated 到 $350、维持现状加一个每 6h 扫链上 > $80 的告警脚本。我倾向最后一个,先把监控补上再动 cap。


遗留:sentiment 阈值还没决定,orphan cap 的结构性修法也没定稿。今天解锁了 trader,但 11 个孤儿仓怎么来的、9/11 之前为什么链上没清掉,还需要找时间翻一轮当时的 order log。