期货前日高低价区间突破策略

本策略是一种基于 Dual Thrust 思想的期货前日高低价区间突破策略,以前一交易日的最高价与最低价之差作为区间宽度,以当日第一根有效 1 分钟 bar 的开盘价为基准设置上下轨。当价格突破上轨时做多,跌破下轨时做空,并在交易日临近收盘时强制平仓,实现日内开平仓。

本策略在 3 个模拟标的上分别判断信号并独立维护持仓,每笔交易固定 1 手;14:55 及之后不再开仓,并提交平仓委托。策略使用 DolphinDB 的 Backtest 回测引擎实现。

1. 策略介绍

本策略先根据前一交易日高低价计算区间宽度,再以当日开盘价为基准设置上下轨,并根据每根 1 分钟 bar 的价格判断是否触发突破信号。下面分别说明区间与信号的算法、交易标的和执行规则。

1.1 核心因子

策略以前一交易日的最高价与最低价之差衡量当前的波动幅度,再以当日开盘价为基准向两侧扩展出两条轨道:

rangePrice t = HH t - 1 - LL t - 1 \text{rangePrice}_t=\text{HH}_{t-1}-\text{LL}_{t-1}

longPrice t = open t + k 1 · rangePrice t , shortPrice t = open t − k 2 · rangePrice t \text{longPrice}_t=\text{open}_t+k_1\cdot\text{rangePrice}_t,\qquad\text{shortPrice}_t=\text{open}_t-k_2\cdot\text{rangePrice}_t

其中 HH 与 LL 分别表示前一交易日的最高价与最低价,open 表示当日开盘价,k1 = 0.4、k2 = 0.6 为策略设定的两个倍数。三个量都与价格同单位。由于 k1 < k2,上轨距离当日开盘价更近,下轨距离更远,因此在其他条件相同的情况下,多头突破所需的价格变动幅度小于空头突破,多头信号更容易触发。

策略的实现与经典 Dual Thrust 存在以下主要差异,使用时需要留意:

  • 区间只取前一交易日的最高价与最低价之差,没有使用经典 Dual Thrust 中基于最高价、最低价和前收盘价计算的 max(HH - LC, HC - LL),也没有使用 N 日滚动区间。

  • 上下轨倍数非对称(0.4 与 0.6),而不是常见的对称取值。

  • 离场方式改为按时间强制离场:14:55 及之后不再开仓;若存在持仓,则提交平仓委托,不再使用价格回到当日开盘价即离场的规则。

  • 每次交易固定 1 手,不根据波动率或账户权益动态调整仓位。

当日开盘价由策略根据回测数据确定:当日第一根成交量大于 0 的 1 分钟 bar 的 open 作为 dayOpen,之后不再更新;当日最高价与最低价则在每根有效 bar 上滚动更新。该处理方式取的是回测数据中的第一根有效 1 分钟 bar 的开盘价,并不等同于直接读取交易所提供的正式开盘价;但由于开盘价仅取当前已到达的第一根有效 bar,因此不会使用当前 bar 之后的未来行情数据。

1.2 交易标的

  • 资产类型与方向:期货合约。本回测中,引擎分别维护多头和空头持仓,使用普通期货现金账户,不涉及融资融券。

  • 标的映射:数据中的 securityid 被补齐为 6 位字符串,形成 000001、000002、000003 三个模拟标的。三个标的分别独立判断信号和维护持仓,不做跨品种对冲,也不进行横截面排序。

  • 行情频率:1 分钟频(dataType = 3)。本次数据共 15,447 行,对应 3 个模拟连续合约与 19 个交易日,平均每个合约每个交易日约 271 根 bar。bar 的时间戳由数据生成端按交易时段切分。

  • 模拟标的与真实标的的区别:上述合约代码仅用于构造测试数据,并不对应真实期货品种或交易所连续合约。同一代码在不同交易日不代表同一真实期货合约。接入真实行情时,需要替换为交易所合约代码及其对应行情数据。

  • 合约乘数与保证金:基本信息表中合约乘数为 100、保证金率为 0.2。按该模拟参数计算,若价格约为 1000,则 1 手合约的名义价值约为 10 万元,对应的模拟保证金占用约为 2 万元。

  • 交易成本:手续费为每手 1.5 元,税项为 0。config 与基本信息表中的手续费参数均写为 1.5,两处取值一致。

