3、核心数据结构设计:定义价差合约对象、设计K线数据结构、设计价差数据结构、数据存储方案(CSV/HDF5)

好,咱们进入正题。数据结构这东西,说白了就是程序的骨架。骨架搭歪了,后面写再多代码也是白搭。我在做第一个价差分析工具时,就吃过这个亏——一开始图省事,用了一堆散乱的字典和列表,结果后来加功能时改得想哭。所以这一章,咱们把地基打牢。

3.1 定义价差合约对象

先聊聊价差合约。你想想看,一个价差合约本质上是什么?就是两个(或多个)期货合约的配对。比如做豆油和棕榈油的价差,你得知道它们各自的合约代码、交易所、乘数、最小变动价位等等。

我个人习惯用一个类来封装这些信息。这样后面用起来顺手,也方便扩展。

class SpreadContract:
    """价差合约对象"""
    def __init__(self, leg1_code, leg2_code, leg1_multiplier=1.0, leg2_multiplier=1.0):
        self.leg1_code = leg1_code      # 腿1合约代码,如 'Y2401'
        self.leg2_code = leg2_code      # 腿2合约代码,如 'P2401'
        self.leg1_multiplier = leg1_multiplier  # 腿1乘数
        self.leg2_multiplier = leg2_multiplier  # 腿2乘数
        self.exchange = None            # 交易所
        self.tick_size = None           # 最小变动价位
        self.name = f"{leg1_code}-{leg2_code}"  # 价差名称

    def __repr__(self):
        return f"SpreadContract({self.name})"

这里有个细节:乘数。为什么要有乘数?因为不同合约的合约单位可能不一样。比如你做螺纹钢和热卷的价差,它们都是10吨/手,乘数就是1:1。但如果你做股指期货和ETF的价差,乘数就得仔细算了。我在项目中遇到过这种情况,当时没设乘数字段,后来硬着头皮改代码,麻烦得很。

小技巧: 建议把乘数默认设为1.0,这样大多数情况下不用额外配置。遇到特殊品种再单独设置。

3.2 设计K线数据结构

K线数据是价差计算的基础。说白了,每条K线就是一根蜡烛,包含开盘、最高、最低、收盘、成交量这些信息。但咱们做价差分析,还需要额外记录一些东西。

我推荐用 namedtuple 或者 dataclass 来定义。Python 3.7+ 的话,直接用 dataclass 最省事。

from dataclasses import dataclass
from datetime import datetime

@dataclass
class Bar:
    """单条K线数据"""
    datetime: datetime      # 时间戳
    open: float             # 开盘价
    high: float             # 最高价
    low: float              # 最低价
    close: float            # 收盘价
    volume: float           # 成交量
    open_interest: float    # 持仓量(可选)
    symbol: str = ''        # 合约代码

嗯,这里要注意:持仓量对于价差分析其实挺重要的。为什么?因为价差策略往往需要判断市场情绪,持仓量的变化能反映资金进出。我刚开始做的时候忽略了这字段,后来发现有些策略信号需要持仓量配合验证,又回头补上。

3.3 设计价差数据结构

价差数据怎么存?这得看你怎么算价差。最简单的就是直接相减:价差 = 腿1价格 - 腿2价格。但实际中还有比例价差、对数价差等等。

我个人习惯把价差数据设计成两个部分:

  1. 原始价差序列:包含时间戳和计算后的价差值
  2. 统计特征:均值、标准差、分位数等,方便后续分析
@dataclass
class SpreadData:
    """价差数据结构"""
    datetime: datetime          # 时间戳
    spread_value: float         # 价差值
    leg1_price: float           # 腿1价格
    leg2_price: float           # 腿2价格
    z_score: float = 0.0        # Z-score(可选)
    signal: int = 0             # 交易信号(可选,1做多,-1做空,0观望)

    def __post_init__(self):
        """初始化后自动计算一些衍生字段"""
        self.spread_change = 0.0  # 价差变动量,后续可填充

你看,我加了个 z_score 字段。这玩意儿在统计套利里很常用,表示当前价差偏离均值多少个标准差。说白了,就是帮你判断当前价差是不是「异常」了。

核心思路: 价差数据不仅仅是两个价格的差,它应该包含足够的信息,让你能直接做策略回测。所以我把信号字段也塞进去了。

3.4 数据存储方案:CSV vs HDF5

数据存哪?怎么存?这是每个做量化的人都要面对的问题。我两种都用过,说说我的感受。

3.4.1 CSV方案

CSV 的好处是简单、通用。任何工具都能打开,Excel 也能直接看。适合小规模数据、快速原型开发。

import pandas as pd

# 保存为CSV
df.to_csv('spread_data.csv', index=False)

# 读取CSV
df = pd.read_csv('spread_data.csv', parse_dates=['datetime'])

但 CSV 有个硬伤:读写慢,而且不支持数据类型。你存个浮点数进去,读出来可能变成字符串,还得自己转。我曾经有个项目,每天生成几百万条 tick 级价差数据,用 CSV 存,结果回测时加载数据花了半小时……

避坑指南: 如果你做高频价差分析,千万别用CSV。我曾经因为偷懒用CSV存分钟级数据,结果回测时IO成了瓶颈,白白浪费两天优化代码。

3.4.2 HDF5方案

HDF5 是二进制格式,读写快,支持压缩,还能存储复杂的层次结构。对于价差分析这种需要频繁读写历史数据的场景,HDF5 是更好的选择。

# 保存为HDF5
df.to_hdf('spread_data.h5', key='spread', mode='w', complevel=5)

# 读取HDF5
df = pd.read_hdf('spread_data.h5', key='spread')

# 增量追加
df_new.to_hdf('spread_data.h5', key='spread', mode='a', append=True)

你看,HDF5 支持 append 模式,每天的新数据可以直接追加进去,不用重写整个文件。这个特性在实盘场景下特别有用。

特性 CSV HDF5
读写速度 慢(文本解析) 快(二进制)
文件大小 较大 较小(支持压缩)
跨平台性 极好
数据类型支持 弱(全是字符串) 强(原生类型)
适合场景 小数据量、调试 大数据量、生产环境
我的建议: 开发阶段用CSV,方便肉眼检查数据。上线后切到HDF5,性能提升明显。两个格式之间转换也很简单,一行代码的事。

3.5 整体数据流架构

说了这么多,咱们把整个数据流串起来看看。下面这张图展示了从原始行情到价差数据的完整链路。

价差分析数据流架构 行情数据源 CTP / 交易所API 历史数据文件 CSV / HDF5 数据库 MySQL / MongoDB 数据解析层 Bar对象构建 · 数据清洗 · 缺失值处理 · 时间对齐 价差计算引擎 SpreadContract · SpreadData · 价差公式 · Z-score计算 存储输出层 CSV导出 · HDF5存储 · 数据库写入 · 可视化

从这张图你能看到,数据从多个源头进来,经过解析、计算,最后落到存储层。每个环节的数据结构我们都定义好了,后面写代码就是填空的事。

好了,这一章的内容就到这。数据结构这块虽然看起来枯燥,但确实是整个工具的基石。你花半小时把数据结构设计好,后面能省下好几个通宵的调试时间。我自己就是这么过来的,深有体会。


专注资料整理