债券与利率互换套利策略

本策略基于 5 年期国债活跃券与 5 年期 IRS 的分钟级行情,构建现券-IRS 基差均值回归策略。核心思路是以国债 YTM 与 IRS 利率之差作为 AssetSwap 利差,当利差相对历史窗口均值突破 ± 1.5 σ 时开仓,回落至 ± 0.3 σ 内时平仓,开仓时按债券与互换修正久期做 DV01 中性配平。本文使用分钟行情,通过 Backtest 引擎完成指标计算、双边配平下单和结果分析。

1. 策略介绍

本策略同时关注利差均值回归与久期中性配平。它先基于债券 YTM 与 IRS 利率之差构建 AssetSwap 利差,在滚动窗口内估计均值与标准差,再以 σ 带宽作为开平仓信号,最后按债券修正久期与互换近似修正久期推算 IRS 对冲手数,构建 DV01 中性组合。

1.1 核心因子

AssetSwap 利差用于刻画现券与利率互换之间的相对定价关系,定义如下:

spread = 国债 YTM − IRS 利率

策略在滚动窗口 lookback 内估计利差的均值 μ 与标准差 σ ,并以此构建四条信号带:

upEntry = μ + 1.5 σ , loEntry = μ − 1.5 σ

upExit = μ + 0.3 σ , loExit = μ − 0.3 σ

其中:

μ = 1 n ∑ i = 1 n w i

σ = 1 n − 1 ∑ i = 1 n ( w i − μ ) 2

w 为 lookback 窗口内的利差序列,n 为窗口长度。

债券修正久期 D b 与互换近似修正久期 D s 用于 DV01 配平,确定 IRS 对冲手数:

D b = Macaulay Duration 1 + YTM / f

D s = 1 − ( 1 + c ) − n r , c = r f , n = years × f

其中 r 为互换利率(小数),f 为付息频率,years 为剩余年限。

DV01 中性配平要求两腿价格点损益乘数一致:

N bond × D b = N IRS × D s

由此推算 IRS 手数:

N IRS = ⌊ N bond × D b D s × IRS Multiplier + 0.5 ⌋

1.2 交易标的

本例交易的标的基于 5 年期国债活跃券与 5 年期 IRS 行情标的。

资产 行情频率 账户方向
5 年期国债 分钟 现券多头或融券空头
5 年期 IRS 分钟 付固定或收固定

1.3 交易规则

在每次行情回调中,先校验两腿分钟行情是否对齐,再更新利差列表,最后按 \sigma 带宽判断开平仓。每次回调只对同一时刻对齐的两腿行情做决策。

交易方向:
  • 利差突破上侧开仓线:买入债券 + IRS 付固定;

  • 利差跌破下侧开仓线:融券卖出债券 + IRS 收固定;

  • 利差向均值回归至对应平仓线后:同时平掉债券和 IRS 两腿。

开平仓规则:

组合 严格触发条件 现券腿 IRS 腿
正套开仓 spread > μ + 1.5 σ 买入现券 付固定
正套平仓 spread ≤ μ + 0.3 σ 卖平现券 收固定
反套开仓 spread < μ − 1.5 σ 卖空现券 收固定
反套平仓 spread > μ − 0.3 σ 买平现券 付固定

DV01 中性配平:开仓时按债券与互换修正久期计算 IRS 手数,确保两腿价格点损益乘数一致。

2. 策略回测

以下按照准备数据、定义策略、设置参数、创建引擎、回放行情和读取结果的顺序展开。本案例已在 DolphinDB Server 3.00.6.1、Backtest 3.00.6.6 与模拟撮合引擎插件(Matching Engine Simulator)3.00.6.6 上运行。服务器需要安装与 DolphinDB Server 版本匹配的两个插件;创建引擎前,代码会检查插件是否已安装,并加载这两个插件。

2.1 准备数据

本例准备了 5 年期国债活跃券与 5 年期 IRS 的分钟级 OHLC 行情,并同时包含债券条款、互换条款、利率曲线与浮动基准历史定盘。

表 1. 表 2-1 数据源
数据源 用途
ohlcData.csv 5 年期国债与 5 年期 IRS 的分钟级 OHLC 行情,作为回测行情输入
bondRef.csv 债券基础信息:票息、起息日、到期日、付息频率、发行价、乘数
xswapRef.csv 互换合约条款:名义本金、付息频率、合约乘数
swapCurve.csv 利率曲线,用于互换估值
swapAssetPriceHistCurve.csv 浮动基准历史定盘,用于浮动端重估

