DhanHQ 2.1.6 → 2.1.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5b28412f443ad7ef0da9f10e99cee98dff8194b0d9da3c9d828dba28e3443fce
4
- data.tar.gz: 96c1411c9e3c579f35637bfb37f9644348e0d75f5fbe9a502da59789da4607ba
3
+ metadata.gz: 9a414cb1f2ba6e22e0c4014ea9ca5656607611787d7f4f18e26c50069a8c3811
4
+ data.tar.gz: 05bbdf626ae74c2a64b963bccbff2ff35bd64972a7ea825491c42ae2069377f3
5
5
  SHA512:
6
- metadata.gz: 313c052fc68d670ce53005b091f70280617c0414a9fa1f6222d0f641b3181660bcdbf92eae5f6ece36fa500f1a554faee024114b2e132c1b2e3602b384a67cc3
7
- data.tar.gz: e20baecdc074b57d78024bf5004a8808eb5397d3d1edb5743178ad50aa369e15250219c1f52384e1f00619fc8edd56bc97501ac8df375c95c7e61ca08dca96d9
6
+ metadata.gz: b9cb7044f23b775360cc999bec4c3475278d923f8f11d083fc3874827f3565c19dbe8bc1f201da6619b9bd6c1d6911f2774ee0d8e329b0c8f2f7e66155f161d0
7
+ data.tar.gz: c81c3c6685511cfef78b1083c49e5d4ea4ec692f494d36b2ee6a74580c1de4655aa9e9139c7b63dd3c40a075c02e07bd7137a30b81e6062b83f4ec7000498fc5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [2.1.8] - 2025-10-30
4
+
5
+ ### Fixed
6
+ - Correctly map `underlying_seg` for option chain APIs:
7
+ - Index instruments use `IDX_I`.
8
+ - Stocks map to `NSE_FNO` or `BSE_FNO` based on the instrument's exchange.
9
+ - Implemented via `underlying_segment_for_options` in `DhanHQ::Models::InstrumentHelpers` and applied to `expiry_list` and `option_chain`.
10
+
11
+ ## [2.1.7] - 2025-01-28
12
+
13
+ ### Added
14
+ - **Instrument instance methods**: Added convenience methods to Instrument model for accessing market feed, historical data, and option chain data
15
+ - `instrument.ltp` - Fetches last traded price using `DhanHQ::Models::MarketFeed.ltp`
16
+ - `instrument.ohlc` - Fetches OHLC data using `DhanHQ::Models::MarketFeed.ohlc`
17
+ - `instrument.quote` - Fetches full quote depth using `DhanHQ::Models::MarketFeed.quote`
18
+ - `instrument.daily(from_date:, to_date:, **options)` - Fetches daily historical data using `DhanHQ::Models::HistoricalData.daily`
19
+ - `instrument.intraday(from_date:, to_date:, interval:, **options)` - Fetches intraday historical data using `DhanHQ::Models::HistoricalData.intraday`
20
+ - `instrument.expiry_list` - Fetches expiry list using `DhanHQ::Models::OptionChain.fetch_expiry_list`
21
+ - `instrument.option_chain(expiry:)` - Fetches option chain using `DhanHQ::Models::OptionChain.fetch`
22
+ - All methods automatically use the instrument's `security_id`, `exchange_segment`, and `instrument` attributes
23
+ - **InstrumentHelpers module**: Created reusable module to provide these convenience methods
24
+
3
25
  ### Changed
4
26
  - Align Super Order documentation across README, README1, and GUIDE with the latest API contract (place, modify, cancel, list).
5
27
  - Normalise remaining documentation examples to snake_case, including order update WebSocket callbacks and kill switch response guidance.
