4、价差计算与可视化:价差序列计算、使用Matplotlib绘制价差图、识别异常点

跨期套利的核心,说白了就是盯着两个合约的价差做文章。你想想看,如果价差偏离了正常范围,迟早会回归。那问题来了——怎么算价差?怎么看出它不正常?

这一节,我就带你手把手搞定价差计算、可视化,以及异常点的识别。嗯,都是实战中每天要用的东西。

4.1 价差序列的计算方法

价差计算本身不复杂,但细节决定成败。我个人习惯用近月合约价格减去远月合约价格,这样价差为正时,说明近月比远月贵,市场处于back结构;为负时,说明远月更贵,是contango结构。

我的习惯:统一用「近月 - 远月」,别换来换去。我在项目中遇到过团队里有人用「远月 - 近月」,结果信号全反了,亏了一波才反应过来。

具体计算步骤:

  1. 获取数据:从行情接口拉取两个合约的1分钟或5分钟收盘价
  2. 时间对齐:确保两个合约的时间戳完全匹配
  3. 计算价差spread = price_near - price_far
  4. 处理缺失值:遇到停牌或数据缺失,直接剔除或向前填充

来看一段代码,这是我常用的价差计算函数:

import pandas as pd
import numpy as np

def calc_spread(near_df, far_df):
    """
    计算跨期价差序列
    :param near_df: 近月合约DataFrame,包含['datetime', 'close']
    :param far_df: 远月合约DataFrame,包含['datetime', 'close']
    :return: 价差序列DataFrame
    """
    # 合并数据,按时间对齐
    merged = pd.merge(
        near_df[['datetime', 'close']],
        far_df[['datetime', 'close']],
        on='datetime',
        how='inner',
        suffixes=('_near', '_far')
    )
    
    # 计算价差
    merged['spread'] = merged['close_near'] - merged['close_far']
    
    # 去掉缺失值
    merged.dropna(inplace=True)
    
    return merged[['datetime', 'spread']]

这里有个坑——合约换月。每个月主力合约会切换,价差序列会跳变。我曾经因为这个没处理好,回测曲线漂亮得不行,实盘一跑就崩。后来我加了换月处理逻辑,才稳住。

避坑指南:换月时,价差会有一个「断层」。我建议在换月当天,把新旧合约的价差做归一化处理,或者干脆从换月点重新开始计算。

4.2 使用Matplotlib绘制价差图

光看数字不行,得画出来。一张好的价差图,能让你一眼看出趋势、波动和异常。

我个人偏爱用Matplotlib,虽然它有点老派,但胜在灵活。下面是我常用的绘图模板:

import matplotlib.pyplot as plt
import matplotlib.dates as mdates

def plot_spread(spread_df, title='跨期价差图', 
                mean_line=True, std_lines=True):
    """
    绘制价差序列图
    :param spread_df: 包含['datetime', 'spread']的DataFrame
    """
    fig, ax = plt.subplots(figsize=(12, 6))
    
    # 绘制价差曲线
    ax.plot(spread_df['datetime'], spread_df['spread'], 
            color='#2196F3', linewidth=1.5, label='价差')
    
    # 添加均值线
    if mean_line:
        mean_val = spread_df['spread'].mean()
        ax.axhline(y=mean_val, color='#FF5722', linestyle='--', 
                   linewidth=1, label=f'均值: {mean_val:.2f}')
    
    # 添加标准差线
    if std_lines:
        std_val = spread_df['spread'].std()
        for i in [1, 2, 3]:
            ax.axhline(y=mean_val + i*std_val, 
                       color='#9E9E9E', linestyle=':', 
                       linewidth=0.8, alpha=0.5)
            ax.axhline(y=mean_val - i*std_val, 
                       color='#9E9E9E', linestyle=':', 
                       linewidth=0.8, alpha=0.5)
    
    # 美化
    ax.set_title(title, fontsize=14, fontweight='bold')
    ax.set_xlabel('时间', fontsize=11)
    ax.set_ylabel('价差', fontsize=11)
    ax.legend(loc='best')
    ax.grid(True, alpha=0.3)
    
    # 时间轴格式化
    ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d'))
    ax.xaxis.set_major_locator(mdates.WeekdayLocator())
    
    plt.tight_layout()
    plt.show()