1.3 交易规则

一个交易日的完整流程按以下顺序执行:

  1. 每日盘前回调清空当日的最高价、最低价、开盘价记录,为当天的行情累积做准备。

  2. 每根 1 分钟 bar 到来时,先跳过成交量为 0 的 bar。对于有效 bar,若尚未记录当日开盘价,则将该 bar 的 open 记录为 dayOpen,同时更新当日最高价和最低价。

  3. 如果该合约还未获得前一交易日的有效最高价和最低价,例如回测第一个交易日或前一交易日没有有效行情,则跳过当前 bar,不产生交易信号。

  4. 计算 rangePrice = 前一交易日最高价 − 前一交易日最低价,并据此计算上轨 open + 0.4 × rangePrice、下轨 open − 0.6 × rangePrice。

  5. 读取该合约当前的多头与空头持仓。

  6. 当 bar 时间早于 14:55 时,按照策略代码中的分支顺序判断交易条件,并仅执行第一个成立的分支。

  7. 当 bar 时间不早于 14:55 时不再开仓。若存在多头持仓,则以 收盘价 × 0.99 提交平多委托;若存在空头持仓,则以 收盘价 × 1.01 提交平空委托。这里的 1% 为平仓委托价格相对于当前 bar 收盘价的偏移,用于模拟尾盘采用更激进报价进行平仓的情形。

  8. 平仓数量取引擎返回的实际持仓数量。若平仓委托全部成交,则对应方向的持仓被清零,从策略设计上不留隔夜仓。

订单执行时点需要特别注意:信号判断与委托提交发生在当前 bar 上。开仓委托价格使用当前 bar 的收盘价,尾盘平仓委托价格则在当前 bar 收盘价的基础上进行 1% 的价格偏移。委托提交后由后续行情进行撮合,因此信号判断、委托提交和实际成交并非同一时点,也不会使用委托提交之后的未来行情计算信号。

2. 策略回测

本策略基于期货分钟频进行回测,需要服务器已安装 Backtest 与 MatchingEngineSimulator 两个插件,并能够读取策略指定的分钟行情 CSV 文件。

2.1 准备数据

下载附录中的futuresBar_dualThrust.csv文件作为回测数据。将 <path> 替换为 CSV 实际存储路径后,导入对应数据。

回测输入是一张按时间排序的分钟行情表。案例数据由模拟数据生成流程构造:先按交易日得到期货快照,再把快照聚合成 1 分钟 bar,把每个交易日随机生成的合约代码映射为固定的模拟连续合约。

表 1. 表 2-1 分钟行情表字段说明
列名 类型 说明
securityid SYMBOL 合约代码,取值为 000001 至 000003
tradeTime TIMESTAMP 时间戳
open DOUBLE 开盘价
high DOUBLE 最高价
low DOUBLE 最低价
close DOUBLE 收盘价
vol INT 成交量
val DOUBLE 成交额
upLimitPrice DOUBLE 涨停价,最高价上浮 10%
downLimitPrice DOUBLE 跌停价,最低价下浮 10%
prevClosePrice DOUBLE 上一根 bar 的收盘价,首根 bar 取该 bar 的开盘价

本次回测数据包含 3 个模拟连续合约、19 个交易日、共 15,447 行 1 分钟 bar。

barRaw = loadText("/.../futuresBar_dualThrust.csv")
tradeBars = select lpad(string(securityid), 6, "0") as symbol, "MockData" as symbolSource,
        timestamp(tradeTime) as tradeTime, date(tradeTime) as tradingDay,
        double(open) as open, double(high) as high, double(low) as low, double(close) as close,
        long(vol) as volume, double(val) as amount,
        double(upLimitPrice) as upLimitPrice, double(downLimitPrice) as downLimitPrice,
        double(prevClosePrice) as prevClosePrice
    from barRaw order by tradeTime, symbol

2.2 编写策略逻辑

策略由 initialize、beforeTrading、onBar 和 afterTrading 回调函数组成。其中,initialize 用于初始化策略参数和状态变量,beforeTrading 用于清空当日行情缓存,onBar 负责逐根处理行情、计算交易信号并提交委托,afterTrading 用于在交易日结束后保存当日行情数据,供下一交易日计算区间。

