数据准备:获取分钟级K线数据、数据清洗与对齐、成交量与价格的预处理
做量化交易的朋友都知道一句话:垃圾进,垃圾出。VWAP算法调优更是如此——你数据没整干净,后面所有回测都是自欺欺人。
我个人习惯,在开始任何策略开发前,先花至少30%的时间在数据准备上。别嫌多,这步省了,后面哭都来不及。
一、获取分钟级K线数据
VWAP算法对数据粒度有要求。日线太粗,tick级又太细(回测跑起来慢得想砸电脑)。分钟级K线是最佳平衡点。
我一般用1分钟或5分钟K线。为什么?你想想看,VWAP的核心是计算每个时间段的成交量加权均价,分钟级数据刚好能捕捉到日内交易节奏,又不会让计算量爆炸。
数据来源方面,我个人偏好直接从交易所API拉取,或者用第三方数据服务商。这里给个简单的获取示例:
import pandas as pd
import requests
def fetch_minute_kline(symbol, start_date, end_date):
"""
获取分钟级K线数据
实际使用时请替换为真实API
"""
# 伪代码示意
url = f"https://api.example.com/kline?symbol={symbol}&interval=1min&start={start_date}&end={end_date}"
response = requests.get(url)
data = response.json()
df = pd.DataFrame(data)
df.columns = ['timestamp', 'open', 'high', 'low', 'close', 'volume']
df['timestamp'] = pd.to_datetime(df['timestamp'])
df.set_index('timestamp', inplace=True)
return df
# 使用示例
df_1min = fetch_minute_kline('BTCUSDT', '2024-01-01', '2024-01-31')
print(f"获取到 {len(df_1min)} 条1分钟K线数据")
二、数据清洗与对齐
拿到原始数据后,别急着用。先做三件事:去重、补缺、对齐。
2.1 去重与异常值处理
原始数据里经常有重复行。比如网络重连导致同一根K线被推送两次。我习惯用drop_duplicates()直接干掉。
# 去重
df = df[~df.index.duplicated(keep='first')]
# 检查异常值
# 比如成交量突然为0,或者价格跳空太大
df = df[df['volume'] > 0] # 剔除成交量为0的K线
df = df[df['close'] > 0] # 剔除价格为0的K线
嗯,这里要注意:有些品种在夜盘或节假日成交量极低,出现连续几根零成交K线很正常。要不要剔除?看你的策略逻辑。如果VWAP计算需要连续时间序列,我建议保留但标记出来,而不是直接删掉。
2.2 缺失值填充
交易所偶尔会漏发K线。比如某根1分钟K线因为网络波动没推送过来。这时候时间序列就断了。
我遇到过最头疼的情况:回测时VWAP曲线突然跳变,查了半天发现是中间缺了一根K线,导致累计成交量计算错误。
# 重采样到固定频率,填充缺失值
df = df.resample('1T').asfreq() # 1分钟频率
# 前向填充价格,成交量填0
df['close'].fillna(method='ffill', inplace=True)
df['volume'].fillna(0, inplace=True)
df['open'].fillna(method='ffill', inplace=True)
df['high'].fillna(method='ffill', inplace=True)
df['low'].fillna(method='ffill', inplace=True)
2.3 多品种时间对齐
如果你同时交易多个品种(比如BTC和ETH),它们的K线时间戳可能不完全对齐。有的交易所整点推送,有的延迟几秒。
我的做法是:统一以某个品种的时间为基准,其他品种做最近邻对齐。
# 假设df_btc和df_eth是两个品种的1分钟K线
# 以BTC时间为基准,对齐ETH
df_eth_aligned = df_eth.reindex(df_btc.index, method='nearest')
说白了,就是让所有数据站在同一个时间轴上。否则你算出来的VWAP是错位的,策略信号会乱套。
三、成交量与价格的预处理
数据对齐后,终于到了预处理环节。这一步直接影响VWAP计算的准确性。
3.1 成交量预处理
原始成交量数据有几个常见问题:
- 单位不统一:有的品种用"手",有的用"张",有的用"币"
- 复权影响:股票除权除息后,成交量会异常放大或缩小
- 异常大单:某根K线突然出现天量成交
我一般这样处理:
# 统一单位(假设全部转为"币")
df['volume'] = df['volume'] * contract_multiplier # 合约乘数
# 剔除异常成交量(比如超过均值3个标准差)
mean_vol = df['volume'].mean()
std_vol = df['volume'].std()
df = df[df['volume'] < mean_vol + 3 * std_vol]
# 或者用中位数绝对偏差(MAD)方法,更稳健
from scipy.stats import median_abs_deviation
mad = median_abs_deviation(df['volume'])
median_vol = df['volume'].median()
df = df[df['volume'] < median_vol + 5 * mad]
3.2 价格预处理
价格数据相对干净,但有几个坑要注意:
- 跳空缺口:开盘直接跳过前收盘价,比如隔夜重大消息
- 价格反转:最高价低于最低价(数据错误)
- 涨跌停板:价格封死,成交量萎缩
# 检查价格合理性
assert (df['high'] >= df['low']).all(), "存在最高价低于最低价的错误数据"
assert (df['high'] >= df['close']).all(), "收盘价高于最高价"
assert (df['low'] <= df['close']).all(), "收盘价低于最低价"
# 处理涨跌停:标记出来,但不删除
df['is_limit_up'] = (df['close'] == df['high']) & (df['volume'] < 0.1 * df['volume'].rolling(20).mean())
df['is_limit_down'] = (df['close'] == df['low']) & (df['volume'] < 0.1 * df['volume'].rolling(20).mean())
我曾经在回测中没处理涨跌停,结果VWAP在涨停板附近出现奇怪的拐点。后来发现是因为涨停时成交量极低,但价格没变,导致加权均价被前几根K线带偏。
3.3 计算VWAP所需的中间变量
预处理完,就可以生成VWAP计算需要的中间变量了:
# 计算典型价格 (Typical Price)
df['typical_price'] = (df['high'] + df['low'] + df['close']) / 3
# 计算价格×成交量
df['pv'] = df['typical_price'] * df['volume']
# 计算累计值(用于滚动VWAP)
df['cum_pv'] = df['pv'].cumsum()
df['cum_vol'] = df['volume'].cumsum()
# 计算VWAP
df['vwap'] = df['cum_pv'] / df['cum_vol']
嗯,这里有个细节:VWAP通常从当天开盘开始重新计算。如果你做的是日内策略,记得每天重置累计值。我习惯按日期分组计算:
# 按日期分组计算VWAP
df['date'] = df.index.date
df['vwap_daily'] = df.groupby('date').apply(
lambda x: (x['pv'].cumsum() / x['volume'].cumsum())
).reset_index(level=0, drop=True)
四、知识体系总览
下面这张图概括了本章的核心流程。我画的时候特意把数据清洗放在中间位置——因为它是承上启下的关键环节。
五、避坑指南
最后,分享几个我踩过的坑:
- 别信默认数据:有些数据服务商返回的K线已经做过平滑处理。我遇到过"漂亮"的VWAP曲线,结果发现是数据被插值过。
- 注意交易时间:数字货币是7×24小时,但股票有开盘收盘。VWAP计算范围要对应交易时段。
- 复权处理要谨慎:股票除权后,价格和成交量都会变。我建议用前复权数据,但成交量也要相应调整。
- 保存原始数据:预处理后的数据别覆盖原始文件。我习惯加个
_clean后缀,方便回溯。
好了,数据准备这块就聊到这儿。记住一句话:数据干净了,VWAP才有意义。下一节我们会基于这些数据,开始真正的参数调优。
公众号:蓝海资料掘金营,微信deep3321