本例直接通过 loadText读取已准备好的行情与参考信息。

// 分钟行情同时包含债券价格/YTM和 IRS 固定端利率;不同标的共用同一 schema。
path = "/.../"
name =["symbol","timestamp","datetime","source","period","priceType","open","high","low","close","volume","openYTM","highYTM","lowYTM","closeYTM"]
type = ["STRING","LONG","TIMESTAMP","STRING","INT","INT","DOUBLE","DOUBLE","DOUBLE","DOUBLE","LONG","DOUBLE","DOUBLE","DOUBLE","DOUBLE"]
ohlcData = loadText(path + "ohlcData.csv",schema=table(name,type))
// ============================================================================
// 导入 FICC 合约基本信息数据
// ============================================================================
// 债券静态信息:包含乘数、票息、付息频率、起息/到期日、借券费率等。
name =["symbol","multiplier","priceTick","couponRate","frequency","valueDate","maturityDate","bondLendingRate","commission","marginRatio","bondType","issuePrice","faceValue"]
type = ["STRING","DOUBLE","DOUBLE","DOUBLE","INT","DATE","DATE","DOUBLE","DOUBLE","DOUBLE","STRING","DOUBLE","DOUBLE"]
bondRef = loadText(path + "bondRef.csv",schema=table(name,type))

// IRS 合约条款:包含互换类型、固定/浮动端规则、乘数、频率、曲线名称等。
name = ["symbol","swapType","irSwapType","isShiborOn","marginRatio","multiplier","priceTick","frequency","maturityDate","calendar","fixedDayCountConvention","floatingDayCountConvention","notionalCurrency","iborIndex","spread","discountCurveName","forwardCurveName"]
type =["STRING","STRING","STRING","BOOL","DOUBLE","DOUBLE","DOUBLE","STRING","DATE","STRING","STRING","STRING","STRING","STRING","DOUBLE","STRING","STRING"]
xswapRef = loadText(path + "xswapRef.csv",schema=table(name,type))

// 当期利率曲线:为 IRS 估值提供期限节点和对应利率。
name = ["timestamp","mktDataType","curveType","currency","curveName","dayCountConvention","compounding","interpMethod","extrapMethod","frequency","dates","values"]
type =["TIMESTAMP","STRING","STRING","STRING","STRING","STRING","STRING","STRING","STRING","STRING","DATE[]","DOUBLE[]"]
swapCurve = loadText(path + "swapCurve.csv",schema=table(name,type))

// 历史浮动端定盘曲线:为已经发生的浮动端计息期间提供历史参考利率。
name =["referenceDate","curveName","mktDataType","curveType","currency","dates","values"]
type =["DATE","STRING","STRING","STRING","STRING","DATE[]","DOUBLE[]"]
swapAssetPriceHistCurve = loadText(path + "swapAssetPriceHistCurve.csv",schema=table(name,type))

2.2 编写策略逻辑

下面我们开始策略的程序化实现,该策略的主要实现逻辑为使用“国债到期收益率 - 同期限 IRS 固定端利率”定义利差。债券收益率与IRS 利率反映的信用、流动性、融资和供需因素不同,短期偏离历史中枢后可能发生均值回归。策略同时交易债券和 IRS,尽量降低单边利率方向风险。

  • 指标计算:

    • 使用过去 lookback 个已完成的同步分钟利差,计算滚动均值 μ 和样本标准差 σ ;当前利差在交易判断后才加入窗口,避免把当前观测提前纳入自身阈值。

    • 上侧开仓线为 μ + 1.5 σ ,下侧开仓线为 μ − 1.5 σ 。

    • 上下平仓线分别为 μ ± 0.3 σ ,形成开平仓滞回区间,减少频繁交易。

  • 交易方向:

    • 利差突破上侧开仓线:买入债券 + IRS 付固定。

    • 利差跌破下侧开仓线:融券卖出债券 + IRS 收固定。

    • 利差向均值回归至对应平仓线后,同时平掉债券和 IRS 两腿。

  • 对冲规模:

    • 通过债券修正久期与 IRS 近似修正久期估算两腿 DV01,计算 IRS 手数。

