文章目錄
-
-
- 一、摘要
- 二、OrderFlow訂單流簡介
- 三、OrderFlow訂單流原理
- 四、訂單流中的Delta結構資料
- 五、政策實作
- 六、實盤運作
- 七、總結
-
一、摘要
在電子交易興起之前,要想了解成交量是如何在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.com/strategy/291843
六、實盤運作
七、總結
本篇詳細介紹了OrderFlow訂單流政策基礎知識,以及利用發明者量化交易平台,實作了一個足迹圖(Footprint Chart)政策,該政策可以直接用于商品期貨實盤賬戶。在接下來的章節中,我們将深入研究OrderFlow訂單流資料,進而開發一系列基于OrderFlow訂單流資料的交易政策。