初始化回调:initialize,可变 DICTIONARY,由创建引擎时传入。

  • k1:DOUBLE 类型标量,上轨倍数,取 0.4。

  • k2:DOUBLE 类型标量,下轨倍数,取 0.6。

  • qty:INT 类型标量,每次开仓的委托手数,取 1。

  • closeTime:SECOND 类型标量,尾盘平仓的起始时间,取 14:55:00。

  • lastHigh、lastLow:以 SYMBOL 为键、DOUBLE 为值的 DICTIONARY,分别保存前一交易日的最高价与最低价,初始为空字典。

def initialize(mutable context) {
    context["k1"] = 0.4
    context["k2"] = 0.6
    context["qty"] = 1
    context["closeTime"] = 14:55:00
    context["lastHigh"] = dict(SYMBOL, DOUBLE)     // 前一交易日最高价
    context["lastLow"] = dict(SYMBOL, DOUBLE)      // 前一交易日最低价
}

每日盘前回调:beforeTrading

该函数没有返回值,副作用是把 dayHigh、dayLow、dayOpen 三个字典重建为空字典(键为 SYMBOL,值为 DOUBLE),表示当日还没有累积任何行情。三者都按合约分别记录,因为各合约独立判断交易信号和管理持仓。

def beforeTrading(mutable context) {
    // 每日期初重置当日最高/最低/开盘
    context["dayHigh"] = dict(SYMBOL, DOUBLE)
    context["dayLow"] = dict(SYMBOL, DOUBLE)
    context["dayOpen"] = dict(SYMBOL, DOUBLE)
}

每日盘后回调:afterTrading

afterTrading(mutable context) 在当日行情处理完成后调用,将当日累积的最高价与最低价字典分别赋给 lastHigh 与 lastLow,供下一交易日计算区间使用。由于赋值发生在当日行情处理完成之后,下一交易日使用的区间仅基于前一交易日已经完成的行情数据,不会使用当前交易日尚未发生的行情,因此不存在未来数据泄露。

def afterTrading(mutable context) {
    // 收盘后把当日高低价留给下一交易日计算区间
    context["lastHigh"] = context["dayHigh"]
    context["lastLow"] = context["dayLow"]
}

行情回调:onBar

onBar(mutable context, msg, indicator) 是策略的核心,在每根 1 分钟 bar 到达时调用,负责累积当日行情、计算前一交易日区间和当日上下轨、判断交易信号并提交委托。

  • context:可变 DICTIONARY,保存 k1、k2、qty、closeTime、lastHigh、lastLow 以及当日的 dayHigh、dayLow、dayOpen 等状态变量。

  • msg:DICTIONARY,以合约代码为键、以当前 bar 的行情为值。函数通过 for (s in msg.keys()) 遍历其中的全部合约,因此可以逐合约处理当前回调中的行情;单根 bar 包含 symbol、symbolSource、tradeTime、open、high、low、close、volume 等字段。

  • indicator:本策略没有订阅指标,配置中也没有传入指标,因此该参数虽然由接口保留,但在函数内部未使用。

函数没有返回值,副作用是更新上下文中的当日行情缓存,并根据交易信号向引擎提交委托。

关键实现细节:

  1. 逐合约、逐根 bar 独立判断:每根 1 分钟 bar 到达时,分别处理当前回调中的各个合约。若某个合约当前 bar 的成交量为 0,则跳过该 bar;各合约分别判断交易信号,持仓互不影响。

  2. 维护当日的开盘价与最高最低价:每个合约当天第一次出现有效 bar 时,将该 bar 的 open 记录为当日开盘价 dayOpen,并将 high 和 low 初始化为当日最高价和最低价;之后每出现一根有效 bar 就分别更新当日最高价和最低价。由此形成截至当前 bar 已出现的当日价格区间。

  3. 根据前一交易日区间计算当日上下轨:以前一交易日的最高价减最低价得到区间宽度,再以当日开盘价 dayOpen 为基准计算上轨和下轨:上轨为 dayOpen + 0.4 × rangePrice,下轨为 dayOpen - 0.6 × rangePrice。如果该合约尚未保存前一日的有效最高价和最低价,例如回测首日,则仅累积当日行情,不产生交易信号。

  4. 根据突破方向开仓或反手:14:55 之前,根据当前 bar 的收盘价判断是否突破上下轨。若收盘价高于上轨,则产生做多信号;若收盘价低于下轨,则产生做空信号。空仓时按信号方向开仓;若已有反方向持仓,则先提交平仓委托,再按信号方向提交开仓委托。每次开仓固定 1 手。

  5. 尾盘强平:当时间不早于 14:55 时不再开新仓。若存在多头持仓,则以当前 bar 收盘价的 99% 作为平仓委托价格;若存在空头持仓,则以当前 bar 收盘价的 101% 作为平仓委托价格。这里的价格偏移用于以更激进的报价提交尾盘平仓委托,以降低临近收盘时无法成交的可能性。若平仓委托全部成交,则当日持仓被清零,从策略设计上不留隔夜仓。

