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/README1.md ADDED
@@ -0,0 +1,521 @@
1
+ # DhanHQ - Ruby Client for DhanHQ API
2
+
3
+ DhanHQ is a **Ruby client** for interacting with **Dhan API v2.0**. It provides **ActiveRecord-like** behavior, **RESTful resource management**, and **ActiveModel validation** for seamless integration into **algorithmic trading applications**.
4
+
5
+ ## ⚡ Features
6
+
7
+ ✅ **ORM-like Interface** (`find`, `all`, `where`, `save`, `update`, `destroy`)
8
+ ✅ **ActiveModel Integration** (`validations`, `errors`, `serialization`)
9
+ ✅ **Resource Objects for Trading** (`Orders`, `Positions`, `Holdings`, etc.)
10
+ ✅ **Supports WebSockets for Market Feeds**
11
+ ✅ **Error Handling & Validations** (`ActiveModel::Errors`)
12
+ ✅ **DRY & Modular Code** (`Helpers`, `Contracts`, `Request Handling`)
13
+
14
+ ---
15
+
16
+ ## 📌 Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ ```ruby
21
+ gem 'DhanHQ', git: 'https://github.com/shubhamtaywade82/dhanhq-client.git', branch: 'main'
22
+ ```
23
+
24
+ Then execute:
25
+
26
+ ```
27
+ bundle install
28
+ ```
29
+
30
+ Or install it manually:
31
+
32
+ ```
33
+ gem install dhanhq
34
+ ```
35
+
36
+ 🔹 Configuration
37
+ Set your DhanHQ API credentials:
38
+
39
+ ```ruby
40
+ require 'DhanHQ'
41
+
42
+ DhanHQ.configure_with_env
43
+ DhanHQ.logger.level = (ENV["DHAN_LOG_LEVEL"] || "INFO").upcase.then { |level| Logger.const_get(level) }
44
+ ```
45
+
46
+ **Minimum environment variables**
47
+
48
+ * `CLIENT_ID` – trading account client id issued by Dhan.
49
+ * `ACCESS_TOKEN` – API access token generated from the Dhan console.
50
+
51
+ If either key is missing `configure_with_env` raises an error. Ensure your
52
+ application loads them into `ENV` before requiring the gem.
53
+
54
+ **Optional overrides**
55
+
56
+ * `DHAN_LOG_LEVEL` – change logger verbosity (`INFO` default).
57
+ * `DHAN_BASE_URL` – point REST calls to an alternate host.
58
+ * `DHAN_WS_VERSION` – lock WebSocket connections to a specific version.
59
+ * `DHAN_WS_ORDER_URL` – override the order update WebSocket endpoint.
60
+ * `DHAN_WS_USER_TYPE` – choose between `SELF` and `PARTNER` streaming modes.
61
+ * `DHAN_PARTNER_ID` / `DHAN_PARTNER_SECRET` – required when streaming as a partner.
62
+
63
+ Create a `.env` file in your project root to supply the minimum values (and any
64
+ optional overrides you need):
65
+
66
+ ```dotenv
67
+ CLIENT_ID=your_client_id
68
+ ACCESS_TOKEN=your_access_token
69
+ ```
70
+
71
+ The gem requires `dotenv/load`, so these variables are loaded automatically when you require `DhanHQ`.
72
+
73
+ To override defaults (base URL, WebSocket settings, partner credentials), set
74
+ `DHAN_BASE_URL`, `DHAN_WS_VERSION`, `DHAN_WS_ORDER_URL`, `DHAN_WS_USER_TYPE`,
75
+ `DHAN_PARTNER_ID`, and `DHAN_PARTNER_SECRET` before calling
76
+ `configure_with_env`.
77
+
78
+ ## 🚀 Usage
79
+
80
+ ✅ Placing an Order
81
+
82
+ ```ruby
83
+ order = DhanHQ::Models::Order.new(
84
+ transaction_type: "BUY",
85
+ exchange_segment: "NSE_FNO",
86
+ product_type: "MARGIN",
87
+ order_type: "LIMIT",
88
+ validity: "DAY",
89
+ security_id: "43492",
90
+ quantity: 125,
91
+ price: 100.0
92
+ )
93
+
94
+ order.save
95
+ puts order.persisted? # true
96
+ ```
97
+
98
+ ✅ Fetching an Order
99
+
100
+ ```ruby
101
+ order = DhanHQ::Models::Order.find("452501297117")
102
+ puts order.price # Current price of the order
103
+ ```
104
+
105
+ ✅ Updating an Order
106
+
107
+ ```ruby
108
+ order.update(price: 105.0)
109
+ puts order.price # 105.0
110
+ ```
111
+
112
+ ✅ Canceling an Order
113
+
114
+ ```ruby
115
+ order.cancel
116
+ ```
117
+
118
+ ✅ Fetching All Orders
119
+
120
+ ```ruby
121
+ orders = DhanHQ::Models::Order.all
122
+ puts orders.count
123
+ ```
124
+
125
+ ✅ Querying Orders
126
+
127
+ ```ruby
128
+ pending_orders = DhanHQ::Models::Order.where(status: "PENDING")
129
+ puts pending_orders.first.order_id
130
+ ```
131
+
132
+ ✅ Exiting Positions
133
+
134
+ ```ruby
135
+ positions = DhanHQ::Models::Position.all
136
+ position = positions.first
137
+ position.exit!
138
+ ```
139
+
140
+ ### Orders
141
+
142
+ #### Place
143
+
144
+ ```ruby
145
+ order = DhanHQ::Models::Order.new(transaction_type: "BUY", security_id: "123", quantity: 1)
146
+ order.save
147
+ ```
148
+
149
+ #### Modify
150
+
151
+ ```ruby
152
+ order.modify(price: 102.5)
153
+ ```
154
+
155
+ #### Cancel
156
+
157
+ ```ruby
158
+ order.cancel
159
+ ```
160
+
161
+ ### Trades
162
+
163
+ ```ruby
164
+ DhanHQ::Models::Trade.today
165
+ DhanHQ::Models::Trade.find_by_order_id("452501297117")
166
+ ```
167
+
168
+ ### Positions
169
+
170
+ ```ruby
171
+ positions = DhanHQ::Models::Position.all
172
+ active = DhanHQ::Models::Position.active
173
+ DhanHQ::Models::Position.convert(position_id: "1", product_type: "CNC")
174
+ ```
175
+
176
+ ### Holdings
177
+
178
+ ```ruby
179
+ DhanHQ::Models::Holding.all
180
+ ```
181
+
182
+ ### Funds
183
+
184
+ ```ruby
185
+ DhanHQ::Models::Funds.fetch
186
+ balance = DhanHQ::Models::Funds.balance
187
+ ```
188
+
189
+ ### Option Chain
190
+
191
+ ```ruby
192
+ DhanHQ::Models::OptionChain.fetch(security_id: "1333", expiry_date: "2024-06-30")
193
+ DhanHQ::Models::OptionChain.fetch_expiry_list(security_id: "1333")
194
+ ```
195
+
196
+ ### Historical Data
197
+
198
+ ```ruby
199
+ DhanHQ::Models::HistoricalData.daily(security_id: "1333", from_date: "2024-01-01", to_date: "2024-01-31")
200
+ DhanHQ::Models::HistoricalData.intraday(security_id: "1333", interval: "15")
201
+ ```
202
+
203
+ ## 🔹 Available Resources
204
+
205
+ | Resource | Model | Actions |
206
+ | ------------------------ | -------------------------------- | --------------------------------------------------- |
207
+ | Orders | `DhanHQ::Models::Models::Order` | `find`, `all`, `where`, `place`, `update`, `cancel` |
208
+ | Trades | `DhanHQ::Models::Models::Trade` | `all`, `find_by_order_id` |
209
+ | Forever Orders | `DhanHQ::Models::Models::ForeverOrder` | `create`, `find`, `modify`, `cancel`, `all` |
210
+ | Holdings | `DhanHQ::Models::Models::Holding` | `all` |
211
+ | Positions | `DhanHQ::Models::Models::Position` | `all`, `find`, `exit!` |
212
+ | Funds & Margin | `DhanHQ::Models::Models::Funds` | `fund_limit`, `margin_calculator` |
213
+ | Ledger | `DhanHQ::Models::Models::Ledger` | `all` |
214
+ | Market Feeds | `DhanHQ::Models::Models::MarketFeed` | `ltp, ohlc`, `quote` |
215
+ | Historical Data (Charts) | `DhanHQ::Models::Models::HistoricalData` | `daily`, `intraday` |
216
+ | Option Chain | `DhanHQ::Models::Models::OptionChain` | `fetch`, `fetch_expiry_list` |
217
+
218
+ ## 📌 Development
219
+
220
+ Set `DHAN_DEBUG=true` to log HTTP requests during development:
221
+
222
+ ```bash
223
+ export DHAN_DEBUG=true
224
+ ```
225
+
226
+ Running Tests
227
+
228
+ ```bash
229
+ bundle exec rspec
230
+ ```
231
+
232
+ Installing Locally
233
+
234
+ ```bash
235
+ bundle exec rake install
236
+ ```
237
+
238
+ Releasing a New Version
239
+
240
+ ```bash
241
+ bundle exec rake release
242
+ ```
243
+ ---
244
+
245
+ ## WebSocket Market Feed (NEW)
246
+
247
+ ### What you get
248
+
249
+ * **Modes**
250
+
251
+ * `:ticker` → LTP + LTT
252
+ * `:quote` → OHLCV + totals (recommended default)
253
+ * `:full` → quote + **OI** + **best-5 depth**
254
+ * **Normalized ticks** (Hash):
255
+
256
+ ```ruby
257
+ {
258
+ kind: :quote, # :ticker | :quote | :full | :oi | :prev_close | :misc
259
+ segment: "NSE_FNO", # string enum
260
+ security_id: "12345",
261
+ ltp: 101.5,
262
+ ts: 1723791300, # LTT epoch (sec) if present
263
+ vol: 123456, # quote/full
264
+ atp: 100.9, # quote/full
265
+ day_open: 100.1, day_high: 102.4, day_low: 99.5, day_close: nil,
266
+ oi: 987654, # full or OI packet
267
+ bid: 101.45, ask: 101.55 # from depth (mode :full)
268
+ }
269
+ ```
270
+
271
+ ### Start, subscribe, stop
272
+
273
+ ```ruby
274
+ DhanHQ.configure_with_env
275
+ DhanHQ.logger.level = (ENV["DHAN_LOG_LEVEL"] || "INFO").upcase.then { |level| Logger.const_get(level) }
276
+
277
+ ws = DhanHQ::WS::Client.new(mode: :quote).start
278
+
279
+ ws.on(:tick) do |t|
280
+ puts "[#{t[:segment]}:#{t[:security_id]}] LTP=#{t[:ltp]} kind=#{t[:kind]}"
281
+ end
282
+
283
+ # Subscribe instruments (≤100 per frame; send multiple frames if needed)
284
+ ws.subscribe_one(segment: "IDX_I", security_id: "13") # NIFTY index value
285
+ ws.subscribe_one(segment: "NSE_FNO", security_id: "12345") # an option
286
+
287
+ # Unsubscribe
288
+ ws.unsubscribe_one(segment: "NSE_FNO", security_id: "12345")
289
+
290
+ # Graceful disconnect (sends broker disconnect code 12, no reconnect)
291
+ ws.disconnect!
292
+
293
+ # Or hard stop (no broker message, just closes and halts loop)
294
+ ws.stop
295
+
296
+ # Safety: kill all local sockets (useful in IRB)
297
+ DhanHQ::WS.disconnect_all_local!
298
+ ```
299
+
300
+ ### Under the hood
301
+
302
+ * **Request codes** (per Dhan docs)
303
+
304
+ * Subscribe: **15** (ticker), **17** (quote), **21** (full)
305
+ * Unsubscribe: **16**, **18**, **22**
306
+ * Disconnect: **12**
307
+ * **Limits**
308
+
309
+ * Up to **100 instruments per SUB/UNSUB** message (client auto-chunks)
310
+ * Up to 5 WS connections per user (per Dhan)
311
+ * **Backoff & 429 cool-off**
312
+
313
+ * Exponential backoff with jitter
314
+ * Handshake **429** triggers a **60s cool-off** before retry
315
+ * **Reconnect & resubscribe**
316
+
317
+ * On reconnect the client resends the **current subscription snapshot** (idempotent)
318
+ * **Graceful shutdown**
319
+
320
+ * `ws.disconnect!` or `ws.stop` prevents reconnects
321
+ * An `at_exit` hook stops all registered WS clients to avoid leaked sockets
322
+
323
+ ---
324
+
325
+ ## Exchange Segment Enums
326
+
327
+ Use the string enums below in WS `subscribe_*` and REST params:
328
+
329
+ | Enum | Exchange | Segment |
330
+ | -------------- | -------- | ----------------- |
331
+ | `IDX_I` | Index | Index Value |
332
+ | `NSE_EQ` | NSE | Equity Cash |
333
+ | `NSE_FNO` | NSE | Futures & Options |
334
+ | `NSE_CURRENCY` | NSE | Currency |
335
+ | `BSE_EQ` | BSE | Equity Cash |
336
+ | `MCX_COMM` | MCX | Commodity |
337
+ | `BSE_CURRENCY` | BSE | Currency |
338
+ | `BSE_FNO` | BSE | Futures & Options |
339
+
340
+ ---
341
+
342
+ ## Accessing ticks elsewhere in your app
343
+
344
+ ### Direct handler
345
+
346
+ ```ruby
347
+ ws.on(:tick) { |t| do_something_fast(t) } # avoid heavy work here
348
+ ```
349
+
350
+ ### Shared TickCache (recommended)
351
+
352
+ ```ruby
353
+ # app/services/live/tick_cache.rb
354
+ class TickCache
355
+ MAP = Concurrent::Map.new
356
+ def self.put(t) = MAP["#{t[:segment]}:#{t[:security_id]}"] = t
357
+ def self.get(seg, sid) = MAP["#{seg}:#{sid}"]
358
+ def self.ltp(seg, sid) = get(seg, sid)&.dig(:ltp)
359
+ end
360
+
361
+ ws.on(:tick) { |t| TickCache.put(t) }
362
+ ltp = TickCache.ltp("NSE_FNO", "12345")
363
+ ```
364
+
365
+ ### Filtered callback
366
+
367
+ ```ruby
368
+ def on_tick_for(ws, segment:, security_id:, &blk)
369
+ key = "#{segment}:#{security_id}"
370
+ ws.on(:tick){ |t| blk.call(t) if "#{t[:segment]}:#{t[:security_id]}" == key }
371
+ end
372
+ ```
373
+
374
+ ---
375
+
376
+ ## Rails integration (example)
377
+
378
+ **Goal:** Generate signals from clean **Historical Intraday OHLC** (5-min bars), and use **WebSocket** only for **exits/trailing** on open option legs.
379
+
380
+ 1. **Initializer**
381
+ `config/initializers/dhanhq.rb`
382
+
383
+ ```ruby
384
+ DhanHQ.configure_with_env
385
+ DhanHQ.logger.level = (ENV["DHAN_LOG_LEVEL"] || "INFO").upcase.then { |level| Logger.const_get(level) }
386
+ ```
387
+
388
+ 2. **Start WS supervisor**
389
+ `config/initializers/stream.rb`
390
+
391
+ ```ruby
392
+ INDICES = [
393
+ { segment: "IDX_I", security_id: "13" }, # NIFTY index value
394
+ { segment: "IDX_I", security_id: "25" } # BANKNIFTY index value
395
+ ]
396
+
397
+ Rails.application.config.to_prepare do
398
+ $WS = DhanHQ::WS::Client.new(mode: :quote).start
399
+ $WS.on(:tick) do |t|
400
+ TickCache.put(t)
401
+ Execution::PositionGuard.instance.on_tick(t) # trailing & fast exits
402
+ end
403
+ INDICES.each { |i| $WS.subscribe_one(segment: i[:segment], security_id: i[:security_id]) }
404
+ end
405
+ ```
406
+
407
+ 3. **Bar fetch (every 5 min) via Historical API**
408
+
409
+ * Fetch intraday OHLC at 5-minute boundaries.
410
+ * Update your `CandleSeries`; on each closed bar, run strategy to emit signals.
411
+ *(Use your existing `Bars::FetchLoop` + `CandleSeries` code.)*
412
+
413
+ 4. **Routing & orders**
414
+
415
+ * On signal: place **Super Order** (SL/TP/TSL) or fallback to Market + local trailing.
416
+ * After a successful place, **register** the leg in `PositionGuard` and **subscribe** its option on WS.
417
+
418
+ 5. **Shutdown**
419
+
420
+ ```ruby
421
+ at_exit { DhanHQ::WS.disconnect_all_local! }
422
+ ```
423
+
424
+ ---
425
+
426
+ ## Super Orders (example)
427
+
428
+ ```ruby
429
+ intent = {
430
+ exchange_segment: "NSE_FNO",
431
+ security_id: "12345", # option
432
+ transaction_type: "BUY",
433
+ quantity: 50,
434
+ # derived risk params from ATR/ADX
435
+ take_profit: 0.35, # 35% target
436
+ stop_loss: 0.18, # 18% SL
437
+ trailing_sl: 0.12 # 12% trail
438
+ }
439
+
440
+ # If your SuperOrder model exposes create/modify:
441
+ o = DhanHQ::Models::SuperOrder.create(intent)
442
+ # or fallback:
443
+ mkt = DhanHQ::Models::Order.new(
444
+ transaction_type: "BUY", exchange_segment: "NSE_FNO",
445
+ order_type: "MARKET", validity: "DAY",
446
+ security_id: "12345", quantity: 50
447
+ ).save
448
+ ```
449
+
450
+ If you placed a Super Order and want to trail SL upward using WS ticks:
451
+
452
+ ```ruby
453
+ DhanHQ::Models::SuperOrder.modify(
454
+ order_id: o.order_id,
455
+ stop_loss: new_abs_price, # broker API permitting
456
+ trailing_sl: nil
457
+ )
458
+ ```
459
+
460
+ ---
461
+
462
+ ## Packet parsing (for reference)
463
+
464
+ * **Response Header (8 bytes)**:
465
+ `feed_response_code (u8, BE)`, `message_length (u16, BE)`, `exchange_segment (u8, BE)`, `security_id (i32, LE)`
466
+ * **Packets supported**:
467
+
468
+ * **1** Index (surface as raw/misc unless documented)
469
+ * **2** Ticker: `ltp`, `ltt`
470
+ * **4** Quote: `ltp`, `ltt`, `atp`, `volume`, totals, `day_*`
471
+ * **5** OI: `open_interest`
472
+ * **6** Prev Close: `prev_close`, `oi_prev`
473
+ * **7** Market Status (raw/misc unless documented)
474
+ * **8** Full: quote + `open_interest` + 5× depth (bid/ask)
475
+ * **50** Disconnect: reason code
476
+
477
+ ---
478
+
479
+ ## Best practices
480
+
481
+ * Keep the `on(:tick)` handler **non-blocking**; push work to a queue/thread.
482
+ * Use `mode: :quote` for most strategies; switch to `:full` only if you need depth/OI in real-time.
483
+ * Call **`ws.disconnect!`** (or `ws.stop`) when leaving IRB / tests.
484
+ Use **`DhanHQ::WS.disconnect_all_local!`** to be extra safe.
485
+ * Don’t exceed **100 instruments per SUB frame** (the client auto-chunks).
486
+ * Avoid rapid connect/disconnect loops; the client already **backs off & cools off** when server replies 429.
487
+
488
+ ---
489
+
490
+ ## Troubleshooting
491
+
492
+ * **429: Unexpected response code**
493
+ 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.
494
+ * **No ticks after reconnect**
495
+ Ensure you re-subscribed after a clean start (the client resends the snapshot automatically on reconnect).
496
+ * **Binary parse errors**
497
+ Run with `DHAN_LOG_LEVEL=DEBUG` to inspect; we safely drop malformed frames and keep the loop alive.
498
+
499
+ ---
500
+
501
+
502
+ ## 📌 Contributing
503
+
504
+ Bug reports and pull requests are welcome on GitHub at:
505
+ 🔗 <https://github.com/shubhamtaywade82/dhanhq>
506
+
507
+ This project follows a code of conduct to maintain a safe and welcoming community.
508
+
509
+ ## 📌 License
510
+
511
+ This gem is available under the MIT License.
512
+ 🔗 <https://opensource.org/licenses/MIT>
513
+
514
+ ## 📌 Code of Conduct
515
+
516
+ Everyone interacting in the DhanHQ project is expected to follow the
517
+ 🔗 Code of Conduct.
518
+
519
+ ```markdown
520
+ This **README.md** file is structured and formatted for **GitHub** or any **Markdown-compatible** documentation system. 🚀
521
+ ```
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
data/TAGS ADDED
@@ -0,0 +1,10 @@
1
+
2
+ lib/DhanHQ/version.rb,97
3
+ module DhanHQDhanHQ3,0
4
+ VERSION = "0.1.0"VERSION4,0
5
+ VERSION = "0.1.0"DhanHQ::VERSION4,0
6
+
7
+ lib/DhanHQ.rb,123
8
+ module DhanHQDhanHQ5,0
9
+ class Error < StandardError; endError6,0
10
+ class Error < StandardError; endDhanHQ::Error6,0
data/TODO-1.md ADDED
@@ -0,0 +1,14 @@
1
+ # TODO List
2
+
3
+ - [x] Wire `DhanHQ::Models::Order.place` to the resource’s `create` endpoint so order placement stops raising `NoMethodError` (`lib/DhanHQ/models/order.rb:73`, `lib/DhanHQ/resources/orders.rb:14`).
4
+ - [x] Align `Order#cancel` with `DhanHQ::Resources::Orders#cancel` to restore cancellation support (`lib/DhanHQ/models/order.rb:120`, `lib/DhanHQ/resources/orders.rb:26`).
5
+ - [x] Rework `Order#modify` to send a proper payload and capture the response instead of delegating to the broken generic update flow (`lib/DhanHQ/models/order.rb:99`, `lib/DhanHQ/core/base_model.rb:155`).
6
+ - [x] Repair or replace the shared CRUD helpers in `BaseModel` so URL construction and response handling behave (`lib/DhanHQ/core/base_model.rb:120`, `lib/DhanHQ/core/base_model.rb:129`, `lib/DhanHQ/core/base_model.rb:155`).
7
+ - [x] Make `BaseModel#save!` raise a real exception type (e.g. `DhanHQ::Error`) to avoid the current `TypeError` (`lib/DhanHQ/core/base_model.rb:165`).
8
+ - [x] Strip read-only attributes before posting modify requests to pass API validation (`lib/DhanHQ/models/order.rb:166`, `lib/DhanHQ/core/base_model.rb:197`).
9
+ - [x] Require `fileutils` in the WebSocket singleton lock so acquiring the lock no longer raises (`lib/DhanHQ/ws/singleton_lock.rb:11`).
10
+ - [x] Implement EDIS and kill-switch endpoints surfaced in the OpenAPI spec so the client can call `/edis/bulkform`, `/edis/form`, `/edis/inquire/{isin}`, `/edis/tpin`, and `/killswitch` (`/home/nemesis/dhanhq-bundled.json:827`, `/home/nemesis/dhanhq-bundled.json:873`, `/home/nemesis/dhanhq-bundled.json:949`).
11
+ - [x] Correct `ForeverOrders#all` to hit `/v2/forever/orders` instead of the undocumented `/v2/forever/all` path (`lib/DhanHQ/resources/forever_orders.rb:9`, `/home/nemesis/dhanhq-bundled.json:578`).
12
+ - [x] Run the existing `MarginCalculatorContract` before posting to `/margincalculator` so required fields like `transactionType` and `productType` are enforced client-side (`lib/DhanHQ/models/margin.rb:23`, `lib/DhanHQ/contracts/margin_calculator_contract.rb:5`).
13
+ - [ ] Validate slice-order payloads with `SliceOrderContract` to uphold STOP_LOSS requirements before calling `/orders/slicing` (`lib/DhanHQ/models/order.rb:217`, `lib/DhanHQ/contracts/slice_order_contract.rb:30`, `/home/nemesis/dhanhq-bundled.json:234`).
14
+ - [x] Add a contract-backed validation for `Position.convert` so `PositionConversionRequest` fields like `fromProductType` and `convertQty` are checked prior to hitting `/positions/convert` (`lib/DhanHQ/models/position.rb:39`, `/home/nemesis/dhanhq-bundled.json:1391`).
data/TODO.md ADDED
@@ -0,0 +1,127 @@
1
+ Below are several suggestions and improvements you can consider to further enhance your gem’s structure, error handling, validations, and overall design:
2
+
3
+ ---
4
+
5
+ ### **1. Decouple HTTP/Resource Handling from Models**
6
+
7
+ - **Inject the API instance:**
8
+ Instead of having your BaseModel inherit from or tightly couple to BaseAPI, you already moved to instantiating a shared API object via the `api` method. This is good for testing and future flexibility. Consider allowing dependency injection so that in tests you can supply a mock client.
9
+
10
+ - **Separate Resource Objects:**
11
+ Create separate “Resource” classes for each endpoint (e.g. Orders, Funds, MarketFeed, etc.) that are solely responsible for forming the correct URL and HTTP call. Then, your models (Order, Funds, etc.) become thin wrappers on top of these resources.
12
+
13
+ ---
14
+
15
+ ### **2. Improved Error Handling**
16
+
17
+ - **Consistent Error Hierarchy:**
18
+ You already have a structured set of custom errors. Ensure that all API responses are wrapped in a uniform error response. For instance, for network errors, consider implementing a retry mechanism for transient errors (such as timeouts or 429 responses).
19
+
20
+ - **Retry Mechanism:**
21
+ Enhance your Client by adding an optional retry strategy (with exponential backoff) for cases when a request fails due to rate limiting or temporary network issues.
22
+
23
+ - **Detailed Logging:**
24
+ Enable more granular logging (perhaps controlled via configuration) to help troubleshoot errors without exposing sensitive information.
25
+
26
+ ---
27
+
28
+ ### **3. Enhanced Validations**
29
+
30
+ - **Centralize Contracts:**
31
+ Use your `BaseContract` as a foundation for all contracts so that common rules and messages are shared. Consider adding custom predicates if you need more complex business rules.
32
+ For example, you can write a custom predicate for “valid_order_quantity” that could check against exchange limits.
33
+
34
+ - **Error Messages & Localization:**
35
+ Consider standardizing error messages across contracts so that errors returned by your gem are predictable. If needed, use a localization mechanism to map raw error keys to user-friendly messages.
36
+
37
+ - **Use Dry-Struct (Optional):**
38
+ If you want stronger typing and immutability for your models, you might consider using [dry-struct](https://dry-rb.org/gems/dry-struct/) in combination with dry-validation. This can help enforce attribute types and reduce runtime errors.
39
+
40
+ ---
41
+
42
+ ### **4. Model Improvements and Convenience Methods**
43
+
44
+ - **Dynamic Attribute Getters/Setters:**
45
+ Your current approach to dynamically assign attribute getters is good. You might consider also generating setters if you want to allow updating the local object state before pushing an update to the API.
46
+
47
+ - **CRUD Methods Consistency:**
48
+ Ensure that your instance methods like `update`, `delete`, `refresh`, etc., always return either a new instance of the model (with updated values) or a well-formed error object. This will make it easier for users to chain operations.
49
+
50
+ - **Merge Updated Attributes Correctly:**
51
+ In your `modify` method (for orders, for instance), make sure you correctly merge the existing attributes with the new ones before issuing the PUT request. Currently, there’s a commented line and then an immediate call to `update(attributes)`—this should be updated to use the merged `updated_params`.
52
+
53
+ - **Caching or Memoization:**
54
+ If your API endpoints don’t change frequently (e.g., for retrieving configuration or instruments), consider caching responses to minimize API calls.
55
+
56
+ ---
57
+
58
+ ### **5. Testing & VCR**
59
+
60
+ - **VCR Cassette Management:**
61
+ Ensure that your VCR cassettes capture both successful and error responses. When you need to simulate scenarios (e.g., update order, cancellation), manually edit the cassette files to reflect those states if the real API cannot produce them reliably.
62
+
63
+ - **Spec Coverage:**
64
+ Write comprehensive specs for each model that exercises both the “happy path” and error cases. For instance, ensure that Order.create returns an order with the proper attributes, and that Order.update merges new attributes as expected.
65
+
66
+ ---
67
+
68
+ ### **6. Configuration Improvements**
69
+
70
+ - **Global Configuration Object:**
71
+ Continue iterating on the existing configuration helpers while keeping `DhanHQ.configure_with_env` as the primary entrypoint. Provide any additional toggles by reading from ENV so docs can focus on the single bootstrap path.
72
+
73
+ ---
74
+
75
+ ### **7. Overall Architecture and Documentation**
76
+
77
+ - **Document Model Methods:**
78
+ Ensure that each model (Order, Funds, OptionChain, etc.) is well documented. Explain the expected inputs/outputs and any side effects.
79
+
80
+ - **Separation of Concerns:**
81
+ Keep your gem’s responsibilities clear:
82
+
83
+ - **Client:** Low-level HTTP calls with error handling and rate limiting.
84
+ - **Resources:** Form URLs and endpoint-specific logic.
85
+ - **Models:** Map resource data to business objects and provide CRUD operations with validation.
86
+ - **Contracts:** Define dry-validation contracts for input validation.
87
+
88
+ - **Extensibility:**
89
+ Consider ways to allow users to extend models or override default behavior. For instance, provide hooks (callbacks) before or after an update or create operation.
90
+
91
+ ---
92
+
93
+ ### **Example of a Revised Modify Method in Order Model**
94
+
95
+ Here’s a small snippet that shows how you might update the `modify` method in the Order model to merge attributes properly:
96
+
97
+ ```ruby
98
+ def modify(new_params)
99
+ raise "Order ID is required to modify an order" unless id
100
+
101
+ # Merge current attributes with new ones
102
+ updated_params = attributes.merge(new_params)
103
+ validate_params!(updated_params, DhanHQ::Contracts::ModifyOrderContract)
104
+
105
+ # Perform the PUT request with merged parameters
106
+ response = self.class.api.put("#{self.class.resource_path}/#{id}", params: updated_params)
107
+
108
+ # If the response indicates a transitional status (e.g., "TRANSIT"), re-fetch the order
109
+ if success_response?(response) && response[:orderStatus] == "TRANSIT"
110
+ return self.class.find(id)
111
+ end
112
+
113
+ DhanHQ::ErrorObject.new(response)
114
+ end
115
+ ```
116
+
117
+ ---
118
+
119
+ ### **Conclusion**
120
+
121
+ Implementing these improvements will result in a more robust, testable, and maintainable gem. Enhancing error handling, validations, and separation of concerns not only eases future modifications but also improves the overall developer experience when using the gem.
122
+
123
+ Feel free to ask if you’d like more details or examples on any of these suggestions!
124
+
125
+ PROGRESS:
126
+
127
+ 1. OptionChain working