happydasch/btoandav20

Missing order notifications when running live

booboothefool opened this issue · 21 comments

When running my algorithm on a live account with real money, the furthest order notification I seem to get is Submitted -> Accepted.

I have logic that executes after a first take profit target (limit order) is hit, so it cannot activate because I never get the Completed notification for that take profit.

Everything seems to work as expected while backtesting and I get all of the notifications including Completed, but they appear to be missing live.

Then your order is still pending. You will receive completed once the order is executed by oanda.

I am saying what I am seeing is that the order is completed, as in I see it go through on the Oanda dashboard/TradingView, but I never receive the notification in backtrader.

the order is entered (it appears in oanda) but did it execute? like did it open a position?

Yes the orders execute as in there was an open position and I won/lost money, haha.

For example:

self.E1 = self.buy(price=entry_price, size=size, exectype=bt.Order.Stop, transmit=False)
self.SL1 = self.sell(price=stoploss_price, size=size, exectype=bt.Order.Stop, transmit=False, parent=self.E1)
self.TP1 = self.sell(price=TP1_price, size=size, exectype=bt.Order.Limit, transmit=True, parent=self.E1)

E1 is Submitted -> Accepted
E1 is hit and it opens a new position (but no Completed)
TP1 is hit and won money (but no Completed)

could you provide some sample code which shows the problem

Goal is to try to move SL2 to breakeven after TP1 is hit. I am logging out every order notification in the beginning and it never seems to print Completed, so order.status == order.Completed logic never executes.

    def notify_order(self, order):
        logs = True
            
        logs and print('{}: Order ref: {} / Type {} / Status {} / ExecType {} / Size {} / Alive {} / Price {} / Position {}'.format(
            self.data.datetime.date(0),
            order.ref,
            'Buy' * order.isbuy() or 'Sell',
            order.getstatusname(),
            order_exectypes[order.exectype],
            order.size,
            order.alive(),
            order.executed.price,
            self.position.size,
        ))

        if order.alive():
            # Submitted, Accepted, Partial
            if order.status in (order.Partial, order.Submitted):
                # otherwise these ^ get stuck sometimes
                if self.E1 and order.ref == self.E1.ref: self.E1 = None
                elif self.E2 and order.ref == self.E2.ref: self.E2 = None
            # pass
        else:
            # Completed, Canceled, Rejected
            if self.E1 and order.ref == self.E1.ref: self.E1 = None
            elif self.SL1 and order.ref == self.SL1.ref: self.SL1 = None
                
            elif self.TP1 and order.ref == self.TP1.ref:
                self.TP1 = None

                if self.params.breakeven:
                    if order.status == order.Completed:
                        logs and print('HIT TP1')
                        if self.SL2 and self.last_entry_price:
                            # if hit TP1, move SL2 to breakeven
                            logs and print('MOVE SL2 TO BREAKEVEN', self.last_entry_price)
                            self.move_stop_loss(self.last_entry_price)
                            self.last_entry_price = None

            elif self.E2 and order.ref == self.E2.ref:
                self.E2 = None

                if self.params.breakeven:
                    if order.status == order.Completed:
                        self.last_entry_price = order.executed.price
                
            elif self.SL2 and order.ref == self.SL2.ref:
                self.SL2 = None

                if self.params.breakeven:
                    if order.status == order.Completed:
                        logs and print('CREATED NEW SL2')
                        # only when the new SL2 gets successfully created, then can we cancel the old one
                        if self.old_SL2:
                            logs and print('CANCEL OLD SL2')
                            self.old_SL2.cancel()
                            self.old_SL2 = None
                
            elif self.TP2 and order.ref == self.TP2.ref:
                self.TP2 = None

                if self.params.breakeven:
                    if order.status == order.Completed:
                        logs and print('HIT TP2')
                        # if SL2 was moved to breakeven after TP1, and therefore it is not of the original transmit group, then we now need to manually cancel it after TP2 is hit
                        if self.SL2 and self.moved_SL2:
                            logs and print('CANCEL NEW SL2')
                            self.SL2.cancel()
                            self.moved_SL2 = False

