期权中高频波动率择时价差策略

在金融衍生品市场中,机构投资者利用期权及相关策略进行精细化风险管理,并创造 alpha 收益。隐含波动率(Implied Volatility,简称 IV)作为期权定价的核心参数,其短期波动为波动率套利提供了丰富的交易机会。实践中除了构建 Delta 中性组合从波动率的绝对定价错误中获利之外,还可以将波动率状态本身作为一个择时因子,融入期权方向性策略的构建之中。

本文将以一个融合波动率择时的期权快照频垂直价差策略为例,说明如何利用 DolphinDB Backtest 插件完成从数据准备、策略编写、参数配置到执行回测、获取结果的完整流程。策略以上期所黄金期货主力合约对应的平值与虚值二档看涨、看跌期权为交易对象。当标的呈现上升趋势且隐含波动率偏低时,构建牛市看涨价差;当标的呈现下降趋势且隐含波动率偏高时,构建熊市看跌价差。持仓期间,实时监控组合价差的变化,以决定平仓时机。本案例使用的行情与合约信息均为模拟数据。

1. 策略介绍

本章介绍期权中高频波动率择时价差策略的逻辑、盈亏情景及整体实现结构。

1.1 策略逻辑

期权合约赋予买方一种选择权,即在约定日期或之前,以约定价格买入或卖出对应资产。期权价格的影响因素包括当前标的价格(S0)、执行价格(K)、期限(T)、标的未来波动率(σ)以及无风险利率(r)。其中,标的的未来波动率 σ 是唯一不可直接观测的变量,这使其成为期权定价与交易的核心因素。在交易实践中,通常使用布莱克–斯科尔斯–默顿(Black-Scholes-Merton,BSM)定价模型等定价模型,用市场上的期权报价反推计算隐含波动率,以衡量市场对未来波动率的共识预期。

波动率交易者可以将波动率状态作为一个关键性的择时因子,融入期权方向性策略的构建之中。例如,在持有温和看涨观点且隐含波动率偏低时,选择牛市看涨价差策略可以在较低成本下建立方向性头寸,同时限制标的价格下跌带来的损失;反之,在持有温和看跌观点且隐含波动率偏高时,可以通过熊市看跌价差建立方向性头寸,同时限制标的价格上涨带来的损失。垂直价差由买入和卖出的两个期权构成,二者类型相同、行权月份相同。看涨价差中,卖出期权的行权价高于买入期权;看跌价差中,卖出期权的行权价低于买入期权。卖出期权获得的权利金用于对冲买入成本。

本文实现的策略以一个交易日内的快照行情为输入,依次判断期权隐含波动率水平与标的期货价格的趋势方向:当 IV 信号与趋势信号同时为正时触发牛市看涨信号,做多平值看涨期权并做空虚值二档看涨期权;当两者同时为负时触发熊市看跌信号,做多平值看跌期权并做空虚值二档看跌期权。持仓建立后,策略实时监控(平值 − 虚值)这一组合价差相对开仓时的变动比例,当价差向盈利或亏损方向突破设定阈值时对该期权对执行双平。策略的主要参数如下表所示。

表 1. 表 1-1 策略参数
策略参数 参数值
开始日期 2025.02.17(上午时段开始)
结束日期 2025.02.17(上午时段结束)
回测频率 快照频
初始资金 1,000,000
回测标的 黄金期货主力合约对应的看涨期权 & 看跌期权
开仓信号
  • 牛市看涨开仓信号(IV 偏低 & 标的价格上升趋势):

    • 期权 IV < 过去 30 个快照的 IV 均值 - 1.0 * 标准差

    • 期货最新价 MACD > 0,上一个快照的 MACD < 0

  • 熊市看跌开仓信号(IV 偏高 & 标的价格下降趋势):

    • 期权 IV > 过去 30 个快照的 IV 均值 + 1.0 * 标准差

    • 期货最新价 MACD < 0,上一个快照的 MACD > 0

开仓规则

实时根据 IV 信号进行期权对市价单双开:

  • 牛市看涨价差:做多平值 + 做空虚值二档看涨期权

  • 熊市看跌价差:做多平值 + 做空虚值二档看跌期权

平仓信号

实时监控(平值 – 虚值)期权对的价差情况,对该期权对进行市价单双平:

牛市看涨价差 & 熊市看跌价差:平值 - 虚值价差越大越好

价差相对初始值变动比例上下界:[-0.03, 0.05]

1.2 盈亏情景

对策略的期权组合进行情景分析。假设当前的黄金期货价格为 675 元/克,此时对应的看涨、看跌方向的平值与虚值二档期权及其开仓权利金如下表所示。由于平值期权权利金高于虚值期权权利金,故 C1 > C2、C3 > C4。

表 2. 表 1-2 情景假设对象
情景假设对象 名称 开仓权利金
平值看涨期权 AU2504-C-672 C1
虚值二档看涨期权 AU2504-C-688 C2
平值看跌期权 AU2504-P-672 C3
虚值二档看跌期权 AU2504-P-656 C4

1.2.1 牛市看涨价差期权组合

假设当前隐含波动率偏低并且标的当前价格呈现上升态势,触发了牛市看涨价差信号,即做多平值看涨期权并做空虚值二档看涨期权。未来黄金期货价格(St)可能出现以下三种情况:

  • 大幅下跌(St << 672)

  • 小幅上涨或震荡(672 ≤ St ≤ 688)

  • 大幅上涨(St >> 688)

三种情况下平值看涨期权、虚值二档看涨期权以及 1:1 期权组合的盈亏方向与大小如下表所示。

