25. 基差交易的算法执行:TWAP/VWAP算法、冰山订单、拆单策略、冲击成本控制
基差交易的核心,说白了就是「吃价差」。但当你拿着几百万甚至几千万的资金进场时,你会发现一个残酷的现实:你还没吃完价差,市场已经被你推走了。这就是冲击成本——你每下一笔单,都在跟市场博弈。
我早年做股指期货期现套利时,吃过一次大亏。当时基差到了5个点,我信心满满地一把梭进去,结果现货端还没买完,期货价格已经往回跑了3个点。嗯,那次之后我才真正明白:策略再好,执行不到位,一切都是白搭。
为什么需要算法执行?
基差交易通常涉及双边操作——一边买现货,一边卖期货(或者反过来)。你想想看,如果两边的订单同时砸进去,市场会怎么反应?
- 流动性冲击:大单瞬间吃掉盘口,价格剧烈波动
- 信号暴露:别人一看你的挂单,就知道有人在套利,马上跟风或反向操作
- 滑点放大:基差本来只有2个点,结果执行完变成了4个点,套利变亏损
所以,我们需要一套「隐形」的执行方案。把大单拆成小单,分散到时间或成交量中,让市场感觉不到你的存在。
核心目标:在基差窗口关闭之前,以最小的冲击成本完成双边建仓或平仓。
TWAP算法:时间加权平均价格
TWAP(Time-Weighted Average Price)是最基础的算法。它的逻辑很简单:把总订单量均匀地分配到每个时间切片里。
比如你要买1000手,交易时间还剩100分钟。那就每分钟下10手,雷打不动。
def twap_executor(total_qty, total_seconds, interval=60):
"""
TWAP算法执行器
:param total_qty: 总下单量
:param total_seconds: 总执行时间(秒)
:param interval: 每次下单间隔(秒)
"""
slices = total_seconds // interval
qty_per_slice = total_qty // slices
orders = []
for i in range(slices):
# 模拟下单逻辑
order = {
'time': i * interval,
'qty': qty_per_slice,
'price': 'market' # 通常用市价单
}
orders.append(order)
print(f"第{i+1}次下单: 时间={order['time']}s, 数量={order['qty']}")
return orders
# 示例:1000手,10分钟执行完,每30秒下一单
twap_executor(1000, 600, 30)
TWAP的优点是什么?简单、透明、容易预测。但缺点也很明显——它完全无视市场成交量。如果某个时间段市场很冷清,你硬塞进去10手,照样会造成冲击。
我的经验:TWAP适合流动性较好的品种,比如股指期货主力合约。对于流动性差的品种,我建议把间隔拉长,或者配合限价单使用。
VWAP算法:成交量加权平均价格
VWAP(Volume-Weighted Average Price)比TWAP聪明一点。它不按时间均匀分配,而是按历史成交量分布来分配。
举个例子:根据历史数据,上午9:30-10:00的成交量占全天的20%,那这段时间你就下20%的单子。10:00-10:30占15%,那就下15%。
def vwap_executor(total_qty, volume_profile):
"""
VWAP算法执行器
:param total_qty: 总下单量
:param volume_profile: 成交量分布字典 {时间段: 成交量占比}
"""
orders = []
for period, ratio in volume_profile.items():
qty = int(total_qty * ratio)
order = {
'period': period,
'qty': qty,
'ratio': ratio
}
orders.append(order)
print(f"时段{period}: 下单{qty}手, 占比{ratio*100:.1f}%")
return orders
# 示例:根据历史成交量分布下单
volume_profile = {
'09:30-10:00': 0.20,
'10:00-10:30': 0.15,
'10:30-11:00': 0.12,
'11:00-11:30': 0.10,
'13:00-13:30': 0.18,
'13:30-14:00': 0.15,
'14:00-14:30': 0.10
}
vwap_executor(1000, volume_profile)
VWAP的好处是:你在成交量大的时候多下,成交量小的时候少下,冲击成本自然降低。但这里有个坑——历史分布不代表未来分布。万一今天突然放量或者缩量,你的计划就乱了。
我曾经踩过的坑:有一次做国债期货基差交易,我用了VWAP算法,但那天正好有宏观数据发布,成交量分布完全变了。结果我在低流动性时段下了大量单子,冲击成本比TWAP还高。后来我加了一个「实时成交量修正」模块,每5分钟重新计算一次分布。
冰山订单:隐藏你的真实意图
冰山订单(Iceberg Order)是一种特殊的订单类型。你只暴露一小部分订单在盘口上,大部分订单藏在「水下」。当暴露的部分成交后,系统自动再挂出一部分。
比如你想买1000手,设置显示量为100手。那么盘口上只看到100手买单,成交后自动再挂100手,直到全部成交。
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 = total_qty
self.current_display = min(display_qty, total_qty)
def execute(self, market_qty):
"""
模拟冰山订单执行
:param market_qty: 市场吃单量
"""
if market_qty >= self.current_display:
# 全部成交,重新挂单
self.remaining -= self.current_display
self.current_display = min(self.display_qty, self.remaining)
print(f"冰山订单全部成交,剩余{self.remaining}手,重新挂{self.current_display}手")
else:
# 部分成交
self.current_display -= market_qty
self.remaining -= market_qty
print(f"冰山订单部分成交,剩余显示{self.current_display}手,总剩余{self.remaining}手")
return self.remaining == 0
# 示例:总1000手,每次显示100手
iceberg = IcebergOrder(1000, 100, 3500)
iceberg.execute(100) # 第一次被吃100手
iceberg.execute(50) # 第二次被吃50手
冰山订单的核心价值在于「隐藏意图」。别人看到盘口只有100手,以为压力不大,但实际上你还有900手在排队。这样就不会吓跑对手盘。
我建议:冰山订单的显示量不要设得太小。如果总1000手你只显示10手,那成交速度太慢,基差窗口可能就关闭了。一般设总量的5%-10%比较合适。
拆单策略:把大象装进冰箱
拆单策略是算法执行的核心。说白了就是把一个大订单拆成多个小订单,分散到不同时间、不同价格、不同交易对手。
常见的拆单策略有几种:
| 策略名称 | 核心逻辑 | 适用场景 |
|---|---|---|
| 时间切片 | 按固定时间间隔拆单 | 流动性好、波动小的品种 |
| 成交量跟随 | 按市场成交量比例拆单 | 流动性变化大的品种 |
| 价格触发 | 价格有利时多下,不利时少下 | 基差快速收窄时 |
| 自适应拆单 | 根据实时冲击成本动态调整 | 高波动、低流动性品种 |
我个人比较喜欢「成交量跟随+价格触发」的组合。先按市场成交量分布拆单,然后根据基差变化动态调整。比如基差从5个点缩到3个点,我就加快下单速度;如果基差反而扩大了,我就放慢甚至暂停。
def adaptive_slicing(total_qty, base_price, spread, volume_ratio):
"""
自适应拆单策略
:param total_qty: 总下单量
:param base_price: 基准价格
:param spread: 当前基差
:param volume_ratio: 当前成交量占比
"""
# 基差越有利,下单比例越高
spread_factor = max(0.5, min(2.0, spread / 3.0))
# 成交量占比越高,下单比例越高
volume_factor = max(0.3, min(1.5, volume_ratio * 5))
# 综合因子
factor = spread_factor * volume_factor
qty = int(total_qty * factor * 0.1) # 每次下总量的10%
qty = min(qty, total_qty) # 不超过剩余量
print(f"基差={spread}点, 成交量占比={volume_ratio:.2f}, 下单因子={factor:.2f}, 本次下单={qty}手")
return qty
# 模拟执行
remaining = 1000
while remaining > 0:
qty = adaptive_slicing(remaining, 3500, 4.5, 0.15)
remaining -= qty
if qty == 0:
break
冲击成本控制:算法执行的终极目标
冲击成本控制不是单一策略,而是一套组合拳。我把它总结为三个层次:
- 事前控制:根据历史数据预估冲击成本,设定最大容忍度
- 事中控制:实时监控盘口变化,动态调整下单节奏
- 事后控制:分析执行结果,优化算法参数
这里有一个关键指标——实现差价(Implementation Shortfall)。它衡量的是「理论成交价」和「实际成交价」之间的差距。
def calculate_shortfall(theoretical_price, actual_avg_price, qty):
"""
计算实现差价
:param theoretical_price: 理论价格(决策时的价格)
:param actual_avg_price: 实际成交均价
:param qty: 成交量
"""
shortfall = (actual_avg_price - theoretical_price) * qty
shortfall_bps = (actual_avg_price - theoretical_price) / theoretical_price * 10000
print(f"实现差价: {shortfall:.2f}元")
print(f"实现差价: {shortfall_bps:.2f}个基点")
return shortfall, shortfall_bps
# 示例
calculate_shortfall(3500, 3502.5, 1000)
注意:基差交易的冲击成本控制比单边交易更难。因为你要同时控制两边的冲击,而且两边的流动性可能不一样。我一般会先做流动性好的一边,再做流动性差的一边。或者用「配对下单」的方式,两边同时下,但每边都拆得很碎。
实战中的算法选择
说了这么多,到底该用哪个算法?我根据经验给个参考:
| 基差交易类型 | 推荐算法 | 理由 |
|---|---|---|
| 股指期货期现套利 | VWAP + 冰山订单 | 流动性好,但信号敏感,需要隐藏意图 |
| 商品期货跨期套利 | TWAP + 自适应拆单 | 流动性差异大,需要动态调整 |
| ETF与期货套利 | 成交量跟随 + 价格触发 | ETF申赎有延迟,需要灵活应对 |
| 国债期货基差交易 | 冰山订单 + 时间切片 | 流动性集中,需要耐心等待 |
最后说一句:算法执行不是万能的。如果你的基差策略本身有问题,再好的算法也救不了。但反过来,一个好的策略配上糟糕的执行,同样会亏钱。两者缺一不可。
核心总结:TWAP求稳,VWAP求准,冰山求隐,拆单求活。冲击成本控制是贯穿始终的底线。记住,在基差交易里,你不是在跟市场对赌,而是在跟市场「躲猫猫」。