Files
Chan/research/lib/shadow_budget.py
jackyu66gitandCursor 32c18f0201 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>
2026-08-28 00:05:56 +08:00

129 lines
5.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""影子测量要对照的那条线:逐币滑点预算、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 "放弃该腿"