refactor: 精简仓库为 chanlun 核心与 web 分析,移除威科夫与遗留模块

删除根目录旧 Chan 模块、策略、配置、文档及 wyckoff 相关代码;更新缠论 pipeline 与笔中枢计算;补充 research 研究与 web 测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-27 01:05:12 +08:00
co-authored by Cursor
parent 5c10e35b76
commit 7f393b93ed
360 changed files with 140008 additions and 41167 deletions
+222
View File
@@ -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()