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

商品期货量化交易实践系列课程

Author: 雨幕(youquant), Created: 2023-09-12 09:54:22, Updated: 2023-12-18 16:43:42

来的,不能随着价格的趋势发生变化。针对于这一个问题,我们可以设置一个更加灵活的网格区间,伴随价格的走势,我们可以不断地调整。

/*backtest
start: 2023-01-03 09:00:00
end: 2023-10-09 15:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
*/

var obj = $.NewPositionManager(); // 使用优宽量化交易类库
var band = []; // 定义全局变量 band

function bandCal(records){ //band计算函数
    var ma = TA.MA(records, maPeriod)[records.length - 1]
    var std = talib.STDDEV(records, stdPeriod)[records.length - 1]
    var array = [-1.96, -0.85, -0.53, 0.53, 0.85, 1.96];

    // 获取网格区间分界线
    var band = array.map(num => _N(ma + num * std, 2))
    return band
}

function onTick(records) {
    // 此处用来获取持仓信息
    var positions = _C(exchange.GetPosition) // 获取持仓数组
    var position_amount = 0
    
    for (var i = 0; i < positions.length; i++) { // 遍历持仓数组
        if (positions[i]['Type'] === PD_LONG || positions[i]['Type'] === PD_LONG_YD) {
            position_amount = 1 * positions[i].Amount // 将position_long标记为正数
        } else if (positions[i]['Type'] === PD_SHORT || positions[i]['Type'] === PD_SHORT_YD) {
            position_amount = -1 * positions[i].Amount // 将position_short标记为负数
        }
    }

    // 根据价格落在(-inf,-1.96],(-1.96,-0.85],(-0.85,-0.53],(-0.53,0.53],(0.53,0.85], (0.85,1.96], (1.96, Inf) 的区间范围来获取最新收盘价所在的价格区间
    var grid = null
    var close_01 = records[records.length - 1].Close;

    if (close_01 > band[5]) {
        grid = 6
    } else if (close_01 > band[4]) {
        grid = 5
    } else if (close_01 > band[3]) {
        grid = 4
    } else if (close_01 > band[2]) {
        grid = 3
    } else if (close_01 > band[1]) {
        grid = 2
    } else if (close_01 > band[0]) {
        grid = 1
    } else {
        grid = 0
    }

    // 若无仓位且价格突破则按照设置好的区间开仓
    if (!position_amount) {
        if (grid == 6 || grid == 0) {
            band = bandCal(records) // 重新计算band
            return('更新band')
        }

        if (grid > 3) {
            obj.OpenShort(symbol, 1); // 以市价单开空仓到仓位
            Log('空仓grid', grid)
        }
        if (grid < 3) {
            obj.OpenLong(symbol, 1); // 以市价单开多仓到仓位
            Log('多仓grid', grid)
        }
    }

    // 持有空仓的处理
    if (position_amount < 0) {
        // 突破区间,进行止损,重新计算band
        if (grid == 6) {
            Log('止损平仓grid', grid)
            obj.CoverAll(symbol) // 以市价单全平空仓
            band = bandCal(records) // 重新计算band
        }
        // 等于3为在中间网格,平仓
        else if (grid <= 3) {
            Log('止盈平仓grid', grid)
            obj.CoverAll(symbol) // 以市价单全平空仓
            band = bandCal(records) // 重新计算band
        }
        // 大于3为在中间网格的下方,加仓
        else if (grid > 3 && -position_amount < maxPos) {
            obj.OpenShort(symbol, 1) // 以市价单调空仓到仓位
        }
    }

    // 持有多仓的处理
    if (position_amount > 0) {
        // 突破区间,进行止损,重新计算band
        if (grid == 0) {
            Log('止损平仓grid', grid)
            obj.CoverAll(symbol) // 以市价单全平多仓
            band = bandCal(records) // 重新计算band
        }
        // 等于3为在中间网格,平仓
        else if (grid >= 3) {
            Log('止盈平仓grid', grid)
            obj.CoverAll(symbol) // 以市价单全平空仓
            band = bandCal(records) // 重新计算band
        }
        // 小于3为在中间网格的下方,加仓
        else if (grid < 3 && position_amount < maxPos) {
            obj.OpenLong(symbol, 1) // 以市价单调多仓到仓位
        }
    }
}

function main() {
    var isFirst = true
    var preBartime = 0
    while (true) { // 进入循环模式
        if(exchange.IO("status")){
            LogStatus("行情和交易服务器连接成功, " + _D())

            _C(exchange.SetContractType, symbol)
            var records = _C(exchange.GetRecords)

            if(isFirst){
                band = bandCal(records)
                isFirst =false
                preBartime = records[records.length - 1].Time
            }else{
                if(preBartime != records[records.length - 1].Time){
                    preBartime = records[records.length - 1].Time
                    onTick(records)
                }
            }

        }else{
            LogStatus("正在等待与交易服务器连接, " + _D())
        }
        Sleep(3000)
    }
}

我们来使用代码来进行实现。首先我们设置两个全局变量,obj交易类库单品种控制对象,band网格区间。接下来我们计算网格区间,定义bandCal函数,参数是k线records。相对于以往固定的使用5或者10作为网格价格的间隔,这里我们使用一些统计学知识,决定使用正态分布来决定价格的分布区间。我们价格价格的波动在一段范围内是符合正态分布的,因此使用均值加上标准差倍数乘以标准差,可以表示价格的分界线。这里我们选用了标准差倍数正负0.53代表,均值附近40%的波动区间,正负0.85代表均值附近60%的波动区间,正负1.96代表95%的波动区间。所以我们的网格区间,是以移动平均值作为均值,价格的不同分布概率作为网格的区间。

至于具体均值和标准差的计算,我们使用一定周期窗口内的数值,所以这里我们设置两个参数,maPeriod均线周期和stdPeriod标准差周期。