表 3. 表 1-3 牛市看涨价差期权组合盈亏情景
黄金期货 平值看涨期权(多单) 虚值二档看涨期权(空单) 1 : 1 期权组合
1. 大幅下跌 大幅浮亏,期权价值快速衰减,损益:−C1 大幅浮盈,期权价值快速衰减,损益:+C2 组合浮亏,达到最大亏损,平值 − 虚值二档期权价差急剧收窄,损益:−(C1 − C2)
2. 小幅上涨 浮动盈利:此时 Delta 通常为正(期权价格随标的价格的上升而上升),多头盈利上升,损益:> −C1 浮动亏损:此时 Delta 通常为正,空头盈利下降,损益:< +C2

看涨期权:|平值 Delta| > |虚值 Delta|,盈亏取决于多单 - 空单的价差变动:

  • 标的价格上涨时,价差通常扩大,组合浮动盈利

  • 标的价格下跌时,价差通常缩小,组合浮动亏损

3. 大幅上涨 深度实值,多头浮盈加速,损益:(St − 672) − C1 深度实值,空头浮亏加速,损益:−(St − 688) + C2 组合浮盈,达到最大盈利,平值 − 虚值二档期权价差急剧扩大,损益:(688 − 672) − (C1 − C2)

1.2.2 熊市看跌价差期权组合

假设当前隐含波动率偏高并且标的当前价格呈现下降态势,触发了熊市看跌价差信号,即做多平值看跌期权并做空虚值二档看跌期权。未来黄金期货价格(St)可能出现以下三种情况:

  • 大幅下跌(St << 656)

  • 小幅下跌或震荡(656 ≤ St ≤ 672)

  • 大幅上涨(St >> 672)

三种情况下平值看跌期权、虚值二档看跌期权以及 1:1 期权组合的盈亏方向与大小如下表所示。

表 4. 表 1-4 熊市看跌价差期权组合盈亏情景
黄金期货 平值看跌期权(多单) 虚值二档看跌期权(空单) 1 : 1 期权组合
1. 大幅下跌 深度实值,多头浮盈加速,损益:(672 − St) − C3 深度实值,空头浮亏加速,损益:(St − 656) + C4 组合浮盈,达到最大盈利,平值 − 虚值二档期权价差急剧扩大,损益:(672 − 656) − (C3 − C4)
2. 小幅下跌 浮动盈利:此时 Delta 通常为负(期权价格随标的价格的下跌而上升),多头盈利上升,损益:> −C3 浮动亏损:此时 Delta 通常为负,空头盈利下降,损益:< +C4

看跌期权:|平值 Delta| > |虚值 Delta|,盈亏取决于多单 - 空单的价差变动:

  • 标的价格下跌时,价差通常扩大,组合浮动盈利

  • 标的价格上涨时,价差通常缩小,组合浮动亏损

3. 大幅上涨 大幅浮亏,期权价值快速衰减,损益:−C3 大幅浮盈,期权价值快速衰减,损益:+C4 组合浮亏,达到最大亏损,平值 − 虚值二档期权价差急剧缩小,损益:−(C3 − C4)

牛市看涨价差与熊市看跌价差均通过卖出期权获得权利金收入,以对冲买入期权的开仓成本。这一设计具有两个关键特征:

  • 最大亏损被锁定为期初净支出,当标的价格向不利方向移动时,期权组合的整体损失有限;

  • 最大盈利上限为行权价之差减去净权利金支出。策略以牺牲盈利方向的无限潜在收益为代价,显著降低了整体策略的盈亏平衡点与减仓成本。

1.3 策略结构

DolphinDB Backtest 回测插件提供了多种事件回调函数,包括 onSnapshot(快照回调函数)、onBar(Bar 回调函数)、onTrade(成交回报函数)等。本文主要利用 onSnapshot 快照回调函数,结合回测插件中的 context 状态字典完成快照频期权波动率择时价差策略的开发。

策略在 context 上下文状态变量中共设计了三个部分:

  • lastTimeDict:每个期货合约的上个时间戳

  • longPair 与 shortPair:各期货合约对应的牛市与熊市价差期权对

  • longState 与 shortState:牛市与熊市价差期权对及设定的价差上下限

整体实现逻辑如下图:快照行情进入 onSnapshot 回调后,先获取期权标的维度信息并过滤非主力合约,再依据 IV 信号与趋势信号刷新目标期权对;只有当同一期货合约进入新的快照截面时,才依据缓存的期权对状态执行平仓与开仓下单。



图 1. 图 1-1 快照频期权波动率择时价差策略结构

2. 策略回测

本章基于 DolphinDB Backtest 回测插件实现上述策略。回测需要 DolphinDB Server 与 Backtest 回测插件,快照行情的撮合由 MatchingEngineSimulator 模拟撮合插件完成,因此脚本开头需要同时加载这两个插件;插件的安装与通用环境准备可参考 DolphinDB 回测插件官方文档。本案例使用两个模拟数据文件:期权快照行情文件 optData.csv 和期权合约基本信息文件 optSecurityReference.csv。本章按准备数据、编写策略逻辑、设置回测参数、创建回测引擎、执行回测、获取回测结果的顺序展开,脚本入口为 myStrategy 函数。



图 2. 图 2-1 用 Backtest 回测插件进行中高频期权回测的流程

2.1 准备数据

