第三章:数据源对接与统一接口封装

做量化交易,数据就是你的弹药库。弹药不行,枪法再好也白搭。

我见过太多人,策略逻辑写得漂漂亮亮,一上实盘就崩。为什么?数据源换了,接口不统一,字段名对不上。今天我们就来解决这个最基础也最要命的问题。

3.1 常见数据源:各有各的脾气

国内量化圈,主流数据源就那么几个。我用过的有Tushare、Wind、聚宽、米筐。每个我都踩过坑,今天一并告诉你。

Tushare

免费开源,社区活跃。适合个人研究。但有个问题——它的数据格式经常小改。我记得有一次升级,字段名从trade_date改成了cal_date,我整个回测框架全崩。嗯,从那以后我加了一层适配器。

Wind

机构标配,数据全,质量高。但贵,而且接口是C++风格的。我个人习惯用Python调用WindPy,但要注意——Wind的Tick数据有时会丢包,尤其是开盘那几分钟。

聚宽 & 米筐

这两家都是云端平台,数据封装得比较好。但如果你要本地跑策略,就得自己拉数据。聚宽的API返回的是DataFrame,米筐的返回的是dict。你想想看,同一个策略,换个数据源就得改一堆代码,多烦人。

数据源 优势 坑点
Tushare 免费、社区好 字段名不稳定
Wind 数据全、质量高 贵、Tick丢包
聚宽 封装好、易用 本地化麻烦
米筐 数据规范 格式不统一

3.2 统一数据接口设计

说白了,我们要做一个「数据中间人」。不管背后是哪个数据源,对外暴露的接口都一样。

Bar数据接口

Bar就是K线。日线、小时线、分钟线。我设计的接口长这样:

class BarData:
    def __init__(self, symbol, open, high, low, close, volume, timestamp):
        self.symbol = symbol
        self.open = open
        self.high = high
        self.low = low
        self.close = close
        self.volume = volume
        self.timestamp = timestamp

    def to_dict(self):
        return {
            'symbol': self.symbol,
            'open': self.open,
            'high': self.high,
            'low': self.low,
            'close': self.close,
            'volume': self.volume,
            'timestamp': self.timestamp
        }

为什么要统一成类?因为你可以加方法。比如to_dict(),方便序列化。比如validate(),做数据校验。

Tick数据接口

Tick数据更细,每一笔成交都记录。我遇到过最头疼的事——不同数据源的Tick时间戳格式不一样。有的用毫秒,有的用微秒,有的直接用字符串。

class TickData:
    def __init__(self, symbol, price, volume, timestamp, bid_price=None, ask_price=None):
        self.symbol = symbol
        self.price = price
        self.volume = volume
        self.timestamp = timestamp  # 统一为毫秒时间戳
        self.bid_price = bid_price
        self.ask_price = ask_price

    def is_valid(self):
        # 我曾经遇到price为0的Tick,直接导致策略下单
        return self.price > 0 and self.volume > 0
注意:时间戳一定要统一。我建议全部转成毫秒时间戳(int类型)。字符串时间戳在排序和比较时容易出问题。

3.3 数据缓存与本地化存储

每次跑策略都去拉数据?太慢了。而且万一网络断了呢?

我的做法是:第一次拉取后,存到本地。下次优先读本地,本地没有再去拉。

存储格式选择

  • CSV:简单,但慢。适合小数据量。
  • Parquet:快,压缩率高。我推荐这个。
  • SQLite:适合需要查询的场景。

我个人习惯用Parquet。为什么?因为回测时经常要读大量历史数据,Parquet的列式存储能快10倍以上。

import pandas as pd
import os

class DataCache:
    def __init__(self, cache_dir='./data_cache'):
        self.cache_dir = cache_dir
        os.makedirs(cache_dir, exist_ok=True)

    def get(self, symbol, start_date, end_date):
        file_path = f"{self.cache_dir}/{symbol}_{start_date}_{end_date}.parquet"
        if os.path.exists(file_path):
            return pd.read_parquet(file_path)
        return None

    def save(self, symbol, start_date, end_date, data):
        file_path = f"{self.cache_dir}/{symbol}_{start_date}_{end_date}.parquet"
        data.to_parquet(file_path)
小技巧:缓存文件名里带上日期范围。这样下次查询时,可以快速判断缓存是否命中。

3.4 数据质量校验与清洗

数据质量是量化交易的命门。我见过最离谱的事——某数据源某天的收盘价全是0,策略直接满仓买入。幸好是仿真交易,不然亏大了。

校验规则

  1. 空值检查:有没有NaN?有没有0?
  2. 单调性检查:时间戳是不是递增的?
  3. 范围检查:价格是不是在合理范围内?比如A股不可能有10000元的收盘价。
  4. 跳空检查:相邻两根K线的价格差是不是太大?
def validate_bar_data(df):
    errors = []
    # 空值检查
    if df.isnull().any().any():
        errors.append("存在空值")
    # 时间戳单调性
    if not df['timestamp'].is_monotonic_increasing:
        errors.append("时间戳不单调")
    # 价格范围
    if (df['close'] < 0).any():
        errors.append("收盘价小于0")
    # 跳空检查(超过10%视为异常)
    pct_change = df['close'].pct_change()
    if (pct_change.abs() > 0.1).any():
        errors.append("存在超过10%的跳空")
    return errors

清洗策略

发现问题后怎么办?我的原则是:

  • 能修复的修复(比如时间戳排序)
  • 不能修复的丢弃(比如价格异常)
  • 丢弃比例超过5%的,整个数据源标记为不可用

核心原则:宁可少数据,不要坏数据。坏数据会让你的策略「学习」到错误模式,实盘时死得很难看。

3.5 整体架构图

下面这张图,是我在实际项目中总结出来的数据层架构。你看一眼就明白了。

统一数据接口架构图 数据源层 Tushare Wind 聚宽 米筐 适配器层(数据格式转换) 统一字段名、统一时间戳、统一数据类型 统一接口层 BarData / TickData 统一对象 缓存与校验层 本地缓存(Parquet) 数据校验与清洗 空值检查 | 单调性 | 范围检查 | 跳空检查

你看,从数据源到策略层,中间隔了三层。每一层解决一个问题。这样设计的好处是——换数据源时,只需要改适配器层,上层代码完全不用动。

我曾经帮一个团队重构数据层,他们原来每个策略都直接调Tushare。后来要切Wind,改了整整两周。用了我这套架构后,切数据源只需要一天。

嗯,今天就讲到这里。数据源对接这件事,看似简单,但细节决定成败。下一章我们聊聊策略回测框架的搭建,到时候你会感谢今天做的这些基础工作。


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