前言
本主要讨论BackTrader中的Broker定制化。如果你已经对于BackTrader的交易管理非常熟悉,并且自己有了成熟的适配方案,那么并不需要看这篇文章。但是如果你还没有深入研究过,那么这篇文章可能会给到你启发。
背景与需求
网上现存大量资源详尽介绍了BackTrader的订单和交易模块。笔者无意再去额外写一篇冗余的基础教程来污染中文网络。如果读者对于交易管理相关模块尚未不熟悉,可以参考对应的官网教程Broker - Backtrader、Orders - General - Backtrader以及搜索包括CSDN在内的大量中文资源讲解。
笼统的讲,BackTrader提供了一个Broker模块来模拟处理资金、账户、买卖等等操作。同时BackTrader也提供了Live Trading - Oanda v1.0 - Backtrader等其他对接一些国外外汇交易商的Broker接口。
但目前BackTrader提供的bt.BackBroker的实现直接使用和适配起来却略显复杂和缺乏灵活性。
另一方面,如果我们想要对接CTP的模拟或者实盘接口,显然目前BT并没有直接提供一个合适的Broker来提供这个功能。
更进一步的,BackTrader提供的Order类型包含了大量和外汇交易紧密相关的概念和逻辑,这对于我们的需求属实有些多余,也十分容易引起混淆。强行去将一些概念与我们的内盘期货交易习惯相对应也非常多余。
定制化Broker
出于打造自己的回测和交易框架系统的目的,有必要定制一个独立的Broker。这个定制化的Broker可以接管包括订单、TPSL、资金管理、基础统计等全部功能。当然我们不会从头书写这个类,因为这将脱离框架的一些自动化流程,而是采用继承BackTrader的bt.BrokerBase类的方式。
虽然我们的Broker主要专注于回测,但是掌握了改写方法,也很容易实现一个针对CTP模拟或者实盘交易的Broker适配器。
定制化的根本的原因在于,实现起来非常简单且清晰,高度可控。毕竟交易的事情,自己全拿在自己手里才是最稳妥的!而回测,越是按照市场规则的定制,越准确!
本文仅分析框架的类层次和如何继承和定义一个定制化的Broker,不会讨论Broker实现细节。
BackTrader的交易管理
首先,关于交易管理BackTrader提供了Order用于表示订单数据,Broker处理交易的各种操作,一个简单的类层次关系如图所示:
上图左侧的几个Broker是BackTrader用于交易的几个实现,其中IBBroker更像一个通用的交互式可用于实盘交易的实现。如果感兴趣直接使用这个Broker可以参考:Live Trading - Interactive Brokers - Backtrader
但是即使直接使用这个Broker,灵活性依然不足,因为市场差异本身较大,仍然需要大量的定制化工作。而这些定制化工作所需要的时间,并不会比直接继承bt.BrokerBase,进而自行实现一些逻辑来的轻松。
首先简单地了解一下目前框架所提供的实体,并分析我们需要哪些功能,不需要哪些功能。
订单实体Orders
如上图所示,订单的类层次机构结构如图所示,从下往上:
- SellOrder和BuyOrder:指明了Order的多空类型,并直接继承了Order类
- Order类:处理了拆单、超时和移动止损三个部分。
- OrderBase类:
- 核心实现,实际数据的存储封装在了OrderData类中,
- 实现中使用created(OrderData)和executed(OrderData)两个属性分别作为挂单和成交单。
其中Order类在BackTrader的回测Broker中是需要的,因为这个类中模拟了执行订单的行为。后面会提到,我们更希望将订单的状态、行为、操作等切换成为i更类似于内盘期货市场的风格,这样更加符合我们一般在交易软件中的操作习惯,比如文华。并且把交易的所有操作都提升到Broker层面去执行,Orders仅做为数据载体。这样的好处在于:
- 回测过程中,更方便去利用Tick中的挂单数量信息实现真实模拟;
- 在模拟和实盘中,可以利用到CTP返回的成交结果。
我们将继承OrderBase类,在本文中会定制一个非常简单的MyOrder类型。
- 注意,完全可以在生产中舍弃框架的Order类。实例中继承OrderBase类仅仅是为了少写一点代码。
经纪商Broker
BackTrader提供的Broker接口会在整个策略运行的生命周期中被使用。BrokerBase类也包含了start、stop、next等生命周期方法,他们在策略运行的对应生命周期时被调用。
交易的核心方法是buy和sell,同时为了保留扩展性,BrokerBase类的实现中为这两个接口预设了可变参数**kwargs。可见BackTrader最初的设计是鼓励这种继承BrokerBase类来实现定制化的Broker的魔改行为的。
我们可以覆盖BrokerBase类的绝大多数方法。显然,包括与手续费相关的计算也会被完全重写。
另外,BackTrader糟糕的将一些没有在BrokerBase类中定义的接口也在其他地方进行了调用。这很糟糕。我们已经初步整理了这些接口,并且也会实现这些接口。下文中使用一个空的MyBroker定义代码来展示所需要实现的接口。
MyBroker
示例策略
我们首先结合我们之前的tick数据源写一个基于五分钟均线的示例策略程序,策略逻辑很简单,5bar均上穿20bar均则买入,下传则卖出(为了方便调试,不同于官网示例是,没有使用框架的CrossOver函数)。这是非常简单的操作。部分代码如下:
class TestStrategy(bt.Strategy):
def __init__(self):
"""
为管线数据取别名
"""
# ...略
def next(self):
dt = self.get_dt_str()
if self.sma5_5[-1] is None:
return
if dt == self.last_dt5:
return
mean_val = np.mean([self.bid[0], self.sma5_5[0], self.sma5_20[0]])
delta = abs(mean_val - min(self.ask[0], self.sma5_5[0], self.sma5_20[0]))
# 在5分钟级别上执行双均线系统
if self.sma5_5[-1] < self.sma5_20[-1] and self.sma5_5[0] > self.sma5_20[0]:
print('5 cross over 20, buy')
tp = self.ask[0] + 10 * delta
sl = self.ask[0] - 5 * delta
orders = self.buy_bracket(price=self.ask[0],
stopprice=sl,
limitprice=tp
)
self.orders.append(order.to_dict())
elif self.sma5_5[-1] > self.sma5_20[-1] and self.sma5_5[0] < self.sma5_20[0]:
print('5 cross over 20, sell')
tp = self.bid[0] - 10 * delta
sl = self.bid[0] + 5 * delta
orders = self.sell_bracket(price=self.bid[0],
stopprice=sl,
limitprice=tp
)
这是一个非常naive且直白的双均线策略。忽略五分钟KBar的没有完成的情况。在五分钟K线完成后,如果M5均线向上穿M20则买入,如果M5向下穿M20则卖出,同时每笔交易都携带了止盈止损。相信写过策略的朋友都非常熟悉这种入门策略了。
这段程序运行很正常。但是有一些需要调整的地方:
- 止盈止损创建额外订单
我们在代码中设置了止盈止损,但是似乎是出于一致性和简化API的原因,这两个订单被当框架做Stop和Limit订单创建了实际的Order。无论对于回测的Broker、还是模拟或者实盘,这种实现方式都过于粗放,并且不符合我们“条件单”的习惯,而对于交易分析也会比较容易混淆和混乱。
在MyBroker中,我们会将止盈止损作为订单的一部分,同时交易过程完全由Broker接管并统一调度。因为使用了这个改动,我们也不会在使用*_bracket的两个API,而是直接使用简单的buy和sell接口。之所以这样做的原因可以从框架源码中strategy.*_bracket看到。
- 杠杆和倍数
虽然示例中没有用到,但是杠杆和倍数是需要的。我们有过外汇交易经验的朋友都知道,乘数和我们熟悉的期货一样,代表一手多少倍,同时交易商还可以支持一个杠杆率,可能是100~400。这个我们内盘商品是没有的。
我们应在MyBroker中定义不同品种对应的倍数,然后将倍数计算后结果添加到资金计算中。
- 爆仓
虽然不希望看到,但是保证金不足时停止策略运行有助于我们提高回测效率,所以我们需要这个检查,不然的话,如果明明已经爆仓了,还在继续运行策略,也和现实有些出入。
这就需要我们为MyBroker添加针对保证金和总资金的额度检测,当爆仓后,立刻停止运行回测。
- 安全锁
为了整个软件系统最终能够快速无缝切换进入实盘,我们可以考虑可以在Broker中增加额外的安全锁,如果亏损到达一定数量,则强制锁定所有交易接口。这可以在我们的程序出现BUG时,及时阻止任何可能得损失。产品级别的实现中,安全锁的添加应该独立于交易框架,这样可以避免程序崩溃后或者某个地方跑入死循环,能够及时停止程序阻止交易继续进行。
继承BrokerBase
本系列文章中的继承自BrokerBase的MyBroker主要目的只是回测。篇幅和其他一些很好猜到的原因,此处不给出具体实现,所有实际的内容则会留空,如下:
class MyBroker(bt.BrokerBase):
def __init__(self, **kwargs):
super(MyBroker, self).__init__()
pass
def set_fund_history(self, fund):
pass
def setcash(self, cash):
pass
def getcash(self):
pass
def get_notification(self):
return None
def getvalue(self, datas=None):
pass
def getposition(self, data):
pass
def add_order_history(self, orders, notify=False):
pass
def submit(self, order):
pass
def cancel(self, order):
pass
def buy(self, owner, data, size, price=None,
plimit=None, exectype=None, valid=None, tradeid=0, # 不使用这些参数
oco=None, trailamount=None, trailpercent=None, # 不使用这些参数
**kwargs):
pass
def sell(self, owner, data, size, price=None,
plimit=None, exectype=None, valid=None, tradeid=0, # 不使用这些参数
oco=None, trailamount=None, trailpercent=None, # 不使用这些参数
**kwargs):
pass
# =========================================
# 新增方法
def start(self):
pass
def stop(self):
pass
def next(self):
# 刷新并计算Value和cash
# 检查挂单成交情况
# 根据tick值触发订单TPSL
pass
注意next函数的注释解释了这个函数需要做的工作。订单的管理可以完全在自定义的Broker中实现,从而实现定制化交易管理的功能。
结语
本文简略介绍了BT中Broker的框架,同时介绍了应该定制化自己的Broker类。文中没有给出具体实现方式,这里的具体实现并不复杂,主要是订单的管理以及资金的刷新。注意,实现时候一个能改关注周期级别,同时为了支持更灵活的分析器使用,一些分析器会用到的接口必须要实现。