用Backtrader回测策略基本有五个步骤:
- 创建一个Cerebro引擎
- 加入一个Strategy
- 加载数据
- 执行:cerebor.run()
- 对执行结果可视化
从0开始实现策略回测
对于量化投资相关的学习,最好的方法就是自己跟着教程一步一步开始做,在不断迭代的过程中,从简单到复杂实现策略的回测。在实践中发现问题,并不断改进问题。
创建第一个程序
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import backtrader as bt
if __name__ == '__main__':
cerebro = bt.Cerebro()
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
cerebro.run()
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
执行下,看看啥结果:
Starting Portfolio Value: 10000.00
Final Portfolio Value: 10000.0
这个程序的主要功能:
-
将Backtrader引入到咱们程序中,命名为bt。
-
创建了一个机器人大脑(Cerebro),同时隐含创建了一个borker(券商)。
-
让机器人大脑开始运行。
-
显示了机器人在券商那里存有多少钱。
给空白的大脑加载数据
import backtrader as bt
import pandas as pd
from datetime import datetime
if __name__ == '__main__':
cerebro = bt.Cerebro()
#获取数据
stock_hfq_df = pd.read_excel("./data/sh600000.xlsx",index_col='date',parse_dates=True)
start_date = datetime(2010, 9, 30) # 回测开始时间
end_date = datetime(2021, 9, 30) # 回测结束时间
data = bt.feeds.PandasData(dataname=stock_hfq_df, fromdate=start_date, todate=end_date) # 加载数据
cerebro.adddata(data) # 将数据传入回测系统
cerebro.broker.setcash(100000.0)
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
cerebro.run()
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
再执行看看:
Starting Portfolio Value: 100000.00
Final Portfolio Value: 100000.00
大脑开始发育:
- 能接受外部数据,浦发银行的行情数据
- 能对datetime进行处理,根据需要回测特定时期的数据
给大脑第一个策略
资金有了,行情数据也有了,下面就是进行投资,建立一个投资策略,针对数据(通常是收盘价)来决定投资策略。策略首先来输出收盘价测试下。
from __future__ import (absolute_import, division, print_function, unicode_literals)
import backtrader as bt
import pandas as pd
from utils.backtesting import *
from utils.utility import *
# Create a Strategy
class TestStrategy(bt.Strategy):
def log(self, txt, dt=None):
""" 提供记录功能 """
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt))
def __init__(self):
# 引用到输入数据的close价格
self.dataclose = self.datas[0].close
def next(self):
# 当前策略:显示收盘价
self.log('Close, %.2f' % self.dataclose[0])
if __name__ == '__main__':
cerebro = bt.Cerebro()
# 增加一个策略
cerebro.addstrategy(TestStrategy)
# 获取数据
stock_hfq_df = pd.read_excel("./data/600000.xlsx", index_col='date', parse_dates=True)
start_date = datetime.datetime(2010, 9, 30) # 回测开始时间
end_date = datetime.datetime(2021, 9, 30) # 回测结束时间
data = bt.feeds.PandasData(dataname=stock_hfq_df, fromdate=start_date, todate=end_date) # 加载数据
cerebro.adddata(data) # 将数据传入回测系统
cerebro.broker.setcash(100000.0)
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
cerebro.run()
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
执行后结果:
Starting Portfolio Value: 100000.00
2020-09-30, Close, 125.97
......
2021-09-27, Close, 127.11
2021-09-28, Close, 127.26
2021-09-29, Close, 127.11
2021-09-30, Close, 126.83
Final Portfolio Value: 100000.00
关于这个策略,需要重点关注:
- Strategy初始化的时候,将大脑加载的数据更新到dataclose属性中(这是个列表,保存股票回测开始时间到结束时间所有close数据)。self.datas[0]指向的事大脑通过cerebro.adddata函数加载的第一个数据,本例中加载浦发银行的股票数据。
- self.dataclose = self.datas[0].close指向的是close(收盘价)line。
- strategy的next方法针对self.dataclose(也就是收盘价Line)的每一行(也就是Bar)进行处理。在本例中,只是打印了下close的值。next方法是Strategy最重要的的方法,具体策略的实现都在这个函数中,后续还会详细介绍。
加入买的逻辑到Strategy中
没有买卖就没有收入,开始买卖,东西就要买便宜的,那就用价格连续降2天我们就买入的策略。
# 创建测试策略
class TestStrategy(bt.Strategy):
def log(self, txt, dt=None):
""" 记录策略信息 """
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt))
def __init__(self):
# 引用第一个数据源的收盘价
self.dataclose = self.datas[0].close
def next(self):
# 当前策略:当价格连续下降两天便买入
# 简单记录收盘价序列
self.log('Close, %.2f' % self.dataclose[0])
if self.dataclose[0] < self.dataclose[-1]:
# 当前价格比上一交易日价格低
if self.dataclose[-1] < self.dataclose[-2]:
# 上一交易日价格比上上一交易日价格低,则开始买入
self.log('BUY CREATE, %.2f' % self.dataclose[0])
self.buy()
执行结果如下:
Starting Portfolio Value: 100000.00
2010-09-30, Close, 65.55
2010-09-30, BUY CREATE, 65.55
2010-10-08, Close, 67.58
2010-10-11, Close, 70.26
2010-10-12, Close, 70.61
......
2021-09-17, BUY CREATE, 128.29
2021-09-22, Close, 127.16
2021-09-22, BUY CREATE, 127.16
2021-09-23, Close, 127.16
2021-09-24, Close, 127.02
2021-09-27, Close, 127.02
2021-09-28, Close, 127.16
2021-09-29, Close, 127.02
2021-09-30, Close, 126.74
2021-09-30, BUY CREATE, 126.74
Final Portfolio Value: 110309.45
策略居然真钱了,具体买卖过程:
- self.datas[0] 就是我们购买了的股票。本例中没有输入其他数据,如果输入了其他数据,购买的股票就不一定是啥了,这个要看具体的策略执行情况。
- 买了多少股本(stake)的股票?这个通过机器人大脑的position sizer属性来记录,缺省值为1,就是缺省咱们每一次操作只买卖1股。
- 当前order执行的时候,采用的价格是第二天的开盘价。
- 当前order执行的时候,没有收佣金。佣金如何设置后续还会说明。
不仅要买,还要卖出
一次完整的交易,不仅要买入,还要卖出。为了简单起见,我们处理5个bar数据(注意这个bar没有包含任何时间的概念,可以是1分钟、1小时、1天、一个月,是基于你输入的数据来决定的),本例中代买使用len函数与Python中不同,返回的事已经处理过得数据行(也就是bar)。注意,如果不在市场内,就不能卖出,不在市场内就是你不拥有任何股票头寸,也就是没有买入资产,在策略中通过position属性来记录。
本次代码中,将要增加:
- 访问position获取是否在市场内
- 创建买和卖的订单(order)
- 订单状态的改变会通过notify方法通知strategy
class TestStrategy(bt.Strategy):
def log(self, txt, dt=None):
""" 记录策略信息 """
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt))
def __init__(self):
# 引用第一个数据源的收盘价
self.dataclose = self.datas[0].close
# 跟踪记录未执行交易
self.order = None
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
# Buy/Sell order submitted/accepted to/by broker - Nothing to do
return
# Check if an order has been completed
# Attention: broker could reject order if not enough cash
if order.status in [order.Completed]:
if order.isbuy():
self.log('BUY EXECUTED, %.2f' % order.executed.price)
elif order.issell():
self.log('SELL EXECUTED, %.2f' % order.executed.price)
self.bar_executed = len(self)
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log('Order Canceled/Margin/Rejected')
# Write down: no pending order
self.order = None
def next(self):
# 当前策略:当价格连续下降两天便买入
# 简单记录收盘价序列
self.log('Close, %.2f' % self.dataclose[0])
# Check if an order is pending ... if yes, we cannot send a 2nd one
if self.order:
return
# 检查是否在市场
if not self.position:
# 不在,那么连续3天价格下跌就买点
if self.dataclose[0] < self.dataclose[-1]:
# 当前价格比上一次低
if self.dataclose[-1] < self.dataclose[-2]:
# 上一次的价格比上上次低
# 买入!!!
self.log('BUY CREATE, %.2f' % self.dataclose[0])
# Keep track of the created order to avoid a 2nd order
self.order = self.buy()
else:
# 已经在市场,5天后就卖掉。
if len(self) >= (self.bar_executed + 5):
# 这里注意,Len(self)返回的是当前执行的bar数量,
# 每次next会加1.而Self.bar_executed记录的最后一次交易执行时的bar位置。
# SELL, SELL, SELL!!! (with all possible default parameters)
self.log('SELL CREATE, %.2f' % self.dataclose[0])
# Keep track of the created order to avoid a 2nd order
self.order = self.sell()
执行结果:
Starting Portfolio Value: 100000.00
2010-09-30, Close, 65.55
2010-09-30, BUY CREATE, 65.55
2010-10-08, BUY EXECUTED, 66.21
2010-10-08, Close, 67.58
2010-10-11, Close, 70.26
......
2021-09-23, Close, 127.16
2021-09-24, Close, 127.02
2021-09-24, SELL CREATE, 127.02
2021-09-27, SELL EXECUTED, 127.02
2021-09-27, Close, 127.02
2021-09-28, Close, 127.16
2021-09-29, Close, 127.02
2021-09-30, Close, 126.74
2021-09-30, BUY CREATE, 126.74
Final Portfolio Value: 100039.20
从结果看,买入股票5天就卖出,结果只赚了40元,看来会卖的才是师傅。
券商的佣金呢?
券商说,你这里又买又卖的,我的佣金在哪儿?好的,咱们加上佣金,就0.1%吧,一行代码的事情:
# 0.1% ... 除以100去掉%号。 cerebro.broker.setcommission(commission=0.001)
下面我们看看佣金对收益的影响。
if __name__ == '__main__':
cerebro = bt.Cerebro()
# 增加一个策略
cerebro.addstrategy(TestStrategy)
# 获取数据
stock_hfq_df = pd.read_excel("./data/600000.xlsx", index_col='date', parse_dates=True)
start_date = datetime.datetime(2010, 9, 30) # 回测开始时间
end_date = datetime.datetime(2021, 9, 30) # 回测结束时间
data = bt.feeds.PandasData(dataname=stock_hfq_df, fromdate=start_date, todate=end_date) # 加载数据
cerebro.adddata(data) # 将数据传入回测系统
cerebro.broker.setcash(100000.0)
# 设置佣金0.1% ... 除以100去掉%号。
cerebro.broker.setcommission(commission=0.001)
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
cerebro.run()
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
结果如下:
2021-09-23, Close, 127.16
2021-09-24, Close, 127.02
2021-09-24, SELL CREATE, 127.02
2021-09-27, SELL EXECUTED, 127.02
2021-09-27, Close, 127.02
2021-09-28, Close, 127.16
2021-09-29, Close, 127.02
2021-09-30, Close, 126.74
2021-09-30, BUY CREATE, 126.74
Final Portfolio Value: 99988.58
挣的钱都给了券商,所以一定不要频繁交易。
同时,我们注意到,每次订单执行,都有一个操作盈利记录(Gross:毛利;Net:净利):
2021-09-27, OPERATION PROFIT, GROSS -2.68, NET -2.94