data/GUIDE.md CHANGED
@@ -42,9 +42,9 @@ DhanHQ.logger.level = (ENV["DHAN_LOG_LEVEL"] || "INFO").upcase.then { |level| Lo
42
42
 
43
43
  `configure_with_env` reads from `ENV` and raises unless both variables are set:
44
44
 
45
- | Variable | Description |
46
- | --- | --- |
47
- | `CLIENT_ID` | Your Dhan trading client id. |
45
+ | Variable | Description |
46
+ | -------------- | ---------------------------------------------------- |
47
+ | `CLIENT_ID` | Your Dhan trading client id. |
48
48
  | `ACCESS_TOKEN` | REST/WebSocket access token generated via Dhan APIs. |
49
49
 
50
50
  Provide them via `.env`, Rails credentials, or your secret manager of choice
@@ -55,14 +55,14 @@ before the initializer runs.
55
55
  Set any of the following environment variables _before_ calling
56
56
  `configure_with_env` to customise runtime behaviour:
57
57
 
58
- | Variable | Purpose |
59
- | --- | --- |
60
- | `DHAN_LOG_LEVEL` | Change logger verbosity (`INFO` default). |
61
- | `DHAN_BASE_URL` | Override the REST API host. |
62
- | `DHAN_WS_VERSION` | Target a specific WebSocket API version. |
63
- | `DHAN_WS_ORDER_URL` | Customise the order update WebSocket endpoint. |
64
- | `DHAN_WS_USER_TYPE` | Toggle between `SELF` and `PARTNER` streaming modes. |
65
- | `DHAN_PARTNER_ID` / `DHAN_PARTNER_SECRET` | Required when streaming as a partner. |
58
+ | Variable | Purpose |
59
+ | ----------------------------------------- | ---------------------------------------------------- |
60
+ | `DHAN_LOG_LEVEL` | Change logger verbosity (`INFO` default). |
61
+ | `DHAN_BASE_URL` | Override the REST API host. |
62
+ | `DHAN_WS_VERSION` | Target a specific WebSocket API version. |
63
+ | `DHAN_WS_ORDER_URL` | Customise the order update WebSocket endpoint. |
64
+ | `DHAN_WS_USER_TYPE` | Toggle between `SELF` and `PARTNER` streaming modes. |
65
+ | `DHAN_PARTNER_ID` / `DHAN_PARTNER_SECRET` | Required when streaming as a partner. |
66
66
 
67
67
  ---
68
68
 
@@ -106,32 +106,32 @@ order.refresh
106
106
 
107
107
  Required fields validated by `DhanHQ::Contracts::PlaceOrderContract`:
108
108
 
109
- | Key | Type | Allowed Values / Notes |
110
- | ----------------- | ------- | ---------------------- |
111
- | `transaction_type`| String | `BUY`, `SELL` |
112
- | `exchange_segment`| String | Use `DhanHQ::Constants::EXCHANGE_SEGMENTS` |
113
- | `product_type` | String | `CNC`, `INTRADAY`, `MARGIN`, `MTF`, `CO`, `BO` |
114
- | `order_type` | String | `LIMIT`, `MARKET`, `STOP_LOSS`, `STOP_LOSS_MARKET` |
115
- | `validity` | String | `DAY`, `IOC` |
116
- | `security_id` | String | Security identifier from the scrip master |
117
- | `quantity` | Integer | Must be > 0 |
109
+ | Key | Type | Allowed Values / Notes |
110
+ | ------------------ | ------- | -------------------------------------------------- |
111
+ | `transaction_type` | String | `BUY`, `SELL` |
112
+ | `exchange_segment` | String | Use `DhanHQ::Constants::EXCHANGE_SEGMENTS` |
113
+ | `product_type` | String | `CNC`, `INTRADAY`, `MARGIN`, `MTF`, `CO`, `BO` |
114
+ | `order_type` | String | `LIMIT`, `MARKET`, `STOP_LOSS`, `STOP_LOSS_MARKET` |
115
+ | `validity` | String | `DAY`, `IOC` |
116
+ | `security_id` | String | Security identifier from the scrip master |
117
+ | `quantity` | Integer | Must be > 0 |
118
118
 
119
119
  Optional fields and special rules:
120
120
 
121
- | Key | Type | Notes |
122
- | --------------------- | ------- | ----- |
123
- | `correlation_id` | String | ≤ 25 chars; useful for idempotency |
124
- | `disclosed_quantity` | Integer | ≥ 0 and ≤ 30% of `quantity` |
125
- | `trading_symbol` | String | Optional label |
126
- | `price` | Float | Mandatory for `LIMIT` |
127
- | `trigger_price` | Float | Mandatory for SL / SLM |
128
- | `after_market_order` | Boolean | Require `amo_time` when true |
129
- | `amo_time` | String | `OPEN`, `OPEN_30`, `OPEN_60` (check `DhanHQ::Constants::AMO_TIMINGS` for updates) |
130
- | `bo_profit_value` | Float | Required with `product_type: "BO"` |
131
- | `bo_stop_loss_value` | Float | Required with `product_type: "BO"` |
132
- | `drv_expiry_date` | String | Pass ISO `YYYY-MM-DD` for derivatives |
133
- | `drv_option_type` | String | `CALL`, `PUT`, `NA` |
134
- | `drv_strike_price` | Float | > 0 |
121
+ | Key | Type | Notes |
122
+ | -------------------- | ------- | --------------------------------------------------------------------------------- |
123
+ | `correlation_id` | String | ≤ 25 chars; useful for idempotency |
124
+ | `disclosed_quantity` | Integer | ≥ 0 and ≤ 30% of `quantity` |
125
+ | `trading_symbol` | String | Optional label |
126
+ | `price` | Float | Mandatory for `LIMIT` |
127
+ | `trigger_price` | Float | Mandatory for SL / SLM |
128
+ | `after_market_order` | Boolean | Require `amo_time` when true |
129
+ | `amo_time` | String | `OPEN`, `OPEN_30`, `OPEN_60` (check `DhanHQ::Constants::AMO_TIMINGS` for updates) |
130
+ | `bo_profit_value` | Float | Required with `product_type: "BO"` |
131
+ | `bo_stop_loss_value` | Float | Required with `product_type: "BO"` |
132
+ | `drv_expiry_date` | String | Pass ISO `YYYY-MM-DD` for derivatives |
133
+ | `drv_option_type` | String | `CALL`, `PUT`, `NA` |
134
+ | `drv_strike_price` | Float | > 0 |
135
135
 
136
136
  Example:
137
137
 
@@ -212,12 +212,12 @@ DhanHQ::Models::Order.resource.slicing(payload)
212
212
 
213
213
  #### Endpoints
214
214
 
215
- | Method | Path | Description |
216
- | --- | --- | --- |
217
- | `POST` | `/super/orders` | Create a new super order |
218
- | `PUT` | `/super/orders/{order_id}` | Modify a pending super order |
219
- | `DELETE` | `/super/orders/{order_id}/{order_leg}` | Cancel a pending super order leg |
220
- | `GET` | `/super/orders` | Retrieve the list of all super orders |
215
+ | Method | Path | Description |
216
+ | -------- | -------------------------------------- | ------------------------------------- |
217
+ | `POST` | `/super/orders` | Create a new super order |
218
+ | `PUT` | `/super/orders/{order_id}` | Modify a pending super order |
219
+ | `DELETE` | `/super/orders/{order_id}/{order_leg}` | Cancel a pending super order leg |
220
+ | `GET` | `/super/orders` | Retrieve the list of all super orders |
221
221
 
222
222
  #### Place Super Order
223
223
 
@@ -252,20 +252,20 @@ Request body:
252
252
 
253
253
  Key parameters:
254
254
 
255
- | Field | Type | Notes |
256
- | --- | --- | --- |
257
- | `dhan_client_id` | string *(required)* | User specific identifier generated by Dhan. Automatically merged when you call through the Ruby model helpers. |
258
- | `correlation_id` | string | Optional caller supplied correlation id. |
259
- | `transaction_type` | enum string *(required)* | `BUY` or `SELL`. |
260
- | `exchange_segment` | enum string *(required)* | Exchange segment. |
261
- | `product_type` | enum string *(required)* | `CNC`, `INTRADAY`, `MARGIN`, or `MTF`. |
262
- | `order_type` | enum string *(required)* | `LIMIT` or `MARKET`. |
263
- | `security_id` | string *(required)* | Exchange security identifier. |
264
- | `quantity` | integer *(required)* | Entry quantity. |
265
- | `price` | float *(required)* | Entry price. |
266
- | `target_price` | float *(required)* | Target price for the super order. |
267
- | `stop_loss_price` | float *(required)* | Stop-loss price for the super order. |
268
- | `trailing_jump` | float *(required)* | Trailing jump size. |
255
+ | Field | Type | Notes |
256
+ | ------------------ | ------------------------ | -------------------------------------------------------------------------------------------------------------- |
257
+ | `dhan_client_id` | string *(required)* | User specific identifier generated by Dhan. Automatically merged when you call through the Ruby model helpers. |
258
+ | `correlation_id` | string | Optional caller supplied correlation id. |
259
+ | `transaction_type` | enum string *(required)* | `BUY` or `SELL`. |
260
+ | `exchange_segment` | enum string *(required)* | Exchange segment. |
261
+ | `product_type` | enum string *(required)* | `CNC`, `INTRADAY`, `MARGIN`, or `MTF`. |
262
+ | `order_type` | enum string *(required)* | `LIMIT` or `MARKET`. |
263
+ | `security_id` | string *(required)* | Exchange security identifier. |
264
+ | `quantity` | integer *(required)* | Entry quantity. |
265
+ | `price` | float *(required)* | Entry price. |
266
+ | `target_price` | float *(required)* | Target price for the super order. |
267
+ | `stop_loss_price` | float *(required)* | Stop-loss price for the super order. |
268
+ | `trailing_jump` | float *(required)* | Trailing jump size. |
269
269
 
270
270
  > 🐍 Pass snake_case keys when invoking `DhanHQ::Models::SuperOrder.create`—the client camelizes internally before calling the REST API and injects your configured `dhan_client_id`, so the key is optional in Ruby payloads.
271
271
 
@@ -308,14 +308,14 @@ Example payload:
308
308
 
309
309
  Conditional fields:
310
310
 
311
- | Field | Required when | Notes |
312
- | --- | --- | --- |
313
- | `order_type` | Updating `ENTRY_LEG` | `LIMIT` or `MARKET`. |
314
- | `quantity` | Updating `ENTRY_LEG` | Adjusts entry quantity. |
315
- | `price` | Updating `ENTRY_LEG` | Adjusts entry price. |
316
- | `target_price` | Updating `ENTRY_LEG` or `TARGET_LEG` | Adjusts target price. |
317
- | `stop_loss_price` | Updating `ENTRY_LEG` or `STOP_LOSS_LEG` | Adjusts stop-loss price. |
318
- | `trailing_jump` | Updating `ENTRY_LEG` or `STOP_LOSS_LEG` | Pass `0` or omit to cancel trailing. |
311
+ | Field | Required when | Notes |
312
+ | ----------------- | --------------------------------------- | ------------------------------------ |
313
+ | `order_type` | Updating `ENTRY_LEG` | `LIMIT` or `MARKET`. |
314
+ | `quantity` | Updating `ENTRY_LEG` | Adjusts entry quantity. |
315
+ | `price` | Updating `ENTRY_LEG` | Adjusts entry price. |
316
+ | `target_price` | Updating `ENTRY_LEG` or `TARGET_LEG` | Adjusts target price. |
317
+ | `stop_loss_price` | Updating `ENTRY_LEG` or `STOP_LOSS_LEG` | Adjusts stop-loss price. |
318
+ | `trailing_jump` | Updating `ENTRY_LEG` or `STOP_LOSS_LEG` | Pass `0` or omit to cancel trailing. |
319
319
 
320
320
  Response:
321
321
 
@@ -337,10 +337,10 @@ curl --request DELETE \
337
337
 
338
338
  Path parameters:
339
339
 
340
- | Field | Description | Example |
341
- | --- | --- | --- |
342
- | `order_id` | Super order identifier. | `11211182198` |
343
- | `order_leg` | Leg to cancel (`ENTRY_LEG`, `TARGET_LEG`, or `STOP_LOSS_LEG`). | `ENTRY_LEG` |
340
+ | Field | Description | Example |
341
+ | ----------- | -------------------------------------------------------------- | ------------- |
342
+ | `order_id` | Super order identifier. | `11211182198` |
343
+ | `order_leg` | Leg to cancel (`ENTRY_LEG`, `TARGET_LEG`, or `STOP_LOSS_LEG`). | `ENTRY_LEG` |
344
344
 
345
345
  Response:
346
346
 
@@ -470,15 +470,15 @@ Both endpoints return arrays and skip validation because they represent historic
470
470
 
471
471
  `DhanHQ::Models::HistoricalData` enforces `HistoricalDataContract` before delegating to `/v2/charts`.
472
472
 
473
- | Key | Type | Notes |
474
- | ------------------ | ------ | ----- |
475
- | `security_id` | String | Required |
476
- | `exchange_segment` | String | See `EXCHANGE_SEGMENTS` |
477
- | `instrument` | String | Use `DhanHQ::Constants::INSTRUMENTS` |
478
- | `from_date` | String | `YYYY-MM-DD` |
479
- | `to_date` | String | `YYYY-MM-DD` |
480
- | `expiry_code` | Integer| Optional (`0`, `1`, `2`) |
481
- | `interval` | String | Optional (`1`, `5`, `15`, `25`, `60`) for intraday |
473
+ | Key | Type | Notes |
474
+ | ------------------ | ------- | -------------------------------------------------- |
475
+ | `security_id` | String | Required |
476
+ | `exchange_segment` | String | See `EXCHANGE_SEGMENTS` |
477
+ | `instrument` | String | Use `DhanHQ::Constants::INSTRUMENTS` |
478
+ | `from_date` | String | `YYYY-MM-DD` |
479
+ | `to_date` | String | `YYYY-MM-DD` |
480
+ | `expiry_code` | Integer | Optional (`0`, `1`, `2`) |
481
+ | `interval` | String | Optional (`1`, `5`, `15`, `25`, `60`) for intraday |
482
482
 
483
483
  ```ruby
484
484
  bars = DhanHQ::Models::HistoricalData.intraday(
@@ -543,7 +543,55 @@ ohlc = DhanHQ::Models::MarketFeed.ohlc(payload)
543
543
  quote = DhanHQ::Models::MarketFeed.quote(payload)
544
544
  ```
545
545
 
546
- These endpoints are rate-limited by Dhan. The clients internal `RateLimiter` throttles calls—consider batching symbols sensibly.
546
+ These endpoints are rate-limited by Dhan. The client's internal `RateLimiter` throttles calls—consider batching symbols sensibly.
547
+
548
+ ### Instrument Convenience Methods
549
+
550
+ The Instrument model provides convenient instance methods that automatically use the instrument's attributes (`security_id`, `exchange_segment`, `instrument`) to fetch market data. This eliminates the need to manually construct parameters for each API call.
551
+
552
+ ```ruby
553
+ # Find an instrument first
554
+ instrument = DhanHQ::Models::Instrument.find("IDX_I", "NIFTY")
555
+ # or
556
+ reliance = DhanHQ::Models::Instrument.find("NSE_EQ", "RELIANCE")
557
+
558
+ # Market Feed Methods - automatically uses instrument's attributes
559
+ ltp_data = instrument.ltp # Last traded price
560
+ ohlc_data = instrument.ohlc # OHLC data
561
+ quote_data = instrument.quote # Full quote depth
562
+
563
+ # Historical Data Methods
564
+ daily_data = instrument.daily(
565
+ from_date: "2024-01-01",
566
+ to_date: "2024-01-31",
567
+ expiry_code: 0 # Optional, only for derivatives
568
+ )
569
+
570
+ intraday_data = instrument.intraday(
571
+ from_date: "2024-09-11",
572
+ to_date: "2024-09-15",
573
+ interval: "15" # 1, 5, 15, 25, or 60 minutes
574
+ )
575
+
576
+ # Option Chain Methods (for F&O instruments)
577
+ fn_instrument = DhanHQ::Models::Instrument.find("IDX_I", "NIFTY")
578
+ expiries = fn_instrument.expiry_list # Get all available expiries
579
+ chain = fn_instrument.option_chain(expiry: "2024-02-29") # Get option chain for specific expiry
580
+ ```
581
+
582
+ **Available Instance Methods:**
583
+
584
+ | Method | Description | Underlying API |
585
+ | ----------------------------------------------------------------- | -------------------------------- | ----------------------------------------------- |
586
+ | `instrument.ltp` | Fetches last traded price | `DhanHQ::Models::MarketFeed.ltp` |
587
+ | `instrument.ohlc` | Fetches OHLC data | `DhanHQ::Models::MarketFeed.ohlc` |
588
+ | `instrument.quote` | Fetches full quote depth | `DhanHQ::Models::MarketFeed.quote` |
589
+ | `instrument.daily(from_date:, to_date:, **options)` | Fetches daily historical data | `DhanHQ::Models::HistoricalData.daily` |
590
+ | `instrument.intraday(from_date:, to_date:, interval:, **options)` | Fetches intraday historical data | `DhanHQ::Models::HistoricalData.intraday` |
591
+ | `instrument.expiry_list` | Fetches expiry list | `DhanHQ::Models::OptionChain.fetch_expiry_list` |
592
+ | `instrument.option_chain(expiry:)` | Fetches option chain | `DhanHQ::Models::OptionChain.fetch` |
593
+
594
+ All methods automatically extract `security_id`, `exchange_segment`, and `instrument` from the instrument instance, making it easier to work with market data without manually managing these parameters.
547
595
 
548
596
  ### WebSocket Market Feed
549
597
 
data/README.md CHANGED
@@ -327,6 +327,49 @@ puts "Sell Margin %: #{reliance.sell_co_min_margin_per}%"
327
327
  - `sell_co_min_margin_per` - Sell CO minimum margin percentage
328
328
  - `mtf_leverage` - Margin Trading Facility leverage
329
329
 
330
+ ### Instrument Convenience Methods
331
+
332
+ The Instrument model provides convenient instance methods that automatically use the instrument's attributes (`security_id`, `exchange_segment`, `instrument`) to fetch market data:
333
+
334
+ ```ruby
335
+ # Find an instrument
336
+ instrument = DhanHQ::Models::Instrument.find("IDX_I", "NIFTY")
337
+
338
+ # Market Feed Methods - automatically uses instrument's attributes
339
+ ltp_data = instrument.ltp # Last traded price
340
+ ohlc_data = instrument.ohlc # OHLC data
341
+ quote_data = instrument.quote # Full quote depth
342
+
343
+ # Historical Data Methods
344
+ daily_data = instrument.daily(
345
+ from_date: "2024-01-01",
346
+ to_date: "2024-01-31",
347
+ expiry_code: 0 # Optional
348
+ )
349
+
350
+ intraday_data = instrument.intraday(
351
+ from_date: "2024-09-11",
352
+ to_date: "2024-09-15",
353
+ interval: "15" # 1, 5, 15, 25, or 60 minutes
354
+ )
355
+
356
+ # Option Chain Methods
357
+ expiries = instrument.expiry_list # Get all available expiries
358
+
359
+ chain = instrument.option_chain(expiry: "2024-02-29") # Get option chain for specific expiry
360
+ ```
361
+
362
+ **Available Instance Methods:**
363
+ - `instrument.ltp` - Fetches last traded price using `DhanHQ::Models::MarketFeed.ltp`
364
+ - `instrument.ohlc` - Fetches OHLC data using `DhanHQ::Models::MarketFeed.ohlc`
365
+ - `instrument.quote` - Fetches full quote depth using `DhanHQ::Models::MarketFeed.quote`
366
+ - `instrument.daily(from_date:, to_date:, **options)` - Fetches daily historical data using `DhanHQ::Models::HistoricalData.daily`
367
+ - `instrument.intraday(from_date:, to_date:, interval:, **options)` - Fetches intraday historical data using `DhanHQ::Models::HistoricalData.intraday`
368
+ - `instrument.expiry_list` - Fetches expiry list using `DhanHQ::Models::OptionChain.fetch_expiry_list`
369
+ - `instrument.option_chain(expiry:)` - Fetches option chain using `DhanHQ::Models::OptionChain.fetch`
370
+
371
+ All methods automatically use the instrument's `security_id`, `exchange_segment`, and `instrument` attributes, eliminating the need to manually pass these parameters.
372
+
330
373
  ### Comprehensive Documentation
331
374
 
332
375
  The gem includes detailed documentation for different integration scenarios:
data/README1.md CHANGED
@@ -196,14 +196,36 @@ DhanHQ::Models::OptionChain.fetch_expiry_list(security_id: "1333")
196
196
  ### Historical Data
197
197
 
198
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")
199
+ DhanHQ::Models::HistoricalData.daily(security_id: "1333", exchange_segment: "NSE_EQ", instrument: "EQUITY", from_date: "2024-01-01", to_date: "2024-01-31")
200
+ DhanHQ::Models::HistoricalData.intraday(security_id: "1333", exchange_segment: "NSE_EQ", instrument: "EQUITY", interval: "15", from_date: "2024-09-11", to_date: "2024-09-15")
201
+ ```
202
+
203
+ ### Instrument Model with Convenience Methods
204
+
205
+ The Instrument model provides convenient instance methods that automatically use the instrument's attributes:
206
+
207
+ ```ruby
208
+ # Find an instrument
209
+ instrument = DhanHQ::Models::Instrument.find("IDX_I", "NIFTY")
210
+
211
+ # Market Feed Methods - automatically uses instrument's attributes
212
+ ltp_data = instrument.ltp # Last traded price
213
+ ohlc_data = instrument.ohlc # OHLC data
214
+ quote_data = instrument.quote # Full quote depth
215
+
216
+ # Historical Data Methods
217
+ daily_data = instrument.daily(from_date: "2024-01-01", to_date: "2024-01-31")
218
+ intraday_data = instrument.intraday(from_date: "2024-09-11", to_date: "2024-09-15", interval: "15")
219
+
220
+ # Option Chain Methods
221
+ expiries = instrument.expiry_list
222
+ chain = instrument.option_chain(expiry: "2024-02-29")
201
223
  ```
202
224
 
203
225
  ## 🔹 Available Resources
204
226
 
205
- | Resource | Model | Actions |
206
- | ------------------------ | -------------------------------- | --------------------------------------------------- |
227
+ | Resource | Model | Actions |
228
+ | ------------------------ | ---------------------------------------- | --------------------------------------------------- |
207
229
  | Orders | `DhanHQ::Models::Models::Order` | `find`, `all`, `where`, `place`, `update`, `cancel` |
208
230
  | Trades | `DhanHQ::Models::Models::Trade` | `all`, `find_by_order_id` |
209
231
  | Forever Orders | `DhanHQ::Models::Models::ForeverOrder` | `create`, `find`, `modify`, `cancel`, `all` |
@@ -431,12 +453,12 @@ The gem exposes all related REST endpoints so you can create, modify, cancel, an
431
453
 
432
454
  ### Endpoints
433
455
 
434
- | Method | Path | Description |
435
- | --- | --- | --- |
436
- | `POST` | `/super/orders` | Create a new super order |
437
- | `PUT` | `/super/orders/{order_id}` | Modify a pending super order |
438
- | `DELETE` | `/super/orders/{order_id}/{order_leg}` | Cancel a pending super order leg |
439
- | `GET` | `/super/orders` | Retrieve the list of all super orders |
456
+ | Method | Path | Description |
457
+ | -------- | -------------------------------------- | ------------------------------------- |
458
+ | `POST` | `/super/orders` | Create a new super order |
459
+ | `PUT` | `/super/orders/{order_id}` | Modify a pending super order |
460
+ | `DELETE` | `/super/orders/{order_id}/{order_leg}` | Cancel a pending super order leg |
461
+ | `GET` | `/super/orders` | Retrieve the list of all super orders |
440
462
 
441
463
  ### Place Super Order
442
464
 
@@ -473,20 +495,20 @@ curl --request POST \
473
495
 
474
496
  #### Parameters
475
497
 
476
- | Field | Type | Description |
477
- | --- | --- | --- |
478
- | `dhan_client_id` | string *(required)* | User specific identification generated by Dhan. When you call via `DhanHQ::Models::SuperOrder`, the gem injects your configured client id so you can omit the key in Ruby. |
479
- | `correlation_id` | string | Caller supplied identifier for tracking |
480
- | `transaction_type` | enum string *(required)* | Trading side. `BUY` or `SELL`. |
481
- | `exchange_segment` | enum string *(required)* | Exchange segment (see appendix). |
482
- | `product_type` | enum string *(required)* | Product type. `CNC`, `INTRADAY`, `MARGIN`, or `MTF`. |
483
- | `order_type` | enum string *(required)* | Order type. `LIMIT` or `MARKET`. |
484
- | `security_id` | string *(required)* | Exchange standard security identifier. |
485
- | `quantity` | integer *(required)* | Quantity for the entry leg. |
486
- | `price` | float *(required)* | Entry price. |
487
- | `target_price` | float *(required)* | Target price for the super order. |
488
- | `stop_loss_price` | float *(required)* | Stop-loss price for the super order. |
489
- | `trailing_jump` | float *(required)* | Price jump size for trailing stop-loss. |
498
+ | Field | Type | Description |
499
+ | ------------------ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
500
+ | `dhan_client_id` | string *(required)* | User specific identification generated by Dhan. When you call via `DhanHQ::Models::SuperOrder`, the gem injects your configured client id so you can omit the key in Ruby. |
501
+ | `correlation_id` | string | Caller supplied identifier for tracking |
502
+ | `transaction_type` | enum string *(required)* | Trading side. `BUY` or `SELL`. |
503
+ | `exchange_segment` | enum string *(required)* | Exchange segment (see appendix). |
504
+ | `product_type` | enum string *(required)* | Product type. `CNC`, `INTRADAY`, `MARGIN`, or `MTF`. |
505
+ | `order_type` | enum string *(required)* | Order type. `LIMIT` or `MARKET`. |
506
+ | `security_id` | string *(required)* | Exchange standard security identifier. |
507
+ | `quantity` | integer *(required)* | Quantity for the entry leg. |
508
+ | `price` | float *(required)* | Entry price. |
509
+ | `target_price` | float *(required)* | Target price for the super order. |
510
+ | `stop_loss_price` | float *(required)* | Stop-loss price for the super order. |
511
+ | `trailing_jump` | float *(required)* | Price jump size for trailing stop-loss. |
490
512
 
491
513
  > 🐍 When you call `DhanHQ::Models::SuperOrder.create`, supply snake_case keys exactly as shown. The client automatically camelizes
492
514
  > them before submitting to Dhan's REST API and injects your configured `dhan_client_id`, allowing you to omit the key in Ruby code.
@@ -500,9 +522,9 @@ curl --request POST \
500
522
  }
501
523
  ```
502
524
 
503
- | Field | Type | Description |
504
- | --- | --- | --- |
505
- | `order_id` | string | Order identifier generated by Dhan |
525
+ | Field | Type | Description |
526
+ | -------------- | ----------- | --------------------------------------------------- |
527
+ | `order_id` | string | Order identifier generated by Dhan |
506
528
  | `order_status` | enum string | Latest status. `TRANSIT`, `PENDING`, or `REJECTED`. |
507
529
 
508
530
  ### Modify Super Order
@@ -537,17 +559,17 @@ curl --request PUT \
537
559
 
538
560
  #### Parameters
539
561
 
540
- | Field | Type | Description |
541
- | --- | --- | --- |
542
- | `dhan_client_id` | string *(required)* | User specific identification generated by Dhan. Automatically merged when using the Ruby model helpers. |
543
- | `order_id` | string *(required)* | Super order identifier generated by Dhan. |
544
- | `order_type` | enum string *(conditionally required)* | `LIMIT` or `MARKET`. Required when changing `ENTRY_LEG`. |
545
- | `leg_name` | enum string *(required)* | `ENTRY_LEG`, `TARGET_LEG`, or `STOP_LOSS_LEG`. Entry edits allowed only while status is `PENDING` or `PART_TRADED`. |
546
- | `quantity` | integer *(conditionally required)* | Quantity update for `ENTRY_LEG`. |
547
- | `price` | float *(conditionally required)* | Entry price update for `ENTRY_LEG`. |
548
- | `target_price` | float *(conditionally required)* | Target price update for `ENTRY_LEG` or `TARGET_LEG`. |
549
- | `stop_loss_price` | float *(conditionally required)* | Stop-loss price update for `ENTRY_LEG` or `STOP_LOSS_LEG`. |
550
- | `trailing_jump` | float *(conditionally required)* | Trailing jump update for `ENTRY_LEG` or `STOP_LOSS_LEG`. Omit or set to `0` to cancel trailing. |
562
+ | Field | Type | Description |
563
+ | ----------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
564
+ | `dhan_client_id` | string *(required)* | User specific identification generated by Dhan. Automatically merged when using the Ruby model helpers. |
565
+ | `order_id` | string *(required)* | Super order identifier generated by Dhan. |
566
+ | `order_type` | enum string *(conditionally required)* | `LIMIT` or `MARKET`. Required when changing `ENTRY_LEG`. |
567
+ | `leg_name` | enum string *(required)* | `ENTRY_LEG`, `TARGET_LEG`, or `STOP_LOSS_LEG`. Entry edits allowed only while status is `PENDING` or `PART_TRADED`. |
568
+ | `quantity` | integer *(conditionally required)* | Quantity update for `ENTRY_LEG`. |
569
+ | `price` | float *(conditionally required)* | Entry price update for `ENTRY_LEG`. |
570
+ | `target_price` | float *(conditionally required)* | Target price update for `ENTRY_LEG` or `TARGET_LEG`. |
571
+ | `stop_loss_price` | float *(conditionally required)* | Stop-loss price update for `ENTRY_LEG` or `STOP_LOSS_LEG`. |
572
+ | `trailing_jump` | float *(conditionally required)* | Trailing jump update for `ENTRY_LEG` or `STOP_LOSS_LEG`. Omit or set to `0` to cancel trailing. |
551
573
 
552
574
  #### Response
553
575
 
@@ -558,9 +580,9 @@ curl --request PUT \
558
580
  }
