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

商品期货量化交易必学的知识--基于tick数据推算逐笔交易历史

Author: 雨幕(youquant), Created: 2023-01-04 17:57:03, Updated: 2023-11-17 21:32:12

img

商品期货量化交易必学的知识–基于tick数据推算逐笔交易历史

由于在商品期货市场,CTP协议没有提供订单流数据。所以如果想做一些基于订单流数据的策略则无从下手。好在CTP协议给出的tick行情有足够的数据可以反推出订单流,这里反推出的订单流也只是tick切片之间的成交情况的合并信息。不过有总比没有强。

订单有卖单列表,买单列表。卖单订单无非是:「卖出开空」或者「卖出平多」。买单订单无非是:「买入开多」或者「买入平空」。

盘口订单 下单方向1 下单方向2
卖单 卖出开空 卖出平多
买单 买入开多 买入平空

4种订单类型的成交组合:

方向类别 卖出开空 卖出平多
买入开多 卖出开空、买入开多 => 双开,持仓量增加 卖出平多、买入开多 => 多换,持仓量不变
买入平空 卖出开空、买入平空 => 空换,持仓量不变 卖出平多、买入平空 => 双平,持仓量减少

实际上盘面上交易过程是非常复杂、快速的,可能一次tick切片行情变动中混合了以上多种成交组合,所以还需要根据盘口价格变动做后续判断。

算法解析

优宽国内站上公开了一个反推算法(javascript语言实现)十分有学习意义,是量化交易入门者必学知识。这里就对这个公开的代码做分析,方便学习到这个算法逻辑,为了方便学习我直接把代码注释写上。

var NewFuturesTradeFilter = function() {      // 该函数是一个构造函数,构造出用于计算逐笔成交的对象
    var type_enum = {                         // 定义逐笔成交信息的枚举类型
        OPENLONG:"多开|OpenLong",             // 多开:新多头入场开仓
        OPENSHORT:"空开|OpenShort",           // 空开:新空头入场开仓
        OPENDOUBLE:"双开|OpenDouble",         // 双开:多头、空头入场开仓
        CLOSELONG:"多平|CloseLong",           // 多平:多头平仓离场
        CLOSESHORT:"空平|CloseShort",         // 空平:空头平仓离场
        CLOSEDOUBLE:"双平|CloseDouble",       // 双平:多空平仓离场
        EXCHANGELONG:"多换|ExchangeLong",     // 多换:多头换手
        EXCHANGESHORT:"空换|ExchangeShort",   // 空换:空头换手
        OPENUNKOWN:"开仓|OpenUnkown",         // 开仓:无法判断出主动成交的方向
        CLOSEUNKOWN:"平仓|CloseUnkown",       // 平仓:无法判断出主动成交的方向
        EXCHANGEUNKOWN:"换仓|ExchangeUnkown", // 换仓:无法判断出主动成交的方向
        UNKOWN:"未知|Unkown",                 // 未知:无法判断
        NOCHANGE:"空闲|NoChange",             // 空闲:没有变化
    }
    
    // 定义涨为红色,跌为绿色,白色为价格不变
    var color_enum = {RED:"#00ff00", GREEN:"#ff0000", WHITE:"#666"} // Reverse China color
    
    // 定义一些动作的枚举
    var tick_dict = {
        delta_enum_NONE: {
            forward_enum_UP: [ type_enum.NOCHANGE, color_enum.WHITE ],
            forward_enum_DOWN: [ type_enum.NOCHANGE, color_enum.WHITE ],
            forward_enum_MIDDLE: [ type_enum.NOCHANGE, color_enum.WHITE ]
        },
        delta_enum_EXCHANGE: {
            forward_enum_UP: [ type_enum.EXCHANGELONG, color_enum.RED ],
            forward_enum_DOWN: [ type_enum.EXCHANGESHORT, color_enum.GREEN ],
            forward_enum_MIDDLE: [ type_enum.EXCHANGEUNKOWN, color_enum.WHITE ]
        },
        delta_enum_OPENFWDOUBLE: {
            forward_enum_UP: [ type_enum.OPENDOUBLE, color_enum.RED ],
            forward_enum_DOWN: [ type_enum.OPENDOUBLE, color_enum.GREEN ],
            forward_enum_MIDDLE: [ type_enum.OPENDOUBLE, color_enum.WHITE ]
        },
        delta_enum_OPEN: {
            forward_enum_UP: [ type_enum.OPENLONG, color_enum.RED ],
            forward_enum_DOWN: [ type_enum.OPENSHORT, color_enum.GREEN ],
            forward_enum_MIDDLE: [ type_enum.OPENUNKOWN, color_enum.WHITE ]
        },
        delta_enum_CLOSEFWDOUBLE: {
            forward_enum_UP: [ type_enum.CLOSEDOUBLE, color_enum.RED ],
            forward_enum_DOWN: [ type_enum.CLOSEDOUBLE, color_enum.GREEN ],
            forward_enum_MIDDLE: [ type_enum.CLOSEDOUBLE, color_enum.WHITE ]
        },
        delta_enum_CLOSE: {
            forward_enum_UP: [ type_enum.CLOSESHORT, color_enum.RED ],
            forward_enum_DOWN: [ type_enum.CLOSELONG, color_enum.GREEN ],
            forward_enum_MIDDLE: [ type_enum.CLOSEUNKOWN, color_enum.WHITE ]
        },
    }
    var preInfo = null;               // 用于记录前一次tick数据
    var feed = function(info) {       // 函数实现主要的功能,反推逐笔交易信息,传入的参数info为tick数据
        if (!preInfo) {               // 如果第一次执行feed,没有preInfo则使用当前info赋值给preInfo后(闭包:preInfo不会被释放),直接返回
            preInfo = info;           
            return null;
        }
        var volume_delta = info.Volume - preInfo.Volume;                         // 反推算法主要依赖于以下两个数据,前后两次tick数据的成交量变化值:volume_delta
        var open_interest_delta = info.OpenInterest - preInfo.OpenInterest;      // 前后两次tick数据的持仓量变化值:open_interest_delta

        var delta_forward = 'delta_enum_UNKOWN'                                  // 初始为未知状态
        
        // 以下这组if判断涵盖了正常情况,一种异常状态就是volume_delta小于0,通常来说不可能,一个交易日内成交量是一个递增的量,如果出现归于delta_enum_UNKOWN处理
        if (open_interest_delta == 0 && volume_delta == 0) {                     // 持仓量和成交量都没有变动,正常来讲成交量没有变动,持仓量也可定不变,所以就是没有任何新的成交
            delta_forward = 'delta_enum_NONE'
        } else if(open_interest_delta == 0 && volume_delta > 0) {                // 持仓量没有变动,成交量增加
            // 说明有人开仓,有人平仓,开仓平仓的合约数量相等,根据后续对盘口价格变动的判断,价格推高表示开仓多头主动,价格下降表示开仓空头主动,
            // 持仓量未变,说明有同样数量的平仓单,此时可能多头换手,空头换手都存在。
            delta_forward = 'delta_enum_EXCHANGE'
        } else if (open_interest_delta > 0) {                                    // 持仓量增加
            if (open_interest_delta - volume_delta == 0) {                       // 持仓量增加的情况下,持仓量变动和成交量变动相同(成交量也是增加的)
                // 说明成交量变动,新增成交的这部分都是开仓,没有平仓。例如:多头开仓和空头开仓成交1张,增加1张的持仓量
                delta_forward = 'delta_enum_OPENFWDOUBLE'
            } else {                                                             // 持仓量增加的情况下,持仓量变动和成交量变动不同
                // 说明有开仓,可能有平仓,有换手,总之持仓量是增加的,有新的资金入场,判定为“多开”还是“空开”等,根据之后的盘口变动检测而定
                delta_forward = 'delta_enum_OPEN'
            }
        } else if (open_interest_delta < 0) {                                    // 持仓量下降
            if (open_interest_delta + volume_delta == 0) {                       // 持仓量下降的情况下,持仓量和成交量变动相同
                // 说明成交量变动,新增成交的这部分都是平仓,没有开仓,双平。
                delta_forward = 'delta_enum_CLOSEFWDOUBLE'
            } else {                                                             // 持仓量下降的情况下,持仓量和成交量变动不同
                // 说明有平仓,可能有开仓,有换手,总之持仓量是减少的,有资金离场,判定为“空平”还是“多平”等,根据之后的盘口变动检测而定
                delta_forward = 'delta_enum_CLOSE'
            }
        }
        var obj = tick_dict[delta_forward];                                      // 找到对应的初步判定类型
        var ret = null;
        if (typeof(obj) !== 'undefined') {                                       // 根据价格变动进一步分析处理
            var order_forward = '';
            if (info.Last >= preInfo.Sell) {                                     // 最新成交价较上一次tick相比,大于等于上一个tick的卖一,判定为价格上涨
                order_forward = 'forward_enum_UP';
            } else if (info.Last <= preInfo.Buy) {                               // ...判定为价格下跌
                order_forward = 'forward_enum_DOWN';
            } else {                                                             // 如果盘面盘口较大,最新成交价停留在盘口中间的某个位置
                if (info.Last >= info.Sell) {                                    // 和当前tick的盘口卖一价格做比较,大于等于当前卖一,判定为价格上涨
                    order_forward = 'forward_enum_UP';
                } else if (info.Last <= info.Buy) {                              // ...判定为价格下跌
                    order_forward = 'forward_enum_DOWN';
                } else {
                    order_forward = 'forward_enum_MIDDLE';                       // 中间位置,这种表示无法判断出此次tick变动,推算出的逐笔成交主动交易的方向
                }
            }
            if (order_forward != '') {
                var d = obj[order_forward];
                if (typeof(d) !== 'undefined') {
                    ret = [info.Last, volume_delta, d[0], d[1]]  // 此次tick前后对比得出的逐笔成交数据,[最新成交价, 成交量变动, 成交类型(多开, 双平 ...), 颜色]
                }
            }
        }
        preInfo = info;
        return ret;
    }
    return {
        feed: feed,
        reset: function() {
            preInfo = null;
        },
    }
}

