海龟交易法则的精髓部分是明确给出入市位置、开仓规模、加仓位置、止损位置、离市平仓位置的交易系统,可以说是最全面的交易系统,非常适合期货交易者学习。
建仓资金(头寸规模)选择 海龟交易法则将市场价格上下波动量化为N值,每次建仓的头寸和加仓规模与波动量N相关。 波动量N(平均真实波动振幅ATR)ATR是日内价格最大波动的平均振幅,计算方法: TR=MAX(最高价-最低价,最高价-前日收盘价,前日收盘价-最低价) ATR=TR的N日简单移动平均价格波动 每次开仓(建仓)规模就是将总资产乘1%再除以DV(价值波动量) DV = N * (合约乘数) Unit(开仓数量)=(总资产×1%)/DV
入市位置选择 海龟交易法则的入市判断方式是以唐奇安的通道突破系统为基础。 海龟系统认为:短期价格突破20日、中长期突破55日唐奇安通道为入市信号(包含向上突破和向下突破,即做多和做空)。
加仓和止损 海龟交易法加仓位置为建仓后,价格向盈利方向突破1/2N时加仓,最多4个单位; 止损位置为亏损2N,即达到平均真实振幅值的两倍。
离市平仓位置 短期(20日唐奇安通道),多头头寸在突破过10日最低处平仓离场;空头头寸在突破过10日最高处平仓离场; 中长期(55日唐奇安通道),多头头寸突破过20日最低处平仓离场;空头头寸在突破20日最高处平仓离场。
'''backtest
start: 2022-11-01 00:00:00
end: 2022-11-30 23:59:00
period: 1d
basePeriod: 1h
exchanges: [{"eid":"Futures_CTP","currency":"FUTURES"}]
'''
import re
import json
import time
_bot = ext.NewPositionManager()
class Manager:
ACT_IDIE = 0
ACT_LONG = 1
ACT_SHORT = 2
ACT_COVER = 3
ERR_SUCCESS = 0
ERR_SET_SYMBOL = 1
ERR_GET_ORDERS = 2
ERR_GET_POS = 3
ERR_TRADE = 4
ERR_GET_DEPTH = 5
ERR_NOT_TRADING = 6
errMsg = ["成功","切换合约失败","获取订单失败","获取持仓失败","交易下单失败","获取深度失败","不在交易时间"]
def __init__(self,needRestore,symbol,keepBalance,riskRatio,atrLen,enterPeriodA,leavePeriodA,enterPeriodB,leavePeriodB,useFilter,multiplierN,multiplierS,maxLots):
symbolDetail = _C(exchange.SetContractType,symbol)
if symbolDetail["VolumeMultiple"] == 0 or symbolDetail["MaxLimitOrderVolume"] == 0 or symbolDetail["MinLimitOrderVolume"] == 0 or symbolDetail["LongMarginRatio"] == 0 or symbolDetail["ShortMarginRatio"] == 0 :
Log(symbolDetail)
raise Exception("合约信息异常")
else:
Log("合约:",symbolDetail["InstrumentName"],"一手",symbolDetail["VolumeMultiple"],"份,最大下单量:",symbolDetail["MaxLimitOrderVolume"],"保证金比率:",symbolDetail["LongMarginRatio"],symbolDetail["ShortMarginRatio"],"交割日期:",symbolDetail["StartDelivDate"])
self.symbol = symbol
self.keepBalance = keepBalance
self.riskRatio = riskRatio
self.atrLen = atrLen
self.enterPeriodA = enterPeriodA
self.leavePeriodA = leavePeriodA
self.enterPeriodB = enterPeriodB
self.leavePeriodB = leavePeriodB
self.useFilter = useFilter
self.multiplierN = multiplierN
self.multiplierS = multiplierS
self.maxLots = maxLots
self.symbolDetail = symbolDetail
self.lastPrice = 0
self.task = {
"action" : Manager.ACT_IDIE ,
"amount" : 0 ,
"dealAmount" : 0 ,
"avgPrice" : 0 ,
"preCost" : 0 ,
"preAmount" : 0 ,
"init" :False ,
"retry" : 0 ,
"desc" : "空闲" ,
"onFinish" : None
}
self.status = {
"symbol" : symbol ,
"recordsLen" : 0 ,
"vm" : [] ,
"open" : 0 ,
"cover" : 0 ,
"st" : 0,
"marketPosition" : 0 ,
"lastPrice" : 0,
"holdPrice" : 0 ,
"holdAmount" : 0 ,
"holdProfit" : 0,
"N" : 0,
"upLine" : 0,
"downLine" : 0,
"symbolDetail" : symbolDetail,
"lastErr" : "",
"lastErrTime" : "",
"stopPrice" : "",
"leavePrice" : "",
"isTrading" : False
}
vm = None
if RMode == 0 :
vm = _G(self.symbol)
else:
vm = json.loads(VMStatus)[self.symbol]
if vm :
Log("准备恢复进度,当前合约状态为",vm)
self.reset(vm[0],vm[1],vm[2],vm[3],vm[4])
else:
if needRestore :
Log("没有找到" + self.symbol +"的进度恢复信息")
self.reset()
def setLastError(self,err = None):
if err is None :
self.status["lastErr"] = ""
self.status["lastErrTime"] = ""
return
t = _D()
self.status["lastErr"] = err
self.status["lastErrTime"] = t
def reset(self,marketPosition = None,openPrice = None,N = None,leavePeriod = None,preBreakoutFailure = None):
if marketPosition is not None:
self.marketPosition = marketPosition
self.openPrice = openPrice
self.N = N
self.leavePeriod = leavePeriod
self.preBreakoutFailure = preBreakoutFailure
pos = _bot.GetPosition(self.symbol,PD_LONG if marketPosition > 0 else PD_SHORT)
if pos is not None:
self.holdPrice = pos["Price"]
self.holdAmount = pos["Amount"]
Log(self.symbol,"仓位:",pos)
else:
raise Exception("恢复" + self.symbol + "的持仓状态出错,没有找到仓位信息")
Log("恢复",self.symbol,"加仓次数:",self.marketPosition,"持仓均价:",self.holdPrice,"持仓数量:",self.holdAmount,"最后一次加仓价:",self.openPrice,"N值:",self.N,"离市周期:",self.leavePeriod,"上次突破:","失败" if self.preBreakoutFailure else "成功")
self.status["open"] = 1
self.status["vm"] = [self.marketPosition,self.openPrice,self.N,self.leavePeriod,self.preBreakoutFailure]
else:
self.marketPosition = 0
self.holdPrice = 0
self.openPrice = 0
self.holdAmount = 0
self.holdProfit = 0
self.preBreakoutFailure = True
self.N = 0
self.leavePeriod = self.leavePeriodA
self.holdProfit = 0
self.lastErr = ""
self.lastErrTime = ""
def Status(self):
self.status["N"] = self.N
self.status["marketPosition"] = self.marketPosition
self.status["holdProfit"] = self.holdProfit
self.status["holdAmount"] = self.holdAmount
self.status["lastPrice"] = self.lastPrice
if self.lastPrice > 0 and self.holdAmount > 0 and self.marketPosition != 0 :
self.status["holdProfit"] = _N((self.lastPrice - self.holdPrice)*self.holdAmount * self.symbolDetail["VolumeMultiple"], 4) *(1 if self.marketPosition > 0 else -1)
else:
self.status["holdProfit"] = 0
return self.status
def setTask(self,action,amount = None,onFinish = None):
self.task["init"] = False
self.task["retry"] = 0
self.task["action"] = action
self.task["preAmount"] = 0
self.task["preCost"] = 0
self.task["amount"] = 0 if amount is None else amount
self.task["onFinish"] = onFinish
if action == Manager.ACT_IDIE :
self.task["desc"] = "空闲"
self.task["onFinish"] = None
else:
if action != Manager.ACT_COVER:
self.task["desc"] = ("加多仓" if action == Manager.ACT_LONG else "加空仓") + "(" + str(amount) + ")"
else:
self.task["desc"] = "平仓"
Log("接收到任务",self.symbol,self.task["desc"])
self.Poll(True)
def processTask(self):
insDetail = exchange.SetContractType(self.symbol)
if not insDetail:
return Manager.ERR_SET_SYMBOL
SlideTick = 1
ret = False
if self.task["action"] == Manager.ACT_COVER:
hasPosition = False
while True:
if not ext.IsTrading(self.symbol):
return Manager.ERR_NOT_TRADING
hasPosition = False
positions = exchange.GetPosition()
if positions is None:
return Manager.ERR_GET_POS
depth = exchange.GetDepth()
if depth is None:
return Manager.ERR_GET_DEPTH
orderID = None
for i in range(len(positions)):
if positions[i]["ContractType"] != self.symbol:
continue
amount = min(insDetail["MaxLimitOrderVolume"], positions[i]["Amount"])
if positions[i]["Type"] == PD_LONG or positions[i]["Type"] == PD_LONG_YD :
exchange.SetDirection("closebuy_today" if positions[i]["Type"] == PD_LONG else "closebuy")
orderID = exchange.Sell(_N(depth["Bids"][0]["Price"] - (insDetail["PriceTick"] * SlideTick), 2),min(amount, depth["Bids"][0]["Amount"]), self.symbol,"平今" if positions[i]["Type"] == PD_LONG else "平昨", "Bid",depth["Bids"][0])
hasPosition = True
elif positions[i]["Type"] == PD_SHORT or positions[i]["Type"] == PD_SHORT_YD :
exchange.SetDirection("closesell_today" if positions[i]["Type"] == PD_SHORT else "closesell")
orderID = exchange.Buy(_N(depth["Asks"][0]["Price"] + (insDetail["PriceTick"] * SlideTick), 2),min(amount, depth["Asks"][0]["Amount"]), self.symbol,"平今" if positions[i]["Type"] == PD_SHORT else "平昨","Ask",depth["Asks"][0])
hasPosition = True
if hasPosition:
if not orderID:
return Manager.ERR_TRADE
Sleep(1000)
while True:
orders = exchange.GetOrders()
if orders is None:
return Manager.ERR_GET_ORDERS
if len(orders) == 0:
break
for i in range(len(orders)):
exchange.CancelOrder(orders[i]["Id"])
Sleep(500)
if not hasPosition:
break
ret = True
elif self.task["action"] == Manager.ACT_LONG or self.task["action"] == Manager.ACT_SHORT:
while True:
if not ext.IsTrading(self.symbol):
return Manager.ERR_NOT_TRADING
Sleep(1000)
while True:
orders = exchange.GetOrders()
if orders is None:
return Manager.ERR_GET_ORDERS
if len(orders) == 0:
break
for i in range(len(orders)):
exchange.CancelOrder(orders[i]["Id"])
Sleep(500)
positions = exchange.GetPosition()
if positions is None:
return Manager.ERR_GET_POS
pos = None
for i in range(len(positions)):
if positions[i]["ContractType"] == self.symbol and (((positions[i]["Type"] == PD_LONG or positions[i]["Type"] == PD_LONG_YD) and self.task["action"] == Manager.ACT_LONG) or ((positions[i]["Type"] == PD_SHORT) or positions[i]["Type"] == PD_SHORT_YD) and self.task["action"] == Manager.ACT_SHORT):
if not pos:
pos = positions[i]
pos["Cost"] = positions[i]["Price"] * positions[i]["Amount"]
else:
pos["Amount"] += positions[i]["Amount"]
pos["Profit"] += positions[i]["Profit"]
pos["Cost"] += positions[i]["Price"] * positions[i]["Amount"]
if not self.task["init"]:
self.task["init"] = True
if pos:
self.task["preAmount"] = pos["Amount"]
self.task["preCost"] = pos["Cost"]
else:
self.task["preAmount"] = 0
self.task["preCost"] = 0
remain = self.task["amount"]
if pos:
self.task["dealAmount"] = pos["Amount"] - self.task["preAmount"]
remain = self.task["amount"] - self.task["dealAmount"]
if remain <= 0 or self.task["retry"] >= MaxTaskRetry:
ret = {
"price" : (pos["Cost"] - self.task["preCost"]) / (pos["Amount"] - self.task["preAmount"]),
"amount" : (pos["Amount"] - self.task["preAmount"]),
"position" : pos
}
break
elif self.task["retry"] >= MaxTaskRetry:
ret = None
break
depth = exchange.GetDepth()
if depth is None:
return Manager.ERR_GET_DEPTH
orderID = None
if self.task["action"] == Manager.ACT_LONG:
exchange.SetDirection("buy")
orderID = exchange.Buy(_N(depth["Asks"][0]["Price"] + (insDetail["PriceTick"] * SlideTick), 2), min(remain,depth["Asks"][0]["Amount"]),self.symbol,"Ask",depth["Asks"][0])
else:
exchange.SetDirection("sell")
orderID = exchange.Sell(_N(depth["Bids"][0]["Price"] - (insDetail["PriceTick"] * SlideTick), 2), min(remain,depth["Bids"][0]["Amount"]),self.symbol,"Bid",depth["Bids"][0])
if orderID is None:
self.task["retry"] += 1
return Manager.ERR_TRADE
if self.task["onFinish"]:
self.task["onFinish"](ret)
self.setTask(Manager.ACT_IDIE)
return Manager.ERR_SUCCESS
def Poll(self,subroutine = False):
self.status["isTrading"] = ext.IsTrading(self.symbol)
if not self.status["isTrading"]:
return
if self.task["action"] != Manager.ACT_IDIE:
retCode = self.processTask()
if self.task["action"] != Manager.ACT_IDIE:
self.setLastError("任务没有处理成功:" + Manager.errMsg[retCode] + ", " + self.task["desc"] + ", 重试:" + str(self.task["retry"]))
else:
self.setLastError()
return
if subroutine:
return
suffix = "@" if Push else ""
_C(exchange.SetContractType,self.symbol)
records = exchange.GetRecords()
if records is None:
self.setLastError("获取K线数据失败")
return
self.status["recordsLen"] = len(records)
if len(records) < self.atrLen:
self.setLastError("K线数据长度小于ATR:" + str(self.atrLen))
return
opCode = 0
lastPrice = records[-1]["Close"]
self.lastPrice = lastPrice
if self.marketPosition == 0 :
self.status["stopPrice"] = "--"
self.status["leavePrice"] = "--"
self.status["upLine"] = 0
self.status["downLine"] = 0
for i in range(2):
if i == 0 and self.useFilter and not self.preBreakoutFailure:
continue
enterPeriod = self.enterPeriodA if i == 0 else self.enterPeriodB
if len(records) < (enterPeriod+1):
continue
highest = TA.Highest(records, enterPeriod, "High")
lowest = TA.Lowest(records, enterPeriod, "Low")
self.status["upLine"] = highest if self.status["upLine"] == 0 else min(self.status["upLine"], highest)
self.status["downLine"] = lowest if self.status["downLine"] == 0 else max(self.status["upLine"], lowest)
if lastPrice > highest:
opCode = 1
elif lastPrice < lowest:
opCode = 2
self.leavePeriod = self.leavePeriodA if (enterPeriod == self.enterPeriodA) else self.leavePeriodB
else:
spread = (self.openPrice - lastPrice) if self.marketPosition > 0 else (lastPrice - self.openPrice)
self.status["stopPrice"] = _N(self.openPrice + (self.N * StopLossRatio * (-1 if self.marketPosition > 0 else 1)))
if spread > (self.N * StopLossRatio):
opCode = 3
self.preBreakoutFailure = True
Log(self.symbolDetail["InstrumentName"], "止损平仓", suffix)
self.status["st"] += 1
elif -spread > (IncSpace * self.N):
opCode = 1 if self.marketPosition > 0 else 2
elif len(records) > self.leavePeriod :
self.status["leavePrice"] = TA.Lowest(records,self.leavePeriod,"Low") if self.marketPosition > 0 else TA.Highest(records,self.leavePeriod,"High")
if (self.marketPosition > 0 and lastPrice < self.status["leavePrice"]) or (self.marketPosition < 0 and lastPrice > self.status["leavePrice"]):
self.preBreakoutFailure = True
Log(self.symbolDetail["InstrumentName"] , "正常平仓", suffix)
opCode = 3
self.status["cover"] += 1
if opCode == 0:
return
if opCode == 3:
def coverCallBack(ret):
self.reset()
_G(self.symbol,None)
self.setTask(Manager.ACT_COVER,0,coverCallBack)
return
if abs(self.marketPosition) >= self.maxLots:
self.setLastError("禁止开仓,超过最大开仓次数" + str(self.maxLots))
return
atrs = TA.ATR(records,self.atrLen)
N = _N(atrs[len(atrs) - 1],4)
account = _bot.GetAccount()
currMargin = json.loads(exchange.GetRawJSON())["CurrMargin"]
unit = int((account["Balance"] + currMargin - self.keepBalance) * (self.riskRatio / 100) / N / self.symbolDetail["VolumeMultiple"])
canOpen = int((account["Balance"] - self.keepBalance) / (self.symbolDetail["LongMarginRatio"] if opCode == 1 else self.symbolDetail["ShortMarginRatio"]) / (lastPrice*1.2) / self.symbolDetail["VolumeMultiple"])
unit = min(unit,canOpen)
if unit < self.symbolDetail["MinLimitOrderVolume"]:
self.setLastError(str(unit) + "手,无法开仓")
return
def setTaskCallBack(ret):
if not ret :
self.setLastError("下单失败")
return
Log(self.symbolDetail["InstrumentName"], "开仓" if self.marketPosition == 0 else "加仓", "离市周期", self.leavePeriod, suffix)
self.N = N
self.openPrice = ret["price"]
self.holdPrice = ret["position"]["Price"]
self.holdAmount = ret["position"]["Amount"]
if self.marketPosition == 0 :
self.status["open"] += 1
self.marketPosition += (1 if opCode == 1 else -1)
self.status["vm"] = [self.marketPosition, self.openPrice, self.N, self.leavePeriod, self.preBreakoutFailure]
_G(self.symbol,self.status["vm"])
self.setTask(Manager.ACT_LONG if opCode == 1 else Manager.ACT_SHORT, unit, setTaskCallBack)
def onexit():
Log("已退出策略....")
def main():
while not exchange.IO("status"):
Sleep(3000)
LogStatus("正在等待与交易服务器连接")
positions = _C(exchange.GetPosition)
if len(positions) > 0 :
Log("检测到当前持有仓位,系统开始恢复进度....")
Log("持仓信息:",positions)
Log("风险参数:",RiskRatio, "N值周期:",ATRlength, "系统1:入市周期",EnterPeriodA, "离市周期",LeavePeriodA, "系统2:入市周期",EnterPeriodB, "离市周期",LeavePeriodB,"加仓系数:",IncSpace,"止损系数:",StopLossRatio,"单品种最多开仓",MaxLots,"次")
initAccount = _bot.GetAccount()
initMargin = json.loads(exchange.GetRawJSON())["CurrMargin"]
keepBalance = _N((initAccount["Balance"] + initMargin) * (keepRatio / 100), 3)
Log("资产信息:",initAccount, "保留资金:",keepBalance)
tts = []
symbolFilter = {}
arr = Instruments.split(",")
for i in range(len(arr)):
symbol = re.sub(r'/\s+$/g', "", re.sub(r'/^\s+/g', "", arr[i]))
if symbol in symbolFilter.keys():
raise Exception(symbol + "已经存在,请检查合约参数")
symbolFilter[symbol] = True
hasPosition = False
for j in range(len(positions)):
if positions[j]["ContractType"] == symbol :
hasPosition = True
break
obj = Manager(hasPosition,symbol,keepBalance,RiskRatio,ATRlength,EnterPeriodA,LeavePeriodA,EnterPeriodB,LeavePeriodB,UseEnterFilter,IncSpace,StopLossRatio,MaxLots)
tts.append(obj)
while True:
while not exchange.IO("status"):
Sleep(1000)
LogStatus("正在等待与交易服务器连接")
tblStatus = {
"type" : "table",
"title" : "持仓信息",
"cols" : ["合约名称","持仓方向","持仓均价","持仓数量","持仓盈亏","加仓次数","开仓次数","止损次数","成功次数","当前价格","N"],
"rows" : []
}
tblMarket = {
"type" : "table",
"title" : "运行状态",
"cols" : ["合约名称","合约乘数","保证金比率","交易时间","柱线长度","上线","下线","止损价","离市价","异常描述","发生时间"],
"rows" : []
}
totalHold = 0
vmStatus = {}
holdSymbol = 0
for i in range(len(tts)):
tts[i].Poll()
d = tts[i].Status()
if d["holdAmount"] > 0 :
vmStatus[d["symbol"]] = d["vm"]
holdSymbol += 1
tblStatus["rows"].append([d["symbolDetail"]["InstrumentName"], "--" if d["holdAmount"] == 0 else ("多" if d["marketPosition"] > 0 else "空"), d["holdPrice"], d["holdAmount"], d["holdProfit"], d["marketPosition"], d["open"], d["st"], d["cover"], d["lastPrice"], d["N"]])
tblMarket["rows"].append([d["symbolDetail"]["InstrumentName"], d["symbolDetail"]["VolumeMultiple"], str(d["symbolDetail"]["LongMarginRatio"]) + "/"+ str(d["symbolDetail"]["ShortMarginRatio"]), "是#0000ff" if d["isTrading"] else "否#0000ff", d["recordsLen"],d["upLine"], d["downLine"], d["stopPrice"],d["leavePrice"],d["lastErr"],d["lastErrTime"]])
totalHold += abs(d["holdAmount"])
lastStatus = "`" + json.dumps([tblStatus, tblMarket]) + "`\n" + "当前时间:"+_D() + ",持有品种个数:" + str(holdSymbol)
if totalHold > 0 :
lastStatus += "\n手动恢复字符串:" + json.dumps(vmStatus)
LogStatus(lastStatus)
Sleep(LoopInterval * 1000)