happydasch / btoandav20

Support for Oanda-V20 API in backtrader
Apache License 2.0
130 stars 52 forks source link

Single Stop Trail Order #52

Closed einnairo closed 4 years ago

einnairo commented 4 years ago

Hi! As discussed, am having an error putting in a single stoptrail order without brackets.

Code is as below.

import backtrader as bt
import btoandav20
import backtrader.version as btvers

print(btvers.__version__)

class BaseStrategy(bt.Strategy):  # Strategy is a lines object
    params = dict(sizer=None, ticker='EURUSD')
    long_parent_order, long_stoploss, long_partial, long_trailstop = None, None, None, None  

    short_parent_order, short_stoploss, short_partial, short_trailstop = None, None, None, None

    def __init__(self):

        self.order_list = [self.long_parent_order, self.long_stoploss,
                           self.short_parent_order, self.short_stoploss,
                           self.long_trailstop, self.short_trailstop]
        self.action = 'nothing'
        self.one_test_trade = True
        self.move_to_position_px = False
        self.trail_stop_started = False

        self.data0 = self.datas[0]
        # self.data1 = self.datas[1]
        # self.lendata1 = 0

        self.ATR_at_exe = -1
        self.atr = bt.indicators.ATR(self.data0, period=14, plot=False)

    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):  # called when a buy or sell is requested

        if order.status in [order.Canceled]:
            print('Canceled: {}->{}'.format(order.ref, order.info['name']))
            pass

        if order.status in [order.Submitted]:  # status 1
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            print('Order Submitted...', order.ref)
            return

        if order.status in [order.Accepted]:  # status 2
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            print('-' * 65, 'ACCEPTED ORDER', '-' * 65)
            print('{} ACC-D, REF {}, AT {}: {:.2f} shares of {}, Created Price: {:.5f}'.format(
                order.info['name'], order.ref, bt.num2date(order.created.dt), order.created.size, self.p.ticker,
                order.created.price))
            print('-' * 146)
            return

        if order.status in [order.Margin, order.Rejected]:
            print('Order Margin/Rejected', order.ref)
            return

        if order.status in [order.Completed]:  # status 4

            print('-' * 65, 'EXECUTED ORDER', '-' * 65)
            print('{} EXE-D, REF {}: {:.2f} shares {}, Exe Px: {:.5f}, ATR: {:.5f}, Val: {:.2f}, Comm: {:.2f}, PNL: {:.5f}'.format(
                order.info['name'], order.ref, order.executed.size, self.p.ticker,
                order.executed.price, self.ATR_at_exe, order.executed.value, order.executed.comm, order.executed.pnl))
            print('-' * 146)  # bt.num2date(order.executed.dt)

            print('self.position: ', self.position)
            if not self.position:
                print('came in here')
                self.long_parent_order, self.long_stoploss, self.long_partial, self.long_trailstop = None, None, None, None
                self.short_parent_order, self.short_stoploss, self.short_partial, self.short_trailstop = None, None, None, None
                for each_order in self.order_list:
                    self.cancel(each_order) if each_order else None
                if 'TrailStop' in order.info['name']:
                    self.move_to_position_px, self.trail_stop_started = False, False

    def start(self): 
        if self.data0.contractdetails is not None:
            print('-- Contract Details:')
            print(self.data0.contractdetails)
        print('Started')
        acc_cash = self.broker.getcash()
        acc_val = self.broker.getvalue()
        print('Account Cash = {}'.format(acc_cash))
        print('Account Value = {}'.format(acc_val))

    data_live = False

    def next(self):
        logdata('1Min', self.data0)

        # if len(self.data1) > self.lendata1:
        #     self.logdata('3Min', self.data1)
        #     self.lendata1 = len(self.data1)

        if not self.data_live:
            return

        if not self.position:
            if self.one_test_trade:
                self.action = 'long'
                price = self.data0.close[0]
                sl_px = round(price - self.atr[0] * 2, 5)
                sl_pips = round(abs(sl_px - price) / 0.0001, 2)
                self.ATR_at_exe = self.atr[0]
                self.sizer = btoandav20.sizers.OandaV20Risk(risk_percents=2, stoploss=sl_pips)
                exe_size = self.sizer.getsizing(data=self.data0, isbuy=True)

            if self.action == 'long':
                print('sl_pips, ATR: ', sl_pips, self.atr[0])
                print('final exe size: ', exe_size)
                print('prices: ', sl_px, price)

                self.long_parent_order, self.long_stoploss, _ = self.buy_bracket(exectype=bt.Order.Market, size=exe_size, stopprice=sl_px, limitprice=None)
                self.long_parent_order.addinfo(name='Parent Long Bracket Main')
                self.long_stoploss.addinfo(name='Parent Long Bracket Stoploss, sl_px {:.5f}'.format(sl_px))

                self.action = 'nothing'
                self.one_test_trade = False

        elif self.position:
            if self.position.size > 0:  # todo this is long
                # Have not partial closed and price hit 1 x ATR -> partial close 1/2 the size and move stop to position price
                if not self.move_to_position_px and self.data.close[0] >= self.position.price + self.ATR_at_exe * 1:

                    half_size = int(self.position.size/2)
                    size_left = abs(self.position.size - half_size)
                    stop_px = round(self.position.price, 5)
                    self.long_partial = self.sell(exectype=bt.Order.Market, size=half_size)
                    self.long_partial.addinfo(name='Parent Long Partial Close, size left {}'.format(half_size))

                    self.cancel(self.long_stoploss) if self.long_stoploss else None
                    self.long_stoploss = self.sell(price=stop_px, exectype=bt.Order.Stop, size=size_left)
                    self.long_stoploss.addinfo(name='Parent Long Move Stop to Position Price {}, size left {}, next limit {}'.format(stop_px, size_left, self.position.price + self.ATR_at_exe * 1.15)) 
                    self.move_to_position_px = True

                # already partial closed and price hit 2 x ATR -> change to trailing stop
                elif not self.trail_stop_started and self.move_to_position_px and self.data.close[0] >= self.position.price + self.ATR_at_exe * 1.15:
                    stop_size = abs(self.long_stoploss.size)
                    trail_stop_px = self.data0.close[0]
                    self.cancel(self.long_stoploss) if self.long_stoploss else None

                    # THIS ARE THE LINES WHERE I IMPLEMENT TRAILING STOP
                    self.long_trailstop = self.sell(price=trail_stop_px, exectype=bt.Order.StopTrail, size=stop_size, trailamount=self.ATR_at_exe * 1.5) 
                    # self.long_trailstop = self.sell(price=trail_stop_px, exectype=bt.Order.StopTrail, size=stop_size, id='888', trailamount=self.ATR_at_exe * 1.5)  # I tried with an id here. 
                    self.long_trailstop.addinfo(name='Parent Long TrailStop {}, id:{}'.format(trail_stop_px, '888'))
                    self.trail_stop_started = True