Goal is to try to move SL2 to breakeven after TP1 is hit. I am logging out every order notification in the beginning and it never seems to get Completed, so order.status == order.Completed logic never executes.

    def notify_order(self, order):
        logs = True
            
        logs and print('{}: Order ref: {} / Type {} / Status {} / ExecType {} / Size {} / Alive {} / Price {} / Position {}'.format(
            self.data.datetime.date(0),
            order.ref,
            'Buy' * order.isbuy() or 'Sell',
            order.getstatusname(),
            order_exectypes[order.exectype],
            order.size,
            order.alive(),
            order.executed.price,
            self.position.size,
        ))

        if order.alive():
            # Submitted, Accepted, Partial
            if order.status in (order.Partial, order.Submitted):
                # otherwise these ^ get stuck sometimes
                if self.E1 and order.ref == self.E1.ref: self.E1 = None
                elif self.E2 and order.ref == self.E2.ref: self.E2 = None
            # pass
        else:
            # Completed, Canceled, Rejected
            if self.E1 and order.ref == self.E1.ref: self.E1 = None
            elif self.SL1 and order.ref == self.SL1.ref: self.SL1 = None
                
            elif self.TP1 and order.ref == self.TP1.ref:
                self.TP1 = None

                if self.params.breakeven:
                    if order.status == order.Completed:
                        logs and print('HIT TP1')
                        if self.SL2 and self.last_entry_price:
                            # if hit TP1, move SL2 to breakeven
                            logs and print('MOVE SL2 TO BREAKEVEN', self.last_entry_price)
                            self.move_stop_loss(self.last_entry_price)
                            self.last_entry_price = None

            elif self.E2 and order.ref == self.E2.ref:
                self.E2 = None

                if self.params.breakeven:
                    if order.status == order.Completed:
                        self.last_entry_price = order.executed.price
                
            elif self.SL2 and order.ref == self.SL2.ref:
                self.SL2 = None

                if self.params.breakeven:
                    if order.status == order.Completed:
                        logs and print('CREATED NEW SL2')
                        # only when the new SL2 gets successfully created, then can we cancel the old one
                        if self.old_SL2:
                            logs and print('CANCEL OLD SL2')
                            self.old_SL2.cancel()
                            self.old_SL2 = None
                
            elif self.TP2 and order.ref == self.TP2.ref:
                self.TP2 = None

                if self.params.breakeven:
                    if order.status == order.Completed:
                        logs and print('HIT TP2')
                        # if SL2 was moved to breakeven after TP1, and therefore it is not of the original transmit group, then we now need to manually cancel it after TP2 is hit
                        if self.SL2 and self.moved_SL2:
                            logs and print('CANCEL NEW SL2')
                            self.SL2.cancel()
                            self.moved_SL2 = False

please post how you create the orders.

    def buy_risk(self):
        stop_dist = self.set_stop_dist()
        entry_dist = self.set_entry_dist()

        entry_price = self.last5high[0] + entry_dist
        stoploss_price = self.l[0] - stop_dist

        R = entry_price - stoploss_price
        R_pips_big = R * 10000
        
        TP1_price = entry_price + (1 * R)
        TP2_price = entry_price + (2 * R)
        
        size = self.calculate_risk_size(R_pips_big)
        
        print('SL %.4f / Price %.4f / BuyStop %.4f / R=%.4f %.4f / TP1=%.4f / TP2=%.4f / size %.4f / Stop Dist %.4f' % (
            stoploss_price,
            self.c[0],
            entry_price,
            R, R_pips_big,
            TP1_price,
            TP2_price,
            size,
            stop_dist,
        ))
                        
        self.E1 = self.buy(price=entry_price, size=size, exectype=bt.Order.Stop, transmit=False)
        self.SL1 = self.sell(price=stoploss_price, size=size, exectype=bt.Order.Stop, transmit=False, parent=self.E1)
        self.TP1 = self.sell(price=TP1_price, size=size, exectype=bt.Order.Limit, transmit=True, parent=self.E1)
        
        self.E2 = self.buy(price=entry_price, size=size, exectype=bt.Order.Stop, transmit=False)
        self.SL2 = self.sell(price=stoploss_price, size=size, exectype=bt.Order.Stop, transmit=False, parent=self.E2)
        self.TP2 = self.sell(price=TP2_price, size=size, exectype=bt.Order.Limit, transmit=True, parent=self.E2)

