3、VWAP实战计算:使用Python计算单只股票的日级VWAP、处理缺失数据与异常值、VWAP的滚动计算实现

好了,咱们直接进入正题。VWAP这个指标,说白了就是「用成交量给价格加权」。你想想看,如果一只股票全天成交了1000万股,其中800万股是在10块钱成交的,那这个10块钱显然比尾盘那几笔小单子更有代表性。VWAP要抓的就是这个「真实成交重心」。

我个人习惯把VWAP当作日内交易的「锚」。价格在VWAP上方,说明多头占优;在下方,空头控场。但前提是——你得算得准。今天我就带你手撸一遍Python实现,把那些坑都踩平了。

3.1 日级VWAP的标准公式

先上公式,别嫌简单:

VWAP = Σ(价格 × 成交量) / Σ(成交量)

这里的「价格」通常用成交均价,也就是 (开盘+最高+最低+收盘)/4,或者直接用收盘价。我个人更推荐用均价,因为能平滑掉一些极端波动。

举个例子:

时间 均价 成交量 价格×成交量
09:30 10.02 50000 501000
10:00 10.05 80000 804000
10:30 10.01 30000 300300
合计 - 160000 1605300

VWAP = 1605300 / 160000 = 10.033。嗯,比简单均价10.027略高一点,因为成交量大的那笔价格更高。

3.2 Python实现:从数据到VWAP

直接上代码。我用的是pandas,因为处理时间序列太方便了。

import pandas as pd
import numpy as np

# 模拟一分钟级别的行情数据
data = {
    'time': pd.date_range('2024-01-15 09:30', periods=240, freq='1min'),
    'open': np.random.uniform(10.0, 10.5, 240),
    'high': np.random.uniform(10.2, 10.8, 240),
    'low': np.random.uniform(9.8, 10.3, 240),
    'close': np.random.uniform(10.0, 10.6, 240),
    'volume': np.random.randint(1000, 50000, 240)
}

df = pd.DataFrame(data)

# 计算每根K线的均价
df['avg_price'] = (df['open'] + df['high'] + df['low'] + df['close']) / 4

# 计算价格×成交量
df['pv'] = df['avg_price'] * df['volume']

# 计算累计值
df['cum_pv'] = df['pv'].cumsum()
df['cum_vol'] = df['volume'].cumsum()

# 计算VWAP
df['vwap'] = df['cum_pv'] / df['cum_vol']

print(df[['time', 'avg_price', 'volume', 'vwap']].head())

跑完这段代码,你会看到VWAP从开盘价附近开始,随着交易进行逐渐稳定。这就是VWAP的特性——越到收盘,越难被大单拉偏。

我的小技巧: 如果你做的是高频策略,建议用逐笔成交数据算VWAP,精度更高。但如果是日频策略,分钟级数据完全够用。

3.3 处理缺失数据:别让空值毁了你的VWAP

真实数据哪有那么干净?我遇到过最离谱的情况——某只股票下午开盘后连续30分钟没有成交。你想想看,如果直接跳过这些时间点,累计成交量就断了,VWAP会突然跳变。

我的处理思路是这样的:

  1. 识别缺失时段:检查时间序列是否连续
  2. 填充策略:用前一笔的VWAP填充,或者用插值法
  3. 成交量处理:缺失时段成交量设为0,不影响累计
# 检查是否有缺失时间
full_time = pd.date_range('2024-01-15 09:30', periods=240, freq='1min')
df = df.set_index('time').reindex(full_time)

# 填充均价:用前向填充
df['avg_price'] = df['avg_price'].ffill()

# 成交量缺失的填0
df['volume'] = df['volume'].fillna(0)

# 重新计算VWAP
df['pv'] = df['avg_price'] * df['volume']
df['cum_pv'] = df['pv'].cumsum()
df['cum_vol'] = df['volume'].cumsum()
df['vwap'] = df['cum_pv'] / df['cum_vol']
注意: 如果开盘前30分钟就缺失数据,前向填充会出问题。我一般会检查第一个有效数据点,如果缺失太多,直接跳过这只股票。

3.4 异常值处理:别被「乌龙指」带偏

有一次我回测一个VWAP策略,发现某天VWAP突然飙升了2%。查了半天,原来是某笔成交价格是正常价的3倍——典型的「乌龙指」或者数据错误。

怎么处理?我总结了三个步骤:

  • 价格异常:用布林带或Z-score检测,超出3倍标准差就标记
  • 成交量异常:单笔成交量超过日均量10倍,大概率是数据合并错误
  • 处理方式:直接剔除该笔数据,或者用前后均值替换
# 用Z-score检测价格异常
from scipy import stats

z_scores = np.abs(stats.zscore(df['avg_price']))
df['price_outlier'] = z_scores > 3

# 替换异常价格:用前一个正常值
df.loc[df['price_outlier'], 'avg_price'] = np.nan
df['avg_price'] = df['avg_price'].ffill()

# 重新计算VWAP
df['pv'] = df['avg_price'] * df['volume']
df['cum_pv'] = df['pv'].cumsum()
df['cum_vol'] = df['volume'].cumsum()
df['vwap'] = df['cum_pv'] / df['cum_vol']

嗯,这里要注意:剔除异常值后一定要重新计算累计值,否则VWAP会偏离真实值。

3.5 VWAP的滚动计算:捕捉日内动态变化

日级VWAP是静态的,但很多时候我们需要看「过去N笔」或者「过去N分钟」的VWAP。比如,我想知道最近1小时的成交重心在哪里。

滚动VWAP的计算逻辑:

# 滚动窗口为60分钟(1小时)
window = 60

# 计算滚动累计值
df['rolling_pv'] = df['pv'].rolling(window=window, min_periods=1).sum()
df['rolling_vol'] = df['volume'].rolling(window=window, min_periods=1).sum()

# 滚动VWAP
df['rolling_vwap'] = df['rolling_pv'] / df['rolling_vol']

# 对比日级VWAP和滚动VWAP
print(df[['time', 'vwap', 'rolling_vwap']].tail(10))

滚动VWAP比日级VWAP敏感得多。我曾在实盘中发现,当滚动VWAP和日级VWAP出现背离时,往往是短期趋势反转的信号。

实战经验: 我一般用两个窗口——60分钟滚动VWAP做短线参考,日级VWAP做全天基准。当价格同时突破这两个VWAP时,信号可靠性更高。

3.6 知识体系总览

下面这张图帮你理清本章的核心逻辑:

VWAP实战计算知识体系 原始行情数据 数据预处理 缺失值填充 | 异常值剔除 VWAP计算 累计PV / 累计VOL 日级VWAP(全天基准) 滚动VWAP(动态窗口) 异常值检测与修复 核心逻辑:数据清洗 → 累计计算 → 多维度输出

从这张图能看出来,VWAP计算的核心其实就三步:数据预处理、累计计算、多维度输出。但每一步都有坑,尤其是数据质量这块,我吃过不少亏。

好了,这一章的内容就到这里。代码你拿去跑一跑,把数据换成真实的日线或分钟线,感受一下VWAP的稳定性。下一章我们会聊TWAP——那个看起来简单,但实际坑更多的算法。


公众号:蓝海资料掘金营,微信deep3321