erdewit / ib_insync

Python sync/async framework for Interactive Brokers API
BSD 2-Clause "Simplified" License
2.76k stars 726 forks source link

Assistance with a custom order type! Turning an initial SL into a TRAILING SL from a BRACKET order? #629

Closed windowshopr closed 10 months ago

windowshopr commented 11 months ago

Hey!

I've been working on this for a few hours, but am stuck on understanding if my syntax is correct, and given that the markets are usually closed when I get the chance to work on this, it's hard to test, so I'm hoping the gurus here can lend a hand.

I am creating a Flask endpoint that receives some Options order information to place a new options order. This endpoint spins up a new thread where it handles everything from order submission to actively monitoring the position.

Initially, I place a bracket order with a LimitOrder buy order, a LimitOrder TP, and a StopOrder for the SL. I make this a bracket order being mindful of the transmit property. You'll notice in the code that I have 2 TP levels, and I'll explain why in a bit, but for the initial order, I submit the higher TP level (TP2) along with the stop loss, the logic here being, if for some reason my connection to TWS Gateway is interrupted and my program can no longer actively manage the position, the "fail safes" are in place with the broker.

With the order submitted, what I'm doing is watching the bid price of the contract. If the bid price >= TP1 level (not TP2, but TP1), I want to modify the SL from a StopOrder to a TrailingStopOrder, such that once the price hits the first TP level, the SL is adjusted to break even/the entry limit price, and as the bid moves higher, so does the SL.

I'm confused about the differences between auxPrice, trailStopPrice, trailingPercent, etc., and I'm not sure how to implement what I'm looking for. I've spent some time on this and have provided some nice comments to follow along with so I'm hoping someone here can help me set this up properly! Can be read pretty much from top to bottom to follow along with the logic as I've described it.

Working with Python 3.8, Windows 10, IB Gateway, latest version of ib_insync, etc.

Thanks!!!

# Route for accepting POST requests to open a new position
@app.route('/post-order', methods=['POST'])
def post_order():
    # Get the option order details from the request. Assume the frontend is
    # handling the validation of the data
    data = request.json
    ticker_symbol = str(data.get('ticker_symbol')).upper()
    expiry = str(data.get('expiry'))
    strike = float(data.get('strike'))
    call_or_put = str(data.get('call_or_put')).upper()
    order_size = int(data.get('order_size'))
    limit_entry_price = float(data.get('limit_entry_price'))
    stop_loss_price = float(data.get('stop_loss_price'))
    take_profit_1_price = float(data.get('take_profit_1_price'))
    take_profit_2_price = float(data.get('take_profit_1_price'))

    # Create a contract for the option via ticker symbol
    contract = Option(ticker_symbol, expiry, strike, call_or_put, 'SMART')
    ib.qualifyContracts(contract)

    # Start - Function to build and submit the bracket order, called with the start of the thread
    def build_and_submit_bracket_order():

        # Create the initial bracket order
        parent_order = LimitOrder(action='BUY', totalQuantity=order_size, lmtPrice=limit_entry_price)
        parent_order.transmit = False

        take_profit_2_order = LimitOrder(action='SELL', totalQuantity=order_size, lmtPrice=take_profit_2_price)
        take_profit_2_order.transmit = False

        stop_loss_order = StopOrder(action='SELL', totalQuantity=order_size, stopPrice=stop_loss_price)
        stop_loss_order.transmit = True

        # Place the bracket order
        bracket_order = BracketOrder(parent=parent_order, takeProfitLimit=take_profit_2_order, stopLoss=stop_loss_order)
        trades = ib.placeOrder(contract, bracket_order)

        # Wait until there's a log entry for the entry order
        while not trades[0].log:
            ib.sleep(1)

        # Wait until the entry order is filled
        while True:
            if trades[0].log[-1].status == 'Filled':
                # Start monitoring bid price/manage the order once filled
                monitor_bid_price_manage_order(trades)
                break
            ib.sleep(1)

    # Function to monitor bid price and manage position
    def monitor_bid_price_manage_order(trades):
        #|Start requesting of market data
        ib.reqMktData(contract, '', False, False)
        # Get the ticker object to monitor
        ticker = ib.ticker(contract)
        while True:
            # Wait for the bid price to surpass the first tp level
            if ticker.bid >= take_profit_1_price:
                # Turn the original stop loss order into a trailing stop loss order,
                # with the trigger price set to the original entry price (or "break even"), 
                # and as the bid price increases, the stop price will trail upwards at the same rate
                trades[2] = Order(orderId=trades[2].orderId, 
                                  action='SELL', 
                                  totalQuantity=order_size, 
                                  orderType='TRAIL',
                                  auxPrice=limit_entry_price)
                trades[2].transmit = True
                ib.placeOrder(trades[2].orderId, contract, trades[2])
                break
            ib.sleep(1)

    # Define a function to wait for the order to close either by hitting the stop loss
    # or take profit price, and ib.cancelMktData(contract) to stop the market data, and
    # then stop the thread (work in progress)

    # Start monitoring the entry order status in a separate thread
    entry_order_thread = threading.Thread(target=build_and_submit_bracket_order)
    entry_order_thread.start()
    # Stop the thread once completed
    entry_order_thread.join()

    return jsonify({'message': 'Order placed successfully'})
