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

206 lines
7.6 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 19:区间套在不同级别对上的一致性。
15m/1h+4h 只有 57 笔核心样本,无法定论。但区间套是级别无关的结构,
把整套上移一级(1h 小级别有 2524 天,是 15m 的两倍)既能扩样本,
又能验证它究竟是普适结构还是只在某一个级别对上凑巧成立。
级别对:
5m / 15m + 1h
15m / 1h + 4h step16~17 已测)
1h / 4h + 1d
"""
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.data import fetch_ohlcv
from lib.fast_bsp3 import find_fast_bsp3
from lib.fx_signal import extract_fx_signals, signals_to_frame
from lib.nested_bsp import attach_htf_context, htf_fx_timeline
from lib.nested_level import build_htf_zones
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from chanlun import TF_DF
pd.set_option("display.width", 260)
SYMBOLS = ["BTC/USDT:USDT", "ETH/USDT:USDT", "SOL/USDT:USDT"]
DEFAULT_PAIRS = "5m:15m:1h,15m:1h:4h,1h:4h:1d"
SL, TP, MAXB = 1.5, 3.0, 48
def E(g):
return list(zip(g["entry_idx"].astype(int), g["direction"].astype(int)))
# 同一 (品种, 周期) 的 pipeline 在多个级别对之间反复出现,缓存后能省掉大部分耗时
_engine_cache: dict = {}
_timeline_cache: dict = {}
def _engine(symbol: str, tf: str, min_rows: int):
key = (symbol, tf)
if key not in _engine_cache:
try:
df = fetch_ohlcv(symbol, tf, 10**9)
except Exception:
df = None
if df is None or len(df) < min_rows:
_engine_cache[key] = None
else:
_engine_cache[key] = TF_DF(df, 1, tf)
return _engine_cache[key]
def _timeline(symbol: str, tf: str):
key = (symbol, tf)
if key not in _timeline_cache:
chan = _engine(symbol, tf, 300)
if chan is None:
_timeline_cache[key] = None
else:
s = signals_to_frame(extract_fx_signals(chan, chan.dataframe))
_timeline_cache[key] = htf_fx_timeline(s, chan.dataframe)
return _timeline_cache[key]
def build(symbol: str, ltf: str, htf1: str, htf2: str):
chan_l = _engine(symbol, ltf, 3000)
if chan_l is None:
return None, None
cdf_l = chan_l.dataframe
sig = find_fast_bsp3(cdf_l, build_htf_zones(cdf_l, ltf, chan=chan_l))
if sig.empty:
return cdf_l, None
for tf, pref in ((htf1, "h1"), (htf2, "h2")):
tl = _timeline(symbol, tf)
if tl is None or tl.empty:
continue
sig = attach_htf_context(sig, cdf_l, tl, pref)
return cdf_l, sig
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--core-only", action="store_true",
help="只输出核心组合(大级别同向 + 浅回抽)")
ap.add_argument("--pairs", default=DEFAULT_PAIRS,
help="级别对,格式 小级别:大级别1:大级别2,逗号分隔")
ap.add_argument("--symbols", default="BTC,ETH,SOL")
args = ap.parse_args()
global SYMBOLS
SYMBOLS = [f"{s.strip()}/USDT:USDT" for s in args.symbols.split(",") if s.strip()]
pairs = [tuple(p.split(":")) for p in args.pairs.split(",") if p]
all_rows, pool = [], []
for ltf, h1, h2 in pairs:
print(f"\n{'=' * 100}")
print(f"级别对:小级别 {ltf} 大级别 {h1} + {h2}")
rows = []
pair_pool = []
for sym in SYMBOLS:
cdf, sig = build(sym, ltf, h1, h2)
if sig is None or sig.empty or len(sig) < 10:
continue
tr = run_trades(cdf, E(sig), SL, TP, MAXB)
if tr.empty:
continue
s = summarize_trades(tr, f"{sym.split('/')[0]:>4} 全部")
s["滞后"] = f"{sig['lag'].median():.0f}"
rows.append(s)
m = sig.set_index("entry_idx")
tr = tr.copy()
tr["symbol"] = sym.split("/")[0]
tr["pair"] = f"{ltf}/{h1}"
tr["date"] = cdf["date"].to_numpy()[tr["entry_idx"].to_numpy()]
tr["h1_agree"] = tr["entry_idx"].map(m["h1_agree"]) if "h1_agree" in m else np.nan
tr["depth"] = tr["entry_idx"].map(m["depth"])
pair_pool.append(tr)
if not pair_pool:
print(" 样本不足")
continue
if rows and not args.core_only:
print(pd.DataFrame(rows).to_string(index=False))
pt = pd.concat(pair_pool, ignore_index=True)
pool.append(pt)
a1 = pt["h1_agree"] == 1
dq = pt["depth"].quantile(0.66)
core = pt[a1 & (pt["depth"] >= dq)]
sub = [summarize_trades(pt, f"{ltf} 全部")]
if len(pt[a1]) >= 20:
sub.append(summarize_trades(pt[a1], f"{ltf} +{h1}同向"))
if len(core) >= 15:
sub.append(summarize_trades(core, f"{ltf} 核心(同向+浅回抽)"))
if len(pt[~a1]) >= 15:
sub.append(summarize_trades(pt[~a1], f"{ltf} 反向(对照)"))
print(pd.DataFrame(sub).to_string(index=False))
if len(core) >= 15:
r = core["ret"].to_numpy()
all_rows.append({
"级别对": f"{ltf} / {h1}+{h2}", "核心笔数": len(r),
"胜率": f"{(r > 0).mean() * 100:.1f}%",
"均收益": f"{r.mean() * 100:+.3f}%",
"中位数": f"{np.median(r) * 100:+.3f}%",
"PF": f"{r[r > 0].sum() / abs(r[r <= 0].sum()):.2f}",
"偏度": f"{pd.Series(r).skew():.2f}",
"t值": f"{r.mean() / (r.std(ddof=1) / np.sqrt(len(r))):+.2f}",
})
print(f"\n{'=' * 100}")
print("########## 跨级别对:核心组合汇总 ##########")
if all_rows:
print(pd.DataFrame(all_rows).to_string(index=False))
if pool:
allp = pd.concat(pool, ignore_index=True)
a1 = allp["h1_agree"] == 1
cores = []
for p, g in allp.groupby("pair"):
dq = g["depth"].quantile(0.66)
cores.append(g[(g["h1_agree"] == 1) & (g["depth"] >= dq)])
core_all = pd.concat(cores, ignore_index=True)
r = core_all["ret"].to_numpy()
print(f"\n 三个级别对合并核心样本 {len(r)} 笔:")
print(f" 胜率 {(r > 0).mean() * 100:.1f}% 均收益 {r.mean() * 100:+.3f}% "
f"中位数 {np.median(r) * 100:+.3f}%")
print(f" PF {r[r > 0].sum() / abs(r[r <= 0].sum()):.2f} "
f"偏度 {pd.Series(r).skew():.2f} "
f"t值 {r.mean() / (r.std(ddof=1) / np.sqrt(len(r))):+.2f}")
for k in (2, 5, 10):
v = r[r <= np.quantile(r, 1 - k / 100)]
print(f" 剔除最赚{k:>2}%: PF {v[v > 0].sum() / abs(v[v <= 0].sum()):.2f} "
f"t {v.mean() / (v.std(ddof=1) / np.sqrt(len(v))):+.2f} "
f"中位 {np.median(v) * 100:+.3f}%")
core_all["year"] = pd.to_datetime(core_all["date"]).dt.year
print("\n 核心样本分年:")
rows = [{"年份": y, "笔数": len(g), "胜率": f"{(g.ret > 0).mean() * 100:.0f}%",
"均收益": f"{g.ret.mean() * 100:+.3f}%",
"PF": f"{g.ret[g.ret > 0].sum() / abs(g.ret[g.ret <= 0].sum()):.2f}"
if (g.ret <= 0).any() else "inf"}
for y, g in core_all.groupby("year") if len(g) >= 8]
print(pd.DataFrame(rows).to_string(index=False))
out = Path(__file__).parent / "out" / "step19_level_pairs.csv"
out.parent.mkdir(exist_ok=True)
allp.to_csv(out, index=False)
print(f"\n明细已写入 {out}")
if __name__ == "__main__":
main()