DhanHQ 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +26 -0
  4. data/CHANGELOG.md +20 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/GUIDE.md +555 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +463 -0
  9. data/README1.md +521 -0
  10. data/Rakefile +12 -0
  11. data/TAGS +10 -0
  12. data/TODO-1.md +14 -0
  13. data/TODO.md +127 -0
  14. data/app/services/live/order_update_guard_support.rb +75 -0
  15. data/app/services/live/order_update_hub.rb +76 -0
  16. data/app/services/live/order_update_persistence_support.rb +68 -0
  17. data/config/initializers/order_update_hub.rb +16 -0
  18. data/diagram.html +184 -0
  19. data/diagram.md +34 -0
  20. data/docs/rails_integration.md +304 -0
  21. data/exe/DhanHQ +4 -0
  22. data/lib/DhanHQ/client.rb +116 -0
  23. data/lib/DhanHQ/config.rb +32 -0
  24. data/lib/DhanHQ/configuration.rb +72 -0
  25. data/lib/DhanHQ/constants.rb +170 -0
  26. data/lib/DhanHQ/contracts/base_contract.rb +15 -0
  27. data/lib/DhanHQ/contracts/historical_data_contract.rb +28 -0
  28. data/lib/DhanHQ/contracts/margin_calculator_contract.rb +19 -0
  29. data/lib/DhanHQ/contracts/modify_order_contract copy.rb +100 -0
  30. data/lib/DhanHQ/contracts/modify_order_contract.rb +22 -0
  31. data/lib/DhanHQ/contracts/option_chain_contract.rb +31 -0
  32. data/lib/DhanHQ/contracts/order_contract.rb +102 -0
  33. data/lib/DhanHQ/contracts/place_order_contract.rb +119 -0
  34. data/lib/DhanHQ/contracts/position_conversion_contract.rb +24 -0
  35. data/lib/DhanHQ/contracts/slice_order_contract.rb +111 -0
  36. data/lib/DhanHQ/core/base_api.rb +105 -0
  37. data/lib/DhanHQ/core/base_model.rb +266 -0
  38. data/lib/DhanHQ/core/base_resource.rb +50 -0
  39. data/lib/DhanHQ/core/error_handler.rb +19 -0
  40. data/lib/DhanHQ/error_object.rb +49 -0
  41. data/lib/DhanHQ/errors.rb +45 -0
  42. data/lib/DhanHQ/helpers/api_helper.rb +17 -0
  43. data/lib/DhanHQ/helpers/attribute_helper.rb +72 -0
  44. data/lib/DhanHQ/helpers/model_helper.rb +7 -0
  45. data/lib/DhanHQ/helpers/request_helper.rb +69 -0
  46. data/lib/DhanHQ/helpers/response_helper.rb +98 -0
  47. data/lib/DhanHQ/helpers/validation_helper.rb +36 -0
  48. data/lib/DhanHQ/json_loader.rb +23 -0
  49. data/lib/DhanHQ/models/edis.rb +58 -0
  50. data/lib/DhanHQ/models/forever_order.rb +85 -0
  51. data/lib/DhanHQ/models/funds.rb +50 -0
  52. data/lib/DhanHQ/models/historical_data.rb +77 -0
  53. data/lib/DhanHQ/models/holding.rb +56 -0
  54. data/lib/DhanHQ/models/kill_switch.rb +49 -0
  55. data/lib/DhanHQ/models/ledger_entry.rb +60 -0
  56. data/lib/DhanHQ/models/margin.rb +54 -0
  57. data/lib/DhanHQ/models/market_feed.rb +41 -0
  58. data/lib/DhanHQ/models/option_chain.rb +79 -0
  59. data/lib/DhanHQ/models/order.rb +239 -0
  60. data/lib/DhanHQ/models/position.rb +60 -0
  61. data/lib/DhanHQ/models/profile.rb +44 -0
  62. data/lib/DhanHQ/models/super_order.rb +69 -0
  63. data/lib/DhanHQ/models/trade.rb +79 -0
  64. data/lib/DhanHQ/rate_limiter.rb +107 -0
  65. data/lib/DhanHQ/requests/optionchain/nifty.json +5 -0
  66. data/lib/DhanHQ/requests/optionchain/nifty_expiries.json +4 -0
  67. data/lib/DhanHQ/requests/orders/create.json +0 -0
  68. data/lib/DhanHQ/resources/edis.rb +44 -0
  69. data/lib/DhanHQ/resources/forever_orders.rb +53 -0
  70. data/lib/DhanHQ/resources/funds.rb +21 -0
  71. data/lib/DhanHQ/resources/historical_data.rb +34 -0
  72. data/lib/DhanHQ/resources/holdings.rb +21 -0
  73. data/lib/DhanHQ/resources/kill_switch.rb +21 -0
  74. data/lib/DhanHQ/resources/margin_calculator.rb +22 -0
  75. data/lib/DhanHQ/resources/market_feed.rb +56 -0
  76. data/lib/DhanHQ/resources/option_chain.rb +31 -0
  77. data/lib/DhanHQ/resources/orders.rb +70 -0
  78. data/lib/DhanHQ/resources/positions.rb +29 -0
  79. data/lib/DhanHQ/resources/profile.rb +25 -0
  80. data/lib/DhanHQ/resources/statements.rb +42 -0
  81. data/lib/DhanHQ/resources/super_orders.rb +46 -0
  82. data/lib/DhanHQ/resources/trades.rb +23 -0
  83. data/lib/DhanHQ/version.rb +6 -0
  84. data/lib/DhanHQ/ws/client.rb +182 -0
  85. data/lib/DhanHQ/ws/cmd_bus.rb +38 -0
  86. data/lib/DhanHQ/ws/connection.rb +240 -0
  87. data/lib/DhanHQ/ws/decoder.rb +83 -0
  88. data/lib/DhanHQ/ws/errors.rb +0 -0
  89. data/lib/DhanHQ/ws/orders/client.rb +59 -0
  90. data/lib/DhanHQ/ws/orders/connection.rb +148 -0
  91. data/lib/DhanHQ/ws/orders.rb +13 -0
  92. data/lib/DhanHQ/ws/packets/depth_delta_packet.rb +20 -0
  93. data/lib/DhanHQ/ws/packets/disconnect_packet.rb +15 -0
  94. data/lib/DhanHQ/ws/packets/full_packet.rb +40 -0
  95. data/lib/DhanHQ/ws/packets/header.rb +23 -0
  96. data/lib/DhanHQ/ws/packets/index_packet.rb +14 -0
  97. data/lib/DhanHQ/ws/packets/market_depth_level.rb +21 -0
  98. data/lib/DhanHQ/ws/packets/market_status_packet.rb +14 -0
  99. data/lib/DhanHQ/ws/packets/oi_packet.rb +15 -0
  100. data/lib/DhanHQ/ws/packets/prev_close_packet.rb +16 -0
  101. data/lib/DhanHQ/ws/packets/quote_packet.rb +26 -0
  102. data/lib/DhanHQ/ws/packets/ticker_packet.rb +16 -0
  103. data/lib/DhanHQ/ws/registry.rb +46 -0
  104. data/lib/DhanHQ/ws/segments.rb +75 -0
  105. data/lib/DhanHQ/ws/singleton_lock.rb +54 -0
  106. data/lib/DhanHQ/ws/sub_state.rb +59 -0
  107. data/lib/DhanHQ/ws/websocket_packet_parser.rb +165 -0
  108. data/lib/DhanHQ/ws.rb +37 -0
  109. data/lib/DhanHQ.rb +135 -0
  110. data/lib/ta/technical_analysis.rb +405 -0
  111. data/sig/DhanHQ.rbs +4 -0
  112. data/watchlist.csv +3 -0
  113. metadata +283 -0
