Files
Chan/research/live/penetration.py
T
jackandCursor 181bca303f 影子测量改用框架吃单原语,并补齐容量与 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>
2026-08-28 02:39:47 +08:00

153 lines
5.9 KiB
Python
Raw 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.
"""止盈位被首次触及那一根,价格穿透了多深。
为什么要这个数:影子成交流显示,限价单若正好落在某根的最高价,该价位之上
的主动买成交额只有几十到几千美元——对十万量级的仓位等于不成交。但那是
最坏情形。真实成交率取决于**止盈位被穿透了多深**:若价格一路冲过目标,
成交没问题;若只是上影线点一下就回落,就成交不了。
这个分布不需要再采数据,历史 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()