以上策略逻辑主要由五个辅助函数和三个回调函数组成。

  • calcSpreadStats 计算滚动窗口的均值与标准差;

  • toRate 将利率从百分数或小数统一为小数;

  • approxSwapModDur 推算互换近似修正久期;

  • calcIrsQtyByDv01 按 DV01 中性配平计算 IRS 手数;

  • initialize、beforeTrading、onBar、afterTrading 分别负责初始化、盘前重置、日内决策与盘后处理。

利差统计函数:计算历史利差的滚动均值和样本标准差。

def calcSpreadStats(spreadList, lookback){
  // spreadList:不包含当前观测的历史利差向量。
  // lookback:形成有效信号所需的最小观测数量。
    out = dict(STRING, ANY)
    // 默认未就绪;用 NULL 避免上层误用无效统计量。
    out["ready"] = false
    out["mu"] = NULL
    out["sigma"] = NULL
    // 历史样本不足时直接返回,不产生交易信号。
    if (size(spreadList) < lookback) return out
    // 计算历史均值及样本标准差,分母使用 lookback-1。
    mu = avg(spreadList)
    sig = sqrt(sum(pow(spreadList - mu, 2.0)) / (lookback - 1.0))
    out["ready"] = true
    out["mu"] = mu
    out["sigma"] = sig
    return out
}

利率与久期函数:

def toRate(x){
    // 将利率统一转换为小数形式。
    // 例如 2.5 被视为 2.5%,转换为 0.025;0.025 已是小数形式,保持不变。
    if (isNull(x)) return NULL
    return iif(double(x) > 0.5, double(x) \ 100.0, double(x))
}

def approxSwapModDur(rate, years, freq){
    // 使用固定利率年金近似 IRS 固定端的修正久期。
    // rate:IRS 固定端利率;years:互换期限;freq:每年付息次数。
    // 当利率无效或非正时,使用 years*0.85 作为保守近似值。
    r = toRate(rate)
    f = double(freq)
    // 总付息期数,例如 5 年、季付对应 20 期。
    n = double(years) * f
    c = r / f
    if (c <= 0.0) return double(years) * 0.85
    return (1.0 - pow(1.0 + c, -n)) / r
}

def calcIrsQtyByDv01(bondQty, bondMult, bondDur, irsMult, irsDur){
    // 基于两腿名义本金和修正久期进行近似 DV01 配平,返回 IRS 整数手数。
    // 由于 DV01 中共同的 1bp 系数会抵消,手数比可直接用“名义本金*久期”计算。
    // 久期缺失时退化为 1 手,保证示例仍可继续运行。
    if (isNull(bondDur) or isNull(irsDur) or bondDur <= 0.0 or irsDur <= 0.0){
      return 1l
    }
    // 现券腿实际名义本金。
    nBond = double(bondQty) * double(bondMult)
    // 根据两腿久期比例计算 IRS 目标名义本金。
    nIrs = nBond * double(bondDur) / double(irsDur)
    // 除以 IRS 合约乘数并四舍五入为整数手数。
    q = long(floor(nIrs / double(irsMult) + 0.5))
    // 至少交易 1 手,避免计算结果被取整为 0。
    if (q < 1) q = 1
    return q
}

初始化函数 initialize 在回测引擎启动时调用一次,负责初始化策略参数。这里我们设置一些策略的全局参数。

def initialize(mutable context){
    // initialize 在回测引擎启动时调用一次,负责初始化策略参数。
    // ---------- 1. 交易标的与信号参数 ----------
    // 银行间国债代码;“.T1”表示本例使用的结算方式后缀。
    context["bondSym"] = "230021.T1"
    // 5 年期、以 FR007 为浮动端基准的利率互换合约。
    context["irsSym"] = "FR007_5Y"
    // 每次债券开仓数量(面额单位);IRS 数量由 DV01 配平动态计算。
    context["Lot"] = 1000000l
    // 滚动窗口长度:累计 380 个同步分钟利差后才允许产生交易信号。
    context["lookback"] = 380
    // 开仓阈值为均值上下 1.5 倍标准差。
    context["entryZ"] = 1.5
    // 平仓阈值为均值上下 0.3 倍标准差。
    context["exitZ"] = 0.3

    // ---------- 2. 最新行情缓存 ----------
    // 债券收盘净价、债券到期收益率和 IRS 固定端利率;收到行情前均为 NULL。
    context["bondClose"] = NULL
    context["bondYtm"] = NULL
    context["irsRate"] = NULL
    // 历史同步利差窗口;只保存最近 lookback 个观测。
    context["spreadList"] = array(DOUBLE, 0)
    // 记录债券和 IRS 最新一根 K 线时间,用于保证两腿数据严格同步。
    context["bondFreshDt"] = NULL
    context["irsFreshDt"] = NULL
}

