"""Step 1b:诊断「多空双向短期均亏」的成因。 假设:sure_time 那根确认K线本身就是一段快速行情的末端, 在它的收盘价入场等于追单,因此无论方向下一根都倾向回撤。 检验办法:看 entry bar 收盘价在其前后窗口里的分位位置。 """ from __future__ import annotations import sys from pathlib import Path import numpy as np import pandas as pd sys.path.insert(0, str(Path(__file__).resolve().parent)) from lib.bsp_eval import forward_returns, run_pipeline, to_events from lib.data import fetch_ohlcv pd.set_option("display.width", 220) def main() -> None: symbol, tf = "BTC/USDT:USDT", "1h" df = fetch_ohlcv(symbol, tf, 50000) chan, bsp_list = run_pipeline(df, tf) events = to_events(bsp_list, df) fwd = forward_returns(events, df) closes = df["close"].to_numpy(dtype=float) n = len(df) look = 12 # 回看窗口 rows = [] for ev in events: i = ev.entry_idx if i - look < 0 or i + look >= n: continue past = closes[i - look : i + 1] # 入场价在过去 look 根里的分位:1.0=最高 pct_past = float((past <= closes[i]).mean()) # 顺信号方向的分位:做多时越靠近区间高点越像追高 dir_pct = pct_past if ev.direction == 1 else 1.0 - pct_past # 确认K线自身的涨跌幅,及其相对近期波动的倍数 bar_ret = (closes[i] - closes[i - 1]) / closes[i - 1] vol = np.std(np.diff(past) / past[:-1], ddof=1) rows.append( { "bsp_type": ev.bsp_type, "side": "LONG" if ev.direction == 1 else "SHORT", "dir_pct": dir_pct, "bar_ret_signed": ev.direction * bar_ret, "bar_ret_z": ev.direction * bar_ret / vol if vol > 0 else np.nan, # 从分型点到入场点,价格已经走掉多少(信号被吃掉的部分) "move_since_fx": ev.direction * (closes[i] - closes[i - ev.lag_bars]) / closes[i - ev.lag_bars], "lag_bars": ev.lag_bars, } ) diag = pd.DataFrame(rows) print(f"[样本] n={len(diag)}\n") print("[入场价在过去12根中的顺向分位] 0.5=中性, >0.5=追单(做多买在高位/做空卖在低位)") print(f" 全体均值 = {diag['dir_pct'].mean():.3f} 中位数 = {diag['dir_pct'].median():.3f}") print(diag.groupby("side")["dir_pct"].agg(["count", "mean", "median"]).to_string()) print() print("[确认K线自身的顺向涨跌] 正=确认那根K线就在朝信号方向猛冲") print(f" 均值 = {diag['bar_ret_signed'].mean() * 100:+.3f}% z均值 = {diag['bar_ret_z'].mean():+.2f}") print(diag.groupby("side")["bar_ret_signed"].agg(["mean", "median"]).map(lambda v: f"{v*100:+.3f}%").to_string()) print() print("[从分型点到确认点,价格已顺向走掉多少] 这是滞后吃掉的利润") print(f" 均值 = {diag['move_since_fx'].mean() * 100:+.2f}% 中位数 = {diag['move_since_fx'].median() * 100:+.2f}%") print( diag.groupby("bsp_type")[["move_since_fx", "lag_bars"]] .agg({"move_since_fx": "mean", "lag_bars": "mean"}) .assign(move_since_fx=lambda d: (d["move_since_fx"] * 100).map(lambda v: f"{v:+.2f}%")) .to_string() ) print() # 对照:若能在分型点入场(理想但不可实现),收益如何 ideal = [] for ev in events: i = ev.entry_idx - ev.lag_bars # 分型所在K线 if i < 0: continue p0 = closes[i] for h in (1, 5, 10, 20, 40): j = i + h if j < n: ideal.append({"h": h, "ret": ev.direction * (closes[j] - p0) / p0}) idf = pd.DataFrame(ideal) print("[对照] 假想在分型点入场(含未来函数,仅作上界参考)") print( idf.groupby("h")["ret"] .agg(n="count", mean="mean", winrate=lambda s: (s > 0).mean()) .assign(mean=lambda d: (d["mean"] * 100).map(lambda v: f"{v:+.2f}%"), winrate=lambda d: (d["winrate"] * 100).map(lambda v: f"{v:.0f}%")) .to_string() ) if __name__ == "__main__": main()