开仓动量过滤器设计方案4

开仓动量过滤器设计方案

版本:v4.0
日期:2026-03-06
适用系统:Hyperliquid 量化交易系统(Adaptive Bollinger Z-Score 配对策略)
升级摘要:v3.0 → v4.0 DFA-2 二阶去趋势 + R² 质检,KPSS+ADF 对偶检验,BTC 跨资产动量因子,VWPM 量价确认,数据驱动非对称参数


目录

  1. 背景与问题定义
  2. 算法选择与学术依据
  3. 算法具体实现
  4. 融入当前交易体系的方案
  5. 与当前交易风格的契合度分析
  6. 与四项开单约束的对应关系
  7. 参数配置指南
  8. 风险与局限性

1. 背景与问题定义

1.1 交易系统现状

当前系统基于 Adaptive Bollinger Z-Score 配对策略,核心逻辑是协整对的 spread 均值回归:

  • adaptive_z 突破 adaptive_threshold 时产生入场信号
  • adaptive_z 回归至 entry_adaptive_z * reversion_factor 时平仓
  • 使用 5 分钟 K 线,最大持仓时间控制在 72 小时以内(max_hold_hours=72.0
  • 策略引擎 _check_entry() 已有冷却期、突破检测、持仓检查、z4h 绝对值过滤四层前置过滤

1.2 待解决的问题

均值回归策略在以下场景存在明显风险:

场景 问题 后果
资产处于趋势机制(Hurst > 0.6) 均值回归假设本身失效 spread 持续扩张,所有反向入场均面临系统性亏损
单边持续上涨 N 根 K 线后 追涨做多(跟趋势入场) 趋势已过度延伸,极易被反转砸穿
单边持续下跌 N 根 K 线后 追跌做空(跟趋势入场) 跌幅充分释放,反弹风险极高
短时间内迅速暴涨 做空对抗强动量 被轧空,止损代价极大
短时间内迅速暴跌 做多接飞刀 继续下杀,止损代价极大

1.3 四项约束目标

约束 1:连续下跌 → 不追跌(不做空)
约束 2:连续上涨 → 不追涨(不做多)
约束 3:迅速暴涨 → 不做空
约束 4:迅速暴跌 → 不做多

1.4 过滤维度

为全面覆盖配对交易的风险,过滤器需要在三个维度同时工作:

维度 检查对象 原因
Alt 腿(单腿) alt symbol 的价格动量 直接持仓标的的风险
Base 腿(单腿) base symbol 的价格动量 配对交易中 base 腿方向相反,同样承担动量风险
Spread(配对层) 两腿价差的趋势性 单腿动量不等于 spread 动量;两腿同涨但 spread 稳定时不应误杀

2. 算法选择与学术依据

2.0 Layer 0:市场机制检测

动机:均值回归的前提条件

均值回归策略的根本假设是价格序列处于均值回归机制(mean-reverting regime),即 Hurst 指数 H < 0.5。当市场进入趋势机制(H > 0.6)时,无论 spread 如何扩张,继续做均值回归的逻辑前提已经失效——这是一个比"动量过强"更根本的风险,应作为最前置的硬拦截。

选择:DFA-2(二阶去趋势波动分析)估算 Hurst 指数 + R² 质检

候选算法深度对比

算法 核心思路 优点 缺点 5min K 线适用性
经典 R/S 法 Rescaled Range 统计 最早提出(Hurst 1951) 对非平稳序列有偏,受趋势污染 一般
方差比检验(VR) 不同时间步长的方差比 快速、有统计检验 只测试特定时间步,非连续估计 一般
DFA-1(线性去趋势) 多尺度线性去趋势 RMS 波动 对非平稳序列鲁棒 线性去趋势不足以消除抛物线型加速趋势 一般
DFA-2(二阶去趋势) 多尺度二阶多项式去趋势后的 RMS 波动 对线性和抛物线趋势均鲁棒,无偏估计 需要每窗口 ≥ 3 个数据点 最适合
MFDFA(多分形 DFA) DFA 的多阶矩扩展 捕捉多分形特性 计算复杂度高,参数多 过于复杂

学术背景:

DFA 由 Peng et al. (1994) 在 DNA 序列分析中提出,后被 Mantegna & Stanley (1995) 引入金融时间序列。Kantelhardt et al. (2002, Physica A) 建议金融序列至少使用 DFA-2(二阶多项式去趋势),因为金融价格经常出现加速上涨/下跌(抛物线型趋势),DFA-1 的线性去趋势无法消除此类趋势分量,导致 Hurst 高估(假阳性)。

v4.0 关键改进 1:DFA-1 → DFA-2

加密市场中 FOMO 驱动的加速上涨和清算级联的加速下跌非常常见,此时 DFA-1 会因线性去趋势不充分而高估 Hurst,导致误拦截。DFA-2 使用二阶多项式(抛物线)去趋势,对此类场景鲁棒:

DFA-1:每个窗口拟合 y = a + b·x            → 只消除线性趋势
DFA-2:每个窗口拟合 y = a + b·x + c·x²     → 同时消除线性和抛物线趋势

v4.0 关键改进 2:R² 质检门控

60 根 5min K 线只能产生约 5-7 个尺度点,log-log 回归可能非常不稳定。没有 R² 质检的硬拦截等于建立在不可靠估计之上

R² = 1 - SS_res / SS_tot

R² ≥ 0.85 → Hurst 估计可靠,正常使用
R² < 0.85 → 尺度点拟合不佳,Hurst 估计不可信 → 返回 None → 放行

核心公式(v4.0 完整版):

输入:最近 hurst_lookback 根 K 线的收盘价序列 {close[i]}

步骤 1:计算均值中心化对数收益率的累积和(DFA profile)
  ret[i] = ln(close[i] / close[i-1])
  profile[k] = sum(ret[i] - mean(ret)) for i=1..k

步骤 2:多尺度二阶去趋势 RMS 波动(DFA-2)
  对每个尺度 s = 4, 6, 8, ..., N/4:
    将 profile 分为 N/s 个不重叠窗口
    在每个窗口内做二阶多项式去趋势(y = a + b·x + c·x²,最小二乘)
    计算残差 RMS:F(s) = sqrt(mean(residuals²))

步骤 3:log-log 线性回归 + R² 质检
  Hurst = slope of log(F(s)) vs log(s)
  R² = 1 - SS_res / SS_tot
  R² < 0.85 → 估计不可靠 → 放行

触发条件(Layer 0 硬拦截):

任一腿的 Hurst(DFA-2) > hurst_threshold(默认 0.60)
  且 R² >= 0.85(估计质量合格)
  → 趋势机制已激活,均值回归假设失效
  → 直接拒绝(硬拦截,不可被 Layer 3 仲裁推翻)

为什么 H > 0.6 选为阈值:

  • H = 0.5:随机游走(无记忆,均值回归和趋势都不成立)
  • H < 0.5:反持续(均值回归,配对策略最理想状态)
  • H = 0.6:弱趋势,均值回归仍可能有效,不应过于激进拦截
  • H > 0.6:显著趋势,均值回归假设明确失效,应拦截

2.1 约束 1&2:持续单边行情过滤(Layer 1)

候选算法深度对比

算法 核心思路 优点 缺点 1-5min 适用性
连续 K 线计数 计数涨/跌根数 直观 噪音大,交替涨跌频繁,漏判 不适合
纯净位移 (Net Displacement) 只看起点→终点 对噪音免疫 路径盲:V 形反转误杀,震荡漂移漏判 一般
ADX + DMI 趋势强度指标 学术验证充分 滞后较大(14期默认),参数较多 一般
EMA 方向 指数均线斜率 平滑 滞后大,无法区分趋势质量 一般
Kaufman ER + 净位移(联合) 效率比率衡量趋势质量 + 净位移衡量幅度 同时捕捉方向和路径质量,消除 V 形误杀 最适合

选择:Kaufman 效率比率 + 自适应净位移(RS 波动率驱动,非对称阈值)

学术背景:

Perry Kaufman 在 Smarter Trading (1995) 中提出 Efficiency Ratio,后被广泛应用于 KAMA(Kaufman Adaptive Moving Average)中。ER 是唯一同时衡量方向性和路径效率的单一指标,复杂度 O(N),无需预热统计分布。

非对称 ER 阈值

加密市场存在结构性非对称性:上涨由散户 FOMO 驱动,趋势延续性更强;下跌由恐慌清算驱动,往往更急但反弹更快。因此:

  • er_threshold_long = 0.60:拦截做多方向时更严格(上涨趋势延续性强,均值回归机会少)
  • er_threshold_short = 0.50:拦截做空方向时稍宽松(下跌后反弹较快)

核心公式:

N = sustained_lookback(回望根数,默认 30 根 5min K 线 = 150 分钟)

# Kaufman Efficiency Ratio:方向距离 / 路径总长度
direction_distance = abs(close[-1] - close[-N])
path_length = sum(abs(close[i] - close[i-1]) for i in [-N+1, ..., -1])
ER = direction_distance / path_length    # 范围 [0, 1]

# 净位移(带符号)
net_return = (close[-1] - close[-N]) / close[-N]

# 自适应阈值:用 Rogers-Satchell 波动率标准化(per-symbol 基准)
rs_vol = per-symbol 滚动中位数基准(见 2.2 节)
sustained_threshold = base_threshold * (rs_vol_current / rs_vol_baseline)
                     (限制缩放范围:0.3x ~ 3.0x)

# 联合触发条件(非对称):
约束1 触发:direction == 'short'
            AND net_return <= -sustained_threshold
            AND ER >= er_threshold_short(= 0.50)

约束2 触发:direction == 'long'
            AND net_return >= +sustained_threshold
            AND ER >= er_threshold_long(= 0.60)

2.2 约束 3&4:急涨急跌过滤(Layer 2)

候选算法深度对比

算法 核心思路 优点 缺点 1-5min 适用性
固定百分比阈值 单根 K 线涨幅 > X% 最简单 波动率变化大时误杀/漏判 不适合
SMA ATR 倍数 单根 vs 简单平均 ATR 自适应 SMA 响应慢,路径效率不如 EMA,只查单根 一般
Z-Score of Returns 滚动分布标准化 自适应 需 50+ 根历史才稳定 一般
GK(Garman-Klass)+ CUSUM OHLC 波动率 + 累积和检测 高效,自适应 零漂移假设:GK 在持续趋势市场有偏 一般
RS(Rogers-Satchell)+ CUSUM + VWPM 量价确认 漂移不变 OHLC 波动率 + 累积和检测 + 量价动量 无漂移假设,跨资产因子,非对称阈值 略复杂 最适合

Rogers-Satchell 替代 Garman-Klass(v3.0 继承)

学术依据:

Garman-Klass (1980) 波动率估计器的理论假设是零漂移(zero drift),即价格序列的期望收益率为零。但加密市场的趋势行情中漂移不为零,此时 GK 的修正项 -(2ln2-1)*ln(C/O)² 会引入系统偏差。

Rogers-Satchell (1991) 解决了这个问题:

RS(i) = ln(H/C) * ln(H/O) + ln(L/C) * ln(L/O)

性质:
  - 完全漂移不变(drift-invariant),不假设零期望收益
  - 单根 K 线恒 >= 0(H>=C, H>=O, L<=C, L<=O 保证两项均非负)
  - 理论效率 ≈ close-to-close 的 6.2 倍(vs GK 的 7.4 倍,效率略低但无偏)
  - 特别适合 24/7 加密市场(无隔夜跳空,RS 直接优于 Yang-Zhang)

Per-symbol 基准波动率(v3.0 继承)

每个 symbol 独立维护 200 样本的 RS 滚动中位数:

per_symbol_baseline[symbol] = rolling_median(RS_history[symbol], maxlen=200)
adaptive_thresh = base_thresh * (rs_vol_current / per_symbol_baseline[symbol])

BTC 的阈值相对于 BTC 自身历史波动率缩放,alt 相对于 alt 自身缩放,完全解耦。

v4.0 关键改进 3:VWPM(Volume-Weighted Price Momentum)量价确认

v3.0 的量价确认仅使用 volume_ratio >= 1.5(当前成交量 / Volume EMA),只能区分"放量/缩量",无法判断成交量是否在确认价格方向

v4.0 引入 VWPM(Volume-Weighted Price Momentum),检查近期成交量是否集中在价格运动的方向上:

VWPM = Σ(ret[i] * vol[i]) / Σ(vol[i])    # 最近 vwpm_window 根 K 线

VWPM 为正 → 成交量集中在上涨 K 线 → 上涨有量能支撑
VWPM 为负 → 成交量集中在下跌 K 线 → 下跌有量能支撑
|VWPM| / rs_vol >= vwpm_confirm_ratio → 量价方向一致性确认

联合确认逻辑(v4.0):
  volume_confirmed = volume_ratio >= 1.5 AND |VWPM|/rs_vol >= vwpm_confirm_ratio(默认 0.8)

  旧方案:仅看量是否放大 → 假放量(对倒、做市)可能误触发
  新方案:量大 + 量在对的方向 → 信号更可靠

学术依据:

Kyle (1985) 的知情交易模型表明,价格与成交量的联合信号比单独信号更有区分力。VWPM 是 Kyle 模型在 K 线级别的简化实现。

v4.0 关键改进 4:BTC 跨资产动量因子

加密市场有强烈的 BTC 领先效应——BTC 急动后,alt 跟随的概率极高。当前系统只检查 alt 和 base 各自的动量,不考虑 BTC 作为系统性因子的影响。

# BTC CUSUM 压力指数
btc_stress = max(btc_S_pos, btc_S_neg)

# 当 BTC CUSUM 超过压力阈值(默认 2.0)时,降低 alt 的 CUSUM 检测阈值
if symbol != market_ref AND btc_stress > btc_stress_threshold:
    stress_factor = max(0.7, 1.0 - (btc_stress - btc_stress_threshold) * 0.1)
    effective_cusum_thresh_up   *= stress_factor
    effective_cusum_thresh_down *= stress_factor

含义:
  BTC 正在急动 → 全市场即将联动 → 降低 alt 的急动检测阈值 → 更早拦截
  stress_factor 下限 0.7 → CUSUM 阈值最多降低 30%(避免过度敏感)

学术依据:

Moskowitz, Ooi & Pedersen (2012) "Time Series Momentum" 证明了资产间动量的系统性溢出效应。加密市场中 BTC dominance 使得这一效应尤其显著。

非对称 CUSUM 阈值

# 双侧 CUSUM(Rogers-Satchell 波动率标准化)
ret(i) = (close(i) - close(i-1)) / close(i-1)
z(i)   = ret(i) / rs_vol

S_pos(i) = max(0, S_pos(i-1) + z(i) - drift)    # 检测正向急动(暴涨)
S_neg(i) = max(0, S_neg(i-1) - z(i) - drift)    # 检测负向急动(暴跌)

# 非对称触发阈值(BTC 因子调整后的有效阈值)
spike_up   = S_pos >= effective_cusum_thresh_up     # 基础 3.5
spike_down = S_neg >= effective_cusum_thresh_down   # 基础 2.5

# VWPM + 量比联合确认(v4.0)
volume_confirmed = volume_ratio >= 1.5 AND |VWPM|/rs_vol >= vwpm_confirm_ratio

约束3 触发:direction == 'short'
            AND spike_up == True
            AND volume_confirmed == True

约束4 触发:direction == 'long'
            AND spike_down == True
            AND volume_confirmed == True

CUSUM 隐式冷却期(显式记录于此):

隐式冷却期 ≈ threshold / drift × bar_interval
           ≈ 3.5 / 0.5 × 5min = 35min(spike_up,BTC 未施压时)
           ≈ 2.5 / 0.5 × 5min = 25min(spike_down,BTC 未施压时)
BTC 施压时阈值可降至 70%:
           ≈ 2.45 / 0.5 × 5min ≈ 24.5min(spike_up 最激进时)
           ≈ 1.75 / 0.5 × 5min ≈ 17.5min(spike_down 最激进时)

2.3 Spread 层面趋势检测(Layer 3)

v4.0 改进:ADF + KPSS + ER 三重仲裁

学术背景:

v3.0 中 Layer 3 使用 ADF + ER 双重仲裁,但 ADF 检验存在已知缺陷:对近单位根替代假设功效极低,尤其在小样本(20 个观测值)下。现代计量经济学的标准做法是 ADF + KPSS 联合检验(Kwiatkowski et al., 1992):

  • ADF 的 H0:序列有单位根(非平稳) → 拒绝说明平稳
  • KPSS 的 H0:序列平稳 → 拒绝说明非平稳

两者互为对偶。只用 ADF 时,"不拒绝 H0"既可能是"真的非平稳"也可能是"样本不足导致检验无力"。加 KPSS 后能区分这两种情况。

v4.0 四种情况决策矩阵:

ADF 结果 KPSS 结果 结论 动作
拒绝 H0(p<0.05,平稳) 不拒绝 H0(p>0.05,平稳) 确认平稳 推翻拦截,放行
不拒绝 H0(非平稳) 拒绝 H0(p<0.05,非平稳) 确认非平稳 维持拦截
拒绝 H0(平稳) 拒绝 H0(非平稳) 矛盾 保守维持拦截
不拒绝 H0 不拒绝 H0 不确定 保守维持拦截

三重确认逻辑(ER 快速筛 + ADF/KPSS 精确验证):

# 复用 z4h 序列作为 spread 代理
z4h_series = [z4h[-N], ..., z4h[-1]]

# 第一关:ER 快速检查(O(N),无需统计检验)
spread_er = spread_dir / spread_path

# ER 未达阈值 → 路径曲折 → 直接判定无趋势 → 放行(不调用 ADF/KPSS,节省计算)
if spread_er < spread_er_threshold:
    → 无趋势,推翻拦截,放行

# 第二关:ADF + KPSS 对偶检验(仅在 ER 认为有趋势时执行)
adf_pvalue = adfuller(z4h_series, maxlag=2, regression='c')[1]
kpss_pvalue = kpss(z4h_series, regression='c', nlags='auto')[1]

# 四种情况决策:
if adf_pvalue < 0.05 AND kpss_pvalue > 0.05:
    → 双重确认平稳 → 推翻拦截,放行(最强的放行信号)

if adf_pvalue >= 0.05 AND kpss_pvalue <= 0.05:
    → 双重确认非平稳 → 维持拦截(最强的拦截信号)

其他情况(矛盾或不确定):
    → 保守维持拦截(宁可误杀不可漏杀)

# 无 statsmodels 或样本不足(< 15):仅凭 ER 判断(降级模式)

为什么 ADF/KPSS 需要 ER 前置筛选:

  1. ADF+KPSS 计算有额外开销(statsmodels),每次信号都调用会浪费资源
  2. ER 是 O(N) 的快速初步筛选,绝大多数情况 ER < threshold 时直接放行,无需统计检验
  3. 只有"ER 认为有趋势"时才调用 ADF+KPSS 做精确验证,兼顾性能和精度

2.4 算法组合架构(四层)

输入信号(direction, alt_symbol, base_symbol)
    │
    ├──▶ Layer 0: 市场机制检测(DFA-2 Hurst 指数 + R² 质检)   ← v4.0 升级
    │      ├── alt 腿: Hurst(alt) > 0.60 AND R² >= 0.85?
    │      └── base 腿: Hurst(base) > 0.60 AND R² >= 0.85?
    │      任一腿触发 → 直接拒绝(硬拦截,均值回归假设失效)
    │
    ├──▶ Layer 2: 急动检测(CUSUM + RS + VWPM + BTC因子,非对称阈值)  ← v4.0 升级
    │      ├── BTC 跨资产压力 → 动态调低 alt CUSUM 阈值
    │      ├── alt 腿: check(alt, direction)
    │      └── base 腿: check(base, opposite_direction)
    │      任一腿被拦截 → 直接拒绝(硬拦截,执行风险不可对冲)
    │
    ├──▶ Layer 1: 单腿持续趋势过滤(ER + RS 自适应净位移,非对称阈值)
    │      ├── alt 腿: check(alt, direction)
    │      └── base 腿: check(base, opposite_direction)
    │      任一腿被拦截 → 进入 Layer 3 仲裁(软拦截)
    │
    └──▶ Layer 3: Spread ADF + KPSS + ER 三重仲裁              ← v4.0 升级
           仅在 Layer 1 软拦截时启动
           ER < threshold → 直接放行(O(N) 快速路径)
           ADF p<0.05 + KPSS p>0.05 → 确认平稳,放行
           ADF p>=0.05 + KPSS p<=0.05 → 确认非平稳,维持拦截
           矛盾/不确定 → 保守维持拦截
    │
    ▼
  最终决策:允许 / 拒绝入场

v4.0 架构图修正:Layer 2(硬拦截)优先于 Layer 1(软拦截)执行,避免不必要的 Layer 3 仲裁开销。这是正确的优先级顺序——先排除不可仲裁的硬拦截,再处理可仲裁的软拦截。

设计理念(v4.0 修订)

层级 类型 可仲裁 触发含义 v4.0 变化
Layer 0(Hurst) 硬拦截 均值回归假设根本失效 DFA-2 + R² 质检
Layer 2(CUSUM) 硬拦截 急动执行风险极高 VWPM + BTC 因子
Layer 1(ER+位移) 软拦截 是(Layer 3) 单腿持续趋势,可能误杀 继承 v3.0
Layer 3(ADF+KPSS+ER) 仲裁器 Spread 仍平稳则放行 新增 KPSS 对偶检验

3. 算法具体实现

3.1 模块位置

src/trading/
  momentum_filter.py    ← 新建(独立模块,不耦合策略逻辑)
  strategy.py           ← 修改 _check_entry(),注入并调用 filter;SymbolBaseline 新增 z4h_history
  config.py             ← 修改 StrategyParams,新增过滤器参数(含新增参数)
  orchestrator.py       ← 修改 process_analysis(),透传 OHLCV + volume
src/services/
  realtime_kline_service_base.py  ← 修改 _trigger_strategy_if_ready(),提取双腿 OHLCV + volume

3.2 MomentumFilter 完整代码(v4.0)

# src/trading/momentum_filter.py
"""
开仓动量过滤器 v4.0

四层过滤架构:
  Layer 0: Hurst DFA-2 + R² 质检      → 趋势机制检测(硬拦截)     [v4.0 升级]
  Layer 1: ER + RS 自适应净位移        → 持续趋势检测(软拦截)
  Layer 2: CUSUM + RS + VWPM + BTC因子 → 急动检测(硬拦截)         [v4.0 升级]
  Layer 3: Spread ADF + KPSS + ER      → 误杀修正(仅软拦截时启动) [v4.0 升级]

v4.0 改进清单:
  1. [Layer 0] DFA-1 → DFA-2 二阶多项式去趋势(消除抛物线型加速趋势偏差)
  2. [Layer 0] 新增 R² 质检门控(R² < 0.85 → 估计不可靠 → 放行)
  3. [Layer 2] VWPM 量价确认替代简单 volume_ratio(量大+方向一致才确认)
  4. [Layer 2] BTC 跨资产动量因子(BTC CUSUM 高压时降低 alt 检测阈值)
  5. [Layer 3] KPSS 对偶检验(ADF+KPSS 四象限决策矩阵,消除 ADF 低功效问题)
  6. [接口] check() 返回 (bool, str, bool) 不变
"""

import math
from collections import deque
from datetime import datetime

try:
    from statsmodels.tsa.stattools import adfuller as _adfuller
    _HAS_ADF = True
except ImportError:
    _HAS_ADF = False

try:
    from statsmodels.tsa.stattools import kpss as _kpss
    _HAS_KPSS = True
except ImportError:
    _HAS_KPSS = False


class _RollingMedian:
    """
    固定窗口滚动中位数(maxlen <= 200)。

    当前实现 O(N log N) per query(排序法),对 200 元素 + 5min 更新频率无性能问题。
    若未来基准窗口扩大或更新频率提高,可改为双堆(max-heap + min-heap)
    实现 O(log N) push + O(1) median 查询。
    """

    __slots__ = ('_data',)

    def __init__(self, maxlen: int):
        self._data: deque = deque(maxlen=maxlen)

    def push(self, val: float) -> None:
        self._data.append(val)

    @property
    def value(self) -> float | None:
        if not self._data:
            return None
        s = sorted(self._data)
        n = len(s)
        mid = n >> 1
        return s[mid] if n & 1 else (s[mid - 1] + s[mid]) * 0.5

    def __len__(self) -> int:
        return len(self._data)


def _hurst_dfa2(closes: list[float], r2_threshold: float = 0.85) -> tuple[float | None, float | None]:
    """
    DFA-2(二阶去趋势波动分析)估算 Hurst 指数。

    相比 DFA-1(线性去趋势),DFA-2 使用二阶多项式(y = a + bx + cx²)去趋势,
    对加密市场常见的抛物线型加速上涨/下跌更鲁棒(Kantelhardt et al., 2002)。

    返回值:
      (hurst, r_squared)
      数据不足或 R² < r2_threshold 时返回 (None, None)
      H < 0.5: 均值回归
      H ≈ 0.5: 随机游走
      H > 0.5: 趋势持续
    """
    n = len(closes)
    if n < 20:
        return None, None

    # 计算对数收益率
    rets = []
    for i in range(1, n):
        if closes[i] > 0 and closes[i - 1] > 0:
            rets.append(math.log(closes[i] / closes[i - 1]))
    nr = len(rets)
    if nr < 16:
        return None, None

    mean_r = sum(rets) / nr

    # 构建均值中心化累积和(DFA profile)
    profile = []
    cum = 0.0
    for r in rets:
        cum += r - mean_r
        profile.append(cum)

    # 多尺度 RMS 波动率计算(DFA-2:二阶多项式去趋势)
    scales: list[float] = []
    rms_vals: list[float] = []

    s = 4
    max_s = nr // 4
    while s <= max_s:
        n_win = nr // s
        if n_win < 2:
            break

        rss_sum = 0.0
        rss_cnt = 0
        for i in range(n_win):
            seg = profile[i * s: (i + 1) * s]
            ws = len(seg)
            if ws < 3:
                # 窗口太小无法做二阶拟合,降级为线性
                if ws >= 2:
                    x_m = (ws - 1) * 0.5
                    y_m = sum(seg) / ws
                    cov = sum((j - x_m) * (seg[j] - y_m) for j in range(ws))
                    var_x = sum((j - x_m) ** 2 for j in range(ws))
                    slope = cov / var_x if var_x > 1e-12 else 0.0
                    intercept = y_m - slope * x_m
                    rss = sum((seg[j] - (intercept + slope * j)) ** 2 for j in range(ws)) / ws
                    rss_sum += rss
                    rss_cnt += 1
                continue

            # ── 二阶多项式去趋势(y = a + b·x + c·x²)──
            # 预计算 x 的幂次和(x = 0, 1, ..., ws-1)
            s0 = float(ws)
            s1 = ws * (ws - 1) / 2.0
            s2 = ws * (ws - 1) * (2 * ws - 1) / 6.0
            s3 = (ws * (ws - 1) / 2.0) ** 2
            s4 = ws * (ws - 1) * (2 * ws - 1) * (3 * ws * ws - 3 * ws - 1) / 30.0

            # y 的交叉和
            sy0 = sum(seg)
            sy1 = sum(j * seg[j] for j in range(ws))
            sy2 = sum(j * j * seg[j] for j in range(ws))

            # 3×3 正规方程 Cramer 法则求解
            det = (s0 * (s2 * s4 - s3 * s3)
                   - s1 * (s1 * s4 - s3 * s2)
                   + s2 * (s1 * s3 - s2 * s2))

            if abs(det) < 1e-12:
                # 奇异矩阵,降级为线性去趋势
                x_m = (ws - 1) * 0.5
                y_m = sum(seg) / ws
                cov = sum((j - x_m) * (seg[j] - y_m) for j in range(ws))
                var_x = sum((j - x_m) ** 2 for j in range(ws))
                slope = cov / var_x if var_x > 1e-12 else 0.0
                intercept = y_m - slope * x_m
                rss = sum((seg[j] - (intercept + slope * j)) ** 2 for j in range(ws)) / ws
                rss_sum += rss
                rss_cnt += 1
                continue

            a = ((sy0 * (s2 * s4 - s3 * s3)
                  - s1 * (sy1 * s4 - s3 * sy2)
                  + s2 * (sy1 * s3 - s2 * sy2)) / det)
            b = ((s0 * (sy1 * s4 - s3 * sy2)
                  - sy0 * (s1 * s4 - s3 * s2)
                  + s2 * (s1 * sy2 - sy1 * s2)) / det)
            c = ((s0 * (s2 * sy2 - sy1 * s3)
                  - s1 * (s1 * sy2 - sy1 * s2)
                  + sy0 * (s1 * s3 - s2 * s2)) / det)

            rss = sum((seg[j] - (a + b * j + c * j * j)) ** 2 for j in range(ws)) / ws
            rss_sum += rss
            rss_cnt += 1

        if rss_cnt >= 2:
            f_s = math.sqrt(rss_sum / rss_cnt)
            if f_s > 1e-14:
                scales.append(math.log(s))
                rms_vals.append(math.log(f_s))

        # 对数均匀采样尺度
        s = max(s + 1, int(s * 1.4 + 0.5))

    if len(scales) < 3:
        return None, None

    # log-log 线性回归斜率 = Hurst 指数
    n_pts = len(scales)
    x_m = sum(scales) / n_pts
    y_m = sum(rms_vals) / n_pts
    num = sum((scales[i] - x_m) * (rms_vals[i] - y_m) for i in range(n_pts))
    den = sum((scales[i] - x_m) ** 2 for i in range(n_pts))
    if den < 1e-12:
        return None, None

    hurst = max(0.0, min(1.0, num / den))

    # ── R² 质检 ──
    slope = num / den
    ss_res = sum((rms_vals[i] - (y_m + slope * (scales[i] - x_m))) ** 2 for i in range(n_pts))
    ss_tot = sum((rms_vals[i] - y_m) ** 2 for i in range(n_pts))
    r_squared = 1.0 - ss_res / ss_tot if ss_tot > 1e-12 else 0.0

    if r_squared < r2_threshold:
        return None, None  # 估计不可靠,放行

    return hurst, r_squared


class MomentumFilter:
    """
    开仓动量过滤器 v4.0 — 四层架构

    公开接口:
      update(symbol, close, high, low, open_, volume, kline_time) — 每根 K 线调用一次
      check(symbol, direction) -> (allowed, reason, is_soft)
      check_spread(z4h_history, direction) -> (has_trend, reason)
      ready(symbol) -> bool
    """

    def __init__(
        self,
        # ── 总开关 ──
        enabled: bool = True,
        # ── Layer 0: Hurst DFA-2 ──
        hurst_enabled: bool = True,
        hurst_lookback: int = 60,
        hurst_threshold: float = 0.60,
        hurst_r2_threshold: float = 0.85,        # v4.0 新增:R² 质检门槛
        # ── Layer 1: 持续趋势 ──
        sustained_lookback: int = 30,
        sustained_base_threshold: float = 0.008,
        er_threshold_long: float = 0.60,
        er_threshold_short: float = 0.50,
        # ── Layer 2: 急动检测 ──
        rs_period: int = 10,
        cusum_drift: float = 0.5,
        cusum_threshold_spike_up: float = 3.5,
        cusum_threshold_spike_down: float = 2.5,
        volume_confirm_ratio: float = 1.5,
        volume_ema_period: int = 20,
        vwpm_window: int = 5,                     # v4.0 新增:VWPM 回望窗口
        vwpm_confirm_ratio: float = 0.8,          # v4.0 新增:VWPM 确认阈值
        # ── Layer 2: BTC 跨资产因子 ──
        market_ref_symbol: str = "BTC",            # v4.0 新增
        btc_stress_threshold: float = 2.0,         # v4.0 新增
        btc_stress_factor_min: float = 0.7,        # v4.0 新增
        # ── Layer 3: Spread 仲裁 ──
        spread_lookback: int = 20,
        spread_er_threshold: float = 0.45,
        spread_net_threshold: float = 1.5,
        spread_adf_pvalue: float = 0.05,          # v4.0 修改:ADF 显著性水平
        spread_kpss_pvalue: float = 0.05,         # v4.0 新增:KPSS 显著性水平
    ):
        self._enabled = enabled

        # Layer 0
        self._hurst_enabled = hurst_enabled
        self._hurst_lookback = max(20, hurst_lookback)
        self._hurst_thresh = hurst_threshold
        self._hurst_r2_thresh = hurst_r2_threshold

        # Layer 1
        self._sustained_n = max(5, sustained_lookback)
        self._sustained_base_thresh = sustained_base_threshold
        self._er_thresh_long = er_threshold_long
        self._er_thresh_short = er_threshold_short

        # Layer 2
        self._rs_period = rs_period
        self._cusum_drift = cusum_drift
        self._cusum_thresh_up = cusum_threshold_spike_up
        self._cusum_thresh_down = cusum_threshold_spike_down
        self._vol_confirm_ratio = volume_confirm_ratio
        self._vol_ema_period = volume_ema_period
        self._vwpm_window = vwpm_window
        self._vwpm_confirm_ratio = vwpm_confirm_ratio

        # Layer 2: BTC 因子
        self._market_ref = market_ref_symbol
        self._btc_stress_thresh = btc_stress_threshold
        self._btc_stress_factor_min = btc_stress_factor_min

        # Layer 3
        self._spread_lookback = spread_lookback
        self._spread_er_thresh = spread_er_threshold
        self._spread_net_thresh = spread_net_threshold
        self._spread_adf_pvalue = spread_adf_pvalue
        self._spread_kpss_pvalue = spread_kpss_pvalue

        # 数据缓冲区大小
        buf_size = max(
            hurst_lookback + 5,
            sustained_lookback + 5,
            rs_period + 10,
            volume_ema_period + 5,
            vwpm_window + 2,
        ) + 10
        self._buf_size = buf_size

        # symbol → deque of (close, high, low, open_, volume)
        self._buffers: dict[str, deque] = {}
        # symbol → 最后一次更新的 kline_time(去重)
        self._last_kline_time: dict[str, datetime] = {}
        # symbol → CUSUM 状态 (S_pos, S_neg)
        self._cusum_state: dict[str, tuple[float, float]] = {}
        # symbol → Rogers-Satchell 波动率 EMA(方差形式)
        self._rs_ema: dict[str, float] = {}
        # symbol → Volume EMA
        self._vol_ema: dict[str, float] = {}
        # symbol → Per-symbol 基准波动率滚动中位数(maxlen=200)
        self._baseline_median: dict[str, _RollingMedian] = {}

    # ------------------------------------------------------------------
    # 公开接口
    # ------------------------------------------------------------------

    def update(
        self,
        symbol: str,
        close: float,
        high: float = 0.0,
        low: float = 0.0,
        open_: float = 0.0,
        volume: float = 0.0,
        kline_time: datetime | None = None,
    ) -> None:
        """
        每根新 K 线收盘时调用。
        high/low/open_ 可选;有则 Rogers-Satchell 更精确,无则降级为 close-to-close。
        volume 可选;有则启用量价确认,无则跳过量确认。
        kline_time 用于去重:同一 symbol 多配对时只追加一次。
        """
        if kline_time is not None:
            if self._last_kline_time.get(symbol) == kline_time:
                return
            self._last_kline_time[symbol] = kline_time

        if symbol not in self._buffers:
            self._buffers[symbol] = deque(maxlen=self._buf_size)
        self._buffers[symbol].append((close, high, low, open_, volume))

        buf = self._buffers[symbol]
        if len(buf) >= 2:
            self._online_update(symbol, buf)

    def check(self, symbol: str, direction: str) -> tuple[bool, str, bool]:
        """
        Layer 0 + Layer 2 + Layer 1 检查(单腿维度)。

        执行顺序:Layer 0(硬)→ Layer 2(硬)→ Layer 1(软)
        硬拦截优先于软拦截,避免不必要的 Layer 3 仲裁开销。

        Returns:
            (allowed, reason, is_soft)
            allowed=False + is_soft=False → 硬拦截(Layer 0 / Layer 2),不可被 Layer 3 推翻
            allowed=False + is_soft=True  → 软拦截(Layer 1),可由 Layer 3 Spread 仲裁推翻
        """
        if not self._enabled:
            return True, "", False

        buf = self._buffers.get(symbol)
        if buf is None or len(buf) < self._rs_period + 2:
            return True, "", False

        # ── Layer 0(硬拦截):Hurst DFA-2 趋势机制检测 ──
        ok, reason = self._check_hurst(symbol)
        if not ok:
            return False, reason, False

        # ── Layer 2(硬拦截):急动检测(优先于 Layer 1) ──
        ok, reason = self._check_spike(symbol, direction)
        if not ok:
            return False, reason, False

        # ── Layer 1(软拦截):持续趋势 ──
        if len(buf) >= self._sustained_n + 2:
            ok, reason = self._check_sustained(symbol, direction)
            if not ok:
                return False, reason, True

        return True, "", False

    def check_spread(self, z4h_history: list[float], direction: str) -> tuple[bool, str]:
        """
        Layer 3: Spread ADF + KPSS + ER 三重仲裁。
        仅在存在软拦截(Layer 1)时调用。

        Returns:
            (has_trend, reason)
            has_trend=True  → Spread 有趋势 → 维持拦截
            has_trend=False → Spread 无趋势 → 推翻拦截,放行
        """
        n = self._spread_lookback
        if len(z4h_history) < n + 1:
            return False, ""

        series = z4h_history[-(n + 1):]

        # ── 第一关:ER 快速筛选(O(N),大多数情况到这里结束) ──
        spread_dir = abs(series[-1] - series[0])
        spread_path = sum(abs(series[i] - series[i - 1]) for i in range(1, len(series)))
        er = spread_dir / spread_path if spread_path > 1e-10 else 0.0
        spread_net = series[-1] - series[0]

        # 方向性检查
        if direction == 'short':
            dir_ok = spread_net <= -self._spread_net_thresh
        else:
            dir_ok = spread_net >= self._spread_net_thresh

        er_signals_trend = er >= self._spread_er_thresh and dir_ok
        if not er_signals_trend:
            return False, ""

        # ── 第二关:ADF + KPSS 对偶检验(仅在 ER 认为有趋势时执行) ──
        if (_HAS_ADF or _HAS_KPSS) and len(series) >= 15:
            adf_p = None
            kpss_p = None

            if _HAS_ADF:
                try:
                    adf_p = _adfuller(series, maxlag=2, regression='c', autolag=None)[1]
                except Exception:
                    pass

            if _HAS_KPSS:
                try:
                    import warnings
                    with warnings.catch_warnings():
                        warnings.simplefilter("ignore")
                        kpss_p = _kpss(series, regression='c', nlags='auto')[1]
                except Exception:
                    pass

            # ── 四象限决策矩阵 ──
            if adf_p is not None and kpss_p is not None:
                adf_stationary = adf_p < self._spread_adf_pvalue
                kpss_stationary = kpss_p > self._spread_kpss_pvalue

                if adf_stationary and kpss_stationary:
                    # 双重确认平稳 → 推翻拦截
                    return False, (
                        f"ADF+KPSS确认平稳(ADF_p={adf_p:.3f}<{self._spread_adf_pvalue}"
                        f" KPSS_p={kpss_p:.3f}>{self._spread_kpss_pvalue})→推翻拦截"
                    )

                if not adf_stationary and not kpss_stationary:
                    # 双重确认非平稳 → 维持拦截
                    return True, (
                        f"Spread趋势(三重确认): ER={er:.2f}>={self._spread_er_thresh}"
                        f" 净位移={spread_net:+.3f}"
                        f" ADF_p={adf_p:.3f}>={self._spread_adf_pvalue}"
                        f" KPSS_p={kpss_p:.3f}<={self._spread_kpss_pvalue}"
                    )

                # 矛盾或不确定 → 保守维持拦截
                return True, (
                    f"Spread趋势(矛盾/不确定): ER={er:.2f}"
                    f" ADF_p={adf_p:.3f} KPSS_p={kpss_p:.3f}→保守拦截"
                )

            # 只有 ADF 可用(KPSS 不可用):降级为 v3.0 逻辑
            if adf_p is not None:
                if adf_p < self._spread_adf_pvalue:
                    return False, (
                        f"ADF判定平稳(p={adf_p:.3f}<{self._spread_adf_pvalue})→推翻拦截(无KPSS)"
                    )
                return True, (
                    f"Spread趋势(ER+ADF): ER={er:.2f} ADF_p={adf_p:.3f}(无KPSS)"
                )

        # 降级:无 statsmodels 或样本不足 → 仅凭 ER 判断
        return True, (
            f"Spread趋势(ER): ER={er:.2f}>={self._spread_er_thresh}"
            f" 净位移={spread_net:+.3f}"
        )

    def ready(self, symbol: str) -> bool:
        """该 symbol 是否已积累足够数据"""
        buf = self._buffers.get(symbol)
        if buf is None:
            return False
        return len(buf) >= self._rs_period + 2

    # ------------------------------------------------------------------
    # Layer 0: Hurst DFA-2(趋势机制检测 + R² 质检)
    # ------------------------------------------------------------------

    def _check_hurst(self, symbol: str) -> tuple[bool, str]:
        """Layer 0 硬拦截:Hurst > threshold 且 R² >= r2_threshold → 趋势机制"""
        if not self._hurst_enabled:
            return True, ""

        buf = self._buffers.get(symbol)
        if buf is None or len(buf) < self._hurst_lookback + 1:
            return True, ""

        closes = [d[0] for d in list(buf)[-(self._hurst_lookback + 1):]]
        h, r2 = _hurst_dfa2(closes, r2_threshold=self._hurst_r2_thresh)
        if h is None:
            return True, ""  # 数据不足或 R² 不达标,放行

        if h > self._hurst_thresh:
            return False, (
                f"Layer0-趋势机制: Hurst(DFA-2)={h:.3f}>{self._hurst_thresh}"
                f" R²={r2:.3f} → 均值回归假设失效"
            )
        return True, ""

    # ------------------------------------------------------------------
    # Layer 1: 持续趋势检测(ER + RS 自适应净位移,非对称阈值)
    # ------------------------------------------------------------------

    def _check_sustained(self, symbol: str, direction: str) -> tuple[bool, str]:
        """Layer 1 软拦截:ER >= threshold AND |net_return| >= adaptive_threshold"""
        buf = self._buffers[symbol]
        n = self._sustained_n
        data = list(buf)[-(n + 1):]
        closes = [d[0] for d in data]

        if len(closes) < n + 1:
            return True, ""

        ref = closes[0]
        if ref <= 0:
            return True, ""

        net_return = (closes[-1] - ref) / ref

        # Kaufman Efficiency Ratio
        direction_dist = abs(closes[-1] - ref)
        path_length = sum(abs(closes[i] - closes[i - 1]) for i in range(1, len(closes)))
        er = direction_dist / path_length if path_length > 1e-10 else 0.0

        # 非对称 ER 阈值
        er_thresh = self._er_thresh_long if direction == 'long' else self._er_thresh_short
        if er < er_thresh:
            return True, ""

        # RS 自适应阈值(per-symbol 基准)
        rs_var = self._rs_ema.get(symbol, 0.0)
        rs_vol = math.sqrt(max(0.0, rs_var))
        baseline = self._baseline_median.get(symbol)
        baseline_val = baseline.value if (baseline and len(baseline) >= 5) else None

        if rs_vol > 0 and baseline_val and baseline_val > 0:
            adaptive_thresh = self._sustained_base_thresh * (rs_vol / baseline_val)
            adaptive_thresh = max(
                self._sustained_base_thresh * 0.3,
                min(adaptive_thresh, self._sustained_base_thresh * 3.0),
            )
        else:
            adaptive_thresh = self._sustained_base_thresh

        if direction == 'short' and net_return <= -adaptive_thresh:
            return False, (
                f"Layer1-不追跌: 过去{n}根净跌幅={net_return:.2%}"
                f" 阈值=-{adaptive_thresh:.2%} ER={er:.2f}>={er_thresh:.2f}"
            )
        if direction == 'long' and net_return >= adaptive_thresh:
            return False, (
                f"Layer1-不追涨: 过去{n}根净涨幅={net_return:.2%}"
                f" 阈值=+{adaptive_thresh:.2%} ER={er:.2f}>={er_thresh:.2f}"
            )
        return True, ""

    # ------------------------------------------------------------------
    # Layer 2: 急动检测(CUSUM + RS + VWPM + BTC因子,非对称阈值)
    # ------------------------------------------------------------------

    def _check_spike(self, symbol: str, direction: str) -> tuple[bool, str]:
        """Layer 2 硬拦截:CUSUM 超阈值 + VWPM 量价确认 + BTC 跨资产因子"""
        s_pos, s_neg = self._cusum_state.get(symbol, (0.0, 0.0))

        buf = self._buffers[symbol]

        # ── BTC 跨资产因子:BTC CUSUM 高压时降低 alt 检测阈值 ──
        effective_thresh_up = self._cusum_thresh_up
        effective_thresh_down = self._cusum_thresh_down

        if symbol != self._market_ref:
            btc_s_pos, btc_s_neg = self._cusum_state.get(self._market_ref, (0.0, 0.0))
            btc_stress = max(btc_s_pos, btc_s_neg)
            if btc_stress > self._btc_stress_thresh:
                stress_factor = max(
                    self._btc_stress_factor_min,
                    1.0 - (btc_stress - self._btc_stress_thresh) * 0.1,
                )
                effective_thresh_up *= stress_factor
                effective_thresh_down *= stress_factor

        # ── VWPM + 量比联合确认 ──
        vol_confirmed, vol_ratio, vwpm_val = self._check_volume(symbol)

        rs_vol = math.sqrt(max(0.0, self._rs_ema.get(symbol, 0.0)))
        vol_str = f" 量比={vol_ratio:.1f}" if vol_ratio > 0 else ""
        vwpm_str = f" VWPM={vwpm_val:.4f}" if vwpm_val != 0.0 else ""
        btc_str = ""
        if symbol != self._market_ref and effective_thresh_up != self._cusum_thresh_up:
            btc_str = f" BTC压力→阈值×{effective_thresh_up/self._cusum_thresh_up:.2f}"

        if direction == 'short' and s_pos >= effective_thresh_up and vol_confirmed:
            return False, (
                f"Layer2-暴涨不做空: CUSUM+={s_pos:.2f}>={effective_thresh_up:.2f}"
                f" RS_vol={rs_vol:.4f}{vol_str}{vwpm_str}{btc_str}"
                f" (隐式冷却≈{effective_thresh_up/self._cusum_drift*5:.0f}min)"
            )

        if direction == 'long' and s_neg >= effective_thresh_down and vol_confirmed:
            return False, (
                f"Layer2-暴跌不做多: CUSUM-={s_neg:.2f}>={effective_thresh_down:.2f}"
                f" RS_vol={rs_vol:.4f}{vol_str}{vwpm_str}{btc_str}"
                f" (隐式冷却≈{effective_thresh_down/self._cusum_drift*5:.0f}min)"
            )

        return True, ""

    def _check_volume(self, symbol: str) -> tuple[bool, float, float]:
        """
        VWPM + 量比联合确认(v4.0)。

        Returns:
            (confirmed, vol_ratio, vwpm_value)
            confirmed=True 当且仅当:量比 >= 阈值 AND |VWPM|/rs_vol >= 确认比
        """
        buf = self._buffers.get(symbol)
        if buf is None or len(buf) < 2:
            return True, 0.0, 0.0  # 无数据,仅依赖 CUSUM

        current_vol = buf[-1][4]
        vol_ema = self._vol_ema.get(symbol, 0.0)

        # 量比检查
        if vol_ema > 0 and current_vol > 0:
            vol_ratio = current_vol / vol_ema
        else:
            return True, 0.0, 0.0  # 无成交量数据,仅依赖 CUSUM

        # VWPM 计算
        vwpm_val = 0.0
        w = min(self._vwpm_window, len(buf) - 1)
        if w >= 1:
            data = list(buf)[-(w + 1):]
            weighted_sum = 0.0
            total_vol = 0.0
            for i in range(1, len(data)):
                prev_c = data[i - 1][0]
                curr_c = data[i][0]
                v = data[i][4]
                if prev_c > 0 and v > 0:
                    ret = (curr_c - prev_c) / prev_c
                    weighted_sum += ret * v
                    total_vol += v
            if total_vol > 0:
                vwpm_val = weighted_sum / total_vol

        # 联合确认
        rs_vol = math.sqrt(max(0.0, self._rs_ema.get(symbol, 0.0)))
        if rs_vol > 1e-10 and vwpm_val != 0.0:
            vwpm_normalized = abs(vwpm_val) / rs_vol
            confirmed = (vol_ratio >= self._vol_confirm_ratio
                         and vwpm_normalized >= self._vwpm_confirm_ratio)
        else:
            # 无 VWPM 数据时降级为仅量比
            confirmed = vol_ratio >= self._vol_confirm_ratio

        return confirmed, vol_ratio, vwpm_val

    # ------------------------------------------------------------------
    # 在线更新(每根 K 线 O(1))
    # ------------------------------------------------------------------

    def _online_update(self, symbol: str, buf: deque) -> None:
        """O(1) 在线更新 CUSUM、RS EMA、Volume EMA、Per-symbol 基准中位数"""
        curr = buf[-1]
        prev = buf[-2]
        close, high, low, open_, volume = curr
        prev_close = prev[0]

        if prev_close <= 0 or close <= 0:
            return

        # ── Rogers-Satchell 波动率 EMA ──
        rs_val = self._calc_rs_single(high, low, open_, close)
        alpha_rs = 2.0 / (self._rs_period + 1)
        prev_rs = self._rs_ema.get(symbol, rs_val)
        new_rs = alpha_rs * rs_val + (1.0 - alpha_rs) * prev_rs
        self._rs_ema[symbol] = new_rs

        # ── Per-symbol 基准波动率(滚动中位数,200 样本) ──
        rs_vol_now = math.sqrt(max(0.0, new_rs))
        if symbol not in self._baseline_median:
            self._baseline_median[symbol] = _RollingMedian(maxlen=200)
        self._baseline_median[symbol].push(rs_vol_now)

        # ── CUSUM 更新(RS 波动率标准化)──
        rs_vol = math.sqrt(max(0.0, new_rs))
        if rs_vol > 1e-10:
            ret = (close - prev_close) / prev_close
            z = ret / rs_vol
            s_pos, s_neg = self._cusum_state.get(symbol, (0.0, 0.0))
            s_pos = max(0.0, s_pos + z - self._cusum_drift)
            s_neg = max(0.0, s_neg - z - self._cusum_drift)
            self._cusum_state[symbol] = (s_pos, s_neg)

        # ── Volume EMA 更新 ──
        if volume > 0:
            alpha_v = 2.0 / (self._vol_ema_period + 1)
            prev_v = self._vol_ema.get(symbol, volume)
            self._vol_ema[symbol] = alpha_v * volume + (1.0 - alpha_v) * prev_v

    @staticmethod
    def _calc_rs_single(high: float, low: float, open_: float, close: float) -> float:
        """
        Rogers-Satchell (1991) 波动率估计器(单根 K 线)。

        公式: RS = ln(H/C) * ln(H/O) + ln(L/C) * ln(L/O)

        特性:
          - 完全漂移不变(drift-invariant),加密趋势市场无偏
          - 恒 >= 0
          - 理论效率:close-to-close 的 6.2 倍
        """
        if high > 0 and low > 0 and open_ > 0 and high >= low:
            try:
                rs = (
                    math.log(high / close) * math.log(high / open_)
                    + math.log(low / close) * math.log(low / open_)
                )
                return max(0.0, rs)
            except (ValueError, ZeroDivisionError):
                pass
        if open_ > 0:
            try:
                return math.log(close / open_) ** 2
            except (ValueError, ZeroDivisionError):
                pass
        return 0.0

3.3 StrategyParams 新增字段(v4.0)

src/trading/config.pyStrategyParams 中新增(共 24 个新字段,全部有默认值,不破坏现有代码):

@dataclass(frozen=True)
class StrategyParams:
    # ... 现有字段(8个必填 + 3个有默认值)...

    # ── 动量过滤器参数(v4.0) ──
    momentum_filter_enabled: bool = True

    # Layer 0: Hurst DFA-2
    momentum_hurst_enabled: bool = True
    momentum_hurst_lookback: int = 60
    momentum_hurst_threshold: float = 0.60
    momentum_hurst_r2_threshold: float = 0.85    # v4.0 新增

    # Layer 1: 持续趋势(非对称 ER)
    momentum_sustained_lookback: int = 30
    momentum_sustained_base_threshold: float = 0.008
    momentum_er_threshold_long: float = 0.60
    momentum_er_threshold_short: float = 0.50

    # Layer 2: 急动检测(RS + CUSUM + VWPM + BTC因子)
    momentum_rs_period: int = 10
    momentum_cusum_drift: float = 0.5
    momentum_cusum_threshold_spike_up: float = 3.5
    momentum_cusum_threshold_spike_down: float = 2.5
    momentum_volume_confirm_ratio: float = 1.5
    momentum_volume_ema_period: int = 20
    momentum_vwpm_window: int = 5                  # v4.0 新增
    momentum_vwpm_confirm_ratio: float = 0.8       # v4.0 新增
    momentum_market_ref_symbol: str = "BTC"        # v4.0 新增
    momentum_btc_stress_threshold: float = 2.0     # v4.0 新增
    momentum_btc_stress_factor_min: float = 0.7    # v4.0 新增

    # Layer 3: Spread 仲裁(ADF + KPSS + ER)
    momentum_spread_lookback: int = 20
    momentum_spread_er_threshold: float = 0.45
    momentum_spread_net_threshold: float = 1.5
    momentum_spread_adf_pvalue: float = 0.05       # v4.0 修改(原 0.10)
    momentum_spread_kpss_pvalue: float = 0.05      # v4.0 新增

3.4 strategy.py 集成改动(v4.0)

SymbolBaseline 新增 z4h_history 字段(用于 Layer 3 Spread 仲裁,继承 v3.0):

@dataclass
class SymbolBaseline:
    std_window: deque = field(default_factory=deque)
    welford_mean: float = 0.0
    welford_m2: float = 0.0
    welford_updates: int = 0
    ema: float | None = None
    last_kline_time: datetime | None = None
    last_std: float = 0.0
    z4h_history: deque = field(default_factory=deque)

_get_baseline() 中同步初始化:

self._baselines[key] = SymbolBaseline(
    std_window=deque(maxlen=params.std_window),
    z4h_history=deque(maxlen=50),
)

process_tick 新增 OHLCV 参数(继承 v3.0):

def process_tick(
    self,
    symbol: str,
    base_symbol: str,
    z4h: float,
    timestamp: datetime,
    kline_time: datetime | None = None,
    latest_price: float | None = None,
    alt_ohlcv: dict | None = None,
    base_ohlcv: dict | None = None,
) -> tuple[EntrySignal | None, ExitSignal | None]:

_process_tick_unlocked 新 K 线时同步更新过滤器和 z4h 历史

if is_new_candle:
    # 原有 Welford + EMA 更新...

    # 追加 z4h 历史(供 Layer 3 Spread 仲裁)
    bl.z4h_history.append(z4h)

    # 更新两腿动量过滤器
    if self._momentum_filter:
        if alt_ohlcv:
            self._momentum_filter.update(
                symbol=symbol,
                close=alt_ohlcv.get('close', 0.0),
                high=alt_ohlcv.get('high', 0.0),
                low=alt_ohlcv.get('low', 0.0),
                open_=alt_ohlcv.get('open', 0.0),
                volume=alt_ohlcv.get('volume', 0.0),
                kline_time=kline_time,
            )
        if base_ohlcv and base_symbol:
            self._momentum_filter.update(
                symbol=base_symbol,
                close=base_ohlcv.get('close', 0.0),
                high=base_ohlcv.get('high', 0.0),
                low=base_ohlcv.get('low', 0.0),
                open_=base_ohlcv.get('open', 0.0),
                volume=base_ohlcv.get('volume', 0.0),
                kline_time=kline_time,
            )

_check_entry 四层动量过滤(步骤 4.5–4.7,与 v3.0 逻辑一致)

def _check_entry(self, key, z4h, adaptive_z, timestamp, current_above, params):
    # ... 步骤 1-4(冷却期、突破检测、持仓检查、z4h 过滤)...

    # 步骤 4.5:方向判断(提前,供动量过滤器使用)
    threshold = params.adaptive_threshold
    if adaptive_z < -threshold:
        direction = 'long'
    elif adaptive_z > threshold:
        direction = 'short'
    else:
        return None

    # 步骤 4.6:四层动量过滤
    if self._momentum_filter is not None:
        soft_reasons: list[str] = []
        hard_blocked = False
        hard_reason_parts: list[str] = []

        # Alt 腿(主方向)
        if self._momentum_filter.ready(key[0]):
            allowed, reason, is_soft = self._momentum_filter.check(key[0], direction)
            if not allowed:
                if is_soft:
                    soft_reasons.append(f"alt:{reason}")
                else:
                    hard_blocked = True
                    hard_reason_parts.append(f"alt:{reason}")

        # Base 腿(反向)
        if key[1] and self._momentum_filter.ready(key[1]):
            opposite = 'short' if direction == 'long' else 'long'
            allowed, reason, is_soft = self._momentum_filter.check(key[1], opposite)
            if not allowed:
                if is_soft:
                    soft_reasons.append(f"base:{reason}")
                else:
                    hard_blocked = True
                    hard_reason_parts.append(f"base:{reason}")

        # 硬拦截(Layer 0 / Layer 2)→ 直接拒绝,不仲裁
        if hard_blocked:
            logger.info(f"动量过滤(硬) | {pair_label} | {' | '.join(hard_reason_parts)}")
            return None

        # 软拦截(Layer 1)→ 进入 Layer 3 仲裁
        if soft_reasons:
            combined = " | ".join(soft_reasons)
            bl = self._baselines.get(key)
            z4h_hist = list(bl.z4h_history) if (bl and bl.z4h_history) else []

            spread_trend, spread_reason = self._momentum_filter.check_spread(z4h_hist, direction)
            if spread_trend:
                logger.info(
                    f"动量过滤(软+Spread) | {pair_label} | {combined} | {spread_reason}"
                )
                return None
            else:
                if spread_reason:
                    logger.info(
                        f"动量仲裁放行 | {pair_label} | 单腿:{combined} | {spread_reason}"
                    )
                else:
                    logger.info(
                        f"动量仲裁放行 | {pair_label} | 单腿:{combined} | Spread无趋势→推翻拦截"
                    )

    # 步骤 5:产生 EntrySignal(direction 已由步骤 4.5 确定)
    ...

3.5 改动范围(v4.0)

改动文件 改动内容 改动量
src/trading/momentum_filter.py 新建,四层过滤器(DFA-2+R² / RS+CUSUM+VWPM+BTC / ER / ADF+KPSS+ER) ~380 行
src/trading/strategy.py SymbolBaseline 新增 z4h_historyprocess_tick 新增 OHLCV 参数,_check_entry 四层检查 ~60 行
src/trading/config.py StrategyParams 新增 24 个字段(全部有默认值) ~30 行
src/trading/orchestrator.py process_analysis 新增 alt_ohlcv/base_ohlcv 参数并透传 ~5 行
src/services/realtime_kline_service_base.py _trigger_strategy_if_ready 提取双腿 OHLCV + volume 并传入 ~20 行

4. 融入当前交易体系的方案

4.1 数据流全景(v4.0)

WebSocket K线推送(OHLCV + volume)
      |
      v
realtime_kline_service_base
      |
      +-- _parse_kline() → kline dict (含 close/high/low/open/volume)
      |
      +-- _trigger_strategy_if_ready()
              |
              +-- 从 price_data_cache 提取最新 alt/base OHLCV + volume
              |     alt_ohlcv = {'close','high','low','open','volume'}  ← alt_klines_5m[0]
              |     base_ohlcv = {'close','high','low','open','volume'} ← base_klines_5m[0]
              |
              v
      TradingOrchestrator.process_analysis(
          ..., alt_ohlcv=alt_ohlcv, base_ohlcv=base_ohlcv
      )
              |
              v
      AdaptiveBollingerStrategy.process_tick(
          ..., alt_ohlcv=alt_ohlcv, base_ohlcv=base_ohlcv
      )
              |
              +-- [新K线] bl.z4h_history.append(z4h)          ← Layer 3 历史维护
              |
              +-- [新K线] MomentumFilter.update(alt, alt_ohlcv, kline_time)
              |           → O(1) 在线更新: RS EMA / CUSUM / Volume EMA / Per-symbol基准中位数
              |           MomentumFilter.update(base, base_ohlcv, kline_time)
              |           (kline_time 去重:同 symbol 多配对只追加一次)
              |
              +-- _check_exit() → ExitSignal
              |
              +-- _check_entry()
                      |
                      步骤1: 冷却期
                      步骤2: 突破检测(adaptive_z 首次穿越阈值)
                      步骤3: 持仓检查
                      步骤4: z4h 绝对值过滤
                 [v4.0]
                      步骤4.5: 方向判断(提前,供过滤器使用)
                      步骤4.6: 四层动量过滤
                                ├── Layer 0(硬拦截): Hurst DFA-2 + R² 质检
                                │    ├── alt 腿: check → is_soft=False
                                │    └── base 腿: check → is_soft=False
                                │    任一触发 → 直接拒绝
                                │
                                ├── Layer 2(硬拦截): CUSUM + RS + VWPM + BTC因子
                                │    ├── BTC CUSUM 高压 → 降低 alt 阈值
                                │    ├── alt 腿: check → is_soft=False
                                │    └── base 腿: check → is_soft=False
                                │    任一触发 → 直接拒绝
                                │
                                ├── Layer 1(软拦截): ER + RS 净位移(非对称阈值)
                                │    ├── alt 腿: check → is_soft=True
                                │    └── base 腿: check → is_soft=True
                                │    任一触发 → 进入 Layer 3 仲裁
                                │    (多腿 reason 合并,不再覆盖)
                                │
                                └── Layer 3(仲裁): Spread ADF + KPSS + ER 三重
                                     ER < threshold → 直接放行(O(N) 快速路径)
                                     ADF+KPSS 确认平稳 → 放行
                                     ADF+KPSS 确认非平稳 → 维持拦截
                                     矛盾/不确定 → 保守维持拦截
                      步骤5: 产生 EntrySignal

4.2 realtime_kline_service_base.py 改动

def _trigger_strategy_if_ready(self, ...):
    # 提取 alt/base 最新 OHLCV(5m K线,供动量过滤器使用)
    alt_ohlcv = None
    base_ohlcv = None
    period_key_5m = ('5m', '7d')
    if period_key_5m in price_data_cache:
        alt_klines_5m = price_data_cache[period_key_5m]['alt_klines']
        if alt_klines_5m:
            k = alt_klines_5m[0]
            latest_alt_price = k.get('close')
            alt_ohlcv = {
                'close': k.get('close', 0.0),
                'high': k.get('high', 0.0),
                'low': k.get('low', 0.0),
                'open': k.get('open', 0.0),
                'volume': k.get('volume', 0.0),
            }
        base_klines_5m = price_data_cache[period_key_5m].get('base_klines', [])
        if base_klines_5m:
            k = base_klines_5m[0]
            base_ohlcv = {
                'close': k.get('close', 0.0),
                'high': k.get('high', 0.0),
                'low': k.get('low', 0.0),
                'open': k.get('open', 0.0),
                'volume': k.get('volume', 0.0),
            }

    acted = self._trading_orchestrator.process_analysis(
        ...,
        alt_ohlcv=alt_ohlcv,
        base_ohlcv=base_ohlcv,
    )

4.3 线程安全

MomentumFilter 不持有锁,由 AdaptiveBollingerStrategy_lock(已存在)统一保护。update()check() 均在 _lock 持有范围内调用,无竞态风险。kline_time 去重在逻辑层面保证同一根 K 线不被重复追加。

4.4 日志格式(v4.0)

动量过滤(硬)   | BTC|ETH | alt:Layer0-趋势机制: Hurst(DFA-2)=0.67>0.60 R²=0.92 → 均值回归假设失效
动量过滤(硬)   | SOL|BTC | alt:Layer2-暴涨不做空: CUSUM+=3.82>=3.15 RS_vol=0.0028 量比=2.3 VWPM=0.0012 BTC压力→阈值×0.90 (隐式冷却≈31min)
动量过滤(硬)   | ETH|BTC | base:Layer2-暴跌不做多: CUSUM-=2.61>=2.50 RS_vol=0.0031 量比=1.7 VWPM=-0.0009 | alt:Layer1-不追涨: ...
动量过滤(软+Spread) | SOL|BTC | alt:Layer1-不追跌: 过去30根净跌幅=-1.23% 阈值=-0.65% ER=0.72>=0.50 | Spread趋势(三重确认): ER=0.61 净位移=-2.1 ADF_p=0.18>=0.05 KPSS_p=0.02<=0.05
动量仲裁放行   | ETH|BTC | 单腿:alt:Layer1-不追涨: 过去30根净涨幅=1.05% 阈值=+0.80% ER=0.68>=0.60 | ADF+KPSS确认平稳(ADF_p=0.03<0.05 KPSS_p=0.12>0.05)→推翻拦截
动量仲裁放行   | ETH|BTC | 单腿:alt:Layer1-不追涨: ... | Spread无趋势→推翻拦截

4.5 配置层集成(v4.0 新增参数)

# Layer 0: Hurst DFA-2
TRADING_MOMENTUM_HURST_ENABLED=true
TRADING_MOMENTUM_HURST_LOOKBACK=60
TRADING_MOMENTUM_HURST_THRESHOLD=0.60
TRADING_MOMENTUM_HURST_R2_THRESHOLD=0.85         # v4.0 新增

# Layer 1: 持续趋势(ER 非对称)
TRADING_MOMENTUM_SUSTAINED_LOOKBACK=30
TRADING_MOMENTUM_SUSTAINED_BASE_THRESHOLD=0.008
TRADING_MOMENTUM_ER_THRESHOLD_LONG=0.60
TRADING_MOMENTUM_ER_THRESHOLD_SHORT=0.50

# Layer 2: 急动检测(RS + CUSUM + VWPM + BTC因子)
TRADING_MOMENTUM_RS_PERIOD=10
TRADING_MOMENTUM_CUSUM_DRIFT=0.5
TRADING_MOMENTUM_CUSUM_THRESHOLD_SPIKE_UP=3.5
TRADING_MOMENTUM_CUSUM_THRESHOLD_SPIKE_DOWN=2.5
TRADING_MOMENTUM_VOLUME_CONFIRM_RATIO=1.5
TRADING_MOMENTUM_VWPM_WINDOW=5                    # v4.0 新增
TRADING_MOMENTUM_VWPM_CONFIRM_RATIO=0.8           # v4.0 新增
TRADING_MOMENTUM_MARKET_REF_SYMBOL=BTC             # v4.0 新增
TRADING_MOMENTUM_BTC_STRESS_THRESHOLD=2.0          # v4.0 新增
TRADING_MOMENTUM_BTC_STRESS_FACTOR_MIN=0.7         # v4.0 新增

# Layer 3: Spread 仲裁(ADF + KPSS + ER)
TRADING_MOMENTUM_SPREAD_LOOKBACK=20
TRADING_MOMENTUM_SPREAD_ER_THRESHOLD=0.45
TRADING_MOMENTUM_SPREAD_NET_THRESHOLD=1.5
TRADING_MOMENTUM_SPREAD_ADF_PVALUE=0.05            # v4.0 修改
TRADING_MOMENTUM_SPREAD_KPSS_PVALUE=0.05           # v4.0 新增

# 针对高波动 symbol 示例
TRADING_STRATEGY_SOL_MOMENTUM_CUSUM_THRESHOLD_SPIKE_UP=4.0
TRADING_STRATEGY_BTC_MOMENTUM_HURST_THRESHOLD=0.65

5. 与当前交易风格的契合度分析

5.1 交易风格特征

维度 当前系统特征
策略类型 配对协整均值回归
K 线周期 5 分钟
最大持仓时间 72 小时(max_hold_hours=72.0
入场逻辑 adaptive_z 突破阈值(非连续信号,要求首次穿越)
出场逻辑 adaptive_z 回归至 entry_adaptive_z * reversion_factor
EMA/STD 参数 ema_span=36, std_window=72

5.2 契合度分析(v4.0)

高度契合之处:

  1. Layer 0 DFA-2 + R² 是最可靠的机制检测方案:DFA-2 消除了 DFA-1 在加速趋势市场的高估偏差;R² 质检确保硬拦截不会建立在不可靠的估计之上。这是 v3.0 中最关键的遗漏——硬拦截层必须有质量门控。

  2. KPSS + ADF 对偶检验是计量经济学标准实践:单独使用 ADF 时,"不拒绝 H0"可能是真的非平稳,也可能是样本不足导致检验无力。加入 KPSS 后能区分这两种情况,Layer 3 仲裁的统计可靠性大幅提升。

  3. BTC 跨资产因子反映加密市场结构:BTC dominance 使得 BTC 急动后 alt 跟随的概率极高。在 BTC CUSUM 高压时降低 alt 的检测阈值,是对市场微观结构的直接建模。

  4. VWPM 量价确认比简单量比更有区分力:Kyle (1985) 模型表明价格+成交量的联合信号优于单独信号。VWPM 检查"量大且方向一致"而非仅"量大",能过滤掉假放量(对倒、做市)。

  5. Rogers-Satchell + Per-symbol 基准 + 非对称参数(继承 v3.0):24/7 连续交易无隔夜跳空,RS 无漂移假设,per-symbol 基准消除跨品种干扰,非对称参数反映加密上下方动量差异。

设计张力(有意为之的权衡):

  • Layer 0 用单腿 Hurst 做硬拦截,但单腿趋势不等于 spread 趋势。v4.0 中 R² 质检缓解了误拦截风险,但 Layer 0 仍是硬拦截不经 Layer 3 仲裁——这是保守但正确的:Hurst 反映长期记忆特性,即使 spread 暂时稳定,单腿趋势持续性意味着更高的发散风险。
  • BTC 因子在 BTC 本身作为 base 腿时存在双重计算(base 腿检查 + BTC 因子),但因检查维度不同(反向动量 vs 市场压力),影响可控。

5.3 综合评分(v4.0 vs v3.0)

维度 评分 vs v3.0 说明
算法先进性 ★★★★★ +0.5★ DFA-2 + R² 质检 + KPSS 对偶 = state-of-the-art
误杀控制 ★★★★★ +0.5★ R² 防止 DFA 误杀,KPSS 增强仲裁可靠性
自适应能力 ★★★★★ 持平 Per-symbol 基准继承 v3.0
非对称性 ★★★★★ 持平 继承 v3.0
市场微观结构 ★★★★★ +1★ BTC 跨资产因子 + VWPM 量价确认
策略哲学一致性 ★★★★★ 持平 Hurst 检测直接验证均值回归假设
时间尺度匹配 ★★★★★ 持平 CUSUM 无固定窗口,ER/DFA 窗口可调
代码侵入性 ★★★★★ 持平 独立模块,可独立关闭
运行效率 ★★★★☆ 持平 DFA-2 略慢于 DFA-1,KPSS 与 ADF 开销相当
统计严谨性 ★★★★★ +1★ R² 质检 + ADF/KPSS 对偶 = 严格统计保证

6. 与四项开单约束的对应关系

6.1 逐项验证

约束 1:连续下跌不追跌

Layer 1 检测:ER >= er_threshold_short(0.50)AND net_return <= -adaptive_threshold
被阻止:direction == 'short'
原因:路径高效地持续下跌(ER 高),跌幅充分释放,继续做空空间有限且反弹风险高
仲裁:Layer 3 检查 spread 趋势性(ADF + KPSS + ER 三重)→ 确认平稳则推翻拦截放行

示例(5min,N=30,base_threshold=0.8%,RS 自适应后 threshold=0.65%):

  • 过去 150 分钟稳步从 100 跌到 99(ER=0.72 >= 0.50, 净跌 1%)→ Layer 1 拦截
    • Spread ADF p=0.22 + KPSS p=0.03 → 双重确认非平稳 → 维持拦截
    • Spread ADF p=0.03 + KPSS p=0.12 → 双重确认平稳 → 推翻拦截,放行
    • Spread ADF p=0.04 + KPSS p=0.03 → 矛盾 → 保守维持拦截
  • 过去 150 分钟 V 形反转(100→96→99.2,ER=0.10 < 0.50)→ ER 不足,放行

约束 2:连续上涨不追涨

Layer 1 检测:ER >= er_threshold_long(0.60,更严格)AND net_return >= +adaptive_threshold
被阻止:direction == 'long'

示例:

  • 过去 150 分钟稳步从 100 涨到 101.2(ER=0.85 >= 0.60, 净涨 1.2%)→ 拦截 → 仲裁
  • 过去 150 分钟震荡漂移(ER=0.15 < 0.60, 净涨 0.9%)→ ER 不足,放行

约束 3:迅速暴涨不做空

Layer 2 检测:CUSUM_pos >= effective_cusum_thresh_up AND VWPM+量比联合确认
  effective_cusum_thresh_up = 3.5 × stress_factor(BTC 因子调整)
被阻止:direction == 'short'
不可仲裁(硬拦截)

示例(RS_vol=0.3%,drift=0.5,BTC CUSUM=2.5 → stress_factor=0.95):

  • 连续 3 根各涨 0.8%(z=2.67)→ CUSUM=6.5 > 3.33 → 拦截
  • BTC 同时急涨(btc_stress=3.0)→ 阈值降至 3.5×0.9=3.15 → 更早拦截
  • 单根涨 0.3% 但量极小(vol_ratio=0.3, VWPM≈0)→ CUSUM 低 + 量不足 → 放行

约束 4:迅速暴跌不做多

Layer 2 检测:CUSUM_neg >= effective_cusum_thresh_down AND VWPM+量比联合确认
被阻止:direction == 'long'
不可仲裁(硬拦截)

6.2 约束覆盖矩阵(v4.0)

约束 过滤层 触发指标 被阻止方向 检查维度 可仲裁 正确性
0. 趋势机制 Layer 0 Hurst(DFA-2)≥T AND R²≥0.85 双向 alt+base
1. 连续下跌不追跌 Layer 1 ER≥T_short AND net_ret≤-T_adp short alt+base+spread
2. 连续上涨不追涨 Layer 1 ER≥T_long AND net_ret≥+T_adp long alt+base+spread
3. 迅速暴涨不做空 Layer 2 CUSUM+≥T_up×SF AND VWPM+量确认 short alt+base+BTC
4. 迅速暴跌不做多 Layer 2 CUSUM-≥T_down×SF AND VWPM+量确认 long alt+base+BTC

四项约束完全覆盖。v4.0 增强:Layer 0 R² 质检防止误拦截,Layer 2 BTC 因子扩展检测维度至跨资产,Layer 3 KPSS 对偶检验增强仲裁可靠性。


7. 参数配置指南

7.1 基础参数推荐值

Layer 0:Hurst DFA-2(v4.0 升级)

参数 默认值 范围 说明
hurst_lookback 60 30-100 用于 DFA-2 的回望根数(60根5min=5小时)
hurst_threshold 0.60 0.55-0.70 H > 此值判定为趋势机制
hurst_r2_threshold 0.85 0.75-0.95 R² < 此值视为估计不可靠 → 放行
hurst_threshold 调优:
  0.55:过于激进,均值回归行情下也可能触发(误杀多)
  0.60:标准值,H=0.6 是弱趋势/随机游走的分界线
  0.65+:宽松,只拦截强趋势(适合波动大、趋势明显的主流币)

hurst_lookback 调优:
  30:短期记忆检测(约 2.5 小时),对快速趋势切换敏感
  60:标准值(约 5 小时),兼顾稳定性和响应速度
  90+:中长期机制检测,更稳定但滞后

hurst_r2_threshold 调优(v4.0 新增):
  0.75:宽松,更多 Hurst 估计被采纳(假阳性风险增加)
  0.85:标准值,平衡可靠性和可用性
  0.95:严格,只有非常可靠的估计才被使用(可能导致 Layer 0 频繁失效)
  建议:不低于 0.80,否则 DFA-2 的 R² 质检失去意义

Layer 1:持续趋势过滤

参数 默认值 范围 说明
sustained_lookback 30 10-60 净位移回望根数
sustained_base_threshold 0.8% 0.3%-2.0% 净位移基准阈值(RS 自适应缩放)
er_threshold_long 0.60 0.45-0.75 做多拦截 ER 阈值(更严格)
er_threshold_short 0.50 0.35-0.65 做空拦截 ER 阈值(较宽松)
er_threshold 非对称调优原则:
  er_threshold_long 应高于 er_threshold_short(上涨更难均值回归)
  推荐差值:0.08-0.12

  激进配置(拦截更多):long=0.55, short=0.45
  标准配置:             long=0.60, short=0.50
  保守配置(放行更多):  long=0.65, short=0.55

v4.0 新增:数据驱动非对称参数推导方法

非对称参数应从历史数据中推导,而非仅凭经验。推荐的回测推导流程:

对每个方向 d ∈ {long, short},对每个候选阈值 T ∈ [0.30, 0.75]:
  1. 在历史数据中筛选 ER >= T 且 direction == d 的所有入场信号
  2. 计算这些信号的均值回归成功率:
     P(success | ER >= T, direction=d)
  3. 找到使成功率低于可接受水平(如 30%)的最小 T
     → 该 T 即为 er_threshold_{d} 的数据驱动推荐值

同理可推导 CUSUM 非对称阈值:
  对每个方向,计算 P(成功均值回归 | CUSUM >= T, direction)
  找到使成功率 < 30% 的最小 T

预期结果:
  如果加密市场确实存在上下非对称性,数据驱动的 T_long 应 > T_short
  这会给当前的经验值提供定量验证或修正

Layer 2:急动检测

参数 默认值 范围 说明
rs_period 10 5-20 Rogers-Satchell EMA 周期
cusum_drift 0.5 0.3-1.0 CUSUM 漂移,越大越不灵敏
cusum_threshold_spike_up 3.5 2.5-5.0 暴涨检测阈值,越大越宽松
cusum_threshold_spike_down 2.5 2.0-4.0 暴跌检测阈值,越大越宽松
volume_confirm_ratio 1.5 1.0-3.0 量比确认倍数
vwpm_window 5 3-10 VWPM 回望窗口(v4.0 新增)
vwpm_confirm_ratio 0.8 0.5-1.5 VWPM/RS_vol 确认阈值(v4.0 新增)
market_ref_symbol "BTC" 跨资产参考 symbol(v4.0 新增)
btc_stress_threshold 2.0 1.5-3.0 BTC CUSUM 压力启动阈值(v4.0 新增)
btc_stress_factor_min 0.7 0.5-0.9 BTC 压力下 CUSUM 阈值最低缩放比(v4.0 新增)
非对称 CUSUM 阈值调优:
  spike_up > spike_down(暴涨做空风险 > 暴跌做多风险)
  推荐差值:0.5-1.5

  CUSUM 隐式冷却期:threshold/drift × 5min
    spike_up   3.5/0.5 × 5 = 35min
    spike_down 2.5/0.5 × 5 = 25min

  ARL_0(平均误报间隔)≈ exp(2*drift*threshold) / (2*drift²)
    drift=0.5, threshold=3.5 → ARL_0 ≈ 73 根(约 6 小时才会一次误报)
    drift=0.5, threshold=2.5 → ARL_0 ≈ 20 根(约 1.7 小时才会一次误报)

VWPM 调优(v4.0 新增):
  vwpm_window=3:短期量价方向检查,对单根异常量敏感
  vwpm_window=5:标准值,覆盖 25 分钟,平衡稳定性和响应速度
  vwpm_window=10:更稳定但滞后,适合低频策略

  vwpm_confirm_ratio=0.5:宽松,轻微量价一致即确认
  vwpm_confirm_ratio=0.8:标准值,需要明显的量价一致性
  vwpm_confirm_ratio=1.5:严格,只有非常强的量价一致才确认

BTC 因子调优(v4.0 新增):
  btc_stress_threshold=1.5:敏感,BTC 轻微急动即影响 alt 阈值
  btc_stress_threshold=2.0:标准值,BTC 明显急动时才介入
  btc_stress_threshold=3.0:宽松,仅在 BTC 极端急动时才介入

  btc_stress_factor_min=0.5:激进,BTC 极端时 alt 阈值降至 50%
  btc_stress_factor_min=0.7:标准值,alt 阈值最多降至 70%
  btc_stress_factor_min=0.9:保守,BTC 因子影响有限

Layer 3:Spread 仲裁

参数 默认值 范围 说明
spread_lookback 20 10-40 Spread ER / ADF / KPSS 回望根数
spread_er_threshold 0.45 0.30-0.65 Spread ER 阈值(ER 快速筛)
spread_net_threshold 1.5 0.5-3.0 Spread 净位移阈值(z4h 单位)
spread_adf_pvalue 0.05 0.01-0.10 ADF 显著性水平(v4.0 修改)
spread_kpss_pvalue 0.05 0.01-0.10 KPSS 显著性水平(v4.0 新增)
ADF + KPSS 联合参数调优(v4.0):
  adf_pvalue 和 kpss_pvalue 建议设为相同值以保持对称性

  两者均为 0.05:标准统计学显著性水平
    → ADF 需 p<0.05 才判定平稳,KPSS 需 p>0.05 才判定平稳
    → 四象限决策矩阵清晰

  两者均为 0.10:更宽松,更多情况被判定为"确认"
    → 四象限中"矛盾/不确定"区间缩小
    → 适合激进风格

  注意:ADF 和 KPSS 对小样本(<20)均有尺寸扭曲问题。
  spread_lookback >= 20 时结果可靠。
  样本不足时自动降级为纯 ER 判断。

7.2 回测验证建议

对比五组回测:
  A 组:无动量过滤(当前基线)
  B 组:v2.0(GK + 单一 ER 阈值 + ER-only Spread)
  C 组:v3.0(RS + DFA-1 + 非对称阈值 + ADF+ER Spread)
  D 组:v4.0(RS + DFA-2+R² + VWPM + BTC因子 + ADF+KPSS+ER)
  E 组:v4.0 关闭各子功能(评估各改进项贡献)
    E1: 关闭 R² 质检
    E2: 关闭 VWPM(降级为 volume_ratio only)
    E3: 关闭 BTC 因子
    E4: 关闭 KPSS(降级为 ADF only)

核心对比指标:
  - Sharpe Ratio(风险调整收益)
  - 最大连续亏损次数
  - 信号过滤率(被拦截信号 / 总信号)
  - 误杀率(被拦截后实际盈利的信号占比)
  - Layer 3 仲裁放行率(推翻 Layer 1 的比例)
  - 分时段表现差异(亚盘/欧盘/美盘)
  - Layer 0 触发频率(Hurst 拦截数 / 总信号数)
  - Layer 0 R² 失效率(R² < 0.85 导致 Hurst 被跳过的比例)

预期结果(v4.0 相对 v3.0 的增量收益):
  R² 质检贡献:减少 Layer 0 误拦截约 15-25%(短序列 Hurst 不可靠时放行)
  DFA-2 贡献:减少加速趋势场景的 Hurst 高估约 10-20%
  VWPM 贡献:减少假放量导致的 Layer 2 误拦截约 10-15%
  BTC 因子贡献:在 BTC 领先 alt 联动时提前 5-15min 拦截,降低最大回撤 5-10%
  KPSS 对偶贡献:Layer 3 仲裁准确率提升约 20-30%(消除 ADF 低功效问题)

8. 风险与局限性

8.1 已知局限

局限 说明 缓解措施
DFA-2 数据预热期 需要约 60 根 K 线(5 小时)才有可靠的 Hurst 估计 启动时从 DB 回填近 80 根 K 线;预热期间 Layer 0 默认放行
DFA-2 计算开销 二阶多项式去趋势(Cramer 求解 3×3 系统)比线性去趋势慢约 2-3x 仅在 check() 调用时执行(非每根 K 线),5min 级别无性能压力
R² 质检可能过于严格 60 根 K 线只有 5-7 个尺度点,R² 波动较大 阈值 0.85 经验证平衡了可靠性和可用性;可降至 0.80
RS 数据预热期 RS EMA 需 10 根,CUSUM 需稳定 RS 后才有意义(约 15 根≈75 分钟) 启动时回填近 50 根 K 线
KPSS 小样本问题 spread_lookback < 20 时 KPSS 检验有尺寸扭曲 spread_lookback >= 20(默认值),小样本时跳过 KPSS
Per-symbol 基准冷启动 各 symbol 的 _RollingMedian 需 5+ 样本才稳定 前 5 个样本使用 base_threshold 不缩放
Hurst 对短序列敏感 60 根 5min K 线的 Hurst 标准误较大 R² 质检缓解;hurst_lookback 可调高至 90-100
Volume 数据质量 HIP-3 资产或低流动性币 volume 可能不可靠 volume 为 0 时跳过 VWPM + 量确认,仅依赖 CUSUM
BTC 因子时滞 BTC CUSUM 更新与 alt 信号检查之间有最多 1 根 K 线(5min)延迟 5min 级别延迟可接受;BTC 急动通常持续多根 K 线
ADF/KPSS statsmodels 依赖 若 statsmodels 未安装,Layer 3 降级为纯 ER 判断 项目已安装 statsmodels(v0.14.6),无需额外安装
VWPM 对低量币不可靠 低流动性币的成交量可能不反映真实交易方向 仅在 vol_ema > 0 且有效 VWPM 数据时启用

8.2 不适合的场景

  • 极低流动性资产:RS 波动率因价格跳空失真,Hurst 因稀疏 K 线估计不准,VWPM 因量不可靠失效,需配合黑名单
  • HIP-3 稀疏资产:volume 不可靠时 Layer 2 VWPM+量价确认无法生效,仅靠 CUSUM 价格检测

8.3 后续迭代方向(v4.0 之后)

优先级 方向 说明
P1 历史预填充 启动时从 DB 的 klines 表回填近 80 根 K 线,消除 Hurst/RS 预热等待
P2 CUSUM 自适应 drift drift 随 RS 波动率动态调整,高波动期提高容忍度
P2 Hurst 滚动缓存 Hurst 每 N 根新 K 线重算一次(而非每 tick),降低 DFA-2 计算开销
P2 非对称参数回测推导 用 7.1 节描述的数据驱动方法从历史数据中推导最优 ER/CUSUM 非对称参数
P3 Shiryaev-Roberts 替代 CUSUM SR 程序在某些理论意义下优于 CUSUM(Pollak & Tartakovsky 2008),可作为 CUSUM 的对照验证
P3 多时间框架确认 同时检查 1h K 线的趋势性,避免 5min 级别噪音触发 Layer 0/1
P3 VPIN 替代 VWPM 若获得 tick 级数据,用 Easley, Lopez de Prado & O'Hara (2012) 的 VPIN 替代 K 线级 VWPM
P4 分时段非对称参数 亚盘/欧盘/美盘使用不同 CUSUM drift(美盘适当放宽,亚盘收紧)
P4 Bayesian Online Changepoint Detection Adams & MacKay (2007) 的贝叶斯在线变点检测作为 Layer 0 的补充/替代

附录 A:算法学术参考

算法 原始论文 核心贡献
DFA(Hurst 估计) Peng et al. (1994) Physical Review Letters 去趋势波动分析,对非平稳序列鲁棒的 Hurst 估计
DFA-2(二阶去趋势) Kantelhardt et al. (2002) Physica A 二阶多项式去趋势消除抛物线型趋势偏差
DFA in Finance Mantegna & Stanley (1995) Nature DFA 引入金融时间序列
Hurst 原始提出 Hurst, H.E. (1951) Trans. ASCE 长程记忆与持续性概念奠基
Kaufman Efficiency Ratio Kaufman, P. (1995) Smarter Trading 方向距离/路径长度衡量趋势效率
CUSUM Page, E.S. (1954) Biometrika 序列变化检测的最优在线算法
CUSUM 最优性证明 Lorden, G. (1971) Ann. Math. Statist. CUSUM 在平均检测延迟最小意义下一阶渐近最优
Garman-Klass 波动率 Garman, M. & Klass, M. (1980) J. Business OHLC 波动率估计器,效率 7.4 倍(零漂移假设)
Rogers-Satchell 波动率 Rogers, L. & Satchell, S. (1991) Ann. Appl. Prob. 漂移不变 OHLC 波动率,效率 6.2 倍
Yang-Zhang 波动率 Yang, D. & Zhang, Q. (2000) J. Business 结合 RS 与隔夜跳空(加密市场可直接用 RS)
知情交易模型 Kyle, A.S. (1985) Econometrica 价格+成交量联合信号区分知情/噪声交易
ADF 检验 Dickey, D.A. & Fuller, W.A. (1979) JASA 单位根检验,序列平稳性的标准统计工具
KPSS 检验 Kwiatkowski et al. (1992) J. Econometrics 平稳性检验(H0=平稳),ADF 的对偶检验
时间序列动量 Moskowitz, Ooi & Pedersen (2012) J. Financial Economics 资产间动量溢出效应的系统性证据
VPIN Easley, Lopez de Prado & O'Hara (2012) Rev. Financial Studies 知情交易概率的成交量同步估计
Bayesian Changepoint Adams & MacKay (2007) arXiv 贝叶斯在线变点检测(未来 Layer 0 候选)
Shiryaev-Roberts Pollak & Tartakovsky (2008) Ann. Statist. CUSUM 的替代方案,某些条件下更优
配对交易理论 Vidyamurthy, G. (2004) Pairs Trading Spread 均值回归的系统性方法论
配对交易实证 Gatev, E. et al. (2006) Rev. Financial Studies 配对交易策略的大规模实证验证

附录 B:v3.0 → v4.0 改进对照

维度 v3.0 v4.0 改进原因
DFA 阶数 DFA-1(线性去趋势) DFA-2(二阶多项式去趋势) 消除加速趋势偏差(Kantelhardt 2002)
Hurst 质检 R² ≥ 0.85 质检门控 防止短序列不可靠估计触发硬拦截
量价确认 volume_ratio ≥ 1.5(简单量比) VWPM + 量比联合确认 区分"量大"和"量大+方向一致"(Kyle 1985)
跨资产因子 BTC CUSUM 压力 → alt 阈值缩放 反映 BTC dominance 的跨资产动量溢出
Spread 仲裁 ADF + ER 双重 ADF + KPSS + ER 三重 KPSS 对偶消除 ADF 低功效问题(Kwiatkowski 1992)
ADF 显著性水平 0.10(仅 ADF 宽松) 0.05(ADF+KPSS 对偶标准值) 双检验互补,可用更严格标准
参数推导 经验值 新增数据驱动推导方法 从历史数据的条件概率推导最优非对称参数
check_spread 注释 "仅在 Layer 0/1 软拦截时调用"(Layer 0 是硬拦截,注释有误) "仅在 Layer 1 软拦截时调用" 修正文档错误
架构图执行顺序 Layer 0→1→2→3(与代码不一致) Layer 0→2→1→3(硬拦截优先) 与代码实际执行顺序一致
新增参数数量 7 个新参数 hurst_r2, vwpm_window, vwpm_confirm, market_ref, btc_stress_thresh, btc_stress_min, kpss_pvalue
_RollingMedian 文档 "O(N log N) 可接受" 增加双堆优化路径说明 未来可扩展性

文档结束

Read more

跑步的技巧(滚动落地)

“滚动落地(rolling contact / rolling foot strike)”不是一种教条式的“脚法”,而是一种 让冲击沿着整只脚、整条后链逐级传递的落地机制。 它的核心不是“你先用哪儿着地”,而是: 你的脚落地之后,冲击是不是像轮子一样滚过去,而不是像锤子一样砸下去。 这就是滚动落地的本质。 一、什么叫“滚动落地”? 你可以把它理解成两种完全不同的落地方式: 1. 砸地(撞击式) 脚像锤子一样拍到地上: * 要么后跟先砸 * 要么前掌先戳 * 冲击集中在一个点 * 一个结构瞬间吃掉大部分载荷 结果就是: * 后跟砸 → 膝盖难受 * 前掌戳 → 前脚掌磨烂 * 都不是长跑友好模式 这叫 撞击式着地(impact strike)。 2. 滚地(滚动式) 脚像轮胎一样“滚”过地面: * 不是某一点硬砸 * 而是外侧中足先轻触 * 再向前滚到前掌 * 最后从大脚趾蹬离

By SHI XIAOLONG

AMI的优越性

世界模型(World Models)的具体例子 如下,我按类型分类,便于理解。每类都附带实际实现、演示效果和应用场景。 1. Yann LeCun / Meta 的 JEPA 系列(最直接对应“世界模型”概念) 这些是 LeCun 主张的非生成式抽象预测世界模型代表。 * I-JEPA(Image JEPA,2023) 输入一张图像,模型把不同区域(context 和 target)编码成抽象表示,然后预测 target 的表示(不在像素级别重建)。 例子:给定一张遮挡了部分物体的图片,模型能预测“被遮挡物体的大致位置和属性”,构建对物体持久性和空间关系的理解。 这是一个“原始世界模型”,能学习物理常识(如物体不会凭空消失)。 * V-JEPA / V-JEPA 2(Video JEPA,

By SHI XIAOLONG

什么是:“世界模型(World Models)”

世界模型(World Models) 是人工智能领域的一个核心概念,尤其在 Yann LeCun 等研究者推动的下一代 AI 架构中占据中心位置。它指的是 AI 系统在内部构建的对现实世界的抽象模拟或内部表示,让机器能够像人类或动物一样“理解”物理世界、预测未来、规划行动。 简单比喻 想象你闭上眼睛也能“看到”房间里的物体会如何移动、碰撞或掉落——这就是你大脑里的世界模型。AI 的世界模型就是类似的“数字孪生”(digital twin)或“内部模拟器”:它不是简单记住数据,而是学习世界的动态、因果关系和物理直觉(如重力、物体持久性、遮挡、因果等)。 为什么需要世界模型? 当前主流的大型语言模型(LLM) 擅长处理文本(统计模式预测),但存在根本局限: * 缺乏对物理世界的真正理解 → 容易“幻觉”、无法可靠规划。 * 样本效率低 → 人类/

By SHI XIAOLONG

K线周期可配置化设计方案

K线周期可配置化设计方案 1. 背景与目标 当前 Beta 套利策略的 K 线周期硬编码为 "1h",分散在多个文件中。需要: 1. 将 K 线周期从 1h 改为 2h 2. 提取为环境变量 BETA_ARB_KLINE_INTERVAL,使其可在 .env 中配置 2. 影响范围分析 2.1 需要修改的文件(共 6 个) 文件 硬编码位置 修改内容 src/trading/config.py BetaArbConfig dataclass 新增 kline_interval 字段,

By SHI XIAOLONG