Files
Chan/research/live/verify_lean_parity.py
T
jackandCursor 6c843639d5 影子路径开 lean 模式,实测完整链路 1410 → 683ms
拉到新引擎后在服务器侧直接实测,没有沿用 3.4 倍那个换算——那是在旧代码上量的。

先做等价性验证。不能直接引用 step46 的对拍结论:它固化的是 bsp_list 那条链的
哈希,而影子路径走 find_fast_bsp3 + build_htf_zones + htf_fx_timeline +
attach_htf_context,两条链读的东西不一样。所以新建 verify_lean_parity.py 在
这条路径上逐根对拍 compute() 的每个返回字段。

其中一个坑:随机取窗口测不到信号分支。信号密度约 1/2000 根,头 12 个窗口命中
0 个,「一致」只覆盖了早退路径。改成一半窗口对齐到已知信号根,命中率才上来。
最终 180 窗口 / 两模式各 90 命中 / 零分歧。

生产实测(inner_ms,容器内同口径):

  compute_ms      646 → 132ms   4.89x
  inner_ms        612 → 128ms   4.80x
  lag_signal_ms  1410 → 683ms   完整链路,落回 800ms 线内

比 3.4 倍更好,因为是引擎 ~3.5x 叠 lean ~1.35x。

两点判读上的订正:

- lag_data_ms 那 576→490ms 是噪声,不要记在引擎账上。均值 721±36 vs
  740±127,重叠;而且引擎本来就影响不到交易所与网络那一段。
- 仍有 44% 的根超 800ms,但尾部现在完全由数据腿主导(lag_data P90 1482ms
  vs compute P90 237ms)。计算既不是瓶颈也不是尾部主因了,继续压计算换不到
  尾部改善。800ms 那道闸取的是最近 30 根的中位数,683ms 已满足。

start.sh 加 SHADOW_LEAN(默认 1),设 0 可退回 full 复量两模式差异。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 04:24:08 +08:00

