第1章:执行算法基础——TWAP、VWAP、POV

各位同学好,我是老张。在量化交易这个行当摸爬滚打了十来年,今天咱们来聊聊执行算法里最基础、也最实用的三个家伙:TWAP、VWAP 和 POV。

你可能会问,为什么一上来就讲这个?

嗯,我见过太多人,策略信号写得漂亮,一到实盘就亏钱。为什么?因为执行太糙。你想想看,一个大单子直接砸下去,市场瞬间就滑出去好几个价位。说白了,策略负责「买什么、卖什么」,执行算法负责「怎么买、怎么卖」。今天这三个,就是执行算法的入门必修课。

一、TWAP:时间加权平均价格

1.1 原理

TWAP 的全称是 Time-Weighted Average Price。它的逻辑很简单:把一个大单子,均匀地拆成小单子,在固定的时间间隔里发出去

举个例子。你想在 10 分钟内买入 100 万股。TWAP 的做法是:把 10 分钟切成 10 个 1 分钟的小段,每段发 10 万股。不管市场怎么波动,我就按时间切。

为什么会有人用这个?

我个人习惯把它叫做「懒人算法」。当你不确定市场短期走势,又不想暴露大单意图时,TWAP 是最稳妥的选择。它追求的是执行价格尽可能接近这段时间的平均价

核心公式:
每段下单量 = 总订单量 / 总时间段数

1.2 实现

代码其实不复杂。我直接给一个 Python 的简化版本,你们感受一下:

class TWAP:
    def __init__(self, total_qty, total_seconds, interval=60):
        self.total_qty = total_qty          # 总数量
        self.total_seconds = total_seconds  # 总执行时间(秒)
        self.interval = interval            # 每次发单间隔(秒)
        self.slices = total_seconds // interval
        self.qty_per_slice = total_qty / self.slices
        self.current_slice = 0

    def next_order(self):
        if self.current_slice >= self.slices:
            return None
        order = {
            'time': self.current_slice * self.interval,
            'qty': self.qty_per_slice
        }
        self.current_slice += 1
        return order

# 使用示例
twap = TWAP(total_qty=1000000, total_seconds=600, interval=60)
for i in range(10):
    print(twap.next_order())

你看,核心逻辑就这几行。但实际生产环境里,要考虑的东西多得多——比如滑点补偿、市场冲击、订单排队等等。我曾经在一个高频做市项目里,就因为 TWAP 的切片粒度太粗,被对手盘盯上了,连续吃了好几个亏。后来我把切片粒度从 60 秒改成了 5 秒,效果立竿见影。

我的经验:TWAP 的切片数量建议在 10-50 段之间。太少容易被识别,太多会增加交易成本。

二、VWAP:成交量加权平均价格

2.1 原理

VWAP 的全称是 Volume-Weighted Average Price。它比 TWAP 聪明一点——不是按时间均匀拆单,而是按历史成交量分布来拆

你想想看,市场在开盘和收盘时成交量通常很大,中午休息时成交量很小。如果还用 TWAP 均匀发单,中午那会儿你发出去的订单可能半天成交不了,反而暴露了意图。

VWAP 的做法是:先统计过去 N 天的成交量分布,然后在成交量大的时段多发单,成交量小的时段少发单。这样你的订单更容易被市场消化。

核心公式:
每段下单量 = 总订单量 × (该时段历史成交量 / 总历史成交量)

2.2 实现

实现 VWAP 需要先准备历史成交量数据。我一般取过去 20 个交易日的分钟级成交量,做平均:

import numpy as np

class VWAP:
    def __init__(self, total_qty, hist_volume_profile):
        """
        hist_volume_profile: list of float, 每个时间段的平均成交量
        """
        self.total_qty = total_qty
        self.volume_profile = np.array(hist_volume_profile)
        self.total_volume = self.volume_profile.sum()
        self.weights = self.volume_profile / self.total_volume
        self.current_slice = 0

    def next_order(self):
        if self.current_slice >= len(self.weights):
            return None
        qty = self.total_qty * self.weights[self.current_slice]
        order = {
            'time': self.current_slice,  # 假设每段1分钟
            'qty': round(qty, 0)
        }
        self.current_slice += 1
        return order

# 假设一天有240个1分钟段,每个段的历史平均成交量
hist_vol = [1000 + np.random.randint(-200, 200) for _ in range(240)]
vwap = VWAP(total_qty=1000000, hist_volume_profile=hist_vol)
for i in range(10):
    print(vwap.next_order())

