商品期货「订单流」系列文章(三):供需失衡和堆积带

在上一篇文章中,我们深入探讨了订单流的Delta指标,这是一个宏观的汇总指标,通过统计在特定周期内累积的买单与卖单的总量,来衡量市场的多空力量对比。然而,除了从宏观角度审视市场动态,我们还可以通过微观分析来洞察每笔订单流中的主动买盘和主动卖盘,进而揭示供需关系的微妙变化。这种细致的分析有助于我们捕捉市场趋势转变的早期信号,为市场趋势的量化预测提供更为精准的视角。

今天,我们要介绍的是一种新的分析工具,它专注于衡量订单流中主动买量与主动卖量之间的供需失衡和相应的堆积带。这个指标旨在通过深入分析订单流的微观结构,帮助我们更全面地理解市场动态,从而对市场趋势做出更加科学和系统的预测。通过识别“供需失衡”的模式和“堆积带”,我们可以更敏锐地感知市场转折点,为交易决策提供有力的数据支持。

供需关系

在本系列第一篇文章中,我们介绍了订单流数据的计算原理,这次我们来尝试分析一张订单流数据中的供需关系。订单流左边是主动卖量,右边是主动买量(前两篇文章我们将主动买量设置在左边,是为了方便计算Delta值)。通过对比两者可以反映出多空双方的强弱关系。接下来,我们细化各个价格点位的数据,统计一下供需失衡状况(包括供应失衡和需求失衡)。

供应失衡

“失衡”在一定程度上意味着过量。在经济学中,当供应过量的时候,价格会逐步降低,因此交易者会倾向于快速抛出自己已经拥有的**,以防止遭受更大的损失。

在订单流图表中,我们使用较低档位的主动卖量(左下)和高一档位的主动买量(右上)数据进行比较。如果两者的比值大于N:1(可调参数,默认为3),证明空头力量比较强势,因此我们可以定义为供应失衡。如上图中绿色所示,第二行数据左边数值26,大于第一行右边数值7的三倍(26/7 > 3),因此我们可以定义为供应(空方)失衡。

需求失衡

在理解供应失衡的基础上,需求失衡的概念也并不复杂。需求失衡意味着商品需求的数量大于供给的数量,也就是通常意义的“供不应求”,因此价格会呈现逐步上涨的趋势,呈现“卖方市场”。

相对于供应失衡,需求失衡只需要将其分子分母进行一下转换,使用较高档位的买方数据(右上)除以低一档位的卖方数据(左下)。同样的,如果这个数值超过3,我们可以定义为需求失衡。如上图中所示,倒数第二行买方数据为23,除以倒数第一行卖方数字7,获得的商大于3,可以定义为需求(买方)失衡。

堆积带

在理解“需求失衡”和“供应失衡”的基础上,我们可以扩展延伸这个概念。当在一个订单流中,出现了连续N(可调参数,默认为3)个价位的失衡现象,我们认为在这一价格区间内,连续出现了需求失衡或者供应失衡,我们可以定义为“失衡堆积”的情况。在我们的理解中,也就是连续的大单压制,可以被视作为“阻力带(供应失衡)”或者“支撑带”(需求失衡)。

如下图所示,订单流中间位置,连续三个供应失衡,可以被视作“堆积带”。

“堆积带”不仅可以帮助我们判断“阻力带”或者“支撑带”的位置,也可以作为多空开仓的信号。当然使用肉眼判断确实比较困难,下面我们将在优宽量化上使用代码进行呈现。

示范策略

本策略源码中,结合了第一篇文章关于订单流数据的计算逻辑,不过这里的重点在于“需求失衡”以及“交易信号”的判断,我们大致介绍一下。

策略思路简述:

  1. 供应失衡和需求失衡的判断

    • 供应失衡:当某个价格的卖出量(sell)大于前一个价格的买入量(buy)的n倍(nMul)时,则标记为供应失衡。
    • 需求失衡:当某个价格的买入量(buy)大于后一个价格的卖出量(sell)的n倍(nMul)时,则标记为需求失衡。
  2. 堆积带(Zone)的判断

    • 通过统计连续失衡的次数(supplyImbalanceCount),如果达到固定的次数(nCount),出现连续的供应失衡,标记该区域为“阻力带(供应失衡)”,在同时设置shortSignal(开空信号)为true
    • 如果出现连续的需求失衡(demandImbalanceCount > nCount),标记该区域为“支撑带(需求失衡)”,设置longSignal(开多信号)为true
  3. 做多和做空信号的判断

    • 做多信号:当检测到支撑带(需求失衡)时,设置longSignaltrue,表示产生做多信号。
    • 做空信号:当检测到阻力带(供应失衡)时,设置shortSignaltrue,表示产生做空信号。