本案例使用上期所黄金期权一个交易日上午时段(09:30–11:30)的模拟快照数据,标的为黄金期货主力合约 AU2504 对应的期权,行情频率为快照频(本案例数据为 3 秒一笔,09:30:00 至 11:30:00)。数据分为两份文件:

  • optSecurityReference.csv 为期权合约基本信息表,字段包括合约代码、标的资产类型、合约乘数、看涨/看跌方向、行权价、保证金率、最小变动价位、手续费、最后交易日与行权日等。回测引擎需要该表来计算保证金、手续费与合约乘数。

  • optData.csv 为期权快照行情表,除 symbol、timestamp、lastPrice 等常规行情字段外,还包含 BSM 模型反推得到的 IV 以及 Delta、Gamma、Theta、Vega 四个希腊值,以及该期权所属的期货合约 contract、行权价 strike 与方向 direction。其中 bidPrice、bidQty、offerPrice、offerQty 为数组向量,分别记录委托买卖的五档价与量。

使用 loadText 加载两个数据文件时需显式声明各列的字段名与数据类型:

// 标的基本信息表
filePath = "./optionData/" // 需要自行换成 DolphinDB 所在服务器上的 csv 路径
schemaTb = table(["symbol","underlyingAssetType","multiplier","type","strikePrice","marginRatio","tradeUnit","priceUnit","priceTick","commission",
            "deliveryCommissionMode","lastTradingDay","exerciseDate","exerciseSettlementDate","exDate","adjStrikePrice","adjMultiplier"] as `name,
["SYMBOL","INT","DOUBLE","INT","DOUBLE","DOUBLE","DOUBLE","DOUBLE","DOUBLE","DOUBLE","INT","DATE","DATE","DATE","DATE","DOUBLE","DOUBLE"] as `types)
securityReference = loadText(filePath+"optSecurityReference.csv",schema=schemaTb)

快照行情表的字段含义如下表所示。表中 direction 表示方向,1 为看涨、2 为看跌;contract 为该期权对应的期货合约。

表 5. 表 2-1 期权快照行情表
字段 类型 说明
symbol SYMBOL 期权合约代码
symbolSource SYMBOL 交易所标识
timestamp TIMESTAMP 快照时间戳
tradingDay DATE 交易日
lastPrice DOUBLE 最新价
upLimitPrice DOUBLE 涨停价
downLimitPrice DOUBLE 跌停价
totalBidQty LONG 区间成交买数量
totalOfferQty LONG 区间成交卖数量
bidPrice DOUBLE[] 委托买价(五档)
bidQty LONG[] 委托买量(五档)
offerPrice DOUBLE[] 委托卖价(五档)
offerQty LONG[] 委托卖量(五档)
highPrice DOUBLE 最高价
lowPrice DOUBLE 最低价
prevClosePrice DOUBLE 前收盘价
prevSettlementPrice DOUBLE 前结算价
underlyingPrice DOUBLE 标的期货最新价
Theta DOUBLE Theta
Vega DOUBLE Vega
Gamma DOUBLE Gamma
Delta DOUBLE Delta
IV DOUBLE 隐含波动率
contract SYMBOL 对应的期货合约
strike DOUBLE 行权价
direction INT 1 为看涨,2 为看跌

2.2 编写策略逻辑

策略以回调函数的形式实现。回测插件在回测开始、每日盘前、每笔快照、每日盘后与回测结束时分别触发 initialize、beforeTrading、onSnapshot、afterTrading 与 finalize 回调。其中策略主体逻辑全部在 onSnapshot 中完成,beforeTrading 用于处理非主力合约的隔夜持仓。下面按实际执行顺序说明 onSnapshot 内部的五个处理步骤。以下代码块为策略实现的节选,完整脚本见附录。

Step 1. 获取基本信息

onSnapshot 的三个入参依次为上下文状态字典 contextInfo、快照行情字典 msg 与自定义指标 indicator。首先获取期权的标的代码、期货合约代码、方向、行权价,以及期权档位、IV 信号、趋势信号三个自定义指标;再读取当日主力合约列表,过滤非主力合约行情。

def onSnapshot(mutable contextInfo, msg, indicator){
    // 获取期权标的维度信息
    ioption = msg.symbol                                // 标的代码
    level = msg["signal"][0]                            // 期权挡位
    direction = contextInfo["directionDict"][ioption]   // 期权看涨(1)还是看跌(2)
    contract = contextInfo["contractDict"][ioption]     // 合约代码
    strike = contextInfo["strikePriceDict"][ioption]    // 期权行权价
    // 获取当前时间维度信息
    tradeTime = contextInfo["tradeTime"]
    tradeDate = contextInfo["tradeDate"]
    // 过滤非主力合约
    contractList = contextInfo["mainContractDict"][tradeDate]
    if (!(contract in contractList)){ // 非主力合约
        return
    }
    ...
}

Step 2. 选择期权组合

由于标的期货的最新价格会实时变动,需要实时对目标期权的档位进行追踪。为提高策略在多品种、多合约场景下的可扩展性,这里使用数据结构为 <String, Dict<String, List>> 的嵌套字典作为状态变量:外层的 String 为期货合约名称,内层的 String 为期权对名称,List 是一个长度为 2 的字符串列表,其值对应 [平值期权名称, 虚值二档期权名称],同一列表中的期权组成一个期权对,后续开仓逻辑每次对该期权对执行对应的下单操作。当牛市看涨信号(ivSignal == 1 且 trendSignal == 1)触发时,把平值看涨与虚值二档看涨期权写入 longPair;当熊市看跌信号(ivSignal == −1 且 trendSignal == −1)触发时,把平值看跌与虚值二档看跌期权写入 shortPair。