这里有个坑,我必须提醒你:历史成交量分布 ≠ 今天的成交量分布。我记得有一次做 A 股 VWAP,那天突然出了个重大利好,开盘成交量暴增 5 倍,我的 VWAP 算法还按历史数据慢慢发单,结果收盘一看,执行价格比 VWAP 基准高了 0.3%。从那以后,我加了一个实时成交量修正模块——每 5 分钟重新计算一次剩余订单的分配权重。

避坑指南:VWAP 对「历史数据质量」极其敏感。如果遇到节假日、财报日、除权除息日,一定要剔除异常数据,否则算法会严重失真。

三、POV:成交量百分比

3.1 原理

POV 的全称是 Percentage of Volume。它的思路和前两个完全不同——不关心时间,也不关心历史,只关心「我现在的订单量占市场成交量的比例」

举个例子。你设定 POV = 10%,意思是:市场每成交 100 股,我就发 10 股。市场成交快,我就发得快;市场成交慢,我就发得慢。说白了,POV 是「跟屁虫」策略——市场怎么动,我就怎么动。

我个人特别喜欢 POV 的一点是:它天然具有自适应能力。市场活跃时你参与得多,市场冷清时你参与得少,不会像 TWAP 那样在流动性差的时候硬塞单子。

核心公式:
本次发单量 = 目标参与率 × 最近一段时间的市场成交量

3.2 实现

POV 的实现需要实时获取市场成交量数据。这里给一个简化版:

class POV:
    def __init__(self, total_qty, participation_rate=0.1):
        self.total_qty = total_qty          # 总订单量
        self.remaining_qty = total_qty
        self.participation_rate = participation_rate  # 目标参与率,如0.1表示10%
        self.last_market_vol = 0

    def update_market_volume(self, recent_vol):
        """更新最近一段时间的市场成交量"""
        self.last_market_vol = recent_vol

    def next_order(self):
        if self.remaining_qty <= 0:
            return None
        # 按参与率计算本次发单量
        order_qty = self.last_market_vol * self.participation_rate
        # 不能超过剩余量
        order_qty = min(order_qty, self.remaining_qty)
        self.remaining_qty -= order_qty
        return {
            'qty': round(order_qty, 0),
            'remaining': self.remaining_qty
        }

# 模拟使用
pov = POV(total_qty=1000000, participation_rate=0.1)
market_volumes = [5000, 8000, 3000, 12000, 7000]  # 模拟市场每分钟成交量
for vol in market_volumes:
    pov.update_market_volume(vol)
    order = pov.next_order()
    print(f"市场成交{vol}股,我发{order['qty']}股,剩余{order['remaining']}股")

嗯,这里要注意:参与率不能设得太高。我曾经在一个小盘股上设了 15% 的 POV,结果市场一活跃,我的订单直接占了 30% 的成交量,瞬间把价格推高了 2%。后来我加了一个上限保护——单次发单量不超过总订单量的 5%。

我的建议:POV 的参与率一般设在 5%-15% 之间。大盘股可以高一点,小盘股一定要低。另外,POV 适合「追涨杀跌」的场景,如果你要做「低买高卖」,TWAP 或 VWAP 可能更合适。

四、三种算法对比

说了这么多,咱们来做个总结。我画了一张对比图,方便你理解:

三种执行算法核心逻辑对比 TWAP 时间加权平均价格 核心逻辑: 按时间均匀拆单 每段下单量 = 总量 / 段数 适用场景: • 市场走势不明 • 不想暴露意图 • 流动性较好 VWAP 成交量加权平均价格 核心逻辑: 按历史成交量分布拆单 每段下单量 = 总量 × 权重 适用场景: • 追求基准跟踪 • 大单执行 • 有历史数据支撑 POV 成交量百分比 核心逻辑: 按市场成交量比例发单 每段下单量 = 市场量 × 比例 适用场景: • 流动性变化大 • 需要自适应 • 小盘股/低流动性 选择建议:流动性好→TWAP,有基准需求→VWAP,流动性差→POV

最后,我再用表格给你对比一下三个算法的关键参数:

特性 TWAP VWAP POV
核心变量 时间 历史成交量 实时成交量
自适应能力 弱(依赖历史) 强(实时跟随)
市场冲击 中等 较低 低(但需控制参与率)
实现难度 中高(需实时数据)
典型参与率 N/A N/A 5%-15%
切片数量 10-50段 20-240段 动态(取决于市场)

好了,这一章的内容就到这里。TWAP、VWAP、POV 这三个算法,说白了就是三种不同的「拆单哲学」。TWAP 相信时间,VWAP 相信历史,POV 相信市场。没有绝对的好坏,关键看你的交易场景和风险偏好。

下一章,咱们聊聊这些算法在实际生产环境中的「坑」——比如如何处理订单排队、如何应对市场冲击、如何做滑点补偿。这些都是我在项目里真金白银换来的教训,到时候一一分享给你们。


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