第二章 数据获取与清洗:数据源选择、数据清洗、数据对齐与重采样

各位同学,欢迎来到第二章。上一章我们聊了量化交易的整体框架,今天咱们来点实在的——数据。

做量化交易,说白了就是跟数据打交道。你策略再牛,模型再花哨,数据是脏的、乱的、缺的,那结果就是垃圾进垃圾出。我见过太多人,策略回测曲线漂亮得不行,一上实盘就崩,查到最后,往往是数据对齐出了问题。

嗯,今天我们就来把这几个硬骨头啃下来。

2.1 数据源选择:你的原材料从哪来?

数据源的选择,决定了你后续工作的基础。我个人习惯,会根据不同场景选不同的数据源。

2.1.1 Yahoo Finance

Yahoo Finance 是很多入门者的首选。免费、覆盖广、接口简单。用 yfinance 库几行代码就能拉数据。

import yfinance as yf

# 获取苹果股票数据
aapl = yf.download('AAPL', start='2020-01-01', end='2023-12-31')
print(aapl.head())

但我要提醒你:Yahoo Finance 的数据质量,说实话,不太稳定。我在项目中遇到过几次,某天的数据突然跳空,或者复权数据算错了。你想想看,这种错误要是没发现,回测结果能信吗?

避坑指南: 我曾经用 Yahoo Finance 做美股回测,发现某只股票在分红日前后价格异常。后来一查,是复权因子更新延迟。从那以后,我只要用 Yahoo 的数据,一定会跟其他源交叉验证。

2.1.2 Binance

做加密货币交易,Binance 几乎是标配。它的 API 文档清晰,数据粒度细到毫秒级。

from binance.client import Client

client = Client(api_key='你的API_KEY', api_secret='你的API_SECRET')
klines = client.get_klines(symbol='BTCUSDT', interval=Client.KLINE_INTERVAL_1HOUR)

Binance 的数据有个好处——它是交易所直接出来的,没有中间商赚差价。但问题也有:不同交易所之间,同一时刻的价格可能差好几个点。你如果做套利策略,这个价差就是利润来源,但也可能是坑。

2.1.3 其他数据源

数据源 适用场景 优点 缺点
Quandl 宏观经济数据、期货 数据质量高,清洗过 收费,贵
Alpha Vantage 美股、外汇 免费额度够用 API 限制多,速度慢
本地数据库 生产环境 完全可控,速度快 维护成本高

我的建议是:开发阶段用免费源,生产阶段一定要上付费或自建数据源。别问为什么,问就是吃过亏。

2.2 数据清洗:把脏数据洗干净

数据拿到手,第一件事不是分析,是洗。就像买菜回来,你得先摘菜、洗菜,才能下锅。

2.2.1 去重

重复数据是常见问题。原因很多:API 重试、数据源推送重复、自己代码写错了。去重很简单,但容易被忽略。

# 去除完全重复的行
df = df.drop_duplicates()

# 按时间戳去重,保留最后一条
df = df[~df.index.duplicated(keep='last')]

你可能会问:「重复就重复呗,影响不大吧?」 我告诉你,影响大了去了。重复数据会导致你的统计指标失真,比如计算日均成交量,重复一天的数据,结果就偏了。

2.2.2 缺失值处理

缺失值,是量化数据里最头疼的问题之一。非交易日、数据推送失败、停牌,都会产生缺失值。

处理方式有三种:

  1. 直接删除:简单粗暴,但会丢失信息。适合缺失比例很小的情况。
  2. 向前填充:用上一个有效值填充。适合价格数据,因为价格在短时间内不会突变。
  3. 插值:用前后值做线性插值。适合成交量、持仓量等。
# 向前填充
df['close'] = df['close'].fillna(method='ffill')

# 线性插值
df['volume'] = df['volume'].interpolate(method='linear')
个人经验: 我建议对价格数据用向前填充,对成交量用插值。为什么?因为价格是「记忆性」的,停牌后复牌,价格大概率跟前收盘价接近。但成交量是「事件性」的,停牌期间没有交易,插值反而更合理。

2.2.3 异常值检测

异常值,比如某天价格突然涨了 1000%,或者成交量突然是前一天的 100 倍。这种数据,要么是数据源错了,要么是发生了极端事件。

我常用的方法是 Z-score 法

from scipy import stats

z_scores = stats.zscore(df['close'])
abs_z_scores = np.abs(z_scores)
filtered_entries = (abs_z_scores < 3)
df_clean = df[filtered_entries]