参数设置好以后,我们使用了TA.MA和talib.STDDEV分别来计算对应参数的均值和标准差。标准差是一种衡量价格波动性的指标,它可以帮助确定价格的波动范围。

然后,代码定义了一个包含6个数值的数组array,这些数值分别是[-1.96, -0.85, -0.53, 0.53, 0.85, 1.96]。这些数值就是我们刚提到的,代表了统计学上的正态分布中的标准差倍数。

接下来,代码使用了map函数对array数组中的每个元素进行遍历,并通过计算ma + num * std得到网格区间的分界线。这里的num就是array数组中的每个元素。

最后,代码使用了_N函数将计算得到的网格区间分界线保留两位小数,并将结果作为一个数组返回。这样呢,我们的网格区间就定义好了,当我们每次完成一笔交易或者价格突破关键点位的时候,我们可以使用这个函数进行band数组的更新。

第二个我们来定义onTick函数,这段代码是一个简单的网格交易策略的实现。首先获取当前的持仓信息,并初始化变量position_amount为0,用于记录持仓数量。接着遍历持仓数组,判断持仓类型是多仓(包括PD_LONG和PD_LONG_YD)还是空仓(PD_SHORT和PD_SHORT_YD),然后根据持仓类型判断持仓数量的正负,并存入position_amount变量中。

接下来根据价格落在我们设置的网格的区间范围,就是变量band存储的阈值来划分,来获取最新收盘价所在的价格区间grid。我们的假设是,在一定时间范围,价格是正态分布波动的,当价格偏离均值一定范围,我们进行相应的开多或者开空操作,当价格回到均值范围,我们进行止盈平仓;当然也有单边情况发生的时候,就是价格的波动超过了95%的置信区间,在一定程度上,代表着均值的转移,这个时候我们就要进行及时的平仓,并进行新的band的计算。

具体来说,grid一共有7个值。当grid等于3的时候,在中间正负0.53的标准差倍数,代表40%的正态分布的概率区间,这一区间内,我们不进行开仓,只进行止盈平仓的操作;当价格在1和2,我们进行开多的操作;当价格在4和5,进行开空的操作;在grid等于0或者6,我们就要及时的更新band,进行止损。

这里我们首先使用代码来判断当前价格所在的grid区间。

然后进行入场信号的判断,如果当前没有持仓,首先需要判断是否需要重新计算band,因为在很多情况下,当价格突破一定的阈值,阻力位就变成了支撑位,在很长时间内,grid一直是6,策略不会进行任何的操作,会错过很多的交易机会,所以我们要重新计算band,然后return,重新计算grid;接着当grid大于3的时候,我们使用市价单开空仓;当grid小于3,使用市价单开多仓。这样就完成了入场的操作。

下面进行加仓和出场的操作,出场包括止盈和止损。当持仓空仓position_amount小于0,首先我们判断止损,当grid等于6,代表突破上限,使用CoverAll以市价单全平空仓,并重新计算band;接下来是止盈的操作,如果grid回到3或者以下,我们就要及时的进行止盈,然后重新更新band; 当然如果没有达到止盈或者止损位,我们也可以跟随市场趋势,进行加仓的操作,当判断如果grid依然大于3,进行加仓开空;但是不要忘了,我们单向持仓是有一定数量限制的,所以添加条件当-position_amount小于maxPos最大持仓数量。

多仓的处理逻辑也是一样,只是grid的条件数值需要改一下。这样onTick函数就定义好了。

最后我们来定义主函数。网格策略作为一个低频趋势策略,我们想使用onBar机制运行,就是只有在k线更新的时候我们进行策略的运行。所以在while循环体外,我们首先定义isFirst,代表策略是否第一次运行,preBartime,用来判断k线的更新,。

下面设置while循环体。当判断是第一次运行策略,isFirst为真,首先需要计算band网格区间,然后定义isFirst为false,preBartime为当前k线的时间;

然后当判断isFirst为false的时候,我们判断最新的k线时间戳是否等于preBartime,如果k线更新,赋值preBartime为最新的k线时间,然后开始执行网格策略的ontick函数。以上呢,我们制定的的网格策略就定义完成。

接下来,我们回测运行一下。这里我们需要挑选合适的品种,网格策略适用于波动率低,趋势不明显的品种,是震荡行情获利法宝,这就要求我们对期货品种有个略微的把握,比如工业品尤其是黑色系更容易走出趋势行情不适合网格策略,反而农产品往往在一个价格区间内波动适合网格策略。另外,我们设置的网格设计理念是正态分布,确实更加适合震荡的行情,所以这里我们选择有名的稳定品种,玉米。根据回测的日志,可以看到这个策略确实取得了不错的收益。我们看下收益概览中回撤的时间,主要集中于四月中旬到五月中旬,和六月中旬到七月中旬,对照一下k线的走势,确实这两段时间是分别一个单边下降和上涨的行情,因此策略的收益出现较多的回撤。而在其他时间,当行情逐渐稳定下来,我们看到收益是逐渐上升的。

最近很多同学反应,在回测系统运行流畅的策略,到了实盘中经常会报很多的bug,其实在回测页面,我们也可以模拟实盘的环境,这里我们选择容错模式回测,我们来看下。容错模式就是模拟实盘环境,各种接口出现报错,去测试我们的策略模型是否足够健壮去处理这些bug,我们看下这里报错,这里的错误类型主要是没有连接交易所或者获取数据失败,所以这里我们使用_C重复尝试,再继续运行一下,可以看到没有运行错误了。这样呢,在容错模式下运行没有bug的模型,在实盘中一般不会出现较大的问题。

我们的课程是实战性课程,仅仅是运行策略肯定是不能满足我们的要求的。我们需要加上状态变量的恢复,这里我们需要记录的状态变量,是band,还记得吗,使用_G函数;还有策略运行状态的展示,包括当前交易品种,持仓方向,均价,数量,盈亏,止盈次数和止损次数,并且还有收益的展示,这些我们在前面的课程中都有提到过,这里不给大家重复的讲解了,如果大家对源码感兴趣的话,可以到我们的文库教程,这里包括了教程的文本讲解和原始的代码,大家可以复制运行一下,或者进行一下改进,欢迎大家进行更多的尝试。

