第二十讲:高频数据下的曲面构建——微观结构噪声处理与快速拟合算法

说实话,高频数据做波动率曲面,是我个人觉得整个课程里最“刺激”的一章。

为什么?因为低频数据你还能慢慢调参数,高频数据根本不给机会。每秒几百笔交易进来,你还没算完,市场已经变了。而且,高频数据里全是“脏东西”——微观结构噪声、买卖价差、报价跳跃……这些玩意儿如果不处理,你拟合出来的曲面就是个笑话。

20.1 微观结构噪声:你看到的不一定是真的

先说说噪声。高频数据里,价格并不是“真实价格”。

举个例子。你看到一笔成交价是100.01,另一笔是99.99。这能说明价格波动了2分钱吗?不一定。可能只是买卖价差导致的来回跳。这种噪声,我们叫它“微观结构噪声”。

核心问题: 高频数据中的价格变动,有一部分是真实的波动,有一部分是市场机制造成的“假动作”。

我在做实盘高频策略时,就吃过这个亏。当时直接用原始tick数据算波动率,结果曲面形状完全不对,IV曲面像锯齿一样。后来才发现,是买卖价差导致的伪波动。

20.1.1 噪声的来源

  • 买卖价差反弹(Bid-Ask Bounce): 价格在买一和卖一之间来回跳,产生虚假波动
  • 离散报价: 价格只能以最小变动单位变化(比如0.01),导致价格路径不连续
  • 订单流不平衡: 大单冲击造成的短暂价格偏离
  • 数据延迟与异步: 不同股票/期权的报价时间戳不一致

20.1.2 噪声的数学刻画

我们通常假设观测价格 = 真实价格 + 噪声。即:

P_obs(t) = P_true(t) + ε(t)

其中 ε(t) 是微观结构噪声,通常假设它是均值为0的序列相关过程。

为什么序列相关很重要?因为如果噪声是白噪声,那还好处理。但实际中,噪声往往有负的自相关——你想想看,价格从100跳到100.01,下一笔很可能又跳回100。这种负相关会严重扭曲波动率估计。

我的经验: 判断噪声严重程度的一个简单方法——计算价格序列的一阶自相关系数。如果显著为负(比如小于-0.3),那你的数据噪声很大,必须处理。

20.2 噪声处理方法:从简单到实用

处理噪声的方法很多,我挑几个真正实用的讲。

20.2.1 预平均法(Pre-averaging)

这个方法说白了就是:别用单笔价格,用一小段时间内的平均价格。

def pre_average(prices, window=5):
    """
    预平均处理:用滑动窗口平均替代原始价格
    """
    import numpy as np
    weights = np.ones(window) / window
    avg_prices = np.convolve(prices, weights, mode='valid')
    return avg_prices

窗口大小怎么选?我建议用5-10笔。太小了去不掉噪声,太大了会抹掉真实波动。

20.2.2 两时间尺度法(Two-Scale Realized Volatility)

这是学术界比较推崇的方法。核心思想:用高频数据算一个“粗糙”的波动率,再用低频数据算一个“平滑”的波动率,然后做差来估计噪声方差。

def two_scale_rv(prices, k1=1, k2=10):
    """
    两时间尺度已实现波动率
    k1: 高频采样间隔(1表示用所有数据)
    k2: 低频采样间隔(比如每10笔取一笔)
    """
    n = len(prices)
    # 高频RV
    rv_fast = np.sum(np.diff(prices[::k1])**2)
    # 低频RV
    rv_slow = np.sum(np.diff(prices[::k2])**2)
    # 调整项
    adj = (n - k1 + 1) / (n - k2 + 1) * rv_slow
    # 最终估计
    rv_ts = rv_fast - adj
    return max(rv_ts, 0)  # 不能为负
注意: 两时间尺度法算出来的RV可能为负(当噪声很大时)。我一般会加一个max(0)保护,或者改用更稳健的核方法。

20.2.3 核方法(Kernel-Based Estimator)

这个方法更优雅。它给不同时间间隔的收益赋予不同权重——间隔越近,权重越低(因为噪声相关性高)。

def kernel_rv(prices, kernel='bartlett', bandwidth=5):
    """
    核方法已实现波动率
    kernel: 'bartlett' 或 'epanechnikov'
    """
    returns = np.diff(prices)
    n = len(returns)
    # 计算所有滞后的自协方差
    gamma = np.zeros(bandwidth)
    for h in range(bandwidth):
        gamma[h] = np.sum(returns[h:] * returns[:n-h]) / n
    
    # 核权重
    if kernel == 'bartlett':
        weights = 1 - np.arange(bandwidth) / bandwidth
    elif kernel == 'epanechnikov':
        weights = 1 - (np.arange(bandwidth) / bandwidth)**2
    
    # 加权求和
    rv_kernel = gamma[0] + 2 * np.sum(weights[1:] * gamma[1:])
    return rv_kernel

我个人习惯用Bartlett核,简单稳定。带宽选多少?一般选5-15,具体要看你的数据频率。

20.3 快速拟合算法:时间就是金钱

噪声处理完了,接下来要快速拟合曲面。高频环境下,你不可能用SVI慢慢优化——那太慢了。

20.3.1 为什么不能用传统方法?

传统方法(比如SVI参数优化)需要迭代求解,一次拟合可能要几百毫秒。高频数据每秒来几百个新报价,你根本来不及。

