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())
避坑指南: 我曾经犯过一个错误——把除权除息日的价格跳空当成异常值给处理掉了。结果VWAP计算完全偏离真实值。所以,处理异常值之前,一定要先确认是不是正常的事件驱动型波动。

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)
核心要点: 重采样时,成交量用求和,价格用首尾和极值。千万别用平均价,否则VWAP就失去了意义。

3.4 知识体系总览

说了这么多,咱们用一张图把整个数据准备流程串起来。这样你心里就有谱了。

数据准备与清洗流程 获取分钟级K线 API / CSV / 数据库 处理缺失值 插值 / 前向填充 / 删除 处理异常值 Z-score / MAD 时间对齐 剔除休市 / 统一时间基准 重采样 1min → 5min / 15min 干净数据 可直接用于VWAP 数据清洗不是一次性的,每次回测前都要重新跑一遍 数据清洗关键指标一览 缺失值比例 < 1%:直接插值 1%-5%:分段处理 > 5%:检查数据源 异常值阈值 价格:Z-score > 3 成交量:MAD > 5

这张图把整个流程串起来了。从获取数据开始,到缺失值、异常值处理,再到时间对齐和重采样,最后得到干净数据。每一步都有坑,但只要你按这个流程走,基本不会出大问题。

好了,数据准备好了。下一章咱们就可以真正开始计算VWAP了。不过在那之前,我建议你把今天讲的代码跑一遍,看看你的数据里藏着多少“惊喜”。


专注资料整理