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>
155 lines
5.6 KiB
Python
155 lines
5.6 KiB
Python
"""逐根对拍「全量重算」与「增量追加」,并量提速。
|
||
|
||
## 为什么必须逐根对拍,不能引用 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()
|