ok, will look into this later.

for now you could try if bracket orders will work with your code

I could not really replicate your issue. I entered these values for the orders:

        self.E1 = self.buy(price=self.data.close[0], size=100, exectype=bt.Order.Stop, transmit=False)
        self.SL1 = self.sell(price=self.data.close[0] + 0.0005, size=100, exectype=bt.Order.Stop, transmit=False, parent=self.E1)
        self.TP1 = self.sell(price=self.data.close[0] - 0.0002, size=100, exectype=bt.Order.Limit, transmit=True, parent=self.E1)

but these orders get canceled. you can use the code at the bottom to provide a strategy which replicates your issue.

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

# For datetime objects
import datetime
import pytz

# Import the backtrader platform
import backtrader as bt
import btoandav20 as bto
import backtrader.version as btvers



print(btvers.__version__)



class strategy(bt.Strategy):

    '''Initialization '''
    def __init__(self):
        self.live = True

    def log(self, txt, dt=None):
        dt = dt or self.data.datetime.datetime(0)
        print('%s, %s' % (dt.isoformat(), txt))

    ''' Store notification '''
    def notify_store(self, msg, *args, **kwargs):
        print('-' * 50, 'STORE BEGIN', datetime.datetime.now())
        print(msg)
        print('-' * 50, 'STORE END')

    ''' Order notification '''
    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')

    ''' Trade notification '''
    def notify_trade(self, trade):
        print('-' * 50, 'TRADE BEGIN', datetime.datetime.now())
        print(trade)
        print('-' * 50, 'TRADE END')

        if not trade.isclosed:
            return

        self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
            (trade.pnl, trade.pnlcomm))

    ''' Data notification '''
    def notify_data(self, data, status, *args, **kwargs):
        print('*' * 5, 'DATA NOTIF:', data._getstatusname(status), *args)
        if status == data.LIVE:
            self.live = True
        elif status == data.DELAYED:
            self.live = False

    def next(self):
        self.log('Next {}, {} {} {} {}'.format(len(self.data), self.data.open[0], self.data.high[0], self.data.low[0], self.data.close[0]))
        print(self.position)
        if not self.position and self.live:

            self.E1 = self.buy(price=self.data.close[0], size=100, exectype=bt.Order.Stop, transmit=False)
            self.SL1 = self.sell(price=self.data.close[0] + 0.0005, size=100, exectype=bt.Order.Stop, transmit=False, parent=self.E1)
            self.TP1 = self.sell(price=self.data.close[0] - 0.0002, size=100, exectype=bt.Order.Limit, transmit=True, parent=self.E1)

        elif self.live:
            #self.close()
            pass

# Create a cerebro entity
cerebro = bt.Cerebro(quicknotify=True)

# Prepare oanda STORE
storekwargs = dict(
    token="",
    account="",
    practice=True,
)
print(bto.stores)
store = bto.stores.OandaV20Store(**storekwargs)

# Prepare oanda data
datakwargs = dict(
    timeframe=bt.TimeFrame.Seconds,
    compression=5,
    tz=pytz.timezone('Europe/Berlin'),
    backfill=False,
    backfill_start=False,
)
data = store.getdata(dataname="EUR_USD", **datakwargs)
#data.resample(timeframe=bt.TimeFrame.Seconds, compression=30,rightedge=True,boundoff=1)
cerebro.adddata(data)

# Add broker
cerebro.setbroker(store.getbroker())

# Add strategy
cerebro.addstrategy(strategy)

cerebro.run()

Also please Check if the both stop orders (one with entry price) are really stop orders or if the first one should be a limit order.

