本文将介绍Backtrader的交易系统,包括Order、Broker、Trade和Sizer等和交易相关关键类。
这个有翻译为订单,也有翻译为委托单的,后续统一为订单。
如之前文章所述,Cerebro是Backtrader的关键控制中心,是大脑。而Strategy是大脑的神经,基于数据和分析做出最终的决策,那么这个决策如何由系统的其他部件去成交呢?订单就承担这样的责任,将Strategy的做出的决策转换为由券商(Broker)执行操作的消息,通过如下三个方式完成:
这个在咱们之前的代码中可以看出,通过Strategy的buy,sell和close,返回的是一个Order实例。
可以通过Strategy的Cancel函数取消,参数必须指定操作的Order
另外,Order也可以反向给使用者Strategy回馈信息,通过通知的方式告知订单的成交情况:
使用Strategy的notify_order函数返回订单实例。
以上咱们之前的示例中均有涉及,下面重点从代码的角度看看咱们是如何使用Order。
在介绍Order操作之前,咱们先看看Order类:
class Order(OrderBase):
class OrderBase(with_metaclass(MetaParams, object)):
从代码可以看出Order类继承自OrderBase,而OrderBase直接继承了所有类的最顶层object和元类MetaParams。从继承关系可以看出,Order是一个极普通的类,没有复杂的继承关系,咱们就不画图了。另外,Order也继承了MetaParams,也就是Order的创建会受到元类的控制,MetaParams之前咱们讲过,主要是针对参数的处理。
先看订单的创建,有多单和空单以及对应的平仓订单,通常,订单是在Strategy的next中根据策略来创建。主要体现就是3个函数:buy、sell和close。比如我们之前示例中,最简单的使用方式如下:
self.order = self.buy()
self.order = self.sell()
顺着这个线索,我们看看Backtrader是如何处理的。
def buy(self, data=None,
size=None, price=None, plimit=None,
exectype=None, valid=None, tradeid=0, oco=None,
trailamount=None, trailpercent=None,
parent=None, transmit=True,
**kwargs):
if isinstance(data, string_types):
data = self.getdatabyname(data)
data = data if data is not None else self.datas[0]
size = size if size is not None else self.getsizing(data, isbuy=True)
if size:
return self.broker.buy(
self, data,
size=abs(size), price=price, plimit=plimit,
exectype=exectype, valid=valid, tradeid=tradeid, oco=oco,
trailamount=trailamount, trailpercent=trailpercent,
parent=parent, transmit=transmit,
**kwargs)
return None
首先看函数的关键参数,咱们之前在讲解Strategy的时候详细介绍过,不过为了方便理解Order,咱们还是在这里再提供下:
参数 | 缺省值 | 含义 |
---|---|---|
data | None | 指定本次操作归属的data。每个data记录的是每个标的(股票、期货等等)的数据(open/close…),买卖操作就是基于这些数据。在多个资产(或者证券,包括股票期货等等)的情况下,你可能需要针对不同的数据创建订单。缺省情况就是针对第一个数据(data0)。 |
size | None | 本单买卖的数量。比如说股票,本次你要买卖多少股。有些地方可能有最小限制,比如国内最小一手100股。这个可以通过addsizer的stake指定。size也是很重要的策略,这一单下多少?还是很有学问,后面我们还要单独研讨。 |
price | None | 指定价格。这个参数在市价订单单(Market,通常是下一个开市价格)或者收市订单(close价格)的时候,不需要设置(也就是None)。因为价格由市场来决定,在Backtrader中使用的开市订单 对于限价委托(Limit)单、止损订单(Stop)和止损限价订单(StopLimit),这个price就是订单的触发价格。几种单子的情况下文还要详细描述。 |
plimit | None | 止损限价。这个只有止损限价订单的有效。因为这种类型的订单需要两个价格,具体参见下文描述。 |
exectype | None | 订单成交类型: None:这个就是市价委托,在backtrader中,采取的下一个bar的开市(open)价格创建订单。 Close:采取下一个bar的收盘价(close)创建订单。 Limit:限价订单。这种在向broker发出买卖某种股票的指令时,对买卖的价格作出限定,对于多单(买),限定一个最高价,只允许broker按其规定的最高价或低于最高价的价格成交,对于空单(卖),限定一个最低价。限价委托的最大特点是,股票的买卖可按照投资人希望的价格或者更好的价格成交,有利于投资人实现预期投资计划。 Stop: 止损订单。对于多单:低于指定价格卖出,防止亏损扩大。对于空单,高于指定价格卖出。这个价格采用的是市价(也就是下一个开市价open),也成为止损市价订单。还有一种止盈订单,和上述策略相反 StopLimit:止损限价订单,就是以限价委托的止损单。止损限价指令避免了止损指令成交价格不确定的不足,在止损价委托中,投资者要注明两个价格:止损价(对应参数price)和限价(对应参数plimit),一旦市场价格达到或超过止损价格,止损限价委托自动形成一个限价委托。 国内后两种券商都不支持,据说期货支持,没玩过。不过现在很多券商会提供一些条件单功能,基本上也可以达成相同的效果。因此,我们在做好策略回测之后,对于验证好的策略,可以通过券商的条件单设置自动完成交易。 还有追踪止损(StopTrail)、追踪踪止损限价(StopTrailLimit)等,订单的成交方式是策略的重要手段,后面专门讨论。 |
valid | None | 有效期。有如下取值: None:无有限期,这种情况下,改订单一致存在直到订单满足条件被成交或者被取消。现实中,通常会有时间限制,但是我们这里还是当做无期限。 datetime.datetime 或者datetime.date 实例:也就是指定时间或者日期。也就是订单截止时间。 Order.DAY 或者0 或者 timedelta():也就是指定订单的持续时间。 数值:使用数值指定的截止时间,这个主要用于matplotlib(Backtrader用于画图)的时间编码方式。 |
tradeid | 0 | 这是一个内部标识。如果多个交易(trade)使用的相同的资产,那么通过整个标识区分不同的交易。在后续通知的处理中,tradeid会返回给Strategy进行区分处理 |
**kwargs | 还要一些broker的实现会支持更多的参数,那么通过**kwargs传递。 |
此外,还有几个参数用于一些特殊的订单,后续专门说明。
回到代码,Strategy中执行buy函数,其实就是调用broker的buy函数。策略的broker来自哪里呢?可以回头看看Strategy的代码解读,来自Cerebro。
那么在broker函数中会做啥呢?
def buy(self, owner, data,
size, price=None, plimit=None,
exectype=None, valid=None, tradeid=0, oco=None,
trailamount=None, trailpercent=None,
parent=None, transmit=True,
histnotify=False, _checksubmit=True,
**kwargs):
order = BuyOrder(owner=owner, data=data,
size=size, price=price, pricelimit=plimit,
exectype=exectype, valid=valid, tradeid=tradeid,
trailamount=trailamount, trailpercent=trailpercent,
parent=parent, transmit=transmit,
histnotify=histnotify)
order.addinfo(**kwargs)
self._ocoize(order, oco)
return self.submit(order, check=_checksubmit)
看第9行,就是创建了一个BuyOrder(就是Order的子类),这里会实例化和初始化一个Order实例,并返回。这个代码后续的处理咱们在Broker部分再讲。至此,一个Order就创建成功了。
对于sell也类似,也就是在broker中实例化和初始化一个SellOrder。
close就是平仓操作,什么是平仓?翻译成专业术语就是执行和现有持仓未平仓头寸完全相反的证券交易,也就是关闭证券的多头头寸需要卖出,而关闭证券的空头头寸则需要买回。
def close(self, data=None, size=None, **kwargs):
if isinstance(data, string_types):
data = self.getdatabyname(data)
elif data is None:
data = self.data
possize = self.getposition(data, self.broker).size
size = abs(size if size is not None else possize)
if possize > 0:
return self.sell(data=data, size=size, **kwargs)
elif possize < 0:
return self.buy(data=data, size=size, **kwargs)
return None
关键点在于:
至于cancel,就是将订单置为Canceled状态,后续不再成交就行了,具体就不讲了。
Backtrader官网提供了一些示例,可供我们代码的时候参考:
# 这是最简单的使用方法,创建买单,使用缺省的规模(size),使用市价成交。
order = self.buy()
# 市价单,指定有效期,这个有效期对于事件单是无效的,因为市价单是下一天成交。
order = self.buy(valid=datetime.datetime.now() + datetime.timedelta(days=3))
# 市价单,指定成交价格,这个价格也会无效,因为市价单使用open价格成交。
order = self.buy(price=self.data.close[0] * 1.02)
# 市价单,手动指定规模。
order = self.buy(size=25)
# 限价单,设定价格和有效期
order = self.buy(exectype=Order.Limit,
price=self.data.close[0] * 1.02,
valid=datetime.datetime.now() + datetime.timedelta(days=3)))
# 止损限价单,设定价格和限定价。
order = self.buy(exectype=Order.StopLimit,
price=self.data.close[0] * 1.02,
plimit=self.data.close[0] * 1.07)
# 所有订单全部取消,是否能取消成功,取决于订单当前的实际状态。
self.broker.cancel(order)
通常我们会在定制Strategy中定义notify_order函数用于跟踪订单的状态信息,如下示例:
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
# 提交和接受委托单不做任何处理
return
# 订单完成,记录。
if order.status in [order.Completed]:
if order.isbuy():
self.log(
'买单成交 成交价格: %.2f, 成交金额: %.2f, 佣金 %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
self.buyprice = order.executed.price
self.buycomm = order.executed.comm
else: # Sell
self.log('卖单成交 成交价格: %.2f, 成交金额: %.2f, 佣金 %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
self.bar_executed = len(self)
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log('Order 取消/金额不足/拒绝')
self.order = None
在该函数中,我们可以或者定制的状态信息,以及成交价格、金额等信息,也可以将相关信息计入日志供后续分析。
如下几点需要关注:
notify是在next函数中触发的
在一次next中,对相同的订单也可能触发多个通知(相同状态或者不同状态)。
订单提交给broker之后,可能会在下个next中之前就会立即成交,这种情况下通常是如下三种状态的订单:
在相同状态下,例如Order.Partial(部分成交),Backtrader也可能产生多个通知。这个在回测系统中不会出现,你就是买1个亿的股票,还是会立即成交。但是在实际交易中可不行。所以说规模是量化交易基金的天敌,你看有些量化基金规模小业绩很好,一上规模就不行了,就是很多交易机会在大资金量下无法成交。
交易数据会保存在order.executed中,这个数据在订单的整个生命周期会一直存在。
订单的状态咱们之前也介绍过,这里再说明下:
在Backtrader中,Order相关的关键类包括Order,OrderData和OrderExecutionBit。
Order类
order类主要用于记录订单相关信息,例如类型、状态,成交方式等。
状态以及成交类型前文已叙及,下面提供下几个关键属性:
还有几个方法经常用到:
OrderData
这个类在Order中使用,用于记录订单创建和成交时的数据,可以用于对比创建和实际成交的差别,主要包含以下关键属性:
OrderExecutionBit
这个类在OrderData中使用,用于记录订单的成交情况,这个类并不能指示订单是完全成交还是部分成交,仅仅指示记录信息。
Backtrader还提供了一种智能化的下单方式:目标订单。咱们前面所述的buy,sell和close都需要指定size(订单的规模,例如1手100股),这个通过Sizer类来完成。但是我们在进行资产配置的时候,希望对资产组合中的进行调整,设定规模就不太方便,因为你很难知道最后的效果,这时,可以通过设定目标,由系统自动根据目标规模下单。
对于规模的设定,可以有3种方式:
一个组合中,特定资产(例如股票)如何指定?通过data来指定,不同的data对应具体的资产。比如,你要建一个组合,其中包含10个股票,那么需要增加10个股票对应的数据,各种操作针对具体的数据,前面参数表格中已有描述。
在Backtrader中,你设定具体的资产目标规模/市值/目标百分比,系统根据当前持仓情况,和目标进行对比,来决定是buy还是sell,抑或是close。
以目标规模为例,如果目标规模大于当前持仓,就会进行买入操作,规模为:目标规模-当前持仓。例如
如果目标规模小于当前持仓,就会进行卖出操作,规模为:当前持仓-目标规模。例如:
在Backtrader中,分别提供了如下3个函数(定义在Strategy类中,因此直接在Strategy中调用)完成目标订单的设定:
order_target_size
下面我们通过源代码看看系统是如何实现目标订单的设置。
def order_target_size(self, data=None, target=0, **kwargs):
if isinstance(data, string_types):
data = self.getdatabyname(data)
elif data is None:
data = self.data
possize = self.getposition(data, self.broker).size
if not target and possize:
return self.close(data=data, size=possize, **kwargs)
elif target > possize:
return self.buy(data=data, size=target - possize, **kwargs)
elif target < possize:
return self.sell(data=data, size=possize - target, **kwargs)
return None # no execution target == possize
关键代码解读如下:
order_target_value
代码如下:
def order_target_value(self, data=None, target=0.0, price=None, **kwargs):
if isinstance(data, string_types):
data = self.getdatabyname(data)
elif data is None:
data = self.data
possize = self.getposition(data, self.broker).size
if not target and possize: # closing a position
return self.close(data=data, size=possize, price=price, **kwargs)
else:
value = self.broker.getvalue(datas=[data])
comminfo = self.broker.getcommissioninfo(data)
# Make sure a price is there
price = price if price is not None else data.close[0]
if target > value:
size = comminfo.getsize(price, target - value)
return self.buy(data=data, size=size, price=price, **kwargs)
elif target < value:
size = comminfo.getsize(price, value - target)
return self.sell(data=data, size=size, price=price, **kwargs)
return None # no execution size == possize
和目标规模相比,这里的主要差别如下:
order_target_percent
代码如下:
def order_target_percent(self, data=None, target=0.0, **kwargs):
if isinstance(data, string_types):
data = self.getdatabyname(data)
elif data is None:
data = self.data
possize = self.getposition(data, self.broker).size
target *= self.broker.getvalue()
return self.order_target_value(data=data, target=target, **kwargs)
这段代码关键就是获取当前资产组合所有市值(注意,这里getvalue没有指定data,就是返回所有市值包括现金),然后乘以目标百分比(第8行)作为当前资产的目标市值,然后使用order_target_value函数完成买卖操作。
通过以上三个函数,可以方便地对投资组合进行调整。投资组合的管理是量化交易系统非常重要的部分,因此需要重点关注。
什么是OCO订单,全称是One Cancel Others,翻译的意思就是成交一个取消其它。OCO订单是一组订单(通常是两个),规则是如果一个订单成交,则其它订单将自动取消。OCO 订单通常结合了自动交易平台上的止损订单和限价订单。当达到止损或限价并成交订单时,另一个订单将自动取消。我们可以使用 OCO 订单来降低风险。要注意,这个需要交易平台支持,目前Backtrader只能是回测实现。
假设投资者拥有 1,000 股价格为 10 元的股票。投资者预计这只股票在短期内将在大范围内交易,目标价为 13元。为了减轻风险,他们不希望每股损失超过 2 元。因此,投资者可以发出 OCO 指令,其中包括以 8 元卖出 1,000 股的止损指令,以及以 13 元卖出 1,000 股的同时限价指令,以先到者为准。如果股票的交易价格高达 13元,则成交卖出限价单,投资者持有的 1,000 股股票以 13元的价格卖出。同时,8元的止损单被交易平台自动取消。如果投资者独立下达这些订单,他们可能会忘记取消止损订单,如果股票随后交易价格下跌至 8元,这可能会导致1,000 股的空头头寸。
在Backtrader中如何使用呢?可以参考如下官方示例:
def next(self):
...
o1 = self.buy(...)
...
o2 = self.buy(..., oco=o1)
...
o3 = self.buy(..., oco=o1) # 也可以oco=o2, 因为o2已经在o1组中
首先,设定一个普通订单O1,可以当做一个组的领头羊。
然后创建另外订单(例如O2/O3)的时候,通过参数指定oco为o1,则这些订单就形成了一个OCO组合,如果该组中的任何订单被成交、取消或到期,其他订单将被取消。
Bracket订单,不好翻译,括号订单?你可以理解会将3个订单放到一个括号内,类似这样(低价单,主订单,高价单),逻辑上也等于一组订单,这一组订单和OCO订单有差别,OCO订单地位平等,Bracket订单有主次之分,下面详细说明。
Bracket订单区分为买单(做多)和卖单(做空)。
对于买单,包括3个订单:
相应地,对于卖单,也包括3个订单:
这些单子的规则如下:
从以上规则可以看出,这种结构的订单能够保护自己的潜在损失,同时在价格上涨(多单)/下跌(空单)时让自己获利。括号之内订单的价格距离(底价单,主订单,高价单)代表交易的潜在盈亏范围。
Braket订单的作何用途呢?以多单为例。假设投资者以 50 元的价格下达 100 股某股票 的买单,下达 55 元的限价卖单和 45 元的止损卖单。如果价格上涨至 55 元或下跌至 45 元,则卖出该头寸。交易者要么通过卖出限价获利 5 元,要么通过止损订单将损失限制在 5 元。
Braket有两大好处,一个是可以在交易成交之前设置括号内的买单,这为投资者提供了灵活性。另外可以提供纪律约束,投资者使用Bracket买单来实现交易计划。下单后,投资者无需采取任何进一步行动,只需等待止损或限价卖单成交即可。
Backtrader提供了两种方式使用Bracketd订单。
自动创建3个订单
Backtrader同样通过Strategy类提供了两个函数(buy_bracket 和sell_bracket),分别对应买入和卖出的Bracket订单。
一个简单的例子:
brackets = self.buy_bracket(limitprice=14.00, price=13.50, stopprice=13.00)
这个函数返回一个列表,包含3个点单(主订单和两个附属订单),返回格式为[main, stop, limit],具体参见前文描述。比如示例的这一行代码就创建3个订单,13.50买入,超过14块或者低于13块卖出止盈止损。
关于返回的stop和limit,针对多单和空单含义不同:
注意,这里高价、低价相对的是主订单的买入/卖出价格。
手动创建3个订单
Backtrader还提供了创建3个订单的方法,方式是通过transmit和parent来指定,规则是:
以下为官网示例代码:
mainside = self.buy(price=13.50, exectype=bt.Order.Limit, transmit=False)
lowside = self.sell(price=13.00, size=mainside.size, exectype=bt.Order.Stop,
transmit=False, parent=mainside)
highside = self.sell(price=14.00, size=mainside.size, exectype=bt.Order.Limit,
transmit=True, parent=mainside)
几个关键点要注意:
总体上,我们可以看出,Order可以理解为一个复杂消息体,主要进行订单信息的传递,真正执行操作的是broker,下面我们介绍broker。
Broker是啥,前面咱们多次说过,在咱们中国,就是券商,例如华泰、中信等等,但是呢?这些券商对个人用户不开放接口,所以咱们也用不上,至于需要啥条件才能用上,这个目前不太清楚,据说也有券商能提供,后面我了解下。由于没有真实(live)券商,咱们只能用模拟Broker进行回测,Backtrader提供了BackBroker类用来模拟券商的行为。如果要实时交易,需要手动挂单,有些券商提供很好的条件单功能。
Backtrader的继承关系比较简单,就先不提供图了。
class BackBroker(bt.BrokerBase)
class BrokerBase(with_metaclass(MetaBroker, object))
class MetaBroker(MetaParams)
从类定义来看,BackBroker也继承了元类,所以其实例化也会受元类控制,下面在实例化和初始化过程进行说明。
回头看看咱们系列文章3中Cerebro代码,可以看出Backtrader是Cerebro在初始化函数中调用:
self._broker = BackBroker()
self._broker.cerebro = self
当然你也可以自己写个Broker(比如针对特定券商提供的不同的接口),按照如下方法重新设定:
broker = MyBroker()
cerebro.broker = broker
在BackBroker中,可以通过参数控制Broker的行为,参数如下:
params = (
('cash', 10000.0),
('checksubmit', True),
('eosbar', False),
('filler', None),
# slippage options
('slip_perc', 0.0),
('slip_fixed', 0.0),
('slip_open', False),
('slip_match', True),
('slip_limit', True),
('slip_out', False),
('coc', False),
('coo', False),
('int2pnl', True),
('shortcash', True),
('fundstartval', 100.0),
('fundmode', False),
)
还有从父类BrokerBase继承的如下参数:
params = (
('commission', CommInfoBase(percabs=True)),
)
这些关键参数的解释如下表所述:
参数 | 缺省值 | 说明 |
---|---|---|
cash | 10000 | 初始现金。在买卖操作的时候,cash会改变。 |
commission | CommInfoBase | 这个参数就是一个CommInfoBase类,用于设置佣金方案,这个后面专门说下。 |
checksubmit | True | 在下单前是否检查现金和保证金(Margin,香港叫孖展,主要用于融资,也就是杠杆交易,例如根据你持股/现金,评估后提供一定额度的融资)。 |
eosbar | False | 代码中该参数并未有实际用途,暂时忽略。 |
filler | None | 回调函数,定义为callable(order, price, ago),返回值是数字(含义为size),也就是在订单成交的时候可以通过这个回调函数进行成交量的订单匹配。 |
slip_xxx | - | 这几个用于滑点功能,后面专题讨论。 |
coc | False | Cheat-On-Close,对于市价单,使用该订单发出时的收盘价成交该订单。这个只用于回测才有意义,实际上无法实施,因此称为cheat。很容易理解,我们知道收盘价了,当天(或者当前Bar)已经结束了咋成交?只能下一天(或bar)才能成交。为啥有这种需求呢?主要是用于回测的时候控制成交时间和价格。比如,我想测试一个股票一个月的收益,那最后一天提交订单,得第二天才能成交,这个很难控制具体回测周期 |
coo | False | Cheat-On-Open,含义同上,使用该订单发出时的开盘价成交该订单。 |
int2pnl | True | 每一次减仓(不管是多头还是空头)都会分别计算pnl。有些情况下没有必要,因为组合中,不同策略实施的操作,主要看最终效果,每一次的操作意义不大。 |
shortcash | True | 设置为True的话,股票类资产做空的时候,现金将会增加,但是资产的市值将会标记为负值,也就是将空头资产记为负债(本来就是借入的资产),市值越低越好。 设置为False的话则相反,现金会扣减作为本次做空的成本扣除,同时市值对应增加。建议使用缺省值,容易理解。 |
fundstartval | 100 | 此参数控制以类似基金的方式衡量组合绩效的起始值,即:增加股票可以增加(做多)和减少现金(做空)。业绩不是用组合的资产净值来衡量的,而是用基金的价值来衡量的。 |
fundmode | False | 如果将其设置为True,则分析器(例如TimeReturn)可以根据价值而不是总资产净值自动计算收益。 |
Note:Backtrader提供 set_xxx 函数来设置参数 (cerebro.broker.set_xxx),其中 xxx 需要设置的参数的名称。
如前所述,BackBroker继承了元类,受MetaBase元类的控制。首先到MetaBase的__call__走一圈,也就是一整套动作(doprenew、donew、dopreinit、doinit和dopostinit),其机制前面几篇文章讲的比较多,这里就简要说下重点:
def __init__(self):
super(BackBroker, self).__init__()
self._userhist = []
self._fundhist = []
# share_value, net asset value
self._fhistlast = [float('NaN'), float('NaN')]
首先调用的是父类的__init__函数,然后初始化内部使用的容器,父类就是BrokerBase,其父类__init__函数是:
def __init__(self):
self.comminfo = dict()
self.init()
主要就是调用init函数,这个self是谁,就是咱们之前初始化的BackBroker,然后又走到init函数:
def init(self):
super(BackBroker, self).init()
self.startingcash = self.cash = self.p.cash
self._value = self.cash
self._valuemkt = 0.0 # no open position
self._valuelever = 0.0 # no open position
self._valuemktlever = 0.0 # no open position
self._leverage = 1.0 # initially nothing is open
self._unrealized = 0.0 # no open position
self.orders = list() # will only be appending
self.pending = collections.deque() # popleft and append(right)
self._toactivate = collections.deque() # to activate in next cycle
self.positions = collections.defaultdict(Position)
self.d_credit = collections.defaultdict(float) # credit per data
self.notifs = collections.deque()
self.submitted = collections.deque()
# to keep dependent orders if needed
self._pchildren = collections.defaultdict(collections.deque)
self._ocos = dict()
self._ocol = collections.defaultdict(list)
self._fundval = self.p.fundstartval
self._fundshares = self.p.cash / self._fundval
self._cash_addition = collections.deque()
首先就是调用父类MetaBase的init函数,其主要作用就是记录佣金方案到comminfo中。
def init(self):
# called from init and from start
if None not in self.comminfo:
self.comminfo = dict({None: self.p.commission})
初始化简单,主要就是初始化各种订单类型的队列(使用的是deque类,前面介绍过,这个是类似于list的容器,可以在队列头部和尾部添加、删除元素,可以快速高效地进行数据的操作)以及一些内部变量,计算现金、市值啥的。
如前所述,BackBrocker是由Cerebro创建的(实例化),启动也是在runstrategies中。启动之前,还进行一些参数的设置:
def runstrategies(self, iterstrat, predata=False):
...
if self.p.cheat_on_open and self.p.broker_coo:
# try to activate in broker
if hasattr(self._broker, 'set_coo'):
self._broker.set_coo(True)
if self._fhistory is not None:
self._broker.set_fund_history(self._fhistory)
for orders, onotify in self._ohistory:
self._broker.add_order_history(orders, onotify)
self._broker.start()
首先进行coo参数的设置,然后记录历史订单和历史资金信息,然后调用start函数:
def start(self):
self.init()
大家有点奇怪吧,前面不是在创建的时候初始化了吗?为啥还要初始化?别忘了,runstrategies是针对一个策略调用一次(回头看看Cerebro代码解读),如果有多个策略,每次调用前都会初始化,保证每个策略不受前一策略的影响。
在1.2节中,我们描述了创建订单的时候,Strategy进行买卖操作的时候,均调用的Broker提供的函数。Broker就是创建(实例化和初始化)一个Order(BuyOrder或者SellOrder)实例,并调用submit函数提交订单,下面看看后续对订单的处理:
def submit(self, order, check=True):
pref = self._take_children(order)
if pref is None: # order has not been taken
return order
pc = self._pchildren[pref]
pc.append(order) # store in parent/children queue
if order.transmit: # if single order, sent and queue cleared
# if parent-child, the parent will be sent, the other kept
rets = [self.transmit(x, check=check) for x in pc]
return rets[-1] # last one is the one triggering transmission
return order
要点如下:
def transmit(self, order, check=True):
if check and self.p.checksubmit:
order.submit()
self.submitted.append(order)
self.orders.append(order)
self.notify(order)
else:
self.submit_accept(order)
return order
这里有个关键参数checksubmit,控制在下单前是否检查现金和保证金。:
如果要检查,需要调用Order的submit函数,这个函数比较简单,就是修改订单状态为submitted,记录broker信息。
然后,对应的队列中条件该订单,然后最关键的是要调用borker的notify函数,克隆该订单,并加入到notifs队列,后续next会检查这个队列。
def notify(self, order):
self.notifs.append(order.clone())
如果不检查,那么调用submit_accept函数:
order.pannotated = None
order.submit()
order.accept()
self.pending.append(order)
self.notify(order)
这个和前面的处理差别就是提交之后,直接accept了(函数中设置订单的状态为Accepted),不加入提交队列。最后还是调用notify,也就是将订单加入到notifs队列。
继续下一步流程之前,你一定理解系统运行当前的位置。从前述可以知道,订单的操作是在Strategy的next中,也就是逐天(bar)进行数据处理过程中,可以回头再看看Strategy代码解读。那么BackBroker将订单加入到notifs队列中,当前处理就结束了,马上进入下一个next,具体就在Cerebro的_runonce/_runnext函数中,都会调用self._brokernotify函数(可以再看看Cerebro代码解读):
def _brokernotify(self):
'''
Internal method which kicks the broker and delivers any broker
notification to the strategy
'''
self._broker.next()
while True:
order = self._broker.get_notification()
if order is None:
break
owner = order.owner
if owner is None:
owner = self.runningstrats[0] # default
owner._addnotification(order, quicknotify=self.p.quicknotify)
这里最关键的是6行broker的next函数,先看它:
def next(self):
while self._toactivate:
self._toactivate.popleft().activate()
if self.p.checksubmit:
self.check_submitted()
...
2,3行是对需要激活的订单进行处理,现在还没有,暂时忽略。如果checksubmit参数为True,就要调用check_submitted进行资金检查:
def check_submitted(self):
cash = self.cash
positions = dict()
while self.submitted:
order = self.submitted.popleft()
if self._take_children(order) is None: # children not taken
continue
comminfo = self.getcommissioninfo(order.data)
position = positions.setdefault(
order.data, self.positions[order.data].clone())
# pseudo-execute the order to get the remaining cash after exec
cash = self._execute(order, cash=cash, position=position)
if cash >= 0.0:
self.submit_accept(order)
continue
order.margin()
self.notify(order)
self._ococheck(order)
self._bracketize(order, cancel=True)
这个函数关键就是取出提交的订单,然后获取对应的佣金方案,调用_execute函数进行伪执行。这个函数代码过于复杂,就不拿出来讲,其关键就是根据佣金,价格以及规模信息进行计算,看看执行完成后还能剩余多少现金。如果大于0的话,那么就调用submit_accept函数接受该订单,和checksubmit为False的场景站到同一起跑线了。
再回头到next函数,忽略一些细节代码,看看如下部分:
def next(self):
...
else:
self._try_exec(order)
if order.alive():
self.pending.append(order)
elif order.status == Order.Completed:
# a bracket parent order may have been executed
self._bracketize(order)
...
这里最关键的就是_try_exec执行这个订单了:
def _try_exec(self, order):
data = order.data
popen = getattr(data, 'tick_open', None)
if popen is None:
popen = data.open[0]
phigh = getattr(data, 'tick_high', None)
if phigh is None:
phigh = data.high[0]
plow = getattr(data, 'tick_low', None)
if plow is None:
plow = data.low[0]
pclose = getattr(data, 'tick_close', None)
if pclose is None:
pclose = data.close[0]
pcreated = order.created.price
plimit = order.created.pricelimit
if order.exectype == Order.Market:
self._try_exec_market(order, popen, phigh, plow)
elif order.exectype == Order.Close:
self._try_exec_close(order, pclose)
elif order.exectype == Order.Limit:
self._try_exec_limit(order, popen, phigh, plow, pcreated)
elif (order.triggered and
order.exectype in [Order.StopLimit, Order.StopTrailLimit]):
self._try_exec_limit(order, popen, phigh, plow, plimit)
elif order.exectype in [Order.Stop, Order.StopTrail]:
self._try_exec_stop(order, popen, phigh, plow, pcreated, pclose)
elif order.exectype in [Order.StopLimit, Order.StopTrailLimit]:
self._try_exec_stoplimit(order,
popen, phigh, plow, pclose,
pcreated, plimit)
elif order.exectype == Order.Historical:
self._try_exec_historical(order)
可以看出,这里根据不同的执行类型对订单进行执行,以Market为例:
def _try_exec_market(self, order, popen, phigh, plow):
ago = 0
if self.p.coc and order.info.get('coc', True):
dtcoc = order.created.dt
exprice = order.created.pclose
else:
if not self.p.coo and order.data.datetime[0] <= order.created.dt:
return # can only execute after creation time
dtcoc = None
exprice = popen
if order.isbuy():
p = self._slip_up(phigh, exprice, doslip=self.p.slip_open)
else:
p = self._slip_down(plow, exprice, doslip=self.p.slip_open)
self._execute(order, ago=0, price=p, dtcoc=dtcoc)
我们先忽略coc以及滑点(后面专题讲)的处理,可以看出,这里直接使用open价格去执行成交。Market(市价单)的逻辑是使用第二天(bar)的open价格匹配成交。_execute中主要就是基于价格以及佣金进行各种计算,太细节了,就不讲了。
后续还有一些特殊处理,均忽略,记住一点,订单的执行都是在下一个bar的处理过程,如果当前bar无法成交,则继续到下一个bar。对我们而言,比较重要的各种类型订单的成交机制,影响我们选择对应的策略,所以下面重点讲各种类型订单的作用以及在Backtrader中的成交机制。
不同类型的订单,执行的方式是不一样的,我们需要弄清楚其中的机制,对我们后续策略的设计以及自动执行也有重要的作用。
什么是市价单,就是投资者不指定价格,而是愿意按市场的价格支付当前的订单。在BackBroker中,会取下一天(bar)的open价格匹配成交(这里要注意,如果是期货,相同时间内有更小的tick,这个也必须要求时间改变了才认为是下一个bar),代码前面讲过了。
市价单是市场上最常见和最直接的交易。它的目的是以当前要价尽快执行,并且在大多数情况下是买卖双方共同协商的选择。通常作为券商的默认方式。
市价单对于大盘股都是安全的选择,因为它们的流动性很高。也就是说,在交易日的任何特定时刻,他们的股票都会有大量换手,交易可立即完成。除非当时市场非常不稳定,波动剧烈,否则当我们点击“买入”或“卖出”时显示的价格与最终成交的价格几乎相同。
但是,当交易流动性较低的投资时,这些股票换手率低,买卖价差往往较大。因此,市价订单可能很难成交,或者成交价格和预期相差很大。由于这样的问题,通常会使用限价单。
这个也是市价单的一种,采用的是下订单的下一天(bar)的收盘价,而市价单的开盘价。其它和市价单的描述一致,不单独说明。
限价单是一种以指定价格或更高价格购买或出售证券的订单。对于买入限价订单,订单将仅以限价或更低的价格执行,而对于卖出限价订单,订单将仅以限价或更高的价格执行。这种订单让让交易者更好地控制交易的价格。看下代码,这个 成交策略非常明了:
def _try_exec_limit(self, order, popen, phigh, plow, plimit):
if order.isbuy():
if plimit >= popen:
# open smaller/equal than requested - buy cheaper
pmax = min(phigh, plimit)
p = self._slip_up(pmax, popen, doslip=self.p.slip_open,
lim=True)
self._execute(order, ago=0, price=p)
elif plimit >= plow:
# day low below req price ... match limit price
self._execute(order, ago=0, price=plimit)
else: # Sell
if plimit <= popen:
# open greater/equal than requested - sell more expensive
pmin = max(plow, plimit)
p = self._slip_down(plimit, popen, doslip=self.p.slip_open,
lim=True)
self._execute(order, ago=0, price=p)
elif plimit <= phigh:
# day high above req price ... match limit price
self._execute(order, ago=0, price=plimit)
简要说明如下:
这种订单可以保证投资者支付该价格或更少。价格有保证,但是订单有可能不成交,除非目标证券的价格符合订单条件,否则限价单不会被执行。如果未达到指定价格,投资者可能错失交易机会。
这种订单和市场订单的差异是市场订单以现行市场价格执行交易,没有指定任何价格限制。
很多情况下建议使用限价单,例如当股票快速上涨或下跌时,交易者担心从市价单得到一个不希望的价格。此外,如果交易者不关注股票并且愿意以特定价格购买或出售该证券的,限价单也会有作用。限价单也可以在指定日期,在到期日保持开放状态,只要满足价格条件就成交。
通常限价单和止损单配合使用,防止出现超出预期的亏损。
止损单是在证券价格超过特定价格的时候买入或卖出证券的订单,以确保尽可能了达到预定的进场或出场价格,限制投资者的损失或锁定利润。一旦价格超过预定义的进入或退出点,止损单就变成了市价单,按照市价单的逻辑成交。
同样的,止损单也分为做多和做空,处理逻辑相反:
def _try_exec_stop(self, order, popen, phigh, plow, pcreated, pclose):
if order.isbuy():
if popen >= pcreated:
# price penetrated with an open gap - use open
p = self._slip_up(phigh, popen, doslip=self.p.slip_open)
self._execute(order, ago=0, price=p)
elif phigh >= pcreated:
# price penetrated during the session - use trigger price
p = self._slip_up(phigh, pcreated)
self._execute(order, ago=0, price=p)
else: # Sell
if popen <= pcreated:
# price penetrated with an open gap - use open
p = self._slip_down(plow, popen, doslip=self.p.slip_open)
self._execute(order, ago=0, price=p)
elif plow <= pcreated:
# price penetrated during the session - use trigger price
p = self._slip_down(plow, pcreated)
self._execute(order, ago=0, price=p)
# not (completely) executed and trailing stop
if order.alive() and order.exectype == Order.StopTrail:
order.trailadjust(pclose)
要点:
实际上,还有一种止盈单,就是对于多单,价格如果超过设定价格就sell,锁定一定的利润。对于空单,价格如果低于设定价格,就买入平仓,也达到锁定利润的效果。但是看代码,这个函数无法完成止盈操作,可以考虑使用限价单和止损单一起使用。
使用技术分析的交易者会在主要移动平均线、趋势线、摆动高点、摆动低点或其他关键支撑位或阻力位下方放置止损单,在趋势形成的时候进行止损操作。
止损单也有风险,比如市场的一个波动,可能意外触及止损点并激活订单时,可能会导致本可以盈利或更多盈利的交易出现亏损,也就是所谓的卖飞了。
这种订单是止损单和限价单的综合,其要点是:
代码就不贴了,根据上述要点,结合咨询单和限价单的代码很容易理解。
在使用止损限价单的时候,需要设定两个参数:
止损:交易指定目标止损价格,对应Backtrader中订单的price参数
限价:交易指定限价对应Backtrader中订单的plimit参数。
还必须设置一个时间点,也就是Backtrader中订单的valid参数。
止损限价单的主要好处是交易者可以精确控制何时应执行订单。
止损限价单也有不利之处,所有限价单一样,如果证券在指定时间段内未达到止损价格,则不能保证交易会执行。
止损限价单将在达到给定止损价格后以指定价格或更好的价格执行。一旦达到止损价,止损限价单就成为限价单,以限价或更好的价格买入或卖出。这种类型的订单使用比较广泛。
这里给个例子说明:例如,假设 某股票的交易价格为 155 元,并且一旦该股票开始显示出某种明显的上涨势头,投资者就想购买该股票,就可以下达止损限价单,止损价为 160 元,限价为 165 元。如果 股票的价格高于 160 元的止损价,则订单被激活并变为限价订单。只要订单可以在 165 元(即限价)以下成交,交易就会被成交。如果价格高于 165 元,则订单将不会被执行。
这种订单也是对止损单的一种优化,也需要券商的支持。目前盈透证券支持OCO以及StopTrail。据说还有一个StopTrailLimit,越来越复杂,可以看出,订单的执行方式对于交易系统非常重要。
但是呢?咱们Backtrader的模拟中没有实现,只在真实的盈透证券(ibbroker)中使用,咱们用不上。
# not (completely) executed and trailing stop
if order.alive() and order.exectype == Order.StopTrail:
order.trailadjust(pclose)
def trailadjust(self, price):
pass # generic interface
虽然咱们用不上,但是了解下跟踪止损单也是有好处的。只需要简单了解下,具体用法就不讲了。
前面说了,追踪止损是普通止损单的优化,可以将止损价格设置为当前市场价格上下浮动范围,浮动范围可以设置为百分比或者绝对值。对于多头头寸,我们可以将追踪止损设置在当前市场价格下方。对于空头头寸,可以将追踪止损设置在当前市场价格之上。可以看出,追踪止损比固定止损更灵活,因为它会自动跟踪股票的价格方向,并且不需要像固定止损那样手动重置。
下面给出一个例子说明这种订单的使用方法:
假设您以 1,000 元的价格 购买了某股票。通过查看股票的先前上涨,您会发现价格通常会在再次走高之前经历 5% 到 8%的回调。这些先前的变动可以帮助建立用于追踪止损的百分比水平。
选择 3%,甚至 5%,可能太小了,一个很小的回调就会把你振出局。
选择 20% 的追踪止损是又太大了。根据最近的趋势,平均回调约为 6%,较大的回调接近 8%。更好的追踪止损是 10% 到 12%。这给了交易者一定的回调空间,但如果价格下跌超过 12%,交易者也可以迅速退出。12% 的跌幅大于典型的回调,这意味着可能会发生 趋势逆转,而不仅仅是回调。
假设 10% 的追踪止损,如果价格下跌 10% 低于购买价格,券商将执行卖单,也就是 900 元。如果购买后价格从未超过 1,000 元,那么止损将保持在 900 元。如果价格达到 1,010 元,止损将升至 909 元,比 1,010 元低 10%。如果股票上涨至 1250 元,如果价格跌至 1,125 元,券商就执行卖出指令。如果价格从 1,250 元开始下跌并且没有回升,追踪止损订单将保持在 1,125 元。总结一点就是追踪止损价格跟随价格上涨,但如果价格开始下跌,则保持不变,以潜在地获得利润。
上面的例子是对于多单,如果是空单,正好相反。
虽然国内券商没有这种订单类型(据我所知),但是有些券商支持条件单可以达到相同效果,例如回落卖出,就是上涨途中,下调一定比例(或者绝对值)卖出。
滑点是指交易的预期价格与交易实际执行价格之间的差异。滑点可能随时发生,在使用市价单时市场波动较大的时期最为普遍。当执行大订单但所选价格没有足够的交易量来维持当前的买卖差价时,也会发生这种情况。
由于滑点在真实市场普遍存在,如果Backtrader不考虑这个这个情况,那么回测和实际情况可能相差很大(在量化交易的成本模型中,滑点也作为一个重要的成本进行考虑),基于回测的策略可能就不适用于真实市场,为了尽可能地保证模拟现实情况,Backtrader提供了滑点功能。
BackBroker滑点包含如下可以设置的参数(设置方法参见2.2节参数说明):
参数 | 缺省值 | 说明 |
---|---|---|
slip_perc | 0.0 | 设定允许变动(滑动)范围,以百分比实际值表示,例如:0.01指的是1%,同样的0.001指的是0.1%。 |
slip_fixed | 0.0 | 同上,以百分比值(单位是%)表示,例如0.01指的是0.01%。注意,slip_perc优先级高。 |
slip_open | False | 是否对以下一bar的open价格执行的订单使用滑点,例如市价单。还包括其他所有需要以开盘价匹配成交的情况。 |
slip_match | True | 设置为True,BackBroker会保证滑点后的价格在high和low之间,不能超出。 设置为False,BackBroker不会进行检查,直接使用当前的价格,并在下一个循环成交。 |
slip_limit | True | 对于限价单,即便slip_match设置为false,还是会按照为True处理。 |
slip_out | False | 即便价格不在high-low之间。 |
关于代码,大家可以参见之前各种类型订单的执行逻辑代码中,都有对于滑点的处理。老实说,这个对于咱们个人交易者,作用实在不大,咱们那点钱,还不至于能影响市场,滑点可能性比较小。代码就不详细描述了。
如果要使用的话,也可以考虑一点滑点,设置方法如下,其他参数缺省就好:
cerebro = bt.Cerebro()
cerebro.broker = bt.brokers.BackBroker(slip_perc=0.005) # 0.5%
在交易过程中,我们的Strategy通常要获知头寸情况,Backtrader提供了Position类要记录相关信息。这个类比较简单,代码就不讲了。
几个关键点:
交易应该是人类最古老的行为了。什么才能叫交易?一买一卖成交了才能称为交易。从投资的角度来看,交易有两个重要的状态:
当然还可能存在如下两种情况:
这两种情况可以看做分为了两部:
还记得咱们咋使用Trade的?在Strategy代码解读中,咱们说过,可以每次交易后记录交易的相关信息。由于Trade类也比较简单,不准备进行代价解读了,我们重点看看Trade提供的我们需要了解的信息:
属性 | 含义 |
---|---|
status | 当前状态,一个三种:Created(这个类初始化的状态)、open 和closed。 |
tradeid | Order创建时传递的ID,用于识别不同的交易。 |
size | 当前交易的规模 |
price | 当前交易的价格 |
value | 当前交易的金额 |
commission | 累积的佣金 |
pnl | 毛利 |
pnlcomm | 减去佣金的净利润 |
isclosed | 是否平仓. |
isopen | 是否处于未平仓状态 |
justopened | 是否刚开仓 |
baropen | 在哪个bar开仓 |
dtopen | 开仓的时间 |
barclose | 哪个bar平仓 |
dtclose | 平仓时间 |
barlen | 未平仓状态持续了多少bar |
historyon | 是否记录历史信息 |
history | 保存了历史更新信息。最开始是开仓,然后中途会有不断的更新(仓位变动),最后是平仓。 |
佣金是交易系统中成本模型的一部分。不过,这种成本是固定且可预计的,因此较为简单,只要根据和券商的协议填写清楚即可,由于这个类过程并不复杂,不解读代码了,看几个关键点。
看过之前Cerebro代码就会知道,咱们在实例化Cerebro之后,就可以直接设置佣金方案了,如下:
cerebro.broker.setcommission(commission=0.001)#设定交易费用(买卖都收)
Cerebro获取broker(broker的实例化和初始化在2.1节描述),然后调用setcommission设置佣金:
def setcommission(self,
commission=0.0, margin=None, mult=1.0,
commtype=None, percabs=True, stocklike=False,
interest=0.0, interest_long=False, leverage=1.0,
automargin=False,
name=None):
comm = CommInfoBase(commission=commission, margin=margin, mult=mult,
commtype=commtype, stocklike=stocklike,
percabs=percabs,
interest=interest, interest_long=interest_long,
leverage=leverage, automargin=automargin)
self.comminfo[name] = comm
Broker首先会实例化一个CommInfoBase,然后记录到commoninfo字典中,这里要注意,一个Broker可以拥有多个CommInfoBase,通过name来区分,name和Data类创建时候使用的名字一致,也就是不同的数据源(对应不同的资产)佣金方案可以不同。在相同的券商中,针对不同的资产不同的佣金,例如对于股票和ETF佣金不同,这里就可以设置不同佣金方案。
CommInfoBase就是设置相关参数,关键参数具体解释如下表:
参数 | 缺省值 | 含义 |
---|---|---|
commission | 0.0 | 就是基本佣金数据,按照百分比或者绝对值。百分比就是按照交易的金额(size*price)的百分比提取佣金。绝对值,就是针对size提取固定佣金。比如一笔交易(或者一个合约)收取2元钱。至于具体是采取百分比还是固定值,参见下面参数说明。 |
commtype | None | 就是用来指定佣金数据是百分比还是固定值。 ommInfoBase.COMM_PERC:按照百分比解释。 CommInfoBase.COMM_FIXED:按照固定值解释。 None:这种情况下系统根据资产情况自动判决,判决方法参见参数margin。为啥搞得这么复杂,主要是为了兼容原来老的类(CommissionInfo),原来是通过margin来判断是否百分比。 |
percabs | False | 当 commtype 设置为 COMM_PERC 时,指定百分比数字的填写方法。比如说,你要设置0.1%,这个值设置为True,那么commission参数填写为0.001,为False的话,只要填写为0.1.为啥搞得怎么麻烦?还是兼容性问题。因为老版本中commission填写值不包括百分号,还记得咱们之前的例子要求“0.1% ,需要除以100然后去掉%”,这个就是老的处理方式,在新版本中,如果保持老的处理方式,percaps需要填写为TRUE。 |
stocklike | False | 这里用来指示股票类资产还是期权类资产,在老的CommissionInfo,是通过margin来确定是否期权。在新的类中,当commtype为None的时候,可以通过这个参数来指示是否股票类资产。 |
margin | None | 保证金,港股称为孖展。Margin为0或者None,那么commission就是按照百分比解释。否则按照固定值解释。主要是因为Margin主要针对的期货/期权产品,通常按合约计价。这个是之前类的使用方法,新版本不再采用。 |
mult | 1.0 | 杠杆比,这个主要应用于期权等可以加杠杆的资产,Backtrader据此计算盈利和亏损。 |
name | 空 | 前面已叙及。 |
interest | 0.0 | 利息。有些情况下,需要对所持资产计算利息,例如借入证券卖空,由于这些对于我们个人交易者使用不多,咱们先忽略。 |
interest_long | False | 是否对多头仓位收取利息,和上面一样,是否对做多的资产收取利息,先忽略。 |
系统佣金方案还有缺点,从代码来看,有些常用的场景就无法支持,比如常用的一笔交易最小5块,这个咋设置,还有除了佣金费用之外,港股/美股还需要平台费,也没法模拟。这就需要我们进行定制。
如前所述,在不同国家,甚至不同券商,都可能有不同的佣金方案,Backtrader提供了定制佣金方案,而且由于采用了面向对象的机制,所以定制方案也很很简单。
如何定制呢?首先,继承CommInfoBase类。然后根据你的需求修改参数和重载函数。
先说简单的自定义方法,修改参数。比如,对于股票和期权,参数不一样,那么,我们可以分别定义参数重写针对股票和期权的佣金方案。
比如,对于期权,我们定义固定值的佣金方案:
class CommInfo_Futures_Fixed(CommInfoBase):
params = (
('stocklike', False),
('commtype', CommInfoBase.COMM_FIXED),
)
这样的话,使用的时候只需要输入commision就行了,其他参数就不要关注了。
同样的,也可以定义百分比的股票佣金方案:
class CommInfo_Stocks_Perc(CommInfoBase):
params = (
('stocklike', True),
('commtype', CommInfoBase.COMM_PERC),
)
假如你希望按照老方法输入0.001表示0.1%,那么还可以这样定义:
class CommInfo_Stocks_PercAbs(CommInfoBase):
params = (
('stocklike', True),
('commtype', CommInfoBase.COMM_PERC),
('percabs', True),
)
如果你还需要修改佣金的计算方法,那么你可以重载_getcommission 函数,定义如下,这个后面会给你一个详细的实例。
def _getcommission(self, size, price, pseudoexec):
'''根据规模以及价格计算佣金
'''
定义好佣金方案之后,下一步就是通过Cerebro调用broker.addcommissioninfo添加这个方案。注意不是使用setcommission,因为这个函数只是针对老把能的CommissionInfo对象。如下示例:
...
comminfo = CommInfo_Stocks_PercAbs(commission=0.001) # 0.1%
cerebro.broker.addcommissioninfo(comminfo)
addcommissioninfo函数定义如下:
def addcommissioninfo(self, comminfo, name=None):
'''Adds a ``CommissionInfo`` object that will be the default for all assets if
``name`` is ``None``'''
self.comminfo[name] = comminfo
注意,可以设定佣金方案对应的资产,通过data的name来指定。
好了,来一个实战的案例,比如在咱们国内,佣金方案可能如下:
按照这个需求,可以定义佣金方案如下:
class MyStockCommissionScheme(bt.CommInfoBase):
'''
1.佣金按照百分比。
2.每一笔交易有一个最低值,比如5块,当然有些券商可能会免5.
3.卖出股票还需要收印花税。
4.可能有的平台还需要收平台费。
'''
params = (
('stampduty', 0.005), # 印花税率
('commission', 0.005), # 佣金率
('stocklike', True),#股票类资产,不考虑保证金
('commtype', bt.CommInfoBase.COMM_PERC),#按百分比
('minCommission', 5),#最小佣金
('platFee', 0),#平台费用
)
def _getcommission(self, size, price, pseudoexec):
'''
size>0,买入操作。
size<0,卖出操作。
'''
if size > 0: # 买入,不考虑印花税,需要考虑最低收费
return max(size * price * self.p.commission,self.p.minCommission)+platFee
elif size < 0: # 卖出,考虑印花税。
return max(abs(size) * price * self.p.commission,self.p.minCommission)+abs(size) * price * self.p.stampduty+platFee
else:
return 0 # 防止特殊情况下size为0.
注释很清楚,就不解释了。
有的同学眼尖,_getcommission看到还有一个参数pseudoexec,这个干嘛的? 这个参数用于指示当前的调用是否用于真正的订单执行过程。那么怎么还会有不是订单的执行过程呢?大家还记得前面订单有一个参数checksubmit,这个参数就是要求在提交订单前检查现金是否足够,这个检查过程中也会调用_getcommission计算佣金,那么,在一次买/卖交易中,可能多次计算佣金。为了区分哪一次调用是真正的为了订单实际执行的调用,就需要使用这个参数。
区分订单实际执行有啥作用呢?比如这样一个场景,你和券商商量好了,如果交易量(规模)超过5000,就给我佣金50%的折扣。这交易量的计算就得使用这个参数来区分,否则的话,系统将一些并未执行的佣金计算也计入的话,券商也不认啊。
下面给个例子(官网)来看看。
import backtrader as bt
class CommInfo_Fut_Discount(bt.CommInfoBase):
params = (
('stocklike', False), # 期权类资产
('commtype', bt.CommInfoBase.COMM_FIXED), # 固定佣金
# 优惠方案参数
('discount_volume', 5000), # 优惠所需最小合约数量
('discount_perc', 50.0), # 50.0% 的折扣
)
negotiated_volume = 0 # 跟踪实际交易合约规模的参数
def _getcommission(self, size, price, pseudoexec):
if self.negotiated_volume > self.p.discount_volume:#超过最小次数,这设置折扣50%
actual_discount = self.p.discount_perc / 100.0
else:
actual_discount = 0.0#没有超过,那么没有折扣
commission = self.p.commission * (1.0 - actual_discount)#打折后的实际佣金
commvalue = size * price * commission#计算折扣后的佣金。这里的size应该是abs(size),不然有负数。
if not pseudoexec:
# 跟踪实际交易合约规模
self.negotiated_volume += size #如果实际执行(非伪执行),这需要累加本次实际交易的规模。
return commvalue
通过上述例子,应该就很容易理解pseudoexec参数的使用了。
还有一个交易的类是sizer,就是一次买卖操作的规模,也可称之为赌注大小。比如你看多一个股票,那么你准备下多大赌注?在股票交易中,通常的单位是手。一手多少股呢?国内通常是100股,港股也有几十的。一手大概是多少钱?不同股票不同,比如伯克希尔哈撒韦,一手就得几百万吧。但是注意Backtrader中,单位是股。
赌注的大小在量化交易中非常重要,如何决定赌注的大小也有很多技巧,咱们后续在量化系统中再详细研究,咱们先看Backtrader中如何使用sizer。
首先看Backtrader的Sizer如何使用。
在咱们Strategy代码解读中,Strategy预初始化中,会创建一个缺省的FixedSize:
def dopreinit(cls, _obj, *args, **kwargs):
_obj, args, kwargs = \
super(MetaStrategy, cls).dopreinit(_obj, *args, **kwargs)
_obj.broker = _obj.env.broker
_obj._sizer = bt.sizers.FixedSize()
缺省的FixedSize是啥样呢?
class FixedSize(bt.Sizer):
params = (('stake', 1),
('tranches', 1))
缺省就是1股。tranches这个请忽略,是复杂资产的分片,咱们用不上。
在strategy中我们也可以通过setSizer函数替换这个缺省的值
def setsizer(self, sizer):
'''
Replace the default (fixed stake) sizer
'''
self._sizer = sizer
sizer.set(self, self.broker)
return sizer
当然也可以通过getsizer函数获取当前使用的sizer实例。可以看出,在Strategy中,sizer 是可以直接获取/设置的属性。
另外,我们在定制Strategy的时候,可以通过参数设置sizer,参考如下官网代码:
class MyStrategy(bt.Strategy):
params = (('sizer', None),)
def __init__(self):
if self.p.sizer is not None:
self.sizer = self.p.sizer
这样的话,我们可以调用 Cerebro 的同时创建一个 Sizer,并将其作为参数传递给系统中的所有策略,也就是其他的策略可以共享这个sizer。
同样的,我们也可以在Cerebro中设置sizer,我们之前的实例都是采取这种方法。
在Cerebro中,可以通过addsizer和addsizer_byidx函数添加sizer。
addsizer
这个函数添加一个缺省的sizer,适用于任何添加到当前Cerebro的Strategy,如下代码所示:
cerebro = bt.Cerebro()
cerebro.addsizer(bt.sizers.SizerFix, stake=20) # 应用于所有策略的缺省sizer
addsizer_byidx
这个函数可以针对特定strategy添加sizer,Strategy通过idx来指定。idx是addstrategy的返回值。如下代码所示:
cerebro = bt.Cerebro()
cerebro.addsizer(bt.sizers.SizerFix, stake=20) # 应用于所有策略的缺省sizer
idx = cerebro.addstrategy(MyStrategy, myparam=myvalue)
cerebro.addsizer_byidx(idx, bt.sizers.SizerFix, stake=5)
cerebro.addstrategy(MyOtherStrategy)
这一段代码中,增加了一个缺省的sizer,规模为20.同时为MyStrategy策略增加了一个规模为5的sizer。对于MyOtherStrategy,没有指定特殊的sizer,就使用缺省的sizer。
前面咱们讲过,sizer在量化交易中非常重要,可能需要针对不同情况决定不同的sizer,所以,咱们还需要介绍如何定制开发sizer。说明下,以下示例来自官网。
感谢面向对象的机制,自定义sizer并不复杂,步骤如下:
首先继承backtrader.Sizer。通过这个类,可以访问执行买卖操作的到strategy和broker,然后可以获取相应信息决定size大小。通过broker我们可以获取如下信息:
还有一些信息通过如下函数的接口获取。
然后重写_getsizing(self, comminfo, cash, data, isbuy)函数,这个函数接口入参信息如下:
该方法返回操作的规模信息。注意返回的正负号无意义,即:如果操作是卖出操作(isbuy 为 False),则该方法可能返回 5 或 -5。卖出操作只会使用绝对值5。
大家可能不太理解,一个size计算需要这么多信息干啥?实际上,在量化自动化交易中,每一个下注(买卖操作)的大小直接影响收益。比如我们策略,收到第一个做多信号,买入20%,收到第二个,再买40%,收到第三个信号,全部买入。我们就可以在Strategy中记录做多信号次数,然后在自定义sizer中访问strategy的这个变量(做多信号次数),根据现金金额的比例计算出购买的规模,每一次返回的规模不一样,这样我们将这种下注的方式自定义sizer,后续专门用于采用这种下注方式的策略。
下面我们看看Backrtrader的FixedSize代码实例:
import backtrader as bt
class FixedSize(bt.Sizer):
params = (('stake', 1),)
def _getsizing(self, comminfo, cash, data, isbuy):
return self.params.stake
这个固定规模就比较简单,没有任何计算,直接返回参数设置的数量。
下面看一个复杂点的FixedRerverser。
class FixedRerverser(bt.FixedSize):
def _getsizing(self, comminfo, cash, data, isbuy):
position = self.broker.getposition(data)
size = self.p.stake * (1 + (position.size != 0))
return size
这个类继承FixedSize的参数,并且完成:
这个用于一些特殊的处理,比如可以直接将多头头寸改为空头头寸。
下面看一个实际应用案例(官网提供)。
在不改变strategy算法的情况下,我们可以通过修改sizer来实现只做多交易,替代多空交易策略。
比如如下一个策略:
class CloseSMA(bt.Strategy):
params = (('period', 15),)
def __init__(self):
sma = bt.indicators.SMA(self.data, period=self.p.period)
self.crossover = bt.indicators.CrossOver(self.data, sma)
def next(self):
if self.crossover > 0:
self.buy()
elif self.crossover < 0:
self.sell()
这个就是我们之前多次使用的均线策略,这个策略中,只会根据crossover信号进行多空操作,而不管当前的头寸情况。而通过sizer就可以改变这个策略。
class LongOnly(bt.Sizer):
params = (('stake', 1),)
def _getsizing(self, comminfo, cash, data, isbuy):
if isbuy:
return self.p.stake
# Sell situation
position = self.broker.getposition(data)
if not position.size:
return 0 # do not sell if nothing is open
return self.p.stake
在这个sizer中,只有在持有头寸的情况下,才会卖出。当然这里没有判断持有头寸的大小,我们可以优化,如果stake大于持有头寸,只卖出当前头寸。
通过这个示例,我们可以看出,通过sizer可以改变策略。其实大家已可以看出,咱们也可以在strategy中直接计算,比如前面1.5节目标订单中,在strategy中可以自己计算出size完成策略的需要。至于在哪里做比较合适,还是看大家的喜好。
Backtrader提供了几个常用的sizer。
FixedSize
这个是最常用的sizer,前面咱们也讲过,它只会返回一个规定的大小
FixedReverser
这个代码前面讲过,就是返回需要一个固定大小,反转一个头寸或固定大小开一个。
PercentSizer
这个代码看下:
class PercentSizer(bt.Sizer):
params = (
('percents', 20),
('retint', False), #返回整数,而不是浮点数。
)
def __init__(self):
pass
def _getsizing(self, comminfo, cash, data, isbuy):
position = self.broker.getposition(data)
if not position:
size = cash / data.close[0] * (self.params.percents / 100)
else:
size = position.size
if self.p.retint:
size = int(size)
return size
看_getsizing函数:
AllInSizer
赌桌上,“all in",这个气势!但是投资的时候 all in是一种病,得治。记住永远留有后手。
回到Backtrader中,AllInSizer是PercentSizer子类,参数percents为100.
还提供了两个返回整数的对应类:PercentSizerInt和AllInSizerInt。
本文重点描述了Backtrader中交易相关的类,这些类将来会作为咱们量化系统交易模型的建模基础。
后续咱们再继续介绍可视化模块、评估模块以及自动运行相关代码。