Dhan WebSocket Live Capture → Dashboard Wiring — Architecture That Does Not Lie (2026)

QUICK ANSWER

Q: How do I wire Dhan's WebSocket to a live dashboard that survives disconnects? Connect in ticker mode for a 50-name watchlist and full mode only for instruments you trade; add ping_interval=20, ping_timeout=10 to catch silently-dead sockets; parse binary depth defensively (Dhan sends prices as integer paise — divide by 100); and keep REST (OI/IV/PCR) and WS (LTP/depth) as independent failure domains that both write SQLite. [SOURCE: Dhan API WebSocket spec + reconnect pattern validated in production.] Caveat: a "connected but silent" socket is worse than a dead one — heartbeat is mandatory, not optional.

WHO THIS IS FOR / PREREQUISITES

This article assumes a Dhan account and the SQLite store from the companion piece (WS and REST both write there). You need Python with the websocket-client package and basic async/threading comfort. If you only trade from a chart, you do not need this — but if you want a model or dashboard fed by live flow, the WebSocket is non-negotiable. Read the scraper article first if you have no data source yet.

WHY THIS MATTERS

REST snapshots give you OI, IV, and PCR every minute. But if you want to see a 362-point Sensex spike in two seconds — the kind SEBI flagged in the August 2026 CAS order — you need a WebSocket. Dhan's API exposes ticker, quote, and full modes over a single WebSocket. This article is the wiring: connect, parse binary frames, survive disconnects, keep the stream schema-synced with your REST OI store, and visually QA the dashboard so a silent gap never poisons your model.

The dashboard is not a nice-to-have; it is your trust layer. A model is only as good as the data it trained on, and a single silent gap — a dead WebSocket, an expired token — introduces a hole the model "fills in" with a false pattern. The wiring in this article is designed so that failure is visible (heartbeat, visual QA) rather than silent (flat line that looks like calm). Build for failure you can see, not perfection you assume.

RESEARCH QUESTION / HYPOTHESIS

Hypothesis: a heartbeat + exponential-backoff reconnect makes the WS resilient enough that a model never trains on a silent gap. Test: kill the network mid-session and confirm the client detects death within ping_timeout and reconnects without data loss on the REST side. [OBSERVED: silent socket caught at ~10s via ping; REST continued feeding OI throughout.]

DATA & METHODOLOGY BOX

RESULTS

ModePayloadUse
TickerLTP + last volume50-name watchlist
QuoteBid/ask + depth snapshotspread/quote-stuffing features
Full5-level depth + OI + IVtraded underlyings only
ping_interval=20heartbeatcatches silent death ~10s

Finding 1: Full mode on all 50 names chokes phone CPU/bandwidth — use ticker for watchlist. [OBSERVED]
Finding 2: Forgetting to divide paise by 100 makes depth show 100× prices — silent feature corruption. [OBSERVED bug]
Finding 3: REST+WS as independent domains means one failure degrades gracefully, not blind. [OBSERVED]
Finding 4: Cross-source equality join (WS LTP = REST OI at same ts) is a bug — always bucket by time window. [SOURCE: design principle]

REPRODUCIBILITY (code)

import websocket, time, json, struct, threading

def on_message(ws, raw):
    if isinstance(raw, bytes):
        parse_binary_depth(raw)   # see below
    else:
        d = json.loads(raw); route_tick(d)

def connect_with_backoff(url, api_key, max_retries=8):
    delay = 2
    for i in range(max_retries):
        try:
            ws = websocket.WebSocketApp(url, header={"api-key": api_key},
                on_open=on_open, on_message=on_message,
                on_error=on_error, on_close=on_close)
            ws.run_forever(ping_interval=20, ping_timeout=10)  # heartbeat
            return
        except Exception as e:
            log(f"WS down: {e}; retry in {delay}s")
            time.sleep(delay); delay = min(delay*2, 120)
    raise RuntimeError("WebSocket unrecoverable")

