Backtrader 文档学习-Quickstart
0. 前言
backtrader,功能十分完善,有完整的使用文档,安装相对简单(直接pip安装即可)。
优点是运行速度快,支持pandas的矢量运算;支持参数自动寻优运算,内置了talib股票分析技术指标库;支持多品种、多策略、多周期的回测和交易;支持pyflio、empyrica分析模块库、alphalens多因子分析模块库等;扩展灵活,可以集成TensorFlow、PyTorch和Keras等机器学习、神经网络分析模块。
缺点:backtrader学习起来相对复杂,编程过程中使用了大量的元编程(类class),如果Python编程基础不扎实(尤其是类的操作),学习困难。另外一点,BackTrader不更新。
如果将backtrader包分解为核心组件,主要包括以下组成部分:
- (1)数据加载(Data Feed):将交易策略的数据加载到回测框架中。
- (2)交易策略(Strategy):该模块是编程过程中最复杂的部分,需要设计交易决策,得出买入/卖出信号。
- (3)回测框架设置( Cerebro):
需要设置:(i)初始资金(ii)佣金(iii)数据馈送(iv)交易策略(v)交易头寸大小。 - (4)运行回测:运行Cerebro回测并打印出所有已执行的交易。
- (5)评估性能(Analyzers):以图形和风险收益等指标对交易策略的回测结果进行评价。
官网说明资料详细,有演示用例,逐步跟着一步步学习。
1. 两个基本概念
(1)Lines
"Lines"是backtrader回测的数据,由一系列的点组成,通常包括以下类别的数据:Open(开盘价), High(最高价), Low(最低价), Close(收盘价), Volume(成交量), OpenInterest(无的话设置为0)。Data Feeds(数据加载)、Indicators(技术指标)和Strategies(策略)都会生成 Lines。
价格数据中的所有"Open" (开盘价)按时间组成一条 Line。所以,一组含有以上6个类别的价格数据,共有6条 Lines。如果算上"DateTime"(时间,可以看作是一组数据的主键),一共有7条 Lines。当访问一条 Line 的数据时,会默认指向下标为 0 的数据。最后一个数据通过下标 -1 来访问,在-1之后是索引0,用于访问当前时刻。因此,在回测过程中,无需知道已经处理了多少条/分钟/天/月,"0"一直指向当前值,下标 -1 来访问最后一个值。
Lines包括一个或多个line,line是一系列的数据,在图中可以形成一条线(line),有6个列数据,就是股票的主要数据集,最后一列没有用。
Open, High, Low, Close, Volume, OpenInterest
包括索引列"DateTime",日期时间类型,注意:Datetime类型,不是Date类型。
(2)Index 0 Approach
访问行中的值时,将使用索引0 访问当前值;
"最后一个"输出值是用索引**-1**访问,index-1用于访问可迭代项/数组的"最后"项。
在Backtrader中提供了1个函数来度量已处理数据bar的长度:
len:返回当前系统已经处理的数据(bars)。这个和python标准的len定义差异。
(3)版本
通过 pip index versions backtrader 检查版本。
版本:backtrader (1.9.78.123)
pip index versions backtrader
WARNING: pip index is currently an experimental command. It may be removed/changed in a future release without prior warning.
backtrader (1.9.78.123)
Available versions: 1.9.78.123, 1.9.77.123, 1.9.76.123, 1.9.75.123, 1.9.74.123
2. 基本使用
(1)初始设置现金
cerebro.broker.setcash(100000.0)
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import backtrader as bt
if __name__ == '__main__':
cerebro = bt.Cerebro()
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
(2)加载数据
示例使用的是Oracle的记录,实际使用,调整到国内数据。
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import datetime # For datetime objects
import os.path # To manage paths
import sys # To find out the script name (in argv[0])
# Import the backtrader platform
import backtrader as bt
if __name__ == '__main__':
# Create a cerebro entity
cerebro = bt.Cerebro()
# Datas are in a subfolder of the samples. Need to find where the script is
# because it could have been called from anywhere
modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
datapath = os.path.join(modpath, '../../datas/orcl-1995-2014.txt')
# Create a Data Feed
data = bt.feeds.YahooFinanceCSVData(
dataname=datapath,
# Do not pass values before this date
fromdate=datetime.datetime(2000, 1, 1),
# Do not pass values after this date
todate=datetime.datetime(2000, 12, 31),
reverse=False)
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# Set our desired cash start
cerebro.broker.setcash(100000.0)
# Print out the starting conditions
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
# Run over everything
cerebro.run()
# Print out the final result
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
编写一个从数据库中取数据的函数使用:
注意:数据库中交易日期是date ,backtrader的数据集要求是datetime ,必须做好转换才能载入数据。
from sqlalchemy import create_engine
def get_code (stock_code):
engine_ts = create_engine(connect parameter)
# 执行sql操作
sql = "select * from ts_stock t where t.stock_code=" + stock_code + ";"
#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)
if __name__ == '__main__':
# Create a cerebro entity
cerebro = bt.Cerebro()
stock_hfq_df = get_code('000858')
#起止时间
start_date = datetime.datetime(2015, 1, 1) # 回测开始时间
end_date = datetime.datetime(2019, 12, 31) # 回测结束时间
data = bt.feeds.PandasData(dataname=stock_hfq_df, fromdate=start_date, todate=end_date) # 加载数据
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# Set our desired cash start
cerebro.broker.setcash(100000.0)
# Print out the starting conditions
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
# Run over everything
cerebro.run()
# Print out the final result
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
(3)第一个策略 买入
在init方法中,可以使用载入的数据集,第一个数据是列表 self.datas[0] ,最后一个是 self.datas[-1] 。
self.dataclose=self.datas[0]。赋值close的引用,以后只需要一个间接引用dataclose ,就可以访问收盘值。
策略next方法将在系统时钟的每个bar上调用(self.datas[0]),直到符合策略条件,比如指标值设置,才能开始产生输出。
策略:
连续下跌三天,开始买入。
策略实施在next()方法中。
## 3.第一个策略
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import datetime # For datetime objects
import os.path # To manage paths
import sys # To find out the script name (in argv[0])
# Import the backtrader platform
import backtrader as bt
# Create a Stratey
class TestStrategy(bt.Strategy):
def log(self, txt, dt=None):
''' Logging function for this strategy'''
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt))
def __init__(self):
# Keep a reference to the "close" line in the data[0] dataseries
self.dataclose = self.datas[0].close
def next(self):
# Simply log the closing price of the series from the reference
self.log('Close, %.2f' % self.dataclose[0])
if __name__ == '__main__':
# Create a cerebro entity
# delete log file
log_file = './bt_log.txt'
delete_file(log_file)
cerebro = bt.Cerebro()
# Add a strategy
cerebro.addstrategy(TestStrategy)
# 五粮液测试
stock_hfq_df = get_code('000858')
start_date = datetime.datetime(2015, 1, 1) # 回测开始时间
end_date = datetime.datetime(2019, 12, 31) # 回测结束时间
data = bt.feeds.PandasData(dataname=stock_hfq_df, fromdate=start_date, todate=end_date) # 加载数据
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# Set our desired cash start
cerebro.broker.setcash(100000.0)
# Print out the starting conditions
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
# Run over everything
cerebro.run()
# Print out the final result
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
调整:
输出都是close数据,数据显示比较多,都放到log文件中。 日志路径:
log_file = './bt_log.txt'
修改TestStrategy 中的log方法,日志写入文件,便于查询。后不赘述。
## 3.第一个策略
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import datetime # For datetime objects
import os.path # To manage paths
import sys # To find out the script name (in argv[0])
# Import the backtrader platform
import backtrader as bt
import os
# delete log file
def delete_file(filename):
# if log file exist
if os.path.exists(filename):
os.remove(filename)
# Create a Stratey
class TestStrategy(bt.Strategy):
def log(self, txt, dt=None):
''' Logging function for this strategy'''
dt = dt or self.datas[0].datetime.date(0)
#print('%s, %s' % (dt.isoformat(), txt))
with open(log_file, 'a') as file:
file.write('%s, %s' % (dt.isoformat(), txt))
file.write('\n')
def __init__(self):
# Keep a reference to the "close" line in the data[0] dataseries
self.dataclose = self.datas[0].close
def next(self):
# Simply log the closing price of the series from the reference
self.log('Close, %.2f' % self.dataclose[0])
if __name__ == '__main__':
# delete log file
log_file = './bt_log.txt'
delete_file(log_file)
# Create a cerebro entity
cerebro = bt.Cerebro()
# Add a strategy
cerebro.addstrategy(TestStrategy)
stock_hfq_df = get_code('000858')
start_date = datetime.datetime(2015, 1, 1) # 回测开始时间
end_date = datetime.datetime(2019, 12, 31) # 回测结束时间
data = bt.feeds.PandasData(dataname=stock_hfq_df, fromdate=start_date, todate=end_date) # 加载数据
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# Set our desired cash start
cerebro.broker.setcash(100000.0)
# Print out the starting conditions
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
# Run over everything
cerebro.run()
# Print out the final result
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
执行后,没有交易过程记录,都在日志文件中显示:
Starting Portfolio Value: 100000.00
Final Portfolio Value: 100000.00
再增加策略中的逻辑:
策略:连续三天下跌,开始买入操作
# Create a Stratey
class TestStrategy(bt.Strategy):
def log(self, txt, dt=None):
''' Logging function for this strategy'''
dt = dt or self.datas[0].datetime.date(0)
#print('%s, %s' % (dt.isoformat(), txt))
with open(log_file, 'a') as file:
file.write('%s, %s' % (dt.isoformat(), txt))
file.write('\n')
def __init__(self):
# Keep a reference to the "close" line in the data[0] dataseries
self.dataclose = self.datas[0].close
#Open, High, Low, Close, Volume, OpenInterest
self.dataclose = self.datas[0].close
self.dataopen = self.datas[0].open
self.datahigh = self.datas[0].high
self.datalow = self.datas[0].low
self.datavol = self.datas[0].volume
def next(self):
# Simply log the closing price of the series from the reference
self.log('Close, %.2f' % self.dataclose[0])
if self.dataclose[0] < self.dataclose[-1]:
# current close less than previous close
if self.dataclose[-1] < self.dataclose[-2]:
# previous close less than the previous close
# BUY, BUY, BUY!!! (with all possible default parameters)
self.log('BUY CREATE, %.2f' % self.dataclose[0])
self.buy()
说明:
-
self.datas[0] 就是购买的股票。
-
默认购买单位是1,每次买1股 。position
sizer属性来记录,缺省值为1,就是每一次操作只买卖1股。当前order订单执行的时候,采用的价格是触发购买条件第二天的开盘价。2018-01-02, Close, 80.58
2018-01-03, Close, 80.90
2018-01-04, Close, 82.99
2018-01-05, Close, 82.68
2018-01-08, Close, 82.20
2018-01-08, BUY CREATE, 82.20
2018-01-09, Close, 86.10
2018-01-10, Close, 88.90
5号第一天下跌,8日第二天连续下跌,触发购买信号,购买价格就是8号的收盘价,就是9日的开盘价。
- 当前order执行的时候,没有收佣金。佣金如何设置后续还会说明。
可以看Strategy类有什么方法、属性:
method = ""
for i in dir(bt.Strategy):
if i[:1] != '_' :
method += i + ','
print(method)
方法和属性:
IndType,ObsType,PriceClose,PriceDateTime,PriceHigh,PriceLow,PriceOpen,PriceOpenInteres,PriceVolume,StratType,add_timer,addindicator,addminperiod,advance,alias,aliased,array,backwards,bind2line,bind2lines,bindlines,buy,buy_bracket,cancel,clear,close,csv,extend,forward,frompackages,getdatabyname,getdatanames,getindicators,getindicators_lines,getobservers,getposition,getpositionbyname,getpositions,getpositionsbyname,getsizer,getsizing,getwriterheaders,getwriterinfo,getwritervalues,home,incminperiod,linealias,lines,minbuffer,next,next_open,nextstart,nextstart_open,notify_cashvalue,notify_data,notify_fund,notify_order,notify_store,notify_timer,notify_trade,once,oncestart,order_target_percent,order_target_size,order_target_value,packages,params,plotinfo,plotlabel,plotlines,position,positionbyname,positions,positionsbyname,prenext,prenext_open,preonce,qbuffer,reset,rewind,sell,sell_bracket,set_tradehistory,setminperiod,setsizer,sizer,start,stop,updateminperiod,
(4)还要卖出
-
Strategy对象提供了对默认数据的位置属性的访问
-
方法buy和sell 都创建(尚未执行)执行订单
-
Strategy订单状态的变化将通过notify 方法调用
-
卖出策略是:持仓5天,在第6天卖出
4.不但买入,还要卖出
Create a Stratey
class TestStrategy(bt.Strategy):
def log(self, txt, dt=None): ''' Logging function for this strategy''' dt = dt or self.datas[0].datetime.date(0) #print('%s, %s' % (dt.isoformat(), txt)) with open(log_file, 'a') as file: file.write('%s, %s' % (dt.isoformat(), txt)) file.write('\n') def __init__(self): # Keep a reference to the "close" line in the data[0] dataseries self.dataclose = self.datas[0].close #Open, High, Low, Close, Volume, OpenInterest self.dataclose = self.datas[0].close self.dataopen = self.datas[0].open self.datahigh = self.datas[0].high self.datalow = self.datas[0].low self.datavol = self.datas[0].volume # To keep track of pending orders self.order = None def notify_order(self, order): # 买卖订单的状态:提交和接受,通过broker控制 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 # broker如果资金不足将reject订单 #订单状态是完成 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) #定义bar_executed 变量,记录处理bar的数量 #len:返回当前系统已经处理的数据(bars)。这个和python标准的len定义差异。 self.bar_executed = len(self) self.bar_buffer = lenbuf(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): # Simply log the closing price of the series from the reference 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 # Check if we are in the market if not self.position: # Not yet ... we MIGHT BUY if ... #连续两天下跌,开始买入 if self.dataclose[0] < self.dataclose[-1]: # current close less than previous close if self.dataclose[-1] < self.dataclose[-2]: # previous close less than the previous close # BUY, BUY, BUY!!! (with default parameters) self.log('BUY CREATE, %.2f' % self.dataclose[0]) # Keep track of the created order to avoid a 2nd order self.order = self.buy() else: # Already in the market ... we might sell if len(self) >= (self.bar_executed + 5): # 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()
if name == 'main':
# delete log file log_file = './bt_log.txt' delete_file(log_file) # Create a cerebro entity cerebro = bt.Cerebro() # Add a strategy cerebro.addstrategy(TestStrategy) stock_hfq_df = get_code('000858') start_date = datetime.datetime(2015, 1, 1) # 回测开始时间 end_date = datetime.datetime(2019, 12, 31) # 回测结束时间 data = bt.feeds.PandasData(dataname=stock_hfq_df, fromdate=start_date, todate=end_date) # 加载数据 # Add the Data Feed to Cerebro cerebro.adddata(data) # Set our desired cash start cerebro.broker.setcash(100000.0) # Print out the starting conditions print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue()) # Run over everything cerebro.run() # Print out the final result print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
执行流程,每次买卖数量都是1股:
5日下跌,6日下跌,创建买单,9日执行买单,是9日的开盘价。
买单时,处理的是6个bar 。
从10日开始,到16日是持仓第5天,创建卖单,17日,开盘卖出。
执行结果:
2018-01-02, Close, 80.58
2018-01-03, Close, 80.90
2018-01-04, Close, 82.99
2018-01-05, Close, 82.68
2018-01-08, Close, 82.20
2018-01-08, BUY CREATE, 82.20
2018-01-09, BUY EXECUTED, 82.40
2018-01-09, Bar executed :6
2018-01-09, Close, 86.10
2018-01-10, Close, 88.90
2018-01-11, Close, 87.96
2018-01-12, Close, 91.37
2018-01-15, Close, 91.75
2018-01-16, Close, 90.82
2018-01-16, SELL CREATE, 90.82
2018-01-17, SELL EXECUTED, 90.30
2018-01-17, Bar executed :12
... ...
... ...
... ...
订单的状态是通过Order对象的status属性来表示的。status属性可以是以下几个值之一:
- Order.Submitted:订单已提交,但尚未成交。
- Order.Accepted:订单已被接受,正在等待成交。
- Order.Completed:订单已完全成交。
- Order.Canceled:订单已取消。
- Order.Margin:订单由于保证金不足而被拒绝。
- Order.Rejected:订单被拒绝,原因可能是无效的价格、数量等。
(5)考虑券商佣金
在main函数中增加
# Set the commission - 0.1% ... divide by 100 to remove the %
cerebro.broker.setcommission(commission=0.001)
修改后的策略:
增加 方法 def notify_trade(self, trade):
用于计算毛利和纯利 ,通过trade对象计算。
查看在backtrader 目录下的trade.py源码:
定义属性:
pnl定义毛利,pnlcomm定义毛利-佣金
Attributes:
- ``status`` (``dict`` with '.' notation): Holds the resulting status of
an update event and has the following sub-attributes
- ``status`` (``int``): Trade status
- ``dt`` (``float``): float coded datetime
- ``barlen`` (``int``): number of bars the trade has been active
- ``size`` (``int``): current size of the Trade
- ``price`` (``float``): current price of the Trade
- ``value`` (``float``): current monetary value of the Trade
- ``pnl`` (``float``): current profit and loss of the Trade
- ``pnlcomm`` (``float``): current profit and loss minus commission
- ``event`` (``dict`` with '.' notation): Holds the event update
- parameters
- ``order`` (``object``): the order which initiated the``update``
- ``size`` (``int``): size of the update
- ``price`` (``float``):price of the update
- ``commission`` (``float``): price of the update
'''
#5. 考虑佣金
# Create a Stratey
class TestStrategy(bt.Strategy):
def log(self, txt, dt=None):
''' Logging function for this strategy'''
dt = dt or self.datas[0].datetime.date(0)
#print('%s, %s' % (dt.isoformat(), txt))
with open(log_file, 'a') as file:
file.write('%s, %s' % (dt.isoformat(), txt))
file.write('\n')
def __init__(self):
# Keep a reference to the "close" line in the data[0] dataseries
self.dataclose = self.datas[0].close
#Open, High, Low, Close, Volume, OpenInterest
self.dataclose = self.datas[0].close
self.dataopen = self.datas[0].open
self.datahigh = self.datas[0].high
self.datalow = self.datas[0].low
self.datavol = self.datas[0].volume
# To keep track of pending orders
self.order = None
# To keep track of pending orders and buy price/commission
self.order = None
self.buyprice = None
self.buycomm = None
# 统计毛利和净利润
self.gross = 0.0
self.net = 0.0
def notify_order(self, order):
# 买卖订单的状态:提交和接受,通过broker控制
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
# broker如果资金不足将reject订单
#订单状态是完成
if 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))
self.buyprice = order.executed.price
self.buycomm = order.executed.comm
#判读是卖单,写日志
elif order.issell():
self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
#定义bar_executed 变量,记录处理bar的数量
#len:返回当前系统已经处理的数据(bars)。这个和python标准的len定义差异。
self.bar_executed = len(self)
#日志显示处理的bar数量,逐渐递增。
strlog = 'Bar executed :' + str(self.bar_executed)
self.log(strlog)
# 订单取消、保证金不足、退回
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 notify_trade(self, trade):
# 如果不是平仓,返回
if not trade.isclosed:
return
# 平仓计算成本和利润
self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
(trade.pnl, trade.pnlcomm))
# 累计毛利和净利润
self.gross += trade.pnl
self.net =+ trade.pnlcomm
self.log ('Accumulated profit,GROSS %.2f, NET %.2f' % (self.gross,self.net))
def next(self):
# Simply log the closing price of the series from the reference
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
# Check if we are in the market
if not self.position:
# Not yet ... we MIGHT BUY if ...
#连续两天下跌,开始买入
if self.dataclose[0] < self.dataclose[-1]:
# current close less than previous close
if self.dataclose[-1] < self.dataclose[-2]:
# previous close less than the previous close
# BUY, BUY, BUY!!! (with default parameters)
self.log('BUY CREATE, %.2f' % self.dataclose[0])
# Keep track of the created order to avoid a 2nd order
self.order = self.buy()
else:
# Already in the market ... we might sell
# 持仓5天
if len(self) >= (self.bar_executed + 5):
# 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
Final Portfolio Value: 100040.35
日志内容:
2018-01-02, Close, 80.58
2018-01-03, Close, 80.90
2018-01-04, Close, 82.99
2018-01-05, Close, 82.68
2018-01-08, Close, 82.20
2018-01-08, BUY CREATE, 82.20
2018-01-09, BUY EXECUTED, Price: 82.40, Cost: 82.40, Comm 0.01
2018-01-09, Bar executed :6
2018-01-09, Close, 86.10
2018-01-10, Close, 88.90
2018-01-11, Close, 87.96
2018-01-12, Close, 91.37
2018-01-15, Close, 91.75
2018-01-16, Close, 90.82
2018-01-16, SELL CREATE, 90.82
2018-01-17, SELL EXECUTED, Price: 90.30, Cost: 82.40, Comm 0.01
2018-01-17, Bar executed :12
2018-01-17, OPERATION PROFIT, GROSS 7.90, NET 7.88
2018-01-17, Accumulated profit,GROSS 7.90, NET 7.88
2018-01-17, Close, 86.01
... ...
... ...
2019-12-12, BUY CREATE, 127.78
2019-12-13, BUY EXECUTED, Price: 128.58, Cost: 128.58, Comm 0.01
2019-12-13, Bar executed :475
2019-12-13, Close, 129.52
2019-12-16, Close, 128.83
2019-12-17, Close, 130.25
2019-12-18, Close, 130.94
2019-12-19, Close, 129.86
2019-12-20, Close, 129.10
2019-12-20, SELL CREATE, 129.10
2019-12-23, SELL EXECUTED, Price: 127.50, Cost: 128.58, Comm 0.01
2019-12-23, Bar executed :481
2019-12-23, OPERATION PROFIT, GROSS -1.08, NET -1.11
2019-12-23, Accumulated profit,GROSS 36.59, NET -1.11
2019-12-23, Close, 128.14
2019-12-23, BUY CREATE, 128.14
2019-12-24, BUY EXECUTED, Price: 128.44, Cost: 128.44, Comm 0.01
2019-12-24, Bar executed :482
2019-12-24, Close, 128.70
2019-12-25, Close, 128.10
2019-12-26, Close, 128.15
2019-12-27, Close, 129.00
2019-12-30, Close, 132.82
2019-12-31, Close, 133.01
2019-12-31, SELL CREATE, 133.01
可以看出
2018-01-17, SELL EXECUTED, Price: 90.30, Cost: 82.40, Comm 0.01
盈利:90.30 - 82.40 = 7.90元,佣金0.01
2018-01-17, OPERATION PROFIT, GROSS 7.90, NET 7.88
毛利:7.90元 ,买卖两次,佣金0.02
净利润:7.90 - 0.02 = 7.88 元
(6)优化策略参数
在main函数中增加,每次购买10股,默认是1股。
# Add a FixedSize sizer according to the stake cerebro.addsizer(bt.sizers.FixedSize, stake=10)
在TestStrategy(bt.Strategy) 类定义中,增加参数。
params = (
('exitbars', 5),
)
用于持仓天数,默认是5天。
修改后代码:
#6. 优化参数
# Create a Stratey
class TestStrategy(bt.Strategy):
params = (
('exitbars', 5),
)
def log(self, txt, dt=None):
''' Logging function for this strategy'''
dt = dt or self.datas[0].datetime.date(0)
#print('%s, %s' % (dt.isoformat(), txt))
with open(log_file, 'a') as file:
file.write('%s, %s' % (dt.isoformat(), txt))
file.write('\n')
def __init__(self):
# Keep a reference to the "close" line in the data[0] dataseries
self.dataclose = self.datas[0].close
#Open, High, Low, Close, Volume, OpenInterest
self.dataclose = self.datas[0].close
self.dataopen = self.datas[0].open
self.datahigh = self.datas[0].high
self.datalow = self.datas[0].low
self.datavol = self.datas[0].volume
# To keep track of pending orders
self.order = None
# To keep track of pending orders and buy price/commission
self.order = None
self.buyprice = None
self.buycomm = None
# 统计毛利和净利润
self.gross = 0.0
self.net = 0.0
def notify_order(self, order):
# 买卖订单的状态:提交和接受,通过broker控制
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
# broker如果资金不足将reject订单
#订单状态是完成
if 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))
self.buyprice = order.executed.price
self.buycomm = order.executed.comm
#判读是卖单,写日志
elif order.issell():
self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
#定义bar_executed 变量,记录处理bar的数量
#len:返回当前系统已经处理的数据(bars)。这个和python标准的len定义差异。
self.bar_executed = len(self)
#日志显示处理的bar数量,逐渐递增。
strlog = 'Bar executed :' + str(self.bar_executed)
self.log(strlog)
# 订单取消、保证金不足、退回
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 notify_trade(self, trade):
# 如果不是平仓,返回
if not trade.isclosed:
return
# 平仓计算成本和利润
self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
(trade.pnl, trade.pnlcomm))
# 累计毛利和净利润
self.gross += trade.pnl
self.net =+ trade.pnlcomm
self.log ('Accumulated profit,GROSS %.2f, NET %.2f' % (self.gross,self.net))
def next(self):
# Simply log the closing price of the series from the reference
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
# Check if we are in the market
if not self.position:
# Not yet ... we MIGHT BUY if ...
#连续两天下跌,开始买入
if self.dataclose[0] < self.dataclose[-1]:
# current close less than previous close
if self.dataclose[-1] < self.dataclose[-2]:
# previous close less than the previous close
# BUY, BUY, BUY!!! (with default parameters)
self.log('BUY CREATE, %.2f' % self.dataclose[0])
# Keep track of the created order to avoid a 2nd order
self.order = self.buy()
else:
# Already in the market ... we might sell
# 持仓5天
if len(self) >= (self.bar_executed + self.params.exitbars):
# 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()
if __name__ == '__main__':
# delete log file
log_file = './bt_log.txt'
delete_file(log_file)
# Create a cerebro entity
cerebro = bt.Cerebro()
# Add a strategy
cerebro.addstrategy(TestStrategy)
stock_hfq_df = get_code('111969')
start_date = datetime.datetime(2015, 1, 1) # 回测开始时间
end_date = datetime.datetime(2019, 12, 31) # 回测结束时间
data = bt.feeds.PandasData(dataname=stock_hfq_df, fromdate=start_date, todate=end_date) # 加载数据
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# Set our desired cash start
cerebro.broker.setcash(100000.0)
# Set the commission - 0.1% ... divide by 100 to remove the %
# 按万一的佣金 ,买卖操作都要扣除
cerebro.broker.setcommission(commission=0.0001)
# Add a FixedSize sizer according to the stake
cerebro.addsizer(bt.sizers.FixedSize, stake=10)
# Print out the starting conditions
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
# Run over everything
cerebro.run()
# Print out the final result
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
执行输出结果:
Starting Portfolio Value: 100000.00
Final Portfolio Value: 100403.51
由于每次购买10股,盈利增加。
日志:
2018-01-02, Close, 80.58
2018-01-03, Close, 80.90
2018-01-04, Close, 82.99
2018-01-05, Close, 82.68
2018-01-08, Close, 82.20
2018-01-08, BUY CREATE, 82.20
2018-01-09, BUY EXECUTED, Price: 82.40, Cost: 824.00, Comm 0.08
2018-01-09, Bar executed :6
2018-01-09, Close, 86.10
2018-01-10, Close, 88.90
2018-01-11, Close, 87.96
2018-01-12, Close, 91.37
2018-01-15, Close, 91.75
2018-01-16, Close, 90.82
2018-01-16, SELL CREATE, 90.82
2018-01-17, SELL EXECUTED, Price: 90.30, Cost: 824.00, Comm 0.09
2018-01-17, Bar executed :12
2018-01-17, OPERATION PROFIT, GROSS 79.00, NET 78.83
2018-01-17, Accumulated profit,GROSS 79.00, NET 78.83
2018-01-17, Close, 86.01
一次买卖,平仓后,净利润和毛利都增加。
(7)增加指示器indicator
上面的例子,买入是连跌三天,卖出是持仓5天。策略简单粗暴。
通过indicator的均线,做买入卖出指标,更加合理一点。
- 如果收盘价高于平均值,则买入
- 如果收盘价小于平均值,则卖出
- 只允许1个交易活动操作,买一单,卖出一单的模式
修改内容:
- 策略增加参数,SMA周期参数,默认设置30日 。
params = ( ('maperiod', 30),('exitbars', 5), )
-
在next方法中,调整买卖的判断。
#7. 使用指示器
Create a Stratey
class TestStrategy(bt.Strategy):
params = (
('maperiod', 30),('exitbars', 5),
)def log(self, txt, dt=None): ''' Logging function for this strategy''' dt = dt or self.datas[0].datetime.date(0) #print('%s, %s' % (dt.isoformat(), txt)) with open(log_file, 'a') as file: file.write('%s, %s' % (dt.isoformat(), txt)) file.write('\n') def __init__(self): # Keep a reference to the "close" line in the data[0] dataseries self.dataclose = self.datas[0].close #Open, High, Low, Close, Volume, OpenInterest self.dataclose = self.datas[0].close self.dataopen = self.datas[0].open self.datahigh = self.datas[0].high self.datalow = self.datas[0].low self.datavol = self.datas[0].volume # To keep track of pending orders self.order = None # To keep track of pending orders and buy price/commission self.order = None self.buyprice = None self.buycomm = None # 统计毛利和净利润 self.gross = 0.0 self.net = 0.0 # Add a MovingAverageSimple indicator # 使用简单移动平均线确定买入和卖出操作 self.sma = bt.indicators.SimpleMovingAverage( self.datas[0], period=self.params.maperiod) def notify_order(self, order): # 买卖订单的状态:提交和接受,通过broker控制 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 # broker如果资金不足将reject订单 #订单状态是完成 if 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)) self.buyprice = order.executed.price self.buycomm = order.executed.comm #判读是卖单,写日志 elif order.issell(): self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % (order.executed.price, order.executed.value, order.executed.comm)) #定义bar_executed 变量,记录处理bar的数量 #len:返回当前系统已经处理的数据(bars)。这个和python标准的len定义差异。 self.bar_executed = len(self) #日志显示处理的bar数量,逐渐递增。 strlog = 'Bar executed :' + str(self.bar_executed) self.log(strlog) # 订单取消、保证金不足、退回 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 notify_trade(self, trade): # 如果不是平仓,返回 if not trade.isclosed: return # 平仓计算成本和利润 self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' % (trade.pnl, trade.pnlcomm)) # 累计毛利和净利润 self.gross += trade.pnl self.net =+ trade.pnlcomm self.log ('Accumulated profit,GROSS %.2f, NET %.2f' % (self.gross,self.net)) def next(self): # Simply log the closing price of the series from the reference 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 # Check if we are in the market if not self.position: # Not yet ... we MIGHT BUY if ... #收盘穿过简单平均移动线,买入 if self.dataclose[0] > self.sma[0]: # current close less than previous close if self.dataclose[-1] < self.dataclose[-2]: # previous close less than the previous close # BUY, BUY, BUY!!! (with default parameters) self.log('BUY CREATE, %.2f' % self.dataclose[0]) # Keep track of the created order to avoid a 2nd order self.order = self.buy() else: # Already in the market ... we might sell #收盘穿过简单平均移动线,买入 if self.dataclose[0] < self.sma[0]: # 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()
if name == 'main':
# delete log file log_file = './bt_log.txt' delete_file(log_file) # Create a cerebro entity cerebro = bt.Cerebro() # Add a strategy cerebro.addstrategy(TestStrategy) stock_hfq_df = get_code('111969') start_date = datetime.datetime(2015, 1, 1) # 回测开始时间 end_date = datetime.datetime(2019, 12, 31) # 回测结束时间 data = bt.feeds.PandasData(dataname=stock_hfq_df, fromdate=start_date, todate=end_date) # 加载数据 # Add the Data Feed to Cerebro cerebro.adddata(data) # Set our desired cash start cerebro.broker.setcash(100000.0) # Set the commission - 0.1% ... divide by 100 to remove the % # 按万一的佣金 ,买卖操作都要扣除 cerebro.broker.setcommission(commission=0.0001) # Add a FixedSize sizer according to the stake cerebro.addsizer(bt.sizers.FixedSize, stake=10) # Print out the starting conditions print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue()) # Run over everything cerebro.run() # Print out the final result print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
执行结果:
Starting Portfolio Value: 100000.00
Final Portfolio Value: 100522.68
比简单判断下跌买入,持仓5天卖出的策略,收益高119.17。
Starting Portfolio Value: 100000.00
Final Portfolio Value: 100403.51
(8)可视化
内置的plot方法,参数如下:
def plot(self, plotter=None, numfigs=1, iplot=True, start=None, end=None, width=16, height=9, dpi=300, tight=True, use=None, **kwargs):
说明:如果在jupyter中直接绘图,报错
cerebro.plot()
报错:Javascript Error: IPython is not defined
解决方法:
%matplotlib inline
调用绘图:
cerebro.plot(iplot=False)
在jupyter中可以绘图。
在init方法中增加绘图指示器指标:
# Indicators for the plotting show
bt.indicators.ExponentialMovingAverage(self.datas[0], period=25)
bt.indicators.WeightedMovingAverage(self.datas[0], period=25,
subplot=True)
bt.indicators.StochasticSlow(self.datas[0])
bt.indicators.MACDHisto(self.datas[0])
rsi = bt.indicators.RSI(self.datas[0])
bt.indicators.SmoothedMovingAverage(rsi, period=10)
bt.indicators.ATR(self.datas[0], plot=False)
#8. 可视化
# Create a Stratey
class TestStrategy(bt.Strategy):
params = (
('maperiod', 30),('exitbars', 5),
)
def log(self, txt, dt=None):
''' Logging function for this strategy'''
dt = dt or self.datas[0].datetime.date(0)
#print('%s, %s' % (dt.isoformat(), txt))
with open(log_file, 'a') as file:
file.write('%s, %s' % (dt.isoformat(), txt))
file.write('\n')
def __init__(self):
# Keep a reference to the "close" line in the data[0] dataseries
self.dataclose = self.datas[0].close
#Open, High, Low, Close, Volume, OpenInterest
self.dataclose = self.datas[0].close
self.dataopen = self.datas[0].open
self.datahigh = self.datas[0].high
self.datalow = self.datas[0].low
self.datavol = self.datas[0].volume
# To keep track of pending orders
self.order = None
# To keep track of pending orders and buy price/commission
self.order = None
self.buyprice = None
self.buycomm = None
# 统计毛利和净利润
self.gross = 0.0
self.net = 0.0
# Add a MovingAverageSimple indicator
# 使用简单移动平均线确定买入和卖出操作
self.sma = bt.indicators.SimpleMovingAverage(
self.datas[0], period=self.params.maperiod)
# Indicators for the plotting show
bt.indicators.ExponentialMovingAverage(self.datas[0], period=25)
bt.indicators.WeightedMovingAverage(self.datas[0], period=25,
subplot=True)
bt.indicators.StochasticSlow(self.datas[0])
bt.indicators.MACDHisto(self.datas[0])
rsi = bt.indicators.RSI(self.datas[0])
bt.indicators.SmoothedMovingAverage(rsi, period=10)
bt.indicators.ATR(self.datas[0], plot=False)
def notify_order(self, order):
# 买卖订单的状态:提交和接受,通过broker控制
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
# broker如果资金不足将reject订单
#订单状态是完成
if 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))
self.buyprice = order.executed.price
self.buycomm = order.executed.comm
#判读是卖单,写日志
elif order.issell():
self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
#定义bar_executed 变量,记录处理bar的数量
#len:返回当前系统已经处理的数据(bars)。这个和python标准的len定义差异。
self.bar_executed = len(self)
#日志显示处理的bar数量,逐渐递增。
strlog = 'Bar executed :' + str(self.bar_executed)
self.log(strlog)
# 订单取消、保证金不足、退回
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 notify_trade(self, trade):
# 如果不是平仓,返回
if not trade.isclosed:
return
# 平仓计算成本和利润
self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
(trade.pnl, trade.pnlcomm))
# 累计毛利和净利润
self.gross += trade.pnl
self.net =+ trade.pnlcomm
self.log ('Accumulated profit,GROSS %.2f, NET %.2f' % (self.gross,self.net))
def next(self):
# Simply log the closing price of the series from the reference
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
# Check if we are in the market
if not self.position:
# Not yet ... we MIGHT BUY if ...
#收盘穿过简单平均移动线,买入
if self.dataclose[0] > self.sma[0]:
# current close less than previous close
if self.dataclose[-1] < self.dataclose[-2]:
# previous close less than the previous close
# BUY, BUY, BUY!!! (with default parameters)
self.log('BUY CREATE, %.2f' % self.dataclose[0])
# Keep track of the created order to avoid a 2nd order
self.order = self.buy()
else:
# Already in the market ... we might sell
#收盘穿过简单平均移动线,买入
if self.dataclose[0] < self.sma[0]:
# 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()
%matplotlib inline
if __name__ == '__main__':
# delete log file
log_file = './bt_log.txt'
delete_file(log_file)
# Create a cerebro entity
cerebro = bt.Cerebro()
# Add a strategy
cerebro.addstrategy(TestStrategy)
stock_hfq_df = get_code('111969')
start_date = datetime.datetime(2015, 1, 1) # 回测开始时间
end_date = datetime.datetime(2019, 12, 31) # 回测结束时间
data = bt.feeds.PandasData(dataname=stock_hfq_df, fromdate=start_date, todate=end_date) # 加载数据
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# Set our desired cash start
cerebro.broker.setcash(100000.0)
# Set the commission - 0.1% ... divide by 100 to remove the %
# 按万一的佣金 ,买卖操作都要扣除
cerebro.broker.setcommission(commission=0.0001)
# Add a FixedSize sizer according to the stake
cerebro.addsizer(bt.sizers.FixedSize, stake=10)
# Print out the starting conditions
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
# Run over everything
cerebro.run()
# Print out the final result
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
cerebro.plot(iplot=False)
#cerebro.plot() # Javascript Error: IPython is not defined
绘图结果如下: