3. 订单切片与时间调度优化:基于波动率自适应的切片算法、VWAP对齐、随机化执行时间窗口
各位同学,咱们今天聊一个实战中特别磨人的话题——订单切片和时间调度。
说实话,我早年做TWAP策略的时候,踩过一个坑。当时用的是最简单的等量切片,每30秒扔一笔。结果呢?遇到某只小盘股突然放量,我的订单全砸在了一个流动性枯竭的窗口里,滑点直接吃掉了我一周的收益。嗯,从那以后我就明白了——切片不能死板,得跟着市场节奏走。
3.1 为什么等量切片不够用?
标准的TWAP策略,就是把总订单量均匀切分到每个时间片里。比如你要买10万股,执行期1小时,每分钟切1667股。听起来很公平对吧?
但实际跑起来你会发现一个问题:市场波动不是均匀的。
你想想看,如果某分钟突然出现大单砸盘,价格瞬间跳水。这时候你按原计划买入,等于主动接飞刀。反过来,如果市场风平浪静,你按部就班地买,又可能错过更好的价格窗口。
所以,我个人习惯的做法是:让切片大小跟着波动率走。
3.2 波动率自适应的切片算法
核心思路其实很简单——波动率大的时候少切一点,波动率小的时候多切一点。这样既能避免在剧烈波动中吃大滑点,又能利用平静期积累仓位。
具体怎么实现呢?我给你们看一段核心代码:
class VolAdaptiveSlice:
def __init__(self, total_qty, total_seconds, base_slice_qty):
self.total_qty = total_qty
self.total_seconds = total_seconds
self.base_slice_qty = base_slice_qty
self.vol_window = 20 # 波动率计算窗口
def calc_vol_factor(self, price_series):
# 计算最近N笔的波动率因子
if len(price_series) < self.vol_window:
return 1.0
returns = np.diff(price_series[-self.vol_window:]) / price_series[-self.vol_window:-1]
current_vol = np.std(returns)
baseline_vol = np.median([np.std(returns[i:i+5])
for i in range(0, len(returns)-5, 5)])
# 波动率因子:当前波动率 / 基准波动率
factor = current_vol / (baseline_vol + 1e-8)
# 限制因子范围,防止极端值
return np.clip(factor, 0.3, 3.0)
def get_slice_qty(self, price_series, remaining_qty, remaining_time):
vol_factor = self.calc_vol_factor(price_series)
# 波动率越高,切片越小
slice_qty = self.base_slice_qty / vol_factor
# 确保剩余订单能在剩余时间内完成
max_slice = remaining_qty / (remaining_time / self.base_slice_qty * 1.5)
return min(slice_qty, max_slice)
这段代码里有个关键点:波动率因子不是直接用原始值,而是做了归一化处理。我拿当前波动率和历史中位数波动率做对比,这样不同股票之间才有可比性。
核心逻辑:波动率因子 = 当前波动率 / 基准波动率。因子越大,切片越小。因子范围限制在0.3到3.0之间,防止极端情况。
3.3 VWAP对齐:别让订单跑偏
TWAP和VWAP最大的区别是什么?TWAP追求时间上的均匀,VWAP追求成交量上的均匀。但实际执行中,我们往往希望两者兼顾——时间上不能太偏,成交量上也不能太离谱。
我建议的做法是:用VWAP作为参考基准,动态调整切片大小。
具体来说,每执行完一个切片,就计算一下当前累计成交量占比和累计时间占比的偏差。如果偏差太大,就调整下一个切片的大小,把订单拉回正轨。
def vwap_aligned_slice(executed_qty, total_qty, elapsed_sec, total_sec,
market_volume_sofar, expected_volume_sofar):
# 时间偏差:当前时间占比
time_ratio = elapsed_sec / total_sec
# 成交量偏差:当前成交量占比
vol_ratio = market_volume_sofar / expected_volume_sofar
# 偏差因子:如果成交量落后于时间,加大切片
deviation = time_ratio - vol_ratio
# 调整系数,范围0.8~1.2
adjust_factor = 1.0 + np.clip(deviation * 2, -0.2, 0.2)
# 剩余订单量
remaining_qty = total_qty - executed_qty
# 剩余时间内的基准切片
base_slice = remaining_qty / (total_sec - elapsed_sec) * 5 # 假设5秒一个切片
return base_slice * adjust_factor
这里有个小技巧:偏差因子不要调得太激进。我曾经试过偏差超过10%就大幅调整,结果订单像抽风一样忽大忽小,反而增加了市场冲击。后来我把调整幅度限制在±20%以内,效果好了很多。
3.4 随机化执行时间窗口
为什么要随机化?说白了,就是不想让市场发现你的执行规律。
你想想看,如果每次都在整分钟的时候下单,做市商很快就能识别出你的模式。他们会提前挂单等你来吃,或者在你下单前拉高价格。这就是典型的「被狙击」。
我个人的做法是:给每个切片的时间窗口加上随机偏移。
import random
class RandomWindowScheduler:
def __init__(self, base_interval=5.0, jitter_range=1.5):
self.base_interval = base_interval
self.jitter_range = jitter_range
self.next_exec_time = 0.0
def schedule_next(self, current_time):
# 基础间隔 + 随机偏移
jitter = random.uniform(-self.jitter_range, self.jitter_range)
interval = self.base_interval + jitter
self.next_exec_time = current_time + interval
return self.next_exec_time
def should_execute(self, current_time):
return current_time >= self.next_exec_time
注意,随机化不是乱来。我建议保持期望间隔不变,只是让实际执行时间在期望值附近波动。比如5秒的基准间隔,加上±1.5秒的随机偏移,这样平均下来还是5秒一次,但每次的具体时间点不可预测。
实战技巧:随机偏移的分布最好用均匀分布,而不是正态分布。因为正态分布容易在均值附近聚集,反而暴露了规律。均匀分布更「随机」,更难被预测。
3.5 三者如何协同工作?
好了,现在我们有三个工具:波动率自适应切片、VWAP对齐、随机化时间窗口。它们不是孤立的,需要配合使用。
我画了一张流程图,展示它们之间的协作关系:
实际执行时,顺序是这样的:
- 先算波动率因子,确定当前切片的基础大小
- 再用VWAP偏差做修正,确保整体执行不跑偏
- 最后随机化时间窗口,决定具体什么时候下单
这三步走完,一个切片才算真正准备好。
3.6 避坑指南
我曾经犯过的错误:
- 波动率窗口太小:一开始我用5笔数据算波动率,结果噪声太大,切片忽大忽小。后来改成20笔,稳定多了。
- VWAP对齐过度:有一次我把偏差容忍度设成5%,结果频繁调整,反而增加了交易成本。建议容忍度设在10%~15%。
- 随机化范围太大:有次我把抖动范围设成±5秒,结果两个切片之间间隔了10秒,错过了最佳执行窗口。建议抖动范围不超过基准间隔的30%。
3.7 性能考量
在高频环境下,这些计算不能拖慢主循环。我建议:
- 波动率计算用增量更新,不要每次都全量算一遍
- VWAP偏差可以每5个切片算一次,不用每笔都算
- 随机数生成用预计算表,避免在关键路径上调用random函数
嗯,这些优化细节,等后面讲性能调优的时候再展开。今天先把核心逻辑吃透。
好了,关于订单切片和时间调度,咱们就聊到这儿。记住一句话:切片不是越均匀越好,而是越「适应市场」越好。
公众号:蓝海资料掘金营,微信deep3321