Issue with Stop-Loss on Multiple Stock Datas



  • Hi,

    I have a strategy which assesses multiple stocks (datas) and enters a long position only if a certain criteria is met in the next statement.

    My issue is I want to have Stop-loss and take-profit for all long positions to cap losses, I am trying to do this utilising only the "notify_order" and "Notify_trade" backtrader functionalities.

    I have been fidling arround with this for AGES and cant seem to get it write... some how this code is generating multiple duplicate orders, and actually causing huge unexplainable losses (-1000%) on a strategy which was previously possitive. I must be missing something significant! Any help you could provide as you cast your eyes over my code for "notify_trade" and "notify_Order" would be greatly appreciated!!

    def parse_args():
        parser = argparse.ArgumentParser(description='MultiData Strategy')
    
        parser.add_argument('--data0', '-d0',
                            default=r'T:\PD_Stock_Data\DBLOAD',
                            help='Directory of CSV data source')
    
    
    
        parser.add_argument('--stoploss',
                            action='store',
                            default=0.05, type=float,
                            help=('sell a long position if loss exceeds'))
    
        parser.add_argument('--takeprofit',
                            action='store',
                            default=0.5, type=float,
                            help=('Exit a long position if profit exceeds'))
    
    
    # Create a Strategy. Defn "Class": a logical grouping of data and functions (the later of which are frequently referred to as "methods" when defined within a class). Classes can be thought of as blueprints for creating objects. https://jeffknupp.com/blog/2014/06/18/improve-your-python-python-classes-and-object-oriented-programming/
    class TestStrategy(bt.Strategy):
        # Attributes which apply to ALL instances of a Class object are defined here, i.e. prior to __init__
        args = parse_args()
        params = (
            ('printlog', True),
            ('prtrade', False),
        )
    
        def log(self, txt, dt=None, doprint=False):
            ''' Logging function for this strategy'''
            if self.p.printlog or doprint:  # NB: self.p = self.params
                dt = dt or self.datas[0].datetime.date(0)
                print('%s, %s' % (dt.isoformat(), txt))
    
        def __init__(self):  # __init__ = creates object defined by the Class. INITIALISES the object - sets all attributes
            self.order = {}
            self.order_sl = {}
            self.order_tkp = {}
            self.buyprice = {}
            self.buycomm = {}
            self.bar_executed = {}
    
            for i, d in enumerate(d for d in self.datas):
                self.order[d._name] = None
                self.buyprice[d._name] = None
                self.buycomm[d._name] = None
                self.bar_executed[d._name] = None
    
    
        def start(self):
            pass
    
        #Receives an order whenever there has been a change in one
        def notify_order(self, order):  # Methods (def in Class) have access to all the data contained on the instance of the object; they can access and modify anything previously set on self.
            if order.status in [order.Submitted, order.Accepted]:
                return # Buy/Sell order submitted/accepted to/by broker - Nothing to do
    
            elif order.status == order.Margin:
                #self.bar_executed[order.data._name] = len(self) #length of dataframe when enter market (used to calc days held)
                pass
    
            elif order.status == order.Rejected:
                # **Attention: broker could reject order if not enough cash**
                self.log('REJECTED: %s, Ref: %s, Price: %.2f, Cost: %.2f, Size: %.2f, Comm %.2f' %(order.data._name, order.ref, order.executed.price, order.executed.value, order.executed.size, order.executed.comm))
    
            elif order.status == order.Completed:
    
                #ENTERING MARKET: LONG POSITION
                if order.isbuy():
                    self.log('BUY EXECUTED: %s, Ref: %s, Price: %.2f, Cost: %.2f, Size: %.2f, Comm %.2f' %(order.data._name,
                                                                                      order.ref,
                                                                                      order.executed.price,
                                                                                      order.executed.value,
                                                                                      order.executed.size,
                                                                                      order.executed.comm))
    
                    stop_loss = order.executed.price*(1.0 - args.stoploss)
                    take_profit = order.executed.price*(1.0 + args.takeprofit)
    
    
                    sl_ord = self.sell(data=order.data,
                                       exectype=bt.Order.Stop,
                                       price=stop_loss)
                    sl_ord.addinfo(name="Stop")
    
                    tkp_ord = self.sell(data=order.data,
                                        exectype=bt.Order.Limit,
                                        price=take_profit)
                    tkp_ord.addinfo(name="Prof")
    
                    self.order_sl[order.data._name] = sl_ord
                    self.order_tkp[order.data._name] = tkp_ord
    
                    self.bar_executed[order.data._name] = len(self) #length of dataframe when enter market (used to calc days held)
                    self.order[order.data._name] = None
    
                elif order.issell():
                    #ENTERING MARKET: SHORT POSITION
                    self.log('SELL EXECUTED: %s, Ref: %s, Price: %.2f, PNL: %.2f, Purchase Cost: %.2f, Comm %.2f' %(order.data._name,
                                                                                                                    order.ref,
                                                                                                                    order.executed.price,
                                                                                                                    order.executed.pnl,
                                                                                                                    order.executed.value,
                                                                                                                    order.executed.comm))
                    stop_loss = order.executed.price*(1.0 + args.stoploss)
                    take_profit = order.executed.price*(1.0 - args.takeprofit)
    
                    sl_ord = self.buy(data=order.data,
                                      exectype=bt.Order.Stop,
                                      price=stop_loss)
                    sl_ord.addinfo(name="Stop")
    
                    tkp_ord = self.buy(data=order.data,
                                       exectype=bt.Order.Limit,
                                       price=take_profit)
                    tkp_ord.addinfo(name="Prof")
    
                    self.order_sl[order.data._name] = sl_ord
                    self.order_tkp[order.data._name] = tkp_ord
                    self.bar_executed[order.data._name] = len(self) #length of dataframe when enter market (used to calc days held)
                    self.order[order.data._name] = None
    
    
            elif order.status == order.Canceled:
                #status.canceled occurs when: a "self.broker.cancel()" has occured by user
                if order == self.order_tkp[order.data._name]:
                    self.log('STOP-LOSS EXECUTED. Cancelling Take-Profit Order: %s, Ref: %s, Price: %.2f, Cost: %.2f, Size: %.2f, Comm %.2f' %(order.data._name, order.ref))
    
                elif order == self.order_sl[order.data._name]:
                    self.log('TAKE-PROFIT EXECUTED. Cancelling Stop-Loss Order: %s, Ref: %s, Price: %.2f, Cost: %.2f, Size: %.2f, Comm %.2f' %(order.data._name, order.ref))
    
                else:
                    self.log('Order Cancelled: %s, Ref: %s, Price: %.2f, Cost: %.2f, Size: %.2f, Comm %.2f' %(order.data._name, order.ref))
    
        #Receives a trade whenever there has been a change in one.  **TRADE = EXITED MARKET POSITION**
        def notify_trade(self, trade):
    
            #MARKET POSITION EXITED
            if trade.isclosed:
                self.log('OPERATION PROFIT: %s, GROSS %.2f, NET %.2f' %(trade.data._name, trade.pnl, trade.pnlcomm))
    
                #If this order has a Stop-Loss or Take-Profit associated, get rid of them
                if self.order_tkp[trade.data._name]: #i.e have a Stop-Loss or Take-Profit open, so in the market
                    print('killing TP')
                    self.broker.cancel(self.order_tkp[trade.data._name]) #cancel associated Take-Profit
                    self.order_tkp[trade.data._name] = None
                    
                if self.order_sl[trade.data._name]:
                    print('killing SL')
                    self.broker.cancel(self.order_sl[trade.data._name]) #cancel associated Stop-Loss
                    self.order_sl[trade.data._name] = None
    

    @ab_trader, perhaps you may have a clue!!

    Cheers,
    CWE



  • I've tried to implement same approach using notify_order() and notify_trade() initially and got crazy results. :) I didn't dig deeply into it, but looks like somehow bt starts to issue stop/take orders on the execution of previous stop/take orders. At the end I had unpredictable mix of regular entries, executed correct stop/take orders and executed incorrect stop/take orders as new entries. Finally I moved all stop/take orders logic to next() and it works. Other approach can be to obtain order reference ID and track them.

    If you use fixed size stop/take levels and don't change them while the position is open, you may check out new OCO orders implemented recently. I think that can work better.

    If you would add some code to finalize the strategy so It can be run easily, I can look into your code more carefully.



  • Thanks @ab_trader, very interesting you had the same issue!!! Would be great to get some bt feedback here. I thought it would make more intuitive sense to have the functionality managed by the notify order and notify trade steps. Any thoughts @administrators @backtrader ?

    Thanks



  • @cwse notify_order() doesn't recognize if it was regular entry order or it was stop/take order. So besides check of the type of order executed (isbuy or issell) additional check should be done if it was entry order executed or not. So maybe something like this:

    if order.isbuy() and order.name != 'Stop' and order.name != 'Prof':
    
        # do your stop/take orders magic
    

    PS not sure if I used order name call correctly. Never used it, shown only as idea.


  • administrators

    The latest commit on the development branch contains support for StopTrail and StopTrailLimit orders.

    It is still untested. At least nothing seems to be broken. Usage pattern:

    # For a StopTrail going downwards
    self.buy(size=1, exectype=bt.Order.StopTrail, trailamount=0.25)  # last price will be used as reference
    # or
    self.buy(size=1, exectype=bt.Order.StopTrail, price=10.50, trailamount=0.25)
    
    # For a StopTrail going upwards
    self.sell(size=1, exectype=bt.Order.StopTrail, trailamount=0.25)  # last price will be used as reference
    # or
    self.sell(size=1, exectype=bt.Order.StopTrail, price=10.50, trailamount=0.25)
    

    One can also specify trailpercent instead of trailamount and the distance to the price will be calculated as a percentage of the price

    # For a StopTrail going downwards with 2% distance
    self.buy(size=1, exectype=bt.Order.StopTrail, trailpercent=0.02)  # last price will be used as reference
    # or
    self.buy(size=1, exectype=bt.Order.StopTrail, price=10.50, trailpercent=0.0.02)
    
    # For a StopTrail going upwards with 2% difference
    self.sell(size=1, exectype=bt.Order.StopTrail, trailpercent=0.02)  # last price will be used as reference
    # or
    self.sell(size=1, exectype=bt.Order.StopTrail, price=10.50, trailpercent=0.02)
    

    This behaves like a Stop order when the stop price is reached. The order is executed as if it were a Market order.

    The same concepts can be applied with StopTrailLimit as exectype. In this case one has to also specify plimit, just like with a StopLimit order. Once the stop price is reached, the execution takes place as a Limit order.

    Commit: https://github.com/mementum/backtrader/commit/cb2dcecf2b055f4669531a059d2dc7a7a93b8430



  • @backtrader, can you please explain what this stoptrail functionality is and does? is it for trailing stop losses?

    Because that is not what this post is about, I am querying static stop losses and take profits and how to make that work with notify_order and notify_trade. Hopefully you can assist here with my above code!!

    Thank you,
    CWE


  • administrators

    @cwse said in Issue with Stop-Loss on Multiple Stock Datas:

    can you please explain what this stoptrail functionality is and does? is it for trailing stop losses?

    See Blog - StopTrail(Limit)

    Posted here in case it could also be useful



  • @backtrader, again, this is trailing, but I am after static... and a solution that works with live trading. Does Backtrader have a solution for this at all? As I am not the only one with issues here.

    Thanks


  • administrators

    The platform goes as far as to try to replicate order types which are broadly available in some brokers. A combined stop + take profit order is not available.

    For anyone to diagnose a potential problem, the following would be useful:

    • The next logic which is issuing the original orders

      It can be any (like a moving average cross). The important thing is to have a complete running and logically connected sample

    • The duplicate operations log produced

    In any case the point made by @ab_trader could be right on spot.

    • On Completed, new Stop and Limit orders are produced, and the status Completed applies also to both of them.

    The extra info name can be tracked, but it could be cheaper to track the built-in ref attribute which provides a unique identifier per order. The ref of the Stop and Limit order can be tied together to the ref of the original order.


  • administrators

    You may also consider the OCO order type to bind the stop and profit-take orders together and avoid the cancellation logic.

    Blog - OCO orders

    With that in mind you only need to keep track of the ref of the order that got you in the market



  • Thanks. Will OCO be supported by interactive brokers in the near future if i go down this avenue?


  • administrators

    Eventually. It's just a matter of decoding the IB fields responsible for it



  • @cwse to answer your question -- Will OCO be supported by interactive brokers in the near future if i go down this avenue?

    There is something better available in IB called bracket order which combines OCO , Stop Loss Order and Take Profit Order together with main order.

    It works like this.
    Issue 3 orders in one go,

    1. The main order (Limit)
    2. Take Profit (Limit)
    3. Stop Loss (this could be actual stop loss or trail or trail limit order)

    All of these are in one group (grouped by parent orders id) and when we create the orders one by one; the transmit flag (of IBOrder) is false for first 2 and should be true on last one. So all of them gets into IB system at once based on last orders transmit=true when you submit them one by one.

    From IB side, Only first main order is sent/live at the exchange (rest 2 are not active), Only when first one is filled next 2 are propagated to exchange. Since these 2 (take profit & stop loss) are OCO; whichever gets filled at exchange first, IB cancels the other one.

    Example:

    1. Main Order - Buy @ Limit price 100 - transmit=false
    2. Take Profit - Sell @ Limit price 110 - transmit=false, m_parentId= main order id.
    3. Stop Loss - Sell @ Stop Loss price 98 - transmit=true, m_parentId= main order id.

    submit all 3, they are entered in IB system, however IB will send only parent order to exchange first. Only when that is executed it will send next 2 as OCO. When one of this is filled (resulting in profit or loss) other is cancelled automatically.



  • @ backtrader and @ab_trader I have copied my code below your assistance in debugging would be greatly appreciated!! As advised ab_trader, I have gone down the avenue of creating the SL and TP in the next() iteration instead of notify order.

    Nonetheless, as my output sample below illustrates I am still getting many duplciate Stop-Loss and Take-Profit orders for no apparent reason!?? Getting desperate here!! So looking forward to your advice on the below!

    My Code:

    from __future__ import (absolute_import, division, print_function,
                            unicode_literals)
    import argparse
    import pandas as pd, numpy as np
    import datetime
    import ntpath
    from scipy.stats import norm
    # import matplotlib.pyplot as plt
    # import matplotlib
    # import matplotlib.dates as mdates
    import operator
    import math
    import sys
    import os
    import scipy.optimize as spo
    import backtrader as bt
    import backtrader.indicators as btind
    import backtrader.feeds as btfeeds
    import glob
    import ntpath
    
    
    def parse_args():
        parser = argparse.ArgumentParser(description='MultiData Strategy')
    
        parser.add_argument('--data0', '-d0',
                            default=r'T:\PD_Stock_Data\DBLOAD',
                            help='Directory of CSV data source')
    
        parser.add_argument('--betaperiod',
                            default=1.4, type=float,
                            help='Per "Inside the Black Box" Mean. 1.4 also outperforms old model of 3months.')
    
        parser.add_argument('--fromdate', '-f',
                            default='2012-01-01',
                            help='Starting date in YYYY-MM-DD format')
    
        parser.add_argument('--todate', '-t',
                            default='2016-12-31',
                            help='Ending date in YYYY-MM-DD format')
    
        parser.add_argument('--buyscore',
                            action='store',  # 0.91884558 #0.92 is best over 10 years..
                            default=0.91, type=float,
                            help=('Min BRM score for a buy'))
    
        parser.add_argument('--stoploss',
                            action='store',
                            default=0.05, type=float,
                            help=('sell a long position if loss exceeds'))
    
        parser.add_argument('--takeprofit',
                            action='store',
                            default=0.5, type=float,
                            help=('Exit a long position if profit exceeds'))
    
        parser.add_argument('--limitpct',
                            action='store',
                            default=0.03, type=float, #DEFAULT: 0.03 (gives good result)
                            help=('For buying at LIMIT, this will only purchase if the price is less than (1+limitpct)*Closing price'))
    
        parser.add_argument('--validdays',
                            action='store',
                            default=7, type=int,
                            help=('The number of days which a buy order remains valid'))
    
        parser.add_argument('--sellscore',
                            action='store',
                            default=-0.91, type=float,
                            help=('Max score for a sell'))
    
        parser.add_argument('--marketindex',
                            default='XJO',
                            help=('XAO = All Ords, XJO = ASX200'))
    
        parser.add_argument('--cash',
                            default=100000, type=int,
                            help='Starting Cash')
    
        parser.add_argument('--pctperstock',
                            action='store', #0.083 = 1/12... i.e. a portfolio of up to 12 stocks
                            default=0.083, type=float, #i.e. 10% portfolio value in each stock
                            help=('Pct of portfolio starting cash to invest in each stock purchase'))
    
        parser.add_argument('--mintrade',
                            default=1000, type=float,
                            help='Smallest dollar value to invest in a stock (if cash level below amount required for pctperstock)')
    
        parser.add_argument('--tradefee',
                            default=10.0, type=float,
                            help='CMC Markets Fee per stock trade (BUY OR SELL)')
    
        # **UNUSED** see BROKER COMMISSION which performs calculation based on purchase amount
        parser.add_argument('--commperc',
                            default=0.002, type=float,
                            help='Percentage commission for operation (0.005 is 0.5%%')
    
        parser.add_argument('--plot', '-p',
                            action='store_true',
                            default=True,
                            help='Plot the read data')
    
        parser.add_argument('--numfigs', '-n',
                            default=1, type=int,
                            help='Plot using numfigs figures')
    
        return parser.parse_args()
    
    # Create a Strategy. Defn "Class": a logical grouping of data and functions (the later of which are frequently referred to as "methods" when defined within a class). Classes can be thought of as blueprints for creating objects. https://jeffknupp.com/blog/2014/06/18/improve-your-python-python-classes-and-object-oriented-programming/
    class TestStrategy(bt.Strategy):
        # Attributes which apply to ALL instances of a Class object are defined here, i.e. prior to __init__
        args = parse_args()
        params = (
            ('printlog', True),
            ('prtrade', False),
        )
    
        def log(self, txt, dt=None, doprint=False):
            ''' Logging function for this strategy'''
            if self.p.printlog or doprint:  # NB: self.p = self.params
                dt = dt or self.datas[0].datetime.date(0)
                print('%s, %s' % (dt.isoformat(), txt))
    
        def __init__(self):  # __init__ = creates object defined by the Class. INITIALISES the object - sets all attributes
            self.order = {}
            self.order_sl = {}
            self.order_tp = {}
            self.bar_executed = {}
            for i, d in enumerate(d for d in self.datas):
                self.order[d._name] = None
                self.order_sl[d._name] = None
                self.order_tp[d._name] = None
                self.bar_executed[d._name] = None
    
    
            # Add a MovingAverageSimple indicator
            #self.sma = bt.indicators.SimpleMovingAverage(self.datas[0], period=self.p.maperiod)
            # Plot Indicators if plot function called
            # bt.indicators.ExponentialMovingAverage(self.datas[0], period=25)
            # bt.indicators.WeightedMovingAverage(self.datas[0], period=25, subplot=True)
            # bt.indicators.StochasticSlow(self.datas[0])
            # bt.indicators.MACDHisto(self.datas[0])
            # rsi = bt.indicators.RSI(self.datas[0])
            # bt.indicators.SmoothedMovingAverage(rsi, period=10)
            # bt.indicators.ATR(self.datas[0], plot=False)
    
    
        def start(self):
            pass
    
        def notify_trade(self, trade):
            '''
            Receives a trade whenever there has been a change in one.  **TRADE = EXITED MARKET POSITION**
            Keeps track of the life of an trade: size, price, commission (and value?) An trade starts at 0 can be
            increased and reduced and can be considered closed if it goes back to 0.The trade can be long (positive size)
            or short (negative size) An trade is not meant to be reversed (no support in the logic for it)
            Note: "print(Trade.__dict__)" gives: {'barclose': 1226, 'justopened': False, 'status': 2, 'size': 0, 'data': <__main__.CustomDataLoader object at 0x0000027B9F0DA198>, 'price': 11.03, 'long': True, 'isclosed': True, 'pnl': 536.0500000000006, 'isopen': False, 'barlen': 6, 'ref': 605, 'historyon': False, 'commission': 20.712469879518075, 'baropen': 1220, 'tradeid': 0, 'dtclose': 736275.0, 'dtopen': 736265.0, 'pnlcomm': 515.3375301204826, 'history': [], 'value': 0.0}
            '''
    
            #MARKET POSITION EXITED
            if trade.isclosed:
                self.log('OPERATION PROFIT: %s, GROSS %.2f, NET %.2f' %(trade.data._name, trade.pnl, trade.pnlcomm))
    
                print(self.order_sl[trade.data._name])
    
                # clear stop loss and take profit order variables for no position state
                if self.order_sl[trade.data._name]:
                    print('cancelling SL')
                    self.broker.cancel(self.order_sl[trade.data._name])
                    self.order_sl[trade.data._name] = None
    
                if self.order_tp[trade.data._name]:
                    print('cancelling TP')
                    self.broker.cancel(self.order_tp[trade.data._name])
                    self.order_tp[trade.data._name] = None
    
        def notify_order(self, order):
            '''Receives an order whenever there has been a change in one
                Methods (def in Class) have access to all the data contained on the instance of the object; they can access and modify anything previously set on self.
                Note: "print(order.executed.__dict__)" gives all components of "Order.Executed": {'comm': 9.8, 'psize': 0, 'remsize': 0, 'dt': 735810.0, 'p2': 1, 'pnl': 0.0, 'pclose': 0.0, 'pricelimit': 0.0, 'value': 8134.0, 'margin': None, 'size': -33200, 'p1': 0, 'pprice': 0.0, 'price': 0.245, 'exbits': deque([<backtrader.order.OrderExecutionBit object at 0x0000020FEA2624E0>])}
            '''
    
            if order.status in [order.Submitted, order.Accepted]: # SUBMITTED: Marks an order as submitted and stores the broker to which it was submitted. ACCEPTED: Marks an order as accepted
                return # Buy/Sell order submitted/accepted to/by broker - Nothing to do
    
            elif order.status == order.Margin: #Marks an order as having met a margin call
                #self.bar_executed[order.data._name] = len(self) #length of dataframe when enter market (used to calc days held)
                print('MARGIN CALL!!!')
                self.order[order.data._name] = None # clear order variable
                return
    
            elif order.status == order.Rejected:
                # **Attention: broker could reject order if not enough cash**
                self.log('ORDER REJECTED: %s, Ref: %s, Price: %.2f, Cost: %.2f, Size: %.2f, Comm %.2f' %(order.data._name, order.ref, order.executed.price, order.executed.value, order.executed.size, order.executed.comm))
    
            elif order.status == order.Completed: #Marks an order as completely filled
    
                if order.isbuy():
                    if order.info['name'] == 'Enter':
                        self.log('BUY EXECUTED - Entered Long Pos: %s, Ref: %s, Price: %.2f, Cost: %.2f, Size: %.2f, Comm %.2f' %(order.data._name,
                                                                                                              order.ref,
                                                                                                              order.executed.price,
                                                                                                              order.executed.value,
                                                                                                              order.executed.size,
                                                                                                              order.executed.comm))
    
                        self.bar_executed[order.data._name] = len(self) #length of dataframe when enter market (used to calc days held)
                        self.order[order.data._name] = None # clear order variable
    
                    elif order.info['name'] == 'Exit':
                        #EXITING MARKET (CLOSING SHORT POSITION) - This code MUST come before "BUY EXECUTED - Entering Long Position" because else TP and SL will be created!
                        self.log('BUY EXECUTED - Closed Short Pos: %s, Ref: %s, Price: %.2f, PNL: %.2f, Purchase Cost: %.2f, Comm %.2f' %(order.data._name,
                                                                                                                                          order.ref,
                                                                                                                                          order.executed.price,
                                                                                                                                          order.executed.pnl,
                                                                                                                                          order.executed.value,
                                                                                                                                          order.executed.comm))
                        self.order[order.data._name] = None # clear order variable
    
                    elif order.info['name'] == 'Stop':
                        #EXITING MARKET (CLOSING SHORT POSITION) - This code MUST come before "BUY EXECUTED - Entering Long Position" because else TP and SL will be created!
                        self.log('STOP-LOSS (BUY) EXECUTED - Exited Market Posn: %s, Ref: %s, Price: %.2f, Cost: %.2f, Size: %.2f, Comm %.2f' %(order.data._name,
                                                                                                                                                order.ref,
                                                                                                                                                order.executed.price,
                                                                                                                                                order.executed.value,
                                                                                                                                                order.executed.size,
                                                                                                                                                order.executed.comm))
                        self.order_sl[order.data._name] = None # clear order variable
    
                    elif order.info['name'] == 'Take':
                        #EXITING MARKET (CLOSING SHORT POSITION) - This code MUST come before "BUY EXECUTED - Entering Long Position" because else TP and SL will be created!
                        self.log('TAKE-PROFIT (BUY) EXECUTED - Exited Market Posn: %s, Ref: %s, Price: %.2f, Cost: %.2f, Size: %.2f, Comm %.2f' %(order.data._name,
                                                                                                                                                  order.ref,
                                                                                                                                                  order.executed.price,
                                                                                                                                                  order.executed.value,
                                                                                                                                                  order.executed.size,
                                                                                                                                                  order.executed.comm))
                        self.order_tp[order.data._name] = None # clear order variable
                    else:
                        print('**BUY ERROR** -  Unknown Transaction Type')
    
    
                elif order.issell():
                    if order.info['name'] == 'Enter':
                        #ENTERING MARKET: SHORT POSITION
                        self.log('SELL EXECUTED - Entered Short Posn: %s, Ref: %s, Price: %.2f, PNL: %.2f, Purchase Cost: %.2f, Comm %.2f' %(order.data._name,
                                                                                                                                             order.ref,
                                                                                                                                             order.executed.price,
                                                                                                                                             order.executed.pnl,
                                                                                                                                             order.executed.value,
                                                                                                                                             order.executed.comm))
                        self.bar_executed[order.data._name] = len(self) #length of dataframe when enter market (used to calc days held)
                        self.order[order.data._name] = None # clear order variable
    
                    elif order.info['name'] == 'Exit':
                        #EXITING MARKET (CLOSING LONG POSITION)
                        self.log('SELL EXECUTED - Closed Long Pos: %s, Ref: %s, Price: %.2f, PNL: %.2f, Purchase Cost: %.2f, Comm %.2f' %(order.data._name,
                                                                                                                                          order.ref,
                                                                                                                                          order.executed.price,
                                                                                                                                          order.executed.pnl,
                                                                                                                                          order.executed.value,
                                                                                                                                          order.executed.comm))
                        self.order[order.data._name] = None # clear order variable
    
                    elif order.info['name'] == 'Stop':
                        #EXITING MARKET (CLOSING SHORT POSITION) - This code MUST come before "BUY EXECUTED - Entering Long Position" because else TP and SL will be created!
                        self.log('STOP-LOSS (SELL) EXECUTED - Exited Market Posn: %s, Ref: %s, Price: %.2f, Cost: %.2f, Size: %.2f, Comm %.2f' %(order.data._name,
                                                                                                                                                order.ref,
                                                                                                                                                order.executed.price,
                                                                                                                                                order.executed.value,
                                                                                                                                                order.executed.size,
                                                                                                                                                order.executed.comm))
                        self.order_sl[order.data._name] = None # clear order variable
    
                    elif order.info['name'] == 'Take':
                        #EXITING MARKET (CLOSING SHORT POSITION) - This code MUST come before "BUY EXECUTED - Entering Long Position" because else TP and SL will be created!
                        self.log('TAKE-PROFIT (SELL) EXECUTED - Exited Market Posn: %s, Ref: %s, Price: %.2f, Cost: %.2f, Size: %.2f, Comm %.2f' %(order.data._name,
                                                                                                                                                  order.ref,
                                                                                                                                                  order.executed.price,
                                                                                                                                                  order.executed.value,
                                                                                                                                                  order.executed.size,
                                                                                                                                                  order.executed.comm))
                        self.order_tp[order.data._name] = None # clear order variable
    
                    else:
                        print('**BUY ERROR** -  Unknown Transaction Type')
    
            elif order.status == order.Cancelled: #Marks an order as cancelled
                #status.canceled occurs when: a "self.broker.cancel()" has occured by user
                if order.info['name'] == 'Take':
                    self.log('ORDER CANCELLED - TP: %s, Ref: %s' %(order.data._name, order.ref))
                    self.order_tp[order.data._name] = None
    
                elif order.info['name'] == 'Stop':
                    self.log('ORDER CANCELLED - SL: %s, Ref: %s' %(order.data._name, order.ref))
                    self.order_sl[order.data._name] = None
    
                else:
                    self.log('ORDER CANCELLED - Unknown reason: %s, Ref: %s' %(order.data._name, order.ref))
    
        def prenext(self):
            '''
            overrides PRENEXT() so that the "NEXT()" calculations run regardless of when each data date range starts.
            Typically PRENEXT would stop NEXT occuring until the minimum period of an indicator has occured, or all dataframe LINES are running.
            '''
            self.next()
    
        def next(self):  # Methods (def in Class) have access to all the data contained on the instance of the object; they can access and modify anything previously set on self.
            weekday = self.getdatabyname(args.marketindex).datetime.date(0).isoweekday() #Monday = 1, Sunday = 7
            if weekday in range(1,8): # analyse on all weekdays (MONDAY to SUNDAY)
                num_long = 0
                for i, d in enumerate(d for d in self.datas if len(d) and d._name !=args.marketindex):  # Loop through Universe of Stocks. "If Len(d)" is used to check that all datafeeds have delivered values. as if using minute data, some may have had many minutes, 500, and another may not have 1 record yet (if its still on daily)
                    position = self.broker.getposition(d)
    
                    if position.size > 0: # Currently LONG
                        daysheld = len(d) - self.bar_executed[d._name] + 1
                        #Log what currently holding
                        self.log('Stock held: %s, Close: %.2f, score: %.2f, scoreyest: %.2f, posn: %.2f, hold days %i' %(d._name,
                                                                                                                         d.close[0],
                                                                                                                         d.lines.TOTAL_SCORE[0],
                                                                                                                         d.lines.TOTAL_SCORE[-1],
                                                                                                                         self.broker.getposition(d).size,
                                                                                                                         daysheld))
    
                        if self.order_sl[d._name] is None and self.order_tp[d._name] is None: #create Stop-Loss and Take-Profit WHEN IN THE MARKET
                            stop_loss = d.close[-1]*(1.0 - args.stoploss)
                            take_profit = d.close[-1]*(1.0 + args.takeprofit)
                            self.order_sl[d._name] = self.order_target_percent(target = 0.0,
                                                               data=d,
                                                               exectype=bt.Order.Stop,
                                                               price=stop_loss).addinfo(name="Stop")
    
                            self.order_tp[d._name] = self.order_target_percent(target = 0.0,
                                                                data=d,
                                                                exectype=bt.Order.Limit,
                                                                price=take_profit).addinfo(name="Take")
    
                        num_long +=1
                        if d.lines.TOTAL_SCORE[0] < args.buyscore:  # NB: Lines[0] = TODAY, lines[-1] = YESTERDAY, lines[-2] = day before yesterday...
                            self.log('CLOSE LONG POSN: %s, Close: %.2f, score: %.2f' %(d._name,
                                                                                       d.close[0],
                                                                                       d.lines.TOTAL_SCORE[0]))
                            self.order[d._name] = self.close(data=d).addinfo(name="Exit")
    
                    elif position.size == 0 and d.lines.TOTAL_SCORE[0] >= args.buyscore: # Currently NOT IN MARKET
                        if self.order[d._name]: #order pending
                            return
                        # BUY!
                        self.log('CREATE BUY: %s, Close: %.2f, score: %.2f' %(d._name,d.close[0],d.lines.TOTAL_SCORE[0]))
                        self.order[d._name] = self.buy(data=d,
                                                       exectype=bt.Order.Limit,
                                                       price=d.close[0]*(1+args.limitpct),
                                                       valid=datetime.datetime.now() + datetime.timedelta(days=args.validdays)).addinfo(name="Enter")
    
                self.log('Stocks held: %s' %(str(num_long)))
    
            def stop(self):
                #Print TOTAL PORTFOLIO VALUE
                self.log('Ending Value %.2f' %(self.broker.getvalue()),doprint=True)
    
    
    
    class PortfolioSizer(bt.Sizer):
        params = (('stake', 1),)
        def _getsizing(self, comminfo, cash, data, isbuy):
            args = parse_args()
            position = self.broker.getposition(data)
            '''NB: position output options:
            - Size
            - Price
            - Price orig
            - Closed
            - Opened
            - Adjbase'''
            price = data.close[0]
            investment = args.cash * args.pctperstock
            if cash < investment:
                investment = max(cash,args.mintrade) # i.e. we will NEVER invest less than the "mintrade" $value in a particular stock
            qty = math.floor(investment/price) #buys quantities in accordance with allocated % per stock, or the remaining cash left over..provided it is greater than the "mintrade" value
    
            if isbuy:  # if buying
                if position.size < 0:  # if currently short, buy the amount which are short to close out trade.
                     return -position.size  # This method returns the desired size for the buy/sell operation
                if position.size > 0:
                    return 0  # dont buy if already hold
                return qty  # num. stocks to buy
    
            if not isbuy:  # if selling..
                if position.size < 0:
                    return 0  # dont sell if already SHORT
                if position.size > 0:
                    return position.size  # was previously a buy... sell what currently hold
                return qty  # num. stocks to SHORT
    
    
    class CustomDataLoader(btfeeds.PandasData):
        lines = ('TOTAL_SCORE', 'Beta',)
        params = (
            ('openinterest', None),     # None= column not present
            ('TOTAL_SCORE', -1),        # -1 = autodetect position or case-wise equal name
            ('Beta', -1))
        datafields = btfeeds.PandasData.datafields + (['TOTAL_SCORE', 'Beta'])
    
    

    Output extract with duplicates:

    2012-11-30, Stock held: FLT, Close: 27.00, score: 1.00, scoreyest: 1.00, posn: 302.00, hold days 9
    2012-11-30, Stock held: MYR, Close: 2.03, score: 0.95, scoreyest: 0.94, posn: 4221.00, hold days 7
    2012-11-30, Stocks held: 3
    2012-12-03, TAKE-PROFIT (SELL) EXECUTED - Exited Market Posn: PPT, Ref: 86, Price: 31.52, Cost: -12889.64, Size: -409.00, Comm 15.53
    2012-12-03, STOP-LOSS (SELL) EXECUTED - Exited Market Posn: DL_GRY, Ref: 2259, Price: 0.59, Cost: -7885.00, Size: -13280.00, Comm 9.50
    2012-12-03, STOP-LOSS (SELL) EXECUTED - Exited Market Posn: MYR, Ref: 2298, Price: 1.99, Cost: 8298.57, Size: -4221.00, Comm 10.13
    2012-12-03, STOP-LOSS (SELL) EXECUTED - Exited Market Posn: MYR, Ref: 2304, Price: 2.00, Cost: -8446.76, Size: -4221.00, Comm 10.18
    2012-12-03, OPERATION PROFIT: MYR, GROSS 110.65, NET 90.52
    None
    2012-12-03, Stock held: CBA, Close: 60.48, score: 0.00, scoreyest: 0.95, posn: 147.00, hold days 30
    2012-12-03, CLOSE LONG POSN: CBA, Close: 60.48, score: 0.00
    2012-12-03, Stock held: FLT, Close: 27.25, score: 1.00, scoreyest: 1.00, posn: 302.00, hold days 10
    2012-12-03, Stocks held: 2
    2012-12-04, SELL EXECUTED - Closed Long Pos: CBA, Ref: 2326, Price: 60.24, PNL: 580.44, Purchase Cost: 8275.27, Comm 10.67
    2012-12-04, OPERATION PROFIT: CBA, GROSS 580.44, NET 559.80
    None
    2012-12-04, Stock held: FLT, Close: 27.36, score: 1.00, scoreyest: 1.00, posn: 302.00, hold days 11
    2012-12-04, Stocks held: 1
    2012-12-05, Stock held: FLT, Close: 27.96, score: 1.00, scoreyest: 1.00, posn: 302.00, hold days 12
    2012-12-05, Stocks held: 1
    2012-12-06, Stock held: FLT, Close: 26.87, score: 0.99, scoreyest: 1.00, posn: 302.00, hold days 13
    2012-12-06, Stocks held: 1
    2012-12-07, TAKE-PROFIT (SELL) EXECUTED - Exited Market Posn: PPT, Ref: 92, Price: 32.25, Cost: -13190.25, Size: -409.00, Comm 15.89
    2012-12-07, TAKE-PROFIT (SELL) EXECUTED - Exited Market Posn: PPT, Ref: 134, Price: 32.49, Cost: -13288.41, Size: -409.00, Comm 16.01
    2012-12-07, Stock held: FLT, Close: 26.83, score: 0.99, scoreyest: 0.99, posn: 302.00, hold days 14
    2012-12-07, Stocks held: 1
    2012-12-10, STOP-LOSS (SELL) EXECUTED - Exited Market Posn: FLT, Ref: 2255, Price: 26.11, Cost: 8289.90, Size: -302.00, Comm 9.50
    2012-12-10, STOP-LOSS (SELL) EXECUTED - Exited Market Posn: FLT, Ref: 2264, Price: 26.02, Cost: -7858.19, Size: -302.00, Comm 9.47
    2012-12-10, STOP-LOSS (SELL) EXECUTED - Exited Market Posn: FLT, Ref: 2308, Price: 26.04, Cost: -7863.93, Size: -302.00, Comm 9.47
    2012-12-10, STOP-LOSS (SELL) EXECUTED - Exited Market Posn: FLT, Ref: 2331, Price: 25.99, Cost: -7849.58, Size: -302.00, Comm 9.46
    2012-12-10, STOP-LOSS (SELL) EXECUTED - Exited Market Posn: FLT, Ref: 2333, Price: 26.56, Cost: -8021.72, Size: -302.00, Comm 9.66
    2012-12-10, OPERATION PROFIT: FLT, GROSS -405.89, NET -425.37
    None
    2012-12-10, Stocks held: 0
    2012-12-11, STOP-LOSS (SELL) EXECUTED - Exited Market Posn: FLT, Ref: 2276, Price: 25.34, Cost: -7651.62, Size: -302.00, Comm 9.22
    2012-12-11, STOP-LOSS (SELL) EXECUTED - Exited Market Posn: FLT, Ref: 2287, Price: 25.13, Cost: -7588.51, Size: -302.00, Comm 9.14
    2012-12-11, STOP-LOSS (SELL) EXECUTED - Exited Market Posn: FLT, Ref: 2296, Price: 25.25, Cost: -7625.80, Size: -302.00, Comm 9.19
    2012-12-11, STOP-LOSS (SELL) EXECUTED - Exited Market Posn: FLT, Ref: 2302, Price: 25.22, Cost: -7617.19, Size: -302.00, Comm 9.18
    2012-12-11, STOP-LOSS (SELL) EXECUTED - Exited Market Posn: FLT, Ref: 2314, Price: 25.82, Cost: -7797.94, Size: -302.00, Comm 9.40
    2012-12-11, STOP-LOSS (SELL) EXECUTED - Exited Market Posn: FLT, Ref: 2320, Price: 25.76, Cost: -7780.73, Size: -302.00, Comm 9.37
    2012-12-11, STOP-LOSS (SELL) EXECUTED - Exited Market Posn: FLT, Ref: 2327, Price: 25.65, Cost: -7746.30, Size: -302.00, Comm 9.33
    2012-12-11, STOP-LOSS (SELL) EXECUTED - Exited Market Posn: FLT, Ref: 2329, Price: 25.89, Cost: -7818.02, Size: -302.00, Comm 9.42
    2012-12-11, STOP-LOSS (SELL) EXECUTED - Exited Market Posn: FLT, Ref: 2335, Price: 25.53, Cost: -7709.00, Size: -302.00, Comm 9.29
    2012-12-11, Stocks held: 0
    2012-12-12, TAKE-PROFIT (SELL) EXECUTED - Exited Market Posn: PPT, Ref: 99, Price: 33.26, Cost: -13601.30, Size: -409.00, Comm 16.39
    2012-12-12, TAKE-PROFIT (SELL) EXECUTED - Exited Market Posn: PPT, Ref: 104, Price: 32.78, Cost: -13404.98, Size: -409.00, Comm 16.15
    2012-12-12, TAKE-PROFIT (SELL) EXECUTED - Exited Market Posn: PPT, Ref: 110, Price: 33.46, Cost: -13687.18, Size: -409.00, Comm 16.49
    2012-12-12, TAKE-PROFIT (SELL) EXECUTED - Exited Market Posn: PPT, Ref: 122, Price: 33.41, Cost: -13662.65, Size: -409.00, Comm 16.46
    2012-12-12, Stocks held: 0
    2012-12-13, Stocks held: 0
    

  • administrators

    A long shot is that you are using something prior to this commit: https://github.com/mementum/backtrader/commit/9b9ae174da282c1e7ea41af240acc73abc5abc3e

    It was introduced to avoid the corner cases generated by mixing things like seen above:

    • Issuing pure buy, sell orders which are controlled with a Sizer
    • Issuing some other orders with the other family of methods: order_target_xxx

    What seems odd (but should be ok) is the constant access to the broker, for things like cancel.



  • @backtrader what are you suggesting I change or do differently here?

    I have to be able to cancel stoploss and ake profit orders and the only way i know fo do that is with the 'broker'.

    I am all ears for another stop loss take profit approach with multiple stocks... which works..!


  • administrators

    That you install something past the mentioned commit because your mixing of different families of order management methods may collide.

    Or that if you issue a buy(data=dataX, size=S) (where S can also later be determined by means of self.getposition(dataX), you then issue a sell(data=dataX ,size=S) (with the appropriate options for take-profit or stop-loss) to close the position.

    At your choice you may also apply close(data=dataX, appropriate_options_for_each_order) which will automatically determine the position for you.

    See Docs - Strategy for the buy, sell and close methods.



  • @backtrader, No luck... using the latest backtrader install and tried various iterations of close / sell / order_target_percentage and still geting rubbish output.

    Another issue seems to be related to when I set the order record dictionaries back to none. If it do it under notify_order where elif order.status = order.completed: I get different results to when I do it under if not order.isalive(): not sure why this is the case?

    However both still give poor results. Are there any iterations of stop-loss take-profit with multiple stocks which have been implemented successfully??? I would love to see an example.

    Thanks


  • administrators

    Bracket orders are implemented including support in IB.

    See Community - Release 1.9.37.117



  • Thanks @backtrader, this will be useful!

    Can you give a demonstration of how to manage the live "bracket orders" when you are in the market?
    i.e. if I am long with multiple stocks, but decide to exit the market for one reason or another, how do I best make sure to cancel the live stop/limit orders?

    For example, in your post https://www.backtrader.com/blog/posts/2017-04-01-bracket/bracket.html

    You would need some logic under

            else:  # in the market
                if (len(self) - self.holdstart) >= self.p.hold:
                    pass  # do nothing in this cas
    

    to exit the market and then manage these orders. Just wondering how best to do this will multiple stocks and therefore many live orders and many different order refs?


Log in to reply
 

Looks like your connection to Backtrader Community was lost, please wait while we try to reconnect.