/*backtest
start: 2023-01-03 09:00:00
end: 2023-09-30 15:00:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES","depthDeep":20}]
args: [["symbol","c888"],["maxPos",3],["stdPeriod",30]]
*/

var obj = $.NewPositionManager(); // 使用优宽量化交易类库
var band = []; // 定义全局变量 band
var pos_dir = '--'
var pos_amount = '--'
var pos_price = '--'
var pos_profit = '--'
var pos_margin = 0
var stopprofit = 0
var stoploss = 0

function bandCal(records){
    var ma = TA.MA(records, maPeriod)[records.length - 1]
    var std = talib.STDDEV(records, stdPeriod)[records.length - 1]
    var array = [-1.96, -0.85, -0.53, 0.53, 0.85, 1.96];

    // 获取网格区间分界线
    var band = array.map(num => _N(ma + num * std, 2))
    return band
}

function onTick(records) {

    // 此处用来获取持仓信息
    var positions = _C(exchange.GetPosition) // 获取持仓数组
    var position_amount = 0
    
    
    for (var i = 0; i < positions.length; i++) { // 遍历持仓数组
        if (positions[i]['Type'] === PD_LONG || positions[i]['Type'] === PD_LONG_YD) {
            position_amount = 1 * positions[i].Amount // 将position_long标记为正数
        } else if (positions[i]['Type'] === PD_SHORT || positions[i]['Type'] === PD_SHORT_YD) {
            position_amount = -1 * positions[i].Amount // 将position_short标记为负数
        }
    }

    // 根据价格落在(-inf,-1.96],(-1.96,-0.85],(-0.85,-0.53],(-0.53,0.53],(0.53,0.85], (0.85,1.96], (1.96, Inf) 的区间范围来获取最新收盘价所在的价格区间
    var grid = null
    var close_01 = records[records.length - 1].Close;

    if (close_01 > band[5]) {
        grid = 6
    } else if (close_01 > band[4]) {
        grid = 5
    } else if (close_01 > band[3]) {
        grid = 4
    } else if (close_01 > band[2]) {
        grid = 3
    } else if (close_01 > band[1]) {
        grid = 2
    } else if (close_01 > band[0]) {
        grid = 1
    } else {
        grid = 0
    }

    // 若无仓位且价格突破则按照设置好的区间开仓
    if (!position_amount) {
        if (grid == 6 || grid == 0) {
            band = bandCal(records) // 重新计算band
            _G('band', band)
            return('更新band')
        }

        if (grid > 3) {
            obj.OpenShort(symbol, 1); // 以市价单开空仓到仓位
            Log('空仓grid', grid)
        }
        if (grid < 3) {
            obj.OpenLong(symbol, 1); // 以市价单开多仓到仓位
            Log('多仓grid', grid)
        }
    }

    // 持有空仓的处理
    if (position_amount < 0) {
        // 突破区间,进行止损,重新计算band
        if (grid == 6) {
            Log('止损平仓grid', grid)
            obj.CoverAll(symbol) // 以市价单全平空仓
            band = bandCal(records) // 重新计算band
            stoploss += 1
        }
        // 等于3为在中间网格,平仓
        else if (grid <= 3) {
            Log('止盈平仓grid', grid)
            obj.CoverAll(symbol) // 以市价单全平空仓
            band = bandCal(records) // 重新计算band
            stopprofit += 1
        }
        // 大于3为在中间网格的下方,加仓
        else if (grid > 3 && -position_amount < maxPos) {
            obj.OpenShort(symbol, 1) // 以市价单调空仓到仓位
        }
    }

    // 持有多仓的处理
    if (position_amount > 0) {
        // 突破区间,进行止损,重新计算band
        if (grid == 0) {
            Log('止损平仓grid', grid)
            obj.CoverAll(symbol) // 以市价单全平多仓
            band = bandCal(records) // 重新计算band
            stoploss += 1
        }
        // 等于3为在中间网格,平仓
        else if (grid >= 3) {
            Log('止盈平仓grid', grid)
            obj.CoverAll(symbol) // 以市价单全平空仓
            band = bandCal(records) // 重新计算band
            stopprofit += 1
        }
        // 小于3为在中间网格的下方,加仓
        else if (grid < 3 && position_amount < maxPos) {
            obj.OpenLong(symbol, 1) // 以市价单调多仓到仓位
        }
    }
    
    _G('band', band)
}

function main() {
    var preBartime = 0
    var initAccount = _C(exchange.GetAccount)
    
    // 读取保存网格数据;
    band = _G('band')

    // 保存网格数据为空;
    if (band == null) {
        Log('无存储网格数据')
            _C(exchange.SetContractType, symbol)
            records = _C(exchange.GetRecords)
            
            if(records.length < 20) return
            band = bandCal(records)
            Log(band)
            _G('band', band)
            Log('已储存新的网格数据')
    }else{
        Log('载入已保存的网格数据')
    }

    while (true) { // 进入循环模式
        if(exchange.IO("status")){
            LogStatus("行情和交易服务器连接成功, " + _D())

            // 订阅合约,并且取得优宽量化平台当前周期的所有收盘价
            _C(exchange.SetContractType, symbol)
            var records = _C(exchange.GetRecords)
            
            if(preBartime != records[records.length - 1].Time){
                preBartime = records[records.length - 1].Time
                onTick(records)
            }

            var tblStatus = {
                type: "table",
                title: "持仓信息",
                cols: ["合约名称", "持仓方向", "持仓均价", "持仓数量", "持仓盈亏", "止盈次数", "止损次数"],
                rows: []
            }

            var statusPos = _C(exchange.GetPosition) // 获取持仓数组

            if(statusPos.length != 0){
                pos_dir = statusPos[0]['Type'] === PD_LONG || statusPos[0] === PD_LONG_YD ? '多头' : '空头'
                pos_amount = statusPos[0]['Amount']
                pos_price = statusPos[0]['Price']
                pos_profit = statusPos[0]['Profit']
                pos_margin = statusPos[0].Margin
            }else{
                pos_dir = '--'
                pos_amount = '--'
                pos_price = '--'
                pos_profit = '--'
                pos_margin = 0
            }

            tblStatus.rows.push([symbol, pos_dir, pos_price, pos_amount, pos_profit, stopprofit, stoploss])

            lastStatus = '`' + JSON.stringify([tblStatus]) + '`'
            LogStatus(lastStatus)

            var accountInfo = _C(exchange.GetAccount)

            var curprofit = accountInfo.Balance + pos_margin - initAccount.Balance
            LogProfit(curprofit, "权益", '&')
    
        }else{
            LogStatus("正在等待与交易服务器连接, " + _D())
        }
        Sleep(3000)
    }
}

