Learning Hub Market Data & APIs Lesson 16
Phase 2 — Lesson 7 of 8
Hub
Phase 2 · Market Data & APIs · Lesson 16

WebSocket & Live Tick Feed

Stream real-time NIFTY and BANKNIFTY ticks via KiteConnect WebSocket. Convert raw ticks into live candles, update indicators on each tick, and drive your signal engine — all in real time.

~50 min
Advanced
3 Quiz Questions
3 Exercises
Phase 2 Progress88%
Section 1
LESSON 16 · WEBSOCKET TICK FEED
REST vs WebSocket
advancedreal-time

The REST API (lessons 14–15) requires you to poll repeatedly — call, wait, call again. WebSocket is a persistent connection that pushes data to you every time a trade happens on NSE. This is how all serious live algo systems receive market data.

📡
Tick Stream
Every trade that happens on NSE generates a tick — LTP, volume, bid/ask, OI. KiteSocket delivers these in milliseconds.
🕯️
Tick → Candle
Your code aggregates incoming ticks into OHLCV candles (5-min, 15-min) and appends them to the DataFrame when each candle closes.
Quote Modes
LTP mode (lightest), Quote mode (adds depth/OI), Full mode (everything including 5-level depth). Use LTP for indices.
🔁
Auto Reconnect
KiteSocket handles reconnection automatically. Your on_ticks callback runs after every reconnection — no manual retry code needed.
NSE trade
KiteSocket tick
on_ticks()
TickToCandle
Candle closes
Signal engine
Order / Log
Section 2

KiteTicker — Basic Setup

from kiteconnect import KiteTicker
import json, threading

# Load credentials
with open("config.json") as f:
    cfg = json.load(f)

# Instrument tokens to subscribe
TOKENS = {
    256265: "NIFTY 50",
    260105: "BANKNIFTY"
}

# ── Create KiteTicker instance ──
kws = KiteTicker(api_key=cfg["api_key"], access_token=cfg["access_token"])

# ── Callback: runs on every tick ──
def on_ticks(ws, ticks):
    for tick in ticks:
        token     = tick["instrument_token"]
        ltp       = tick["last_price"]
        vol       = tick.get("volume", 0)
        timestamp = tick["exchange_timestamp"]
        symbol    = TOKENS.get(token, str(token))

        print(f"[{timestamp.strftime('%H:%M:%S')}] {symbol:12} LTP: ₹{ltp:,.2f}  Vol: {vol:,}")

# ── Callback: runs on WebSocket connect ──
def on_connect(ws, response):
    print("✅ Connected to KiteTicker")
    ws.subscribe(list(TOKENS.keys()))
    ws.set_mode(ws.MODE_LTP, list(TOKENS.keys()))  # lightest mode for indices

# ── Callback: runs on disconnect ──
def on_close(ws, code, reason):
    print(f"❌ Disconnected: {code} — {reason}")

# ── Callback: runs on error ──
def on_error(ws, code, reason):
    print(f"⚠️  Error: {code} — {reason}")

# ── Assign callbacks ──
kws.on_ticks   = on_ticks
kws.on_connect  = on_connect
kws.on_close    = on_close
kws.on_error    = on_error

# ── Connect (runs in background thread) ──
kws.connect(threaded=True)
Live Tick Stream
✅ Connected to KiteTicker [09:15:01] NIFTY 50 LTP: ₹21,850.20 Vol: 0 [09:15:01] BANKNIFTY LTP: ₹47,210.50 Vol: 0 [09:15:02] NIFTY 50 LTP: ₹21,853.40 Vol: 1,250 [09:15:02] BANKNIFTY LTP: ₹47,218.90 Vol: 850 [09:15:03] NIFTY 50 LTP: ₹21,849.70 Vol: 2,100
💡
MODE_LTP — receive only the last traded price. Use for indices and when you only need price. MODE_QUOTE adds OHLC + depth. MODE_FULL adds 5-level market depth + OI. Start with LTP and upgrade when needed.
Section 3

Tick → Candle Builder

Ticks arrive multiple times per second. You need to aggregate them into OHLCV candles. The TickToCandle class builds candles from raw ticks and fires a callback when each candle closes.

from datetime import datetime
import pandas as pd

