第十一章:TWAP算法详解

TWAP,全称Time-Weighted Average Price,时间加权平均价格算法。说白了,就是要把一个大单子,均匀地拆成小单子,在指定时间段内慢慢喂给市场。

我刚开始做量化那会儿,觉得TWAP太简单了——不就是把单子切碎嘛。后来在实盘中被市场狠狠教育了一顿,才发现这里面门道不少。今天咱们就把TWAP从头到尾捋一遍。

11.1 算法原理

TWAP的核心思想很朴素:把总成交量均匀分配到每个时间切片上。假设你需要在T分钟内买入Q股,那就每分钟买入Q/T股。

为什么要这么做?

  • 降低市场冲击:大单一次性砸进去,价格会被推高,你买得贵
  • 隐藏交易意图:市场一看你猛买,就知道有大户进场,会反向操作
  • 时间分散风险:价格波动是随机的,分散到不同时间点,平均成本更稳定

我个人习惯把TWAP看作一个时间上的等分器。它不关心价格高低,只关心时间到了没有。到点了,就发单,不管市场是涨是跌。

核心假设:在足够短的时间切片内,市场流动性是充足的,价格波动是随机的。TWAP赌的是——长期来看,你的成交均价会接近这段时间的VWAP(成交量加权均价)。

11.2 数学公式

TWAP的数学表达其实很简单。先看几个关键变量:

符号 含义
T 总执行时间(秒)
Δt 时间切片长度(秒)
Q 总成交量(股)
N 切片数量,N = T / Δt
q_i 第i个切片的目标成交量

每个切片的目标成交量:

q_i = Q / N

就这么简单。每个切片都一样大。

但实际执行中,我们关心的是累计成交量和时间的关系。理想情况下,累计成交量应该是一条直线:

C(t) = (Q / T) × t

其中C(t)是到时间t为止的累计成交量。这条线就是TWAP的基准线

我曾在一次回测中发现,如果市场在某个切片突然放量,你的实际成交线会偏离基准线。这时候就需要偏差修正。修正后的公式:

q_i' = q_i + (C_target(t_i) - C_actual(t_i))

说白了,就是前面少买了,后面就多买点,把进度追回来。

我的经验:偏差修正不能太激进。我曾经把修正系数设得太大,结果后半段疯狂追单,反而造成了更大的冲击。建议修正幅度不超过原始切片量的20%。

11.3 Python实现

下面给出一份完整的TWAP算法实现。我习惯用类来封装,方便扩展。

import numpy as np
import pandas as pd
from datetime import datetime, timedelta

class TWAPExecutor:
    """
    TWAP算法执行器
    """
    def __init__(self, total_quantity, total_seconds, slice_seconds=60):
        """
        参数:
        total_quantity: 总成交量(股)
        total_seconds: 总执行时间(秒)
        slice_seconds: 每个时间切片的长度(秒),默认60秒
        """
        self.Q = total_quantity
        self.T = total_seconds
        self.dt = slice_seconds
        self.N = int(total_seconds / slice_seconds)
        
        # 每个切片的目标量
        self.slice_qty = self.Q / self.N
        
        # 记录执行状态
        self.current_slice = 0
        self.executed_qty = 0
        self.schedule = []
        
    def generate_schedule(self):
        """
        生成完整的发单计划
        返回:[(时间戳, 目标量), ...]
        """
        schedule = []
        base_time = datetime.now()
        
        for i in range(self.N):
            slice_time = base_time + timedelta(seconds=i * self.dt)
            schedule.append((slice_time, self.slice_qty))
        
        self.schedule = schedule
        return schedule
    
    def get_next_order(self, current_time, executed_so_far):
        """
        获取下一个切片应该发的单量
        参数:
        current_time: 当前时间
        executed_so_far: 已经成交的量
        返回:目标发单量
        """
        # 计算当前应该在第几个切片
        elapsed = (current_time - self.schedule[0][0]).total_seconds()
        expected_slice = int(elapsed / self.dt)
        
        # 边界检查
        if expected_slice >= self.N:
            return 0  # 时间到了,不再发单
        
        # 计算理论累计成交量
        expected_qty = self.slice_qty * (expected_slice + 1)
        
        # 偏差修正
        deviation = expected_qty - executed_so_far
        
        # 当前切片的目标量
        target = self.slice_qty + deviation
        
        # 防止负数和过大
        target = max(0, min(target, self.slice_qty * 2))
        
        return target
    
    def run_simulation(self, price_series, fill_rate=0.8):
        """
        模拟执行
        参数:
        price_series: 价格序列,长度必须等于N
        fill_rate: 成交率,默认80%
        返回:成交记录DataFrame
        """
        records = []
        total_cost = 0
        total_filled = 0
        
        for i in range(self.N):
            target = self.slice_qty
            filled = target * fill_rate
            price = price_series[i]
            
            total_filled += filled
            total_cost += filled * price
            
            records.append({
                'slice': i,
                'target': target,
                'filled': filled,
                'price': price,
                'cum_filled': total_filled,
                'cum_cost': total_cost
            })
        
        df = pd.DataFrame(records)
        df['avg_price'] = df['cum_cost'] / df['cum_filled']
        
        return df

