4、统计特征信号:均值、方差、偏度、峰度、分位数、滑动窗口统计

各位做量化的朋友,咱们今天聊聊统计特征信号。说实话,这东西看着基础,但我在实盘里吃过不少亏。很多人一上来就上深度学习,结果连最基本的均值回归都没搞清楚。我个人习惯是,先把这些统计量玩透了,再谈别的。

统计特征信号,说白了就是通过数学工具,把价格序列的「脾气秉性」给量化出来。你想想看,一个品种是爱走极端,还是温温吞吞?是突然暴涨暴跌,还是稳步推进?这些都能用统计量来描述。

统计特征信号 集中趋势 均值 中位数 分位数 离散程度 方差 标准差 波动率 分布形态 偏度 峰度 Jarque-Bera 滑动窗口统计

4.1 均值与中位数:价格的「重心」在哪?

均值,就是一段时间内价格的平均值。但这里有个坑——均值对极端值非常敏感。我记得有一次做ETH的统计,突然一个插针把均值拉高了3%,结果信号全乱了。

所以我建议,做均值信号时,最好同时看中位数。中位数不受极端值影响,更能反映「大多数人的成交价」。

实战经验: 我个人习惯用「均值-中位数」的差值作为市场失衡信号。差值越大,说明极端交易越多,市场情绪越亢奋。
import numpy as np
import pandas as pd

def mean_median_signal(price_series, window=20):
    """
    均值-中位数偏离信号
    返回:偏离度(百分比)
    """
    rolling_mean = price_series.rolling(window).mean()
    rolling_median = price_series.rolling(window).median()
    
    # 偏离度 = (均值 - 中位数) / 中位数
    deviation = (rolling_mean - rolling_median) / rolling_median
    
    return deviation

4.2 方差与标准差:市场「脾气」有多大?

方差衡量的是价格偏离均值的程度。说白了,就是市场波动有多大。标准差是方差的平方根,单位跟价格一样,更好理解。

嗯,这里要注意:方差大的时候,趋势策略容易赚钱;方差小的时候,震荡策略更合适。我曾在一次实盘中,用标准差做自适应参数调整——波动率高了就缩短均线周期,效果还不错。

小技巧: 用标准差做「布林带」的宽度。当带宽收缩到历史低位时,往往预示着大行情要来了。这个信号我用了好几年,胜率大概在65%左右。
def bollinger_band_signal(price, window=20, num_std=2):
    """
    布林带信号
    返回:带宽、价格在带内的位置
    """
    sma = price.rolling(window).mean()
    std = price.rolling(window).std()
    
    upper = sma + num_std * std
    lower = sma - num_std * std
    
    # 带宽 = (上轨 - 下轨) / 中轨
    bandwidth = (upper - lower) / sma
    
    # 价格位置 = (价格 - 下轨) / (上轨 - 下轨)
    position = (price - lower) / (upper - lower)
    
    return bandwidth, position

4.3 偏度:涨跌是否「偏心」?

偏度描述的是分布的不对称性。正偏度意味着右尾更长——也就是容易出现暴涨;负偏度意味着左尾更长——容易出现暴跌。

为什么会这样?因为市场参与者的行为不对称。散户喜欢追涨,所以顶部往往有长尾;机构喜欢止损,所以底部也容易有长尾。

我曾经用偏度做过一个「尾部风险预警」信号:当偏度绝对值超过1.5时,说明市场处于极端状态,这时候开仓要格外小心。

def skewness_signal(returns, window=30):
    """
    偏度信号
    返回:滚动偏度值
    """
    # 使用 pandas 的滚动偏度
    skew = returns.rolling(window).skew()
    
    # 生成信号:|偏度| > 1.5 时预警
    signal = pd.Series(0, index=returns.index)
    signal[skew > 1.5] = 1    # 正偏度预警
    signal[skew < -1.5] = -1  # 负偏度预警
    
    return skew, signal

4.4 峰度:行情是否「极端」?

峰度衡量的是分布的「尖峭」程度。峰度大于3,说明分布比正态分布更尖——也就是极端值更多。说白了,就是市场经常出现「意料之外」的大波动。

我记得2020年3月那次熔断,标普500的20日滚动峰度直接飙到了12以上。正常市场峰度在3-5之间,超过8就要高度警惕了。

