Request Contract Details

Resolve a symbol: send a partial description, collect every contract the venue lists under it, stop when the end marker lands. This is how you turn "AAPL" into a con_id you can quote and trade on.

What this shows

  • Building a partial Contract from symbol, sec_type, exchange and currency.
  • req_contract_details(req_id, contract) and the contract_details callback.
  • Driving the callback loop on a daemon thread with EClient.run.
  • Reading con_id, primary_exchange and trading_class off the returned ContractDetails.

What comes back

One contract_details callback per matching contract, then one contract_details_end for the request id. A description that matches several listings answers with all of them, so more than one row means an ambiguous description rather than an error.

The shorter form

Two blocking methods do the same work with no wrapper and no callbacks:

rows = c.contract_details(aapl)   # list, every match
one  = c.qualify_contract(aapl)   # a single Contract

qualify_contract refuses an ambiguous description rather than handing back whichever the venue listed first, which would be a different contract from the one you asked about. c.qualify_contracts([...]) takes several at once. Nothing that answers these calls reaches your wrapper, even what arrives after the call has returned.

These send, wait and return. They take their answer off the queue by request id and release the interpreter lock while waiting, so they need no run() thread of their own. Do not run both styles on one client: run() drains every queue rather than only its own, so a dispatch loop beside a blocking call competes with it for the answer. Pick the callbacks or pick these.

Lookups by contract id

A lookup by con_id includes the requested exchange. Leaving the exchange empty asks for the preferred definition. The same rule applies when a market data request needs to name a contract given only by id.

Contract details reuse exchange rule scopes already returned during the session. A first lookup can still need a separate request for each missing exchange; later lookups reuse those answers. CORPACT is excluded because it does not name a trading venue.

Run it

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

Source

"""Recipe: req_contract_details for AAPL on paper, print con_id and primary exchange.

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

import os
import threading

from ib_dx import EClient, EWrapper, Contract


class DetailsWrapper(EWrapper):
    def __init__(self):
        self.rows = []
        self.done = threading.Event()

    def contract_details(self, req_id, details):
        self.rows.append(details)

    def contract_details_end(self, req_id):
        self.done.set()

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


w = DetailsWrapper()
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()

aapl = Contract()
aapl.symbol = "AAPL"
aapl.sec_type = "STK"
aapl.exchange = "SMART"
aapl.currency = "USD"
c.req_contract_details(1, aapl)

if not w.done.wait(timeout=15):
    raise RuntimeError("contract_details_end not received")

print(f"matches: {len(w.rows)}")
for d in w.rows:
    print(f"  con_id={d.contract.con_id:>8}  "
          f"primary={d.contract.primary_exchange:<10}  "
          f"trading_class={d.contract.trading_class}")

c.disconnect()