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

167 lines
6.4 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 22:执行假设压力测试。
Step 20/21 的结论建立在「信号K线收盘价成交、只扣 0.08% 手续费」上。
实盘拿不到这个价。本步逐层加码,看结论在哪一层塌掉:
A 收盘入场(基准)
B 次根开盘入场 —— 真实下单节奏
C 次根开盘 + 5bp 滑点
D 次根开盘 + 10bp 滑点
"""
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", 280)
SL, TP, MAXB = 1.5, 3.0, 48
VARIANTS = [("A 收盘入场", 0, 0.0), ("B 次根开盘", 1, 0.0),
("C 次根+5bp", 1, 0.0005), ("D 次根+10bp", 1, 0.0010)]
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, h1, h2 = task
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
sig = find_fast_bsp3(cdf, build_htf_zones(cdf, ltf, chan=chan_l))
if sig.empty or len(sig) < 10:
return None
for tf, pref in ((h1, "h1"), (h2, "h2")):
df_h = fetch_ohlcv(f"{sym}/USDT:USDT", 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))
sig = attach_htf_context(sig, cdf, htf_fx_timeline(s, chan_h.dataframe), pref)
entries = list(zip(sig["entry_idx"].astype(int), sig["direction"].astype(int)))
m = sig.set_index("entry_idx")
frames = []
for name, delay, slip in VARIANTS:
tr = run_trades(cdf, entries, SL, TP, MAXB, entry_delay=delay, slippage=slip)
if tr.empty:
continue
tr["variant"] = name
tr["symbol"] = sym
tr["ltf"] = ltf
tr["date"] = cdf["date"].to_numpy()[tr["entry_idx"].to_numpy()]
for c in ("h1_agree", "h2_agree"):
tr[c] = tr["entry_idx"].map(m[c]) if c in m.columns else np.nan
frames.append(tr)
return {"task": f"{sym} {ltf}", "trades": pd.concat(frames, ignore_index=True)}
except Exception as e:
return {"task": f"{sym} {ltf}", "error": repr(e)[:200]}
def desc(r: np.ndarray, label: str) -> dict:
if len(r) < 5:
return {}
win, loss = r[r > 0], r[r <= 0]
sd = r.std(ddof=1)
return {
"口径": label, "笔数": len(r),
"胜率": f"{(r > 0).mean() * 100:.1f}%",
"均收益": f"{r.mean() * 100:+.3f}%",
"中位": f"{np.median(r) * 100:+.3f}%",
"PF": f"{win.sum() / abs(loss.sum()):.2f}" if len(loss) else "inf",
"t值": f"{r.mean() / (sd / np.sqrt(len(r))):+.2f}",
}
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--pairs", default="15m:1h:4h,30m:2h:4h,1h:4h:1d")
ap.add_argument("--symbols", default="BTC,ETH,SOL")
ap.add_argument("--workers", type=int, default=6)
args = ap.parse_args()
pairs = [tuple(p.split(":")) for p in args.pairs.split(",") if p]
syms = [s.strip() for s in args.symbols.split(",") if s.strip()]
tasks = [(s, *p) for p in pairs for s in syms]
print(f"[压测] {len(tasks)} 个任务 × {len(VARIANTS)} 种执行口径\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}/{len(tasks)}] 跳过 {r['task'] if r else futs[f]}", flush=True)
continue
res.append(r)
print(f" [{i}/{len(tasks)}] {r['task']} ok", flush=True)
if not res:
print("无结果")
return
allt = pd.concat([r["trades"] for r in res], ignore_index=True)
allt.to_csv(HERE / "out" / "step22_execution.csv", index=False)
a1 = allt["h1_agree"] == 1
both = a1 & (allt["h2_agree"] == 1)
print("\n########## 1. 大级别同向,逐层加码执行成本 ##########")
rows = [desc(allt[a1 & (allt.variant == v)]["ret"].to_numpy(), v)
for v, _, _ in VARIANTS]
print(pd.DataFrame([r for r in rows if r]).to_string(index=False))
print("\n########## 2. 双大级别同向 ##########")
rows = [desc(allt[both & (allt.variant == v)]["ret"].to_numpy(), v)
for v, _, _ in VARIANTS]
print(pd.DataFrame([r for r in rows if r]).to_string(index=False))
print("\n########## 3. 最保守口径(D)下的分级别 ##########")
d = allt[(allt.variant == VARIANTS[-1][0]) & a1]
rows = [desc(g["ret"].to_numpy(), f"{tf} 同向") for tf, g in d.groupby("ltf")]
print(pd.DataFrame([r for r in rows if r]).to_string(index=False))
print("\n########## 4. 最保守口径(D)下的分年(同向)##########")
d = d.copy()
d["year"] = pd.to_datetime(d["date"]).dt.year
rows = [desc(g["ret"].to_numpy(), str(y)) for y, g in d.groupby("year") if len(g) >= 25]
print(pd.DataFrame([r for r in rows if r]).to_string(index=False))
print("\n########## 5. 最保守口径(D)下的尾部依赖(同向)##########")
r = d["ret"].to_numpy()
for k in (0, 5, 10, 20):
v = r if k == 0 else r[r <= np.quantile(r, 1 - k / 100)]
win, loss = v[v > 0], v[v <= 0]
sd = v.std(ddof=1)
print(f" 剔除最赚{k:>2}%: n={len(v):>4} PF={win.sum() / abs(loss.sum()):.2f} "
f"t={v.mean() / (sd / np.sqrt(len(v))):+.2f} 中位={np.median(v) * 100:+.3f}%")
if __name__ == "__main__":
main()