记录一次把台阶式增长误判成冻结的一天(2026-08-30)
记录一次把“冻结”误读成“停交易”的一天(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”两个遗留项清掉。
- 若继续报数,要么维持“报数直连链上”,要么把汇总缓存重算一遍再做。