删除根目录旧 Chan 模块、策略、配置、文档及 wyckoff 相关代码;更新缠论 pipeline 与笔中枢计算;补充 research 研究与 web 测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
206 lines
7.6 KiB
Python
206 lines
7.6 KiB
Python
"""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()
|