Backtrader库 | 均线买入卖出策略实现
Backtrader是用于量化投资python框架,作者是德国人Daniel Rodriguez。相比于zipline等量化回测包,backtrader是一个易懂、易上手的量化投资框架,今天我们试着用Backtrader进行简单的均线买入卖出量化策略回溯,即5日均线上穿20日均线,则表示股票处于强势,买入。反之,处于弱势,卖出。
来源: Python实用宝典
原文标题: 量化投资原来这么简单(1)
原文链接 https://mp.weixin.qq.com/s/_fCD_7YpuGUQtawxWk0EWQ
安装
pip3 install backtrader
快速入门
买入:MA5上穿MA20, 即五日价格移动平均线(MA5)和二十日价格移动平均线(MA20), 最近处于涨势
卖出:MA20下穿MA5, 即五日价格移动平均线(MA5)和二十日价格移动平均线(MA20), 最近处于跌
import datetime
import backtrader as bt
if __name__ == "__main__":
#初始化
cerebro = bt.Cerebro()
#设定初始资金
cerebro.broker.setcash(100000.0)
#策略执行前的资金
print('启动资金: {}'.format(cerebro.broker.getvalue()))
#策略执行
cerebro.run()
#策略执行前的资金
print('启动资金: {}'.format(cerebro.broker.getvalue()))
启动资金: 100000.0
启动资金: 100000.0
每次股票交易,证券经纪人会收取一定的佣金,如万三(每一万元交易收三元)即0.003
cerebro.broker.setcommission(0.003)
交易会有最小的购买/卖出份额,一般一手100股
cerebro.addsizer(bt.sizers.FixedSize, stake=100)
加载数据
前复权:保持当前价格不变,将历史价格进行增减,从而使股价连续。前复权用来看盘非常方便,能一眼看出股价的历史走势,叠加各种技术指标也比较顺畅,是各种行情软件默认的复权方式。这种方法虽然很常见,但也有两个缺陷需要注意。
为了保证当前价格不变,每次股票除权除息,均需要重新调整历史价格,因此其历史价格是时变的。这会导致在不同时点看到的历史前复权价可能出现差异。 对于有持续分红的公司来说,前复权价可能出现负值。
后复权 :保证历史价格不变,在每次股票权益事件发生后,调整当前的股票价格。后复权价格和真实股票价格可能差别较大,不适合用来看盘。其优点在于,可以被看作投资者的长期财富增长曲线,反映投资者的真实收益率情况。在量化投资研究中普遍采用后复权数据。使用 https://github.com/mpquant/Ashare 下载的股票数据
Backtrader将数据集称作数据流Data Feeds, 默认数据集格式是OHLCV(open/high/low/close/volume),通过以下代码即可加载:
# 创建数据
data = bt.feeds.YahooFinanceCSVData(
dataname='sz000725.csv',
datetime=0,
open=1,
high=2,
low=3,
close=4,
volume=5,
dtformat=('%Y-%m-%d'),
fromdate = datetime.datetime(2014, 7, 11),
todate = datetime.datetime(2021, 12, 1)
)
添加指标
backtrader中内置了许多计算值表,比如移动平滑线、MACD、RSI等等, 这一篇文章仅需要移动平均线MA, 设置方法如下
self.sma5 = bt.indicators.MovingAverageSimple(self.datas[0], period=5)
self.sma20 = bt.indicators.MovingAverageSimple(self.datas[0], period=20)
datas[0]是第一个数据集, period是指多少天的移动平均线,比如5,则返回MA5的相关数据。
构建策略
使用backtrader构建策略是一件很简单的事情, 继承backtrader的策略类,并重写部分方法,就能实现策略。比如
重写属于我们自己的log函数 均线金叉死叉策略
class TestStrategy(bt.Strategy):
"""
继承并构建自己的策略
"""
def log(self, txt, dt=None, doprint=False):
"日志函数,用于统一输出日志格式"
if doprint:
dt = dt or self.datas[0].datetime.date(0)
print('{}, {}'.format(dt.isoformat(), txt))
def __init__(self):
# 初始化相关数据
self.dataclose = self.datas[0].close
self.order = None
self.buyprice = None
self.buycomm = None
# 移动平均线初始化
self.sma5 = bt.indicators.MovingAverageSimple(self.datas[0], period=5)
self.sma20 = bt.indicators.MovingAverageSimple(self.datas[0], period=20)
def notify_order(self, order):
"""
订单状态处理
Arguments:
order {object} -- 订单状态
"""
if order.status in [order.Submitted, order.Accepted]:
# 如订单已被处理,则不用做任何事情
return
# 检查订单是否完成
if order.status in [order.Completed]:
if order.isbuy():
self.buyprice = order.executed.price
self.buycomm = order.executed.comm
self.bar_executed = len(self)
# 订单因为缺少资金之类的原因被拒绝执行
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log('Order Canceled/Margin/Rejected')
# 订单状态处理完成,设为空
self.order = None
def notify_trade(self, trade):
"""
交易成果
Arguments:
trade {object} -- 交易状态
"""
if not trade.isclosed:
return
# 显示交易的毛利率和净利润
self.log('OPERATION PROFIT, GROSS {}, NET {}'.format(trade.pnl, trade.pnlcomm), doprint=True)
def next(self):
''' 下一次执行 '''
# 记录收盘价
self.log('Close, {}'.format(self.dataclose[0]))
# 是否正在下单,如果是的话不能提交第二次订单
if self.order:
return
# 是否已经买入
if not self.position:
# 还没买,如果 MA5 > MA10 说明涨势,买入
if self.sma5[0] > self.sma20[0]:
self.order = self.buy()
else:
# 已经买了,如果 MA5 < MA10 ,说明跌势,卖出
if self.sma5[0] < self.sma20[0]:
self.order = self.sell()
#def stop(self):
#self.log(u'(金叉死叉有用吗) Ending Value {}'.format(self.broker.getvalue()), doprint=True)
策略回测
为了验证我们开头提到的策略,咱使用了 京东方sz000725 在2014年7月11日至今2021年12月3日的股票数据,将数据命名为sz000725.csv, 我们先用pandas审查下csv
import pandas as pd
df = pd.read_csv('data/sz000725.csv')
df.head()
| | Unnamed: 0 | date | open | high | low | close | volume |
|---:|-------------:|------------:|-------:|-------:|------:|--------:|------------:|
| 0 | 0 | 2.01407e+07 | 2.17 | 2.2 | 2.16 | 2.19 | 7.49341e+07 |
| 1 | 1 | 2.01407e+07 | 2.18 | 2.2 | 2.17 | 2.2 | 8.10931e+07 |
| 2 | 2 | 2.01407e+07 | 2.19 | 2.21 | 2.18 | 2.2 | 8.19694e+07 |
| 3 | 3 | 2.01407e+07 | 2.2 | 2.21 | 2.19 | 2.21 | 7.96481e+07 |
| 4 | 4 | 2.01407e+07 | 2.2 | 2.21 | 2.19 | 2.21 | 8.75106e+07 |
在backtrader中,使用GenericCSVData函数来加载csv,需要注明日期始末、open/high/low/close/volume等字段在csv中的列数(第几列,从0开始,0表示第一列)
import backtrader as bt
import datetime
if __name__ == "__main__":
# 初始化模型
cerebro = bt.Cerebro()
init_cash = 100000.0
fromdate = datetime.datetime(2014, 7, 11)
todate = datetime.datetime(2021, 12, 3)
#构建策略
strategy = cerebro.addstrategy(TestStrategy)
#每次买100股
cerebro.addsizer(bt.sizers.FixedSize, stake=100)
#加载数据到模型
data = bt.feeds.GenericCSVData(
dataname='data/sz000725.csv',
fromdate=fromdate,
todate=todate,
dtformat='%Y%m%d',
datetime=1,
open=2,
high=3,
low=4,
close=5,
volume=6
)
cerebro.adddata(data)
# 设定初始资金和佣金
cerebro.broker.setcash(init_cash)
cerebro.broker.setcommission(0.003)
print('会不会玩了个寂寞?')
#策略执行前的资金
print('启动资金: {}'.format(cerebro.broker.getvalue()))
#策略执行
cerebro.run()
#策略结束时的资金
print('策略结束时资金: {}'.format(cerebro.broker.getvalue()))
duration_year = (todate-fromdate).days/360
end_value = cerebro.broker.getvalue()
roi = pow(end_value/init_cash, 1/duration_year)-1
print('策略年华收益率: {}%'.format(roi*100))
会不会玩了个寂寞?
启动资金: 100000.0
2014-08-27, OPERATION PROFIT, GROSS -3.000000000000025, NET -4.365000000000025
2014-10-28, OPERATION PROFIT, GROSS 10.999999999999988, NET 9.568999999999988
2014-11-24, OPERATION PROFIT, GROSS -4.0000000000000036, NET -5.584000000000003
2015-01-15, OPERATION PROFIT, GROSS 52.0, NET 50.242
2015-05-08, OPERATION PROFIT, GROSS 113.00000000000003, NET 110.82500000000003
2015-07-02, OPERATION PROFIT, GROSS 25.0, NET 22.075
2015-08-25, OPERATION PROFIT, GROSS -96.0, NET -98.076
2015-11-03, OPERATION PROFIT, GROSS -8.999999999999986, NET -10.760999999999985
2015-11-30, OPERATION PROFIT, GROSS -16.000000000000014, NET -17.812000000000015
2015-12-31, OPERATION PROFIT, GROSS -8.999999999999986, NET -10.820999999999986
2016-03-14, OPERATION PROFIT, GROSS -10.999999999999988, NET -12.514999999999988
2016-04-14, OPERATION PROFIT, GROSS 0.0, NET -1.548
2016-06-16, OPERATION PROFIT, GROSS -6.000000000000005, NET -7.404000000000005
2016-07-28, OPERATION PROFIT, GROSS 0.0, NET -1.404
2016-09-08, OPERATION PROFIT, GROSS 8.000000000000007, NET 6.566000000000007
2016-12-19, OPERATION PROFIT, GROSS 31.999999999999986, NET 30.421999999999986
2017-02-10, OPERATION PROFIT, GROSS 14.000000000000012, NET 12.110000000000012
2017-02-20, OPERATION PROFIT, GROSS -4.999999999999982, NET -6.940999999999982
2017-03-06, OPERATION PROFIT, GROSS -10.999999999999988, NET -12.976999999999988
2017-05-12, OPERATION PROFIT, GROSS 44.99999999999997, NET 42.86699999999997
2017-06-01, OPERATION PROFIT, GROSS -22.00000000000002, NET -24.38200000000002
2017-07-13, OPERATION PROFIT, GROSS -14.000000000000012, NET -16.406000000000013
2017-09-18, OPERATION PROFIT, GROSS -2.9999999999999805, NET -5.31899999999998
2017-11-27, OPERATION PROFIT, GROSS 153.00000000000003, NET 150.12900000000002
2018-01-08, OPERATION PROFIT, GROSS -8.000000000000007, NET -11.360000000000007
2018-02-01, OPERATION PROFIT, GROSS 2.000000000000046, NET -1.6419999999999533
2018-03-23, OPERATION PROFIT, GROSS -25.0, NET -28.303
2018-08-07, OPERATION PROFIT, GROSS -12.00000000000001, NET -14.124000000000011
2018-09-04, OPERATION PROFIT, GROSS -27.0, NET -29.115000000000002
2018-11-26, OPERATION PROFIT, GROSS -14.000000000000012, NET -15.668000000000012
2019-01-29, OPERATION PROFIT, GROSS -4.999999999999982, NET -6.598999999999982
2019-03-15, OPERATION PROFIT, GROSS 70.00000000000001, NET 67.90000000000002
2019-04-12, OPERATION PROFIT, GROSS -15.99999999999997, NET -18.35799999999997
2019-04-29, OPERATION PROFIT, GROSS -14.999999999999991, NET -17.34299999999999
2019-06-06, OPERATION PROFIT, GROSS -5.000000000000027, NET -7.043000000000027
2019-06-19, OPERATION PROFIT, GROSS 4.999999999999982, NET 2.9509999999999823
2019-08-07, OPERATION PROFIT, GROSS 36.999999999999964, NET 34.84299999999996
2019-08-30, OPERATION PROFIT, GROSS -10.999999999999988, NET -13.282999999999987
2019-09-27, OPERATION PROFIT, GROSS -41.99999999999995, NET -44.35799999999995
2020-02-04, OPERATION PROFIT, GROSS 30.00000000000003, NET 27.67200000000003
2020-03-06, OPERATION PROFIT, GROSS 9.999999999999964, NET 7.017999999999965
2020-05-28, OPERATION PROFIT, GROSS -25.0, NET -27.301000000000002
2020-07-21, OPERATION PROFIT, GROSS 54.999999999999986, NET 52.25499999999999
2020-09-14, OPERATION PROFIT, GROSS 52.999999999999936, NET 50.002999999999936
2020-10-19, OPERATION PROFIT, GROSS -5.999999999999961, NET -8.98199999999996
2020-12-10, OPERATION PROFIT, GROSS 2.000000000000046, NET -1.083999999999954
2021-02-02, OPERATION PROFIT, GROSS 66.00000000000001, NET 62.454000000000015
2021-03-03, OPERATION PROFIT, GROSS -13.000000000000078, NET -16.67500000000008
2021-03-11, OPERATION PROFIT, GROSS -33.999999999999986, NET -37.64199999999999
2021-05-12, OPERATION PROFIT, GROSS 31.00000000000005, NET 27.12700000000005
2021-07-06, OPERATION PROFIT, GROSS -33.00000000000001, NET -36.717000000000006
2021-07-26, OPERATION PROFIT, GROSS -34.999999999999964, NET -38.70499999999996
2021-09-17, OPERATION PROFIT, GROSS -21.999999999999975, NET -25.467999999999975
2021-11-22, OPERATION PROFIT, GROSS -7.000000000000028, NET -9.979000000000028
策略结束时资金: 100120.964
策略年华收益率: 0.016108152552885002%
策略可视化
from backtrader_plotting import Bokeh
from backtrader_plotting.schemes import Tradimo
b = Bokeh(style='bar', plot_mode='single', scheme=Tradimo())
cerebro.plot(b)
代码下载
https://github.com/hiDaDeng/DaDengAndHisPython/tree/master/backtrader01
近期文章
如何在DataFrame中使用If-Else条件语句创建新列