这段代码主要通过前后两次tick的对比,算出:1、成交量变动,2、持仓量变动。然后根据这两个数据推算出此次tick变动的综合动作:

  • 空闲,没有任何成交
  • 换手
  • 增仓
  • 减仓

再根据后续对盘口价格变动的分析,判断出此次推算的逐笔成交信息中主动交易的方向。例如此次是“换手”,根据盘口价格变动继续判断出是“多换”(多头主动)还是“空换”(空头主动)。

从另一个角度去实践验证

这里引申出另一个问题,在写策略时经常有需求要计算成交金额,但是通常我们只有成交量。这个成交金额怎么获取呢?

有了以上代码推算出的逐笔成交,不就可以计算出成交金额了吗?只需要累计这个量就可以了。

当然,CTP协议也给我们提供了充足的数据,也可以直接计算出成交金额。只是我们平时不太在意CTP协议的tick行情数据中的AveragePrice属性,AveragePrice表示持续平均计算得出的成交均价。需要注意的是这个数值是没有除以合约乘数的,例如合约是rb2305,那么AveragePrice表示的是10吨的均价。

成交金额 = AveragePrice / VolumeMultiple * Volume

我们就可以使用这两种方式计算成交金额来进行对比,测试代码如下:

function main() {
    SetErrorFilter("market not ready|not login")
    while (true) {
        if (exchange.IO("status")) {
            LogStatus(_D(), "已连接")
            break
        } else {
            LogStatus(_D(), "未连接")
            Sleep(1000)
        }
    }
    var info = _C(exchange.SetContractType, "rb2305");    // 使用螺纹钢2305合约测试
    var filt = NewFuturesTradeFilter();                   // 创建用来推算逐笔交易的对象
    var firstTicker = _C(exchange.GetTicker)              // 获取首个tick行情 
    Log(firstTicker);
    // AveragePrice Volume
    var initTransactionAmount = firstTicker.Info.AveragePrice / info.VolumeMultiple * firstTicker.Info.Volume  // 计算初始成交金额
    var initVolume = firstTicker.Info.Volume   // 记录初始成交量
    
    var sum = 0;          // 用于累计逐笔成交信息的总成交金额
    var volume = 0;       // 用于累计逐笔成交信息的总成交量
    var t = firstTicker
    while (true) {
        if (t) {
            var ret = filt.feed(t);   // 使用filt对象的feed函数推算出逐笔成交信息
            if (ret) {
                Log("Price:", ret[0], "Amount:", ret[1], _T(ret[2]), ret[3]);
                sum += ret[0] * ret[1]    // 此次逐笔成交信息,价格乘以数量算出金额,累计成交金额
                volume += ret[1]          // 累计成交量
            }
            
            // 计算当前时刻和初始时刻的 AveragePrice / VolumeMultiple * Volume 差值,以及 Volume差值
            LogStatus(_D(), "tick:", t, "\n", "sum:", sum, "volume:", volume, "\n", 
                "transactionAmount:", t.Info.AveragePrice / info.VolumeMultiple * t.Info.Volume - initTransactionAmount, "transactionVolume:", t.Info.Volume - initVolume);
        } else {
            Sleep(100)
        }
        t = exchange.GetTicker();   // 每次循环更新tick
    }
}

测试

使用以上测试代码,实盘测试:

img

img

可以发现成交量两种统计方式算出的数值是一致的,成交金额有一点点差别(误差原因:1、可能是tick数据中的AveragePrice即成交均价的数据精度引起的误差。2、两次tick之间成交有可能有很多小幅度价格变动,最新成交价可能和实际的两次tick之间的交易成交均价有差别,毕竟tick数据是切片数据)。不过成交金额差别不算大,基本是一致的。


更多内容