559
581
  ```
560
582
 
561
- | Field | Type | Description |
562
- | --- | --- | --- |
563
- | `order_id` | string | Order identifier generated by Dhan |
583
+ | Field | Type | Description |
584
+ | -------------- | ----------- | ------------------------------------------------------------- |
585
+ | `order_id` | string | Order identifier generated by Dhan |
564
586
  | `order_status` | enum string | Latest status. `TRANSIT`, `PENDING`, `REJECTED`, or `TRADED`. |
565
587
 
566
588
  ### Cancel Super Order
@@ -578,10 +600,10 @@ curl --request DELETE \
578
600
 
579
601
  #### Path parameters
580
602
 
581
- | Field | Description | Example |
582
- | --- | --- | --- |
583
- | `order_id` | Super order identifier. | `11211182198` |
584
- | `order_leg` | Leg to cancel. `ENTRY_LEG`, `TARGET_LEG`, or `STOP_LOSS_LEG`. | `ENTRY_LEG` |
603
+ | Field | Description | Example |
604
+ | ----------- | ------------------------------------------------------------- | ------------- |
605
+ | `order_id` | Super order identifier. | `11211182198` |
606
+ | `order_leg` | Leg to cancel. `ENTRY_LEG`, `TARGET_LEG`, or `STOP_LOSS_LEG`. | `ENTRY_LEG` |
585
607
 
586
608
  #### Response
587
609
 
@@ -592,9 +614,9 @@ curl --request DELETE \
592
614
  }
593
615
  ```
