删除根目录旧 Chan 模块、策略、配置、文档及 wyckoff 相关代码;更新缠论 pipeline 与笔中枢计算;补充 research 研究与 web 测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
193 lines
7.4 KiB
Python
193 lines
7.4 KiB
Python
"""Step 25:同一中枢的二次、三次三买值不值得做。
|
|
|
|
此前每个中枢只取第一个入场点,首次三买被止损后再次突破回抽的机会被丢弃。
|
|
这既可能是漏掉的利润,也可能是避开了在失败中枢上反复挨打。本步实测。
|
|
"""
|
|
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", 300)
|
|
|
|
SL, TP, MAXB = 1.5, 3.0, 48
|
|
FEE, SLIP = 0.0004, 0.0001
|
|
MAX_OCC = 3
|
|
|
|
|
|
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
|
|
zones = build_htf_zones(cdf, ltf, chan=chan_l)
|
|
sig = find_fast_bsp3(cdf, zones, max_per_zone=MAX_OCC)
|
|
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)))
|
|
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 ("h1_agree", "h2_agree", "occ", "depth"):
|
|
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 desc(r: np.ndarray, label: str) -> dict:
|
|
if len(r) < 15:
|
|
return {}
|
|
w, o = 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"{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 curve(g: pd.DataFrame, label: str) -> dict:
|
|
g = g.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
|
|
return {
|
|
"方案": label, "笔数": len(r), "年笔数": f"{len(r) / yrs:.0f}",
|
|
"年化": f"{(eq[-1] ** (1 / yrs) - 1) * 100:+.1f}%",
|
|
"回撤": f"{(1 - eq / np.maximum.accumulate(eq)).max() * 100:.1f}%",
|
|
"Sharpe": f"{pnl.mean() / pnl.std(ddof=1) * np.sqrt(len(pnl) / yrs):.2f}",
|
|
"中位": f"{np.median(r) * 100:+.3f}%",
|
|
}
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--pairs", default="5m:15m:1h,15m:1h:4h,30m:2h:4h")
|
|
ap.add_argument("--symbols", default="BTC,ETH,SOL")
|
|
ap.add_argument("--workers", type=int, default=6)
|
|
ap.add_argument("--reuse", action="store_true")
|
|
args = ap.parse_args()
|
|
|
|
cache = HERE / "out" / "step25_reentry.csv"
|
|
if args.reuse and cache.exists():
|
|
allt = pd.read_csv(cache, parse_dates=["date"])
|
|
print(f"[复用] {len(allt)} 笔\n")
|
|
else:
|
|
tasks = [(s, *tuple(p.split(":")))
|
|
for p in args.pairs.split(",") if p
|
|
for s in args.symbols.split(",")]
|
|
print(f"[二次入场] {len(tasks)} 个任务,每中枢最多 {MAX_OCC} 次\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
|
|
a = allt[allt["h1_agree"] == 1].copy()
|
|
|
|
print("=" * 110)
|
|
print("########## 1. 放开后每个中枢实际入场几次 ##########")
|
|
rows = []
|
|
for tf, g in a.groupby("ltf"):
|
|
c = g["occ"].value_counts().sort_index()
|
|
rows.append({
|
|
"级别": tf, "总信号": len(g),
|
|
"第1次": int(c.get(1, 0)), "第2次": int(c.get(2, 0)), "第3次": int(c.get(3, 0)),
|
|
"新增占比": f"{(len(g) - c.get(1, 0)) / len(g) * 100:.0f}%",
|
|
})
|
|
print(pd.DataFrame(rows).to_string(index=False))
|
|
|
|
print("\n########## 2. 第 N 次入场的质量(核心)##########")
|
|
for tf, g in a.groupby("ltf"):
|
|
rows = [desc(g[g.occ == k]["ret_net"].to_numpy(), f"{tf} 第{k}次")
|
|
for k in (1, 2, 3)]
|
|
rows = [r for r in rows if r]
|
|
if rows:
|
|
print(pd.DataFrame(rows).to_string(index=False))
|
|
print(" 第2/3次若明显差于第1次,说明失败中枢会继续失败,应维持只做首次。")
|
|
|
|
print("\n########## 3. 组合层面:只做首次 vs 放开二次三次 ##########")
|
|
def pick(g, maxocc):
|
|
parts = []
|
|
for tf in ("5m", "15m", "30m"):
|
|
x = g[(g.ltf == tf) & (g.occ <= maxocc)]
|
|
if x.empty:
|
|
continue
|
|
if tf == "5m":
|
|
q = x["risk_pct"].quantile(0.67)
|
|
x = x[(x["h2_agree"] == 1) & (x["risk_pct"] > q)]
|
|
parts.append(x)
|
|
return pd.concat(parts) if parts else pd.DataFrame()
|
|
|
|
rows = [curve(pick(a, k), f"最多{k}次入场") for k in (1, 2, 3)]
|
|
print(pd.DataFrame([r for r in rows if r]).to_string(index=False))
|
|
|
|
print("\n########## 4. 仅 30m:第2次是否值得单独做 ##########")
|
|
g = a[a.ltf == "30m"]
|
|
rows = [desc(g[g.occ == 1]["ret_net"].to_numpy(), "30m 首次"),
|
|
desc(g[g.occ >= 2]["ret_net"].to_numpy(), "30m 二次及以后")]
|
|
print(pd.DataFrame([r for r in rows if r]).to_string(index=False))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|