优宽量化 为了降低量化策略开发难度,能使 优宽量化 的原生编程语言开发策略的速度、难易程度达到使用封装语言的量化交易平台、量化交易软件的水平。升级了 优宽量化 的 “商品期货交易类库模板” , 新增了 $.CTA = function(contractType, onTick, interval){…} 这个导出函数, 用以 快速 构建 CTP多品种商品期货 策略。
我们来学习一下 如何使用 $.CTA 这个 函数,快速构建一个 CTP商品期货 多品种 均线策略。
# 由于 商品期货交易类库 升级,所以本帖子的范例代码只能用旧版本 : https://www.youquant.com/strategy/127341
# 复制旧版本,让范例策略引用这个旧版本的 商品期货交易类库 即可。
简单多品种商品期货均线策略 :https://www.youquant.com/strategy/57029
// 引用 商品期货交易类库 模板 的策略代码 (策略端) --->看注释向右拖动滑动块
/* 界面参数
变量 描述 类型 默认值
Symbols 操作品种 字符串(string) rb1801,MA801
FastPeriod 快线周期 数字型(number) 5
SlowPeriod 慢线周期 数字型(number) 20
ConfirmPeriod 确认周期 数字型(number) 2
Lots 开仓手数 数字型(number) 1
*/
function main() { // 策略端 入口函数 ,策略从这个 main 函数 开始执行。
$.CTA(Symbols, function(r, mp, symbol) { // 整个策略端 很简单 , 就是 调用了 一次 商品期货交易类库 的导出函数 $.CTA , 传入了 2个参数
// 第一个参数 是 策略端的 界面参数 Symbols ,这个参数是一个 合约代码字符串, 合约之间用 逗号间隔,
// 第二个参数 是 传入 用于回调的 匿名函数,对于 交易动作的触发逻辑代码 在此 匿名函数中 实现。
/*
r为K线, mp为当前品种持仓数量, 正数指多仓, 负数指空仓, 0则不持仓, symbol指品种名称
返回值如为n:
n = 0 : 指全部平仓
n > 0 : 如果当前持多仓,则加n个多仓, 如果当前为空仓则平n个空仓
n < 0 : 如果当前持空仓,则加n个空仓, 如果当前为多仓则平n个多仓
*/ // 一下代码 在 $.CTA 函数的 主要循环 中 回调执行
if (r.length < SlowPeriod) { // 如果回调函数执行时 传入的 r (即 K线数据) 的长度 (即 Bar数量)小于 SlowPeriod(界面参数上的 慢线周期)
return // 回调函数 直接返回 不做任何操作(因为 K线 柱数量 不足 慢线 计算的最小个数,无法计算指标,等待K线数量符合要求)
}
var cross = _Cross(TA.EMA(r, FastPeriod), TA.EMA(r, SlowPeriod)); // 调用API 指标计算函数 TA.EMA 即 指数平均数指标 , 给 TA.EMA 传入 r :K线数据, FastPeriod、SlowPeriod 计算出 均线(数据结构为数组,最后一个元素为最新的指标值)
// _Cross 函数 接受 TA.EMA 指标函数返回的指标数组,计算交叉周期,返回给声明的 cross 变量, _Cross 函数详见API 文档 全局函数栏
if (mp <= 0 && cross > ConfirmPeriod) { // 根据 当前回调函数 执行时 传入的 持仓 mp 和 当前行情 计算出来的 指标交叉情况 cross , 判断是否执行 if 块内代码
Log(symbol, "金叉周期", cross, "当前持仓", mp); // 触发金叉操作, 输出日志 显示 合约名称、金叉周期、当前持仓
return Lots * (mp < 0 ? 2 : 1) // 返回 开多仓 数量 Lots (界面参数 开仓手数) 根据 mp 是否小于0 确定 是否反手(Lots * 2 即反手)
} else if (mp >= 0 && cross < -ConfirmPeriod) { // 同上 判断 mp 持仓方向, cross 交叉情况是否 满足 ConfirmPeriod (确认周期)
Log(symbol, "死叉周期", cross, "当前持仓", mp); // 同上
return -Lots * (mp > 0 ? 2 : 1)
}
});
}
第一眼看到这个策略, 感觉就是 简洁,除去注释部分:
function main() {
// 使用了商品期货类库的CTA策略框架
$.CTA(Symbols, function(r, mp, symbol) {
if (r.length < SlowPeriod) {
return
}
var cross = _Cross(TA.EMA(r, FastPeriod), TA.EMA(r, SlowPeriod));
if (mp <= 0 && cross > ConfirmPeriod) {
Log(symbol, "金叉周期", cross, "当前持仓", mp);
return Lots * (mp < 0 ? 2 : 1)
} else if (mp >= 0 && cross < -ConfirmPeriod) {
Log(symbol, "死叉周期", cross, "当前持仓", mp);
return -Lots * (mp > 0 ? 2 : 1)
}
});
}
即: $.CTA( Symbols, function(r, mp, symbol) {…} )
最新版的 商品期货交易类库 : https://www.youquant.com/strategy/12961
策略执行流程:
当然 $.CTA 内实现都不用 策略编写者 去 思考 、实现了! 这里做个了解,在最后 我会贴出 《商品期货交易类库》 新增 $.CTA 函数 ,这部分的代码注释,有兴趣 研究 的可以看下。
$.CTA 函数 的第一个参数 需要是一个 字符串 例如 : “rb1801,MA801, jm1801” 这样的格式。或者设置为策略的界面参数如图:
字符串中 逗号间隔的这些 合约 都会成为策略执行的标的物。
function(r, mp, symbol) {…}
这可以说是整个策略的 核心了, 因为 策略的 交易逻辑都在与此。对于程序来说 代码已经精简到极限,整个需要编写的 仅仅就只有 开平仓逻辑了。
虽然简单 但是逻辑也需要严谨,就以 本例中的 回调函数来说明:
回调函数 的参数列表 需要接收 三个 参数, 第一: r ,回调函数 会接收到 最新的 K线数据 r ,数据结构为 数组。第二:mp ,当前调用该回调函数时 设置的合约的持仓数量。(即 目前 行情K线 r 这个品种的 持仓信息)。第三:symbol ,即 当前的合约代码。
对于根据 行情数据 计算指标,再以指标作为 操作依据的 策略来说, 要计算出有效的指标,必须 有足够的K线数据,所以 开始做了 一个判断:
if (r.length < SlowPeriod) {
return
}
就是判断 获取的 K线数据 数量 必须 足够计算出有效指标,举个例子 在求 MA10 平均线的时候,我们会计算 样本 10个数值 的平均值 ,如果样本的数量只有9个 ,是无法计算出 MA10 的。
然后调用了 _Cross 函数 计算 TA.EMA 指标函数 算出的 指标线 的相交状态。
关于 _Cross 函数 详见 : https://www.youquant.com/bbs-topic/1140
然后关于 开多仓 、开空仓、平仓 等逻辑 需要根据 当前 持仓 和 指标交叉 状态 发出的信号做不同的操作。
function(r, mp, symbol) {
if (r.length < SlowPeriod) {
return
}
var cross = _Cross(TA.EMA(r, FastPeriod), TA.EMA(r, SlowPeriod));
if (mp <= 0 && cross > ConfirmPeriod) {
Log(symbol, "金叉周期", cross, "当前持仓", mp);
return Lots * (mp < 0 ? 2 : 1)
} else if (mp >= 0 && cross < -ConfirmPeriod) {
Log(symbol, "死叉周期", cross, "当前持仓", mp);
return -Lots * (mp > 0 ? 2 : 1)
}
}
if (mp <= 0 && cross > ConfirmPeriod) {
Log(symbol, "金叉周期", cross, "当前持仓", mp);
return Lots * (mp < 0 ? 2 : 1)
} else if (mp >= 0 && cross < -ConfirmPeriod) {
Log(symbol, "死叉周期", cross, "当前持仓", mp);
return -Lots * (mp > 0 ? 2 : 1)
}
当回调函数没有 return 时 , 商品期货交易类库 模板中 的 $.CTA 不会做 任何 操作。
当回调函数 return 正数 时, 商品期货交易类库 模板中 的 $.CTA 会执行 开多仓任务;如果当前 持仓 为 持有空头仓位,则 会执行 平空仓;如果 return 的数值 大于 目前持空头仓位的数量,在平空头仓位之后 会反手 开多仓 ,开仓数量为: return 的数值的绝对值 减去 平空仓的数量。
当回调函数 return 负数 时, 商品期货交易类库 模板中 的 $.CTA 会执行 开空仓任务;如果当前 持仓 为 持有多头仓位,则 会执行 平多仓; 如果 return 的数值 大于 目前持多头仓位的数量,在平多头仓位之后 会反手 开空仓 ,开仓数量为: return 的数值的绝对值 减去 平多仓的数量。
第三个参数 用来设置 $.CTA 函数的 轮询间隔,如果不传该参数, $.CTA 函数 会使用 默认的 轮询间隔 500 毫秒,详见 《商品期货交易类库》模板代码。
《商品期货交易类库》 新增 $.CTA 函数 部分代码注释:
// 商品期货交易类库 扩展CTA函数 视频讲解 代码段
/*
onTick(r, mp, symbol):
r为K线, mp为当前品种持仓数量, 正数指多仓, 负数指空仓, 0则不持仓, symbol指品种名称
返回值如为n:
n = 0 : 指全部平仓
n > 0 : 如果当前持多仓,则加n个多仓, 如果当前为空仓则平n个空仓
n < 0 : 如果当前持空仓,则加n个空仓, 如果当前为多仓则平n个多仓
无返回值表示什么也不做
*/
/* 界面参数
变量 描述 类型 默认值
...
CTAShowPosition 在状态栏显示持仓信息 布尔型(true/false) true
PositionRefresh@CTAShowPosition 持仓更新周期(秒) 数字型(number) 20
*/
$.CTA = function(contractType, onTick, interval) { // 函数参数 contractType 合约代码字符串, onTick 匿名回调函数, interval 轮询间隔
SetErrorFilter("login") // 调用 API 过滤 not login 报错
if (typeof(interval) !== 'number') { // 如果 参数 interval 不等于 number 数值类型(比如没有传入 interval), 则 执行 if 块内代码
interval = 500 // 给 interval 设置 默认 数值 500
}
exchange.IO("mode", 0) // 切换行情模式 为 立即返回模式,详见 API文档 IO 函数(交易函数栏)
var lastUpdate = 0 // 记录仓位信息上次更新的时间
var e = exchange // 声明一个 变量 e ,引用 exchange 交易所对象
var symbols = contractType.split(','); // 调用 split 函数 以 逗号 "," 分割 contractType , split 返回一个 分割后的字符串数组 给 symbols
var holds = {} // 声明一个 空对象 用来记录持仓信息
var tblAccount = {}; // 声明一个 空对象 用来记录 显示在 状态栏表格 的信息
var refreshHold = function() { // 声明一个 变量 refreshHold 用 一个 匿名函数 给其赋值, 此处是 匿名 函数实现,并非调用。
while (!e.IO("status")) { // 循环判断 exchange.IO("status") 这个 API (e 引用的即是 exchange),直到IO("status") 返回true
Sleep(5000) // 返回true 代表 已经和交易所服务器 连接, !true 为 false 就会跳出 while 循环
}
_.each(symbols, function(ins) { // 迭代 获取 symbols 这个数组中的 每个元素 , 当做参数 传递给 匿名函数 的参数 ins
holds[ins] = 0 // 给 holds 对象 增加 名称 为 ins 这个字符串值 的属性,即 ins 为 "MA801" 的话 holds : {"MA801" : 0, ....}
}); // 所有 在 symbols 里面 的合约代码都会 添加为 holds 对象的属性
var total = 0 // 初始 总持仓量
var positions = _C(e.GetPosition); // 调用 API 函数 GetPosition 获取 当前 所有 持仓信息, 使用了 _C 函数 进行容错处理,详见 API 文档
_.each(positions, function(pos) { // 迭代 positions 数组中的 每个元素 传递给 匿名函数 参数 pos
var hold = holds[pos.ContractType]; // 根据 持仓信息中的 合约名称 取出 holds 对象对应属性 的值 ,赋值给 hold。
if (typeof(hold) == 'undefined') { // 如果 pos 的合约 名称 在 holds 对象 属性中 没有找到 即 hold 被赋值 undefined ,则 执行 if 块
return // 返回本次 匿名函数调用
}
if (pos.Type == PD_LONG || pos.Type == PD_LONG_YD) { // 根据 持仓的类型 进行 分别处理 , 如果 持仓类型为 今日多仓 或者 为 昨日多仓
if (hold < 0) { // 如果 取出的 持仓数量 小于 0 , 即持有的是 空头持仓,则抛出错误
throw "不能同时持有多仓空仓" // 不能同时持有 空头 多头持仓
}
hold += pos.Amount // 持仓数量 累计 pos的 Amount 属性值
} else { // 处理 空头持仓
if (hold > 0) { // 同样 , 不能 有多头 和 空头持仓, 如果有则 抛出错误
throw "不能同时持有多仓空仓"
}
hold -= pos.Amount // 因为 空头持仓是 用负值来代表, 所以 累计 为 反向即 减去pos.Amount
}
total += pos.Amount // 累计 总持仓
holds[pos.ContractType] = hold // 把 累计 后的持仓数量 赋值给 holds 对象中 对应的 属性
})
if (CTAShowPosition) { // 如果界面参数 上的 CTAShowPosition (在状态栏显示持仓信息) 设置为 true, 构造用于显示信息的 对象
var tblPosition = { // 声明一个对象, 有四个属性, type 、 title 、 cols 、 rows , 详见 API 文档 LogStatus 函数
type: 'table', // 如果 要显示在 状态栏表格 需要设置 type 属性为 字符串 "table"
title: '持仓状态', // title 是 状态栏表格的 标题 , 这里设置为 “持仓状态”
cols: ['品种', '方向', '均价', '数量', '浮动盈亏'], // cols 是 表格的 第一行的 表头,列名称, 值为一个字符串数组
rows: [] // rows 是 表格的 表头下边的 所有行, 值为 一个 二维字符串数组,第一个元素 就是第一行信息(结构为字符串数组)
};
_.each(positions, function(pos) { // 把 positions 即 调用API获取的持仓信息 中持仓压入 tblPosition.rows 中,迭代执行
tblPosition.rows.push([pos.ContractType, ((pos.Type == PD_LONG || pos.Type == PD_LONG_YD) ? '多#0000ff' : '空#ff0000'), pos.Price, pos.Amount, pos.Profit])
// [合约代码, 根据 pos 中的 Type 值即 持仓方向 设置 “多” 或者 “空” #ff0000 为 把字符显示为 红色, 持仓价格, 持仓数量, 持仓盈亏 ]
});
lastUpdate = new Date().getTime() // 记录 状态栏表格 对象 更新时间(刷新为当前时间戳 ,如果不明白 时间戳 详见百度)
var account = exchange.GetAccount() // 声明一个 变量 account , 调用 API exchange 对象的 GetAccount 函数(详见 API 文档的 交易函数 栏)
if (account) { // 如果 获取到了 account 。(有可能 会调用 GetAccount 失败 获取到 null 值)
tblAccount = $.AccountToTable(exchange.GetRawJSON(), "资金信息") // 使用 商品期货交易类库 的 导出函数 $.AccountToTable 把 详细账户信息 格式化为 和 tblPosition 一样的对象,用于在 状态栏上显示
}
LogStatus('`' + JSON.stringify([tblPosition, tblAccount]) + '`\n', '更新于: ' + _D()) // 调用 API LogStatus 在状态栏上显示 tblPosition, tblAccount 表格对象内容, '\n' 是换行, _D() 是 API 函数 用于显示当前日期时间字符串(可读时间,并非时间戳)
}
return total // refreshHold 函数最后 返回 总持仓数 total
}
refreshHold() // 在 CTA 函数执行的 最初 首先调用 refreshHold 函数 , 刷新 持仓信息
var q = $.NewTaskQueue(function(task, ret) { // 调用 商品期货交易类库 的 导出函数 $.NewTaskQueue 生成一个 可以执行队列下单任务 的 控制对象 引用赋值给 声明的 对象 q
Log("任务结束", task.desc) // $.NewTaskQueue 函数调用时 参数 传入一个 匿名函数 作为 回调函数, 在回调函数中执行 refreshHold 函数 ,实现 在完成任务回调时 刷新持仓
refreshHold()
})
while (true) { // CTA 函数的 主要 循环 , 策略 程序运行时 ,主要在这个循环中 轮询。
var ts = new Date().getTime() // 记录当前时间戳
_.each(symbols, function(ins) { // 迭代 symbols 数组, 每个 元素 都传递给 匿名函数的 参数 ins
if (!e.IO("status") || !$.IsTrading(ins) || q.hasTask(ins)) { // 匿名函数 实现代码, 首先调用 API IO("status") 判断 与交易所服务器连接状态, 调用 $.IsTrading() 函数 判断 代码为 ins 值的 这个合约是否在交易时间, hasTask 用来判断 当前有没有 合约为 ins 的交易任务在执行中,代码详见模板。
return // 所以此处判断, 交易所未连接 或者 不在交易时间 或者 q对象队列中 有 名为 ins 值的 合约交易任务在执行 ,都会触发 return 执行, _.each 中的 return 会 跳过本次迭代,继续执行。
}
var c = e.SetContractType(ins); // 在满足 确定连接交易所, 在交易时间, 当前合约 没有任务在执行的情况下,调用 API SetContractType 设置 操作的合约为 ins 的值
if (!c) { // 如果 设置 合约 失败 , 即 c 为 false , !c 为 true , 执行 return 跳过本次
return // 每次迭代 所有API 调用 均采用非阻塞 模式,如果 调用错误 就立即 return ,实现 模拟并发
}
var r = e.GetRecords() // 调用 API GetRecords 获取 当前 合约的 K线数据 , 赋值给 r
if (!r || r.length == 0) { // 如果 获取的 K线数据 r 为 null , 或者 r 的长度 等于0(空数组) , 则 return 跳过本次
return
}
var hold = holds[ins]; // 获取 当前品种的 持仓数量, 赋值给 声明的 hold
var n = onTick(r, hold, ins) // 执行 CTA 函数 参数 onTick 接受 的 回调函数 ,给 回调函数 onTick 传入参数 r :最新的K线数据, hold : 当前合约的持仓数量, ins : 当前合约代码
if (typeof(n) !== 'number') { // 回调函数 具体 的判断逻辑 会根据 行情 判断要执行的 动作 (引用该模板的策略中 由用户设定,简称策略端),如果 onTick 返回的 n 不是数值类型
return // 即 没有 触发 任何要 执行的任务,return
}
var ret = null // 声明一个 变量 ret 用来记录 结果
if (n > 0) { // 如果 策略端 即 onTick 函数 内的逻辑判断后 , onTick 返回 的 n 是大于0 的数值 ,代表 要开多仓。
if (hold < 0) { // 如果此时 当前合约 是持有 空头仓位的
q.pushTask(e, ins, 'closesell', Math.min(-hold, n)) // 给 对象 q 的执行队列中 放入任务 e:交易所对象,ins:合约代码, 'closesell':代表操作方向为平空头持仓, Math.min(-hold, n):平仓数量
// Math.min(-hold, n) 为计算 hold,n 中绝对值最小的 那个 作为 平仓数量。
n += hold // 重新计算 n,计算反手 数量
}
if (n > 0) { // n 大于 0 ,即 开多仓 操作
q.pushTask(e, ins, 'buy', n) // 给 对象 q 的执行队列中 放入开多仓 任务
}
} else if (n < 0) { // 如果 onTick 返回的 n 是小于0 的数值
if (hold > 0) { // 如果此时 当前合约 是持有 多头仓位
q.pushTask(e, ins, 'closebuy', Math.min(hold, -n)) // 放入 平多仓 任务, 平仓数量 选择 hold 和 n 两个变量 绝对值 中最小的。限制 平仓数量 不会大于 持仓数量
n += hold // 重新计算 n,计算反手 数量
}
if (n < 0) { // n 小于 0 ,即 开空仓 操作
q.pushTask(e, ins, 'sell', -n) // 放入 开空仓 任务
}
} else if (n == 0 && hold != 0) { // 当 onTick 返回 0 (n == 0) , 并且 hold 不等于 0 (有持仓), 此时:
q.pushTask(e, ins, (hold > 0 ? 'closebuy' : 'closesell'), Math.abs(hold)) // 放入 全部平仓任务,平掉所有仓位
}
}) // 迭代函数 _.each 全部完成后 执行以下, _.each 函数 详见 http://underscorejs.org/
q.poll() // 调用 q 对象的成员函数 poll 执行所有 队列内的任务, 代码实现详见 商品期货交易类库 模板
var now = new Date().getTime() // 记录当前的时间戳
if (CTAShowPosition && (now - lastUpdate) > (PositionRefresh*1000)) { // 如果 商品期货交易类库 模板 上的界面参数设置 CTAShowPosition 为 true 并且 当前时间戳 减去 上次更新时记录的时间戳的差值 大于 界面参数 持仓更新周期(秒) 乘以 1000
refreshHold() // 则 执行 刷新持仓 refreshHold 函数 ,更新 持仓 并刷新状态栏显示。
}
var delay = interval - (now - ts) // 计算 now 和 ts 的差值
if (delay > 0) { // 如果 以上 流程 消耗的时间 (即 now - ts) 小于 interval ,则执行 Sleep , 如果大于 interval 则本次不 Sleep
Sleep(delay) // 根据 CTA 函数 传入的 参数 interval - (now - ts) 设置 等待时间,可以控制 最少等待 interval 值的毫秒数, 控制 轮询间隔。
}
}
}
以上是 新增部分注释说明,其中对象 q 的代码参看 https://www.youquant.com/strategy/12961 何不来试试手,写个 多品种 MACD 策略怎么样? by. 优宽量化 小小梦