Files
Chan/research/step11_breakout_follow.py
T
jackyu66gitandCursor 7f393b93ed refactor: 精简仓库为 chanlun 核心与 web 分析,移除威科夫与遗留模块
删除根目录旧 Chan 模块、策略、配置、文档及 wyckoff 相关代码;更新缠论 pipeline 与笔中枢计算;补充 research 研究与 web 测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 01:05:12 +08:00

150 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.
"""Step 11:突破跟随(分型失败 / 中枢突破),叠加已验证的波动率信号。
前十步确立:方向不可预测(AUC 0.52),但大波动可预测(AUC 0.689)。
反转逻辑已被否定,本步测其镜像——分型转折失败即为趋势延续,
以及缠论正统的「离开中枢做趋势」。
"""
from __future__ import annotations
import argparse
import sys
import warnings
from pathlib import Path
import numpy as np
import pandas as pd
warnings.filterwarnings("ignore")
sys.path.insert(0, str(Path(__file__).resolve().parent))
from lib.breakout import find_breakout_entries, run_trades, summarize_trades
from lib.data import fetch_ohlcv
from lib.fx_signal import add_forward_returns, extract_fx_signals, signals_to_frame
from lib.nested_level import annotate_position, build_htf_zones
from step9_endpoint_predictability import build_features
from step10_direct_return_label import ALL_FEATS, walk_forward
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from chanlun import TF_DF
pd.set_option("display.width", 260)
HORIZONS = (3, 5, 10, 20)
def zs_breakout_entries(df: pd.DataFrame, zones: pd.DataFrame, window: int = 200):
"""中枢突破:价格自内部向外突破 zg/zd 的第一根。"""
if zones.empty:
return []
ts = df["timestamp"].to_numpy()
close = df["close"].to_numpy(dtype=float)
n = len(df)
entries = []
for _, z in zones.iterrows():
start = int(np.searchsorted(ts, z["available_ts"], side="left"))
was_inside = False
for j in range(start, min(start + window, n)):
c = close[j]
if z["zd"] <= c <= z["zg"]:
was_inside = True
continue
if not was_inside:
continue
entries.append((j, 1 if c > z["zg"] else -1))
break
return entries
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--symbol", default="BTC/USDT:USDT")
ap.add_argument("--tf", default="1h")
ap.add_argument("--htf", default="4h")
ap.add_argument("--window", type=int, default=10, help="分型后等待突破的K线数")
args = ap.parse_args()
df = fetch_ohlcv(args.symbol, args.tf, 10**9)
chan = TF_DF(df, 1, args.tf)
cdf = chan.dataframe
sig = signals_to_frame(extract_fx_signals(chan, cdf))
sig = add_forward_returns(sig, cdf, HORIZONS)
zones = build_htf_zones(fetch_ohlcv(args.symbol, args.htf, 10**9), args.htf)
sig = annotate_position(sig, cdf, zones, tol=0.015)
print(f"[样本] {len(cdf)} 根K线 {cdf['date'].iloc[0]} -> {cdf['date'].iloc[-1]}")
print(f"[分型] {len(sig)} [大级别中枢] {len(zones)}\n")
bo = find_breakout_entries(sig, cdf, window=args.window, mode="fail")
rev = find_breakout_entries(sig, cdf, window=args.window, mode="reverse")
zsb = zs_breakout_entries(cdf, zones)
print(f"[入场点] 分型失败突破 {len(bo)} 反转对照 {len(rev)} 中枢突破 {len(zsb)}\n")
print("########## 1. 突破跟随 vs 反转对照(sl=1.5ATR tp=3ATR max=48##########")
rows = [
summarize_trades(run_trades(cdf, bo, 1.5, 3.0, 48), "分型失败突破"),
summarize_trades(run_trades(cdf, rev, 1.5, 3.0, 48), "分型反转(对照)"),
]
if zsb:
rows.append(summarize_trades(run_trades(cdf, zsb, 1.5, 3.0, 48), "中枢突破"))
print(pd.DataFrame(rows).to_string(index=False))
print("\n########## 2. 止损止盈网格(分型失败突破)##########")
rows = []
for sl in (1.0, 1.5, 2.0):
for tp in (2.0, 3.0, 5.0):
rows.append(summarize_trades(
run_trades(cdf, bo, sl, tp, 48), f"sl{sl} tp{tp}"))
print(pd.DataFrame(rows).to_string(index=False))
print("\n########## 3. 移动止损(trailing##########")
rows = []
for sl in (1.0, 1.5, 2.0, 3.0):
rows.append(summarize_trades(
run_trades(cdf, bo, sl, 99.0, 96, trail=True), f"trail {sl}ATR"))
print(pd.DataFrame(rows).to_string(index=False))
# ---- 叠加波动率模型:只在预测到大波动时才跟随突破 ----
print("\n########## 4. 叠加波动率过滤(标签C3根内 >1% 波动,AUC~0.69##########")
f = build_features(sig, cdf)
for c in ("near_support", "near_resistance", "inside_zone"):
f[c] = f[c].astype(float)
f = f.dropna(subset=[f"ret_{h}" for h in HORIZONS]).reset_index(drop=True)
y = (f["ret_3"].to_numpy() > 0.01).astype(int)
X = f[ALL_FEATS].to_numpy(dtype=float)
oof = walk_forward(X, y, folds=5, purge=60)
f["volp"] = oof
ev = f.dropna(subset=["volp"])
print(f" 样本外 {len(ev)} 个分型带波动率预测")
# 把预测概率挂回突破入场点(按其来源分型)
prob_by_confirm = dict(zip(ev["confirm_idx"].astype(int), ev["volp"]))
bo_with_p = []
for _, r in sig.iterrows():
c0 = int(r["confirm_idx"])
if c0 not in prob_by_confirm:
continue
sub = find_breakout_entries(pd.DataFrame([r]), cdf, args.window, "fail")
for e, d in sub:
bo_with_p.append((e, d, prob_by_confirm[c0]))
rows = []
if bo_with_p:
probs = np.array([p for _, _, p in bo_with_p])
for q, name in [(0.0, "全部"), (0.5, "top50%"), (0.7, "top30%"), (0.9, "top10%")]:
thr = np.quantile(probs, q)
sel = [(e, d) for e, d, p in bo_with_p if p >= thr]
if len(sel) < 30:
continue
rows.append(summarize_trades(run_trades(cdf, sel, 1.5, 3.0, 48), f"波动率{name}"))
print(pd.DataFrame(rows).to_string(index=False))
print("\n########## 5. 多空拆分(分型失败突破,sl1.5 tp3##########")
rows = []
for d, name in [(1, "做多"), (-1, "做空")]:
sel = [(e, dd) for e, dd in bo if dd == d]
if sel:
rows.append(summarize_trades(run_trades(cdf, sel, 1.5, 3.0, 48), name))
print(pd.DataFrame(rows).to_string(index=False))
if __name__ == "__main__":
main()