这一节我们来深入实践交易的设置以及自定义绘图。
01_交易设置
参考文档:
- Backtrader系列教程④:交易篇(上)_backtrader撤单操作-CSDN博客
- 量化投资之工具篇一:Backtrader从入门到精通(8)-交易系统代码详解-CSDN博客
001_股票软件的交易评测系统
炒股的人电脑上通常都会有几个PC版的股票软件,这些软件现在也在与时俱进做了量化交易的功能,可以在软件上进行量化模拟回测(免费)以及量化交易(收费)。我们暂且不谈这些新增的量化交易的部分,先看一下经典的交易评测系统的构成以及进行评测前有哪些设置以及与backtrader所对应的功能:
A_建仓规则
包含评测时间段的选择,对应到backtrader里就是feeds.PandasData()中的fromdate和todate,这里可以看到它们很细心的设置了向前多取100个数据用于计算,这主要是对于均线比如60日需要到第60天才会有计算数据的小问题,所以我们的交易策略里是不是也需要加入这个功能?
另外默认评测结束时强制平仓计入收益,这也对应了backtrader里如果是isopen的状态不会计算利润的问题,这个在上一节我们是添加了判断语句做成了浮动盈亏和浮动交易成功率,显示出来会更清楚些,毕竟强制平仓并没有遵守策略。
建仓规则里的评测公式,就对应backtrader的指标判断了,而分配资金对应setCash(),开仓使用多少资金或固定数量对应sizer中的固定数量和固定百分比。
股票软件中对于连续信号不再买卖,这是因为它的平仓规则相对简单,也没有一次买入必须对应一次卖出的要求,这个在backtrader中是没有这个问题的。
B_交易方式
首先是手续费,可以看到手续费的设置也是比较简单的示意做法,并没有与实际相符合,我们先忽略关于期货的部分,股票交易通过是双向收取佣金,卖出时还要收取印花税,另外还有一个不免5的规则即手续费最低5元。另外ETF是近来出现的,它不需要收取印花税,这里没有这个选择,我们在backtrader里也会回测ETF,所以需要添加的。
介入价位这里就很有意思了,可以有很多的选择例如本周期收盘价(信号触发当天收盘价),本周期中价,本周期开盘价,最高价,最低价;次周期(第二日)收盘价,开盘价...... 默认都是本周期收盘价。这个在backtrader里就有比较科学的分辨,它是这么定义的,正常的情况下,今天收盘后才能确认信号触发,所以成交是第二天的开盘价。
滑点设置,这个在backtrader中对应Slippage滑点设置。
C_平仓规则
可以看到,股票软件中对于平仓的规则非常的简陋,采用的是固定止盈,固定止损和固定周期平仓这三种,并没有使用到指标的卖出信号,在这一点上,backtrader的回测就相对要高级和严谨的多了,这也是为什么我们没在股票软件里评测系统而选择用backtrader的一个重要因素。
其他设置中,计算相对收益率选择了默认的沪深300,这里可以选择任何一个指数或股票做基准,目的是计算类似于阿尔法-贝塔的评价指标,它们需要有一个基准才能进行计算。
002_backtrader的交易设置
看完了股票软件中的交易评测系统的设置,再回过头来学习和实践backtrader里的交易设置,就会发现很多问题迎刃而解了。
A_持仓与交易规模
这里有个单词是position,金融行业翻译过来叫做"头寸",在百度里查了一下,据说是跟袁大头(银元)有关系,十个银元叠起来的高度刚好是一寸,大家就用"头寸"这个词来形容持有的资产。我还是喜欢称它为"持仓",感觉更容易明白是什么。
假设在回测中设定了setCash(100000),那么开始的时账户里就有10万的现金,这个跟position没关系,只有进行了买入操作才会有position的产生。注意一点,为什么我们到现在的程序中next()函数必须有一句 if not self.position的判断?
def next(self):
if self.order: # 检查是否有指令等待执行
return
if not self.position: # 没有持仓 才会进入
if self.crs.l.crsup:
self.order = self.buy() # 执行买入
else:
if self.crs.l.crsdn:
self.order = self.sell() # 执行卖出
这是因为期货以及其他国家的股票是可以卖空的,没有持仓这个条件限制,那么先触发了sell的条件就会进行卖空操作了,其实在下跌趋势过程中卖空是很赚钱的,但是由于规则限制,我们只能先买入,这是对应现实的规矩,那么在backtrader中回测就要按着实际的情况来。
position有两个重要的信息:size,规模;price,价格。
我们看到在股票软件建仓规则里分2种交易数量的控制,一种是固定数量,另一种是按百分比(全仓100%,部分可以设置比如50%)。在backtrader里,这两种规模都是可以设置的。
于是,我们把run_main_plot_01中的addsizer替换掉,重命名为run_main_plot_02
cerebro.addsizer(bt.sizers.SizerFix,stake=2000) # 设置固定2000股
run_main_plot_02(mystrategy5,df_stock_list[1], myStockList[1])
----------------------
2022-03-22, 买入, No.002179, Price: 44.52, Cost: 89040.00, Comm :89.04,size :2000.00
2022-06-06, 卖出, Price: 46.42, Comm 92.84
2022-06-06, OPERATION PROFIT, 毛利润 3800.00, 净利润 3618.12
2022-08-02, 买入, No.002179, Price: 47.08, Cost: 94160.00, Comm :94.16,size :2000.00
2022-09-15, 卖出, Price: 50.28, Comm 100.56
2022-09-15, OPERATION PROFIT, 毛利润 6400.00, 净利润 6205.28
.......
设置成固定数量操作非常简单,但并不符合实际需求,因为市场上的股票价格区间差异很大,有的1块钱1股,有的要几百块,这样股票与股票之间就不能进行有效的对比了。因此,我们更倾向于使用资金百分比的设置,这样各股票持仓就差不多(size * price)。
前面我们一直使用的就是资金的50%规模买入,使用的语句如下,
cerebro.addsizer(bt.sizers.PercentSizer, percents=50) # 设置每次交易使用资金的比例为50%
-------------------
Starting Portfolio Value: 100000.00
2022-03-22, 买入, No.002179, Price: 44.52, Cost: 50000.00, Comm :50.00,size :1123.09
2022-06-06, 卖出, Price: 46.42, Comm 52.13
2022-06-06, OPERATION PROFIT, 毛利润 2133.87, 净利润 2031.74
2022-08-02, 买入, No.002179, Price: 47.08, Cost: 51299.17, Comm :51.30,size :1089.62
2022-09-15, 卖出, Price: 50.28, Comm 54.79
.............
在这种情况下,100000的资金的时候,cost=50000,size=1123.09,当卖出有了利润后,继续以50%的资金买入,cost = 51299,size=1089,size降低因为price从44.52增加到了47.08。
这种模式的sizer是可以用的,但远远不够准确,因为A股的最小买入成交量是100股,所以我们还不能这么简单的使用PercentSizer,需要自定义一个更符合实际的。
# 自定义percent_sizer
class MyPercentSizer2(bt.Sizer):
'''This sizer return percents of fixed cash
My sizer will clip min size to 100
'''
params = (
('percents', 20),
('retint', False), # return an int size or rather the float value
)
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) # (资金 * 20%)/ 收盘价
size_raw = 100000 / data.close[0] * (self.params.percents / 100)
size = round(size_raw /100.0, 0) * 100 # 取最小100整数
else:
size = position.size
if self.p.retint:
size = int(size)
return size
而在run_main_plot中,需要更改代码如下:
# cerebro.addsizer(bt.sizers.PercentSizer, percents=50)
cerebro.addsizer(MyPercentSizer2,percents=50)
再次运行回测输出如下,每次买入的是接近资金50%的100股的整数倍,与A股的规则就同步了。
run_main_plot_03(mystrategy5,df_stock_list[1], myStockList[1])
----------------------
Starting Portfolio Value: 100000.00
2022-03-22, 买入, No.002179, Price: 44.52, Cost: 48972.00, Comm :48.97,size :1100.00
2022-06-06, 卖出, Price: 46.42, Comm 51.06
2022-06-06, OPERATION PROFIT, 毛利润 2090.00, 净利润 1989.97
2022-08-02, 买入, No.002179, Price: 47.08, Cost: 51788.00, Comm :51.79,size :1100.00
2022-09-15, 卖出, Price: 50.28, Comm 55.31
2022-09-15, OPERATION PROFIT, 毛利润 3520.00, 净利润 3412.90
....................
B_交易手续费与滑点
在实际交易中,由于市场波动、网络延迟等原因,指定的交易价格与实际成交价格会存在差别,出现滑点。
滑点是指交易的预期价格与交易实际执行价格之间的差异。滑点可能随时发生,在使用市价单时市场波动较大的时期最为普遍。当执行大订单但所选价格没有足够的交易量来维持当前的买卖差价时也会发生这种情况。这个对于个人交易者作用实在不大,咱们那点钱,还不至于能影响市场,滑点可能性比较小。
为了让回测结果更真实,在交易前可以通过 brokers 设置滑点,滑点的类型有 2 种:
- 百分比滑点
- 固定滑点
不论哪种设置方式,都是起到相同的作用: 即买的 “更贵”,卖的 “更便宜” 。
cerebro = bt.Cerebro()
#......
cerebro.broker.set_slippage_perc(perc=0.01) # 设置1%的滑点
我们用同一个数据分别有滑点和没滑点的回测对应买入和卖出位置进行对比
不使用滑点 | 设置 1%的滑点 |
---|---|
组合期初资金: 100000.00 ------------- next 的第19次循环 -------------- 当前持仓量 0 当前持仓成本 0.0 2023-03-24, 已买入, 价格: 10.70, 费用: 10700.00 ------------- next 的第27次循环 -------------- 当前持仓量 1000 当前持仓成本 10.7 2023-04-06, 已卖出, 价格: 10.30, 费用: 9630.00 | 组合期初资金: 100000.00 ------------- next 的第19次循环 -------------- 当前持仓量 0 当前持仓成本 0.0 2023-03-24, 已买入, 价格: 10.81, 费用: 10807.00 # 原价为10.7 取1%为 0.107 # 即 原价*(1.01) = 10.7*(1.01) = 10.81 ------------- next 的第27次循环 -------------- 当前持仓量 1000 当前持仓成本 10.807 2023-04-06, 已卖出, 价格: 10.20, 费用: 9726.30, # 原价10.30 所以 10.30*(0.99) = 10.20 |
手续费是在进行金融交易或使用某些服务时,由服务提供者收取的费用。手续费的计算标准通常基于多种因素,包括交易金额、交易类型、支付方式、商户类型、支付平台以及交易性质等。
在前面的实践中,我们一直使用的是一个简单设置的百分比费用:
cerebro.broker.setcommission(commission=0.001) # 设置交易佣金
这就是设定了一个双向收取的千分之一的手续费。但这个实在太简单了,不能与实际的交易相符。
交易时是否考虑交易费用对回测的结果影响很大,所以在回测是通常会设置交易税费,不同标的的费用收取规则也各不相同:
- 股票:目前 A 股的交易费用分为 2 部分:佣金和印花税,其中佣金双边征收,不同证券公司收取的佣金各不相同,一般在 0.02%-0.03%(万二) 左右,单笔佣金不少于 5 元;印花税只在卖出时收取,税率当前为 0.05%(万五)。
- 期货:期货交易费用包括交易所收取手续费和期货公司收取佣金 2 部分,交易所手续费较为固定,不同期货公司佣金不一致,而且不同期货品种的收取方式不相同,有的按照固定费用收取,有的按成交金额的固定百分比收取:合约现价*合约乘数*手续费费率。
- ETF:ETF即交易所交易基金(Exchange Traded Fund),是一种特殊类型的开放式基金。中国大陆的税务政策规定,在二级市场买卖ETF时不征收印花税. 这是ETF相较于其他投资产品的一个优势。
考虑到目前自己的情况,我们暂时只做A股的回测(股票和ETF),所以根据交易费用的设置,把佣金和印花税按实际情况计算出来。这个只要做一次就行,后面的所有策略都不会去改变它。最麻烦的可能就是在回测时要判断是股票还是ETF。
我们不在commission的部分啰嗦,想深入了解详细信息的可以去看参考的文档或者源码,这里就直接把税费管理类做出来
### 税费管理类
class StockCommission(bt.CommInfoBase):
params = (
( 'stocklike', True), # 指定为股票模式
( 'commtype', bt.CommInfoBase.COMM_PERC), # 使用百分比费用模式
( 'percabs', True), # commission 不以 % 为单位
( 'isETF',False), # 是否是ETF
( 'stamp_duty', 0.0005),) # 印花税
# 自定义费用计算公式
def _getcommission(self, size, price, pseudoexec):
comm1 = max(abs(size) * price * self.p.commission, 5)
if size > 0: # 买入时,只考虑佣金
return comm1
elif size < 0: # 卖出时,同时考虑佣金和印花税
if self.p.isETF == False: # 股票要计算印花税
return comm1 + abs(size) *price *self.p.stamp_duty
else:
return comm1 # ETF不计算印花税
else:
return 0
然后在run_main_plot函数中,把原来的语句替换掉,这里用参数简单判断是否是股票列表中的来确定ETF还是股票,后续再做细节的调整
def run_main_plot_05(strategy1, df_data0,name1):
cerebro = bt.Cerebro() # 创建Cerebro引擎
# 将数据源设置为PandasData,并加载数据
data = bt.feeds.PandasData(dataname=df_data0,fromdate=datetime(2022, 1, 15),todate=datetime(2024,9,11))
cerebro.adddata(data,name=name1) # 将数据添加到Cerebro
cerebro.addstrategy(strategy1) # 添加策略
cerebro.broker.setcash(100000.0) # 设置初始资金
# cerebro.broker.setcommission(commission=0.001) # 取消初始设置交易佣金
if name1 in myStockList:
comm_etf = False
mycomm = StockCommission(commission=0.0002,isETF=comm_etf) # 佣金万分之二
cerebro.broker.addcommissioninfo(mycomm) # 佣金设置
cerebro.addsizer(MyPercentSizer2,percents=50)
print(f'Starting Portfolio Value: {cerebro.broker.getvalue():.2f}') # 打印起始组合价值
cerebro.run() # 运行分析
print(f'Final Portfolio Value: {cerebro.broker.getvalue():.2f}') # 打印最后组合价值
cerebro.plot(style='candle') # 绘制结果图
run_main_plot_04(mystrategy5,df_stock_list[1], myStockList[1]) # 默认千一佣金
---------------------
Starting Portfolio Value: 100000.00
2022-03-22, 买入, No.002179, Price: 44.52, Cost: 48972.00, Comm :48.97,size :1100.00
2022-06-06, 卖出, Price: 45.96, Comm 50.55
2022-06-06, OPERATION PROFIT, 毛利润 1579.38, 净利润 1479.86
........
run_main_plot_05(mystrategy5,df_stock_list[1], myStockList[1]) # 新的手续费
---------------------
Starting Portfolio Value: 100000.00
2022-03-22, 买入, No.002179, Price: 44.52, Cost: 48972.00, Comm :9.79,size :1100.00
2022-06-06, 卖出, Price: 46.42, Comm 35.74
2022-06-06, OPERATION PROFIT, 毛利润 2090.00, 净利润 2044.46
C_交易时机管理
交易时机管理就对应于股票软件的交易价位选择。对于交易订单生成和执行时间,Backtrader 默认是 : “当日收盘后下单,次日以开盘价成交”,这种模式在回测过程中能有效避免使用未来数据。
为了应对一些特殊交易场景,Backtrader 还提供了一些 cheating 式的交易时机模式:
- Cheat-On-Open 是“当日下单,当日以开盘价成交”模式, 在该模式下,Strategy 中的交易逻辑不再写在 next 方法里, 而是写在特定的 next_open、nextstart_open 、prenext_open 函数中
- Cheat-On-Close 是“当日下单,当日以收盘价成交”模式, 在该模式下,Strategy 中的交易逻辑仍写在 next 中
实际情况下,只有收盘当时这根K线走完了才能指标触发(策略如果是分钟线相同),这个时候已经无法买入了,真实的情况就应该是第二根时间一出来就买入。
联想一下很多短视频里大V们讲指标的时候总是故意避开这个交易时机的问题,有时甚至把指标向日K线对齐的时候还偏了那么一点,造成指标提前的假象或者让你感觉你能在涨起来的开盘价(甚至是最低价)买入,在计算盈亏的时候也是从这根红柱的实体底部开始计算买入而在高位下跌的顶部计算卖出,这就会造成这个指标能赚很多的假象。 而事实上永远不可能。
在股票软件中从来不会讨论这个,但实际上这个非常的重要,很多的时候指标触发买入是因为当天的K线涨得足够高,计算下来指标就触发了,但是等你看到它涨起来了再买进去,它第二天又下跌或者就是一直横盘了。如果你选择了cheat的模式,那评测得到的收益可能是高的,所以在这种实际问题上,使用backtrader进行回测就可以更接近实际情况,数据真实有效。
这也是为什么在绘制的图形上,crossover的信号已经触发了,但向上或向下的箭头会在下一个交易日才出现,特别是A股现阶段容易走抵消K线,就是那种昨天大涨,今天大跌恰好抵消回到前天收盘的位置,那么真实情况下,指标触发了才买入,接着第二天买进就大跌,第三天卖出又涨了进入恶性循环圈,散户往往就是这么亏了大钱。
我们必须要知道以当天的开盘或收盘成交都属于cheat模式。很多讲指标的都是使用了这个模式。不过对于日线交易来说,我们会比较多的在2:30之后感觉K线没有悬念时就买入,近似可以认为买在收盘价,我个人觉得是可以应用这种情况的。
设置Cheat只需要一句代码,若是cheat-on-open则需要在next_open里写策略,这显然很麻烦。综上,我们还是设置一下cheat-on-close。
cerebro.broker.set_coc(True) # coc即Cheat On Close
# 原模式, 当天下单以第二天开盘价成交
------------- next 的第189次循环 --------------
卖单日期 2023-12-08 收盘价 12.49 明日开盘价 12.0
2023-12-11, 已卖出, 价格: 12.00, 费用: 10590.00, 佣金 17.00
#以第二天开盘价成交
------------- next 的第236次循环 --------------
买单日期 2024-02-22 收盘价 8.83 明日开盘价 8.85
2024-02-23, 已买入, 价格: 8.85, 费用: 8850.00, 佣金 5.00
组合期末资金: 101316.36
# Cheat-On-Close模式, 第二天成交,但成交价是当天的收盘价
------------- next 的第189次循环 --------------
卖单日期 2023-12-08 收盘价 12.49 明日开盘价 12.0 #以收盘价成交
2023-12-11, 已卖出, 价格: 12.49, 费用: 10640.00, 佣金 17.49
------------- next 的第236次循环 --------------
买单日期 2024-02-22 收盘价 8.83 明日开盘价 8.85
2024-02-23, 已买入, 价格: 8.83, 费用: 8830.00, 佣金 5.00
组合期末总资产: 101595.93
从上面的对比输出结果可以看到,使用了Cheat-On-Close模式后,成交价是当天的,但成交日期仍然是第二天的。
D_订单类型
关于订单类型,直接问AI得到:
以下是Backtrader中一些常见订单类型的代码示例和解释:
市价单 (Market Order): 市价单以当前市场价格立即执行。在Backtrader中,如果没有指定
exectype
,那么默认就是市价单。市价单的价格通常由下一个Bar的开盘价决定。# 市价单,以默认的持仓大小购买 order = self.buy()
限价单 (Limit Order): 限价单只有在市场价格达到或更好于指定的
price
时才会执行。限价单可以设置有效期valid
。# 限价单,设置价格和有效期 order = self.buy( exectype=Order.Limit, price=self.data.close[0] * 1.02, valid=datetime.datetime.now() + datetime.timedelta(days=3))
收盘价单 (Close Order): 收盘价单会在下一个Bar实际收盘时以收盘价执行。
# 收盘价单 order = self.buy(exectype=Order.Close)
止损单 (Stop Order): 止损单在市场价格达到或超过指定的
price
时触发,并以市价单的方式执行。这通常用于在价格达到某个不利水平时退出市场。# 止损单,设置触发价格和有效期 order = self.buy(exectype=Order.Stop, price=self.data.close[0] * 1.02, valid=datetime.datetime.now() + datetime.timedelta(days=3))
止损限价单 (Stop Limit Order): 止损限价单在市场价格达到或超过指定的
price
时触发,然后以plimit
价格或更好的价格执行。这种订单结合了止损单和限价单的特点。# 止损限价单,设置触发价格和限价 order = self.buy(exectype=Order.StopLimit, price=self.data.close[0] * 1.02, plimit=self.data.close[0] * 1.07)
取消订单 (Cancel Order): 取消订单用于撤销之前提交的订单。
# 取消已存在的订单 self.cancel(order)
直接看订单类型可能是有点懵的,这个东西要么在期货交易模拟盘中交易一下,要么在股票软件APP中用一下“条件单”这个功能,经过这两个实践以后再回来看订单类型就迎刃而解了。
看一下止损单,当资产触及设定的止损价格时触发。这里重要的是学到在任何情况下要设置止损,股票交易总是有亏有盈,但是不设止损就交易是非常危险的行为,错了就要认。
条件单最基本是的限价单和价格条件单,前者同上面order中的限价单,只设定1个值(都有时间有效范围),后者与限价止损单类似,先触发再以某价格成交。有的软件的条件单功能比较多,比如日涨跌幅买卖,反弹买,回落卖,定时买卖,还有的自带止盈止损设置,设定好了严格执行也是非常不错的选择。其实有些软件的条件单已经有很不错的量化或自动交易的功能在里面了,比如说网格交易。
003_交易设置与交易系统
由上面的股票软件的交易评测系统和backtrader的交易设置,我们在实践过程中发现了一些有趣的知识点。如果不自己在backtrader里面实践一番,我们就很可能跟股票软件的评测系统一样,忽略了某些其实很关键的因素。
比如说佣金的问题,不免5情况下,你的真实收益会被折扣很多,再比如期货交易模拟盘里是不设佣金的,而实际上手续费高的吓死人,特别是日内交易,有时候连赚的钱连手续费都不够......
比如说介入价位,讲指标的总是在说"你看指标触发买点了,这个时候你买进去,那这一波的涨幅你就全都拿下了",其实的情况是指标触发时可能已经涨完了。再比如金叉的交叉位置,是昨天和今天的连线,所以显示在图上是在今天之前的,给人的错觉就是指标先触发了,但其实还是今天大涨了以后,才会得到计算出的短周期上穿长周期,才会在昨天和今天之间产生一个金叉。
再比如说滑点,从滑点的概念,我们需要学习到的主要是交易过程中的最高/低价格和此价格的成交量的关系,很多时候,你已经成功设定到了当天的最低价买入,订单也submitted了,但是盯着它看了半天,它就是没有成交。实际的交易中,我们常遇到价格对了,但成交不了,以前我们总是拍大腿认为自己运气不好,其实还是交易系统认知不到位。
backtrader是个很不错的工具,通过它我们可以一步一步认识到交易上的问题、难点和关键点,逐渐建立起自己的交易系统,逐渐完善自己的交易系统。至少实践到这里,我们听别人讲指标时能有一个客观的评判,不再那么单纯的只要是坑就掉进去了。
02_绘图增强
参考文档:
- Backtrader系列教程⑦:可视化篇(重构)_backtrader benchmark-CSDN博客
- 《BackTrader量化交易图解》第8章:plot 绘制金融图_backtrader plot-CSDN博客
- Backtrader 文档学习- Plotting_backtrader plot-CSDN博客
这里提一下,参考文档第1个里代码区的显示不知道出了什么问题,我是用BS4把DIV中有文本的给抓出来,重新调整(去空行,去空格)以后才看的。
001_观测器observers
A_观测器模块与绘图
observers 是 Backtrader 的“观测器模块”,主要用于统计回测信息,并在 plot( ) 的帮助下实现信息的可视化展示。
Observers | 功能 | 可视化 |
---|---|---|
Broker | 记录了经纪商 broker 中各时间点的可用资金和总资产 | 同时展示 cash 和 values 曲线 |
BuySell | 记录了回测过程中的买入和卖出信号 | 会在价格曲线上标注买卖点 |
Trades | 记录了回测过程中每次交易的盈亏 | 会绘制盈亏点 |
TimeReturn | 记录了回测过程中的收益率(每日) | 绘制 TimeReturn 收益曲线 |
DrawDown | 过程的回撤情况 | 绘制回撤曲线 |
Benchmark | 记录了业绩基准的收益序列,业绩基准的数据必须事先通过 adddata | 同时绘制策略本身的收益序列和业绩基准的收益曲线 |
其中,Broker、Trades、BuySell 3个观测器,默认是自动添加给 cerebro 的。我们可以关闭它们再重新添加,否则就会在这3个观测器的基础上添加。
简单实践几种情况下的代码与图形如下
# 左上图
cerebro = bt.Cerebro() # 创建Cerebro引擎,默认3个观测器
# 右上图
cerebro = bt.Cerebro(stdstats=False) # 创建Cerebro引擎,去掉默认观测器
# 左下图
cerebro = bt.Cerebro() # 创建Cerebro引擎
cerebro.addobserver(bt.observers.TimeReturn)
cerebro.addobserver(bt.observers.DrawDown)
# 右下图
cerebro = bt.Cerebro(stdstats=False) # 创建Cerebro引擎,去掉默认观测器
cerebro.addobserver(bt.observers.TimeReturn)
cerebro.addobserver(bt.observers.DrawDown)
由上面结果可知,默认观测器是buy/sell, Broker,Trades这三个,如果默认Cerebro()则绘制的图形是顶部带有2个小图Broker和Trades,且主图里会显示buy/sell图标的;如果去掉默认(stdstats=False)则只显示主图和附图。
前面也谈到过我们对于国际的绿涨红跌非常不习惯,以及Trades显示的蓝和红圆不够直观,我们会把它们替换掉,这就需要先去掉默认的观测器的基础上再添加。
B_观测器与数据
从代码cerebro.addobserver(bt.observers.Broker)可以跳转到源码\observers\__init__.py
from __future__ import (absolute_import, division, print_function,
unicode_literals)
# The modules below should/must define __all__ with the Indicator objects
# of prepend an "_" (underscore) to private classes/variables
from .broker import *
from .buysell import *
from .trades import *
from .drawdown import *
from .timereturn import *
from .benchmark import *
from .logreturns import *
再任意点开一个观测器,以Broker为例
class Broker(Observer):
'''This observer keeps track of the current cash amount and portfolio value in
the broker (including the cash)
Params: None
'''
_stclock = True
params = (
('fund', None),
)
alias = ('CashValue',)
lines = ('cash', 'value')
plotinfo = dict(plot=True, subplot=True)
def start(self):
if self.p.fund is None:
self._fundmode = self._owner.broker.fundmode
else:
self._fundmode = self.p.fund
if self._fundmode:
self.plotlines.cash._plotskip = True
self.plotlines.value._name = 'FundValue'
def next(self):
if not self._fundmode:
self.lines.value[0] = value = self._owner.broker.getvalue()
self.lines.cash[0] = self._owner.broker.getcash()
else:
self.lines.value[0] = self._owner.broker.fundvalue
observers 中记录了各种回测数据,可以将其看作是一个支持可视化展示的数据存储器,所以 observers 属于 lines 对象。由上面源码可知,如果不是基金模式(当前我们在做股票回测,默认不是基金模式 fundmode = False),则observers中的next赋值的数据就是 broker.getvalue和broker.getcash的值,而Broker这个观测器有2条lines,分别为cash和value.
如果想在 Strategy 中读取 observers 中的数据,就会用到 line 的相关操作,observers 的数据通过 self.stats 对象 来连接。
# 请先确保有Broker观测器
#......
def next(self):
if not self.position:
if self.crsup > 0:
print('buy Broker getvalue', self.broker.getvalue())
print('observers getvalue', self.stats.broker.value[0])
self.buy()
else:
if self.crsdn > 0:
print('sell Broker getvalue', self.broker.getvalue())
print('observers getvalue', self.stats.broker.value[0])
self.sell()
-----------------------------
Starting Portfolio Value: 100000.00
buy Broker getvalue 100000.0
observers getvalue 100000.0
2022-10-12, 买入, No.002179, Price: 46.18, Cost: 50798.00, Comm :10.16,size :1100.00
sell Broker getvalue 97074.8404
observers getvalue 96414.8404
2023-06-30, 卖出, Price: 43.75, Comm 33.69
2023-06-30, OPERATION PROFIT, 毛利润 -2673.00, 净利润 -2716.85
......
从输出结果上看,使用self.broker和self.stats.broker都能得到值,另外buy之前的两者是相同的,但sell之前的value的值却不相等,看一些说明observers 是在所有指标被计算完之后、在执行 Strategy 的 next 方法之后才运行并统计数据的,所以某些情况下或许也有手续费的关系它们就不相等。
话说回来,在运行策略的时候,我们所需要的数据都是已经计算得到的,而observers的数据只是需要在图上绘制出来才额外准备的数据列表,所以除非绘图我们没有必要去取observers里的值,而有些时候如果有额外的绘图需求,我们需要自定义observers。
C_自定义observers
首先,内置的observers的确很少,除去buysell是用在主图上的标记,trades是显示交易的日期和盈亏外,其他的基本上都可以说是评价指标,比如收益率,回撤,基准收益等。除了这些评价指标外,还有许多其他的评价指标,比如说夏普率,MAR率,波动率,阿尔法-贝塔等,这些的确可以通过自定义observers添加到图中进行显示。
但是,我们也看到backtrader的图形的处理并不是非常的棒,而且很多指标需要用其他类型的图(比如热力图,直方图等)而不是散点图来显示,最重要的,已经有其他的库做好了评价指标的计算以及各种图形的可视化操作,我们可以直接把关键数据交给它们即可。
参考第四篇量化交易backtrader实践(四)_评价统计篇(3)_更多评价与可视化-CSDN博客。
所以,我们偷懒了,自定义observers就不做实践了。我们就拿着内置的Broker的代码分析一下:
class Broker(Observer): # 继承自Observer
_stclock = True # 开启时钟属性
params = ( # 只有一个参数 fund (基金模式)
('fund', None),
)
alias = ('CashValue',) # 设置别名
lines = ('cash', 'value') # 重点,有2条lines,分别对应现金和资产值
plotinfo = dict(plot=True, subplot=True) # 绘图信息
def start(self):
pass # 基金模式设置内容,不用看
def next(self): # 在next中给lines赋值
if not self._fundmode:
self.lines.value[0] = value = self._owner.broker.getvalue()
self.lines.cash[0] = self._owner.broker.getcash()
其中主要的就是定义lines的内容,以及在next里进行赋值,params一般用于判断模式和辅助计算。
D_更换图标与颜色
我们对于国际的绿涨红跌的颜色系统已经烦它很久了,这里来改掉一部分,后面在plot()里再解决剩下的部分。
打开trades.py的源码,很容易看到绘图中plotlines = XXX里面有marker='o', color = 'blue'这样的设置,即画蓝色圆和红色圆。继续看下去,在MetaDataTrades类里面,有markers的列表和colors的列表,也就是最起码我们可以选取列表中的图标和颜色。
class Trades(Observer):
_stclock = True
lines = ('pnlplus', 'pnlminus')
params = dict(pnlcomm=True)
plotinfo = dict(plot=True, subplot=True,
plotname='Trades - Net Profit/Loss',
plotymargin=0.10,
plothlines=[0.0])
plotlines = dict(
pnlplus=dict(_name='Positive',
ls='', marker='o', color='blue',
markersize=8.0, fillstyle='full'),
pnlminus=dict(_name='Negative',
ls='', marker='o', color='red',
markersize=8.0, fillstyle='full')
)
def __init__(self):
.......
class MetaDataTrades(Observer.__class__):
def donew(cls, *args, **kwargs):
#.......
# Generate plotlines info
markers = ['o', 'v', '^', '<', '>', '1', '2', '3', '4', '8', 's', 'p',
'*', 'h', 'H', '+', 'x', 'D', 'd']
colors = ['b', 'g', 'r', 'c', 'm', 'y', 'k', 'b', 'g', 'r', 'c', 'm',
'y', 'k', 'b', 'g', 'r', 'c', 'm']
basedict = dict(ls='', markersize=8.0, fillstyle='full')
plines = dict()
for lname, marker, color in zip(lnames, markers, colors):
plines[lname] = d = basedict.copy()
d.update(marker=marker, color=color)
plotlines = cls.plotlines._derive(
uuid.uuid4().hex, plines, [], recurse=True)
_obj.plotlines = plotlines()
return _obj, args, kwargs # return the instantiated object and args
于是我们用最省力的方法,直接继承它,然后改写它的plotlines
class my_Trades(bt.observers.Trades):
plotlines = dict( pnlplus=dict(_name= 'Win', ls='',
marker= '^', color= '#ed665d',
markersize= 8.0, fillstyle= 'full'),
pnlminus=dict(_name= 'Lose',ls='',
marker= 'v', color= '#729ece',
markersize= 8.0, fillstyle= 'full'))
--------
cerebro.addobserver(my_Trades)
同理,直接继承buysell的类,然后改写plotlines
class my_BuySell(bt.observers.BuySell): # 这个是继承BuySell的
params = ((
'barplot', True), (
'bardist', 0.02))
plotlines = dict( buy=dict(marker=r'$\Uparrow$',
markersize=10.0, color='#d62728' ),
sell=dict(marker=r'$\Downarrow$',
markersize=10.0, color='#2ca02c')) # 改为箭头
--------
cerebro.addobserver(my_BuySell)
输出图形,可以看到Trades和buy/sell的图标和颜色都换掉了。图标还可以换成打勾打叉的不同型式,这里就不一一列举了。
E_基准收益benchmark
在__init__.py中,我们前面没有实践的还有logreturns和benchmark这两个,其中logreturns从名字上看就是对数收益,其使用方式与TimeReturn是一样的,只有benchmark这个与它们都不一样。
“Benchmark” 在金融和投资领域通常被称为“基准”。它是一个参考点或标准,用于衡量投资的表现。基准通常是一个市场指数,如上证指数,沪深300指数,标普500指数、纳斯达克指数等,或者是某个特定的资产,如一只股票或债券。通过将投资的表现与基准进行比较,投资者可以评估投资策略的效果,是否跑赢了市场平均水平,或者是否需要调整策略。
因此,Benchmark一般是至少有2个data的数据,不然怎么能进行比较,代码上注意把data和bench_data千万要分开分清,包括adddata()时以及addobserver()时。这里补充一点,由于是做策略回测,TimeReturn得到的策略的执行结果,在策略中有时会空仓(空仓与股票本身的日收益就没有关系了),因此是可以在仅有一个data的情况下来比较使用/不使用策略的收益。
cerebro = bt.Cerebro(stdstats=False) # 关闭默认observers
cerebro.addobserver(bt.observers.Broker)
cerebro.addobserver(my_Trades)
cerebro.addobserver(my_BuySell)
data = bt.feeds.PandasData(dataname=df_data0,fromdate=datetime(2022, 1, 15))
cerebro.adddata(data,name=name1) # 将数据添加到Cerebro
bench_data = bt.feeds.PandasData(dataname=df_index_list[0],fromdate=datetime(2022, 1, 15)
cerebro.adddata(bench_data,name='bench') # 额外添加bench_data(上证指数)
cerebro.addstrategy(strategy1) # 添加策略
cerebro.addobserver(bt.observers.Benchmark, data=bench_data ) # 添加bench_data到Observers
#........
cerebro.run() # 运行分析
cerebro.plot(style='candle') # 绘制结果图
由上面代码绘制图形如下,从上往下数第三栏里就是benchmark的图线。
从这张图上,benchmark显示实在是没办法看,即使有办法把它的高度增大一些也看不清,所以一般情况下我们不会在backtrader中来使用benchmark的observers,在后续的评价篇里我们会去实践关于基准指标的数据,完全不需要通过observers或者Analyzers来计算,而是直接用pandasData计算出基准日收益率,然后放到第三方库里去计算例如阿尔法-贝塔系数等,也直接在第三方库里去显示各种不同的图形。
另外,如果只是想可视化对比基准收益的话,股票软件会更加的清晰。例如我们在主图上右键"叠加品种"--"叠加指定品种"--"沪深300"--按百分比显示,另外在副图上用公式显示(1.这支股票的日收益率,2.这支股票的累积日收益率, 3.沪深300的日收益率, 4. 沪深300的累积日收益率),则可以看到这支股票与基准之间的区别。
上面主图的灰色K线就是叠加的沪深300指数,而副图红线是这支股票的日收益,绿线就是沪深300的日收益,我们再把它们分别进行累加,就得到了累加日收益率,这支股票的累加用紫线表示,沪深300的用蓝线表示。从这张图可以看到,这支股票与基准的走势差别不是很大,也从侧面说明了这支股票基本上是跟随沪深300的趋势的。
往往从图上(视觉发现)就会冒出一个又一个的想法甚至闪光点,也等待着我们去验证它们,比如
- 它们跟阿尔法-贝塔的关系?
- 把副图数据直接导出给backtrader使用?或是在backtrader的策略中加入类似的指标?
- 可不可以用股票的累加收益与某个基准的累加收益金叉死叉进行选股?
- 可不可以给它再加一条平均线,做金叉死叉?
- ......
这里先不继承深入了,与股票软件看来还有深度结合应用的方向,这里先简单记录一下它的代码
RET1:(C-REF(C,1))/REF(C,1),COLORRED,LINETHICK3;
ACCRET:SUM(RET1,252), LINETHICK3;
RBASE:("399300$C"-REF("399300$C",1))/REF("399300$C",1);
ACCBASE:SUM(RBASE,252),COLORBLUE;
002_绘图plot
A_绘图参数
plot() 中的参数主要用于系统性的配置图形,具体参数如下所示:
plot(plotter=None, # 包含各种绘图属性的对象或类,如果为None,默认取 PlotScheme 类,如下所示
numfigs=1, # 是否将图形拆分成多幅图展示,如果时间区间比较长,建议分多幅展示
iplot=True, # 在 Jupyter Notebook 上绘图时是否自动 plot inline
**kwargs) # 对应 PlotScheme 中的各个参数
# PlotScheme 中的参数如下所示
class PlotScheme(object):
def __init__(self):
# to have a tight packing on the chart wether only the x axis or also
# the y axis have (see matplotlib)
self.ytight = False
# y-margin (top/bottom) for the subcharts. This will not overrule the
# option plotinfo.plotymargin
self.yadjust = 0.0
# Each new line is in z-order below the previous one. change it False
# to have lines paint above the previous line
self.zdown = True
# Rotation of the date labes on the x axis
self.tickrotation =15
# How many "subparts" takes a major chart (datas) in the overall chart
# This is proportional to the total number of subcharts
self.rowsmajor =
# How many "subparts" takes a minor chart (indicators/observers) in the
# overall chart. This is proportional to the total number of subcharts
# Together with rowsmajor, this defines a proportion ratio betwen data
# charts and indicators/observers charts
self.rowsminor =
# Distance in between subcharts
self.plotdist = 0.0
# Have a grid in the background of all charts
self.grid = True
# Default plotstyle for the OHLC bars which (line -> line on close)
# Other options:'bar' and 'candle'
self.style ='line'
# Default color for the 'line on close' plot
self.loc ='black'
# Default colorfor a bullish bar/candle (0.75 -> intensity of gray)
self.barup ='0.75'
# Default color for a bearish bar/candle
self.bardown = 'red'
# Level of transparency to apply to bars/cancles (NOT USED)
self.bartrans = 1.0
# Wether the candlesticks have to be filled or be transparent
self.barupfill = True
self.bardownfill = True
# Wether the candlesticks have to be filled or be transparent
self.fillalpha = 0.20
# Wether to plot volume or not. Note:if the data in question has no
# volume values, volume plotting will be skipped even if this is True
self.volume = True
# Wether to overlay the volume on the data or use a separate subchart
self.voloverlay = True
# Scaling of the volume to the data when plotting as overlay
self.volscaling = 0.33
# Pushing overlay volume up for better visibiliy. Experimentation
# needed if the volume and data overlap too much
self.volpushup = 0.00
# Default colour for the volume of a bullish day
self.volup = '#aaaaaa' # 0.66 of gray
# Default colour for the volume of a bearish day
self.voldown = '#cc6073' # ( 204, 96, 115)
# Transparency to apply to the volume when overlaying
self.voltrans = 0.50
# Transparency for text labels (NOT USED CURRENTLY)
self.subtxttrans = 0.66
# Default font text size for labels on the chart
self.subtxtsize =
# Transparency for the legend (NOT USED CURRENTLY)
self.legendtrans = 0.25
# Wether indicators have a leged displaey in their charts
self.legendind = True
# Location of the legend for indicators (see matplotlib)
self.legendindloc = 'upper left'
# Plot the last value of a line after the Object name
self.linevalues = True
# Plot a tag at the end of each line with the last value
self.valuetags = True
# Default color for horizontal lines (see plotinfo.plothlines)
self.hlinescolor = '0.66' # shade of gray
# Default style for horizontal lines
self.hlinesstyle = '--'
# Default width for horizontal lines
self.hlineswidth = 1.0
# Default color scheme: Tableau 10
self.lcolors = tableau10
# strftime Format string for the display of ticks on the x axis
self.fmt_x_ticks = None
# strftime Format string for the display of data points values
self.fmt_x_data = None
如果想要系统性修改图形样式,可以重新定义 PlotScheme 类,然后修改里面用到的参数;也可以直接在plot() 中修改。
B_图中的绿涨红跌更改
前面已经提到很多次了,国际上使用的是绿涨红跌,所以K线和volume柱都是这样,现在我们就可以进行更改了,从上面的plot()参数中找到这几个参数的默认值,可以看到K线涨是灰色,跌是红色,成交量涨也是灰色,跌是近红色:
# Default colorfor a bullish bar/candle (0.75 -> intensity of gray)
self.barup ='0.75'
# Default color for a bearish bar/candle
self.bardown = 'red'
# Default colour for the volume of a bullish day
self.volup = '#aaaaaa' # 0.66 of gray
# Default colour for the volume of a bearish day
self.voldown = '#cc6073' # ( 204, 96, 115)
这里直接修改这4个参数的值即可,再次运行回测,图中的K线和成交量颜色就变成我们熟悉的红涨绿跌了。
cerebro.plot(
style='candel', # 设置主图行情数据的样式为蜡烛图
barup = '#ff9896',
bardown= '#98df8a', # 设置蜡烛图上涨和下跌的颜色
volup= '#ff9896',
voldown= '#98df8a', # 设置成交量在行情上涨和下跌情况下的颜色
)
C_局部绘图与是否绘图
先看一下上面的绘制图形,简单把它分成三类
- 观察器图形- observers - Broker , Trades, 以及主图里的buy/sell
- 主图 - K线,股价相关指标,成交量,以及observers控制的buy/sell
- 附图 - 通常是非股价相关指标产生的
由上,大量的图线是与指标相关联的,比如上面的布林线,金叉/死叉标志位等,这些图线的控制通过在策略类的init()里进行。
我们先进到源码中看一下不同的指标的设置,以下列这些指标为例,
# moving averages (so envelope and oscillators can be auto-generated)
from .sma import *
from .ema import *
from .bollinger import *
from .cci import *
from .crossover import *
from .lrsi import *
from .macd import *
sma,ema等都从MovingAverageBase来,需要到backtrader.indicators.MovingAverageBase去查看,其他直接可以跳转过去,将它们的plotinfo列出来:
class MovingAverageBase(with_metaclass(MetaMovAvBase, Indicator)):
params = (('period', 30),)
plotinfo = dict(subplot=False) # 主图绘制
class BollingerBands(Indicator):
lines = ('mid', 'top', 'bot',)
plotinfo = dict(subplot=False) # 主图绘制
class MACD(Indicator):
lines = ('macd', 'signal',)
plotinfo = dict(plothlines=[0.0]) # 没有subplot=False
class LaguerreRSI(PeriodN):
lines = ('lrsi',)
plotinfo = dict( # 没有subplot=False
plotymargin=0.15,
plotyticks=[0.0, 0.2, 0.5, 0.8, 1.0]
)
class _CrossBase(Indicator):
lines = ('cross',)
plotinfo = dict(plotymargin=0.05, plotyhlines=[0.0, 1.0]) # 没有subplot=False
显然,要主图绘制的需要写上subplot=False这句,否则就绘制到副图去了。
我们可以在策略类里改变它们的是否绘制及主图绘制。将默认与更改的分别执行,图形如下:
def __init__(self):
self.order = None
# 默认主图
# sma1 = bt.indicators.SMA(self.data, period=60)
sma1 = bt.indicators.SMA(self.data, period=60, subplot=True) # 画在副图上
# BOLL1 = bt.indicators.BBands(self.data,period =self.p.p1, devfactor=self.p.p2)
BOLL1 = bt.indicators.BBands(self.data,period =self.p.p1, devfactor=self.p.p2,subplot=True) # 画在副图上
bot = BOLL1.l.bot
top = BOLL1.l.top
# 默认副图
# CCI1 = bt.indicators.CCI(self.data)
CCI1 = bt.indicators.CCI(self.data, subplot=False) # 画到主图上
# RSI1 = bt.indicators.RSI(self.data)
RSI1 = bt.indicators.RSI(self.data, subplot= False) # 画到主图上
# self.crsup = bt.indicators.CrossUp(self.data.close,bot)
self.crsup = bt.indicators.CrossUp(self.data.close,bot, plot=False) # 不画图
# self.crsdn = bt.indicators.CrossDown(top,self.data.close)
self.crsdn = bt.indicators.CrossDown(top,self.data.close, plot=False) # 不画图
D_plot属性调整更多方式
刚刚是直接在indicators的语句里全用参数plot=和subplot=进行的控制,现在换一些方式来,第1种是先出indicators,再改绘图属性的方式,这样产生的结果与上面的一模一样。
# 默认主图
sma1 = bt.indicators.SMA(self.data, period=60)
sma1.plotinfo.subplot=True
BOLL1 = bt.indicators.BBands(self.data,period =self.p.p1, devfactor=self.p.p2)
BOLL1.plotinfo.subplot=True
bot = BOLL1.l.bot
top = BOLL1.l.top
# 默认副图
CCI1 = bt.indicators.CCI(self.data)
CCI1.plotinfo.subplot=False
RSI1 = bt.indicators.RSI(self.data)
RSI1.plotinfo.subplot=False
self.crsup = bt.indicators.CrossUp(self.data.close,bot)
self.crsup.plotinfo.plot=False # 不画图
self.crsdn = bt.indicators.CrossDown(top,self.data.close)
self.crsdn.plotinfo.plot=False # 不画图
另外,我们还可以用类继承再重写的方式,把plot或subplot的默认属性改掉,这样使用起来更方便。例如下面代码就是通过重写CrossUp中的plotinfo,从而实现不在图上显示这个指标的功能。
class myCrsup(bt.indicators.CrossUp):
plotinfo = dict(plotymargin=0.05, plotyhlines=[0.0, 1.0],plot=False)
#............
# self.crsup = bt.indicators.CrossUp(self.data.close,bot)
# self.crsup.plotinfo.plot=False # 不画图
self.crsup = myCrsup(self.data.close,bot) # 与上面2句功能相同
E_强制绘图的方法
从上面的实践看到,indicators出来的默认能绘制图线的,不管是主图还是副图,有时候我们不需要它显示一些指标,则用上面C和D的方式都可以让它们不显示。
反过来,有些数据我们想在图上画出来,需要怎么做?
使用bt.LinePlotterIndicator()方法
这个主要应用于一些需要对指标进行简单计算的数据,例如就是在30天平均线的数值上加个2(每次股价超过平均线2块就卖出盈利),这就需要在sma30的线上+2再画一条线
def __init__(self):
self.order = None
# 默认主图
sma1 = bt.indicators.SMA(self.data, period=30) # 默认绘图
sma_up = sma1 + 2 # 计算默认不绘图
print(sma1) # sma1的类型
print(sma_up) # sma_up的类型
----------------
<backtrader.indicators.sma.SMA object at 0x00000146D36E1810>
<backtrader.linebuffer.LinesOperation object at 0x00000146D3925210>
从输出结果看,indicators出来的就是indicators类型,而进行计算后变成了LineOperation类型,这里就需要用LinePlotterIndicator的方法,在主图上强制绘图(subplot=False)
bt.LinePlotterIndicator(sma_up, name='up',subplot=False)
加上这句后,就可以在主图上把计算的线绘制出来了。下图上部分和下部分就分别是没有以及添加了LinePlotterIndicator语句的效果:
将数据制作成indicators
经过计算后得到的类型是lineoperation,这个类型可以使用LinePlotterIndicator强制绘图,再加上subplot=False属性则能画在主图上,但是也看到了图上的标签是LinePlotterIndicator xx.xx,如果我们需要更有个性化的显示,以及让data里面的lines数据也能绘制图线(LineBuffer是不能用LinePlotterIndicator绘制的),就需要制作自定义indicator类。
自定义新指标类格式如下
class MyInd(bt.Indicator):
lines = (xxx,xxx, ) # 最后一个 “,” 别省略
params = ((xxx, n),) # 最后一个 “,” 别省略
def __init__(self):
'''可选'''
pass
def next(self):
'''可选'''
pass
def once(self):
'''可选'''
pass
plotinfo = dict(...)
plotlines = dict(...)
我们根据这个先简单写上面这个根据SMA1加上一个数的自定义类
class myAdd(bt.Indicator):
lines=('out',)
params=(('p1',2.0),) # 加上的数
plotinfo = dict(subplot=False) # 画在主图上
plotlines = dict(out=dict(ls='--',color='g')) # 画虚线,绿色
def __init__(self):
self.l.out = self.data + self.p.p1 # 直接相加
----------------
class mystrategy11(SmaCross_s4):
#......
def __init__(self):
self.order = None
# 默认主图
sma1 = bt.indicators.SMA(self.data, period=30)
sma_up = myAdd(sma1,p1=2.0) # 使用自定义指标类
# bt.LinePlotterIndicator(sma_up, name='up',subplot=False) # 这句就不需要了
运行后输出图形如下,可以看到把sma1+2.0的绿色虚线显示出来了,并且标签的名字变成了myAdd(),这样就更有辨识度了。
使用这种方法,还可以把我们前面说的从股票软件里导出的某些指标的值(甚至是加密指标)制作成LineBuffer然后通过自定义indicator类转换成指标,也就能方便的在backtrader中使用股票软件的指标了,连公式移植都不用,大大节省工作量,提高了工作效率。
小结
本节对交易的设置和绘图的一些关键点进行了实践,通过试验对backtrader以及自己的交易系统都有了一定的提升。
backtrader实践第二篇之基础加强篇到这里就结束了,在这一篇里我们以第一篇得到的数据进行进一步的实践,拓展了对数据Datas,Data以及lines的认识,加深了对策略类的应用包括一次回测能否添加2个策略,能否将一个策略用到所有的数据上等,对交易系统的评测准备和交易设置也有了更深层次的了解,并且也可以比较自由的进行绘图的操作了。
在这一篇里,我们在实践过程中逐渐更改和添加了一些类和函数代码,把这些代码放到一个.py文件中,我们就拥有了一个算是比较全面的backtrader回测框架了,这些代码后面都不要改,但每次都要用到,后续要开始对指标和策略进行研究、分析,有了这个框架就能把精力放在关键的东西上,而不要耗费在这琐碎的、重复的没有效率的东西上了。