3、VWAP策略的经典拆分逻辑:时间切片与成交量分布模型
做VWAP执行,最核心的问题就一个:怎么把大单拆成小单,跟着市场的节奏走。
我刚开始做算法交易那会儿,总觉得VWAP很简单——不就是按时间平均分嘛。结果实盘跑起来,偏差大得吓人。后来才明白,时间切片只是基础,真正的精髓在成交量分布模型里。
今天咱们就把这两个核心逻辑拆开揉碎了讲清楚。
3.1 时间切片:最朴素的拆分方式
时间切片,说白了就是把交易时段切成N等份,每份分配等量的订单。
举个例子:你要在4小时内买入100万股。如果切成240个1分钟切片,每个切片就分配约4167股。
代码实现很简单:
def time_slice(total_qty, start_time, end_time, slice_seconds=60):
"""
时间切片拆分
total_qty: 总成交量
start_time/end_time: 交易时段
slice_seconds: 切片长度(秒)
"""
total_seconds = (end_time - start_time).total_seconds()
num_slices = int(total_seconds / slice_seconds)
slice_qty = total_qty / num_slices
slices = []
for i in range(num_slices):
slice_time = start_time + timedelta(seconds=i * slice_seconds)
slices.append({
'time': slice_time,
'qty': slice_qty
})
return slices
嗯,这里要注意——时间切片有个致命缺陷:它假设成交量在时间上是均匀分布的。但真实市场根本不是这样。
我遇到过最惨的一次,上午10点那波放量,我的切片量根本不够用,眼睁睁看着价格往上冲。到了下午缩量阶段,我又被迫把剩下的单子砸进去,直接把价格打穿了。
所以,时间切片只适合做基准,不能直接用于实盘。
3.2 成交量分布模型:这才是VWAP的灵魂
VWAP的全称是Volume-Weighted Average Price。你想想看,既然要按成交量加权,那拆分逻辑当然也要跟着成交量走。
核心思想很简单:成交量大的时段多分单,成交量小的时段少分单。
怎么做?需要先预测每个时间片的成交量占比。
3.2.1 历史成交量分布曲线
我习惯用过去20个交易日的分钟级成交量数据,计算每个时间片的平均占比。
def volume_profile(hist_data, slice_seconds=60):
"""
计算历史成交量分布
hist_data: DataFrame,包含timestamp和volume列
"""
# 提取交易时段(假设9:30-15:00)
market_open = pd.Timestamp('09:30:00').time()
market_close = pd.Timestamp('15:00:00').time()
# 过滤交易时段数据
mask = (hist_data['timestamp'].dt.time >= market_open) & \
(hist_data['timestamp'].dt.time <= market_close)
trading_data = hist_data[mask].copy()
# 按时间切片分组
trading_data['time_bin'] = trading_data['timestamp'].dt.floor(f'{slice_seconds}S')
# 计算每个切片的成交量占比
profile = trading_data.groupby('time_bin')['volume'].sum()
profile = profile / profile.sum()
return profile
这张图就是典型的A股成交量分布曲线:
典型特征:
- 开盘前30分钟:成交量最大,占比约20%-25%
- 上午10:30-11:30:成交量逐渐萎缩
- 下午开盘13:00-13:30:有一波小放量
- 尾盘14:30-15:00:再次放量,占比约15%-18%
我用SVG画了一张成交量分布曲线图,方便你直观理解:
3.2.2 实时成交量修正
光靠历史数据不够。市场每天都在变,必须引入实时修正机制。
我常用的方法是:
def adaptive_vwap_split(total_qty, current_time, hist_profile, real_volume, alpha=0.7):
"""
自适应VWAP拆分
alpha: 历史权重(0-1),越大越依赖历史数据
"""
# 获取当前时间片的预期占比
expected_pct = hist_profile.get(current_time, 0)
# 获取已实现的成交量占比
realized_pct = real_volume / total_qty if total_qty > 0 else 0
# 动态调整:如果实际成交量已经偏大,就减少后续分配
adjustment = 1 - (realized_pct - expected_pct) * 2
# 限制调整幅度
adjustment = max(0.5, min(1.5, adjustment))
# 计算当前切片应分配的量
slice_qty = total_qty * expected_pct * adjustment
return slice_qty
实战技巧:
alpha值怎么调?我一般这样设置:
- 开盘前30分钟:alpha=0.8(历史数据更可靠)
- 盘中:alpha=0.6(实时数据更重要)
- 尾盘:alpha=0.7(折中处理)
3.3 两种模型的对比
| 维度 | 时间切片 | 成交量分布模型 |
|---|---|---|
| 核心逻辑 | 按时间均匀分配 | 按成交量加权分配 |
| VWAP跟踪精度 | 低(偏差通常>0.5%) | 高(偏差可控制在0.1%以内) |
| 对市场冲击 | 大(缩量时硬塞) | 小(跟随流动性) |
| 实现复杂度 | 简单 | 中等 |
| 适用场景 | 小单、流动性极好的股票 | 大单、流动性一般的股票 |
3.4 我踩过的坑
曾经有一次,我过于依赖历史成交量分布,没考虑当天有重大利好公告。结果开盘成交量是平时的3倍,我的模型还在按历史数据慢慢分配,导致大量订单没来得及成交,VWAP偏差直接爆表。
从那以后,我加了一个异常检测模块:
def anomaly_detection(current_volume, hist_mean, hist_std, threshold=3):
"""
成交量异常检测
如果当前成交量超过历史均值±3倍标准差,触发调整
"""
z_score = (current_volume - hist_mean) / hist_std
if abs(z_score) > threshold:
return True, z_score
return False, z_score
避坑指南:
我曾经吃过亏的几个点:
- 不要用全部历史数据,只用最近20-30个交易日
- 剔除异常交易日(如分红、除权日)的数据
- 遇到停牌复牌,成交量分布会完全失效,需要特殊处理
3.5 实战中的选择建议
说白了,没有完美的模型,只有适合的模型。
我个人建议:
- 小单(<总成交量1%):直接用时间切片就行,省事
- 中单(1%-5%):用成交量分布模型,alpha设0.7
- 大单(>5%):必须用成交量分布模型,还要加上实时修正和异常检测
嗯,这里再补充一句——模型再好,也抵不过市场突变。所以一定要留一手:当模型预测和实际偏差超过阈值时,果断切换到保守模式,宁可少成交,也别把价格打飞了。
这就是VWAP拆分的核心逻辑。你想想看,其实没那么复杂——跟着成交量走,别跟市场对着干。
公众号:蓝海资料掘金营,微信deep3321