资源加载中... loading...

Python商品期货量化入门教程

Author: 雨幕(youquant), Created: 2024-09-12 09:11:33, Updated: 2024-09-12 09:17:23

实比较多,因此逐句进行讲解可能会耽误大家太多的时间,因此,海龟策略的讲解重点将放在策略整体的设计框架,但是我们也会讲解到每个函数的功能以及具体的代码逻辑。其实大部分的知识都是我们所讲过的。策略的设计架构在量化交易系统中起着重要的作用。作为初学者,我们常常过于关注策略的盈利能力,而忽视了设计架构的重要性。一个良好的架构,在升级功能,调试测试,和扩展优化都是非常方便的,并且不容易出现潜藏BUG。在同时,一个好的设计架构可以让策略交易逻辑和策略下单处理逻辑等其它与策略不相关的功能代码,进行很好的分离。这些代码耦合很低,所以非常容易修改,当然前提是要在通读过策略,完全理解策略架构之后。除去海龟策略,还有很多的多品种交易策略,比如均线策略,R-Breaker策略等等,其实我们完全可以把原版策略中,和交易策略相关的内容,分离出来删除掉,只留下一个多品种策略框架,就可以根据自己的需求进行其他策略的开发。

首先,我们来设置策略的参数,在了解完这些参数以后,我们可以对策略的整体脉络有一个程序化的认识。

变量 描述 类型 默认值
Instruments 合约列表 字符串(string) MA888,pp888,v888,rb888,jm888
LoopInterval 轮询周期(秒) 数字型(number) 3
RiskRatio % Risk Per N (0 - 100) 数字型(number) 1
ATRLength ATR计算周期 数字型(number) 20
EnterPeriodA 系统一入市周期 数字型(number) 20
LeavePeriodA 系统一离市周期 数字型(number) 10
EnterPeriodB 系统二入市周期 数字型(number) 55
LeavePeriodB 系统二离市周期 数字型(number) 20
UseEnterFilter 使用入市过滤 布尔型(true/false) false
IncSpace 加仓间隔(N的倍数) 数字型(number) 0.5
StopLossRatio 止损系数(N的倍数) 数字型(number) 2
MaxLots 单品种加仓次数 数字型(number) 4
Push 推送交易信息 布尔型(true/false) true
KeepRatio 预留保证金比例 数字型(number) 20
RMode 进度恢复模式 下拉框(selected) 自动
VMStatus@RMode==1 手动恢复字符串 字符串(string) {}

对于海龟交易策略的各个参数,我们是这样设置的:

  • Instruments(合约列表):指定要交易的合约列表,以逗号分隔不同合约的代码。
  • LoopInterval(轮询周期):指定策略运行轮询休息周期。
  • RiskRatio(% Risk Per N):表示每个交易单元风险的百分比。N代表ATR(平均真实波动幅度)的值。用于确定每个交易单元的头寸规模。
  • ATRLength(ATR计算周期):用于计算ATR指标的时间周期长度。ATR是一种衡量市场波动性的指标,用于确定止损和加仓的位置。
  • EnterPeriodA(系统一入市周期):在系统一中,进入市场的时间周期长度。当市场价格突破该周期内的最高价时,产生进场信号。
  • LeavePeriodA(系统一离市周期):在系统一中,离开市场的时间周期长度。当市场价格跌破该周期内的最低价时,产生出场信号。
  • EnterPeriodB(系统二入市周期):在系统二中,进入市场的时间周期长度。当市场价格突破该周期内的最高价时,产生进场信号。
  • LeavePeriodB(系统二离市周期):在系统二中,离开市场的时间周期长度。当市场价格跌破该周期内的最低价时,产生出场信号。
  • UseEnterFilter(入市过滤):是否采用入市过滤。
  • IncSpace(加仓间隔):加仓的价格间隔,以N倍的ATR为单位。在每次加仓时,头寸规模将乘以IncSpace。
  • StopLossRatio(止损系数):止损价格相对于入场价格的距离。以N倍的ATR为单位。当市场价格跌破止损价格时,触发止损。
  • MaxLots(单品种加仓次数):允许的最大加仓次数。超过该次数后将不再进行加仓。
  • WXPush(推送交易信息):确定是否推送交易信息到微信或其他渠道。
  • KeepRatio(预留保证金比例):在计算可用保证金时的预留比例。用于确保保证金余额不会过低,以应对不利行情。
  • Mode和VMStatus@RMode,是选择进度恢复模式,手动还是自动,如果是自动的话,可以使用_G进行读取;如果是手动,可以填写字符串,然后使用JavaScriptON.parse函数进行读取。

