Skip to main content

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.

IDRequested policyEntitlement boundary
1Live only.Requires the applicable live market-data subscription.
2Frozen when live is unavailable, typically outside the session.Requires the same subscriptions as live data.
3Prefer 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.
4Prefer 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.time and Ticker.timestamp describe 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 shapeLifecycleImportant boundary
snapshot=FalseContinuing subscription until cancellation, connection loss, or reset.Own and cancel it explicitly.
snapshot=TrueCollects 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=TruePaid 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 conditionInterpretationRequired response
Error 354The 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 101The session reached its ticker-line limit.Do not retry in a loop; deduplicate, cancel unused streams, and reduce or provision capacity.
Error 10197A competing session prevents market data.Stop quote readiness and resolve session ownership; do not substitute stale values.
Error 2103 or 1100A market-data farm or upstream connection is unavailable.Mark affected quotes stale immediately and suspend dependent decisions.
Error 1101Connectivity returned but data was lost.Rebuild desired subscriptions idempotently and wait for fresh readiness.
Error 1102Connectivity returned and data was maintained.Do not blindly duplicate subscriptions; still apply freshness and application-invariant checks.
Timeout with no usable fieldsThe 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

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.