序数模式熵:用涨跌排列的「形状」识别市场状态切换
排列熵(PE)的经典用法是「用一个数区分正弦/混沌/噪声」。但真正在量化里值钱的是把它做成实时状态监测器:滑动窗 PE + 模式转移矩阵 + 加权 PE(WPE),能在有序趋势市和无序震荡/危机市之间划出清晰边界。本文从零实现序数模式映射、PE、WPE 与转移矩阵,在「有序-无序」交替合成行情上以 97.6% 正确率识别 regime,并诚实拆穿「PE 量的是结构不是波动」这条最容易被误解的边界(中阶)。
上一篇文章我们讲了**排列熵(PE)**怎么用一个数区分规则正弦、混沌、纯随机。但那只是 PE 的「静态刻画」。真正在量化里值钱的是它另一半能力:把 PE 做成实时状态监测器。
结论先放这:排列熵不关心价格涨了多少,只关心局部涨跌的「形状」。当行情从「有序趋势市」滑进「无序震荡/危机市」时,局部形状会从「少数几种反复出现」变成「乱七八糟全都有」——PE 会随之从低抬到高。用滑动窗 PE + 模式转移矩阵 + 加权 PE(WPE),我们能在合成「有序-无序」交替行情上以 97.6% 正确率实时识别 regime。所有图表均由下文 Python 真实计算,非占位图。

