Files
Chan/research/step43_fill_aware_budget.py
jackandCursor 75fbf4167b 修掉 gzip 追加会毁掉整个文件的数据丢失,并分开买卖两侧的成交分布曲线
两件事,都是「静默出错」那一类。

一、BookLog/TapeLog 追加到同一个 .gz,进程被 SIGKILL 时当前成员停在 deflate
块中间,下一轮追加的新成员接在垃圾字节之后。顺序解压在损坏点抛 invalid
block type,该点之后全部读不出来——包括后续每轮写进去的。而读侧的异常处理
把这个当成「正常的尾部截断」静默跳过,于是只读出 21 行还不报错。
原 docstring 里写的「只丢最后一个缓冲块,不会毁掉整个文件」是错的,已证伪。

写侧改成每轮运行一个文件;读侧按 gzip 成员边界扫描、坏成员单独跳过并出声
报告,同时把同前缀的多轮文件一并读入。旧损坏文件因此多恢复出 31/21 条
(tape)与 132/95 条(books)。

二、tape_shape 只统计主动买、只自区间顶部累积,这条曲线只适用于多头止盈。
exit_fill 两侧共用它,等于把空头的可成交量按多头分布高估。实测二者不对称:
主动买在顶部 20% 内已占 40%,主动卖在底部 20% 内只有 18%。分成 SHAPE_F 与
SHAPE_F_SHORT,avail_at 按方向查各自曲线。

两条曲线只有 45 根成交流样本,所以补了 --sensitivity:把空头可成交量砍一半,
10 万仓位下 BTC/ETH 预算完全不动、SOL 动 0.07bp。结论不依赖这 45 根样本。
顺带撤掉 step43 docstring 里已作废的「成交率 30%/16%/1.5%」。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 03:23:07 +08:00