ivSignal = msg["signal"][1]  // ivFactor
trendSignal = msg["signal"][2] // trendFactor
// 牛市看涨/熊市看跌信号是否触发 -> 添加至longPair & shortPair
if (ivSignal == 1 and trendSignal == 1){
    // 牛市看涨信号触发 -> 做多平值看涨期权 + 做空虚值二档看涨期权
    if (!(contract in keys(contextInfo["longPair"]))){
        contextInfo["longPair"][contract] = array(STRING, 2)
    }
    if (level == 0 and direction == 1){ // 平值看涨期权
       contextInfo["longPair"][contract][0] = ioption
    }else if (level == -2 and direction == 1){ // 虚值二档看涨期权
       contextInfo["longPair"][contract][1] = ioption
    }
}else if (ivSignal == -1 and trendSignal == -1){
    // 熊市看跌信号触发 -> 做多平值看跌期权 + 做空虚值二档看跌期权
    if (!(contract in keys(contextInfo["shortPair"]))){
        contextInfo["shortPair"][contract] = array(STRING, 2)
    }
    if (level == 0 and direction == 2){ // 平值看跌期权
       contextInfo["shortPair"][contract][0] = ioption
    }else if (level == -2 and direction == 2){ // 虚值二档看跌期权
       contextInfo["shortPair"][contract][1] = ioption
    }
}

Step 3. 划分快照截面

在回测插件中,每个标的每收到一条快照,都会触发一次 onSnapshot 回调,因此可以根据每类标的时间戳的变动时刻划分不同的截面,从而实现截面下单的逻辑。由于注入引擎的数据已经按时间戳排序,每个合约回调的 tradeTime 一定单调递增,这一逻辑成立。具体做法是:若当前时间戳与该期货合约的上一个时间戳相同,则直接结束本次回调;若二者不同,即进入新的快照截面,则从 context 中取出对应的状态变量,执行平仓与开仓操作。

// 获取当前时间维度信息
... // 所有标的都需要执行的逻辑代码块
tradeTime = contextInfo["tradeTime"]
if (contextInfo["lastTimeDict"][contract]==tradeTime){
    return
}else{ // 下一个截面到来
    contextInfo["lastTimeDict"][contract]=tradeTime.copy()
}
... // 只有触发快照变动时需要执行的逻辑代码块

Step 4. 组合平仓下单

由 1.2 节的分析可知,平值 − 虚值期权的价差在本策略构造的期权组合中对最终损益起决定性作用,因此需要实时追踪价差变化,并在合适时机对期权对平仓。策略以开仓时的价差为基准,记录价差的上下界 [初始价差 × (1 − lowLimit), 初始价差 × (1 + upLimit)];当最新价差突破该区间时,对该期权对执行双平,即先平掉平值期权的多仓、再平掉虚值二档期权的空仓,并删除该期权对的价差记录。以下为牛市看涨价差期权对的平仓代码:

if (contract in keys(contextInfo["longState"])){
    // longState 中存放做多平值看涨期权 + 做空虚值二档看涨期权 -> 平仓时应为 [平多, 平空]
    for (optPair in keys(contextInfo["longState"][contract])){
        opt1 = optPair.split("$")[0]    // 取出平值看涨期权
        opt2 = optPair.split("$")[1]    // 取出虚值二档看涨期权
        price1 = contextInfo["lastPriceDict"][opt1]
        price2 = contextInfo["lastPriceDict"][opt2]
        state = price1 - price2 // 计算组合最新价差
        stateList = contextInfo["longState"][contract][optPair] // 组合最低价差 & 组合最高价差
        // 判断最新价差是否突破阈值
        if (state<stateList[0] or state>stateList[1]){
            vol1 = Backtest::getPosition(contextInfo["engine"], symbol=opt1)["longPosition"][0]
            // 平值看涨期权多仓
            vol2 = Backtest::getPosition(contextInfo["engine"], symbol=opt2)["shortPosition"][0]
            // 虚值二档看涨期权空仓
            if (isNull(vol1) or isNull(vol2)){
                continue
            }
            if (vol1>0 and vol2>0){
                orderDirection = 3 // 平多
                Backtest::submitOrder(contextInfo["engine"],
                    (opt1, "SHFE", tradeTime, 0, price1, price1, vol1, orderDirection, 0),
                    label="closeLong",orderType=0)
                orderDirection = 4 // 平空
                Backtest::submitOrder(contextInfo["engine"],
                    (opt2, "SHFE", tradeTime, 0, price2, price2, vol2, orderDirection, 0),
                    label="closeShort",orderType=0)
                // 删除价差记录
                contextInfo["longState"][contract].erase!(opt1+"$"+opt2)
                contextInfo["longPair"][contract] = array(STRING,2)
            }
        }
    }
}

Step 5. 组合开仓下单

开仓部分首先用 Backtest::getAvailableCash 查看实时可用资金,可用资金低于 10 万时不再开仓,防止资金不足无法下单;随后根据 longPair 与 shortPair 中的牛市价差、熊市价差期权对下单。为防止重复下单,需要先用 Backtest::getPosition 取出该标的持仓,检查是否已有对应方向的持仓。开仓以市价单完成,每条腿各 1 手。以下为牛市看涨价差期权对的开仓代码:

