This document describes how data moves through the Pankaj Bhatia trading system — from Zerodha Kite through the Node.js backend to the React dashboard. It covers the full market-data pipeline (instrument sync → live ticks → snapshots), orders, settings, and financial updates. For strategy rules see Trading overview; for system layout see Backend overview.

1. Overview

The system uses a dual-path real-time model: raw broker data is processed on the server, then pushed to browsers via Socket.io. Automated strategies read from an in-memory tick cache; the UI listens for refresh signals and re-fetches via REST where needed. Before live ticks flow, symbol and instrument metadata is synced from Kite into MongoDB. Active index settings drive WebSocket subscriptions; underlying LTP drives dynamic option strike selection; snapshots are built every second and fan out to the UI, database, and automation.

2. Real-Time Market Data Pipeline

Live market data is not a single WebSocket hook — it is a multi-stage pipeline from instrument metadata through strike calculation to snapshot broadcast.

2.1 Instrument & symbol sync → DB

Source: indexSettingGenerator.tssyncIndexSettings()
  1. Fetches instruments from Kite for NSE, NFO, MCX, CDS via getInstruments(exchange).
  2. For each symbol, builds or updates a document in the indexSetting collection:
    • cash — NSE EQ instrumentToken, lot size, tradingsymbol
    • futuresChainByExpiry — all FUT expiries mapped to token, tradingsymbol, expiryType (weekly/monthly)
    • optionChainByExpiry — per expiry: strikeList, baseDiff, strikes map (CE/PE tokens per strike)
    • optionExpiriesByFuturesExpiry — which option expiries are valid for each futures expiry
    • optionsUnderlyingTypeSPOT or FUTURES (options priced off cash or linked futures)
  3. Upserts by index (lowercase symbol). Preserves admin-controlled fields: isActive, tradingModes, recurring/trailing settings, strike differences.
  4. Emits indexSettingEmitter on updates so the live processor can reload subscriptions.
When it runs:

2.2 Active symbols → token registration

Source: zerodha-event.utils.tsloadIndexSettings(), registerBaseToken(), subscribeBaseTokens() When ZerodhaEventProcessor.start() runs:
  1. Loads indexSettingModel.find({ isActive: true }) — only active indices participate in streaming.
  2. For each active index, enriches optionExpiriesByFuturesExpiry via enrichOptionExpiriesMap().
  3. Registers base tokens when the corresponding tradingModes.* flag is enabled:
  1. Subscribes all base tokens through ZerodhaConnectionManager.subscribe().
On settings change: When indexSettingEmitter fires indexSettingUpdated, onIndexSettingUpdated reloads settings, clears strike/snapshot cache for the affected index, resubscribes base tokens, and recalculates option subscriptions.

2.3 Real-time LTP (Kite WebSocket)

Source: zerodha-connection.utils.ts, processTick()
  1. ZerodhaConnectionManager creates a Kite Ticker instance, connects, and subscribes tokens in FULL mode (depth + OI).
  2. Every incoming tick is stored in the liveData Map keyed by instrument_token — this holds all subscribed tokens including option legs.
  3. Each tick is forwarded to ZerodhaEventProcessor.processTick().
  4. Base segment ticks (cash, futures, options underlying) update segmentTicks[indexName][segment].
  5. An options underlying tick additionally triggers handleOptionUnderlyingTick() for strike recalculation.
  6. Option leg ticks do not enter segmentTicks. They remain in liveData and are read at snapshot time via zerodhaConnection.getLatest(token) and getBidAsk(token).

2.4 Option strike calculation & dynamic subscribe

Source: calculateRequiredStrikes(), updateOptionSubscriptions(), handleOptionUnderlyingTick() When the options underlying LTP moves, the processor computes which strikes to track and which option instrument tokens to subscribe. Strike formulas (from calculateRequiredStrikes): From these strikes, 8 instrument tokens are resolved from optionChainByExpiry[expiryDate].strikes (CE/PE for each role) and subscribed via updateOptionSubscriptions(). Tokens are added or removed only when strikes change (compared against lastCalculatedStrikes). When optionsUnderlyingType === "FUTURES", option expiry is validated against the linked futures expiry via getOptionExpiriesForFutures() — invalid pairings skip subscription.

2.5 Snapshot build (buildSnapshot)