data/README.md ADDED
@@ -0,0 +1,463 @@
1
+ # DhanHQ — Ruby Client for DhanHQ API (v2)
2
+
3
+ A clean Ruby client for **Dhan API v2** with ORM-like models (Orders, Positions, Holdings, etc.) **and** a robust **WebSocket market feed** (ticker/quote/full) built on EventMachine + Faye.
4
+
5
+ * ActiveRecord-style models: `find`, `all`, `where`, `save`, `update`, `cancel`
6
+ * Validations & errors exposed via ActiveModel-like interfaces
7
+ * REST coverage: Orders, Super Orders, Forever Orders, Trades, Positions, Holdings, Funds/Margin, HistoricalData, OptionChain, MarketFeed
8
+ * **WebSocket**: subscribe/unsubscribe dynamically, auto-reconnect with backoff, 429 cool-off, idempotent subs, header+payload binary parsing, normalized ticks
9
+
10
+ ---
11
+
12
+ ## Installation
13
+
14
+ Add to your Gemfile:
15
+
16
+ ```ruby
17
+ gem 'DhanHQ', git: 'https://github.com/shubhamtaywade82/dhanhq-client.git', branch: 'main'
18
+ ```
19
+
20
+ Install:
21
+
22
+ ```bash
23
+ bundle install
24
+ ```
25
+
26
+ Or:
27
+
28
+ ```bash
29
+ gem install DhanHQ
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Configuration
35
+
36
+ ### From ENV / .env
37
+
38
+ ```ruby
39
+ require 'DhanHQ'
40
+
41
+ DhanHQ.configure_with_env
42
+ DhanHQ.logger.level = (ENV["DHAN_LOG_LEVEL"] || "INFO").upcase.then { |level| Logger.const_get(level) }
43
+ ```
44
+
45
+ **Minimum environment variables**
46
+
47
+ | Variable | Purpose |
48
+ | --- | --- |
49
+ | `CLIENT_ID` | Trading account client id issued by Dhan. |
50
+ | `ACCESS_TOKEN` | API access token generated from the Dhan console. |
51
+
52
+ `configure_with_env` raises if either value is missing. Load them via `dotenv`,
53
+ Rails credentials, or any other mechanism that populates `ENV` before
54
+ initialisation.
55
+
56
+ **Optional overrides**
57
+
58
+ Set these variables _before_ calling `configure_with_env` when you need to
59
+ override defaults supplied by the gem:
60
+
61
+ | Variable | When to use |
62
+ | --- | --- |
63
+ | `DHAN_LOG_LEVEL` | Adjust logger verbosity (`INFO` by default). |
64
+ | `DHAN_BASE_URL` | Point REST calls to a different API hostname. |
65
+ | `DHAN_WS_VERSION` | Pin to a specific WebSocket API version. |
66
+ | `DHAN_WS_ORDER_URL` | Override the order update WebSocket endpoint. |
67
+ | `DHAN_WS_USER_TYPE` | Switch between `SELF` and `PARTNER` streaming modes. |
68
+ | `DHAN_PARTNER_ID` / `DHAN_PARTNER_SECRET` | Required when `DHAN_WS_USER_TYPE=PARTNER`. |
69
+
70
+ ### Logging
71
+
72
+ ```ruby
73
+ DhanHQ.logger.level = (ENV["DHAN_LOG_LEVEL"] || "INFO").upcase.then { |level| Logger.const_get(level) }
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Quick Start (REST)
79
+
80
+ ```ruby
81
+ # Place an order
82
+ order = DhanHQ::Models::Order.new(
83
+ transaction_type: "BUY",
84
+ exchange_segment: "NSE_FNO",
85
+ product_type: "MARGIN",
86
+ order_type: "LIMIT",
87
+ validity: "DAY",
88
+ security_id: "43492",
89
+ quantity: 50,
90
+ price: 100.0
91
+ )
92
+ order.save
93
+
94
+ # Modify / Cancel
95
+ order.modify(price: 101.5)
96
+ order.cancel
97
+
98
+ # Positions / Holdings
99
+ positions = DhanHQ::Models::Position.all
100
+ holdings = DhanHQ::Models::Holding.all
101
+
102
+ # Historical Data (Intraday)
103
+ bars = DhanHQ::Models::HistoricalData.intraday(
104
+ security_id: "13", # NIFTY index value
105
+ exchange_segment: "IDX_I",
106
+ instrument: "INDEX",
107
+ interval: "5", # minutes
108
+ from_date: "2025-08-14",
109
+ to_date: "2025-08-18"
110
+ )
111
+
112
+ # Option Chain (example)
113
+ oc = DhanHQ::Models::OptionChain.fetch(
114
+ underlying_scrip: 1333, # example underlying ID
115
+ underlying_seg: "NSE_FNO",
116
+ expiry: "2025-08-21"
117
+ )
118
+ ```
119
+
120
+ ### Rails integration
121
+
122
+ Need a full-stack example inside Rails (REST + WebSockets + automation)? Check
123
+ out the [Rails integration guide](docs/rails_integration.md) for
124
+ initializers, service objects, workers, and ActionCable wiring tailored for the
125
+ `DhanHQ` gem.
126
+
127
+ ---
128
+
129
+ ## WebSocket Market Feed (NEW)
130
+
131
+ ### What you get
132
+
133
+ * **Modes**
134
+
135
+ * `:ticker` → LTP + LTT
136
+ * `:quote` → OHLCV + totals (recommended default)
137
+ * `:full` → quote + **OI** + **best-5 depth**
138
+ * **Normalized ticks** (Hash):
139
+
140
+ ```ruby
141
+ {
142
+ kind: :quote, # :ticker | :quote | :full | :oi | :prev_close | :misc
143
+ segment: "NSE_FNO", # string enum
144
+ security_id: "12345",
145
+ ltp: 101.5,
146
+ ts: 1723791300, # LTT epoch (sec) if present
147
+ vol: 123456, # quote/full
148
+ atp: 100.9, # quote/full
149
+ day_open: 100.1, day_high: 102.4, day_low: 99.5, day_close: nil,
150
+ oi: 987654, # full or OI packet
151
+ bid: 101.45, ask: 101.55 # from depth (mode :full)
152
+ }
153
+ ```
154
+
155
+ ### Start, subscribe, stop
156
+
157
+ ```ruby
158
+ require 'DhanHQ'
159
+
160
+ DhanHQ.configure_with_env
161
+ DhanHQ.logger.level = (ENV["DHAN_LOG_LEVEL"] || "INFO").upcase.then { |level| Logger.const_get(level) }
162
+
163
+ ws = DhanHQ::WS::Client.new(mode: :quote).start
164
+
165
+ ws.on(:tick) do |t|
166
+ puts "[#{t[:segment]}:#{t[:security_id]}] LTP=#{t[:ltp]} kind=#{t[:kind]}"
167
+ end
168
+
169
+ # Subscribe instruments (≤100 per frame; send multiple frames if needed)
170
+ ws.subscribe_one(segment: "IDX_I", security_id: "13") # NIFTY index value
171
+ ws.subscribe_one(segment: "NSE_FNO", security_id: "12345") # an option
172
+
173
+ # Unsubscribe
174
+ ws.unsubscribe_one(segment: "NSE_FNO", security_id: "12345")
175
+
176
+ # Graceful disconnect (sends broker disconnect code 12, no reconnect)
177
+ ws.disconnect!
178
+
179
+ # Or hard stop (no broker message, just closes and halts loop)
180
+ ws.stop
181
+
182
+ # Safety: kill all local sockets (useful in IRB)
183
+ DhanHQ::WS.disconnect_all_local!
184
+ ```
185
+
186
+ ### Under the hood
187
+
188
+ * **Request codes** (per Dhan docs)
189
+
190
+ * Subscribe: **15** (ticker), **17** (quote), **21** (full)
191
+ * Unsubscribe: **16**, **18**, **22**
192
+ * Disconnect: **12**
193
+ * **Limits**
194
+
195
+ * Up to **100 instruments per SUB/UNSUB** message (client auto-chunks)
196
+ * Up to 5 WS connections per user (per Dhan)
197
+ * **Backoff & 429 cool-off**
198
+
199
+ * Exponential backoff with jitter
200
+ * Handshake **429** triggers a **60s cool-off** before retry
201
+ * **Reconnect & resubscribe**
202
+
203
+ * On reconnect the client resends the **current subscription snapshot** (idempotent)
204
+ * **Graceful shutdown**
205
+
206
+ * `ws.disconnect!` or `ws.stop` prevents reconnects
207
+ * An `at_exit` hook stops all registered WS clients to avoid leaked sockets
208
+
209
+ ---
210
+
211
+ ## Order Update WebSocket (NEW)
212
+
213
+ Receive live updates whenever your orders transition between states (placed → traded → cancelled, etc.).
214
+
215
+ ### Standalone Ruby script
216
+
217
+ ```ruby
218
+ require 'DhanHQ'
219
+
220
+ DhanHQ.configure_with_env
221
+ DhanHQ.logger.level = (ENV["DHAN_LOG_LEVEL"] || "INFO").upcase.then { |level| Logger.const_get(level) }
222
+
223
+ ou = DhanHQ::WS::Orders::Client.new.start
224
+
225
+ ou.on(:update) do |payload|
226
+ data = payload[:Data] || {}
227
+ puts "ORDER #{data[:OrderNo]} #{data[:Status]} traded=#{data[:TradedQty]} avg=#{data[:AvgTradedPrice]}"
228
+ end
229
+
230
+ # Keep the script alive (CTRL+C to exit)
231
+ sleep
232
+
233
+ # Later, stop the socket
234
+ ou.stop
235
+ ```
236
+
237
+ Or, if you just need a quick callback:
238
+
239
+ ```ruby
240
+ DhanHQ::WS::Orders.connect do |payload|
241
+ # handle :update callbacks only
242
+ end
243
+ ```
244
+
245
+ ### Rails bot integration
246
+
247
+ Mirror the market-feed supervisor by adding an Order Update hub singleton that hydrates your local DB and hands off to execution services.
248
+
249
+ 1. **Service** – `app/services/live/order_update_hub.rb`
250
+
251
+ ```ruby
252
+ Live::OrderUpdateHub.instance.start!
253
+ ```
254
+
255
+ The hub wires `DhanHQ::WS::Orders::Client` to:
256
+
257
+ * Upsert local `BrokerOrder` rows so UIs always reflect current broker status.
258
+ * Auto-subscribe traded entry legs on your existing `Live::WsHub` (if defined).
259
+ * Refresh `Execution::PositionGuard` (if present) with fill prices/qty for trailing exits.
260
+
261
+ 2. **Initializer** – `config/initializers/order_update_hub.rb`
262
+
263
+ ```ruby
264
+ if ENV["ENABLE_WS"] == "true"
265
+ Rails.application.config.to_prepare do
266
+ Live::OrderUpdateHub.instance.start!
267
+ end
268
+
269
+ at_exit { Live::OrderUpdateHub.instance.stop! }
270
+ end
271
+ ```
272
+
273
+ Flip `ENABLE_WS=true` in your Procfile or `.env` to boot the hub alongside the existing feed supervisor. On shutdown the client is stopped cleanly to avoid leaked sockets.
274
+
275
+ The hub is resilient to missing dependencies—if you do not have a `BrokerOrder` model, it safely skips persistence while keeping downstream callbacks alive.
276
+
277
+ ---
278
+
279
+ ## Exchange Segment Enums
280
+
281
+ Use the string enums below in WS `subscribe_*` and REST params:
282
+
283
+ | Enum | Exchange | Segment |
284
+ | -------------- | -------- | ----------------- |
285
+ | `IDX_I` | Index | Index Value |
286
+ | `NSE_EQ` | NSE | Equity Cash |
287
+ | `NSE_FNO` | NSE | Futures & Options |
288
+ | `NSE_CURRENCY` | NSE | Currency |
289
+ | `BSE_EQ` | BSE | Equity Cash |
290
+ | `MCX_COMM` | MCX | Commodity |
291
+ | `BSE_CURRENCY` | BSE | Currency |
292
+ | `BSE_FNO` | BSE | Futures & Options |
293
+
294
+ ---
295
+
296
+ ## Accessing ticks elsewhere in your app
297
+
298
+ ### Direct handler
299
+
300
+ ```ruby
301
+ ws.on(:tick) { |t| do_something_fast(t) } # avoid heavy work here
302
+ ```
303
+
304
+ ### Shared TickCache (recommended)
305
+
306
+ ```ruby
307
+ # app/services/live/tick_cache.rb
308
+ class TickCache
309
+ MAP = Concurrent::Map.new
310
+ def self.put(t) = MAP["#{t[:segment]}:#{t[:security_id]}"] = t
311
+ def self.get(seg, sid) = MAP["#{seg}:#{sid}"]
312
+ def self.ltp(seg, sid) = get(seg, sid)&.dig(:ltp)
313
+ end
314
+
315
+ ws.on(:tick) { |t| TickCache.put(t) }
316
+ ltp = TickCache.ltp("NSE_FNO", "12345")
317
+ ```
318
+
319
+ ### Filtered callback
320
+
321
+ ```ruby
322
+ def on_tick_for(ws, segment:, security_id:, &blk)
323
+ key = "#{segment}:#{security_id}"
324
+ ws.on(:tick){ |t| blk.call(t) if "#{t[:segment]}:#{t[:security_id]}" == key }
325
+ end
326
+ ```
327
+
328
+ ---
329
+
330
+ ## Rails integration (example)
331
+
332
+ **Goal:** Generate signals from clean **Historical Intraday OHLC** (5-min bars), and use **WebSocket** only for **exits/trailing** on open option legs.
333
+
334
+ 1. **Initializer**
335
+ `config/initializers/dhanhq.rb`
336
+
337
+ ```ruby
338
+ DhanHQ.configure_with_env
339
+ DhanHQ.logger.level = (ENV["DHAN_LOG_LEVEL"] || "INFO").upcase.then { |level| Logger.const_get(level) }
340
+ ```
341
+
342
+ 2. **Start WS supervisor**
343
+ `config/initializers/stream.rb`
344
+
345
+ ```ruby
346
+ INDICES = [
347
+ { segment: "IDX_I", security_id: "13" }, # NIFTY index value
348
+ { segment: "IDX_I", security_id: "25" } # BANKNIFTY index value
349
+ ]
350
+
351
+ Rails.application.config.to_prepare do
352
+ $WS = DhanHQ::WS::Client.new(mode: :quote).start
353
+ $WS.on(:tick) do |t|
354
+ TickCache.put(t)
355
+ Execution::PositionGuard.instance.on_tick(t) # trailing & fast exits
356
+ end
357
+ INDICES.each { |i| $WS.subscribe_one(segment: i[:segment], security_id: i[:security_id]) }
358
+ end
359
+ ```
360
+
361
+ 3. **Bar fetch (every 5 min) via Historical API**
362
+
363
+ * Fetch intraday OHLC at 5-minute boundaries.
364
+ * Update your `CandleSeries`; on each closed bar, run strategy to emit signals.
365
+ *(Use your existing `Bars::FetchLoop` + `CandleSeries` code.)*
366
+
367
+ 4. **Routing & orders**
368
+
369
+ * On signal: place **Super Order** (SL/TP/TSL) or fallback to Market + local trailing.
370
+ * After a successful place, **register** the leg in `PositionGuard` and **subscribe** its option on WS.
371
+
372
+ 5. **Shutdown**
373
+
374
+ ```ruby
375
+ at_exit { DhanHQ::WS.disconnect_all_local! }
376
+ ```
377
+
378
+ ---
379
+
380
+ ## Super Orders (example)
381
+
382
+ ```ruby
383
+ intent = {
384
+ exchange_segment: "NSE_FNO",
385
+ security_id: "12345", # option
386
+ transaction_type: "BUY",
387
+ quantity: 50,
388
+ # derived risk params from ATR/ADX
389
+ take_profit: 0.35, # 35% target
390
+ stop_loss: 0.18, # 18% SL
391
+ trailing_sl: 0.12 # 12% trail
392
+ }
393
+
394
+ # If your SuperOrder model exposes create/modify:
395
+ o = DhanHQ::Models::SuperOrder.create(intent)
396
+ # or fallback:
397
+ mkt = DhanHQ::Models::Order.new(
398
+ transaction_type: "BUY", exchange_segment: "NSE_FNO",
399
+ order_type: "MARKET", validity: "DAY",
400
+ security_id: "12345", quantity: 50
401
+ ).save
402
+ ```
403
+
404
+ If you placed a Super Order and want to trail SL upward using WS ticks:
405
+
406
+ ```ruby
407
+ DhanHQ::Models::SuperOrder.modify(
408
+ order_id: o.order_id,
409
+ stop_loss: new_abs_price, # broker API permitting
410
+ trailing_sl: nil
411
+ )
412
+ ```
413
+
414
+ ---
415
+
416
+ ## Packet parsing (for reference)
417
+
418
+ * **Response Header (8 bytes)**:
419
+ `feed_response_code (u8, BE)`, `message_length (u16, BE)`, `exchange_segment (u8, BE)`, `security_id (i32, LE)`
420
+ * **Packets supported**:
421
+
422
+ * **1** Index (surface as raw/misc unless documented)
423
+ * **2** Ticker: `ltp`, `ltt`
424
+ * **4** Quote: `ltp`, `ltt`, `atp`, `volume`, totals, `day_*`
425
+ * **5** OI: `open_interest`
426
+ * **6** Prev Close: `prev_close`, `oi_prev`
427
+ * **7** Market Status (raw/misc unless documented)
428
+ * **8** Full: quote + `open_interest` + 5× depth (bid/ask)
429
+ * **50** Disconnect: reason code
430
+
431
+ ---
432
+
433
+ ## Best practices
434
+
435
+ * Keep the `on(:tick)` handler **non-blocking**; push work to a queue/thread.
436
+ * Use `mode: :quote` for most strategies; switch to `:full` only if you need depth/OI in real-time.
437
+ * Call **`ws.disconnect!`** (or `ws.stop`) when leaving IRB / tests.
438
+ Use **`DhanHQ::WS.disconnect_all_local!`** to be extra safe.
439
+ * Don’t exceed **100 instruments per SUB frame** (the client auto-chunks).
440
+ * Avoid rapid connect/disconnect loops; the client already **backs off & cools off** when server replies 429.
441
+
442
+ ---
443
+
444
+ ## Troubleshooting
445
+
446
+ * **429: Unexpected response code**
447
+ You connected too frequently or have too many sockets. The client auto-cools off for **60s** and backs off. Prefer `ws.disconnect!` before reconnecting; and call `DhanHQ::WS.disconnect_all_local!` to kill stragglers.
448
+ * **No ticks after reconnect**
449
+ Ensure you re-subscribed after a clean start (the client resends the snapshot automatically on reconnect).
450
+ * **Binary parse errors**
451
+ Run with `DHAN_LOG_LEVEL=DEBUG` to inspect; we safely drop malformed frames and keep the loop alive.
452
+
453
+ ---
454
+
455
+ ## Contributing
456
+
457
+ PRs welcome! Please include tests for new packet decoders and WS behaviors (chunking, reconnect, cool-off).
458
+
459
+ ---
460
+
461
+ ## License
462
+
463
+ MIT.