if (Backtest::getAvailableCash(contextInfo["engine"]) <= 100000){
    return
}
// 牛市看涨价差 -> 做多平值看涨期权 + 做空虚值二档看涨期权
if (contract in keys(contextInfo["longPair"])){
    opt1 = contextInfo["longPair"][contract][0]
    opt2 = contextInfo["longPair"][contract][1]
    vol1 = Backtest::getPosition(contextInfo["engine"], symbol=opt1)["longPosition"][0]
    vol2 = Backtest::getPosition(contextInfo["engine"], symbol=opt2)["shortPosition"][0]
    price1 = contextInfo["lastPriceDict"][opt1]
    price2 = contextInfo["lastPriceDict"][opt2]
    optPair = opt1+"$"+opt2
    if (!isNull(opt1) and !isNull(opt2) and price1!=price2){  // 初始价差不为0
        // 判断当前期权是否持仓
        state = false
        if (!(isNull(vol1))){
            if (vol1 >0){
                state = true
            }
        }
        if (!(isNull(vol2))){
            if (vol2 >0){
                state = true
            }
        }
        if (!state){
            // 开仓1手
            orderDirection = 1 // 平值看涨期权开多
            Backtest::submitOrder(contextInfo["engine"],
                (opt1, "SHFE", tradeTime, 0, price1, price1, 1, orderDirection, 0),
                label="openLong",orderType=0) // 下单(0:市价单)
            orderDirection = 2 // 虚值二档看涨期权开空
            Backtest::submitOrder(contextInfo["engine"],
                (opt2, "SHFE", tradeTime, 0, price2, price2, 1, orderDirection, 0),
                label="openShort",orderType=0) // 下单(0:市价单)
            // 添加价差记录
            if (!(contract in keys(contextInfo["longState"]))){
                contextInfo["longState"][contract] = dict(SYMBOL, ANY)
                // 期权对代码: [最低价差限制, 最高价差限制]
            }
            contextInfo["longState"][contract][optPair] = [
                (price1-price2) * (1-contextInfo["lowLimit"]),
                (price1-price2) * (1+contextInfo["upLimit"])
            ]
        }
    }
}

此外,若回测区间跨越合约换月,则开盘时可能持有非当前主力合约的隔夜持仓。beforeTrading 回调在每日开盘前检查持仓,对非主力合约的多仓与空仓分别以市价单平仓,避免旧合约持仓长期滞留。

def beforeTrading(mutable contextInfo){
    tradeDate = contextInfo["tradeDate"]
    tradeTime = contextInfo["tradeTime"]
    // 当日主力合约池及对应持仓过期标的
    contractList = contextInfo["mainContractDict"][tradeDate]
    if (size(contractList) == 0){
        return
    }
    posTable = Backtest::getPosition(contextInfo["engine"])
    if (size(posTable) == 0){
        return
    }
    // 平多(非近月合约)
    orderDirection = 3
    longIdx = at(posTable[`longPosition]>0 and !(in(posTable["symbol"], contractList)))
    if (size(longIdx)>0){
        symbolList = posTable["symbol"][longIdx]
        volList = posTable[`longPosition][longIdx]
        for (i in 0..(size(symbolList)-1)){
            opt = symbolList[i]
            price = contextInfo["lastPriceDict"][opt]
            vol = volList[i]
            Backtest::submitOrder(contextInfo["engine"],
                (opt, "SHFE", tradeTime, 0, price, price, vol, orderDirection, 0),
                label="closeLong(overTime)", orderType=0)
        }
    }
    // 平空(非近月合约)
    orderDirection = 4
    shortIdx = at(posTable[`shortPosition]>0 and !(in(posTable["symbol"], contractList)))
    if (size(shortIdx)>0){
        symbolList = posTable["symbol"][shortIdx]
        volList = posTable[`shortPosition][shortIdx]
        for (i in 0..(size(symbolList)-1)){
            opt = symbolList[i]
            price = contextInfo["lastPriceDict"][opt]
            vol = volList[i]
            Backtest::submitOrder(contextInfo["engine"],
                (opt, "SHFE", tradeTime, 0, price, price, vol, orderDirection, 0),
                label="closeShort(overTime)", orderType=0)
        }
    }
}

2.3 设置回测参数

回测区间、初始资金、行情类型、撮合方式、订单延时等均通过配置字典 config 传入。本案例的核心参数取值如下:

  • 回测区间为 2025.02.17 至 2025.02.17;

  • 策略类型 strategyGroup 设为 option;

  • dataType 设为 1 表示使用快照行情;

  • callbackForSnapshot 设为 0,表示快照行情只触发 onSnapshot;

  • cash 为初始资金 1000000;

  • matchingMode 设为 3;

  • latency 为模拟订单延时 50 毫秒;

  • enableAlgoOrder 与 outputOrderInfo 分别用于开启算法订单与输出拒单原因。

除这些引擎级参数外,策略自身的全局变量通过 config 的 context 字段注入。其中:

  • upLimit 与 lowLimit 是价差平仓阈值,分别表示价差向盈利、亏损方向突破 5% 与 3% 时对期权对双平;

  • lastTimeDict、longPair、shortPair、longState、shortState 是策略运行期维护的状态变量,初始为空字典;

  • contractDict、directionDict、strikePriceDict 分别保存期权到期货合约、期权方向和行权价的映射。这些映射从合约基本信息表中提取,便于在 onSnapshot 中按标的代码快速获取信息。

  • 主力合约列表 mainContractDict 按交易日登记,本案例仅有 2025.02.17 一天,对应主力合约 AU2504。

