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 知识体系与核心逻辑

为了让你更直观地理解,我画了一张图。这张图展示了信号如何一步步影响最终的执行决策。

信号驱动的动态TWAP核心逻辑 订单流不平衡 (OFI) 买卖价差 (Spread) 短期动量信号 信号融合与评分 加权计算综合得分 (Score) 调整系数映射 Score → Adjustment Factor [0.5, 2.0] 动态执行量 = 基础量 × 调整系数

说白了,整个过程就是一个“感知-决策-执行”的闭环。市场信号是输入,我们的算法是决策中枢,最终输出的是调整后的订单量。

4.5 一些实战中的细节

最后,分享几个我在实战中踩过的坑,希望能帮你省点学费。

  • 信号噪声:高频信号噪声很大。我建议对原始信号做平滑处理,比如用指数移动平均。否则,你的TWAP会像个神经病一样,忽快忽慢。
  • 信号延迟:你看到的信号,已经是过去式了。要尽量使用低延迟的数据源,并且在计算时考虑信号的有效期。
  • 不要过度优化:我曾经花了两周时间,调了十几个参数,试图让回测曲线完美。结果实盘一跑,亏得一塌糊涂。记住,简单有效的策略,往往比复杂花哨的策略更稳健。
  • 风控永远是第一位的:动态调整一定要有上下限。我习惯在代码里加一个全局的“紧急停止”开关。一旦市场出现极端情况,比如OFI绝对值超过某个阈值,直接暂停所有执行。

警告: 动态TWAP不是万能的。在流动性极差的市场,或者消息面驱动的单边行情中,任何信号都可能失效。这时候,最好的策略就是不做。我曾经在财报发布前几分钟,还开着动态TWAP,结果被瞬间打穿。从那以后,我学会了在重大事件前“空仓观望”。

好了,这一章的内容就到这。信号驱动的动态TWAP,核心就是让算法学会“看人下菜碟”。下一章,我们会聊聊更高级的玩法——如何利用限价单簿的深度信息,来进一步优化执行。


专注资料整理