这些参数可以根据具体的交易策略和市场情况进行调整,以达到更好的交易结果。

8.6.2 多品种海龟交易策略框架

我们来继续海龟策略的编写。进入我们的代码,这段代码确实比较复杂,具体的变量和函数有很多,我们先来看一下策略的框架。整体来看,海龟策略的代码分为两个板块,第一个是交易逻辑类对象 Manager。作为一个交易逻辑对象类,这个对象主要就是用来构造海龟交易逻辑对象的。整个的「海龟交易法则」用代码表达的部分都封装在这个部分。

第二个部分是main主函数。在主函数中,主要包括了while循环之前的程序初始化部分的设置工作,在这里会调用交易逻辑对象 Manager,使用轮询的方式,构造每个要交易的合约对应的海龟交易逻辑对象。下面是while循环,该循环为策略的主要循环,这一部分主要是遍历所有的海龟交易逻辑对象,调用每个海龟交易逻辑对象的处理函数进行相应的交易操作,最后一部分进行了策略运行时的界面显示的设计。可以看到,这里把海龟交易逻辑相关的操作都完全独立了出来,让整个策略层次比较分明。

class Manager:
    # 定义常量...
    # 定义类函数...
        
def main():
    # 主函数实现...

8.6.3 Manager交易对象

首先,我们来看第一部分的代码。相对于以往我们在主函数中编写策略逻辑,这段代码实现了一个名为 Manager 类,其中包含各种海龟交易逻辑所需的属性和方法。通过调用Manager,可以创建一个新的合约交易对象。

这里我们稍作补充,使用类在策略设计中的好处:

  • 更符合面向对象的编程思想:类是面向对象编程的核心概念,通过类可以更好地模拟现实世界中的对象,使得代码结构更加清晰和易于理解。

  • 封装性和抽象性:类能够将数据和方法封装在一起,实现了数据和行为的关联性,提高了代码的封装性和抽象性。

  • 继承和多态:类支持继承和多态,可以通过继承现有的类来创建新的类,并且可以重写父类的方法以实现不同的行为,提高了代码的复用性和灵活性。

  • 代码组织和管理:类使得代码组织更加结构化,能够更轻松地管理和维护代码,提高了代码的可维护性和可扩展性。

  • 更好的代码复用和扩展性:类的属性和方法可以在不同的对象之间共享和复用,通过创建新的子类或者扩展现有类,可以轻松地实现新的功能,提高了代码的复用性和扩展性。

综上所述,使用类能够更好地体现面向对象编程的特性,使得代码更加模块化、可维护性更高、扩展性更强。