初始化函数 beforeTrading 在每个交易日开始时调用,我们在每个交易日开盘前重置当日行情缓存:

def beforeTrading(mutable context){
    // beforeTrading 在每个交易日开始时调用。
    // 清空两腿“当日最新行情时间”,防止新交易日误用上一日的缓存时间配对。
    // 历史 spreadList 不清空,使滚动统计窗口能够跨交易日指标计算。
    context["bondFreshDt"] = NULL
    context["irsFreshDt"] = NULL
}

日内决策函数 onBar 是分钟行情回调,也是策略的主执行函数。其主要实现功能包括更新两腿行情缓存,并校验两腿分钟是否对齐;计算当前利差并更新滚动窗口;按 \sigma 带宽判断开平仓,并在开仓时按久期计算 IRS 手数。具体策略的实现分为 6 个步骤实现,大致如下:

def onBar(mutable context, data1){
    // onBar 是策略核心回调。data1 为本次到达的一根分钟 K 线,可能来自债券或 IRS。
    // 两种标的共用同一个回调,通过 symbol 判断并更新各自的最新行情缓存。
    sym = data1.symbol
    dt = data1.datetime
    // ---------- Step 1. 更新债券或 IRS 最新行情 ----------
    // ---------- Step 2. 检查两腿行情是否同步 ----------
    // ---------- Step 3. 计算利差并读取持仓 ----------
    // ---------- Step 4. 计算滚动统计和交易阈值 ----------
    // ---------- Step 5. 优先处理已有组合的平仓 ----------
    // ---------- Step 6. 检查开仓信号并维护窗口 ----------
  }

Step 1:更新最新行情,根据 symbol 判断当前分钟行情属于债券还是 IRS。债券行情更新收盘价、到期收益率和时间;IRS 行情更新固定端利率和时间。

if (sym == context["bondSym"]){
        // 债券 close 用于订单定价,closeYTM 用于计算与 IRS 的收益率利差。
        context["bondClose"] = data1.close
        ytm = data1.closeYTM
        context["bondYtm"] = ytm
        context["bondFreshDt"] = dt
} else if (sym == context["irsSym"]){
        // IRS close 表示本分钟固定端利率,同时作为订单价格使用。
        context["irsRate"] = data1.close
        context["irsFreshDt"] = dt
}

Step 2:检查两腿行情是否同步,只有债券和 IRS 都已经收到行情,并且两者时间戳与当前分钟完全一致时,才继续计算利差,避免混用不同时间的价格。

// ----------Step 2. 校验两腿行情是否齐备且时间完全同步 ----------
// 任意一腿尚未到达时返回,等待另一腿同一时间戳的 K 线。
if (isNull(context["bondFreshDt"]) or isNull(context["irsFreshDt"])) return
// 要求债券和 IRS 的最新时间都等于当前回调时间,防止新旧 K 线错配。
if (context["bondFreshDt"] != dt or context["irsFreshDt"] != dt) return

Step 3:计算利差并读取持仓,按照“债券到期收益率 − IRS 固定端利率”计算当前利差,同时从回测引擎读取债券和 IRS 的实际多头、空头数量。

// ---------- Step 3. 计算当前利差并读取实际持仓 ----------
// 当前债券收盘价用于下单;利差统一定义为“债券 YTM - IRS 固定端利率”。
px = context["bondClose"]
spread = context["bondYtm"] - context["irsRate"]
bond = context["bondSym"]
irs = context["irsSym"]
// 从回测引擎读取两腿真实持仓,不仅依赖策略内部标志。
posB = Backtest::getPosition(context["engine"], bond)
posI = Backtest::getPosition(context["engine"], irs)
// 分别取得债券和 IRS 的多头、空头数量,用于判断开平仓状态。
longB = long(posB.longPosition)
shortB = long(posB.shortPosition)
longI = long(posI.longPosition)
shortI = long(posI.shortPosition)

