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

223 lines
8.5 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 28:小级别 × 大级别 全矩阵扫描。
此前配对都是拍脑袋定的(5m配15m、15m配1h),而远距实验里 5m 配 1h 明显好于配 15m,
说明「隔几级去挂分型」本身是个从没调过的参数。本步把它扫完。
效率关键:一笔交易的入场与出场只由小级别决定,大级别只改变过滤标签。
所以每个小级别只回测一次,各大级别的同向标记以列的形式附加上去,
18 个组合的成本接近 4 个。
"""
from __future__ import annotations
import argparse
import os
import sys
import warnings
from concurrent.futures import ProcessPoolExecutor, as_completed
from pathlib import Path
import numpy as np
import pandas as pd
warnings.filterwarnings("ignore")
for v in ("OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS"):
os.environ.setdefault(v, "1")
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
sys.path.insert(0, str(HERE.parent))
pd.set_option("display.width", 320)
SL, TP, MAXB = 1.5, 3.0, 48
FEE, SLIP = 0.0004, 0.0001
LTFS = ["5m", "15m", "30m", "1h"]
HTFS = ["15m", "30m", "1h", "2h", "4h", "1d", "1w"]
ORDER = {t: i for i, t in enumerate(["1m", "5m", "15m", "30m", "1h", "2h", "4h", "1d", "1w"])}
def run_symbol(sym: str) -> dict | None:
"""一个品种跑全部级别对。小级别结构与大级别分型各算一次后交叉组合。"""
import warnings as _w
_w.filterwarnings("ignore")
sys.path.insert(0, str(HERE))
sys.path.insert(0, str(HERE.parent))
from chanlun import TF_DF
from lib.breakout import run_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
pair = f"{sym}/USDT:USDT"
frames = []
try:
# 大级别分型时间线:每个周期只算一次
timelines = {}
for tf in HTFS:
df_h = fetch_ohlcv(pair, tf, 10**9)
if df_h is None or len(df_h) < 300:
continue
chan_h = TF_DF(df_h, 1, tf)
s = signals_to_frame(extract_fx_signals(chan_h, chan_h.dataframe))
timelines[tf] = htf_fx_timeline(s, chan_h.dataframe)
for ltf in LTFS:
df_l = fetch_ohlcv(pair, ltf, 10**9)
if df_l is None or len(df_l) < 3000:
continue
chan_l = TF_DF(df_l, 1, ltf)
cdf = chan_l.dataframe
zones = build_htf_zones(cdf, ltf, chan=chan_l).reset_index(drop=True)
if zones.empty:
continue
sig = find_fast_bsp3(cdf, zones)
if sig.empty or len(sig) < 20:
continue
# 中枢阶梯方向(只依赖小级别,与大级别无关)
z = zones.copy()
pg, pdn = z["zg"].shift(), z["zd"].shift()
z["z_above"] = z["zd"] > pg
z["z_below"] = z["zg"] < pdn
z["zone_i"] = np.arange(len(z))
sig = sig.merge(z[["zone_i", "z_above", "z_below"]], on="zone_i", how="left")
# 各大级别的同向标记,逐列附加
for tf in HTFS:
if ORDER[tf] <= ORDER[ltf] or tf not in timelines:
continue
sig = attach_htf_context(sig, cdf, timelines[tf], f"x{tf}")
entries = list(zip(sig["entry_idx"].astype(int), sig["direction"].astype(int)))
tr = run_trades(cdf, entries, SL, TP, MAXB, fee=0.0, entry_delay=1)
if tr.empty:
continue
m = sig.drop_duplicates("entry_idx").set_index("entry_idx")
tr["symbol"], tr["ltf"] = sym, ltf
tr["date"] = cdf["date"].to_numpy()[tr["entry_idx"].to_numpy()]
for c in m.columns:
if c.endswith("_agree") or c in ("z_above", "z_below", "direction"):
tr[c if c != "direction" else "dir_sig"] = tr["entry_idx"].map(m[c])
frames.append(tr)
if not frames:
return None
return {"sym": sym, "trades": pd.concat(frames, ignore_index=True)}
except Exception as e:
return {"sym": sym, "error": repr(e)[:300]}
def stat(r: np.ndarray) -> dict:
if len(r) < 30:
return {}
w, o = r[r > 0], r[r <= 0]
sd = r.std(ddof=1)
return {
"笔数": len(r), "胜率": (r > 0).mean() * 100,
"均收益": r.mean() * 100, "中位": np.median(r) * 100,
"PF": w.sum() / abs(o.sum()) if len(o) else np.inf,
"偏度": pd.Series(r).skew(),
"t值": r.mean() / (sd / np.sqrt(len(r))),
}
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--symbols", default="BTC,ETH,SOL")
ap.add_argument("--reuse", action="store_true")
args = ap.parse_args()
cache = HERE / "out" / "step28_matrix.csv"
if args.reuse and cache.exists():
allt = pd.read_csv(cache, parse_dates=["date"])
print(f"[复用] {len(allt)}\n")
else:
syms = [s.strip() for s in args.symbols.split(",") if s.strip()]
print(f"[全矩阵] {len(syms)} 个品种 × {len(LTFS)} 小级别 × {len(HTFS)} 大级别\n",
flush=True)
res = []
with ProcessPoolExecutor(max_workers=len(syms)) as ex:
futs = {ex.submit(run_symbol, s): s for s in syms}
for f in as_completed(futs):
r = f.result()
if r is None or "error" in (r or {}):
print(f" {futs[f]} 失败 {(r or {}).get('error', '')}", flush=True)
continue
res.append(r)
print(f" {r['sym']} 完成 — {len(r['trades'])} 笔", flush=True)
if not res:
return
allt = pd.concat([r["trades"] for r in res], ignore_index=True)
allt.to_csv(cache, index=False)
allt["date"] = pd.to_datetime(allt["date"])
allt["ret_net"] = allt["gross"] - FEE - SLIP
allt["push"] = np.where(allt["dir_sig"] == 1, allt["z_above"], allt["z_below"])
allt["push"] = allt["push"].fillna(False).astype(bool)
print("=" * 118)
print("########## 1. PF 矩阵:小级别(行) × 大级别分型(列),仅同向 ##########")
for metric in ("PF", "t值", "中位"):
grid = {}
for ltf, g in allt.groupby("ltf"):
for tf in HTFS:
col = f"x{tf}_agree"
if col not in g.columns:
continue
s = stat(g[g[col] == 1]["ret_net"].to_numpy())
if s:
grid.setdefault(ltf, {})[tf] = s[metric]
if not grid:
continue
df = pd.DataFrame(grid).T
df = df.reindex(index=[t for t in LTFS if t in df.index],
columns=[t for t in HTFS if t in df.columns])
print(f"\n-- {metric} --")
print(df.round(2).to_string())
print("\n########## 2. 叠加中枢阶梯方向过滤后的 PF ##########")
grid = {}
for ltf, g in allt[allt.push].groupby("ltf"):
for tf in HTFS:
col = f"x{tf}_agree"
if col not in g.columns:
continue
s = stat(g[g[col] == 1]["ret_net"].to_numpy())
if s:
grid.setdefault(ltf, {})[tf] = s["PF"]
df = pd.DataFrame(grid).T
df = df.reindex(index=[t for t in LTFS if t in df.index],
columns=[t for t in HTFS if t in df.columns])
print(df.round(2).to_string())
print("\n########## 3. 全部组合排行(按 t 值,含笔数下限)##########")
rows = []
for ltf, g in allt.groupby("ltf"):
for tf in HTFS:
col = f"x{tf}_agree"
if col not in g.columns:
continue
for pf_name, sub in (("同向", g[g[col] == 1]),
("同向+阶梯", g[(g[col] == 1) & g.push])):
s = stat(sub["ret_net"].to_numpy())
if s and s["笔数"] >= 80:
rows.append({"组合": f"{ltf}/{tf} {pf_name}", **s})
r = pd.DataFrame(rows).sort_values("t值", ascending=False)
for c in ("胜率", "均收益", "中位", "PF", "偏度", "t值"):
r[c] = r[c].round(2)
print(r.head(25).to_string(index=False))
print("\n########## 4. 每个小级别的最佳搭档 ##########")
best = []
for ltf in LTFS:
sub = r[r["组合"].str.startswith(f"{ltf}/")]
if not sub.empty:
best.append(sub.iloc[0])
if best:
print(pd.DataFrame(best).to_string(index=False))
if __name__ == "__main__":
main()