参考文章: 商品期货「订单流」系列文章(三):供需失衡和堆积带
/*backtest start: 2024-07-25 09:00:00 end: 2024-07-31 23:00:00 period: 1m basePeriod: 1m exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}] mode: 1 args: [["contract","SA888"],["stopWin",10],["stopLoss",10]] */ var longSignal = false var shortSignal = false 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 = [] longSignal = false shortSignal = false self.feed = function(ticker, rData, 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 Log('更新交易日#ff0000') } 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 ) { if(self.records.length > 0){ var curBar = self.records[self.records.length - 1].data // 将data对象转换为数组并排序 var dataArray = Object.keys(curBar).map(function(price) { return { price: parseInt(price), sell: curBar[price].sell, buy: curBar[price].buy }; }).sort(function(a, b) { return b.price - a.price; // 从大到小排序 }); // 定义变量 var fromV = null; var endV = null; var zone = ''; var supplyImbalanceCount = 0; var demandImbalanceCount = 0; // 遍历数组并进行判断 for (var i = 1; i < dataArray.length; i++) { var currentRow = dataArray[i]; var previousRow = dataArray[i - 1]; if (currentRow.sell > previousRow.buy * nMul) { //Log('价格(price ' + currentRow.price + ')出现供应失衡'); supplyImbalanceCount++; demandImbalanceCount = 0; // 重置需求失衡计数器 if (supplyImbalanceCount == 1) { fromV = currentRow.price; } endV = previousRow.price - 1; } else if (previousRow.buy > currentRow.sell * nMul) { //Log('价格(price ' + previousRow.price + ')出现需求失衡'); demandImbalanceCount++; supplyImbalanceCount = 0; // 重置供应失衡计数器 if (demandImbalanceCount == 1) { fromV = previousRow.price; } endV = currentRow.price + 1; } else { supplyImbalanceCount = 0; // 重置供应失衡计数器 demandImbalanceCount = 0; // 重置需求失衡计数器 } if (supplyImbalanceCount >= nCount) { zone = '阻力带(供应失衡)'; shortSignal = true break; } else if (demandImbalanceCount >= nCount) { zone = '支撑带(需求失衡)'; longSignal = true break; } else{ longSignal = false shortSignal = false } } var color = zone ? zone == '阻力带(供应失衡)' ? '#00ff00' : '#ff0000' : null if (zone) { Log(endV + ' 到 ' + fromV + ' 的区域是 ' + zone + color); } } 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 + ' → ' + 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, contract) // 订阅数据 Log('交割日期:', symbolDetail['StartDelivDate']) Log('最小下单量:', symbolDetail['MaxLimitOrderVolume']) Log('最小价差:', symbolDetail['PriceTick']) Log('一手:', symbolDetail["VolumeMultiple"], '份') Log('合约代码:', symbolDetail['InstrumentID']) var filt = NewFuturesTradeFilter(60000) // 创建一个对象 $.CTA(contract, function(st) { var ticker = exchange.GetTicker(); var r = exchange.GetRecords(); var priceTick = exchange.SetContractType(contract).PriceTick; if (ticker) { filt.feed(ticker, r, contract); if (st.position.amount === 0 && longSignal) { Log('多头开仓#ff0000'); return 1; } if (st.position.amount > 0 && r[r.length - 1].Close - st.position.price >= stopWin * priceTick) { Log('多头盈利平仓#ff0000'); return -1; } if (st.position.amount > 0 && (r[r.length - 1].Close - st.position.price < -stopLoss * priceTick )) { Log('多头亏损平仓#ff0000'); return -1; } if (st.position.amount === 0 && shortSignal) { Log('空头开仓#0000ff'); return -1; } if (st.position.amount < 0 && r[r.length - 1].Close - st.position.price <= -stopWin * priceTick) { Log('空头盈利平仓#0000ff'); return 1; } if (st.position.amount < 0 && (r[r.length - 1].Close - st.position.price > stopLoss * priceTick )) { Log('空头亏损平仓#0000ff'); return 1; } } }); }