Request live or delayed market data
Top-of-book market data is a continuing, entitlement-dependent stream. A successful request returns a mutable Ticker immediately; it does not prove that a usable quote, the requested data type, or any particular field has arrived.
Complete connection and readiness, account scope, and contract qualification first. The examples use a paper port and read-only connection, request delayed fallback without buying a regulatory snapshot, and place no order.
Minimal live-preferred subscription
Request market-data type 3 before the subscription to permit delayed data when live entitlement is unavailable. TWS still returns live data when it is available. Treat Ticker.marketDataType, not the requested number, as the actual mode.
import asyncio
from ib_async import Contract, IB, Stock, Ticker
async def wait_for_bid_ask(ticker: Ticker, timeout: float) -> None:
loop = asyncio.get_running_loop()
deadline = loop.time() + timeout
while not ticker.hasBidAsk():
remaining = deadline - loop.time()
if remaining <= 0:
raise TimeoutError("no usable bid/ask arrived")
await asyncio.wait_for(ticker.updateEvent, timeout=remaining)
async def main() -> None:
ib = IB()
contract: Contract | None = None
subscribed = False
try:
await ib.connectAsync(
host="127.0.0.1",
port=4002,
clientId=10,
timeout=10,
readonly=True,
raiseSyncErrors=True,
)
search = Stock(
"AAPL", "SMART", "USD", primaryExchange="NASDAQ"
)
qualified = await ib.qualifyContractsAsync(search)
if len(qualified) != 1 or not isinstance(qualified[0], Contract):
raise LookupError("contract did not resolve uniquely")
contract = qualified[0]
ib.reqMarketDataType(3) # Live when entitled; delayed otherwise.
ticker = ib.reqMktData(contract)
subscribed = True
await wait_for_bid_ask(ticker, timeout=10)
if ticker.marketDataType not in {1, 3}:
raise RuntimeError("unexpected market-data type")
print(ticker.bid, ticker.ask, ticker.marketDataType)
finally:
if subscribed and contract is not None:
ib.cancelMktData(contract)
ib.disconnect()
asyncio.run(main())
The timeout is an application deadline, not evidence that the instrument has no market. Closed sessions, illiquidity, missing entitlement, an upstream outage, or a field not provided for this product can all leave a value unset.
Requested mode versus actual mode
IB.reqMarketDataType changes the mode used by later market-data requests on the connection. It does not return a per-request acknowledgement. The actual mode for a subscription is delivered by the protocol's market-data-type callback and stored in Ticker.marketDataType.
| ID | Requested policy | Entitlement boundary |
|---|---|---|
1 | Live only. | Requires the applicable live market-data subscription. |
2 | Frozen when live is unavailable, typically outside the session. | Requires the same subscriptions as live data. |
3 | Prefer live; permit delayed streaming data when live is unavailable. | Delayed data is normally 15–20 minutes behind and uses delayed tick IDs. If live is available, TWS ignores the delayed preference and returns live data. |
4 | Prefer the freshest available mode while permitting delayed-frozen data. | Intended for a user without the relevant live subscription when markets are closed. |
Delayed fallback applies to reqMktData and historical-data requests, not to tick-by-tick data. It is not an entitlement bypass and is not available for every product, field, venue, or market-data endpoint.
Because the mode selector has no request ID, make one connection owner responsible for setting a mode and starting its subscription. If concurrent tasks alternate modes and requests without serialization, the application cannot reliably associate its requested preference with the next subscription. This is an application-level ownership rule derived from the connection-scoped protocol shape.
What reqMktData returns
IB.reqMktData allocates a request ID, registers a mutable Ticker, sends the request, and returns that ticker before data arrives. Official watchlist data consists of aggregate snapshots delivered several times per second; it is not the tick-by-tick feed.
Default price and size fields start unset. Additional Ticker fields require a comma-separated genericTickList of numeric generic-tick IDs. Request only fields the application needs; generic ticks can have separate entitlement and product constraints.
Pinned ib_async processes a received network packet, timestamps every changed ticker, emits each ticker's updateEvent, then emits IB.pendingTickersEvent for the changed set. Consequences for consumers:
- One update can contain several changed fields, while another can contain only one.
- The same logical value may be observed more than once; consumers must be idempotent.
Ticker.timeandTicker.timestampdescribe local packet processing, not an exchange event timestamp for every field.- Read the current ticker state inside the event handler; do not assume a particular callback is the final field for a quote.
Ticker.hasBidAsk requires set bid/ask prices and positive sizes. Ticker.marketPrice returns the last price when it lies inside a valid spread, otherwise the midpoint, otherwise the last price. It is a convenience estimate, not a firm or executable price.
Streaming, ordinary snapshots, and regulatory snapshots
| Request shape | Lifecycle | Important boundary |
|---|---|---|
snapshot=False | Continuing subscription until cancellation, connection loss, or reset. | Own and cancel it explicitly. |
snapshot=True | Collects available default ticks until tickSnapshotEnd, normally about 11 seconds later. | Generic ticks are not allowed, and some fields may remain absent. Do not treat it as an instantaneous complete quote. |
regulatorySnapshot=True | Paid US regulatory snapshot subject to eligibility and pacing. | It can incur a per-request fee, including in paper accounts. Never enable it as an automatic fallback. |
This guide uses streaming delayed fallback and leaves regulatorySnapshot=False, so it does not make the fee-bearing regulatory-snapshot request described above. An application that intentionally uses ordinary snapshots should model the completion callback separately rather than waiting for an arbitrary first tick.
Production-pattern subscription owner
This owner prevents duplicate high-level subscriptions for one contract, records the accepted actual modes in its readiness gate, cancels exactly once, and marks old values inactive after closure.
from __future__ import annotations
import asyncio
from ib_async import Contract, IB, Stock, Ticker
LIVE_OR_DELAYED = frozenset({1, 3})
def quote_is_ready(
ticker: Ticker, accepted_types: frozenset[int]
) -> bool:
return ticker.marketDataType in accepted_types and ticker.hasBidAsk()
async def wait_for_quote(
ticker: Ticker,
accepted_types: frozenset[int],
timeout: float,
) -> Ticker:
loop = asyncio.get_running_loop()
deadline = loop.time() + timeout
while not quote_is_ready(ticker, accepted_types):
remaining = deadline - loop.time()
if remaining <= 0:
raise TimeoutError("usable quote did not arrive")
await asyncio.wait_for(ticker.updateEvent, timeout=remaining)
return ticker
class MarketDataSubscription:
def __init__(self, ib: IB, contract: Contract) -> None:
self.ib = ib
self.contract = contract
self.ticker: Ticker | None = None
def start(self, market_data_type: int = 3) -> Ticker:
if self.ticker is not None:
raise RuntimeError("market-data subscription already active")
self.ib.reqMarketDataType(market_data_type)
self.ticker = self.ib.reqMktData(self.contract)
return self.ticker
def replace(self, market_data_type: int) -> Ticker:
if self.ticker is not None and not self.close():
raise RuntimeError("existing subscription could not be cancelled")
return self.start(market_data_type)
def close(self) -> bool:
if self.ticker is None:
return False
cancelled = self.ib.cancelMktData(self.contract)
if cancelled:
self.ticker = None
return cancelled
async def main() -> None:
ib = IB()
owner: MarketDataSubscription | None = None
try:
await ib.connectAsync(
host="127.0.0.1",
port=4002,
clientId=10,
timeout=10,
readonly=True,
raiseSyncErrors=True,
)
search = Stock(
"AAPL", "SMART", "USD", primaryExchange="NASDAQ"
)
qualified = await ib.qualifyContractsAsync(search)
if len(qualified) != 1 or not isinstance(qualified[0], Contract):
raise LookupError("contract did not resolve uniquely")
owner = MarketDataSubscription(ib, qualified[0])
ticker = owner.start()
await wait_for_quote(ticker, LIVE_OR_DELAYED, timeout=10)
observation = {
"conId": qualified[0].conId,
"observed_at": ticker.time,
"market_data_type": ticker.marketDataType,
"bid": ticker.bid,
"bid_size": ticker.bidSize,
"ask": ticker.ask,
"ask_size": ticker.askSize,
}
_ = observation
finally:
if owner is not None:
owner.close()
ib.disconnect()
if __name__ == "__main__":
asyncio.run(main())
Do not call reqMktData twice for the same contract on one IB instance. Pinned 2.1.0 reuses the same Ticker and stores only the latest request ID in its high-level cancellation map; a duplicate request can therefore leave the earlier server subscription without a high-level cancellation handle. Deduplicate in the owner before calling the library.
Cancellation and cache ownership
IB.cancelMktData looks up the ticker by contract, removes its current market-data cancellation mapping, sends the matching request ID, and returns True. It returns False when that mapping is absent.
Cancellation does not erase the ticker or its last values from the wrapper cache. After close(), the owner—not the presence of IB.ticker(contract)—is the authority on whether the stream is active. Never publish a cached price after cancellation as if it were current. Disconnect resets the wrapper cache, but Ticker.updateEvent is not a terminal disconnect signal; the connection owner must stop any quote wait and mark the old epoch inactive. A new connection epoch must create new subscriptions and pass readiness again.
Failures and recovery
| Signal or condition | Interpretation | Required response |
|---|---|---|
| Error 354 | The requested market data is not subscribed; delayed availability may differ. | Close and successfully cancel the current owner first. If policy permits, call reqMarketDataType(3), create one replacement subscription, and rerun actual-mode/quote readiness. Otherwise fail closed. |
| Error 101 | The session reached its ticker-line limit. | Do not retry in a loop; deduplicate, cancel unused streams, and reduce or provision capacity. |
| Error 10197 | A competing session prevents market data. | Stop quote readiness and resolve session ownership; do not substitute stale values. |
| Error 2103 or 1100 | A market-data farm or upstream connection is unavailable. | Mark affected quotes stale immediately and suspend dependent decisions. |
| Error 1101 | Connectivity returned but data was lost. | Rebuild desired subscriptions idempotently and wait for fresh readiness. |
| Error 1102 | Connectivity returned and data was maintained. | Do not blindly duplicate subscriptions; still apply freshness and application-invariant checks. |
| Timeout with no usable fields | The request produced no quote meeting application policy. | Cancel the owned stream, report the requested/actual mode and contract identity, and fail closed. |
Informational farm messages can arrive without a socket disconnect and can be repeated. Keep the desired-subscription set separate from active handles so recovery can reconcile to the desired set exactly once.
Persistence boundary
Market-data state is an observation, not a ledger. If an application must retain it, store the qualified contract identity, actual market-data type, selected fields, field-specific source time when available, local observation time, and the connection epoch. Do not persist a Ticker object or infer continuity across disconnects.
Before an order workflow consumes a quote, require application-specific freshness, accepted data type, valid fields, and account/product permissions. Delayed and frozen observations should never silently pass a policy that requires live executable pricing.
Sources and applicability
- IBKR Campus: delayed market data —
official-current; retrieved2026-07-14T19:13:54.602937Z; supports mode IDs, live preference, delayed timing/tick IDs, the actual-mode callback, and endpoint limitations. - IBKR Campus: top-of-book watchlist data, generic ticks, ordinary snapshots, and regulatory snapshots —
official-current; retrieved2026-07-14T19:13:54.602937Z; support aggregate update semantics, generic-tick selection, snapshot completion, and billing warnings. - IBKR Campus: cancel watchlist data and system message codes —
official-current; retrieved2026-07-14T19:13:54.602937Z; support cancellation and data-lost/data-maintained recovery boundaries. IBmarket-data methods —library-source,ib_async2.1.0; retrieved2026-07-15T03:42:42Z; supports mode selection, immediate ticker return, request registration, and boolean cancellation.- Wrapper ticker ownership and reset, actual mode, and packet events —
library-source, 2.1.0; retrieved2026-07-15T03:42:42Z; support deduplication constraints, cache/reset boundaries, and event ordering. Tickerfields and quote helpers —library-source, 2.1.0; retrieved2026-07-15T03:42:42Z; support default mode/fields, validity checks, and convenience pricing.
No live or delayed market-data request is required for this page. CI compiles both examples as ordinary scripts, binds their ib_async calls to 2.1.0 signatures, executes the quote-readiness gate against offline tickers, and verifies pinned request registration, actual-mode updates, duplicate ownership, cancellation, and stale-cache behavior with deterministic fakes.