天天看點

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

文章目錄

      • 一、摘要
      • 二、OrderFlow訂單流簡介
      • 三、OrderFlow訂單流原理
      • 四、訂單流中的Delta結構資料
      • 五、政策實作
      • 六、實盤運作
      • 七、總結

一、摘要

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

二、OrderFlow訂單流簡介

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

1、市場深度(L2資料)

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

2、成交量分布(VP)

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

3、足迹圖(Footprint Chart)

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

4、成交明細(Sales Details)

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

三、OrderFlow訂單流原理

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

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

四、訂單流中的Delta結構資料

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

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

在上面的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.com/strategy/291843

六、實盤運作

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

七、總結

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

繼續閱讀