P(trending) 算法原理说明
P(trending) 算法原理说明
1. 概述
P(trending) 是 BOCPD(贝叶斯在线变点检测)的核心输出,表示"当前市场处于趋势状态"的概率。当 P(trending) > 0.8 时,动量过滤器在 Layer0 硬拦截开仓信号。
总公式:
P(trending) = Σ P(r) × P(trending | r)
含义:对每个可能的 run length r,分别计算"该 regime 是否有趋势"的概率,然后按 r 自身的概率加权求和。
这个计算同时处理了两层不确定性:
- 变点在哪?(不确定 run length)→ 用 P(r) 加权
- 给定变点位置,漂移是否显著?(不确定均值大小)→ 用后验分布的尾部面积
2. 计算流程
2.1 总体结构
def trend_probability(self) -> float:
delta = self._drift_threshold # 0.0005(0.05%/5min)
total = 0.0
for i in range(len(self._log_probs)):
prob = P(r=i) # 第一层:run length 的概率
p_trend = P(trending | r=i) # 第二层:该 run length 下的趋势概率
total += prob * p_trend # 加权累加
return total
以下逐步展开每一层的计算。
3. 第一层:P(r) — run length 的概率
3.1 什么是 run length
run length r 表示"距离上一个变点已经过了多少根 K 线"。算法同时维护 r=0, 1, 2, ..., 59,每个 r 都有一个概率 P(r),所有概率之和为 1。
P(r=0) = 0.05 "刚刚发生了变点"
P(r=1) = 0.60 "当前 regime 持续了 1 根 K 线" ← MAP(最大后验)
P(r=2) = 0.15 "当前 regime 持续了 2 根 K 线"
P(r=5) = 0.10 "当前 regime 持续了 5 根 K 线"
...
3.2 P(r) 的更新机制
每收到一根新 K 线,P(r) 通过两个路径更新:
增长路径(没有发生变点,r 加 1):
P_new(r+1) ∝ P_old(r) × π(x|r) × (1 - H)
变点路径(发生了变点,r 重置为 0):
P_new(r=0) ∝ Σ_r [ P_old(r) × π(x|r) × H ]
其中:
π(x|r)是 run length r 对当前观测 x 的预测概率(似然)H = 0.05是变点先验概率(hazard rate)- 更新后归一化使所有 P(r) 之和为 1
4. 第二层:P(trending | r) — 给定 run length 下的趋势概率
这是 P(trending) 计算的核心。对每个 r,需要回答:"如果当前 regime 从 r 根 K 线前开始,那这段时间的数据是否表现出显著的趋势?"
4.1 μ_r 的后验分布
对 run length r,贝叶斯更新后得到 NIG 后验参数 (μ_r, κ_r, α_r, β_r)。其中 μ_r(均值的点估计)的真实值不是一个确定的数,而是服从 Student-t 分布:
μ_r 的真实值 ~ Student-t(df=2α_r, center=μ_r, scale=√(β_r/(α_r·κ_r)))
三个参数的含义:
| 参数 | 公式 | 含义 | 数据越多时 |
|---|---|---|---|
| center(中心) | μ_r | 均值的最佳猜测 | 越接近真实均值 |
| scale(尺度) | √(β_r/(α_r·κ_r)) | 猜测的不确定性 | 越小(越自信) |
| df(自由度) | 2α_r | 分布的厚尾程度 | 越大(越接近正态) |
4.2 后验分布随数据量的变化
r=1(1 个观测):
κ₁ = 2, α₁ = 3.5, scale 较大, df = 7
→ 分布宽且厚尾,不确定性大
┌───────────────────────┐
╱ ╲
╱ ╲
╱ ╲
──╱───────────────────────────────────╲──
μ₁
←── scale 大,分布宽 ──→
r=30(30 个观测):
κ₃₀ = 31, α₃₀ = 18, scale 较小, df = 36
→ 分布窄且接近正态,不确定性小
┌───┐
╱│ │╲
╱ │ │ ╲
╱ │ │ ╲
────────╱─────│───│─────╲────────
μ₃₀
←scale 小→
4.3 P(trending | r) 的计算
定义:μ 的真实值落在"无趋势区间" [-δ, +δ] 之外的概率(δ = drift_threshold = 0.0005)。
P(trending | r) = P(μ真实 > +δ) + P(μ真实 < -δ)
= 右尾面积 + 左尾面积
图示:
无趋势区间
┃←──────→┃
┃ ┃
┌─────────┃────────┃─────────┐
╱ 左尾 ┃ ┃ 右尾 ╲
╱ (下跌趋势)┃ (噪声) ┃(上涨趋势) ╲
──╱─────────────┃────────┃───────────╲──
-0.0005 +0.0005
┃ ┃
P(trending|r) = 左尾面积 + 右尾面积
= 阴影部分的总面积
代码实现:
# 将 ±δ 转换为 Student-t 分布上的标准化位置
t_upper = (delta - mu) / scale # +0.0005 的位置
t_lower = (-delta - mu) / scale # -0.0005 的位置
# 计算尾部面积
p_trend = (1.0 - CDF(t_upper)) # 右尾:P(μ真实 > +δ)
+ CDF(t_lower) # 左尾:P(μ真实 < -δ)
4.4 不同场景下的 P(trending | r)
场景 A:数据少,观测极端(r=1, μ=-0.004, scale=0.0027)
t_upper = (0.0005 - (-0.004)) / 0.0027 = 1.67
t_lower = (-0.0005 - (-0.004)) / 0.0027 = 1.30
P(trending|r=1) = (1 - CDF(1.67)) + CDF(1.30)
≈ 0.07 + 0.87 = 0.94
→ 94%。观测值太极端(-0.8%),即使只有 1 个数据点,
后验分布大部分面积也在无趋势区间之外。
-δ +δ
┃ ┃
████████████████┃ ┃███
╱████████████████┃ ┃███╲
╱█████████████████┃ ┃████╲
─╱──────────────────┃────┃─────╲─
μ₁=-0.004
大部分面积在 -δ 左边(下跌趋势)
场景 B:数据多,均值显著(r=30, μ=-0.001, scale=0.0003)
t_upper = (0.0005 - (-0.001)) / 0.0003 = 5.0
t_lower = (-0.0005 - (-0.001)) / 0.0003 = -1.67
P(trending|r=30) = (1 - CDF(5.0)) + CDF(-1.67)
≈ 0.00 + 0.95 = 0.95
→ 95%。均值只有 -0.1%,但 scale 极小(0.03%),
算法非常自信真实均值在无趋势区间之外。
-δ +δ
┃ ┃
████████┃ ┃
╱████████┃ ┃
╱█████████┃ ┃
──────────────╱───────────┃────┃──
μ₃₀=-0.001
分布窄,几乎全部面积在 -δ 左边
场景 C:数据中等,均值不明显(r=20, μ=-0.0003, scale=0.0004)
t_upper = (0.0005 - (-0.0003)) / 0.0004 = 2.0
t_lower = (-0.0005 - (-0.0003)) / 0.0004 = -0.5
P(trending|r=20) = (1 - CDF(2.0)) + CDF(-0.5)
≈ 0.03 + 0.31 = 0.34
→ 34%。均值接近 0,不确定性中等,
大部分面积在无趋势区间之内。
-δ +δ
┃ ┃
██████┃─────────┃██████
╱██████┃ ┃██████╲
╱████████┃ ┃████████╲
─────────╱──────────┃─────────┃──────────╲──
μ₂₀=-0.0003
分布中等宽度,大部分面积在无趋势区间之内
场景 D:数据多,均值接近零(r=40, μ=-0.00005, scale=0.0002)
t_upper = (0.0005 - (-0.00005)) / 0.0002 = 2.775
t_lower = (-0.0005 - (-0.00005)) / 0.0002 = -2.225
P(trending|r=40) = (1 - CDF(2.775)) + CDF(-2.225)
≈ 0.004 + 0.015 = 0.019
→ 1.9%。均值几乎为零,且分布很窄,
算法自信地判定没有趋势。
5. 第三层:加权求和
将所有 r 的结果汇总:
P(trending) = Σ P(r) × P(trending | r)
5.1 完整数值示例
| r | P(r) | μ_r | scale | P(trending|r) | 贡献 |
|---|---|---|---|---|---|
| 0 | 5% | 0.0000 | — | 50%(先验) | 0.025 |
| 1 | 60% | -0.0040 | 0.0027 | 94% | 0.564 |
| 5 | 15% | -0.0025 | 0.0015 | 88% | 0.132 |
| 20 | 8% | -0.0003 | 0.0004 | 34% | 0.027 |
| 30 | 10% | -0.0010 | 0.0003 | 95% | 0.095 |
| 其他 | 2% | ... | ... | ... | 0.010 |
| 合计 | 100% | 0.853 |
P(trending) = 0.853 > 0.8 → 触发拦截
5.2 各 r 的贡献分析
从上面的例子可以看出:
- r=1 贡献了 0.564(占总值的 66%):概率最高且 P(trending|r) 也高,主导了结果
- r=5 贡献了 0.132(15%):次要贡献
- r=30 贡献了 0.095(11%):虽然概率不高(10%),但 P(trending|r) 很高(95%)
- r=20 贡献了 0.027(3%):均值不明显,P(trending|r) 低,贡献小
6. drift_threshold 的作用
drift_threshold = 0.0005(每根 5m K 线 0.05%)是"有经济意义的最小漂移"。
6.1 为什么需要这个阈值
如果没有阈值(δ=0),那么问题变成"μ 是否恰好等于 0"。在连续分布下,P(μ=0) 永远是 0,所以 P(trending) 永远是 100%——这没有意义。
δ=0.0005 定义了一个"噪声带":收益率在 ±0.05% 以内视为正常波动,只有超出这个范围才算有趋势。
6.2 阈值对结果的影响
| drift_threshold | 效果 |
|---|---|
| 太小(如 0.00001) | 几乎任何微小漂移都算趋势,P(trending) 持续偏高,频繁误拦截 |
| 合适(0.0005) | 过滤掉正常波动,只捕捉有意义的趋势 |
| 太大(如 0.01) | 只有极端趋势才触发,失去保护作用 |
7. 与 drift_mean 的区别
| drift_mean | P(trending) | |
|---|---|---|
| 公式 | Σ P(r) × μ_r |
Σ P(r) × P(|μ_r| > δ | 后验) |
| 输出 | 一个数值(有方向和大小) | 一个概率(0~1) |
| 考虑不确定性 | 否,直接用点估计 μ_r | 是,用 μ_r 的完整后验分布 |
| 用途 | 诊断信息:趋势方向和强度 | 决策依据:是否拦截 |
关键区别:drift_mean 可能因为少量数据下 μ_r 的噪声而偏大,但 P(trending) 会通过后验分布的宽度(scale)自动降权——数据少时 scale 大,P(trending|r) 不会盲目偏高。
示例:r=1 只有 1 个观测 x=-0.002
- drift_mean 的贡献:P(r=1) × μ₁ = 0.6 × (-0.001) = -0.0006(看起来有趋势)
- P(trending|r=1):后验分布宽,μ 在 ±0.0005 内的概率不低 → P(trending|r=1) 可能只有 60%
- P(trending) 的贡献:0.6 × 0.6 = 0.36(被不确定性压低了)
8. Student-t CDF 的近似实现
P(trending|r) 的计算依赖 Student-t 分布的 CDF。代码中使用两种近似:
df < 30 时:正则化不完全 Beta 函数的连分数展开
p = t² / (df + t²)
# 然后用连分数近似计算 I_p(df/2, 1/2)
df >= 30 时:正态近似(Abramowitz & Stegun 多项式逼近)
z = t × (1 - 1/(4·df)) / √(1 + t²/(2·df))
# 然后用正态 CDF 的多项式近似
这使得整个计算无需依赖 scipy 等外部库,适合在线实时环境。
9. 完整代码注释版
@property
def trend_probability(self) -> float:
delta = self._drift_threshold # 0.0005
total = 0.0
for i in range(len(self._log_probs)):
# ── 第一层:P(r=i) ──
prob = math.exp(self._log_probs[i])
if prob < 1e-8: # 概率太小,跳过
continue
# ── 第二层:构建 μ 的后验 Student-t 分布 ──
mu, kappa, alpha, beta = self._suff[i]
df = 2 * alpha # 自由度
scale = math.sqrt(beta / (alpha * kappa)) # 不确定性尺度
# ── 第二层:计算 P(trending | r=i) ──
# 将 ±delta 转换为 Student-t 分布上的标准化坐标
t_upper = (delta - mu) / scale # +δ 的位置
t_lower = (-delta - mu) / scale # -δ 的位置
# 尾部面积 = P(μ真实 > +δ) + P(μ真实 < -δ)
p_trend = (1.0 - _student_t_cdf(t_upper, df) # 右尾
+ _student_t_cdf(t_lower, df)) # 左尾
# ── 加权累加 ──
total += prob * p_trend
return min(1.0, max(0.0, total))
10. 算法特性总结
| 特性 | 说明 |
|---|---|
| 双层不确定性建模 | 同时处理"变点在哪"和"均值是否显著"两个不确定性 |
| 自适应置信度 | 数据少时后验宽(保守判断),数据多时后验窄(自信判断) |
| 概率输出 | 输出连续概率而非二值判断,便于与阈值比较和跨层共享 |
| 无外部依赖 | Student-t CDF 用连分数/正态近似实现,无需 scipy |
| 在线计算 | 每根 K 线更新复杂度 O(R),R=60,适合实时系统 |
| 经济意义 | drift_threshold 保证只检测有交易意义的趋势,过滤噪声 |