DhanHQ 3.4.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +61 -1
- data/GUIDE.md +40 -0
- data/README.md +103 -0
- data/docs/CONFIGURATION.md +29 -2
- data/docs/ENDPOINTS_AND_SANDBOX.md +83 -38
- data/lib/DhanHQ/client.rb +101 -26
- data/lib/DhanHQ/concerns/order_audit.rb +26 -3
- data/lib/DhanHQ/configuration.rb +178 -27
- data/lib/DhanHQ/constants.rb +60 -1
- data/lib/DhanHQ/contracts/company_info_contract.rb +24 -0
- data/lib/DhanHQ/contracts/market_movers_contract.rb +57 -0
- data/lib/DhanHQ/contracts/news_headline_contract.rb +21 -0
- data/lib/DhanHQ/contracts/technical_data_contract.rb +25 -0
- data/lib/DhanHQ/core/base_api.rb +3 -17
- data/lib/DhanHQ/core/base_model.rb +32 -12
- data/lib/DhanHQ/dry_run/simulator.rb +3 -2
- data/lib/DhanHQ/helpers/attribute_helper.rb +0 -22
- data/lib/DhanHQ/helpers/request_helper.rb +13 -3
- data/lib/DhanHQ/models/option_chain.rb +7 -0
- data/lib/DhanHQ/models/order.rb +7 -7
- data/lib/DhanHQ/rate_limiter.rb +34 -40
- data/lib/DhanHQ/resources/global_stocks/funds.rb +1 -1
- data/lib/DhanHQ/resources/global_stocks/holdings.rb +1 -1
- data/lib/DhanHQ/resources/global_stocks/margin_calculator.rb +1 -1
- data/lib/DhanHQ/resources/global_stocks/market_status.rb +1 -1
- data/lib/DhanHQ/resources/global_stocks/orders.rb +1 -1
- data/lib/DhanHQ/resources/global_stocks/trades.rb +1 -1
- data/lib/DhanHQ/resources/market_feed.rb +1 -1
- data/lib/DhanHQ/resources/scanx.rb +98 -0
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/base_connection.rb +2 -1
- data/lib/DhanHQ/ws/client.rb +10 -13
- data/lib/DhanHQ/ws/connection.rb +13 -15
- data/lib/DhanHQ/ws/market_depth/client.rb +9 -4
- data/lib/DhanHQ/ws/orders/client.rb +4 -4
- data/lib/DhanHQ/ws/orders/connection.rb +1 -1
- data/lib/DhanHQ/ws/registry.rb +10 -6
- data/lib/dhan_hq.rb +81 -48
- metadata +27 -22
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d79f2577f59f6c04874eb44e4d318e5584e711e205be25f6fa000053ec772846
|
|
4
|
+
data.tar.gz: dca8144795ab2b77700a3947512c04e73c8edcbaa287b74adb746e671158e583
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e5a38d2663377da284188dc32fd0e796394ee899a34b61d5f82b5a487b0a47477a9e6bc1c0fdfc0b7fa93959d8673e257a83b7eea1af1a48885526ecc506a89f
|
|
7
|
+
data.tar.gz: 3f2023c47fc5e5b1f6fcce29f704c2c4ac03ad887a91dbc0c33bba201a151e8f03793bdc736915a295e98a5e661810e6229e32b37ea11c09b977fbcbb1b1c5d3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,61 @@
|
|
|
1
|
+
## [4.1.0] - 2026-10-10
|
|
2
|
+
|
|
3
|
+
### Fixed (full-gem review pass — `ruby-agent-skills` audit)
|
|
4
|
+
|
|
5
|
+
- **WebSocket feed request codes now match the official Annexure table** (`dhanhq.co/docs/v2/annexure#feed-request-code`). Market feed subscribe per mode: 15/17/21 (was 15 for all three); unsubscribe: 16/18/22 (was 12 for all — 12 is the whole-connection *Disconnect Feed* frame, so per-instrument unsubscribes most plausibly tore down the entire socket). Market depth unsubscribe: 24 (was 12; subscribe stays 23). New spec `spec/dhan_hq/ws/connection_spec.rb` asserts the mapping. **Caveat:** verified against the published table only — `wss://api-feed.dhan.co` is not reachable from the CI environment (geo-block outside India), so a one-constant revert is documented in the review notes if live testing disagrees.
|
|
6
|
+
- **Error contract consistency.** `Models::Order#modify/#cancel/#refresh/#slice_order` and `WS::Client` initialization now raise typed errors (`DhanHQ::OrderError`, `DhanHQ::AuthenticationError`) instead of bare `RuntimeError`s, so callers rescuing `DhanHQ::Error` no longer miss guard-clause failures.
|
|
7
|
+
- **`RateLimiter` cleanup threads** now spawn only for intervals with finite limits (was 3 immortal threads per tier — 18 in a typical process; now 0 for quote/non-trading, 1 for order/data/global, 3 for option chain). The misleading "matches the published rate-limit table" comment was corrected: values follow this repo's own documented operational limits; per-minute platform caps remain an open question (table unreachable — broken docs site).
|
|
8
|
+
- **`WS::Registry`** is now mutex-synchronized (register/unregister/stop_all were unsynchronized) and logs swallowed `stop` failures at debug level instead of rescuing silently.
|
|
9
|
+
- **`BaseModel` attribute accessors** are defined once per class instead of two singleton methods per attribute per instance (allocation-heavy on collections). Parameter key style dispatches through `BaseModel.param_key_style` (:title for OptionChain) instead of the fragile `class.name.include?("OptionChain")` check. `validate_attributes` no longer crashes when no contract is declared.
|
|
10
|
+
- **Timeouts on the token-endpoint fetch** (`configure_from_token_endpoint` / `configure_from_dashboard`): bounded open/read timeouts matching `Client#build_connection`; previously the only unbounded HTTP path in the gem.
|
|
11
|
+
- **Dead code removed:** `BaseAPI#request` (unused private), the commented-out `format_params`/`optionchain_api?` block in `AttributeHelper`, the commented `validate_params!` line in `BaseModel.create`, and the redundant `class Error` reopen in `lib/dhan_hq.rb` (already fully defined in `errors.rb`).
|
|
12
|
+
- **Docs:** `docs/ENDPOINTS_AND_SANDBOX.md` no longer documents the non-existent `bin/call_all_endpoints.rb`; the smoke-testing section now references `bin/console`, the sandbox connectivity spec, and `bin/ws_feed_test.rb`. The `DHAN_DEBUG` `puts` in `RateLimiter` was replaced with `DhanHQ.logger&.debug`.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Environment modes — `config.mode = :live | :sandbox | :hybrid` (`DHAN_MODE`)** — one switch that decides which Dhan host every request hits. `:live` (default) keeps today's behaviour; `:sandbox` replaces the old all-or-nothing `sandbox = true` flag; and the new **`:hybrid`** mode is the strategy-rehearsal combination the docs site describes — **live market data + sandbox execution**: market feed, option chain, historical charts, instruments and ScanX keep hitting `https://api.dhan.co/v2` for real prices while orders, positions, funds, statements and the rest of the trading surface hit `https://sandbox.dhan.co/v2`, where orders are validated but never fill.
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
DhanHQ.configure do |config|
|
|
20
|
+
config.mode = :hybrid # or DHAN_MODE=hybrid
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
client.market_feed.ltp("NSE_EQ" => [11536]) # -> api.dhan.co (live prices)
|
|
24
|
+
client.orders.create(...) # -> sandbox.dhan.co (paper execution)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- Resolution is per API tier (`order_api`, `data_api`, `quote_api`, `option_chain`, `non_trading_api`, `global_stocks_api`) via `Configuration#base_url_for(api_type)` / `#environment_for(api_type)`; the client rebuilds its Faraday connection when the resolved URL changes, so a mid-flight mode switch takes effect.
|
|
28
|
+
- `config.environment_overrides = { non_trading_api: :live }` (or `DHAN_ENV_OVERRIDES="non_trading_api=live"`) overrides any tier on top of the mode — e.g. keep real fund limits while everything else runs on the sandbox.
|
|
29
|
+
- `sandbox = true` / `DHAN_SANDBOX=true` remain supported aliases for `mode = :sandbox`; `mode` takes precedence when both are given. Unknown modes raise `ArgumentError` at configuration time, not on the first request.
|
|
30
|
+
- Per-client override: `DhanHQ::Client.new(config: shared, mode: :hybrid)` **forks** the configuration rather than mutating the shared/global one, so two clients can run different modes side by side.
|
|
31
|
+
- **Global Stocks (US equities) stays on production in `:hybrid`** on its own `global_stocks_api` rate-limit tier (same limits as order APIs) — `sandbox.dhan.co` does not mirror the `/v2/globalstocks/*` surface.
|
|
32
|
+
- **Sandbox-aware LIVE_TRADING gate** — the `ENV["LIVE_TRADING"]` guard now applies only to writes whose effective target is production. Any write routed to the sandbox host (full `:sandbox`, or the trading tiers under `:hybrid`) skips the gate automatically, because no real money can move there. Every order-audit log line now also records `"target":"live"|"sandbox"`, so the audit trail shows which host each attempt fired at.
|
|
33
|
+
- **ScanX data API — `client.scanx`** — wraps the four `/v2/data/*` endpoints from the official API surface, each validated by a dry-validation contract, riding the `data_api` tier (live in `:hybrid`):
|
|
34
|
+
- `#company_info` — `POST /v2/data/companyinfo`: company overview, valuation/profitability ratios and shareholding pattern (`metrics: %w[CO RATIOS SHP]`).
|
|
35
|
+
- `#market_movers` — `POST /v2/data/marketmovers`: top options/futures/stocks by OI, volume or price movement; the contract enforces the API's own rule that `expiry` is required for derivative instruments and `universe` for EQUITY.
|
|
36
|
+
- `#news_headlines` — `POST /v2/data/newsheadline`: live news by category/stock (limit 1–50); `dhanClientId` is injected automatically.
|
|
37
|
+
- `#technical_data` — `POST /v2/data/technical`: 24 server-side indicators (SMA/EMA families, RSI_14, MACD_HIST, STOCH, ATR_14, ADX_14, pivots…) computed on the latest closed candle.
|
|
38
|
+
- `/v2/data` joins the `client-id`-header prefixes so ScanX requests authenticate like the other data APIs.
|
|
39
|
+
|
|
40
|
+
## [4.0.0] - 2026-08-31
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- **Multi-Instance Client Isolation (`DhanHQ::Client.new`)** — run multiple isolated client instances in the same process with separate configurations, credentials, sandbox environments, and rate limiters. Ideal for multi-tenant applications, managing multiple trading accounts concurrently, or comparing sandbox vs. production environments side-by-side.
|
|
45
|
+
```ruby
|
|
46
|
+
client_a = DhanHQ::Client.new(client_id: "ACC_A", access_token: "TOKEN_A")
|
|
47
|
+
client_b = DhanHQ::Client.new(client_id: "ACC_B", access_token: "TOKEN_B", sandbox: true)
|
|
48
|
+
|
|
49
|
+
orders_a = client_a.orders.all
|
|
50
|
+
orders_b = client_b.orders.all
|
|
51
|
+
```
|
|
52
|
+
- **Instance-bound Resource Accessors** — direct accessors on `DhanHQ::Client` instances (`client.orders`, `client.positions`, `client.holdings`, `client.funds`, `client.trades`, `client.market_feed`, `client.option_chain`, `client.historical_data`, `client.forever_orders`, `client.alert_orders`, `client.iceberg_orders`, `client.super_orders`, `client.twap_orders`, `client.statements`, `client.kill_switch`, `client.pnl_exit`, `client.trader_control`, `client.ip_setup`, `client.edis`) bound to that client's configuration and routed to matching rate-limiting tiers (`client_for(API_TYPE)`).
|
|
53
|
+
- **Instance-bound WebSocket Helpers** — `client.ws_market_feed`, `client.ws_order_update`, and `client.ws_market_depth` configured automatically with the client instance's credentials and URLs.
|
|
54
|
+
- **Keyword Argument Initialization** — `DhanHQ::Client.new` accepts keyword configuration parameters (`dry_run: true`, `sandbox: true`, `client_id: "..."`, `access_token: "..."`) or a `Configuration` instance.
|
|
55
|
+
- **Per-Instance Dry-Run Mode** — `DhanHQ::Client.new(dry_run: true)` simulates state-changing writes locally for that instance regardless of the global configuration.
|
|
56
|
+
- **Dynamic Model Resource Invalidation** — `DhanHQ.reset_model_resources!` automatically refreshes class-level model caches (`Models::Order.resource`, etc.) whenever `DhanHQ.configure`, `configure_with_env`, `configuration=`, or `reset_configuration!` is invoked.
|
|
57
|
+
- **Per-Client Auto Token Management** — `client.enable_auto_token_management!(dhan_client_id: ..., pin: ..., totp_secret: ...)` for automated TOTP-based token lifecycle management per client instance.
|
|
58
|
+
|
|
1
59
|
## [3.4.0] - 2026-08-15
|
|
2
60
|
|
|
3
61
|
### Added
|
|
@@ -19,7 +77,9 @@
|
|
|
19
77
|
### Fixed
|
|
20
78
|
|
|
21
79
|
- **`docs/RAILS_INTEGRATION.md`'s Sidekiq examples (§5, §6) called `client.wait!`, `client.subscribe(array)`, and `DhanHQ::WS::Client.new(kind: :order_updates)`** — none of which exist. `DhanHQ::WS::Client`/`DhanHQ::WS::Orders::Client` have no blocking wait method at all; `Client#start` spawns a background thread and returns immediately. Replaced with the real API (`DhanHQ::WS.connect`/`DhanHQ::WS::Orders.connect` plus a `connected?`-based liveness loop), guarded by a new spec that locks down the method names these examples call by name.
|
|
22
|
-
-
|
|
80
|
+
- **Every runtime dependency except `faraday` had no version floor at all** (`add_dependency "x"`, equivalent to `>= 0` — satisfied by any version ever published). Found while investigating whether the Ruby floor could drop to 3.1 (it can't — see below): forcing a fresh `bundle install` under Ruby 3.1.6 resolved `dry-validation` to `0.4.1`, a 2016-era, pre-`Dry::Validation::Contract` API generation every contract in `lib/DhanHQ/contracts/` is incompatible with — every contract spec failed at `require`-time. Pinning that alone wasn't enough: with `dry-validation` fixed, the same fresh resolve then pulled `activesupport` down to `7.2.3.2` (ActiveSupport 8.x itself requires Ruby ≥ 3.2), and AS 7-vs-8 behavioral differences broke `Agent::ToolRegistry`, `MCP::Server`, and `Risk::Pipeline` — 63 failures with no connection to Ruby version syntax at all.
|
|
81
|
+
|
|
82
|
+
All ten now have an explicit floor matching their currently locked major (`activesupport >= 8.0`, `dry-validation ~> 1.0`, and so on) — closing the class of bug, not just the two instances that happened to surface. The `dry-validation` constraint went through two attempts: the first pass in this same release pinned it to `~> 1.11`, which — because `~>` locks to the *last* segment given — actually meant `>= 1.11, < 1.12` and would have silently blocked every future 1.12+ release. Corrected to `~> 1.0`, which excludes the ancient `0.x` line (the actual problem) without capping legitimate 1.x updates.
|
|
23
83
|
- **`spec/dhan_hq/contracts/expired_options_data_contract_spec.rb` and `expired_options_data_spec.rb` hardcoded `from_date: "2021-08-02"`**, which is itself now more than 5 years in the past and so failed the contract's own "cannot be more than 5 years ago" rule — the shared fixture failed the exact rule it existed to exercise, cascading into every test merged onto it. Replaced every literal 2021 date with one computed relative to `Date.today`, preserving each test's original intent (span length, ordering, the exact-31-day boundary).
|
|
24
84
|
|
|
25
85
|
### Investigated, not changed
|
data/GUIDE.md
CHANGED
|
@@ -57,6 +57,8 @@ Set any of the following environment variables _before_ calling
|
|
|
57
57
|
|
|
58
58
|
| Variable | Purpose |
|
|
59
59
|
| ----------------------------------------- | ---------------------------------------------------- |
|
|
60
|
+
| `DHAN_MODE` | `live` (default), `sandbox`, or `hybrid` — see [Environments](#environments-live-sandbox-hybrid) below. |
|
|
61
|
+
| `DHAN_ENV_OVERRIDES` | String-form per-tier overrides, e.g. `non_trading_api=live`. |
|
|
60
62
|
| `DHAN_LOG_LEVEL` | Change logger verbosity (`INFO` default). |
|
|
61
63
|
| `DHAN_BASE_URL` | Override the REST API host. |
|
|
62
64
|
| `DHAN_WS_VERSION` | Target a specific WebSocket API version. |
|
|
@@ -77,6 +79,44 @@ For token rotation without restarting the app, set `access_token_provider` (Proc
|
|
|
77
79
|
|
|
78
80
|
See [docs/AUTHENTICATION.md](docs/AUTHENTICATION.md) and README “Dynamic access token”.
|
|
79
81
|
|
|
82
|
+
**Multi-instance clients (multi-account support)**
|
|
83
|
+
|
|
84
|
+
When you need to trade across multiple accounts, run multi-tenant setups, or isolate sandbox/testing environments in a single process, use `DhanHQ::Client.new`:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
# Account 1
|
|
88
|
+
client1 = DhanHQ::Client.new(client_id: "ACC_1", access_token: "TOKEN_1")
|
|
89
|
+
|
|
90
|
+
# Account 2 (Sandbox)
|
|
91
|
+
client2 = DhanHQ::Client.new(client_id: "ACC_2", access_token: "TOKEN_2", sandbox: true)
|
|
92
|
+
|
|
93
|
+
# Resource accessors directly bound to client instance:
|
|
94
|
+
orders1 = client1.orders.all
|
|
95
|
+
orders2 = client2.orders.all
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Environments: live / sandbox / hybrid**
|
|
99
|
+
|
|
100
|
+
`config.mode` picks which host each tier hits. `:hybrid` is the strategy-rehearsal mode —
|
|
101
|
+
live market data from `api.dhan.co`, paper execution on `sandbox.dhan.co`:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
DhanHQ.configure do |config|
|
|
105
|
+
config.mode = :hybrid # or DHAN_MODE=hybrid
|
|
106
|
+
# Optional: per-tier escape hatch on top of the mode
|
|
107
|
+
# config.environment_overrides = { non_trading_api: :live }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
client.market_feed.ltp("NSE_EQ" => [11536]) # live prices from api.dhan.co
|
|
111
|
+
client.orders.create(...) # paper execution on sandbox.dhan.co
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The `LIVE_TRADING` gate is relaxed automatically for sandbox-targeted writes; production
|
|
115
|
+
writes still require `ENV["LIVE_TRADING"]="true"`. Global Stocks always stays on production
|
|
116
|
+
in `:hybrid`, and WebSocket streams are production-only (the Dhan sandbox has no WS).
|
|
117
|
+
See [docs/ENDPOINTS_AND_SANDBOX.md](docs/ENDPOINTS_AND_SANDBOX.md) for the full routing
|
|
118
|
+
and verification table.
|
|
119
|
+
|
|
80
120
|
---
|
|
81
121
|
|
|
82
122
|
## Working With Models
|
data/README.md
CHANGED
|
@@ -218,8 +218,106 @@ end
|
|
|
218
218
|
|
|
219
219
|
When the API returns 401, the client retries **once** with a fresh token from your provider.
|
|
220
220
|
|
|
221
|
+
### Multi-Instance Clients (Multi-Account / Multi-Tenant)
|
|
222
|
+
|
|
223
|
+
For multi-tenant applications, managing multiple trading accounts concurrently, or isolating sandbox from production within the same process, construct explicit `DhanHQ::Client` instances:
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
# Account 1 (Live Trading)
|
|
227
|
+
client1 = DhanHQ::Client.new(
|
|
228
|
+
client_id: "ACC_1_ID",
|
|
229
|
+
access_token: "ACC_1_TOKEN"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Account 2 (Sandbox / Testing)
|
|
233
|
+
client2 = DhanHQ::Client.new(
|
|
234
|
+
client_id: "ACC_2_ID",
|
|
235
|
+
access_token: "ACC_2_TOKEN",
|
|
236
|
+
sandbox: true,
|
|
237
|
+
dry_run: true
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Resource accessors are bound to the client instance:
|
|
241
|
+
positions = client1.positions.all
|
|
242
|
+
orders = client2.orders.all
|
|
243
|
+
|
|
244
|
+
# Instance-bound WebSocket streams:
|
|
245
|
+
ws_stream = client1.ws_market_feed(mode: :ticker) { |tick| puts tick[:ltp] }
|
|
246
|
+
ws_orders = client1.ws_order_update
|
|
247
|
+
```
|
|
248
|
+
|
|
221
249
|
> **Full details**: TOTP flows, partner mode, token endpoint bootstrap, auto-management — see [docs/AUTHENTICATION.md](docs/AUTHENTICATION.md).
|
|
222
250
|
|
|
251
|
+
### Environments: Live / Sandbox / Hybrid
|
|
252
|
+
|
|
253
|
+
`config.mode` decides which Dhan host each request hits:
|
|
254
|
+
|
|
255
|
+
| Mode | Market data (quotes, option chain, charts, instruments, ScanX) | Trading & account (orders, positions, funds, statements) |
|
|
256
|
+
| ---------- | -------------------------------------------------------------- | -------------------------------------------------------- |
|
|
257
|
+
| `:live` | `api.dhan.co` (default) | `api.dhan.co` |
|
|
258
|
+
| `:sandbox` | `sandbox.dhan.co` | `sandbox.dhan.co` |
|
|
259
|
+
| `:hybrid` | `api.dhan.co` — **live prices** | `sandbox.dhan.co` — **paper execution** |
|
|
260
|
+
|
|
261
|
+
`hybrid` is the strategy-rehearsal mode: your code reads real market data and places
|
|
262
|
+
orders that are accepted and validated by the Dhan sandbox (which mirrors the live REST
|
|
263
|
+
surface) without risking money.
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
DhanHQ.configure do |config|
|
|
267
|
+
config.client_id = ENV["DHAN_CLIENT_ID"]
|
|
268
|
+
config.access_token = ENV["DHAN_ACCESS_TOKEN"]
|
|
269
|
+
config.mode = :hybrid # or :live (default) / :sandbox
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Same code, split routing:
|
|
273
|
+
client.market_feed.ltp("NSE_EQ" => [11536]) # -> https://api.dhan.co/v2/marketfeed/ltp
|
|
274
|
+
client.option_chain.fetch(...) # -> https://api.dhan.co/v2/optionchain
|
|
275
|
+
client.orders.create(...) # -> https://sandbox.dhan.co/v2/orders (no LIVE_TRADING needed)
|
|
276
|
+
client.positions.all # -> https://sandbox.dhan.co/v2/positions
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Per-tier escape hatch when the defaults don't fit:
|
|
280
|
+
|
|
281
|
+
```ruby
|
|
282
|
+
config.mode = :hybrid
|
|
283
|
+
config.environment_overrides = { non_trading_api: :live } # e.g. read the real fund limits
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
- Env vars: `DHAN_MODE=live|sandbox|hybrid` (or the legacy `DHAN_SANDBOX=true`).
|
|
287
|
+
- The `LIVE_TRADING` guard is **relaxed automatically** for requests routed to the
|
|
288
|
+
sandbox host — no real money can move there. Orders still hit the guard on `:live`.
|
|
289
|
+
- Per-client override: `DhanHQ::Client.new(config: shared_config, mode: :hybrid)` forks
|
|
290
|
+
the config instead of mutating the shared one.
|
|
291
|
+
- Global Stocks (US equities) always stay on production in `:hybrid` — the sandbox host
|
|
292
|
+
does not mirror them.
|
|
293
|
+
- WebSocket streams are production-only (Dhan sandbox has no WS); in `:hybrid` the order
|
|
294
|
+
update stream reports your **real** account, not sandbox orders.
|
|
295
|
+
- `sandbox = true` remains a supported alias for `mode = :sandbox`.
|
|
296
|
+
|
|
297
|
+
> **Full details**: what each tier routes where, verified-vs-unverified sandbox endpoints —
|
|
298
|
+
> see [docs/ENDPOINTS_AND_SANDBOX.md](docs/ENDPOINTS_AND_SANDBOX.md).
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## ScanX Data API (company info, market movers, news, technicals)
|
|
303
|
+
|
|
304
|
+
The `/v2/data/*` endpoints are wrapped by `client.scanx` — fundamentals, ranked movers,
|
|
305
|
+
live news headlines and server-side indicators, all validated by dry-validation contracts:
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
client.scanx.company_info(security_id: "1333", exchange_segment: "NSE_EQ",
|
|
309
|
+
instrument: "EQUITY", metrics: %w[CO RATIOS SHP])
|
|
310
|
+
|
|
311
|
+
client.scanx.technical_data(security_id: "1333", exchange_segment: "NSE_EQ",
|
|
312
|
+
instrument: "EQUITY", timeframe: "D",
|
|
313
|
+
indicators: %w[SMA_20 RSI_14 MACD_HIST PIVOT_CLASSIC])
|
|
314
|
+
|
|
315
|
+
client.scanx.market_movers(exchange_segment: "NSE_FNO", instrument: %w[OPTIDX OPTSTK],
|
|
316
|
+
category: "HIGHEST_OI", expiry: "2026-10-29", limit: 10)
|
|
317
|
+
|
|
318
|
+
client.scanx.news_headlines(categories: %w[ALL], limit: 25)
|
|
319
|
+
```
|
|
320
|
+
|
|
223
321
|
---
|
|
224
322
|
|
|
225
323
|
## Order Safety
|
|
@@ -238,6 +336,11 @@ LIVE_TRADING=false # or simply omit
|
|
|
238
336
|
|
|
239
337
|
Attempting to place an order without `LIVE_TRADING=true` raises `DhanHQ::LiveTradingDisabledError`.
|
|
240
338
|
|
|
339
|
+
**Sandbox exception:** orders routed to the sandbox host (`mode = :sandbox`, or the trading
|
|
340
|
+
tiers under `mode = :hybrid`) skip this guard automatically — the sandbox cannot move real
|
|
341
|
+
money, so the gate only protects production writes. Every audit log line records the
|
|
342
|
+
`target` host (`live`/`sandbox`) it fired at.
|
|
343
|
+
|
|
241
344
|
### Dry-Run Mode
|
|
242
345
|
|
|
243
346
|
`dry_run` suppresses every request that would change account state — order placement,
|
data/docs/CONFIGURATION.md
CHANGED
|
@@ -24,7 +24,9 @@ Set these _before_ calling `configure_with_env` to override defaults:
|
|
|
24
24
|
|
|
25
25
|
| Variable | Default | Description |
|
|
26
26
|
| -------------------------------- | ---------- | ------------------------------------------------------- |
|
|
27
|
-
| `
|
|
27
|
+
| `DHAN_MODE` | `live` | `live`, `sandbox`, or `hybrid`. `hybrid` routes market data (quotes, option chain, charts, instruments, ScanX) to production and trading/account calls (orders, positions, funds, statements) to `https://sandbox.dhan.co/v2` — live prices, paper execution. Takes precedence over `DHAN_SANDBOX`. |
|
|
28
|
+
| `DHAN_SANDBOX` | `false` | Legacy alias: set `"true"` to route all REST calls to `https://sandbox.dhan.co/v2`. Note: Dhan's sandbox validates request/response plumbing only — it does not execute real order fills/matching. Placed orders stay `PENDING` indefinitely. See [ENDPOINTS_AND_SANDBOX.md](ENDPOINTS_AND_SANDBOX.md). |
|
|
29
|
+
| `DHAN_ENV_OVERRIDES` | unset | String-form per-tier overrides on top of the mode, e.g. `"non_trading_api=live,data_api=sandbox"`. Equivalent to `config.environment_overrides`. |
|
|
28
30
|
| `DHAN_BASE_URL` | Dhan prod | Point REST calls to a different API hostname. Takes precedence over `DHAN_SANDBOX` only when explicitly set to something other than the production default. |
|
|
29
31
|
| `DHAN_WS_VERSION` | latest | Pin WebSocket connections to a specific API version |
|
|
30
32
|
| `DHAN_WS_ORDER_URL` | Dhan prod | Override the order update WebSocket endpoint |
|
|
@@ -46,7 +48,7 @@ These change what a write request *does*, not just where it goes. All default to
|
|
|
46
48
|
|
|
47
49
|
| Variable | Config attribute | Default | Effect |
|
|
48
50
|
| ------------------------------------- | ------------------------------------ | ------- | ------ |
|
|
49
|
-
| `LIVE_TRADING=true` | — | unset | **Required to place, modify, or cancel any real order, position exit, kill switch, EDIS, or P&L exit.** Without it, every write-path resource raises `DhanHQ::LiveTradingDisabledError` before making the request. This is the primary safety gate — see [ARCHITECTURE.md](../ARCHITECTURE.md) and the risk pipeline. |
|
|
51
|
+
| `LIVE_TRADING=true` | — | unset | **Required to place, modify, or cancel any real order, position exit, kill switch, EDIS, or P&L exit that targets production.** Without it, every production-bound write-path resource raises `DhanHQ::LiveTradingDisabledError` before making the request. Writes whose effective target is the sandbox host (`DHAN_MODE=sandbox`, or the trading tiers under `hybrid`) are exempt — no real money can move there. This is the primary safety gate — see [ARCHITECTURE.md](../ARCHITECTURE.md) and the risk pipeline. |
|
|
50
52
|
| `DHAN_DRY_RUN=true` | `config.dry_run` | `false` | Suppresses every state-changing request, logs the payload as `DHAN_DRY_RUN`, and answers order placements with a simulated `DRYRUN-…` id so caller code paths still run to completion. Reads still hit the API. |
|
|
51
53
|
| `DHAN_RETRY_WRITES=true` | `config.retry_non_idempotent_writes` | `false` | Auto-retries a non-idempotent write (order placement, modify, cancel) after a transient failure (429, 5xx, timeout). Off by default because the API has no idempotency key — a timed-out POST may have already reached the exchange, and retrying it can place a duplicate order. |
|
|
52
54
|
| `DHAN_AUTO_CORRELATION_ID=true` | `config.auto_correlation_id` | `false` | Fills in a `correlationId` (`dhq-<hex>`) on order placements that lack one, so a timed-out placement can be reconciled via `GET /v2/orders/external/{correlation-id}`. Off by default because it changes the request body; an explicit correlation id is always preserved. |
|
|
@@ -106,6 +108,31 @@ end
|
|
|
106
108
|
- **`access_token_provider`**: Callable (Proc/lambda) returning the token string. Called on every request (no memoization). When set, the gem uses it instead of `access_token`.
|
|
107
109
|
- **`on_token_expired`**: Optional callable invoked when a 401/token-expired triggers a **single retry** (only when `access_token_provider` is set).
|
|
108
110
|
|
|
111
|
+
## Multi-Instance Client Configuration
|
|
112
|
+
|
|
113
|
+
For multi-account or multi-tenant architectures, construct explicit `DhanHQ::Client` instances without depending on global singleton state:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
# Direct configuration via keyword arguments
|
|
117
|
+
client = DhanHQ::Client.new(
|
|
118
|
+
client_id: "YOUR_CLIENT_ID",
|
|
119
|
+
access_token: "YOUR_ACCESS_TOKEN",
|
|
120
|
+
sandbox: false,
|
|
121
|
+
dry_run: false
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Or via an explicit Configuration object
|
|
125
|
+
custom_config = DhanHQ::Configuration.new do |c|
|
|
126
|
+
c.client_id = "TENANT_CLIENT_ID"
|
|
127
|
+
c.access_token = "TENANT_ACCESS_TOKEN"
|
|
128
|
+
c.dry_run = true
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
client = DhanHQ::Client.new(config: custom_config)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Each client instance manages its own Faraday connection, isolated configuration, dry-run state, and instance-bound resource accessors (`client.orders`, `client.positions`, `client.market_feed`, etc.).
|
|
135
|
+
|
|
109
136
|
For detailed authentication flows, see [AUTHENTICATION.md](AUTHENTICATION.md).
|
|
110
137
|
|
|
111
138
|
## Available Resources
|
|
@@ -1,14 +1,46 @@
|
|
|
1
|
-
# DhanHQ gem — Endpoints and sandbox support
|
|
1
|
+
# DhanHQ gem — Endpoints, environments and sandbox support
|
|
2
|
+
|
|
3
|
+
## Environments: `:live`, `:sandbox`, `:hybrid`
|
|
4
|
+
|
|
5
|
+
`DhanHQ.configuration.mode` (or `DHAN_MODE`) selects which host each REST request hits.
|
|
6
|
+
The legacy `sandbox = true` / `DHAN_SANDBOX=true` remains an alias for `mode = :sandbox`.
|
|
7
|
+
|
|
8
|
+
| Mode | Data tiers (`data_api`, `quote_api`, `option_chain`) | Trading/account tiers (`order_api`, `non_trading_api`) | Global Stocks (`global_stocks_api`) |
|
|
9
|
+
| ---------- | ---------------------------------------------------- | ------------------------------------------------------ | ----------------------------------- |
|
|
10
|
+
| `:live` (default) | `api.dhan.co/v2` | `api.dhan.co/v2` | `api.dhan.co/v2` |
|
|
11
|
+
| `:sandbox` | `sandbox.dhan.co/v2` | `sandbox.dhan.co/v2` | `sandbox.dhan.co/v2` |
|
|
12
|
+
| `:hybrid` | `api.dhan.co/v2` — live prices | `sandbox.dhan.co/v2` — paper execution | `api.dhan.co/v2` (sandbox does not mirror it) |
|
|
13
|
+
|
|
14
|
+
`hybrid` exists for strategy rehearsal: read live market data, place orders against the
|
|
15
|
+
sandbox host. Per-tier overrides (`environment_overrides`) win over the mode defaults —
|
|
16
|
+
for example keeping the real fund limits while everything else runs on the sandbox:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
DhanHQ.configure do |c|
|
|
20
|
+
c.mode = :hybrid
|
|
21
|
+
c.environment_overrides = { non_trading_api: :live } # or String form: "non_trading_api=live"
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Per-client override (forks the config, never mutates the shared one):
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
client = DhanHQ::Client.new(config: shared_config, mode: :hybrid)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Resolution order for any request: explicit `config.base_url` (if set, it wins for every
|
|
32
|
+
request) → `environment_overrides[tier]` → mode default for that tier.
|
|
2
33
|
|
|
3
34
|
## Sandbox behavior
|
|
4
35
|
|
|
5
36
|
- **Sandbox does NOT execute real order fills/matching.** Confirmed both via Dhan's own docs ("the sandbox behaves like the live API but does not execute real orders") and empirically: a `MARKET` order placed on sandbox validates and returns a real `orderId`, but stays at `PENDING` indefinitely — it never transitions to `TRADED`, even after 24+ seconds of polling. Use sandbox to validate request/response plumbing (auth, payload shape, error codes), not to test fill, cancel, or square-off behavior.
|
|
6
|
-
- **REST:**
|
|
7
|
-
- **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
|
|
8
|
-
- **
|
|
37
|
+
- **REST:** every wrapped REST endpoint listed below is sent to the host its tier resolves to (see the mode table above) — `:sandbox` sends everything to `https://sandbox.dhan.co/v2`, `:hybrid` splits data vs trading.
|
|
38
|
+
- **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 mode. There are no sandbox WebSocket endpoints in the Dhan v2 API; do not rely on sandbox for real-time streams. Under `:hybrid` the order-update stream therefore reports your **real** account, not sandbox orders.
|
|
39
|
+
- **LIVE_TRADING gate** is relaxed automatically for any write whose effective target is the sandbox host (no real money at risk). Production-targeted writes still require `ENV["LIVE_TRADING"]="true"`. Order audit log lines carry `"target":"live"|"sandbox"`.
|
|
40
|
+
- **Auth endpoints** (`DhanHQ::Auth`) do **not** follow the mode. They always call:
|
|
9
41
|
- `https://auth.dhan.co` — token generation
|
|
10
|
-
- `https://api.dhan.co/v2` — token renewal
|
|
11
|
-
So token generation/renewal always hit production; only data/order REST calls follow the
|
|
42
|
+
- `https://api.dhan.co/v2` — token renewal
|
|
43
|
+
So token generation/renewal always hit production; only data/order REST calls follow the mode.
|
|
12
44
|
|
|
13
45
|
---
|
|
14
46
|
|
|
@@ -41,36 +73,43 @@
|
|
|
41
73
|
- `/v2/marketfeed/ltp`, `/v2/marketfeed/ohlc`, `/v2/marketfeed/quote`
|
|
42
74
|
- `/v2/optionchain`, `/v2/optionchain/expirylist`
|
|
43
75
|
- `/v2/charts/historical`, `/v2/charts/intraday`, `/v2/charts/rollingoption`
|
|
76
|
+
- `/v2/data/companyinfo`, `/v2/data/marketmovers`, `/v2/data/newsheadline`, `/v2/data/technical` (ScanX)
|
|
77
|
+
- `/v2/globalstocks/*` (Global Stocks — assumed unavailable on sandbox; always production in `:hybrid`)
|
|
44
78
|
|
|
45
79
|
---
|
|
46
80
|
|
|
47
81
|
## REST endpoints integrated in the gem
|
|
48
82
|
|
|
49
|
-
Paths are as built by the gem (HTTP_PATH + endpoint). Base URL is
|
|
83
|
+
Paths are as built by the gem (HTTP_PATH + endpoint). Base URL is resolved per tier — see the mode table above.
|
|
50
84
|
|
|
51
85
|
| Resource / model | Path(s) | Methods | API type |
|
|
52
|
-
|
|
53
|
-
| **Profile**
|
|
54
|
-
| **Funds**
|
|
55
|
-
| **Statements**
|
|
56
|
-
| **Orders**
|
|
57
|
-
| **Positions**
|
|
58
|
-
| **Holdings**
|
|
59
|
-
| **Trades**
|
|
60
|
-
| **Forever orders**
|
|
61
|
-
| **Super orders**
|
|
62
|
-
| **Kill switch**
|
|
63
|
-
| **Trader control**
|
|
64
|
-
| **IP setup**
|
|
65
|
-
| **EDIS**
|
|
66
|
-
| **Alert orders**
|
|
67
|
-
| **PnL exit**
|
|
68
|
-
| **Margin calculator**
|
|
69
|
-
| **Instruments**
|
|
70
|
-
| **Market feed**
|
|
71
|
-
| **Option chain**
|
|
72
|
-
| **Historical data**
|
|
73
|
-
| **Expired options data**
|
|
86
|
+
|----------------------------|----------------------------------------------|-----------|-----------------|
|
|
87
|
+
| **Profile** | `/v2/profile` | GET | non_trading_api |
|
|
88
|
+
| **Funds** | `/v2/fundlimit` | GET | order_api |
|
|
89
|
+
| **Statements** | `/v2/ledger`, `/v2/trades/{from}/{to}/{page}`| GET | non_trading_api |
|
|
90
|
+
| **Orders** | `/v2/orders`, `/v2/orders/{id}`, `/v2/orders/external/{correlation_id}`, `/v2/orders/slicing` | GET, POST, PUT, DELETE | order_api |
|
|
91
|
+
| **Positions** | `/v2/positions`, `/v2/positions/convert` | GET, POST, DELETE | order_api |
|
|
92
|
+
| **Holdings** | `/v2/holdings` | GET | order_api |
|
|
93
|
+
| **Trades** | `/v2/trades`, `/v2/trades/{order_id}` | GET | order_api |
|
|
94
|
+
| **Forever orders** | `/v2/forever/orders`, `/v2/forever/orders/{id}` | GET, POST, PUT, DELETE | order_api |
|
|
95
|
+
| **Super orders** | `/v2/super/orders`, `/v2/super/orders/{id}`, leg delete | GET, POST, PUT, DELETE | order_api |
|
|
96
|
+
| **Kill switch** | `/v2/killswitch` | GET, POST | order_api |
|
|
97
|
+
| **Trader control** | `/trader-control` | GET, POST | order_api |
|
|
98
|
+
| **IP setup** | `/ip/getIP`, `/ip/setIP`, `/ip/modifyIP` | GET, POST, PUT | order_api |
|
|
99
|
+
| **EDIS** | `/edis/tpin`, `/edis/form`, `/edis/bulkform`, `/edis/inquire/{isin}` | GET, POST | order_api |
|
|
100
|
+
| **Alert orders** | `/alerts/orders` | GET, POST, PUT, DELETE | order_api |
|
|
101
|
+
| **PnL exit** | `/v2/pnlExit` | GET, POST, DELETE | order_api |
|
|
102
|
+
| **Margin calculator** | `/v2/margincalculator`, `/v2/margincalculator/multi` | POST | order_api |
|
|
103
|
+
| **Instruments** | `/v2/instrument/{segment}` (redirect to CSV) | GET | data_api |
|
|
104
|
+
| **Market feed** | `/v2/marketfeed/ltp`, `/v2/marketfeed/ohlc`, `/v2/marketfeed/quote` | POST | data_api / quote_api |
|
|
105
|
+
| **Option chain** | `/v2/optionchain`, `/v2/optionchain/expirylist` | POST | data_api |
|
|
106
|
+
| **Historical data** | `/v2/charts/historical`, `/v2/charts/intraday` | POST | data_api |
|
|
107
|
+
| **Expired options data** | `/v2/charts/rollingoption` | POST | data_api |
|
|
108
|
+
| **ScanX — company info** | `/v2/data/companyinfo` | POST | data_api |
|
|
109
|
+
| **ScanX — market movers** | `/v2/data/marketmovers` | POST | data_api |
|
|
110
|
+
| **ScanX — news** | `/v2/data/newsheadline` | POST | data_api |
|
|
111
|
+
| **ScanX — technicals** | `/v2/data/technical` | POST | data_api |
|
|
112
|
+
| **Global Stocks** | `/v2/globalstocks/orders`, `/v2/globalstocks/holdings`, `/v2/globalstocks/fundlimit`, `/v2/globalstocks/trades`, `/v2/globalstocks/marketstatus`, `/v2/globalstocks/margincalculator`, `/v2/globalstocks/transEstimate` | GET/POST/PUT/DELETE | global_stocks_api |
|
|
74
113
|
|
|
75
114
|
---
|
|
76
115
|
|
|
@@ -81,7 +120,7 @@ Paths are as built by the gem (HTTP_PATH + endpoint). Base URL is either `https:
|
|
|
81
120
|
| Generate token | `https://auth.dhan.co/app/generateAccessToken` | POST |
|
|
82
121
|
| Renew token | `https://api.dhan.co/v2/RenewToken` | POST |
|
|
83
122
|
|
|
84
|
-
These are **not**
|
|
123
|
+
These are **not** mode-aware; they always use the URLs above.
|
|
85
124
|
|
|
86
125
|
---
|
|
87
126
|
|
|
@@ -93,24 +132,30 @@ These are **not** sandbox-aware; they always use the URLs above.
|
|
|
93
132
|
| Market feed | `wss://api-feed.dhan.co` |
|
|
94
133
|
| Market depth | `wss://depth-api-feed.dhan.co/twentydepth` |
|
|
95
134
|
|
|
96
|
-
**Sandbox:** Dhan sandbox does **not** provide WebSocket services. These endpoints are production-only. The gem never switches WS URLs based on
|
|
135
|
+
**Sandbox:** Dhan sandbox does **not** provide WebSocket services. These endpoints are production-only. The gem never switches WS URLs based on the mode; 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.
|
|
97
136
|
|
|
98
137
|
---
|
|
99
138
|
|
|
100
139
|
## Summary
|
|
101
140
|
|
|
102
|
-
- **
|
|
141
|
+
- **Modes:** `:live` (default — everything on production), `:sandbox` (everything on `sandbox.dhan.co`), `:hybrid` (data tiers live, trading tiers sandbox, Global Stocks live). Set via `config.mode`, `DHAN_MODE`, or the legacy `config.sandbox` / `DHAN_SANDBOX`.
|
|
142
|
+
- **Verified on sandbox:** only `GET /v2/profile` and `GET /v2/fundlimit` — see "Sandbox: verified vs not working / unverified" above.
|
|
143
|
+
- **LIVE_TRADING:** required only for writes that target production; sandbox-targeted writes are exempt.
|
|
103
144
|
- **Auth:** Token generation and renewal always use production hosts.
|
|
104
145
|
- **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.
|
|
105
146
|
|
|
106
|
-
##
|
|
147
|
+
## Smoke testing
|
|
107
148
|
|
|
108
|
-
|
|
149
|
+
There is no shipped call-all-endpoints script (an earlier revision of this page
|
|
150
|
+
referenced `bin/call_all_endpoints.rb`, which does not exist in the repo). Use
|
|
151
|
+
what is actually here:
|
|
109
152
|
|
|
110
153
|
```bash
|
|
111
|
-
bin/
|
|
112
|
-
|
|
113
|
-
|
|
154
|
+
bin/console # IRB with the gem preloaded — try client.profile.get,
|
|
155
|
+
# client.market_feed.ltp(...), client.fundlimit.get ...
|
|
156
|
+
bundle exec rspec spec/dhan_hq/sandbox_connectivity_spec.rb # guarded sandbox probes
|
|
157
|
+
bin/ws_feed_test.rb # live market-feed smoke test (production WS only)
|
|
114
158
|
```
|
|
115
159
|
|
|
116
|
-
Requires `DHAN_CLIENT_ID` and `DHAN_ACCESS_TOKEN`. Optional: `
|
|
160
|
+
Requires `DHAN_CLIENT_ID` and `DHAN_ACCESS_TOKEN`. Optional: `DHAN_MODE=hybrid`
|
|
161
|
+
(or `DHAN_SANDBOX=true`).
|