def parse_binary_depth(b):
    try:
        token = struct.unpack_from(">I", b, 0)[0]
        n = struct.unpack_from(">H", b, 4)[0]
        bids, asks = [], []; off = 6
        for _ in range(n):
            price, qty, ordQty = struct.unpack_from(">iii", b, off); off += 12
            (bids if _ < n//2 else asks).append((price/100, qty, ordQty))  # /100 paise
        return token, bids, asks
    except struct.error as e:
        log(f"bad frame len={len(b)}: {e}"); return None, [], []

WHAT FAILED / COUNTER-EVIDENCE

Failed: run_forever() without ping → silent death for an hour (flat line looked like "no movement"). Failed: Full mode on 50 names → CPU saturation. Failed: raise in parser → crashed loop on one malformed frame. Counter-evidence to "WS is enough": WS alone misses OI/IV/PCR which only REST provides — you need both.

LIMITATIONS (explicit non-claims)

THE FULL PRODUCTION PIPELINE (Data Engine → Predictor → Filter)

1. DATA ENGINE     Dhan WS (LTP/depth) + REST (OI/IV/PCR) -> SQLite
2. FEATURE ENGINE  build_features() -> point-in-time lag, dedupe, label
3. PREDICTOR       gradient-boosting model -> prob_up per strike
4. FILTER          Greeks + regime + prob-band rules -> allow/block
5. EXECUTOR        paper or live entry sized by position_size()

def filter(prob, vix_z, dte, maxpain_dist):
    if not (0.58 <= prob <= 0.80): return "BLOCK"
    if vix_z > 2: return "BLOCK"
    if dte < 1: return "BLOCK"
    if maxpain_dist < 0.003: return "SHRINK"
    return "ALLOW"

WS+REST together are Stage 1. The dashboard is your visual trust layer — if it shows NaN at 09:15, your auth expired and the model would have trained on a gap.

SCHEMA SYNC: WS + REST

Tag each row with its own ts and source. The model reads REST for features, WS for execution context only. Never join them as simultaneous.

CREATE TABLE ltp_live (ts INTEGER, symbol_id INTEGER, ltp REAL,
    bid REAL, ask REAL, source TEXT DEFAULT 'ws', PRIMARY KEY(symbol_id, ts));
CREATE TABLE oi_rest (ts INTEGER, symbol_id INTEGER, oi INTEGER,
    iv REAL, pcr REAL, source TEXT DEFAULT 'rest', PRIMARY KEY(symbol_id, ts));
-- JOIN only on time window, never raw equality across sources
SELECT l.symbol_id, l.ltp, o.oi FROM ltp_live l JOIN oi_rest o
  ON l.symbol_id=o.symbol_id AND l.ts BETWEEN o.ts AND o.ts+60000;

PRODUCTION HARDENING: THE FULL LOOP

def orchestrate(conn, ws_url, api_key, rest_opener, symbols, stop):
    def rest_thread():
        while not stop.is_set():
            if market_open(datetime.now()):
                for sym in symbols:
                    try:
                        oc = fetch_with_backoff(rest_opener, f"{BASE}/api/option-chain-indices?symbol={sym}")
                        store(conn, sym, full_extract(oc, sym))
                    except Exception as e: log(f"rest {sym}: {e}")
            time.sleep(60)
    threading.Thread(target=rest_thread, daemon=True).start()
    while not stop.is_set():
        try: connect_with_backoff(ws_url, api_key)
        except RuntimeError:
            log("WS unrecoverable; REST continues independently"); break

REST and WS are independent failure domains. If WS dies, REST continues; if REST rate-limits, WS keeps feeding price.

AUTH FLOW: GETTING THE TOKEN

def get_ws_token(rest_client):
    resp = rest_client.login()  # client_id + access_token + expiry
    return resp["access_token"], resp["expiry_ts"]
def on_open(ws):
    tok, exp = get_ws_token(rest_client)
    ws.send(json.dumps({"action":"subscribe","mode":"full",
        "client_id":CLIENT_ID,"token":tok,"instruments":[YOUR_IDS]}))

Store the token in an env var or a permissions-700 file, never in the script. A leaked Dhan token is a funded-account risk.

VISUAL QA BEFORE YOU TRUST THE FEED

Render the dashboard every morning and diff against yesterday. If 09:15 is empty/NaN, auth expired overnight. Automate a screenshot + assert non-empty; alert on divergence. The cheapest bug-catcher is a picture, not a log line.

RESEARCH APPENDIX: DHAN WEBSOCKET MODES

Dhan's market-data WebSocket exposes three subscription modes [SOURCE: Dhan API documentation]:

Binary depth frames encode prices as integer paise (×100); the parser divides by 100 to recover rupees. The connection requires a valid access token from the REST login, refreshed on a schedule — never hardcoded. The ping_interval=20, ping_timeout=10 heartbeat is what detects a silently-dead socket; without it, a "connected" WS can sit idle for an hour and your model trains on the gap. REST (OI/IV/PCR every 60s) and WS (LTP/depth live) are independent failure domains — if one dies, the other keeps feeding.

MONITORING LOOP (post-publish)

Per the V2 pickup standard, track this article's external pickup at Day 7/14/30: search the title + canonical + author phrase; classify pickup as editorial, aggregator, scraper, or owned. Only editorial/aggregator improve weights. Monthly: roll findings into the next 10 experiments. Conservative weight changes only — human review for major shifts. The moat is the growing library of original, attributable infrastructure write-ups (WS parsing, heartbeat, schema-sync) that did not exist in useful form before.

WORKED EXAMPLE (illustrative numbers)

At 14:55 IST, full-mode tick on NIFTY shows bid 24,848.50 / ask 24,849.00 (paise 2484850/2484900 in the frame — divided by 100). [DERIVED example] Depth: 5 levels each side. CVD computed from the tick stream shows -1.8M over the last minute while price rose 12 points — a bull-trap divergence (price up, delta down). Your regime gate (VIX z 0.4, not expiry, not chop) allows the CVD feature to fire; the risk filter then checks prob-band and max-pain distance before any ALLOW. The dashboard renders this live; if at 09:15 it shows NaN, the morning visual QA alerts you before the model trains on a gap.

LEGAL AND ETHICAL NOTE

Dhan's data is for your own research and trading, not redistribution. Keep token handling secure (env var, permissions-700), never hardcode in scripts you share, and respect rate limits. The WebSocket is a window into the market — use it to build honest signals, not to replicate the expiry-window manipulation SEBI flagged in August 2026.

WHAT TO BUILD NEXT

Once WS+REST are stable: (1) add CVD/order-flow computation (companion article) on the full-mode ticks; (2) compute intraday PCR/IV-skew from REST; (3) write both point-in-time to the feature store; (4) train XGBoost with purged walk-forward; (5) gate behind the risk filter. The WS is Stage 1 — the live nervous system of the pipeline.

CHECKLIST: IS YOUR WS PIPELINE RESILIENT?

If any box is N, your pipeline will blind the model silently. The dashboard screenshot at 09:15 is worth more than any log line — a flat line means auth died overnight, and a model trained on that gap learns the open never moves.

GLOSSARY

COMMON MISTAKES

WEEKLY ROUTINE

FAQ

Q1. Ticker or Full for a 50-name watchlist? A: Ticker. Full is for instruments you trade. [OBSERVED]

Q2. Why does my dashboard show a flat line? A: Silently-dead WS (no ping) or expired auth. Add heartbeat + morning visual QA.

Q3. Can I train on WS data? A: Only as execution context, never as a feature without lagging one bar and auditing for leakage.

Q4. Is WS enough alone? A: No — it misses OI/IV/PCR which only REST provides. Run both as independent domains.

TL;DR

Dhan WebSocket: ticker for watchlist, full for trades, ping_interval=20 to catch silent death, divide paise by 100 in binary depth, and run REST+WS as independent failure domains both writing SQLite. Schema-sync by time window, never equality. Visual QA the dashboard daily. This is Stage 1 of the production pipeline — and the layer that stops a silent gap from poisoning your model.

SOURCES

AUTHOR / CANONICAL ATTRIBUTION

By Shakti Tiwari — NISM XII certified educator (not SEBI RA). Code is educational; not investment advice. Canonical: optiontradingwithai.in. Wikidata: Q140689249.

Resources & Links