Why doesn't my 'buy' trigger?
-
I have a very simple system, but I'm running into a small problem. I'm using an if/else block to facilitate my buys and sells.
and its structured like this:
if not self.position: if (entry signal): print('entry signal triggered') self.buy()
now I'm finding that the buy function isn't following through all the time, sometimes it works and sometimes it doesn't. I'm tracking the trades via order.status. sometimes order.status reports 'BUY EXECUTED' other times it doesn't while the if/else blocks still prints 'entry signal triggered'. this is again confirmed on the plot where I can visually see a buy should have been triggered, but it's not.
Am I missing something about the self.buy() function?
-
It would be helpful to share your code.
-
Here's my code:
class HighBreakout(bt.Strategy): params = (('max', 100), ('min', 100), ('Long_MA', 200), ('Short_MA', 50), ('oneplot', True) ) def log(self, txt, dt=None): ''' Logging function for this strategy''' dt = dt or self.datas[0].datetime.date(0) print('%s, %s' % (dt.isoformat(), txt)) def __init__(self): #Keep a reference to the "close" line in the data[0] dataseries self.inds = dict() self.holding = 0 self.buyprice = 0 self.sellprice = 0 self.wins = 0 self.losses = 0 self.totalwin = 0 self.totalloss =0 print(self.params.max) for i, d in enumerate(self.datas): self.inds[d] = dict() self.inds[d]['MAX'] = bt.talib.MAX(d.close, timeperiod=self.params.max) self.inds[d]['MIN'] = bt.talib.MIN(d.close, timeperiod=self.params.min) self.inds[d]['sma_long'] = bt.indicators.SMA(d.close, period=self.params.Long_MA) self.inds[d]['sma_short'] = bt.indicators.SMA(d.close, period=self.params.Short_MA) if i > 0: # Check we are not on the first loop of data feed: if self.p.oneplot == True: d.plotinfo.plotmaster = self.datas[0] def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: # Buy/Sell order submitted/accepted to/by broker - Nothing to do return # Check if an order has been completed # Attention: broker could reject order if not enough cash if order.status in [order.Completed]: if order.isbuy(): self.log( 'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % (order.executed.price, order.executed.value, order.executed.comm)) print(f'max prev = {self.max_prev} > max prev prev = {self.max_prev_prev}') self.buyprice = order.executed.price self.buycomm = order.executed.comm else: # Sell self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % (order.executed.price, order.executed.value, order.executed.comm)) self.bar_executed = len(self) # Write down: no pending order self.order = None def notify_trade(self, trade): if not trade.isclosed: return self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' % (trade.pnl, trade.pnlcomm)) if trade.pnl > 0: self.totalwin += trade.pnl if trade.pnl < 0: self.totalloss -= trade.pnl if trade.pnl > 0: self.wins += 1 if trade.pnl < 0: self.losses += 1 def next(self): for i, d in enumerate(self.datas): self.C = self.datas[i].close[0] self.O = self.datas[i].open[0] self.L = self.datas[i].low[0] self.H = self.datas[i].high[0] self.C_prev = self.datas[i].close[-1] self.O_prev = self.datas[i].open[-1] self.L_prev = self.datas[i].low[-1] self.H_prev = self.datas[i].high[-1] self.symbol = self.datas[i]._name self.date = self.datas[0].datetime.datetime(0) max_ = self.inds[d]['MAX'][0] self.max_prev = self.inds[d]['MAX'][-1] self.max_prev_prev = self.inds[d]['MAX'][-2] min_prev = self.inds[d]['MIN'][-1] if not self.position: if self.max_prev > self.max_prev_prev: print(f'{self.max_prev} is greater than {self.max_prev_prev}') self.buy() else: if self.L_prev <= min_prev: self.sell() def stop(self): if self.losses + self.wins != 0: batting_average = self.wins / (self.losses + self.wins) percent_return = self.broker.getvalue() / self.broker.startingcash if self.wins != 0: average_win = self.totalwin / self.wins if self.losses != 0: average_loss = self.totalloss / self.losses print('==================================================') print('Starting Value - %.2f' % self.broker.startingcash) print('Ending Value - %.2f' % self.broker.getvalue()) print('Percent Return: %.2f' % percent_return) if self.losses + self.wins != 0: print('Batting average %.2f' % batting_average) print('Wins: %.0f' % self.wins) print('Losses: %.0f' % self.losses) if self.wins != 0: print('Average win: %.2f' % average_win) if self.losses != 0: print('Average loss: %.2f' % average_loss) print('==================================================')
and here's a sample output:
176.38 is greater than 175.37 176.495 is greater than 176.38 2020-04-16, BUY EXECUTED, Price: 175.25, Cost: 11505.49, Comm 0.00 max prev = 176.495 > max prev prev = 176.38 2020-04-21, SELL EXECUTED, Price: 172.00, Cost: 11505.49, Comm 0.00 2020-04-21, OPERATION PROFIT, GROSS -213.51, NET -213.51 175.89 is greater than 174.84 175.94 is greater than 175.89 176.07 is greater than 175.94 176.215 is greater than 176.07 176.23 is greater than 176.215 176.41 is greater than 176.23 176.93 is greater than 176.41 177.415 is greater than 176.93 177.51 is greater than 177.415 2020-04-29, BUY EXECUTED, Price: 177.24, Cost: 11298.87, Comm 0.00 max prev = 177.51 > max prev prev = 177.415 2020-05-13, SELL EXECUTED, Price: 182.43, Cost: 11298.87, Comm 0.00 2020-05-13, OPERATION PROFIT, GROSS 331.01, NET 331.01 185.745 is greater than 185.57 2020-05-18, BUY EXECUTED, Price: 186.16, Cost: 11629.25, Comm 0.00 max prev = 185.745 > max prev prev = 185.57 2020-05-21, SELL EXECUTED, Price: 183.79, Cost: 11629.25, Comm 0.00 2020-05-21, OPERATION PROFIT, GROSS -147.68, NET -147.68 184.175 is greater than 184.04 2020-05-29, BUY EXECUTED, Price: 183.81, Cost: 11482.20, Comm 0.00 max prev = 184.175 > max prev prev = 184.04 ================================================== Starting Value - 10000.00 Ending Value - 12299.57 Percent Return: 1.23 Batting average 0.50 Wins: 7 Losses: 7 Average win: 373.73 Average loss: 161.99 ==================================================
you can see the code passes through the if/else block and prints the "XX is greater than XX", but does not execute the buy operation despite the code calling the function.
-
is there enough cash?
def log(self, arg): print('{} {}'.format(self.datetime.date(), arg)) # This section is for logging of orders in greater detail to figure out whether the strategy is actually having no problem with orders def notify_order(self, order): if order.status in [order.Accepted]: # Buy/Sell order submitted/accepted to/by broker - Nothing to do return if order.status in [order.Submitted]: if order.isbuy(): dt, dn = self.datetime.date(), order.data._name print('Buy {} {} {} Price {:.2f} Value {:.2f} Size {} Cash {:.2f}'.format( order.getstatusname(), dt, dn, order.created.price, order.created.size * order.created.price , order.created.size, self.broker.getcash())) if order.issell(): dt, dn = self.datetime.date(), order.data._name print('Sell {} {} {} Price {:.2f} Value {:.2f} Size {}'.format( order.getstatusname(), dt, dn, order.created.price, order.created.size * order.created.price, order.created.size)) # Buy/Sell order submitted/accepted to/by broker - Nothing to do return # Check if an order has been completed # Attention: broker could reject order if not enough cash if order.status in [order.Completed]: if order.isbuy(): dt, dn = self.datetime.date(), order.data._name print('Buy {} {} Price {:.2f} Value {:.2f} Size {}'.format( dt, dn, order.executed.price, order.executed.value, order.executed.size)) if order.issell():# Sell dt, dn = self.datetime.date(), order.data._name print('Sell {} {} Price {:.2f} Value {:.2f} Size {}'.format( dt, dn, order.executed.price, order.executed.value, order.executed.size)) elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log('Order Canceled/Margin/Rejected')
Code to have a closer look at the orders.
-
There is enough cash, here's a snip of the graph to visually demonstrate the issue I'm facing.
The intent is to make a purchase every time the 'max' indicator makes a local high. It's passing over a number of new highs and then making a purchase at a seemingly random time, but still a new high. This graph tells me its not something wrong with my data or the way I've implemented the indicators, but probably something wrong with the way I've implemented the backtrader buy functionality. I just cant wrap my head around what the issue might be
-
@Chris-Cappel Add a time stamp in front of
print(f'{self.max_prev} is greater than {self.max_prev_prev}')
and implement the code (def notify_order(self, order):) as suggested by @Jonny8 then share your log so we can see what's going on. Tough to tell without this information. -
Good call. The issue was I didn't have enough cash. Odd because size should be determined by total cash/price.
Even odder that this buy-order didn't fill.
Buy Submitted 2018-11-26 Price 105.81 Value 9324.90 Size 88.12458567695634 Cash 9324.90
Among the results I saw orders like this, which clearly, correctly shouldn't have filled (and didn't):
Buy Submitted 2018-11-26 Price 105.81 Value 9328.43 Size 88.15791097525063 Cash 9324.90
I've use a the workaround below to continue testing the efficiency of the strategy (previously 100%):
cerebro.addsizer(bt.sizers.PercentSizer, percents=99)
Thanks for the help everyone!