1. 程式人生 > 其它 >CTA策略之orderflow訂單流策略(2)

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

一、摘要

在上一個章節中,我們初步認識了OrderFlow訂單流以及其分類,大概包括市場深度資料、成交量分佈(VP)、足跡圖(Footprint Chart)、成交明細(Sales Details)等等,並利用程式碼實現一個足跡圖(Footprint Chart)K線圖表,這對於瞭解OrderFlow訂單流資料結構和原理很有幫助。本章節我們將繼續探索OrderFlow訂單流,以足跡圖(Footprint Chart)為主,開發一系列的交易策略。

二、訂單流資料分類

在交易中,可以利用的訂單流資料大致可以分為兩類,一類是未成交的訂單流(市場深度資料),另一類是已成交的訂單流(Footprint足跡圖,也稱為成交痕跡)。而成交量分佈(VP)和成交明細(Sales Details)嚴格來說並不算是訂單流的應用。

其中市場深度資料是市場中尚未成交的訂單流資料,這些資料就是各交易軟體中常見的五檔行情(如上圖所示)。做過交易的都知道,這些資料通常變化無常,有時候突然來一個大單,又突然憑空消失,存在很強的欺騙和誘導作用。對於散戶來說,很難從中汲取出有效的規律,所以本系列教程主要以足跡圖(Footprint Chart)為主。

足跡圖(Footprint Chart)展示了已經成交的訂單流資料,相比市場深度L2資料而言,其資料是客觀的存在,真實可靠。足跡圖(Footprint Chart)是由Tick資料換算而成,實時記錄市場買賣方的每個tick訂單,可以讓交易者及時看到市場微觀結構,比如:價格變化、訂單型別、流動性,從而輔助交易決策。

三、足跡圖的微觀構成


足跡圖(Footprint Chart)是基於真實的Tick行情來計算,詳細的資料都附加在K線上,當滑鼠懸停在K線上時,即可呈現量能足跡資料(如上圖所示)。方塊中的資料就是其計算結果,總共分為兩列,箭頭左邊一列是當前K線所有的價格點位,依次由大到小向上排列。

箭頭右邊一列就是每個價格水平的交易量,細分為買入交易量和賣出交易量,並用使用分隔符“▲、▼、♦”分隔。在分隔符的左邊是主動賣出的成交量,在分隔符的右邊就是主動買入的成交量。當某個價位主動買入量大於主動賣出量,分隔符為“▲”,表示向上;當某個價位主動買入量小於主動賣出量,分隔符為“▼”,表示向下;當某個價位主動買入量等於主動賣出量,分隔符為“♦”,表示相等;

最後在最上方是所有買入和賣出的成交量之和,並以“◉”表示;在最下方則是主動買入量與主動賣出量的差,以“⊗”表示。這樣可以很直觀的看出K線的整體成交量和多頭與空頭的力量懸殊對比。

四、買賣均衡與價格背離

影響價格的漲跌有很多種因素,包括:供求關係、經濟週期、政府政策、政治因素、社會因素、季節性因素、市場情緒、外匯政策等等...但這些因素最終都要落實到交易中,也就是買方和賣方。理論上當買方成交量大於賣方成交量,價格就會上漲;當賣方成交量大於買方成交量,價格就會下跌。

也就是說成交量是先行於價格的,買賣雙方成交量的多少是原因,價格的變化是結果。如果賣方成交量大於買方成交量,理論上價格應該下跌,但實際上價格卻是上漲,那麼此時買賣力量均衡與價格產生了背離;如果買方成交量大於買方成交量,理論上價格應該上漲,但實際上價格卻是下跌,那麼此時買賣力量均衡與價格產生了背離;

五、策略邏輯

通過觀察發現,量增價漲是一種常態,大部分K線都保持這種規律,而買賣均衡與價格背離卻是一種偶然。接下來我們利用買賣均衡與價格背離這種現象,看能不能發現交易的祕密。以下是策略邏輯:

  • 多頭開倉:如果當前無持倉,並且收盤價大於開盤價,並且主動買量小於主動賣量
  • 空頭開倉:如果當前無持倉,並且收盤價小於開盤價,並且主動買量大於主動賣量
  • 多頭平倉:如果有多頭持倉,並且利潤超過100
  • 空頭平倉:如果有空頭持倉,並且利潤超過100

六、策略回測

  • 回測開始日期:2021-06-01
  • 回測結束日期:2021-07-01
  • 資料品種:螺紋鋼主力連續
  • 資料週期:一分鐘
  • 滑點:開平倉各2跳

回測配置

回測績效

收益概覽

七、策略實現

/*backtest
start: 2021-06-01 00:00:00
end: 2021-07-01 23:59:00
period: 1h
basePeriod: 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 = []
    arr = []
    lastTime = 0
    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
            )
            arr.push({
                'open': bar.open,
                'close': bar.close,
                'diff': initiativeBuy - initiativeSell
            })
            if (arr.length > 2) {
                arr.shift()
            }
            let position = exchange.GetPosition()
            let holdAmount = 0
            let profit = 0
            if (position.length > 0) {
                if (position[0].Type == 0 || position[0].Type == 2) {
                    holdAmount = position[0].Amount
                } else {
                    holdAmount = -position[0].Amount
                }
                profit = position[0].Profit
            }
            if (bar.time != lastTime) {
                lastOpen = arr[0].open
                lastClose = arr[0].close
                diff = arr[0].diff
                lastTime = bar.time
                priceDiff = lastClose - lastOpen
                volDiff = diff
                if (holdAmount == 0 && priceDiff > 0 && volDiff < 0) {
                    Log('多開')
                    exchange.SetDirection("buy")
                    exchange.Buy(arr[1].close, 1)
                }
                if (holdAmount == 0 && priceDiff < 0 && volDiff > 0) {
                    Log('空開')
                    exchange.SetDirection("sell")
                    exchange.Sell(arr[1].close - 1, 1)
                }
                if (holdAmount > 0 && profit > 100) {
                    Log('多平')
                    exchange.SetDirection("closebuy")
                    exchange.Sell(arr[1].close - 1, holdAmount)
                }
                if (holdAmount < 0 && profit > 100) {
                    Log('空平')
                    exchange.SetDirection("closesell")
                    exchange.Buy(arr[1].close, -holdAmount)
                }
            }
            
        }
        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/299037