我见过有人用SVI拟合高频数据,结果曲面更新速度比市场慢了好几秒——等曲面画出来,行情已经变了。这就像开车看后视镜,看到的都是过去。

20.3.2 快速拟合的核心思路

说白了就一句话:用解析解代替数值解,用递推代替全量计算。

具体来说,有两条路:

  1. 参数化简化: 用更简单的曲面形式(比如二次曲面),牺牲一点精度换速度
  2. 递推更新: 每次新数据来了,只更新局部参数,不重新拟合全局

20.3.3 递推最小二乘法(RLS)

这是我最喜欢的方法。假设你的曲面模型是线性的(或者可以线性化),那么每次新数据来了,只需要O(n²)的计算量就能更新参数,而不是O(n³)。

class RLS_Surface:
    """
    递推最小二乘法拟合波动率曲面
    假设模型: IV = a0 + a1*K + a2*K^2 + a3*T + a4*T^2 + a5*K*T
    """
    def __init__(self, n_features=6, lambda_=0.99):
        self.n = n_features
        self.lambda_ = lambda_  # 遗忘因子
        self.P = np.eye(n_features) * 100  # 初始协方差矩阵
        self.theta = np.zeros(n_features)  # 参数向量
    
    def update(self, K, T, IV):
        """在线更新:来一个新数据点就更新一次"""
        # 构造特征向量
        x = np.array([1, K, K**2, T, T**2, K*T])
        # 计算增益
        g = self.P @ x / (self.lambda_ + x @ self.P @ x)
        # 更新参数
        self.theta += g * (IV - x @ self.theta)
        # 更新协方差
        self.P = (self.P - np.outer(g, x @ self.P)) / self.lambda_
        return self.theta
    
    def predict(self, K, T):
        """预测任意点上的IV"""
        x = np.array([1, K, K**2, T, T**2, K*T])
        return x @ self.theta
避坑指南: 遗忘因子lambda_很关键。太接近1(比如0.999),模型更新太慢,跟不上市场变化。太小(比如0.9),模型太敏感,容易被噪声带偏。我一般设0.95-0.98,具体要看数据频率。

20.3.4 局部加权回归(LOWESS)

另一种思路:不拟合全局曲面,而是对每个需要预测的点,只取它附近的期权数据做局部拟合。

def local_surface_fit(K_target, T_target, data, bandwidth=0.1):
    """
    局部加权回归拟合曲面
    data: 包含K, T, IV的DataFrame
    """
    # 计算每个数据点的权重(距离越近权重越大)
    distances = np.sqrt((data['K'] - K_target)**2 + 
                        (data['T'] - T_target)**2)
    weights = np.exp(-distances**2 / (2 * bandwidth**2))
    
    # 局部线性回归
    X = np.column_stack([np.ones(len(data)), 
                         data['K'], data['T'],
                         data['K']**2, data['T']**2,
                         data['K']*data['T']])
    W = np.diag(weights)
    y = data['IV'].values
    
    # 加权最小二乘
    theta = np.linalg.inv(X.T @ W @ X) @ (X.T @ W @ y)
    
    # 预测
    x_target = np.array([1, K_target, T_target, 
                         K_target**2, T_target**2, 
                         K_target*T_target])
    return x_target @ theta

这个方法的好处是:你不需要维护一个全局模型,每次预测都是“就地取材”。缺点是计算量稍大,但如果你只预测几个关键点(比如ATM附近的点),速度还是很快的。

20.4 实战中的取舍

讲到这里,你可能想问:到底用哪种方法?

我的建议是分场景:

场景 推荐方法 理由
做市商高频报价 RLS递推 + 预平均去噪 速度最快,能跟上tick级别更新
盘中波动率监控 核方法RV + LOWESS曲面 精度更高,适合分钟级更新
盘后分析/回测 两时间尺度法 + SVI 精度最高,不要求实时性

嗯,这里要注意一点:没有万能的方法。我在实盘中,白天用RLS做快速报价,收盘后用核方法做精细分析。工具是死的,人是活的。

20.5 本章知识体系

下面这张图总结了高频数据下曲面构建的完整流程:

高频数据下波动率曲面构建流程 原始Tick数据 微观结构噪声处理 预平均法 | 两时间尺度法 | 核方法 已实现波动率 / 隐含波动率计算 去噪后的RV | 期权IV插值 快速曲面拟合算法 递推最小二乘(RLS) | 局部加权回归(LOWESS) 参数化简化模型 | 在线递推更新 实时波动率曲面 关键挑战 • 买卖价差噪声 • 离散报价 • 数据异步 • 计算速度要求 核心原则 • 先降噪再拟合 • 用递推代替全量 • 精度与速度平衡 适用场景 • 做市:RLS • 监控:LOWESS • 回测:SVI

这张图把整个流程串起来了。从原始tick数据开始,先做噪声处理,再算波动率,然后用快速算法拟合曲面。每一步都有多种方法可选,关键是根据你的场景做取舍。

好了,这一讲的内容就到这里。高频数据下的曲面构建,说白了就是跟噪声赛跑、跟时间赛跑。方法学会了,剩下的就是多练、多踩坑——嗯,我自己也是踩了无数坑才总结出这些经验的。

公众号:蓝海资料掘金营,微信deep3321