这些判断是依据最新的市场数据和交易量的变化,自动生成交易信号并执行相应的交易操作。

/*backtest
start: 2024-07-25 09:00:00
end: 2024-07-31 23:00:00
period: 1m
basePeriod: 1m
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}]
mode: 1
args: [["contract","SA888"],["stopWin",10],["stopLoss",10]]
*/

var longSignal = false
var shortSignal = false

var NewFuturesTradeFilter = function(period) {
    var self = {} // 创建一个对象

    self.c = Chart({ // 创建Chart图表
        chart: {
            zoomType: 'x', // 缩放
            backgroundColor: '#272822',
            borderRadius: 5,
            panKey: 'shift',
            animation: false,
        },
        plotOptions: {
            candlestick: {
                color: '#00F0F0',
                lineColor: '#00F0F0',
                upColor: '#272822',
                upLineColor: '#FF3C3C'
            },
        },
        tooltip: {
            xDateFormat: '%Y-%m-%d %H:%M:%S, %A',
            pointFormat: '{point.tips}',
            borderColor: 'rgb(58, 68, 83)',
            borderRadius: 0,
        },
        series: [{
            name: exchange.GetName(),
            type: 'candlestick',
            data: []
        }],
        yAxis: {
            gridLineColor: 'red',
            gridLineDashStyle: 'Dot',
            labels: {
                style: {
                    color: 'rgb(204, 214, 235)'
                }
            }
        },
        rangeSelector: {
            enabled: false
        },
        navigation: {
            buttonOptions: {
                height: 28,
                width: 33,
                symbolSize: 18,
                symbolX: 17,
                symbolY: 14,
                symbolStrokeWidth: 2,
            }
        }
    })
    self.c.reset() // 清空图表数据

    self.pre = null // 用于记录上一个数据
    self.records = []
    longSignal = false
    shortSignal = false
    self.feed = function(ticker, rData, contractCode) {
        if (!self.pre) { // 如果上一个数据不为真
            self.pre = ticker // 赋值为最新数据
        }
        var action = '' // 标记为空字符串
        if (ticker.Last >= self.pre.Sell) { // 如果最新数据的最后价格大于等于上一个数据的卖价
            action = 'buy' // 标记为buy
        } else if (ticker.Last <= self.pre.Buy) { // 如果最新数据的最后价格小于等于上一个数据的买价
            action = 'sell' // 标记为sell
        } else {
            if (ticker.Last >= ticker.Sell) { // 如果最新数据的最后价格大于等于最新数据的卖价
                action = 'buy' // 标记为buy
            } else if (ticker.Last <= ticker.Buy) { // 如果最新数据的最后价格小于等于最新数据的买价
                action = 'sell' // 标记为sell
            } else {
                action = 'both' // 标记为both
            }
        }

        // reset volume
        if (ticker.Volume < self.pre.Volume) { // 如果最新数据的成交量小于上一个数据的成交量
            self.pre.Volume = 0 // 把上一个数据的成交量赋值为0
            Log('更新交易日#ff0000')
        }
        var amount = ticker.Volume - self.pre.Volume // 最新数据的成交量减去上一个数据的成交量
        
        if (action != '' && amount > 0) { // 如果标记不为空字符串,并且action大于0
            var epoch = parseInt(ticker.Time / period) * period // 计算K线时间戳并取整


            var bar = null
            var pos = undefined
            if (
                self.records.length == 0 || // 如果K线长度为0或者最后一根K线时间戳小于epoch
                self.records[self.records.length - 1].time < epoch
            ) {
                if(self.records.length > 0){
                    var curBar = self.records[self.records.length - 1].data

                    // 将data对象转换为数组并排序
                    var dataArray = Object.keys(curBar).map(function(price) {
                        return {
                            price: parseInt(price),
                            sell: curBar[price].sell,
                            buy: curBar[price].buy
                        };
                    }).sort(function(a, b) {
                        return b.price - a.price; // 从大到小排序
                    });

                    // 定义变量
                    var fromV = null;
                    var endV = null;
                    var zone = '';
                    var supplyImbalanceCount = 0;
                    var demandImbalanceCount = 0;

                    // 遍历数组并进行判断
                    for (var i = 1; i < dataArray.length; i++) {
                        var currentRow = dataArray[i];
                        var previousRow = dataArray[i - 1];

                        if (currentRow.sell > previousRow.buy * nMul) {
                            //Log('价格(price ' + currentRow.price + ')出现供应失衡');
                            supplyImbalanceCount++;
                            demandImbalanceCount = 0; // 重置需求失衡计数器
                            if (supplyImbalanceCount == 1) {
                                fromV = currentRow.price;
                            }
                            endV = previousRow.price - 1;
                        } else if (previousRow.buy > currentRow.sell * nMul) {
                            //Log('价格(price ' + previousRow.price + ')出现需求失衡');
                            demandImbalanceCount++;
                            supplyImbalanceCount = 0; // 重置供应失衡计数器
                            if (demandImbalanceCount == 1) {
                                fromV = previousRow.price;
                            }
                            endV = currentRow.price + 1;
                        } else {
                            supplyImbalanceCount = 0; // 重置供应失衡计数器
                            demandImbalanceCount = 0; // 重置需求失衡计数器
                        }

                        if (supplyImbalanceCount >= nCount) {
                            zone = '阻力带(供应失衡)';
                            shortSignal = true
                            break;
                        } else if (demandImbalanceCount >= nCount) {
                            zone = '支撑带(需求失衡)';
                            longSignal = true
                            break;
                        } else{
                            longSignal = false
                            shortSignal = false
                        }
                    }

                    var color = zone ? zone == '阻力带(供应失衡)' ? '#00ff00' : '#ff0000' : null

                    if (zone) {
                        Log(endV + ' 到 ' + fromV + ' 的区域是 ' + zone + color);
                        
                    }
                }
                
                bar = {
                    time: epoch,
                    data: {},
                    open: ticker.Last,
                    high: ticker.Last,
                    low: ticker.Last,
                    close: ticker.Last
                } // 把最新的数据赋值给bar
                self.records.push(bar) // 把bar添加到records数组中
            } else { // 重新给bar赋值
                bar = self.records[self.records.length - 1] // 上一个数据最后一根K线
                bar.high = Math.max(bar.high, ticker.Last) // 上一个数据最后一根K线的最高价与最新数据最后价格的最大值
                bar.low = Math.min(bar.low, ticker.Last) // 上一个数据最后一根K线的最低价与最新数据最后价格的最小值
                bar.close = ticker.Last // 最新数据的最后价格
                pos = -1
            }
            if (typeof bar.data[ticker.Last] === 'undefined') { // 如果数据为空
                bar.data[ticker.Last] = { // 重新赋值
                    buy: 0,
                    sell: 0
                }
            }
            if (action == 'both') { // 如果标记等于both
                bar.data[ticker.Last]['buy'] += amount // buy累加
                bar.data[ticker.Last]['sell'] += amount // sell累加
            } else {
                bar.data[ticker.Last][action] += amount // 标记累加
            }
            
            var initiativeBuy = 0
            var initiativeSell = 0
            var sellLongMax = 0
            var buyLongMax = 0
            var sellVol = 0
            var buyVol = 0
            for (var i in bar.data) {
                sellLong = bar.data[i].sell.toString().length
                buyLong = bar.data[i].buy.toString().length
                if (sellLong > sellLongMax) {
                    sellLongMax = sellLong
                }
                if (buyLong > buyLongMax) {
                    buyLongMax = buyLong
                }
                sellVol += bar.data[i].sell
                buyVol += bar.data[i].buy
            }

            tips = '<b>◉ ' + (sellVol + buyVol) + '</b>'
            Object.keys(bar.data) // 将对象里的键放到一个数组中
                .sort() // 排序
                .reverse() // 颠倒数组中的顺序
                .forEach(function(p) { // 遍历数组
                    pSell = bar.data[p].sell
                    pBuy = bar.data[p].buy
                    if (pSell > pBuy) {
                        arrow = ' ▼ '
                    } else if (pSell < pBuy) {
                        arrow = ' ▲ '
                    } else {
                        arrow = ' ♦ '
                    }
                    initiativeSell += pSell
                    initiativeBuy += pBuy
                    sellLongDiff = sellLongMax - pSell.toString().length
                    buyLongDiff = buyLongMax - pBuy.toString().length
                    if (sellLongDiff == 1) {
                        pSell = '0' + pSell
                    }
                    if (sellLongDiff == 2) {
                        pSell = '00' + pSell
                    }
                    if (sellLongDiff == 3) {
                        pSell = '000' + pSell
                    }
                    if (sellLongDiff == 4) {
                        pSell = '0000' + pSell
                    }
                    if (sellLongDiff == 5) {
                        pSell = '00000' + pSell
                    }
                    if (buyLongDiff == 1) {
                        pBuy = '0' + pBuy
                    }
                    if (buyLongDiff == 2) {
                        pBuy = '00' + pBuy
                    }
                    if (buyLongDiff == 3) {
                        pBuy = '000' + pBuy
                    }
                    if (buyLongDiff == 4) {
                        pBuy = '0000' + pBuy
                    }
                    if (buyLongDiff == 5) {
                        pBuy = '00000' + pBuy
                    }

                    code = contractCode.match(/[a-zA-Z]+|[0-9]+/g)[0]
                    if (code == 'IF' || code == 'j' || code == 'IC' || code == 'i' || code == 'ZC' || code == 'sc' || code == 'IH' || code == 'jm' || code == 'fb') {
                        p = parseFloat(p).toFixed(1)
                    } else if (code == 'au') {
                        p = parseFloat(p).toFixed(2)
                    } else if (code == 'T' || code == 'TF' || code == 'TS') {
                        p = parseFloat(p).toFixed(3)
                    } else {
                        p = parseInt(p)
                    }
                    tips += '<br>' + p + ' → ' + pSell + arrow + pBuy

                })
            tips += '<br>' + '<b>⊗ ' + (initiativeBuy - initiativeSell) + '</b>'
           
            self.c.add( // 添加数据
                0, {
                    x: bar.time,
                    open: bar.open,
                    high: bar.high,
                    low: bar.low,
                    close: bar.close,
                    tips: tips
                },
                pos
            )

        }
        self.pre = ticker // 重新赋值
    }
    return self // 返回对象
}


