Files
Chan/research/live/verify_incr_parity.py
T
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

155 lines
5.6 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.
"""逐根对拍「全量重算」与「增量追加」,并量提速。
## 为什么必须逐根对拍,不能引用 HANDOFF §5.5
§5.5 验的是 step46 那批用例(走 bsp_list 那条链),且是「追加 150~200 根 vs
全量重建」的整体哈希。影子路径不同:
- 走 find_fast_bsp3 + build_htf_zones + htf_fx_timeline + attach_htf_context
- 流式对象**跨根复用**,而 worker 轮流拿多个币,同一条流可能隔几根才被
再次追加。状态污染只会让信号悄悄换一批,不报错、不崩
而且代码阅读已经暴露一处偏差:`init_stream/append_bar` 从不调 `cal_trend`
(它只在 `get_klc_list` 里),所以增量路径下 `klc.trend` 恒为 UNKNOWN。
HANDOFF 说「笔的计算依赖 klc.trend」——若为真,增量的笔就和全量不同。
那句话所引的 bi.py:221 其实在 `cal_trend` 自己的循环里,不是 `cal_bi_list`
的依赖。**这条只能由对拍来定论**,不能靠读代码。
## 判据
逐字段相同,排除 last_idx/n_bars(随窗口长度必然变,见 verify_window_sens
与计时字段。数量相同而标志不同一样算失败。
模拟真实调用模式:连续推进,且每根都按「全量」和「增量」各算一次,增量那侧
复用同一条流。
python research/live/verify_incr_parity.py --syms BTC,ETH,SOL --n 300
"""
from __future__ import annotations
import argparse
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))
BASE_L, BASE_H = 2001, 801
def run_one(sym: str, cache: Path, n: int, start_at: int | None) -> dict:
import shadow_signal as ss
from verify_lean_parity import SKIP, WINDOW_KEYS, canon, load, signal_bars
skip = SKIP + WINDOW_KEYS + ("stream_bars",)
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")
# 从最后一个信号根往前 n 根开始,保证这段里一定有信号分支被执行
if start_at is None:
try:
sb = signal_bars(sym, cache)
sb = sb[(sb > BASE_L + n) & (sb < len(l_all) - 1)]
start_at = int(sb[-1]) - n + 5 if len(sb) else BASE_L + 10
except Exception:
start_at = BASE_L + 10
ends = [e for e in range(start_at, start_at + n) if e < len(l_all) - 1]
if not ends:
raise RuntimeError("窗口不足")
ss._STREAMS.clear()
same = diff = 0
t_full = t_incr = 0.0
n_hits = 0
first = None
rebuilds = 0
prev_grown = 0
for e in ends:
hi = int(np.searchsorted(h_ts, l_ts[e], side="right"))
df_l = l_all.iloc[e - BASE_L + 1:e + 1]
df_h = h_all.iloc[max(0, hi - BASE_H):hi]
entry = float(l_all["open"].to_numpy(float)[e + 1])
t0 = time.perf_counter()
rf = ss.compute(df_l.copy(), df_h.copy(), entry, incr=False)
t_full += time.perf_counter() - t0
t0 = time.perf_counter()
ri = ss.compute(df_l.copy(), df_h.copy(), entry, sym=sym, incr=True)
t_incr += time.perf_counter() - t0
grown = len(ss._STREAMS[(sym, "1m")][0].dataframe)
if grown <= prev_grown:
rebuilds += 1
prev_grown = grown
n_hits += len(rf.get("hits") or [])
if canon(rf, skip) == canon(ri, skip):
same += 1
else:
diff += 1
if first is None:
first = (e, canon(rf, skip), canon(ri, skip))
k = len(ends)
print(f" {k} 根 · 一致 {same} · 不一致 {diff} · 命中 {n_hits} 个 · "
f"重建 {rebuilds} 次 · 末窗 {prev_grown} 根")
print(f" 单根 全量 {t_full / k * 1000:.1f}ms → "
f"增量 {t_incr / k * 1000:.1f}ms "
f"{t_full / max(t_incr, 1e-9):.2f}x")
if first:
e, a, b = first
print(f" ⚠ 首个分歧 idx={e}\n 全量: {a[:300]}\n 增量: {b[:300]}")
ss._STREAMS.clear()
del l_all, h_all
return {"sym": sym, "n": k, "same": same, "diff": diff, "hits": n_hits,
"full_ms": t_full / k * 1000, "incr_ms": t_incr / k * 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=300)
ap.add_argument("--start", type=int, default=None)
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.start))
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()):,} 根 · 不一致 {int(d['diff'].sum())} · "
f"命中 {int(d['hits'].sum())} 个")
print(f" 单根 全量 {d['full_ms'].mean():.1f}ms → "
f"增量 {d['incr_ms'].mean():.1f}ms "
f"{d['full_ms'].sum() / max(d['incr_ms'].sum(), 1e-9):.2f}x")
if int(d["diff"].sum()) == 0:
print("\n 逐字段一致,增量可以上线。")
else:
print("\n ⛔ 有分歧,不要上线。增量流的状态与全量重建不等价。")
if __name__ == "__main__":
main()