// 准备回测配置参数
config = dict(STRING, ANY)
config["startDate"] = startDate // 回测开始日期
config["endDate"] = endDate // 回测结束日期
config["strategyGroup"] = "option" ///策略类型
config["dataType"] = 1 // 行情选择快照行情
config["callbackForSnapshot"] = 0 // 当dataType==1 时生效, 0: 只触发onSnapshot, 1: 触发onSnapshot和onBar, 2: 只触发onBar
config["maintenanceMargin"] = 1.0 // 维持保证金比率
config["cash"] = 1000000
config["enableAlgoOrder"] = true // 开启算法订单
config["outputOrderInfo"] = true // 是否输出拒单原因
config["matchingMode"] = 3
config["latency"] = 50 // 模拟订单延时(ms)
contractDict = dict(securityReference[`symbol], split(securityReference[`symbol],"-")[0])
directionDict = dict(securityReference[`symbol], securityReference[`type])
strikePriceDict = dict(securityReference[`symbol],securityReference[`strikePrice])

// 配置主力合约信息
mainContractDict = dict(DATE,ANY)
mainContractDict[2025.02.17] = ["AU2504"] // 存放每日主力合约列表

// 配置回测参数
config["context"] = {
    // 基本信息部分
    "lastTimeDict": dict(SYMBOL, TIMESTAMP), // {期货名称: 上一个时间戳}
    "contractDict": contractDict,  // {期权名称: 期货合约名称}
    "directionDict": directionDict, // 1为看涨期权, 2为看跌期权
    "strikePriceDict": strikePriceDict, // {期权标的名称: 行权价}
    "mainContractDict": mainContractDict, // {日期: 主力合约列表}

    // 信号部分
    "upLimit": 0.05,  // 当期权价差向盈利方向突破该比例时, 期权对进行双平
    "lowLimit": 0.03,  // 当期权价差向亏损方向突破该比例时, 期权对进行双平
    "longState": dict(SYMBOL, ANY), // {期货:{牛市看涨期权对: [价差下限, 价差上限]}}
    "shortState": dict(SYMBOL, ANY), // {期货:{熊市看跌期权对: [价差下限, 价差上限]}}

    // 下单部分
    "lastPriceDict": dict(SYMBOL, DOUBLE), // 期权最新价格
    "longPair": dict(SYMBOL, ANY), // 多头期权对
    "shortPair": dict(SYMBOL, ANY) // 看跌期权对
}

需要说明的是,回测的开始与结束日期 startDate、endDate 由策略入口中的变量控制,修改回测区间时需同步调整 mainContractDict 中的主力合约登记;调整价差平仓的松紧程度只需修改 context 中的 upLimit 与 lowLimit 两个比例,二者以开仓价差为基准按比例换算上下界,因此与期权价格量级无关,可在不同标的间直接沿用。

本案例不开启 JIT 优化,即 jit 参数保持默认值 false。若运行环境支持 JIT,可将其改为 true 以提升回测性能,注意事项可参考回测插件官方文档。

2.4 创建回测引擎

回测引擎通过 Backtest::createBacktester 创建,该函数包含以下核心参数:

  • eventCallBacks 为回调函数字典,用于定义策略在不同事件触发点下的处理逻辑;

  • config 为引擎配置字典,包含策略运行所需的各类参数以及用于维护策略状态的上下文字典 context;

  • securityReference 为标的基本信息表。

// 回调函数字典
eventCallBacks = dict(STRING, ANY) // 回调函数字典
eventCallBacks["initialize"] = initialize  // 初始化函数
eventCallBacks["beforeTrading"] = beforeTrading // 每日盘前回调函数
eventCallBacks["onSnapshot"] = onSnapshot // 快照回调函数
eventCallBacks["afterTrading"] = afterTrading // 每日盘后回调函数
eventCallBacks["finalize"] = finalized // 回测结束函数
try{Backtest::dropBacktestEngine(strategyName)}catch(ex){print(ex)}  // 删除已经存在的回测引擎

// 创建回测引擎
engine = Backtest::createBacktester(strategyName, config, eventCallBacks, jit, securityReference)

其中 initialize 与 afterTrading 只输出一条日志,finalized 在回测结束时输出结束信息。引擎创建完成后,同一个引擎实例即可反复注入行情数据,每次运行前使用 dropBacktestEngine 清除上一次的状态。

2.5 执行回测

执行回测分为四个步骤:读取行情数据、计算期权档位、计算开仓信号,最后合并为数组向量并注入引擎。以下代码块同为节选,完整脚本见附录。

Step 1. 读取行情数据

由于策略只交易主力合约,这里只取出主力合约对应的数据,并按时间戳升序排列:

// 获取数据并执行策略回测
schemaTb = table(["symbol","symbolSource","timestamp","tradingDay","lastPrice",
                  "upLimitPrice","downLimitPrice","totalBidQty","totalOfferQty",
                  "bidPrice","bidQty","offerPrice","offerQty","highPrice","lowPrice",
                  "prevClosePrice","prevSettlementPrice","underlyingPrice",
                  "Theta","Vega","Gamma","Delta","IV",
                  "contract","strike","direction"] as `name,
                  ["SYMBOL","SYMBOL","TIMESTAMP","DATE","DOUBLE","DOUBLE","DOUBLE",
                  "LONG","LONG","DOUBLE[]","LONG[]","DOUBLE[]","LONG[]","DOUBLE",
                  "DOUBLE","DOUBLE","DOUBLE","DOUBLE","DOUBLE","DOUBLE","DOUBLE",
                  "DOUBLE","DOUBLE","SYMBOL","DOUBLE","INT"] as `types)
data = select * from loadText(filePath+"optData.csv",schema=schemaTb) where symbol.startsWith("AU2504") // 只取主力合约进行交易

Step 2. 计算期权档位

