影子测量改用框架吃单原语,并补齐容量与 maker 成交率两项测算

吃单查询换成 Hummingbot 的 OrderBook.get_vwap_for_volume:手写的 walk_book
返回的是按计价币吃单的加权均价,但框架的 get_price_for_quote_volume 返回
边际价、get_vwap_for_volume 收基础币量,两者语义不同。改为按基础币下单
(真实委托与 PositionExecutor.amount 均是基础币计价),深度不足由
query_volume/result_volume 判定,框架此时返回 nan 而非一个看似正常的
部分成交均价。

落盘完整盘口(双边 50 档)。此前只记三个固定名义额的成交价,这批数据的
寿命就等于那几个档位的寿命;存完整深度后任意资金量级的冲击都能离线重算。
仓位档同时从 1k/5k/20k 提到十万量级,此前低估真实仓位约两个数量级。

订阅成交流,按根按价位聚合。买卖分开存——多头在目标位挂卖出靠主动买盘
成交,混在一起会把成交率高估约一倍。BTC 每根总成交额中位与 210 天历史
的 volume×close 差 0.3%,可确认采集完整。

新增两项测算:
- 冲击不是绑定约束。32 万仓位单边冲击 0.19~2.39bp,对 8.58~20.64bp 的
  预算只占 1.6~14.2%,冲击反推的资金上限 100~500 万。
- maker 成交率才是。止盈位被首次触及时,限价在该根价格区间中的位置
  中位 k=0.28(63.9 万次触及,三币一致);合并每根成交额后,32 万仓位
  的全额成交率仅 30.1%/15.6%/1.5%。要 80% 全额成交,仓位须 ≤ 4.7 万
  /1.4 万/0.26 万——比冲击反推的上限低 40~370 倍。

