影子信号走增量:清空十币 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:
@@ -363,7 +363,8 @@ class Shadow:
|
||||
# compute_ms 含排队;queue_ms/inner_ms 把它拆开,用来判断加核有没有用
|
||||
"compute_ms", "queue_ms", "inner_ms",
|
||||
"n_bars", "n_hits",
|
||||
"n_pass", "atr_bp", "lag_med_ms", "lag_ok"])
|
||||
"n_pass", "atr_bp", "lag_med_ms", "lag_ok",
|
||||
"stream_bars"])
|
||||
# 无条件漂移:每根都记,用来和信号根上的条件漂移对照
|
||||
self.f_drf, self.w_drf = _writer(d / "shadow_drift.csv", [
|
||||
"site", "sym", "kline_ts", "delay_label", "delay_ms",
|
||||
@@ -571,7 +572,7 @@ class Shadow:
|
||||
|
||||
t0 = time.perf_counter()
|
||||
payload = (df_l[NUM_COLS].values.tolist(),
|
||||
df_h[NUM_COLS].values.tolist(), baseline, time.time())
|
||||
df_h[NUM_COLS].values.tolist(), baseline, time.time(), sym)
|
||||
loop = asyncio.get_running_loop()
|
||||
from shadow_signal import compute_packed
|
||||
try:
|
||||
@@ -610,7 +611,10 @@ class Shadow:
|
||||
"queue_ms": res.get("queue_ms"), "inner_ms": res.get("inner_ms"),
|
||||
"n_bars": res.get("n_bars", 0),
|
||||
"n_hits": len(hits), "n_pass": n_pass, "atr_bp": atr_bp,
|
||||
"lag_med_ms": lag_med, "lag_ok": int(lag_ok)})
|
||||
"lag_med_ms": lag_med, "lag_ok": int(lag_ok),
|
||||
# 增量流当前窗口。恒等于 2001 说明缺口判定在每根都
|
||||
# 回退重建,增量静默失效——只从耗时上看不出是哪一环
|
||||
"stream_bars": res.get("stream_bars")})
|
||||
self.f_lat.flush()
|
||||
|
||||
if baseline is not None and np.isfinite(baseline):
|
||||
|
||||
@@ -40,10 +40,64 @@ for _v in ("OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS"):
|
||||
|
||||
|
||||
LEAN = os.environ.get("SHADOW_LEAN", "1") not in ("0", "", "false")
|
||||
INCR = os.environ.get("SHADOW_INCR", "1") not in ("0", "", "false")
|
||||
|
||||
# 增量流缓存。worker 进程被复用,所以这个 dict 跨根存活。
|
||||
# 键是 (symbol, timeframe)——2 个 worker 轮流拿 10 个币,每个 worker 最终会
|
||||
# 缓存全部 10 个币,共 20 条流。
|
||||
_STREAMS: dict[tuple, tuple] = {}
|
||||
|
||||
# 两次重建之间允许窗口长多少根。
|
||||
#
|
||||
# init_stream/append_bar **没有 trim**:dataframe 靠 pd.concat 无界增长。所以
|
||||
# 增量必然让窗口每根 +1,只能周期性 init_stream 拉回。取 500 的两个理由:
|
||||
# 1. append_bar 里 rebuild_bi_zs 要整表重扫笔,是 O(n)。窗口涨 25% 成本也涨
|
||||
# 约 25%,500/2001 正好把这个膨胀压在 25% 以内。
|
||||
# 2. 重建约 51ms、追加约 14ms,摊到 500 根上重建只加 0.07ms/根。
|
||||
# 前提「输出对窗口长度不敏感」由 verify_window_sens.py 验过(+200/+500/+1000
|
||||
# 全部逐字段一致),否则这个方案等于静默换掉一批信号。
|
||||
MAX_GROW = 500
|
||||
|
||||
|
||||
def _chan_for(key: tuple, df, tf: str, lean: bool):
|
||||
"""拿该窗口对应的 chan 对象,能增量就增量,否则重建。
|
||||
|
||||
三种情况必须回退到全量重建,否则会拿一个状态不对的流去出信号:
|
||||
|
||||
缓存没有 首次见到这个币
|
||||
窗口已长过阈值 见 MAX_GROW
|
||||
缓存末根不在新窗口 说明中间断了很多根(或时间戳回退),接不上
|
||||
|
||||
第三种是最要紧的。2 个 worker 轮流拿 10 个币,某个 worker 可能隔几根才再
|
||||
看到同一个币,那几根要补齐;但若缺口大到超出窗口,就没法补,只能重建。
|
||||
不检查而直接 append 会把不连续的 K 线接在一起,笔和中枢全错且不报错。
|
||||
"""
|
||||
from chanlun import TF_DF
|
||||
|
||||
ts = df["timestamp"].to_numpy("int64")
|
||||
st = _STREAMS.get(key)
|
||||
if st is not None:
|
||||
chan, last_ts, base_n = st
|
||||
if len(chan.dataframe) <= base_n + MAX_GROW and last_ts >= ts[0] \
|
||||
and last_ts <= ts[-1] and (ts == last_ts).any():
|
||||
for _, row in df[df["timestamp"] > last_ts].iterrows():
|
||||
chan.append_bar(row)
|
||||
_STREAMS[key] = (chan, int(ts[-1]), base_n)
|
||||
return chan
|
||||
|
||||
# 重建走**批量** init_TF_DF,不用 init_stream。init_stream 是逐行
|
||||
# `dataframe.iloc[idx]`,正是引擎提速刚修掉的反模式:实测 2001 根要
|
||||
# 238.5ms,而批量 lean 只要 74.3ms,慢 3.2 倍。
|
||||
# append_bar 能接在批量构建的对象上——_ensure_stream_state 会补出
|
||||
# _klc_feed_last_klu,其余列表 init_TF_DF 都建好了。
|
||||
chan = TF_DF(df.copy(), 1, tf, lean=lean)
|
||||
_STREAMS[key] = (chan, int(ts[-1]), len(df))
|
||||
return chan
|
||||
|
||||
|
||||
def compute(df_l, df_h, entry_px: float | None = None,
|
||||
lean: bool | None = None) -> dict:
|
||||
lean: bool | None = None, sym: str | None = None,
|
||||
incr: bool | None = None) -> dict:
|
||||
"""在 df_l 的最后一根上找信号。df_l/df_h 都只含已收盘 K 线。
|
||||
|
||||
entry_px 是次根开盘价(回测 entry_delay=1 的成交价),用作 atr_pct 的
|
||||
@@ -66,6 +120,8 @@ def compute(df_l, df_h, entry_px: float | None = None,
|
||||
import pandas as pd
|
||||
|
||||
lean = LEAN if lean is None else lean
|
||||
# 没有 sym 就无法给流分键,只能走全量——对拍脚本会用这条路径当基准
|
||||
incr = (INCR if incr is None else incr) and sym is not None
|
||||
|
||||
try:
|
||||
from chanlun import TF_DF
|
||||
@@ -75,7 +131,8 @@ def compute(df_l, df_h, entry_px: float | None = None,
|
||||
from lib.nested_level import build_htf_zones
|
||||
from lib.shadow_budget import ATR_GATE_BP
|
||||
|
||||
chan_l = TF_DF(df_l, 1, "1m", lean=lean)
|
||||
chan_l = _chan_for((sym, "1m"), df_l, "1m", lean) if incr \
|
||||
else TF_DF(df_l, 1, "1m", lean=lean)
|
||||
cdf = chan_l.dataframe
|
||||
last = len(cdf) - 1
|
||||
base = {"last_idx": last, "n_bars": int(len(df_l)), "hits": [],
|
||||
@@ -111,7 +168,8 @@ def compute(df_l, df_h, entry_px: float | None = None,
|
||||
|
||||
# 5m 同向。算不出时 h1_agree 记 0,该信号自然不会通过 pass_all
|
||||
if df_h is not None and len(df_h) > 0:
|
||||
chan_h = TF_DF(df_h, 1, "5m", lean=lean)
|
||||
chan_h = _chan_for((sym, "5m"), df_h, "5m", lean) if incr \
|
||||
else TF_DF(df_h, 1, "5m", lean=lean)
|
||||
hdf = chan_h.dataframe
|
||||
tl = htf_fx_timeline(
|
||||
signals_to_frame(extract_fx_signals(chan_h, hdf)), hdf)
|
||||
@@ -180,11 +238,15 @@ def compute_packed(payload: tuple) -> dict:
|
||||
t_start = time.time()
|
||||
l_rows, h_rows, entry_px, *rest = payload
|
||||
t_submit = rest[0] if rest else None
|
||||
sym = rest[1] if len(rest) > 1 else None
|
||||
|
||||
t0 = time.perf_counter()
|
||||
df_l = _rebuild(l_rows)
|
||||
df_h = _rebuild(h_rows) if h_rows else None
|
||||
out = compute(df_l, df_h, entry_px)
|
||||
out = compute(df_l, df_h, entry_px, sym=sym)
|
||||
# 落盘这两个数才能在线看出增量是否在生效:走了重建的根 grown 会等于窗口
|
||||
st = _STREAMS.get((sym, "1m"))
|
||||
out["stream_bars"] = int(len(st[0].dataframe)) if st else None
|
||||
out["inner_ms"] = int((time.perf_counter() - t0) * 1000)
|
||||
# 同一台机器,父子进程时钟一致,可直接相减
|
||||
out["queue_ms"] = int((t_start - t_submit) * 1000) \
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
"""逐根对拍「全量重算」与「增量追加」,并量提速。
|
||||
|
||||
## 为什么必须逐根对拍,不能引用 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()
|
||||
@@ -50,7 +50,12 @@ def load(sym: str, tf: str, cache: Path) -> pd.DataFrame:
|
||||
return pd.read_feather(c[0])
|
||||
|
||||
|
||||
def canon(d: dict) -> str:
|
||||
# 随窗口长度必然改变的记账字段。比「窗口长度会不会改信号」时要排除它们,
|
||||
# 否则一定 0/12 不一致,而那是记账字段在变,不是信号在变
|
||||
WINDOW_KEYS = ("last_idx", "n_bars")
|
||||
|
||||
|
||||
def canon(d: dict, skip: tuple = SKIP) -> str:
|
||||
"""把返回值规范化成可比较的字符串。
|
||||
|
||||
浮点直接比会被末位差异误判,但 lean 走的是同一段算术、不该有任何差异,
|
||||
@@ -68,7 +73,7 @@ def canon(d: dict) -> str:
|
||||
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)
|
||||
if k not in skip}, sort_keys=True, ensure_ascii=False)
|
||||
|
||||
|
||||
def signal_bars(sym: str, cache: Path) -> np.ndarray:
|
||||
|
||||
@@ -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