Source: buildSnapshot(), buildCashSnapshot(), buildFuturesSnapshot(), buildOptionsSnapshot() Every 1000ms, broadcastAll() calls buildSnapshot() for each active index:
  1. Reads latest ticks from segmentTicks for cash, futures, and options underlying.
  2. Sets baseValue from activeSegment setting:
    • cashcash.lastPrice
    • futuresfutures.lastPrice
    • optionsoptions.underlyingLastPrice
    • Fallback chain: options underlying → futures → cash
  3. Builds nested segment objects (cash, futures, options) with quote fields (lastPrice, bidPrice, askPrice, oi, volumeTraded).
  4. For options, reads leg premiums from liveData via getOptionQuote() and attaches an options.legs[] array (role, strike, right, prices).
  5. Merges legacyFields (flat strike numbers + bid/ask premiums) onto the snapshot root for backward compatibility with automation and UI.
See §3 Snapshot schema reference for field definitions and an example payload.

2.6 Persist, emit, fan-out

Source: saveToDbAndEmit(), socket-manager.ts Each broadcast cycle produces three outputs: Additionally, ZerodhaEventProcessor.latestSnapshotData holds the most recent snapshot per index (getLatestData()). Write rate: approximately one IndexData document per active index per second.

2.7 Streaming lifecycle

Market data starts when a valid Zerodha access token exists:
  • handlePostZerodhaLogin()ensureMarketDataRunning({ forceRestart: true })restartMarketData()ZerodhaEventProcessor.start()
  • Stopped on disconnect, 6 AM token refresh, or server shutdown via handleZerodhaDisconnect()
See Zerodha auto-login for token refresh and always-on streaming behavior. Typical startup order: server boot → backfillOptionExpiriesMap() → Zerodha login → ZerodhaEventProcessor.start() → live ticks; full instrument sync runs after each exchange session close.

3. Snapshot Schema Reference

Each tick snapshot is a single JSON object stored in IndexData and emitted over Socket.io. The shape is built by buildSnapshot() in zerodha-event.utils.ts.

Root metadata

Legacy option fields (root level)

These flat fields are merged from options.legacyFields for backward compatibility. Strike keys hold strike prices (numbers); premium keys hold bid/ask quotes.

Nested segment objects

cash (when tradingModes.cash enabled): futures (when tradingModes.futures enabled): options (when tradingModes.options enabled): options.legs[] entry:

Example snapshot (trimmed)

4. Application Consumers

Market data reaches the application through two paths: direct Socket.io for live UI updates, and latestDataMap for server-side automation.

React UI (Socket.io direct)

Components subscribe to ${indexName}TickData and update local state for live prices, strikes, and tables. Each page passes tick data to IndexDataTable for historical/recent tick display.

Automation (latestDataMap)