Hi if you did:

        self.E1 = self.buy(price=self.data.close[0], size=100, exectype=bt.Order.Stop, transmit=False)
        self.SL1 = self.sell(price=self.data.close[0] + 0.0005, size=100, exectype=bt.Order.Stop, transmit=False, parent=self.E1)
        self.TP1 = self.sell(price=self.data.close[0] - 0.0002, size=100, exectype=bt.Order.Limit, transmit=True, parent=self.E1)

It is a Buy position, so SL1 stop loss should be below, TP1 take profit should be above. So maybe just:

        self.E1 = self.buy(price=self.data.close[0] + 0.0005, size=100, exectype=bt.Order.Stop, transmit=False)
        self.SL1 = self.sell(price=self.data.close[0] - 0.0005, size=100, exectype=bt.Order.Stop, transmit=False, parent=self.E1)
        self.TP1 = self.sell(price=self.data.close[0] + 0.0010, size=100, exectype=bt.Order.Limit, transmit=True, parent=self.E1)

And yes, it should be:

  • Entry: Buy Stop (to buy when price goes up and hits the price)
  • Stop Loss: Sell Stop (below)
  • Take Profit: Sell Limit (above)

But from what I have seen, it doesn't matter what the order type is, I never see Completed status when running live even with regular Market order instead of Stop order e.g. self.E1 = self.buy(size=100, exectype=bt.Order.Market, transmit=False).

I am not really sure, how to send this type of orders. When sending your orders, oanda will reject it with CLIENT_ORDER_ID_ALREADY_EXISTS

i committed some changes today, you can check them out. you will get better error codes and descriptions for further investigation. the issue seems to be that either the stores creates the stop order wrong or there is an other issue.

CLIENT_ORDER_ID_ALREADY_EXISTS: The client Order ID specified is already assigned to another pending Order

these are the generated order details:

{'instrument': 'EUR_USD',
 'units': 100,
 'type': 'STOP',
 'price': '1.11979',
 'timeInForce': 'GTC',
 'stopLossOnFill': {'price': '1.11879', 'timeInForce': 'GTC', 'clientExtensions': {'id': '2'}},
 'takeProfitOnFill': {'price': '1.12029', 'timeInForce': 'GTC', 'clientExtensions': {'id': '3'}},
 'clientExtensions': {'id': '1'}}

the clientExtensions id is the id of the order in backtrader.

you can check out https://developer.oanda.com/rest-live-v20/transaction-df/ for the needed details for the type of orders you would like to submit.

Would need a better explanation of what you want to achieve. Will wait for more details.

Thanks, I appreciate you exposing the errors! Ok, I will look into this more. Yes, I've been struggling so much with the manual brackets that I just switched to Market orders, and it turns out my algorithm actually performs better...

I heard back from Oanda support. They said what you said:

1) Transaction no.: 40749

Reason for Stop Order Reject: TRAILING_STOP_LOSS_ON_FILL_PRICE_DISTANCE_MINIMUM_NOT_MET
*Please note that Trailing Stop must be at least 5 pips away however you had it at 3 pips away. Hence, it was rejected

2) Transaction no.: 40748, 40747

Reason for Stop Order Reject: CLIENT_ORDER_ID_ALREADY_EXISTS

*Wet cannot create a new Stop order using an order ID that already exists from past orders. Please  use a new order ID. (e.g Transaction #: 40747:  You are trying to create a Stop order with ID 25 but the order ID 25 already exists from past transaction.).

As for more explanation/details, was basically just trying to do the risk management strategy as shown here: https://youtu.be/zhEukjCzXwM?t=747

we could ensure, that the id is unique, so adding some unique key for id, which changes when you restart the script

As for more explanation/details, was basically just trying to do the risk management strategy as shown here: https://youtu.be/zhEukjCzXwM?t=747

for that, you could achieve this by setting limit orders in opposite direction. you would need to cancel the order once you would set a new order when the price reaches new high (or low).

added a unique client id, now you will not get CLIENT_ORDER_ID_ALREADY_EXISTS messages, for the other issues, Stop orders work, will close this issue.

@ booboothefool
I did not fully go through your post here, but today I will open an issue but you can check my code, I think it is something along the lines of what u want to do.