影子测量改用框架吃单原语,并补齐容量与 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:
@@ -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()
|
||||
Reference in New Issue
Block a user