class TickToCandle:
    """
    Converts a stream of LTP ticks into OHLCV candles.
    Fires on_candle_close(candle_dict) when a candle completes.
    """

    def __init__(self, interval_mins: int, on_candle_close):
        self.interval        = interval_mins
        self.on_candle_close = on_candle_close
        self._reset()

    def _reset(self):
        self.candle_open_time = None
        self.open  = None
        self.high  = None
        self.low   = None
        self.close = None
        self.volume = 0
        self._prev_vol = 0

    def _candle_slot(self, ts: datetime) -> datetime:
        """Round timestamp down to the start of the candle's time slot."""
        minute_floor = (ts.minute // self.interval) * self.interval
        return ts.replace(minute=minute_floor, second=0, microsecond=0)

    def on_tick(self, ltp: float, volume: int, timestamp: datetime):
        """Process a single tick."""
        slot = self._candle_slot(timestamp)

        if self.candle_open_time is None:
            # First tick — start the first candle
            self.candle_open_time = slot
            self.open  = ltp
            self.high  = ltp
            self.low   = ltp
            self._prev_vol = volume

        elif slot > self.candle_open_time:
            # New candle started — fire callback with completed candle
            completed_candle = {
                "datetime": self.candle_open_time,
                "open":     self.open,
                "high":     self.high,
                "low":      self.low,
                "close":    self.close,
                "volume":   self.volume
            }
            self.on_candle_close(completed_candle)

            # Reset for new candle
            self._reset()
            self.candle_open_time = slot
            self.open      = ltp
            self._prev_vol = volume

        # Update current candle
        self.high  = max(self.high, ltp)
        self.low   = min(self.low, ltp)
        self.close = ltp
        self.volume += max(0, volume - self._prev_vol)
        self._prev_vol = volume
Section 4

Wiring It All Together

import pandas as pd
from datetime import datetime, time as dtime

# ── In-memory candle buffer (pre-filled with warm-up data from Lesson 15) ──
candle_buffer = warm_df.copy()  # DataFrame with historical OHLCV + indicators

def on_new_candle(candle: dict):
    """Called each time a candle closes. Updates indicators and runs signal engine."""
    global candle_buffer

    # Append the new candle row
    new_row = pd.DataFrame([candle]).set_index("datetime")
    candle_buffer = pd.concat([candle_buffer, new_row])

    # Recalculate indicators incrementally on last 50 rows
    tail = candle_buffer.tail(50)
    ema9  = tail["close"].ewm(span=9,  adjust=False).mean()
    ema21 = tail["close"].ewm(span=21, adjust=False).mean()

    latest_ema9  = ema9.iloc[-1]
    latest_ema21 = ema21.iloc[-1]
    latest_close = candle["close"]

    signal = "BUY" if latest_ema9 > latest_ema21 else "SELL"

    print(f"[{candle['datetime'].strftime('%H:%M')}] "
          f"NIFTY ₹{latest_close:,.1f} | EMA9={latest_ema9:.1f} EMA21={latest_ema21:.1f} | {signal}")

# ── Create tick builder for NIFTY 5-minute candles ──
nifty_builder = TickToCandle(interval_mins=5, on_candle_close=on_new_candle)

# ── Updated on_ticks callback that uses the builder ──
def on_ticks(ws, ticks):
    now = datetime.now()

    # Stop trading after 15:15 (no new entries in last 15 min)
    if now.time() > dtime(15, 15):
        return

    for tick in ticks:
        if tick["instrument_token"] == 256265:  # NIFTY 50 only
            nifty_builder.on_tick(
                ltp       = tick["last_price"],
                volume    = tick.get("volume_traded", 0),
                timestamp = tick["exchange_timestamp"]
            )
Live Candle Output
[09:20] NIFTY ₹21,876.4 | EMA9=21862.1 EMA21=21845.3 | BUY [09:25] NIFTY ₹21,891.2 | EMA9=21868.7 EMA21=21849.8 | BUY [09:30] NIFTY ₹21,845.6 | EMA9=21863.2 EMA21=21849.1 | BUY [09:35] NIFTY ₹21,820.3 | EMA9=21851.4 EMA21=21847.5 | BUY [09:40] NIFTY ₹21,798.9 | EMA9=21838.2 EMA21=21844.6 | SELL
Section 5

Multi-Symbol WebSocket

# Subscribe multiple tokens and route ticks to separate builders
NIFTY_TOKEN    = 256265
BANKNIFTY_TOKEN = 260105

# Separate TickToCandle instances for each instrument
nifty_builder    = TickToCandle(5, lambda c: on_candle("NIFTY", c))
banknifty_builder = TickToCandle(5, lambda c: on_candle("BANKNIFTY", c))

builders = {
    NIFTY_TOKEN:    nifty_builder,
    BANKNIFTY_TOKEN: banknifty_builder
}

def on_candle(symbol: str, candle: dict):
    print(f"✅ New {symbol} candle: {candle['datetime'].strftime('%H:%M')} "
          f"O={candle['open']:.1f} H={candle['high']:.1f} "
          f"L={candle['low']:.1f} C={candle['close']:.1f} V={candle['volume']:,}")

def on_ticks(ws, ticks):
    for tick in ticks:
        token = tick["instrument_token"]
        if token in builders:
            builders[token].on_tick(
                ltp       = tick["last_price"],
                volume    = tick.get("volume_traded", 0),
                timestamp = tick["exchange_timestamp"]
            )

def on_connect(ws, response):
    print("✅ WebSocket connected")
    tokens = [NIFTY_TOKEN, BANKNIFTY_TOKEN]
    ws.subscribe(tokens)
    ws.set_mode(ws.MODE_LTP, tokens)
💜
Architecture Rule
One WebSocket, multiple instruments. KiteConnect allows subscribing up to 3,000 tokens on a single WebSocket connection. Never open multiple WebSocket connections — use the routing pattern above to handle all instruments from one callback.
Section 6

Market Hours Kill Switch

from datetime import datetime, time as dtime

MARKET_OPEN  = dtime(9,  15)
MARKET_CLOSE = dtime(15, 30)
LAST_ENTRY   = dtime(15, 15)   # No new entries after this

def is_trading_time() -> bool:
    """Return True only during NSE trading hours."""
    now = datetime.now().time()
    return MARKET_OPEN <= now <= LAST_ENTRY

def is_session_active() -> bool:
    """Return True while WebSocket should remain connected."""
    now = datetime.now().time()
    return MARKET_OPEN <= now <= MARKET_CLOSE

# Updated on_ticks with time gate
def on_ticks(ws, ticks):
    if not is_session_active():
        ws.close()
        print("🔴 Market closed — WebSocket disconnected")
        return

    for tick in ticks:
        token = tick["instrument_token"]
        if token in builders:
            builders[token].on_tick(
                tick["last_price"],
                tick.get("volume_traded", 0),
                tick["exchange_timestamp"]
            )
            # Only run signal engine during active entry window
            if is_trading_time():
                run_signal_engine(token)
Section 7

Quiz

Q1. What is the main advantage of WebSocket over polling the REST API for live data?
Q2. In the TickToCandle class, when does the on_candle_close callback fire?
Q3. How many WebSocket connections should you open to monitor NIFTY and BANKNIFTY simultaneously?
Section 8

Exercises

Exercise 01
Tick Logger
Set up a KiteTicker that subscribes to NIFTY 50 and BANKNIFTY. In the on_ticks callback, append every tick to a CSV file with columns: timestamp, symbol, ltp, volume. Run for 5 minutes during market hours and inspect the CSV.
Exercise 02
Candle Builder Unit Test
Without a live API, simulate 60 ticks for a 5-minute NIFTY candle (use datetime objects spaced 5 seconds apart, with realistic LTP values near 21900). Feed them to TickToCandle and verify: (1) candle open = first tick's LTP, (2) candle high = max LTP, (3) candle low = min LTP, (4) candle close = last tick's LTP before the next slot.
Exercise 03
Live Signal Monitor
Combine everything: warm-up 50 candles of NIFTY data from the historical API. Set up KiteTicker with TickToCandle for 5-minute candles. When each candle closes, recalculate EMA9/EMA21 on the last 30 candles and print a signal (BUY/SELL/WAIT) with the timestamp and current EMA values.
Section 9

Lesson Summary

KiteTicker
Persistent WebSocket. Callbacks: on_ticks, on_connect, on_close, on_error. Set threaded=True for non-blocking.
Subscribe Modes
MODE_LTP (price only), MODE_QUOTE (OHLC+depth), MODE_FULL (full depth+OI). LTP is sufficient for index signals.
TickToCandle
Detects new candle slot on each tick. Fires on_candle_close with complete OHLCV dict when slot changes.
Multi-Symbol
One WebSocket, many tokens. Route ticks to separate TickToCandle instances via token lookup dict.
Time Gate
is_trading_time() guards all signal logic. Stop new entries at 15:15, close WebSocket at 15:30.
Next Lesson
Lesson 17 combines everything into a complete live data pipeline — Phase 2 finale!
Prev