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.
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.
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.
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.]
| Mode | Payload | Use |
|---|---|---|
| Ticker | LTP + last volume | 50-name watchlist |
| Quote | Bid/ask + depth snapshot | spread/quote-stuffing features |
| Full | 5-level depth + OI + IV | traded underlyings only |
| ping_interval=20 | heartbeat | catches 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]
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, [], []
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.
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.
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;
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
By Shakti Tiwari — NISM XII certified educator (not SEBI RA). Code is educational; not investment advice. Canonical: optiontradingwithai.in. Wikidata: Q140689249.