删除根目录旧 Chan 模块、策略、配置、文档及 wyckoff 相关代码;更新缠论 pipeline 与笔中枢计算;补充 research 研究与 web 测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
225 lines
9.2 KiB
Python
225 lines
9.2 KiB
Python
"""Step 30:引擎原版三类买卖点 vs 自写的快速版,同条件对拍。
|
|
|
|
之前所有结论都建立在 research/lib/fast_bsp3.py 上,那是我另写的实现:
|
|
形态判定照搬引擎(回抽不跌回中枢上沿),但不等回拉笔 is_sure,
|
|
直接用K线收盘判定,因此滞后从 9~10 根压到 1~2 根。
|
|
|
|
早期测过引擎原版说它不赚钱,但那次用的是残缺数据、近距配对、且没有中枢阶梯过滤,
|
|
结论不能作数。本步在完全相同的条件下重测:
|
|
同一套 pure 笔中枢、同一个大级别分型过滤、同一套阶梯方向过滤、
|
|
同样的 1.5/3.0/48 出场与次根开盘成交。唯一的差别就是信号从哪来。
|
|
"""
|
|
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"}
|
|
|
|
|
|
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 chanlun.core.ChanEnum import Chan_BSP_DIR
|
|
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
|
|
|
|
# 两边共用同一套 pure 笔中枢
|
|
zs_list = chan_l.cal_bi_zs_list_pure(chan_l.bi_list)
|
|
if not zs_list:
|
|
return None
|
|
zg = [float(z.zg) for z in zs_list]
|
|
zd = [float(z.zd) for z in zs_list]
|
|
step_up = [False] + [zd[i] > zg[i - 1] for i in range(1, len(zs_list))]
|
|
step_dn = [False] + [zg[i] < zd[i - 1] for i in range(1, len(zs_list))]
|
|
ladder = {id(z): (step_up[i], step_dn[i]) for i, z in enumerate(zs_list)}
|
|
|
|
zones = build_htf_zones(cdf, ltf, chan=chan_l).reset_index(drop=True)
|
|
if zones.empty:
|
|
return None
|
|
|
|
# 大级别分型时间线
|
|
df_h = fetch_ohlcv(pair, htf, 10**9)
|
|
if df_h is None or len(df_h) < 300:
|
|
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)
|
|
|
|
idx_map = {k: i for i, k in
|
|
enumerate(cdf["date"].dt.strftime("%Y-%m-%d %H:%M:%S"))}
|
|
out = []
|
|
|
|
# ---- A 引擎原版 B3/S3 ----
|
|
bsp_list = chan_l.find_all_bsp(chan_l.bi_list, zs_list) or []
|
|
rows = []
|
|
for b in bsp_list:
|
|
t = str(b.type).replace("Chan_BSP_TYPE.", "")
|
|
if t not in ("B3", "S3") or not b.is_sure or b.sure_time is None:
|
|
continue
|
|
ek, fk = str(b.sure_time), str(b.end_time)
|
|
if ek not in idx_map:
|
|
continue
|
|
up, dn = ladder.get(id(getattr(b, "zs", None)), (False, False))
|
|
d = 1 if b.dir == Chan_BSP_DIR.BUY else -1
|
|
rows.append({"entry_idx": idx_map[ek], "direction": d,
|
|
"z_above": up, "z_below": dn,
|
|
"lag": idx_map[ek] - idx_map.get(fk, idx_map[ek])})
|
|
eng = pd.DataFrame(rows).drop_duplicates("entry_idx")
|
|
|
|
# ---- B 快速版 ----
|
|
fast = find_fast_bsp3(cdf, zones)
|
|
if not fast.empty:
|
|
zmap = {i: id(z) for i, z in enumerate(zs_list)}
|
|
fast["_zid"] = fast["zone_i"].map(zmap)
|
|
fast["z_above"] = fast["_zid"].map(lambda k: ladder.get(k, (False, False))[0])
|
|
fast["z_below"] = fast["_zid"].map(lambda k: ladder.get(k, (False, False))[1])
|
|
fast = fast.drop_duplicates("entry_idx")
|
|
|
|
for name, sig in (("A 引擎B3/S3", eng), ("B 快速三买", fast)):
|
|
if sig is None or sig.empty or len(sig) < 15:
|
|
continue
|
|
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.set_index("entry_idx")
|
|
tr["src"], tr["symbol"], tr["ltf"] = name, sym, ltf
|
|
tr["date"] = cdf["date"].to_numpy()[tr["entry_idx"].to_numpy()]
|
|
for c in ("h1_agree", "z_above", "z_below", "lag", "direction"):
|
|
if c in m.columns:
|
|
tr[c if c != "direction" else "dir_sig"] = tr["entry_idx"].map(m[c])
|
|
out.append(tr)
|
|
if not out:
|
|
return None
|
|
return {"task": f"{sym} {ltf}", "trades": pd.concat(out, ignore_index=True)}
|
|
except Exception as e:
|
|
return {"task": f"{sym} {ltf}", "error": repr(e)[:250]}
|
|
|
|
|
|
def stat(g: pd.DataFrame, label: str) -> dict:
|
|
r = g["gross"].to_numpy() - FEE - SLIP
|
|
if len(r) < 15:
|
|
return {}
|
|
w, o = r[r > 0], r[r <= 0]
|
|
sd = r.std(ddof=1)
|
|
return {"信号源": label, "笔数": len(r),
|
|
"滞后": f"{g['lag'].mean():.1f}" if "lag" in g and g["lag"].notna().any() else "—",
|
|
"胜率": f"{(r > 0).mean() * 100:.1f}%",
|
|
"均收益": f"{r.mean() * 100:+.3f}%",
|
|
"中位": f"{np.median(r) * 100:+.3f}%",
|
|
"PF": f"{w.sum() / abs(o.sum()):.2f}" if len(o) else "inf",
|
|
"偏度": f"{pd.Series(r).skew():.2f}",
|
|
"t值": f"{r.mean() / (sd / np.sqrt(len(r))):+.2f}"}
|
|
|
|
|
|
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" / "step30_engine_vs_fast.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(",")]
|
|
tasks = [(s, l) for l in BEST for s in syms]
|
|
print(f"[对拍] {len(tasks)} 个任务\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)
|
|
|
|
allt["date"] = pd.to_datetime(allt["date"])
|
|
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. 原始信号(不加任何过滤)##########")
|
|
print(pd.DataFrame([r for r in
|
|
[stat(g, s) for s, g in allt.groupby("src")] if r]).to_string(index=False))
|
|
|
|
print("\n########## 2. 逐层加过滤,分级别 ##########")
|
|
for ltf, g in allt.groupby("ltf"):
|
|
rows = []
|
|
for s, x in g.groupby("src"):
|
|
rows.append(stat(x, f"{ltf} {s} 原始"))
|
|
rows.append(stat(x[x.h1_agree == 1], f"{ltf} {s} +大级别同向"))
|
|
rows.append(stat(x[(x.h1_agree == 1) & x.push], f"{ltf} {s} +同向+阶梯"))
|
|
rows = [r for r in rows if r]
|
|
if rows:
|
|
print(pd.DataFrame(rows).to_string(index=False))
|
|
|
|
print("\n########## 3. 最终配置对比(同向+阶梯,全级别合并)##########")
|
|
rows = [stat(g[(g.h1_agree == 1) & g.push], s) for s, g in allt.groupby("src")]
|
|
print(pd.DataFrame([r for r in rows if r]).to_string(index=False))
|
|
|
|
print("\n########## 4. 资金曲线 ##########")
|
|
for s, g in allt.groupby("src"):
|
|
sub = g[(g.h1_agree == 1) & g.push].sort_values("date")
|
|
if len(sub) < 30:
|
|
continue
|
|
r = sub["gross"].to_numpy() - FEE - SLIP
|
|
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)
|
|
yrs = (sub["date"].max() - sub["date"].min()).days / 365.25
|
|
dd = (1 - eq / np.maximum.accumulate(eq)).max()
|
|
print(f" {s:>12}: n={len(r):>4} 年{len(r) / yrs:>3.0f}笔 "
|
|
f"年化 {(eq[-1] ** (1 / yrs) - 1) * 100:+7.1f}% 回撤 {dd * 100:5.1f}% "
|
|
f"Sharpe {pnl.mean() / pnl.std(ddof=1) * np.sqrt(len(pnl) / yrs):5.2f}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|