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
+239
View File
@@ -0,0 +1,239 @@
"""Step 28:小级别 × 大级别的全网格扫描。
此前的配对(15m配1h、30m配2h)是拍脑袋定的,而 step26 意外发现
5m 配 1h 明显好过配 15m —— 说明「大级别该拉多远」本身是个未扫过的维度。
关键省算:大级别只充当过滤标记,不改变入场点,所以每个小级别的
中枢/信号/回测只跑一次,然后把所有候选大级别一次性挂上去。
"""
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
LTF_HTFS = {
"5m": ["15m", "30m", "1h", "2h", "4h"],
"15m": ["30m", "1h", "2h", "4h", "1d"],
"30m": ["1h", "2h", "4h", "1d"],
"1h": ["2h", "4h", "1d", "1w"],
}
def run_one(task: tuple) -> 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
sym, ltf = task
htfs = LTF_HTFS[ltf]
try:
df_l = fetch_ohlcv(f"{sym}/USDT:USDT", ltf, 10**9)
if df_l is None or len(df_l) < 3000:
return None
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:
return None
sig = find_fast_bsp3(cdf, zones)
if sig.empty or len(sig) < 10:
return None
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:
df_h = fetch_ohlcv(f"{sym}/USDT:USDT", tf, 10**9)
if df_h is None or len(df_h) < 200:
continue
chan_h = TF_DF(df_h, 1, tf)
s = signals_to_frame(extract_fx_signals(chan_h, chan_h.dataframe))
if s.empty:
continue
sig = attach_htf_context(sig, cdf, htf_fx_timeline(s, chan_h.dataframe),
f"p{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:
return None
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 ["z_above", "z_below"] + [f"p{tf}_agree" for tf in htfs]:
tr[c] = tr["entry_idx"].map(m[c]) if c in m.columns else np.nan
return {"task": f"{sym} {ltf}", "trades": tr}
except Exception as e:
return {"task": f"{sym} {ltf}", "error": repr(e)[:200]}
def stat(r: np.ndarray) -> dict | None:
if len(r) < 40:
return None
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("--ltfs", default="5m,15m,30m,1h")
ap.add_argument("--workers", type=int, default=6)
ap.add_argument("--reuse", action="store_true")
args = ap.parse_args()
cache = HERE / "out" / "step28_grid.csv"
if args.reuse and cache.exists():
allt = pd.read_csv(cache, parse_dates=["date"])
print(f"[复用] {len(allt)}\n")
else:
tasks = [(s, l) for l in args.ltfs.split(",") for s in args.symbols.split(",")]
print(f"[网格] {len(tasks)} 个任务,每个内部挂多个大级别\n", flush=True)
res = []
with ProcessPoolExecutor(max_workers=args.workers) as ex:
futs = {ex.submit(run_one, t): t for t in tasks}
for i, f in enumerate(as_completed(futs), 1):
r = f.result()
if r is None or "error" in (r or {}):
print(f" [{i}] 跳过 {(r or {}).get('error', '')}", flush=True)
continue
res.append(r)
print(f" [{i}/{len(tasks)}] {r['task']}{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["direction"] == 1,
allt["z_above"], allt["z_below"]).astype(bool)
print("=" * 120)
print("########## 1. 全网格:小级别 × 大级别(仅大级别同向)##########")
rows = []
for ltf, g in allt.groupby("ltf"):
for tf in LTF_HTFS.get(ltf, []):
col = f"p{tf}_agree"
if col not in g.columns:
continue
s = stat(g[g[col] == 1]["ret_net"].to_numpy())
if s:
rows.append({"小级别": ltf, "大级别": tf, **s})
df = pd.DataFrame(rows)
if df.empty:
print("样本不足")
return
for c, f in (("胜率", "{:.1f}%"), ("均收益", "{:+.3f}%"), ("中位", "{:+.3f}%"),
("PF", "{:.2f}"), ("偏度", "{:.2f}"), ("t值", "{:+.2f}")):
df[c] = df[c].map(f.format)
print(df.to_string(index=False))
print("\n########## 2. 加上顺向推进过滤后的全网格 ##########")
rows = []
for ltf, g in allt[allt.push].groupby("ltf"):
for tf in LTF_HTFS.get(ltf, []):
col = f"p{tf}_agree"
if col not in g.columns:
continue
s = stat(g[g[col] == 1]["ret_net"].to_numpy())
if s:
rows.append({"小级别": ltf, "大级别": tf, **s})
df2 = pd.DataFrame(rows)
if not df2.empty:
best = df2.sort_values("PF", ascending=False).head(3)
for c, f in (("胜率", "{:.1f}%"), ("均收益", "{:+.3f}%"), ("中位", "{:+.3f}%"),
("PF", "{:.2f}"), ("偏度", "{:.2f}"), ("t值", "{:+.2f}")):
df2[c] = df2[c].map(f.format)
print(df2.to_string(index=False))
print(f"\n PF 前三:{', '.join(f'{r.小级别}/{r.大级别}' for r in best.itertuples())}")
print("\n########## 3. 每个小级别的最优大级别(按 t 值,中位需为正)##########")
rows = []
for ltf, g in allt[allt.push].groupby("ltf"):
cand = []
for tf in LTF_HTFS.get(ltf, []):
col = f"p{tf}_agree"
if col not in g.columns:
continue
s = stat(g[g[col] == 1]["ret_net"].to_numpy())
if s and s["中位"] > 0:
cand.append((tf, s))
if cand:
tf, s = max(cand, key=lambda x: x[1]["t值"])
rows.append({"小级别": ltf, "最优大级别": tf,
"笔数": s["笔数"], "胜率": f"{s['胜率']:.1f}%",
"中位": f"{s['中位']:+.3f}%", "PF": f"{s['PF']:.2f}",
"t值": f"{s['t值']:+.2f}"})
if rows:
print(pd.DataFrame(rows).to_string(index=False))
print("\n########## 4. 最优配对的组合资金曲线 ##########")
picks = {r["小级别"]: r["最优大级别"] for r in rows} if rows else {}
if picks:
parts = []
for ltf, tf in picks.items():
g = allt[(allt.ltf == ltf) & allt.push & (allt[f"p{tf}_agree"] == 1)]
parts.append(g)
g = pd.concat(parts).sort_values("date")
r = g["ret_net"].to_numpy()
lev = np.clip(0.01 / np.clip(g["risk_pct"].to_numpy(), 0.002, None), 0, 20)
pnl = r * lev
eq = np.cumprod(1 + pnl)
yrs = (g["date"].max() - g["date"].min()).days / 365.25
dd = (1 - eq / np.maximum.accumulate(eq)).max()
print(f" 配对 {picks}")
print(f" n={len(r)}{len(r) / yrs:.0f}"
f"年化 {(eq[-1] ** (1 / yrs) - 1) * 100:+.1f}% 回撤 {dd * 100:.1f}% "
f"Sharpe {pnl.mean() / pnl.std(ddof=1) * np.sqrt(len(pnl) / yrs):.2f} "
f"中位 {np.median(r) * 100:+.3f}%")
gg = g.copy()
gg["y"] = gg["date"].dt.year
yr = [(y, (x["ret_net"] > 0).mean() * 100, x["ret_net"].mean() * 100, len(x))
for y, x in gg.groupby("y") if len(x) >= 20]
print(" 分年:" + " ".join(f"{y}:{m:+.2f}%({n})" for y, _, m, n in yr))
if __name__ == "__main__":
main()