DhanHQ 4.0.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 +39 -0
- data/GUIDE.md +24 -0
- data/README.md +75 -0
- data/docs/CONFIGURATION.md +4 -2
- data/docs/ENDPOINTS_AND_SANDBOX.md +83 -38
- data/lib/DhanHQ/client.rb +30 -5
- data/lib/DhanHQ/concerns/order_audit.rb +26 -3
- data/lib/DhanHQ/configuration.rb +152 -3
- 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 +0 -15
- data/lib/DhanHQ/core/base_model.rb +32 -12
- data/lib/DhanHQ/helpers/attribute_helper.rb +0 -22
- 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/scanx.rb +98 -0
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/client.rb +5 -10
- data/lib/DhanHQ/ws/connection.rb +13 -15
- data/lib/DhanHQ/ws/market_depth/client.rb +5 -1
- data/lib/DhanHQ/ws/registry.rb +10 -6
- data/lib/dhan_hq.rb +7 -2
- metadata +6 -1
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,42 @@
|
|
|
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
|
+
|
|
1
40
|
## [4.0.0] - 2026-08-31
|
|
2
41
|
|
|
3
42
|
### Added
|
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. |
|
|
@@ -93,6 +95,28 @@ orders1 = client1.orders.all
|
|
|
93
95
|
orders2 = client2.orders.all
|
|
94
96
|
```
|
|
95
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
|
+
|
|
96
120
|
---
|
|
97
121
|
|
|
98
122
|
## Working With Models
|
data/README.md
CHANGED
|
@@ -248,6 +248,76 @@ ws_orders = client1.ws_order_update
|
|
|
248
248
|
|
|
249
249
|
> **Full details**: TOTP flows, partner mode, token endpoint bootstrap, auto-management — see [docs/AUTHENTICATION.md](docs/AUTHENTICATION.md).
|
|
250
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
|
+
|
|
251
321
|
---
|
|
252
322
|
|
|
253
323
|
## Order Safety
|
|
@@ -266,6 +336,11 @@ LIVE_TRADING=false # or simply omit
|
|
|
266
336
|
|
|
267
337
|
Attempting to place an order without `LIVE_TRADING=true` raises `DhanHQ::LiveTradingDisabledError`.
|
|
268
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
|
+
|
|
269
344
|
### Dry-Run Mode
|
|
270
345
|
|
|
271
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. |
|
|
@@ -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`).
|
data/lib/DhanHQ/client.rb
CHANGED
|
@@ -49,8 +49,12 @@ module DhanHQ
|
|
|
49
49
|
# @param config_or_attrs [DhanHQ::Configuration, Hash, nil] Optional configuration or attribute hash
|
|
50
50
|
# @param config [DhanHQ::Configuration, nil] Optional keyword configuration
|
|
51
51
|
# @param api_type [Symbol] Type of API (`:order_api`, `:data_api`, `:non_trading_api`)
|
|
52
|
+
# @param mode [Symbol, nil] Optional environment override (`:live`, `:sandbox`, `:hybrid`)
|
|
53
|
+
# for this client. When combined with +config:+ the configuration is forked,
|
|
54
|
+
# so a shared/global configuration is never mutated.
|
|
52
55
|
# @param attrs [Hash] Additional keyword configuration attributes
|
|
53
|
-
def initialize(config_or_attrs = nil, config: nil, api_type: :order_api, **attrs)
|
|
56
|
+
def initialize(config_or_attrs = nil, config: nil, api_type: :order_api, mode: nil, **attrs)
|
|
57
|
+
attrs = attrs.merge(mode: mode) if mode
|
|
54
58
|
@config = resolve_config(config_or_attrs, config, attrs)
|
|
55
59
|
@api_type = api_type
|
|
56
60
|
@rate_limiter = RateLimiter.for(api_type)
|
|
@@ -59,11 +63,12 @@ module DhanHQ
|
|
|
59
63
|
end
|
|
60
64
|
|
|
61
65
|
# The Faraday connection used for HTTP requests.
|
|
62
|
-
# Built on first use and rebuilt whenever the
|
|
66
|
+
# Built on first use and rebuilt whenever the base URL for this client's API
|
|
67
|
+
# tier changes — including a mid-flight mode switch.
|
|
63
68
|
#
|
|
64
69
|
# @return [Faraday::Connection]
|
|
65
70
|
def connection
|
|
66
|
-
current_url = @config.
|
|
71
|
+
current_url = @config.base_url_for(@api_type)
|
|
67
72
|
return @connection if @connection && @last_base_url == current_url
|
|
68
73
|
|
|
69
74
|
@last_base_url = current_url
|
|
@@ -92,6 +97,15 @@ module DhanHQ
|
|
|
92
97
|
def trader_control = @trader_control ||= Resources::TraderControl.new(client: client_for(Resources::TraderControl::API_TYPE))
|
|
93
98
|
def ip_setup = @ip_setup ||= Resources::IPSetup.new(client: client_for(Resources::IPSetup::API_TYPE))
|
|
94
99
|
def edis = @edis ||= Resources::Edis.new(client: client_for(Resources::Edis::API_TYPE))
|
|
100
|
+
def scanx = @scanx ||= Resources::ScanX.new(client: client_for(Resources::ScanX::API_TYPE))
|
|
101
|
+
|
|
102
|
+
# The effective environment (+:live+ or +:sandbox+) for requests issued by
|
|
103
|
+
# this client, resolved from its API tier and the configured mode.
|
|
104
|
+
#
|
|
105
|
+
# @return [Symbol]
|
|
106
|
+
def environment
|
|
107
|
+
@config.environment_for(@api_type)
|
|
108
|
+
end
|
|
95
109
|
|
|
96
110
|
# WebSocket streams bound to this client instance configuration
|
|
97
111
|
def ws_market_feed(mode: :ticker, url: nil, &block)
|
|
@@ -258,11 +272,11 @@ module DhanHQ
|
|
|
258
272
|
|
|
259
273
|
def resolve_config(config_or_attrs, keyword_config, attrs = {})
|
|
260
274
|
if config_or_attrs.is_a?(DhanHQ::Configuration)
|
|
261
|
-
config_or_attrs
|
|
275
|
+
fork_config_with_attrs(config_or_attrs, attrs)
|
|
262
276
|
elsif config_or_attrs.is_a?(Hash)
|
|
263
277
|
DhanHQ::Configuration.new(config_or_attrs.merge(attrs))
|
|
264
278
|
elsif keyword_config
|
|
265
|
-
keyword_config
|
|
279
|
+
fork_config_with_attrs(keyword_config, attrs)
|
|
266
280
|
elsif attrs.any?
|
|
267
281
|
DhanHQ::Configuration.new(attrs)
|
|
268
282
|
else
|
|
@@ -270,6 +284,17 @@ module DhanHQ
|
|
|
270
284
|
end
|
|
271
285
|
end
|
|
272
286
|
|
|
287
|
+
# Copies a Configuration when per-instance attributes (e.g. +mode:+) are
|
|
288
|
+
# given alongside it, so an injected or global configuration is never
|
|
289
|
+
# mutated. Without extra attributes the original object is shared unchanged.
|
|
290
|
+
def fork_config_with_attrs(config, attrs)
|
|
291
|
+
return config if attrs.empty?
|
|
292
|
+
|
|
293
|
+
config.dup.tap do |forked|
|
|
294
|
+
attrs.each { |key, value| forked.public_send("#{key}=", value) }
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
273
298
|
# Simulator that answers state-changing requests locally when dry run is on.
|
|
274
299
|
#
|
|
275
300
|
# @return [DhanHQ::DryRun::Simulator]
|
|
@@ -37,13 +37,28 @@ module DhanHQ
|
|
|
37
37
|
module OrderAudit
|
|
38
38
|
private
|
|
39
39
|
|
|
40
|
-
# Raises an error
|
|
41
|
-
#
|
|
40
|
+
# Raises an error unless live trading is explicitly enabled OR the request
|
|
41
|
+
# targets the sandbox host, where no real money is at risk.
|
|
42
|
+
#
|
|
43
|
+
# Set ENV["LIVE_TRADING"]="true" to allow order submission against the
|
|
44
|
+
# production host. Orders routed to the sandbox host (config.mode =
|
|
45
|
+
# :sandbox, or the trading tiers under :hybrid) skip the gate because they
|
|
46
|
+
# cannot place real orders.
|
|
42
47
|
def ensure_live_trading!
|
|
43
48
|
return if ENV["LIVE_TRADING"] == "true"
|
|
49
|
+
return if sandbox_target?
|
|
44
50
|
|
|
45
51
|
raise DhanHQ::LiveTradingDisabledError,
|
|
46
|
-
"Live trading is disabled. Set ENV[\"LIVE_TRADING\"]=\"true\" to enable order placement
|
|
52
|
+
"Live trading is disabled. Set ENV[\"LIVE_TRADING\"]=\"true\" to enable order placement, " \
|
|
53
|
+
"or switch to the sandbox with DhanHQ.configure { |c| c.mode = :sandbox } / :hybrid."
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# True when this resource's requests resolve to the sandbox host.
|
|
57
|
+
# Tolerates hosts that never got a client wired in (bare test doubles).
|
|
58
|
+
def sandbox_target?
|
|
59
|
+
return false unless respond_to?(:client) && client.respond_to?(:environment)
|
|
60
|
+
|
|
61
|
+
client.environment == :sandbox
|
|
47
62
|
end
|
|
48
63
|
|
|
49
64
|
# Runs the risk pipeline for the given order params.
|
|
@@ -120,6 +135,7 @@ module DhanHQ
|
|
|
120
135
|
event: event,
|
|
121
136
|
hostname: inspector.hostname,
|
|
122
137
|
env: inspector.environment,
|
|
138
|
+
target: request_target,
|
|
123
139
|
ipv4: inspector.public_ipv4,
|
|
124
140
|
ipv6: inspector.public_ipv6,
|
|
125
141
|
security_id: extract_param(params, :securityId, :security_id),
|
|
@@ -131,6 +147,13 @@ module DhanHQ
|
|
|
131
147
|
DhanHQ.logger&.warn(JSON.generate(entry))
|
|
132
148
|
end
|
|
133
149
|
|
|
150
|
+
# The host the order will be sent to, for the audit trail.
|
|
151
|
+
def request_target
|
|
152
|
+
return "sandbox" if sandbox_target?
|
|
153
|
+
|
|
154
|
+
"live"
|
|
155
|
+
end
|
|
156
|
+
|
|
134
157
|
# Extracts a value from params trying both camelCase and snake_case keys,
|
|
135
158
|
# as well as both symbol and string key types.
|
|
136
159
|
def extract_param(params, camel_key, snake_key)
|