市场水温算法设计方案:多因子热力累积模型
市场水温算法设计方案:多因子热力累积模型
Market Temperature — Heat Accumulation Index (HAI)
一、背景与动机
1.1 核心观察
在加密市场中存在一个"水温"现象:
- 当锚定资产(BTC)价格持续运行在高位时,ALT 资产的 Beta 系数、相关性、跟随性会发生相变
- 高温持续 → ALT 表现暴躁:Beta 跳变、跟随性增强、波动幅度夸张
- 低温持续 → ALT 表现迟钝:Beta 低迷、跟随性差、情绪摆烂
关键特征:不是瞬时温度决定行为,而是温度在高位维持足够长时间后,行情才发生质变。
1.2 在量化金融中的对应
这个现象在学术界对应 Market Regime Detection(市场状态识别):
| 方法 | 优点 | 缺点 | 适用性 |
|---|---|---|---|
| HMM(隐马尔可夫) | 学术主流,概率输出 | 需离线训练,状态数主观,推断滞后 | 不适合:与实时 WebSocket 架构冲突 |
| Markov Switching | 经济学标准 | 同 HMM,计算更重 | 不适合:宏观频率,非 5m 级别 |
| MSM(多分形切换) | 捕捉长记忆 | 参数估计困难 | 不适合:实现复杂,调试困难 |
| GMM(高斯混合) | 自动聚类 | 需批量训练,特征工程敏感 | 不适合:非增量计算 |
| 深度学习(VAE/Transformer) | 理论上限高 | 需训练数据/GPU,黑箱 | 不适合:过度工程 |
| 多因子热力累积 | 增量计算,可解释,零训练 | 权重需调优 | 最适合:完美匹配现有架构 |
1.3 选择理由
本系统已在 momentum_filter.py 中实现了所有必要的构建组件:
- Rogers-Satchell 波动率估计
- BOCPD 趋势概率
- Huber-CUSUM 急动检测
- Efficiency Ratio 趋势强度
- Volume EMA 量能放大
无需引入新的外部依赖或数据源,只需将已有信号聚合为市场级温度指标。
二、算法架构
2.1 整体结构
┌───────────────────────────────────────────────────┐
│ BTC 5m K 线数据(WebSocket) │
└──────────────────────┬────────────────────────────┘
│
┌────────────▼────────────────┐
│ 第一层:瞬时温度计 │
│ (5 因子加权 → 0~100 分) │
│ │
│ RS波动率 × 0.25 │
│ BOCPD趋势概率 × 0.25 │
│ CUSUM急动幅度 × 0.20 │
│ ER趋势强度 × 0.15 │
│ 量能放大比 × 0.15 │
└────────────┬────────────────┘
│ instant_temp (0~100)
┌────────────▼────────────────┐
│ 第二层:热力累积器 │
│ (持续高温 → 热量累积) │
│ (降温 → 指数衰减) │
│ │
│ 半衰期 ≈ 11.5 小时 │
└────────────┬────────────────┘
│ heat (0~100)
┌────────────▼────────────────┐
│ 第三层:Regime 输出 │
│ │
│ cold (< 30) → 收紧策略 │
│ warm (30~70) → 正常运行 │
│ hot (> 70) → 风控介入 │
└─────────────────────────────┘
2.2 与现有系统的关系
┌─ momentum_filter(配对级过滤)─────────────────────┐
│ Layer 0: BOCPD → 单个配对的机制切换 │
│ Layer 1: ER → 单个配对的趋势强度 │
│ Layer 2: CUSUM → 单个配对的急动检测 │
│ Layer 3: 平稳性 → 单个配对的均值回复能力 │
└──────────────────────────────────────────────────┘
┌─ market_temperature(市场级过滤)── 本方案 ─────────┐
│ 聚焦 BTC,输出一个全局温度 │
│ 影响所有 ALT 配对的入场/出场决策 │
└──────────────────────────────────────────────────┘
决策流程:
should_enter()
├─ ① market_temperature.regime → 市场级闸门
└─ ② momentum_filter.check() → 配对级过滤
三、第一层:瞬时温度计
3.1 五因子定义
因子 1:Rogers-Satchell 波动率(权重 0.25)
已有实现位置:momentum_filter.py 中 _update_rs_vol()
# Rogers-Satchell 波动率(OHLC 估计,优于 close-to-close)
rs_var = log(H/C) * log(H/O) + log(L/C) * log(L/O)
rs_vol = sqrt(EMA(rs_var, span=rs_span))
归一化方式:滚动百分位排名(过去 7 天 = 2016 根 5m bar)
rs_percentile = percentile_rank(rs_vol_current, rs_vol_history[-2016:])
# 输出 0~100
选择理由:
- 基础风险度量,最稳定的温度底色
- Rogers-Satchell 不受漂移影响(比 Parkinson 更适合有趋势的市场)
因子 2:BOCPD 趋势概率(权重 0.25)
已有实现位置:momentum_filter.py 中 _bocpd_trend_probability()
# Bayesian Online Changepoint Detection
# 输出:P(|μ| > drift_threshold | data)
trend_prob = bocpd.trend_probability # 0.0 ~ 1.0
归一化方式:直接映射到 0~100
bocpd_score = trend_prob * 100
选择理由:
- 直接测量 regime shift 概率
- 在线贝叶斯推断,无滞后(相比 HMM 优势)
- 已在 Layer 0 中验证可靠
因子 3:CUSUM 急动幅度(权重 0.20)
已有实现位置:momentum_filter.py 中 _huber_cusum_check()
# 取双向 CUSUM 最大值,反映最大偏离幅度
cusum_max = max(S_pos, S_neg)
归一化方式:滚动百分位排名(过去 7 天)
cusum_percentile = percentile_rank(cusum_max, cusum_history[-2016:])
选择理由:
- 捕捉急剧价格变动(暴涨/暴跌)
- Huber 裁剪已内置,对插针鲁棒
- 与波动率互补:波动率是"震幅",CUSUM 是"方向性冲击"
因子 4:Efficiency Ratio(权重 0.15)
已有实现位置:momentum_filter.py 中 _er_check()
# Kaufman ER = 位移 / 路径长度
displacement = abs(close[-1] - close[-er_window])
volatility = sum(abs(close[i] - close[i-1]) for i in range(er_window))
er = displacement / volatility # 0.0 ~ 1.0
归一化方式:直接映射到 0~100
er_score = er * 100
选择理由:
- 测量趋势的"纯度"——ER 高说明价格单向运动
- 与水温概念对应:高 ER = 市场有方向 = 温度升高
因子 5:量能放大比(权重 0.15)
已有实现位置:momentum_filter.py 中成交量 EMA
# 当前成交量 / EMA 成交量
vol_ratio = current_volume / ema_volume
归一化方式:滚动百分位排名(过去 7 天)
volume_percentile = percentile_rank(vol_ratio, vol_ratio_history[-2016:])
选择理由:
- 量价验证:无量上涨是虚火,放量上涨才是真热
- 与其他 4 个因子互补:唯一的非价格维度
3.2 加权合成
instant_temp = (
0.25 * rs_percentile
+ 0.25 * bocpd_score
+ 0.20 * cusum_percentile
+ 0.15 * er_score
+ 0.15 * volume_percentile
)
# 输出范围:0 ~ 100
3.3 归一化策略:滚动百分位
为什么用百分位而不是固定阈值?
问题:2021 年 BTC 的 RS 波动率 = 0.05 是"高"
2025 年 BTC 的 RS 波动率 = 0.05 可能是"正常"
解决:percentile_rank 自动适应当前市场环境
永远在 0~100 范围内,无需手动校准
滚动窗口选择:7 天(2016 根 5m bar)
- 太短(1 天):百分位波动剧烈,温度抖动
- 太长(30 天):对近期变化不敏感
- 7 天:平衡稳定性与敏感性,覆盖一个完整的周周期
实现方式:增量排序缓冲区(deque),O(n) 插入 + O(1) 查询排名
from collections import deque
import bisect
class RollingPercentile:
"""增量滚动百分位计算器"""
def __init__(self, window: int = 2016):
self.window = window
self.values = deque(maxlen=window) # FIFO 原始值
self.sorted_vals = [] # 排序副本
def update(self, value: float) -> float:
"""添加新值,返回当前百分位 (0~100)"""
# 移除最旧值
if len(self.values) == self.window:
old = self.values[0]
idx = bisect.bisect_left(self.sorted_vals, old)
self.sorted_vals.pop(idx)
# 添加新值
self.values.append(value)
bisect.insort(self.sorted_vals, value)
# 计算百分位
rank = bisect.bisect_right(self.sorted_vals, value)
return (rank / len(self.sorted_vals)) * 100
四、第二层:热力累积器
4.1 核心思想
瞬时温度只告诉你"现在热不热",但水温模型的关键洞察是:
持续高温一段时间后,市场性格才会改变
这类似物理学中的热力累积:
- 温度高于基准 → 热量持续累积
- 温度低于基准 → 热量缓慢散失
- 累积热量超过阈值 → 相变发生
4.2 算法公式
class HeatAccumulator:
"""热力累积器:建模"持续高温→爆发"的动力学"""
def __init__(
self,
base_temp: float = 50.0, # 基准温度(低于此不累积)
decay_factor: float = 0.995, # 每根 5m bar 的衰减系数
accumulation_rate: float = 1.0, # 累积速率
max_heat: float = 10000.0, # 热量上限(防止无限累积)
):
self.base_temp = base_temp
self.decay_factor = decay_factor
self.accumulation_rate = accumulation_rate
self.max_heat = max_heat
self.heat = 0.0
def update(self, instant_temp: float) -> float:
"""每根 5m K 线调用一次"""
if instant_temp > self.base_temp:
# 高温累积:超出基准部分按速率累积
delta = (instant_temp - self.base_temp) * self.accumulation_rate
self.heat += delta
else:
# 降温衰减:指数衰减(比升温慢)
self.heat *= self.decay_factor
# 上限钳位
self.heat = min(self.heat, self.max_heat)
return self.heat
4.3 参数设计
基准温度 base_temp = 50
- 百分位 50 = 中位数 = "正常"水平
- 低于 50 的温度不会累积热量,只会衰减
- 含义:只有"高于正常"的状态才会推动 regime 变化
衰减系数 decay_factor = 0.995
半衰期计算:
ln(0.5) / ln(0.995) = 138.3 根 5m bar
138.3 × 5min = 691.5 min ≈ 11.5 小时
含义:
- BTC 从 80° 突然降到 40°
- ALT 的"暴躁状态"还会持续约 11.5 小时才衰减一半
- 这符合观察:ALT 的反应是迟缓的,不会立刻冷却
累积速率 accumulation_rate = 1.0
- 默认 1:1 累积
- 可调高(如 1.5)使温度更敏感
- 可调低(如 0.7)使温度更迟钝
热量上限 max_heat = 10000
- 防止极端持续牛市导致热量无穷大
- 归一化后限制在 0~100 范围
4.4 热量归一化
def normalized_heat(self) -> float:
"""将累积热量归一化到 0~100"""
# 使用 sigmoid 映射,中点在 max_heat/2
# 这样低热量区分辨率高,高热量区渐近饱和
midpoint = self.max_heat * 0.3 # 30% 处为 sigmoid 中点
k = 5.0 / midpoint # 控制斜率
score = 100.0 / (1.0 + math.exp(-k * (self.heat - midpoint)))
return score
选择 sigmoid 而非线性映射的原因:
- 低温区(0~30)需要更高分辨率——区分"冷淡"和"正常"
- 高温区(70~100)差异不大——都是"暴躁",具体多暴躁不重要
- 避免极端值导致输出跳变
4.5 热力累积动力学示意
瞬时温度 (instant_temp)
100│ ╱╲
│ ╱ ╲ ╱╲
80│────────╱────╲────╱──╲──── 高温线
│ ╱ ╲ ╱ ╲
50│──────╱────────╲╱──────── 基准线
│ ╱
│ ╱
0└─────────────────────────── 时间
累积热量 (heat)
100│ ╱──── ← 持续高温后累积到 regime 阈值
│ ╱
70│──────────────────╱────── HOT 阈值
│ ╱╲ ╱
│ ╱ ╲ ╱ ← 第一次高温不够持久,热量衰减
30│─────────╱────╲╱───────── COLD 阈值
│ ╱
│ ╱
0└─────────────────────────── 时间
↑ ↑
第一波高温 第二波持续高温
(热量累积 (热量持续累积
后衰减) 突破阈值→HOT)
五、第三层:Regime 分类与策略调整
5.1 三态 Regime 定义
class MarketRegime(Enum):
COLD = "cold" # 摆烂态
WARM = "warm" # 正常态
HOT = "hot" # 暴躁态
| Regime | 热量范围 | 市场特征 | ALT 表现 |
|---|---|---|---|
| COLD | < 30 | BTC 低波动/震荡 | Beta 低,跟随差,均值回复弱 |
| WARM | 30 ~ 70 | BTC 正常运行 | 配对交易理想环境 |
| HOT | > 70 | BTC 持续强势/暴动 | Beta 跳变,追涨杀跌,均值回复失效 |
5.2 Regime 对策略参数的调整
def get_regime_adjustments(regime: MarketRegime) -> dict:
"""根据 regime 调整策略参数"""
if regime == MarketRegime.COLD:
return {
# 收紧入场:市场太冷,均值回复信号不可靠
"zscore_entry_multiplier": 1.3, # 入场阈值 ×1.3(更难触发)
"cointegration_min_passed": 4, # 协整要求从 3 提高到 4
"position_size_multiplier": 0.7, # 仓位缩小 30%
"signal_strength_required": "strong", # 只接受强信号
}
elif regime == MarketRegime.WARM:
return {
# 正常参数:配对交易理想环境
"zscore_entry_multiplier": 1.0,
"cointegration_min_passed": 3,
"position_size_multiplier": 1.0,
"signal_strength_required": "medium",
}
elif regime == MarketRegime.HOT:
return {
# 保守策略:市场太热,均值回复假设可能失效
"zscore_entry_multiplier": 1.5, # 入场阈值 ×1.5(大幅收紧)
"cointegration_min_passed": 5, # 协整要求提高到 5
"position_size_multiplier": 0.5, # 仓位减半
"signal_strength_required": "strong", # 只接受强信号
"max_concurrent_positions": 2, # 最大持仓数减少
}
5.3 Regime 切换防抖
为避免在阈值边界频繁切换:
class RegimeClassifier:
"""带滞后的 Regime 分类器"""
# 正向阈值(进入)和反向阈值(退出)不同 → 滞后效应
THRESHOLDS = {
# (进入阈值, 退出阈值)
MarketRegime.COLD: (None, 35), # 热量 > 35 才退出 COLD
MarketRegime.WARM: (30, 70), # 标准区间
MarketRegime.HOT: (65, None), # 热量 < 65 才退出 HOT
}
def __init__(self):
self.current_regime = MarketRegime.WARM
self.regime_since = datetime.now() # 记录进入当前 regime 的时间
self.min_regime_duration = 300 # 最少维持 5 分钟(1 根 bar)
def classify(self, heat_normalized: float) -> MarketRegime:
"""带滞后的分类"""
now = datetime.now()
# 防抖:最少维持一段时间
if (now - self.regime_since).total_seconds() < self.min_regime_duration:
return self.current_regime
new_regime = self.current_regime
if self.current_regime == MarketRegime.COLD:
if heat_normalized > 35: # 退出阈值高于进入阈值
new_regime = MarketRegime.WARM
elif self.current_regime == MarketRegime.WARM:
if heat_normalized < 30:
new_regime = MarketRegime.COLD
elif heat_normalized > 70:
new_regime = MarketRegime.HOT
elif self.current_regime == MarketRegime.HOT:
if heat_normalized < 65: # 退出阈值低于进入阈值
new_regime = MarketRegime.WARM
if new_regime != self.current_regime:
self.current_regime = new_regime
self.regime_since = now
return self.current_regime
滞后设计:
- 进入 HOT 需要 heat > 70,退出 HOT 只需 heat < 65
- 进入 COLD 需要 heat < 30,退出 COLD 只需 heat > 35
- 5 分百分位的滞后带防止在边界来回切换
六、集成设计
6.1 新增文件
src/trading/market_temperature.py # ~150 行,核心实现
6.2 修改文件
src/trading/config.py # ~15 行,新增配置参数
src/trading/strategy.py # ~30 行,集成温度判断
src/services/realtime_kline_service_base.py # ~20 行,驱动温度更新
6.3 配置参数
# trading/config.py → StrategyParams 新增
# --- 市场水温 ---
market_temperature_enabled: bool = True
market_temperature_base_temp: float = 50.0 # 基准温度
market_temperature_decay: float = 0.995 # 衰减系数(半衰期≈11.5h)
market_temperature_percentile_window: int = 2016 # 百分位窗口(7天×288根/天)
market_temperature_weights: dict = field(default_factory=lambda: {
"rs_vol": 0.25,
"bocpd": 0.25,
"cusum": 0.20,
"er": 0.15,
"volume": 0.15,
})
market_temperature_cold_threshold: float = 30.0 # COLD regime 阈值
market_temperature_hot_threshold: float = 70.0 # HOT regime 阈值
market_temperature_hysteresis: float = 5.0 # 滞后带宽度
6.4 数据流集成
realtime_kline_service_base.py
│
├─ _on_candle_update(symbol="BTC/USDC:USDC", timeframe="5m")
│ │
│ ├─ [已有] momentum_filter._update_rs_vol(btc_candle)
│ ├─ [已有] momentum_filter._bocpd_trend_probability(btc_data)
│ ├─ [已有] momentum_filter._huber_cusum_check(btc_data)
│ ├─ [已有] momentum_filter._er_check(btc_data)
│ │
│ └─ [新增] market_temperature.update(
│ rs_vol=..., bocpd_prob=...,
│ cusum_max=..., er=..., vol_ratio=...
│ )
│
├─ _process_analysis_result(symbol="ALT/USDC:USDC")
│ └─ strategy.should_enter()
│ ├─ [新增] market_temperature.regime → 市场级闸门
│ └─ [已有] momentum_filter.check() → 配对级过滤
6.5 strategy.py 集成点
# strategy.py :: AdaptiveBollingerStrategy.should_enter()
def should_enter(self, pair_key, direction, zscore, signal_strength, ...):
# === 第一关:市场水温(市场级) ===
if self.market_temp and self.market_temp.enabled:
regime = self.market_temp.current_regime
adjustments = get_regime_adjustments(regime)
# 调整入场阈值
effective_zscore_threshold = (
self.params.entry_zscore * adjustments["zscore_entry_multiplier"]
)
# 信号强度要求
if signal_strength_rank(signal_strength) < \
signal_strength_rank(adjustments["signal_strength_required"]):
return False, f"regime={regime.value}, 信号强度不足"
# Z-score 检查(调整后)
if abs(zscore) < effective_zscore_threshold:
return False, f"regime={regime.value}, z-score 未达调整后阈值"
# === 第二关:动量过滤(配对级) ===
# [已有逻辑不变]
...
七、监控与告警
7.1 飞书推送
Regime 切换时推送告警:
🌡️ 市场水温变化
Regime: WARM → HOT
瞬时温度: 82.3
累积热量: 73.1
触发因子:
RS波动率: 91.2 (P95)
BOCPD趋势: 78.5
CUSUM急动: 85.3 (P92)
ER趋势: 67.2
量能放大: 72.8 (P80)
策略调整:
入场阈值: ×1.5
仓位: ×0.5
最大持仓: 2
7.2 日志记录
每根 5m bar 记录温度快照(DEBUG 级别):
[MarketTemp] instant=72.3 heat=2847.5 normalized=58.2 regime=WARM
Regime 切换记录(INFO 级别):
[MarketTemp] REGIME CHANGE: WARM → HOT (heat=73.1, duration_in_warm=4.2h)
7.3 数据库存储(可选)
可将温度数据写入 analysis_results 表的扩展字段,用于回测分析:
ALTER TABLE analysis_results ADD COLUMN IF NOT EXISTS market_temp DOUBLE PRECISION;
ALTER TABLE analysis_results ADD COLUMN IF NOT EXISTS market_regime VARCHAR(10);
八、回测验证方案
8.1 验证目标
- Regime 识别是否与主观判断一致:对照 BTC 历史行情,验证 HOT/COLD 划分是否合理
- 对策略收益的影响:在 HOT regime 收紧参数 vs 不收紧,对比胜率和盈亏比
- 参数敏感性:衰减系数、阈值、权重的变化对结果的影响
8.2 验证方法
# 使用历史 klines 数据回放
for candle in historical_btc_5m_candles:
temp.update(candle)
regime = temp.current_regime
# 对比:
# A 组:不使用水温过滤的原始策略
# B 组:使用水温过滤的策略
# 统计:胜率、盈亏比、最大回撤、夏普比率
8.3 关键检验指标
| 指标 | 期望效果 |
|---|---|
| HOT regime 下 B 组回撤 | 显著低于 A 组 |
| WARM regime 下 AB 组表现 | 基本一致(水温不干预正常交易) |
| COLD regime 下 B 组假信号 | 少于 A 组(收紧入场过滤了弱信号) |
| 整体夏普比率 | B 组 ≥ A 组 |
九、实现优先级
| 优先级 | 组件 | 预估代码量 |
|---|---|---|
| P0 | MarketTemperature 核心类(温度计 + 累积器 + 分类器) |
~120 行 |
| P0 | 配置参数 (StrategyParams 新增字段) |
~15 行 |
| P0 | strategy.py 集成 | ~30 行 |
| P0 | service_base.py 驱动 | ~20 行 |
| P1 | 飞书告警推送 | ~30 行 |
| P1 | 日志输出 | ~10 行 |
| P2 | 数据库存储(回测用) | ~20 行 |
| P2 | 回测验证脚本 | ~100 行 |
总计核心实现:~185 行新代码 + ~50 行修改
十、风险与局限
10.1 已知局限
| 局限 | 影响 | 缓解措施 |
|---|---|---|
| 百分位窗口冷启动 | 启动前 7 天百分位不准 | 使用历史数据预填充 |
| 权重固定 | 不同市场周期最优权重可能不同 | 后续可加入自适应权重 |
| 仅基于 BTC | 部分 ALT 有独立行情 | 可扩展为 BTC + ETH 双锚定 |
| 无情绪/资金费率数据 | 温度计缺少市场情绪维度 | 后续补充 funding rate 因子 |
10.2 后续演进方向
- 资金费率因子:接入 Hyperliquid funding rate API,作为第 6 个温度因子
- 自适应权重:根据各因子近期预测力动态调整权重(滚动 Sharpe)
- 多锚定资产:BTC + ETH 双温度计,取加权平均
- 配对级温度修正:特定 ALT 对 BTC 温度的敏感度不同(β 调节)
- 回测自动调参:基于历史数据的网格搜索最优参数组合