594
616
 
595
- | Field | Type | Description |
596
- | --- | --- | --- |
597
- | `order_id` | string | Order identifier generated by Dhan |
617
+ | Field | Type | Description |
618
+ | -------------- | ----------- | ---------------------------------------------------------------- |
619
+ | `order_id` | string | Order identifier generated by Dhan |
598
620
  | `order_status` | enum string | Latest status. `TRANSIT`, `PENDING`, `REJECTED`, or `CANCELLED`. |
599
621
 
600
622
  ### Super Order List
@@ -666,35 +688,35 @@ curl --request GET \
666
688
 
667
689
  #### Parameters
668
690
 
669
- | Field | Type | Description |
670
- | --- | --- | --- |
671
- | `dhan_client_id` | string | User specific identification generated by Dhan. |
672
- | `order_id` | string | Order identifier generated by Dhan. |
673
- | `correlation_id` | string | Caller supplied correlation identifier. |
674
- | `order_status` | enum string | Latest status. `TRANSIT`, `PENDING`, `CLOSED`, `REJECTED`, `CANCELLED`, `PART_TRADED`, or `TRADED`. |
675
- | `transaction_type` | enum string | Trading side. `BUY` or `SELL`. |
676
- | `exchange_segment` | enum string | Exchange segment. |
677
- | `product_type` | enum string | Product type. `CNC`, `INTRADAY`, `MARGIN`, or `MTF`. |
678
- | `order_type` | enum string | Order type. `LIMIT` or `MARKET`. |
679
- | `validity` | enum string | Order validity. `DAY`. |
680
- | `trading_symbol` | string | Trading symbol reference. |
681
- | `security_id` | string | Exchange security identifier. |
682
- | `quantity` | integer | Ordered quantity. |
683
- | `remaining_quantity` | integer | Quantity pending execution. |
684
- | `ltp` | float | Last traded price. |
685
- | `price` | float | Order price. |
686
- | `after_market_order` | boolean | Indicates if order was placed after market hours. |
687
- | `leg_name` | enum string | Leg identifier: `ENTRY_LEG`, `TARGET_LEG`, or `STOP_LOSS_LEG`. |
688
- | `trailing_jump` | float | Trailing jump for stop-loss. |
689
- | `exchange_order_id` | string | Exchange generated identifier. |
690
- | `create_time` | string | Order creation timestamp. |
691
- | `update_time` | string | Latest update timestamp. |
692
- | `exchange_time` | string | Exchange timestamp. |
693
- | `oms_error_description` | string | OMS error description when applicable. |
694
- | `average_traded_price` | float | Average traded price. |
695
- | `filled_qty` | integer | Quantity traded on the exchange. |
696
- | `triggered_quantity` | integer | Quantity triggered for stop-loss or target legs. |
697
- | `leg_details` | array | Nested leg details for the super order. |
691
+ | Field | Type | Description |
692
+ | ----------------------- | ----------- | --------------------------------------------------------------------------------------------------- |
693
+ | `dhan_client_id` | string | User specific identification generated by Dhan. |
694
+ | `order_id` | string | Order identifier generated by Dhan. |
695
+ | `correlation_id` | string | Caller supplied correlation identifier. |
696
+ | `order_status` | enum string | Latest status. `TRANSIT`, `PENDING`, `CLOSED`, `REJECTED`, `CANCELLED`, `PART_TRADED`, or `TRADED`. |
697
+ | `transaction_type` | enum string | Trading side. `BUY` or `SELL`. |
698
+ | `exchange_segment` | enum string | Exchange segment. |
699
+ | `product_type` | enum string | Product type. `CNC`, `INTRADAY`, `MARGIN`, or `MTF`. |
700
+ | `order_type` | enum string | Order type. `LIMIT` or `MARKET`. |
701
+ | `validity` | enum string | Order validity. `DAY`. |
702
+ | `trading_symbol` | string | Trading symbol reference. |
703
+ | `security_id` | string | Exchange security identifier. |
704
+ | `quantity` | integer | Ordered quantity. |
705
+ | `remaining_quantity` | integer | Quantity pending execution. |
706
+ | `ltp` | float | Last traded price. |
707
+ | `price` | float | Order price. |
708
+ | `after_market_order` | boolean | Indicates if order was placed after market hours. |
709
+ | `leg_name` | enum string | Leg identifier: `ENTRY_LEG`, `TARGET_LEG`, or `STOP_LOSS_LEG`. |
710
+ | `trailing_jump` | float | Trailing jump for stop-loss. |
711
+ | `exchange_order_id` | string | Exchange generated identifier. |
712
+ | `create_time` | string | Order creation timestamp. |
713
+ | `update_time` | string | Latest update timestamp. |
714
+ | `exchange_time` | string | Exchange timestamp. |
715
+ | `oms_error_description` | string | OMS error description when applicable. |
716
+ | `average_traded_price` | float | Average traded price. |
717
+ | `filled_qty` | integer | Quantity traded on the exchange. |
718
+ | `triggered_quantity` | integer | Quantity triggered for stop-loss or target legs. |
719
+ | `leg_details` | array | Nested leg details for the super order. |
698
720
 
