这个例子参考了 backtrader 在 github 上的 samples 中的 ibtest.py 的代码,但是做了一些简单的改进工作,使用了 2 个半小时。
- 做了大量的注释,尤其是每个参数的作用
- 尝试对每个函数,主要的代码做了注释
- 修改了 parser 的使用方式,直接在策略里面设定好需要使用的参数
- 指出了代码里面可能的一些 bug
- 使用起来更加简单了
运行这个程序的过程中,对若干代码进行了修改,解决了好几个小 bug,还需要时间看有没有其他问题。
#!/usr/bin/env python
# -*- coding: utf-8; py-indent-offset:4 -*-
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import argparse
import datetime
# The above could be sent to an independent module
import backtrader as bt
from backtrader.utils import flushfile # win32 quick stdout flushing
class TestStrategy(bt.Strategy):
# 策略的参数,具体的参数的意义在下面有注释
params = dict(
smaperiod=5,
trade=False,
stake=10,
exectype=bt.Order.Market,
stopafter=0,
valid=None,
cancel=0,
donotsell=False,
stoptrail=False,
stoptraillimit=False,
trailamount=None,
trailpercent=None,
limitoffset=None,
oca=False,
bracket=False,
)
def __init__(self):
# To control operation entries
self.orderid = list()
self.order = None
self.counttostop = 0
self.datastatus = 0
# Create SMA on 2nd data
self.sma = bt.indicators.MovAv.SMA(self.data, period=self.p.smaperiod)
print('--------------------------------------------------')
print('Strategy Created')
print('--------------------------------------------------')
def notify_data(self, data, status, *args, **kwargs):
print('*' * 5, 'DATA NOTIF:', data._getstatusname(status), *args)
if status == data.LIVE:
self.counttostop = self.p.stopafter
self.datastatus = 1 # 如果是实盘数据开始了,那么,datastatus 就变为 1
def notify_store(self, msg, *args, **kwargs):
# 显示从 IB 传送过来的信息
print('*' * 5, 'STORE NOTIF:', msg)
def notify_order(self, order):
# 通知订单
if order.status in [order.Completed, order.Cancelled, order.Rejected]:
self.order = None
print('-' * 50, 'ORDER BEGIN', datetime.datetime.now())
print(order)
print('-' * 50, 'ORDER END')
def notify_trade(self, trade):
# 通知 trade
print('-' * 50, 'TRADE BEGIN', datetime.datetime.now())
print(trade)
print('-' * 50, 'TRADE END')
def prenext(self):
print("成功进入 prenext")
self.next(frompre=True)
def next(self, frompre=False):
# print("成功进入 next")
# 准备显示第一个数据
# txt = list()
# txt.append('Data0')
# txt.append('%04d' % len(self.data0))
# dtfmt = '%Y-%m-%dT%H:%M:%S.%f'
# txt.append('{}'.format(self.data.datetime[0]))
# txt.append('%s' % self.data.datetime.datetime(0).strftime(dtfmt))
# txt.append('{}'.format(self.data.open[0]))
# txt.append('{}'.format(self.data.high[0]))
# txt.append('{}'.format(self.data.low[0]))
# txt.append('{}'.format(self.data.close[0]))
# txt.append('{}'.format(self.data.volume[0]))
# txt.append('{}'.format(self.data.openinterest[0]))
# txt.append('{}'.format(self.sma[0]))
if self.datastatus>0:
print(f"{self.data._name},\
datetime:{bt.num2date(self.data.datetime[0])} , \
open:{self.data.open[0]},\
high:{self.data.high[0]},\
low:{self.data.low[0]},\
close:{self.data.close[0]},\
volume:{self.data.volume[0]},\
openinterest:{self.data.openinterest[0]},\
ma:{self.sma[0]}")
# 如果有第二个数据,那么也打印第二个数据
if len(self.datas) > 1 and len(self.data1):
# txt = list()
# txt.append('Data1')
# txt.append('%04d' % len(self.data1))
# dtfmt = '%Y-%m-%dT%H:%M:%S.%f'
# txt.append('{}'.format(self.data1.datetime[0]))
# txt.append('%s' % self.data1.datetime.datetime(0).strftime(dtfmt))
# txt.append('{}'.format(self.data1.open[0]))
# txt.append('{}'.format(self.data1.high[0]))
# txt.append('{}'.format(self.data1.low[0]))
# txt.append('{}'.format(self.data1.close[0]))
# txt.append('{}'.format(self.data1.volume[0]))
# txt.append('{}'.format(self.data1.openinterest[0]))
# txt.append('{}'.format(float('NaN')))
# print(', '.join(txt))
print(f"{self.data1._name},\
datetime:{bt.num2date(self.data1.datetime[0])} , \
open:{self.data1.open[0]},\
high:{self.data1.high[0]},\
low:{self.data1.low[0]},\
close:{self.data1.close[0]},\
volume:{self.data1.volume[0]},\
openinterest:{self.data1.openinterest[0]},\
ma:{self.sma[0]}")
# 多少个 next 之后策略停止
if self.counttostop: # stop after x live lines
self.counttostop -= 1
if not self.counttostop:
self.env.runstop()
return
# 如果 trade 设置的是 False,下面将不会运行
if not self.p.trade:
return
position_size = self.getposition(self.data).size
print(f"self.datastatus:{self.datastatus},position_size:{position_size},len(self.orderid):{len(self.orderid)}")
# 如果是实盘数据,并且没有持仓,并且没有下单
if self.datastatus and position_size==0 and len(self.orderid) < 1:
print("准备下单")
# 如果不是 oca 订单类型,就直接下一个市价单,否则按照当前价格的 90%下一个限价单,有效期是默认取消前有效,如果设置的是 0 的话,当天有效
exectype = self.p.exectype if not self.p.oca else bt.Order.Limit
close = self.data0.close[0]
price = round(close * 1.001, 2)
self.order = self.buy(size=self.p.stake,
exectype=exectype,
price=price,
valid=self.p.valid,
transmit=not self.p.bracket)
self.orderid.append(self.order)
print(f"下单成功:exectype-{exectype},self.p.bracket:{self.p.bracket},self.p.oca:{self.p.oca}")
# 如果是一篮子订单,按照 90%的价格设置止损价,按照 110%的价格设置止盈价
if self.p.bracket:
# low side
self.sell(size=self.p.stake,
exectype=bt.Order.Stop,
price=round(price * 0.90, 2),
valid=self.p.valid,
transmit=False,
parent=self.order)
# high side
self.sell(size=self.p.stake,
exectype=bt.Order.Limit,
price=round(close * 1.10, 2),
valid=self.p.valid,
transmit=True,
parent=self.order)
# 如果是 oca 订单,按照 80%的价格下一个限价单
elif self.p.oca:
self.buy(size=self.p.stake,
exectype=bt.Order.Limit,
price=round(self.data0.close[0] * 0.80, 2),
oco=self.order)
# 如果是跟踪止损单
elif self.p.stoptrail:
self.sell(size=self.p.stake,
exectype=bt.Order.StopTrail,
# price=round(self.data0.close[0] * 0.90, 2),
valid=self.p.valid,
trailamount=self.p.trailamount,
trailpercent=self.p.trailpercent)
# 如果是跟踪止损限价单
elif self.p.stoptraillimit:
p = round(self.data0.close[0] - self.p.trailamount, 2)
# p = self.data0.close[0]
self.sell(size=self.p.stake,
exectype=bt.Order.StopTrailLimit,
price=p,
plimit=p + self.p.limitoffset,
valid=self.p.valid,
trailamount=self.p.trailamount,
trailpercent=self.p.trailpercent)
# 如果持仓大于 0,并没有不允许卖出平仓
elif self.position.size > 0 and not self.p.donotsell:
# 如果没有订单,市价单卖出二分之一仓位
if self.order is None:
self.order = self.sell(size=self.p.stake // 2,
exectype=bt.Order.Market,
price=self.data0.close[0])
# 如果当前存在订单,并且 cancel 设置大于 0 的时候,在 next 运行的次数大于 self.cancel 的时候,会触发取消订单
elif self.order is not None and self.p.cancel:
if self.datastatus > self.p.cancel:
self.cancel(self.order)
# 每个 next,datastatus 加 1
if self.datastatus:
self.datastatus += 1
def start(self):
print("开始运行,获取时区")
if self.data0.contractdetails is not None:
print('Timezone from ContractDetails: {}'.format(
self.data0.contractdetails.m_timeZoneId))
header = ['Datetime', 'Open', 'High', 'Low', 'Close', 'Volume',
'OpenInterest', 'SMA']
print(', '.join(header))
print(dir(self.data0.contractdetails))
self.done = False
def runstrategy(args):
# args = parse_args()
# Create a cerebro
cerebro = bt.Cerebro()
# IBstore 参数
storekwargs = dict(
host=args.host, port=args.port,
clientId=args.clientId, timeoffset=not args.no_timeoffset,
reconnect=args.reconnect, timeout=args.timeout,
notifyall=args.notifyall, _debug=args.debug
)
if args.usestore:
ibstore = bt.stores.IBStore(**storekwargs)
if args.broker:
if args.usestore:
broker = ibstore.getbroker()
else:
broker = bt.brokers.IBBroker(**storekwargs)
cerebro.setbroker(broker)
timeframe = args.timeframe
# Manage data1 parameters
tf1 = args.timeframe1
tf1 = tf1 if tf1 is not None else timeframe
cp1 = args.compression1
cp1 = cp1 if cp1 is not None else args.compression
if args.resample or args.replay:
datatf = datatf1 = bt.TimeFrame.Ticks
datacomp = datacomp1 = 1
else:
datatf = timeframe
datacomp = args.compression
datatf1 = tf1
datacomp1 = cp1
fromdate = None
if args.fromdate:
dtformat = '%Y-%m-%d' + ('T%H:%M:%S' * ('T' in args.fromdate))
fromdate = datetime.datetime.strptime(args.fromdate, dtformat)
# 获取数据
IBDataFactory = ibstore.getdata if args.usestore else bt.feeds.IBData
# 数据的参数
datakwargs = dict(
timeframe=datatf, compression=datacomp,
historical=args.historical, fromdate=fromdate,
rtbar=args.rtbar,
qcheck=args.qcheck,
what=args.what,
backfill_start=not args.no_backfill_start,
backfill=not args.no_backfill,
latethrough=args.latethrough,
tz=args.timezone
)
# 如果没有用 store 模式并且没有用设置 broker,更新参数
if not args.usestore and not args.broker: # neither store nor broker
datakwargs.update(storekwargs) # pass the store args over the data
# 获取数据
data0 = IBDataFactory(dataname=args.data0, **datakwargs)
# 是否获取数据 1
data1 = None
if args.data1 is not None:
if args.data1 != args.data0:
datakwargs['timeframe'] = datatf1
datakwargs['compression'] = datacomp1
data1 = IBDataFactory(dataname=args.data1, **datakwargs)
else:
data1 = data0
rekwargs = dict(
timeframe=tf1, compression=cp1,
bar2edge=not args.no_bar2edge,
adjbartime=not args.no_adjbartime,
rightedge=not args.no_rightedge,
takelate=not args.no_takelate,
)
if args.replay:
cerebro.replaydata(data0, **rekwargs)
if data1 is not None:
rekwargs['timeframe'] = tf1
rekwargs['compression'] = cp1
cerebro.replaydata(data1, **rekwargs)
elif args.resample:
cerebro.resampledata(data0, **rekwargs)
if data1 is not None:
rekwargs['timeframe'] = tf1
rekwargs['compression'] = cp1
cerebro.resampledata(data1, **rekwargs)
else:
cerebro.adddata(data0)
if data1 is not None:
cerebro.adddata(data1)
# 数据的有效期,这个地方需要额外考虑是 0 的时候,当天有效的设置,这里没有设置好,是 0 的时候,有效期基本上相当于市价单,没有成交就会立即撤单
if args.valid is None:
valid = None
else:
valid = datetime.timedelta(seconds=args.valid)
# Add the strategy
cerebro.addstrategy(TestStrategy,
smaperiod=args.smaperiod,
trade=args.trade,
exectype=args.exectype,
stake=args.stake,
stopafter=args.stopafter,
valid=valid,
cancel=args.cancel,
donotsell=args.donotsell,
stoptrail=args.stoptrail,
stoptraillimit=args.traillimit,
trailamount=args.trailamount,
trailpercent=args.trailpercent,
limitoffset=args.limitoffset,
oca=args.oca,
bracket=args.bracket)
# Live data ... avoid long data accumulation by switching to "exactbars"
cerebro.run(exactbars=args.exactbars)
if args.plot and args.exactbars < 1: # plot if possible
cerebro.plot()
args ={"exactbars":1,# exactbars level, use 0/-1/-2 to enable plotting
"stopafter":0,# Stop after x lines of LIVE data,默认情况下,永远不停止,如果是一个大于 0 的整数,就代表多少个 next 之后会停止
"plot":False,# Plot if possible
"usestore":True,#使用 ib 的时候是否使用 store 模式
"notifyall":False,#是否把所有信息都会通过 store notify 告知给策略
"debug":False,#默认不显示所有的从 IB 获取的信息
"host":'127.0.0.1',#连接 TWS 的时候使用的 host
"port":7497,#连接 TWS 的端口号,默认是 7496,模拟交易使用 7497
"qcheck":0.5,#在 resample 或者 replay 的时候多少时间间隔检查一次,用于生成 bar
"clientId":None,#默认情况下可以随机生成一个,开多个 TWS 的时候可以指定每个 clientid,有一个 master id 可以控制其他的 clientid
"no_timeoffset":False,# 是否使用 IB 的系统时间和本地时钟之间做时间的补偿,使得本地时间和系统时间能够对齐,以方便能够生成 bar 更准确
"reconnect":3,#当连接中断之后,尝试的重新连接次数
"timeout":3,#每次尝试连接的时候,间隔的时间,默认是 3 秒
"data0":True,#加载到系统的第一个数据
"data1":None,#加载到系统的第二个数据
"timezone":None,#默认情况下,根据从 IB 获取的时间设置时区,也可以自己去设置
"what":None,#用于请求的历史数据类型
"no_backfill_start":False,#默认开始的时候不填充历史数据
"latethrough":False,#当 resample 或者 replay 的时候,让来的太迟的 tick 传递过去
"no_backfill":False,#当连接中断之后,是否填充,默认是不填充
"rtbar":False,#是否使用 5 秒钟的 bar 代替 250ms 的 tick,默认是不会
"historical":False,#是否仅仅下载历史数据,默认是否
"fromdate":False,#从哪里开始下载历史数据,参数可以设置具体的时间 YYYY-MM-DD[THH:MM:SS]
"smaperiod":5,#策略运行使用的参数
"replay":False,#replay
"resample":True,#resample,和 replay 两个功能不能同时都设置成 True
"timeframe":bt.TimeFrame.Ticks,#交易的时间间隔
"compression":1,#多少个交易的时间间隔形成一个 bar
"timeframe1":bt.TimeFrame.Seconds,#data1 的
"compression1":5,#data1 的
"no_takelate":False,#当 latethrough 设置成 True 的时候,resample 或者 replay 形成新的 bar 的时候,是否使用来的比较迟的 tick
"no_bar2edge":False,# no bar2edge for resample/replay
"no_adjbartime":False,#no adjbartime for resample/replay
"no_rightedge":False,#no rightedge for resample/replay
"broker":False,#使用 IB 作为 broker
"trade":True,#是否进行买卖活动,设置成 False 的时候,不会进行买卖
"donotsell":False,#买了之后是否卖,默认是买了之后会卖的
"exectype":bt.Order.Market,# 下单的类型,默认是市价单
"stake":0.1,#每次下单的手数
"valid":None,#订单的有效期设置,None 代表一直有效,0 代表当天有效
# 下面几种止损不能共存,只能选一个
"stoptrail":False,#是否下一个市价止损单,默认是否
"traillimit":False,#是否下一个限价跟踪止损单,默认是否
"oca":False,#是否下一个 oca 订单,应该是 backtrader 中的 oco 订单
"bracket":False,#是否下一个一篮子订单
# 下面几种也是不能共存的
"trailamount":None,#StopTrail 订单设置的参数
"trailpercent":None,#StopTrail 订单设置的参数
"limitoffset":None,#订单的参数
"cancel":0,# 限价单使用,如果 n 个 bar 之后还没有成交,取消订单
}
class AO():
def __init__(self,args):
for key,value in args.items():
setattr(self,key,value)
if __name__ == '__main__':
args['data0'] = 'EUR.USD-CASH-IDEALPRO'
new_args = AO(args)
runstrategy(new_args)
# def parse_args():
# parser = argparse.ArgumentParser(
# formatter_class=argparse.ArgumentDefaultsHelpFormatter,
# description='Test Interactive Brokers integration')
# parser.add_argument('--exactbars', default=1, type=int,
# required=False, action='store',
# help='exactbars level, use 0/-1/-2 to enable plotting')
# parser.add_argument('--plot',
# required=False, action='store_true',
# help='Plot if possible')
# parser.add_argument('--stopafter', default=0, type=int,
# required=False, action='store',
# help='Stop after x lines of LIVE data')
# parser.add_argument('--usestore',
# required=False, action='store_true',
# help='Use the store pattern')
# parser.add_argument('--notifyall',
# required=False, action='store_true',
# help='Notify all messages to strategy as store notifs')
# parser.add_argument('--debug',
# required=False, action='store_true',
# help='Display all info received form IB')
# parser.add_argument('--host', default='127.0.0.1',
# required=False, action='store',
# help='Host for the Interactive Brokers TWS Connection')
# parser.add_argument('--qcheck', default=0.5, type=float,
# required=False, action='store',
# help=('Timeout for periodic '
# 'notification/resampling/replaying check'))
# parser.add_argument('--port', default=7496, type=int,
# required=False, action='store',
# help='Port for the Interactive Brokers TWS Connection')
# parser.add_argument('--clientId', default=None, type=int,
# required=False, action='store',
# help='Client Id to connect to TWS (default: random)')
# parser.add_argument('--no-timeoffset',
# required=False, action='store_true',
# help=('Do not Use TWS/System time offset for non '
# 'timestamped prices and to align resampling'))
# parser.add_argument('--reconnect', default=3, type=int,
# required=False, action='store',
# help='Number of recconnection attempts to TWS')
# parser.add_argument('--timeout', default=3.0, type=float,
# required=False, action='store',
# help='Timeout between reconnection attempts to TWS')
# parser.add_argument('--data0', default=None,
# required=True, action='store',
# help='data 0 into the system')
# parser.add_argument('--data1', default=None,
# required=False, action='store',
# help='data 1 into the system')
# parser.add_argument('--timezone', default=None,
# required=False, action='store',
# help='timezone to get time output into (pytz names)')
# parser.add_argument('--what', default=None,
# required=False, action='store',
# help='specific price type for historical requests')
# parser.add_argument('--no-backfill_start',
# required=False, action='store_true',
# help='Disable backfilling at the start')
# parser.add_argument('--latethrough',
# required=False, action='store_true',
# help=('if resampling replaying, adjusting time '
# 'and disabling time offset, let late samples '
# 'through'))
# parser.add_argument('--no-backfill',
# required=False, action='store_true',
# help='Disable backfilling after a disconnection')
# parser.add_argument('--rtbar', default=False,
# required=False, action='store_true',
# help='Use 5 seconds real time bar updates if possible')
# parser.add_argument('--historical',
# required=False, action='store_true',
# help='do only historical download')
# parser.add_argument('--fromdate',
# required=False, action='store',
# help=('Starting date for historical download '
# 'with format: YYYY-MM-DD[THH:MM:SS]'))
# parser.add_argument('--smaperiod', default=5, type=int,
# required=False, action='store',
# help='Period to apply to the Simple Moving Average')
# pgroup = parser.add_mutually_exclusive_group(required=False)
# pgroup.add_argument('--replay',
# required=False, action='store_true',
# help='replay to chosen timeframe')
# pgroup.add_argument('--resample',
# required=False, action='store_true',
# help='resample to chosen timeframe')
# parser.add_argument('--timeframe', default=bt.TimeFrame.Names[0],
# choices=bt.TimeFrame.Names,
# required=False, action='store',
# help='TimeFrame for Resample/Replay')
# parser.add_argument('--compression', default=1, type=int,
# required=False, action='store',
# help='Compression for Resample/Replay')
# parser.add_argument('--timeframe1', default=None,
# choices=bt.TimeFrame.Names,
# required=False, action='store',
# help='TimeFrame for Resample/Replay - Data1')
# parser.add_argument('--compression1', default=None, type=int,
# required=False, action='store',
# help='Compression for Resample/Replay - Data1')
# parser.add_argument('--no-takelate',
# required=False, action='store_true',
# help=('resample/replay, do not accept late samples '
# 'in new bar if the data source let them through '
# '(latethrough)'))
# parser.add_argument('--no-bar2edge',
# required=False, action='store_true',
# help='no bar2edge for resample/replay')
# parser.add_argument('--no-adjbartime',
# required=False, action='store_true',
# help='no adjbartime for resample/replay')
# parser.add_argument('--no-rightedge',
# required=False, action='store_true',
# help='no rightedge for resample/replay')
# parser.add_argument('--broker',
# required=False, action='store_true',
# help='Use IB as broker')
# parser.add_argument('--trade',
# required=False, action='store_true',
# help='Do Sample Buy/Sell operations')
# parser.add_argument('--donotsell',
# required=False, action='store_true',
# help='Do not sell after a buy')
# parser.add_argument('--exectype', default=bt.Order.ExecTypes[0],
# choices=bt.Order.ExecTypes,
# required=False, action='store',
# help='Execution to Use when opening position')
# parser.add_argument('--stake', default=10, type=int,
# required=False, action='store',
# help='Stake to use in buy operations')
# parser.add_argument('--valid', default=None, type=int,
# required=False, action='store',
# help='Seconds to keep the order alive (0 means DAY)')
# pgroup = parser.add_mutually_exclusive_group(required=False)
# pgroup.add_argument('--stoptrail',
# required=False, action='store_true',
# help='Issue a stoptraillimit after buy( do not sell')
# pgroup.add_argument('--traillimit',
# required=False, action='store_true',
# help='Issue a stoptrail after buying (do not sell')
# pgroup.add_argument('--oca',
# required=False, action='store_true',
# help='Test oca by putting 2 orders in a group')
# pgroup.add_argument('--bracket',
# required=False, action='store_true',
# help='Test bracket orders by issuing high/low sides')
# pgroup = parser.add_mutually_exclusive_group(required=False)
# pgroup.add_argument('--trailamount', default=None, type=float,
# required=False, action='store',
# help='trailamount for StopTrail order')
# pgroup.add_argument('--trailpercent', default=None, type=float,
# required=False, action='store',
# help='trailpercent for StopTrail order')
# parser.add_argument('--limitoffset', default=None, type=float,
# required=False, action='store',
# help='limitoffset for StopTrailLimit orders')
# parser.add_argument('--cancel', default=0, type=int,
# required=False, action='store',
# help=('Cancel a buy order after n bars in operation,'
# ' to be combined with orders like Limit'))
# return parser.parse_args()