4. 数据获取与清洗:从交易所获取历史行情数据、处理缺失值与异常值、数据重采样与对齐

做量化做市策略,第一步不是写策略逻辑,而是搞定数据。我见过太多人策略写得天花乱坠,结果数据一塌糊涂,回测结果全是幻觉。说白了,数据是地基,地基不稳,楼盖得再高也得塌。

这一章,我们就来聊聊怎么从交易所拿到靠谱的历史行情数据,怎么把脏数据洗干净,以及怎么让不同频率的数据对齐。嗯,这些都是我踩过坑之后才真正重视起来的东西。

4.1 从交易所获取历史行情数据

获取数据的方式有很多种,我个人最推荐的是直接通过交易所的官方API。为什么?因为数据源最可靠,延迟最低,而且字段最全。你想想看,从第三方数据商拿数据,中间多了一层转发,谁知道有没有丢包或者篡改?

以币安交易所为例,它的REST API可以获取历史K线数据。我习惯用Python的requests库来拉取,简单直接。

import requests
import pandas as pd
from datetime import datetime

def fetch_binance_klines(symbol, interval, start_time, end_time):
    """
    从币安获取历史K线数据
    :param symbol: 交易对,如 'BTCUSDT'
    :param interval: K线周期,如 '1m', '5m', '1h'
    :param start_time: 开始时间,毫秒级时间戳
    :param end_time: 结束时间,毫秒级时间戳
    """
    url = 'https://api.binance.com/api/v3/klines'
    params = {
        'symbol': symbol,
        'interval': interval,
        'startTime': start_time,
        'endTime': end_time,
        'limit': 1000  # 单次最多1000条
    }
    
    response = requests.get(url, params=params)
    data = response.json()
    
    # 解析数据
    klines = []
    for k in data:
        klines.append({
            'timestamp': datetime.fromtimestamp(k[0] / 1000),
            'open': float(k[1]),
            'high': float(k[2]),
            'low': float(k[3]),
            'close': float(k[4]),
            'volume': float(k[5])
        })
    
    return pd.DataFrame(klines)

# 获取2024年1月的1分钟K线数据
df = fetch_binance_klines('BTCUSDT', '1m', 
                          int(datetime(2024,1,1).timestamp()*1000),
                          int(datetime(2024,1,31).timestamp()*1000))
print(df.head())
我的小技巧: 币安API单次最多返回1000条数据,如果你要拉取长时间段的数据,记得做分页处理。我一般会写个循环,每次用最后一条数据的时间戳作为下一次请求的startTime。

这里有个坑要注意——不同交易所的API格式差异很大。币安用的是毫秒级时间戳,而有些交易所用的是秒级。我曾经在对接某二线交易所时,因为时间戳单位搞错了,拉回来的数据全是乱的,排查了半天才发现问题。

4.2 处理缺失值与异常值

数据拿到手,别急着用。先检查一下有没有缺失值和异常值。这是数据清洗的核心工作,也是最容易出问题的地方。

4.2.1 缺失值处理

缺失值在行情数据里很常见,比如交易所宕机、网络波动、API限流,都可能导致某根K线没拉回来。我处理缺失值的策略是这样的:

  • 检查缺失:isnull()isna()检查DataFrame中是否有空值
  • 定位缺失: 查看缺失值的时间点,判断是单点缺失还是连续缺失
  • 填充策略: 单点缺失用前向填充(ffill)或插值法;连续缺失超过3根K线,建议直接删除
# 检查缺失值
print(df.isnull().sum())

# 前向填充(用上一根K线的数据填充)
df.fillna(method='ffill', inplace=True)

# 或者用线性插值
df.interpolate(method='linear', inplace=True)

# 连续缺失超过3根,删除
df = df.dropna(thresh=len(df.columns) - 3)
注意: 千万不要用均值填充行情数据!金融时间序列有很强的自相关性,用均值填充会破坏数据的统计特性。我曾经见过有人用收盘价的均值去填充缺失值,结果回测出来的夏普比率虚高,实盘直接被打脸。

4.2.2 异常值处理

异常值比缺失值更隐蔽。比如某根K线的最高价突然比前后高出10倍,这明显是数据错误。我一般用两种方法检测异常值:

  • 3σ原则: 计算价格序列的均值和标准差,超出均值±3倍标准差的值视为异常
  • IQR方法: 计算四分位距,超出Q1-1.5*IQR或Q3+1.5*IQR的值视为异常
