轻松入门
注意:
快速上手文档使用的数据文件会实时更新,修正收盘价以及收盘价会随之变化。这就意味着你尝试时的实际输出可能与文档结果不一样。
使用BackTrader
让我们在实际运行一系列实际案例(从空策略到成熟策略)前先解释两个使用backtrader需要用到的基本概念:
- 线
数据集、指标以及策略都有数据线
一条线是有一系列连续的点连接起来形成的。当我们谈论证券市场,其数据集通常是由每天产生的一系列点形成以下数据构成:
- 开盘价、最高价、最低价、收盘价、交易量以及未平仓量
开盘价数列就是随时间产生的一条线。因此上述数据集通常有6条线。
如果我们将“DateTime”(时间)纳入进来考虑,那么上述数据集就由7条线构成。
2. 0索引假设
当我们访问一条线里的数值时,当前值的位置始终是0;
之前的索引是-1。 在Python 中通常将索引值-1作为序列或者迭代器最末尾数据的索引值。
在我们的示例中这是能够获取的最后一个输出值。
同样,将索引0放置在-1的后边,就意味着要读取数据线中的当前值
按上述概念,假设我们构造一个带有简单移动平均特征的策略时,其初始化:
self.sma=SimpleMovingAverage(......)
读取当前值的最简单方法是:
av=self.sma[0]
在这里无需知道有多少多少个K线、多少分钟、多少天、多少月这些数值。因为“0”就是被用于识别当前实例。
依照python管理,前一个值可以通过-1获取:
previous_value=self.sma[-1]
同理,通过-2,-3获取更早的数字。
0到100:示例
简单启动
运行:
from__future__import(absolute_import,division,print_function,unicode_literals)
importbacktraderasbt
if__name__=='__main__':
cerebro=bt.Cerebro()
print('初始账户额度:%.2f'%cerebro.broker.getvalue())
cerebro.run()
print('最终账户值:%.2f'%cerebro.broker.getvalue())
执行结果为:
初始账户额度:10000.00
最终账户值:10000.00
在这个示例中:
- 引入backtrader
- 实例化Cerebro
- 运行cerebro实例(遍历数据)
- 打印cerebro实例执行结果
虽然这个示例看起来并不复杂,但它说明了几个事情:
- Cerebro引擎在后台创建了一个券商实例
- cerebro会赋予一些启动资金。
券商实例是为了简化用户使用的默认设定。如果用户没有指定券商,backtrader会默认生成一个。
1万货币单位通常是部分券商最低的账户额度。
设定资金
在金融世界,毫无疑问只有“穷人”才会只有1万的启动资金。我们可以设定启动资金:
from__future__import(absolute_import,division,print_function,unicode_literals)
importbacktraderasbt
if__name__=='__main__':
cerebro=bt.Cerebro()
cerebro.broker.setcash(100000.0)
print('初始账户额度:%.2f'%cerebro.broker.getvalue())
cerebro.run()
print('最终账户值:%.2f'%cerebro.broker.getvalue())
执行结果为:
初始账户额度:100000.00
最终账户值:100000.00
任务完成!让我们继续往深水区进发。
添加数据集
拥有资金是快乐的。但我们做这些的目的是使用自动化的策略自动操作资产实现不动根手指使资金快速增长。
额,无数据->无快乐。 让我们为之前的资金添加数据
from__future__import(absolute_import,division,print_function,unicode_literals)
importdatetime
importos.path
importsys
importbacktraderasbt
if__name__=='__main__':
#创建一个cerebro实例
cerebro=bt.Cerebro()
#数据被存放在示例的子文件夹中。需要确认源代码位置
#因为它可能在任何地方被调用
modpath=os.path.dirname(os.path.abspath(sys.argv[0]))
datapath=os.path.join(modpath,'datas/orcl-1995-2014.txt')#数据集需要通过github额外*载下**(见源码安装部分),*载下**后按程序代码和数据集的相对位置设定路径
#创建数据集
data=bt.feeds.YahooFinanceCSVData(dataname=datapath,
#数据的起始时间
fromdate=datetime.datetime(2000,1,1),
todate=datetime.datetime(2000,12,31),
reversed=False)
cerebro.adddata(data)
cerebro.broker.setcash(100000.0)
print('初始账户额度:%.2f'%cerebro.broker.getvalue())
cerebro.run()
print('最终账户值:%.2f'%cerebro.broker.getvalue())
执行结果为:
初始账户额度:100000.00
最终账户值:100000.00
示例逐渐变长了,因为我们添加了:
- 查找代码位置以便于我们加载本地数据文件
- 使用datetime对象从数据当中筛选数据
通过上述操作 ,我们创建了数据集并添加到了cerebro
输出结果还没有发生变化
说明:
Yahoo发生的CSV数据是按日期降序排序的。这不符合标准惯例。 因为考虑到CSV数据已经被逆序了,所以使用reversed = True使数据变成按日期i升序。
第一个策略
资金已经存入券商,数据集也已经准备好。这次冒险看起来已经蠢蠢欲动了。
让我们创建一个策略,打印每天的收盘价
DataSeries对象(数据集的基础类)有别于我们常说的日K线数据。它能够简化我们的打印程序逻辑。
from__future__import(absolute_import,division,print_function,unicode_literals)
importdatetime
importos.path
importsys
importbacktraderasbt
#创建策略
classTestStrategy(bt.Strategy):
deflog(self,txt,dt=None):
'策略的日志函数'
dt=dtorself.datas[0].datetime.date(0)#返回指定日期或当前日期
print('%s,%s'%(dt.isoformat(),txt))
def__init__(self):
#将data[0]数据集中的收盘价线保留一份参照序列
self.dataclose=self.datas[0].close
defnext(self):
#简单记录参照序列中的收盘价
self.log('收盘价,%.2f'%self.dataclose[0])
if__name__=='__main__':
#创建一个cerebro实例
cerebro=bt.Cerebro()
#数据被存放在示例的子文件夹中。需要确认源代码位置
#因为它可能在任何地方被调用
#添加策略
cerebro.addstrategy(TestStrategy)
modpath=os.path.dirname(os.path.abspath(sys.argv[0]))
datapath=os.path.join(modpath,'datas/orcl-1995-2014.txt')#数据集需要通过github额外*载下**(见源码安装部分),*载下**后按程序代码和数据集的相对位置设定路径
#创建数据集
data=bt.feeds.YahooFinanceCSVData(dataname=datapath,
#数据的起始时间
fromdate=datetime.datetime(2000,1,1),
todate=datetime.datetime(2000,12,31),
reversed=False)
cerebro.adddata(data)
cerebro.broker.setcash(100000.0)
print('初始账户额度:%.2f'%cerebro.broker.getvalue())
cerebro.run()
print('最终账户值:%.2f'%cerebro.broker.getvalue())
执行结果:
初始账户额度:100000.00
2000-01-03,收盘价,26.27
2000-01-04,收盘价,23.95
2000-01-05,收盘价,22.68
......
......
......
2000-12-27,收盘价,27.30
2000-12-28,收盘价,27.63
2000-12-29,收盘价,25.85
最终账户值:100000.00
有人说证券市场就风险很高,但看起来好像不是这样。
让我们对上述代码作一些说明:
- 通过初始化(init)调用,策略拥有了平台提供的数据列表
这个数据列表是标准的python序列,可以按照插入顺序进行方案。
序列self.datas[0]的第一个数据是交易操作使用的默认数据,用以保证所有策略元素同步(即系统时钟)
- self.dataclose = self.datas[0].close 实现对收盘价线的引用。 后续只需要通过一层的间接引用就可以访问收盘价数值
- 策略的next方法将会被系统时钟的每个交易周期数据所调用(self.data[0]。这都是成立的,除非有其他类似“指标”对象参与进来,“指标”需要使用交易周期数据来产生输出。稍后详述)
为策略添加逻辑
让我们尝试一些疯狂的想法
- 如果价格连续三期下降,我们就买买买
from__future__import(absolute_import,division,print_function,unicode_literals)
importdatetime
importos.path
importsys
importbacktraderasbt
#创建策略
classTestStrategy(bt.Strategy):
deflog(self,txt,dt=None):
'策略的日志函数'
dt=dtorself.datas[0].datetime.date(0)#返回指定日期或当前日期
print('%s,%s'%(dt.isoformat(),txt))
def__init__(self):
#将data[0]数据集中的收盘价线保留一份参照序列
self.dataclose=self.datas[0].close
defnext(self):
#简单记录参照序列中的收盘价
self.log('收盘价,%.2f'%self.dataclose[0])
ifself.dataclose[0]<self.dataclose[-1]:
#当期收盘价低于前一期收盘价
ifself.dataclose[-1]<self.dataclose[-2]:
#前一日收盘价又低于前2日收盘价
self.log('买入价,%.2f'%self.dataclose[0])
self.buy()
if__name__=='__main__':
#创建一个cerebro实例
cerebro=bt.Cerebro()
#数据被存放在示例的子文件夹中。需要确认源代码位置
#因为它可能在任何地方被调用
#添加策略
cerebro.addstrategy(TestStrategy)
modpath=os.path.dirname(os.path.abspath(sys.argv[0]))
datapath=os.path.join(modpath,'datas/orcl-1995-2014.txt')#数据集需要通过github额外*载下**(见源码安装部分),*载下**后按程序代码和数据集的相对位置设定路径
#创建数据集
data=bt.feeds.YahooFinanceCSVData(dataname=datapath,
#数据的起始时间
fromdate=datetime.datetime(2000,1,1),
todate=datetime.datetime(2000,12,31),
reversed=False)
cerebro.adddata(data)
cerebro.broker.setcash(100000.0)
print('初始账户额度:%.2f'%cerebro.broker.getvalue())
cerebro.run()
print('最终账户值:%.2f'%cerebro.broker.getvalue())
执行结果如下:
初始账户额度:100000.00
2000-01-03,收盘价,26.27
2000-01-04,收盘价,23.95
2000-01-05,收盘价,22.68
2000-01-05,买入价,22.68
...
...
...
2000-12-27,买入价,27.30
2000-12-28,收盘价,27.63
2000-12-29,收盘价,25.85
最终账户值:99740.45
我们执行了几次买入交易,我们的账户余额也缩减了。显而易见,有些重要的事情被忽略掉了:
- 交易被创建了,但并不知道它是否被执行、什么时候以何种价格被执行。
在下一个示例中,我们将监听交易执行的结果。
好奇的读者可能会问:买入那种证券、买入多少以及如何执行?在可能的情况下backtrader会弥补这些缺失:
- 未特别声明,self.datas[0](众所周知的系统时钟里的主数据)就是目标资产。
- 后台提供的默认每股交易量(sizer)为1,后期我们通过其他示例来修改这个数值。
- 指令在“市场”内被执行。即由券商使用下一个交易周期的开盘价执行买入。因为这是当前测试交易周期后的第一桩交易。
- 目前交易的执行仍未设计交易佣金(后续详述)
不要只买...还要卖
在知道如何入市以后,还需要知道“退出概念”并且了解策略是否符合市场规律。
- 幸运的是每个策略对象都为默认数据集支持查询交易仓位(position)属性
- 买入和卖出方法可以返回创建的交易(尚未执行)
- 交易状态的改变将通过“通知”(notify)方法进行通知。
“退出概念”会比较容易实现:
- 连续五个交易周期上涨或者下跌就退出
请注意,这里并没有使用时间或者时间表的概念:交易周期的数量。交易周期可以代表1分钟、1小时、1天、1周或者其他时间周期。
另外,为了简化问题:
- 在没有进入市场(未持有证券)以前只能够进行买入交易
next方法并没有传递交易周期索引,因此看起来很难理解怎么判断有没有渡过5个交易周期。但这个可以通过python方法来实现: 通过调用len()的方法来判断对数据线对象的长度。只需要写下在什么样的长度执行操作并确认是否当前数据线的长度是否为5即可。
from__future__import(absolute_import,division,print_function,unicode_literals)
importdatetime
importos.path
importsys
importbacktraderasbt
#创建策略
classTestStrategy(bt.Strategy):
deflog(self,txt,dt=None):
'策略的日志函数'
dt=dtorself.datas[0].datetime.date(0)#返回指定日期或当前日期
print('%s,%s'%(dt.isoformat(),txt))
def__init__(self):
#将data[0]数据集中的收盘价线保留一份参照序列
self.dataclose=self.datas[0].close
#根据交易挂单情况:
self.order=None
defnotify_order(self,order):
iforder.statusin[order.Submitted,order.Accepted]:
#如果交易已提交或者被接收,则什么都不用作
return
#检查交易是否已完成,注意,如果资金不够交易会被退回
iforder.statusin[order.Completed]:
iforder.isbuy():
self.log('买入已完成,买入价格:%.2f'%order*ex.e**cuted.price)
eliforder.issell():
self.log('卖出已完成,卖出价格:%.2f'%order*ex.e**cuted.price)
self.bar_executed=len(self)
eliforder.statusin[order.Canceled,order.Margin,order.Rejected]:
self.log('交易被取消/要求保证金/退回')
self.order=None
defnext(self):
#简单记录参照序列中的收盘价
self.log('收盘价,%.2f'%self.dataclose[0])
#检查是否由交易挂单(未完成),如果有则不能进行第二次交易。
ifself.order:
return
#检查是否持有证券
ifnotself.position:
#如果没有仓位,则需要买入
ifself.dataclose[0]<self.dataclose[-1]:
#当期收盘价低于前一期收盘价
ifself.dataclose[-1]<self.dataclose[-2]:
#前一日收盘价又低于前2日收盘价
self.log('买入价,%.2f'%self.dataclose[0])
#记录买入交易避免二次挂单
self.order=self.buy()
else:
#如果已持仓则可以卖出:
iflen(self)>=(self.bar_executed+5):
#不管情况如何持有5个周期后都卖出
self.log('卖出价,%.2f'%self.dataclose[0])
#记录卖出交易避免二次挂单
self.order=self.sell()
if__name__=='__main__':
#创建一个cerebro实例
cerebro=bt.Cerebro()
#数据被存放在示例的子文件夹中。需要确认源代码位置
#因为它可能在任何地方被调用
#添加策略
cerebro.addstrategy(TestStrategy)
modpath=os.path.dirname(os.path.abspath(sys.argv[0]))
datapath=os.path.join(modpath,'datas/orcl-1995-2014.txt')#数据集需要通过github额外*载下**(见源码安装部分),*载下**后按程序代码和数据集的相对位置设定路径
#创建数据集
data=bt.feeds.YahooFinanceCSVData(dataname=datapath,
#数据的起始时间
fromdate=datetime.datetime(2000,1,1),
todate=datetime.datetime(2000,12,31),
reversed=False)
cerebro.adddata(data)
cerebro.broker.setcash(100000.0)
print('初始账户额度:%.2f'%cerebro.broker.getvalue())
cerebro.run()
print('最终账户值:%.2f'%cerebro.broker.getvalue())
执行结果如下:
初始账户额度:100000.00
2000-01-03,收盘价,26.27
2000-01-04,收盘价,23.95
2000-01-05,收盘价,22.68
2000-01-05,买入价,22.68
2000-01-06,买入已完成,买入价格:22.27
2000-01-06,收盘价,21.35
2000-01-07,收盘价,22.99
...
...
2000-12-20,买入价,25.35
2000-12-21,买入已完成,买入价格:24.74
2000-12-21,收盘价,26.24
2000-12-22,收盘价,28.35
2000-12-26,收盘价,27.52
2000-12-27,收盘价,27.30
2000-12-28,收盘价,27.63
2000-12-29,收盘价,25.85
2000-12-29,卖出价,25.85
最终账户值:100017.52
怎么回事,系统居然赚钱了...一定是哪里出错了!
券商说:还没给我钱呢
这个费用就是佣金
我们为每一次交易都增加一个合乎逻辑的交易费率0.1%(无论是买入还是卖出)
使用一行代码来实现
cerebro.broker.setcommission(commission=0.001)
在有佣金和没有佣金的情况下,通过平台我们就可以明确买入和卖出究竟是挣钱还是亏损。
from__future__import(absolute_import,division,print_function,unicode_literals)
importdatetime
importos.path
importsys
importbacktraderasbt
frombacktrader.comminfoimportCommissionInfo
#创建策略
classTestStrategy(bt.Strategy):
deflog(self,txt,dt=None):
'策略的日志函数'
dt=dtorself.datas[0].datetime.date(0)#返回指定日期或当前日期
print('%s,%s'%(dt.isoformat(),txt))
def__init__(self):
#将data[0]数据集中的收盘价线保留一份参照序列
self.dataclose=self.datas[0].close
#根据交易挂单情况:
self.order=None
defnotify_order(self,order):
iforder.statusin[order.Submitted,order.Accepted]:
#如果交易已提交或者被接收,则什么都不用作
return
#检查交易是否已完成,注意,如果资金不够交易会被退回
iforder.statusin[order.Completed]:
iforder.isbuy():
self.log('买入已完成,价格:%.2f,交易额:%.2f,佣金:%.2f'%(order*ex.e**cuted.price,order*ex.e**cuted.value,order*ex.e**cuted.comm))
eliforder.issell():
self.log('卖出已完成,价格:%.2f,交易额:%.2f,佣金:%.2f'%(order*ex.e**cuted.price,order*ex.e**cuted.value,order*ex.e**cuted.comm))
self.bar_executed=len(self)
eliforder.statusin[order.Canceled,order.Margin,order.Rejected]:
self.log('交易被取消/要求保证金/退回')
self.order=None
defnext(self):
#简单记录参照序列中的收盘价
self.log('收盘价,%.2f'%self.dataclose[0])
#检查是否由交易挂单(未完成),如果有则不能进行第二次交易。
ifself.order:
return
#检查是否持有证券
ifnotself.position:
#如果没有仓位,则需要买入
ifself.dataclose[0]<self.dataclose[-1]:
#当期收盘价低于前一期收盘价
ifself.dataclose[-1]<self.dataclose[-2]:
#前一日收盘价又低于前2日收盘价
self.log('买入价,%.2f'%self.dataclose[0])
#记录买入交易避免二次挂单
self.order=self.buy()
else:
#如果已持仓则可以卖出:
iflen(self)>=(self.bar_executed+5):
#不管情况如何持有5个周期后都卖出
self.log('卖出价,%.2f'%self.dataclose[0])
#记录卖出交易避免二次挂单
self.order=self.sell()
if__name__=='__main__':
#创建一个cerebro实例
cerebro=bt.Cerebro()
#数据被存放在示例的子文件夹中。需要确认源代码位置
#因为它可能在任何地方被调用
#添加策略
cerebro.addstrategy(TestStrategy)
modpath=os.path.dirname(os.path.abspath(sys.argv[0]))
datapath=os.path.join(modpath,'datas/orcl-1995-2014.txt')#数据集需要通过github额外*载下**(见源码安装部分),*载下**后按程序代码和数据集的相对位置设定路径
#创建数据集
data=bt.feeds.YahooFinanceCSVData(dataname=datapath,
#数据的起始时间
fromdate=datetime.datetime(2000,1,1),
todate=datetime.datetime(2000,12,31),
reversed=False)
cerebro.adddata(data)
cerebro.broker.setcash(100000.0)
#设置佣金比例为0.1%
cerebro.broker.setcommission(commission=0.001)
print('初始账户额度:%.2f'%cerebro.broker.getvalue())
cerebro.run()
print('最终账户值:%.2f'%cerebro.broker.getvalue())
执行结果如下:
初始账户额度:100000.00
2000-01-03,收盘价,26.27
2000-01-04,收盘价,23.95
2000-01-05,收盘价,22.68
2000-01-05,买入价,22.68
2000-01-06,买入已完成,价格:22.27,交易额:22.27,佣金:0.02
2000-01-06,收盘价,21.35
...
...
2000-12-20,买入价,25.35
2000-12-21,买入已完成,价格:24.74,交易额:24.74,佣金:0.02
2000-12-21,收盘价,26.24
2000-12-22,收盘价,28.35
2000-12-26,收盘价,27.52
2000-12-27,收盘价,27.30
2000-12-28,收盘价,27.63
2000-12-29,收盘价,25.85
2000-12-29,卖出价,25.85
最终账户值:100016.06
结果依旧是盈利的。
停一下,让我们关注一下“运营利润”数据线:
2000-01-14T00:00:00,OPERATIONPROFIT,GROSS2.09,NET2.04
2000-02-07T00:00:00,OPERATIONPROFIT,GROSS3.68,NET3.63
2000-02-28T00:00:00,OPERATIONPROFIT,GROSS4.48,NET4.42
2000-03-13T00:00:00,OPERATIONPROFIT,GROSS3.48,NET3.41
2000-03-22T00:00:00,OPERATIONPROFIT,GROSS-0.41,NET-0.49
2000-04-07T00:00:00,OPERATIONPROFIT,GROSS2.45,NET2.37
2000-04-20T00:00:00,OPERATIONPROFIT,GROSS-1.95,NET-2.02
2000-05-02T00:00:00,OPERATIONPROFIT,GROSS5.46,NET5.39
2000-05-11T00:00:00,OPERATIONPROFIT,GROSS-3.74,NET-3.81
2000-05-30T00:00:00,OPERATIONPROFIT,GROSS-1.46,NET-1.53
2000-07-05T00:00:00,OPERATIONPROFIT,GROSS-1.62,NET-1.69
2000-07-14T00:00:00,OPERATIONPROFIT,GROSS2.08,NET2.01
2000-07-28T00:00:00,OPERATIONPROFIT,GROSS0.14,NET0.07
2000-08-08T00:00:00,OPERATIONPROFIT,GROSS4.36,NET4.29
2000-08-21T00:00:00,OPERATIONPROFIT,GROSS1.03,NET0.95
2000-09-15T00:00:00,OPERATIONPROFIT,GROSS-4.26,NET-4.34
2000-09-27T00:00:00,OPERATIONPROFIT,GROSS1.29,NET1.22
2000-10-13T00:00:00,OPERATIONPROFIT,GROSS-2.98,NET-3.04
2000-10-26T00:00:00,OPERATIONPROFIT,GROSS3.01,NET2.95
2000-11-06T00:00:00,OPERATIONPROFIT,GROSS-3.59,NET-3.65
2000-11-16T00:00:00,OPERATIONPROFIT,GROSS1.28,NET1.23
2000-12-01T00:00:00,OPERATIONPROFIT,GROSS2.59,NET2.54
2000-12-18T00:00:00,OPERATIONPROFIT,GROSS-0.06,NET-0.12
将净收益加总最终结果是:
14.96
但是程序告诉我们的结果是:
2000-12-29,卖出价,25.85
最终账户值:100016.06
显然,14.96与16.06并不相等。但这并不是错误,净收益15.83是进入口袋的现金收益。
之所以出现这种情况,是因为在数据集的最后一天仍然持有证券。尽管最后发出了卖出指令,但指令并没有被执行。
“最终账户值”计算了最后一个交易日的收盘价。而真实的交易价格应该是下一个交易日(2001-01-02)的开盘价格。将数据集扩展到这一天,其输出结果为:
2001-01-02,卖出已完成,价格:26.30,交易额:24.74,佣金:0.03
2001-01-02,账户收益:毛收益1.56,净收益1.51
2001-01-02,收盘价,23.46
2001-01-02,买入价,23.46
2001-01-03,买入已完成,价格:22.46,交易额:22.46,佣金:0.02
2001-01-03,收盘价,28.46
最终账户值:100022.22
此时,再将前期的净收益和完成交易后的收益进行加总
14.96+1.51=16.47
这就是在10000启动资金的基础上产生的额外收益(加总收益与程序运行净收益的差异源于四舍五入)
自定义策略:参数
在策略中把一些数值写死不支持修改是不显示的。参数就是用了处理这些问题。
参数可以按以下方式定义:
params=(('myparams':27),('exitbars':5),)
这实际上就是一个标准的嵌套元组,下边的书写方式更容易看懂:
params=(
('myparam',27),
('exitbars',5),
)
无论哪种格式化的参数允许在添加策略的时候进行设定:
#添加策略
cerebro.addstrategy(TestStrategy,myparam=20,exitbars=7)
下边这种setsizeing方法已经不再支持。保留这部分内容的目的是为了那些使用旧示例代码的用户查询。这些代码应按如下修正使用:
cerebro.addsizer(bt.sizers.FixedSize,stake=10)
详细解释请阅读sizer部分内容
在策略里使用参数非常监督。他们都被存在“参数”属性里。比如,我们想设置固定的交易股数,我们可以通过将stake参数传递给position sizer:
#通过参数设置交易股数
self.sizer.setsizing(self.params.stake)
我们可以使用stake参数和self.params.stake值作为买入或卖出的交易量
退出的逻辑应该如下修改:
len(self)>=(self.bar_executed+self.params.exitbars)
整合上述修改以后的代码如下:
from__future__import(absolute_import,division,print_function,unicode_literals)
importdatetime
importos.path
importsys
importbacktraderasbt
frombacktrader.comminfoimportCommissionInfo
#创建策略
classTestStrategy(bt.Strategy):
params=(
('exitbars',5),
)
deflog(self,txt,dt=None):
'策略的日志函数'
dt=dtorself.datas[0].datetime.date(0)#返回指定日期或当前日期
print('%s,%s'%(dt.isoformat(),txt))
def__init__(self):
#将data[0]数据集中的收盘价线保留一份参照序列
self.dataclose=self.datas[0].close
#根据交易挂单情况:
self.order=None
self.buyprice=None
self.buycomm=None
defnotify_order(self,order):
iforder.statusin[order.Submitted,order.Accepted]:
#如果交易已提交或者被接收,则什么都不用作
return
#检查交易是否已完成,注意,如果资金不够交易会被退回
iforder.statusin[order.Completed]:
iforder.isbuy():
self.log('买入已完成,价格:%.2f,交易额:%.2f,佣金:%.2f'%(order*ex.e**cuted.price,order*ex.e**cuted.value,order*ex.e**cuted.comm))
self.buyprice=order*ex.e**cuted.price
self.buycomm=order*ex.e**cuted.comm
eliforder.issell():
self.log('卖出已完成,价格:%.2f,交易额:%.2f,佣金:%.2f'%(order*ex.e**cuted.price,order*ex.e**cuted.value,order*ex.e**cuted.comm))
self.bar_executed=len(self)
eliforder.statusin[order.Canceled,order.Margin,order.Rejected]:
self.log('交易被取消/要求保证金/退回')
self.order=None
defnotify_trade(self,trade):
ifnottrade.isclosed:
return
self.log('账户收益:毛收益%.2f,净收益%.2f'%(trade.pnl,trade.pnlcomm))
defnext(self):
#简单记录参照序列中的收盘价
self.log('收盘价,%.2f'%self.dataclose[0])
#检查是否由交易挂单(未完成),如果有则不能进行第二次交易。
ifself.order:
return
#检查是否持有证券
ifnotself.position:
#如果没有仓位,则需要买入
ifself.dataclose[0]<self.dataclose[-1]:
#当期收盘价低于前一期收盘价
ifself.dataclose[-1]<self.dataclose[-2]:
#前一日收盘价又低于前2日收盘价
self.log('买入价,%.2f'%self.dataclose[0])
#记录买入交易避免二次挂单
self.order=self.buy()
else:
#如果已持仓则可以卖出:
iflen(self)>=(self.bar_executed+self.params.exitbars):
#不管情况如何持有5个周期后都卖出
self.log('卖出价,%.2f'%self.dataclose[0])
#记录卖出交易避免二次挂单
self.order=self.sell()
if__name__=='__main__':
#创建一个cerebro实例
cerebro=bt.Cerebro()
#数据被存放在示例的子文件夹中。需要确认源代码位置
#因为它可能在任何地方被调用
#添加策略
cerebro.addstrategy(TestStrategy)
modpath=os.path.dirname(os.path.abspath(sys.argv[0]))
datapath=os.path.join(modpath,'datas/orcl-1995-2014.txt')#数据集需要通过github额外*载下**(见源码安装部分),*载下**后按程序代码和数据集的相对位置设定路径
#创建数据集
data=bt.feeds.YahooFinanceCSVData(dataname=datapath,
#数据的起始时间
fromdate=datetime.datetime(2000,1,1),
todate=datetime.datetime(2000,12,31),
reversed=False)
cerebro.adddata(data)
cerebro.broker.setcash(100000.0)
#添加每次交易股数
cerebro.addsizer(bt.sizers.FixedSize,stake=10)
#设置佣金比例为0.1%
cerebro.broker.setcommission(commission=0.001)
print('初始账户额度:%.2f'%cerebro.broker.getvalue())
cerebro.run()
print('最终账户值:%.2f'%cerebro.broker.getvalue())
执行结果如下:
初始账户额度:100000.00
2000-01-03,收盘价,26.27
2000-01-04,收盘价,23.95
2000-01-05,收盘价,22.68
2000-01-05,买入价,22.68
2000-01-06,买入已完成,价格:22.27,交易额:222.70,佣金:0.22
....
....
2000-12-21,买入已完成,价格:24.74,交易额:247.40,佣金:0.25
2000-12-21,收盘价,26.24
2000-12-22,收盘价,28.35
2000-12-26,收盘价,27.52
2000-12-27,收盘价,27.30
2000-12-28,收盘价,27.63
2000-12-29,收盘价,25.85
2000-12-29,卖出价,25.85
最终账户值:100160.58
为了看出差异,两次示例使用的时间段保持一致。
将交易股数乘以10,很明显,其产生的收益和损失也乘以10。原来结果是16.06,现在变成了160.58
添加指标
提到指标,每个人都想将其添加到策略里。当然,他们的表现必须好于“3连降”策略。
根据PyAIgoTrade示例中使用简单易懂平均模型的思想:
- 如果收盘价大于均线,就买入;
- 如果收盘价低于均线就卖出
- 每次只允许1次有效操作(即只能买入或者卖出)
大部分现有代码都将保持不变,我们需要将均值引入到init当中
self.sma=bt.indicators.MovingAverageSimple(self.datas[0],period=self.params.maperiod)
当然,买入或卖出的逻辑也将基于均值,代码如下:
注意:PyAIgoTrade模型启动资金是10000个货币单位,佣金是0.
from__future__import(absolute_import,division,print_function,unicode_literals)
importdatetime
importos.path
importsys
importbacktraderasbt
frombacktrader.comminfoimportCommissionInfo
importakshareasak
#创建策略
classTestStrategy(bt.Strategy):
params=(
('maperiod',15),
)
deflog(self,txt,dt=None):
'策略的日志函数'
dt=dtorself.datas[0].datetime.date(0)#返回指定日期或当前日期
print('%s,%s'%(dt.isoformat(),txt))
def__init__(self):
#将data[0]数据集中的收盘价线保留一份参照序列
self.dataclose=self.datas[0].close
#根据交易挂单情况:
self.order=None
self.buyprice=None
self.buycomm=None
#添加移动平均指标
self.sma=bt.indicators.MovingAverageSimple(period=self.params.maperiod)
defnotify_order(self,order):
iforder.statusin[order.Submitted,order.Accepted]:
#如果交易已提交或者被接收,则什么都不用作
return
#检查交易是否已完成,注意,如果资金不够交易会被退回
iforder.statusin[order.Completed]:
iforder.isbuy():
self.log('买入已完成,价格:%.2f,交易额:%.2f,佣金:%.2f'%(order*ex.e**cuted.price,order*ex.e**cuted.value,order*ex.e**cuted.comm))
self.buyprice=order*ex.e**cuted.price
self.buycomm=order*ex.e**cuted.comm
eliforder.issell():
self.log('卖出已完成,价格:%.2f,交易额:%.2f,佣金:%.2f'%(order*ex.e**cuted.price,order*ex.e**cuted.value,order*ex.e**cuted.comm))
self.bar_executed=len(self)
eliforder.statusin[order.Canceled,order.Margin,order.Rejected]:
self.log('交易被取消/要求保证金/退回')
self.order=None
defnotify_trade(self,trade):
ifnottrade.isclosed:
return
self.log('账户收益:毛收益%.2f,净收益%.2f'%(trade.pnl,trade.pnlcomm))
defnext(self):
#简单记录参照序列中的收盘价
self.log('收盘价,%.2f'%self.dataclose[0])
#检查是否由交易挂单(未完成),如果有则不能进行第二次交易。
ifself.order:
return
#检查是否持有证券
ifnotself.position:
#如果没有仓位,则需要买入
ifself.dataclose[0]>self.sma[0]:
self.log('买入价,%.2f'%self.dataclose[0])
#记录买入交易避免二次挂单
self.order=self.buy()
else:
#如果已持仓则可以卖出:
ifself.dataclose[0]<self.sma[0]:
#不管情况如何持有5个周期后都卖出
self.log('卖出价,%.2f'%self.dataclose[0])
#记录卖出交易避免二次挂单
self.order=self.sell()
if__name__=='__main__':
#创建一个cerebro实例
cerebro=bt.Cerebro()
#数据被存放在示例的子文件夹中。需要确认源代码位置
#因为它可能在任何地方被调用
#添加策略
cerebro.addstrategy(TestStrategy)
modpath=os.path.dirname(os.path.abspath(sys.argv[0]))
datapath=os.path.join(modpath,'datas/orcl-1995-2014.txt')#数据集需要通过github额外*载下**(见源码安装部分),*载下**后按程序代码和数据集的相对位置设定路径
#创建数据集
data=bt.feeds.YahooFinanceCSVData(dataname=datapath,
#数据的起始时间
fromdate=datetime.datetime(2000,1,1),
todate=datetime.datetime(2000,12,31),
reversed=False)
cerebro.adddata(data)
cerebro.broker.setcash(10000.0)
#添加每次交易股数
cerebro.addsizer(bt.sizers.FixedSize,stake=10)
#设置佣金比例为0.1%
cerebro.broker.setcommission(commission=0)
print('初始账户额度:%.2f'%cerebro.broker.getvalue())
cerebro.run()
print('最终账户值:%.2f'%cerebro.broker.getvalue())
在继续下一步前,我们要注意特别注意输出起始日期的变化:
- 输出的起始日期不再是2000-01-03(2000年第一个交易日),而是2000-01-24。这是怎么回事?
这些日期并不是凭空消失了。这是因为backrader的运行机制发生了改变。
- 我们在策略当中增加了指标(如简单移动平均)
- 通常指标需要X个交易周期的数据来输出结果:如15天;
- 2000-1-24正是第15个交易周期。
Backrader假设在交易策略中使用指标能够帮助我们更好地进行决策。在指标没有准备好且产生数据之前就进行决策是毫无意义的。
- next方法在所有指标达到能够产生数值的最小周期时才第一次被调用。
- 在示例当中,虽然只有一个指标,但策略可能生成任意多个指标值。
上述示例执行结果如下:
初始账户额度:10000.00
2000-01-24,收盘价,24.10
2000-01-25,收盘价,25.10
2000-01-25,买入价,25.10
2000-01-26,买入已完成,价格:25.24,交易额:252.40,佣金:0.00
2000-01-26,收盘价,24.49
2000-01-27,收盘价,23.04
2000-01-27,卖出价,23.04
2000-01-28,卖出已完成,价格:22.90,交易额:252.40,佣金:0.00
2000-01-28,账户收益:毛收益-23.40,净收益-23.40
......
......
......
2000-12-22,买入已完成,价格:27.02,交易额:270.20,佣金:0.00
2000-12-22,收盘价,28.35
2000-12-26,收盘价,27.52
2000-12-27,收盘价,27.30
2000-12-28,收盘价,27.63
2000-12-29,收盘价,25.85
2000-12-29,卖出价,25.85
最终账户值:9975.60
OMG! 原本赚钱的模型出现了亏损,而且这还是在没有手续费的情况下。看来单纯地增加一个指标并不是包治百病的灵丹妙药。
注意:
在同样的逻辑和数据前提下使用PyAIgo Trade模型可能产生不同的结果(略微差异)。仔细对比整个输出就能发现某些交易操作过程并不完全相同。一般认为造成这种情况的罪魁祸首是:四舍五入。
当使用除权的调整收盘价作为数据时PyAIgoTrade不会对数据进行四舍五入。
在使用经调整的收盘价时backtrader提供的Yahoo数据集经过了向下四舍五入保留2位小数。虽然这些数值打印出来时看起来一样,但显然在某些时候第五位小数存在的差异对结果影响很大。
向下四舍五入保留2为小数似乎更实际,因为证券市场只允许每单位资产保留一定数为小数(股票通常是2为小数)
注意:
从1.8.11.99版本开始,Yahoo数据集允许配置是否保留小数以及保留多少位小数。