问题描述
解决方案
TWAP(Time Weighted Average Price,时间加权平均价格)是一种常见的算法交易策略。其核心思想是将大额订单拆分成多个小额订单,在设定的时间段内匀速执行(例如每分钟买入固定金额或股数),从而降低对市场价格的冲击,并使成交均价尽可能接近该时间段的市场均价。
以下是在聚宽(JoinQuant)平台上实现 日内 TWAP 策略 的完整代码。
策略逻辑说明
- 设定目标:设定要操作的标的(如平安银行)和计划买入的总金额(或股数)。
- 设定时间窗:确定开始和结束时间(例如 09:31 到 14:50)。
- 计算切片:在每个分钟 Bar 运行时,计算剩余的交易时间(分钟数)和剩余未买入的金额。
- 执行交易:当前应下单金额 = 剩余目标金额 / 剩余分钟数。这种动态剩余平均法比固定每笔金额更稳健,能确保在结束时刚好完成建仓,且能自动修正之前的成交偏差。
策略代码
# -*- coding: utf-8 -*-
import math
import datetime
def initialize(context):
"""
初始化函数,设定基准、全局变量等
"""
# 设定沪深300作为基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 过滤掉order系列API产生的比error级别低的log
log.set_level('order', 'error')
# --- TWAP 策略参数设置 ---
# 要操作的标的:平安银行
g.security = '000001.XSHE'
# 计划总买入价值 (例如 10万元)
g.target_value = 100000.0
# TWAP 执行时间窗口 (格式:HH:MM)
g.start_time = '09:31'
g.end_time = '14:50'
# 每天按分钟频率运行 trade_func
run_daily(twap_trade, 'every_bar')
def twap_trade(context):
"""
TWAP 核心交易逻辑,每分钟执行一次
"""
# 获取当前时间
current_dt = context.current_dt
current_time_str = current_dt.strftime('%H:%M')
# 1. 判断是否在交易时间窗口内
if current_time_str < g.start_time or current_time_str > g.end_time:
return
# 2. 计算剩余的交易分钟数
# 获取今天的结束时间点对象
end_dt = datetime.datetime.combine(current_dt.date(), datetime.datetime.strptime(g.end_time, '%H:%M').time())
# 计算距离结束还有多少分钟 (加1是为了包含当前这一分钟)
# 注意:这里简化处理,未剔除中午休市的90分钟。
# 如果跨越中午休市,建议使用聚宽的 get_trade_days 或硬编码剔除中午时间。
# 下面使用一种通用的逻辑:计算剩余的有效分钟 Bar 数量
minutes_remaining = get_remaining_minutes(context, g.end_time)
if minutes_remaining <= 0:
return
# 3. 计算当前持仓价值
# 注意:这里假设账户里只有该策略买入的持仓。如果是混合策略,需要单独记录已买入金额。
position = context.portfolio.positions[g.security]
current_holding_value = position.total_amount * position.price
# 4. 计算剩余需要买入的金额
remaining_target = g.target_value - current_holding_value
# 如果已经买够了或者超买,则停止
if remaining_target <= 0:
return
# 5. 计算本分钟应该下单的金额
# 算法:剩余需买金额 / 剩余分钟数
# 这种动态计算方式可以自动修正之前因为滑点或取整导致的误差
order_val = remaining_target / minutes_remaining
# 6. 下单
# 只有当金额足够买 100 股(一手)时才下单,避免碎股报错
if order_val > position.price * 100:
order_value(g.security, order_val)
log.info("TWAP执行: 时间 %s, 剩余分钟 %d, 本次下单金额 %.2f" % (current_time_str, minutes_remaining, order_val))
def get_remaining_minutes(context, end_time_str):
"""
辅助函数:计算当前时刻到结束时刻之间包含多少个交易分钟
剔除中午 11:30 - 13:00 的休市时间
"""
curr_hour = context.current_dt.hour
curr_min = context.current_dt.minute
end_h, end_m = map(int, end_time_str.split(':'))
# 将时间转换为从 00:00 开始的分钟数
curr_total = curr_hour * 60 + curr_min
end_total = end_h * 60 + end_m
# 中午休市区间的分钟数表示
noon_start = 11 * 60 + 30 # 11:30
noon_end = 13 * 60 + 0 # 13:00
count = 0
# 遍历每一分钟,检查是否在交易时间内
# 我们从当前分钟开始,一直数到结束时间
for m in range(curr_total, end_total + 1):
# 如果在上午交易时段 (09:30 < t <= 11:30)
# 或者 下午交易时段 (13:00 < t <= 15:00)
# 聚宽的分钟回测 bar 是从 09:31 开始的
# 简单判断:不在中午休市期间即可
if not (noon_start < m <= noon_end):
count += 1
return count
代码关键点解析
-
动态剩余分配 (
remaining_target / minutes_remaining):- 这是 TWAP 算法中最实用的实现方式。
- 如果使用固定的
总金额 / 总分钟数,由于 A 股买入必须是 100 股的整数倍,每次下单都会有少量的资金残留。累积到最后,会导致总仓位达不到目标。 - 使用动态分配,每一分钟都会重新计算“还需要买多少”除以“还剩多少时间”,这样越接近尾声,计算越精确,确保任务完成。
-
时间处理 (
get_remaining_minutes):- A 股有中午休市(11:30-13:00)。简单的
datetime相减会把休市的 90 分钟也算进去,导致分母变大,下单金额变小。 - 代码中增加了一个辅助函数,通过循环计数的方式剔除了中午休市时间,确保分母是真实的“有效交易分钟数”。
- A 股有中午休市(11:30-13:00)。简单的
-
运行频率:
- 使用了
run_daily(twap_trade, 'every_bar'),这表示在分钟回测模式下,该函数每分钟都会被调用一次,非常适合算法交易。
- 使用了
如何在回测中使用
- 在聚宽新建策略,将上述代码复制进去。
- 设置回测频率为 “分钟”(TWAP 必须在分钟级别或 Tick 级别运行)。
- 设置初始资金(例如 200,000 元,确保大于
g.target_value)。 - 运行回测,你将看到策略在每天的 09:31 到 14:50 之间,每分钟都会买入一小笔平安银行,直到持仓达到 10 万元。
Q&A
Q: 为什么不使用 VWAP (成交量加权平均价格)?
A: TWAP 假设价格在时间上是均匀分布的,适合成交量比较平稳的股票。VWAP 需要预测当天的成交量分布(例如上午成交量大,下午小),实现起来比 TWAP 复杂,需要历史成交量曲线作为参考。如果资金量不是特别巨大,TWAP 通常已经足够平滑成本。
Q: 这段代码支持卖出吗?
A: 代码逻辑稍作修改即可支持卖出。将 g.target_value 设为 0(目标持仓为0),计算逻辑改为 (current_holding_value - 0) / minutes_remaining,并使用 order_target_value 或 order 卖出负数股数即可。
Q: 如果遇到涨停板买不进去怎么办?
A: 当前的逻辑是基于“剩余未完成金额”计算的。如果某几分钟因为涨停没买进去,current_holding_value 不会增加,remaining_target 保持不变,而 minutes_remaining 在减小。这意味着一旦打开涨停,后续的单笔下单金额会自动变大,尝试追赶进度。