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

零基础入门商品期货程序化交易(4)

Author: 扫地僧, Created: 2021-08-13 09:24:15, Updated: 2023-11-23 21:00:34

img

零基础入门商品期货程序化交易(4)

前几篇文章中我们学习了最简单的策略架构,接下来我们在商品期货策略基础架构上增加一些代码尝试一下其它操作。

订阅合约、设置当前操作的合约

上篇文章中我们已经接触过exchange.SetContractType()这个函数,该函数作用为切换当前操作的合约,并且订阅该合约的行情数据。除了使用上篇文章中提及的虚拟合约代码(虚拟合约代码是优宽平台定义的),还可以传入具体的合约代码设置当前操作的合约。 例如,目前螺纹钢的主力合约为rb2201。就可以调用exchange.SetContractType("rb2201")函数设置当前操作的合约为rb2201exchange.SetContractType()函数会返回当前合约的详细信息。 这里我们依然使用上篇学习的基础策略架构,修改exchange.SetContractType("rb2201"),订阅rb2201这个合约。把该函数的返回值赋值给刚声明的info变量,然后打印出来。

function main(){
    while(true){
        if(exchange.IO("status")){
            var info = exchange.SetContractType("rb2201")
            Log("info:", info)
            LogStatus(_D(), "已经连接CTP !")
        } else {
            LogStatus(_D(), "未连接CTP !")
        }
        Sleep(5000)   // 避免打印过快,5秒打印一次
    }
}

输出内容为:

info: {"MaxLimitOrderVolume":500,"VolumeMultiple":10,"OpenDate":"20210118","PositionType":50,"UnderlyingMultiple":1,"InstrumentName":"螺纹钢2201","ProductID":"rb","ProductClass":49,"EndDelivDate":"20220120","InstLifePhase":49,"IsTrading":1,"DeliveryMonth":1,"MaxMarketOrderVolume":30,"PriceTick":1,"StrikePrice":0,"MinLimitOrderVolume":1,"CreateDate":"20201216","UnderlyingInstrID":"rb","ExchangeID":"SHFE","ShortMarginRatio":0.08,"DeliveryYear":2022,"MinMarketOrderVolume":1,"PositionDateType":49,"MaxMarginSideAlgorithm":49,"CombinationType":48,"OptionsType":48,"ExchangeInstID":"rb2201","ExpireDate":"20220117","StartDelivDate":"20220118","InstrumentID":"rb2201","LongMarginRatio":0.08}

需要注意优宽平台的所有日志输出,如果长度过长日志内容会被截断。好在这个info变量内容并不太长,可以完整输出。使用 https://www.bejson.com/这个网站的工具,可以格式化这一长串字符串。格式化成如下结构就方便观察了。

这个返回的数据结构,里面全部都是合约相关的一些信息,例如合约交割日期,合约一手对应的货物数量,合约的价格一跳,合约的名称等等。

info: {
	"MaxLimitOrderVolume": 500,
	"VolumeMultiple": 10,
	"OpenDate": "20210118",
	"PositionType": 50,
	"UnderlyingMultiple": 1,
	"InstrumentName": "螺纹钢2201",
	"ProductID": "rb",
	"ProductClass": 49,
	"EndDelivDate": "20220120",
	"InstLifePhase": 49,
	"IsTrading": 1,
	"DeliveryMonth": 1,
	"MaxMarketOrderVolume": 30,
	"PriceTick": 1,
	"StrikePrice": 0,
	"MinLimitOrderVolume": 1,
	"CreateDate": "20201216",
	"UnderlyingInstrID": "rb",
	"ExchangeID": "SHFE",
	"ShortMarginRatio": 0.08,
	"DeliveryYear": 2022,
	"MinMarketOrderVolume": 1,
	"PositionDateType": 49,
	"MaxMarginSideAlgorithm": 49,
	"CombinationType": 48,
	"OptionsType": 48,
	"ExchangeInstID": "rb2201",
	"ExpireDate": "20220117",
	"StartDelivDate": "20220118",
	"InstrumentID": "rb2201",
	"LongMarginRatio": 0.08
}

