资源加载中... loading...

用优宽快速构建 CTP多品种 商品期货 策略

Author: 扫地僧, Created: 2017-10-11 19:33:53, Updated: 2019-07-31 18:14:19

用优宽快速构建 CTP多品种 商品期货 策略

优宽量化 为了降低量化策略开发难度,能使 优宽量化 的原生编程语言开发策略的速度、难易程度达到使用封装语言的量化交易平台、量化交易软件的水平。升级了 优宽量化 的 “商品期货交易类库模板” , 新增了 $.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

    策略执行流程:

    img

    当然 $.CTA 内实现都不用 策略编写者 去 思考 、实现了! 这里做个了解,在最后 我会贴出 《商品期货交易类库》 新增 $.CTA 函数 ,这部分的代码注释,有兴趣 研究 的可以看下。

  • 这里 使用 $.CTA 函数 有些规则。

      1. $.CTA(Symbols, function(r, mp, symbol) {…}) 中的 Symbols 参数

      $.CTA 函数 的第一个参数 需要是一个 字符串 例如 : “rb1801,MA801, jm1801” 这样的格式。或者设置为策略的界面参数如图:

      img

      字符串中 逗号间隔的这些 合约 都会成为策略执行的标的物。

      1. $.CTA(Symbols, function(r, mp, symbol) {…}) 中的 匿名回调函数

      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)
        }
    }
  • 比较重要的 一个还有 就是 回调函数 function(r, mp, symbol) {…} 的 返回值。

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 返回了不同的 值。

    • 当回调函数没有 return 时 , 商品期货交易类库 模板中 的 $.CTA 不会做 任何 操作。

    • 当回调函数 return 正数 时, 商品期货交易类库 模板中 的 $.CTA 会执行 开多仓任务;如果当前 持仓 为 持有空头仓位,则 会执行 平空仓;如果 return 的数值 大于 目前持空头仓位的数量,在平空头仓位之后 会反手 开多仓 ,开仓数量为: return 的数值的绝对值 减去 平空仓的数量。

    • 当回调函数 return 负数 时, 商品期货交易类库 模板中 的 $.CTA 会执行 开空仓任务;如果当前 持仓 为 持有多头仓位,则 会执行 平多仓; 如果 return 的数值 大于 目前持多头仓位的数量,在平多头仓位之后 会反手 开空仓 ,开仓数量为: return 的数值的绝对值 减去 平多仓的数量。

  • 3. $.CTA(Symbols, function(r, mp, symbol) {…}) 可以传 第三个参数。

    第三个参数 用来设置 $.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. 优宽量化 小小梦


相关内容

更多内容