回测把这些止盈按「全额成交在目标价」计,故预算所依据的收益流本身需重估。

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 6259f7380b
commit 181bca303f
10 changed files with 1503 additions and 174 deletions
+152
View File
@@ -0,0 +1,152 @@
"""止盈位被首次触及那一根,价格穿透了多深。
为什么要这个数:影子成交流显示,限价单若正好落在某根的最高价,该价位之上
的主动买成交额只有几十到几千美元——对十万量级的仓位等于不成交。但那是
最坏情形。真实成交率取决于**止盈位被穿透了多深**:若价格一路冲过目标,
成交没问题;若只是上影线点一下就回落,就成交不了。
这个分布不需要再采数据,历史 K 线里就有:给定入场价与 ATR,目标位是
`entry + T×ATR`,找到首次 `high ≥ target` 的那根,穿透深度就是
`high − target`。把它折成「占该根价格区间的比例」,就能直接对上成交流
那条「≥ 限价的成交额 vs 限价在区间中的位置」曲线。
口径与 lib/exit_model.walk_exits 对齐:入场取信号次根开盘价,ATR 取信号
根的 Wilder ATR-14,上限 48 根。
**取样方式的局限**:这里用全体 K 线做候选入场点,而非真实的三滤网信号。
真实信号是按结构条件挑出来的,入场时刻可能与波动率状态相关。以 ATR 归一
后的穿透深度对波动率状态应当不敏感,但要精确到信号级别,得重跑一次
step42 的缠论链路(单币约 370s、峰值 24.5GB)。
"""
from __future__ import annotations
import argparse
from pathlib import Path
import numpy as np
import pandas as pd
SCALE_AT = 3.0 # 分批减仓位
RUNNER = 8.0 # 剩余半仓目标
MAX_BARS = 48
def wilder_atr(high, low, close, period: int = 14) -> np.ndarray:
"""与 chanlun.indicators.ta.ATR 逐位一致的 Wilder ATR。"""
n = high.size
out = np.full(n, np.nan)
if n <= period:
return out
prev_close = close[:-1]
tr = np.maximum.reduce([high[1:] - low[1:],
np.abs(high[1:] - prev_close),
np.abs(low[1:] - prev_close)])
sm = np.empty(tr.size)
sm[period - 1] = tr[:period].mean()
a = 1.0 / period
for k in range(period, tr.size):
sm[k] = sm[k - 1] + a * (tr[k] - sm[k - 1])
out[period:] = sm[period - 1:]
return out
def penetration(df: pd.DataFrame, target_atr: float,
stride: int = 1) -> pd.DataFrame:
"""对每个候选入场点,求首次触及 `target_atr` 时的穿透深度。
只统计**触及了**的那些(未触及的属止损或超时出场,不涉及 maker 腿)。
"""
high = df["high"].to_numpy(float)
low = df["low"].to_numpy(float)
close = df["close"].to_numpy(float)
open_ = df["open"].to_numpy(float)
atr = wilder_atr(high, low, close)
n = len(df)
rows = []
for i in range(20, n - MAX_BARS - 2, stride):
a = atr[i]
if not np.isfinite(a) or a <= 0:
continue
e = i + 1
entry = open_[e]
# 多头:目标在上方。空头对称,穿透深度分布按对称性等价,故只算一边
target = entry + target_atr * a
end = e + MAX_BARS
seg_hi = high[e:end + 1]
hit = np.flatnonzero(seg_hi >= target)
if not hit.size:
continue
j = e + int(hit[0])
rng = high[j] - low[j]
if rng <= 0:
continue
pen = high[j] - target
rows.append({
"bar": j,
# 限价在该根价格区间中的位置:0 = 正好在最高价(最坏),
# 1 = 在最低价(该根全部成交都在限价之上)
"k": min(1.0, pen / rng),
"pen_bp": pen / target * 1e4,
"pen_atr": pen / a,
"range_bp": rng / target * 1e4,
})
return pd.DataFrame(rows)
def report(sym: str, df: pd.DataFrame) -> dict:
print(f"\n{'=' * 68}\n{sym}{len(df):,} 根 1m")
out = {}
for tgt, name in ((SCALE_AT, f"减仓位 {SCALE_AT:g}ATR"),
(RUNNER, f"目标位 {RUNNER:g}ATR")):
p = penetration(df, tgt)
if p.empty:
print(f" {name}: 无触及样本")
continue
k = p["k"].to_numpy()
print(f"\n {name} · 触及 {len(p):,}")
print(f" 穿透深度 中位 {p['pen_bp'].median():.2f}bp "
f"({p['pen_atr'].median():.2f} ATR) · "
f"P25 {p['pen_bp'].quantile(.25):.2f}bp · "
f"P75 {p['pen_bp'].quantile(.75):.2f}bp")
print(f" 限价在区间中的位置 k(0=正好在最高价,越大越靠下越易成交)")
print(f" 中位 {np.median(k):.3f} · P10 {np.percentile(k, 10):.3f}"
f" · P25 {np.percentile(k, 25):.3f}"
f" · P75 {np.percentile(k, 75):.3f}")
for thr in (0.05, 0.10, 0.25, 0.50):
print(f" k ≤ {thr:.2f}(限价挤在该根顶部 {thr * 100:.0f}% 内):"
f"{float((k <= thr).mean()) * 100:5.1f}% 的触及")
out[tgt] = p
return out
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/penetration.csv")
a = ap.parse_args()
root = Path(a.cache)
allp = []
for sym in a.syms.split(","):
cands = sorted(root.glob(f"bitget_{sym}_1m_*.feather"),
key=lambda p: p.stat().st_size, reverse=True)
if not cands:
print(f"{sym}: 找不到 1m 缓存,跳过")
continue
df = pd.read_feather(cands[0])
got = report(sym, df)
for tgt, p in got.items():
p = p.copy()
p["sym"], p["target_atr"] = sym, tgt
allp.append(p)
if allp:
out = pd.concat(allp, ignore_index=True)
Path(a.save).parent.mkdir(parents=True, exist_ok=True)
out.to_csv(a.save, index=False)
print(f"\n已存 {a.save}{len(out):,} 行),"
f"供 shadow_depth.py 合并成交量曲线")
if __name__ == "__main__":
main()