4. 数据获取与清洗:交易所API对接、历史数据下载、数据清洗与对齐、缺失值处理

做市策略回测,说白了就是拿历史数据模拟交易。数据不对,回测就是白做。我见过太多人花几个月写策略,最后发现数据里有坑,全白费了。今天咱们就把数据这块彻底讲透。

4.1 交易所API对接:从零开始拿数据

做市策略需要的数据,跟普通交易不太一样。普通交易看K线就够了,做市策略需要的是逐笔成交数据深度快照。为什么?因为做市赚的是买卖价差,你得知道每一笔订单是怎么成交的。

我个人习惯用 ccxt 这个库,它封装了上百家交易所的API。你想想看,如果每家交易所都单独写一套对接代码,那得累死。

核心要点:做市策略回测至少需要三种数据:

  • 深度快照(Order Book Snapshot):每秒或每笔变化时的买卖盘口
  • 逐笔成交(Trade/Tick):每一笔成交的价格、数量、方向
  • K线数据(OHLCV):用于辅助分析,但不是核心

来看一段实际代码。我用的是币安的API,因为它的数据质量最好,而且免费额度够用。

import ccxt
import pandas as pd
from datetime import datetime

# 初始化交易所
exchange = ccxt.binance({
    'rateLimit': 1200,  # 币安的限制是1200次/分钟
    'enableRateLimit': True,  # 自动限速,别把自己封了
})

# 获取深度数据
def fetch_orderbook(symbol='BTC/USDT', limit=100):
    """获取订单簿深度"""
    try:
        ob = exchange.fetch_order_book(symbol, limit=limit)
        return {
            'bids': ob['bids'][:10],  # 取前10档买单
            'asks': ob['asks'][:10],  # 取前10档卖单
            'timestamp': ob['timestamp']
        }
    except Exception as e:
        print(f'获取深度失败: {e}')
        return None

# 获取历史逐笔成交
def fetch_trades(symbol='BTC/USDT', since=None, limit=1000):
    """获取历史成交记录"""
    trades = exchange.fetch_trades(symbol, since=since, limit=limit)
    df = pd.DataFrame(trades)
    df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
    return df

避坑指南:我曾经在对接API时犯过一个低级错误——没处理限速。结果程序跑了10分钟就被交易所封了IP。后来我加了个简单的限速器,问题就解决了。记住:永远不要用单线程高频请求,交易所会把你当攻击者。

4.2 历史数据下载:别只盯着最近一个月

做市策略回测,至少需要3个月以上的数据。为什么?因为市场环境会变。牛市的做市策略在熊市可能亏得裤衩都不剩。

我建议你下载以下时间段的数据:

数据用途 推荐时间跨度 数据频率
策略开发 最近3个月 逐笔成交 + 深度快照
参数优化 最近6个月 逐笔成交
稳健性测试 最近12个月 K线 + 深度快照
极端行情测试 包含312、519等事件 全部数据

下载数据时,要注意时间对齐。不同交易所的时间戳格式不一样,有的用毫秒,有的用微秒。我习惯统一转成毫秒时间戳,方便后续处理。

def download_historical_data(symbol, start_date, end_date):
    """批量下载历史数据"""
    all_trades = []
    since = exchange.parse8601(start_date)
    end = exchange.parse8601(end_date)
    
    while since < end:
        trades = exchange.fetch_trades(symbol, since=since, limit=1000)
        if len(trades) == 0:
            break
        all_trades.extend(trades)
        since = trades[-1]['timestamp'] + 1  # 避免重复
        time.sleep(exchange.rateLimit / 1000)  # 限速
    
    df = pd.DataFrame(all_trades)
    df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
    return df

注意:下载数据时,一定要检查数据连续性。我遇到过交易所某天数据丢失的情况,如果不检查,回测结果会严重失真。建议下载后先画个时间戳分布图,看看有没有明显的缺口。

4.3 数据清洗与对齐:脏数据比没数据更可怕

数据下载下来,你以为就能直接用了吗?太天真了。交易所的数据经常有各种问题:

  • 重复数据:同一笔成交被推送了两次
  • 乱序数据:时间戳不是严格递增的
  • 异常价格:比如BTC突然变成0.01美元
  • 缺失字段:某些成交记录没有买卖方向

我在项目中遇到过最离谱的一次,币安的某只山寨币深度数据里,卖一价竟然是负数。你想想看,这要是直接拿去回测,策略会疯狂买入,因为觉得价格太便宜了。

清洗流程我总结为三步:

  1. 去重:按交易ID和时间戳去重
  2. 排序:按时间戳升序排列
  3. 过滤异常:剔除价格、数量明显异常的数据
def clean_trades(df):
    """清洗逐笔成交数据"""
    # 1. 去重
    df = df.drop_duplicates(subset=['id', 'timestamp'])
    
    # 2. 排序
    df = df.sort_values('timestamp').reset_index(drop=True)
    
    # 3. 过滤异常价格(比如价格偏离中位数太多)
    median_price = df['price'].median()
    std_price = df['price'].std()
    df = df[(df['price'] > median_price - 5*std_price) & 
            (df['price'] < median_price + 5*std_price)]
    
    # 4. 过滤异常数量(比如数量为0或负数)
    df = df[df['amount'] > 0]
    
    return df