这个 Manager 类是一个用于管理交易活动的类。让我们来解释一下它的功能和工作原理:

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)
  1. 常量

    • ACT_IDLE, ACT_LONG, ACT_SHORT, ACT_COVER:这些常量定义了管理器可以执行的不同操作或状态。例如,ACT_IDLE表示空闲状态,ACT_LONGACT_SHORT表示开多和开空仓位,而ACT_COVER表示平仓或关闭仓位。
    • ERR_SUCCESS, ERR_SET_SYMBOL, ERR_GET_ORDERS等:这些常量是类在操作过程中可能遇到的错误代码。errMsg列表提供了与这些错误代码相对应的可读的错误消息。
  2. 初始化方法 (__init__)

    • 初始化时,它接受一系列参数,包括是否需要恢复、交易合约、风险管理相关参数等。
    • 它检查交易合约的详细信息,如成交量倍数、最大/最小限价订单量、保证金比率等,并在初始化时记录这些信息。
    • 还初始化了内部变量和字典,用于跟踪任务和状态。
  3. 辅助方法

    • setLastError:设置管理器遇到的最后一个错误。
    • reset:重置管理器的状态,可选择根据提供的相关参数将其恢复到先前的状态。
    • Status:获取管理器的当前状态,包括持仓情况、利润等。
    • setTask:为管理器设置新任务,例如开仓,加仓或平仓。
    • processTask:处理为管理器设置的当前任务,这涉及与交易所的具体的交易操作。
    • Poll:轮询市场条件,并根据海龟交易策略决定行动。
  4. 主要逻辑

    • 类中的 Poll 方法是核心逻辑,它根据市场条件执行不同的操作。包括具体交易直播的计算,以及开仓信号的判断。
    • 通过分析市场数据和预定义的交易策略,它可以决定是否开仓、加仓、平仓,或者维持空闲状态。
    • 交易操作涉及到与外部交易所的交互,包括下单、获取持仓、获取市场深度等操作。
  5. 错误处理

    • 类中有一些错误代码和错误消息的定义,用于处理可能出现的异常情况。
    • 在发生错误时,管理器会记录错误信息,并根据需要采取相应的措施,如重新尝试或报警。

综上所述,这个 Manager 类是一个用于管理交易活动的抽象类,它封装了交易策略的执行逻辑和与交易所的交互过程。不过这里重要的是作为一个实盘级别策略,其中一些策略设计,比如关于错误信息显示,状态变量保存和恢复,具体交易信号的获取和交易操作执行的函数设计,都是值得我们学习和探索的地方,大家可以应用到自己的实盘策略编写技巧当中。

8.6.4 海龟交易逻辑主函数

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)

这段代码是一个主函数 main(),主要是用于初始化交易环境、创建交易管理对象,并在主循环中监控市场情况和持仓状态,以及进行相应的交易操作,具体它执行以下操作:

  1. 等待交易服务器连接

    • 在进入主循环之前,首先等待与交易服务器的连接建立。如果连接未建立,它会每隔3秒检查一次,直到连接建立为止。
  2. 获取初始持仓信息

    • 通过调用交易所的 GetPosition 函数获取当前的持仓信息,并记录到 positions 变量中。
    • 如果检测到有持仓信息,会记录下来并显示在日志中。
  3. 初始化账户和保留资金

    • 获取账户初始信息,包括余额和当前已用保证金。
    • 根据预设的保留比例计算需要保留的资金。
  4. 创建交易管理对象

    • 针对每个指定的交易合约,创建一个 Manager 对象,用于管理该合约的交易活动。
    • 检查是否已经持有该合约的仓位,并根据情况初始化 Manager 对象。
  5. 主循环

    • 进入一个无限循环,不断检查交易服务器的连接状态和市场情况。
    • 对每个交易合约执行 Poll 操作,更新交易状态和市场情况。
    • 将持仓信息和市场状态以表格的形式展示,并记录在 tblStatustblMarket 中。
    • 更新日志状态,显示最新的持仓信息和市场状态,以及持有的交易品种数量。
    • 如果有持仓,则记录手动恢复字符串,用于手动恢复持仓状态。
    • 根据设定的循环间隔(LoopInterval)等待一段时间后,重新开始下一轮循环。

通过这些完善的设计,希望能够有效地帮助大家理解多品种海龟交易策略。海龟交易逻辑并不复杂,但是怎样将这种逻辑使用程序的语言一步步搭建起来,确实考验我们的能力和耐心,我们要做好各个品种,各个交易环节,程序的容错以及结果呈现等一系列的工作,希望通过本策略学习,可以帮助大家了解一下一个真正的实盘级别的大模型应该怎样搭建。当然,这对比于一个工业级的实盘大模型确实比较稚嫩,这里重要的是,理解并掌握这里的程序设计,学会一个复杂模型的搭建方式,希望大家都有所领会。

