For code/output blocks: Use ``` (aka backtick or grave accent) in a single line before and after the block. See: http://commonmark.org/help/

Continuous working of limit orders



  • Hi there BT community.

    Been using Backtrader for a few months now, and love it. Such a great resource.

    However I'm not a great coder, and am struggling with a concept for a mean reversion strategy. Any guidance would be much appreciated. To explain, I have created a stationary time series, and would like to continually work limit orders at the calculated top and bottom Bollinger Bands.

    So for buys, say the lower Bollinger is at a price of 10, I would like to be always working a bid to buy at 10, so that I know if I see a close at 9 in the self.data.close[0], I would have a fill in the real world.

    Up to now, I've been using something like:

    if self.data.close[0] < self.bband.lines.bot: 
        self.log('BUY, %.4f' % (self.data.close[0]))
        self.order = self.buy()
    

    But this (as expected) only generating an order after the close is through the lower Bollinger Band.

    I have tried playing with cheat on close, and also to make it a limit order and set the price to be the self.bband.lines.bot[-1]. But I've not managed to get it to work. And it feels like I'm looking for the solution in the wrong place.

    Could anyone help with setting up the strategy? To be clear, I would like to have orders in the market already, so that if the price series trades through my limit orders, I will have a fill at the calculated bollinger band [-1] levels, not the most recent close.

    Many thanks,
    Chris.


  • administrators

    Really, I have tried to decode the information you tried to paste into this message and I couldn't.

    See

    1. The title is "Continous working of limit orders", but the code shown issues a Market order
    2. cheat-on-close is meant only for Market orders, yet it is mentoned.

    Then
    @chrismo said in Continuous working of limit orders:

    So for buys, say the lower Bollinger is at a price of 10, I would like to be always working a bid to buy at 10, so that I know if I see a close at 9 in the self.data.close[0], I would have a fill in the real world.

    You seem to try to let us know that if you see a value of 10 in the lower Bollinger Band, you want to place a buy order with (I guess) Limit price 10.

    @chrismo said in Continuous working of limit orders:

    But this (as expected) only generating an order after the close is through the lower Bollinger Band.

    All orders are generated after the close. Were the orders generated before, you could buy what's not available.

    @chrismo said in Continuous working of limit orders:

    and also to make it a limit order and set the price to be the self.bband.lines.bot[-1]. But I've not managed to get it to work. And it feels like I'm looking for the solution in the wrong place.

    And that's why you show a snippet with Market orders?

    @chrismo said in Continuous working of limit orders:

    so that if the price series trades through my limit orders, I will have a fill at the calculated bollinger band [-1] levels, not the most recent close.

    self.buy(exectype=bt.Order.Limit, price=self.bollinger_band.bot[-1])
    

    ????

    Execution is of course NOT guaranteed. Because it is a Limit order.



  • Hi @backtrader Thanks for the response.

    I should have been more careful with the code snippet I included. Limit order should have been in there, yes.

    The point I wanted to make with it was that I can see what I am doing with respect to code order is not correct.

    I can see that I am waiting for the initial IF statemant to True before submitting the buy order. I just don't know how else to set it up.

    To try and explain what I'm trying to do more clearly:

    The data I am using is 30 minute intervals. I am only interested in the closing prices. Again just focussing on the buy order.

    Running through the data, say the current self.data.close[0] is 12, and the self.bband.lines.bot is 10.

    For the next 30 minutes (i.e. until the next price update), I wish to work a limit buy order at a price of 10.

    Once we have the next() update, if the close is now 9 (or lower), I can be 100% sure that I have a fill at a price of 10 (it had to have traded through my 10 bid for the 9's to have printed). And for any close of >= 10, I will assume I have no fill.

    so for each half hour, the self.bband.lines.top and self.bband.lines.bot levels are updated to work limit orders for the subsequent 30 minutes.

    Does that clear it up? Is that possible to implement?


  • administrators

    @chrismo said in Continuous working of limit orders:

    For the next 30 minutes (i.e. until the next price update), I wish to work a limit buy order at a price of 10.

    When you issue a Limit order and unless the price is a match, it is active until the valid.

    In any case there seems to be some misunderstanding, because there are no "next 30 minutes", because we work with discrete bars, 30-minutes in your case. So the order is evaluated from bar to bar.

    @chrismo said in Continuous working of limit orders:

    Once we have the next() update, if the close is now 9 (or lower), I can be 100% sure that I have a fill at a price of 10

    If that's the case, you will already have been notified of order execution. There is no need to be sure of anything. The fill price can be better than 10 if the opening price formed a gap in your favor.

    @chrismo said in Continuous working of limit orders:

    And for any close of >= 10, I will assume I have no fill.

    There is nothing to assume. There will have been no notification.

    @chrismo said in Continuous working of limit orders:

    so for each half hour, the self.bband.lines.top and self.bband.lines.bot levels are updated to work limit orders for the subsequent 30 minutes.

    The indicators are continuously updated, but the orders are inmutable. You will have to cancel the existing order and issue a new one to match your current requirements.

    @chrismo said in Continuous working of limit orders:

    Is that possible to implement?

    Of course, as pointed out above. Cancel and reissue.



  • Hi again,

    So I have been trying to implement your instructions and, borrowing heavily from backtest-rookies, have the following code. However a problem remains.

    Using this data:
    TestBrackets.csv

    import backtrader as bt
    import matplotlib.pyplot as plt
    from datetime import datetime
     
     
    class BOLLStrat(bt.Strategy):
     
        params = (
            ("period", 50),
            ("devfactor", 1),
            ("size", 10),
            )
     
        def __init__(self):
            self.boll = bt.indicators.BollingerBands(period=self.p.period, devfactor=self.p.devfactor)
            #self.sx = bt.indicators.CrossDown(self.data.close, self.boll.lines.top)
            #self.lx = bt.indicators.CrossUp(self.data.close, self.boll.lines.bot)
     
        def notify_order(self, order):
            date = self.data.datetime.datetime().date()
    
            if order.status == order.Completed:
                print('-'*32,' NOTIFY ORDER ','-'*32)
                print('Order Completed')
                print('{}, Status {}: Ref: {}, Size: {}, Limit: {}, Close: {}'.format(
                                                            date,
                                                            order.status,
                                                            order.ref,
                                                            order.size,
                                                            'NA' if not order.price else round(order.price,5),
                                                            self.data.close[0],
                                                            ))
                print('Created: {} Price: {} Size: {}'.format(bt.num2date(order.created.dt), order.created.price,order.created.size))
                print('-'*80)
    
            if order.status == order.Rejected:
                print('-'*32,' NOTIFY ORDER ','-'*32)
                print('WARNING! Order Rejected')
                print('{}, Status {}: Ref: {}, Size: {}, Limit: {}, Close: {}'.format(
                                                            date,
                                                            order.status,
                                                            order.ref,
                                                            order.size,
                                                            'NA' if not order.price else round(order.price,5),
                                                            self.data.close[0]
                                                            ))
                print('-'*80)
    
        def notify_trade(self, trade):
            date = self.data.datetime.datetime()
            if trade.isclosed:
                print('-'*32,' NOTIFY TRADE ','-'*32)
                print('{}, Profit, Gross {}, Net {}'.format(
                                                    date,
                                                    round(trade.pnl,2),
                                                    round(trade.pnlcomm,2)))
                print('-'*80)
                
        def next(self):
            orders = self.broker.get_orders_open()
     
            # Cancel open orders so we can update the levels
            if orders:
                for order in orders:
                    self.broker.cancel(order)
                    
            # If we are not in a position, work an offer and a bid at the bollinger bands
            if not self.position:
                if self.data.close < self.boll.lines.top:
                    self.sell(exectype=bt.Order.Limit, price=self.boll.lines.top[0], size=self.p.size)
     
                if self.data.close > self.boll.lines.bot:
                    self.buy(exectype=bt.Order.Limit, price=self.boll.lines.bot[0], size=self.p.size)
     
            # If we are in a position, work an exit and a counter trade.
            else:
                if self.position.size > 0:
                    self.close(exectype=bt.Order.Limit, price=self.boll.lines.mid[0])
                    self.sell(exectype=bt.Order.Limit, price=self.boll.lines.top[0])
                    
                elif self.position.size < 0:
                    self.close(exectype=bt.Order.Limit, price=self.boll.lines.mid[0])
                    self.buy(exectype=bt.Order.Limit, price=self.boll.lines.bot[0])
    
     
    #Variable for our starting cash
    startcash = 10000
     
    # Create an instance of cerebro
    cerebro = bt.Cerebro()
     
    # Add our strategy
    cerebro.addstrategy(BOLLStrat)
     
    # Create a Data Feed
    data = bt.feeds.GenericCSVData(
        timeframe=bt.TimeFrame.Days,
        compression=1,
        dataname='location/TestBrackets.csv',
        dtformat=('%m/%d/%Y'),
        datetime=0,
        time=-1,
        high=1,
        low=-1,
        open=1,
        close=1,
        volume=-1,
        openinterest=-1 #-1 means not used
        )
    # Add the data to Cerebro
    cerebro.adddata(data)
     
    # Add a sizer
    cerebro.addsizer(bt.sizers.FixedReverser, stake=10)
     
    # Run over everything
    cerebro.run()
     
    #Get final portfolio Value
    portvalue = cerebro.broker.getvalue()
    pnl = portvalue - startcash
     
    #Print out the final result
    print('Final Portfolio Value: ${}'.format(round(portvalue,2)))
    print('P/L: ${}'.format(round(pnl,2)))
     
    # Finally plot the end results
    cerebro.plot(style='candlestick')
    
    

    The output of the first trade is thus:

    --------------------------------  NOTIFY ORDER  --------------------------------
    Order Completed
    2018-02-26, Status 4: Ref: 6411, Size: -10, Limit: 254.37602, Close: 260.0
    Created: 2018-02-25 23:59:59.999989 Price: 254.37602458033007 Size: -10
    --------------------------------------------------------------------------------
    --------------------------------  NOTIFY ORDER  --------------------------------
    Order Completed
    2018-03-11, Status 4: Ref: 6437, Size: 10, Limit: 212.6, Close: 210.0
    Created: 2018-03-10 23:59:59.999989 Price: 212.6 Size: 10
    --------------------------------------------------------------------------------
    --------------------------------  NOTIFY TRADE  --------------------------------
    2018-03-11 23:59:59.999989, Profit, Gross 500.0, Net 500.0
    --------------------------------------------------------------------------------
    

    This appears to executing at the right times, but there is a problem with the p&l calculation somewhere.

    The first NOTIFY ORDER shows the calculated bollinger.lines.top from the previous discrete bar (the Limit order.price of 254.37602), which I assume is filled at 254.37602 now that the current close is 260.0. We have a short trade.

    Later we have a closing NOTIFY ORDER and the bollinger.lines.mid was calculated as 212.6 with accompanying Limit order. This was filled once the Close was below it. Here it's 210 and we are out.

    Unfortunately NOTIFY TRADE shows a Profit of 500 instead of the expected ((254.37602 - 212.6 )*10= 417.76), which is using the Close prices, not the Limit order.price.

    How do I get the code to use the order.price instead of the next self.data.close[0]? Is the issue because I am using single price data feed? Does it assume the next discrete price is the open and the close, and I am getting improved fill when the next update comes in?

    I tried creating a column labelled Close, and a second one called Open, which was simple the previous close value. But this resulted in no trades at all for some reason. Is this the correct way to go, and I just need to get it to work?

    Again, your time, patience and help are much appreciated.

    Best,
    Chris.



  • If I understand correctly: you passed to backtrader only one price a day (close only) and expected that backtrader executes the order on the other prices, am I right?

    Pass OHLC to get what you expect.


  • administrators

    @chrismo said in Continuous working of limit orders:

    The first NOTIFY ORDER shows the calculated bollinger.lines.top from the previous discrete bar (the Limit order.price of 254.37602), which I assume is filled at 254.37602 now that the current close is 260.0. We have a short trade.

    See, "assuming" when you are trading is a dangerous business. Instead of relying on what you passed, why don't check the actual execution price? order.executed.price. It's in the documents. There is an entire section with several entries about order.

    For excample Docs - Order Creation/Execution

    @chrismo said in Continuous working of limit orders:

    which is using the Close prices, not the Limit order.price.

    That's again your assumption. Check the right things and you will see that the calculations are right.

    @chrismo said in Continuous working of limit orders:

    How do I get the code to use the order.price

    You CANNOT. You can say that you don't want an execution which is worst than the Limit price, but it can be different if the price gaps in your favor.



  • @ab_trader You are correct on all counts! I did in fact expect that behaviour.

    As a trader, if Mondays close in the 10yr T-Note Future is 129'05 and I then work a limit sell at 129'10 for Tuesdays session. If I see the Tuesday close is 129'15, I know I was filled at 129'10. But as I write this, I see this is not a reasonable expectation when using a generic backtester that can take any data as a feed.

    But, as you correctly point out, passing OHLC feed in resolves the problem. Muchas gracias.

    @backtrader Thanks. One day I'll find it easy...


Log in to reply
 

});