数据模块:历史行情数据获取、数据清洗与对齐、OHLCV数据结构设计、数据存储与读取

做市商策略回测,说白了就是拿历史数据模拟交易。数据要是出了问题,后面全白搭。我见过太多人花大把时间调策略参数,结果最后发现是数据没对齐——那种感觉,就像你跑完马拉松才发现鞋里有个石子。

这一章,咱们就把数据模块彻底讲透。从怎么拿到数据,到怎么洗干净,再到怎么存、怎么读,一条龙搞定。

历史行情数据获取:从哪里来,怎么来

数据源的选择,直接决定了你回测的可靠性。我个人习惯把数据源分成三类:

  • 交易所官方API:比如Binance、OKX、Coinbase。数据最权威,但有限流,历史数据拉取慢。
  • 第三方数据平台:比如Kaiko、CoinGecko、CryptoDataDownload。数据全,但收费。
  • 本地CSV/Parquet文件:自己攒的数据仓库。最灵活,但需要维护。

我建议初学者先从交易所API入手。为什么?因为免费,而且能让你理解数据最原始的形态。下面是我常用的一个数据获取函数:

import ccxt
import pandas as pd
from datetime import datetime, timedelta

def fetch_ohlcv(symbol='BTC/USDT', timeframe='1m', limit=1000):
    """
    从交易所获取OHLCV数据
    """
    exchange = ccxt.binance({
        'rateLimit': 1200,
        'enableRateLimit': True,
    })
    
    since = exchange.parse8601('2024-01-01T00:00:00Z')
    all_candles = []
    
    while True:
        candles = exchange.fetch_ohlcv(
            symbol, 
            timeframe=timeframe,
            since=since,
            limit=limit
        )
        
        if len(candles) == 0:
            break
            
        all_candles += candles
        since = candles[-1][0] + 1  # 避免重复
        
        # 控制请求频率
        time.sleep(exchange.rateLimit / 1000)
    
    df = pd.DataFrame(
        all_candles, 
        columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']
    )
    df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
    df.set_index('timestamp', inplace=True)
    
    return df

这里有个坑:交易所API返回的时间戳是毫秒级的Unix时间戳。我刚开始做的时候直接当整数用了,结果时间轴全乱套。嗯,记得转成datetime。

数据清洗与对齐:脏数据是回测的隐形杀手

拿到原始数据后,千万别直接扔进回测引擎。我曾在项目中遇到过这样的情况:某交易所某天的数据突然跳了一个大缺口,导致策略在那天开仓量暴增,回测结果直接翻倍。当时我还以为自己发现了圣杯,后来才发现是数据问题。

数据清洗,我一般做这几件事:

  1. 去重:同一个时间戳出现多条记录,保留最后一条。
  2. 补缺失:某些时间点没有数据,用前值填充或插值。
  3. 去异常值:价格突然跳变超过阈值,标记出来人工判断。
  4. 时间对齐:不同数据源的时间戳可能差几毫秒,统一对齐到整点。

核心原则:宁可少数据,不要错数据。回测中一个错误的价格点,可能让你误以为策略有效。

下面是我常用的清洗函数:

def clean_ohlcv(df):
    """
    清洗OHLCV数据
    """
    # 1. 去重:保留最后一条
    df = df[~df.index.duplicated(keep='last')]
    
    # 2. 排序
    df.sort_index(inplace=True)
    
    # 3. 补缺失:前向填充
    full_index = pd.date_range(
        start=df.index.min(), 
        end=df.index.max(), 
        freq='1min'
    )
    df = df.reindex(full_index, method='ffill')
    
    # 4. 去异常:价格变化超过20%标记
    price_change = df['close'].pct_change().abs()
    outliers = price_change > 0.2
    if outliers.any():
        print(f"发现 {outliers.sum()} 个异常点,已标记")
        df.loc[outliers, 'is_outlier'] = True
    
    return df

注意:前向填充(ffill)在长时间停盘时会有问题。比如周末没有交易,你用前一个周五的数据填充整个周末,回测时策略可能会在周末「交易」——这显然不对。我建议对停盘时段做特殊标记,或者直接删除。

OHLCV数据结构设计:别小看这六列数据

OHLCV,就是Open、High、Low、Close、Volume。看起来简单,但设计得好不好,直接影响后续所有模块的代码质量。

我个人习惯用Pandas DataFrame作为核心数据结构。为什么?因为Pandas的时间序列操作太方便了,滚动计算、重采样、合并,一行代码搞定。

下面是我推荐的数据结构:

字段 类型 说明
timestamp datetime64[ns] 时间戳,作为索引
open float64 开盘价
high float64 最高价
low float64 最低价
close float64 收盘价
volume float64 成交量
symbol str 交易对(可选)
is_outlier bool 异常标记(可选)

你想想看,为什么要把timestamp设成索引?因为回测时你经常需要按时间切片,比如「只回测2024年1月的数据」。用索引切片,效率高、代码也干净。

小技巧:如果你同时回测多个交易对,建议用MultiIndex,第一层是symbol,第二层是timestamp。这样合并数据、按品种筛选都非常方便。

数据存储与读取:别每次都重新拉数据

从交易所拉数据很慢,而且有限流。我建议把清洗好的数据存到本地,下次直接读文件。

存储格式我试过好几种:

  • CSV:通用,但慢,文件大。
  • Parquet:快,压缩率高,推荐。
  • HDF5:适合大规模数据,但依赖多。
  • SQLite:适合需要查询的场景。

我个人最常用Parquet。为什么?因为读写速度比CSV快10倍以上,而且文件大小只有CSV的1/3。下面是我的存储和读取代码:

def save_ohlcv(df, filepath):
    """
    存储OHLCV数据到Parquet文件
    """
    df.to_parquet(filepath, compression='snappy')
    print(f"数据已保存到 {filepath}")

def load_ohlcv(filepath):
    """
    从Parquet文件读取OHLCV数据
    """
    df = pd.read_parquet(filepath)
    return df

这里有个细节:存储时保留索引。Parquet默认会保存索引,但如果你用CSV,记得设置index=True,否则时间戳就丢了。

我的工作流:每天定时拉取最新数据 → 清洗 → 追加到本地Parquet文件。回测时直接读文件,几秒钟就能加载一年的分钟级数据。

知识体系总览

下面这张图,把数据模块的核心逻辑串起来了。你一看就明白:

数据模块核心流程 数据获取 交易所API / 第三方 数据清洗 去重 / 补缺失 / 去异常 数据对齐 时间戳统一 / 频率对齐 OHLCV数据结构 DataFrame / MultiIndex 数据存储 Parquet / CSV 回测引擎 策略执行 / 盈亏计算 数据输入 读取数据

这张图展示了数据从获取到最终被回测引擎使用的完整链路。你注意看,数据清洗和对齐是承上启下的关键环节——数据质量不过关,后面全是白费功夫。

好了,数据模块的核心内容就这些。记住一句话:数据是回测的基石,花再多时间打磨都不为过。我曾经因为数据没对齐,白白浪费了两周调参数,后来发现是数据源的时间戳差了1秒。嗯,从那以后,我每次回测前都会先跑一遍数据校验。


专注资料整理