策略仅用于回测研究、分析。 相关文章:https://www.youquant.com/digest-topic/8978
/*backtest start: 2016-05-01 00:00:00 end: 2022-02-19 23:59:00 period: 1d basePeriod: 1d exchanges: [{"eid":"Futures_XTP","currency":"STOCK","minFee":0}] args: [["Instruments","600519.SH,600690.SH,600006.SH,601328.SH,600887.SH,600121.SH,601633.SH"],["ATRLength",30],["EnterPeriodA",30],["LeavePeriodA",50],["EnterPeriodB",60],["LeavePeriodB",80],["KeepRatio",5]] */ var SlideTick = 10 // 下单滑价点数,设置10下买单时价格加10跳 var Interval = 1000 // 程序暂停毫秒数 /* TTManager : 海龟交易逻辑对象的构造函数 参数:needRestore, symbol, initBalance, keepBalance, riskRatio, atrLen, enterPeriodA, leavePeriodA, enterPeriodB, leavePeriodB, useFilter, multiplierN, multiplierS, maxLots 需要恢复持仓、交易品种代码、初始资产、保留资产、风险系数、ATR参数、入市周期A,离市周期A、入市周期B、离市周期B、是否使用入市过滤、加仓间隔(N的倍数)、止损系数(N的倍数)、最大加仓次数 */ 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) // 切换合约代码,合约代码为symbol的值 if (symbolDetail.VolumeMultiple == 0) { // SetContractType会返回切换的品种的一些信息,检测返回的数据中的VolumeMultiple字段是否正常 Log(symbolDetail) throw "股票合约信息异常" } else { Log("合约", symbolDetail.InstrumentName, "一手", symbolDetail.VolumeMultiple, "股, 最大下单量", symbolDetail.MaxLimitOrderVolume, ", 最小下单量", symbolDetail.VolumeMultiple) // 输出相关信息 } // 声明当前构造函数TTManager返回的对象obj,该对象记录每个海龟交易逻辑的相关信息,例如执行的品种(股票代码)、ATR参数、加仓、止损N值系数等 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.getPosition = function(e, contractTypeName) { var allAmount = 0 var allProfit = 0 var allFrozen = 0 var posMargin = 0 var price = 0 var direction = null positions = _C(e.GetPosition) // 根据参数e调用指定的交易所对象的获取持仓函数GetPosition,e即代表一个配置的账户,e.GetPosition即代表获取这个账户目前的持仓数据 for (var i = 0; i < positions.length; i++) { // 遍历持仓数据,找到指定的股票 if (positions[i].ContractType != contractTypeName) { continue } if (positions[i].Type == PD_LONG) { posMargin = positions[i].MarginLevel allAmount += positions[i].Amount allProfit += positions[i].Profit allFrozen += positions[i].FrozenAmount price = positions[i].Price direction = positions[i].Type } } if (allAmount === 0) { return null } return { MarginLevel: posMargin, FrozenAmount: allFrozen, Price: price, Amount: allAmount, Profit: allProfit, Type: direction, ContractType: contractTypeName, CanCoverAmount: allAmount - allFrozen } } // 获取当前时间对象 obj.newDate = function() { var timezone = 8 var offset_GMT = new Date().getTimezoneOffset() var nowDate = new Date().getTime() var targetDate = new Date(nowDate + offset_GMT * 60 * 1000 + timezone * 60 * 60 * 1000) return targetDate } // 判断是否开市 obj.isSymbolTrading = function() { // 使用 newDate() 代替 new Date() 因为服务器时区问题 var now = obj.newDate() var day = now.getDay() var hour = now.getHours() var minute = now.getMinutes() StatusMsg = "非交易时段" if (day === 0 || day === 6) { return false } if((hour == 9 && minute >= 30) || (hour == 11 && minute < 30) || (hour > 9 && hour < 11)) { // 9:30-11:30 StatusMsg = "交易时段" return true } else if (hour >= 13 && hour < 15) { // 13:00-15:00 StatusMsg = "交易时段" return true } return false } // 买入函数 obj.buy = function(e, contractType, opAmount, insDetail) { var initPosition = obj.getPosition(e, contractType) // 获取初始时的持仓 var isFirst = true var initAmount = initPosition ? initPosition.Amount : 0 // 设置初始持仓数量 var positionNow = initPosition // 设置当前持仓数据 if(!IsVirtual() && opAmount % insDetail.LotSize != 0) { // 判断需要交易的数量opAmount(股数)是否符合整手数 throw "每手数量不匹配" } while (true) { var needOpen = opAmount if (isFirst) { isFirst = false } else { Sleep(Interval*20) positionNow = obj.getPosition(e, contractType) if (positionNow) { needOpen = opAmount - (positionNow.Amount - initAmount) } } // 需要交易的量如果小于一手数量,或者不符合整手数跳出循环 if (needOpen < insDetail.LotSize || (needOpen % insDetail.LotSize != 0 && !IsVirtual())) { break } var depth = _C(e.GetDepth) // 需要检测是否涨跌停 var amount = needOpen e.SetDirection("buy") var orderId = e.Buy(depth.Asks[0].Price + (insDetail.PriceSpread * SlideTick), amount, contractType, 'Ask', depth.Asks[0]) // CancelPendingOrders while (true) { Sleep(Interval*20) var orders = _C(e.GetOrders) if (orders.length === 0) { break } for (var j = 0; j < orders.length; j++) { e.CancelOrder(orders[j].Id) if (j < (orders.length - 1)) { Sleep(Interval*20) } } } } var ret = null if (!positionNow) { return ret } ret = positionNow return ret } // 卖出函数 obj.sell = function(e, contractType, lots, insDetail) { var initAmount = 0 var firstLoop = true if(!IsVirtual() && lots % insDetail.LotSize != 0) { throw "每手数量不匹配" } while (true) { var n = 0 var total = 0 var positions = _C(e.GetPosition) var nowAmount = 0 for (var i = 0; i < positions.length; i++) { if (positions[i].ContractType != contractType) { continue } nowAmount += positions[i].Amount } if (firstLoop) { initAmount = nowAmount firstLoop = false } var amountChange = initAmount - nowAmount if (typeof(lots) == 'number' && amountChange >= lots) { break } for (var i = 0; i < positions.length; i++) { if (positions[i].ContractType != contractType) { continue } var amount = positions[i].Amount var depth var opAmount = 0 var opPrice = 0 if (positions[i].Type == PD_LONG) { depth = _C(e.GetDepth) // 需要检测是否涨跌停 opAmount = amount opPrice = depth.Bids[0].Price - (insDetail.PriceSpread * SlideTick) } if (typeof(lots) === 'number') { opAmount = Math.min(opAmount, lots - (initAmount - nowAmount)) } if (opAmount > 0) { if (positions[i].Type == PD_LONG) { e.SetDirection("closebuy") e.Sell(opPrice, opAmount, contractType, "平仓", 'Bid', depth.Bids[0]) } n++ } // break to check always if (typeof(lots) === 'number') { break } } if (n === 0) { break } while (true) { Sleep(Interval*20) var orders = _C(e.GetOrders) if (orders.length === 0) { break } for (var j = 0; j < orders.length; j++) { e.CancelOrder(orders[j].Id) if (j < (orders.length - 1)) { Sleep(Interval*20) } } } } } // 恢复控制对象数据 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 = obj.getPosition(exchange, obj.tradeSymbol) 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 = obj.isSymbolTrading(obj.symbol) if (!obj.status.isTrading) { return } var suffix = WXPush ? '@' : '' // switch symbol var insDetail = exchange.SetContractType(obj.symbol) if (!insDetail) { return } // 获取tick数据 var ticker = exchange.GetTicker() if (!ticker) { obj.setLastError("获取tick失败") return } if (IsVirtual()) { ticker.Info = {} ticker.Info.LotSize = obj.symbolDetail.VolumeMultiple ticker.Info.PriceSpread = 0.01 } var records = exchange.GetRecords() if (!records) { obj.setLastError("获取K线失败") return } obj.status.recordsLen = records.length if (records.length < obj.atrLen) { obj.setLastError("K线长度小于 " + obj.atrLen) return } var opCode = 0 // 0: IDLE, 1: LONG, 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 } 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) { var pos = obj.getPosition(exchange, obj.tradeSymbol) obj.sell(exchange, obj.tradeSymbol, pos.Amount, ticker.Info) obj.reset() _G(obj.symbol, null) var account = _C(exchange.GetAccount) 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 = _C(exchange.GetAccount) var unit = parseInt((obj.initBalance-obj.keepBalance) * (obj.riskRatio / 100) / N / obj.symbolDetail.VolumeMultiple) var canOpen = parseInt((account.Balance-obj.keepBalance) / (lastPrice * 1.2) / obj.symbolDetail.VolumeMultiple) unit = Math.min(unit, canOpen) unit = unit * obj.symbolDetail.VolumeMultiple if (unit < obj.symbolDetail.VolumeMultiple) { obj.setLastError("可开 " + unit + " 手 无法开仓, " + (canOpen >= obj.symbolDetail.VolumeMultiple ? "风控触发" : "资金限制") + "。 可用: " + account.Balance) return } // 交易函数 if (opCode == 2) { throw "股票不支持做空" } var ret = obj.buy(exchange, obj.tradeSymbol, unit, ticker.Info) if (ret) { Log(obj.symbolDetail.InstrumentName, obj.marketPosition == 0 ? "开仓" : "加仓", "离市周期", obj.leavePeriod, suffix) obj.N = N obj.openPrice = ticker.Last obj.holdPrice = ret.Price if (obj.marketPosition == 0) { obj.status.open++ } obj.holdAmount = ret.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) } else { obj.setLastError("下单失败") return } } 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((!IsVirtual() && exchange.GetCurrency() != "STOCK" && exchange.GetName() != "Futures_Futu") || (IsVirtual() && exchange.GetCurrency() != "STOCK_CNY" && exchange.GetName() != "Futures_XTP")) { Log("currency:", exchange.GetCurrency(), "name:", exchange.GetName()) throw "不支持" } SetErrorFilter("login|ready|流控|连接失败|初始|Timeout|market not ready") 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 = _C(exchange.GetAccount) var realInitBalance = initAccount.Balance 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.symbolDetail.InstrumentName, 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.InstrumentName, 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 lastStatus = '`' + JSON.stringify([tblStatus, tblMarket]) + '`\n轮询耗时: ' + elapsed + ' 毫秒, 当前时间: ' + _D() + ', 星期' + ['日', '一', '二', '三', '四', '五', '六'][now.getDay()] + ", 持有品种个数: " + holdSymbol + ", 手动恢复字符串: " + JSON.stringify(vmStatus) LogStatus(lastStatus) Sleep(LoopInterval * 1000) } }