最后,我们来稍微讲下策略的优化思路。任何策略是有适用范围的,网格策略也不例外,商品期货属于杠杆交易,尤其是网格交易有逆势加仓的特点,风险是比较大的。所以判断走势和控制风险是网格策略两个重要的优化方向。在本节课我们讲解的网格策略中,我们的设计原理是正态分布,意味着对震荡的行情有了一个初始的假设;而当出现单边行情的时候,正态分布可能失效,我们可以尝试非正态分布,比如对数正态分布和伽马分布等一些偏态的分布去进行拟合;第二点,关于风险控制,一个可以优化的思路是我们可以利用主力合约与次主力合约的价差或者有相关性的跨品种价差做网格策略,相比于单品种价差的波动是相对稳定的。

以上呢,都是我们可以做的一些探索,大家如果有好的想法,也可以提出来,我们一起努力来学习,共同进步。

视频参考链接:

《商品期货等差网格策略》

《用python中的Pandas库实现一个商品期货网格策略》

15.浅谈期货交易中的资金流分析

价格不是上就是下,长期而言,价格的涨跌概率应各是50%,那么要正确预测未来的价格,就需要实时获取影响价格的全部因素,然后给每个因素一个正确权重,最后作出客观理性分析。要把影响价格的全部因素罗列出来,可能会写满整个屏幕。

市场是真的无法预测吗?也不是,所有的宏观因素和微观因素都已经反映到价格上了,也就是说价格是全部因素相互作用的结果。我们只需要分析价格,就可以做出一个完整的交易策略。

那么影响价格的原因是什么呢?我们可能会总结,因为:国家对相关产业政策扶持、原产地又又下暴雨了、国际贸易战、MACD金叉了、别人都买了等等,当然这些也许都没错。事后看,总能找出推动价格上涨的理由。

但是,作为散户的我们,信息在很多时候只能延迟被动的接受,并且分析的方向并不一定正确。那么有没有一种方法可以实时的了解价格的涨跌原因呢?

其实,价格的涨跌类似于水涨船高。这里的水就是资金的推动,盘面上,如果买的人多过卖的人,价格就会上涨。反之,如果卖的人多过买的人,价格就会下跌。有了这个概念,我们就可以根据资金净流向流动反映出来的供求关系,对未来价格的走势给出合理的预期。

但是在很多情况下,趋势是由大资金导向把控的。所以很多炒期货的同学,经常会关注于龙虎榜的数据,每日在收盘以后查看各大机构在空头和多头的开仓和平仓数量,并以此来推测价格的未来走势。但是这个龙虎榜数据是在收盘以后两个小时左右发布的,具有一定的滞后性。并且也无法观测区分大资金和小资金的资金走势。

但是呢,拥有量化分析的工具。我们就可以实时的收集数据,进行整理,分类和汇总,从而实时的展示各品种在各个时间段的开多,开空,平空和平多的数量,并且可以依据仓位变动的大小,大致区分是大资金的交易,还是小资金的交易。今天呢,我们就试着根据实时的数据,做一个大资金和小资金的资金流的实时汇总和展示。

我们首先来分析一下一个分时数据的截图,可以看到有时间,价格,现量,仓差和性质。现量是交易量,仓差是持仓量的变化。这个数据是在交易时间段,实时更新的,我们可以获取到,并由此汇总这个数据,分别计算出来多头和空头的资金流向。

这里的大资金和小资金力度的区分呢,我们初步制定的规则,当仓差大于阈值,我们定义为大资金的变化,小于阈值,定义为小资金的资金变化。当然这是一个初步的参考,也有很多小资金账户在同一时间汇总交易的数量大于阈值,并且也有很多大资金会进行拆单,将一个大单子进行拆分,从而避免价格的大量波动,达到自己理想的成交价格。所以我们的资金流分析只是一个初步的尝试,大家可以在这个基础上,进行更仔细的划分。

function getTypeName(zcl, jylx, vol){
    if(zcl > 0 && (Math.abs(zcl) == vol)){
        return '双开'
    }else if(zcl < 0 && Math.abs(zcl) == vol){
        return '双平'
    }else if(jylx == 1 && zcl < 0){
        return '多平'
    }else if(jylx == 2 && zcl < 0){
        return '空平'
    }else if(jylx == 2 && zcl > 0){
        return '多开'
    }else if(jylx == 1 && zcl > 0){
        return '空开'
    }else if(jylx == 2 && zcl == 0){
        return '多换'
    }else if(jylx == 1 && zcl == 0){
        return '空换'
    }
    return ''
}