综上,本策略的区间宽度完全来自前一交易日的最高价与最低价之差,上下轨道围绕当日开盘价分别按 0.4 倍(做多)和 0.6 倍(做空)区间宽度设置。由于两条轨道不对称,上轨距离开盘价更近,因此向上突破比向下突破更容易触发。每个合约各自独立判断信号和管理持仓,每次开仓固定 1 手,不进行跨品种对冲。 14:55 之后只平不开;回测首日由于尚未获得前一交易日的高低价数据,因此不产生交易信号。

def onBar(mutable context, msg, indicator) {
    for (s in msg.keys()) {
        bar = msg[s]
        if (bar["volume"] <= 0) { continue }
        // 当日最高/最低/开盘
        dayHigh = iif(context["dayHigh"][s] == 0., bar["high"], max(context["dayHigh"][s], bar["high"]))
        dayLow = iif(context["dayLow"][s] == 0., bar["low"], min(context["dayLow"][s], bar["low"]))
        dayOpen = iif(context["dayOpen"][s] == 0., bar["open"], context["dayOpen"][s])
        context["dayHigh"][s] = dayHigh
        context["dayLow"][s] = dayLow
        context["dayOpen"][s] = dayOpen
        // 首日没有前一交易日区间,只累积当日高低价
        if (isNull(context["lastHigh"][s]) || context["lastHigh"][s] <= 0.) { continue }
        rangePrice = context["lastHigh"][s] - context["lastLow"][s]
        longPrice = dayOpen + context["k1"] * rangePrice
        shortPrice = dayOpen - context["k2"] * rangePrice
        closePx = bar["close"]
        pos = Backtest::getPosition(context.engine, s)
        longPos = iif(size(pos) > 0, long(pos["longPosition"]), 0l)
        shortPos = iif(size(pos) > 0, long(pos["shortPosition"]), 0l)
        if (time(bar["tradeTime"]) < context["closeTime"]) {
            if (longPos <= 0 && shortPos <= 0 && closePx > longPrice) {
                Backtest::submitOrder(context.engine,
                    (s, bar["symbolSource"], bar["tradeTime"], 5, closePx, 0., context["qty"], 1, 0), "上破开多")
            } else if (longPos <= 0 && shortPos <= 0 && closePx < shortPrice) {
                Backtest::submitOrder(context.engine,
                    (s, bar["symbolSource"], bar["tradeTime"], 5, closePx, 0., context["qty"], 2, 0), "下破开空")
            } else if (shortPos > 0 && closePx > longPrice) {
                Backtest::submitOrder(context.engine,
                    (s, bar["symbolSource"], bar["tradeTime"], 5, closePx, 0., shortPos, 4, 0), "平空")
                Backtest::submitOrder(context.engine,
                    (s, bar["symbolSource"], bar["tradeTime"], 5, closePx, 0., context["qty"], 1, 0), "反手做多")
            } else if (longPos > 0 && closePx < shortPrice) {
                Backtest::submitOrder(context.engine,
                    (s, bar["symbolSource"], bar["tradeTime"], 5, closePx, 0., longPos, 3, 0), "平多")
                Backtest::submitOrder(context.engine,
                    (s, bar["symbolSource"], bar["tradeTime"], 5, closePx, 0., context["qty"], 2, 0), "反手做空")
            }
        } else {
            // 尾盘强平
            if (longPos > 0) {
                Backtest::submitOrder(context.engine,
                    (s, bar["symbolSource"], bar["tradeTime"], 5, closePx * 0.99, 0., longPos, 3, 0), "尾盘平多")
            }
            if (shortPos > 0) {
                Backtest::submitOrder(context.engine,
                    (s, bar["symbolSource"], bar["tradeTime"], 5, closePx * 1.01, 0., shortPos, 4, 0), "尾盘平空")
            }
        }
    }
}