def logdata(name, which_data):
    txt = list()
    txt.append(name)
    txt.append('{:4d}'.format(len(which_data)))
    dtfmt = '%Y-%m-%d %H:%M:%S'
    txt.append('{}'.format(which_data.datetime.datetime(0).strftime(dtfmt)))
    txt.append('{:.5f}'.format(which_data.open[0]))
    txt.append('{:.5f}'.format(which_data.high[0]))
    txt.append('{:.5f}'.format(which_data.low[0]))
    txt.append('{:.5f}'.format(which_data.close[0]))
    # txt.append('{:.4f}'.format(which_data.volume[0]))
    print(','.join(txt))

StoreCls = btoandav20.stores.OandaV20Store
BrokerCls = btoandav20.brokers.OandaV20Broker
DataCls = btoandav20.feeds.OandaV20Data

cerebro = bt.Cerebro()

storekwargs = dict(token='', account='101- - -', practice=True)
store = StoreCls(**storekwargs)

broker = BrokerCls(**storekwargs)
cerebro.setbroker(broker)

dataX = store.getdata(dataname='EUR_USD', timeframe=bt.TimeFrame.Minutes, compression=1, qcheck=0.5, backfill_start=True, backfill=False,
                      reconnect=True, reconntimeout=10, bidask=True, bar2edge=True, adjbartime=True, rightedge=True, takelate=True)
cerebro.resampledata(dataX, timeframe=bt.TimeFrame.Minutes, compression=1, name='1M')
# cerebro.resampledata(dataX, timeframe=bt.TimeFrame.Minutes, compression=3, name='3M')