function main() {
    var url = "数据源头url";
    var isFirst = true

    while(true){
        var t = new Date()                  // 获取当前时间对象
        var hour = t.getHours()             // 获取当前小数:0~23
        var minute = t.getMinutes()         // 获取当前分钟:0~59 

        //重置数据
        if(isFirst){
            var openLongSumBig = 0
            var openShortSumBig = 0
            var closeLongSumBig = 0
            var closeShortSumBig = 0

            var openLongSumSmall = 0
            var openShortSumSmall = 0
            var closeLongSumSmall = 0
            var closeShortSumSmall = 0
            var dataList = [] //交易记录id保存列表

            isFirst = false
        }else{
            if(hour == 20 && minute == 59){
                Log('新的交易日,重置数据')
                var openLongSumBig = 0
                var openShortSumBig = 0
                var closeLongSumBig = 0
                var closeShortSumBig = 0

                var openLongSumSmall = 0
                var openShortSumSmall = 0
                var closeLongSumSmall = 0
                var closeShortSumSmall = 0
                var dataList = []
            }
        }

        if($.IsTrading(symbol)){
            
        }else{
            LogStatus('非交易时间段')
        }

        if($.IsTrading(symbol)){
            var response = HttpQuery(url);
            // 提取响应内容中的 JSON 数据部分
            var startIndex = response.indexOf("(") + 1
            var endIndex = response.lastIndexOf(")")
            var jsonContent = response.substring(startIndex, endIndex)
            var info = JSON.parse(jsonContent)

            // 格式化并输出结果
            if(info.mx){
                for (var i = 0; i < info.mx.length; i++) {
                    var v = info.mx[i]
                    var price = v.p
                    var time = _D(v.utime * 1000)
                    var amount = Math.abs(v.zcl) 
                    var type = getTypeName(v.zcl, v.jylx, v.vol)
                    var id = v.utime + v.vol + v.zcl
                    if (!dataList.includes(id)) {
                        dataList.push(id)
                        // 输出结果
                        Log('时间:', time, '价格:', price, '数量:', amount, '类型:', type)
                        if (type == '多开'){
                            amount >= threshold ? openLongSumBig += amount : openLongSumSmall += amount
                        }
                        if (type == '空开'){
                            amount >= threshold ? openShortSumBig += amount : openShortSumSmall += amount
                        }
                        if (type == '多平'){
                            amount >= threshold ? closeLongSumBig += amount : closeLongSumSmall += amount
                        }
                        if (type == '空平'){
                            amount >= threshold ? closeShortSumBig += amount : closeShortSumSmall += amount
                        }
                        if (type == '双开'){
                            if(amount >= threshold){
                                openLongSumBig += amount/2  
                                openShortSumBig += amount/2
                            }else{
                                openLongSumSmall += amount/2
                                openShortSumSmall += amount/2 
                            }
                        }
                        if (type == '双平'){
                            if(amount >= threshold){
                                closeLongSumBig += amount/2  
                                closeShortSumBig += amount/2
                            }else{
                                closeLongSumSmall += amount/2
                                closeShortSumSmall += amount/2 
                            }
                        }

                        var capTable = {type: 'table', title: '仓位操作汇总信息', cols: ['类型', '大资金仓位数量', '小资金仓位数量'], rows: [ 
                        ['多开',openLongSumBig,openLongSumSmall],
                        ['空开',openShortSumBig,openShortSumSmall],
                        ['多平',closeLongSumBig,closeLongSumSmall],
                        ['空平',closeShortSumBig,closeShortSumSmall]]}
                        var treTable = {type: 'table', title: '仓位类型汇总信息', cols: ['类型', '大资金仓位数量', '小资金仓位数量'], rows: [ 
                        ['多头',openLongSumBig - closeLongSumBig ,openLongSumSmall - closeLongSumSmall ],
                        ['空头',openShortSumBig - closeShortSumBig,openShortSumSmall - closeShortSumSmall]]}
                        LogStatus('`' + JSON.stringify(capTable) + '`\n' + '`' + JSON.stringify(treTable) + '`')

                        $.PlotMultLine("资金走势", "大资金走势", openLongSumBig + openShortSumBig - closeLongSumBig - closeShortSumBig,v.utime * 1000)
                        $.PlotMultLine("资金走势", "小资金走势", openLongSumSmall + openShortSumSmall - closeLongSumSmall - closeShortSumSmall,v.utime * 1000)
                        $.PlotMultLine("趋势分析", "大资金多头趋势", openLongSumBig - closeLongSumBig,v.utime * 1000)
                        $.PlotMultLine("趋势分析", "小资金多头趋势", openLongSumSmall - closeLongSumSmall,v.utime * 1000)
                        $.PlotMultLine("趋势分析", "大资金空头趋势", openShortSumBig - closeShortSumBig,v.utime * 1000)
                        $.PlotMultLine("趋势分析", "小资金空头趋势", openShortSumSmall - closeShortSumSmall,v.utime * 1000)
                    }
                }
            }else{
                Log('暂时无法获取数据')
            }

        }else{
            LogStatus('非交易时间段')
        }

        Sleep(10)
    }
}

话不多说,进入我们的代码。这次我们获取数据的方式是,从网页上获取期货交易的分时数据,然后进行汇总分析。分时数据提供的渠道有很多,这里我们挑选一个渠道,定义url变量。然后我们定义isFirst,代表第一次运行这个策略。

然后在while循环体中,我们就要开始分时数据的收集,整理和汇总。因此我们想统计每个交易日的具体资金流变化,所以在每天晚上的8点59分,我们需要定时重置清空我们的数据。获取当前时间对象t,然后获取具体小时和分钟。然后开始定义重置数据的模块。

如果是第一次运行策略,当isFirst为真。我们定义一些变量来汇总大资金和小资金的多头和空头的信息,包括openLongSumBig大资金开多,openShortSumBig开空, closeLongSumBig多平,和closeShortSumBig空平,对于小资金这里的变量后缀改为small就可以。

然后我们还需要定义一个空的列表,dataList,因为每次返回的分时数据中可能存在已经收集过的数据,需要做一个重复过滤筛选,所以定义一个类似交易记录id的变量。最后isFirst定义为false。