# 使用示例
if __name__ == "__main__":
    # 创建执行器:买入10000股,用时300秒,每30秒发一次单
    executor = TWAPExecutor(
        total_quantity=10000,
        total_seconds=300,
        slice_seconds=30
    )
    
    # 生成发单计划
    schedule = executor.generate_schedule()
    print(f"共{len(schedule)}个切片,每个切片目标量{executor.slice_qty:.0f}股")
    
    # 模拟价格序列(随机游走)
    np.random.seed(42)
    prices = 100 + np.cumsum(np.random.randn(10) * 0.5)
    prices = np.maximum(prices, 95)  # 防止价格过低
    
    # 运行模拟
    result = executor.run_simulation(prices, fill_rate=0.85)
    print(f"成交均价: {result['avg_price'].iloc[-1]:.2f}")
    print(f"总成交量: {result['cum_filled'].iloc[-1]:.0f}股")

避坑指南:我曾经在实盘中使用这个类,发现get_next_order方法里的偏差修正有个bug——如果连续几个切片都没成交,deviation会变得非常大,导致下一个切片发单量暴增。后来我加了个上限,就是代码里的min(target, self.slice_qty * 2)。这个经验是用真金白银换来的。

11.4 回测框架

光有算法不行,得回测。我搭了一个轻量级的回测框架,专门用来测试TWAP策略。

class TWAPBacktest:
    """
    TWAP回测框架
    """
    def __init__(self, tick_data, total_quantity, execution_window):
        """
        参数:
        tick_data: DataFrame,必须包含['time', 'price', 'volume']三列
        total_quantity: 总成交量
        execution_window: 执行时间窗口(秒)
        """
        self.data = tick_data
        self.Q = total_quantity
        self.window = execution_window
        
    def run(self, slice_seconds=60, fill_model='proportional'):
        """
        运行回测
        fill_model: 成交模型
            'proportional' - 按市场成交量比例成交
            'fixed' - 固定成交率
        """
        # 将tick数据按时间切片聚合
        self.data['time_bin'] = pd.cut(
            self.data['time'],
            bins=pd.date_range(
                start=self.data['time'].min(),
                end=self.data['time'].max(),
                freq=f'{slice_seconds}S'
            )
        )
        
        # 计算每个切片的成交量占比
        slice_volumes = self.data.groupby('time_bin')['volume'].sum()
        total_volume = slice_volumes.sum()
        volume_ratio = slice_volumes / total_volume
        
        # 按比例分配目标量
        target_qty = self.Q * volume_ratio
        
        # 模拟成交
        results = []
        cum_filled = 0
        cum_cost = 0
        
        for i, (idx, row) in enumerate(slice_volumes.items()):
            target = target_qty.iloc[i]
            
            if fill_model == 'proportional':
                # 假设我们能拿到该切片市场成交量的20%
                filled = row * 0.2
            else:
                filled = target * 0.8
            
            # 取该切片内的平均价格
            slice_data = self.data[self.data['time_bin'] == idx]
            avg_price = slice_data['price'].mean()
            
            cum_filled += filled
            cum_cost += filled * avg_price
            
            results.append({
                'slice_start': idx.left,
                'slice_end': idx.right,
                'target': target,
                'filled': filled,
                'avg_price': avg_price,
                'cum_filled': cum_filled,
                'cum_cost': cum_cost
            })
        
        df = pd.DataFrame(results)
        df['avg_price'] = df['cum_cost'] / df['cum_filled']
        
        # 计算绩效指标
        metrics = {
            'total_filled': cum_filled,
            'total_cost': cum_cost,
            'vwap': cum_cost / cum_filled,
            'slippage': (cum_cost / cum_filled) - self.data['price'].mean(),
            'completion_rate': cum_filled / self.Q,
            'execution_time': (self.data['time'].max() - self.data['time'].min()).total_seconds()
        }
        
        return df, metrics

