Backtrader 文档学习- 架构功能分析理解
1. 概述
backtrader是一个用于开发和实验交易策略的Python框架。它提供了一套完整的工具和功能,使得用户可以方便地举行策略回测、实盘交易以及数据分析。
backtrader的入口为Cerebro类,该类将全部输入(Data Feeds)、策略 (Strategy)、观察者(Observers)、策略评估(Analyzers) 、经纪人(Broker)、订单(Order)、交易(Trade)和日记记录(Writers)整合起来,实现回测以及交易,并返回结果和图表。
backtrader优缺点,先说缺点:
(1)缺点:
- 在于使用元类编程技能,使用了大量的类,调试的时间会比较难。
- 对于python的要求高,需要掌握pandas,matplotlib ,有基础上手更容易。
- 对环境存在兼容性, 使用pandas ,numpy ,matplotlib 的版本不能太高。
- 对回测结果分析,需定制化做大量工作,比如需要将各类策略的测试结果,除了能打印日记外,需要保存到数据库中,便于后期对比分析,比如周期参数,5天、10天、20天同一个策略不同的周期,实验的绩效记录处置惩罚。
(2)优点:
- 功能非常美满,支持多品种、多策略、多周期的回测和交易,编写策略简单,上手容易。
- 交易的品种非常丰富(股票、期货、期权、外汇等)。
- 详细的文档,提供的用例比较多,可以学习和根据需求改进 。
- 在量化框架上,收费的居多,优矿,米筐,聚宽,不能当地化部署。免费开源的少。
- 支持内部方法的重写,可以方便定制化策略。
- 内置多个指标参数,很丰富,包括Talib 。
(3)使用目的:
- 以研究策略为主,验证策略效果,选择标的股票。
- 不需要太多的参数因子,基于通用指标,可以灵活组合。
- 不做实盘高频交易。
- 适合个人学习使用,探索根本量化方向,避免盲目交易,有数据支撑。
2.架构理解
backtrader的整体架构可以分为以下紧张组件:
(1)数据源(Data Feeds):
backtrader支持多种数据源,包括CSV文件、Pandas DataFrame、及时数据源等。用户可以根据自己的需求选择适合的数据源,并通过数据加载器将数据加载到backtrader中举行处置惩罚。
backtrader回测的数据类型是由一系列的点构成的Lines,通常包括以下种别的数据:Open(开盘价)、High(最高价)、Low(最低价)、 Close(收盘价)、Volume(成交量)、OpenInterest(未平仓权益)。Data Feeds(数据加载)、Indicators(技能指标)和Strategies(策略)都会天生 Lines。代价数据中的全部的开盘价按时间构成一条 Line,因此一组含有以上6个种别的代价数据,共有6条 Lines。如果算上DateTime(时间,可以看作是一组数据的主键),一共有7条 Lines。Lines根据需要是可以扩展的。
焦点概念:
Line的概念和Python中的list索引不同,Python List中索引-1,表示最后一个数据。
一条Line数据的下标为0表示访问当前时刻数据,-1表示访问下一个数据。在回测过程中,无需知道已经处置惩罚了多少条/分钟/天/月,”0”不停指向当前值,下标 -1 来访问下一个值,下标 -2 访问下下一个值。
在backtrader中,-1指的是当前处置惩罚数据(索引为0)的上一个数据。
Lines是随时变革的,run的时间,next不断改变Lines的长度,在数据载入,策略,指示器应用中,需要丈量Lines的长度。
两个长度函数:len和buflen之间的区别:
- len已处置惩罚了Lines
- buflen为数据加载Lines的总数
(2)策略(Strategies):
backtrader提供了一个基类Strategy,用户可以继承该类并实现自己的交易策略。策略类中界说了一系列的回调函数,用户可以在回测函数中编写详细的交易逻辑。
该模块是回测体系最焦点的部门,需要设计交易决策,得出买入/卖出信号。策略类代码包罗紧张的参数和用于实验策略的功能,要界说的参数或函数名如下:
(1)params:全局参数,可选,用于更改交易策略中变量/参数的值,可用于参数调优。
(2)log:日记,可选,用于记录策略的实验日记,可以打印出该函数提供的日期时间和txt变量。
(3)init:用于初始化交易策略,类中使用的全局变量界说,在此中声明的任何指标都会在next()方法调用之进步行计算。部门python操纵符不支持,需要使用bt内置函数来处置惩罚,比方bt.And, bt.Or, bt.All, bt.Any等。
(4)notify_order,可选,用于跟踪交易订单(order)的状态。order具有提交,担当,买入/卖出实验和代价,已取消/拒绝等状态。
(5)notify_trade,可选,用于跟踪交易的状态,任何已平仓的交易都将报告毛利和净利润。
(6)next,必选,用于订定交易策略的函数,策略模块最焦点的部门。
当满足全部data/indicators的最小周期后,实验将对全部剩余数据点调用。
如period是10 ,最小周期达到后,开始实验next方法。
(7)nextstart(),可选,方法实验一次,在最小周期的data/indicators满足时,默认实验next方法。
如period是10 ,在到达9实验最小周期达到后,实验一次nextstart,之后实验next方法。
(8)prenext(),可选,方法将在全部数据/指标的最小周期满足策略开始实验之前调用实验。
如period是10 ,在1到9实验最小周期之间,实验prenext。
(9)start(),可选,在回测即将开始之前调用,实验一次。
(10)stop(),可选,在回测即将结束之前调用,实验一次。
(11)notify_cashvalue(),可选,吸收当前使用的资金余额
(12)notify_fund(),可选,策略的资金和代价发生变革时被调用。用于跟踪和记录策略的现金、代价、基金代价和持仓股票的数目。
(3)指标(Indicators):
backtrader内置了许多常用的技能指标,如移动平均线、MACD、RSI等。用户可以通过指标类来计算这些指标,并在策略中使用。
backtrader在指标中也集成了TA-Lib库
(4)订单(Order):
backtrader提供了一套美满的订单管理体系,用户可以通过创建订单对象来实验买入和卖出操纵。订单管理体系还支持订单状态的跟踪和管理。
将策略中逻辑做出的决策转换为适合经纪人实验操纵的消息,通常在交易策略中调用。
Order将strategy的逻辑做出的决策转换为适合broker实验操纵的消息。通过以下方式完成:
创建 ,通过strategy 的方法:buy sell 和close 都可以返回Order的实例
取消,通过strategy的方法:cancel 也可以产生Order实例
通知,notify_order 的方法,也返回Order 实例
订单还作为反馈给用户的通信方法,通知代理中的运行环境
(5)经纪人(Broker):
通过设置回测的初始自己、佣金费率、税收费率、滑点率等交易条件,模仿不同的订单类型,限价订单,控制订单的有效期,并根据现金检查订单,计算每次交易的现金和权益,保存交易数据。
Trade :backtrader会自动记录每笔交易的详细信息,包括买入代价、卖出代价、手续费等。用户可以通过这些交易记录举行后续的分析和统计。
Position :仓位管理 。
(6)策略评估(Analyzers):
用于分析交易策略的利润和风险,分析交易体系的绩效。
内置各类评价指标,可以参加到评测对象中,对结果举行绩效分析,分析指标:年度回报率,卡尔玛比率,最大回撤,资金杠杆,仓位资金,组合投资值等,是对策略的效果评估参数。
(7)观察者(Observers):
用于记录交易过程,包括现金、权益、费用以及交易动作、买卖订单等数据。
(8)回测引擎(Backtesting Engine):
cerebro.run()
backtrader提供了一个强盛的回测引擎,用户可以通过指定回测时间段和初始资金等参数来举行策略回测。回测引擎会模仿真实的交易环境,并根据用户界说的策略举行交易。
(9)画图(Plot):
分析和可视化(Analysis and Visualization):
backtrader提供了多种分析工具和可视化功能,用户可以对回测结果举行详细的分析和可视化展示,可以增长图示内容,包括收益曲线、风险指标、交易位置等。
通过图形的方式显示交易丈量回测的结果,画图显示的结果包括三部门类型:现金及权益、交易损益、买卖动作。
画图设置通过plotinfo来设置,其参数紧张有:plot(是否画图,默认为True),subplot(是否单独窗口画图,默认为True,MA类指标该参数为False),plotname(指标图名,默认为指标类名),plotabove(画图位置在数据上方,默认为False),plotlinelabels, plotymargin, plotyticks,plothlines, plotyhlines, plotforce。
3.代码示例
做一个最简单的SMA测试示例。
取100个股票,举行测试,测试的时间范围从2016年到2020年,一共5年的数据。
在策略中的init()初始化界说了Indicator ,在next()中买卖操纵。
增长了Observer和Analyzer ,把大多数的监控绩效的指标都参加了。
- #!/usr/bin/env python
- import datetime
- import pandas as pd
- import numpy as np
- import pymysql
- import backtrader as bt
- import backtrader.feeds as btfeeds
- import backtrader.indicators as btind
- from sqlalchemy import create_engine
- engine_ts = create_engine(
- 'mysql+pymysql://user:pwd@ip:3306/dbname?charset=utf8&use_unicode=1')
- def get_stock_code():
- sql = "select t.ts_code from ts_stock_basic t where t.list_status='L' limit 100;"
- stock_data = pd.read_sql(sql, con=engine_ts)
- return stock_data
- def get_code(stock_code):
- sql = "select t.trade_date as date,t.`open`,t.high,t.low,t.`close`,t.vol,t.amount from ts_stock_daily t where \
- t.ts_code='" + stock_code + "' and t.trade_date > '2016-01-01' and t.trade_date < '2020-12-31' order by date ;"
- # print(sql)
- # stock_data = pd.read_sql(sql, con=engine_ts,index_col="date")
- # 因为BackTrader日期类型必须是datetime ,从数据库中读取的日期类型是date 。
- # 读数据,先不设置索引
- stock_data = pd.read_sql(sql, con=engine_ts) # ,index_col="date"
- # 增加一列,select 字段名是date,赋值到trade_date,同时转datetime类型
- stock_data['trade_date'] = pd.to_datetime(stock_data['date'], format='%Y%m%d %H:%M:%S')
- # 删除原来的date列
- stock_data.drop(columns=['date'])
- # 新datetime列作为索引列
- stock_data.set_index(['trade_date'], inplace=True)
- # 索引列改名
- stock_data.index.name = 'date'
- # 按backtrader 格式要求,第7列openinterest ,也可以不用
- # stock_data['openinterest'] = 0
- data = stock_data.sort_index(ascending=True)
- #engine_ts.dispose()
- return data
- class OrderObserver(bt.observer.Observer):
- lines = ('created', 'expired',)
- plotinfo = dict(plot=True, subplot=True, plotlinelabels=True)
- plotlines = dict(
- created=dict(marker='*', markersize=8.0, color='lime', fillstyle='full'),
- expired=dict(marker='s', markersize=8.0, color='red', fillstyle='full')
- )
- def next(self):
- for order in self._owner._orderspending:
- if order.data is not self.data:
- continue
- if not order.isbuy():
- continue
- # Only interested in "buy" orders, because the sell orders
- # in the strategy are Market orders and will be immediately
- # executed
- if order.status in [bt.Order.Accepted, bt.Order.Submitted]:
- self.lines.created[0] = order.created.price
- elif order.status in [bt.Order.Expired]:
- self.lines.expired[0] = order.created.price
- # CrossOver Strategy
- class St_CrossOver(bt.Strategy):
- params = (
- ('smaperiod', 15),
- ('limitperc', 1.0),
- ('valid', 7),
- ('print', False),
- )
- def log(self, txt, dt=None):
- ''' Logging function fot this strategy'''
- dt = dt or self.data.datetime[0]
- if isinstance(dt, float):
- dt = bt.num2date(dt).date() # no Hour mintue second
- if self.params.print :
- print('%s, %s' % (dt.isoformat(), txt))
- def notify_order(self, order):
- if order.status in [order.Submitted, order.Accepted]:
- # Buy/Sell order submitted/accepted to/by broker - Nothing to do
- self.log('ORDER ACCEPTED/SUBMITTED', dt=order.created.dt)
- self.order = order
- return
- if order.status in [order.Expired]:
- self.log('BUY EXPIRED')
- elif order.status in [order.Completed]:
- if order.isbuy():
- self.log(
- 'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
- (order.executed.price,
- order.executed.value,
- order.executed.comm))
- else: # Sell
- self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
- (order.executed.price,
- order.executed.value,
- order.executed.comm))
- # Sentinel to None: new orders allowed
- self.order = None
- def __init__(self):
- # SimpleMovingAverage on main data
- # Equivalent to -> sma = btind.SMA(self.data, period=self.p.smaperiod)
- sma = btind.SMA(period=self.p.smaperiod)
- # CrossOver (1: up, -1: down) close / sma
- self.buysell = btind.CrossOver(self.data.close, sma, plot=True)
- # Sentinel to None: new ordersa allowed
- self.order = None
- def next(self):
- if self.order:
- # pending order ... do nothing
- return
- # Check if we are in the market
- if self.position:
- if self.buysell < 0:
- self.log('SELL CREATE, %.2f' % self.data.close[0])
- self.sell()
- elif self.buysell > 0:
- plimit = self.data.close[0] * (1.0 - self.p.limitperc / 100.0)
- valid = self.data.datetime.date(0) + \
- datetime.timedelta(days=self.p.valid)
- self.log('BUY CREATE, %.2f' % plimit)
- self.buy(exectype=bt.Order.Limit, price=plimit, valid=valid)
- def runstrat(stock_code):
- cerebro = bt.Cerebro()
- # 初始资金 100,000,000
- cerebro.broker.setcash(100000.0)
- # 佣金,双边各 0.0003
- cerebro.broker.setcommission(commission=0.0003)
- # 滑点:双边各 0.0001
- cerebro.broker.set_slippage_perc(perc=0.0001)
- # 获取数据
- stock_df = get_code(stock_code)
- start_date = datetime.datetime(2016, 1, 1) # 回测开始时间
- end_date = datetime.datetime(2020, 12, 31) # 回测结束时间
- data = bt.feeds.PandasData(dataname=stock_df, fromdate=start_date, todate=end_date) # 加载数据
- cerebro.adddata(data)
- cerebro.addobserver(OrderObserver)
- cerebro.addstrategy(St_CrossOver)
- tframes = dict(
- days=bt.TimeFrame.Days,
- weeks=bt.TimeFrame.Weeks,
- months=bt.TimeFrame.Months,
- years=bt.TimeFrame.Years)
- cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='mysharpe')
- cerebro.addanalyzer(bt.analyzers.DrawDown,_name = 'mydrown')
- cerebro.addanalyzer(bt.analyzers.AnnualReturn,_name = 'myannualreturn')
- cerebro.addanalyzer(bt.analyzers.SQN,_name = 'mysqn')
- cerebro.addanalyzer(bt.analyzers.TradeAnalyzer,_name = 'mytradeanalyzer')
- cerebro.addanalyzer(bt.analyzers.PositionsValue,_name = 'mypositionvalue')
- cerebro.addanalyzer(bt.analyzers.Returns,_name = 'myreturns')
- cerebro.addanalyzer(bt.analyzers.LogReturnsRolling,timeframe=tframes['years'],_name = 'mylogreturnsrolling')
- cerebro.addanalyzer(bt.analyzers.Transactions, _name='mytransactions')
- thestrats = cerebro.run()
- thestrat = thestrats[0]
- print('Sharpe Ratio:', thestrat.analyzers.mysharpe.get_analysis())
- print('DrawDown:', thestrat.analyzers.mydrown.get_analysis())
- print('AnnualReturn:', thestrat.analyzers.myannualreturn.get_analysis())
- print('SQN:', thestrat.analyzers.mysqn.get_analysis())
- print('TradeAnalyzer:', thestrat.analyzers.mytradeanalyzer.get_analysis())
- print('PositionsValue:', thestrat.analyzers.mypositionvalue.get_analysis())
- print('Returns:', thestrat.analyzers.myreturns.get_analysis())
- print('LogReturnsRolling:', thestrat.analyzers.mylogreturnsrolling.get_analysis())
- print('Transactions:', thestrat.analyzers.mytransactions.get_analysis())
- #cerebro.plot()
- if __name__ == '__main__':
-
- t1 = datetime.datetime.now()
- stock_data = get_stock_code()
-
- for i in range(len(stock_data)):
- print(stock_data.loc[i, 'ts_code'])
- runstrat(stock_data.loc[i, 'ts_code'])
-
- t2 = datetime.datetime.now()
- print('Spend time:', t2 - t1)
- engine_ts.dispose()
- print('ok!')
复制代码 详细结果内容多,就不展示了。
结果是用了32.59秒完成计算,100个股票,5年的数据,速度还可以。
Spend time: 0:00:32.591037
ok!
4.小结
backtrader的整体架构是基于事件驱动的,用户通过编写策略类来界说交易逻辑,并通过回测引擎举行策略回测和分析。
通过上面的比较全面的功能测试,根本上覆盖前期学习的backtrader紧张功能,完成了一个自界说的策略回测验证。
在不考虑数据收罗的环境下,假设数据全部在数据库中,需要后期完成的工作很多:
- 组合策略的界说,肯定不能是简单的SMA,可以结合MACD /RSI/KDJ/ADX 等等指标。
- 时间周期的界说,触发不同指标的效果。
- 策略评估结果保存入库后,举行横向比较。
- 参数调优,最简单的如前篇文章,测试胜率和盈亏比组合,组合参数的递增或递减,绩效结果对比。
- 黑天鹅事件,比如千股跌停的环境,异常极端环境的风险控制,2008年6月次贷危机天下经济危机,2015年6月到8月去杠杆,2016年1月熔断机制上线,尚有本年2月5日 。
- 加仓机制实现
- 止损机制实现
- 止盈机制实现
- 仓位管理实现
- 对于不同走势界说不同的策略,如短期打板策略,需要看成交量、换手率,严格的止盈止损。
- 策略模块化,可以是内置类
- 日记,绩效结果模块化,到数据库
- 绩效参考基准,比如沪深500
对于自界说的回测功能或控制内容,接待各人可以留言探究。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |