Live IBKR - multi symbol, different way + plot issue
-
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
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.