windowshopr commented 11 months ago

To make this issue even easier, how could I use ib_insync to place a trailing stop loss order as detailed on this page under the "Mosaic Example" section:

Mosaic Example

There you can define a trailing amount as a dollar value ($0.25 in that example), but I can only see a trailing percentage in the documentation for ib_insync? Maybe?

windowshopr commented 11 months ago

To update, this is the best I have so far:

# Route for accepting POST requests to open a new position
@app.route('/post-order', methods=['POST'])
def post_order():
    # Get the option order details from the request. Assume the frontend is
    # handling the validation of the data
    data = request.json
    ticker_symbol = str(data.get('ticker_symbol')).upper()
    expiry = str(data.get('expiry'))
    strike = float(data.get('strike'))
    call_or_put = str(data.get('call_or_put')).upper()
    order_size = int(data.get('order_size'))
    limit_entry_price = float(data.get('limit_entry_price'))
    stop_loss_price = float(data.get('stop_loss_price'))
    take_profit_1_price = float(data.get('take_profit_1_price'))

    # Create a contract for the option via ticker symbol
    contract = Option(ticker_symbol, expiry, strike, call_or_put, 'SMART')
    ib.qualifyContracts(contract)

    # Create the initial bracket order, good for the day
    parent_order = LimitOrder(action='BUY', 
                              totalQuantity=order_size, 
                              lmtPrice=limit_entry_price,
                              tif='DAY')
    parent_order.transmit = False

    # Create the take profit order
    take_profit_1_order = LimitOrder(action='SELL', 
                                        totalQuantity=order_size, 
                                        lmtPrice=take_profit_1_price,
                                        ocaGroup=ticker_symbol + "_OCA",
                                        tif='GTC')
    take_profit_1_order.transmit = False

    # Create a custom trailing stop loss order that is part of the same
    # OCA group as the take profit order
    stop_loss_order = Order(action='SELL', 
                            orderType="TRAIL", 
                            totalQuantity=order_size, 
                            auxPrice=round(limit_entry_price - stop_loss_price, 2),
                            ocaGroup=ticker_symbol + "_OCA",
                            tif='GTC')
                            )
    stop_loss_order.transmit = True

    # Place the bracket order
    bracket_order = BracketOrder(parent=parent_order, takeProfit=take_profit_1_order, stopLoss=stop_loss_order)
    trades = ib.placeOrder(contract, bracket_order)

    # Wait until there's a log entry for the entry order
    while not trades[0].log:
        ib.sleep(1)

    # Wait until the entry order is filled. This is not the appropriate way to do this,
    # but for this example it's fine.
    while True:
        if trades[0].log[-1].status == 'Filled':
            break
        ib.sleep(1)

    return jsonify({'message': 'Order placed successfully'})

...which I believe closely matches this setup:

image

erdewit commented 10 months ago

The IB documentatation for orders can be found here:

https://interactivebrokers.github.io/tws-api/orders.html