research: 1m 出场口径重定、费率修正,与影子测量的判据常数
起因是用户看图指出「止盈没做好」,查下来 TP=3.0 确实把右尾截早了, 而且 1m 不该沿用 5m/15m/30m 的参数——成本固定在 bp、目标随 ATR 缩放, 1m 的 3 ATR 只有 0.39% 而 30m 是 1.87%,成本占比差 5 倍。 step41(5m/15m/30m)与 step42(1m)跑同一张全网格: SL × TP × MAX_BARS × 分批(3 ATR 减半 → 剩余目标 × 剩余半仓止损位)。 - 1m 最优 SL2 / 3ATR 减半 / 剩余止损保持 2.0 / 目标 8ATR / 48 根, 样本外 8/8 币、7/7 年全面提升,均R/R夏普/回撤/剔10%PF 四项全赢 - 分批要做,但**减仓后不要动止损**。止损位 0/0.5/1/1.5/2 ATR 严格单调, 越紧越差,三组初始 SL 全一致。保本损是全表最差的一档 - SL=1.0 在 1m 上是废的:剔10%PF 0.78~0.99、中位收益 −0.122% 费率此前写的 taker 3bp / maker 1bp 隐含「原始 taker 6bp」的错误前提, 实际是原始 taker 0.040% / maker 0.016%、返 50% 后 2.0 / 0.8bp。方向是保守的, 所以首轮跑出来的数字全部偏低。exit_model 已改,费率只在分析阶段套用, 不必重跑模拟。改完 1m 的均R +8%,5m/15m/30m 只动 2%——费率只对 1m 有杠杆。 顺带查证了用户的一个假设:余量逐年递减是不是跟波动率有关。成立,而且 r = +0.989。毛/ATR 七年在 2.26~2.67 之间没有趋势,衰减的是 ATR 本身 (2021 的 22.1bp 压到 2026 的 8.8bp)。**是波动率压缩,不是 alpha 衰减。** 由此引出 ATR 门控:低 ATR 桶的毛 R 其实最高(1.16 vs 高 ATR 桶的 0.94), 断崖只在扣费之后出现。所以阈值是**费率的函数**(约 5 + 1.1×taker费), 不是市场常数。当前费率下 ≥8bp,在 5m/15m/30m 上几乎不触发,可作全局规则。 lib/shadow_budget.py 放影子测量要对照的常数:逐币预算、门控阈值、 腿→maker/taker 映射、lag 阈值、判据。记录与报表归 research/live/, 分工的理由是这些数会变——今天预算就动了四次。 out/*.feather 转为 ignore:70MB+ 且重跑可得,摘要都在 HANDOFF。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
"""Step 41:出场口径全扫(5m/15m/30m)——SL × TP × MAX_BARS × 分批 + 真实费率模型。
|
||||
|
||||
起因有三,都来自用户:
|
||||
1. 「止盈没做好」→ 量 MFE,确认 TP=3.0 截在中位数以下
|
||||
2. 「可以测 1pt/1.5pt/2pt 损啊」→ SL 一起扫,不再钉死 1.5
|
||||
3. 「只有开仓才是 taker,止盈止损都是 maker 才对」→ 费率按出场原因分别计
|
||||
|
||||
第 3 条只对了一半:止盈挂限价确实是 maker,但止损是 stop-market,
|
||||
成交时仍是 taker(做成 stop-limit 会有急跌不成交的风险,损失远大于省下的费)。
|
||||
所以模型是「入场 taker + 止盈 maker + 止损/超时 taker」,见 lib/exit_model.py。
|
||||
|
||||
信号口径与 step37 完全一致(pure 笔中枢 + 大级别分型同向 + 中枢阶梯),只换出场。
|
||||
样本内 BTC/ETH/SOL,样本外是 step37 那 8 个没参与过调参的币。
|
||||
1m 另见 step42——它的右尾量纲和这三个级别不同,不能混用参数。
|
||||
"""
|
||||
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", 400)
|
||||
|
||||
SLS = [1.0, 1.5, 2.0]
|
||||
TPS = [2.0, 3.0, 4.0, 5.0, 6.0, 8.0]
|
||||
MAXBS = [48, 96]
|
||||
RUNNERS = [5.0, 6.0, 8.0, None]
|
||||
SCALE_AT = 3.0
|
||||
# 减仓后剩余半仓的止损位(开仓价下方几个 ATR)。0 = 保本损,1.5 = 与原止损同位
|
||||
RUNNER_STOPS = [0.0, 0.5, 1.0, 1.5, 2.0]
|
||||
PAIRS = [("5m", "30m"), ("15m", "1h"), ("30m", "2h")]
|
||||
IS_SYMS = ["BTC", "ETH", "SOL"]
|
||||
OOS_SYMS = ["BNB", "XRP", "DOGE", "ADA", "AVAX", "LINK", "LTC", "TRX"]
|
||||
|
||||
|
||||
def cfgs() -> list[tuple[str, str, float]]:
|
||||
"""所有配置的 (列名, 可读标签, 初始 SL)。"""
|
||||
from lib.exit_model import cfg_name
|
||||
out = []
|
||||
for sl in SLS:
|
||||
for b in MAXBS:
|
||||
for t in TPS:
|
||||
out.append((cfg_name(sl, t, b), f"SL{sl:g} 整仓TP{t:g} {b}根", sl))
|
||||
for rn in RUNNERS:
|
||||
tgt = f"{rn:g}ATR" if rn else "不设目标"
|
||||
for k in RUNNER_STOPS:
|
||||
kk = "保本" if k == 0 else f"留损{k:g}"
|
||||
out.append((cfg_name(sl, rn, b, k),
|
||||
f"SL{sl:g} 分批→{tgt} {kk} {b}根", sl))
|
||||
return out
|
||||
|
||||
|
||||
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.data import fetch_ohlcv
|
||||
from lib.exit_model import walk_exits
|
||||
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, htf, group = task
|
||||
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 {"task": f"{sym} {ltf}", "error": "小级别数据不足"}
|
||||
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 {"task": f"{sym} {ltf}", "error": "无中枢"}
|
||||
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 or len(df_h) < 300:
|
||||
return {"task": f"{sym} {ltf}", "error": "大级别数据不足"}
|
||||
chan_h = TF_DF(df_h, 1, htf)
|
||||
tl = htf_fx_timeline(
|
||||
signals_to_frame(extract_fx_signals(chan_h, chan_h.dataframe)), chan_h.dataframe)
|
||||
|
||||
sig = find_fast_bsp3(cdf, zones)
|
||||
if sig.empty:
|
||||
return {"task": f"{sym} {ltf}", "error": "无信号"}
|
||||
sig = sig.merge(z[["zone_i", "z_above", "z_below"]], on="zone_i", how="left")
|
||||
sig = attach_htf_context(sig, cdf, tl, "h1")
|
||||
push = np.where(sig["direction"] == 1, sig["z_above"], sig["z_below"])
|
||||
keep = (sig["h1_agree"] == 1) & pd.Series(push, index=sig.index).fillna(False).astype(bool)
|
||||
sig = sig[keep]
|
||||
if len(sig) < 25:
|
||||
return {"task": f"{sym} {ltf}", "error": f"过滤后仅 {len(sig)} 笔"}
|
||||
|
||||
r = walk_exits(cdf, sig, SLS, TPS, MAXBS, SCALE_AT, RUNNERS, RUNNER_STOPS)
|
||||
if r.empty:
|
||||
return {"task": f"{sym} {ltf}", "error": "无成交"}
|
||||
r["symbol"], r["ltf"], r["group"] = sym, ltf, group
|
||||
r["date"] = cdf["date"].to_numpy()[r["sig_idx"].to_numpy()]
|
||||
return {"task": f"{sym} {ltf}", "rows": r}
|
||||
except Exception as e:
|
||||
return {"task": f"{sym} {ltf}", "error": repr(e)[:200]}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--workers", type=int, default=6)
|
||||
ap.add_argument("--reuse", action="store_true")
|
||||
args = ap.parse_args()
|
||||
|
||||
from lib.exit_model import all_taker_cost, cfg_name, cost_of, flat_cost, rstat, stat
|
||||
|
||||
cache = HERE / "out" / "step41_exit_tp.feather"
|
||||
if args.reuse and cache.exists():
|
||||
allr = pd.read_feather(cache)
|
||||
print(f"[复用] {len(allr)} 笔\n")
|
||||
else:
|
||||
tasks = ([(s, l, h, "样本内") for s in IS_SYMS for l, h in PAIRS]
|
||||
+ [(s, l, h, "样本外") for s in OOS_SYMS for l, h in PAIRS])
|
||||
print(f"[出场口径全扫] {len(tasks)} 个任务,{len(cfgs())} 个配置\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 not r or "error" in r:
|
||||
print(f" [{i}/{len(tasks)}] 跳过 {(r or {}).get('task', '')} "
|
||||
f"{(r or {}).get('error', '')}", flush=True)
|
||||
continue
|
||||
res.append(r["rows"])
|
||||
print(f" [{i}/{len(tasks)}] {r['task']} n={len(r['rows'])}", flush=True)
|
||||
if not res:
|
||||
return
|
||||
allr = pd.concat(res, ignore_index=True)
|
||||
allr.to_feather(cache)
|
||||
|
||||
oos = allr[allr["group"] == "样本外"]
|
||||
ins = allr[allr["group"] == "样本内"]
|
||||
base = f"s1.5_tp{SCALE_AT:g}_m48"
|
||||
|
||||
print("=" * 165)
|
||||
print("########## 1. 费率模型的影响(样本外,同一配置 SL1.5/TP3/48根)##########")
|
||||
print(" 旧:进出都当 taker,双边 6bp + 双边滑点")
|
||||
print(" 新:入场 taker + 止盈 maker + 止损/超时 taker,滑点只加在 taker 腿上")
|
||||
rows = [stat(oos, base, "旧 全taker", all_taker_cost),
|
||||
stat(oos, base, "新 混合费率", cost_of),
|
||||
stat(oos, base, "研究口径 固定5bp", flat_cost)]
|
||||
print(pd.DataFrame([r for r in rows if r]).to_string(index=False))
|
||||
|
||||
print("\n########## 2. 减仓后剩余半仓的止损位(样本外,48根)##########")
|
||||
print(" 0 = 保本损;1.5 = 与原止损同位(不动);更大 = 减仓后主动放宽")
|
||||
for sl in SLS:
|
||||
rows = []
|
||||
for rn in (6.0, 8.0):
|
||||
for k in RUNNER_STOPS:
|
||||
kk = "保本" if k == 0 else f"留损{k:g}ATR"
|
||||
rows.append(stat(oos, cfg_name(sl, rn, 48, k),
|
||||
f"SL{sl:g} 分批→{rn:g}ATR {kk}"))
|
||||
rows = [r for r in rows if r]
|
||||
if rows:
|
||||
print(pd.DataFrame(rows).to_string(index=False))
|
||||
print()
|
||||
|
||||
print("########## 3. 初始 SL 扫描:R 倍数口径(唯一可比)##########")
|
||||
print(" SL 越宽每笔风险越大、固定风险下仓位越小,直接比百分比会把仓位差异算成策略优势")
|
||||
for name, frame in (("样本外", oos), ("样本内", ins)):
|
||||
rows = []
|
||||
for sl in SLS:
|
||||
rows.append(rstat(frame, cfg_name(sl, 3.0, 48), f"SL{sl:g} 整仓TP3/48", sl))
|
||||
rows.append(rstat(frame, cfg_name(sl, 8.0, 48, sl),
|
||||
f"SL{sl:g} 分批→8ATR 留损{sl:g} /48", sl))
|
||||
print(f" 【{name}】")
|
||||
print(pd.DataFrame(rows).to_string(index=False))
|
||||
print()
|
||||
|
||||
print("########## 4. 全网格排名(样本外,按剔10%PF)##########")
|
||||
rank = [r for r in (stat(oos, c, lab) for c, lab, _ in cfgs()) if r]
|
||||
rk = pd.DataFrame(rank)
|
||||
rk["_k"] = rk["剔10%PF"].astype(float)
|
||||
print(rk.sort_values("_k", ascending=False).drop(columns="_k").head(15).to_string(index=False))
|
||||
|
||||
print("\n########## 5. 现用 vs 最优:样本内外对照 ##########")
|
||||
best = rk.sort_values("_k", ascending=False).iloc[0]["口径"]
|
||||
lut = {lab: (c, sl) for c, lab, sl in cfgs()}
|
||||
best_cfg, best_sl = lut[best]
|
||||
rows = []
|
||||
for name, frame in (("样本内", ins), ("样本外", oos)):
|
||||
rows.append(stat(frame, base, f"{name} 现用 SL1.5/TP3/48根"))
|
||||
rows.append(stat(frame, best_cfg, f"{name} 最优 {best}"))
|
||||
print(pd.DataFrame([r for r in rows if r]).to_string(index=False))
|
||||
print("\n 同一对比的 R 倍数口径:")
|
||||
rows = []
|
||||
for name, frame in (("样本内", ins), ("样本外", oos)):
|
||||
rows.append(rstat(frame, base, f"{name} 现用 SL1.5/TP3/48根", 1.5))
|
||||
rows.append(rstat(frame, best_cfg, f"{name} 最优 {best}", best_sl))
|
||||
print(pd.DataFrame(rows).to_string(index=False))
|
||||
|
||||
print(f"\n########## 6. {best} 的逐币与分年稳定性(样本外)##########")
|
||||
f = oos.assign(year=pd.to_datetime(oos["date"]).dt.year)
|
||||
for key, name in (("symbol", "币"), ("year", "年")):
|
||||
rows = []
|
||||
for k, g in f.groupby(key):
|
||||
a, bst = stat(g, base, str(k)), stat(g, best_cfg, str(k))
|
||||
if a and bst:
|
||||
rows.append({name: k, "笔数": a["笔数"],
|
||||
"现用 净收益": a["净均收益"], "最优 净收益": bst["净均收益"],
|
||||
"现用 PF": a["PF"], "最优 PF": bst["PF"],
|
||||
"现用 剔10%": a["剔10%PF"], "最优 剔10%": bst["剔10%PF"],
|
||||
"现用 余量bp": a["滑点余量bp"], "最优 余量bp": bst["滑点余量bp"]})
|
||||
print(pd.DataFrame(rows).to_string(index=False))
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user