DhanHQ 2.6.1 → 2.6.2
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 +4 -4
- data/CHANGELOG.md +24 -0
- data/Rakefile +3 -1
- data/docs/ENDPOINTS_AND_SANDBOX.md +103 -0
- data/lib/DhanHQ/client.rb +34 -21
- data/lib/DhanHQ/configuration.rb +44 -9
- data/lib/DhanHQ/constants.rb +5 -1
- data/lib/DhanHQ/core/base_api.rb +3 -2
- data/lib/DhanHQ/helpers/request_helper.rb +18 -5
- data/lib/DhanHQ/models/forever_order.rb +2 -1
- data/lib/DhanHQ/models/super_order.rb +2 -2
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/client.rb +2 -1
- data/lib/DhanHQ/ws/market_depth/client.rb +6 -5
- data/lib/dhan_hq.rb +21 -13
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4469f87602b1825aa53ac5d53ef3493fb3a6fd40b00fd58199f23ba3c4a3e129
|
|
4
|
+
data.tar.gz: 7b5c65ab06cf2f945f488af2ab63499eb9a5fe2cea6ec04579db12a227ac121e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 13683dc64df357c014a4427bbf3ee9981f3c52ad12795cd7f08d02e3d2cab3597efdb3668f63521f3bd2d43d616ac5ba577b11e104b304738c1b1c0769438bd2
|
|
7
|
+
data.tar.gz: cc2992611f9d3d4c785be89edc8bb3c33bffb6170a2575a20c532c8ff4ee9838bc1bf0021cdec6327349ff008fa7ae278b8e56d7cc2286310df3d0f4cd604292
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## [2.6.2] - 2026-03-07
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
|
|
5
|
+
- **Release from main** after merging add-sandbox-support. Includes sandbox REST base URL, `ensure_configuration!`, payload non-mutation, Rakefile/VCR fixes, and docs. Full feature list is under [2.6.0](#260---2026-03-06); 2.6.0 and 2.6.1 are already on RubyGems.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
1
9
|
## [2.6.1] - 2026-03-07
|
|
2
10
|
|
|
3
11
|
### Changed
|
|
@@ -8,6 +16,22 @@
|
|
|
8
16
|
|
|
9
17
|
## [2.6.0] - 2026-03-06
|
|
10
18
|
|
|
19
|
+
### Sandbox & configuration
|
|
20
|
+
|
|
21
|
+
- **Sandbox environment**: `DhanHQ.configuration.sandbox` (or `ENV["DHAN_SANDBOX"]=true`) switches REST base URL to `https://sandbox.dhan.co/v2`. Only `GET /v2/profile` and `GET /v2/fundlimit` are verified on sandbox; other REST endpoints are unverified. See `docs/ENDPOINTS_AND_SANDBOX.md`.
|
|
22
|
+
- **WebSocket**: Sandbox does **not** support WebSocket. Order updates, market feed, and market depth always use production URLs regardless of `sandbox`; no sandbox WS URLs are published.
|
|
23
|
+
- **Env-only bootstrap**: `DhanHQ.ensure_configuration!` ensures configuration exists (from ENV when nil). Called automatically in `Client#initialize` so apps using only `DHAN_CLIENT_ID` / `DHAN_ACCESS_TOKEN` work without calling `configure_with_env`.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **Payload mutation**: `prepare_payload` no longer mutates the caller's hash when injecting `dhanClientId` for DATA APIs; uses a duplicate so frozen or reused hashes are safe.
|
|
28
|
+
- **VCR**: Removed erroneous `/v2/v2/` market feed cassette entries (404 responses).
|
|
29
|
+
- **Rakefile**: Single RuboCop task; removed redundant `rubocop:fix` / `rubocop:fix_all` and deprecated `--auto-correct-all` flag.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **docs/ENDPOINTS_AND_SANDBOX.md**: Lists all REST/WebSocket endpoints, sandbox behavior, and endpoints verified vs not supported on sandbox.
|
|
34
|
+
|
|
11
35
|
### Fixed (API docs alignment)
|
|
12
36
|
|
|
13
37
|
- **Kill Switch**: Manage API now uses query parameter per [dhanhq.co/docs/v2/traders-control](https://dhanhq.co/docs/v2/traders-control/). `Resources::KillSwitch#update(status)` sends `POST /v2/killswitch?killSwitchStatus=ACTIVATE` (or `DEACTIVATE`) with no body. `Models::KillSwitch.update("ACTIVATE")` / `.activate` / `.deactivate` unchanged.
|
data/Rakefile
CHANGED
|
@@ -7,6 +7,8 @@ RSpec::Core::RakeTask.new(:spec)
|
|
|
7
7
|
|
|
8
8
|
require "rubocop/rake_task"
|
|
9
9
|
|
|
10
|
-
RuboCop
|
|
10
|
+
# Single RuboCop task; the gem also registers rubocop:autocorrect and rubocop:autocorrect_all.
|
|
11
|
+
desc "Run RuboCop"
|
|
12
|
+
RuboCop::RakeTask.new(:rubocop)
|
|
11
13
|
|
|
12
14
|
task default: %i[spec rubocop]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# DhanHQ gem — Endpoints and sandbox support
|
|
2
|
+
|
|
3
|
+
## Sandbox behavior
|
|
4
|
+
|
|
5
|
+
- **REST:** When `DhanHQ.configuration.sandbox == true` (or `ENV["DHAN_SANDBOX"]=true`), the client uses `https://sandbox.dhan.co/v2` as the base URL for **all** requests that go through `DhanHQ::Client`. Every wrapped REST endpoint listed below is therefore **sent** to the sandbox host when sandbox is enabled.
|
|
6
|
+
- **Sandbox does NOT support WebSocket.** Order updates, market feed, and market depth are **production-only**. The gem always uses production WebSocket URLs regardless of the `sandbox` setting. There are no sandbox WebSocket endpoints in the Dhan v2 API; do not rely on sandbox for real-time streams.
|
|
7
|
+
- **Auth endpoints** (`DhanHQ::Auth`) do **not** use sandbox. They always call:
|
|
8
|
+
- `https://auth.dhan.co` — token generation
|
|
9
|
+
- `https://api.dhan.co/v2` — token renewal
|
|
10
|
+
So token generation/renewal always hit production; only data/order REST calls follow the sandbox flag.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Sandbox: verified vs not working / unverified
|
|
15
|
+
|
|
16
|
+
| Status | Endpoints |
|
|
17
|
+
|-----------|-----------|
|
|
18
|
+
| **Verified on sandbox** (gem specs) | `GET /v2/profile`, `GET /v2/fundlimit` |
|
|
19
|
+
| **Sandbox connectivity spec** | `spec/dhan_hq/sandbox_connectivity_spec.rb` — uses VCR `record: :new_episodes`. In CI, use committed cassettes or skip without sandbox credentials; locally, run with `VCR_RECORD=new_episodes` and sandbox credentials to record. |
|
|
20
|
+
| **Not supported in sandbox** | All WebSocket endpoints (order updates, market feed, market depth). Use production only. |
|
|
21
|
+
| **Not verified on sandbox** | All other REST endpoints below. They may fail, return differently, or be unavailable in sandbox. Use Dhan documentation or manual testing before relying on them in sandbox. |
|
|
22
|
+
|
|
23
|
+
**REST endpoints not verified / may not work on sandbox** (only profile and funds are verified):
|
|
24
|
+
|
|
25
|
+
- `/v2/ledger`, `/v2/trades/{from}/{to}/{page}` (statements)
|
|
26
|
+
- `/v2/orders` (all order CRUD, slicing, external)
|
|
27
|
+
- `/v2/positions`, `/v2/positions/convert`
|
|
28
|
+
- `/v2/holdings`
|
|
29
|
+
- `/v2/trades`, `/v2/trades/{order_id}`
|
|
30
|
+
- `/v2/forever/orders` (all)
|
|
31
|
+
- `/v2/super/orders` (all)
|
|
32
|
+
- `/v2/killswitch`
|
|
33
|
+
- `/trader-control`
|
|
34
|
+
- `/ip/getIP`, `/ip/setIP`, `/ip/modifyIP`
|
|
35
|
+
- `/edis/tpin`, `/edis/form`, `/edis/bulkform`, `/edis/inquire/{isin}`
|
|
36
|
+
- `/alerts/orders`
|
|
37
|
+
- `/v2/pnlExit`
|
|
38
|
+
- `/v2/margincalculator`, `/v2/margincalculator/multi`
|
|
39
|
+
- `/v2/instrument/{segment}`
|
|
40
|
+
- `/v2/marketfeed/ltp`, `/v2/marketfeed/ohlc`, `/v2/marketfeed/quote`
|
|
41
|
+
- `/v2/optionchain`, `/v2/optionchain/expirylist`
|
|
42
|
+
- `/v2/charts/historical`, `/v2/charts/intraday`, `/v2/charts/rollingoption`
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## REST endpoints integrated in the gem
|
|
47
|
+
|
|
48
|
+
Paths are as built by the gem (HTTP_PATH + endpoint). Base URL is either `https://api.dhan.co/v2` (production) or `https://sandbox.dhan.co/v2` (sandbox).
|
|
49
|
+
|
|
50
|
+
| Resource / model | Path(s) | Methods | API type |
|
|
51
|
+
|---------------------------|----------------------------------------------|-----------|-----------------|
|
|
52
|
+
| **Profile** | `/v2/profile` | GET | non_trading_api |
|
|
53
|
+
| **Funds** | `/v2/fundlimit` | GET | non_trading_api |
|
|
54
|
+
| **Statements** | `/v2/ledger`, `/v2/trades/{from}/{to}/{page}`| GET | non_trading_api |
|
|
55
|
+
| **Orders** | `/v2/orders`, `/v2/orders/{id}`, `/v2/orders/external/{correlation_id}`, `/v2/orders/slicing` | GET, POST, PUT, DELETE | order_api |
|
|
56
|
+
| **Positions** | `/v2/positions`, `/v2/positions/convert` | GET, POST, DELETE | order_api |
|
|
57
|
+
| **Holdings** | `/v2/holdings` | GET | order_api |
|
|
58
|
+
| **Trades** | `/v2/trades`, `/v2/trades/{order_id}` | GET | order_api |
|
|
59
|
+
| **Forever orders** | `/v2/forever/orders`, `/v2/forever/orders/{id}` | GET, POST, PUT, DELETE | order_api |
|
|
60
|
+
| **Super orders** | `/v2/super/orders`, `/v2/super/orders/{id}`, leg delete | GET, POST, PUT, DELETE | order_api |
|
|
61
|
+
| **Kill switch** | `/v2/killswitch` | GET, POST | order_api |
|
|
62
|
+
| **Trader control** | `/trader-control` | GET, POST | order_api |
|
|
63
|
+
| **IP setup** | `/ip/getIP`, `/ip/setIP`, `/ip/modifyIP` | GET, POST, PUT | order_api |
|
|
64
|
+
| **EDIS** | `/edis/tpin`, `/edis/form`, `/edis/bulkform`, `/edis/inquire/{isin}` | GET, POST | order_api |
|
|
65
|
+
| **Alert orders** | `/alerts/orders` | GET, POST, PUT, DELETE | order_api |
|
|
66
|
+
| **PnL exit** | `/v2/pnlExit` | GET, POST, DELETE | order_api |
|
|
67
|
+
| **Margin calculator** | `/v2/margincalculator`, `/v2/margincalculator/multi` | POST | order_api |
|
|
68
|
+
| **Instruments** | `/v2/instrument/{segment}` (redirect to CSV) | GET | data_api |
|
|
69
|
+
| **Market feed** | `/v2/marketfeed/ltp`, `/v2/marketfeed/ohlc`, `/v2/marketfeed/quote` | POST | data_api / quote_api |
|
|
70
|
+
| **Option chain** | `/v2/optionchain`, `/v2/optionchain/expirylist` | POST | data_api |
|
|
71
|
+
| **Historical data** | `/v2/charts/historical`, `/v2/charts/intraday` | POST | data_api |
|
|
72
|
+
| **Expired options data** | `/v2/charts/rollingoption` | POST | data_api |
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Auth (outside main client base URL)
|
|
77
|
+
|
|
78
|
+
| Purpose | URL / path | Method |
|
|
79
|
+
|-------------------|--------------------------------|--------|
|
|
80
|
+
| Generate token | `https://auth.dhan.co/app/generateAccessToken` | POST |
|
|
81
|
+
| Renew token | `https://api.dhan.co/v2/RenewToken` | POST |
|
|
82
|
+
|
|
83
|
+
These are **not** sandbox-aware; they always use the URLs above.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## WebSocket endpoints (production only; sandbox not supported)
|
|
88
|
+
|
|
89
|
+
| Purpose | URL |
|
|
90
|
+
|----------------|-----|
|
|
91
|
+
| Order updates | `wss://api-order-update.dhan.co` |
|
|
92
|
+
| Market feed | `wss://api-feed.dhan.co` |
|
|
93
|
+
| Market depth | `wss://depth-api-feed.dhan.co/twentydepth` |
|
|
94
|
+
|
|
95
|
+
**Sandbox:** Dhan sandbox does **not** provide WebSocket services. These endpoints are production-only. The gem never switches WS URLs based on `sandbox`; you can still override via `DhanHQ.configuration.ws_order_url`, `ws_market_feed_url`, `ws_market_depth_url`, or env vars `DHAN_WS_ORDER_URL`, `DHAN_WS_MARKET_FEED_URL`, `DHAN_WS_MARKET_DEPTH_URL` if you have a different production URL.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Summary
|
|
100
|
+
|
|
101
|
+
- **REST:** When `sandbox` is true, all REST calls go to `https://sandbox.dhan.co/v2`. Only `GET /v2/profile` and `GET /v2/fundlimit` are verified working on sandbox; other REST endpoints are not verified — see "Sandbox: verified vs not working / unverified" above.
|
|
102
|
+
- **Auth:** Token generation and renewal always use production hosts.
|
|
103
|
+
- **WebSockets:** Sandbox does **not** support WebSocket. Order updates, market feed, and market depth always use production URLs; the gem does not publish or use any sandbox WebSocket URLs.
|
data/lib/DhanHQ/client.rb
CHANGED
|
@@ -40,29 +40,15 @@ module DhanHQ
|
|
|
40
40
|
# @return [DhanHQ::Client] A new client instance.
|
|
41
41
|
# @raise [DhanHQ::Error] If configuration is invalid or rate limiter initialization fails
|
|
42
42
|
def initialize(api_type:)
|
|
43
|
-
|
|
44
|
-
# Validation happens at request time in build_headers, not here
|
|
45
|
-
DhanHQ.configure_with_env if ENV.fetch("DHAN_CLIENT_ID", nil)
|
|
46
|
-
|
|
43
|
+
DhanHQ.ensure_configuration!
|
|
47
44
|
# Use shared rate limiter instance per API type to ensure proper coordination
|
|
48
45
|
@rate_limiter = RateLimiter.for(api_type)
|
|
49
46
|
|
|
50
47
|
raise DhanHQ::Error, "RateLimiter initialization failed" unless @rate_limiter
|
|
51
48
|
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
write_timeout = ENV.fetch("DHAN_WRITE_TIMEOUT", 30).to_i
|
|
56
|
-
|
|
57
|
-
@connection = Faraday.new(url: DhanHQ.configuration.base_url) do |conn|
|
|
58
|
-
conn.request :json, parser_options: { symbolize_names: true }
|
|
59
|
-
conn.response :json, content_type: /\bjson$/
|
|
60
|
-
conn.response :logger if ENV["DHAN_DEBUG"] == "true"
|
|
61
|
-
conn.options.timeout = read_timeout
|
|
62
|
-
conn.options.open_timeout = connect_timeout
|
|
63
|
-
conn.options.write_timeout = write_timeout
|
|
64
|
-
conn.adapter Faraday.default_adapter
|
|
65
|
-
end
|
|
49
|
+
# Store initial URL to detect changes
|
|
50
|
+
@last_base_url = DhanHQ.configuration.base_url
|
|
51
|
+
@connection = build_connection(@last_base_url)
|
|
66
52
|
end
|
|
67
53
|
|
|
68
54
|
# Sends an HTTP request to the API with automatic retry for transient errors.
|
|
@@ -77,13 +63,15 @@ module DhanHQ
|
|
|
77
63
|
@token_manager&.ensure_valid_token!
|
|
78
64
|
@rate_limiter.throttle! # **Ensure we don't hit rate limit before calling API**
|
|
79
65
|
|
|
66
|
+
# Ensure connection matches current configuration (e.g. sandbox toggle)
|
|
67
|
+
refresh_connection!
|
|
68
|
+
|
|
80
69
|
attempt = 0
|
|
81
70
|
auth_retry_done = false
|
|
82
71
|
begin
|
|
83
|
-
response = connection.send(method) do |req|
|
|
84
|
-
req.url path
|
|
72
|
+
response = connection.send(method, path) do |req|
|
|
85
73
|
req.headers.merge!(build_headers(path))
|
|
86
|
-
prepare_payload(req, payload, method)
|
|
74
|
+
prepare_payload(req, payload, method, path)
|
|
87
75
|
end
|
|
88
76
|
|
|
89
77
|
handle_response(response)
|
|
@@ -201,6 +189,31 @@ module DhanHQ
|
|
|
201
189
|
|
|
202
190
|
private
|
|
203
191
|
|
|
192
|
+
def refresh_connection!
|
|
193
|
+
current_url = DhanHQ.configuration.base_url
|
|
194
|
+
return if @last_base_url == current_url
|
|
195
|
+
|
|
196
|
+
@last_base_url = current_url
|
|
197
|
+
@connection = build_connection(current_url)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def build_connection(url)
|
|
201
|
+
# Get timeout values from configuration or environment, with sensible defaults
|
|
202
|
+
connect_timeout = ENV.fetch("DHAN_CONNECT_TIMEOUT", 10).to_i
|
|
203
|
+
read_timeout = ENV.fetch("DHAN_READ_TIMEOUT", 30).to_i
|
|
204
|
+
write_timeout = ENV.fetch("DHAN_WRITE_TIMEOUT", 30).to_i
|
|
205
|
+
|
|
206
|
+
Faraday.new(url: url) do |conn|
|
|
207
|
+
conn.request :json, parser_options: { symbolize_names: true }
|
|
208
|
+
conn.response :json, content_type: /\bjson$/
|
|
209
|
+
conn.response :logger if ENV["DHAN_DEBUG"] == "true"
|
|
210
|
+
conn.options.timeout = read_timeout
|
|
211
|
+
conn.options.open_timeout = connect_timeout
|
|
212
|
+
conn.options.write_timeout = write_timeout
|
|
213
|
+
conn.adapter Faraday.default_adapter
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
204
217
|
# Calculates exponential backoff time
|
|
205
218
|
#
|
|
206
219
|
# @param attempt [Integer] Current attempt number (1-based)
|
data/lib/DhanHQ/configuration.rb
CHANGED
|
@@ -12,6 +12,10 @@ module DhanHQ
|
|
|
12
12
|
#
|
|
13
13
|
# @return [String]
|
|
14
14
|
BASE_URL = "https://api.dhan.co/v2"
|
|
15
|
+
|
|
16
|
+
# Default Sandbox API host.
|
|
17
|
+
# @return [String]
|
|
18
|
+
SANDBOX_URL = "https://sandbox.dhan.co/v2"
|
|
15
19
|
# The client ID for API authentication.
|
|
16
20
|
# @return [String, nil] The client ID or `nil` if not set.
|
|
17
21
|
attr_accessor :client_id
|
|
@@ -32,8 +36,11 @@ module DhanHQ
|
|
|
32
36
|
attr_accessor :on_token_expired
|
|
33
37
|
|
|
34
38
|
# The base URL for API requests.
|
|
35
|
-
# @return [String]
|
|
36
|
-
|
|
39
|
+
# @return [String]
|
|
40
|
+
attr_writer :base_url
|
|
41
|
+
|
|
42
|
+
# Whether to use the sandbox environment.
|
|
43
|
+
attr_accessor :sandbox
|
|
37
44
|
|
|
38
45
|
# URL for the compact CSV format of instruments.
|
|
39
46
|
# @return [String] URL for compact CSV.
|
|
@@ -48,21 +55,33 @@ module DhanHQ
|
|
|
48
55
|
attr_accessor :ws_version
|
|
49
56
|
|
|
50
57
|
# Websocket order updates endpoint.
|
|
58
|
+
# Sandbox does not support WebSocket; always returns production URL unless overridden.
|
|
51
59
|
# @return [String]
|
|
52
|
-
|
|
60
|
+
def ws_order_url
|
|
61
|
+
@ws_order_url || "wss://api-order-update.dhan.co"
|
|
62
|
+
end
|
|
53
63
|
|
|
54
64
|
# Websocket market feed endpoint.
|
|
65
|
+
# Sandbox does not support WebSocket; always returns production URL unless overridden.
|
|
55
66
|
# @return [String]
|
|
56
|
-
|
|
67
|
+
def ws_market_feed_url
|
|
68
|
+
@ws_market_feed_url || "wss://api-feed.dhan.co"
|
|
69
|
+
end
|
|
57
70
|
|
|
58
71
|
# Websocket market depth endpoint.
|
|
72
|
+
# Sandbox does not support WebSocket; always returns production URL unless overridden.
|
|
59
73
|
# @return [String]
|
|
60
|
-
|
|
74
|
+
def ws_market_depth_url
|
|
75
|
+
@ws_market_depth_url || "wss://depth-api-feed.dhan.co/twentydepth"
|
|
76
|
+
end
|
|
61
77
|
|
|
62
78
|
# Market depth level (20 or 200).
|
|
63
79
|
# @return [Integer]
|
|
64
80
|
attr_accessor :market_depth_level
|
|
65
81
|
|
|
82
|
+
# Setters for websocket URLs
|
|
83
|
+
attr_writer :ws_order_url, :ws_market_feed_url, :ws_market_depth_url
|
|
84
|
+
|
|
66
85
|
# Websocket user type for order updates.
|
|
67
86
|
# @return [String] "SELF" or "PARTNER".
|
|
68
87
|
attr_accessor :ws_user_type
|
|
@@ -91,6 +110,21 @@ module DhanHQ
|
|
|
91
110
|
end
|
|
92
111
|
end
|
|
93
112
|
|
|
113
|
+
# Returns the base URL to use. If {#sandbox} is true and {#base_url}
|
|
114
|
+
# is nil or the default production URL, returns {SANDBOX_URL}.
|
|
115
|
+
# @return [String]
|
|
116
|
+
def base_url
|
|
117
|
+
if sandbox? && (@base_url.nil? || @base_url == BASE_URL)
|
|
118
|
+
SANDBOX_URL
|
|
119
|
+
else
|
|
120
|
+
@base_url || BASE_URL
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def sandbox?
|
|
125
|
+
@sandbox == true
|
|
126
|
+
end
|
|
127
|
+
|
|
94
128
|
# Initializes a new configuration instance with default values.
|
|
95
129
|
#
|
|
96
130
|
# @example
|
|
@@ -100,11 +134,12 @@ module DhanHQ
|
|
|
100
134
|
def initialize
|
|
101
135
|
@client_id = ENV.fetch("DHAN_CLIENT_ID", nil)
|
|
102
136
|
@access_token = ENV.fetch("DHAN_ACCESS_TOKEN", nil)
|
|
103
|
-
@
|
|
137
|
+
@sandbox = ENV.fetch("DHAN_SANDBOX", "false").to_s.casecmp("true").zero?
|
|
138
|
+
@base_url = ENV.fetch("DHAN_BASE_URL", nil)
|
|
104
139
|
@ws_version = ENV.fetch("DHAN_WS_VERSION", 2).to_i
|
|
105
|
-
@ws_order_url = ENV.fetch("DHAN_WS_ORDER_URL",
|
|
106
|
-
@ws_market_feed_url = ENV.fetch("DHAN_WS_MARKET_FEED_URL",
|
|
107
|
-
@ws_market_depth_url = ENV.fetch("DHAN_WS_MARKET_DEPTH_URL",
|
|
140
|
+
@ws_order_url = ENV.fetch("DHAN_WS_ORDER_URL", nil)
|
|
141
|
+
@ws_market_feed_url = ENV.fetch("DHAN_WS_MARKET_FEED_URL", nil)
|
|
142
|
+
@ws_market_depth_url = ENV.fetch("DHAN_WS_MARKET_DEPTH_URL", nil)
|
|
108
143
|
@market_depth_level = ENV.fetch("DHAN_MARKET_DEPTH_LEVEL", "20").to_i
|
|
109
144
|
@ws_user_type = ENV.fetch("DHAN_WS_USER_TYPE", "SELF")
|
|
110
145
|
@partner_id = ENV.fetch("DHAN_PARTNER_ID", nil)
|
data/lib/DhanHQ/constants.rb
CHANGED
|
@@ -417,7 +417,11 @@ module DhanHQ
|
|
|
417
417
|
DATA_API_PREFIXES = [
|
|
418
418
|
"/v2/marketfeed/",
|
|
419
419
|
"/v2/optionchain",
|
|
420
|
-
"/v2/instrument/"
|
|
420
|
+
"/v2/instrument/",
|
|
421
|
+
"/v2/charts",
|
|
422
|
+
"/v2/margincalculator",
|
|
423
|
+
"/v2/profile",
|
|
424
|
+
"/v2/fundlimit"
|
|
421
425
|
].freeze
|
|
422
426
|
|
|
423
427
|
# Mapping of exchange and segment combinations to canonical exchange segment names.
|
data/lib/DhanHQ/core/base_api.rb
CHANGED
|
@@ -88,9 +88,10 @@ module DhanHQ
|
|
|
88
88
|
|
|
89
89
|
# Format parameters based on API endpoint
|
|
90
90
|
def format_params(endpoint, params)
|
|
91
|
-
|
|
91
|
+
full_path = build_path(endpoint)
|
|
92
|
+
return params if marketfeed_api?(full_path) || params.empty?
|
|
92
93
|
|
|
93
|
-
optionchain_api?(
|
|
94
|
+
optionchain_api?(full_path) ? titleize_keys(params) : camelize_keys(params)
|
|
94
95
|
end
|
|
95
96
|
|
|
96
97
|
# Determines if the API endpoint is for Option Chain
|
|
@@ -39,7 +39,7 @@ module DhanHQ
|
|
|
39
39
|
"access-token" => token
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
# Add client-id for DATA APIs
|
|
42
|
+
# Add client-id for DATA APIs (now including sandbox profile/funds)
|
|
43
43
|
if data_api?(path)
|
|
44
44
|
client_id = DhanHQ.configuration&.client_id
|
|
45
45
|
unless client_id
|
|
@@ -70,18 +70,31 @@ module DhanHQ
|
|
|
70
70
|
# @param req [Faraday::Request] The request object.
|
|
71
71
|
# @param payload [Hash] The request payload.
|
|
72
72
|
# @param method [Symbol] The HTTP method.
|
|
73
|
-
def prepare_payload(req, payload, method)
|
|
74
|
-
return if payload.nil? || payload.empty?
|
|
73
|
+
def prepare_payload(req, payload, method, path = nil)
|
|
74
|
+
return if payload.nil? || (payload.empty? && (path.nil? || !data_api?(path)))
|
|
75
75
|
|
|
76
76
|
unless payload.is_a?(Hash)
|
|
77
77
|
raise DhanHQ::InputExceptionError,
|
|
78
78
|
"Invalid payload: Expected a Hash, got #{payload.class}"
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
+
out = payload
|
|
82
|
+
if path && data_api?(path) && %i[post put patch].include?(method)
|
|
83
|
+
client_id = DhanHQ.configuration&.client_id
|
|
84
|
+
if client_id && !payload.key?(:dhanClientId) && !payload.key?("dhanClientId")
|
|
85
|
+
out = payload.dup
|
|
86
|
+
if out.keys.any?(String)
|
|
87
|
+
out["dhanClientId"] = client_id
|
|
88
|
+
else
|
|
89
|
+
out[:dhanClientId] = client_id
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
81
94
|
case method
|
|
82
95
|
when :delete then req.params = {}
|
|
83
|
-
when :get then req.params =
|
|
84
|
-
else req.body =
|
|
96
|
+
when :get then req.params = out
|
|
97
|
+
else req.body = out.to_json
|
|
85
98
|
end
|
|
86
99
|
end
|
|
87
100
|
end
|
|
@@ -218,7 +218,8 @@ module DhanHQ
|
|
|
218
218
|
def create(params)
|
|
219
219
|
# Normalize params and auto-inject dhan_client_id from configuration if not provided
|
|
220
220
|
normalized_params = snake_case(params)
|
|
221
|
-
|
|
221
|
+
config = DhanHQ.configuration
|
|
222
|
+
normalized_params[:dhan_client_id] ||= config.client_id if config&.client_id
|
|
222
223
|
response = resource.create(normalized_params)
|
|
223
224
|
return nil unless response.is_a?(Hash) && response["orderId"]
|
|
224
225
|
|
|
@@ -207,9 +207,9 @@ module DhanHQ
|
|
|
207
207
|
# )
|
|
208
208
|
#
|
|
209
209
|
def create(params)
|
|
210
|
-
# Normalize params and auto-inject dhan_client_id from configuration if not provided
|
|
211
210
|
normalized_params = snake_case(params)
|
|
212
|
-
|
|
211
|
+
config = DhanHQ.configuration
|
|
212
|
+
normalized_params[:dhan_client_id] ||= config.client_id if config&.client_id
|
|
213
213
|
response = resource.create(normalized_params)
|
|
214
214
|
return nil unless response.is_a?(Hash) && response["orderId"]
|
|
215
215
|
|
data/lib/DhanHQ/version.rb
CHANGED
data/lib/DhanHQ/ws/client.rb
CHANGED
|
@@ -32,7 +32,8 @@ module DhanHQ
|
|
|
32
32
|
|
|
33
33
|
cid = DhanHQ.configuration.client_id or raise "DhanHQ.client_id not set"
|
|
34
34
|
ver = (DhanHQ.configuration.respond_to?(:ws_version) && DhanHQ.configuration.ws_version) || 2
|
|
35
|
-
|
|
35
|
+
base = url || DhanHQ.configuration.ws_market_feed_url
|
|
36
|
+
@url = base.include?("?") ? base : "#{base}?version=#{ver}&token=#{token}&clientId=#{cid}&authType=2"
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
# Starts the WebSocket connection and event loop.
|
|
@@ -86,11 +86,12 @@ module DhanHQ
|
|
|
86
86
|
cid = config.client_id or raise "DhanHQ.client_id not set"
|
|
87
87
|
depth_level = config.market_depth_level || 20 # Default to 20 level depth
|
|
88
88
|
|
|
89
|
-
if depth_level == 200
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
base = if depth_level == 200
|
|
90
|
+
"wss://full-depth-api.dhan.co/twohundreddepth"
|
|
91
|
+
else
|
|
92
|
+
config.ws_market_depth_url
|
|
93
|
+
end
|
|
94
|
+
base.include?("?") ? base : "#{base}?token=#{token}&clientId=#{cid}&authType=2"
|
|
94
95
|
end
|
|
95
96
|
|
|
96
97
|
##
|
data/lib/dhan_hq.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
require "logger"
|
|
5
5
|
require "zeitwerk"
|
|
6
6
|
require "dotenv/load"
|
|
7
|
+
require "faraday"
|
|
7
8
|
# Minimal eager requires for backward-compatible constants.
|
|
8
9
|
# These are widely referenced (e.g. `DhanHQ::BaseAPI`) and should not depend on
|
|
9
10
|
# the autoloader being fully configured.
|
|
@@ -12,6 +13,7 @@ require_relative "DhanHQ/helpers/attribute_helper"
|
|
|
12
13
|
require_relative "DhanHQ/helpers/validation_helper"
|
|
13
14
|
require_relative "DhanHQ/helpers/request_helper"
|
|
14
15
|
require_relative "DhanHQ/helpers/response_helper"
|
|
16
|
+
require_relative "DhanHQ/errors"
|
|
15
17
|
require_relative "DhanHQ/core/base_api"
|
|
16
18
|
require_relative "DhanHQ/core/base_model"
|
|
17
19
|
require_relative "DhanHQ/core/base_resource"
|
|
@@ -89,15 +91,21 @@ module DhanHQ
|
|
|
89
91
|
#
|
|
90
92
|
# @return [void]
|
|
91
93
|
def configure_with_env
|
|
94
|
+
self.configuration = Configuration.new
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Ensures configuration exists, bootstrapping from ENV when nil.
|
|
98
|
+
# Called automatically when building a Client so env-only integrations work without
|
|
99
|
+
# an explicit configure/configure_with_env call. Idempotent when configuration is already set.
|
|
100
|
+
#
|
|
101
|
+
# @return [DhanHQ::Configuration]
|
|
102
|
+
def ensure_configuration!
|
|
92
103
|
self.configuration ||= Configuration.new
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
configuration
|
|
98
|
-
configuration.ws_user_type = ENV.fetch("DHAN_WS_USER_TYPE", configuration.ws_user_type)
|
|
99
|
-
configuration.partner_id = ENV.fetch("DHAN_PARTNER_ID", configuration.partner_id)
|
|
100
|
-
configuration.partner_secret = ENV.fetch("DHAN_PARTNER_SECRET", configuration.partner_secret)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Resets the configuration to nil.
|
|
107
|
+
def reset_configuration!
|
|
108
|
+
self.configuration = nil
|
|
101
109
|
end
|
|
102
110
|
|
|
103
111
|
# Configures the DhanHQ client by fetching credentials from a token endpoint.
|
|
@@ -120,12 +128,12 @@ module DhanHQ
|
|
|
120
128
|
base_url ||= ENV.fetch("DHAN_TOKEN_ENDPOINT_BASE_URL", nil)
|
|
121
129
|
bearer_token ||= ENV.fetch("DHAN_TOKEN_ENDPOINT_BEARER", nil)
|
|
122
130
|
|
|
123
|
-
raise TokenEndpointError, "base_url and bearer_token (or ENV DHAN_TOKEN_ENDPOINT_*) are required" if base_url.to_s.empty? || bearer_token.to_s.empty?
|
|
131
|
+
raise DhanHQ::TokenEndpointError, "base_url and bearer_token (or ENV DHAN_TOKEN_ENDPOINT_*) are required" if base_url.to_s.empty? || bearer_token.to_s.empty?
|
|
124
132
|
|
|
125
133
|
url = "#{base_url.to_s.chomp("/")}/auth/dhan/token"
|
|
126
|
-
conn = Faraday.new(url: url) do |c|
|
|
134
|
+
conn = ::Faraday.new(url: url) do |c|
|
|
127
135
|
c.response :json, content_type: /\bjson$/
|
|
128
|
-
c.adapter Faraday.default_adapter
|
|
136
|
+
c.adapter ::Faraday.default_adapter
|
|
129
137
|
end
|
|
130
138
|
|
|
131
139
|
response = conn.get("") do |req|
|
|
@@ -144,7 +152,7 @@ module DhanHQ
|
|
|
144
152
|
end
|
|
145
153
|
end
|
|
146
154
|
msg = body["error"] || body["message"] || body["errorMessage"] || response.body.to_s
|
|
147
|
-
raise TokenEndpointError, "Token endpoint returned #{response.status}: #{msg}"
|
|
155
|
+
raise DhanHQ::TokenEndpointError, "Token endpoint returned #{response.status}: #{msg}"
|
|
148
156
|
end
|
|
149
157
|
|
|
150
158
|
data = if response.body.is_a?(Hash)
|
|
@@ -160,7 +168,7 @@ module DhanHQ
|
|
|
160
168
|
|
|
161
169
|
access_token = data["access_token"] || data[:access_token]
|
|
162
170
|
client_id = data["client_id"] || data[:client_id]
|
|
163
|
-
raise TokenEndpointError, "Token endpoint response missing access_token or client_id" if access_token.to_s.empty? || client_id.to_s.empty?
|
|
171
|
+
raise DhanHQ::TokenEndpointError, "Token endpoint response missing access_token or client_id" if access_token.to_s.empty? || client_id.to_s.empty?
|
|
164
172
|
|
|
165
173
|
self.configuration ||= Configuration.new
|
|
166
174
|
configuration.access_token = access_token.to_s
|
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.6.
|
|
4
|
+
version: 2.6.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shubham Taywade
|
|
@@ -214,6 +214,7 @@ files:
|
|
|
214
214
|
- docs/CONFIGURATION.md
|
|
215
215
|
- docs/CONSTANTS_REFERENCE.md
|
|
216
216
|
- docs/DATA_API_PARAMETERS.md
|
|
217
|
+
- docs/ENDPOINTS_AND_SANDBOX.md
|
|
217
218
|
- docs/LIVE_ORDER_UPDATES.md
|
|
218
219
|
- docs/RAILS_INTEGRATION.md
|
|
219
220
|
- docs/RAILS_WEBSOCKET_INTEGRATION.md
|