699
721
  > ✅ `CLOSED` indicates the entry leg and either target or stop-loss completed for the full quantity. `TRIGGERED` appears on target and stop-loss legs to highlight which one fired; inspect `triggered_quantity` for executed quantity.
700
722
 
@@ -511,6 +511,53 @@ end
511
511
  | `sell_co_min_margin_per` | Float | Sell CO minimum margin percentage | 0.0, 20.0 |
512
512
  | `mtf_leverage` | Float | Margin Trading Facility leverage | 0.0, 4.545455 |
513
513
 
514
+ ### Instrument Convenience Methods
515
+
516
+ The Instrument model provides convenient instance methods that automatically use the instrument's attributes (`security_id`, `exchange_segment`, `instrument`) to fetch market data. This eliminates the need to manually construct parameters for each API call.
517
+
518
+ ```ruby
519
+ # Find an instrument first
520
+ instrument = DhanHQ::Models::Instrument.find("IDX_I", "NIFTY")
521
+ reliance = DhanHQ::Models::Instrument.find("NSE_EQ", "RELIANCE")
522
+
523
+ # Market Feed Methods - automatically uses instrument's attributes
524
+ ltp_data = instrument.ltp # Last traded price
525
+ ohlc_data = instrument.ohlc # OHLC data
526
+ quote_data = instrument.quote # Full quote depth
527
+
528
+ # Historical Data Methods
529
+ daily_data = instrument.daily(
530
+ from_date: "2024-01-01",
531
+ to_date: "2024-01-31",
532
+ expiry_code: 0 # Optional, only for derivatives
533
+ )
534
+
535
+ intraday_data = instrument.intraday(
536
+ from_date: "2024-09-11",
537
+ to_date: "2024-09-15",
538
+ interval: "15" # 1, 5, 15, 25, or 60 minutes
539
+ )
540
+
541
+ # Option Chain Methods (for F&O instruments)
542
+ fn_instrument = DhanHQ::Models::Instrument.find("NSE_FNO", "NIFTY")
543
+ expiries = fn_instrument.expiry_list # Get all available expiries
544
+ chain = fn_instrument.option_chain(expiry: "2024-02-29") # Get option chain for specific expiry
545
+ ```
546
+
547
+ **Available Instance Methods:**
548
+
549
+ | Method | Description | Underlying API |
550
+ | ----------------------------------------------------------------- | -------------------------------- | ----------------------------------------------- |
551
+ | `instrument.ltp` | Fetches last traded price | `DhanHQ::Models::MarketFeed.ltp` |
552
+ | `instrument.ohlc` | Fetches OHLC data | `DhanHQ::Models::MarketFeed.ohlc` |
553
+ | `instrument.quote` | Fetches full quote depth | `DhanHQ::Models::MarketFeed.quote` |
554
+ | `instrument.daily(from_date:, to_date:, **options)` | Fetches daily historical data | `DhanHQ::Models::HistoricalData.daily` |
555
+ | `instrument.intraday(from_date:, to_date:, interval:, **options)` | Fetches intraday historical data | `DhanHQ::Models::HistoricalData.intraday` |
556
+ | `instrument.expiry_list` | Fetches expiry list | `DhanHQ::Models::OptionChain.fetch_expiry_list` |
557
+ | `instrument.option_chain(expiry:)` | Fetches option chain | `DhanHQ::Models::OptionChain.fetch` |
558
+
559
+ All methods automatically extract `security_id`, `exchange_segment`, and `instrument` from the instrument instance, making it easier to work with market data without manually managing these parameters.
560
+
514
561
  ## Rails Integration