Automated buy/sell logic does not subscribe to Kite directly. Controllers read latestDataMap[\$TickData`]` on each interval tick. Recurring buyrecurringOrder.controller.ts
  • executeBuyOrder reads the latest snapshot for tradingsymbol, strike, and price:
    • Options: putCall1 / callPut1 for strike; putCallAskPrice1 / callPutAskPrice1 for price
    • Cash: cash.lastPrice or baseValue
    • Futures: futures.lastPrice or baseValue
  • Skips if tick data is missing or exchange is closed.
Trailing / scheduled sellscheduleOrder.controller.ts
  • startInterval() runs every 1 second, iterating active symbols.
  • Reads latestDataMap[\$TickData`]and passes it totrailingSell()`.
  • For candle min/max over the sell duration window, also aggregates from indexDataModel (and an in-memory tick buffer at candle end).
  • On price match, calls handlePlaceOrder for MARKET SELL.
Manual orderszerodha.controller.ts
  • Resolves order price and strike from latestDataMap when placing limit or protected market orders.
For business rules (strike match formulas, leg switching), see Trading overview.

5. Order Placement Flow

All live and dummy orders converge on a single function: handlePlaceOrder in zerodha.controller.ts.

After placement

Once an order is saved, the controller emits internal events: registerOrderListeners() in socket-manager.ts bridges these to client-facing Socket.io events (see §7).

6. Order Status Update Flow (Webhook)

Kite sends asynchronous order updates to the backend webhook. Route: POST /api/zerodha/webhookhandlePostback in zerodha.controller.ts

Step-by-step

  1. Kite POSTs order status (e.g. COMPLETE, REJECTED, partial fills).
  2. Backend parses payload via parseZerodhaPostback.
  3. Order document is upserted in MongoDB; final statuses (COMPLETE, REJECTED) and isSold are preserved on update.
  4. orderEmitter.emit("orderUpdated", updatedOrder) and orderEmitter.emit("notification", …) fire.
  5. Socket bridge emits fetchOrderData and fetchFinancialData to all clients.
  6. Frontend order tables listen on fetchOrderData and re-fetch order lists via REST.

7. Socket.io Bridge (Internal → Client)

The backend uses a two-layer event model: internal Node EventEmitter instances decouple business logic from Socket.io transport.

Event mapping table

Connection setup

  • Frontend: socketConnect.ts connects with query: { userId } and a 15s heartbeat (ping / pong).
  • Backend: userSocketMap maps userId → array of socket IDs for targeted emits (updateUserSettingData).
  • Most refresh events use io.emit (broadcast to all connected clients).

8. Financial / PnL Flow

Position and PnL data runs on a separate 5-second loop, started alongside market data after Zerodha login. Source: startFinacialInterval() in zerodha.controller.ts

Step-by-step

  1. Every 5 seconds, startFinacialInterval fetches net positions from Kite REST.
  2. getLivePnl() opens (or reuses) a dedicated Kite Ticker WebSocket for position instrument tokens.
  3. On each tick batch, realized/unrealized PnL is recalculated per position.
  4. orderEmitter.emit("fetchFinancialSocketData", financialData) pushes the array to clients.
  5. Admin dashboard listens on fetchFinancialSocketData and updates the financial table.
  6. fetchFinancialData (generic refresh signal) is also emitted on order place/update events.
Started via ensureMarketDataRunning()startFinacialInterval(). Stopped via handleZerodhaDisconnect()stopInterval('startFinacialInterval') + resetFinancialWebSocket().

9. Settings Sync Flow

When index or user settings change via API, the backend updates MongoDB and notifies clients.

Index settings

  • Emitted from indexSetting.controller.ts, order.controller.ts, indexSettingGenerator.ts, and recurringOrder.controller.ts.
  • indexSettingUpdated triggers:
    • Socket: fetchIndexSettingData → all clients refetch settings
    • Market data: processor reloads active settings and resubscribes tokens (see §2.2)
  • indexRecurringSettingUpdated also triggers fetchIndexSettingData.

User settings

  • Emitted from user.controller.ts and indexSetting.controller.ts when page access changes.
  • userSettingUpdated sends populated user document to only that user’s socket IDs via updateUserSettingData.

10. Session Lifecycle vs Data Flow

When each data flow is active relative to market sessions and Zerodha token state. Market data streaming is not gated on market hours — ticks may show last-close prices when exchanges are closed. Order automation is gated via isExchangeMarketOpen() in controllers and marketSessionManager.ts.

11. End-to-End Combined Flow

High-level path from login through automated trade to UI update: Plain-language sequence:
  1. Admin logs into Zerodha → WebSocket streaming starts (after active index settings are loaded).
  2. Ticks broadcast to UI and populate latestDataMap.
  3. Recurring buy reads latest tick → places BUY via handlePlaceOrder.
  4. Kite webhook confirms BUY complete → order record updated → UI refreshes.
  5. Schedule order created for trailing sell → 1s loop watches baseValue vs strike/target.
  6. On match, MARKET SELL placed → webhook updates → UI refreshes again.

12. Code Map

Summary

  • Instrument sync pulls NSE/NFO/MCX/CDS instruments from Kite into IndexSetting; only isActive: true indices stream live data.
  • Market ticks flow through base-token LTP → dynamic option strike subscribe → 1s snapshot → three outputs: Socket.io, MongoDB, and latestDataMap.
  • Option leg prices live in liveData (not segmentTicks); snapshots read them at build time into nested options.legs[] and flat legacy fields.
  • latestDataMap is the shared hub — recurring buy and trailing sell read from it every interval, not from Kite directly.
  • Orders always flow through handlePlaceOrder; internal orderEmitter events are bridged to client refresh signals by socket-manager.ts.
  • Kite webhooks update order status asynchronously; the UI refetches order lists on fetchOrderData rather than receiving full order payloads over Socket.io.
  • Financial PnL runs on a separate 5s loop with its own Kite Ticker subscription, pushed via fetchFinancialSocketData.
  • Settings changes propagate via emitters, trigger market-data resubscription, and cause targeted or broadcast UI refreshes.