出场模型改为按成交量结算止盈限价单,并出预算对仓位规模的曲线
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:
@@ -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 = (high−P)/(high−low) 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)
|
||||||
@@ -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
|
||||||
|
@@ -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()
|
||||||
Reference in New Issue
Block a user