2.3 设置回测参数

config = {
    startDate: 2022.01.04,
    endDate: 2022.01.28,
    strategyGroup: "futures",
    cash: 10000000.0,
    commission: 1.5,
    tax: 0.0,
    dataType: 3
}
  • startDate:回测开始日期。

  • endDate:回测结束日期。

  • strategyGroup:策略类型。

  • cash:初始资金。

  • commission:手续费,取 1.5,单位为元/手。

  • tax:税费,取 0.0,即不计提税费。纳税口径因品种与地区而异,接入真实交易前应按实际规则确认。

  • dataType:行情类型,取 3 表示分钟频行情。

合约基本信息表由行情中出现的合约代码生成,每个合约使用同一组基本信息:

// 合约信息:乘数 / 保证金率 / 最小变动单位 / 手续费
symbolList = distinct(tradeBars.symbol)
securityReference = table(symbolList as symbol,
        take(100.0, size(symbolList)) as multiplier,
        take(0.2, size(symbolList)) as marginRatio,
        take(0.01, size(symbolList)) as tradeUnit,
        take(0.02, size(symbolList)) as priceUnit,
        take(0.03, size(symbolList)) as priceTick,
        take(1.5, size(symbolList)) as commission,
        take(1, size(symbolList)) as deliveryCommissionMode)
表 2. 表 2-2 合约基本信息表字段说明
列名 类型 本例取值 说明
symbol SYMBOL 三个合约 合约代码,与行情表 symbol 对应
multiplier DOUBLE 100.0 合约乘数,用于把价格折算为合约名义金额
marginRatio DOUBLE 0.2 保证金率,按名义金额的比例占用保证金
tradeUnit DOUBLE 0.01 交易单位,策略未读取,建议按交易所公布口径填写
priceUnit DOUBLE 0.02 价格单位,策略未读取,建议按交易所公布口径填写
priceTick DOUBLE 0.03 最小变动价位,策略未读取,建议按交易所公布口径填写
commission DOUBLE 1.5 每手手续费,单位为元,与 config 中的 commission 一致
deliveryCommissionMode INT 1 交割手续费计费模式,策略未读取
  • 该表采用静态模式:表中不含 tradeDate 列,整个回测区间内始终使用这一份基本信息。若要在回测中途修改乘数、保证金率或手续费,需要在表中增加 tradeDate 列,并按交易日提供记录。

  • 合约乘数与保证金率直接影响账户权益与保证金占用。本次回测中,合约乘数为 100,保证金率为 0.2。按价格约为 1000 计算,1 手合约的名义价值约为 10 万元,保证金占用约为 2 万元,占初始资金 1000 万元的比例较小。

  • 合约基本信息表中的参数以及手续费沿用官方分钟频期货回测示例中的模拟取值。使用真实行情进行回测时,应根据实际合约规则逐项核对合约乘数、保证金率和手续费的单位及计算口径。

2.4 创建回测引擎

创建引擎前需要先加载两个插件:

  • MatchingEngineSimulator:撮合引擎模拟器,为 Backtest 提供模拟撮合能力。

  • Backtest:回测插件,提供 createBacktester、submitOrder、appendQuotationMsg 等接口。

// ============================ 回测插件 ============================
try{loadPlugin("MatchingEngineSimulator")}catch(ex){ }
try{loadPlugin("Backtest")}catch(ex){ }
go

创建引擎时登记策略实际使用的四个回调函数。一个节点同时只能运行一个 Backtest 引擎实例。引擎会占用节点内存,因此重复运行策略前应先删除已有引擎。本案例策略已通过 dropBacktestEngine 处理,回测结束后也建议及时释放引擎。

// ========================= 创建回测引擎 =========================
callbacks = dict(STRING, ANY)
callbacks["initialize"] = initialize
callbacks["beforeTrading"] = beforeTrading
callbacks["onBar"] = onBar
callbacks["afterTrading"] = afterTrading

engineName = "dualThrustStrategy"
try { Backtest::dropBacktestEngine(engineName) } catch(ex) { }
engine = Backtest::createBacktester(engineName, config, callbacks, false, securityReference)

2.5 执行回测

