出场模型改为按成交量结算止盈限价单,并出预算对仓位规模的曲线

exit_model.walk_exits 给止盈记的毛收益是 target*a/entry,即假定限价单全额
成交在目标价。新增 lib/exit_fill.py:两张挂单常驻(半仓 3ATR、半仓 8ATR),
每根按该根在限价之上的可成交量逐步吃进,未成交部分继续持有,止损触发时
市价平掉剩余。可成交量 = 形状函数 f(k) × 该根主动买成交额,f 由影子成交流
实测(近似线性,即区间内均匀分布,故结论对形状假设不敏感)。

结果:预算对仓位规模远比预期稳健。到 100 万名义额,BTC 10.97→10.69、
ETH 15.34→15.20、SOL 17.21→15.83bp。原因是挂单常驻多根而非只在首次触及
那一根成交,且价格决定性穿过限价时整根成交量都可用。

首版实现有个静默 bug 值得记:avail_above 里有个 `hi <= 0` 的守卫,而空头
用「价格取负」处理,负价格空间里 hi 恒为负——所有空头挂单的可成交量一律
判 0,空头全被拖到 48 根超时收盘。下跌段里那比 3ATR 目标赚得多,于是预算
反而偏高 0.76bp,表现为「一个看似合理的模型差异」。已改为显式方向参数,
并加 assert_converges:仓位趋近 0 时必须逐笔收敛到 walk_exits,不符即抛错。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jack
2026-08-28 02:39:47 +08:00
co-authored by Cursor
parent 181bca303f
commit 28075173af
3 changed files with 407 additions and 0 deletions
+218
View File
@@ -0,0 +1,218 @@
"""出场模拟,但止盈按限价单的**真实成交量**结算,而非假定全额成交。
## 为什么要另写一份
`exit_model.walk_exits` 给止盈记的毛收益是 `target * a / entry`,即假定挂在
目标价的限价单全额成交在目标价。影子交易的成交流数据显示这个假定在真实
仓位上不成立:止盈位被首次触及那一根,限价落在该根价格区间中的位置中位
k≈0.28,而该位置之上可供成交的主动买量,对 32 万仓位只够覆盖 30%/16%/1.5%
BTC/ETH/SOL)。
不成交不等于仓位消失——它继续持有,结果从「继续走下去」的分布里抽,其中
包含反转打到止损。所以这不是给预算打折能修的事,是出场规则变了。
## 模型
两张挂单常驻:半仓在 `scale_at`、半仓在 `runner`。每根按该根在限价之上的
可成交量逐步吃进,未成交部分继续持有;整仓止损始终有效,触发时未成交的
部分市价平掉;到 `maxb` 根仍未了结的按收盘市价平。
某根在限价 P 之上的可成交量:
P ≤ low 整根成交量都在限价之上 avail = V
P > high 该根没到限价 avail = 0
否则 k = (highP)/(highlow) avail = f(k) × V
`V` 是该根的**主动买**成交额(多头出场靠主动买盘打上来)。实测买卖大致
均衡,取总成交额的一半;以 BTC 校验,历史 `volume×close` 中位与影子成交流
实测差 0.3%。`f` 由成交流定,实测几乎是线性(f(k)≈k,即区间内均匀分布),
所以结论对形状假设不敏感。
## 仍然乐观的两处
1. **未计排队**。我们的单排在该价位既有挂单之后,真实成交更少。
2. **未计自身的流动性效应**。大单挂在 3ATR 会吸收本该冲到 8ATR 的买盘,
即两张挂单在真实市场里互相竞争,此处按独立处理。
两处都指向同一方向:真实成交率比本模型更低。
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from lib.exit_model import FEE_MAKER, FEE_TAKER, SLIP
# 成交流实测的区间内成交分布形状(BTC/ETH/SOL 均值,见 shadow_depth.tape_shape
SHAPE_K = np.linspace(0.0, 1.0, 21)
SHAPE_F = np.array([0.044, 0.088, 0.110, 0.179, 0.204, 0.240, 0.282, 0.316,
0.390, 0.430, 0.465, 0.537, 0.583, 0.617, 0.662, 0.714,
0.761, 0.804, 0.857, 0.904, 1.000])
TAKER_SHARE = 0.5
def avail_at(price: float, hi: float, lo: float, vol_notional: float,
is_long: bool, kgrid=SHAPE_K, f=SHAPE_F) -> float:
"""该根里能打到限价 `price` 的对手方成交额。
多头在 `price` 挂卖出,靠价格 ≥ price 的主动买成交;空头挂买回,靠
价格 ≤ price 的主动卖成交。方向用显式参数而非「把价格取负」——取负会
让所有价格变成负数,任何对价格正负的假设都会静默失效。
"""
if vol_notional <= 0 or not (np.isfinite(hi) and np.isfinite(lo)):
return 0.0
if hi <= lo:
# 该根无波动:只要限价被覆盖就算整根可成交
return vol_notional if (hi >= price if is_long else lo <= price) \
else 0.0
if is_long:
if price > hi:
return 0.0 # 该根没涨到限价
if price <= lo:
return vol_notional # 整根都在限价之上
k = (hi - price) / (hi - lo)
else:
if price < lo:
return 0.0 # 该根没跌到限价
if price >= hi:
return vol_notional # 整根都在限价之下
k = (price - lo) / (hi - lo)
return float(np.interp(k, kgrid, f)) * vol_notional
def walk_filled(cdf: pd.DataFrame, sig: pd.DataFrame, notional: float,
sl: float = 2.0, scale_at: float = 3.0, runner: float = 8.0,
runner_stop: float = 2.0, maxb: int = 48,
taker_share: float = TAKER_SHARE) -> pd.DataFrame:
"""前推每笔信号,返回按成交量结算的出场权重与毛收益。
每行的 `w_*` 是各出场去向占**全仓名义额**的比例,四者相加为 1。
"""
high = cdf["high"].to_numpy(float)
low = cdf["low"].to_numpy(float)
close = cdf["close"].to_numpy(float)
open_ = cdf["open"].to_numpy(float)
vol = cdf["volume"].to_numpy(float)
atr = cdf["atr"].to_numpy(float)
n = len(cdf)
out = []
for s, d in zip(sig["entry_idx"].astype(int), sig["direction"].astype(int)):
e = s + 1
if e >= n - 1:
continue
a = atr[s]
if not np.isfinite(a) or a <= 0:
continue
entry = open_[e]
cap = min(e + maxb, n - 1)
p_stop = entry - d * sl * a
p_scale = entry + d * scale_at * a
p_run = entry + d * runner * a
# 两张挂单各半仓,单位是「占全仓的比例」
rem_scale, rem_run = 0.5, 0.5
w_scale = w_run = w_stop = w_time = 0.0
scaled_any = False
stop_bar = None
for j in range(e, cap + 1):
hi, lo = high[j], low[j]
# 同根内止损优先,与 walk_exits 一致,宁可低估
hit_stop = (lo <= p_stop) if d == 1 else (hi >= p_stop)
if hit_stop:
stop_bar = j
w_stop = rem_scale + rem_run
rem_scale = rem_run = 0.0
break
v = vol[j] * close[j] * taker_share
is_long = d == 1
if rem_scale > 0:
got = avail_at(p_scale, hi, lo, v, is_long)
fill = min(rem_scale, got / notional) if notional > 0 else \
rem_scale
if fill > 0:
rem_scale -= fill
w_scale += fill
scaled_any = True
if rem_run > 0:
got = avail_at(p_run, hi, lo, v, is_long)
fill = min(rem_run, got / notional) if notional > 0 else \
rem_run
if fill > 0:
rem_run -= fill
w_run += fill
if rem_scale <= 1e-12 and rem_run <= 1e-12:
break
# 减仓成交后,剩余半仓的止损位可以另设;此处 runner_stop 等于初始 SL
# 即止损不动,与 step42 的 k=2.0 一致,故上面那个统一止损已覆盖
left = rem_scale + rem_run
if left > 1e-12 and stop_bar is None:
w_time = left
r_scale = d * (p_scale - entry) / entry
r_run = d * (p_run - entry) / entry
r_stop = d * (p_stop - entry) / entry
r_time = d * (close[cap] - entry) / entry
gross = (w_scale * r_scale + w_run * r_run
+ w_stop * r_stop + w_time * r_time)
# 入场整仓 taker;两张挂单成交的部分是 maker;止损与超时是 taker
fee = (FEE_TAKER * 1.0 + FEE_MAKER * (w_scale + w_run)
+ FEE_TAKER * (w_stop + w_time))
tk = 1.0 + w_stop + w_time
out.append({"sig_idx": s, "direction": d, "atr_pct": a / entry,
"w_scale": w_scale, "w_run": w_run,
"w_stop": w_stop, "w_time": w_time,
"maker_share": w_scale + w_run,
"gross": gross, "fee": fee, "taker_notional": tk,
"scaled": int(scaled_any)})
return pd.DataFrame(out)
def budget_bp(r: pd.DataFrame) -> float:
"""盈亏平衡的单边滑点上限(bp)。与 exit_model.slip_budget 同口径。"""
if r.empty:
return float("nan")
net = r["gross"].mean() - r["fee"].mean()
return net / r["taker_notional"].mean() * 1e4
def assert_converges(cdf: pd.DataFrame, sig: pd.DataFrame, sl: float = 2.0,
scale_at: float = 3.0, runner: float = 8.0,
runner_stop: float = 2.0, maxb: int = 48,
tol: float = 1e-9) -> None:
"""仓位趋近 0 时必须逐笔收敛到 exit_model.walk_exits,否则抛错。
这个断言是必需的。首版实现里空头的可成交量恒为 0(负价格空间踩到了一个
`hi <= 0` 的守卫),后果是空头全被拖到超时收盘——而下跌段里那比 3ATR
目标赚得多,于是预算反而**偏高** 0.76bp,看上去像个合理的模型差异。
没有这条断言,这种错只会表现为「数字有点不一样」。
"""
from lib.exit_model import cfg_name, walk_exits
old = walk_exits(cdf, sig, [sl], [scale_at], [maxb], scale_at,
[runner], [runner_stop])
c = cfg_name(sl, runner, maxb, runner_stop)
new = walk_filled(cdf, sig, 1e-12, sl, scale_at, runner, runner_stop, maxb)
j = old[["sig_idx"]].copy()
j["g_old"] = old[f"{c}_g"].to_numpy()
j = j.merge(new[["sig_idx", "gross"]], on="sig_idx")
d = (j["gross"] - j["g_old"]).abs()
bad = int((d > tol).sum())
if bad:
worst = j.loc[d.idxmax()]
raise AssertionError(
f"仓位趋近 0 时应与 walk_exits 一致,但 {bad}/{len(j)} 笔不符;"
f"最大差 {d.max():.3e}sig_idx {int(worst['sig_idx'])}"
f"{worst['g_old']:.6f}{worst['gross']:.6f}")
def net_bp(r: pd.DataFrame, slip: float = SLIP) -> float:
"""扣掉手续费与滑点后的净均收益(bp)。"""
if r.empty:
return float("nan")
net = r["gross"] - r["fee"] - slip * r["taker_notional"]
return float(net.mean() * 1e4)
+28
View File
@@ -0,0 +1,28 @@
sym,notional,budget_bp,budget_bp_old,maker_share,w_stop,w_time,net_bp,n
BTC,5000.0,10.971497515124305,10.971497515124295,0.4230769230769231,0.3516483516483517,0.22527472527472528,15.724284543080632,91
BTC,10000.0,10.971497515124305,10.971497515124295,0.4230769230769231,0.3516483516483517,0.22527472527472528,15.724284543080632,91
BTC,20000.0,10.971497515124305,10.971497515124295,0.4230769230769231,0.3516483516483517,0.22527472527472528,15.724284543080632,91
BTC,50000.0,10.971497515124305,10.971497515124295,0.4230769230769231,0.3516483516483517,0.22527472527472528,15.724284543080632,91
BTC,100000.0,10.971497515124305,10.971497515124295,0.4230769230769231,0.3516483516483517,0.22527472527472528,15.724284543080632,91
BTC,200000.0,10.971497515124305,10.971497515124295,0.4230769230769231,0.3516483516483517,0.22527472527472528,15.724284543080632,91
BTC,320000.0,10.971497515124305,10.971497515124295,0.4230769230769231,0.3516483516483517,0.22527472527472528,15.724284543080632,91
BTC,530000.0,10.908448857487189,10.971497515124295,0.4214204503001539,0.3524912405185926,0.22608830918125347,15.641274735676083,91
BTC,1000000.0,10.690710190313572,10.971497515124295,0.41456048099090254,0.35467750033199696,0.23076201867710053,15.36403490298731,91
ETH,5000.0,15.336810540229132,15.336810540229127,0.4722222222222222,0.3253968253968254,0.20238095238095238,21.90346054757228,126
ETH,10000.0,15.336810540229132,15.336810540229127,0.4722222222222222,0.3253968253968254,0.20238095238095238,21.90346054757228,126
ETH,20000.0,15.336810540229132,15.336810540229127,0.4722222222222222,0.3253968253968254,0.20238095238095238,21.90346054757228,126
ETH,50000.0,15.336810540229132,15.336810540229127,0.4722222222222222,0.3253968253968254,0.20238095238095238,21.90346054757228,126
ETH,100000.0,15.336810540229132,15.336810540229127,0.4722222222222222,0.3253968253968254,0.20238095238095238,21.90346054757228,126
ETH,200000.0,15.336810540229132,15.336810540229127,0.4722222222222222,0.3253968253968254,0.20238095238095238,21.90346054757228,126
ETH,320000.0,15.336810540229132,15.336810540229127,0.4722222222222222,0.3253968253968254,0.20238095238095238,21.90346054757228,126
ETH,530000.0,15.336810540229132,15.336810540229127,0.4722222222222222,0.3253968253968254,0.20238095238095238,21.90346054757228,126
ETH,1000000.0,15.19839735083419,15.336810540229127,0.46778822061185865,0.3266775971833763,0.20553418220476508,21.75495166938152,126
SOL,5000.0,17.20729581305582,17.207295813055815,0.49310344827586206,0.2655172413793103,0.2413793103448276,24.42271817346687,145
SOL,10000.0,17.20729581305582,17.207295813055815,0.49310344827586206,0.2655172413793103,0.2413793103448276,24.42271817346687,145
SOL,20000.0,17.20729581305582,17.207295813055815,0.49310344827586206,0.2655172413793103,0.2413793103448276,24.42271817346687,145
SOL,50000.0,17.12169148018871,17.207295813055815,0.49112524587410755,0.26723407222095347,0.24164068190493895,24.325613268263233,145
SOL,100000.0,17.024617862994077,17.207295813055815,0.4874068783573466,0.2680997947311664,0.24449332691148706,24.238726756516837,145
SOL,200000.0,16.83524812354264,17.207295813055815,0.4813611824188418,0.26935037627246794,0.24928844130869035,24.048022486441045,145
SOL,320000.0,16.524586307202075,17.207295813055815,0.4732031722818229,0.27277272324336915,0.254024104474808,23.702889125473174,145
SOL,530000.0,16.275686387067427,17.207295813055815,0.4660589094844057,0.2753631016069268,0.2585779889086674,23.432003034952427,145
SOL,1000000.0,15.829170691791866,17.207295813055815,0.4547125585779213,0.27881565544002057,0.2664717859820582,22.915331236730324,145
1 sym notional budget_bp budget_bp_old maker_share w_stop w_time net_bp n
2 BTC 5000.0 10.971497515124305 10.971497515124295 0.4230769230769231 0.3516483516483517 0.22527472527472528 15.724284543080632 91
3 BTC 10000.0 10.971497515124305 10.971497515124295 0.4230769230769231 0.3516483516483517 0.22527472527472528 15.724284543080632 91
4 BTC 20000.0 10.971497515124305 10.971497515124295 0.4230769230769231 0.3516483516483517 0.22527472527472528 15.724284543080632 91
5 BTC 50000.0 10.971497515124305 10.971497515124295 0.4230769230769231 0.3516483516483517 0.22527472527472528 15.724284543080632 91
6 BTC 100000.0 10.971497515124305 10.971497515124295 0.4230769230769231 0.3516483516483517 0.22527472527472528 15.724284543080632 91
7 BTC 200000.0 10.971497515124305 10.971497515124295 0.4230769230769231 0.3516483516483517 0.22527472527472528 15.724284543080632 91
8 BTC 320000.0 10.971497515124305 10.971497515124295 0.4230769230769231 0.3516483516483517 0.22527472527472528 15.724284543080632 91
9 BTC 530000.0 10.908448857487189 10.971497515124295 0.4214204503001539 0.3524912405185926 0.22608830918125347 15.641274735676083 91
10 BTC 1000000.0 10.690710190313572 10.971497515124295 0.41456048099090254 0.35467750033199696 0.23076201867710053 15.36403490298731 91
11 ETH 5000.0 15.336810540229132 15.336810540229127 0.4722222222222222 0.3253968253968254 0.20238095238095238 21.90346054757228 126
12 ETH 10000.0 15.336810540229132 15.336810540229127 0.4722222222222222 0.3253968253968254 0.20238095238095238 21.90346054757228 126
13 ETH 20000.0 15.336810540229132 15.336810540229127 0.4722222222222222 0.3253968253968254 0.20238095238095238 21.90346054757228 126
14 ETH 50000.0 15.336810540229132 15.336810540229127 0.4722222222222222 0.3253968253968254 0.20238095238095238 21.90346054757228 126
15 ETH 100000.0 15.336810540229132 15.336810540229127 0.4722222222222222 0.3253968253968254 0.20238095238095238 21.90346054757228 126
16 ETH 200000.0 15.336810540229132 15.336810540229127 0.4722222222222222 0.3253968253968254 0.20238095238095238 21.90346054757228 126
17 ETH 320000.0 15.336810540229132 15.336810540229127 0.4722222222222222 0.3253968253968254 0.20238095238095238 21.90346054757228 126
18 ETH 530000.0 15.336810540229132 15.336810540229127 0.4722222222222222 0.3253968253968254 0.20238095238095238 21.90346054757228 126
19 ETH 1000000.0 15.19839735083419 15.336810540229127 0.46778822061185865 0.3266775971833763 0.20553418220476508 21.75495166938152 126
20 SOL 5000.0 17.20729581305582 17.207295813055815 0.49310344827586206 0.2655172413793103 0.2413793103448276 24.42271817346687 145
21 SOL 10000.0 17.20729581305582 17.207295813055815 0.49310344827586206 0.2655172413793103 0.2413793103448276 24.42271817346687 145
22 SOL 20000.0 17.20729581305582 17.207295813055815 0.49310344827586206 0.2655172413793103 0.2413793103448276 24.42271817346687 145
23 SOL 50000.0 17.12169148018871 17.207295813055815 0.49112524587410755 0.26723407222095347 0.24164068190493895 24.325613268263233 145
24 SOL 100000.0 17.024617862994077 17.207295813055815 0.4874068783573466 0.2680997947311664 0.24449332691148706 24.238726756516837 145
25 SOL 200000.0 16.83524812354264 17.207295813055815 0.4813611824188418 0.26935037627246794 0.24928844130869035 24.048022486441045 145
26 SOL 320000.0 16.524586307202075 17.207295813055815 0.4732031722818229 0.27277272324336915 0.254024104474808 23.702889125473174 145
27 SOL 530000.0 16.275686387067427 17.207295813055815 0.4660589094844057 0.2753631016069268 0.2585779889086674 23.432003034952427 145
28 SOL 1000000.0 15.829170691791866 17.207295813055815 0.4547125585779213 0.27881565544002057 0.2664717859820582 22.915331236730324 145
+161
View File
@@ -0,0 +1,161 @@
"""Step 43:把「限价单全额成交」的假设换成按成交量结算,重算滑点预算。
step42 的预算(BTC 8.58 / ETH 20.64 / SOL 16.83bp)建立在一个假设上:挂在
3ATR 与 8ATR 的止盈限价单全额成交在目标价。影子交易的成交流数据推翻了它——
真实仓位下全额成交率只有 30%/16%/1.5%32 万仓位)。
预算因此不再是一个常数,而是**仓位规模的函数**。规模越大,止盈越难成交,
越多仓位被拖到止损或超时(taker,且吃滑点),预算越低。这条曲线与「冲击
反推的容量」是两个不同的约束,而后者宽松得多(100~500 万 vs 数万)。
数据用 Bitget 210 天 1m,与影子测量同源同交易所。全量 366 万根峰值 24.5GB,
本机 15GB 跑不动;210 天 30 万根峰值约 2GB。
python research/step43_fill_aware_budget.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().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 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")
a = ap.parse_args()
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()