cerebro.addstrategy(BaseStrategy)
strat = cerebro.run(maxcpus=2, exactbars=-1, runonce=False)

error: Traceback (most recent call last): File "C:/Users/bob/Trade/Algo/oanda.py", line 195, in <module> strat = cerebro.run(maxcpus=2, exactbars=-1, runonce=False) File "C:\Users\bob\AppData\Local\Programs\Python\Python38\lib\site-packages\backtrader\cerebro.py", line 1127, in run runstrat = self.runstrategies(iterstrat) File "C:\Users\bob\AppData\Local\Programs\Python\Python38\lib\site-packages\backtrader\cerebro.py", line 1298, in runstrategies self._runnext(runstrats) File "C:\Users\bob\AppData\Local\Programs\Python\Python38\lib\site-packages\backtrader\cerebro.py", line 1630, in _runnext strat._next() File "C:\Users\bob\AppData\Local\Programs\Python\Python38\lib\site-packages\backtrader\strategy.py", line 347, in _next super(Strategy, self)._next() File "C:\Users\bob\AppData\Local\Programs\Python\Python38\lib\site-packages\backtrader\lineiterator.py", line 271, in _next self.next() File "C:/Users/bob/Dropbox/Trade/Algo/oanda.py", line 157, in next self.long_trailstop = self.sell(price=trail_stop_px, exectype=bt.Order.StopTrail, size=stop_size, trailamount=self.ATR_at_exe * 1.5) File "C:\Users\bob\AppData\Local\Programs\Python\Python38\lib\site-packages\backtrader\strategy.py", line 963, in sell return self.broker.sell( File "C:\Users\bob\AppData\Local\Programs\Python\Python38\lib\site-packages\btoandav20\brokers\oandav20broker.py", line 315, in sell return self._transmit(order) File "C:\Users\bob\AppData\Local\Programs\Python\Python38\lib\site-packages\btoandav20\brokers\oandav20broker.py", line 277, in _transmit return self.o.order_create(order) File "C:\Users\bob\AppData\Local\Programs\Python\Python38\lib\site-packages\btoandav20\stores\oandav20store.py", line 360, in order_create okwargs['type'] = self._ORDEREXECS[order.exectype] KeyError: 5

Other actions taken:

Quick change to oandav20store.py

# Order type matching with oanda 
_ORDEREXECS = {
        bt.Order.Market: 'MARKET',
        bt.Order.Limit: 'LIMIT',
        bt.Order.Stop: 'STOP',
        bt.Order.StopLimit: 'STOP',
        bt.Order.StopTrail: 'TRAILING_STOP_LOSS',  # added this line
    }

But bunch of errors. Program continues running. I think there is not enough code to link single trail orders to oanda api.

Pls help us to have a look. Thanks!!

happydasch commented 4 years ago

Other actions taken:

Quick change to oandav20store.py

# Order type matching with oanda 
_ORDEREXECS = {
        bt.Order.Market: 'MARKET',
        bt.Order.Limit: 'LIMIT',
        bt.Order.Stop: 'STOP',
        bt.Order.StopLimit: 'STOP',
        bt.Order.StopTrail: 'TRAILING_STOP_LOSS',  # added this line
    }

But bunch of errors. Program continues running. I think there is not enough code to link single trail orders to oanda api.

This will not work, since we need to process the trail stop transactions correctly. I will add some support this weekend when I get to it.

happydasch commented 4 years ago

I have committed a update to the store which enabled trail stop orders, but when using it, you will need to provide the order id to close when creating an order. Put the order id to cancel in the param "oref".

happydasch commented 4 years ago

Because the order type is not supported by oanda, I am not really sure how to add this else. Here you can see all supported order types in oanda:

http://developer.oanda.com/rest-live-v20/order-df/

happydasch commented 4 years ago

i also see, that the code won't work, since the trail stop order needs to be set for a trade. at the moment, there are no trade ids stored, but I will most probably add them. we then know, which order created an trade. so we can later look, which order created which trade. but to open a trail stop order, you will have to have an open trade, provide the oref to buy or sell call, then it should work.

in code I will look up the trade id, if there is any available for the order.

happydasch commented 4 years ago

Support for StopTrail, StopLimit or StopTrailLimit is only available through bracket_orders, see README.md for details.

happydasch commented 4 years ago

If there are any issues with the implementation open a new issue