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的特性——越到收盘,越难被大单拉偏。
3.3 处理缺失数据:别让空值毁了你的VWAP
真实数据哪有那么干净?我遇到过最离谱的情况——某只股票下午开盘后连续30分钟没有成交。你想想看,如果直接跳过这些时间点,累计成交量就断了,VWAP会突然跳变。
我的处理思路是这样的:
- 识别缺失时段:检查时间序列是否连续
- 填充策略:用前一笔的VWAP填充,或者用插值法
- 成交量处理:缺失时段成交量设为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']
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出现背离时,往往是短期趋势反转的信号。
3.6 知识体系总览
下面这张图帮你理清本章的核心逻辑:
从这张图能看出来,VWAP计算的核心其实就三步:数据预处理、累计计算、多维度输出。但每一步都有坑,尤其是数据质量这块,我吃过不少亏。
好了,这一章的内容就到这里。代码你拿去跑一跑,把数据换成真实的日线或分钟线,感受一下VWAP的稳定性。下一章我们会聊TWAP——那个看起来简单,但实际坑更多的算法。
公众号:蓝海资料掘金营,微信deep3321