515
562
 
516
563
  ### Basic Rails Integration
File without changes
File without changes
@@ -101,7 +101,7 @@ summary_timer = Thread.new do
101
101
  end
102
102
  end
103
103
 
104
- puts "\n" + ("=" * 50)
104
+ puts "\n#{"=" * 50}"
105
105
  end
106
106
  end
107
107
 
@@ -74,14 +74,14 @@ depth_client = DhanHQ::WS::MarketDepth.connect(symbols: symbols) do |depth_data|
74
74
  puts " Ask Levels: #{depth_data[:asks].size}"
75
75
 
76
76
  # Show top 3 bid/ask levels if available
77
- if depth_data[:bids] && depth_data[:bids].size > 0
77
+ if depth_data[:bids]&.size&.positive?
78
78
  puts " Top Bids:"
79
79
  depth_data[:bids].first(3).each_with_index do |bid, i|
80
80
  puts " #{i + 1}. Price: #{bid[:price]}, Qty: #{bid[:quantity]}"
81
81
  end
82
82
  end
83
83
 
84
- if depth_data[:asks] && depth_data[:asks].size > 0
84
+ if depth_data[:asks]&.size&.positive?
85
85
  puts " Top Asks:"
86
86
  depth_data[:asks].first(3).each_with_index do |ask, i|
87
87
  puts " #{i + 1}. Price: #{ask[:price]}, Qty: #{ask[:quantity]}"
File without changes
@@ -109,7 +109,7 @@ def calculate_margin_requirements(instrument, name, quantity, price)
109
109
  puts " Sell CO Margin Required: ₹#{sell_margin}"
110
110
 
111
111
  # Calculate MTF leverage
112
- if instrument.mtf_leverage > 0
112
+ if instrument.mtf_leverage.positive?
113
113
  mtf_value = total_value * instrument.mtf_leverage
114
114
  puts " MTF Leverage: #{instrument.mtf_leverage}x"
115
115
  puts " MTF Value: ₹#{mtf_value}"
data/lib/DhanHQ/client.rb CHANGED
@@ -38,7 +38,8 @@ module DhanHQ
38
38
  # @return [DhanHQ::Client] A new client instance.
39
39
  def initialize(api_type:)
40
40
  DhanHQ.configure_with_env if ENV.fetch("CLIENT_ID", nil)
41
- @rate_limiter = RateLimiter.new(api_type)
41
+ # Use shared rate limiter instance per API type to ensure proper coordination
42
+ @rate_limiter = RateLimiter.for(api_type)
42
43
 
43
44
  raise "RateLimiter initialization failed" unless @rate_limiter
44
45
 
@@ -138,6 +138,22 @@ module DhanHQ
138
138
  /v2/instrument/
139
139
  ].freeze
140
140
 
141
+ # Mapping of exchange and segment combinations to canonical exchange segment names.
142
+ # Used for translating between CSV column values (EXCH_ID, SEGMENT) and API segment names.
143
+ # Key format: [exchange, segment] => exchange_segment
144
+ # Example: ["NSE", "E"] => "NSE_EQ"
145
+ SEGMENT_MAP = {
146
+ %w[NSE E] => "NSE_EQ",
147
+ %w[BSE E] => "BSE_EQ",
148
+ %w[NSE D] => "NSE_FNO",
149
+ %w[BSE D] => "BSE_FNO",
150
+ %w[NSE C] => "NSE_CURRENCY",
151
+ %w[BSE C] => "BSE_CURRENCY",
152
+ %w[MCX M] => "MCX_COMM",
153
+ %w[NSE I] => "IDX_I",
154
+ %w[BSE I] => "IDX_I"
155
+ }.freeze
156
+
141
157
  # Mapping of DhanHQ error codes to SDK error classes for consistent exception handling.