避坑指南: 我曾经犯过一个错误——直接用原始价格算峰度。结果因为趋势项的存在,峰度值一直很大,信号完全失效。正确的做法是用收益率序列来计算。
def kurtosis_signal(returns, window=30):
    """
    峰度信号
    返回:滚动峰度值(超额峰度,即减去3)
    """
    # 计算滚动峰度,pandas默认返回超额峰度
    kurt = returns.rolling(window).kurt()
    
    # 生成信号
    signal = pd.Series(0, index=returns.index)
    signal[kurt > 3] = 1   # 厚尾预警
    signal[kurt < 0] = -1  # 薄尾(市场过于平静)
    
    return kurt, signal

4.5 分位数:市场的「水位线」

分位数比均值更稳健。25%分位数意味着「有25%的交易价格低于这个值」。我个人最喜欢用分位数做「相对价值」判断。

举个例子:如果当前价格跌到了5%分位数以下,说明价格已经处于历史极低位置。这时候做多,胜率往往不错。但要注意——「低」不代表不会更低,所以一定要配合止损。

分位数 含义 交易信号
5% 极端低位 超卖,考虑做多
25% 偏低 偏弱,观望或轻仓
50% 中位数 中性,无明确方向
75% 偏高 偏强,考虑做空
95% 极端高位 超买,考虑做空
def quantile_signal(price, window=100, quantiles=[0.05, 0.25, 0.5, 0.75, 0.95]):
    """
    分位数信号
    返回:当前价格在历史分位数中的位置
    """
    # 计算滚动分位数
    quantile_values = price.rolling(window).quantile(quantiles)
    
    # 当前价格位置(0~1之间)
    current_price = price.iloc[-1]
    position = 0
    for i, q in enumerate(quantiles):
        if current_price <= quantile_values.iloc[-1, i]:
            position = q
            break
    else:
        position = 1.0
    
    return position

4.6 滑动窗口统计:动态视角

前面说的所有统计量,都需要一个「窗口」来计算。窗口大小怎么选?这问题我纠结了很久。

我的经验是:

  • 短窗口(5-10期):捕捉短期波动,适合高频交易
  • 中窗口(20-60期):捕捉中期趋势,适合日内或隔夜
  • 长窗口(100-200期):捕捉长期结构,适合趋势跟踪

但别死板。我建议用多窗口组合——比如同时看20日和60日的偏度。如果两个窗口的偏度方向一致,信号可靠性更高。

核心思路: 滑动窗口统计的本质,是把「静态」的统计量变成「动态」的信号。你想想看,一个均值如果一直在上升,说明趋势在延续;如果均值走平了,说明可能要变盘。
def multi_window_stats(price, windows=[10, 20, 60]):
    """
    多窗口统计特征
    返回:各窗口的均值、标准差、偏度、峰度
    """
    results = {}
    for w in windows:
        stats = {
            'mean': price.rolling(w).mean().iloc[-1],
            'std': price.rolling(w).std().iloc[-1],
            'skew': price.rolling(w).skew().iloc[-1],
            'kurt': price.rolling(w).kurt().iloc[-1],
        }
        results[f'window_{w}'] = stats
    
    return results

4.7 组合信号:把统计量串起来

单个统计量往往不够用。我习惯把多个统计量组合成一个综合信号。比如:

  1. 当价格低于20%分位数(超卖)
  2. 且偏度为正(右尾风险小)
  3. 且标准差处于低位(波动不大)
  4. ——这时候做多,胜率会高很多

说白了,就是让多个条件互相印证。单一指标可能骗人,但多个指标同时指向一个方向,那可信度就高了。

我的习惯: 每个统计量给一个0-1的分数,然后加权求和。权重根据历史回测来定。这样既保留了每个统计量的信息,又避免了「一刀切」的硬性阈值。
def composite_stat_signal(price, returns):
    """
    综合统计信号(0~1之间)
    0 = 强烈看空,1 = 强烈看多
    """
    # 1. 分位数信号(0~1)
    quantile_pos = quantile_signal(price, window=100)
    
    # 2. 偏度信号(归一化到0~1)
    skew, _ = skewness_signal(returns, window=30)
    skew_norm = (skew.iloc[-1] + 3) / 6  # 假设偏度在-3~3之间
    
    # 3. 波动率信号(低波动=安全)
    std = returns.rolling(20).std().iloc[-1]
    std_norm = 1 - (std / returns.rolling(100).std().iloc[-1])  # 当前波动/历史波动
    
    # 综合得分(等权)
    composite = (quantile_pos * 0.4 + skew_norm * 0.3 + std_norm * 0.3)
    
    return composite

好了,统计特征信号就聊到这儿。这些工具看着简单,但用好了威力很大。我见过太多人一上来就搞复杂模型,结果连最基本的均值回归都没跑通。先把这些基础打牢,后面的路会顺很多。


专注资料整理