原文地址:https://www.fmz.cn/digest-topic/8978
什么是海龟策略?
几乎所有的宽客(Quant)都听说过海龟交易策略,该策略以海龟交易法则为核心。海龟交易法则,起源于八十年代的美国,是一套简单有效的交易法则。这个法则以及使用这个法则的人的故事被写成了一本书——《海龟交易法则》,这是一本入门量化投资的经典书籍。
股票证券的程序化、量化交易以前门槛可不低,以前软件支持少,账户开户门槛极高。FMZ.CN(国内站)支持富途证券、中泰XTP,开通了富途证券就可以很方便的做程序化模拟盘、实盘测试。本篇我们就一起学习设计一个股票版本的多品种海龟交易策略,初期我们主要基于回测系统进行设计、研究,慢慢的扩展至富途证券的模拟盘(模拟账户)。
策略设计:
策略架构我们参考http://FMZ.CN上开源的「商品期货多品种海龟策略」。和商品期货版本一样,我们设计一个海龟交易逻辑管理对象的构造函数TTManager。构造的对象(obj)用来操作、管理每个股票的海龟交易逻辑的执行。
```javascript
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
}
...
股票市场和商品期货市场又有些差别,下面我们来一起分析一下这些差别,然后对于策略进行具体的修改、设计。
交易时间差别
我们需要单独设计一个函数,识别开盘休盘时间,如下函数设计,给构造函数TTManager返回的对象obj增加方法:
```javascript
```javascript
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函数获取的持仓列表中显示。
需要我们对交易下单的部分做设计,增加函数:
```javascript
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