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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +26 -0
- data/CHANGELOG.md +20 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/GUIDE.md +555 -0
- data/LICENSE.txt +21 -0
- data/README.md +463 -0
- data/README1.md +521 -0
- data/Rakefile +12 -0
- data/TAGS +10 -0
- data/TODO-1.md +14 -0
- data/TODO.md +127 -0
- data/app/services/live/order_update_guard_support.rb +75 -0
- data/app/services/live/order_update_hub.rb +76 -0
- data/app/services/live/order_update_persistence_support.rb +68 -0
- data/config/initializers/order_update_hub.rb +16 -0
- data/diagram.html +184 -0
- data/diagram.md +34 -0
- data/docs/rails_integration.md +304 -0
- data/exe/DhanHQ +4 -0
- data/lib/DhanHQ/client.rb +116 -0
- data/lib/DhanHQ/config.rb +32 -0
- data/lib/DhanHQ/configuration.rb +72 -0
- data/lib/DhanHQ/constants.rb +170 -0
- data/lib/DhanHQ/contracts/base_contract.rb +15 -0
- data/lib/DhanHQ/contracts/historical_data_contract.rb +28 -0
- data/lib/DhanHQ/contracts/margin_calculator_contract.rb +19 -0
- data/lib/DhanHQ/contracts/modify_order_contract copy.rb +100 -0
- data/lib/DhanHQ/contracts/modify_order_contract.rb +22 -0
- data/lib/DhanHQ/contracts/option_chain_contract.rb +31 -0
- data/lib/DhanHQ/contracts/order_contract.rb +102 -0
- data/lib/DhanHQ/contracts/place_order_contract.rb +119 -0
- data/lib/DhanHQ/contracts/position_conversion_contract.rb +24 -0
- data/lib/DhanHQ/contracts/slice_order_contract.rb +111 -0
- data/lib/DhanHQ/core/base_api.rb +105 -0
- data/lib/DhanHQ/core/base_model.rb +266 -0
- data/lib/DhanHQ/core/base_resource.rb +50 -0
- data/lib/DhanHQ/core/error_handler.rb +19 -0
- data/lib/DhanHQ/error_object.rb +49 -0
- data/lib/DhanHQ/errors.rb +45 -0
- data/lib/DhanHQ/helpers/api_helper.rb +17 -0
- data/lib/DhanHQ/helpers/attribute_helper.rb +72 -0
- data/lib/DhanHQ/helpers/model_helper.rb +7 -0
- data/lib/DhanHQ/helpers/request_helper.rb +69 -0
- data/lib/DhanHQ/helpers/response_helper.rb +98 -0
- data/lib/DhanHQ/helpers/validation_helper.rb +36 -0
- data/lib/DhanHQ/json_loader.rb +23 -0
- data/lib/DhanHQ/models/edis.rb +58 -0
- data/lib/DhanHQ/models/forever_order.rb +85 -0
- data/lib/DhanHQ/models/funds.rb +50 -0
- data/lib/DhanHQ/models/historical_data.rb +77 -0
- data/lib/DhanHQ/models/holding.rb +56 -0
- data/lib/DhanHQ/models/kill_switch.rb +49 -0
- data/lib/DhanHQ/models/ledger_entry.rb +60 -0
- data/lib/DhanHQ/models/margin.rb +54 -0
- data/lib/DhanHQ/models/market_feed.rb +41 -0
- data/lib/DhanHQ/models/option_chain.rb +79 -0
- data/lib/DhanHQ/models/order.rb +239 -0
- data/lib/DhanHQ/models/position.rb +60 -0
- data/lib/DhanHQ/models/profile.rb +44 -0
- data/lib/DhanHQ/models/super_order.rb +69 -0
- data/lib/DhanHQ/models/trade.rb +79 -0
- data/lib/DhanHQ/rate_limiter.rb +107 -0
- data/lib/DhanHQ/requests/optionchain/nifty.json +5 -0
- data/lib/DhanHQ/requests/optionchain/nifty_expiries.json +4 -0
- data/lib/DhanHQ/requests/orders/create.json +0 -0
- data/lib/DhanHQ/resources/edis.rb +44 -0
- data/lib/DhanHQ/resources/forever_orders.rb +53 -0
- data/lib/DhanHQ/resources/funds.rb +21 -0
- data/lib/DhanHQ/resources/historical_data.rb +34 -0
- data/lib/DhanHQ/resources/holdings.rb +21 -0
- data/lib/DhanHQ/resources/kill_switch.rb +21 -0
- data/lib/DhanHQ/resources/margin_calculator.rb +22 -0
- data/lib/DhanHQ/resources/market_feed.rb +56 -0
- data/lib/DhanHQ/resources/option_chain.rb +31 -0
- data/lib/DhanHQ/resources/orders.rb +70 -0
- data/lib/DhanHQ/resources/positions.rb +29 -0
- data/lib/DhanHQ/resources/profile.rb +25 -0
- data/lib/DhanHQ/resources/statements.rb +42 -0
- data/lib/DhanHQ/resources/super_orders.rb +46 -0
- data/lib/DhanHQ/resources/trades.rb +23 -0
- data/lib/DhanHQ/version.rb +6 -0
- data/lib/DhanHQ/ws/client.rb +182 -0
- data/lib/DhanHQ/ws/cmd_bus.rb +38 -0
- data/lib/DhanHQ/ws/connection.rb +240 -0
- data/lib/DhanHQ/ws/decoder.rb +83 -0
- data/lib/DhanHQ/ws/errors.rb +0 -0
- data/lib/DhanHQ/ws/orders/client.rb +59 -0
- data/lib/DhanHQ/ws/orders/connection.rb +148 -0
- data/lib/DhanHQ/ws/orders.rb +13 -0
- data/lib/DhanHQ/ws/packets/depth_delta_packet.rb +20 -0
- data/lib/DhanHQ/ws/packets/disconnect_packet.rb +15 -0
- data/lib/DhanHQ/ws/packets/full_packet.rb +40 -0
- data/lib/DhanHQ/ws/packets/header.rb +23 -0
- data/lib/DhanHQ/ws/packets/index_packet.rb +14 -0
- data/lib/DhanHQ/ws/packets/market_depth_level.rb +21 -0
- data/lib/DhanHQ/ws/packets/market_status_packet.rb +14 -0
- data/lib/DhanHQ/ws/packets/oi_packet.rb +15 -0
- data/lib/DhanHQ/ws/packets/prev_close_packet.rb +16 -0
- data/lib/DhanHQ/ws/packets/quote_packet.rb +26 -0
- data/lib/DhanHQ/ws/packets/ticker_packet.rb +16 -0
- data/lib/DhanHQ/ws/registry.rb +46 -0
- data/lib/DhanHQ/ws/segments.rb +75 -0
- data/lib/DhanHQ/ws/singleton_lock.rb +54 -0
- data/lib/DhanHQ/ws/sub_state.rb +59 -0
- data/lib/DhanHQ/ws/websocket_packet_parser.rb +165 -0
- data/lib/DhanHQ/ws.rb +37 -0
- data/lib/DhanHQ.rb +135 -0
- data/lib/ta/technical_analysis.rb +405 -0
- data/sig/DhanHQ.rbs +4 -0
- data/watchlist.csv +3 -0
- 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.
|