期货凯尔特纳通道突破策略
凯尔特纳通道突破策略是一种典型的趋势跟踪型量化交易策略,通过价格突破基于均线和波动率构建的通道,识别潜在的趋势行情,并据此进行买卖。本文先阐述策略的基本思路,再以期货交易案例,说明数据准备、交易逻辑与回测分析方法。
1. 策略介绍
本策略包含通道计算、双向挂单和持仓退出三个环节。下面分别介绍因子含义、交易标的和交易规则。
1.1 核心因子
简单移动平均线( Simple Moving Average,SMA)表示最近一段时间收盘价的平均水平;平均真实波幅(Average True
Range,ATR)描述价格波动大小,并考虑相邻 K 线之间的价格跳空。脚本调用 ta 模块的 sma 和
atr 函数,两个指标的 timePeriod 均为 10,不自行实现 ATR 平滑过程。
其中,C 是收盘价,A 是订阅返回的 10 周期 ATR,U 和 L 分别是上下轨;三者都采用合约报价单位。
1.2 交易标的
交易对象为中国商品期货合约 A2305、AG2306。策略支持买开、卖开、卖平、买平,行情频率为 5 分钟,回测日期为 2023 年 2 月 1 日至 28 日。
合约基本信息对两个品种统一设定乘数、保证金和报价单位,均为模拟数据,不能直接当作这两个真实合约的交易所规格。策略不处理主力合约切换、连续合约换月或交割,不设置真实指数基准。
1.3 交易规则
-
每次
onBar到来,读取当前合约行情和指标,计算上下轨。 -
尝试撤销该合约已记录的全部订单,再将订单列表重置为空。
-
无多头、空头持仓时,将当前 K 线高低价赋给局部变量;上下轨均为正时,在上轨买开 1 手,在下轨卖开 1 手。
-
有多头持仓时,计算高价基准的 99.2% 作为卖平委托价,委托数量等于查询到的全部多头持仓。
-
有空头持仓时,计算当前 K 线最高价的 100.8% 作为买平委托价,委托数量等于全部空头持仓。
-
成交回报中重新查询持仓:有多仓时撤销已记录的卖开单,有空仓时撤销已记录的买开单。
订单元组中的订单类型始终为 5,即限价单;方向 1、2、3、4 分别对应买开、卖开、卖平、买平,有效性 0 表示当日有效。
OCO(一单成交后撤销另一单)由 onTrade 内的撤单逻辑尝试实现;两个开仓单分别提交,没有使用原生订单组保证互斥。不能保证同一根 K 线内绝不会出现双向成交。多头和空头分支是两个独立的 if,若同时存在双向持仓,会分别提交两个平仓单。
2. 策略回测
以下按照准备数据、定义策略、设置参数、创建引擎、回放行情和读取结果的顺序展开。回测前需要导入附件 CSV 文件,并加载 Backtest 和 MatchingEngineSimulator 插件。本案例已在 DolphinDB Server 3.00.6.1、Backtest 3.00.6.9 与 MatchingEngineSimulator 3.00.6.9 上运行。服务器需要安装与 DolphinDB Server 版本匹配的插件;创建引擎前,脚本会检查并加载所需插件。
2.1 准备数据
本案例使用附录提供的 CSV 模拟数据。先将配套附件保存到 DolphinDB 服务器可读取的目录,再将路径变量修改为对应 CSV 文件在服务器上的实际路径。
csvPath = "KeltnerChannel.csv"
dataStart = 2023.02.01
dataEnd = 2023.02.28
codes = [`A2305, `AG2306]
barMinutes = 5
csvSchema = table(
`symbol`symbolSource`tradeTime`tradingDay`open`low`high`close`volume`amount`upLimitPrice`downLimitPrice`prevClosePrice`settlementPrice`prevSettlementPrice as name,
["SYMBOL","SYMBOL","TIMESTAMP","DATE","DOUBLE","DOUBLE","DOUBLE","DOUBLE","LONG","DOUBLE","DOUBLE","DOUBLE","DOUBLE","DOUBLE","DOUBLE"] as type)
input = loadText(csvPath, ",", csvSchema, containHeader=true)
模拟数据产生的字段如下:
| CSV 字段 | DolphinDB 类型 | 含义和单位 |
|---|---|---|
| symbol、symbolSource | SYMBOL | 模拟合约及交易所标签 |
| tradeTime | TIMESTAMP | 已完成分钟线的结束时间 |
| tradingDay | DATE | 与自然日期一致;不含夜盘 |
| open、low、high、close | DOUBLE | 模拟 OHLC,合约报价单位,保留 2 位小数 |
| volume | LONG | 正的模拟成交手数 |
| amount | DOUBLE | 模拟均价 × 手数 × 100.0,单位为元,与示例合约乘数一致 |
| upLimitPrice、downLimitPrice | DOUBLE | 上一模拟交易日收盘价的 120%、80% |
| prevClosePrice | DOUBLE | 上一模拟交易日收盘价,首日为 5000.0 |
| settlementPrice | DOUBLE | 当前分钟收盘价的模拟代理,不代表真实交易所结算价 |
| prevSettlementPrice | DOUBLE | 上一模拟交易日收盘价的代理 |
CSV 联合键为 symbol, symbolSource, tradeTime,按时间和合约排序。没有数组列;导入聚合后才构造 signal。无复权、公司行为或连续合约调整,所有模拟结果仅用于研究流程,不代表真实历史表现。
聚合 5 min OHLC 数据:
mintb = select * from input where symbol in codes,
tradingDay >= dataStart, tradingDay <= dataEnd order by tradeTime, symbol
// 将完成时刻09:31至09:35聚合为09:35的5分钟K线,避免标签早于完成时刻。
tb = select first(open) as open, min(low) as low, max(high) as high,
last(close) as close, sum(volume) as volume, sum(amount) as amount,
first(upLimitPrice) as upLimitPrice, first(downLimitPrice) as downLimitPrice,
last(prevClosePrice) as prevClosePrice, last(settlementPrice) as settlementPrice,
last(prevSettlementPrice) as prevSettlementPrice
from mintb group by symbol, symbolSource,
bar(tradeTime - 1, barMinutes*60*1000) + barMinutes*60*1000 as tradeTime, tradingDay
// 调整字段仅用于保持行情结构;指标仍直接使用OHLC,不模拟复权或换月。
update tb set signal = fixedLengthArrayVector([high, low, close])
tb = select * from tb order by tradeTime, symbol
构建基本信息表:
symbol = distinct(tb.symbol)
securityReference = table(symbol as symbol, take(100., size(symbol)) as multiplier, take(0.2, size(symbol)) as marginRatio,
take(0.01, size(symbol)) as tradeUnit, take(0.02, size(symbol)) as priceUnit,
take(0.03, size(symbol)) as priceTick, take(1.5, size(symbol)) as commission, take(1, size(symbol)) as deliveryCommissionMode)
2.2 编写策略逻辑
策略由开仓辅助函数、初始化函数、盘前处理、分钟行情处理、成交行情处理和盘后处理组成。主要回调策略逻辑在分钟行情处理 onBar 和成交行情处理 onTrade 中。
初始化函数
initialize
参数 context 是引擎提供的上下文字典。回测初始化时将手数、通道倍数、退出比例及各状态字典写入上下文,并通过 Backtest::subscribeIndicator 计算核心因子传入引擎。
def initialize(mutable context){
//初始化
print("initialize")
// 订阅指标
d=dict(STRING,ANY)
d["SMA"]=<sma(close, timePeriod = 10)>
d["ATR"]=<atr(high, low, close, timePeriod = 10)>
Backtest::subscribeIndicator(context["engine"], "kline", d)
//每次下单固定 qty
context["qty"] = 1
//指标参数
context["dev"] = 1.6
//止损水平
context["trailingPercent"] = 0.8
// 交易最高价以及最低价
context["lowPrice"] = dict(SYMBOL, DOUBLE)
context["highPrice"] = dict(SYMBOL, DOUBLE)
//记录买单、卖单的 ID
context["longOrderID"] = dict(SYMBOL, ANY)
context["shortOrderID"] = dict(SYMBOL, ANY)
context["allOrderID"] = dict(SYMBOL, ANY)
}
盘前回调
beforeTrading
context:上下文字典。每日盘前打印 tradeDate,不重置订单或价格状态,不生成交易信号。
def beforeTrading(mutable context){
//每日盘前回调函数
//通过context["tradeDate"]可以获取当日;
print ("beforeTrading: " + context["tradeDate"])
}
行情处理
onBar
context:可变 DICTIONARY,包含引擎注入的当前日期、时间和引擎句柄,也包含初始化状态、筛选参数、成本参数及日志表。
msg:DICTIONARY,以股票代码为键,以当日该股票的日线及因子记录为值。函数从中读取开盘价、收盘价、涨跌停价、成交量。
indicator:是按合约索引的指标 DICTIONARY,提供 SMA、ATR 两个之前已经传入的因子。
函数主要流程包括:
-
获取所需要的因子和指标信息,计算凯尔特纳通道上下轨。
-
函数遍历本次消息的合约,先撤旧单,再按实际持仓开仓或退出。
def onBar(mutable context, msg, indicator){
for(istock in msg.keys()){
price = msg[istock][`close]
symbolSource = msg[istock][`symbolSource]
//已订阅的指标
sma = indicator[istock][`SMA]
atr = indicator[istock][`ATR]
lowPrice = iif(context["lowPrice"][istock] == NULL, 0., context["lowPrice"][istock])
highPrice = iif(context["highPrice"][istock] == NULL, 0., context["highPrice"][istock])
//凯尔特纳通道上下轨
kelUpper = sma + atr * context["dev"]
kelLower = sma - atr * context["dev"]
//取消当前的所有订单 , 并清空 context["allOrderID"]
if(context["allOrderID"][istock][0] != NULL){
orders = context["allOrderID"][istock]
Backtest::cancelOrder(context["engine"], , orders,)
}
context["allOrderID"][istock] = array(INT, 0)
// 获取持仓信息
pos = Backtest::getPosition(context["engine"], istock)
longPos = pos.longPosition[0]
shortPos = pos.shortPosition[0]
if(longPos <= 0 and shortPos <= 0){
highPrice = msg[istock][`high]
lowPrice = msg[istock][`low]
if(kelUpper > 0 and kelLower > 0){
sendocoOrder(context, istock, symbolSource, kelUpper, kelLower)
}
}
if(longPos > 0){
highPrice = max(highPrice, msg[istock][`high])
lowPrice = msg[istock][`low]
sellPrice = highPrice *(1 - context["trailingPercent"]/100)
orderID = Backtest::submitOrder(context["engine"], (istock, symbolSource, context["tradeTime"], 5, sellPrice, 0., longPos, 3, 0), "sellclose")
//记录订单号
orders = context["allOrderID"][istock]
orders.append!(orderID)
context["allOrderID"][istock] = orders
}
if(shortPos > 0){
highPrice = msg[istock][`high]
lowPrice = min(lowPrice, msg[istock][`low])
buyPrice = highPrice *(1 + context["trailingPercent"]/100)
orderID = Backtest::submitOrder(context["engine"], (istock, symbolSource, context["tradeTime"], 5, buyPrice, 0., shortPos, 4, 0), "buyclose")
//记录订单号
orders = context["allOrderID"][istock]
orders.append!(orderID)
context["allOrderID"][istock] = orders
}
}
}
成交回报回调
onTrade
context:上下文字典。
trades:成交回报对象,用其 symbol 字段取得合约,按单个合约查询持仓。
函数主要作用为:成交后,根据当前持仓撤销另一方向开仓单,再写回 allOrderID。
def onTrade(mutable context,trades){
//成交回报回调函数
istock = trades[`symbol]
longPos = Backtest::getPosition(context["engine"], istock).longPosition[0]
shortPos = Backtest::getPosition(context["engine"], istock).shortPosition[0]
orders = context["allOrderID"][istock]
//取消与当前持仓方向相反的所有挂单,更新所有订单列表 context["allOrderID"]
if(longPos > 0){
shortId = context["shortOrderID"][istock]
Backtest::cancelOrder(context["engine"], , shortId,)
for(id in context["shortOrderID"][istock]){
if(id in orders){orders[orders!=id] }
}
}
if(shortPos > 0){
longId = context["longOrderID"][istock]
Backtest::cancelOrder(context["engine"], , longId,)
for(id in context["longOrderID"][istock]){
if(id in orders){orders[orders!=id]}
}
}
context["allOrderID"][istock] = orders
}
2.3 设置回测参数
以下配置包括资产类型、数据读取范围、初始资金和其他回测引擎参数。
config=dict(STRING,ANY)
config["startDate"]= 2023.02.01
config["endDate"]= 2023.02.28
config["strategyGroup"]= "futures"
config["cash"]= 10000000
config["commission"]= 1.5
config["tax"]= 0.0
config["dataType"]=3
-
startDate:DATE 标量,2023.02.01,回测开始日期。
-
endDate:DATE 标量,2023.02.28,回测结束日期。
-
strategyGroup:STRING 标量,"futures",指定期货类型。
-
cash: DOUBLE 标量,表示初始资金 10,000,000 元。
-
commission:DOUBLE 标量,1.5,顶层手续费配置。
-
tax:DOUBLE 标量,0.0,税费配置为零。
-
dataType:INT 标量,3,使用分钟 K 线行情。
2.4 创建回测引擎
strategyName 为 STRING 标量,callbacks 为 dict(STRING,ANY) 格式的字典,依次注册初始化、盘前、行情、委托、成交、盘后、结束七个回调。创建前脚本尝试删除同名引擎,执行时应确认该名称属于本次回测,避免影响其他任务。第五个参数为合约基本信息表。
strategyName = "KeltnerChannelBacktest"
// == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == == =
callbacks = dict(STRING, ANY)
callbacks["initialize"] = initialize
callbacks["beforeTrading"] = beforeTrading
callbacks["onBar"] = onBar
callbacks["onOrder"] = onOrder
callbacks["onTrade"] = onTrade
callbacks["afterTrading"] = afterTrading
callbacks["finalize"] = finalized
try{Backtest::dropBacktestEngine(strategyName)}catch(ex){print ex}
engine = Backtest::createBacktester(strategyName, config, callbacks, , securityReference)
2.5 执行回测
将排序后的模拟行情数据送入引擎;回调按交易日推进,依次执行信号计算和后续交易。
行情发送完毕后,使用 Backtest::appendEndMarker(engine) 发送结束标志。
timer Backtest::appendQuotationMsg(engine, tb)
// 回测结束标志
Backtest::appendEndMarker(engine)
2.6 获取回测结果
行情提交结束后,通过以下接口查询回测交易明细、收益统计、每日组合权益和当前持仓,并将结果保存到会话变量中。
tradeDetails = select * from Backtest::getTradeDetails(engine)
returnSummary = Backtest::getReturnSummary(engine)
TotalPortfolios = Backtest::getDailyTotalPortfolios(engine)
//print(TotalPortfolios)
position = Backtest::getPosition(engine)
各结果对象的主要用途如下:
-
position:查看各日持仓、买卖数量和收盘估值。
-
returnSummary:查看引擎计算的收益、风险和交易统计。
-
tradeDetails:查看订单状态与成交;
tradeQty > 0的记录才表示发生了成交。 -
totalPortfolios:查看每日账户权益和 strategyNetValue。
脚本没有设置真实指数基准,收益概述中与基准有关的默认字段不能解释为沪深300表现或有效超额收益。
3. 回测结果分析
在上述软件版本和非 JIT 模式下,使用附录提供的模拟数据进行回测。本次回测处理 1920 条行情记录,初始权益为 1,000,000.00 元,区间总收益为 -0.9352%。
由于输入数据为模拟数据,且回测结果受行情生成方式、成交价格假设和成本配置影响,这些指标仅用于说明策略在本案例数据上的运行结果,不能据此评价策略在真实市场中的表现。
| 指标 | 本次结果 | 解读 |
|---|---|---|
| 区间总收益 | -0.9352% | 期末权益相对初始资金的累计变化 |
| 年化收益 | -11.1651% | 引擎按回测日收益年化后的结果 |
| 年化波动率 | 0.0413% | 价格路径波动幅度 |
| 夏普比率 | -270.5278 | 去掉风险波动后的收益 |
| 最大回撤 | 0.9352% | 期内净值相对先前高点的最大下跌 |
| 回撤比率 | -11.9386 | 引擎返回的收益与回撤相关统计 |
| 日胜率 | 0% | 每日收益为正的交易日占比 |
4. 总结
本策略以 SMA 加减 1.6 倍 ATR 建立通道,每次行情到来撤旧单、按持仓提交开仓或平仓单,并在成交回报中尝试撤销相反方向的开仓单。脚本展示了指标订阅、期货订单管理、5 分钟行情聚合与结果查询的组合流程。
其当前实现采用限价单,且价格极值状态。应用于真实历史研究前,应先明确目标委托语义,核对时间顺序和合约规格,再复现并分析实际成交与账户结果。模拟数据的简化时间网格及统一合约规格只用于流程研究,真实期货研究需另行准备可靠数据。
5. 附录
完整源脚本包含 CSV 导入、行情转换、回调、参数配置、引擎创建及结果查询。
