功能
运行状态
回测示例
/*backtest
start: 2016-05-01 00:00:00
end: 2020-02-15 23:59:00
period: 1d
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}]
*/
var _q = $.NewTaskQueue();
var TTManager = {
New: function(needRestore, symbol, initBalance, keepBalance, riskRatio, atrLen, enterPeriodA, leavePeriodA, enterPeriodB, leavePeriodB, useFilter,
multiplierN, multiplierS, maxLots) {
// subscribe
var symbolDetail = _C(exchange.SetContractType, symbol);
if (symbolDetail.VolumeMultiple == 0 || symbolDetail.MaxLimitOrderVolume == 0 || symbolDetail.MinLimitOrderVolume == 0 || symbolDetail.LongMarginRatio == 0 || symbolDetail.ShortMarginRatio == 0) {
Log(symbolDetail);
throw "合约信息异常";
} else {
Log("合约", symbolDetail.InstrumentName, "一手", symbolDetail.VolumeMultiple, "份, 最大下单量", symbolDetail.MaxLimitOrderVolume, "保证金率:", _N(symbolDetail.LongMarginRatio), _N(symbolDetail.ShortMarginRatio), "交割日期", symbolDetail.StartDelivDate);
}
var obj = {
symbol: symbol,
tradeSymbol: symbolDetail.InstrumentID,
initBalance: initBalance,
keepBalance: keepBalance,
riskRatio: riskRatio,
atrLen: atrLen,
enterPeriodA: enterPeriodA,
leavePeriodA: leavePeriodA,
enterPeriodB: enterPeriodB,
leavePeriodB: leavePeriodB,
useFilter: useFilter,
multiplierN: multiplierN,
multiplierS: multiplierS
};
obj.maxLots = maxLots;
obj.lastPrice = 0;
obj.symbolDetail = symbolDetail;
obj.status = {
symbol: symbol,
recordsLen: 0,
vm: [],
open: 0,
cover: 0,
st: 0,
marketPosition: 0,
lastPrice: 0,
holdPrice: 0,
holdAmount: 0,
holdProfit: 0,
switchCount: 0,
N: 0,
upLine: 0,
downLine: 0,
lastErr: "",
lastErrTime: "",
stopPrice: '',
leavePrice: '',
isTrading: false
};
obj.setLastError = function(err) {
if (typeof(err) === 'undefined' || err === '') {
obj.status.lastErr = "";
obj.status.lastErrTime = "";
return;
}
var t = new Date();
obj.status.lastErr = err;
obj.status.lastErrTime = t.toLocaleString();
};
obj.reset = function(marketPosition, openPrice, N, leavePeriod, preBreakoutFailure) {
if (typeof(marketPosition) !== 'undefined') {
obj.marketPosition = marketPosition;
obj.openPrice = openPrice;
obj.preBreakoutFailure = preBreakoutFailure;
obj.N = N;
obj.leavePeriod = leavePeriod;
var pos = _q.GetPosition(exchange, obj.tradeSymbol, marketPosition > 0 ? PD_LONG : PD_SHORT);
if (pos) {
obj.holdPrice = pos.Price;
obj.holdAmount = pos.Amount;
Log(obj.symbol, "仓位", pos);
} else {
throw "恢复" + obj.symbol + "的持仓状态出错, 没有找到仓位信息";
}
Log("恢复", obj.symbol, "加仓次数", obj.marketPosition, "持仓均价:", obj.holdPrice, "持仓数量:", obj.holdAmount, "最后一次加仓价", obj.openPrice, "N值", obj.N, "离市周期:", leavePeriod, "上次突破:", obj.preBreakoutFailure ? "失败" : "成功");
obj.status.open = 1;
obj.status.vm = [obj.marketPosition, obj.openPrice, obj.N, obj.leavePeriod, obj.preBreakoutFailure];
} else {
obj.marketPosition = 0;
obj.holdPrice = 0;
obj.openPrice = 0;
obj.holdAmount = 0;
obj.holdProfit = 0;
obj.preBreakoutFailure = true; // test system A
obj.N = 0;
obj.leavePeriod = leavePeriodA;
}
obj.holdProfit = 0;
obj.lastErr = "";
obj.lastErrTime = "";
};
obj.Status = function() {
obj.status.N = obj.N;
obj.status.marketPosition = obj.marketPosition;
obj.status.holdPrice = obj.holdPrice;
obj.status.holdAmount = obj.holdAmount;
obj.status.lastPrice = obj.lastPrice;
if (obj.lastPrice > 0 && obj.holdAmount > 0 && obj.marketPosition !== 0) {
obj.status.holdProfit = _N((obj.lastPrice - obj.holdPrice) * obj.holdAmount * obj.symbolDetail.VolumeMultiple, 4) * (obj.marketPosition > 0 ? 1 : -1);
} else {
obj.status.holdProfit = 0;
}
obj.status.symbolDetail = obj.symbolDetail;
return obj.status;
};
obj.Poll = function() {
obj.status.isTrading = $.IsTrading(obj.symbol);
if (!obj.status.isTrading) {
return;
}
// busy
if (_q.hasTask(obj.tradeSymbol)) {
return
}
// Loop
var suffix = WXPush ? '@' : '';
// switch symbol
var insDetail = exchange.SetContractType(obj.symbol);
if (!insDetail) {
return
}
var records = exchange.GetRecords();
if (!records) {
obj.setLastError("获取K线失败");
return;
}
// update tradeSymbol
var tradeSymbol = insDetail.InstrumentID;
if (tradeSymbol != obj.tradeSymbol) {
var oldSymbol = obj.tradeSymbol;
var pos = _q.GetPosition(exchange, oldSymbol);
if (pos && pos.Amount > 0) {
Log("开始移仓", oldSymbol, "->", tradeSymbol, "数量:", pos.Amount, "#ff0000");
obj.status.switchCount++;
_q.pushTask(exchange, oldSymbol, (pos.Type == PD_LONG ? "closebuy" : "closesell"), pos.Amount, function(task, ret) {
if (!ret) {
Log(oldSymbol, "移仓平仓失败 #ff0000");
return;
}
Log("移仓进度平仓成功, 开始开仓", oldSymbol, "->", tradeSymbol, "数量:", pos.Amount, "#0000ff");
obj.tradeSymbol = tradeSymbol;
obj.symbolDetail = insDetail;
_q.pushTask(exchange, tradeSymbol, (pos.Type == PD_LONG ? "buy" : "sell"), pos.Amount, function(task, ret) {
if (!ret) {
Log(tradeSymbol, "移仓开仓失败, 重置品种进度 #ff0000");
obj.marketPosition = 0;
return;
}
Log("移仓成功", oldSymbol, "->", tradeSymbol, "#0000ff");
});
});
return;
} else {
obj.tradeSymbol = tradeSymbol;
obj.symbolDetail = insDetail;
}
}
obj.status.recordsLen = records.length;
if (records.length < obj.atrLen) {
obj.setLastError("K线长度小于 " + obj.atrLen);
return;
}
var opCode = 0; // 0: IDLE, 1: LONG, 2: SHORT, 3: CoverALL
var lastPrice = records[records.length - 1].Close;
obj.lastPrice = lastPrice;
if (obj.marketPosition === 0) {
obj.status.stopPrice = '--';
obj.status.leavePrice = '--';
obj.status.upLine = 0;
obj.status.downLine = 0;
for (var i = 0; i < 2; i++) {
if (i == 0 && obj.useFilter && !obj.preBreakoutFailure) {
continue;
}
var enterPeriod = i == 0 ? obj.enterPeriodA : obj.enterPeriodB;
if (records.length < (enterPeriod + 1)) {
continue;
}
var highest = TA.Highest(records, enterPeriod, 'High');
var lowest = TA.Lowest(records, enterPeriod, 'Low');
obj.status.upLine = obj.status.upLine == 0 ? highest : Math.min(obj.status.upLine, highest);
obj.status.downLine = obj.status.downLine == 0 ? lowest : Math.max(obj.status.downLine, lowest);
if (lastPrice > highest) {
opCode = 1;
} else if (lastPrice < lowest) {
opCode = 2;
}
if (opCode != 0) {
obj.leavePeriod = (enterPeriod == obj.enterPeriodA) ? obj.leavePeriodA : obj.leavePeriodB;
break;
}
}
} else {
var spread = obj.marketPosition > 0 ? (obj.openPrice - lastPrice) : (lastPrice - obj.openPrice);
obj.status.stopPrice = _N(obj.openPrice + (obj.N * StopLossRatio * (obj.marketPosition > 0 ? -1 : 1)));
if (spread > (obj.N * StopLossRatio)) {
opCode = 3;
obj.preBreakoutFailure = true;
Log(obj.symbolDetail.InstrumentName, "止损平仓", suffix);
obj.status.st++;
} else if (-spread > (IncSpace * obj.N) && Math.abs(obj.marketPosition) < obj.maxLots) {
opCode = obj.marketPosition > 0 ? 1 : 2;
}
if (opCode == 0 && records.length > obj.leavePeriod) {
obj.status.leavePrice = obj.marketPosition > 0 ? TA.Lowest(records, obj.leavePeriod, 'Low') : TA.Highest(records, obj.leavePeriod, 'High');
if ((obj.marketPosition > 0 && lastPrice < obj.status.leavePrice) ||
(obj.marketPosition < 0 && lastPrice > obj.status.leavePrice)) {
obj.preBreakoutFailure = false;
Log(obj.symbolDetail.InstrumentName, "正常平仓", suffix);
opCode = 3;
obj.status.cover++;
}
}
}
if (opCode == 0) {
return;
}
if (opCode == 3) {
_q.pushTask(exchange, obj.tradeSymbol, "coverall", 0, function(task, ret) {
obj.reset();
_G(obj.symbol, null);
var account = _q.GetAccount(exchange);
var accountInfo = JSON.parse(exchange.GetRawJSON());
LogProfit(accountInfo.Balance, obj.tradeSymbol, "平仓后权益");
});
return;
}
// Open
if (Math.abs(obj.marketPosition) >= obj.maxLots) {
obj.setLastError("禁止开仓, 超过最大持仓 " + obj.maxLots);
return;
}
var atrs = TA.ATR(records, atrLen);
var N = _N(atrs[atrs.length - 1], 4);
var account = _q.GetAccount(exchange);
var unit = parseInt((obj.initBalance-obj.keepBalance) * (obj.riskRatio / 100) / N / obj.symbolDetail.VolumeMultiple);
var canOpen = parseInt((account.Balance-obj.keepBalance) / (opCode == 1 ? obj.symbolDetail.LongMarginRatio : obj.symbolDetail.ShortMarginRatio) / (lastPrice * 1.2) / obj.symbolDetail.VolumeMultiple);
unit = Math.min(unit, canOpen);
if (unit < obj.symbolDetail.MinLimitOrderVolume) {
obj.setLastError("可开 " + unit + " 手 无法开仓, " + (canOpen >= obj.symbolDetail.MinLimitOrderVolume ? "风控触发" : "资金限制") + "可用: " + account.Balance);
return;
}
_q.pushTask(exchange, obj.tradeSymbol, (opCode == 1 ? "buy" : "sell"), unit, function(task, ret) {
if (!ret) {
obj.setLastError("下单失败");
return;
}
Log(obj.symbolDetail.InstrumentName, obj.marketPosition == 0 ? "开仓" : "加仓", "离市周期", obj.leavePeriod, suffix);
obj.N = N;
obj.openPrice = ret.price;
obj.holdPrice = ret.position.Price;
if (obj.marketPosition == 0) {
obj.status.open++;
}
obj.holdAmount = ret.position.Amount;
obj.marketPosition += opCode == 1 ? 1 : -1;
obj.status.vm = [obj.marketPosition, obj.openPrice, N, obj.leavePeriod, obj.preBreakoutFailure];
_G(obj.symbol, obj.status.vm);
});
};
var vm = null;
if (RMode === 0) {
vm = _G(obj.symbol);
} else {
vm = JSON.parse(VMStatus)[obj.symbol];
}
if (vm) {
Log("准备恢复进度, 当前合约状态为", vm);
obj.reset(vm[0], vm[1], vm[2], vm[3], vm[4]);
} else {
if (needRestore) {
Log("没有找到" + obj.symbol + "的进度恢复信息");
}
obj.reset();
}
return obj;
}
};
function onexit() {
Log("已退出策略...");
}
function main() {
if (exchange.GetName().indexOf('CTP') == -1) {
throw "只支持商品期货CTP";
}
SetErrorFilter("login|ready|流控|连接失败|初始|Timeout");
var mode = exchange.IO("mode", 0);
if (typeof(mode) !== 'number') {
throw "切换模式失败, 请更新到最新托管者!";
}
while (!exchange.IO("status")) {
Sleep(3000);
LogStatus("正在等待与交易服务器连接, " + _D());
}
var positions = _C(exchange.GetPosition);
if (positions.length > 0) {
Log("检测到当前持有仓位, 系统将开始尝试恢复进度...");
Log("持仓信息", positions);
}
Log("风险系数:", RiskRatio, "N值周期:", ATRLength, "系统1: 入市周期", EnterPeriodA, "离市周期", LeavePeriodA, "系统二: 入市周期", EnterPeriodB, "离市周期", LeavePeriodB, "加仓系数:", IncSpace, "止损系数:", StopLossRatio, "单品种最多开仓:", MaxLots, "次");
var initAccount = _q.GetAccount(exchange);
var initMargin = JSON.parse(exchange.GetRawJSON()).CurrMargin;
var realInitBalance = initAccount.Balance + initMargin;
if (CustomBalance) {
realInitBalance = InitBalance;
Log("自定义启动资产为", realInitBalance);
}
var keepBalance = _N(realInitBalance * (KeepRatio/100), 3);
Log("当前资产信息", initAccount, "保留资金:", keepBalance);
var tts = [];
var filter = [];
var arr = Instruments.split(',');
for (var i = 0; i < arr.length; i++) {
var symbol = arr[i].replace(/^\s+/g, "").replace(/\s+$/g, "");
if (typeof(filter[symbol]) !== 'undefined') {
Log(symbol, "已经存在, 系统已自动过滤");
continue;
}
filter[symbol] = true;
var hasPosition = false;
for (var j = 0; j < positions.length; j++) {
if (positions[j].ContractType == symbol) {
hasPosition = true;
break;
}
}
var obj = TTManager.New(hasPosition, symbol, realInitBalance, keepBalance, RiskRatio, ATRLength, EnterPeriodA, LeavePeriodA, EnterPeriodB, LeavePeriodB, UseEnterFilter, IncSpace, StopLossRatio, MaxLots);
tts.push(obj);
}
var tblAssets = null;
var nowAccount = null;
var lastStatus = '';
while (true) {
if (GetCommand() === "暂停/继续") {
Log("暂停交易中...");
while (GetCommand() !== "暂停/继续") {
Sleep(1000);
}
Log("继续交易中...");
}
while (!exchange.IO("status")) {
Sleep(3000);
LogStatus("正在等待与交易服务器连接, " + _D() + "\n" + lastStatus);
}
var tblStatus = {
type: "table",
title: "持仓信息",
cols: ["合约名称", "持仓方向", "持仓均价", "持仓数量", "持仓盈亏", "加仓次数", "开仓次数", "止损次数", "成功次数", "当前价格", "N"],
rows: []
};
var tblMarket = {
type: "table",
title: "运行状态",
cols: ["合约名称", "合约乘数", "保证金率", "交易时间", "移仓次数", "柱线长度", "上线", "下线", "止损价", "离市价", "异常描述", "发生时间"],
rows: []
};
var totalHold = 0;
var vmStatus = {};
var ts = new Date().getTime();
var holdSymbol = 0;
var tradingCount = 0;
for (var i = 0; i < tts.length; i++) {
tts[i].Poll();
var d = tts[i].Status();
if (d.holdAmount > 0) {
vmStatus[d.symbol] = d.vm;
holdSymbol++;
}
if (d.isTrading) {
tradingCount++;
}
tblStatus.rows.push([d.symbolDetail.InstrumentID, d.holdAmount == 0 ? '--' : (d.marketPosition > 0 ? '多' : '空'), d.holdPrice, d.holdAmount, d.holdProfit, Math.abs(d.marketPosition), d.open, d.st, d.cover, d.lastPrice, d.N]);
tblMarket.rows.push([d.symbolDetail.InstrumentID, d.symbolDetail.VolumeMultiple, _N(d.symbolDetail.LongMarginRatio, 4) + '/' + _N(d.symbolDetail.ShortMarginRatio, 4), (d.isTrading ? '是#0000ff' : '否#ff0000'), d.switchCount, d.recordsLen, d.upLine, d.downLine, d.stopPrice, d.leavePrice, d.lastErr, d.lastErrTime]);
totalHold += Math.abs(d.holdAmount);
}
var now = new Date();
var elapsed = now.getTime() - ts;
if (tradingCount > 0 || !nowAccount) {
tblAssets = _q.GetAccount(exchange, true);
nowAccount = _q.Account();
if (tblAssets.rows.length > 10) {
// replace AccountId
tblAssets.rows[0] = ["InitAccount", "初始资产", realInitBalance];
} else {
tblAssets.rows.unshift(["NowAccount", "当前可用", nowAccount], ["InitAccount", "初始资产", realInitBalance]);
}
if (totalHold > 0) {
tblAssets.rows.push(["手动恢复字符串", {body:JSON.stringify(vmStatus), colspan: 2}]);
}
}
lastStatus = '`' + JSON.stringify([tblStatus, tblMarket, tblAssets]) + '`\n轮询耗时: ' + elapsed + ' 毫秒, 当前时间: ' + _D() + ', 星期' + ['日', '一', '二', '三', '四', '五', '六'][now.getDay()] + ", 持有品种个数: " + holdSymbol + ", 交易任务: " + _q.size();
LogStatus(lastStatus);
_q.poll();
Sleep(LoopInterval * 1000);
}
}