Step 4:计算滚动统计和交易阈值,使用此前 \text{lookback }个历史利差计算均值和标准差,得到上下开仓线与上下平仓线。当前利差此时尚未加入窗口,因此不会产生前视偏差。

// ----------Step 4. 使用历史窗口计算动态阈值 ----------
// 当前 spread 尚未加入 spreadList,因此阈值只依赖此前已完成的观测。
st = calcSpreadStats(context["spreadList"], context["lookback"])
 if (st["ready"]){
    // ---------- step 5. 优先处理已有组合的平仓 ----------
    // ---------- step 6. 检查开仓信号并维护窗口 ----------
 }
// 记录价差。
context["spreadList"].append!(spread)
// 窗口超过 lookback 时只保留尾部最近 lookback 个价差数据。
if (size(context["spreadList"]) > context["lookback"])
    context["spreadList"] = tail(context["spreadList"], context["lookback"])
// 清空同步时间,确保下一次计算必须重新收到债券和 IRS 同时间戳行情。
context["bondFreshDt"] = NULL
context["irsFreshDt"] = NULL

Step 5:优先处理已有组合的平仓,持有“买债+付固定”组合时,利差回落到上侧平仓线以内便卖出债券并平掉付固定;持有“卖债+收固定”组合时,利差回升到下侧平仓线以内便买回债券并平掉收固定。

mu = st["mu"]
sig = st["sigma"]
// 上下开仓线:均值 ± 1.5σ。
upEntry = mu + context["entryZ"] * sig
loEntry = mu - context["entryZ"] * sig
// 上下平仓线:均值 ± 0.3σ,形成比开仓区间更窄的退出区间。
upExit  = mu + context["exitZ"] * sig
loExit  = mu - context["exitZ"] * sig
// 债券目标数量和 IRS 当前固定端利率。
lot = context["Lot"]
irsPx = context["irsRate"]
// ---------- step 5. 优先执行已有组合的平仓逻辑 ----------
// step  5.1 当前持有债券多头时,代表此前在高利差区间执行了“买债+付固定”。
// 当利差回落至上侧退出线以下,卖出债券并平掉付固定 IRS。
if (longB > 0 and spread < upExit){
    // 债券:direction=2 卖出,openClose=2 平仓,数量使用全部实际多头。
    Backtest::submitOrder(context["engine"],
        dict(`symbol`source`qty`orderType`direction`openClose`price,
            (bond, "AGG", longB, 5, 2, 2, double(px))), "卖平债")
    // IRS:付固定在本例中对应多头;direction=2 用于反向平仓。
    if (longI > 0)
        Backtest::submitOrder(context["engine"],
            dict(`symbol`source`qty`orderType`direction`price,
                (irs, "XSWAP", longI, 5, 2, double(irsPx))), "平付固")
}
// step 5.2 当前持有债券空头时,代表此前在低利差区间执行了“卖债+收固定”。
// 当利差回升至下侧退出线以上,买回债券并平掉收固定 IRS。
else if (shortB > 0 and spread > loExit){
    // 债券:direction=1 买入,openClose=2 平仓,数量使用全部实际空头。
    Backtest::submitOrder(context["engine"],
        dict(`symbol`source`qty`orderType`direction`openClose`price,
            (bond, "AGG", shortB, 5, 1, 2, double(px))), "买平债")
    // IRS:收固定在本例中对应空头;direction=1 用于反向平仓。
    if (shortI > 0)
        Backtest::submitOrder(context["engine"],
            dict(`symbol`source`qty`orderType`direction`price,
                (irs, "XSWAP", shortI, 5, 1, double(irsPx))), "平多利差-平收固")
}

Step 6:检查开仓信号并维护窗口,只有债券和 IRS 两腿都为空仓时才允许开仓。利差突破上侧开仓线时执行“买债+付固定”,跌破下侧开仓线时执行“卖债+收固定”。开仓前根据债券和 IRS 的修正久期进行 DV01 配平,计算 IRS 手数。最后将当前利差加入历史窗口、删除超出窗口长度的旧数据,并清空两腿同步标志,等待下一组分钟行情。

