第三章:数据准备与清洗——获取历史行情数据,处理缺失值与异常值

数据准备,说白了就是量化交易的「地基」。

我见过太多人,策略逻辑写得天花乱坠,结果一跑回测,亏得底裤都不剩。为什么?数据有问题。你想想看,地基没打好,楼盖得再漂亮也是危房。今天我们就来聊聊,怎么把历史行情数据这块地基夯实。

3.1 数据来源与获取

做套利策略,数据源的选择很关键。我个人习惯用这几种:

  • 交易所官方API:比如Binance、OKX的REST API。数据最原始,但需要自己处理限频。
  • 第三方数据平台:像QuantConnect、聚宽。封装好了,拿来就用,但可能有延迟。
  • 本地CSV/Parquet文件:我经常把历史数据存成Parquet格式,读取快,省空间。

嗯,这里要注意:千万别直接用免费的聚合数据源。我在项目中遇到过,某平台提供的BTC/USDT 1分钟K线,居然有30%的缺失。你拿这种数据做套利,结果可想而知。

核心原则:数据源越原始越好,宁可自己清洗,也别用别人洗过的「二手货」。

3.2 数据清洗流程

拿到原始数据后,第一步不是分析,是清洗。我一般按这个流程走:

  1. 格式统一:时间戳转成UTC,价格统一成浮点数。
  2. 去重:同一时间戳出现多条记录,保留第一条或最后一条。
  3. 排序:按时间升序排列,这是必须的。
  4. 缺失值处理:这个我们重点讲。
  5. 异常值检测:这个也是重点。

为什么会这样排序?因为不先去重就排序,你会得到一堆乱序数据。我刚开始做的时候吃过这个亏,排序后发现时间戳居然倒着走,排查了半天才发现是去重没做好。

3.3 缺失值处理

缺失值在历史行情里太常见了。交易所宕机、网络波动、数据源抽风,都可能导致某根K线消失。

处理缺失值,我常用的方法有这几种:

方法 适用场景 缺点
前向填充(ffill) 短时间缺失(1-2根K线) 会引入滞后
线性插值 价格走势平稳时 剧烈波动时不准
删除缺失行 缺失比例小于1% 会破坏时间连续性
模型预测填充 缺失比例较大时 计算成本高

我个人最常用的是前向填充。为什么?因为套利策略对实时性要求高,用前向填充至少保证了「最新可用数据」。我曾经试过线性插值,结果在闪崩行情里插出了负价格,你说离谱不离谱?

小技巧:处理缺失值时,建议先统计缺失比例。如果某只币种缺失超过5%,我建议直接放弃这个品种。不值得为它浪费时间。

3.4 异常值检测

异常值比缺失值更隐蔽。你看着数据挺完整,但里面可能藏着「炸弹」。

常见的异常值类型:

  • 价格跳变:比如上一秒100,下一秒10000。这通常是数据录入错误。
  • 成交量异常:突然暴增100倍,可能是交易所数据推送bug。
  • 买卖价差为负:买一价高于卖一价,这在正常市场里不可能出现。

检测方法,我推荐用Z-score:

import numpy as np
import pandas as pd

def detect_outliers_zscore(data, threshold=3):
    z_scores = np.abs((data - data.mean()) / data.std())
    return z_scores > threshold

# 示例:检测价格异常
df['price_outlier'] = detect_outliers_zscore(df['close'])
outliers = df[df['price_outlier'] == True]
print(f"发现 {len(outliers)} 个异常价格点")

嗯,这里要注意:Z-score的阈值不要设得太死。3倍标准差是经典值,但我在处理BTC数据时,经常调到4倍。因为BTC波动大,3倍会把很多正常波动误判为异常。

避坑指南:我曾经用Z-score检测ETH的异常值,阈值设成2.5,结果把2017年牛市的所有上涨都标记成了异常。你想想看,那还做什么策略?直接空仓得了。所以阈值一定要根据品种特性来调。

3.5 实战:完整的数据清洗流水线

说了这么多,我们来写一个完整的清洗函数。这是我实际项目中用的版本,简化了一下:

def clean_ohlcv_data(df):
    """
    清洗OHLCV数据
    """
    # 1. 去重
    df = df.drop_duplicates(subset='timestamp')
    
    # 2. 排序
    df = df.sort_values('timestamp').reset_index(drop=True)
    
    # 3. 缺失值处理(前向填充)
    df = df.fillna(method='ffill')
    
    # 4. 异常值检测(价格)
    for col in ['open', 'high', 'low', 'close']:
        z_scores = np.abs((df[col] - df[col].mean()) / df[col].std())
        df.loc[z_scores > 4, col] = np.nan
    
    # 5. 再次填充被标记为异常的值
    df = df.fillna(method='ffill')
    
    # 6. 检查买卖价差
    if 'ask' in df.columns and 'bid' in df.columns:
        df['spread'] = df['ask'] - df['bid']
        df = df[df['spread'] >= 0]
    
    return df

这个函数看起来简单,但我在项目里跑了两年,基本没出过问题。唯一一次翻车,是某个小交易所的数据里出现了负成交量,我忘了加成交量检查。从那以后,我就在函数里加了一行 df = df[df['volume'] > 0]

3.6 数据质量评估

清洗完数据,别急着跑策略。先评估一下数据质量。我一般看这几个指标:

  • 完整率:实际数据量 / 理论数据量。比如1分钟K线,一天应该有1440根,如果只有1400根,完整率就是97.2%。
  • 异常比例:被标记为异常的数据占比。超过2%就要警惕。
  • 时间连续性:相邻时间戳的间隔是否一致。如果出现间隔突然变大,说明有数据缺失。

你想想看,如果完整率只有90%,你拿这个数据做套利,回测结果能信吗?反正我是不敢信。

我的经验:数据质量评估应该做成自动化脚本,每次获取新数据后自动跑一遍。别手动检查,人眼看不出来的。我吃过这个亏,手动检查了三天,结果漏了一个时间戳错位的问题,导致回测结果完全失真。

3.7 知识体系总览

下面这张图,是我对数据准备与清洗这个环节的总结。你可以把它当成一个检查清单:

数据准备与清洗知识体系 数据获取 数据清洗 数据评估 交易所API 第三方平台 本地文件 去重排序 缺失值处理 异常值检测 完整率 异常比例 时间连续性 高质量可用数据 数据准备是量化交易的基石,花80%的时间做数据,只花20%的时间写策略

这张图把整个流程串起来了。你从数据获取开始,经过清洗,最后评估质量。每一步都不能跳过。我见过有人直接跳过清洗,拿原始数据跑回测,结果策略收益高得离谱——后来发现是数据里有个价格少了个小数点,把100块变成了10块,买入信号满天飞。

最后一句:数据清洗这件事,做一次不难,难的是每次获取新数据都做。我建议你把清洗流程写成脚本,每次跑数据前自动执行。别偷懒,偷懒的代价是回测结果不可信,策略上线就亏钱。

专注资料整理