Files
Chan/research/step15_nested_bsp3.py
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

164 lines
6.5 KiB
Python
Raw Permalink 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 15:正确的区间套 —— 1h/4h 分型定位 + 15m 三类买卖点入场。
此前 step7~step14 一直把中枢放在大级别、并在大级别自己交易,级别关系搞反了。
本步按缠论正统结构重做:
大级别 1h / 4h 只出分型,负责方向与「预设转折点」(确认滞后 1 根)
小级别 15m 出中枢、出突破、出三类买卖点,负责精确入场
逐层验证:
A 全部 15m 三买三卖(基线)
B + 1h 分型方向一致
C + 4h 分型方向一致
D + 1h 与 4h 同时一致(完整区间套)
E + 大级别分型带背驰
"""
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 run_trades, summarize_trades
from lib.bsp_eval import forward_returns, run_pipeline, to_events
from lib.data import fetch_ohlcv
from lib.fx_signal import extract_fx_signals, signals_to_frame
from lib.nested_bsp import attach_htf_context, htf_fx_timeline
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, 40)
def show(rows: list[dict]) -> None:
rows = [r for r in rows if r.get("笔数", 0) > 0]
if not rows:
print(" (样本不足)")
return
print(pd.DataFrame(rows).to_string(index=False))
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--symbol", default="BTC/USDT:USDT")
ap.add_argument("--ltf", default="15m", help="小级别:出中枢与三类买卖点")
ap.add_argument("--htf1", default="1h", help="大级别一:出分型")
ap.add_argument("--htf2", default="4h", help="大级别二:出分型")
ap.add_argument("--zs", default="pure", choices=["pure", "seg"])
ap.add_argument("--sl", type=float, default=1.5)
ap.add_argument("--tp", type=float, default=3.0)
ap.add_argument("--max-bars", type=int, default=48)
args = ap.parse_args()
# ---- 小级别:三类买卖点 ----
df_l = fetch_ohlcv(args.symbol, args.ltf, 10**9)
chan_l, bsp_list = run_pipeline(df_l, args.ltf, args.zs)
cdf_l = chan_l.dataframe
ev = forward_returns(to_events(bsp_list, cdf_l), cdf_l, HORIZONS)
if ev.empty:
print("小级别无买卖点")
return
ev3 = ev[ev["bsp_type"].isin(["B3", "S3"])].reset_index(drop=True)
print(f"[小级别 {args.ltf}] {len(cdf_l)} 根K线 "
f"{cdf_l['date'].iloc[0]} -> {cdf_l['date'].iloc[-1]}")
print(f" 买卖点合计 {len(ev)} 其中三类 {len(ev3)} "
f"(B3 {(ev3.bsp_type == 'B3').sum()} / S3 {(ev3.bsp_type == 'S3').sum()})")
if not ev3.empty:
print(f" 三类买卖点确认滞后:中位 {ev3['lag_bars'].median():.0f} 根 "
f"均值 {ev3['lag_bars'].mean():.1f} 根")
# ---- 大级别:分型时间线 ----
ctx = ev3
for tf, pref in ((args.htf1, "h1"), (args.htf2, "h2")):
df_h = fetch_ohlcv(args.symbol, tf, 10**9)
chan_h = TF_DF(df_h, 1, tf)
sig_h = signals_to_frame(extract_fx_signals(chan_h, chan_h.dataframe))
tl = htf_fx_timeline(sig_h, chan_h.dataframe)
print(f"[大级别 {tf}] 分型 {len(tl)} 个")
ctx = attach_htf_context(ctx, cdf_l, tl, pref)
a1 = ctx["h1_agree"] == 1
a2 = ctx["h2_agree"] == 1
print(f"\n[方向一致率] 1h {a1.mean() * 100:.0f}% 4h {a2.mean() * 100:.0f}% "
f"两者同时 {(a1 & a2).mean() * 100:.0f}%")
entries = lambda g: list(zip(g["entry_idx"].astype(int), g["direction"].astype(int)))
print("\n########## 逐层过滤(事件驱动,sl1.5ATR tp3ATR max48##########")
rows = [
summarize_trades(run_trades(cdf_l, entries(ctx), args.sl, args.tp, args.max_bars),
"A 全部三类"),
summarize_trades(run_trades(cdf_l, entries(ctx[a1]), args.sl, args.tp, args.max_bars),
f"B +{args.htf1}分型同向"),
summarize_trades(run_trades(cdf_l, entries(ctx[a2]), args.sl, args.tp, args.max_bars),
f"C +{args.htf2}分型同向"),
summarize_trades(run_trades(cdf_l, entries(ctx[a1 & a2]), args.sl, args.tp, args.max_bars),
"D 双大级别同向"),
]
div1 = ctx["h1_div"] == 1
rows.append(summarize_trades(
run_trades(cdf_l, entries(ctx[a1 & div1]), args.sl, args.tp, args.max_bars),
f"E +{args.htf1}同向且背驰"))
# 反向对照:大级别分型与信号相悖时应当更差
rows.append(summarize_trades(
run_trades(cdf_l, entries(ctx[~a1]), args.sl, args.tp, args.max_bars),
f"F {args.htf1}分型反向(对照)"))
show(rows)
print("\n########## 大级别分型的新鲜度(距分型确认的15m根数)##########")
rows = []
for lo, hi, name in [(0, 8, "0-8根"), (8, 24, "8-24根"),
(24, 96, "24-96根"), (96, 10**9, ">96根")]:
g = ctx[a1 & (ctx["h1_age_bars"] >= lo) & (ctx["h1_age_bars"] < hi)]
if len(g) >= 15:
rows.append(summarize_trades(
run_trades(cdf_l, entries(g), args.sl, args.tp, args.max_bars), name))
show(rows)
print("\n########## 多空拆分(双大级别同向)##########")
rows = []
both = ctx[a1 & a2]
for d, nm in ((1, "做多 B3"), (-1, "做空 S3")):
g = both[both["direction"] == d]
if len(g) >= 10:
rows.append(summarize_trades(
run_trades(cdf_l, entries(g), args.sl, args.tp, args.max_bars), nm))
show(rows)
print("\n########## 固定持有期收益(不设止损,看原始 alpha##########")
rows = []
for name, g in (("A 全部三类", ctx), (f"B +{args.htf1}同向", ctx[a1]),
("D 双大级别同向", ctx[a1 & a2])):
if len(g) < 15:
continue
r = {"分组": name, "n": len(g)}
for h in HORIZONS:
v = g[f"ret_{h}"].dropna().to_numpy()
if len(v) < 10:
continue
sd = v.std(ddof=1)
r[f"{h}根"] = f"{v.mean() * 100:+.2f}%"
r[f"{h}根t"] = f"{v.mean() / (sd / np.sqrt(len(v))):+.1f}" if sd else "—"
rows.append(r)
if rows:
print(pd.DataFrame(rows).to_string(index=False))
out = Path(__file__).parent / "out" / f"step15_nested_{args.symbol.split('/')[0]}.csv"
out.parent.mkdir(exist_ok=True)
ctx.to_csv(out, index=False)
print(f"\n明细已写入 {out}")
if __name__ == "__main__":
main()