// ----------Step 6. 仅在两腿完全空仓时检查新开仓信号 ----------
// 该条件防止已有组合尚未平仓时重复加仓或同时持有相反方向。
else if (longB < 1 and shortB < 1 and longI < 1 and shortI < 1){
    // 只有当前利差突破任一开仓边界时,才计算久期和 IRS 对冲数量。
    if (spread > upEntry or spread < loEntry){
        // 将债券 YTM 转成小数形式。
        bytm = toRate(context["bondYtm"])
        // bondDuration 返回债券麦考利久期;日期、票息、频率和 YTM 来自参考数据及行情。
        macD = bondDuration(context["bondStart"], 
        context["bondMaturity"], context["bondIssuePrice"],
            context["bondCoupon"], context["bondFreq"], 
            "ActualActualISMA", "FixedRate",
            date(dt), bytm, "YTM")
        // 麦考利久期转换为修正久期,用于近似债券 DV01。
        Db = macD / (1.0 + bytm / double(context["bondFreq"]))
        // 本例假设 IRS 期限为 5 年、固定端按季支付,每年 4 次。
        Ds = approxSwapModDur(irsPx, 5.0, 4)
        // 根据两腿修正久期和合约乘数,将债券数量换算为 IRS 整数手数。
        irsLot = long(calcIrsQtyByDv01(context["Lot"], 
        context["bondMult"], Db, context["irsMult"], Ds))

        // step 6.1 利差高于上侧开仓线:买入债券,同时在 IRS 上付固定。
        // 买债提供正久期,付固定提供负久期,形成近似 DV01 中性的组合。
        if (spread > upEntry){
            Backtest::submitOrder(context["engine"],
                      dict(`symbol`source`qty`orderType`direction`openClose`price,
                            (bond, "AGG", lot, 5, 1, 1, double(px))), "买债")
                    Backtest::submitOrder(context["engine"],
                        dict(`symbol`source`qty`orderType`direction`price,
                            (irs, "XSWAP", irsLot, 5, 1, double(irsPx))), "付固定")
          }
        //step 6.2 利差低于下侧开仓线:融券卖出债券,同时在 IRS 上收固定。
        // 卖债提供负久期,收固定提供正久期,形成反向配对组合。
        else if (spread < loEntry){
            Backtest::submitOrder(context["engine"],
                      dict(`symbol`source`qty`orderType`direction`openClose`price,
                            (bond, "AGG", lot, 5, 2, 1, double(px))), "卖债")
                    Backtest::submitOrder(context["engine"],
                        dict(`symbol`source`qty`orderType`direction`price,
                            (irs, "XSWAP", irsLot, 5, 2, double(irsPx))), "收固定")
                }
          }
}

利率互换下单时,submitOrder 不需要 openClose 字段,只需指定交易方向 direction:付固定 direction = 1,收固定 direction = 2;现券订单仍需同时给出 direction 与 openClose,具体参考 submitOrder 接口说明。

2.3 设置回测参数

本案例回测配置示例如下:策略组为 FICC,使用 OHLC(dataType=1),并输出订单信息。

config = {
    // 回测起止日期。
    startDate: begt,
    endDate: endt,
    // 债券与 IRS 属于固收品种,使用 FICC 策略组。
    strategyGroup: "FICC",
    // 初始资金 10 亿元。
    cash: 1000000000.0,
    // dataType=1 表示使用 OHLC K 线行情驱动 onBar。
    dataType: 1,
    // matchingMode=1 使用本例对应的撮合模式。
    matchingMode: 1,
    // 策略只接收指定债券和 5 年期 FR007 IRS 的行情。
    universe: [["230021.T1", "FR007_5Y"]],
    // 允许债券卖空,反向套利依赖此配置。
    enableBondLending: true,
    // 输出订单状态,便于回测结束后筛选有效成交。
    outputOrderInfo: true,
    //从合约参考表读取估值和对冲参数
    context:{
    	// // IRS 每手代表的名义本金,用于将目标 IRS 名义本金换算为合约手数。
    	irsMult: double((exec multiplier from xswapRef)[0]),
    	 // 债券年付息次数、起息日、到期日、发行价和乘数,用于计算久期与名义本金。
    	bondFreq: int((exec frequency from bondRef)[0]),
    	bondStart: date((exec valueDate from bondRef)[0]),
    	bondMaturity:date((exec maturityDate from bondRef)[0]),
    	bondIssuePrice: double((exec issuePrice from bondRef)[0]),
    	bondMult:double((exec multiplier from bondRef)[0]),
    	 // 票面利率统一转换为小数形式,供 bondDuration 使用。
    	bondCoupon: toRate((exec couponRate from bondRef)[0])	 	
    }
    
}

