第三章 VWAP数据准备:获取历史Tick数据与分钟级K线数据,数据清洗与预处理实战

做VWAP策略,数据是地基。地基不稳,楼盖得再漂亮也得塌。

我见过太多人,一上来就调参数、写策略,结果回测曲线漂亮得像假的一样——后来一查,数据里全是坑。嗯,今天我们就来聊聊,怎么把数据这关给过了。

3.1 数据源的选择:Tick vs 分钟K线

先搞清楚我们要什么。VWAP的核心是「成交量加权」,所以你需要两样东西:价格成交量

数据类型 特点 适用场景
Tick数据 每笔成交记录,最精细 高频VWAP、日内T+0
分钟K线 聚合后的OHLCV 标准VWAP、中低频策略

我个人习惯:做回测用分钟K线,做实盘用Tick数据。为什么?回测时Tick数据量太大,动不动几个G,跑一次要半天。实盘时精度不够,VWAP算出来会偏。

核心原则:VWAP对成交量的敏感度远高于价格。数据可以缺价格,但不能缺成交量。缺了成交量,VWAP就是瞎算。

3.2 获取Tick数据实战

假设我们从某数据商拿到CSV格式的Tick数据。结构大概长这样:

# tick_data_sample.csv
timestamp,price,volume,bid,ask
2024-01-02 09:30:00.123,10.25,100,10.24,10.26
2024-01-02 09:30:00.456,10.26,200,10.25,10.27
2024-01-02 09:30:00.789,10.24,150,10.23,10.25
...

读进来很简单,但坑在后面。

import pandas as pd

df_tick = pd.read_csv('tick_data_sample.csv', parse_dates=['timestamp'])
df_tick.set_index('timestamp', inplace=True)
print(df_tick.head())

你看,就这么几行。但我在项目中遇到过一个问题:时间戳精度不一致。有的数据商给到毫秒,有的给到微秒,还有的干脆只给到秒。合并多个数据源时,时间轴对不上,VWAP算出来全是错的。

避坑指南:我曾经因为没统一时间精度,回测年化收益差了8%。统一用毫秒,别偷懒。

3.3 分钟K线数据构建

Tick数据太细,我们通常要聚合到分钟级。这里有个关键点:成交量要累加,价格要加权平均

# 将Tick数据聚合为1分钟K线
df_minute = df_tick.resample('1T').agg({
    'price': 'ohlc',        # 生成开盘、最高、最低、收盘
    'volume': 'sum',        # 成交量累加
    'bid': 'last',          # 最后一笔买价
    'ask': 'last'           # 最后一笔卖价
})

# 展平多级列索引
df_minute.columns = ['_'.join(col).strip() for col in df_minute.columns.values]
df_minute.rename(columns={
    'price_open': 'open',
    'price_high': 'high',
    'price_low': 'low',
    'price_close': 'close',
    'volume_sum': 'volume'
}, inplace=True)

print(df_minute.head())

说白了,这就是把一堆零散的成交记录,压缩成一根根K线。你想想看,如果直接用Tick数据算VWAP,计算量会爆炸。聚合到分钟级,既保留了日内波动特征,又大幅降低了数据量。

3.4 数据清洗:那些年我们踩过的坑

数据清洗是VWAP准备中最枯燥、但最重要的一步。我总结了三个必做动作:

3.4.1 缺失值处理

分钟K线里经常出现空值——比如某分钟没有成交。怎么办?

  • 成交量缺失:直接填0。VWAP计算时,成交量=0的分钟不贡献权重。
  • 价格缺失:用前一根K线的收盘价填充(向前填充)。
  • 连续缺失超过5分钟:建议标记为异常,不要强行填充。
# 缺失值处理
df_minute['volume'].fillna(0, inplace=True)
df_minute['close'].fillna(method='ffill', inplace=True)
df_minute['open'].fillna(method='ffill', inplace=True)

# 检查连续缺失
null_streaks = df_minute['close'].isna().astype(int).groupby(
    df_minute['close'].notna().cumsum()
).cumsum()
if null_streaks.max() > 5:
    print("警告:存在连续5分钟以上缺失数据,建议检查数据源")