数据准备完成后,将行情写入引擎并按时间顺序回放。回放结束时,通过 Backtest::appendEndMarker(engine) 向引擎发送结束标记,以通知引擎行情已全部发送完毕,之后引擎不再接收行情。

// ========================= 执行回测 =========================
Backtest::appendQuotationMsg(engine, tradeBars)
Backtest::appendEndMarker(engine)

引擎按交易时间顺序处理行情:每进入一个新交易日先触发 beforeTrading,每根 bar 触发一次 onBar,当日行情结束后触发 afterTrading。

回放过程中,onBar 根据当前行情判断交易信号并提交委托;委托经引擎相关校验后进入模拟撮合流程,成交结果以及持仓、资金等状态随回放过程更新。因此,行情回放结束后即可读取回测结果,无需额外执行结算步骤。

2.6 获取回测结果

策略在回放结束后读取四类结果:

// ============================ 回测结果 ============================
tradeDetails = Backtest::getTradeDetails(engine)
returnSummary = Backtest::getReturnSummary(engine)
dailyPosition = Backtest::getDailyPosition(engine)
totalPortfolios = Backtest::getDailyTotalPortfolios(engine)
  • tradeDetails:记录委托、成交及状态变化,可通过 label 区分上破开多、下破开空、平多、平空、反手做多、反手做空、尾盘平多、尾盘平空等动作。本次回测共成交 108 手。

  • returnSummary:查看区间收益、年化收益与波动、偏度峰度、夏普、最大回撤与回撤比率、beta/alpha、换手率与日胜率。

  • dailyPosition:查看每个合约逐日的双向持仓(多头与空头分别记录)、各自的持仓均价、当日买卖量与金额以及收盘价。

  • totalPortfolios:查看账户现金、市值、总权益、净值与累计收益率的逐日曲线,以及浮盈浮亏、已实现盈亏、手续费和冻结资金。

3. 回测结果分析

本节结果来自 DolphinDB Server 3.00.6上的一次实际运行。回测区间为 2022-01-04 至 2022-01-28,初始资金 1,000 万元,每次下单 1 手,手续费 1.5 元/手。尾盘平仓委托价格分别按当前 bar 收盘价的 0.99 倍和 1.01 倍提交,用于提高平仓委托的成交概率。

表 3. 表 3-1 策略回测绩效指标汇总
指标 数值
区间总收益 -0.0351%
年化收益 -0.4651%
年化波动率 0.0071%
最大回撤 0.0351%
夏普比率 -65.3793
换手率 0%
日胜率 0%

回测明细共 216 行,其中 108 行是成交记录,成交数量合计 108 手;成交覆盖 3 个模拟连续合约和 18 个具备前一交易日区间的交易日。回测首日没有可用的前一交易日区间,因此按策略设计不产生交易。3 个合约 × 18 个可用交易日共形成 54 个合约-交易日,108 手成交与这 54 个合约-交易日的一开一平记录相对应。单手合约的名义金额约为 10 万元(1 手 × 价格约 1,000 × 合约乘数 100),约占 1,000 万元初始资金的 1%。因此,单笔交易对账户总权益的影响较小,整体权益曲线接近水平线。

结合上述结果,可以得到以下几点结论:

  1. 本次样本的收益和风险指标不适合用于评价策略有效性。区间总收益 -0.0351% 对应约 3,510 元的亏损,而按 108 手、1.5 元/手估算的手续费合计约 162 元,只占初始资金的 0.0016%。因此,除手续费外的大部分损益来自实际成交价格与策略委托价格之间的价差等撮合结果。由于单笔交易名义金额仅约占账户资金的 1%,本次样本的收益、波动率等指标本身就较小;夏普比率达到 -65.3793,主要是由于收益波动的分母极小,不宜将其解读为策略具有极高的风险或极差的风险调整收益。换手率和日胜率同样不宜脱离本次样本规模单独评价策略。

  2. 本次开仓方向单一,不能据此判断策略的方向性判断能力。本次运行触发的开仓全部是“上破开多”,没有触发“下破开空”。这与策略设置的非对称上下轨有关:k1 = 0.4、k2 = 0.6,因此上轨距离当日开盘价更近,下轨距离更远;同时,本次模拟行情的日内价格波动也影响了实际信号触发。由于样本仅包含 18 个具备前一交易日区间的交易日,不能据此判断策略在多头或空头方向上的表现差异。

  3. 本次收益接近零与模拟数据的构造方式有关,不能据此推断真实期货市场中的策略表现。原始快照数据中的部分价格字段不适合直接用于构造连续价格序列,因此加工脚本根据逐笔变化的 AveragePrice 与当日均值之间的偏差重建价格序列。该处理方式使构造出的日内价格变化围绕零值波动,因而不会自然形成明显的跨日趋势。策略收益主要由日内突破及其后的平仓结果决定,在样本较短、单笔仓位相对初始资金较小的情况下,最终收益接近零是可以预期的。真实期货行情还会受到趋势、基差、合约换月等因素影响,因此本次模拟数据上的回测结果仅用于展示策略逻辑和 Backtest 回测流程,不能据此评价策略在真实期货品种上的实际表现。