这段代码画出来的图,包含了价差曲线、均值线和1/2/3倍标准差线。你想想看,价差跑到3倍标准差外面,基本就是异常了。

小技巧:我习惯把均值线用红色虚线标出来,标准差线用灰色点线。这样一眼就能看出价差在什么位置。颜色别太花哨,红蓝灰三色就够了。

4.3 识别异常点

异常点,就是价差偏离正常范围太远的地方。怎么定义「太远」?我一般用两种方法:

方法 原理 适用场景
标准差法 价差超过均值±2倍或3倍标准差 价差分布接近正态分布时
百分位法 价差超过历史数据的1%或99%分位数 价差分布偏态严重时

我个人更常用标准差法,简单粗暴。但要注意——金融数据往往有厚尾,3倍标准差之外的事件比正态分布假设下更常见。所以我会把阈值设到2.5倍标准差,既不过于敏感,也不过于迟钝。

来看异常点检测的代码:

def detect_outliers(spread_df, method='std', threshold=2.5):
    """
    检测价差异常点
    :param spread_df: 价差序列DataFrame
    :param method: 'std' 或 'percentile'
    :param threshold: 标准差倍数或百分位阈值
    :return: 异常点DataFrame
    """
    spread = spread_df['spread'].values
    
    if method == 'std':
        mean_val = np.mean(spread)
        std_val = np.std(spread)
        upper = mean_val + threshold * std_val
        lower = mean_val - threshold * std_val
    elif method == 'percentile':
        upper = np.percentile(spread, 100 - threshold)
        lower = np.percentile(spread, threshold)
    
    # 标记异常点
    outliers = spread_df[
        (spread_df['spread'] > upper) | 
        (spread_df['spread'] < lower)
    ].copy()
    
    outliers['label'] = '异常'
    
    return outliers, upper, lower

检测出异常点后,我会在图上用红圈标出来:

def plot_with_outliers(spread_df, outliers, upper, lower):
    """在价差图上标注异常点"""
    fig, ax = plt.subplots(figsize=(12, 6))
    
    # 绘制价差曲线
    ax.plot(spread_df['datetime'], spread_df['spread'], 
            color='#2196F3', linewidth=1, alpha=0.7)
    
    # 标注异常点
    ax.scatter(outliers['datetime'], outliers['spread'], 
               color='#F44336', s=50, zorder=5, 
               label=f'异常点 ({len(outliers)}个)')
    
    # 阈值线
    ax.axhline(y=upper, color='#FF9800', linestyle='--', 
               linewidth=1, label=f'上阈值: {upper:.2f}')
    ax.axhline(y=lower, color='#FF9800', linestyle='--', 
               linewidth=1, label=f'下阈值: {lower:.2f}')
    
    ax.set_title('价差异常点识别', fontsize=14, fontweight='bold')
    ax.legend(loc='best')
    ax.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.show()

为什么异常点重要?因为跨期套利的入场信号,往往就来自这些异常点。价差跑到阈值外面,意味着市场情绪过度反应,接下来大概率会回归。

注意:不是所有异常点都会回归。我遇到过几次,价差突破阈值后继续扩大,最后被迫止损。所以异常点只是「候选信号」,还需要结合成交量、持仓量等指标做二次确认。

4.4 本章知识体系

下面这张图,把价差计算与可视化的核心逻辑串起来了:

价差计算与可视化核心流程 数据输入 近月合约 + 远月合约 收盘价 价差计算 时间对齐 → 价差 = 近月 - 远月 → 缺失值处理 注意:合约换月时的断层处理 可视化 Matplotlib绘制价差曲线 均值线 + 标准差线 + 时间轴 异常点检测 标准差法 / 百分位法 阈值:2.5倍标准差或1%/99%分位 输出:价差序列 + 异常点标记 + 入场候选信号

从数据输入到价差计算,再到可视化和异常检测,最后输出候选信号——这就是一套完整的价差分析流水线。你在实战中,可以把这个流程封装成一个类,每天跑一遍,自动标记异常点。

好了,价差计算与可视化就讲到这里。记住一句话:图比数字直观,异常比常态重要。把图画好,把异常点抓准,你的套利系统就成功了一半。


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