4、统计特征信号:均值、方差、偏度、峰度、分位数、滑动窗口统计
各位做量化的朋友,咱们今天聊聊统计特征信号。说实话,这东西看着基础,但我在实盘里吃过不少亏。很多人一上来就上深度学习,结果连最基本的均值回归都没搞清楚。我个人习惯是,先把这些统计量玩透了,再谈别的。
统计特征信号,说白了就是通过数学工具,把价格序列的「脾气秉性」给量化出来。你想想看,一个品种是爱走极端,还是温温吞吞?是突然暴涨暴跌,还是稳步推进?这些都能用统计量来描述。
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 方差与标准差:市场「脾气」有多大?
方差衡量的是价格偏离均值的程度。说白了,就是市场波动有多大。标准差是方差的平方根,单位跟价格一样,更好理解。
嗯,这里要注意:方差大的时候,趋势策略容易赚钱;方差小的时候,震荡策略更合适。我曾在一次实盘中,用标准差做自适应参数调整——波动率高了就缩短均线周期,效果还不错。
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 组合信号:把统计量串起来
单个统计量往往不够用。我习惯把多个统计量组合成一个综合信号。比如:
- 当价格低于20%分位数(超卖)
- 且偏度为正(右尾风险小)
- 且标准差处于低位(波动不大)
- ——这时候做多,胜率会高很多
说白了,就是让多个条件互相印证。单一指标可能骗人,但多个指标同时指向一个方向,那可信度就高了。
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
好了,统计特征信号就聊到这儿。这些工具看着简单,但用好了威力很大。我见过太多人一上来就搞复杂模型,结果连最基本的均值回归都没跑通。先把这些基础打牢,后面的路会顺很多。