3.4.2 异常值检测

价格突然跳空、成交量突然暴增——这些可能是数据错误,也可能是真实行情。怎么区分?

我习惯用Z-score方法

from scipy import stats

# 对价格变化率做Z-score检测
df_minute['return'] = df_minute['close'].pct_change()
z_scores = stats.zscore(df_minute['return'].dropna())
outliers = abs(z_scores) > 3

print(f"检测到异常价格变动:{outliers.sum()} 条")

嗯,这里要注意:不要直接删除异常值。我曾经删了一堆「异常」数据,结果发现那是真实的闪崩行情。正确的做法是:标记出来,人工复核。

3.4.3 时间对齐

不同交易所、不同品种的交易时间不一样。A股是9:30-15:00,中间有午休。期货有夜盘。如果你把全天数据混在一起算VWAP,结果会偏。

我的做法:只保留交易时段内的数据。开盘前和收盘后的集合竞价数据,单独处理,不混入连续竞价VWAP计算。

# 过滤A股交易时段(9:30-11:30, 13:00-15:00)
trading_mask = (
    (df_minute.index.time >= pd.Timestamp('09:30').time()) &
    (df_minute.index.time <= pd.Timestamp('11:30').time())
) | (
    (df_minute.index.time >= pd.Timestamp('13:00').time()) &
    (df_minute.index.time <= pd.Timestamp('15:00').time())
)

df_minute_clean = df_minute[trading_mask].copy()

3.5 知识体系总览

下面这张图,是我做VWAP数据准备时的完整流程。你可以把它当成一个检查清单:

VWAP数据准备流程 数据获取 Tick数据(每笔成交) 分钟K线数据(OHLCV) 数据清洗与预处理 缺失值处理 异常值检测 时间对齐 ✅ 干净的VWAP可用数据

3.6 实战:完整的数据预处理Pipeline

最后,我把上面所有步骤打包成一个函数。你以后做VWAP,直接调这个就行:

def prepare_vwap_data(tick_file, freq='1T'):
    """
    从Tick数据到VWAP可用数据的完整Pipeline
    
    参数:
        tick_file: Tick数据CSV路径
        freq: 聚合频率,默认1分钟
    
    返回:
        df_clean: 清洗后的分钟K线DataFrame
    """
    # 1. 读取数据
    df = pd.read_csv(tick_file, parse_dates=['timestamp'])
    df.set_index('timestamp', inplace=True)
    
    # 2. 聚合到分钟K线
    df_min = df.resample(freq).agg({
        'price': 'ohlc',
        'volume': 'sum'
    })
    df_min.columns = ['open', 'high', 'low', 'close', 'volume']
    
    # 3. 缺失值处理
    df_min['volume'].fillna(0, inplace=True)
    df_min['close'].fillna(method='ffill', inplace=True)
    df_min['open'].fillna(method='ffill', inplace=True)
    
    # 4. 异常值标记(不删除)
    df_min['return'] = df_min['close'].pct_change()
    z_scores = stats.zscore(df_min['return'].dropna())
    df_min['is_outlier'] = False
    df_min.loc[df_min['return'].notna(), 'is_outlier'] = abs(z_scores) > 3
    
    # 5. 过滤交易时段
    morning = (df_min.index.time >= pd.Timestamp('09:30').time()) & \
              (df_min.index.time <= pd.Timestamp('11:30').time())
    afternoon = (df_min.index.time >= pd.Timestamp('13:00').time()) & \
                (df_min.index.time <= pd.Timestamp('15:00').time())
    df_clean = df_min[morning | afternoon].copy()
    
    return df_clean

# 使用示例
df_vwap_ready = prepare_vwap_data('tick_data_sample.csv')
print(f"数据准备完成,共 {len(df_vwap_ready)} 根K线")

个人经验:这个Pipeline我用了三年,回测和实盘的数据一致性很高。唯一要提醒的是——实盘时记得用最新数据重新跑一遍,别用昨天的数据算今天的VWAP。

数据准备好了,下一步就是真正的VWAP计算了。不过那是下一章的事。今天先把数据关过了,后面就顺了。


专注资料整理