19. 算法交易执行:TWAP、VWAP、冰山订单、减少市场冲击
做基差交易的朋友,很多都栽在同一个坑里——策略逻辑没问题,信号也对,但一执行就亏钱。为什么?因为你的订单把市场打穿了。
我刚开始做实盘的时候,就吃过这个亏。策略回测年化30%,实盘一跑,直接变成负收益。后来一查,全是执行成本吃掉利润。说白了,你看到的价差是纸面富贵,真正成交的价格才是你的命。
这一章,我们就来聊聊怎么把订单“优雅”地塞进市场。核心就四个字:减少冲击。
19.1 市场冲击:你才是自己的对手盘
先想一个问题:你下了一笔大单,市场会怎么反应?
如果你买100手,盘口上的卖单瞬间被吃掉,价格直接往上跳。你还没买完,成本已经上去了。这就是市场冲击——你的交易行为本身改变了价格。
我见过最夸张的一次,有人用市价单扫货,直接把基差从-5块拉到+3块。等他买完,基差已经没了,还倒亏。嗯,这就是典型的“自己把自己玩死”。
市场冲击分两种:
- 瞬时冲击:订单瞬间吃掉流动性,价格跳变
- 持久冲击:你的订单改变了市场供需结构,价格回不去
对于基差交易来说,持久冲击更致命。因为你的套利逻辑依赖于价差回归,但如果你把价差打没了,还回归个啥?
核心原则:你的订单规模,不能超过市场能“消化”的量。否则,你就是在跟自己的钱过不去。
19.2 TWAP:时间加权平均价格
TWAP,全称Time-Weighted Average Price。说白了,就是把一个大单拆成很多小单,每隔固定时间扔一笔出去。
举个例子:你想买1000手,分10分钟执行。那就每分钟扔100手。不管市场怎么走,我就按时间切。
代码实现很简单:
def twap_executor(total_qty, total_seconds, interval_seconds):
"""
TWAP算法执行器
:param total_qty: 总数量
:param total_seconds: 总执行时间(秒)
:param interval_seconds: 每次下单间隔(秒)
"""
num_slices = total_seconds // interval_seconds
qty_per_slice = total_qty // num_slices
for i in range(num_slices):
# 这里调用你的下单接口
place_order(qty=qty_per_slice, price='market')
time.sleep(interval_seconds)
# 处理余数
remainder = total_qty % num_slices
if remainder > 0:
place_order(qty=remainder, price='market')
TWAP的优点是什么?简单、透明、容易预测。缺点也很明显——它不关心市场状态。如果市场正在剧烈波动,你还在机械地往外扔单,那跟送钱没区别。
我个人习惯,TWAP只用在流动性比较好的品种上。比如股指期货主力合约,盘口厚,冲击小,用TWAP就够了。
19.3 VWAP:成交量加权平均价格
VWAP,Volume-Weighted Average Price。这个比TWAP聪明一点——它跟着成交量走。
逻辑是这样的:市场成交量大的时候,我多下点;成交量小的时候,我少下点。这样我的订单就“藏”在市场正常的交易流里,不容易被发现。
实现VWAP需要先知道历史成交量分布。比如过去10天,每分钟的成交量占比是多少:
def vwap_executor(total_qty, volume_profile):
"""
VWAP算法执行器
:param total_qty: 总数量
:param volume_profile: 成交量分布,格式为[(timestamp, volume_ratio), ...]
"""
for timestamp, ratio in volume_profile:
qty = int(total_qty * ratio)
if qty > 0:
place_order(qty=qty, price='market')
time.sleep(60) # 假设每分钟执行一次
这里有个坑——历史成交量分布跟实时的不一定一样。我曾经在某个品种上,用历史数据算好了VWAP计划,结果当天突然放量,我的订单全被打在了高点。
避坑指南:VWAP最好结合实时成交量做动态调整。别死板地按历史比例走,市场会教你做人。
19.4 冰山订单:藏在水面下的巨兽
冰山订单,顾名思义,只露出冰山一角。你实际想买1000手,但盘口上只显示100手。等这100手成交了,再自动挂出新的100手。
这样做的好处很明显——别人看不到你的真实意图。你想想看,如果所有人都知道你要买1000手,那价格不飞起来才怪。
实现冰山订单,核心逻辑是这样的:
class IcebergOrder:
def __init__(self, total_qty, display_qty, price):
self.total_qty = total_qty
self.display_qty = display_qty
self.price = price
self.remaining_qty = total_qty
def execute(self):
while self.remaining_qty > 0:
# 挂出冰山一角
qty = min(self.display_qty, self.remaining_qty)
order_id = place_limit_order(qty=qty, price=self.price)
# 等待成交
wait_for_fill(order_id)
# 更新剩余数量
self.remaining_qty -= qty
嗯,这里要注意:冰山订单不是万能的。如果市场深度不够,你的冰山一角也可能被吃掉,然后价格就跑了。我建议在盘口厚度至少是你冰山量的5倍以上时,再用这个策略。
19.5 减少市场冲击的组合策略
实际交易中,我不会只用一种算法。我会根据市场状态动态切换:
| 市场状态 | 推荐算法 | 原因 |
|---|---|---|
| 高流动性、低波动 | TWAP | 简单高效,冲击小 |
| 高流动性、高波动 | VWAP | 跟随成交量,避免追涨杀跌 |
| 低流动性 | 冰山订单 | 隐藏意图,减少价格影响 |
| 极端行情 | 暂停交易 | 保命要紧 |
我自己的经验是,把三种算法组合起来用效果最好。比如:先用冰山订单慢慢吸筹,等市场成交量上来了,切换到VWAP加速执行,最后用TWAP收尾。
重要提醒:算法交易不是万能的。如果你的策略本身有问题,再好的执行算法也救不了你。先确保策略逻辑正确,再考虑执行优化。
19.6 实战中的避坑指南
最后,分享几个我踩过的坑:
- 别在开盘和收盘时用大单:这两个时间段波动最大,你的订单会被放大冲击。我曾经在收盘前用VWAP扫货,结果直接把基差打穿了,第二天才回来。
- 注意交易所的费率结构:有些交易所对挂单和吃单收费不同。如果你用市价单(吃单),成本可能比限价单(挂单)高好几倍。算清楚再动手。
- 别迷信算法:算法只是工具,不是圣杯。市场环境变了,算法也要跟着调。我见过有人一套TWAP用三年,结果市场结构变了,亏得底裤都不剩。
好了,这一章的内容就到这里。记住,执行是策略的最后一公里,也是最容易出问题的地方。把订单管理好了,你的基差交易才算真正落地。