BackTrader对一些状态改变的通知是以回调的方式实现的,需要重写对回调函数的实现。目前支持以下通知:
- notify_order(order):每次订单状态改变会触发回调
- notify_trade(trade):任何开仓/更新/平仓交易的通知
- notify_cashvalue(cash, value) :通知当前现金和投资组合
- notify_store(msg, *args, **kwargs):关于存储的通知
- notify_data(self, data, status, *args, **kwargs)::关于数据的通知
- notify_timer(self, timer, when, *args, **kwargs):定时器通知,定时器可以通过成员函数add_timer()添加
订单状态值,及流程如下:
- Order.Created 创建
- Order.Submitted 提交给broker
- Order.Accepted broker已接收
- Order.Partial 订单部分被执行 order.executed查看订单
- Order.Complete 订单已完成平均价格
- Order.Rejected 被broker拒绝
- Order.Margin 保证金不足\没有足够的现金执行订单。
- Order.Cancelled 用户取消
- Order.Expired 过期
order.size, order.price, order.pricelimit, order.ExecTypes[order.exectype], order.tradeid, order.Status[order.status]
order.Status订单的状态有:注:查阅订单为:order.Status[order.status] # Submitted
[‘Created’, ‘Submitted’, ‘Accepted’, ‘Partial’, ‘Completed’, ‘Canceled’, ‘Expired’, ‘Margin’, ‘Rejected’]
order.ExecTypes 的状态有:注:查阅为:order.ExecTypes[order.exectype]
[‘Market’, ‘Close’, ‘Limit’, ‘Stop’, ‘StopLimit’, ‘StopTrail’, ‘StopTrailLimit’, ‘Historical’]
trade.size, trade.price, trade.value, trade.tradeid, trade.status_names[trade.status]
trade.status_names状态有:注:查阅订单为:trade.status_names[trade.status] # Open
[‘Created’, ‘Open’, ‘Closed’]
self.order 状态有:
Ref: 25
OrdType: 0
OrdType: Buy
Status: 2
Status: Accepted
Size: 9282
Price: 12.1671
Price Limit: None
TrailAmount: None
TrailPercent: None
ExecType: 2
ExecType: Limit
CommInfo: None
End of Session: 737374.9999999999
Info: AutoOrderedDict()
Broker: None
Alive: True
判断在不在场内,如不在场,则可以进场
#检查是否有订单等待处理,如果是就不再进行其他下单
if self.order:
return
# 检查是否已经进场
if not self.position:
# 还未进场,则只能进行买入
position用于表示当前的状态,由size和price两个成员属性构成,其中size表示当前持有资产数量,price表示当前价格,比如以16.90的价格买入1股平安银行,那么此时position.size=1,position.price=16.90。当把这1股平安银行卖出时,position.size=0,position.price=0。
trade表示一笔交易,当position.size由0变为X时,表示交易开启,当position.size由X变回0时,表示交易结束。代码中的trade.pnl表示当前这笔交易的在不算佣金的情况下的盈亏情况,trade.pnlcomm表示当前这笔交易的在计算佣金的情况下的盈亏情况,实际上trade.pnlcomm=trade.pnl-佣金。
import backtrader as bt # 引入backtrader 框架
import datetime
import pandas as pd
import numpy as np
import os # 用于路径管理 数据导入时使用
# 创建策略
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))
params = (('period', 30),)
def __init__(self):
self.sma = bt.indicators.SimpleMovingAverage(period=self.p.period)
# self.order = None # 如在定义 def start(self)函数内,则此处不需要
def notify_order(self, order):
print("\033[35m日期:{},订单通知 size:{},price:{},pricelimit:{},exectype:{},tradeid:{},status:{}\033[0m"
.format(self.datas[0].datetime.date(0), order.size, order.price, order.pricelimit, order.ExecTypes[order.exectype],
order.tradeid, order.Status[order.status]))
# self.log( 'BUY EXECUTED, Price: %.4f, Cost: %.4f, Comm %.4f' %
# (order.executed.price,
# order.executed.value,
# order.executed.comm))
if not order.alive():
self.order = None
def notify_trade(self, trade):
print("\033[32m日期:{},交易通知 size:{},price:{},value:{},tradeid:{},status:{}\033[0m"
.format(self.datas[0].datetime.date(0), trade.size, trade.price, trade.value, trade.tradeid, trade.status_names[trade.status]))
# def notify_cashvalue(self, cash, value):
# print("\033[33mbar序:{},资产通知 cash:{},value:{}\033[0m"
# .format(len(self),cash,value))
# def notify_cashvalue(self, cash, value):
# print("\033[33mbar序:%s,资产通知 cash:%.2f,value:%.2f\033[0m"
# % (len(self),cash,value))
def start(self):
self.order = None
def next(self):
# print("bar序:{},next sma={},close={}".format(len(self),self.sma[0],self.data.close[0]))
if self.order:
return
if not self.position:
if self.sma > self.data.close:
self.order = self.order_target_percent(target=1.0, price=self.data.close[0],
exectype=bt.Order.Limit)
# self.order = self.buy(price=self.data.close[0]*0.99,exectype=bt.Order.Limit)
else:
if self.sma < self.data.close:
self.close()
# 创建cerebro实例
cerebro = bt.Cerebro()
# 添加策略
cerebro.addstrategy(TestStrategy)
# 添加策略开始和结束日期
start_time = datetime.datetime(2018, 1, 1)
end_time = datetime.datetime(2019, 12, 31)
# 先找到脚本文件的位置
modepath = os.path.dirname(__file__)
# 然后根据脚本与数据的相对关系找到数据位置,这样脚本从任意地方被调用,都可以正确地访问到数据
# datapath = modepath+'/data/000998.SZ.csv'
# datapath = os.path.join(modepath, './data/000998.SZ.csv')
datapath = os.path.join(modepath, '600000qfq.csv')
"""# data feeds 创建 通过GenericCSVData方式,Backtrader里使用的GenericCSVData函数,
要求CSV文件数据是按时间升序排列的。因此,如果股票数据未按时间升序排序,需要我们在做回测前对数据重新排序,否则会导致回测结果错误。
data = bt.feeds.GenericCSVData(
dataname=datapath,
fromdate=start_time,
todate=end_time,
nullvalue=0.0,
dtformat='%Y-%m-%d', # 自动把index数据中的符合日期的格式变成datetime类型
datetime=0,
open=2,
high=3,
low=4,
close=5,
volume=9,
openinterest=-1
)"""
# 通过PandasData 方式导入dataframe数据,此方式较为直观推荐此种方案
"""直接在数据导入时,对列表名称处理好,usecols是抽取的列,header =0 取首行为列名,names进行改列名
在PandasData时,可以自载匹配数据"""
df = pd.read_csv(datapath,usecols=[2,3,4,5,6,10],header=0,
names=['datetime','open','high','low','close','volume'],
index_col=0,parse_dates=True)
df['openinterest'] = 0
df['ma5'] = 0
df['ma20'] = 20
# data = bt.feeds.PandasData(dataname=df,
# fromdate=start_time,
# todate=end_time)
# 拓展技术数据
class NewLines_PandasData(bt.feeds.PandasData):
# 新增加两条数据线
lines =('ma5','ma20',)
# 新增数据在dataframe中的位置,最好使用-1进行自动匹配
# -1表示自动按列明匹配数据,也可以设置为线在数据源中列的位置索引 (('pe',7),('pb',8),)
params = (
('ma5',-1),
('ma20',-1),
)
data =NewLines_PandasData(dataname=df,
fromdate=start_time,
todate=end_time)
# 在cerebro中添加价格数据
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())
后续采用下述模版进行交易,思路清晰点:
import backtrader as bt # 引入backtrader 框架
import datetime
import pandas as pd
import numpy as np
import os # 用于路径管理 数据导入时使用
# 创建策略
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):
# 引用data[0]数据的收盘价数据
self.dataclose = self.datas[0].close
# 用于记录订单状态
self.order = None
self.buyprice = None
self.buycomm = None
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
# 提交给代理或者由代理接收的买/卖订单 - 不做操作
return
# 检查订单是否执行完毕
# 注意:如果没有足够资金,代理会拒绝订单
if order.status in [order.Completed]:
if order.isbuy():
print("\033[35m日期:{},BUY EXECUTED,Price:{:.2f},Cost:{:.2f},Comm:{:.2f}\033[0m"
.format(self.datas[0].datetime.date(0),
order.executed.price, order.executed.value,order.executed.comm))
# 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
else: # 卖
print("\033[32m日期:{},SELL EXECUTED,Price:{:.2f},Cost:{:.2f},Comm:{:.2f}\033[0m"
.format(self.datas[0].datetime.date(0),
order.executed.price, order.executed.value,order.executed.comm))
# self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
# (order.executed.price,
# order.executed.value,
# order.executed.comm))
self.bar_executed = len(self)
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
# self.log('Order Canceled/Margin/Rejected')
print("\033[31m Order Canceled/Margin/Rejected\033[0m")
# 订单状态处理完成,设为空
self.order = None
# 交易成果
def notify_trade(self, trade):
if not trade.isclosed:
return
# 显示交易的毛利率和净利润
print("\033[31mOPERATION PROFIT, GROSS:{:.2f}, NET:{:.2f}\033[0m"
.format(trade.pnl,trade.pnlcomm))
# self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
# (trade.pnl, trade.pnlcomm))
def next(self):
# 日志输出收盘价数据
# self.log('Close, %.2f' % self.dataclose[0])
# 检查是否有订单等待处理,如果是就不再进行其他下单
if self.order:
return
# 检查是否已经进场
if not self.position:
# 还未进场,则只能进行买入
# 当日收盘价小于前一日收盘价
if self.dataclose[0] < self.dataclose[-1]:
# 前一日收盘价小于前前日收盘价
if self.dataclose[-1] < self.dataclose[-2]:
# 买买买
self.log('BUY CREATE, %.2f' % self.dataclose[0])
# 记录订单避免二次下单
self.order = self.buy()
# 如果已经在场内,则可以进行卖出操作
else:
# 卖卖卖
if len(self) >= (self.bar_executed + 5):
self.log('SELL CREATE, %.2f' % self.dataclose[0])
# 记录订单避免二次下单
self.order = self.sell()
# 创建cerebro实例
cerebro = bt.Cerebro()
# 添加策略
cerebro.addstrategy(TestStrategy)
# 添加策略开始和结束日期
start_time = datetime.datetime(2018, 1, 1)
end_time = datetime.datetime(2019, 12, 31)
# 先找到脚本文件的位置
modepath = os.path.dirname(__file__)
# 然后根据脚本与数据的相对关系找到数据位置,这样脚本从任意地方被调用,都可以正确地访问到数据
# datapath = modepath+'/data/000998.SZ.csv'
# datapath = os.path.join(modepath, './data/000998.SZ.csv')
datapath = os.path.join(modepath, '600000qfq.csv')
"""# data feeds 创建 通过GenericCSVData方式,Backtrader里使用的GenericCSVData函数,
要求CSV文件数据是按时间升序排列的。因此,如果股票数据未按时间升序排序,需要我们在做回测前对数据重新排序,否则会导致回测结果错误。
data = bt.feeds.GenericCSVData(
dataname=datapath,
fromdate=start_time,
todate=end_time,
nullvalue=0.0,
dtformat='%Y-%m-%d', # 自动把index数据中的符合日期的格式变成datetime类型
datetime=0,
open=2,
high=3,
low=4,
close=5,
volume=9,
openinterest=-1
)"""
# 通过PandasData 方式导入dataframe数据,此方式较为直观推荐此种方案
"""直接在数据导入时,对列表名称处理好,usecols是抽取的列,header =0 取首行为列名,names进行改列名
在PandasData时,可以自载匹配数据"""
df = pd.read_csv(datapath,usecols=[2,3,4,5,6,10],header=0,
names=['datetime','open','high','low','close','volume'],
index_col=0,parse_dates=True)
df['openinterest'] = 0
df['ma5'] = 0
df['ma20'] = 20
# data = bt.feeds.PandasData(dataname=df,
# fromdate=start_time,
# todate=end_time)
# 拓展技术数据
class NewLines_PandasData(bt.feeds.PandasData):
# 新增加两条数据线
lines =('ma5','ma20',)
# 新增数据在dataframe中的位置,最好使用-1进行自动匹配
# -1表示自动按列明匹配数据,也可以设置为线在数据源中列的位置索引 (('pe',7),('pb',8),)
params = (
('ma5',-1),
('ma20',-1),
)
data =NewLines_PandasData(dataname=df,
fromdate=start_time,
todate=end_time)
# 在cerebro中添加价格数据
cerebro.adddata(data)
# 设置启动资金
cerebro.broker.setcash(100000.0)
# 设置佣金率为千分之一
cerebro.broker.setcommission(commission=0.001)
# 打印开始资金
print('Starting Portfolio Value:%.2f' % cerebro.broker.getvalue())
# 遍历所有数据
cerebro.run()
# 打印最后结果
print('Final Portfolio Value:%.2f' % cerebro.broker.getvalue())