142
158
  DHAN_ERROR_MAPPING = {
143
159
  "DH-901" => DhanHQ::InvalidAuthenticationError,
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../contracts/instrument_list_contract"
4
+ require_relative "instrument_helpers"
4
5
 
5
6
  module DhanHQ
6
7
  module Models
7
8
  # Model wrapper for fetching instruments by exchange segment.
8
9
  class Instrument < BaseModel
9
- attributes :security_id, :symbol_name, :display_name, :exchange_segment, :instrument, :series,
10
+ include InstrumentHelpers
11
+
12
+ attributes :security_id, :symbol_name, :display_name, :exchange, :segment, :exchange_segment, :instrument, :series,
10
13
  :lot_size, :tick_size, :expiry_date, :strike_price, :option_type, :underlying_symbol,
11
14
  :isin, :instrument_type, :expiry_flag, :bracket_flag, :cover_flag, :asm_gsm_flag,
12
15
  :asm_gsm_category, :buy_sell_indicator, :buy_co_min_margin_per, :sell_co_min_margin_per,
@@ -112,11 +115,24 @@ module DhanHQ
112
115
  end
113
116
 
114
117
  def normalize_csv_row(row)
118
+ # Extract exchange and segment from CSV
119
+ exchange_id = row["EXCH_ID"] || row["EXCHANGE"]
120
+ segment_code = row["SEGMENT"]
121
+
122
+ # Calculate exchange_segment using SEGMENT_MAP from Constants
123
+ exchange_segment = if exchange_id && segment_code
124
+ DhanHQ::Constants::SEGMENT_MAP[[exchange_id, segment_code]]
125
+ else
126
+ row["EXCH_ID"] # Fallback to original value
127
+ end
128
+
115
129
  {
116
130
  security_id: row["SECURITY_ID"].to_s,
117
131
  symbol_name: row["SYMBOL_NAME"],
118
132
  display_name: row["DISPLAY_NAME"],
119
- exchange_segment: row["EXCH_ID"],
133
+ exchange: exchange_id,
134
+ segment: segment_code,
135
+ exchange_segment: exchange_segment,
120
136
  instrument: row["INSTRUMENT"],
121
137
  series: row["SERIES"],
122
138
  lot_size: row["LOT_SIZE"]&.to_f,
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Models
5
+ # Helper module providing instance methods for Instrument objects
6
+ # to access market feed, historical data, and option chain data.
7
+ module InstrumentHelpers
8
+ ##
9
+ # Fetches last traded price (LTP) for this instrument.
10
+ #
11
+ # @return [Float, nil] Last traded price value, or nil if not available
12
+ # @example
13
+ # instrument = DhanHQ::Models::Instrument.find("IDX_I", "NIFTY")
14
+ # instrument.ltp # => 26053.9
15
+ def ltp
16
+ params = build_market_feed_params
17
+ response = DhanHQ::Models::MarketFeed.ltp(params)
18
+
19
+ # Extract last_price from nested response structure
20
+ # Response format: {"data" => {"EXCHANGE_SEGMENT" => {"security_id" => {"last_price" => value}}}, "status" => "success"}
21
+ data = response[:data] || response["data"]
22
+ return nil unless data
23
+
24
+ segment_data = data[exchange_segment] || data[exchange_segment.to_sym]
25
+ return nil unless segment_data
26
+
27
+ security_data = segment_data[security_id] || segment_data[security_id.to_s]
28
+ return nil unless security_data
29
+
30
+ security_data[:last_price] || security_data["last_price"]
31
+ end
32
+
33
+ ##
34
+ # Fetches OHLC (Open-High-Low-Close) data for this instrument.
35
+ #
36
+ # @return [Hash] Market feed OHLC response
37
+ # @example
38
+ # instrument = DhanHQ::Models::Instrument.find("IDX_I", "NIFTY")
39
+ # instrument.ohlc
40
+ def ohlc
41
+ params = build_market_feed_params
42
+ DhanHQ::Models::MarketFeed.ohlc(params)
43
+ end
44
+
45
+ ##
46
+ # Fetches full quote depth and analytics for this instrument.
47
+ #
48
+ # @return [Hash] Market feed quote response
49
+ # @example
50
+ # instrument = DhanHQ::Models::Instrument.find("NSE_FNO", "RELIANCE")
51
+ # instrument.quote
52
+ def quote
53
+ params = build_market_feed_params
54
+ DhanHQ::Models::MarketFeed.quote(params)
55
+ end
56
+
57
+ ##
58
+ # Fetches daily historical data for this instrument.
59
+ #
60
+ # @param from_date [String] Start date in YYYY-MM-DD format
61
+ # @param to_date [String] End date in YYYY-MM-DD format
62
+ # @param options [Hash] Additional options (e.g., expiry_code)
63
+ # @return [Hash] Historical data with open, high, low, close, volume, timestamp arrays
64
+ # @example
65
+ # instrument = DhanHQ::Models::Instrument.find("NSE_EQ", "RELIANCE")
66
+ # instrument.daily(from_date: "2024-01-01", to_date: "2024-01-31")
67
+ def daily(from_date:, to_date:, **options)
68
+ params = build_historical_data_params(from_date: from_date, to_date: to_date, **options)
69
+ DhanHQ::Models::HistoricalData.daily(params)
70
+ end
71
+
72
+ ##
73
+ # Fetches intraday historical data for this instrument.
74
+ #
75
+ # @param from_date [String] Start date in YYYY-MM-DD format
76
+ # @param to_date [String] End date in YYYY-MM-DD format
77
+ # @param interval [String] Time interval in minutes (1, 5, 15, 25, 60)
78
+ # @param options [Hash] Additional options
79
+ # @return [Hash] Historical data with open, high, low, close, volume, timestamp arrays
80
+ # @example
81
+ # instrument = DhanHQ::Models::Instrument.find("IDX_I", "NIFTY")
82
+ # instrument.intraday(from_date: "2024-09-11", to_date: "2024-09-15", interval: "15")
83
+ def intraday(from_date:, to_date:, interval:, **options)
84
+ params = build_historical_data_params(from_date: from_date, to_date: to_date, interval: interval, **options)
85
+ DhanHQ::Models::HistoricalData.intraday(params)
86
+ end
87
+
88
+ ##
89
+ # Fetches the expiry list for this instrument (option chain).
90
+ #
91
+ # @return [Array<String>] List of expiry dates in YYYY-MM-DD format
92
+ # @example
93
+ # instrument = DhanHQ::Models::Instrument.find("NSE_FNO", "NIFTY")
94
+ # expiries = instrument.expiry_list
95
+ def expiry_list
96
+ params = {
97
+ underlying_scrip: security_id.to_i,
98
+ underlying_seg: underlying_segment_for_options
99
+ }
100
+ DhanHQ::Models::OptionChain.fetch_expiry_list(params)
101
+ end
102
+
103
+ ##
104
+ # Fetches the option chain for this instrument.
105
+ #
106
+ # @param expiry [String] Expiry date in YYYY-MM-DD format
107
+ # @return [Hash] Option chain data
108
+ # @example
109
+ # instrument = DhanHQ::Models::Instrument.find("NSE_FNO", "NIFTY")
110
+ # chain = instrument.option_chain(expiry: "2024-02-29")
111
+ def option_chain(expiry:)
112
+ params = {
113
+ underlying_scrip: security_id.to_i,
114
+ underlying_seg: underlying_segment_for_options,
115
+ expiry: expiry
116
+ }
117
+ DhanHQ::Models::OptionChain.fetch(params)
118
+ end
119
+
120
+ private
121
+
122
+ ##
123
+ # Builds market feed params from instrument attributes.
124
+ #
125
+ # @return [Hash] Market feed params in format { "EXCHANGE_SEGMENT": [security_id] }
126
+ def build_market_feed_params
127
+ { exchange_segment => [security_id.to_i] }
128
+ end
129
+
130
+ ##
131
+ # Builds historical data params from instrument attributes.
132
+ #
133
+ # @param from_date [String] Start date
134
+ # @param to_date [String] End date
135
+ # @param interval [String, nil] Time interval for intraday
136
+ # @param options [Hash] Additional options
137
+ # @return [Hash] Historical data params
138
+ def build_historical_data_params(from_date:, to_date:, interval: nil, **options)
139
+ params = {
140
+ security_id: security_id,
141
+ exchange_segment: exchange_segment,
142
+ instrument: instrument,
143
+ from_date: from_date,
144
+ to_date: to_date
145
+ }
146
+
147
+ params[:interval] = interval if interval
148
+ params.merge!(options) if options.any?
149
+
150
+ params
151
+ end
152
+
153
+ ##
154
+ # Determines the correct underlying segment for option chain APIs.
155
+ # Index uses IDX_I; stocks map to NSE_FNO or BSE_FNO by exchange.
156
+ def underlying_segment_for_options
157
+ seg = exchange_segment.to_s
158
+ ins = instrument.to_s
159
+
160
+ # If already a valid underlying seg, return as-is
161
+ return seg if %w[IDX_I NSE_FNO BSE_FNO MCX_FO].include?(seg)
162
+
163
+ # Index detection by instrument kind or segment
164
+ return "IDX_I" if ins == "INDEX" || seg == "IDX_I"
165
+
166
+ # Map equities/stock-related segments to respective FNO
167
+ return "NSE_FNO" if seg.start_with?("NSE")
168
+ return "BSE_FNO" if seg.start_with?("BSE")
169
+
170
+ # Fallback to IDX_I to avoid contract rejection
171
+ "IDX_I"
172
+ end
173
+ end
174
+ end
175
+ end
@@ -15,13 +15,31 @@ module DhanHQ
15
15
  per_day: Float::INFINITY }
16
16
  }.freeze