经验之谈:清洗数据时,别用太严格的过滤条件。比如价格偏离5倍标准差,在正常行情下可能过滤掉一些极端行情的数据。但做市策略恰恰需要这些极端行情来测试风控。我一般先用3倍标准差做标记,人工检查后再决定是否剔除。

4.4 缺失值处理:别让数据缺口毁了你的回测

数据缺失是做市策略回测中最头疼的问题。为什么?因为做市策略是连续报价的,数据一断,你的策略就相当于在真空中交易,结果完全不可信。

常见的缺失情况有两种:

  • 时间戳缺失:某段时间内完全没有数据
  • 字段缺失:有数据但某些字段是NaN

处理方式取决于缺失的原因:

缺失类型 可能原因 处理方法
短时间缺失(<1分钟) 网络抖动、交易所限流 线性插值或前向填充
长时间缺失(>1小时) 交易所维护、数据源问题 标记为不可交易时段
字段缺失 数据格式不完整 从其他数据源补全
def handle_missing_data(df, freq='1s'):
    """处理缺失值"""
    # 设置时间索引
    df = df.set_index('timestamp')
    
    # 生成完整的时间序列
    full_index = pd.date_range(start=df.index.min(), 
                               end=df.index.max(), 
                               freq=freq)
    
    # 重采样并前向填充(短时间缺失)
    df = df.reindex(full_index)
    df = df.fillna(method='ffill', limit=60)  # 最多填充60个缺失值
    
    # 标记长时间缺失
    df['is_valid'] = ~df['price'].isna()
    
    return df

警告:千万不要用全局均值填充缺失值!做市策略对价格敏感,用均值填充会严重扭曲回测结果。我见过有人用前一天的平均价格填充缺失的深度数据,结果回测出来的夏普比率高达5.0,实盘直接亏成狗。

4.5 数据对齐:把不同频率的数据整合到一起

做市策略回测需要同时用到多种数据:深度数据(毫秒级)、逐笔成交(随机时间)、K线(分钟级)。怎么把它们对齐到同一个时间轴上?

我的做法是:

  1. 以深度数据的时间戳为主轴
  2. 将逐笔成交按时间戳归并到最近的深度快照上
  3. K线数据作为辅助,用于计算波动率等指标
def align_data(orderbook_df, trades_df, kline_df):
    """对齐三种数据"""
    # 1. 将逐笔成交对齐到深度快照
    trades_df['aligned_ts'] = pd.cut(
        trades_df['timestamp'],
        bins=orderbook_df['timestamp'],
        labels=orderbook_df['timestamp'][:-1]
    )
    
    # 2. 合并数据
    merged = orderbook_df.merge(
        trades_df.groupby('aligned_ts').agg({
            'price': ['mean', 'std'],
            'amount': 'sum'
        }),
        left_on='timestamp',
        right_index=True,
        how='left'
    )
    
    # 3. 添加K线数据
    merged = merged.merge(
        kline_df[['close', 'volume']],
        left_on=pd.to_datetime(merged['timestamp'], unit='ms').dt.floor('1min'),
        right_index=True,
        how='left'
    )
    
    return merged

小技巧:数据对齐时,要注意时间窗口的选择。深度数据变化快,用1秒窗口就够了。逐笔成交比较稀疏,可以用5秒窗口。我一般先画个时间分布图,看看数据密度再决定窗口大小。

4.6 本章知识体系

说了这么多,咱们用一张图把整个数据获取与清洗的流程串起来:

数据获取与清洗流程 交易所API对接 ccxt / 原生API 历史数据下载 深度 / 逐笔 / K线 数据清洗 去重 / 排序 / 过滤 缺失值处理 插值 / 标记 / 补全 数据对齐 时间轴统一 / 合并 数据质量检查 ✓ 时间戳连续性 ✓ 价格合理性 ✓ 数量非负性 ✓ 买卖方向完整性 ✓ 数据源一致性 ✅ 可用的回测数据 深度快照 + 逐笔成交 + K线

这张图把整个流程串起来了。从API对接开始,到数据下载、清洗、缺失值处理、对齐,最后得到可用的回测数据。每一步都有坑,每一步都得小心。

数据这块,说白了就是「垃圾进,垃圾出」。你花再多时间优化策略,数据不对全是白搭。我见过太多人把精力花在策略上,结果数据一塌糊涂。记住:数据质量决定了回测的天花板

本章核心要点:

  • 做市策略需要深度快照 + 逐笔成交数据,K线只是辅助
  • API对接要注意限速,别被封IP
  • 数据清洗三步走:去重、排序、过滤异常
  • 缺失值处理要区分短时缺失和长时缺失,别用全局均值
  • 数据对齐以深度数据为主轴,其他数据向它靠拢

专注资料整理