但注意:Z-score 假设数据是正态分布,金融数据往往不是。所以我会结合业务逻辑来判断。比如,某只股票一天涨 20%,在 A 股是涨停,正常;但在美股,一天涨 20% 就很少见,需要人工确认。

2.3 数据对齐与重采样:让不同频率的数据「对上话」

这是很多新手容易翻车的地方。你拿日线数据做策略,但你的信号是小时级别的,怎么办?或者你有多个股票,它们的交易时间不完全重合,怎么处理?

2.3.1 时间对齐

不同股票的交易时间不同。比如 A 股和港股,开盘时间差半小时。如果你同时交易这两个市场,必须把数据对齐到同一个时间轴上。

# 对齐到北京时间 15:00(收盘价)
aligned = pd.concat([df_a_share['close'], df_hk_share['close']], axis=1, join='inner')
aligned.columns = ['A股', '港股']

join='inner' 表示只保留两个数据都有的时间点。这样能避免因为时间错位导致的「未来函数」问题。

避坑指南: 我曾经犯过一个错误——用 A 股的收盘价和港股的收盘价直接做回归分析。结果发现相关性特别高,后来一查,是因为 A 股收盘比港股晚半小时,港股收盘后的半小时内,A 股价格已经反映了港股的信息。这本质上是一种「未来信息泄露」。

2.3.2 重采样

重采样,就是把高频数据转成低频,或者反过来。最常见的是把分钟线转成日线。

# 1分钟数据转成日线
daily = df_minute.resample('1D').agg({
    'open': 'first',
    'high': 'max',
    'low': 'min',
    'close': 'last',
    'volume': 'sum'
})

这里要注意:重采样时,OHLC 的聚合方式不同。开盘价取第一个,收盘价取最后一个,最高价取最大值,最低价取最小值,成交量求和。这个逻辑看起来简单,但很多人会搞混。

反过来,把日线转成分钟线,就需要插值了。但说实话,我不建议这么做。因为低频转高频,本质上是在「无中生有」,会引入大量噪声。

2.3.3 多频率数据融合

实际项目中,你经常需要把不同频率的数据融合在一起。比如,用日线数据计算均线,用分钟线数据生成交易信号。

# 计算日线均线
daily_ma = daily['close'].rolling(20).mean()

# 将日线均线对齐到分钟线
minute_ma = daily_ma.reindex(df_minute.index, method='ffill')

这里用 reindex 配合 method='ffill',把日线的均线值填充到每一分钟。这样,你的分钟级策略就能用到日线级别的信息了。

2.4 实战:一个完整的数据处理流程

说了这么多,我们来走一遍完整的流程。假设我要做 BTC/USDT 的 1 小时级别策略。

import pandas as pd
import numpy as np
from binance.client import Client

# 1. 获取数据
client = Client()
klines = client.get_klines(symbol='BTCUSDT', interval=Client.KLINE_INTERVAL_1HOUR)
df = pd.DataFrame(klines, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume',
                                    'close_time', 'quote_asset_volume', 'number_of_trades',
                                    'taker_buy_base_asset_volume', 'taker_buy_quote_asset_volume', 'ignore'])
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
df.set_index('timestamp', inplace=True)

# 2. 数据清洗
# 去重
df = df[~df.index.duplicated(keep='last')]
# 缺失值处理
df['close'] = df['close'].fillna(method='ffill')
# 异常值处理(Z-score)
z_scores = np.abs((df['close'] - df['close'].mean()) / df['close'].std())
df = df[z_scores < 3]

# 3. 数据对齐(如果有多个币种,这里做对齐)
# 4. 重采样(如果需要,比如转成4小时线)
df_4h = df.resample('4H').agg({
    'open': 'first',
    'high': 'max',
    'low': 'min',
    'close': 'last',
    'volume': 'sum'
})

print(f"原始数据量: {len(klines)}")
print(f"清洗后数据量: {len(df)}")
print(f"重采样后数据量: {len(df_4h)}")

你看,整个流程下来,数据从原始状态变成了可用的状态。这个过程,我建议你每次拿到新数据都走一遍,形成肌肉记忆。

2.5 本章小结

数据获取与清洗,听起来枯燥,但它是量化交易的基石。我见过太多人,策略写得花里胡哨,结果数据没处理好,回测结果全是假的。

记住三个要点:

  • 数据源选对:开发用免费,生产用付费
  • 数据洗得干净:去重、补缺、去异常,一步不能少
  • 对齐和重采样要谨慎:别让时间错位毁了你的策略

下一章,我们会聊技术指标的计算。但如果你这章的数据没处理好,后面再好的指标也是白搭。所以,好好练,别偷懒。

嗯,今天就到这里。有问题欢迎在课程群里讨论。