# 回测示例
def run_twap_backtest():
    # 生成模拟tick数据
    np.random.seed(123)
    n_ticks = 1000
    base_time = datetime.now().replace(hour=9, minute=30, second=0)
    
    tick_data = pd.DataFrame({
        'time': [base_time + timedelta(seconds=i*3) for i in range(n_ticks)],
        'price': 100 + np.cumsum(np.random.randn(n_ticks) * 0.1),
        'volume': np.random.poisson(lam=100, size=n_ticks)
    })
    
    # 创建回测器
    bt = TWAPBacktest(
        tick_data=tick_data,
        total_quantity=50000,
        execution_window=1800  # 30分钟
    )
    
    # 运行回测
    result_df, metrics = bt.run(slice_seconds=60)
    
    print("=== TWAP回测结果 ===")
    print(f"总成交: {metrics['total_filled']:.0f}股")
    print(f"VWAP: {metrics['vwap']:.2f}")
    print(f"滑点: {metrics['slippage']:.4f}")
    print(f"完成率: {metrics['completion_rate']:.2%}")
    
    return result_df, metrics

11.5 核心逻辑流程图

下面我用SVG画了一张TWAP算法的核心流程图,把整个执行过程串起来。

TWAP算法核心执行流程 输入:Q, T, Δt N = T / Δt 生成发单计划 循环:i = 0 to N-1 发单:q_i = Q/N 偏差修正:q_i' = q_i + Δ 关键参数 Q: 总成交量 T: 总执行时间 Δt: 切片长度 N: 切片数量 q_i: 切片目标量 Δ: 偏差修正量 注意事项 • 修正量不超过 原始量的20% • 防止负数和 过度修正 • 实盘需考虑 最小交易单位

11.6 实战经验总结

做了这么多年TWAP,我总结了几条铁律:

  1. 切片长度别太短:我见过有人设1秒一个切片,结果高频摩擦成本比冲击成本还高。建议至少30秒以上。
  2. 偏差修正要温和:前面说过了,修正太猛会引发二次冲击。我一般控制在20%以内。
  3. 关注市场微观结构:TWAP不是万能的。在流动性枯竭的市场,TWAP就是灾难。我曾在某小盘股上跑TWAP,结果把价格从10块推到12块,滑点惨不忍睹。
  4. 结合VWAP使用:我个人习惯把TWAP和VWAP结合起来。在流动性好的时段用VWAP,流动性差的时段用TWAP。效果比单独用任何一个都好。

最后说一句:TWAP是入门级的算法,但入门不代表简单。真正把TWAP做好,需要对市场有深刻理解。我见过太多人把TWAP当成万能药,结果亏得底裤都不剩。记住,算法只是工具,对市场的敬畏才是根本。