第9章 用科学的方法进行回测

前面的章节详细介绍了如何开发一个交易策略,包括 CTA 趋势策略和震荡策略。一个新开发出来的交易策略,需要全方位检测才能应用于实战,同样一个优秀的策略也是在试错中不断改进得以产生。因此对于平台的回测机制,回测优化手段,我们需要全面认识,以便更好的进行回测。

9.1 回测数据级别

量化交易回测的目的是还原交易过程,进而验证策略的逻辑和可行性,所以回测的准确性尤为重要。CTA 策略有很多种风格,从频率上来讲:有高频策略、中低频策略。从另一个角度来讲:有日内策略,也有隔夜策略。通常不同类型策略对于回测时选用的数据也是不同的。

9.1.1 回测需要哪些数据

如何做到精准回测是很多量化交易者关心的问题,那么首先要弄清楚回测中都有哪些数据,因为数据的质量很大程度上已经决定了回测的质量。常见的数据有开盘价、最高价、最低价、收盘价、成交量等等,这些统称为 K 线数据。

另外还有一种原始的 Tick 快照,如果把交易所内的数据想象成一条河流,其中包含每个订单的详细数据,那么 Tick 快照就是这个数据流中的某个切片,频率是每秒 2 次,是当时某一时刻市场交易情况的再现。

事实上 K 线数据就是基于 Tick 数据合成的,按照时间周期 1 分钟的 K 线数据是由 1 分钟内的 Tick 数据组成,5 分钟的 K 线数据是由 5 分钟内的 Tick 数据组成,以此类推…形成了各种分钟图、小时图、日线图等等。这就意味着一分钟的 K 线数据可能包含 120 个 Tick 数据。因此,回测的历史数据可以分为:K 线数据和 Tick 数据,并且在同一个周期内 Tick 的数据量要比 K 线数据量大很多,理论上 Tick 数据比 K 线数据回测更加准确。

9.1.2 基于 Bar 数据的回测

市面上量化交易软件几乎都支持 K 线数据的回测,由于数据量少,大大简化了回测引擎的工作量,所以这种回测通常都非常快,十年左右的数据几秒之内就能回测完,甚至叠加几十个期货品种回测也不会超过一分钟。但是 K 线数据回测有很多问题:

极端价格 做过交易的人都知道,在涨停中很难买入,在跌停中很难卖出,但是在回测中是可以成交的,一些做量化交易的新手,如果不在策略中对涨跌停价进行过滤,回测的结果会与实盘不一致。

价格真空 从跌停瞬间到涨停或者价格跳空上涨,在大周期 K 线图上看是一根大阳线,但是这根 K 线中间的实质挂单很少,如果是即时价成交的策略,在 Bar 数据上回测,是可以成交的。举个例子,当前 K 线一直在 5000 价格附近徘徊,临近收盘瞬间涨至 5100,并且中间几乎没有挂单和成交。如果策略信号在这根 K 线上是 5050 开仓,那么在 K 线数据回测中是可以成交的。

过去和未来的数据 理论上 K 线的形成可能是:开盘价>>>最低价>>>最高价>>>收盘价。但实际上它有可能先创新高,再创新低,再收盘;也有可能先创新低,再创新高,再收盘;甚至也可能一波三折先创新低,再创新高,再创新低,再拉高收盘;表面上看是一根有上影线和下影线的 K 线,中间的过程有很多种可能。

假如有一根 K 线是这样的:开盘价 4950、最低价 4900、最高价 5100、收盘价 5050,一根普通阳线。策略是:如果最新价超过前期高点 5000 就买入,买入后设置 1%的止损,也就是价格跌破 4950 就止损。