2.4 创建回测引擎

利率互换策略的 securityReference 需设置四张基础信息表:xswap 合约条款表、swapCurve 利率曲线、swapAssetPriceHistCurve 浮动基准历史定盘,以及债券的 xbond 基础信息表。

// key 必须与插件识别的资产类别一致。
securityReference ={
	xbond: bondRef,
	xswap:xswapRef,
	swapCurve: swapCurve,
	swapAssetPriceHistCurve:swapAssetPriceHistCurve
}

设置引擎名称、配置项与事件回调后,先通过 Backtest::dropBacktestEngine 接口清除同名引擎,通过 Backtest::createBacktester 创建回测引擎。

// 设置策略回调函数
callbacks = {
    initialize: initialize,
    beforeTrading: beforeTrading,
    onBar: onBar,
    afterTrading: afterTrading
}
// 使用固定引擎名称;创建前先删除同名历史引擎,保证脚本可重复执行。
strategyName = "ASW_basis_xswap"
try{ Backtest::dropBacktestEngine(strategyName) }catch(ex){ print(ex) }
engine = Backtest::createBacktester(strategyName, config, callbacks, false, securityReference)

Backtest::createBacktester 用于创建回测引擎,参数说明如下:

Backtest::createBacktester(name, config, eventCallbacks, securityReference)
// name 表示回测引擎名称
// config 字典类型,表示回测引擎的配置项
// eventCallbacks 字典类型,表示策略回调函数
// securityReference 字典类型,合约的基本信息表,key 是回测的资产类型,value 是一个表,代表相应的基本信息表。

2.5 执行回测引擎

先设置行情字典。msg 是字典形式,key 可取 “snapshot”、“ohlc”、”transaction”、“curveSignal” 四种,值为对应行情数据表。本例仅使用分钟行情触发策略。

// Backtest 行情消息使用字典封装;ohlc key 会把数据路由到 onBar。
msg = dict(STRING, ANY)
// 按时间升序回放,确保滚动窗口和两腿时间同步判断正确。
msg["ohlc"] = select * from ohlcData order by datetime

通过 appendQuotationMsg 接口向引擎插入行情并执行回测:

Backtest::appendQuotationMsg(engine, msg)

通过 appendEndMarker 接口结束回测:

Backtest::appendEndMarker(engine)

2.6 获取回测结果

回测结束后,可通过接口获取成交明细、收益汇总以及组合权益与资金等结果。使用 getTradeDetails 查看成交明细,getReturnSummary 查看收益统计,getTotalPortfolios 查看组合层面的权益与可用资金变化:

dailyTotalPortfolios = Backtest::getDailyTotalPortfolios(engine)
allTradeDetails      = Backtest::getTradeDetails(engine)
returnSummary        = Backtest::getReturnSummary(engine)
print(dailyTotalPortfolios)

下图为本例获得的交易明细数据:

图 1. 图 2-1 交易明细

3. 总结

本案例实现了一个基于国债到期收益率与同期限利率互换固定端利率之间利差变化的均值回归套利策略。策略使用同步分钟行情计算“债券 YTM-IRS 固定端利率”利差,并根据历史滚动窗口估计均值与标准差;利差突破均值上方 1.5 倍标准差时买入债券、在 IRS 上付固定,利差跌破均值下方 1.5 倍标准差时融券卖出债券、在 IRS 上收固定,并在利差回归至均值附近 0.3 倍标准差范围时平仓。开仓时根据债券修正久期与 IRS 近似修正久期进行 DV01 配平,确定互换交易数量。

本例展示了从模拟债券与 IRS 分钟行情导入、两腿行情同步校验、滚动利差统计、动态开平仓阈值计算、债券久期估算、IRS 对冲数量确定、两腿配对下单到回测结果读取的完整流程。

用于真实投资研究时,应:

  • 以具有明确可用时点的真实债券行情、到期收益率、IRS 报价、利率曲线和历史定盘数据替换模拟数据。

  • 进一步评估 DV01 与凸性匹配、期限错配、债券流动性、IRS 报价深度、单腿成交风险、滑点、交易成本和样本外表现。

  • 检验债券与 IRS 利差的平稳性、均值回归速度和参数稳定性,避免策略收益仅依赖特定市场阶段。