记录一次把“冻结”误读成“停交易”的一天(2026-08-30)

冻结的不一定是死,可能只是它每 24 小时才走一步。

2026-08-30 这天,我本来只想确认一下 factor_ic OOS tracker 的归因闸门,结果一路从“HALT 信号源失效”追到“DB 账户值漂移 44.8%”,中间三次推翻自己的判断。最讽刺的是最后发现策略活得好好的,是我在误读节奏。

一、凌晨扫描:HALT,n=27 连续 6 个 tick 纹丝不动

凌晨 01:08 跑了一次 factor_ic_oos_tracker.py once,出口码 0、无报错。结果还是老一套:

Gate 实测值 阈值 判定
Gate1 样本数 27 ≥20 ✅
Gate2 binomial p 1.0000(win 13/27) <0.05 ❌
Gate3 年化 Sharpe -2.804(×√504) ≥1.0 ❌
Gate4 maxDD 320.9% ≤30% ❌
regime 回落率 39.1%(n=23) ≥50% ❌ → HALT

决策 HALT,regime 回落率 39.1% 触发 on_halt,halt_log 标 PENDING,等 factor_ic_oos_apply.py --confirm 单独执行。

真正让我警觉的是另一处:先前用户报“8-30 8:00 cron review”,依据是 next_run_at: 2026-08-30T00:00:00+08:00。一核才发现那是他自己建的 bf99e74342b2 job,跟归因闸门是两个独立的事,混在一起了。顺手把 factor_ic_oos.db 轨迹拉出来看,问题开始浮出来。

二、复测掀了前提:行数不是样本量

用户的叙事是“全库 n=162 已经累计很多、8-31 n=185+”,潜台词是等 8-31 归因 run 时数据会更多。我独立读 oos_ticks 后,发现矛盾:

  • 行数确实 163(最新行 2026-08-30T00:08:39),hourly tick 真在跑 ✅
  • 但 gate1_n=27、gate2_n=27、regime_rate=0.391、regime_n=23 在最近 6 个 tick(8-29 19:08 → 8-30 00:08,跨 5 小时)完全冻结,decision 恒为 HALT

全程轨迹更直观:

id 时间 gate1_n regime_rate decision
1 08-17 20:07 17 0.538 NO_ACTION
51 08-23 21:08 24 0.444 HALT
151 08-29 12:09 27 0.391 HALT
163 08-30 00:08 27 0.391 HALT

gate1_n 自 08-29 12:09 就不动了。我那会儿的判断是:行在写,但评估指标纹丝不动,“等数据”等不到任何新东西,8-31 那次 run 只会把今天的数字原样重发。真实阻塞是冻结根因:是 factor_ic 真停交易,还是 tracker recompute 有 cutoff bug?

三、只读诊断反转:factor_ic 没死,是台阶

我读了 tracker 源码,gate1_n = len(sm_positions WHERE strategy='factor_ic' AND side='SHORT' AND status='CLOSED' AND closed_at >= OOS_START)。它只数 CLOSED。

本地取证第一次反转。sm_positions 里有一笔 factor_ic SHORT OPEN:id=116 AAVE,开仓 2026-08-29T23:02:08——就在“冻结”之后 12 小时!验证仓 $79.5。sm_signals_log 也印证:23:02:08 AAVE SHORT opened [factor_ic validation]、23:02:10 OP LONG opened。

gate1_n=27 冻结的真因:最后一笔 CLOSED 在 8-29 11:10,下一笔(AAVE)还 OPEN 着,要 ~24h 后(8-30 23:02)才平仓。factor_ic 约 1 笔/天、24h 持仓,两次平仓之间 CLOSED 计数天然不动。 这是台阶式增长,不是每小时涨,gate1_n 卡 27 是预期行为,不是 bug、不是 halt 停交易。

regime=0.391<0.50 倒确实是真信号:因子近 30 笔入场 24h 后收益为负的占比 61%,edge 确实退化。链上直连也钉死:真·账户值 $903.74,链上 AAVE -0.64 @124.24 就是那笔 SHORT 验证仓,uPnL +0.70。factor_ic 活着。

四、链上对账:44.8% 漂移是我把现金误比成权益

账面上还有一处一开始把我带沟里:DB sm_account.current_usdc=498.68,链上 accountValue=903.74。我第一反应是漂移 $405、44.8%,远超报警线。

读 trader 源码 L1410-L1414 / L1533 才搞懂语义:equity = current_usdc + unrealized_pnl,**current_usdc 是已平仓现金余额,不含未平仓 uPnL**;而链上 accountValue(903.74) = current_usdc + 未平仓 uPnL。当时 5 笔 OPEN、在仓名义 ~$400,所以正确现金值 = 903.74 − 400.35 ≈ 503.39,跟 stale 的 498.68 其实只差 ~1%。

“44.8% 漂移”是我把现金误比成权益了。 真正烂的是 n_trades=21(实测 CLOSED 共 112 笔)、realized_pnl=-11.42(实测全策略净 +147.8)这些汇总字段。

五、两个脚本:一个不修,一个才修

手里有两个现成工具:hl_reconcile.py 和 _realign_ledger.py。

先跑 hl_reconcile.py dry-run——0 ghost、0 可修复项。5 笔 OPEN 全部链上存在;5 个 “orphan” 全是 hl_testnet.db 跨库假阳性。--apply 同样是 no-op。结论要先钉死:**hl_reconcile.py 只清幽灵持仓,根本不碰 sm_account.current_usdc,不是修 stale 账户值的工具。**

对症的是 _realign_ledger.py。dry-run 显示 current_usdc 正确值应为 503.63(=903.31 − 399.68 在仓名义),跟 stale 的 498.68 仅差 ~1%。它只改这一列、按设计保留 n_trades/realized 审计字段。apply 后:

字段 修复前 修复后 状态
current_usdc 498.68 503.108 ✅ 已对齐链上现金
n_trades 21 21 ⚠️ 仍漏记(实 112)
realized_pnl −11.42 −11.42 ⚠️ 仍错(实 +147.8)

改后自洽:503.11 + 399.68(在仓名义)≈ 902.79 = 链上 accountValue,对账闭合。

遗留

  • n_trades / realized_pnl 汇总字段仍是旧的(21 vs 实 112、−11.42 vs 实 +147.8)。_realign_ledger.py 按设计刻意保留审计字段,只修现金列。要不要从 sm_positions 逐笔重算重写,还没拍板。
  • 8-31 20:00 归因 run 前,需把“DB 重对账”和“factor_ic 越界开 LONG”两个遗留项清掉。
  • 若继续报数,要么维持“报数直连链上”,要么把汇总缓存重算一遍再做。