模拟回测:开盘 4950>>>价格超过前期高点 5000>>>信号成立买入开仓>>>收盘时赚了 1%;但真实的情况可能是这样的:开盘 4950>>>价格超过前期高点 5000>>>信号成立买入开仓>>>不久价格开始下跌>>>继续下跌至 4949>>>止损信号成立卖出平仓亏损 1%>>>价格上涨 5100>>>价格下跌至 5050 收盘。同样的策略,在 K 线数据回测和真实的交易中,会有两种截然不同的结果。

9.1.3 基于 Tick 数据的回测

如果能用 Tick 数据来做回测和分析,无疑具有很大的优势,但目前市面上很少能真正做到,有些量化交易软件只是使用了 Tick 价格,并没有使用 Tick 挂单量,可能造成见价成交的现象。比如当前的 Tick 数据是:卖价 5001、买价 5000,如果挂的买单是 5000,结果肯定是买不到的,但事实并非如此。

在真实的交易环境中,订单是在交易所的 Tick 数据流中完成撮合的,交易所的撮合规则是:价格优先、时间优先。如果此时盘口订单只要不是太厚,那么所挂的 5000 买单,是有可能被动成交的。

9.1.4 盘口数据回测引擎原理

真正的 Tick 数据回测不仅根据 Tick 数据的价格优先来撮合订单,还根据价格相同时间优先,通过计算盘口挂单量,来判断当前挂单是否达到被动成交的条件实现见量成交,以此做到真正的模拟实盘环境。以下图为例:

image

首先第 1 个 Tick 买价是 100,挂单量是 30 手;此时产生了买入信号,以 100 的价格买入 20 手等待被动成交;第 2 个 Tick 产生了,买价是 100,挂单量是 50 手,这里面有我们 20 手的挂单;第 3 个 Tick 产生了,买价是 100,挂单量是 30 手,这证明已经有 20 手买单被成交了或者撤单了,我们离成交又近了一步;第 4 个 Tick 产生了,买价是 100,挂单量是 10 手,是一个大卖家,一下子把我们买单全部成交。

通过上面的例子,我们可以发现,在 Tick 数据中,价格未变的前提下,可以通过盘口挂单量的变化,来推算自己的挂单有没有被动成交。利用的就是价格相同,时间优先的方法。这种回测引擎几乎最大程度的仿生了真实的实盘交易环境,杜绝了见价成交和虚假成交,让每一个盘口数据真实回放,使回测与实盘最大可能的一致。

9.1.5 如何选择最佳回测方式

通常中低频策略交易次数不多,滑点成本对策略的影响较小,对数据精度要求也不是太高,所以一般情况下使用 K 线数据回测,只需要在回测的时候加上几跳的滑点就可以,真正需要注意的是过度拟合的问题。

有些日内交易或者涉及到日内开平仓交易的策略,如果有必要,也可以在回测的配置参数页面上调整数据粒度,比如在 1 小时周期上回测,可以调整为更精细的 15 分钟数据力度。必要时也可以使用 Tick 级别的数据,来提高回测的精准度。

注意:如果是隔夜的中低频策略尽量以商品指数为数据,如果是日内的中低频策略尽量以商品主力连续为数据。

高频交易因为策略交易的次数足够多,单品种一天就能交易几十甚至上百次,所以只要撮合引擎是合理的,那么在大数定律的作用下,回测的结果基本靠谱,一般不存在过度拟合的问题。但是由于高频交易的次数很多,因此就需要对回测数据有非常高的要求。

因为在高频交易回测中,交易频率越高,持仓的时间周期就越短,单笔的平均利润就越低,此时如果回测引擎设计的不合理,或者撮合方式与真实的交易环境不一样,那么就会出现差之毫厘,谬以千里的现象,所以对于高频交易来说,盘口级别的回测引擎是不二之选。

9.2 回测绩效报告详解

