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
+210
View File
@@ -0,0 +1,210 @@
"""Step 29:信号为什么这么少——中枢到成交的漏斗,以及放宽参数能救回多少。
30m/2h 六年三个币只有 177 笔,年均 26 笔。先定位卡在哪一步,
再对回抽容差、回抽窗口、扫描窗口做敏感性测试,看样本量与质量的取舍。
"""
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
BEST = {"5m": "30m", "15m": "1h", "30m": "2h"}
# (扫描窗口, 回抽窗口, 回抽容差)
GRID = [
(200, 30, 0.003), # 现状
(200, 30, 0.010),
(200, 60, 0.003),
(200, 60, 0.010),
(400, 60, 0.010),
(400, 90, 0.020),
(600, 120, 0.030),
]
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
htf = BEST[ltf]
pair = f"{sym}/USDT:USDT"
try:
df_l = fetch_ohlcv(pair, 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
z = zones.copy()
pg, pdn = z["zg"].shift(), z["zd"].shift()
z["z_above"], z["z_below"] = z["zd"] > pg, z["zg"] < pdn
z["zone_i"] = np.arange(len(z))
df_h = fetch_ohlcv(pair, htf, 10**9)
if df_h is None:
return None
chan_h = TF_DF(df_h, 1, htf)
s = signals_to_frame(extract_fx_signals(chan_h, chan_h.dataframe))
tl = htf_fx_timeline(s, chan_h.dataframe)
diag0: dict = {}
frames = []
for scan, pw, tol in GRID:
d = diag0 if (scan, pw, tol) == GRID[0] else None
sig = find_fast_bsp3(cdf, zones, scan=scan, pullback_win=pw, tol=tol, diag=d)
if sig.empty:
continue
sig = sig.merge(z[["zone_i", "z_above", "z_below"]], on="zone_i", how="left")
sig = attach_htf_context(sig, cdf, tl, "h1")
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["cfg"] = f"scan{scan}/pb{pw}/tol{tol * 100:g}%"
tr["date"] = cdf["date"].to_numpy()[tr["entry_idx"].to_numpy()]
for c in ("h1_agree", "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 {"task": f"{sym} {ltf}", "diag": diag0,
"trades": pd.concat(frames, ignore_index=True)}
except Exception as e:
return {"task": f"{sym} {ltf}", "error": repr(e)[:250]}
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), "胜率": round((r > 0).mean() * 100, 1),
"中位": round(np.median(r) * 100, 2),
"PF": round(w.sum() / abs(o.sum()), 2) if len(o) else np.inf,
"偏度": round(pd.Series(r).skew(), 2),
"t值": round(r.mean() / (sd / np.sqrt(len(r))), 2)}
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" / "step29_funnel.csv"
dcache = HERE / "out" / "step29_diag.csv"
if args.reuse and cache.exists():
allt = pd.read_csv(cache, parse_dates=["date"])
diag = pd.read_csv(dcache) if dcache.exists() else pd.DataFrame()
print(f"[复用] {len(allt)}\n")
else:
syms = [s.strip() for s in args.symbols.split(",")]
tasks = [(s, l) for l in BEST for s in syms]
print(f"[漏斗] {len(tasks)} 个任务 × {len(GRID)} 组参数\n", flush=True)
res = []
with ProcessPoolExecutor(max_workers=6) 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']}", flush=True)
if not res:
return
allt = pd.concat([r["trades"] for r in res], ignore_index=True)
allt.to_csv(cache, index=False)
diag = pd.DataFrame([{"task": r["task"], **r["diag"]} for r in res if r.get("diag")])
diag.to_csv(dcache, index=False)
allt["date"] = pd.to_datetime(allt["date"])
allt["r"] = 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)
base = allt["cfg"] == f"scan200/pb30/tol0.3%"
if not diag.empty:
print("=" * 118)
print("########## 1. 现状参数下,每个中枢卡在哪一步 ##########")
diag["级别"] = diag["task"].str.split().str[1]
cols = [c for c in diag.columns if c.startswith(("×", "", "中枢"))]
g = diag.groupby("级别")[cols].sum()
tot = g["中枢总数"]
out = g.copy()
for c in cols:
if c != "中枢总数":
out[c] = g[c].astype(int).astype(str) + " (" + \
(g[c] / tot * 100).round(0).astype(int).astype(str) + "%)"
print(out.to_string())
print(" 成交率 = √成交 / 中枢总数。占比最大的那一项就是瓶颈。")
print("\n########## 2. 放宽参数:样本量 vs 质量 ##########")
for ltf, g in allt.groupby("ltf"):
rows = []
for cfg, x in g.groupby("cfg"):
sub = x[(x["h1_agree"] == 1) & x["push"]]
s = stat(sub["r"].to_numpy())
if s:
rows.append({"级别": ltf, "参数": cfg, **s})
if rows:
df = pd.DataFrame(rows)
df["_k"] = df["参数"].map({f"scan{a}/pb{b}/tol{c * 100:g}%": i
for i, (a, b, c) in enumerate(GRID)})
print(df.sort_values("_k").drop(columns="_k").to_string(index=False))
print("\n########## 3. 最宽参数下的年频率变化 ##########")
rows = []
for (ltf, cfg), g in allt.groupby(["ltf", "cfg"]):
sub = g[(g["h1_agree"] == 1) & g["push"]]
if len(sub) < 30:
continue
yrs = (sub["date"].max() - sub["date"].min()).days / 365.25
r = sub["r"].to_numpy()
lev = np.clip(0.01 / np.clip(sub["risk_pct"].to_numpy(), 0.002, None), 0, 20)
pnl = r * lev
eq = np.cumprod(1 + pnl)
rows.append({"级别": ltf, "参数": cfg, "笔数": len(r),
"年笔数": round(len(r) / yrs), "PF": round(
r[r > 0].sum() / abs(r[r <= 0].sum()), 2),
"年化": f"{(eq[-1] ** (1 / yrs) - 1) * 100:+.0f}%",
"回撤": f"{(1 - eq / np.maximum.accumulate(eq)).max() * 100:.1f}%",
"Sharpe": round(pnl.mean() / pnl.std(ddof=1) * np.sqrt(len(r) / yrs), 2)})
print(pd.DataFrame(rows).to_string(index=False))
if __name__ == "__main__":
main()