相对于经典的技术指标使用较长时间内的历史K线数据进行计算,「订单流」更类似于一个商品期货的微观动态指标,它通过实时搜集的最小单位数据-tick数据,用来尝试模拟多头和空头,甚至资金和散户的动态博弈。因此,怎样从订单流提取出来合适有用的信息,从微观层面分析价格趋势的延续或者转折的原因,并预测价格的未来变化,是我们本系列课程的一个理论和实践目标。
图片来源:文华财经
其实前面的文章里,我们花了一系列的篇幅介绍了订单流的相关知识,但是具体其使用方法,我们涉及的很少。因此,我们将要使用一系列的篇幅,尝试完整系统的剖析中国商品期货中订单流的概念以及具体的使用方法。本系列课程第一篇,我们来从零开始,认识一下订单流的概念。
“价格的波动源自供求关系,而成交量则是供求关系的具体体现。大多数技术指标都滞后于价格,而订单流(Order Flow)则实时展示了市场的动态。订单流以最细节的tick数据为依据,可以深入分析单根K线内部的成交量,揭示价格在该区间内的具体反应。这是传统K线图无法提供的信息。订单流的优势在于它能够与任何已掌握的交易技术结合使用,帮助你在现有技术基础上更精准地分析市场现状,有效辨别震荡行情,并找到更理想的入场时机。”
是不是看起来有点懵,并且订单流数据在基本的盘面中并没有呈现啊?其实在金融市场中,订单流是一个推测出来的数据,它的原始来源于–tick数据。
在中国商品期货市场中,交易报单是撮合在单一交易所统一进行的,不包含逐笔成交数据和逐笔挂单撤单数据,信息发布时间对全市场公平。国内交易所发布订单薄数据的频率通常为每秒钟2笔,交易者无论利用什么软件,行情发送频率都是一定的。这种制度设计有助于确保市场的公平性和稳定性。在单一交易所进行交易报单撮合,可以减少市场分割和信息不对称,提高市场的透明度和公平性。同时,信息发布时间的统一也有助于减少市场操纵和信息泄露的可能性。
一条完整的Tick信息如下所示,其中比较关键的信息是买入价(Buy),卖出价(Sell),最后成交价(Last),以及Volume(成交量)和持仓量(OpenInterest):
{"Open":3371,
"High":3373,
"Low":3341,
"Sell":3362,
"Buy":3361,
"Last":3362,
"Volume":1053290,
"OpenInterest":2317594,
"Time":1722223799999}
Tick信息是汇总这一个tick时间段内的总体的交易数据,在这一段时间内,可能发生了很多笔交易,但是最后的交易信息汇总都在一个tick信息内,因此,我们将要尝试从这个信息中尽量推算模拟这段时间内的多方和空方的对决交易情况。
期货交易中的成交量、持仓量和价格变化是反映多空力量对决激烈程度的重要指标。成交量的变化反映了多头和空头之间的竞争情况,持仓量的变化反应了竞争结果,而价格变化则显示了多头和空头谁在掌控市场的主动权。通过观察成交量,持仓量和价格的变化,我们可以理解市场上多空双方的情况。
在期货交易中,我们将交易方向分为多头和空头,具体的交易行为则包括开仓和平仓。因此,涉及到四种情况:多头开仓、空头开仓、多头平仓和空头平仓。由于期货交易是对手盘交易,同一方向的交易行为不会形成对手盘。因此,我们需要简单了解这四种情况对价格和持仓的影响。
多头开仓:价格上涨 + 持仓增加
空头开仓:价格下跌 + 持仓增加
多头平仓:价格下跌 + 持仓减少
空头平仓:价格上涨 + 持仓减少
因此,从对手盘的角度来看,多头开仓和空头开仓可以形成对手盘。在交易软件中,双开常常表示这种情况。多头开仓和多头平仓也可以形成对手盘,这在交易软件中常常显示为多头换手。空头开仓除了与多头开仓形成对手盘,还可以与空头平仓形成对手盘,这在交易软件中显示为空头换手。此外,多头平仓和空头平仓也可以形成对手盘。简而言之:
多头开仓 vs 空头开仓:双开
多头开仓 vs 多头平仓:多头换手
空头开仓 vs 空头平仓:空头换手
多头平仓 vs 空头平仓:双平
这里可能会有一个疑问:既然期货是对手盘交易,多空必相等,为什么价格还会有涨跌呢?这个问题很好理解,正如之前所介绍的,这与买一价、卖一价和当前价的关系有关。当前价等于买一价时,说明空头主动寻找多头来成交,空头占据主动权;反之,当前价等于卖一价时,说明多头主动寻找空头来成交,多头占据主动权。
多头和空头作为对手盘,它们的行为主动性不同,因此导致了价格涨跌的差异。如果当前价持续接近买一价,那么空头占据主动权;如果当前价持续接近卖一价,那么多头占据主动权。除了价格变化外,我们还需要关注持仓量的变化。双开导致持仓量增加,双平导致持仓量减少,多空换手导致持仓量保持不变。
在了解价格,成交量和持仓量三者的互动关系后,我们就可以利用tick数据大致推算在某个价格上,具体多方和空方的主动成交量(注一)。具体的推算步骤如下:
确定时间范围
将数据划分到相应的时间段
初始化动作标记
比较最新价格与上一个数据的卖价和买价
处理动作标记为“买卖双方”的情况
处理动作标记为“买入”或“卖出”的情况
由于数据特殊的返回机制,盘口数据在瞬息万变中会发生减少(主动交易/撤单)或者增加(增加挂单)的情况,这其中需要考虑的条件过于复杂,所以我们对交易数据只能进行一个大概粗略的估计,大家可以根据自己的思考进行更好的建模,欢迎在评论区留言,我们共同讨论。
通过以上四步,我们就基本完成了订单流图表的基本的计算工作,后续我们将根据订单流图表探索一系列的指标,我们后续文章进行介绍。
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, rData) {
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')
deltaList = []
}
var amount = ticker.Volume - self.pre.Volume // 最新数据的成交量减去上一个数据的成交量
if (action != '' && amount > 0) { // 如果标记不为空字符串,并且action大于0
var epoch = parseInt(ticker.Time / period) * period // 计算K线时间戳并取整
preepoch = epoch
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
}
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 + ' → ' + pBuy + arrow + pSell
})
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数据
var r = exchange.GetRecords()
if (ticker) { // 如果成功获取到Tick数据
filt.feed(ticker, contractCode, r) // 开始处理数据
}
}
}
作为本系列第一篇文章,我们进行了订单流概念的介绍以及计算方法,后续我们将以此为基础,开展一系列的指标挖掘和策略构建工作。
注:
(1)主动成交是指交易者以市场当前价格进行交易,接受市场提供的价格以立即完成交易。买方主动成交时买入卖价,卖方主动成交时卖出买价。被动成交是指交易者挂单等待匹配,以设定的价格进行交易。买方被动成交时卖家以买价卖出,卖方被动成交时买家以卖价买入。主动成交优先考虑速度,可能影响市场价格,而被动成交优先考虑价格,提供市场流动性。