function main() {
    if (exchange.GetName().indexOf('CTP') == -1) {
        throw "只支持商品期货CTP";
    }
    SetErrorFilter("login|timeout|GetTicker|ready|流控|连接失败|初始|Timeout");
    while (!exchange.IO("status")) {
        Sleep(3000);
        LogStatus("正在等待与交易服务器连接, " + _D());
    }
    symbolDetail = _C(exchange.SetContractType, contract) // 订阅数据
    Log('交割日期:', symbolDetail['StartDelivDate'])
    Log('最小下单量:', symbolDetail['MaxLimitOrderVolume'])
    Log('最小价差:', symbolDetail['PriceTick'])
    Log('一手:', symbolDetail["VolumeMultiple"], '份')
    Log('合约代码:', symbolDetail['InstrumentID'])
    var filt = NewFuturesTradeFilter(60000) // 创建一个对象

    $.CTA(contract, function(st) {
        var ticker = exchange.GetTicker();
        var r = exchange.GetRecords();
        var priceTick = exchange.SetContractType(contract).PriceTick;

        if (ticker) {
            filt.feed(ticker, r, contract);

            if (st.position.amount === 0 && longSignal) {
                Log('多头开仓#ff0000');
                return 1;
            }

            if (st.position.amount > 0 && r[r.length - 1].Close - st.position.price >= stopWin * priceTick) {
                Log('多头盈利平仓#ff0000');
                return -1;
            }

            if (st.position.amount > 0 && (r[r.length - 1].Close - st.position.price < -stopLoss * priceTick )) {
                Log('多头亏损平仓#ff0000');
                return -1;
            }

            if (st.position.amount === 0 && shortSignal) {
                Log('空头开仓#0000ff');
                return -1;
            }

            if (st.position.amount < 0 && r[r.length - 1].Close - st.position.price <= -stopWin * priceTick) {
                Log('空头盈利平仓#0000ff');
                return 1;
            }

            if (st.position.amount < 0 && (r[r.length - 1].Close - st.position.price > stopLoss * priceTick )) {
                Log('空头亏损平仓#0000ff');
                return 1;
            }
        }
    });
}

通过查看交易日志,可以看到在鉴别到相应的“堆积带”时,会进行对应的多空交易操作。

经过测试,针对于纯碱期货品种,策略回测结果取得了良好的收益,大家可根据自己喜欢的品种,进行策略参数的调试和策略逻辑的修改。

转载自:优宽量化交易平台社区

作者:ianzeng123

 

 

免责声明:信息仅供参考,不构成投资及交易建议。投资者据此操作,风险自担。
如果觉得文章对你有用,请随意赞赏收藏
相关推荐
相关下载
登录后评论
Copyright © 2019 宽客在线