为便于后续策略逻辑统一调用,在获取行情后先用 SQL 向量化计算的方式对期权档位做预处理。期权档位刻画的是某一行权价相对当前标的价格的实值/虚值程度:档位为 0 表示平值期权,−k 表示虚值 k 档,+k 表示实值 k 档。由于标的期货价格实时变动,档位会随之切换,因此策略按小时划分截面重新计算档位,再拼接回原始行情数据。对看涨期权按(行权价 − 期货价格)升序排序、对看跌期权按降序排序,即可得到各合约在同一截面内的相对位置,再减去平值期权的序号即得到最终档位。代码如下:

// 获取每小时价差
hourRank = select firstNot(strike)-firstNot(underlyingPrice) as priceDiff from data
           group by contract, tradingDay, direction, symbol, hour(timestamp) as tradeHour
// 一小时截面获取档位等级
update hourRank set levelRank = iif(direction == 1,
            rank(priceDiff,true), rank(priceDiff,false))
            context by direction, contract, tradingDay, tradeHour
// 一小时截面找到平值期权(价差最接近0的期权)对应的档位
eqData = select sum(eqRank) as eqRank from (
    select tradingDay, tradeHour, direction, contract,
    iif(abs(priceDiff) == min(abs(priceDiff)), levelRank, 0) as eqRank from hourRank
    context by tradingDay, tradeHour, direction, contract)
    group by tradingDay, tradeHour, direction, contract
hourData = lj(hourRank, eqData, `tradingDay`tradeHour`direction`contract)
update hourData set levelRank = eqRank-levelRank
// 截面排序得到档位, 预期输出(0:平值期权, -k:虚值期权, +k:实值期权, k表示档位)

// 拼接回原始行情数据
data = select * from data a left join (
  select contract, tradingDay, direction, symbol, tradeHour, levelRank from hourData) b
        on a.timestamp.hour() == b.tradeHour
        and a.contract == b.contract
        and a.tradingDay == b.tradingDay
        and a.direction == b.direction
        and a.symbol == b.symbol
        order by timestamp asc

Step 3. 计算开仓信号

趋势信号由标的期货价格的 MACD 派生:MACD 由 24、52、18 三个周期的指数移动平均构成,当 MACD 由负转正时趋势信号 trendFactor 记为 1,由正转负时记为 −1,其余为 0。IV 信号由隐含波动率的滚动统计量派生:以过去 30 个快照的 IV 均值为中枢、加减 1 倍标准差为上下轨,当 IV 低于下轨时 ivFactor 记为 1,高于上轨时记为 −1,其余为 0。当 MACD 由负转正且 IV 低于下轨时判定为牛市看涨价差开仓信号;当 MACD 由正转负且 IV 高于上轨时判定为熊市看跌价差开仓信号。

/* 计算MACD趋势信号 */
macdFunc = def (lastPrice, short_=24, long_=52, m=18) {
    dif = ewmMean(lastPrice, span=short_, adjust=false) -
    ewmMean(lastPrice, span=long_, adjust=false)
    dea = ewmMean(dif, span=m, adjust=false)
    macd = (dif - dea) * 2
    return round(macd, 4)
}
update data set macd = macdFunc(underlyingPrice).nullFill(0) context by symbol
update data set trendFactor = iif(macd>0 and prev(macd)<0, 1,
                        iif(macd<0 and prev(macd)>0, -1, 0)) context by symbol
/* 计算IV信号 */
update data set ivLongLevel = mavg(IV.ffill(), 30) - mstd(IV.ffill(), 30) context by symbol
update data set ivShortLevel = mavg(IV.ffill(), 30) + mstd(IV.ffill(), 30) context by symbol
update data set ivFactor = iif(IV<ivLongLevel, 1.0, iif(IV>ivShortLevel, -1.0, 0.0)) // IV信号

以本案例模拟数据中 11:12:33 触发的一次牛市看涨信号为例,虚值二档看涨期权 AU2504-C-688 在该快照上的 IV 已跌破下轨、MACD 由负转正,ivFactor 与 trendFactor 同时取 1;平值看涨期权 AU2504-C-672 则在此前的 10:31:51 已发出同样信号。待两个期权合约均写入期权对后,引擎在下一个快照截面(11:12:36)以市价单对两腿开仓:

表 6. 表 2-2 开仓信号示例
时间 标的 期货最新价 MACD IV ivFactor trendFactor
2025-02-17 10:31:51 AU2504-C-672 670.47 0.0003 0.1490 1 1
2025-02-17 11:12:30 AU2504-C-688 671.61 −0.0014 0.1440 1 0
2025-02-17 11:12:33 AU2504-C-688 671.61 0.0005 0.1435 1 1

Step 4. 合并注入引擎

回测过程中使用到的三个自定义指标(期权档位、IV 信号、趋势信号)通过 fixedLengthArrayVector 合并为一个定长数组向量列 signal 传入引擎,其元素顺序与 onSnapshot 中 msg["signal"] 的下标一一对应。需要注意的是,注入的行情表中 settlementPrice 一列由 prevSettlementPrice 派生,为引擎提供结算价。注入全部行情后,调用 Backtest::appendEndMarker(engine) 结束回测,该接口会触发 afterTrading 与 finalize 事件回调,等价于向引擎追加一条 symbol 为 END 的数据。

// 最终注入引擎的数据 = 行情列 + IV & Greeks列 + 信号列
data = select symbol, symbolSource, timestamp, tradingDay, lastPrice, upLimitPrice, downLimitPrice,
          totalBidQty, totalOfferQty, bidPrice, bidQty, offerPrice, offerQty, highPrice, lowPrice,
          prevClosePrice, prevSettlementPrice,
          prevSettlementPrice as settlementPrice,
          underlyingPrice, Theta, Vega, Gamma, Delta, IV,
          fixedLengthArrayVector(double(levelRank),double(ivFactor),double(trendFactor)) as signal
          from data
          order by timestamp asc

