股票多品种海龟交易策略设计范例

Author: 雨幕, Created: 2022-02-22 09:43:54, Updated: 2023-11-21 21:13:21

img

股票多品种海龟交易策略设计范例

股票证券的程序化、量化交易以前门槛可不低,以前软件支持少,账户开户门槛极高。FMZ.CN(国内站)支持富途证券、中泰XTP,开通了富途证券就可以很方便的做程序化模拟盘、实盘测试。本篇我们就一起学习设计一个股票版本的多品种海龟交易策略,初期我们主要基于回测系统进行设计、研究,慢慢的扩展至富途证券的模拟盘(模拟账户)。

策略设计

策略架构我们参考FMZ.CN上开源的「商品期货多品种海龟策略」。和商品期货版本一样,我们设计一个海龟交易逻辑管理对象的构造函数TTManager

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) {
            Log(symbolDetail)
            throw "股票合约信息异常"
        } else {
            Log("合约", symbolDetail.InstrumentName, "一手", symbolDetail.VolumeMultiple, "股, 最大下单量", symbolDetail.MaxLimitOrderVolume, ", 最小下单量", symbolDetail.VolumeMultiple)
        }
        
        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)用来操作、管理每个股票的海龟交易逻辑的执行。

股票市场商品期货市场又有些差别,下面我们来一起分析一下这些差别,然后对于策略进行具体的修改、设计。

  • 交易时间差别 我们需要单独设计一个函数,识别开盘休盘时间,如下函数设计,给构造函数TTManager返回的对象obj增加方法:

          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 
          }
    
  • 交易方向的差别 商品期货有开仓、平仓。股票只有买、卖,没有开仓平仓。股票类似于现货,但是也有持仓,买入的股票会在GetPosition函数获取的持仓列表中显示。 需要我们对交易下单的部分做设计,增加函数:

    obj.sell = function(e, contractType, lots, insDetail) {
        ...
    }
    
    obj.buy = function(e, contractType, opAmount, insDetail) {
        ...
    }
    
  • 下单头寸计算 商品期货交易下单时是按照合约张数下单,一张合约根据其合约乘数代表一定量的商品(例如rb合约,一张代表10吨螺纹钢)。股票虽说也是有按手计算的(根据板块有的1手100股,有的500股,还有的200股)。但是下单的时候必须是股数,并且要能被一手的股数整除。不能整除的会报错。 这样就需要对海龟交易法计算头寸的部分做一定修改:

              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 "股票不支持做空"
              }
    

策略注释

为了方便理解策略代码,我们对策略通篇注释。

/*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)
    }
}

回测测试、研究

我们选择几只股票回测:600519.SH,600690.SH,600006.SH,601328.SH,600887.SH,600121.SH,601633.SH

其它参数设置:

img

img

回测时状态栏信息输出:

img

img

可以观察到,海龟交易法这种趋势跟踪策略需要在有较大的行情时才会有较好的盈利。在行情反复震荡时可能会有一定回撤。 涨幅较大的贵州茅台贡献了整体收益的绝大部分,看来选股也是十分重要的因素。并且根据状态栏中显示的统计数据来看,海龟交易法的止损次数要远高于策略成功盈利次数。这也是策略的思路核心,用较小的头寸试错。一旦抓住趋势突破加仓,抓住肥尾。创造震荡期损失数倍的盈利。

完整策略:https://www.fmz.cn/strategy/346551

该策略仅用于回测研究,实盘请自行优化、修改。


更多内容