216 lines
8.8 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.
"""Step 43:把「限价单全额成交」的假设换成按成交量结算,重算滑点预算。
step42 的预算建立在一个假设上:挂在 3ATR 与 8ATR 的止盈限价单全额成交在目标
价。本脚本把它换成按成交流实测的成交量结算,让预算变成**仓位规模的函数**。
## 结论(2026-08-28
**主口径 10 万 USDT 下成交率不是绑定约束。** 预算相对全额成交假设的降幅:
BTC 0% / ETH 0% / SOL 1.1%;即便到 100 万也只有 2.6% / 0.9% / 8.0%。
⚠ 此前一版本文档写「真实仓位下全额成交率只有 30%/16%/1.5%」,那是
shadow_depth.composite_fill 的口径——只算首次触及那一根的可成交量,而真实
挂单在那儿常驻最多 48 根、每根都在成交。该数系统性偏悲观,已撤回。
另一个约束是冲击反推的容量上限(100~500 万),比成交率宽松得多。两者都不
绑定,所以 10 万仓位上限制来自别处,不是流动性。
结论对成交分布曲线**不敏感**:把空头侧可成交量砍一半,10 万仓位下 BTC/ETH
预算完全不动、SOL 动 0.07bp。见 `--sensitivity`。这一点重要,因为那两条曲线
目前只有 45 根成交流样本。
数据用 Bitget 210 天 1m,与影子测量同源同交易所。全量 366 万根峰值 24.5GB
本机 15GB 跑不动;210 天 30 万根峰值约 2GB。
python research/step43_fill_aware_budget.py --syms BTC,ETH,SOL
python research/step43_fill_aware_budget.py --sensitivity
"""
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().parent
sys.path.insert(0, str(HERE))
sys.path.insert(0, str(HERE.parent))
pd.set_option("display.width", 400)
LTF, HTF = "1m", "5m"
SL, SCALE_AT, RUNNER, RUNNER_STOP, MAXB = 2.0, 3.0, 8.0, 2.0, 48
NOTIONALS = [5e3, 1e4, 2e4, 5e4, 1e5, 2e5, 3.2e5, 5.3e5, 1e6]
def signals_for(sym: str, cache: Path) -> tuple[pd.DataFrame, pd.DataFrame]:
"""跑缠论链路,返回 (cdf, 过完三滤网的信号)。口径抄 step42.run_one。"""
from chanlun import TF_DF
from lib.fast_bsp3 import find_fast_bsp3
from lib.fx_signal import extract_fx_signals, signals_to_frame
from lib.nested_bsp import attach_htf_context, htf_fx_timeline
from lib.nested_level import build_htf_zones
from lib.shadow_budget import ATR_GATE_BP
def load(tf: str) -> pd.DataFrame:
c = sorted(cache.glob(f"bitget_{sym}_{tf}_*.feather"),
key=lambda p: p.stat().st_size, reverse=True)
if not c:
raise FileNotFoundError(f"没有 {sym} {tf} 缓存")
return pd.read_feather(c[0])
chan_l = TF_DF(load(LTF), 1, LTF)
cdf = chan_l.dataframe
zones = build_htf_zones(cdf, LTF, chan=chan_l).reset_index(drop=True)
if zones.empty:
raise RuntimeError("无中枢")
z = zones.copy()
pg, pdn = z["zg"].shift(), z["zd"].shift()
z["z_above"], z["z_below"] = z["zd"] > pg, z["zg"] < pdn
z["zone_i"] = np.arange(len(z))
chan_h = TF_DF(load(HTF), 1, HTF)
tl = htf_fx_timeline(
signals_to_frame(extract_fx_signals(chan_h, chan_h.dataframe)),
chan_h.dataframe)
del chan_h
sig = find_fast_bsp3(cdf, zones)
if sig.empty:
raise RuntimeError("无信号")
sig = sig.merge(z[["zone_i", "z_above", "z_below"]], on="zone_i",
how="left")
sig = attach_htf_context(sig, cdf, tl, "h1")
push = np.where(sig["direction"] == 1, sig["z_above"], sig["z_below"])
keep = ((sig["h1_agree"] == 1)
& pd.Series(push, index=sig.index).fillna(False).astype(bool))
sig = sig[keep].copy()
# ATR 门控:与 shadow_signal 同源,分母取次根开盘价
idx = sig["entry_idx"].astype(int).to_numpy()
atr = cdf["atr"].to_numpy(float)
op = cdf["open"].to_numpy(float)
ref = op[np.minimum(idx + 1, len(op) - 1)]
with np.errstate(invalid="ignore", divide="ignore"):
atr_bp = atr[idx] / ref * 1e4
sig = sig[np.isfinite(atr_bp) & (atr_bp >= ATR_GATE_BP)]
return cdf, sig
def sensitivity(syms: list[str], cache: Path, notional: float = 1e5) -> None:
"""预算对成交分布曲线的敏感性。
SHAPE_F / SHAPE_F_SHORT 只有 45 根成交流样本,数值精度很低。所以必须先
证明结论对它们不敏感,否则整条预算曲线都建立在 45 根样本上。
三档:两侧共用买盘曲线(旧口径,空头偏乐观)/实测的方向各异曲线/把
空头可成交量再砍一半的悲观上界。
"""
from lib.exit_fill import SHAPE_F, SHAPE_F_SHORT, budget_bp, walk_filled
half = np.clip(SHAPE_F_SHORT * 0.5, 0.0, 1.0)
half[-1] = 1.0
cases = [("对称(旧口径)", SHAPE_F, SHAPE_F),
("实测不对称", SHAPE_F, SHAPE_F_SHORT),
("悲观:空头量减半", SHAPE_F, half)]
print(f"\n{'=' * 74}\n成交分布曲线敏感性 · 仓位 {notional:,.0f} USDT\n")
print(f" {'币':<5}" + "".join(f"{n:>18}" for n, _, _ in cases))
for sym in syms:
try:
cdf, sig = signals_for(sym, cache)
except Exception as e:
print(f" {sym}: 跳过 {e!r}")
continue
row = f" {sym:<5}"
for _, fl, fs in cases:
r = walk_filled(cdf, sig, notional, SL, SCALE_AT, RUNNER,
RUNNER_STOP, MAXB, f=fl, f_short=fs)
row += f"{budget_bp(r):>13.2f}bp " if not r.empty \
else f"{'—':>18}"
print(row)
del cdf
print("\n 2026-08-28 实测三档差异 ≤ 0.07bp,结论对曲线不敏感。")
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("--save", default="research/out/step43_fill_budget.csv")
ap.add_argument("--sensitivity", action="store_true",
help="只跑成交分布曲线的敏感性检查")
a = ap.parse_args()
if a.sensitivity:
sensitivity(a.syms.split(","), Path(a.cache))
return
from lib.exit_fill import (assert_converges, budget_bp, net_bp,
walk_filled)
from lib.exit_model import cfg_name, slip_budget
from lib.exit_model import walk_exits
cache = Path(a.cache)
rows = []
for sym in a.syms.split(","):
print(f"\n{'=' * 74}\n{sym}")
try:
cdf, sig = signals_for(sym, cache)
except Exception as e:
print(f" 跳过:{e!r}")
continue
print(f" {len(cdf):,} 根 1m · 过三滤网 {len(sig)} 笔信号")
if len(sig) < 25:
print(" 样本不足 25 笔,不出统计")
continue
# 老口径:假定限价全额成交
old = walk_exits(cdf, sig, [SL], [SCALE_AT], [MAXB], SCALE_AT,
[RUNNER], [RUNNER_STOP])
c = cfg_name(SL, RUNNER, MAXB, RUNNER_STOP)
b_old = slip_budget(old[f"{c}_g"].to_numpy(),
old[f"{c}_r"].to_numpy(),
old[f"{c}_c"].to_numpy())
# 先证明两套实现在「仓位趋近 0」这个极限上逐笔一致,
# 否则后面看到的差异分不清是成交量效应还是实现 bug
assert_converges(cdf, sig, SL, SCALE_AT, RUNNER, RUNNER_STOP, MAXB)
print(f"\n 老口径(限价全额成交)预算 {b_old:.2f}bp"
f" [极限一致性断言通过]")
print(f"\n {'仓位':>10} {'预算bp':>9} {'maker占比':>10} "
f"{'止损占比':>9} {'超时占比':>9} {'净收益bp':>10}")
for nt in NOTIONALS:
r = walk_filled(cdf, sig, nt, SL, SCALE_AT, RUNNER,
RUNNER_STOP, MAXB)
if r.empty:
continue
b = budget_bp(r)
print(f" {nt:>10,.0f} {b:>9.2f} "
f"{r['maker_share'].mean() * 100:>9.1f}% "
f"{r['w_stop'].mean() * 100:>8.1f}% "
f"{r['w_time'].mean() * 100:>8.1f}% "
f"{net_bp(r):>10.2f}")
rows.append({"sym": sym, "notional": nt, "budget_bp": b,
"budget_bp_old": b_old,
"maker_share": r["maker_share"].mean(),
"w_stop": r["w_stop"].mean(),
"w_time": r["w_time"].mean(),
"net_bp": net_bp(r), "n": len(r)})
del cdf
if rows:
out = pd.DataFrame(rows)
Path(a.save).parent.mkdir(parents=True, exist_ok=True)
out.to_csv(a.save, index=False)
print(f"\n已存 {a.save}")
if __name__ == "__main__":
main()