timer Backtest::appendQuotationMsg(engine, data) // 注入回测引擎, 执行回测

// 回测结束标志
Backtest::appendEndMarker(engine)
go

策略入口 myStrategy 在流程最后被调用,回测区间与是否开启 JIT 优化都在此处指定,其中 jit 传入 false:

/* 回测参数配置 */
strategyName = "optionStrategy"
startDate = 2025.02.17
endDate = 2025.02.17

// 进行回测(jit = false,不开启 JIT 优化)
engine = myStrategy(strategyName, startDate, endDate, false)

2.6 获取回测结果

回测运行结束后,可以通过相应接口获取上下文状态、订单明细、期末持仓、每日损益与收益概述等结果。本案例关注的输出包括:

  • tradeDetails(订单明细表)用于核对每一笔委托的成交情况与标签;

  • totalPosition(期末持仓)与 enableCash(期末可用资金)用于确认回测结束时的账户状态;

  • dailyPortfolios(每日损益指标)与 returnSummary(综合分析结果表)用于查看收益表现。

完整的回测评价接口详见 DolphinDB 回测插件接口说明文档。

// 查看回测结果
contextInfo = Backtest::getContextDict(engine) // 期末 context 上下文字典
tradeDetails = Backtest::getTradeDetails(engine) // 订单明细表
openOrders = Backtest::getOpenOrders(engine) // 期末未成交订单
totalPosition = Backtest::getPosition(engine)  // 当前持仓
dailyPosition = Backtest::getDailyPosition(engine) // 每日持仓表
enableCash = Backtest::getAvailableCash(engine) // 期末可用资金
dailyPortfolios = Backtest::getDailyTotalPortfolios(engine) // 每天的损益指标
returnSummary = Backtest::getReturnSummary(engine) // 返回一个综合分析结果表

取值顺序上,应先取出所需的各项结果再清理引擎;若需要多次对比不同参数下的结果,可在每次回测后把结果表另存为变量或落盘,再重新创建引擎执行下一轮。

3. 回测结果展示

下面给出本案例在模拟数据上运行后 tradeDetails 的成交明细与期权对开平仓汇总。回测区间内共触发 1 组期权对开仓,为 1 组牛市看涨价差;该组合由「平值期权开多 + 虚值二档期权开空」的两腿构成,开仓后按价差阈值双平,全程无未平仓持仓。表中 direction 一列对应下单方向,买开、卖开为两腿开仓,卖平、买平为两腿平仓。

表 7. 表 3-1 成交明细
timestamp symbol price volume direction
2025-02-17 11:12:36 AU2504-C-672 15.56 1 买开
2025-02-17 11:12:36 AU2504-C-688 8.38 1 卖开
2025-02-17 11:12:45 AU2504-C-672 13.48 1 卖平
2025-02-17 11:12:45 AU2504-C-688 6.54 1 买平
表 8. 表 3-2 期权对开平仓汇总
期权对 类型 开仓时间 平仓时间 持仓时长 开仓价差 平仓价差 价差变动
AU2504-C-672 / AU2504-C-688 牛市看涨价差 11:12:36 11:12:45 9 秒 7.18 6.94 −0.24

从结果可以看出策略的下单行为与其设计一致:每次开仓都由两腿市价单组成,出现在 ivFactor 与 trendFactor 同时取 1 的快照上;平仓则出现在价差相对开仓值突破 +5% 或 −3% 的快照上,本案例的这组交易在价差收窄超过 −3% 时触发亏损端平仓。例如 11:12:36 开仓的牛市看涨价差,开仓时平值看涨期权与虚值二档看涨期权的价差为 15.56 − 8.38 = 7.18,随后价差收窄至 13.48 − 6.54 = 6.94,跌幅约 3.3%,超过 3% 的亏损阈值,因而在 3 个快照后(11:12:45)双平。

本案例使用模拟行情和合约信息来演示策略的实现流程。上表的持仓时长与盈亏分布不宜用于评估策略的实际收益能力或有效性。样本范围上,本案例只覆盖一个交易日的上午时段(09:30–11:30)与单一主力合约,未包含换月、跨品种与隔夜跳空等情形;成本假设上,合约基本信息表中手续费填 0,未计入交易成本与滑点。

4. 总结

本文利用 DolphinDB Backtest 回测插件实现了一种融合波动率择时的期权价差快照频策略,并梳理了中高频期权策略回测的全流程:从加载期权合约基本信息表与快照行情表开始,用 SQL 向量化计算期权档位,用 MACD 与 IV 滚动统计量生成趋势信号与波动率信号,再把三个自定义指标合并为数组向量注入回测引擎,最后通过回测插件的结果接口获取订单明细、持仓与损益指标。策略的核心逻辑是根据当前期货标的价格走势与期权隐含波动率水平,通过牛市看涨价差与熊市看跌价差期权组合以较低成本建立方向性头寸,并实时监控期权对价差变动以实现止盈止损平仓。使用时需要注意两点前提:

  • 策略依赖期权档位这一截面指标,其计算依赖同一截面内足够的行权价覆盖,行权价过少会削弱档位的可比性;

  • 价差平仓阈值 upLimit、lowLimit 以开仓价差为基准按比例换算,阈值越窄换手越高,需要与实际交易成本一并权衡。