17
17
 
18
+ # Thread-safe shared rate limiters per API type
19
+ @shared_limiters = Concurrent::Map.new
20
+ @mutexes = Concurrent::Map.new
21
+
22
+ class << self
23
+ attr_reader :shared_limiters, :mutexes
24
+
25
+ # Get or create a shared rate limiter instance for the given API type
26
+ def for(api_type)
27
+ @shared_limiters[api_type] ||= new(api_type)
28
+ end
29
+ end
30
+
18
31
  # Creates a rate limiter for a given API type.
32
+ # Note: For proper rate limiting coordination, use RateLimiter.for(api_type) instead
33
+ # of RateLimiter.new(api_type) to get a shared instance.
19
34
  #
20
35
  # @param api_type [Symbol] One of the keys from {RATE_LIMITS}.
21
36
  def initialize(api_type)
22
37
  @api_type = api_type
23
38
  @buckets = Concurrent::Hash.new
24
39
  @buckets[:last_request_time] = Time.at(0) if api_type == :option_chain
40
+ # Track request timestamps for per-second limiting
41
+ @request_times = []
42
+ @window_start = Time.now
25
43
  initialize_buckets
26
44
  start_cleanup_threads
27
45
  end
@@ -30,34 +48,70 @@ module DhanHQ
30
48
  #
31
49
  # @return [void]
32
50
  def throttle!
33
- if @api_type == :option_chain
34
- last_request_time = @buckets[:last_request_time]
51
+ mutex.synchronize do
52
+ if @api_type == :option_chain
53
+ last_request_time = @buckets[:last_request_time]
35
54
 
36
- sleep_time = 3 - (Time.now - last_request_time)
37
- if sleep_time.positive?
38
- if ENV["DHAN_DEBUG"] == "true"
39
- puts "Sleeping for #{sleep_time.round(2)} seconds due to option_chain rate limit"
55
+ sleep_time = 3 - (Time.now - last_request_time)
56
+ if sleep_time.positive?
57
+ if ENV["DHAN_DEBUG"] == "true"
58
+ puts "Sleeping for #{sleep_time.round(2)} seconds due to option_chain rate limit"
59
+ end
60
+ sleep(sleep_time)
40
61
  end
41
- sleep(sleep_time)
62
+
63
+ @buckets[:last_request_time] = Time.now
64
+ return
42
65
  end
43
66
 
44
- @buckets[:last_request_time] = Time.now
45
- return
46
- end
67
+ # For per-second limits, use timestamp-based sliding window
68
+ per_second_limit = RATE_LIMITS[@api_type][:per_second]
69
+ if per_second_limit && per_second_limit != Float::INFINITY
70
+ now = Time.now
71
+ # Remove requests older than 1 second
72
+ @request_times.reject! { |t| now - t >= 1.0 }
73
+
74
+ # Check if we've hit the per-second limit
75
+ if @request_times.size >= per_second_limit
76
+ # Calculate how long to wait until the oldest request is 1 second old
77
+ oldest_time = @request_times.min
78
+ wait_time = 1.0 - (now - oldest_time)
79
+
80
+ if wait_time.positive?
81
+ sleep(wait_time)
82
+ # Recalculate after sleep
83
+ now = Time.now
84
+ @request_times.reject! { |t| now - t >= 1.0 }
85
+ end
86
+ end
47
87
 
48
- loop do
49
- break if allow_request?
88
+ # Record this request time
89
+ @request_times << Time.now
90
+ end
50
91
 
51
- sleep(0.1)
92
+ # Check other limits (per_minute, per_hour, per_day)
93
+ loop do
94
+ break if allow_request?
95
+
96
+ sleep(0.1)
97
+ end
98
+ record_request
52
99
  end
53
- record_request
54
100
  end
55
101
 
56
102
  private
57
103
 
104
+ # Gets or creates a mutex for this API type for thread-safe throttling
105
+ def mutex
106
+ self.class.mutexes[@api_type] ||= Mutex.new
107
+ end
108
+
58
109
  # Prepares the counters used for each interval in {RATE_LIMITS}.
59
110
  def initialize_buckets
60
111
  RATE_LIMITS[@api_type].each_key do |interval|
112
+ # Skip per_second as we handle it with timestamps
113
+ next if interval == :per_second
114
+
61
115
  @buckets[interval] = Concurrent::AtomicFixnum.new(0)
62
116
  end
63
117
  end
@@ -67,6 +121,9 @@ module DhanHQ
67
121
  # @return [Boolean]
68
122
  def allow_request?
69
123
  RATE_LIMITS[@api_type].all? do |interval, limit|
124
+ # Skip per_second check as it's handled in throttle! with timestamps
125
+ next true if interval == :per_second
126
+
70
127
  @buckets[interval].value < limit
71
128
  end
72
129
  end
@@ -74,34 +131,32 @@ module DhanHQ
74
131
  # Increments the counters for each time window once a request is made.
75
132
  def record_request
76
133
  RATE_LIMITS[@api_type].each_key do |interval|
134
+ # Skip per_second as it's handled with timestamps
135
+ next if interval == :per_second
136
+
77
137
  @buckets[interval].increment
78
138
  end
79
139
  end
80
140
 
81
141
  # Spawns background threads to reset counters after each interval elapses.
82
142
  def start_cleanup_threads
83
- Thread.new do
84
- loop do
85
- sleep(1)
86
- @buckets[:per_second].value = 0
87
- end
88
- end
143
+ # Don't create per_second cleanup thread - we handle it with timestamps
89
144
  Thread.new do
90
145
  loop do
91
146
  sleep(60)
92
- @buckets[:per_minute].value = 0
147
+ @buckets[:per_minute]&.value = 0
93
148
  end
94
149
  end
95
150
  Thread.new do
96
151
  loop do
97
152
  sleep(3600)
98
- @buckets[:per_hour].value = 0
153
+ @buckets[:per_hour]&.value = 0
99
154
  end
100
155
  end
101
156
  Thread.new do
102
157
  loop do
103
158
  sleep(86_400)
104
- @buckets[:per_day].value = 0
159
+ @buckets[:per_day]&.value = 0
105
160
  end
106
161
  end
107
162
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module DhanHQ
4
4
  # Semantic version of the DhanHQ client gem.
5
- VERSION = "2.1.6"
5
+ VERSION = "2.1.8"
6
6
  end
@@ -213,7 +213,9 @@ module DhanHQ
213
213
 
214
214
  instrument = find_instrument(symbol_code, segment_hint)
215
215
  unless instrument
216
- DhanHQ.logger&.warn("[DhanHQ::WS::MarketDepth] Unable to locate instrument for #{symbol_code} (segment hint: #{segment_hint || 'AUTO'})")
216
+ DhanHQ.logger&.warn(
217
+ "[DhanHQ::WS::MarketDepth] Unable to locate instrument for #{symbol_code} (segment hint: #{segment_hint || "AUTO"})"
218
+ )
217
219
  return nil
218
220
  end
219
221
 
@@ -13,9 +13,6 @@ module DhanHQ
13
13
  # Initialize Orders WebSocket connection
14
14
  # @param url [String] WebSocket endpoint URL
15
15
  # @param options [Hash] Connection options
16
- def initialize(url:, **options)
17
- super
18
- end
19
16
 
20
17
  private
21
18
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: DhanHQ
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.6
4
+ version: 2.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shubham Taywade
@@ -227,6 +227,7 @@ files:
227
227
  - lib/DhanHQ/models/historical_data.rb
228
228
  - lib/DhanHQ/models/holding.rb
229
229
  - lib/DhanHQ/models/instrument.rb
230
+ - lib/DhanHQ/models/instrument_helpers.rb
230
231
  - lib/DhanHQ/models/kill_switch.rb
231
232
  - lib/DhanHQ/models/ledger_entry.rb
232
233
  - lib/DhanHQ/models/margin.rb