量化交易与主观交易最重要的区别之一是量化交易通过历史数据复盘,得出一系列绩效报告,交易者可以从报告中发现策略的缺点来优化和改进策略。当然优化和改进策略的前提是需要对策略有一个正确的评价,那么就需要对回测的绩效报告有一定的了解。只有正确解读回测绩效报告,才能知道策略需要改进的方向。

9.2.1 年化收益率

年化收益率表示投资期限为一年的理论收益率,日收益率、月收益率、季度收益率都可以换算成年收益率。如果一个策略回测的日收益率是 0.01%,那么年化收益率是 3.65%。其计算公式为:(收益 / 本金) / 投资天数 * 250 ×100%

注意:策略回测的年化收益率不是从开始开仓的时候算起,而是从数据开始日期算起。实际上年化收益率代表了策略的盈利效率。另外期货市场其有效的投资时间是一年的交易日,扣除节假日约等于 250 天。

9.2.2 年化波动率

波动率是衡量策略风险的指标之一,它是描述策略资金曲线的涨跌幅程度,是对策略稳健性的衡量,也反映了策略风险水平。其计算方式为:(最高价 - 最低价) / 最低价所得到的比率,年化波动率就是每日波动标准差的年化。

波动率越高,其资金曲线的波动越激烈,策略的稳健性就越低。波动率越低,其资金曲线的波动越平缓,策略的稳健性就越高。

9.2.3 最大回撤比率

除了波动率外,更能直观反映风险的绩效指标就是最大回撤率。它是统计资金曲线任意周期内最高点到最低点时的回撤幅度的最大值。它是描述策略可能出现的最糟糕情况。最大回撤是一个重要的风险指标,对于量化交易而言,该指标甚至比波动率还重要。

9.2.4 夏普比率

很多人喜欢用收益率衡量一个策略,这个无可厚非,因为从投资交易的角度讲,只要赚钱的策略都是好策略。但是请看下图:

image

左边的策略收益是 100%,右边的策略收益也是 100%,而左边的策略最大回撤是 50%,右边的策略几乎没有回撤,毫无疑问右边的策略要明显好于左边的策略。所以仅仅用收益率评价一个策略是不科学的。

回撤意味着风险,也意味着波动,正确的方式是将收益率和风险都考虑在内,也就是说不但要考虑收益率,更要考虑每承担每一单位风险所产生的超额收益。夏普比率就是一个对收益和风险综合考虑的指标。其公式为:(策略收益率-无风险利率)/策略收益率的标准差

举个例子,假如十年期国债收益率是 3%,而策略回测的收益率是 15%,那么超额收益就是 15%-3%=12%,12%除以 6%的策略收益标准差等于 2。这就意味着交易者每承担 10%的风险,能得来 20%的超额收益。

每个策略回测都可以计算夏普比率,如果值为正数,则表示策略收益大于策略波动风险;如果值为负数,则表示策略波动风险大于策略收益。也就是说在设计策略时要考虑风险,尽量用最小的风险换取最大的回报。

9.3 如何避免回测陷阱

量化交易回测虽然可以快速验证策略在历史数据中是否有效,但很多时候回测并不代表未来能盈利,回测看起来非常好的策略,往往实盘表现不佳。因此需要在策略设计过程中规避回测的陷阱,才能让策略回测反映真实的结果。

9.3.1 未来函数

未来函数就是利用了未来的价格,交易策略如果包含未来函数,在实盘运行时会造成信号闪烁的问题。比如有一个策略逻辑是这样的:当收盘价大于开盘价就买入,当收盘价小于开盘价就卖出。这在回测时是没有问题的,因为收盘价是已经完成,固定不变的数据。

注意:在实盘交易中,收盘价只有在收盘的时候才能固定下来,所以程序会把当前的最新价格当作收盘价,这种利用未来价格的策略,会导致买卖信号频繁出现和消失。如果一个策略的买卖点不是固定的,回测的数据也是没有意义的。

