3. 数据准备与清洗:获取分钟级K线数据、处理缺失值与异常值、数据对齐与重采样
做量化交易,有一句话我特别认同:垃圾进,垃圾出。你策略再牛,模型再复杂,数据一塌糊涂,结果就是白忙活。VWAP策略对数据质量尤其敏感,因为它是基于价格和成交量的加权计算,数据稍微有点偏差,VWAP线就歪了。
这一章,咱们就聊聊数据准备那些事儿。说白了,就是怎么把原始数据变成能直接喂给策略的干净数据。
3.1 获取分钟级K线数据
VWAP策略通常用分钟级数据。我个人习惯用1分钟或5分钟K线。为什么?因为太短了噪音大,太长了又跟不上节奏。
获取数据的方式有很多,比如从交易所API直接拉,或者从数据服务商买。这里我展示一个从本地CSV文件读取的例子,实际项目中你换成API调用就行。
import pandas as pd
import numpy as np
from datetime import datetime
# 读取原始分钟级数据
def load_minute_data(file_path):
df = pd.read_csv(file_path, parse_dates=['datetime'])
df.set_index('datetime', inplace=True)
# 确保时间频率是分钟级
df = df.asfreq('1min')
return df
# 示例:加载数据
df = load_minute_data('stock_1min_data.csv')
print(df.head())
嗯,这里要注意:asfreq('1min') 这步很关键。它会强制把时间索引对齐到整分钟,并且自动填充缺失的时间点。我在项目中遇到过,有些数据源会漏掉某些分钟,比如中午休市那段时间,如果不处理,后面计算VWAP就会错位。
3.2 处理缺失值与异常值
数据拿到手,第一件事就是检查有没有坑。缺失值和异常值,是数据清洗的两大拦路虎。
3.2.1 缺失值处理
分钟级数据缺失很常见。比如某分钟没有成交,或者网络波动没拉到数据。怎么处理?我一般分情况讨论:
- 连续缺失不超过3分钟:用前向填充(ffill)或线性插值。我个人偏爱线性插值,因为它更平滑,不会让VWAP出现突兀的台阶。
- 连续缺失超过3分钟:比如中午休市、盘前盘后,直接删除这些时间段。因为VWAP是连续计算的,长时间缺失会破坏连续性。
- 开盘第一分钟缺失:用当日第一笔成交数据填充。如果也没有,那就跳过这一分钟。
# 处理缺失值
def handle_missing(df, max_gap=3):
# 先标记连续缺失的区间
df['gap_flag'] = df['close'].isna().astype(int)
df['gap_group'] = df['gap_flag'].ne(df['gap_flag'].shift()).cumsum()
# 计算每个缺失区间的长度
gap_lengths = df[df['gap_flag']==1].groupby('gap_group').size()
# 短缺失:线性插值
short_gaps = gap_lengths[gap_lengths <= max_gap].index
df.loc[df['gap_group'].isin(short_gaps), ['open','high','low','close','volume']] = \
df[['open','high','low','close','volume']].interpolate(method='linear')
# 长缺失:直接删除
long_gaps = gap_lengths[gap_lengths > max_gap].index
df = df[~df['gap_group'].isin(long_gaps)]
df.drop(['gap_flag','gap_group'], axis=1, inplace=True)
return df
df_clean = handle_missing(df.copy())
3.2.2 异常值处理
异常值比缺失值更隐蔽。我曾经遇到过一次,某只股票突然出现一根天量K线,成交量是平时的100倍。后来发现是数据源把复权除权搞错了。
怎么识别异常值?我常用两种方法:
- 价格异常:用Z-score方法。计算每分钟价格相对于过去N分钟(比如20分钟)的均值和标准差,如果偏离超过3倍标准差,就标记为异常。
- 成交量异常:用中位数绝对偏差(MAD)方法。成交量分布往往偏态,用Z-score不太准,MAD更鲁棒。
# 检测并处理异常值
def detect_outliers(df, window=20, z_thresh=3):
# 价格异常:滚动Z-score
df['price_mean'] = df['close'].rolling(window).mean()
df['price_std'] = df['close'].rolling(window).std()
df['price_z'] = (df['close'] - df['price_mean']) / df['price_std']
# 成交量异常:滚动MAD
df['volume_median'] = df['volume'].rolling(window).median()
df['volume_mad'] = (df['volume'] - df['volume_median']).abs().rolling(window).median()
df['volume_mad_score'] = (df['volume'] - df['volume_median']) / (df['volume_mad'] + 1e-8)
# 标记异常
price_outliers = df['price_z'].abs() > z_thresh
volume_outliers = df['volume_mad_score'].abs() > 5 # MAD阈值通常设大一些
# 处理:用前一个正常值替换
df.loc[price_outliers, ['open','high','low','close']] = np.nan
df.loc[volume_outliers, 'volume'] = np.nan
# 再次插值
df[['open','high','low','close','volume']] = df[['open','high','low','close','volume']].ffill()
df.drop(['price_mean','price_std','price_z','volume_median','volume_mad','volume_mad_score'],
axis=1, inplace=True)
return df
df_final = detect_outliers(df_clean.copy())
3.3 数据对齐与重采样
数据清洗完了,接下来就是对齐和重采样。为什么需要这步?因为不同数据源的时间戳可能不一致,或者你想把1分钟数据合成5分钟数据。
3.3.1 时间对齐
对齐的核心是统一时间基准。我习惯把所有数据对齐到交易所的撮合时间。比如上交所的连续竞价时间是9:30-11:30和13:00-15:00,中间休市的时间段要剔除。
# 对齐到交易所时间
def align_to_exchange(df):
# 只保留交易时段
trading_hours = df.between_time('09:30', '11:30').append(
df.between_time('13:00', '15:00'))
# 去除周末和节假日(这里简化处理,实际需要交易日历)
trading_hours = trading_hours[trading_hours.index.weekday < 5]
return trading_hours
df_aligned = align_to_exchange(df_final)
你想想看,如果不做这步,把休市时间的数据也算进去,VWAP就会在中午那段时间出现一条水平线,完全失真。
3.3.2 重采样
重采样就是把高频数据合成低频数据。比如从1分钟合成5分钟。规则很简单:
- 开盘价:取区间第一分钟的开盘价
- 收盘价:取区间最后一分钟的收盘价
- 最高价:取区间内的最高价
- 最低价:取区间内的最低价
- 成交量:取区间内的成交量之和
# 重采样:1分钟 -> 5分钟
def resample_to_5min(df):
rule = '5min'
resampled = df.resample(rule).agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
})
# 删除重采样后可能出现的空行(比如非交易时间)
resampled.dropna(inplace=True)
return resampled
df_5min = resample_to_5min(df_aligned)
3.4 知识体系总览
说了这么多,咱们用一张图把整个数据准备流程串起来。这样你心里就有谱了。
这张图把整个流程串起来了。从获取数据开始,到缺失值、异常值处理,再到时间对齐和重采样,最后得到干净数据。每一步都有坑,但只要你按这个流程走,基本不会出大问题。
好了,数据准备好了。下一章咱们就可以真正开始计算VWAP了。不过在那之前,我建议你把今天讲的代码跑一遍,看看你的数据里藏着多少“惊喜”。