影子信号走增量:清空十币 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>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user