当isFirst为假,在每日的20点59分,我们重置以上的这些变量。

接着我们要开始收集我们的数据了。我们都知道不同的品种具有不同的开盘时间,在非开盘时间,是没有成交记录的。我们这里使用交易类库的IsTrading函数,只有正在交易的时候,才会开始收集数据,在非交易时段,输出状态信息’非交易时间段’。

在交易时间段内,使用HttpQuery获取分时数据的信息,我们对原始获取的数据进行一下正则处理,提取响应内容中的 JSON 数据部分,然后使用JSON.parse进行解析。这样分时数据的初始文本内容我们就获取完成。

数据是保持在infomx当中,当然并不是每时每刻都可以获取到数据的,由于一些特殊的故障,当没有获取到infomx,策略就会报错停止运行。所以这里进行一个容错的处理。如果获取到分时数据,infomx,就开始分条处理;如果没有获取到,打印’暂时无法获取数据’。

返回的分时数据是一个个时间戳对应的交易记录,使用for循环,分别提取每条交易记录的价格,时间戳,返回的是utime时间戳,我们需要乘以1000,持仓数量,也就是仓位资金的变化,是zcl属性,具有正负之分,这里我们统一为正值。xvol,代表的是交易量;type,具体的交易性质,包括多开,多平,空开,空平,多开,多换,空开,和空换这八种类型,原有的分时数据是不提供这个字段的,我们需要一个函数进行转换。

getTypeName函数,具有三个参数,zcl: 表示数据中的 zcl 字段,表示持仓量;jylx: 表示数据中的 jylx 字段,当jylx 为1,代表空头,当jylx为2,代表多头;vol: 表示数据中的 vol 字段,表示成交量。在这个函数中,我们就要根据是多头还是空头,交易量和持仓量的变化,确定交易性质。

如果 zcl 持仓量变化大于 0,且 zcl 的绝对值等于 vol交易量,则返回 ‘双开’。 否则,如果 zcl 小于 0,且 zcl 的绝对值等于 vol,则返回 ‘双平’。 否则,如果 jylx 等于 1,且 zcl 小于 0,返回 ‘多平’。 否则,如果 jylx 等于 2,且 zcl 小于 0,返回 ‘空平’。 否则,如果 jylx 等于 2,且 zcl 大于 0,返回 ‘多开’。 否则,如果 jylx 等于 1,且 zcl 大于 0,返回 ‘空开’。 否则,如果 jylx 等于 2,且 zcl 等于 0,返回 ‘多换’。 否则,如果 jylx 等于 1,且 zcl 等于 0,返回 ‘空换’。 如果以上条件均不满足,则返回空字符串 ‘’。

这里还需要注意的是,其实这里我们获取的交易性质,是汇总不同交易量后的持仓量变化的推断,因为在一条信息中,交易量包含不同的开多,开空,平空和平多的交易操作,最后引起的仓位变化,我们定义为这一时刻的交易的性质。

比如这里的交易量是101手,最后引起的持仓量变化是-19手,这101手交易量可能包含很多开多,开空,平多和平空不同类型的操作,但是最后引起的仓位数量是下降,价格变化是下跌,所以我们定义为多平,在仓位资金的流出定义为-19。

接着我们合成一个id,作为此条交易信息的标识,使用时间戳加成交量加仓差。

接着就要对资金进行分类处理了,如果dataList不包含id,证明该条交易记录没有处理过,那么push到dataList中。然后打印该条交易记录信息。

接着根据type类型,分类统计amount到大小资金流的不同交易方向中。

举例示范一下,如果type是多开,使用三元表达式判断,如果amount大于阈值threshold,累加到openLongSumBig中,如果小于阈值,累加到openLongSumSmall中。

对于空开,空平和多平,处理的逻辑也是一样的。对于双开和双平,我们使用amount除以2,分别累加到不同方向。而对于多换和空换,由于持仓没有变化,所以我们不记录统计。

接下来就要进行图表的展示了,我们分别统计仓位操作汇总信息,包括大资金和小资金的,具体开多,开空,平多和平空的数量;

还有仓位类型汇总信息,分别统计不同资金在多头和空头的汇总信息,例如多头仓位,使用不同资金的开多仓位减去平多仓位。

最后,我们使用画图展示,不同资金的走势,利用开多仓位加上开空仓位,减去平多仓位和平空仓位;

趋势分析,分别就不同类型的资金的多头趋势和空头趋势进行计算展示。

然后我们建立实盘就可以运行了。

在实盘中可以看到,在交易时间段,状态栏和图表会实时的展示,大小不同类型资金,汇总交易操作数量,和资金走势具体情况。通过实盘交易的观察和资金流分析,可以更加清晰地了解不同类型资金在市场中的博弈情况和趋势。另外,根据资金流分析的结果,我们可以对市场趋势有一个更宏观的认识和了解。

此外,对于不同类型资金的趋势分析,我们也可以使用量化的角度更深入地研究大资金和小资金对价格的推动作用。比如,当大资金流入市场时,是否意味着市场将会持续上涨?或者当小资金占据市场主导时,市场是否会出现频繁的短期波动?因此,在实战中,资金流分析是非常有用的工具,可以帮助我们更好地把握市场机会和趋势,制定更科学合理的交易策略。

相对于以往,我们讲述的都是经典的策略原理和具体的代码设计,本节内容我们更像是一个试验性的探索。因为在我们的学习过程中,在我们感觉掌握到一定知识后,总是会试图对于交易的理念和想法使用程序化表达出来。本节课,我们就参考了股票资金流的分类算法,构建出了期货交易中的资金流分析。当然本设计并不是十分的完善,希望本节课作为一个启发性的课程,大家可以对这个算法提出更好的优化方法,我们也会积极采纳,共同学习,共同进步。

视频参考链接:

《资金流计算:划分标准 散户还是机构》

16.优宽量化交易平台的合约代码设置

你知道吗,优宽作为专业的量化交易平台,不仅支持商品期货市场,期权市场和股票市场也是支持的。本节课呢,我们将要学习具体怎样在优宽平台设置不同类型的期货,期权和股票的合约。

期货合约设置

首先我们来看期货合约的设置。由于商品期货合约存续的特殊性,为满足回测的使用需求,针对每一个品种提供主力连续合约和指数合约,其主要是根据当前时间段内有效的商品期货合约数据人工合成。其中:

主力连续合约:由该期货品种不同时期的主力合约(价格和成交量)直接拼接而成,代码以888结尾,例如rb888。合约首次上市时, 以当日收盘同品种持仓量最大者作为从第二个交易日开始的主力合约。如果同品种其他合约持仓量在收盘后超过当前主力合约1.1倍时, 则在第二个交易日进行主力合约切换。

指数合约:由该期货品种所有正在交易的合约,以持仓量加权平均计算。

在回测中,我们就可以使用主力连续合约或者指数合约设置不同的期货品种。这主要是在回测系统中使用更加方便。但是在实盘使用的时候,当我们使用主力或者指数合约下单,交易所是无法识别的,这里我们使用调试工具对接真实的市场进行仿真交易,当我们使用指数合约下单,返回 Not select symbol的错误。

在实盘中,我们需要设置具体的合约名称。针对于不同交易所的具体合约,期货合约代码规则可以总结为这张图中展示的这样。

交易所 具体合约规则 主力连续合约 指数合约 具体合约
中国金融期货交易所 品种代码(大写) + 交割年份(2位) + 交割月份(2位) IF888 IF000 IF2201
上海期货交易所 品种代码(小写) + 交割年份(2位) + 交割月份(2位) ag888 ag000 ag2201
上海国际能源交易中心 品种代码(小写) + 交割年份(2位) + 交割月份(2位) bc888 bc000 bc2201
郑州商品交易所 品种代码(大写) + 交割年份(1位) + 交割月份(2位) AP888 AP000 AP201
大连商品交易所 品种代码(小写) + 交割年份(2位) + 交割月份(2位) a888 a000 a2201
广州期货交易所 品种代码(小写) + 交割年份(2位) + 交割月份(2位) lc888 lc000 lc2405

中国金融期货交易所:合约代码由品种代码(大写)+ 交割年份(2位)+ 交割月份(2位)组成。例如具体合约为IF2201。

上海期货交易所,上海能源期货交易所,大连商品期货交易所和广期所,广期所是最新成立的,这四个交易所它们的合约命名的规则都是一样的:合约代码由品种代码(小写)+ 交割年份(2位)+ 交割月份(2位)组成。例如具体合约为ag2201。

比较特殊的是郑州商品交易所:合约代码由品种代码(大写)+ 交割年份(1位)+ 交割月份(2位)组成。例如具体合约为AP201。

根据不同的交易所和品种,合约代码会有一定的差异,但通常都遵循这种命名规则。

如果我们想获取所有产品的列表数据,可以使用products参数。需要注意,这个函数只能在实盘中使用。

可以看到,一共返回了151个期货品种,不过这里有一些特殊合约,比如efp合约等,我们只需要关注不加特殊后缀的合约。

function main() {
    while (!exchange.IO("status")) {
        LogStatus("正在等待与交易服务器连接, " + new Date())
    }

    Log("开始获取所有产品")
    var products = _C(exchange.IO, "products")
    Log("产品合约列表获取成功")

    for (var i = 0; i < products.length; i++) {
        Log(products[i].ProductID)
    }
}

可以看到,一共返回了153个期货品种,不过这里有一些特殊合约,比如efp合约等,我们只需要关注不加特殊后缀的合约。

如果我们想获得所有合约的列表数据,可以使用instruments参数。

function main() {
    while (!exchange.IO("status")) {
        LogStatus("正在等待与交易服务器连接, " + new Date())
    }

    Log("开始获取所有合约")
    var instruments = _C(exchange.IO, "instruments")
    Log("合约列表获取成功")
    var len = 0
    for (var instrumentId in instruments) {
        len++
    }
    Log("合约列表长度为:",len)
}

可以看到,一共有30844个,这里面不仅包括期货,也包括期权的合约。所以在优宽平台,针对于不同的期货品种,数据的获取和使用都是很方便的。

期权合约设置

接着我们来看下期权合约代码的设置。商品期权是一种很好的商品风险规避和管理的金融工具,大宗商品的风险无处不在,其中最重要的是价格风险,那么我们就可以利用商品期权的各种套期保值工具对冲交易,来规避这种风险。商品期货期权的交易需要期货公司开通相关权限。

商品期货期权合约基本格式:

标的期货合约 + 看涨 / 看跌 + 行权价

由于交易所的标准合约命名规则并不相同,且对大小写敏感,各交易所合约格式可能有差别,以下是具体交易所对应的合约格式。当我们设置不同交易所期权合约代码,可以对照看下。

交易所 代码格式
上期所 小写 + 四个数字 + C/P + 行权价
郑商所 大写 + 三个数字 + C/P + 行权价
中金所 大写 + 四个数字 + -C-/P- + 行权价
大商所 小写 + 四个数字 + -C-/P- + 行权价
上能源 小写 + 四个数字 + -C-/P- + 行权价
广期所 小写 + 四个数字 + -C-/P- + 行权价

这里的大写C是call option的简写,代表看涨, P是put option的简写,代表看跌。 需要注意的是并不是所有的期货品种都具有期权的。比如玻璃和纯碱都不具有期权合约。

下面我们看下在优宽进行期权合约的查找和订阅,例如我们想查询当前的原油合约,首先和刚才一样设置instruments,然后在InstrumentName查找是否包含sc,原油的期货合约代码,并且包含大写C,或者大写P,最后返回当前所有正在交易的期权合约。

