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