23. 时间序列平滑:EWMA、卡尔曼滤波在动态曲面更新中的应用

波动率曲面不是静态的。你想想看,市场每分每秒都在变化,昨天的曲面到了今天可能就完全走样了。我刚开始做期权交易那会儿,就犯过这个错——用昨天的曲面去定价今天的期权,结果被风控部门叫去喝茶了。

所以,动态更新曲面是实战中的刚需。今天我们就聊聊两种最常用的平滑技术:EWMA 和卡尔曼滤波。

为什么需要时间序列平滑?

直接原因很简单:噪声太多。

每次我们重新拟合曲面,都会引入新的参数估计误差。如果直接把每次的拟合结果拼在一起,你会发现曲面在时间轴上跳来跳去,像喝醉了酒一样。这样的曲面没法用于交易,因为你的对冲参数会跟着乱跳。

平滑的本质,就是在「响应速度」和「稳定性」之间找平衡。我个人习惯把这个问题拆成两个维度:

  • 时间维度:相邻时间点的曲面参数应该缓慢变化
  • 结构维度:曲面本身的形状(偏斜、峰度)应该保持合理

EWMA 和卡尔曼滤波,正好分别擅长处理这两个维度的问题。

EWMA:最简单的动态平滑

EWMA 的全称是指数加权移动平均。说白了,就是给历史数据一个衰减的权重,越近的数据权重越大。

公式很简单:

θ_t = λ * θ_{t-1} + (1 - λ) * θ_t_obs

其中 θ_t 是平滑后的参数,θ_t_obs 是当前观测值,λ 是衰减因子(通常取 0.8~0.99)。

我在项目中遇到过一个问题:λ 取大了,曲面更新太慢,跟不上市场突变;取小了,又容易被噪声带偏。后来我总结了一个经验——先用历史数据做回测,找到使预测误差最小的 λ 值。

实战经验:对于股指期权,λ 取 0.94 左右效果不错。对于外汇期权,因为波动更剧烈,我习惯取 0.88。

下面是一个简单的 Python 实现:

import numpy as np

def ewma_smooth(obs_series, lambda_=0.94):
    """
    对观测序列做 EWMA 平滑
    obs_series: 按时间排序的观测值数组
    lambda_: 衰减因子
    """
    smoothed = np.zeros_like(obs_series)
    smoothed[0] = obs_series[0]
    
    for t in range(1, len(obs_series)):
        smoothed[t] = lambda_ * smoothed[t-1] + (1 - lambda_) * obs_series[t]
    
    return smoothed

嗯,这里要注意:EWMA 对突变反应比较慢。如果你发现市场出现了明显的 regime change(比如央行突然加息),最好手动重置平滑参数,或者暂时跳过平滑。

卡尔曼滤波:更聪明的平滑方式

EWMA 虽然简单,但它有个硬伤——它假设所有参数的变化规律是一样的。但实际中,不同期限的波动率参数,变化速度完全不同。

比如,近月合约的波动率变化很快,远月合约则相对稳定。用同一个 λ 去平滑,显然不合理。

卡尔曼滤波就能解决这个问题。它把曲面参数看作一个「状态向量」,用状态转移方程来描述参数的变化规律,再用观测方程来修正。

核心公式组:

# 状态预测
x_pred = F @ x_prev + w    # w ~ N(0, Q)
P_pred = F @ P_prev @ F.T + Q

# 观测更新
K = P_pred @ H.T @ inv(H @ P_pred @ H.T + R)
x_updated = x_pred + K @ (z - H @ x_pred)
P_updated = (I - K @ H) @ P_pred

看着复杂,其实逻辑很清晰:

  • F:状态转移矩阵,描述参数如何随时间演变
  • Q:过程噪声,控制参数变化的灵活度
  • R:观测噪声,控制我们对观测值的信任程度
  • K:卡尔曼增益,自动平衡预测和观测

我的小技巧:刚开始调参时,把 Q 设大一点、R 设小一点,让滤波器更相信观测值。等模型稳定了,再慢慢调小 Q。这样不容易发散。

实战对比:EWMA vs 卡尔曼滤波

我曾经在同一个数据集上对比过两种方法。数据是某股指期权 3 个月的日频曲面参数。

指标 EWMA (λ=0.94) 卡尔曼滤波
均方根误差 0.023 0.017
最大延迟(天) 3.2 1.8
参数稳定性 中等
计算耗时(每次) 0.001s 0.008s

卡尔曼滤波在精度和稳定性上都占优,但计算量也大一些。对于高频交易场景,EWMA 的轻量级优势就体现出来了。

动态曲面更新的完整流程

下面这张图展示了整个流程:

动态曲面更新流程 原始期权报价 买卖价差、成交量 曲面参数拟合 SVI / SSVI 模型 时间序列平滑 EWMA / 卡尔曼滤波 更新曲面数据库 反馈修正 平滑后的参数反馈到下一轮拟合,形成闭环 关键参数: • EWMA:λ 衰减因子(0.8~0.99) • 卡尔曼:Q 过程噪声 / R 观测噪声 / F 状态转移矩阵

避坑指南:我曾经在实盘环境中直接用卡尔曼滤波的预测值去交易,结果发现滤波器发散,曲面参数跑到了不合理范围。后来我加了一个「合理性检查」——每次更新后,检查参数是否在历史合理区间内,如果超出就回退到 EWMA 的平滑结果。

代码实战:卡尔曼滤波平滑曲面参数

下面是一个完整的示例,假设我们有 5 个曲面参数(比如 SVI 模型的 a, b, ρ, m, σ),用卡尔曼滤波做平滑:

import numpy as np
from filterpy.kalman import KalmanFilter

class SurfaceKalmanFilter:
    def __init__(self, n_params=5):
        self.kf = KalmanFilter(dim_x=n_params, dim_z=n_params)
        
        # 状态转移矩阵:假设参数随机游走
        self.kf.F = np.eye(n_params)
        
        # 观测矩阵:直接观测
        self.kf.H = np.eye(n_params)
        
        # 过程噪声:控制参数变化速度
        self.kf.Q = np.eye(n_params) * 0.01
        
        # 观测噪声:基于历史拟合误差
        self.kf.R = np.eye(n_params) * 0.05
        
        # 初始状态
        self.kf.x = np.zeros(n_params)
        self.kf.P = np.eye(n_params) * 100
    
    def update(self, observed_params):
        """
        输入:当前观测到的曲面参数
        输出:平滑后的参数
        """
        self.kf.predict()
        self.kf.update(observed_params)
        return self.kf.x.copy()
    
    def set_Q(self, Q_diag):
        """根据市场状态调整过程噪声"""
        self.kf.Q = np.diag(Q_diag)

# 使用示例
kf = SurfaceKalmanFilter()
obs = np.array([0.05, 0.12, -0.3, 0.02, 0.15])  # 假设的观测值
smoothed = kf.update(obs)
print(f"平滑后参数: {smoothed}")

调参建议:Q 矩阵的对角线元素控制每个参数的变化速度。近月参数(比如短期偏斜)的 Q 值可以设大一些,远月参数的 Q 值设小一些。我一般按期限的倒数来设置比例。

好了,这就是 EWMA 和卡尔曼滤波在动态曲面更新中的应用。两种方法各有千秋,关键是根据你的交易频率和数据质量来选。如果你刚开始做,我建议先从 EWMA 入手,等跑通了再升级到卡尔曼滤波。

记住一点:平滑不是目的,稳定且及时地反映市场才是。别为了平滑而平滑,把真实的市场信号也给抹掉了。


公众号:蓝海数据掘金营,微信 deep3321