def detect_outliers_iqr(series):
    Q1 = series.quantile(0.25)
    Q3 = series.quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    return (series < lower_bound) | (series > upper_bound)

# 检测收盘价的异常值
outliers = detect_outliers_iqr(df['close'])
print(f"发现 {outliers.sum()} 个异常值")

# 将异常值替换为前后均值
df.loc[outliers, 'close'] = (df['close'].shift(1) + df['close'].shift(-1)) / 2

我个人更倾向于IQR方法,因为它不受极端值的影响,更稳健。3σ方法对正态分布假设依赖较强,而金融数据往往有厚尾特征,用3σ可能会漏掉一些异常。

4.3 数据重采样与对齐

做市策略通常需要多频率数据配合。比如,你可能用1分钟K线做高频信号,用1小时K线判断趋势方向。这时候就需要把不同频率的数据对齐到同一个时间轴上。

4.3.1 数据重采样

重采样就是把高频数据降采样到低频,或者把低频数据升采样到高频。Pandas的resample方法就是干这个的。

# 将1分钟数据重采样为5分钟
df_5min = df.resample('5T', on='timestamp').agg({
    'open': 'first',
    'high': 'max',
    'low': 'min',
    'close': 'last',
    'volume': 'sum'
}).dropna()

# 将1分钟数据重采样为1小时
df_1hour = df.resample('1H', on='timestamp').agg({
    'open': 'first',
    'high': 'max',
    'low': 'min',
    'close': 'last',
    'volume': 'sum'
}).dropna()
核心要点: 重采样时,开盘价取第一个,最高价取最大值,最低价取最小值,收盘价取最后一个,成交量求和。这个规则是行业标准,别搞错了。

4.3.2 数据对齐

对齐是指让不同频率的数据在时间戳上保持一致。比如,你的1分钟数据和5分钟数据,时间戳必须能对应上。我常用的方法是:

  • 左对齐: 以低频数据的时间戳为基准,将高频数据对齐到最近的低频时间点
  • 右对齐: 以高频数据的时间戳为基准,将低频数据向前填充
# 左对齐:将1分钟数据对齐到5分钟时间点
df_1min_aligned = df.set_index('timestamp').resample('5T').ffill()

# 右对齐:将5分钟数据向前填充到1分钟时间点
df_5min_aligned = df_5min.reindex(df_1min.index, method='ffill')

这里有个细节要注意——对齐时一定要考虑未来信息。比如,你用5分钟数据去填充1分钟数据,如果用了未来的5分钟数据,那就引入了未来函数,回测结果会虚高。我建议统一用前向填充(ffill),只用过去的数据。

4.4 数据清洗的完整流程

说了这么多,我们来总结一下数据清洗的完整流程。我画了一张图,方便你理解整个链路。

数据清洗完整流程 1. 数据获取 交易所API拉取 2. 缺失值处理 前向填充/插值/删除 3. 异常值检测 3σ/IQR方法 4. 异常值处理 替换/删除/标记 5. 数据重采样 降采样/升采样 6. 数据对齐 左对齐/右对齐 7. 数据验证 检查完整性/一致性 8. 保存数据 HDF5/Parquet格式 数据质量不达标,返回重做

这张图展示了我个人常用的数据清洗流程。从数据获取开始,经过缺失值处理、异常值检测与处理、数据重采样、数据对齐,最后到数据验证和保存。如果验证不通过,就返回前面的步骤重新处理。

我的建议: 数据清洗的每一步都要写日志,记录处理了多少缺失值、多少异常值、用了什么方法。这样出了问题可以追溯。我曾经因为没写日志,回测结果异常时查了三天才发现是数据对齐时用了未来函数。

4.5 数据存储的最佳实践

清洗好的数据怎么存?我推荐用HDF5或Parquet格式,而不是CSV。原因有三:

  • 压缩率高: 同样的数据,Parquet文件大小只有CSV的1/5左右
  • 读取速度快: Parquet支持列式存储,读取指定列时速度极快
  • 支持元数据: 可以存储数据类型、时间戳等信息,避免类型转换问题
# 保存为Parquet格式
df.to_parquet('btcusdt_1m_202401.parquet', index=False)

# 读取Parquet文件
df = pd.read_parquet('btcusdt_1m_202401.parquet')

嗯,数据获取与清洗这部分就讲到这里。记住一句话:花80%的时间在数据上,花20%的时间在策略上。数据干净了,策略才能跑得稳。


公众号:蓝海数据掘金营,微信deep3321