CTA策略之orderflow訂單流策略(1)


一、摘要

在電子交易興起之前,要想了解成交量是如何在K線上分配的是一件很難的事情。如今科技的發展給帶給我們一種前所未有的市場分析方式,大部分軟件都已經支持以Order Book方式向投資者提供價格和成交量數據,以便洞悉價格上漲或下跌背后的原因。本篇作為CTA策略之OrderFlow訂單流策略系列文章的第一篇,主要詳細介紹OrderFlow訂單流。

二、OrderFlow訂單流簡介

在二級交易市場種,影響價格變化的因素是紛繁復雜的,並且每一個因素影響價格變化的權重都不一樣,以至於很難從傳統技術分析圖形中推導出價格行為,因為相對於價格和成交量來說,技術分析圖形相對抽象和滯后。而OrderFlow訂單流工具的橫空出世,使得市場更加通透。OrderFlow訂單流有很多種分類,包括:市場深度(L2數據)、成交量分布(VP)、足跡圖(Footprint Chart)、成交明細(Sales Details)等等,如下圖所示:

1、市場深度(L2數據)

2、成交量分布(VP)

3、足跡圖(Footprint Chart)

4、成交明細(Sales Details)

三、OrderFlow訂單流原理

市場充斥着各種各樣的信息,好的壞的、真的假的,這些信息就像荊棘一樣錯縱交織,導致這些消息很難被理解,很難用正確的邏輯推導,比如利好消息出來,但價格卻沒有很大變動;再比如價格往往在一片質疑中走勢波瀾壯闊的上漲行情;牛市往往在大家都看多時結束等等。所以我們需要站在巨人的肩膀上去分析這個市場,披荊斬棘從中抽取出價格波動的真正原因。

二級交易市場由投資自組成,不管投資者使用的是基本面分析、技術分析、消息面,每一個投資者在交易時,其實都是在為自己的觀點投票,所以我們看到的價格波動,實際上是投資者共同角逐的結果,如果多頭和空頭勢均力敵,雙方力量相等,不分高低,那么價格將會不漲不跌;如果多頭力量大於空頭力量,那么價格將會上漲;如果空頭力量大於多頭力量,那么價格將會下跌。如果把多頭看做是買方,把空頭看做是賣方,那么多頭和空頭供需失衡是導致價格波動的主要原因,而這一切都可以從多頭和空頭的成交量中窺得其中的蛛絲馬跡。

四、訂單流中的Delta結構數據

傳統的日本蠟燭圖(K線)有開盤價、最高價、最低價、收盤價等四個價格,K線僅僅代表這個時間段內的價格變化情況,比如小時線代表了一個小時內的價格變化情況。而Delta結構數據則是根據Tick數據,提供了K線時間段內發生的具體細節,包括K線每個價格的多頭和空頭成交量,可以很清晰的看見多空成交的訂單。如下圖所示:

在上面的Delta結構數據中,每一根K線都有一個獨立的Delta結構數據,在Delta結構數據方框中,最上方是這根K線總的成交量,最下方是這根K線所有多頭成交量和空頭成交量的差,中間則是這根K線每個價格多頭成交量和空頭成交量數據。如你所見Delta結構數據將K線拆分成更詳細的可視化數據,從而幫助投資者理解價格變動的機制。

五、策略實現

/*backtest
start: 2020-03-10 00:00:00
end: 2020-03-10 23:59:00
period: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}]
mode: 1
*/


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 = []
    self.feed = function(ticker, 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
        }
        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
            ) {
                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
            }
            // var date = new Date(bar.time);
            // var Y = date.getFullYear() + '-';
            // var M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-';
            // var D = (date.getDate() < 10 ? '0' + date.getDate() : date.getDate()) + ' ';
            // var h = (date.getHours() < 10 ? '0' + date.getHours() : date.getHours()) + ':';
            // var m = (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()) + '<br>';
            // var tips = Y + M + D + h + m
            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, contractCode) // 訂閱數據
    Log('交割日期:', symbolDetail['StartDelivDate'])
    Log('最小下單量:', symbolDetail['MaxLimitOrderVolume'])
    Log('最小價差:', symbolDetail['PriceTick'])
    Log('一手:', symbolDetail["VolumeMultiple"], '份')
    Log('合約代碼:', symbolDetail['InstrumentID'])
    var filt = NewFuturesTradeFilter(60000) // 創建一個對象
    while (true) { // 進入循環模式
        while (!exchange.IO("status")) {
            Sleep(3000);
            LogStatus("正在等待與交易服務器連接, " + _D());
        }
        LogStatus("行情和交易服務器連接成功, " + _D());
        var ticker = exchange.GetTicker() // 獲取交易所Tick數據
        if (ticker) { // 如果成功獲取到Tick數據
            filt.feed(ticker, contractCode) // 開始處理數據
        }
    }
}

上面附上完整策略代碼以及實盤配置,也可以點擊下方鏈接復制完整策略代碼:

https://www.fmz.cn/strategy/291843

六、實盤運行

七、總結

本篇詳細介紹了OrderFlow訂單流策略基礎知識,以及利用發明者量化交易平台,實現了一個足跡圖(Footprint Chart)策略,該策略可以直接用於商品期貨實盤賬戶。在接下來的章節中,我們將深入研究OrderFlow訂單流數據,進而開發一系列基於OrderFlow訂單流數據的交易策略。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM