ibind icon indicating copy to clipboard operation
ibind copied to clipboard

Not understanding IB open positions and closing them

Open weklund opened this issue 10 months ago • 11 comments

Not technically an ibind issue, but this repo seems to be the best brain trust for understanding the API, outside of spamming IB customer support. Hoping I can get some extra context 😁

My question is 2 part:

  1. How do I go about 'closing' an open position?
  2. Does an API response change based on a particular reason?

1. How do I go about 'closing' an open position?

I'm going through and following the example given in the place order file here I see the response from using client.place_order() from it

{'order_id': '<MY_ORDER_ID>', 'local_order_id': '<MY_LOCAL_ORDER_ID>', 'order_status': 'PreSubmitted', 'encrypt_mes
sage': '1'}

Cool.

So I can double check when the order is filled that it's open with client.positions()

[
    {
        "acctId": "XXXXXXXXX",
        "conid": 672387468,
        "contractDesc": "MNQ      MAR2025",
        "position": 2.0,
        "mktPrice": 21851.8769531,
        "mktValue": 87407.51,
        "currency": "USD",
        "avgCost": 43704.12,
        "avgPrice": 21852.06,
        "realizedPnl": -428.48,
        "unrealizedPnl": -0.73,
        "exchs": null,
        "expiry": null,
        "putOrCall": null,
        "multiplier": null,
        "strike": 0.0,
        "exerciseStyle": null,
        "conExchMap": [],
        "assetClass": "FUT",
        "undConid": 0,
        "model": ""
    }
]

Now I want to close this position. Am I making another order that takes the opposite side and same size?

for position in open_positions:
    if position['conid'] == expected_conid:
        conid = position['conid']
        quantity = position['position']
        side = 'SELL' if quantity > 0 else 'BUY'  # Determine the side to close the position

        order_tag="umcanICloseThis"
        order_type="MKT"

        order_request = make_order_request(
           conid=conid,
           side=side,
           quantity=abs(quantity),
           order_type=order_type,
           acct_id=account_id,
           coid=order_tag
        )

        answers = {
            QuestionType.PRICE_PERCENTAGE_CONSTRAINT: True,
            QuestionType.ORDER_VALUE_LIMIT: True,
            "Unforeseen new question": True,
        }

        # Submit the order
        client.place_order(order_request, answers, account_id)
 

Both open and close orders I made were MKT, but the close orders stayed unfulfilled for a really long time so I feel like I'm missing something.

2. Does an API response change based on a particular reason?

After I opened the position, everytime I called to the status via client.positions() I would get this response:

{
  "acctId": "XXXXXXXXX",
  "conid": 672387468,
  "contractDesc": "MNQ      MAR2025",
  "position": 2.0,
  "mktPrice": 21770.4296875,
  "mktValue": 87081.72,
  "currency": "USD",
  "avgCost": 43536.12,
  "avgPrice": 21768.06,
  "realizedPnl": 0.0,
  "unrealizedPnl": 9.48,
  "exchs": null,
  "expiry": null,
  "putOrCall": null,
  "multiplier": null,
  "strike": 0.0,
  "exerciseStyle": null,
  "conExchMap": [],
  "assetClass": "FUT",
  "undConid": 0,
  "model": ""
}

But other times I'll make the exact same call seconds later but get a different response type

{
  "acctId": "XXXXXXXXX",
  "conid": 672387468,
  "contractDesc": "MNQ      MAR2025",
  "position": 2.0,
  "mktPrice": 21770.4296875,
  "mktValue": 87081.72,
  "currency": "USD",
  "avgCost": 43536.12,
  "avgPrice": 21768.06,
  "realizedPnl": 0.0,
  "unrealizedPnl": 9.48,
  "exchs": null,
  "expiry": "20250321",
  "putOrCall": null,
  "multiplier": 2.0,
  "strike": "0",
  "exerciseStyle": null,
  "conExchMap": [],
  "assetClass": "FUT",
  "undConid": 362687422,
  "model": "",
  "baseMktValue": 69636.34671149254,
  "baseMktPrice": 17409.08642797731,
  "baseAvgCost": 34814.38293585777,
  "baseAvgPrice": 17407.191467928886,
  "baseRealizedPnl": 0.0,
  "baseUnrealizedPnl": 7.580839776992798,
  "incrementRules": [
    {
      "lowerEdge": 0.0,
      "increment": 0.25
    }
  ],
  "displayRule": {
    "magnification": 0,
    "displayRuleStep": [
      {
        "decimalDigits": 2,
        "lowerEdge": 0.0,
        "wholeDigits": 4
      }
    ]
  },
  "time": 13,
  "chineseName": "微型E-迷你纳斯达克100指数",
  "allExchanges": "CME",
  "listingExchange": "CME",
  "countryCode": "US",
  "name": "Micro E-Mini Nasdaq-100 Index",
  "lastTradingDay": "20250321",
  "group": null,
  "sector": null,
  "sectorGroup": null,
  "ticker": "MNQ",
  "type": "",
  "undComp": "Micro E-Mini Nasdaq-100 Index",
  "undSym": "MNQ",
  "underExchange": "CME",
  "hasOptions": true,
  "fullName": "MNQ Mar21'25",
  "isEventContract": false,
  "pageSize": 100
}

Is this common? Is there anything I can do to get consistent API behavior?

weklund avatar Feb 07 '25 01:02 weklund

I can only comment on your 2nd question:

After I opened the position, everytime I called to the status via client.positions() I would get this response: But other times I'll make the exact same call seconds later but get a different response type Is this common? Is there anything I can do to get consistent API behavior?

I was wondering the same: See IBKR docs on the endpoint here: https://www.interactivebrokers.com/campus/ibkr-api-page/webapi-ref/#tag/Trading-Portfolio/paths/~1portfolio~1%7BaccountId%7D~1positions~1%7BpageId%7D/get

There is a parameter named waitForSecDef: "Forcing to wait for all security definition to be received. If false, position may not have secDef portion". This parameter is not used by client.positions() and presumably defaults to false. Your 2nd longer response has just this additional secDef information. It seems repeated positions() calls will include secDef info eventually.

See also position2(), which is not mentioned in IBKR docs. Never tried it.

salsasepp avatar Feb 07 '25 06:02 salsasepp

hey @weklund great questions. @salsasepp gave you a great explanation for the second - do try the position2 endpoint, it's meant to avoid IBKR server caching, - so let me address the first:

Now I want to close this position. Am I making another order that takes the opposite side and same size?

That's correct. 'Closing' a position isn't actually an action that one can take explicitly, as far as I know no trading platform exposes a close_position endpoint. It's more an intent to get out of whatever investment type you were making. I'm trying to avoid simply saying 'sell' because in different instrument types closing a position could mean different things.

In a simple case your example is correct - closing a position equals making one or more orders that will sell all your currently held positions of a stock.

Note that you could close a position using several orders, not just one, eg, over a course of a day - the intent to close all is still there. And contrarily, note that simply selling a portion - but not all - of your stock would probably be referred to as 'reducing' a position rather than 'closing' it.

Both open and close orders I made were MKT, but the close orders stayed unfulfilled for a really long time so I feel like I'm missing something.

Did you happen to submit these when the markets were closed? Eg. see NYSE trading hours: https://www.nyse.com/markets/hours-calendars

Voyz avatar Feb 07 '25 08:02 Voyz

I was wondering the same: See IBKR docs on the endpoint here: https://www.interactivebrokers.com/campus/ibkr-api-page/webapi-ref/#tag/Trading-Portfolio/paths/~1portfolio~1%7BaccountId%7D~1positions~1%7BpageId%7D/get

@salsasepp That helps thank you! wow I've been looking at the wrong docs.... s/web-ref/webapi-ref/g gets you a completely different documentation! I've been looking at https://www.interactivebrokers.com/campus/ibkr-api-page/web-api/

Did you happen to submit these when the markets were closed? Eg. see NYSE trading hours: https://www.nyse.com/markets/hours-calendars

@Voyz I'm doing Futures exclusively but during after hours trading, and on paper account. Maybe it's harder for paper accounts to simulate order books after hours? Will try again during normal market hours and follow up.

weklund avatar Feb 08 '25 01:02 weklund

I'd imagine the after hours would be the cause of your issues here. For futures the concept of closing position is pretty much the same as for stocks.

Voyz avatar Feb 09 '25 12:02 Voyz

Haven't tried position2 but I was able to open and close single orders. Closing this. Thanks for the help @Voyz @salsasepp :D

weklund avatar Feb 12 '25 02:02 weklund

Glad to hear it worked! 👏 Do try position2 though, it skips the IBKR caching which should reduce one breakage point in your system.

Voyz avatar Feb 12 '25 11:02 Voyz

@weklund @Voyz Fyi: there's an undocumented parameter to 'iserver/account/{account_id}/orders' to close an existing position.

order_data = make_order_request(
            conid=symbol,
            side=action,
            quantity=quantity,
            order_type=order_type,
            acct_id=account_id,
            parent_id=parent_id,
            coid=order_id,
            price=price,
        )
        
order_data['isClose'] = True

You still need to know which direction the order has to take, however, one important difference is, that if there are OCO orders (or generally other orders for the given symbol?) they can be cancelled automatically. For this you get the following question on order submission:

['<html>You are attempting to close a position that has open orders.<br><br>Would you like to cancel all open orders and then place new closing order?</html>']

This is how the web trading in the client portal does it, because it has the extra feature of closing an exisiting position.

mmarihart avatar Feb 26 '25 16:02 mmarihart

Wow @mmarihart that is super useful! How did you find about this endpoint? I'll add this parameter 🙌

Voyz avatar Feb 26 '25 18:02 Voyz

Is this in a queue to work on or have we closed this?

weklund avatar Apr 28 '25 14:04 weklund

It's on the backlog hence I'm keeping this open

Voyz avatar May 01 '25 11:05 Voyz

I wonder if we can capture this change in a pydantic model https://github.com/Voyz/ibind/issues/105

weklund avatar May 14 '25 22:05 weklund

Released with 0.1.15 👍

Voyz avatar Jun 17 '25 09:06 Voyz