如何避免使用未来函数?最简单的办法是使用滞后的价格,可以把这个策略条件修改为:当上根 K 线收盘价大于开盘价就买入,当上根 K 线收盘价小于开盘价就卖出。因为在无论是在回测中还是在实盘中,上根 K 线始终是已经完成的,这样就可以保证回测与实盘保持一致。

9.3.2 偷价

相反偷价是利用了已经过去的价格,偷价并不会造成信号频繁出现和消失,但是会造成信号无效。比如有一个策略逻辑是:当收盘价大于开盘价就在开盘时买入,当收盘价小于开盘价就在开盘时卖出。

显然这个策略条件在实盘时是不能成交的,当收盘价出现时,开盘价早就过去了。但是在回测中,程序是会以开盘价买入卖出的,这相当于在原本的资金曲线上叠加了一条斜率为正的直线会造成一种非常夸张的回测资金曲线。

为避免这种情况发生,编写完策略首先要检查策略逻辑,如果策略回测的收益曲线非常平滑,回撤极小,就要警惕了。尤其是策略逻辑存在隐蔽性偷价行为,务必在实盘之前先用仿真交易测试一段时间。

9.3.3 成本冲击

实盘交易中为了保证订单能及时成交,通常需要用对手价或者市价下单,商品期货买一价和卖一价至少相差一个点差,如果是交易不活跃的期货合约就需要更多的点差成本。或者当自己的订单量超过市场现有的订单量时,就会造成自己的订单消耗了市场流动性,触动价格朝向不利于自己的方向移动,使交易成本进一步上升。

不仅如此,手续费、极端行情、软硬件系统、服务器响应、网络延迟都会增加实盘的交易成本。尤其是交易频率比较高的策略对受市场冲击成本更大,为了让回测更接近实盘环境,折中的办法是在回测时加上固定 2 跳左右的滑点。

9.3.4 幸存者偏差

幸存者偏差是一种逻辑上谬误,意思是没有意识数据筛选的过程,忽略被筛选掉的数据,只通过筛选后的数据得出与实际偏离的结论。

image

左边的图是一个非常好的交易策略,资金曲线稳稳向上,没有最大回撤。再看看右边这张图,这个资金曲线只是 100 次随机交易回测中表现最好的一个。通过这个例子可以知道,回测也有运气的成分,有时候的回测结果可能这个策略刚好适应了历史数据,再换几个参数或者回测品种就不一定有这么好的结果了。

9.3.5 过拟合

过拟合是统计学中的术语,它是指过于精确地匹配数据特征,以至于无法在其他数据中良好地拟合,量化交易中的过拟合是一种回测时表现很好,实盘中表现较差的现象。

image

上图分别是模型欠拟合、适当拟合、过拟合的素描,实际上策略回测应该像第 2 张图那样在数据中匹配普遍规律,而不是像第 3 张图那样试图匹配所有规律,这样才能在新的数据中更好地适应,否则将会导致策略泛化能力下降。

由于商品期货历史数据有限,所以过拟合问题就更加严重,尤其是对于中低频策略来说,几乎不可能完全避免过拟合,但可以利用下面几种方法来减少拟合:

  1. 减少核心参数
  2. 简化处理逻辑
  3. 增加数据样本
  4. 样本内外测试

如果策略核心参数过多或策略逻辑非常复杂就很容易过拟合历史数据,尤其是当数据样本过少时,不足以策略获得整个全局特征,如果在样本过少的情况下企图验证策略是否有效,无异于坐井观天,可能会把回测数据自身的特性当成所有潜在样本的共性,这样一来策略再面对实盘时就不适应了,当样本数据足够时,就不会被局部特征所迷惑。

9.4 递进和交叉回测

巴菲特曾经说过:“投资市场里,后视镜永远比挡风玻璃让你看得更清楚。”他的投资哲学是,除非投资标的的“过


更多内容

by2022 企业微信加不上啊啊啊啊啊啊

雨幕(youquant) 您好,企业微信满了,您加这个微信: https://www.youquant.com/upload/asset/1780ac4e8b9064c9d7d9a.png