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:
jackyu66git
2026-08-28 00:05:56 +08:00
co-authored by Cursor
parent 9b72173285
commit 32c18f0201
8 changed files with 2415 additions and 29 deletions
+128
View File
@@ -0,0 +1,128 @@
"""影子测量要对照的那条线:逐币滑点预算、ATR 门控、lag 阈值。
记录与报表由 `research/live/shadow_hb.py` / `shadow_report.py` 负责,
**这里只放从回测推出来的常数和判据**。分工的理由是这些数会变——
2026-08-27 一天之内预算就动了四次(3.91 → 11.06 → 14.25 → 15.19bp),
费率也从 3/1bp 改成 2/0.8bp。写死在 live 侧的任何一份都会静默过期。
## 预算的定义
余量 = (毛均收益 − 实际手续费) / taker 名义额
分母是 **taker 名义额**(入场 1.0 + 非止盈出场的部分),所以这个数的含义是
**每条 taker 腿能承受多少 bp 滑点**。止盈挂限价是 maker、不吃滑点,
不能混进分母——否则会被那 60% 零滑点的止盈腿稀释,低估真实成本。
对照时用入场滑点即可(入场必然是 taker),无需等出场腿。
## 口径必须一致,否则数字不可比
预算算在这套口径上,live 侧的信号路径要对齐,缺一项数就不作数:
滤网 同向(h1_agree)**+ 中枢阶梯**。只有同向会显著偏低,见 §1.4
门控 ATR ≥ ATR_GATE_BP
出场 SL 2.0 / 3 ATR 减半 / 剩余半仓止损保持 2.0 / 目标 8 ATR / 48 根
费率 taker 2.0bp、maker 0.8bp(返佣后)
> 曾经的 `毛均收益 − 6.0bp` 是错的:6bp 假设双边 taker 且原始费率 0.12%
> 而实际是原始 taker 0.040%、返 50% 后 2.0bp,且止盈是 maker。
"""
from __future__ import annotations
import numpy as np
from .exit_model import FEE_TAKER, FEE_MAKER
# ATR 门控。阈值是**费率的函数**,不是市场常数——低 ATR 的信号毛质量其实
# 更好(毛R 1.16 vs 高 ATR 桶的 0.94),断崖只在扣费之后出现。见 §5.3。
ATR_GATE_BP = 8.0
# lag 探针。补丁后实测 506~642ms,理论下限约 500ms。
# 超阈值要**停止开新仓**,不能只打日志——退化是静默的,不崩也不报错。
LAG_ALARM_MS = 800.0
LAG_WINDOW = 30
# 逐币滑点预算(bp / taker 腿)。取 2026 年——那是七年里 ATR 最低的一年,
# 也是当前所处的环境,**不要用全样本平均值**,低波动是现状不是尾部情形。
BUDGET_BP = {
"BTC": 8.58, # 全期 14.61。ATR 中位 2026 仅 6.5bp11 个币里最低,
# 门控要刷掉 58.5% 的信号 —— **不建议作为 1m 交易标的**,
# 但流动性最好,适合做滑点测量的乐观边界
"BNB": 15.48, # 全期 22.11。2026 门控只保留 25.2%
"ETH": 20.64, # 全期 20.80
"SOL": 16.83, # 全期 28.29
"LINK": 9.18, # 全期 26.24。2026 波动压缩得厉害
"LTC": 9.63, # 全期 23.63
"AVAX": 15.02, # 全期 29.00
"XRP": 17.04, # 全期 24.52
"DOGE": 18.55, # 全期 28.48
"ADA": 22.28, # 全期 23.53。2026 唯一没被波动压缩的
"TRX": np.nan, # 2026 门控后只剩 4.2% 的信号,当前环境基本不能做
}
# 组合口径(8 个样本外币合计)。单币样本薄时用这个判整体。
BUDGET_PORTFOLIO_2026 = 15.19
BUDGET_PORTFOLIO_RECENT = 19.17 # 2025+2026
BUDGET_PORTFOLIO_FULL = 25.39 # 全样本外
# 腿 → 是否 taker。这不是日志字段,**这就是成本模型本身**:
# 止盈挂限价是 maker 不吃滑点,止损是 stop-market、超时是市价,都吃。
# 止损做不成 maker——stop-limit 在急跌里可能不成交,损失远大于省下的费。
LEG_IS_TAKER = {
"entry": True,
"scale": False, # 3 ATR 减半,限价
"runner_tp": False, # 剩余半仓到 8 ATR,限价
"stop": True, # stop-market
"timeout": True, # 48 根到点市价平
}
def gate_threshold_bp(fee_taker: float = FEE_TAKER) -> float:
"""换费率档位时的粗略指引,**不是** ATR_GATE_BP 的定义式。
拟合自 taker 0/1.5/3/6bp 上实测的最优阈值 5/8/9/12bp。当前费率下
公式给 7.2 而实测峰值是 8——不必调和:均R 在 5~12bp 之间只在
0.972~0.987 之间动,**曲线是平的,精确值不重要**。
要记住的是阈值随费率上移,换 VIP 档 / 换交易所要重扫,别抄 8bp。
"""
return 5.0 + 1.1 * fee_taker * 1e4
def fee_rate(leg: str) -> float:
return FEE_TAKER if LEG_IS_TAKER[leg] else FEE_MAKER
def budget_of(symbol: str) -> float:
"""取该币的预算。未收录或当前环境不可做的返回 nan。"""
return BUDGET_BP.get(symbol.upper(), np.nan)
def lag_healthy(recent_lag_ms, alarm_ms: float = LAG_ALARM_MS,
window: int = LAG_WINDOW) -> bool:
"""滚动中位数是否还在阈值内。False 时应告警并**停止开新仓**。
用中位数而非均值:单根的网络抖动不该触发停机,持续退化才该。
"""
x = np.asarray(recent_lag_ms, dtype=float)[-window:]
if len(x) < window:
return True
return bool(np.median(x) <= alarm_ms)
def verdict(measured_slip_bp: float, symbol: str | None = None,
budget_bp: float | None = None) -> str:
"""把实测滑点翻成 §6 第 4 步的四档判断。
`measured_slip_bp` 用**每条 taker 腿**的中位滑点,不是每笔的总滑点。
"""
if budget_bp is None:
budget_bp = budget_of(symbol) if symbol else BUDGET_PORTFOLIO_2026
if not np.isfinite(budget_bp):
return "该币当前环境预算不足,不作交易标的"
if measured_slip_bp <= 5:
return "可行,进真实 dry run"
if measured_slip_bp <= 10:
return "可行但仅中高波动期,门控阈值要上调"
if measured_slip_bp <= budget_bp:
return "仅 2021 型牛市能做,常态不要开"
return "放弃该腿"