function main(){
    var productsForFind = null
    while(true){
        if(exchange.IO("status")){
            LogStatus(_D(), "已经连接CTP !")     
            var ret = exchange.IO("instruments")
            ret.forEach(function(product) {
                // Log(product)
                // 这里设置要查的名字,sc铁矿石合约
                if (product.InstrumentName.indexOf("sc") != -1 && (product.InstrumentName.indexOf("P") != -1 || product.InstrumentName.indexOf("C") != -1)) { 
                    Log(product, "#FF0000")
                    productsForFind = product
                }
            })
            break
        } else {
            LogStatus(_D(), "未连接CTP !")
        }
        Sleep(1000)
    }
    
    Log(productsForFind, "#FF0000")
}

如果想设置具体的期权合约,并获取正在交易的ticker数据,可以这样设置。同样使用SetContractType,合约代码为i2311-P-840。代表选择了铁矿石23年11月份的看跌合约,行权价是840。然后日志信息里返回当前正在交易的合约明细和ticker数据。

function main() {
    // 判断商品期货行情和交易连接是否正常
    while(!exchange.IO("status")) {
        Sleep(1000);
    }
    // 设置合约代码
    Log('合约明细', exchange.SetContractType("i2311-P-840"));
    Log('ticker数据', exchange.GetTicker())
}

股票合约的设置

最后我们来看下股票合约的设置。在优宽平台,不仅支持A股,还支持港股和美股的数据获取,策略验证和回测。支持的交易所包括中泰XTP模拟交易,和富途牛牛的实盘交易、模拟盘交易。如果大家对股票的量化交易感兴趣的话,我们后续也会为大家推出一些股票类的量化交易课程。

下面我们来看下具体股票合约的订阅方式,A股分为上交所,深交所和北交所,目前北交所的股票是暂时不支持的,我们后续会进行添加。订阅合约的时候,在股票代码需要具体交易所的后缀。

上交所股票后缀需要加上SH,例如我们这里获取茅台的ticker数据。在回测页面,平台需要选为股票证券。

function main() {
    var info = exchange.SetContractType("600519.SH")    // 设置为股票600519.SH即贵州茅台
    Log(info)
    Log(exchange.GetTicker())                           // 获取股票贵州茅台当前的价格信息
}

深交所股票后缀是SZ,这里展示获取万科股票的数据。

function main() {
    var info = exchange.SetContractType("000002.SZ")    // 设置为股票000002.SZ即万科
    Log(info)
    Log(exchange.GetTicker())                           
}

港股的后缀是HK。

function main() {
    var info = exchange.SetContractType("00700.HK")    // 设置为腾讯控股
    Log(info)
    Log(exchange.GetAccount())                          
    Log(exchange.GetTicker())                           
}

美股的后缀是UK。

function main() {
    var info = exchange.SetContractType("AAPL.US")  // 订阅合约代码AAPL.US 
    Log(info)                                       // 打印订阅的合约的详细数据 
    var ticker = exchange.GetTicker()               // 获取AAPL.US 合约的行情数据
    Log(ticker)                                     // 打印获取到的数据
}

这样我们就可以使用股票的数据进行一些量化策略的编写和验证,并且可以连接交易所的模拟账户,对接真实的市场,进行仿真实盘的演练。

以上呢,就是我们在优宽平台订阅不同类型合约的实际操作展示。可以发现,这里可以使用进行量化探索的地方很多,而且重点是数据获取和回测模拟,全部都是免费的。我们呢,可以在文库中找到一些策略的源码,比如这里的股票双均线,网格策略,我们都可以挑选不同的品种进行尝试。如果大家在学习的过程中,遇到了问题,也欢迎留言,我们会为大家热心解答~

视频参考链接:

《优宽量化商品期货合约代码明细》

《港股美股量化交易指南(一)》

17.期权基础知识介绍

上一节课,我们学习了怎样在优宽平台设置期权和股票合约的明细,有不少的同学非常感兴趣。特别是对于期货和期权的对冲策略,询问我们能不能讲解一下怎样在优宽平台进行实现,本节课,我们就给大家安排。

不熟悉期权的小伙伴确实对这个名词陌生又畏惧,因为相对于期货来说,期权相当于一个期货的衍生品。所以,它的玩法也更多。这是大商所铁矿石的期权合约列表,可以看到,针对于不同月份的合约,包括近月和远月,不同的价格,从低到高依次排列,不同的交易类型,包括认购期权和认沽期权。另外,期权还有不同的交易方向,包括买入期权和卖出期权,不同期权的盈利方式,还有到期时间,包括美式和欧式期权,这些名词压上来,大家肯定都是一头雾水。但是本节课呢,我们将对刚才讲到的名词简单进行一下梳理,帮助大家更全面的认识期权。

期权类型

期权是作为标的资产期货的衍生品而存在的,所以它的价格变化和期货存在紧密的关系。首先,我们来了解一下期权的类型。

看涨期权(认购期权):行权方有权力在未来以约定的价格(行权价格)购买标的资产。 看跌期权(认沽期权):行权方有权力在未来以约定的价格(行权价格)卖出标的资产。

一个期权的交易操作,包括买入和卖出。但是和期货不一样的是,买入期权并不一定是多头,而卖出期权也并一定是空头。要知道,理论上一笔期权合约是同时被买入和卖出的,也就是预期相反的两人成为了同一合约的对手方。买入看涨期权意味着标的资产上涨到行权价之后可以选择行权或卖出,就是用更低的价格获得合约或者交易套利,所以看涨期权的买方是典型的多头,反之卖方就是是空头;另一方面,看跌期权的买方则是希望通过标的资产下跌获利的,所以是空头,而身为其对手方的看跌期权卖方便是多头。

所以总结一下就是,买入看涨和卖出看跌都是多方,而卖出看涨和买入看跌都是空方。

| 期权类型 | 交


更多内容