▶ Full Post Text
# Ibkrs tickByTickBidAsk function is NOT truly tick-by-tick IBKR batches changes into aggregated snapshots.
**tickByTickBidAsk is NOT truly tick-by-tick** — IBKR batches changes over \~200-300ms somtiemseven above 1000ms into aggregated snapshots. Tick-by-tick data corresponding to the data shown in the TWS Time & Sales Window is available starting with TWS v969 and API v973.04.[https://interactivebrokers.github.io/tws-api/tick\_data.html](https://interactivebrokers.github.io/tws-api/tick_data.html)
https://reddit.com/link/1r7xgov/video/sievwdwtq7kg1/player
As you can see, the NBBO arrives only about every 5 to 10 prints, not on every tick, which is what you would normally assume “tick-by-tick” means.
This effectively makes the IBKR API tickByTick stream aggregated, similar to `reqMktData`, which is officially stated to update only every 250 ms.
The confusing part is that `reqTickByTickData`, although faster than `reqMktData`, is still not truly tick-by-tick and does not deliver updates for every individual tick.
It’s unclear why this limitation is not made transparent in the documentation, since `reqTickByTickData` behaves more like a higher-frequency aggregated feed than a true per-event stream.
heres their documentation for tickbytick [https://interactivebrokers.github.io/tws-api/tick\_data.html](https://interactivebrokers.github.io/tws-api/tick_data.html) where they do not state the delay
[https://interactivebrokers.github.io/tws-api/md\_request.html](https://interactivebrokers.github.io/tws-api/md_request.html) here for `reqMktData`
they are transparent and say This data is not tick-by-tick but consists of aggregate snapshots taken several times per second
i even coded a test code to compare the `reqTickByTickData` with the reqMktDepth function to see wich gives more and faster nbbop updates and sadly reqMktDepth win
============================================================
BBO FREQUENCY TEST: tickByTickBidAsk vs reqMktDepth
Symbol: NVDA Duration: 60s
============================================================
Connected. nextValidId=1
Subscribing tickByTickBidAsk (reqId=5001)...
Subscribing reqMktDepth (reqId=5002, numRows=1)...
Collecting data for 60 seconds...
\[1s/60s\] tickByTick: 12 quotes | depth: 0 quotes ERROR: reqId=5002 code=2176 msg=Warning: Your API version does not support fractional share size rules. Please upgrade to a minimum version 163. Trimmed value 100 to 1
ERROR: reqId=5002 code=2152 msg=Exchanges - Depth: IEX; Top: BYX; PEARL; AMEX; T24X; MEMX; OVERNIGHT; EDGEA; CHX; IBEOS; NYSENAT; PSX; LTSE; ISE; DRCTEDGE; Need additional market data permissions - Depth: NASDAQ; BATS; ARCA; BEX; NYSE;
\[60s/60s\] tickByTick: 3051 quotes | depth: 3525 quotes
Cancelling subscriptions...
============================================================
RESULTS (60.5 seconds on NVDA)
============================================================
Method Count Rate Avg Gap
\---------------------------------------------------------
tickByTickBidAsk 3051 50.4/sec 19ms (min=0ms, max=1004ms)
reqMktDepth 3545 58.5/sec 17ms (min=0ms, max=492ms)
\>>> reqMktDepth gives 1.2x MORE updates than tickByTickBidAsk
============================================================
"""
BBO Update Frequency Test: tickByTickBidAsk vs reqMktDepth
Subscribes to BOTH feeds simultaneously for the same symbol
and compares how often each delivers NBBO updates.
Usage: python bbo_test.py [SYMBOL] [DURATION_SECONDS]
e.g. python bbo_test.py AAPL 30
"""
from ibapi.client import EClient
from ibapi.wrapper import EWrapper
from ibapi.contract import Contract
import time
import sys
from threading import Thread
from collections import deque
# ─── Configuration ───
SYMBOL = sys.argv[1] if len(sys.argv) > 1 else "AAPL"
DURATION = int(sys.argv[2]) if len(sys.argv) > 2 else 30 # seconds
TWS_HOST = "127.0.0.1"
TWS_PORT = 7496
CLIENT_ID = 99
# ─── Tracking ───
tbt_updates = deque() # (timestamp, bid, ask) from tickByTickBidAsk
depth_updates = deque() # (timestamp, bid, ask) from reqMktDepth
class TestApp(EWrapper, EClient):
def __init__(self):
EWrapper.__init__(self)
EClient.__init__(self, wrapper=self)
self._depth_bid = None
self._depth_ask = None
def error(self, reqId, errorCode, errorString, advancedOrderRejectJson=""):
if errorCode in (2104, 2106, 2158, 473):
return
print(f" ERROR: reqId={reqId} code={errorCode} msg={errorString}")
def nextValidId(self, orderId):
print(f" Connected. nextValidId={orderId}")
# ── tickByTickBidAsk callback ──
def tickByTickBidAsk(self, reqId, time_stamp, bidPrice, askPrice,
bidSize, askSize, tickAttribBidAsk):
now = time.time()
tbt_updates.append((now, bidPrice, askPrice))
# ── Market Depth callbacks ──
def updateMktDepth(self, reqId, position, operation, side, price, size):
if position != 0:
return
if operation == 2: # delete
return
if side == 1: # bid
self._depth_bid = price
elif side == 0: # ask
self._depth_ask = price
if self._depth_bid is not None and self._depth_ask is not None:
now = time.time()
depth_updates.append((now, self._depth_bid, self._depth_ask))
def updateMktDepthL2(self, reqId, position, marketMaker, operation,
side, price, size, isSmartDepth):
self.updateMktDepth(reqId, position, operation, side, price, size)
def main():
print(f"\n{'='*60}")
print(f" BBO FREQUENCY TEST: tickByTickBidAsk vs reqMktDepth")
print(f" Symbol: {SYMBOL} Duration: {DURATION}s")
print(f"{'='*60}\n")
app = TestApp()
app.connect(TWS_HOST, TWS_PORT, CLIENT_ID)
# Run message loop in background
api_thread = Thread(target=app.run, daemon=True)
api_thread.start()
time.sleep(2) # Wait for connection
# Build contract
contract = Contract()
contract.symbol = SYMBOL
contract.secType = "STK"
contract.exchange = "SMART"
contract.currency = "USD"
# Subscribe to BOTH feeds
TBT_REQ = 5001
DEPTH_REQ = 5002
print(f" Subscribing tickByTickBidAsk (reqId={TBT_REQ})...")
app.reqTickByTickData(TBT_REQ, contract, "BidAsk", 0, True)
print(f" Subscribing reqMktDepth (reqId={DEPTH_REQ}, numRows=1)...")
app.reqMktDepth(DEPTH_REQ, contract, 1, True, [])
print(f"\n Collecting data for {DURATION} seconds...\n")
start = time.time()
last_print = start
# Live counter while running
while time.time() - start < DURATION:
time.sleep(0.5)
elapsed = time.time() - start
tc = len(tbt_updates)
dc = len(depth_updates)
sys.stdout.write(f"\r [{elapsed:.0f}s/{DURATION}s] "
f"tickByTick: {tc} quotes | "
f"depth: {dc} quotes ")
sys.stdout.flush()
# Cancel subscriptions
print("\n\n Cancelling subscriptions...")
app.cancelTickByTickData(TBT_REQ)
app.cancelMktDepth(DEPTH_REQ, True)
time.sleep(0.5)
# ─── Results ───
total_time = time.time() - start
tc = len(tbt_updates)
dc = len(depth_updates)
print(f"\n{'='*60}")
print(f" RESULTS ({total_time:.1f} seconds on {SYMBOL})")
print(f"{'='*60}")
print(f" {'Method':<25} {'Count':>8} {'Rate':>12} {'Avg Gap':>12}")
print(f" {'-'*57}")
for label, updates in [("tickByTickBidAsk", tbt_updates),
("reqMktDepth", depth_updates)]:
count = len(updates)
rate = f"{count/total_time:.1f}/sec" if total_time > 0 else "N/A"
if count >= 2:
gaps = [(updates[i][0] - updates[i-1][0]) * 1000
for i in range(1, count)]
avg_gap = sum(gaps) / len(gaps)
min_gap = min(gaps)
max_gap = max(gaps)
gap_str = f"{avg_gap:.0f}ms"
extra = f" (min={min_gap:.0f}ms, max={max_gap:.0f}ms)"
else:
gap_str = "N/A"
extra = ""
print(f" {label:<25} {count:>8} {rate:>12} {gap_str:>12}{extra}")
if tc > 0 and dc > 0:
ratio = dc / tc
print(f"\n >>> reqMktDepth gives {ratio:.1f}x {'MORE' if ratio > 1 else 'FEWER'} "
f"updates than tickByTickBidAsk")
elif dc > 0 and tc == 0:
print(f"\n >>> tickByTickBidAsk gave ZERO updates! reqMktDepth wins.")
elif tc > 0 and dc == 0:
print(f"\n >>> reqMktDepth gave ZERO updates! tickByTickBidAsk wins.")
print(f"{'='*60}\n")
app.disconnect()
if __name__ == "__main__":
main()
the code of the test