Send a Limit Order

Place a BUY LMT on SPY far below market, watch it acknowledge, then cancel it. Connect, take an id, place, read status, cancel, disconnect.

What this shows

  • Taking the order id off next_valid_id, which connect() fires before it returns. c.next_order_id() reserves one directly if you would rather ask.
  • An Order with order_type = "LMT" and lmt_price set. outside_rth = True is what lets it be acknowledged outside regular hours.
  • order_status callbacks. PreSubmitted then Submitted while it rests, Cancelled once the cancel lands.
  • cancel_order(order_id, order_cancel). The second argument is what the withdrawal states about itself: an OrderCancel (who is withdrawing it, whether a person entered it, a manual time), or a time alone; "" states nothing. The operator and who entered it travel on the cancel. A time does not, and the caller is told so on error; a time a gateway cannot read is refused under 10301 and the order keeps working.

What comes back

order_status on every change and again on each fill. filled and remaining are quantities, avg_fill_price is the average of what has filled so far. An order that rests will sit at Submitted until you cancel it.

The perm_id on that callback is the number the order goes to the venue under, as a gateway states it, which for this order is its own order id. It is stated from the moment the order is sent and stays the same for the life of the order, including across a modify.

Limits

Order ids count from one past the highest id the account is working an order under, which the venue names at every connect, from every session. So one taken here does not collide with an order placed elsewhere.

tif is checked against what the venue carries: DAY, GTC, IOC, FOK, OPG, GTD, GTX, DTC, AUC, NMIN, spelled exactly. An unrecognised value is refused here rather than sent as DAY and quietly expiring at the close.

Paper account only. The price is set far below market so it will not fill.

Run it

IB_USERNAME=... IB_PASSWORD=... python examples/hello_limit_order.py

Source

"""Recipe: place a non-marketable BUY LMT on SPY, watch Submitted, then cancel.

Usage:
    IB_USERNAME=... IB_PASSWORD=... python examples/hello_limit_order.py
"""

import os
import threading

from ib_dx import EClient, EWrapper, Contract, Order


class OrderWrapper(EWrapper):
    def __init__(self):
        self.next_id = None
        self.submitted = threading.Event()
        self.cancelled = threading.Event()

    def next_valid_id(self, order_id):
        self.next_id = order_id

    def order_status(self, order_id, status, filled, remaining,
                     avg_fill_price, perm_id, parent_id,
                     last_fill_price, client_id, why_held, mkt_cap_price):
        print(f"[status] oid={order_id} status={status}")
        if status == "Submitted":
            self.submitted.set()
        if status == "Cancelled":
            self.cancelled.set()

    def error(self, req_id, error_time, code, msg, advanced=""):
        if code not in (2104, 2106, 2158):
            print(f"[error] {code}: {msg}")


w = OrderWrapper()
c = EClient(w)
c.connect(
    username=os.environ["IB_USERNAME"],
    password=os.environ["IB_PASSWORD"],
    paper=True,
)
threading.Thread(target=c.run, daemon=True).start()


order_id = w.next_id

spy = Contract()
spy.con_id = 756733
spy.symbol = "SPY"
spy.sec_type = "STK"
spy.exchange = "SMART"
spy.currency = "USD"

order = Order()
order.action = "BUY"
order.total_quantity = 1
order.order_type = "LMT"
order.lmt_price = 1.00
order.tif = "DAY"
order.outside_rth = True

print(f"placing BUY 1 SPY LMT 1.00 (oid={order_id})")
c.place_order(order_id, spy, order)
w.submitted.wait(timeout=15)

print(f"cancelling oid={order_id}")
c.cancel_order(order_id, "")
w.cancelled.wait(timeout=15)

c.disconnect()