4. 总结

本策略是基于 Dual Thrust 思想的一个变体:以 rangePrice = 前一交易日最高价 − 前一交易日最低价 衡量前一交易日的价格区间,以当日开盘价为基准生成 open + 0.4 × rangePrice 和 open − 0.6 × rangePrice 两条轨道;收盘价高于上轨时开多,低于下轨时开空,出现反向信号时先平仓再开仓;14:55 及之后不再开仓,并按 0.99 与 1.01 的模拟滑点强制平仓。每次下单固定 1 手,3 个模拟标的分别独立判断信号和维护持仓。

实现上的关键点有三处:

  1. 区间仅使用前一交易日数据。afterTrading 在当日行情处理完成后,将当日累积的最高价和最低价保存到上下文中,下一交易日据此计算区间。因此,计算当前交易日信号时不会使用当前交易日尚未发生的行情,不存在未来数据泄露。

  2. 当日开盘价取第一根有效 bar 的 open。策略将当日第一根成交量大于 0 的 1 分钟 bar 的 open 作为 dayOpen,而不是直接读取交易所的正式开盘价。由于该价格来自当前已到达的第一根有效 bar,不会使用其后的行情数据。

  3. 平仓数量根据实际持仓确定。每次下单前通过 Backtest::getPosition 重新读取当前持仓,平仓数量取实际持仓数量;判断是否空仓时同时检查多头和空头持仓,避免超量平仓或反手过程中重复建仓。

使用本策略时需要注意以下限制:

  • 成本与滑点是模拟值。手续费按每手 1.5 元计,尾盘平仓委托价格分别按当前 bar 收盘价的 0.99 倍和 1.01 倍提交,以提高平仓委托的成交概率。这种价格偏移不等同于真实交易中的固定滑点;使用真实行情回测时,应根据实际交易成本、市场冲击和流动性重新设置相关参数。

  • 模拟标的未实现真实连续合约的换月处理。本案例中的模拟标的按交易日映射,并不对应真实交易所连续合约。使用真实期货数据回测时,需要根据具体品种处理主力合约切换、基差和结算价等因素。

  • 固定手数使资金利用率较低。每笔交易的名义金额约占账户资金的 1%,因此本次回测的收益和风险指标较小,部分指标容易受到手续费及成交价格偏差的影响。若用于策略评价,可进一步将 qty 与账户规模或目标波动率关联,并同步检查保证金占用和资金约束。

  • 参数采用固定值且多空阈值非对称。k1、k2 和 closeTime 均为固定参数,其中 k1 = 0.4、k2 = 0.6 使上轨和下轨与开盘价的距离不同。使用真实历史数据进行评估时,建议对这些参数进行敏感性分析,检验回测结果是否依赖特定参数取值。

  • 分钟频率无法还原 bar 内的价格路径。仅使用 1 分钟 bar 时,无法确定价格在 bar 内的具体运行路径,因此难以准确判断突破发生的时点及委托提交后的成交过程。若改用快照或逐笔行情,需要相应调整信号触发和委托撮合逻辑。

  • 当前策略未针对 JIT 模式进行优化。策略回调中使用的嵌套字典操作等实现方式需要结合 JIT 模式的语法和执行限制进行检查。若需要启用 JIT,应先根据实际 JIT 约束重构相关代码并验证运行结果。

  • 回测样本较短。本次仅覆盖 18 个具备前一交易日区间的交易日,且使用的是模拟行情数据。当前结果主要用于验证策略逻辑和 Backtest 回测流程,不能据此评价策略在真实期货市场中的长期盈利能力。