在上一讲中,我们尝试了最简单的双均线策略,如果您已经阅读过前两讲的代码,对 backtrader 应该有一定的了解了,本讲将会深入分析 backtrader 一些常用的模块,使得大家能够有一个全局的了解。
-
bt.Strategy
在这个模块中,我们根据我们的交易思想和交易逻辑,去一步步的实现策略,是重中之重。从这个模块中,我们可以调取到我们加载的数据,可以获取当前的持仓量和持仓状态,可以获取到账户资产、账户可以使用的金额等等信息,中间计算的各种变量,也都可以自定义 log 出来或者保存到 excel 中(很少用原生的 Writer)。
首先,我们编写一个策略的时候,需要继承 backtrader 里面的 Strategy 模块,即 class MyStrategy(bt.Strategy),其中,class 是声明类,MyStrategy 是我们自己编写的策略的名字,bt.Strategy 是我们继承的 backtrader 的 Strategy 的类。
下面详细讲解下,Strategy 里面的主要函数,内容都在函数里面进行注释了,请详细看这个类中的主要函数。
class MyStrategy(bt.Strategy): def __init__(self): '''添加一些指标或者一些 self 属性值''' pass def start(self): '''cerebro 告诉策略开始运行,一般默认是空,可以忽略。有时候在实盘的时候,需要调试,这个函数也是需要用上的''' pass def prenext(self): '''大部分指标的计算都需要一定的 bar 之后才会产生值,在 backtrader 的机制中,会根据各个指标的参数值,设定一个需要满足的最小周期,当 bar 的个数没有达到这个最小周期的时候,就会在 prenext 中运行,这也是一个相当有用的函数,在实际中,很有可能会用到''' pass def nextstart(self): '''只调用一次,当 bar 的个数满足之后,开始从 prenext 转移到 next 中。这个函数默认是空的,也可以不要''' # 相应的调用 next 的代码 def next(self): '''在 next 中,调用相应的数据,指标值,买卖平仓,输出数据等,策略的主要部分''' def stop(self): '''策略结束的时候会调用一次这个函数,如果想要输出到 excel 一些策略运行的信息,可以在这个函数里面进行,一般情况下,我会用 df.to_csv()输出到 excel 中相应的策略运行信息,如每个 bar 的 value 是多少。默认是空的,可以要可以不要''' pass def notify_cashvalue(cash, value): '''使用这个函数获取 cash 和 value 变动的信息,说来惭愧,使用了好几年了,我没有用过这个函数。这也说明了,backtrader 实在是大而全,很多功能都提供了''' pass def notify_fund(cash, value, fundvalue, shares): '''使用这个函数获取 cash,value,fundvalue,shares 的变动信息,同样没有使用过''' pass def notify_order(order): '''使用这个函数,可以获取订单每次变动的信息,一般来说,使用固定的代码就行,下面是我自己在回测的时候,常用的固定代码''' if order.status in [order.Submitted, order.Accepted]: # order 被提交和接受,这个一般不用打印出来,实盘的时候,可能少数情况下,需要测试这两个 return if order.status == order.Rejected: self.log(f"order is rejected : order_ref:{order.ref} order_info:{order.info}") if order.status == order.Margin: self.log(f"order need more margin : order_ref:{order.ref} order_info:{order.info}") if order.status == order.Cancelled: self.log(f"order is concelled : order_ref:{order.ref} order_info:{order.info}") if order.status == order.Partial: # 这个其实一般也只有在实盘中才会有,回测中,一般情况下,都是全部成交 self.log(f"order is partial : order_ref:{order.ref} order_info:{order.info}") # 如果订单完成了,就打印如下信息 if order.status == order.Completed: # 如果是做多或者平空 if order.isbuy(): self.log("buy result : buy_price : {} , buy_cost : {} , commission : {}".format( order.executed.price,order.executed.value,order.executed.comm)) # 如果是平多或者做空 else: self.log("sell result : sell_price : {} , sell_cost : {} , commission : {}".format( order.executed.price,order.executed.value,order.executed.comm)) def notify_trade(trade): '''这个函数可以获取每次交易的变动信息''' # 一个 trade 结束的时候输出信息 if trade.isclosed: self.log('closed symbol is : {} , total_profit : {} , net_profit : {}' .format( trade.getdataname(),trade.pnl, trade.pnlcomm)) # trade 开始的时候输入的信息 if trade.isopen: self.log('open symbol is : {} , price : {} ' .format( trade.getdataname(),trade.price)) # 如果想要把每笔交易都输出出来,可以在 init 中,设置 self.trade_result=[],这样,把每笔交易的开和平,都保存在这里面,在 stop 中输出到本地进行校对策略。 # self.trade_result.append([self.current_date,trade.getdataname(), # trade.size,self.getdatabyname(trade.getdataname()).open[0], # trade.data.open[-1*trade.barlen],trade.pnl,trade.pnlcomm])
扯一段对策略的感悟,不感兴趣的可以忽略,对编写策略影响不大。
在backtrader 的官方文档中,作者把 strategy 按照人的生命周期划分了几个阶段,怀孕(init),出生(start),童年(prenext),成年礼(nextstart,作者没有提这个函数,我自己加的),成年(next),死亡(stop),当看到作者的这几个阶段的不同划分,我特别震惊,越想,越有韵味。原来,人生,就是一个大的策略,原来,我们可以站在策略的角度上,设计我们的人生,用不同的算法,我们的人生经历和结果完全不同。
在怀孕阶段,从策略层面上来说,我们要设置好初始变量,加载上需要调用的指标,大部分策略的初始变量和调用的指标可能差不多,但是,每个人的初始变量和加载上的指标,各有不同,有的人多一些,有的人少一些。这和我们每一个人的投胎,不是异曲同工之妙吗?有的人投胎比较好,加载的初始变量和指标比较多,在以后的成长阶段中,父母和家庭提供的帮助比较多(更多的变量和指标,往往需要计算机使用更多的内存和 cpu),在长大成人之后,素质一般相对比别人更好(可以很方便的调用指标值,如果,加载的指标是有用的话,如果加载的指标是错的,可能反而危害会更大)。
出生,童年,成年礼之前,从主要靠父母家庭,慢慢到自己完全主宰自己的生活,我们一步步成长,长大成人。
当我们长大成人之后,从策略层面上看,我们就可以设计我们的交易策略,决定什么情况下买卖,什么情况下,加仓减仓,什么情况下,止盈止损,交易哪些品种,交易多少。从人生的阶段,我们就要开始独立自主,独立做判断:我们如何认识世界,如何和世界相处,做什么,不做什么,和谁交往,把时间和经历花在什么地方,这就是我们成年后的生活,在某种程度上,是可以做成一个策略的。
总有一天,我们也会死亡。当我们的人生 stop 的时候,你能给这个世界留下来什么呢?
在代码部分,分享完了一个策略的各个部分,在接下来,主要分享一下,在 next 中,也就是在写策略的过程中,主要使用的一些函数,从功能实现的角度讲起
# 1、如何买卖和取消订单 """可以做多,平多,做空,平空,取消下的未成交的限价单,是一个完善的量化平台的基本功能。 在 backtrader 的 strategy 中,主要使用 self.buy()实现做多或者减少一个空头仓位,self.sell()实现做空或者减少一个多头仓位,self.close()实现平仓,self.cancel()实现取消订单。 self.buy(),self.sell()的参数有很多,大家在使用的时候,主要记住两个就可以了,其他的用到的时候可以去查询。这两个关键的参数就是 data 和 size, data 就是选择哪个股票或者期货品种进行买卖,如果 data 没有设置,那么默认的值就是 None,在交易的时候会默认加载到 cerebro 中的第一个数据,所以,如果回测的是多个股票或者品种(即多个数据),记得要设置好数据哦。 size 就是交易的数量,都是正数,做空也用正数。 还有个参数 exectype 需要提一下,当我们忽略这个参数的时候,我们是使用的 Market order,即市价单,也就是说,我们不限定价格,也不知道我们会在什么价格成交,我们下单的下一个瞬间,就尽可能去成交,如果我们是在收盘的时候下单,就是在第二天的开盘成交;如果你的交易量比较小,一般可以默认是对手价加 0 到 2 个滑点成交,如果你的交易量比较大,你就要考虑使用算法交易,进行拆单,降低摩擦成本。 self.close,传入参数是 data,size 一般默认是 None,这样会自动获取当前这个 data 的持仓,然后全部平掉,即 self.close(data)就会把这个 data 上的仓位全部平掉。 self.cancel(),传入的参数是 order,self.cancel(order)会把这个 order 给平掉,如果这个 order 还没有成交的话。 这四个是基本的功能,也比较推荐大家使用这几个进行交易买卖。 """ # 2、如何维持一定量的交易量 """使用 order_target_size 函数来实现,这个函数主要有两个函数, data 和 target,data 就是选择哪个数据来维持 target 量的持仓量,target 就是一个数字,是要维持的持仓量, 当现有的数据的持仓量大于 target,就会平仓一部分, 当现有的数据的持仓量小于 target,就会加仓一部分, 如果持仓量和 target 相等,就不操作。 当你使用这个函数的时候,结果就是,最终 data 上的持仓量就是 target""" # 3、如何调整仓位,使得现在的持仓等于一个固定价值? """使用 order_target_value,同样也是两个参数 data 和 target,data 就是选择哪个数据来维持 target 的价值,target 就是一个数字,是要维持的价值, 当现有的数据的价值大于 target,就会平仓一部分, 当现有的数据的价值小于 target,就会加仓一部分, 如果价值和 target 相等,就不操作。 当你使用这个函数的时候,结果就是,最终 data 上的当前 bar 结束的时候的 value 就是 target """ # 4、如何使得当前的仓位的价值等于账户当前 value 的一定比例? """使用 order_target_percent data 和 target,data 就是选择哪个数据来维持 target 的价值,target 就是一个数字,是要维持的当前账户总的 value 的比例,一般用一个小数表示, 实际上,需要实现的是,data 的持仓价值等于 target*self.broker.get_value(),用 data_value 代表现有的持仓的价值,用 value 代表要实现的目标价值, 当 value>data_value 的时候,就会加仓,原先做多的时候,继续加多;原先做空的时候,继续加空仓; 当 value<data_value 的时候,就会减仓,原先做多的时候,会平掉一部分多单;原先做空的时候,会平掉一部分空单。 """ # 5、如何下一个组合单(成交之后,实现一个止损单和止盈单)? """backtrader 上增加的在 buy 和 sell 的基础上的组合功能。 def buy_bracket(self, data=None, size=None, price=None, plimit=None, exectype=bt.Order.Limit, valid=None, tradeid=0, trailamount=None, trailpercent=None, oargs={}, stopprice=None, stopexec=bt.Order.Stop, stopargs={}, limitprice=None, limitexec=bt.Order.Limit, limitargs={}, **kwargs): 在使用这个功能的时候,主要有五个参数需要注意: 1.data 在哪个数据上去实现 2.size 交易量的大小是多少 3.price 限价单的价格 4.stopprice 止损价的价格,做多时,应该小于 price 5.limitprice 止盈价的价格,做多时,应该大于 price """, # 6、如何获取一个 data 的持仓量? """有好几个函数可以实现,如 self.getposition(data).size 或者用 self.getpositionbyname(data._name).size""" # 7、如何获取当前账户的 value? """获取账户整体价值,使用 self.broker.get_value();获取可用资金,使用 self.broker.get_cash()""" #以上是实现一个策略的时候,经常用到的一些函数。写到这里,不知不觉,已经 11 点了,时间过得真快。 # 人生有时候就是一个策略,在什么地方分配我们的时间和精力,我们的策略不同,对我们的人生,也许会产生很大的影响。即将到来新的一周,祝大家一周快乐。