在整个框架中,策略是重中之重。策略是你交易的逻辑的展现,是你思维的展示。量化交易者要认真对待。
策略模块相比其他模块需要更多的代码编写,交易逻辑是自己的,无法代替。其他模块有既定的、成熟的方案,拿来主义就可,当然也可以根据自己的需求重新编写。
框架中已经有了Strategy类,编写新的策略继承这个类。然后根据自己的需要,重写某些函数。
backtrader模块的编写一般都在继承的继承上编写,backtrader作者为大家无私奉献,劳心劳力。
这一片有三个重点:一是从整体上了解整个Strategy,掌握内部的函数结构。二是交易思维如何落实到代码中,三是了解每个函数具体作用。
1. 策略类中内部函数
import backtrader as bt
class TestStrategy(bt.Strategy):
def __init__(self): # 必选项
pass
def start(self):
pass
def prenext(self):
pass
def nextstart(self):
pass
def next(self): # 必选项
pass
def stop(self):
pass
def notify_cashvalue(cash, value):
# 监听函数,监听cash,value的变化。
pass
def notify_trade(trade):
# 监听函数, 监听trade的变化
pass
2.生命周期函数讲解
2.1__init__函数
_init_ 初始化,和python中的类一样用法。在实例中调用,可以创建需要的变量。
在策略中,init有个重要的作用,那就是访问line对象。此对象是完整的对象。
在操作中经常对线对象或者含线对象进行操作,从而创造出新的线对象。
当然可以直接引用指标的含线对象
def __init__(self):
self.sma = bt.indicators.MovingAverageSimple(self.datas[0].lines.close, period=15)
self.dif = self.datas[0].close - self.sma
需要注意的是init方法是不能访问线的点。线的点在其他函数中则会用到。
在平均线中参数中,close线是默认的。self.datas[0].close 可以改写成 self.datas[0]
在前面 一些概念中介绍,含线对象需要是属于哪个数据时,第一个数据是默认值。那么self.sma可以简化成如下:
self.sma = bt.indicators.MovingAverageSimple(period=15)
2.2 next函数
2.2.1运行方式
line线对象或者含线对象是经过特殊处理的列表。是可以循环迭代的,next方法可以访问线中的点。self.datas[0].close[0]是今日收盘价,self.datas[0].close[-1]是昨日收盘价…
next是不能访问线的整体。
在最小周期为1的情况,next方法调用的次数跟数据量是一样,next方法不停的循环迭代。
如上图,假设这就是全部的交易数据,总共四行。next首先访问20050104日这一行bar,然后一次访问,直到最后20050107日bar结束。
注意索引[0]是当前next正在处理的bar行,代表当前。例如next循环到第二行,self.datas[0].close[0]指的是第二行的收盘价。这个索引是动态的。
不要搞混。这一行数据是一个bar,是概念上的。next还是要通过线对象来访问点的数据信息。
next函数迭代是在满足最小周期交易数据时,才能运行。
2.2.2时间访问
在前面介绍过,在使用多个数据对象时,第一个数据的时间将作为标准,所以第一个数据的时间是长度要足够长,要有连续性。在投资组合中,最好第一个数据使用指数数据,保证了时间线的稳定性,连续性。
存在两个时间对象,都可以进行访问:
- 策略时间对象 即self.datatime。当前时间点:self.datetime[0]
- 行情数据时间对象 即self.datas[0].datetime。 当前时间点:self.datas[0].datetime[0]
然而时间是float类型,使用时需要转化成python数据格式。
可以使用datetime包进行转换,backtrader已经内置其方法。对当前时间进行格式转变:
- 策略时间对象进行格式转化 self.datetime.datetime(0)
- 行情数据进行时间格式转化 self.datas[0].datetime.datetime(0)
注意这里是()而不是[]
在进行时间格式转化时,数据多的话是很消耗算力资源的,backtrader编写了一个函数num2date,来提高运算效率。
用法如下:
- 策略时间对象 bt.num2date(self.datetime[0])
- 数据行情时间对象 bt.num2date(self.datas[0].datetime[0])
数据行情时间线可以访问下个bar的时间点,如self.datas[0].datetime.datetime(1)。用策略时间self.datetime.datetime(1)则会出错。
在使用中,更多的使用的是数据行情的时间对象。
2.3其他声明周期函数
- start 函数是提示策略可以开始了,默认是空函数
- prenext 预告函数 调用了15日的移动平均线,刚开始处理的条数不足15,也就是前14条都会调用prenext函数。这个15日就是最小周期,在满足最小周期之前调用。
- nextstart函数,满足最小周期时调用,只调用一次。默认行为是调用next
- stop函数 停止函数在回测执结束后调用。默认的是空方法。
作者把几个周期函数比作人生命的某个阶段。
init怀孕------->start出生------>prenext童年------>next成年------->stop死亡
3.声明周期函数代码示例:
Strcycle.py
import pandas as pd
import backtrader as bt
from datetime import datetime
import sys, os
# 编写策略
class TestStrategy(bt.Strategy):
def log(self, txt, dt=None):
'''定义打印功能,被next函数调用'''
dt = dt or self.datas[0].datetime.date(0)
print(f'{dt.isoformat()} {txt}')
def start(self):
print('start是开始')
def __init__(self):
self.num_prenext = 0 # prenext的计数器
self.num_next = 0 # next的计数器
self.sma = bt.indicators.MovingAverageSimple(period=15)
print('init是第一个')
def prenext(self):
self.num_prenext += 1
self.log(f'{self.num_prenext}次运行prenext方法')
def nextstart(self):
self.log(f'在第{len(self.data.close)}个bar运行nextstart方法')
def next(self):
self.num_next += 1
self.log(f'这是第{self.num_next}次运行next方法')
def stop(self):
print('stop是结束')
print(f'一共有{round(len(self.data.close))}个交易数据') # len()已处理的数据量。
if __name__ == "__main__":
# 1.大脑实体化
cerebro = bt.Cerebro()
# 2.添加策略
cerebro.addstrategy(TestStrategy)
# 3.获取数据,添加数据
abspath = os.path.dirname(sys.argv[0]) # 获取当前程序所在的目录
pathfile = os.path.join(abspath,'./601318.csv') #获取数据文件
stock_name = pd.read_csv(pathfile, index_col='date', parse_dates=True)
start_date = datetime(2022, 6, 1)
end_date = datetime(2022, 6, 30)
data = bt.feeds.PandasData(dataname=stock_name, fromdate=start_date, todate=end_date)
cerebro.adddata(data)
cerebro.run()
这个代码的结构:
- 策略TestStrategy
- 数据 Data Feeds
- 将策略和数据加入到大脑实例Cerebro中
- 运行回测。
(有些知识还未学习,重点在于了解策略内部的运行机制)
程序运行结果如下:
init第一个
start是开始
2022-06-01 1次运行prenext方法
2022-06-02 2次运行prenext方法
2022-06-06 3次运行prenext方法
2022-06-07 4次运行prenext方法
2022-06-13 8次运行prenext方法
2022-06-14 9次运行prenext方法
2022-06-15 10次运行prenext方法
2022-06-16 11次运行prenext方法
2022-06-17 12次运行prenext方法
2022-06-20 13次运行prenext方法
2022-06-21 14次运行prenext方法
2022-06-22 在第15个bar运行nextstart方法
2022-06-23 ------------这是第1次运行next方法
2022-06-24 ------------这是第2次运行next方法
2022-06-27 ------------这是第3次运行next方法
2022-06-28 ------------这是第4次运行next方法
2022-06-29 ------------这是第5次运行next方法
2022-06-30 ------------这是第6次运行next方法
stop是结束
一共有21个交易数据
选取了一个06月的交易日。从运行结果来看,先是init,start方法,先14次运行prenext方法,然后在第15个bar时运行nextstart方法,紧接着都是运行next方法,最后运行stop方法。
可见策略生命周期与最小周期是高度相关。设置15日的最小周期,在满足15个bar的情况下,next方法才能被调用。