那么这个返回的信息通常有什么用呢?通常有以下使用场景。

  • 通过SetContractType函数调用时返回的数据判断订阅合约是否成功 当该接口返回了null时说明订阅合约失败,可以用if语句判断。 为了篇幅尽量简短,截取一段代码示范,例如:

    var info = exchange.SetContractType("rb888")
    if (info) {
        Log("订阅成功")
    } else {
        Log("订阅失败")
    }
    

    我们也可以使用_C()函数强制在这里订阅合约成功,例如:

    _C(exchange.SetContractType, "rb888")
    

    这里如果exchange.SetContractType函数订阅失败,会自动重试直到订阅成功。虽然_C()使用起来很方便,但是切记不要滥用。另外要注意的是_C()函数传入的第一个参数是exchange.SetContractType而不是exchange.SetContractType()

  • 通过返回的数据中的InstrumentID属性获取当前虚拟合约的映射关系 当策略订阅的是一个优宽平台定义的虚拟合约时(例如rb888,螺纹钢主力合约),获取的行情数据是当前的主力合约的数据。但是下单时是无法下单的(优宽回测系统可以在虚拟合约状态下单),所以就需要知道当前设置的rb888主力合约代指的是具体哪个螺纹钢合约。 这个合约代码如何找呢? 用exchange.SetContractType()函数返回的数据中的InstrumentID就可以拿到。

    function main(){
        while(true){
            if(exchange.IO("status")){
                var info = exchange.SetContractType("rb888")
                Log("rb888对应的具体合约为:", info.InstrumentID)   // 我们要拿到某个数据结构里的某个字段数据用"."符号
                LogStatus(_D(), "已经连接CTP !")
            } else {
                LogStatus(_D(), "未连接CTP !")
            }
            Sleep(5000)   // 避免打印过快,5秒打印一次
        }
    }
    

    img 拿到了rb888对应的具体合约代码,切换到这个具体合约上就可以下单操作了,这样策略获取的行情、下单操作一直都是主力合约。

  • VolumeMultiplePriceTick等数据 在设计策略时很多时候需要计算合约价格一跳对应的一手盈亏,并且下单时的价格也需要符合合约的价格一跳。诸如此类的计算就需要用到用exchange.SetContractType()函数返回的合约详细信息了。

    以上的螺纹钢数据为例:

    info: {
        ..
        "VolumeMultiple": 10,   // 一手为10吨
        ..
        "PriceTick": 1,         // 价格变动为1元(价格一跳)
        ..
    }
    

获取行情数据

当成功订阅合约之后,就可以使用获取行情数据的接口获取相关的行情数据了。行情数据接口在优宽平台上主要有2个。

  • exchange.GetTicker() 获取实时行情数据

    function main(){
        while(true){
            if(exchange.IO("status")){
                exchange.SetContractType("rb2201")
                var ticker = exchange.GetTicker()
                Log(ticker)
                LogStatus(_D(), "已经连接CTP !")
            } else {
                LogStatus(_D(), "未连接CTP !")
            }
            Sleep(5000)   // 避免打印过快,5秒打印一次
        }
    }
    

    返回的数据格式化以后:

    {
        "Info": {
            "BidVolume3": 0,
            "AskVolume3": 0,
            "AskVolume4": 0,
            "AskPrice5": 1.7976931348623157e+308,
            "TradingDay": "20210818",
            "PreSettlementPrice": 5342,
            "BidPrice1": 5154,
            "AskVolume1": 51,
            "AskPrice3": 1.7976931348623157e+308,
            "BidVolume4": 0,
            "BidPrice5": 1.7976931348623157e+308,
            "BidVolume5": 0,
            "InstrumentID": "rb2201",    // 如以上代码中,需要取这个数据就可以 var instrumentID = ticker.Info.InstrumentID
            "HighestPrice": 5363,
            "UpperLimitPrice": 5769,
            "BidPrice3": 1.7976931348623157e+308,
            "BidVolume1": 29,
            "BidVolume2": 0,
            "AskPrice2": 1.7976931348623157e+308,
            "OpenPrice": 5344,
            "Volume": 1454951,
            "Turnover": 76715487230,
            "OpenInterest": 1241030,     // 持仓量
            "CurrDelta": 1.7976931348623157e+308,
            "ActionDay": "20210818",
            "PreDelta": 0,
            "UpdateTime": "10:31:12",
            "LowerLimitPrice": 4914,
            "UpdateMillisec": 0,
            "AskVolume2": 0,
            "BidPrice4": 1.7976931348623157e+308,
            "ExchangeID": "",
            "ExchangeInstID": "",
            "LastPrice": 5154,
            "SettlementPrice": 1.7976931348623157e+308,
            "PreClosePrice": 5345,
            "AskPrice1": 5155,
            "BidPrice2": 1.7976931348623157e+308,
            "AskVolume5": 0,
            "AveragePrice": 52727.19646915944,
            "PreOpenInterest": 1123010,
            "LowestPrice": 5151,
            "ClosePrice": 1.7976931348623157e+308,
            "AskPrice4": 1.7976931348623157e+308
        },
        "High": 5363,
        "Low": 5151,
        "Sell": 5155,
        "Buy": 5154,
        "Last": 5154,         // 如以上代码中,需要取这个数据就可以 var last = ticker.Last
        "Volume": 1454951,
        "OpenInterest": 1241030,
        "Time": 1629253872000
    }
    

    可以看到返回的数据也有Info属性,Info属性内的数据为实时行情数据的原始信息。 举个例子如果我们想获取当前的这个合约的持仓量并打印出来,根据以上的代码就可以写为:

    Log("持仓量:", ticker.Info.OpenInterest)   // 和ticker.OpenInterest一样,这里只是举例子从Info中取
    

    有些策略也以合约的持仓量作为信号判断依据,所以如果需要合约行情的一些其它信息可以在Info属性中查询。对于Info属性中"AskPrice3": 1.7976931348623157e+308这样的数据则是无效数据,1.7976931348623157e+308是一个很大的数值,只是用于填充。 对于ticker数据,使用最多的还是这些数据:

        "High": 5363,       // 最高
        "Low": 5151,        // 最低
        "Sell": 5155,       // 当前卖一
        "Buy": 5154,        // 当前买一
        "Last": 5154,       // 最新成交价
        "Volume": 1454951,  // 成交量
        "OpenInterest": 1241030,  // 持仓量
        "Time": 1629253872000     // 毫秒时间戳
    

    其它可以不用深究,这里学会如何调用这个exchange.GetTicker()函数,然后赋值给声明的ticker变量即可,完整的一句就是:var ticker = exchange.GetTicker()var这个词在之前的文章我们也看到过,这个是JavaScript语言的关键字。你可以简单理解为这个var作用就是创建一个变量,这个变量可以用来接收函数返回的值或者接收其它变量。所以这里我们写var ticker就是创建了一个名字叫ticker的变量。当然你也可以改成别的变量名,例如:var tickerA或者var tickerB等。 如以上代码中,需要取InstrumentID(合约代码,例如本例子中是rb2201,螺纹钢合约)这个数据就可以写代码var instrumentID = ticker.Info.InstrumentID,这样就拿到了当前这个数据的合约代码名称,存在instrumentID变量中。需要取Last(当前合约的最新成交价)这个数据就可以var last = ticker.Last,这样就拿到了这个最新成交价数值,存在了last变量中。做到会取tickerA或者tickerB或者你起的任何名字的变量中的数据即可。 取其它接口返回的数据结构中的数据都是类似的,可以举一反三测试学习。

  • exchange.GetRecords() 获取K线数据 接下来讲一下本篇重头戏,获取K线数据的函数exchange.GetRecords()exchange.GetRecords()函数返回的数据不再是一个对象结构(python中叫字典)。该函数返回的是一个数组,可能没有编程经验的萌新不明白数组是个什么概念。这里我们也不用去引经据典的讲一大篇数组的概念。一切以实用为主,这里exchange.GetRecords()返回的数据你可以就理解成一个个K线柱按照从左到右的顺序排列形成的一长串的数据结构。 我们上点形象易懂的解释,还是用我们熟悉的代码,只不过调用的函数换成了exchange.GetRecords()

    function main(){
        while(true){
            if(exchange.IO("status")){
                exchange.SetContractType("rb2201")
                var r = exchange.GetRecords()
                Log(r)
                throw "stop"
            } else {
                LogStatus(_D(), "未连接CTP !")
            }
            Sleep(5000)   // 避免打印过快,5秒打印一次
        }
    }
    

    并且在打印了一次r这个K线数组数据之后,调用throw "stop"这个函数抛出一个错误让策略停止,throw "stop"这句你可以就简单理解为让策略立即终止并打印一条错误日志内容“stop”(当然stop你可以换成任意字符串,写中文也行,例如throw "停止")。

    我们用回测来测试。

    img

    回测运行。

    img

    日志上输出了一长串的数据:

    [{"Time":1610899200000,"Open":4154,"High":4170,"Low":4103,"Close":4162,"Volume":11333,"OpenInterest":7532},{"Time":1610985600000,"Open":4157,"High":4164,"Low":4080,"Close":4081,"Volume":7484,"OpenInterest":12155},{"Time":1611072000000,"Open":4081,"High":4107,"Low":4060,"Close":4094,"Volume":4835,"OpenInterest":12277}, ... ]
    

    由于过长的日志会被自动截断所以截断后的内容是看不到的,这个打印出来的数据就叫做数组。数组是由[]包裹住其中按顺序排列的每个个体数据,这些个体数据叫做数组的元素。数组中每个元素由逗号,间隔。了解到这些,我们就以上面展示出的数组前三个元素来对比K线图上的K线柱数值。

    • 第一个K线柱(BAR) 数据:

      {"Time":1610899200000,"Open":4154,"High":4170,"Low":4103,"Close":4162,"Volume":11333,"OpenInterest":7532}
      

      img

    • 第二个K线柱(BAR) 数据:

      {"Time":1610985600000,"Open":4157,"High":4164,"Low":4080,"Close":4081,"Volume":7484,"OpenInterest":12155}
      

      img

    • 第三个K线柱(BAR) 数据:

      {"Time":1611072000000,"Open":4081,"High":4107,"Low":4060,"Close":4094,"Volume":4835,"OpenInterest":12277}
      

      img

    那怎么看明白上面的东西呢? K线数组中每个元素代表一个K线柱。这些数组中的每个元素里的数据和图表上的K线柱都是一一对应的,通过上面的第一、第二、第三K线柱数据和图表上的H(最高价High)、O(开盘价Open)、L(最低价Low)、C(收盘价Close)对比可以看到。

    {
       "Time": 1610899200000,   // Time表示这个K线柱周期的起始时间,是一个毫秒时间戳
       "Open": 4154,     // 这个周期的开盘价
       "High": 4170,     // 这个周期的最高价
       "Low": 4103,      // 这个周期的最低价
       "Close": 4162,    // 这个周期的收盘价
       "Volume": 11333,  // 这个周期的成交量
       "OpenInterest": 7532  // 这个周期的持仓量
    }
    

    萌新可能会问:“毫秒级时间戳是什么?”

    百度百科 https://baike.baidu.com/item/时间戳/6439235?fr=aladdin

    可能有的萌新又问了:“这个Time时间戳是一长串数字,我怎么知道这个是什么时间?怎么和图上对比?” 还记得前几篇中我们见过的_D()函数么?忘记了可以翻下回顾下。当_D()函数不传参数时返回的是当前的可读时间字符串。当传入一个毫秒级时间戳返回的就是这个时间戳对应的可读时间字符串。

    把第一个K线柱的时间戳:1610899200000,把第二个K线柱的时间戳:1610985600000,把第三个K线柱的时间戳:1611072000000都用_D()处理下看得到什么时间。

    function main() {
        Log(_D(1610899200000))
        Log(_D(1610985600000))
        Log(_D(1611072000000))
    }
    

    回测运行:

    img

    可以看到就是18号、19号、20号这三天的时刻(回测设置的K线周期是1日K线,所以GetRecords获取的K线数据就是日线,每个K线柱代表一天)。

    读到这里是不是对于exchange.GetRecords()函数返回的K线数据(K线柱数组)有了一个简单的了解。 本例子中声明了变量r然后立即调用了exchange.GetRecords(),该函数返回了一个K线数据并赋值给了r。我们在使用数组时如果要使用数组的第一个元素(本例中第一个K线柱),本例子中可以写作var bar1 = r[0],以此类推使用第二个元素(本例中第二个K线柱)写作var bar2 = r[1]

    那么这个数组的最后一个元素代表什么呢?如何使用呢?总不能一直查下去吧! 首先说,最后一个元素代表调用GetRecords函数时,当前最新的K线柱的数据,也就是距离当前时刻最近的一根K线柱数据。由此可见第一根K线柱即:r[0]就是距离当前时刻最远的那根K线柱。 那如何使用这个最后一根K线柱的数据呢?本例中举例子写法是:var lastBar = r[r.length - 1]。不熟悉的不用深究,记住储存K线数据的变量是r,倒数第一个K线柱就是r[r.length - 1],倒数第二个K线柱就是r[r.length - 2],以此类推。 如果要取某个K线柱数据中的高、开、低、收。例如取第一个K线柱的开盘价,本例中写作:var bar1Open = r[0].Open,其它以此类推。

  • 其它行情接口

    可能熟悉优宽 API文档的同学问exchange.GetDepth()exchange.GetTrades()为什么不介绍了? 主要由于商品期货通常都只有一档深度,所以使用exchange.GetDepth()获取订单薄数据的情况就很少,使用的场景基本就很少了。买一卖一的价格数据在exchange.GetTicker()函数的返回值中也有。exchange.GetTrades()函数返回的市场逐笔成交数据本身在商品期货CTP协议、易盛协议中是不支持的(因为没有这种数据的接口),所以调用会报错。但是如果需要市场上逐笔成交数据怎么办?可以参考:https://www.youquant.com/strategy/201568 例子,例子中可以用实时行情数据中市场持仓量、最新价格等信息的变动推算出逐笔成交记录。 通常这两种数据用到的很少,可以暂时不予深究,仅作了解即可。

查询账户信息

我们学会了查询行情数据,接下来学习一下查询账户资产数据。

  • exchange.GetAccount() 查询资产数据

    function main(){
        while(true){
            if(exchange.IO("status")){
                var info = exchange.SetContractType("rb888")
                Log(exchange.GetAccount())
                LogStatus(_D(), "已经连接CTP !")
            } else {
                LogStatus(_D(), "未连接CTP !")
            }
            Sleep(5000)   // 避免打印过快,5秒打印一次
        }
    }
    

    使用simnow模拟盘运行此代码。

    img

    获取到exchange.GetAccount()函数返回的账户资产数据并打印出来,使用格式化工具格式化之后如下:

    {
        "Info": {
            ...  // 原始数据里有各种账户相关的信息,比较长,这里省略
        },
        "Balance": 19999909.259999998,
        "FrozenBalance": 0
    }
    

    其中Balance记录的是当前可用资产,FrozenBalance记录的是订单未成交时冻结的资产。Info中记录了商品期货接口返回的账户数据的原始信息。里面包含当前账户权益等数据,需要相关计算的时候可以从Info里取出使用。


更多内容