参考文章: 商品期货「订单流」系列文章(三):供需失衡和堆积带
/*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;
}
}
});
}