4. 信号驱动的动态TWAP:结合订单流不平衡(OFI)、买卖价差、短期动量信号的执行速率调整。
好了,咱们进入第四章。这一章,我打算聊聊怎么让TWAP“活”起来。
标准的TWAP,说白了就是个时间切割机。把大单切成小单,均匀地往市场里塞。这在平静的市场里挺好用,但一旦市场开始剧烈波动,它就有点“傻”了。你想想看,明明盘口在疯狂砸盘,你的TWAP还在那按部就班地往外抛单,这不是送人头吗?
所以,我们需要给TWAP装上“眼睛”和“大脑”。让它能感知市场情绪,动态调整自己的执行节奏。这就是信号驱动的动态TWAP。
4.1 核心信号:我们看什么?
我个人习惯,主要盯三个信号。这三个信号组合起来,基本能勾勒出短期市场的微观结构。
- 订单流不平衡 (OFI):这玩意儿是衡量买卖双方谁更“急”的指标。简单算就是主动买盘量减去主动卖盘量。OFI为正,说明买方在追着买,市场偏强;为负,说明卖方在砸,市场偏弱。我在项目中遇到过,OFI的突变往往比价格变动快那么几毫秒,是个很好的先行指标。
- 买卖价差 (Spread):价差就是盘口卖一价和买一价的差值。价差越小,说明市场流动性越好,交易成本低;价差突然拉大,说明流动性枯竭了,或者有人在做市商撤单。这时候,你的订单进去,很容易滑点。
- 短期动量信号:这个我一般用过去N笔交易的加权平均价格变化率。比如,过去10笔交易的价格是快速上涨还是下跌。它反映的是最近一小段时间内的价格趋势。
核心逻辑: 这三个信号不是孤立看的。它们共同决定了当前市场的“微观状态”。比如,OFI为正 + 价差缩小 + 动量向上,这是一个典型的“买方主导、流动性好”的强势市场。反之,OFI为负 + 价差扩大 + 动量向下,那就是个“卖方砸盘、流动性差”的弱势市场。
4.2 如何调整执行速率?
有了信号,我们怎么用?核心就是调整TWAP的“时间切片”权重。
标准TWAP,每个时间片的权重是 1/N。动态TWAP,我们让权重变成信号的一个函数。
举个例子,一个简单的调整逻辑:
- 当市场状态为“强势”时:如果你是买方,可以适当加快执行速度。因为市场在帮你抬轿子,你越早买完,成本越低。如果你是卖方,则应该放慢速度,甚至暂停,等待更好的卖点。
- 当市场状态为“弱势”时:如果你是买方,应该放慢速度,等价格回调。如果你是卖方,则应该加快速度,抢在别人前面出货。
- 当价差过大时:无论买卖,都应该放慢速度。因为这时候进去,滑点成本太高,不划算。
避坑指南: 我曾经犯过一个错误,就是让调整幅度过大。比如,信号一强,我就把执行速度提高了5倍。结果呢?我的订单瞬间把盘口打穿,造成了巨大的冲击成本。后来我学乖了,调整幅度一定要平滑,最好加上一个限幅器,让调整范围在0.5倍到2倍之间。
4.3 代码实现:一个简单的动态TWAP框架
嗯,这里要注意,代码只是演示核心逻辑。生产环境里,你需要处理的事情多得多。
import numpy as np
class DynamicTWAP:
def __init__(self, total_qty, total_time, num_slices):
self.total_qty = total_qty
self.total_time = total_time
self.num_slices = num_slices
self.base_qty_per_slice = total_qty / num_slices
self.slice_duration = total_time / num_slices
def calculate_signal_score(self, ofi, spread, momentum):
"""
根据三个信号计算一个综合得分。
得分越高,表示市场越有利于快速执行(对于买方而言)。
"""
# 归一化处理,这里简化了
ofi_score = np.clip(ofi / 100.0, -1, 1)
spread_score = np.clip(1 - spread / 0.05, -1, 1) # 价差越小,得分越高
momentum_score = np.clip(momentum / 0.001, -1, 1)
# 加权求和,权重可以自己调
score = 0.4 * ofi_score + 0.3 * spread_score + 0.3 * momentum_score
return score
def get_adjusted_qty(self, current_slice, ofi, spread, momentum):
"""
根据信号调整当前时间片应该执行的量。
"""
score = self.calculate_signal_score(ofi, spread, momentum)
# 将得分映射到调整系数 [0.5, 2.0]
adjustment_factor = 1.0 + score * 0.75
adjustment_factor = np.clip(adjustment_factor, 0.5, 2.0)
adjusted_qty = self.base_qty_per_slice * adjustment_factor
# 这里可以加入剩余量的逻辑,防止最后执行不完
# 比如:如果剩余量太少,就一次性执行完
return adjusted_qty
# 使用示例
twap = DynamicTWAP(total_qty=10000, total_time=60, num_slices=60)
# 模拟第10秒时的市场状态
ofi = 50 # 主动买盘多
spread = 0.02 # 价差小
momentum = 0.0005 # 微涨
qty_to_execute = twap.get_adjusted_qty(10, ofi, spread, momentum)
print(f"第10秒,建议执行量: {qty_to_execute:.2f} 股")
你看,核心就是那个 adjustment_factor。它把信号得分映射到了一个合理的调整范围内。这样,你的TWAP就能根据市场情况,灵活地“多吃”或“少吃”了。
4.4 知识体系与核心逻辑
为了让你更直观地理解,我画了一张图。这张图展示了信号如何一步步影响最终的执行决策。
说白了,整个过程就是一个“感知-决策-执行”的闭环。市场信号是输入,我们的算法是决策中枢,最终输出的是调整后的订单量。
4.5 一些实战中的细节
最后,分享几个我在实战中踩过的坑,希望能帮你省点学费。
- 信号噪声:高频信号噪声很大。我建议对原始信号做平滑处理,比如用指数移动平均。否则,你的TWAP会像个神经病一样,忽快忽慢。
- 信号延迟:你看到的信号,已经是过去式了。要尽量使用低延迟的数据源,并且在计算时考虑信号的有效期。
- 不要过度优化:我曾经花了两周时间,调了十几个参数,试图让回测曲线完美。结果实盘一跑,亏得一塌糊涂。记住,简单有效的策略,往往比复杂花哨的策略更稳健。
- 风控永远是第一位的:动态调整一定要有上下限。我习惯在代码里加一个全局的“紧急停止”开关。一旦市场出现极端情况,比如OFI绝对值超过某个阈值,直接暂停所有执行。
警告: 动态TWAP不是万能的。在流动性极差的市场,或者消息面驱动的单边行情中,任何信号都可能失效。这时候,最好的策略就是不做。我曾经在财报发布前几分钟,还开着动态TWAP,结果被瞬间打穿。从那以后,我学会了在重大事件前“空仓观望”。
好了,这一章的内容就到这。信号驱动的动态TWAP,核心就是让算法学会“看人下菜碟”。下一章,我们会聊聊更高级的玩法——如何利用限价单簿的深度信息,来进一步优化执行。