一、PE 快速回顾:它量的是「形状」不是「幅度」#
PE(Bandt-Pompe 2002)只做一件事:看窗口内点相对大小的排列,不看绝对价格。
- 取 m 个点的窗口,用
argsort的秩把它映射成 0..m-1 的一个排列(比如[3,1,2]→ 秩排列(2,0,1)); - 把整条序列切成重叠窗口,数每种排列模式出现的频率;
- 对频率算香农熵,再除以
log(m!)归一化到 [0,1]。
import numpy as np, math
def perm_index(perm):
"""把 0..m-1 的排列映射成 Lehmer 码 0..m!-1"""
m = len(perm); avail = list(range(m)); idx = 0
for i, p in enumerate(perm):
k = avail.index(p); idx = idx * (m - i) + k; avail.pop(k)
return idx
def pattern_of(window):
"""窗口内相对大小排序 → 秩排列 (0..m-1 各一次)"""
return tuple(np.argsort(np.argsort(window, kind="stable")))
def permutation_entropy(x, m=5, tau=1):
"""标准排列熵(归一化到 [0,1])"""
x = np.asarray(x, float); n = len(x); L = (m - 1) * tau
counts = np.zeros(math.factorial(m))
for i in range(n - L):
w = x[i:i + L + 1:tau]
counts[perm_index(pattern_of(w))] += 1
p = counts[counts > 0] / counts.sum()
H = -np.sum(p * np.log2(p))
return float(H / math.log2(math.factorial(m)))python在三个经典序列上(各 2000 点):
PE(正弦) = 0.2161 ← 形状固定,少数模式反复出现 → 低熵
PE(混沌) = 0.6787 ← 确定但不重复 → 中熵
PE(白噪声) = 0.9947 ← 完全无序 → 近满熵
PE(随机游走) = 0.9168 ← 高波动,但增量 i.i.d. → 也近满熵plaintext这里藏着一个最容易被误解的边界:PE(随机游走)=0.92 和 PE(白噪声)=0.99 几乎一样高——尽管随机游走的波动比白噪声大一整个量级。原因:PE 只数「涨跌形状」,随机游走的增量是独立同分布的,局部形状同样乱。所以——
PE 量的是序列的「确定性/结构」,不是「波动大小」。高波动随机游走和低波动随机游走,PE 一样高。
这条边界决定了下节怎么设计 regime 检测:要让 PE 能区分的两段,必须在结构上不同(有序 vs 无序),而不是只在波动大小上不同。
二、变成实时监测器:滑动窗 PE#
把 PE 从「整条序列算一个值」升级成「在长度为 W 的滚动窗口上算一串值」,就得到一条随时间走的 PE 曲线。关键实现:每个窗口内要含足够多的重叠模式(用 60 点窗口、m=5),否则只剩一个模式、熵恒为 0。
def sliding_pe(x, m=5, tau=1, win=60, step=1):
"""在长度 win 的局部片段上算 PE(片段内含多模式才有意义)"""
x = np.asarray(x, float); centers, vals = [], []
for i in range(0, len(x) - win + 1, step):
centers.append(i + win // 2)
vals.append(permutation_entropy(x[i:i + win], m, tau))
return np.array(centers), np.array(vals)python构造一段「有序-无序」交替的合成行情:有序段用平滑正弦 + 极小噪声(局部形状固定 → 低 PE),无序段用纯白噪声(形状乱 → 高 PE),两段振幅相同(都是 0.02,确保只在结构上不同)。跑滑动 PE:
PE 区分 regime 正确率 = 97.6%
有序段 PE 均值 = 0.520 无序段 PE 均值 = 0.778 分类阈值 = 0.649plaintext有序段 PE 低且稳、无序段 PE 高且抖,阈值 0.65 一刀下去判对 97.6%。PE 曲线在每次切换点都干净地翻过阈值——这正是「用涨跌形状抓状态切换」的直观证据(见首图)。
模式频率直方图把这件事说得更死:有序段只有 93/120 种模式被用到、且少数模式被反复使用(聚堆);无序段 120/120 种全出现、铺得均匀。

三、转移矩阵:把「形状」连成马尔可夫链#
单个窗口的 PE 是「此刻有多乱」。要更进一步看「乱的结构有没有变化」,把相邻窗口的模式编号连起来,得到一个模式间的马尔可夫转移矩阵 T[a,b]=P(下一窗口模式=b | 本窗口模式=a)。
def pattern_sequence(x, m=5, tau=1):
x = np.asarray(x, float); L = (m - 1) * tau
return np.array([perm_index(pattern_of(x[i:i + L + 1:tau]))
for i in range(len(x) - L)])
def transition_matrix(seq, m=5):
T = np.zeros((math.factorial(m),) * 2)
for a, b in zip(seq[:-1], seq[1:]):
T[a, b] += 1
rs = T.sum(axis=1, keepdims=True); rs[rs == 0] = 1
return T / rs
def mean_row_entropy(T):
"""转移矩阵平均每行香农熵(bit),范围 [0, log2(行数)]"""
H = -np.sum(T[T > 0] * np.log2(T[T > 0]))
return float(H / T.shape[0])python对有序段和无序段分别建转移矩阵:
转移平均行熵 有序 = 0.397 无序 = 1.692 (无序更均匀 → 更高)
用到的模式数 有序 = 93/120 无序 = 120/120plaintext有序段的转移矩阵「集中」——某个模式出现后,下一窗口大概率还是那几种固定模式(平均行熵 0.40);无序段「到处跳」——每个模式之后都可能接任意模式(平均行熵 1.69,接近满熵)。这给你一个第二维度的 regime 信号:不只是「现在多乱」(PE),还有「乱得有没有记忆结构」(转移熵)。两者可以组合成更稳的状态识别器。

四、加权 PE(WPE):对振幅敏感的那一版#
标准 PE 只看秩(ordinal),对振幅完全免疫——这通常是优点(抗量纲),但有时是缺点。比如一段低振幅信号里突然插了一段大振幅尖峰,标准 PE 因为只看形状,几乎纹丝不动;而 加权排列熵 WPE(Fadlallah 2013) 用窗口振幅方差给每个模式加权,尖峰窗口权重骤增,曲线会明显抬起。
def weighted_pe(x, m=5, tau=1):
"""加权排列熵 WPE:用窗口振幅方差给模式加权,抑制小幅噪声伪迹"""
x = np.asarray(x, float); n = len(x); L = (m - 1) * tau
weights = np.zeros(math.factorial(m))
for i in range(n - L):
w = x[i:i + L + 1:tau]
weights[perm_index(pattern_of(w))] += np.sum((w - w.mean()) ** 2)
W = weights.sum()
if W <= 0:
return 0.0
p = weights[weights > 0] / W
return float(-np.sum(p * np.log2(p)) / math.log2(math.factorial(m)))python基准是平滑低振幅信号,中间插入一段大振幅尖峰。两种熵对比:
尖峰处 PE 与 WPE 最大差 = 0.266 (WPE 被尖峰窗口权重抬起,PE 纹丝不动)plaintext
怎么选:做纯粹的状态/结构识别(趋势市 vs 震荡市)用标准 PE——它只抓形状,干净;做异常检测、想让「振幅突变」也报警时用 WPE——它同时编码形状和振幅。别混用:把 WPE 当结构指标会把「单纯放量」误读成「状态切换」。
五、在量化里它能干什么(以及诚实边界)#
能干的:
- 趋势/震荡状态识别:滑动 PE 低 = 有序(趋势市,可考虑趋势跟随),高 = 无序(震荡/危机,可切震荡策略或降仓)。
- 危机预警:危机市局部形状无序化,PE 抬升往往领先于波动率本身的跳变——可作为波动率的「结构先行指标」。
- regime 组合信号:PE(当前有多乱)+ 转移熵(乱得有没有结构)双维度,比单用 PE 更抗单一窗口噪声。
- 异常/跳空检测:WPE 对振幅突变敏感,适合做「形状没变但量能突变」的告警。
诚实边界(最重要的一条):
- PE 量结构不量波动:高波动随机游走 PE 也满(≈0.92)。想用 PE 区分的 regime,必须在结构上不同,光波动大小不同分不出来(本文 demo 两段振幅刻意相同)。
- m 太小假饱和:m=2/3 时模式太少,正弦都可能显示高熵,失去区分力;常用 m=5~7。
- m 太大稀疏:m=7 就有 5040 种模式,短窗口数不全,熵被低估。
- 平局处理:价格有相等值时
argsort的秩不唯一,需在实现里约定稳定排序(本文用kind="stable")。 - 非因果:滑动 PE 在窗口中心取值,用到未来信息;实盘要把窗口右对齐到「当前」,或用 step 保证只用已发生数据。
六、五类真实陷阱(必看)#
- 波动误读陷阱:以为「PE 高 = 波动大」。错。PE 高 = 结构无序,高波动随机游走和低波动白噪声 PE 一样高。
- 窗口长度陷阱:滑动窗太短(只有 m 个点)熵恒为 0;太长则 regime 切换被平滑掉。本文用 60 点窗口配 m=5 是平衡点。
- m 选择陷阱:m 太小假饱和、m 太大稀疏,两者都让 PE 失真;先在小样本上扫 m 看 PE 对已知结构是否敏感。
- 超前泄漏陷阱:滑动 PE 用了窗口中心作为取值点,含未来数据。实盘必须右对齐到当前时刻,或只用历史窗口。
- WPE/PE 混用陷阱:把 WPE(对振幅敏感)当纯结构指标,会把「单纯放量」误判为「状态切换」,在放量不换趋势时疯狂误报。
结语#
序数模式熵的真正价值不在「把正弦/混沌/噪声分成三档」,而在把它做成一台实时状态监测器:滑动窗 PE 告诉你「此刻行情有多无序」,转移矩阵的转移熵告诉你「乱得有没有结构记忆」,WPE 在你要抓振幅突变时补上权重。三者组合,就能在有序趋势市与无序震荡/危机市之间画出清晰的实时边界——本文合成行情上正确率 97.6%。但它也不是银弹,最大的诚实边界是:PE 量的是结构不是波动,高波动随机游走的 PE 同样满值。把它和上一篇的 DCov/能量距离放一起看,你会得到一套层次分明的工具:DCov 管「变量间有没有非线性关系」、能量距离管「两段行情是不是同一种分布」、序数模式熵管「行情现在处在哪种结构状态」。
完整可复跑代码与全部图表脚本已随本文生成(gen_ope.py),三档经典序列 PE、滑动 PE 状态识别、模式转移矩阵、WPE vs PE 振幅敏感性均由该脚本真实计算。