201 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""在影子信号路径上实测 lean 与 full 是否等价,并量提速。
## 为什么不能直接引用 step46 的对拍结论
step46 固化的是 klc/笔/中枢/`bsp_list`/被消费列的哈希,走的是 `bsp_list` 那条
链。影子路径走的是另一条:`find_fast_bsp3` + `build_htf_zones` +
`htf_fx_timeline` + `attach_htf_context`。两条链读的东西不完全一样,所以
「lean ≡ full」在 step46 用例上成立,不等于在这条路径上成立。
静态检查显示这条链只读 `chan.dataframe` 与 `chan.klc_list`lean 都不跳),
但静态检查漏不掉间接依赖——`build_htf_zones` 收的是 chan 对象本体。所以逐根
实测:同一个窗口分别用 full 和 lean 跑 `compute()`,比对返回的每一个字段。
判据是**逐字段完全相同**,不是「信号数量相同」。数量相同而方向或标志不同,
会让影子测的是另一批信号,且不报错。
python research/live/verify_lean_parity.py --syms BTC,ETH,SOL --n 150
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
import warnings
from pathlib import Path
import numpy as np
import pandas as pd
warnings.filterwarnings("ignore")
for _v in ("OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS"):
os.environ.setdefault(_v, "1")
HERE = Path(__file__).resolve()
sys.path.insert(0, str(HERE.parents[1]))
sys.path.insert(0, str(HERE.parents[2]))
sys.path.insert(0, str(HERE.parent))
LTF_BARS, HTF_BARS = 2001, 801 # 与 shadow_hb 同窗口
SKIP = ("inner_ms", "queue_ms") # 计时字段本就不同,不参与比对
def load(sym: str, tf: str, cache: Path) -> pd.DataFrame:
c = sorted(cache.glob(f"bitget_{sym}_{tf}_*.feather"),
key=lambda p: p.stat().st_size, reverse=True)
if not c:
raise FileNotFoundError(f"没有 {sym} {tf} 缓存")
return pd.read_feather(c[0])
def canon(d: dict) -> str:
"""把返回值规范化成可比较的字符串。
浮点直接比会被末位差异误判,但 lean 走的是同一段算术、不该有任何差异,
所以这里**不设容差**:round 到 12 位只是为了消掉 repr 差异,真有数值
分歧一定会被抓到。
"""
def norm(v):
if isinstance(v, float):
return None if not np.isfinite(v) else round(v, 12)
if isinstance(v, dict):
return {k: norm(x) for k, x in sorted(v.items())}
if isinstance(v, (list, tuple)):
return [norm(x) for x in v]
if isinstance(v, (np.integer, np.floating, np.bool_)):
return norm(v.item())
return v
return json.dumps({k: norm(v) for k, v in sorted(d.items())
if k not in SKIP}, sort_keys=True, ensure_ascii=False)
def signal_bars(sym: str, cache: Path) -> np.ndarray:
"""全量历史里过三滤网的信号根下标。
随机取窗口几乎测不到信号分支——信号密度约 1/2000 根,12 个窗口命中 0 个。
只测早退路径的「一致」是很弱的证据:lean 若真影响了中枢或笔,分歧恰恰
出现在有信号的那些根上。所以把窗口对齐到这些根。
注意这些下标来自**全量历史**建的中枢,而 compute 只看 2000 根窗口,所以
对齐后未必真的命中(这正是尚未收口的窗口左边界效应)。但命中率会从
1/2000 提到可用水平。
"""
from step43_fill_aware_budget import signals_for
_, sig = signals_for(sym, cache)
return sig["entry_idx"].astype(int).to_numpy()
def run_one(sym: str, cache: Path, n: int, step: int,
at_signals: bool = True) -> dict:
from shadow_signal import NUM_COLS, compute
l_all, h_all = load(sym, "1m", cache), load(sym, "5m", cache)
for df in (l_all, h_all):
for c in NUM_COLS:
if c not in df.columns:
raise RuntimeError(f"{sym} 缺列 {c}")
l_ts = l_all["timestamp"].to_numpy("int64")
h_ts = h_all["timestamp"].to_numpy("int64")
# 一半窗口对齐到已知信号根(测信号分支),一半均匀铺开(测早退路径)
ends: list[int] = []
if at_signals:
try:
sb = signal_bars(sym, cache)
sb = sb[(sb > LTF_BARS) & (sb < len(l_all) - 1)]
ends += list(sb[-(n // 2 or 1):])
print(f" 对齐到信号根 {len(ends)} 个")
except Exception as e:
print(f" 取信号根失败,只用均匀窗口:{e!r}")
ends += list(range(len(l_all) - 1, LTF_BARS, -step))[:max(n - len(ends), 1)]
ends = sorted(set(ends))
if not ends:
raise RuntimeError("数据不足一个窗口")
n_same = n_diff = 0
t_full = t_lean = 0.0
first_diff = None
n_hits_full = n_hits_lean = 0
for e in ends:
df_l = l_all.iloc[e - LTF_BARS + 1:e + 1]
# 5m 只取已收盘且不晚于 1m 窗口末尾的根,与实盘一致
hi = int(np.searchsorted(h_ts, l_ts[e], side="right"))
df_h = h_all.iloc[max(0, hi - HTF_BARS):hi]
# 次根开盘价:窗口末尾的下一根,与实盘 entry_px 同义
entry_px = float(l_all["open"].to_numpy(float)[e + 1]) \
if e + 1 < len(l_all) else None
t0 = time.perf_counter()
rf = compute(df_l.copy(), df_h.copy(), entry_px, lean=False)
t_full += time.perf_counter() - t0
t0 = time.perf_counter()
rl = compute(df_l.copy(), df_h.copy(), entry_px, lean=True)
t_lean += time.perf_counter() - t0
n_hits_full += len(rf.get("hits") or [])
n_hits_lean += len(rl.get("hits") or [])
if canon(rf) == canon(rl):
n_same += 1
else:
n_diff += 1
if first_diff is None:
first_diff = (e, canon(rf), canon(rl))
k = len(ends)
print(f" 窗口 {k} 个 · 完全一致 {n_same} · 不一致 {n_diff}")
print(f" 命中数 full {n_hits_full} / lean {n_hits_lean}")
print(f" 单窗耗时 full {t_full / k * 1000:.0f}ms · "
f"lean {t_lean / k * 1000:.0f}ms · "
f"提速 {t_full / max(t_lean, 1e-9):.2f}x")
if first_diff:
e, a, b = first_diff
print(f" ⚠ 首个分歧在窗口末尾 idx={e}")
print(f" full: {a[:400]}")
print(f" lean: {b[:400]}")
return {"sym": sym, "n": k, "same": n_same, "diff": n_diff,
"full_ms": t_full / k * 1000, "lean_ms": t_lean / k * 1000,
"hits_full": n_hits_full, "hits_lean": n_hits_lean}
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--syms", default="BTC,ETH,SOL")
ap.add_argument("--cache", default="research/live/cache")
ap.add_argument("--n", type=int, default=150, help="每币比对多少个窗口")
ap.add_argument("--step", type=int, default=37,
help="窗口间隔根数。取质数避免与任何周期共振")
ap.add_argument("--no-signals", action="store_true",
help="不对齐信号根(快,但测不到信号分支)")
a = ap.parse_args()
rows = []
for sym in a.syms.split(","):
print(f"\n{'=' * 70}\n{sym}")
try:
rows.append(run_one(sym, Path(a.cache), a.n, a.step,
at_signals=not a.no_signals))
except Exception as e:
print(f" 跳过:{e!r}")
if not rows:
return
d = pd.DataFrame(rows)
print(f"\n\n{'=' * 70}\n汇总\n")
print(f" 比对窗口 {int(d['n'].sum()):,} 个 · "
f"不一致 {int(d['diff'].sum())} 个")
print(f" 单窗耗时 full {d['full_ms'].mean():.0f}ms → "
f"lean {d['lean_ms'].mean():.0f}ms "
f"{d['full_ms'].sum() / max(d['lean_ms'].sum(), 1e-9):.2f}x")
if int(d["diff"].sum()) == 0:
print("\n 逐字段完全一致,可以开 lean。")
else:
print("\n ⛔ 存在分歧,不要开 lean。lean 跳掉的东西这条路径确实在用。")
if __name__ == "__main__":
main()