Trying to do multi symbol in live trade. Not adding data to a strat but trading multiple symbols. I have seen enumerating in the strategy as the default way to do this. I am doing it this waym wonder if anyone tried it this way?
from __future__ import (absolute_import, division, print_function, unicode_literals)
import backtrader as bt
from MyStratLive import BaseStrategy
from helper import *
import datetime
import pytz
from backtrader_plotting import Bokeh
from backtrader_plotting.schemes import Tradimo
def saveplots(cerebro, numfigs=1, iplot=True, start=None, end=None, width=16, height=9, dpi=300, tight=False, use=None, file_path='', **kwargs):
from backtrader import plot
if cerebro.p.oldsync:
plotter = plot.Plot_OldSync(**kwargs)
else:
plotter = plot.Plot(**kwargs)
figs = []
for stratlist in cerebro.runstrats:
for si, strat in enumerate(stratlist):
rfig = plotter.plot(strat, figid=si * 100,
numfigs=numfigs, iplot=iplot,
start=start, end=end, use=use)
figs.append(rfig)
for fig in figs:
for f in fig:
f.savefig(file_path, bbox_inches='tight')
return figs
def main():
symbols = ['AMD', 'TSLA']
backfill_date = get_backfill_date() # date & time does not work. Only Date works
print('backfill from: {}'.format(backfill_date))
store = bt.stores.IBStore(host='127.0.0.1', clientId=10, port=7497, notifyall=False, reconnect=-1, timeout=3.0, timeoffset=True, timerefresh=60.0)
cerebro = bt.Cerebro(stdstats=False)
data = []
for x, each_symbol in enumerate(symbols):
data.append(store.getdata(dataname=each_symbol, sectype='STK', exchange='SMART', currency='USD', timeframe=bt.TimeFrame.Minutes, compression=5,
historical=False, backfill=True, fromdate=backfill_date, rtbar=True, qcheck=0.5, backfill_start=True,
useRTH=False, latethrough=False, tz='US/Eastern')
)
cerebro.resampledata(data[x], timeframe=bt.TimeFrame.Minutes, compression=5, name=each_symbol)
cerebro.addstrategy(BaseStrategy, data_id=data[x]._id-1) # data_id=0 1 2 etc
cerebro.broker = store.getbroker()
cerebro.run()
print('ended, come out to main last line')
now = datetime.datetime.now(pytz.timezone('US/Eastern'))
b = Bokeh(style='bar', barup='green', bardown='grey', fmt_x_ticks='%d-%b-%y %H:%M', fmt_x_data='%d-%b-%y %H:%M', valuetags=False, volume=True, plot_mode='multiple',
scheme=Tradimo(), tabs='multi', output_mode='save', filename='charts\\' + '{} {}-{}-{}.html'.format(each_symbol, now.day, now.month, now.year))
cerebro.plot(b)
cerebro.plot(style='candlestick', barup='green', bardown='grey', fmt_x_ticks='%d-%b-%y %H:%M', fmt_x_data='%d-%b-%y %H:%M', valuetags=False, volume=True)
and strategy file, MyStratLive.py only trying to get it to run properly no strat yet:
import backtrader as bt
from datetime import datetime
from pytz import timezone
from helper import *
class BaseStrategy(bt.Strategy): # inherit MyStrat
params = dict(data_id=None, atrperiod=14, emaperiod=200, rsiperiod=30, vix_threshold=25,
risk=2, slmultiple=1.75, trailmultiple=1, multiplenonvolatile=1, multiplevolatile=1.75)
def __init__(self):
self.notoptimize = True
self.data0 = self.datas[self.p.data_id]
symbol = self.data0._name
self.early_close_days = get_early_close_days()
self.long_parent_order, self.long_stoploss, self.long_partial, self.long_trailstop, self.close_posn = None, None, None, None, None
self.short_parent_order, self.short_stoploss, self.short_partial, self.short_trailstop, self.early_exit = None, None, None, None, None
self.data_live = False
self.entered = -1
self.maxdd = 0
self.numtrades = 0
self.tradepnl = 0
self.lasttradewinloss = 0
self.lastclosedlen = 0
self.tws_parent_exe_px = None
self.move_to_position_px = False
self.trail_stop_started = False
self.ATR_at_exe = -1
self.action = 'nothing'
self.atr = bt.ind.AverageTrueRange(self.data0, period=self.p.atrperiod)
self.ema200 = bt.ind.EMA(self.data0, period=self.p.emaperiod)
def notify_data(self, data, status, *args, **kwargs):
print('*' * 5, 'DATA NOTIFY:', data._getstatusname(status), *args)
if status == data.LIVE:
self.data_live = True
def notify_store(self, msg, *args, **kwargs):
print('*' * 5, 'STORE NOTIFY:', msg)
def notify_order(self, order):
if order.status in [order.Cancelled]:
if self.notoptimize: print('Order Cancelled...ref: {}, name: {}'.format(order.ref, order.info['name']))
if order.status in [order.Margin, order.Rejected]:
if self.notoptimize: print('Order Margin/Rejected', order.ref)
return
if order.status in [order.Submitted]: # explain status 1
# Buy/Sell order submitted/accepted to/by broker - Nothing to do
if self.notoptimize: print('Order Submitted...', order.ref, datetime.datetime.now(timezone('US/Eastern')))
return
if order.status in [order.Accepted]: # explain status 2
# Buy/Sell order submitted/accepted to/by broker - Nothing to do
if self.notoptimize: print('Order Accepted...', order.ref)
return
if order.status in [order.Completed]:
value = order.executed.value + order.executed.comm
entry_price = round(abs(value/order.executed.size), 2)
if self.notoptimize:
print('-'*90, 'EXECUTED ORDER', '-'*90)
print('{} EXE-D, REF {}, AT {}: {:d} shares of {}, TWS Px: {:.2f}, Entry Px incl Comm: {:.2f}, Value: {:.2f}, Comm: {:.2f}, PNL Aft Comm: {:.2f}'.format(
order.info['name'], order.ref, self.data0.datetime.datetime(0), order.executed.size, self.data0._name,
order.executed.price, entry_price, order.executed.value, order.executed.comm, order.executed.pnl))
print('-' * 194)
if 'Main-cond' in order.info['name']: # main order opened
self.tws_parent_exe_px = round(order.executed.price, 2)
self.entered = len(self.data0)
self.ATR_at_exe = self.atr[0]
if not self.position and ('Main-cond' not in order.info['name']): # todo -> added this 9 mar. bcos if order open and close on same bar
if self.notoptimize: print('came in to reset everything')
if self.long_parent_order is not None: self.cancel(self.long_parent_order)
if self.long_stoploss is not None: self.cancel(self.long_stoploss)
if self.long_partial is not None: self.cancel(self.long_partial)
if self.long_trailstop is not None: self.cancel(self.long_trailstop)
if self.close_posn is not None: self.cancel(self.close_posn)
if self.short_parent_order is not None: self.cancel(self.short_parent_order)
if self.short_stoploss is not None: self.cancel(self.short_stoploss)
if self.short_partial is not None: self.cancel(self.short_partial)
if self.short_trailstop is not None: self.cancel(self.short_trailstop)
if self.early_exit is not None: self.cancel(self.early_exit)
self.long_parent_order, self.long_stoploss, self.long_partial, self.long_trailstop, self.close_posn = None, None, None, None, None
self.short_parent_order, self.short_stoploss, self.short_partial, self.short_trailstop, self.early_exit = None, None, None, None, None
self.move_to_position_px = False
self.trail_stop_started = False
def notify_trade(self, trade):
if trade.isclosed:
if self.notoptimize:
print('*' * 50, 'TRADE COMPLETE', '*' * 50)
print('DT:{}, Symbol: {}, Held: {}, PNL: {:.2f}, PNL after Comm: {:.2f}, Total comm both sides: {:.2f}'
.format(datetime.datetime.now(timezone('US/Eastern')), trade.data._name, len(self.data0) - self.entered, trade.pnl, trade.pnlcomm, abs(trade.pnlcomm)-abs(trade.pnl)))
print('*' * 116)
self.tradepnl = trade.pnl
self.numtrades += 1
self.lasttradewinloss = 1 if trade.pnl > 0 else -1
self.lastclosedlen = len(self.data0)
# ************TURN OFF FOR PANDAS DATA**************************************************************************************
def start(self):
pass
if self.data0.contractdetails is not None:
print('Timezone from ContractDetails: {}'.format(self.data0.contractdetails.m_timeZoneId))
header = ['Datetime', 'Open', 'High', 'Low', 'Close', 'Volume']
print(', '.join(header))
# ***************************************************************************************************************************
def logdata(self, trigger):
txt = list()
dtfmt = '%Y-%m-%dT%H:%M:%S'
txt.append('{}'.format(self.data0._name))
txt.append('{:4d}'.format(len(self.data0)))
txt.append('{}'.format(self.data0.datetime.datetime(0).strftime(dtfmt)))
txt.append(' {:.2f}'.format(self.data0.open[0]))
txt.append('{:.2f}'.format(self.data0.high[0]))
txt.append('{:.2f}'.format(self.data0.low[0]))
txt.append('{:.2f}'.format(self.data0.close[0]))
txt.append(' {:.2f}'.format(self.ema200.l.ema[0]))
txt.append('{:.2f}'.format(self.atr[0]))
txt.append(' {}'.format(self.position.size))
txt.append('{:.2f}'.format(self.tradepnl))
if self.notoptimize: print(','.join(txt))
def prenext(self):
self.next(frompre=True)
def next(self, frompre=False):
# ************TURN OFF FOR PANDAS DATA**************************************************************************************
if datetime.datetime.now(timezone('US/Eastern')).time() > datetime.time(16, 30):
self.env.runstop() # stop prog at 1940 NY time
else:
pass
# *************************************************************************************************************************
self.logdata(trigger)
if not self.position:
self.action = 'nothing'
sl_px, exe_size = None, None
# **************************************************************My Strategy here later***************************************************************
if self.action != 'nothing':
if self.long_parent_order is not None: self.cancel(self.long_parent_order)
if self.long_stoploss is not None: self.cancel(self.long_stoploss)
if self.short_parent_order is not None: self.cancel(self.short_parent_order)
if self.short_stoploss is not None: self.cancel(self.short_stoploss)
self.long_parent_order, self.short_parent_order = None, None
self.long_stoploss, self.short_stoploss = None, None
if self.action == 'long' and exe_size is not None:
if self.notoptimize:
print('main stoploss to use: ', sl_px)
print('price going long decide on: {:.2f} '.format(self.data0.close[0]), self.data0.datetime.datetime(0))
self.long_parent_order = self.buy(exectype=bt.Order.Market, size=exe_size, transmit=False, valid=datetime.datetime.now() + datetime.timedelta(seconds=120))
self.long_stoploss = self.sell(price=sl_px, exectype=bt.Order.Stop, size=exe_size, transmit=True, parent=self.long_parent_order)
self.long_parent_order.addinfo(name='Parent Long Main-cond')
self.long_stoploss.addinfo(name='Parent Long Main Stoploss Triggered') # explain this stop loss is not the most accurate due to slippage
elif self.action == 'short' and exe_size is not None:
if self.notoptimize:
print('price going short decide on: {:.2f} '.format(self.data0.close[0]), self.data0.datetime.datetime(0))
self.short_parent_order = self.sell(exectype=bt.Order.Market, size=exe_size, transmit=False, valid=datetime.datetime.now() + datetime.timedelta(seconds=120))
self.short_stoploss = self.buy(price=sl_px, exectype=bt.Order.Stop, size=exe_size, transmit=True, parent=self.short_parent_order)
self.short_parent_order.addinfo(name='Parent Short Main-cond')
self.short_stoploss.addinfo(name='Parent Short Main Stoploss Triggered') # explain this stop loss is not the most accurate due to slippage
self.action = 'nothing'
# **************************************************************Explain Order Management*****************************************************************************
elif self.position:
# current time > 1245+10min or 1545+10min
if self.data0.datetime.time(0) >= get_tradeable_time(self.data0.datetime.date(0), self.early_close_days, 2) + datetime.timedelta(minutes=10):
self.early_exit = self.close()
self.early_exit.addinfo(name='Early exit - Times up')
return
if self.position.size > 0: # explain this is a long order
# explain Have not partial closed and price hit 1 ATR -> partial close and move stop to position price
if not self.trail_stop_started and not self.move_to_position_px and (self.tws_parent_exe_px + (self.ATR_at_exe * 1) <= self.data0.close[0] < self.tws_parent_exe_px + (self.ATR_at_exe * 2)):
half_size = int(self.position.size / 2)
size_left = abs(self.position.size - half_size)
stop_px = round(self.tws_parent_exe_px, 2)
if self.notoptimize:
print('Target1ATR: {:.2f} <= curr close[0]: {:.2f} < 2ATR: {:.2f}'.format(self.tws_parent_exe_px + (self.ATR_at_exe * 1), self.data0.close[0], self.tws_parent_exe_px + (self.ATR_at_exe * 2)))
print('Close half size {}, size left {}, new stop px {:.2f}'.format(half_size, size_left, stop_px))
if self.long_stoploss is not None: self.cancel(self.long_stoploss)
if half_size != 0:
self.long_partial = self.sell(exectype=bt.Order.Market, size=half_size, transmit=False)
self.long_partial.addinfo(name='Long Partial Closed, size left {}'.format(size_left))
self.long_stoploss = self.sell(price=stop_px, exectype=bt.Order.Stop, size=size_left, parent=self.long_partial, transmit=True)
self.long_stoploss.addinfo(name='Long Partial Stoploss at Position Price, {:.2f} Triggered'.format(stop_px))
self.move_to_position_px = True
elif half_size == 0:
self.early_exit = self.close()
self.early_exit.addinfo(name='Long Half size is 0, just close')
# explain 2 scenario:
# explain a. hit betw 1 - 2 ATR, sell 1/2, move stop to posn px, then hit 2 ATR change to trail stop
# explain b. hit above 2 ATR, go straight to trail stop
elif not self.trail_stop_started and (self.data0.close[0] >= self.tws_parent_exe_px + (self.ATR_at_exe * 2)):
if self.notoptimize: print('Came into Long Trailstop at: {}, size: {}'.format(self.data0.datetime.datetime(0), self.position.size))
stop_size = abs(self.position.size)
trail_stop_px = self.data0.close[0]
if self.long_stoploss is not None: self.cancel(self.long_stoploss)
self.long_trailstop = self.sell(exectype=bt.Order.StopTrail, price=trail_stop_px, size=stop_size, trailamount=round(self.ATR_at_exe * 1, 2))
self.long_trailstop.addinfo(name='Long TrailStop Stopped out, initially started trail at: {:.2f}, Trail amt was: {:.2f}'.format(trail_stop_px, round(self.ATR_at_exe * 1, 2)))
self.trail_stop_started = True
elif self.position.size < 0: # explain this is short order
if not self.trail_stop_started and not self.move_to_position_px and (self.tws_parent_exe_px - (self.ATR_at_exe * 1) >= self.data0.close[0] > self.tws_parent_exe_px - (self.ATR_at_exe * 2)):
half_size = int(self.position.size / 2)
size_left = abs(self.position.size - half_size)
stop_px = round(self.tws_parent_exe_px, 2)
if self.notoptimize:
print('Target1ATR: {:.2f} >= curr close[0]: {:.2f} > 2ATR: {:.2f}'.format(self.tws_parent_exe_px - (self.ATR_at_exe * 1), self.data0.close[0], self.tws_parent_exe_px - (self.ATR_at_exe * 2)))
print('Close half size: {}, size left {}, new stop px {:.2f}'.format(half_size, size_left, stop_px))
if self.short_stoploss is not None: self.cancel(self.short_stoploss)
if half_size != 0:
self.short_partial = self.buy(exectype=bt.Order.Market, size=half_size, transmit=False)
self.short_partial.addinfo(name='Short Partial Closed, size left {}'.format(size_left))
self.short_stoploss = self.buy(price=stop_px, exectype=bt.Order.Stop, size=size_left, parent=self.short_partial, transmit=True)
self.short_stoploss.addinfo(name='Short Partial Stoploss at Position Price, {:.2f} Triggered'.format(stop_px))
self.move_to_position_px = True
elif half_size == 0:
self.early_exit = self.close()
self.early_exit.addinfo(name='Short Half size is 0, just close')
# explain 2 scenario:
# explain a. hit betw 1 - 2 ATR, sell 1/2, move stop to posn px then, hit 2 ATR change to trail stop
# explain b. hit above 2 ATR, go straight to trail stop
elif not self.trail_stop_started and (self.tws_parent_exe_px - (self.ATR_at_exe * 2) >= self.data0.close[0]):
if self.notoptimize: print('Came into Short Trailstop at: {}, size: {}'.format(self.data0.datetime.datetime(0), self.position.size))
stop_size = abs(self.position.size) # 13 Mar from self.short_stoploss.size
trail_stop_px = self.data0.close[0]
if self.short_stoploss is not None: self.cancel(self.short_stoploss)
self.short_trailstop = self.buy(exectype=bt.Order.StopTrail, price=trail_stop_px, size=stop_size, trailamount=round(self.ATR_at_exe * 1, 2))
self.short_trailstop.addinfo(name='Short TrailStop Stopped out, initially started trail at: {:.2f}, Trail amt was: {:.2f}'.format(trail_stop_px, round(self.ATR_at_exe * 1, 2)))
self.trail_stop_started = True
helper.py:
import datetime
from pandas_market_calendars import get_calendar
def get_backfill_date(): # I want to fill from last trading day
today = datetime.date.today()
offset = max(1, (today.weekday() + 6) % 7 - 3)
timedelta = datetime.timedelta(offset)
last_biz_date = today - timedelta
return last_biz_date # datetime.date object
def get_tradeable_time(trade_dt, early_close_days, last_buy):
"""
returns the last buy time, or the sell all time depending on
whether its an early close day or not.
:param trade_dt: self.data.datetime.date(0)
:param early_close_days: get from get_early_close_days function below
:param last_buy: boolean to choose whether you want last buy time or sell all time
:return: a time obj for comparison
"""
if trade_dt in early_close_days:
if last_buy == 0: # last buy time
return datetime.time(12, 0) # datetime.time object
elif last_buy == 1: # close all time
return datetime.time(12, 45)
else: # market close time
return datetime.time(12, 59)
else:
if last_buy == 0:
return datetime.time(15, 0) # datetime.time object
elif last_buy == 1:
return datetime.time(15, 45)
else:
return datetime.time(15, 59)
def get_early_close_days():
calendar = get_calendar('NYSE')
now = datetime.datetime.now()
schedule = calendar.schedule(start_date=now - datetime.timedelta(weeks=780),
end_date=now + datetime.timedelta(weeks=100))
return calendar.early_closes(schedule).index
some output for ref:
....
AMD, 211,2021-05-07T05:30:00, 78.19,78.19,78.19,78.19, 77.69,0.03, 76.47,77.16,76.47, 77.69, 0,0.00
TSLA, 211,2021-05-07T05:30:00, 666.02,666.02,666.02,666.02, 666.76,0.53, 650.00,663.01,658.00, 650.00, 0,0.00
AMD, 212,2021-05-07T05:35:00, 78.19,78.19,78.15,78.15, 77.70,0.03, 76.47,77.16,76.47, 77.70, 0,0.00
TSLA, 212,2021-05-07T05:35:00, 666.02,666.02,666.02,666.02, 666.76,0.49, 650.00,663.01,658.00, 650.00, 0,0.00
AMD, 213,2021-05-07T05:40:00, 78.17,78.18,78.17,78.18, 77.70,0.03, 76.47,77.16,76.47, 77.70, 0,0.00
TSLA, 213,2021-05-07T05:40:00, 667.00,667.00,667.00,667.00, 666.76,0.53, 650.00,663.01,658.00, 650.00, 0,0.00
AMD, 214,2021-05-07T05:45:00, 78.18,78.18,78.18,78.18, 77.71,0.03, 76.47,77.16,76.47, 77.71, 0,0.00
TSLA, 214,2021-05-07T05:45:00, 667.00,667.00,667.00,667.00, 666.76,0.49, 650.00,663.01,658.00, 650.00, 0,0.00
AMD, 215,2021-05-07T05:50:00, 78.15,78.15,78.15,78.15, 77.71,0.03, 76.47,77.16,76.47, 77.71, 0,0.00
TSLA, 215,2021-05-07T05:50:00, 666.60,666.60,666.18,666.18, 666.76,0.51, 650.00,663.01,658.00, 650.00, 0,0.00
AMD, 216,2021-05-07T05:55:00, 78.15,78.15,78.15,78.15, 77.72,0.02, 76.47,77.16,76.47, 77.72, 0,0.00
TSLA, 216,2021-05-07T05:55:00, 666.18,666.18,666.18,666.18, 666.75,0.48, 650.00,663.01,658.00, 650.00, 0,0.00
AMD, 217,2021-05-07T06:00:00, 78.15,78.15,78.15,78.15, 77.72,0.02, 76.47,77.16,76.47, 77.72, 0,0.00
TSLA, 217,2021-05-07T06:00:00, 667.30,668.69,667.00,668.69, 666.77,0.62, 650.00,663.01,658.00, 650.00, 0,0.00
AMD, 218,2021-05-07T06:05:00, 78.15,78.15,78.15,78.15, 77.72,0.02, 76.47,77.16,76.47, 77.72, 0,0.00
TSLA, 218,2021-05-07T06:05:00, 668.88,668.88,668.49,668.49, 666.79,0.61, 650.00,663.01,658.00, 650.00, 0,0.00
....
Questions:
a. Like to ask the more experienced here, do you foresee any issues with running this way for multi data live? I did not execute any trades yet. but I seem to be getting live data ok. Indicators here seem also to be calculated fine. As a noob I really get confused with enumerating within strategy.
b. The problem I have using this is the plot. Be it using matplotlib or bokeh, it plots both TSLA and AMD on the same figure... I am out of ideas to get rid or 1. Imagine if I need to trade 20 symbols. The plot will be atrocious.
c. any comments on my general code? always looking to improve.