refactor: 精简仓库为 chanlun 核心与 web 分析,移除威科夫与遗留模块
删除根目录旧 Chan 模块、策略、配置、文档及 wyckoff 相关代码;更新缠论 pipeline 与笔中枢计算;补充 research 研究与 web 测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user