Files
Chan/research/live/verify_window_sens.py
jackandCursor 0a0fd2f682 影子信号走增量:清空十币 560ms → 247ms
compute() 每根新建 TF_DF 换成按 (symbol, timeframe) 缓存的流式对象。worker
进程被复用,所以缓存跨根存活;2 worker 轮流拿 10 币,每个 worker 最终缓存
全部 20 条流,实测内存开销落在噪声里(597→598MiB)。

## 前提先验,否则整个改动建立在沙子上

init_stream/append_bar **没有 trim**,dataframe 靠 pd.concat 无界增长。所以
增量必然让窗口每根 +1,只能周期性重建拉回,两次重建之间窗口是 [W, W+500]
而非恒定 W。于是必须先证明 compute() 输出对窗口长度不敏感——否则增量等于
静默换掉一批信号,不报错不崩。

verify_window_sens.py:三币 75 个信号窗口,+200/+500/+1000 三档全部逐字段
一致。step39 说的是「命中率在 2000 根饱和」,饱和不等于不变,这是两回事。

## 对拍

verify_incr_parity.py:三币 1,800 根、21 个命中、各跨 1 次重建边界,逐字段
零分歧。不能引用 HANDOFF §5.5——那验的是 bsp_list 那条链的整体哈希,而这里
是 find_fast_bsp3 那条链,且流式对象跨根复用,状态污染只会让信号悄悄换一批。

对拍顺带定论一件读代码定不了的事:cal_bi_list **不依赖** klc.trend。
init_stream/append_bar 从不调 cal_trend(它只在 get_klc_list 里),所以追加
出来的 klc 其 trend 恒为 UNKNOWN,而批量构建的有值;两者结果逐字段相同。
HANDOFF §5.5 那句「bi.py:221 读 klc.trend,笔的计算依赖它」不成立——221 行
在 cal_trend 自己的循环里,读的是它自身的序列状态。

## 重建不走 init_stream

init_stream 是逐行 dataframe.iloc[idx],正是引擎提速刚修掉的反模式:2001 根
要 238.5ms,而批量 lean 只 74.3ms,慢 3.2 倍。第一版用它重建,10 个币启动时
各来一次,清空反而涨到 1686ms。改用 TF_DF(df, lean=True) 重建,append_bar
靠 _ensure_stream_state 就能接上。

## 实测

append_bar 21.8ms vs 批量 lean 重建 77.7ms = 3.56x,与研究侧测的 3.7x 一致。
拆解:add_indicators 全表 7.5ms(34%,为加一根重算 2001 行)+ cal_bi_list
整表重扫 11.1ms(51%)+ concat 1.4ms。这两项都在引擎侧,值得反馈。

十币 / 2 核:清空 560→247ms,排队 92→10ms,纯计算 219→108ms。判定从
「加 worker 无用,唯一出路是增量」变成「宽裕,无需优化」。

注意 inner 108ms 里 chan 构建只占约 22ms,其余是 build_htf_zones /
find_fast_bsp3 / attach_htf_context。**瓶颈已不在 chan 构建**,再压增量收益
有限。

stream_bars 落到 latency CSV:恒等于 2001 说明缺口判定在每根都回退重建、
增量静默失效,这一点从耗时上看不出是哪一环。实测窗口稳定长大。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 06:17:47 +08:00

116 lines
4.5 KiB
Python
Raw Permalink 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.
"""compute() 的输出对窗口长度是否不变——增量路径的前提。
## 为什么这是增量路径的前提
`init_stream/append_bar` 没有 trim`dataframe` 靠 `pd.concat` 无界增长。所以
增量方案必然意味着**窗口会长大**(每根 +1),只能靠周期性 `init_stream` 重建
拉回。于是在两次重建之间,实际窗口是 [W, W+slack] 而不是恒定 W。
这就把一个问题摆在前面:如果 `compute()` 的输出随窗口长度变化,增量路径等于
静默把信号换了一批——不报错、不崩,只是测的不再是回测那批信号。
step39 的结论是「命中率在 2000 根饱和」。**饱和不等于不变**:再加根数不再提高
命中率,与「结果逐字段相同」是两回事。所以要单独验。
判据是逐字段相同,不是命中数相同。数量相同而方向或滤网标志不同,会让影子测
的是另一批信号。
python research/live/verify_window_sens.py --syms BTC,ETH,SOL
"""
from __future__ import annotations
import argparse
import os
import sys
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))
BASE_L, BASE_H = 2001, 801
# 增量在两次重建之间会长这么多。取 500 是因为它对应约 8 小时,
# 重建摊薄后单根成本仍接近纯追加
GROW = (0, 200, 500, 1000)
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=40)
a = ap.parse_args()
from shadow_signal import compute
from verify_lean_parity import (SKIP, WINDOW_KEYS, canon, load,
signal_bars)
# last_idx/n_bars 必然随窗口长度变。不排除的话结果一定是「全不一致」,
# 而那说明的是记账字段在变,不是信号在变
skip = SKIP + WINDOW_KEYS
cache = Path(a.cache)
print("同一根上,只改窗口长度,比对 compute() 的全部返回字段")
print(f"基准窗口 1m×{BASE_L} + 5m×{BASE_H};增量会让它长大,故试 "
f"+{GROW[1:]}\n")
tot = {g: [0, 0] for g in GROW[1:]} # [相同, 不同]
for sym in a.syms.split(","):
l_all, h_all = load(sym, "1m", cache), load(sym, "5m", cache)
l_ts = l_all["timestamp"].to_numpy("int64")
h_ts = h_all["timestamp"].to_numpy("int64")
try:
sb = signal_bars(sym, cache)
except Exception as e:
print(f"{sym} 取信号根失败:{e!r}")
continue
need = BASE_L + max(GROW)
sb = sb[(sb > need) & (sb < len(l_all) - 1)][-a.n:]
if len(sb) == 0:
print(f"{sym} 可用信号根不足")
continue
res: dict[int, list[str]] = {g: [] for g in GROW}
for e in sb:
hi = int(np.searchsorted(h_ts, l_ts[e], side="right"))
entry = float(l_all["open"].to_numpy(float)[e + 1])
for g in GROW:
df_l = l_all.iloc[e - (BASE_L + g) + 1:e + 1]
df_h = h_all.iloc[max(0, hi - (BASE_H + g // 5)):hi]
res[g].append(canon(compute(df_l.copy(), df_h.copy(), entry),
skip=skip))
print(f"{sym} {len(sb)} 根信号窗口")
for g in GROW[1:]:
same = sum(1 for x, y in zip(res[0], res[g]) if x == y)
tot[g][0] += same
tot[g][1] += len(sb) - same
print(f" +{g:>4} 根 → 一致 {same}/{len(sb)}"
+ ("" if same == len(sb) else " ⚠ 有分歧"))
del l_all, h_all
print(f"\n{'=' * 66}\n汇总\n")
for g in GROW[1:]:
s, d = tot[g]
print(f" 窗口 +{g:>4} 根:一致 {s} · 不一致 {d}")
worst = max(GROW[1:], key=lambda g: tot[g][1])
if tot[worst][1] == 0:
print("\n 窗口长度不影响输出,增量路径的前提成立。")
print(" 可以按「长到 +N 根再 init_stream 重建」摊薄成本。")
else:
print("\n ⛔ 窗口长度会改变输出,增量路径会静默换掉一批信号。")
print(" 此时要么每根都重建(等于没有增量),要么先把窗口效应本身收口。")
if __name__ == "__main__":
main()