Instruction file imported from aaltaay/Nova (
.cursor/rules/single-market-data-feed-catalog.mdc). Copyright stays with the author.
Single market-data feed & quote-panel coherence -- the catalog
This is the full text behind single-market-data-feed.mdc (ADR 051: the headlines load on every request; the mechanics, the anti-patterns and the module map load when you edit the code they govern). Nothing here is weaker for living behind a glob: it is the same law.
Scanner discovery is IBKR-only (NOVA_DISCOVERY_PROVIDER=ibkr). Interactive Brokers is the only price/volume/depth/chart/scanner feed. Alpaca is news + listing metadata only — never a selectable scanner source. Do not silently mix Alpaca market data into quote, chart, scanner, or Level 2.
Hard rules
-
One discovery feed for prices.
- Scanner rows, quote ticks, chart bars, Level 2, and Time & Sales come from IBKR.
- Settings must not offer Alpaca as a scanner source;
/api/configcoerces any stalealpacavalue toibkr. - Never fall back from IBKR → Alpaca without an explicit user-visible error. Silent fallback is forbidden.
- Sim practice exception: the header Paper / Live / Sim toggle replays a real recorded or downloaded session of one ticker (ADR 019): a Session Record, an IBKR historical download, or the operator's own Massive flat files (ADR 046 -- the day's trades, 1-minute bars and NBBO, imported unaltered; the snapshot names the source); either historical window may also draw an exchange's own book imported by hand from a local MBP-10 file (ADR 057 -- that venue's depth only, named as that venue's on every level, never the consolidated book, never a quote or a fill). There is no synthetic instrument and no fabricated tape: with nothing loaded the Sim desk is empty off the live edge -- while the Sim clock follows the wall clock on today's date (
live_edgeon/api/sim/clock), a Sim tab reads the live IBKR paths above exactly as a Paper tab does (ADR 020 live-edge amendment; gate onsim/mode.is_replay_desk, notis_sim_mode). Never mix replayed ticks with IBKR paper/live prices, and never place Sim orders through Gateway. Switching off Sim restores the IBKR paths above.
-
Alpaca listing metadata is not a price feed. Asset flags (tradable / shortable / margin) may still come from Alpaca Assets API. Label them as listing/metadata — never as the quote source. Do not imply Alpaca is pricing the panel.
-
Quote panel symbol gate (mandatory). When opening or switching the quote / side panel / Stock View:
- Clear prior ticker state immediately on symbol change (
detail, Level 2 book, tape prints, chart series). - Render quote body / live chart only when
detail.symbol === selectedSymbol(or show loading / empty — never the previous symbol's surfaces). - Depth and tape hooks must ignore WS messages whose
symbol≠ the hook's current symbol, and ignore events from a non-current WebSocket instance. - Prefer binding Level 2 and T&S to
selectedSymbol(withkey={symbol}), not a staledetail.symbol. - Quote Panel (scanner sidebar) is L1-only — quote header, chart, fundamentals, news. Do not mount Level 2 or Time & Sales there (avoids burning
IBKR_MAX_DEPTH_SYMBOLSon casual clicks). - Trader View (ticker click / Stock View) owns L2 + T&S. A scanner ticker click opens Trader and adds or activates a tab (ADR 011, amended #201) -- never silent-replace.
+adds a draft tab. Extract (Pop outor double-click a tab) opens a detached OS window. A ticker double-click has no special action. Drag that tab onto another Nova window (or Dock) to put it back -- drop target is the host that received the drop, notwindow.opener. A already-popped-out window shows Dock + close only (no second Pop out). Dock must add the symbol on the target host and close the float -- do not close the float unless the host accepted. Live cap isTRADER_MAX_LIVE_TABS(= depth plan cap, currently 3). Extra names stay on the strip gray / suspended (no L2); a new live name or a click on a gray tab evicts the least-recently-focused live tab. Scanner | Trader in the host window is a view switch: keep tabs and the Trader tree mounted (hidden) so tape/L2 WebSockets stay open.closeTraderViewis last-tab X / detached close / explicit leave only. IB forbids a secondreqTickByTickDatawithin 15s of cancel -- do not unsubscribe just to show the scanner. - Row vs ticker is a spatial split, not click-vs-double-click (ADR 011 §7a). On tables that render a
SymbolSelectButton(Gappers/Gainers/Losers/AH/Large Cap, Catalysts, HOD Momo + Running Up, Watchlist, Signals), the row body only callsonSelect(Quote Panel) -- it does not open Trader. Only the blue ticker button opens Trader.SelectableTableRow'sopenOnRowClickprop defaults totruefor rows with no ticker button (Positions, Working/Closed Orders, Journal, Executor, HOD debug), where the row remains the only gesture into Trader. Once Trader is already showing there is no Quote Panel to update, soWorkspaceContext.selectRowSymboladds or activates (traderDesk/useTraderDeskBinding.ts) -- a dock/roster row click while in Trader still lands on that symbol's tab and may gray the oldest live tab if a 4th live slot is needed.
- Clear prior ticker state immediately on symbol change (
-
Fail loud, not quiet. If IBKR bars/ticks/depth/tape are unavailable while discovery is ibkr, return/show a clear error (e.g. HTTP 503). Do not serve Alpaca candles under an IBKR quote. Chart bars (ADR 012): HTTP
/barsis store-first. Archived IBKR bars may be painted immediately with coverage{as_of, complete_through, filling}. A fill is a backgroundhistorical_servicerequest (paced, concurrency 3), not a blocking 25s pull. Empty store + Gateway down is still 503. Never paint Alpaca candles. Never surface "Chart bars timed out -- IBKR historical may be busy" because a click queued behind unrelated cold work. Send rule:reqHistoricalDatais never sent while pacingwait_seconds > 0.background/warmshed on any wait;open_chartreschedules every wait viacall_lateron the IB loop -- the 10-minute bucket debt and the short same-contract (2s) / identical-request (15s) windows alike -- and raisesHistoricalShedso the hist semaphore is released while deferred. Nosleep-then-send at any duration: an awaited pacing wait still parks the fill on the connect-loop holding a hist slot whilereqMktDataL1 ticks share that loop (2026-08-18 Gainers freeze; D-025). 10Sec first-paint rule: In Trader, the first symbol-gated IBKR AllLast print seeds an empty client 10Sec bar immediately; historical bars still replace/reconcile it. A late empty/barsresponse must not erase that live candle. Empty+filling retries must self-schedule until bars arrive or the pane stops being active -- retry continuation cannot depend on React state changing after another identical empty response. Store rule:bars_store.read/write_payloadare SQLite round trips and always run throughasyncio.to_threadfrom the IB loop -- a 1441-bar fill must never put an upsert loop +commit()in front of an L1 tick (D-018). -
Scanner tables vs open ticker (two tiers).
- Table / HOD hot path: bounded persistent IBKR
reqMktDataLevel-1 streams for (a) every row in the active scanner tab and (b) a reserved HOD active pool (capacity-bounded — see rule 8 for exactly what is in that pool). Never stream the whole discovery universe. Coalesce ticks into batched/ws/scannerprice_patchmessages. - Do not use
reqTickersAsyncfor the active-table freshness SLA — IB completes snapshots ontickSnapshotEnd~11s later, so a 1Hz/4s-timeout loop is structurally impossible. - Cold / discovery only: infrequent
reqTickersAsync(timeout ≥12s) for new/cold symbols outside the streamed set; serialized so it cannot starve L1. - A quote someone waits on is never a snapshot (ADR 054). The Trader's quote head, the ticker-detail backstop, the Cryptos page's bridge stocks and the Market page read a line held for a moment (
ibkr/brief_lines.py): at mostIBKR_BRIEF_LINE_BATCHlines open, every reader together, each cancelled withinIBKR_BRIEF_LINE_WAIT_SEC, none opened with fewer thanIBKR_BRIEF_MIN_FREE_LINESspare, the desk's own line borrowed and never cancelled. A stock snapshot waits ~11 s from midnight to 09:30 ET. - Quote Panel open ticker: dedicated
reqMktData(shared owner with table/HOD when overlap) + chart — L1 only; no depth/tape. - Trader View: unbounded symbol tabs; at most
TRADER_MAX_LIVE_TABS(= depth plan cap, currently 3) are live L2 in the current window. Gray / suspended tabs stay visible and do not mount L2/tape. Pop out / double-click a tab for a detached OS window (prefer one per monitor). Drag the tab back onto a Nova window to dock (ADR 011). Each live window/tab may hold live L1 + depth + tape. Host Scanner | Trader does not unmount those live tabs. - Tape linger: last T&S WebSocket close schedules IB
cancelTickByTickDataafterIBKR_TAPE_LINGER_SEC(16s). A remount cancels the linger and reuses_tickers. Immediate cancel + resubscribe hits IB's 15s same-instrument guard ("Resubscribing in 10s"). - Honesty: per-row source age + header stale when the active-tab subscription is incomplete or patches are late. Never hide lag behind a frozen green "updated" badge.
- Table / HOD hot path: bounded persistent IBKR
-
Session-owned persistent scanner rosters (ADR 008).
backend/ibkr/scanner_stream.pyowns persistentreqScannerSubscriptionhandles for the life of the process — it does not poll. Desired subscriptions by session period: Premarket = Gainers only; RTH = Gainers + Losers (UI-only); Afterhours = AH Gainers; Closed = none (at most two persistent slots). Large Cap (ADR 014) adds a third, always-on lease (TOP_VOLUME_RATE+marketCapAbove/aboveVolume/stockTypeFilter="CORP") present in every session period including Closed — it is a swing table, not a day-trade table, and is deliberately exempt from the freeze-at-boundary contract below (see the_ALWAYS_LIVEcarve-out inscanner_session.py).marketCapAboveis in millions of USD, not raw dollars (IB wire fieldmarketCapAbove1e6) (verified 2026-08-25).
- Roster admission is name-only (ADR 010 decision 5). An IB scanner name becomes a row immediately, with
price=nulluntil the first L1 tick. Admission must neverawaita COLDsnapshot_quotes/reqTickersAsync— that coupling emptied the desk for a full premarket on 2026-08-24 when every cold batch died on the 20son_ibceiling. Row order is IB's rank; do not re-sort a ranked scan. - Premarket Gappers is a projection, not a lease.
backend/ibkr/gapper_view.pyfilters the live Gainers roster toGAPPER_MIN_GAP_PCT.TOP_OPEN_PERC_GAINhas no open to measure before 09:30 ET (IB answers with Warning 165), so it gets no premarket lease. The projection freezes at 09:30 like any other table. - One roster owner per table. With
discovery=ibkrthe persistent lease is the only writer:scanner_runners/discovery.pyandscanner_runners/movers.pyrefuse, andadapters/ibkr_scanner.pyraises. Rollback is git revert, not a second writer (ADR 010 decision 10). - A lease that never answers is reopened. The watchdog (
scanner_stream._watchdog_once) cancels and reopens a lease whose first batch has not arrivedIBKR_SCANNER_FIRST_BATCH_RESUBSCRIBE_SECafter it opened, and again that long after each reopen (never faster). An empty batch (updateEventwith 0 rows) is an answer; only no event at all is silence. A lease IBKR refused with Error 322 first frees this client's leaked scanner slots (discovery.recover_scanner_slots, which never cancels a desired lease) and then reopens. On 2026-10-23 a refused Gainers lease sat empty 04:00–16:00 because the watchdog skipped leases that had never delivered. - Every one-shot scan cancels what it sent. Nothing that can raise runs between
reqScannerSubscriptionand thetry/finallythat cancels it indiscovery._one_shot_scanner; a raise there leaks one of IBKR's ten scanner slots per call (the Market page's movers scan filled all ten overnight on 2026-10-23). - Empty is not fresh. A batch with no names (Warning 165) must not stamp
table_state=live+ a newlast_scan. Leave the roster untouched and let integrity alarm inside the table's live window. Closed (HOD_MOMO_INTEGRITY_TICK_IDLE_MODES) has no day-trade leases: empty hist/HOD/scanner buffers and missing L1 must pass, not Integrity fail. Gateway disconnect, live RTH/premarket empty Gainers with IBKR up, and hard-stale L1 still fail loud. Each table (gappers/gainers/losers/afterhours) carries its ownsession_key(04:00 ET-anchored),state(live/frozen/unavailable),revision,roster_ts, andfrozen_atinruntime_state:- Gappers is
live04:00–09:30 ET, thenfrozen(immutable) for the rest of the session. - Gainers is
live04:00–16:00 ET, thenfrozen. - Afterhours is
live16:00–20:00 ET, thenfrozen. - A
frozentable's membership, rank, values, metadata, and timestamp cannot change — HOD-owned L1 may keep evaluating retained symbols without mutating the frozen row. Do not resubscribe L1 for a frozen tab's own display; only alivetab consumes active-tab L1. - Every scanner/hydration callback must be fenced by IB READY generation + subscription epoch + table + session key before it can write state; a result from a superseded generation/epoch/session is discarded, not applied.
- Gappers is
-
Time & Sales = IBKR only (Trader tabs).
- Feed:
reqTickByTickData(AllLast)viabackend/ibkr/tape_stream.py. - Scope: symbols open in Trader View tabs — same lifecycle as Level 2 (refcounted, 15s resubscribe guard). Not mounted in the scanner Quote Panel.
- Transport:
/ws/ibkr/tape/{symbol}withmsg.symbolgates on every message. - No Alpaca tape in the live UI.
backend/l2/tape.py(Alpaca recorder) is for historical recording only. - Clear tape immediately on symbol switch (never bleed prior symbol's prints into the new view).
- Feed:
-
HOD eligibility is the current-session displayed union -- no side-channel scans.
- Price/volume ticks into
hod_momo.on_trade_updatecome from IBKR L1 streams (ibkr/scanner_l1.py/ sharedibkr/ticks.py), not Alpaca WS. - HOD Momo's active set is exactly: current-session Gappers ∪ Gainers ∪ Afterhours ∪ manually curated Former Momo. Nothing else — Losers, the open ticker, IBKR volume seeds (
HOT_BY_VOLUME/TOP_VOLUME_RATE/MOST_ACTIVE), a secondbelowPrice-filtered Gainers pass, Large Cap (ADR 014), and rotating discovery "explore" are excluded from HOD admission (ADR 008). Large Cap's own breakout alerts run on a separate channel (backend/large_cap_alerts.py), never throughhod_momo.on_trade_update. - Sub-$20 stocks are ordinary Gainers rows — no special scanner, quota, or reserved slot.
- HOD spends zero IB historical tokens. Session-high floor is tick-6 + observed prints; if still unseeded after
HOD_MOMO_OBSERVED_SEED_WARMUP_SEC(60s), self-seed from max observed print withopen_alert_window=False. Squeeze buffer isbars_store.readof already-stored 1Min bars. Those bars come from (a) chart hist fills and (b) live scanner L1 rolled byibkr/l1_minute.py-- not from tapebars_1mand not fromreqHistoricalDatain HOD. Empty recent store = live-only ticks (Gainerschange_pctcovers the admission leg). - L1 1Min overlay stamps real size (D-049).
l1_minuteaccumulates lastSize / RTVolume (233) / tick-8 day-volume deltas on the existing scanner/detail/HODreqMktDataowner (IBKR_L1_GENERIC_TICKS). First cumulative print is a baseline, not a day-volume dump. Hist still replaces the overlay candle. Do not paintticker.vwapas a second session VWAP metric. Do not open a privatereqMktDatafor 233 (D-020). - Halt ETA chip (L2 header) reads
ticker.haltedon the same shared L1 line. That field is incoming tick type 49 (EWrapper.tickGeneric). IBKR docs: Generic tick required =-. Never put49ingenericTickList-- Warning 321 poisons the L1 upgrade (#178). Tape freeze ("Waiting for prints...") is a hint only -- it must not invent a countdown.halt_startis the first observedticker.haltedtransition, not the SIP official start (label that in the tooltip). LULD (halted=2) is second-precise:LULD · 1:42 · 3:18 left, then Auction, thenExtended · no ETA(no fake forever countdown). Kind badges: LULD / NEWS / UNK from codes 2 / 1 / other. Ifhalt_startwas observed late (reconnect / opened mid-halt), show elapsed-only unless Nasdaq Trade Halt RSS supplies an official start. Overlay official start / reason / pause threshold / scheduled quote+trade resume fromhttps://www.nasdaqtrader.com/rss.aspx?feed=tradehalts(poll ≤1/min, no HTML scrape). RSS down or symbol missing: keep IBKR+clock, mark exchange detail pending, never invent resume times. MWCB Level 1/2/3 is a rare desk-wide banner from RSSMWC1/MWC2/MWC3-- not per-symbol LULD tier. Clear immediately whenticker.haltedreturns to 0. NaN / missing /halted=-1is not resume -- only code 0 clears (#237). AfterreqMktDataattach, observe the ticker immediately (already-halted names may never fire another tick 49). Depth unsubscribe must notcancelMktDataon areqMktDepth-only line (that steals ticks' L1 and logscancelMktData: No subscription). An RSS open row (notrade_resume) may seed the chip when IBKR never observed that symbol -- do not invent tick 49. IBKR code 0 still wins over a stale RSS row. Do not block Place from this chip. Do not request generic tick 49 and do not open a privatereqMktDatafor halt. websocket.alpaca_trades_drive_hod()is False when discovery=ibkr — skipon_trade_updateandl2.tape.on_alpaca_tradefor Alpaca prints.websocket.stream_loopdoes not open Alpaca's market-data WebSocket under ibkr (idles and polls). Avoids dual-feed and Alpaca's one-connection limit.- Depth/tape hooks must compare
msg.symbolto an uppercased current symbol key.
- Price/volume ticks into
-
Two loops + hot/cold (ADR 010 -- implementation in progress).
- Target: HTTP/WS on uvicorn; all
ib.*on theconnectAsyncloop (dedicated IB thread). One process, oneIBKR_CLIENT_ID. snapshot_quotes/hydrate_rows/ surge-seed / enrichment / reprice are COLD (ibkr/work_class.py). PersistentreqScannerSubscription, L1/depth/tape, and place/cancel are HOT.- New scanner names wait for L1
reqMktData/ tick-6. Do not run a 40-widereqTickersAsyncon the hot path. - A slow IB loop blocks the desk honestly. It never auto-kills the process (
API_WEDGEDmust not callstartLocalApi/ ElectronrestartApionce Task 4 ships). - Do not move
connectAsynconto a new thread during a liveNOVA_API_RELOAD=1session.
- Target: HTTP/WS on uvicorn; all
-
Earnings tab is calendar metadata, not a price feed.
backend/earnings_calendar.pyfetches Finnhub's/calendar/earnings(symbol, date, BMO/AMC session, EPS/revenue estimate + actual) -- no IBKRreqScannerSubscriptionlease, no/ws/scannerprice stream, and no HOD Momo admission (same carve-out as Large Cap, ADR 008). Company name/sector/market cap are decorated read-time from the existing yfinance fundamentals cache (cache-only, background-warmed byearnings_enrich_hooks.py) -- never a blocking per-row Yahoo call. Implied move / IV / surprise-% are not computed -- Finnhub's free calendar has no options data; do not invent a formula. A ticker click opens Trader the same way every other scanner ticker does (SymbolSelectButton->onOpenTrading, ADR 011). -
Nova News is an AI-in-trading headline desk, not a price feed.
backend/nova_news/aggregates targeted Google News RSS (AI trading / quant / bots / wires), Yahoo-scoped AI-trading RSS, trade-press and research feeds, Finnhub/news, and Alpaca/v1beta1/news. Admission requires a high-signal "AI is doing the trading" phrase or a headline that pairs an AI term with a trading term -- NVDA-as-an-AI-stock and generic Fed tape are not rows. It is not an IBKR scanner lease, has no/ws/scannerprice stream, and must never admit symbols into HOD Momo. Empty + all-sources-down is a louderror, not a fresh empty desk. Live sources with only off-topic headlines are an honest empty desk. Ticker chips open Trader (SymbolSelectButton->onOpenTrading, ADR 011). Do not scrape HTML. Do not paint Alpaca/Yahoo/Finnhub prices from this path. Do not reusetools/ai_news_rank.rank_articleson this path. Catalysts stays on-roster-with-news; Nova News is the AI-in-trading desk. -
Volume boost is a derived L1 spike list, not a scanner lease.
backend/volume_boost.pywatches day-volume already arriving on the shared scanner/detail/HODreqMktDataline (apply_l1_quote->observe_l1). Metric is last-60s volume rate vs the prior 10 minutes -- not sessionrvol_5min, not Large Cap swing RVOL, not a third-party unusual-volume scrape. No privatereqMktData. No IBKRreqScannerSubscription. No HOD admission. No/ws/scannerroster. Empty + IBKR ready is an honest empty desk. Empty + Gateway down is a loudfeed_error. Debounce one-shot blips; show spike age and cool-off; cap top-N. Ticker click opens Trader (SymbolSelectButton->onOpenTrading, ADR 011). The bot packvolumestays a stub (BOT_PACK_STUB) -- this tab is not live fire. -
The Cryptos page is a labelled reference board, not a trading feed (ADR 040).
backend/crypto/reads the crypto market from named public sources -- CoinGecko (prices, changes, volumes, caps, dominance), Coinbase Exchange (candles, the 16:00 ET prices), alternative.me (Fear & Greed), Hyperliquid (funding, open interest), DefiLlama (stablecoins), Deribit (options expiries) and Alpaca news -- and the page names the source beside every number. Those numbers never reach a scanner row, a stock's quote / chart / Level 2 / tape, HOD Momo, a setup lane, a bot, an order or a fill. The page's stock numbers (the bridge panel) are IBKR's: coldsnapshot_quotesand regular-hours daily closes through the paced historical service at background priority. No source stands in for another: a failed source's numbers readnullwith its error. Nothing polls while the page has not asked withinCRYPTO_WANTED_SEC. Nova places no crypto order; the "Spot" chip only says IBKR lists the coin.The Market page (ADR 053) is a reference board too, but every price on it is IBKR's: futures front months, Cboe indexes and NYSE's indicators (AD / VOL / TICK / TRIN), ETFs and stocks, each from a line held for a moment (
ibkr/brief_lines.py, ADR 053 amendment, ADR 054), bars through the paced historical service. It keeps no streaming line (a brief line only whileIBKR_BRIEF_MIN_FREE_LINESare spare) and its non-stock requests are fenced (ibkr/reference_errors.py): an error on a FUT / CONTFUT / IND / CASH / CRYPTO contract never sets the stock session's delayed-data fallback or max-tickers note. Its non-price facts -- State Street holdings, BEA / BLS / Fed / SIFMA calendars -- are labelled. Nothing on it feeds a scanner row, a chart, HOD Momo, a setup, a bot or an order.
Anti-patterns
- ❌ Looping fake real tickers (SPY / AAPL) as Sim tape, or placing Sim orders through IBKR Gateway
- ❌ Writing Massive-sourced prints or bars into the IBKR chart store (
bars_store), or drawing IBKR bars under a Massive replay window (ADR 046) - ❌ Drawing an imported exchange book as the consolidated Level 2, dropping its venue label, or pricing a bid / ask or a practice fill from it (ADR 057)
- ❌ Painting Sim ticks as paper/live IBKR, or leaving Sim on while calling
ibkr.orders.place - ❌ Keeping previous
detailwhile loading the next ticker so L2 stays on NXTC under an MVO quote - ❌
chart_barscatching IBKR errors and calling Alpaca when discovery=ibkr - ❌ UI copy that says "IBKR" while a silent Alpaca path still runs
- ❌ Applying
trade_update/ book updates before the matchinginitial/subscribedfor the current symbol - ❌
sleep-then-send for a short pacing window on the IB loop -- reschedule viacall_laterand release the hist semaphore (D-025) - ❌ SQLite from an IB callback or the connect-loop -- ledger ack/fill/facts go through
execution/persist_queue.py,bars_storethroughasyncio.to_thread(D-012, D-018) - ❌
asyncio.to_thread(cancel_order_verified, ...)--cancelOrder+ theopenTradesverify belong on the IB loop as an awaitable (cancel_order_verified_on_ib), never a worker thread thattime.sleep-polls (D-012) - ❌ Putting incoming tick types (49 Halted) into
genericTickList-- IBKR Warning 321 poisons the shared L1 upgrade. Halted arrives asticker.haltedon defaultreqMktData(#178) - ❌ Treating
ticker.haltedNaN / -1 as resume after cancel/resubscribe, orcancelMktDatafrom a depth-only unsubscribe, or hiding an RSS-open halt because tick 49 never arrived (#237) - ❌ A private
reqMktDatafor shortability, RTVolume 233, halt, or the depth L1 fallback -- it is idempotent per contract, so it hands back the pooled ticker with no extra generic ticks, and itscancelMktDatacloses the desk's line. Take aticksowner and merge legal generic ticks only (OWNER_LISTING/OWNER_DEPTH/IBKR_L1_GENERIC_TICKS=233) (D-020, D-049, #178) - ❌ Inventing a LULD countdown from a quiet Time & Sales tape, or keeping a confident forever clock after 10 minutes still halted
- ❌ Nesting table reprice inside
_scan_loopsleep (starves prices for 10–90s during movers scans) - ❌ Using
reqTickersAsync/ 4s timeouts for active-table freshness (IBtickSnapshotEnd≈ 11s) - ❌ Streaming the whole discovery universe via
reqMktData - ❌ Rebuilding every scanner cache list on each L1 tick (
apply_l1_quotepatches one indexed row) - ❌ Returning
commit_tableFalse after REST already advanced because the WS roster push threw - ❌ Leaving
_inflight_scan_reqidsintact across a full reconnect (newIB()never issued those ids) - ❌ Blocking roster admission on a cold
snapshot_quotes/reqTickersAsyncresult (a name with no quote is a row withprice=null, not a dropped row) - ❌ Rendering
0.00for a newly admitted scanner name that has no L1 tick yet - ❌ Opening a
TOP_OPEN_PERC_GAINlease before 09:30 ET, or treating its Warning 165 empty result as "no gaps today" - ❌ A scanner watchdog that skips a lease because it never delivered, or reopens a silent lease faster than
IBKR_SCANNER_FIRST_BATCH_RESUBSCRIBE_SEC - ❌ Letting one empty table vouch for another ("OK if another scanner list is live") when both are empty
- ❌ Painting Integrity fail for empty hist/HOD/scanner buffers when
current_modeis closed (no live roster is expected) - ❌ Mutating a
frozentable's membership/rank/values/timestamp after its session boundary - ❌ Applying a scanner/hydration callback from a superseded IB generation, subscription epoch, or session key
- ❌ Reviving HOD volume seeds, a
belowPriceGainers pass, open-ticker priority, or Losers as HOD inputs - ❌ Pretending table prices are live when the active-tab L1 subscription is incomplete
- ❌ Calling
historical_service.request_bars/reqHistoricalDatafrom HOD surge seed (charts own the 60/10min budget)- ❌ Feeding T&S from the Alpaca recorder (
l2.tape) instead oftape_stream.py
- ❌ Feeding T&S from the Alpaca recorder (
- ❌ Showing prior symbol's prints in T&S after a symbol switch
- ❌ Mounting Level 2 or Time & Sales in the scanner Quote Panel (Trader tabs only)
- ❌ Unmounting Trader (closing tape/L2 WS) when the operator clicks Scanner
- ❌ Opening a 4th live L2 stream without graying the least-recent live tab (depth plan cap) -- extra names may stay on the strip, but they must not all be live
- ❌ Calling
_hod_momo.on_trade_updatefrom the Alpaca WS loop when discovery=ibkr - ❌ Opening Alpaca market-data WebSocket while discovery=ibkr (burns the one WS slot for nothing)
- ❌ Classifying
hydrate_rows/snapshot_quotesas HOT - ❌
asyncio.to_threadaroundib.*(place/cancel/req*) - ❌ Auto-killing a listening API PID because
/api/modeor/api/healthtimed out (loop starve is not a dead process) - ❌ Serializing all
reqHistoricalDatathrough onecold_slotmutex (IB allows 50 in flight; the real limit is pacing) - ❌ Client 25s abort that paints "IBKR historical may be busy" on a ticker click
- ❌ Classifying a pacing violation as retryable without a token bucket (retry storms)
- ❌ Returning a stored stub when pacing wait > 0 and dropping the fill (wedges derived 1Hour / 15Min panes)
- ❌ Letting scanner L1 last upserts rewrite a hist-owned
bars_intradaycandle (same(symbol, timeframe, ts)-- volume is not ownership; IB hist minutes can be volume=0) - ❌ A client-side scanner filter (exchange, watch, etc.) dropping a row because a metadata field is unknown -- unknown must fail open, and a real narrowing must show a visible hidden-count, not a quiet empty table (2026-08-25: NASDAQ-only default + drop-on-unknown blanked 31 gappers/50 gainers to 1 row with no on-screen cause)
- ❌ Starting a second
uvicorn/run_api.pywhile one already holds:8000and clientId 17 (Error 326, session stuckconnecting, door trailFailed to fetch) - ❌ Opening an IBKR scanner lease, streaming L1, or admitting Earnings-tab symbols into HOD Momo -- it is a Finnhub calendar list, not a discovery table
- ❌ Inventing implied move / IV / surprise-% for the Earnings tab from price alone -- Finnhub's free calendar has no options data
- ❌ Treating Nova News as a scanner/price feed, admitting its symbols into HOD Momo, or scraping publisher HTML instead of the HTTP APIs/feeds
- ❌ Letting Catalysts copy claim a news universe while the filter is still IBKR-roster-only -- Nova News is the AI-in-trading desk
- ❌ Filling Nova News with generic tape or "AI stocks to buy" instead of AI-used-in-trading headlines
- ❌ Painting Positions Mkt Price / Day P&L only from a 5s portfolio poll when Nova already has a live L1 last for that symbol -- prefer
ticks.last_quotes(do not open a privatereqMktDatajust to mark a position) - ❌ Firing
accountSummaryAsyncon the 1s account-cluster poll -- read thereqAccountUpdatescache (accountValues/portfolio) - ❌ Opening a private
reqMktDataor IBKR scanner lease for Volume boost, ranking by sessionrvol_5min/ Large Cap swing RVOL as if that were a spike, inventing volume from a quiet tape, or letting the botvolumepack fire from this tab
Related
architecture/decisions/010-ib-loop-isolation.md-- two loops, one clientId, hot/cold schedulerbackend/ibkr/work_class.py-- HOT/COLD label SSOTarchitecture/decisions/008-persistent-ibkr-scanner-rosters.md-- session-owned roster ADRbackend/ibkr/scanner_stream.py— persistent scanner subscriptions, freeze/rolloverarchitecture/decisions/012-local-first-chart-bars.md-- store-first bars + paced historical servicebackend/ibkr/historical_service.py-- sole production scheduler forreqHistoricalDatabackend/bars_store.py-- durable IBKR bars + coveragebackend/chart_bars.py-- store-first IBKR-only when discovery=ibkrbackend/ibkr/scanner_l1.py— active-tab + reserved HOD L1 streams →/ws/scannerbackend/ibkr/l1_minute.py-- scanner L1 last + size deltas → live 1Min overlay (source=ibkr_l1); hist fills own the candle and replace itbackend/ibkr/ticks.py— shared owner-awarereqMktData(detail / scanner / HOD / depth / listing);ticks_generic.pyowns generic-tick merges and drops illegal STK ticks; defaultIBKR_L1_GENERIC_TICKS(233) rides that one linebackend/ibkr/account_marks.py-- Positions / Day P&L prefer existing L1 last;account_stream.pykeepsreqAccountUpdatessubscribedbackend/ibkr/halt_eta.py/halt_status.py-- LULD clock +ticker.haltedobserve; Stock View L2HaltEtaChipbackend/ibkr/nasdaq_halt_rss.py/nasdaq_halt_feed.py-- official Trade Halt RSS parse + ≤1/min poll; MWCB desk banner via/api/halts/deskbackend/ibkr/cancel_verify.py-- IB-loop cancel + awaitable openTrades verifybackend/execution/persist_queue.py-- FIFO off-loop drain for IB-callback ledger writes (telemetry_persist.pybuilds the jobs)backend/ibkr/reprice.py— detail backstop only (cold snapshots, not table SLA)backend/ibkr/tape_stream.py—reqTickByTickData AllLastsubscriberbackend/earnings_calendar.py-- Finnhub calendar fetch/cache;earnings_enrich_hooks.py-- background yfinance name/sector/mcap warmbackend/nova_news/-- Nova News desk (AI-in-trading headlines, criticality columns)backend/volume_boost.py-- Volume boost tab (L1 day-volume rate spike vs recent baseline; no new ticks)backend/scanner_push.py—/ws/scanner+set_active_tabfrontend/src/hooks/useTickerStream.ts,useIbkrDepth.ts,useIbkrTape.ts,useScannerPriceStream.tsfrontend/src/utils/dataSourceMap.ts— honest attribution
Recording source (ADR 017)
AllLast is the only production recording print source. Reuse its bounded capture/L2 fan-out; never open a second subscription or restore Alpaca tape ingest. A real tab requires a connected, non-rejected matching producer; waiting/stale/failed writers must remain visible. Do not manufacture quote/depth rows from print data.
