功能
运行状态
回测示例
/*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); } }
wildfire 找到bug了,cover前的那个cpCode==0有问题:当加仓条件满足,并且超过最大开仓量,加仓那里cpCode=1或2,还是会继续的。此时要求cpCode==0才能平仓就会导致略过平仓条件。所以当行情大涨的时候,永远无法平仓,直到价格跌破上次加仓价......,那可能就3年以后的事了 修改前: } else if (-spread > (IncSpace * obj.N)) { 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++; } } 修改后: } else 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.symbolID=symbolnew.InstrumentID; obj.status.cover++; } else if (-spread > (IncSpace * obj.N)) { //Log(obj.symbolDetail.InstrumentName, "test3"); opCode = obj.marketPosition > 0 ? 1 : 2; obj.symbolID=symbolnew.InstrumentID; }
wildfire 回测cu888一个品种,时间2016.10.01-2019.10.01, 程序不能自动平仓,在这2年多时间里一直持仓,止损价和离市价固定不变,是哪里有问题吗? 另外1个小问题:cu888显示出来是乱码;
autonomism 不同品种持有仓位不同怎么实现
bzpony 移仓换月可以根据成交量或者持仓量来处理,最新合约三天平均持仓量大于前一个合约即可移仓,期待早日做出来呀
lihui1625 mark
zhudi8181 换月怎么操作
18611853098 大哥,我想加一下其他品种,比如铜,回测时就会失败,怎么回事呢?
Zero 这个策略现在实盘的人比较多, 也比较稳定,没什么大更新,小更新想知道更新什么的,可以直接上github上找这个策略查history https://github.com/botvs/strategies
Gavin @yhqwww,你胆子好大,请教你多少资金在实盘呢?
ssder 那个实盘的话~服务器关闭时间要加过滤。。。不然服务器关闭后一直连接服务器被拒会导致下次开盘连接不上
ssder 这个实盘很不稳定很容易连接不上服务器希望老大改进一下~
Gavin zero老大,9月26是更新了神马?
lypyhq 实盘的时候特别是周一刚开盘很容易连接不上交易所服务器要重启策略才可以
Gavin 木有仓位控制,上去就直接干到满仓,这样的结果就是拿了N多白银和锌,却错过了螺纹的大行情。。。 没有止盈逻辑,赚了N多还拿着。。。
Gavin 新版无法恢复仓位,log里面:没有找到xxx的进度恢复信息
Gavin 止损写死为2的问题貌似改了~哈哈
ssder 是要更新最新托管者能解决这个问题吗~。。经常一开盘就连接不上交易所了
ssder 这海龟跑实盘经常动不动就连接不上交易所服务器~希望zero改进一下模拟盘似乎不会实盘就会
Gavin 能不能完备下出租?
yycomyy 改变策略的参数然后保存,参数改变前后分别回测,居然回测数据一致,说明参数更改没生效。。不知道bug在哪里。
Zero 修改一下,加了移仓操作,重新开源
wildfire 回测cu888一个品种,时间2016.10.01-2019.10.01, 程序不能自动平仓,在这期间一直持仓, 是哪里有问题吗?
Zero 加网站首页显示的群可以交流.
18611853098 老大,这个策略回测了2年,收益都很好。膜拜!! 求联系方式!我账户名就是我微信号
Zero 看看是不是品种名写错了, 可以看下出错原因是不是没K线数据造成的
muia 赞老大~
Gavin 银河期货没错~
ssder 老哥你实盘那个稳定没掉线的是银河期货吗我也去开一个
lypyhq 四万多
Zero 看下这个策略手动恢复的选项, 如果是重新建机器人的话,需要保存上次的持仓状态字符串. 不明白可以看一下策略源码
Zero 前两天就改了
Zero 模拟盘的服务器一般是24小时开着的, 实盘的话,开盘前后一段时间才会正式开. 这是区别, 所以一般不开盘的话,无法连接服务器的
Zero 这是个开源策略.