refactor: 精简仓库为 chanlun 核心与 web 分析,移除威科夫与遗留模块
删除根目录旧 Chan 模块、策略、配置、文档及 wyckoff 相关代码;更新缠论 pipeline 与笔中枢计算;补充 research 研究与 web 测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
"""突破跟随的事件驱动回测。
|
||||
|
||||
趋势跟随是低胜率高赔率,固定持有期会把大赢利截断、把小亏损放大,
|
||||
必须用止损/止盈/时间三重出场才能测出真实期望。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
FEE = 0.0008 # 双边
|
||||
|
||||
|
||||
@dataclass
|
||||
class Trade:
|
||||
entry_idx: int
|
||||
exit_idx: int
|
||||
direction: int
|
||||
entry: float
|
||||
exit: float
|
||||
ret: float # 已扣费
|
||||
reason: str # sl / tp / time
|
||||
bars_held: int
|
||||
gross: float # 未扣费,便于事后做费率 what-if
|
||||
risk_pct: float # 止损距离占入场价的比例,用于反推名义仓位
|
||||
boosted: bool = False # 持仓期间是否等到了更大级别的同向确认
|
||||
|
||||
|
||||
def run_trades(
|
||||
df: pd.DataFrame,
|
||||
entries: list[tuple[int, int]],
|
||||
sl_atr: float = 1.5,
|
||||
tp_atr: float = 3.0,
|
||||
max_bars: int = 48,
|
||||
trail: bool = False,
|
||||
fee: float = FEE,
|
||||
entry_delay: int = 0,
|
||||
slippage: float = 0.0,
|
||||
boost_dir: np.ndarray | None = None,
|
||||
tp_boost: float = 2.0,
|
||||
boost_breakeven: bool = False,
|
||||
) -> pd.DataFrame:
|
||||
"""按 (entry_idx, direction) 逐笔模拟。
|
||||
|
||||
出场优先级:同一根内若同时触及止损与止盈,保守地判为止损。
|
||||
entry_delay=1 表示信号次根开盘成交,用来检验「收盘价入场」是否过于乐观。
|
||||
|
||||
boost_dir 给出每根K线上「趋势被更大级别确认」的方向(+1/-1/0)。持仓期间
|
||||
一旦等到同向确认,就把止盈目标放大 tp_boost 倍;boost_breakeven 同时把
|
||||
止损收到成本价。用来检验「新证据出现后该不该改单」。
|
||||
"""
|
||||
high = df["high"].to_numpy(dtype=float)
|
||||
low = df["low"].to_numpy(dtype=float)
|
||||
close = df["close"].to_numpy(dtype=float)
|
||||
open_ = df["open"].to_numpy(dtype=float) if "open" in df.columns else close
|
||||
atr = (
|
||||
df["atr"].to_numpy(dtype=float)
|
||||
if "atr" in df.columns
|
||||
else pd.Series(close).rolling(14).std().bfill().to_numpy()
|
||||
)
|
||||
n = len(df)
|
||||
out: list[Trade] = []
|
||||
|
||||
for e_idx, d in entries:
|
||||
sig_idx = e_idx
|
||||
e_idx = e_idx + entry_delay
|
||||
if e_idx >= n - 1:
|
||||
continue
|
||||
entry = open_[e_idx] if entry_delay else close[e_idx]
|
||||
a = atr[sig_idx]
|
||||
if not np.isfinite(a) or a <= 0:
|
||||
continue
|
||||
sl = entry - d * sl_atr * a
|
||||
tp = entry + d * tp_atr * a
|
||||
best = entry
|
||||
exit_idx, exit_px, reason = None, None, "time"
|
||||
boosted = False
|
||||
|
||||
for j in range(e_idx + 1, min(e_idx + max_bars + 1, n)):
|
||||
if boost_dir is not None and not boosted and boost_dir[j] == d:
|
||||
tp = entry + d * tp_atr * tp_boost * a
|
||||
if boost_breakeven:
|
||||
sl = max(sl, entry) if d == 1 else min(sl, entry)
|
||||
boosted = True
|
||||
if trail:
|
||||
best = max(best, high[j]) if d == 1 else min(best, low[j])
|
||||
sl = max(sl, best - sl_atr * a) if d == 1 else min(sl, best + sl_atr * a)
|
||||
hit_sl = low[j] <= sl if d == 1 else high[j] >= sl
|
||||
hit_tp = high[j] >= tp if d == 1 else low[j] <= tp
|
||||
if hit_sl:
|
||||
exit_idx, exit_px, reason = j, sl, "sl"
|
||||
break
|
||||
if hit_tp:
|
||||
exit_idx, exit_px, reason = j, tp, "tp"
|
||||
break
|
||||
if exit_idx is None:
|
||||
exit_idx = min(e_idx + max_bars, n - 1)
|
||||
exit_px = close[exit_idx]
|
||||
|
||||
gross = d * (exit_px - entry) / entry
|
||||
out.append(Trade(sig_idx, exit_idx, d, entry, float(exit_px),
|
||||
gross - fee - slippage, reason, exit_idx - e_idx,
|
||||
gross, sl_atr * a / entry, boosted))
|
||||
|
||||
return pd.DataFrame([t.__dict__ for t in out])
|
||||
|
||||
|
||||
def run_trades_dynamic(
|
||||
df: pd.DataFrame,
|
||||
entries: list[tuple[int, int]],
|
||||
upgrades: dict[int, int],
|
||||
sl_atr: float = 1.5,
|
||||
tp_atr: float = 3.0,
|
||||
tp_atr_up: float = 6.0,
|
||||
max_bars: int = 48,
|
||||
max_bars_up: int = 96,
|
||||
lock_breakeven: bool = True,
|
||||
fee: float = FEE,
|
||||
entry_delay: int = 0,
|
||||
slippage: float = 0.0,
|
||||
) -> pd.DataFrame:
|
||||
"""持仓中若出现更大级别的同向确认,就把目标放远、并把止损收到成本价。
|
||||
|
||||
upgrades: {K线索引: 方向},表示该根出现了大级别同向三买/中枢突破。
|
||||
对应的交易逻辑是「小级别进场、大级别接力」——趋势被更高级别确认后,
|
||||
原本 3 ATR 的目标就过早了,但同时不该再让这笔回到亏损。
|
||||
"""
|
||||
high = df["high"].to_numpy(dtype=float)
|
||||
low = df["low"].to_numpy(dtype=float)
|
||||
close = df["close"].to_numpy(dtype=float)
|
||||
open_ = df["open"].to_numpy(dtype=float) if "open" in df.columns else close
|
||||
atr = (
|
||||
df["atr"].to_numpy(dtype=float)
|
||||
if "atr" in df.columns
|
||||
else pd.Series(close).rolling(14).std().bfill().to_numpy()
|
||||
)
|
||||
n = len(df)
|
||||
out: list[dict] = []
|
||||
|
||||
for e_idx, d in entries:
|
||||
sig_idx = e_idx
|
||||
e_idx = e_idx + entry_delay
|
||||
if e_idx >= n - 1:
|
||||
continue
|
||||
entry = open_[e_idx] if entry_delay else close[e_idx]
|
||||
a = atr[sig_idx]
|
||||
if not np.isfinite(a) or a <= 0:
|
||||
continue
|
||||
sl = entry - d * sl_atr * a
|
||||
tp = entry + d * tp_atr * a
|
||||
limit = max_bars
|
||||
upgraded = False
|
||||
exit_idx, exit_px, reason = None, None, "time"
|
||||
|
||||
j = e_idx + 1
|
||||
while j < min(e_idx + limit + 1, n):
|
||||
if not upgraded and upgrades.get(j) == d:
|
||||
upgraded = True
|
||||
tp = entry + d * tp_atr_up * a
|
||||
limit = max_bars_up
|
||||
if lock_breakeven:
|
||||
sl = max(sl, entry) if d == 1 else min(sl, entry)
|
||||
hit_sl = low[j] <= sl if d == 1 else high[j] >= sl
|
||||
hit_tp = high[j] >= tp if d == 1 else low[j] <= tp
|
||||
if hit_sl:
|
||||
exit_idx, exit_px, reason = j, sl, "be" if upgraded and sl == entry else "sl"
|
||||
break
|
||||
if hit_tp:
|
||||
exit_idx, exit_px, reason = j, tp, "tp_up" if upgraded else "tp"
|
||||
break
|
||||
j += 1
|
||||
if exit_idx is None:
|
||||
exit_idx = min(e_idx + limit, n - 1)
|
||||
exit_px = close[exit_idx]
|
||||
|
||||
gross = d * (exit_px - entry) / entry
|
||||
out.append({
|
||||
"entry_idx": sig_idx, "exit_idx": exit_idx, "direction": d,
|
||||
"entry": entry, "exit": float(exit_px),
|
||||
"ret": gross - fee - slippage, "reason": reason,
|
||||
"bars_held": exit_idx - e_idx, "gross": gross,
|
||||
"risk_pct": sl_atr * a / entry, "upgraded": upgraded,
|
||||
})
|
||||
|
||||
return pd.DataFrame(out)
|
||||
|
||||
|
||||
def summarize_trades(tr: pd.DataFrame, label: str) -> dict:
|
||||
if tr.empty:
|
||||
return {"策略": label, "笔数": 0}
|
||||
r = tr["ret"].to_numpy()
|
||||
win = r[r > 0]
|
||||
loss = r[r <= 0]
|
||||
pf = win.sum() / abs(loss.sum()) if len(loss) and loss.sum() != 0 else np.inf
|
||||
sd = r.std(ddof=1)
|
||||
eq = np.cumprod(1 + r)
|
||||
dd = float((1 - eq / np.maximum.accumulate(eq)).max()) if len(eq) else 0.0
|
||||
return {
|
||||
"策略": label,
|
||||
"笔数": len(r),
|
||||
"胜率": f"{(r > 0).mean() * 100:.1f}%",
|
||||
"均收益": f"{r.mean() * 100:+.3f}%",
|
||||
"赔率": f"{(win.mean() / abs(loss.mean())):.2f}" if len(win) and len(loss) else "—",
|
||||
"盈亏比PF": f"{pf:.2f}",
|
||||
"总收益": f"{(eq[-1] - 1) * 100:+.1f}%",
|
||||
"最大回撤": f"{dd * 100:.1f}%",
|
||||
"t值": f"{r.mean() / (sd / np.sqrt(len(r))):+.2f}" if sd else "—",
|
||||
"均持有": f"{tr['bars_held'].mean():.0f}",
|
||||
}
|
||||
|
||||
|
||||
def find_breakout_entries(
|
||||
sig: pd.DataFrame, df: pd.DataFrame, window: int = 10, mode: str = "fail"
|
||||
) -> list[tuple[int, int]]:
|
||||
"""分型突破入场点。
|
||||
|
||||
mode="fail" 分型失败 -> 顺势跟随:顶分型被向上突破则做多。
|
||||
mode="reverse" 传统反转 -> 分型成立方向:顶分型做空(作为对照)。
|
||||
"""
|
||||
close = df["close"].to_numpy(dtype=float)
|
||||
n = len(df)
|
||||
entries: list[tuple[int, int]] = []
|
||||
for _, r in sig.iterrows():
|
||||
d_fx = int(r["direction"]) # +1 底分型 / -1 顶分型
|
||||
lvl = float(r["price"])
|
||||
c0 = int(r["confirm_idx"])
|
||||
if mode == "reverse":
|
||||
entries.append((c0, d_fx))
|
||||
continue
|
||||
# 突破方向与分型指向相反:顶分型(-1)被向上(+1)突破
|
||||
d_bo = -d_fx
|
||||
for j in range(c0 + 1, min(c0 + window + 1, n)):
|
||||
broken = close[j] > lvl if d_bo == 1 else close[j] < lvl
|
||||
if broken:
|
||||
entries.append((j, d_bo))
|
||||
break
|
||||
return entries
|
||||
Reference in New Issue
Block a user