🚀 AI 一键生成 joinquant 策略代码

JoinQuant 交易执行:如何编写 TWAP 拆单算法?

joinquant | 发布时间: 2025/12/22
以下内容由 EasyQuant 生成。

问题描述

如何实现 TWAP 时间加权平均价格算法?

解决方案

TWAP(Time Weighted Average Price,时间加权平均价格)是一种常见的算法交易策略。其核心思想是将大额订单拆分成多个小额订单,在设定的时间段内匀速执行(例如每分钟买入固定金额或股数),从而降低对市场价格的冲击,并使成交均价尽可能接近该时间段的市场均价。

以下是在聚宽(JoinQuant)平台上实现 日内 TWAP 策略 的完整代码。

策略逻辑说明

  1. 设定目标:设定要操作的标的(如平安银行)和计划买入的总金额(或股数)。
  2. 设定时间窗:确定开始和结束时间(例如 09:31 到 14:50)。
  3. 计算切片:在每个分钟 Bar 运行时,计算剩余的交易时间(分钟数)和剩余未买入的金额。
  4. 执行交易:当前应下单金额 = 剩余目标金额 / 剩余分钟数。这种动态剩余平均法比固定每笔金额更稳健,能确保在结束时刚好完成建仓,且能自动修正之前的成交偏差。

策略代码

# -*- 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

代码关键点解析

  1. 动态剩余分配 (remaining_target / minutes_remaining)

    • 这是 TWAP 算法中最实用的实现方式。
    • 如果使用固定的 总金额 / 总分钟数,由于 A 股买入必须是 100 股的整数倍,每次下单都会有少量的资金残留。累积到最后,会导致总仓位达不到目标。
    • 使用动态分配,每一分钟都会重新计算“还需要买多少”除以“还剩多少时间”,这样越接近尾声,计算越精确,确保任务完成。
  2. 时间处理 (get_remaining_minutes)

    • A 股有中午休市(11:30-13:00)。简单的 datetime 相减会把休市的 90 分钟也算进去,导致分母变大,下单金额变小。
    • 代码中增加了一个辅助函数,通过循环计数的方式剔除了中午休市时间,确保分母是真实的“有效交易分钟数”。
  3. 运行频率

    • 使用了 run_daily(twap_trade, 'every_bar'),这表示在分钟回测模式下,该函数每分钟都会被调用一次,非常适合算法交易。

如何在回测中使用

  1. 在聚宽新建策略,将上述代码复制进去。
  2. 设置回测频率为 “分钟”(TWAP 必须在分钟级别或 Tick 级别运行)。
  3. 设置初始资金(例如 200,000 元,确保大于 g.target_value)。
  4. 运行回测,你将看到策略在每天的 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_valueorder 卖出负数股数即可。

Q: 如果遇到涨停板买不进去怎么办?
A: 当前的逻辑是基于“剩余未完成金额”计算的。如果某几分钟因为涨停没买进去,current_holding_value 不会增加,remaining_target 保持不变,而 minutes_remaining 在减小。这意味着一旦打开涨停,后续的单笔下单金额会自动变大,尝试追赶进度。