DhanHQ 3.1.0 → 3.2.1
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/ARCHITECTURE.md +47 -1
- data/CHANGELOG.md +73 -0
- data/README.md +241 -3
- data/docs/AUTHENTICATION.md +3 -5
- data/docs/CONFIGURATION.md +3 -2
- data/docs/CONSTANTS_REFERENCE.md +5 -4
- data/docs/ENDPOINTS_AND_SANDBOX.md +1 -0
- data/docs/LIVE_ORDER_UPDATES.md +5 -10
- data/docs/RAILS_INTEGRATION.md +1 -1
- data/docs/RELEASE_GUIDE.md +13 -2
- data/docs/STANDALONE_RUBY_WEBSOCKET_INTEGRATION.md +2 -2
- data/docs/WEBSOCKET_INTEGRATION.md +13 -20
- data/docs/WEBSOCKET_PROTOCOL.md +7 -3
- data/lib/DhanHQ/agent/key_coercion.rb +36 -0
- data/lib/DhanHQ/agent/tool.rb +37 -0
- data/lib/DhanHQ/agent/tool_catalogue.rb +180 -0
- data/lib/DhanHQ/agent/tool_handlers.rb +116 -0
- data/lib/DhanHQ/agent/tool_registry.rb +17 -261
- data/lib/DhanHQ/agent/tool_schemas.rb +160 -0
- data/lib/DhanHQ/ai/prompt_helpers.rb +17 -7
- data/lib/DhanHQ/client.rb +105 -30
- data/lib/DhanHQ/concerns/order_audit.rb +35 -4
- data/lib/DhanHQ/configuration.rb +70 -1
- data/lib/DhanHQ/constants.rb +101 -0
- data/lib/DhanHQ/contracts/expired_options_data_contract.rb +1 -9
- data/lib/DhanHQ/contracts/global_stocks_estimator_contract.rb +28 -0
- data/lib/DhanHQ/contracts/global_stocks_modify_order_contract.rb +26 -0
- data/lib/DhanHQ/contracts/global_stocks_order_contract.rb +33 -0
- data/lib/DhanHQ/contracts/global_stocks_place_order_contract.rb +75 -0
- data/lib/DhanHQ/contracts/historical_data_contract.rb +1 -21
- data/lib/DhanHQ/contracts/multi_order_contract.rb +74 -0
- data/lib/DhanHQ/contracts/trade_history_contract.rb +1 -8
- data/lib/DhanHQ/dry_run/ledger.rb +71 -0
- data/lib/DhanHQ/dry_run/simulator.rb +140 -0
- data/lib/DhanHQ/helpers/attribute_helper.rb +23 -0
- data/lib/DhanHQ/models/alert_order.rb +6 -3
- data/lib/DhanHQ/models/global_stocks/funds.rb +56 -0
- data/lib/DhanHQ/models/global_stocks/holding.rb +101 -0
- data/lib/DhanHQ/models/global_stocks/margin.rb +54 -0
- data/lib/DhanHQ/models/global_stocks/market_status.rb +63 -0
- data/lib/DhanHQ/models/global_stocks/order.rb +189 -0
- data/lib/DhanHQ/models/global_stocks/order_estimate.rb +61 -0
- data/lib/DhanHQ/models/global_stocks/trade.rb +74 -0
- data/lib/DhanHQ/models/margin.rb +5 -1
- data/lib/DhanHQ/models/multi_order.rb +130 -0
- data/lib/DhanHQ/resources/global_stocks/funds.rb +25 -0
- data/lib/DhanHQ/resources/global_stocks/holdings.rb +22 -0
- data/lib/DhanHQ/resources/global_stocks/margin_calculator.rb +70 -0
- data/lib/DhanHQ/resources/global_stocks/market_status.rb +22 -0
- data/lib/DhanHQ/resources/global_stocks/orders.rb +112 -0
- data/lib/DhanHQ/resources/global_stocks/trades.rb +31 -0
- data/lib/DhanHQ/resources/multi_orders.rb +58 -0
- data/lib/DhanHQ/risk/pipeline.rb +0 -2
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/write_paths.rb +57 -0
- data/lib/DhanHQ/ws/client.rb +117 -2
- data/lib/DhanHQ/ws/connection.rb +40 -11
- data/lib/DhanHQ/ws/sub_state.rb +14 -0
- data/lib/dhan_hq.rb +4 -0
- data/lib/dhanhq/analysis/options_buying_advisor.rb +11 -10
- data/skills/dhanhq-ruby/SKILL.md +2 -1
- data/skills/dhanhq-ruby/examples/order_management.rb +7 -0
- data/skills/dhanhq-ruby/references/common-workflows.md +8 -8
- data/skills/dhanhq-ruby/references/instruments.md +7 -1
- data/skills/dhanhq-ruby/references/orders.md +2 -0
- metadata +30 -10
- data/.rspec +0 -3
- data/.rubocop.yml +0 -50
- data/.rubocop_todo.yml +0 -217
- data/Rakefile +0 -14
- data/TAGS +0 -10
- data/core +0 -0
- data/diagram.html +0 -184
- data/watchlist.csv +0 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 287ad8c77adb3623574c0ee14102cfc3e88695168f75e1550a25dbbff9eb8508
|
|
4
|
+
data.tar.gz: a61b3f8e664f45f203b1afc3f840c9ed62a4e3c40e2f2d17fedc080ac682e029
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2c9ad1a3df906b585b0308809fb75235cf475a23c4eaddfea3f699de2e577819a1b78315b60012b6c51bb6c398dcaa35daab31d9ae9452d81d84e3e76741f0a9
|
|
7
|
+
data.tar.gz: 888e390276144b239a1bdd43afb9e77b58d21703587b898497b2426b8b78b47c7e03fd607bb3306fea49f9c2ae0e04a11ccb1b6968fc109fe61a8d2627b750a4
|
data/ARCHITECTURE.md
CHANGED
|
@@ -32,6 +32,11 @@ This document describes the architecture of the DhanHQ v2 API client gem: layers
|
|
|
32
32
|
│ → Dry::Validation, shared macros in BaseContract │
|
|
33
33
|
├─────────────────────────────────────────────────────────────────┤
|
|
34
34
|
│ WebSocket (WS::*) — separate subsystem │
|
|
35
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
36
|
+
│ AI / Agent layer (Agent, Skills, Risk, MCP) — separate subsystem│
|
|
37
|
+
│ MCP server (JSON-RPC over stdio) → Agent::ToolRegistry │
|
|
38
|
+
│ → primitive tools (Models) + skill tools (Skills::Registry) │
|
|
39
|
+
│ → gated by Agent::Policy + Risk::Pipeline before any order │
|
|
35
40
|
└─────────────────────────────────────────────────────────────────┘
|
|
36
41
|
```
|
|
37
42
|
|
|
@@ -45,12 +50,20 @@ This document describes the architecture of the DhanHQ v2 API client gem: layers
|
|
|
45
50
|
| `core/` | Base abstractions | BaseAPI (HTTP verbs, path building, param formatting), BaseModel (attributes, resource, validation, CRUD helpers), BaseResource (CRUD on BaseAPI), AuthAPI, ErrorHandler |
|
|
46
51
|
| `helpers/` | Cross-cutting | APIHelper, AttributeHelper (keys, normalization), ValidationHelper (validate_params!, validate!), RequestHelper (headers, payload, build_from_response), ResponseHelper (parse_json, handle_response, error mapping) |
|
|
47
52
|
| `models/` | Domain / facade | Typed wrappers (Order, Position, Holding, etc.). Define `resource`, optional `validation_contract`, and domain methods. Validate then delegate to resource. |
|
|
53
|
+
| `models/global_stocks/`, `resources/global_stocks/` | US equities book | DhanHQ's Global Stocks API (`/v2/globalstocks/*`) — its own Order, Trade, Holding, Funds (USD), Margin, OrderEstimate and MarketStatus. Namespaced so a USD position is never mistaken for an INR one. Orders here have no exchange segment / product type / validity, quantity is a float (fractional shares), and `AMOUNT` adds notional orders. The India-specific `Risk::Pipeline` does not apply. |
|
|
48
54
|
| `resources/` | REST wrappers | One class per API surface (Orders, Positions, MarketFeed, OptionChain, …). Set `HTTP_PATH`, `API_TYPE`; implement get/post/put/delete via BaseAPI. |
|
|
49
55
|
| `contracts/` | Request/response validation | Dry::Validation contracts (PlaceOrderContract, ModifyOrderContract, OptionChainContract, etc.). BaseContract provides shared macros (e.g. lot_size, tick_size). |
|
|
50
56
|
| `auth/` | Token lifecycle | Token generator/renewal/manager for dynamic tokens. |
|
|
51
57
|
| `concerns/` | Shared behavior | Modules included across layers (e.g. `OrderAudit` for live trading guard + audit logging, included in all order resources). |
|
|
52
58
|
| `utils/` | Utilities | Cross-cutting utilities not tied to a single layer (e.g. `NetworkInspector` for IP/hostname/env lookup used by order audit logging). |
|
|
59
|
+
| `dry_run/`, `write_paths.rb` | Write-path collaborators | `WritePaths` classifies a request as mutating / order-placing; `DryRun::Simulator` answers mutating requests locally and `DryRun::Ledger` remembers simulated orders so the models' refetch resolves without a live call. Extracted from `Client`, which was accumulating HTTP, auth retry, transient retry, rate limiting, path taxonomy and simulation. |
|
|
53
60
|
| `ws/` | WebSocket | Connection, packets, decoder, market depth, orders client — isolated from REST. |
|
|
61
|
+
| `mcp/` | MCP server | `DhanHQ::MCP::Server` — hand-rolled JSON-RPC 2.0 stdio server. Exposes `tools/list`, `tools/call`, `resources/*`, `prompts/*` to any MCP client (Claude Desktop, Claude Code, etc.). Launched via `exe/dhanhq-mcp`. |
|
|
62
|
+
| `agent/` | AI tool registry | `Agent::ToolRegistry` — 12 domestic primitives (`dhan_profile`, `dhan_place_order`, …), 9 Global Stocks / basket tools (`dhan_global_*`, `dhan_multi_order`), plus one `dhan_skill_*` tool per registered `Skills::Registry` entry (32 total). `Agent::Policy` gates every call by scope + `LIVE_TRADING`/`DHANHQ_MCP_ENABLE_WRITES`. `Agent::OrderPreview` validates without submitting. |
|
|
63
|
+
| `skills/` | Composable strategies | `Skills::Base` DSL (`param`, `step`, `risk`, `scope`, `description`) + `Skills::Registry`. 11 builtin skills (`iron_condor`, `straddle`, `strangle`, `buy_atm_call`, `covered_call`, `protective_put`, `bull_put_spread`, `bear_call_spread`, `square_off_all`, `square_off_position`, `market_data_summarizer`) under `skills/builtin/`. |
|
|
64
|
+
| `risk/` | Pre-trade risk gate | `Risk::Pipeline.run!` — runs `TradingPermission`, `AsmGsm`, `ProductSupport`, `OrderType`, `Quantity`, `MarketHours`, `PositionLimits`, `Concentration`, `Options` (options-only), `MaxLoss` (daily) under `risk/checks/`. Wired into every order-placing resource via `Concerns::OrderAudit#run_risk_checks!` and into the `dhan_place_order` MCP tool. |
|
|
65
|
+
| `ai/` | LLM prompt helpers | `AI::PromptHelpers` — human-readable portfolio summaries and risk reports, consumed by the MCP server's `prompts/get`. |
|
|
66
|
+
| `strategy/`, `market_data/`, `option_analytics/`, `events/` | Analysis modules | Strategy base class, market-data snapshot/OHLC types, option analytics (Black-Scholes, max pain), and typed event objects (`Events::Base`, `Events::Bus`). Zeitwerk-autoloaded like the rest of `DhanHQ::*` — distinct from the separate opt-in `lib/dhanhq/analysis` and `lib/dhanhq/ta` modules (lowercase namespace, require explicitly; see [3.0.0] in CHANGELOG.md). |
|
|
54
67
|
|
|
55
68
|
---
|
|
56
69
|
|
|
@@ -96,13 +109,46 @@ So: **Models → Resources → Client**; **Contracts** are used by Models (and o
|
|
|
96
109
|
|
|
97
110
|
- Credentials and URLs live in `DhanHQ::Configuration` (access_token, client_id, base_url, sandbox, optional access_token_provider).
|
|
98
111
|
- `DhanHQ.configure { }`, `configure_with_env`, and `configure_from_token_endpoint` set configuration. Never hardcode credentials; use env vars or token endpoint.
|
|
112
|
+
- Write-path behaviour is also configuration: `dry_run`, `retry_non_idempotent_writes`, `auto_correlation_id` (see below).
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Write-path safety
|
|
117
|
+
|
|
118
|
+
The distinction that matters on this API is **mutating vs read-only**, not GET vs POST — the
|
|
119
|
+
option chain, market feed, historical charts and margin calculators are all read-only POSTs.
|
|
120
|
+
`Constants::MUTATING_PATH_PREFIXES` names the paths whose non-GET requests actually change
|
|
121
|
+
account state, and `ORDER_PLACEMENT_PATH_PREFIXES` narrows that to order creation.
|
|
122
|
+
`DhanHQ::WritePaths` wraps those lists as predicates so `Client` does not carry the taxonomy
|
|
123
|
+
itself. Two behaviours key off it:
|
|
124
|
+
|
|
125
|
+
- **Dry run** (`config.dry_run`): `DryRun::Simulator` logs mutating requests and answers them
|
|
126
|
+
locally; reads still hit the API, so a strategy can be rehearsed against live prices.
|
|
127
|
+
Because the models place-then-refetch, each simulated placement is recorded in
|
|
128
|
+
`DryRun::Ledger` and reads for a `DRYRUN-…` id are replayed from it — otherwise the refetch
|
|
129
|
+
would carry a fabricated id to the live API. Responses are shaped per endpoint family: a
|
|
130
|
+
placement needs an `orderId` for the models to treat it as successful, and the basket
|
|
131
|
+
endpoint needs a per-leg `orders` array.
|
|
132
|
+
- **Retry gating**: the API has no idempotency key, so a `POST /v2/orders` that times out may
|
|
133
|
+
already have reached the exchange. Mutating writes are therefore **not** auto-retried by
|
|
134
|
+
default (`config.retry_non_idempotent_writes`), while reads keep their exponential backoff.
|
|
135
|
+
`config.auto_correlation_id` fills in a `correlationId` so a caller can reconcile a timed-out
|
|
136
|
+
placement through `GET /v2/orders/external/{correlation-id}`.
|
|
99
137
|
|
|
100
138
|
---
|
|
101
139
|
|
|
102
140
|
## WebSocket (WS)
|
|
103
141
|
|
|
104
142
|
- Separate subsystem under `DhanHQ::WS`: own connection, packet types, decoder, market depth, orders client.
|
|
105
|
-
- Shares configuration (e.g. access token) but not the REST Client or Resources.
|
|
143
|
+
- Shares configuration (e.g. access token) but not the REST Client or Resources.
|
|
144
|
+
- `WS::Connection` owns the socket, the reconnect loop with backoff, and subscription replay:
|
|
145
|
+
the server keeps no subscription state across connections, so `SubState`'s snapshot is
|
|
146
|
+
re-sent on every new session. It reports lifecycle transitions to its owner through an
|
|
147
|
+
`on_event` callable rather than knowing about callbacks itself.
|
|
148
|
+
- `WS::Client` bridges those transitions onto its public callback bus (`:open`, `:reconnect`,
|
|
149
|
+
`:close`, `:error`, plus decoded `:tick`s) and maintains health counters — `last_message_at`,
|
|
150
|
+
`reconnect_count`, `healthy?`, `health`. Frame arrival is tracked separately from socket
|
|
151
|
+
state because a feed can stay connected while the server stops publishing.
|
|
106
152
|
|
|
107
153
|
---
|
|
108
154
|
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,76 @@
|
|
|
1
|
+
## [3.2.1] - 2026-07-26
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
|
|
5
|
+
- **Restored the Claude Agent Skill pack to the published gem.** Switching `spec.files` from a reject-list to an allowlist in 3.2.0 stripped 51 MB of junk (a 36 MB core dump and a 17 MB `diagram.html`), but it also dropped `skills/dhanhq-ruby/` — 28 files, 164 KB: `SKILL.md` and its trigger frontmatter, 12 reference guides, 11 examples and 4 helper scripts. That pack is product content, added deliberately in #41. No Ruby code loads it at runtime, so nothing broke and no spec noticed; users installing 3.2.0 simply stopped receiving it. `CODE_OF_CONDUCT.md` and `AGENTS.md` are restored alongside it.
|
|
6
|
+
|
|
7
|
+
Unaffected and still shipped throughout: the MCP server (`lib/DhanHQ/mcp/`, `exe/dhanhq-mcp`), the agent tool layer (all 32 tools), the Ruby skills classes (`lib/DhanHQ/skills/`, 11 builtin skills), the risk pipeline and the WebSocket subsystem.
|
|
8
|
+
|
|
9
|
+
Gem size: 3.1.0 was 5.9 MB, 3.2.0 was 292 KB, and this is 324 KB. The 20× reduction is real and almost entirely the core dump; the skill pack was collateral.
|
|
10
|
+
- **`DhanHQ::AI` was unreachable on a bare `require "dhan_hq"`.** The Zeitwerk inflector had no `ai → AI` acronym, so it looked for `DhanHQ::Ai` while `ai.rb` defines `DhanHQ::AI`; both spellings raised `NameError`. The documented `DhanHQ::AI::PromptHelpers` and `DhanHQ::AI::ContextBuilder` entry points therefore only worked if something had already loaded the file by hand, which only `mcp/server.rb` did. Found while writing the first specs for that module.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`spec/dhan_hq/gem_packaging_spec.rb`** — pins both ends of what ships, since packaging has now failed in both directions. Asserts the entry point, both executables, RBS signatures, Rails initializer, MCP server, agent tool layer, Ruby skills and the Claude Skill pack are all present; asserts the known junk, any crash dump, the spec suite and development config are all absent; and caps the uncompressed payload at 3 MB so a stray binary fails loudly rather than silently adding megabytes to a release.
|
|
15
|
+
- First specs for `AI::PromptHelpers` (13 examples), covering portfolio summaries, the risk report's P&L arithmetic — summed across every position, counted only for open ones — and order confirmation rendering. Built on real model instances rather than doubles, because `BaseModel` defines attribute readers per instance and `instance_double` cannot verify them.
|
|
16
|
+
- Specs for the lazily-built connection (not opened on construction, memoised, rebuilt on a base-URL change) and for retry exhaustion (transport errors translated, SDK errors re-raised unchanged, reads still retried).
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **`Client#with_transient_retry` no longer duplicates its retry branch.** It had two rescue clauses running near-identical backoff-log-sleep-retry logic, differing only in what they raised once retries were spent. Collapsed into one clause, with an `exhausted_error` helper naming the difference: Faraday's transport errors are translated to `DhanHQ::NetworkError` (they are not part of this gem's hierarchy, so a caller rescuing `DhanHQ::Error` would otherwise miss them), while the gem's own errors are re-raised unchanged. Clears the `Lint/DuplicateBranch` pressure on this method.
|
|
21
|
+
- **`Client#initialize` no longer opens a connection.** It now establishes state and checks its one invariant; the Faraday connection is built on first use of `#connection` and rebuilt when the configured base URL changes. Behaviour is unchanged for callers — `#connection` is still public and still reflects a sandbox toggle mid-process — but constructing a client does no network setup.
|
|
22
|
+
- **`Naming/PredicateMethod` is now scoped by method name instead of by file.** Seven file-level exclusions replaced with `AllowedMethods: [cancel, modify, destroy]` plus `AllowBangMethods: true`.
|
|
23
|
+
|
|
24
|
+
These are commands that report whether they succeeded, not predicates — `cancel?` would read as "should I cancel?", which is worse than the name it replaces, so adding `?` aliases would have been the wrong fix. The unambiguous-failure path for them is the bang variant (`cancel!`, `modify!`). Scoping by name rather than by file also means a genuinely mis-named predicate elsewhere in those same files is still caught, and it surfaced a now-redundant inline `rubocop:disable` in `risk/pipeline.rb`.
|
|
25
|
+
- `AI::PromptHelpers.portfolio_summary` computed the open-position set twice (`count(&:open?)` then `select(&:open?)`); it now derives it once. Both it and `.risk_report` wrap their collections in `Array()`, so a nil from a failed fetch renders an empty section instead of raising from inside a prompt helper — `funds` was already guarded, the collections were not.
|
|
26
|
+
|
|
27
|
+
## [3.2.0] - 2026-07-25
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **Global Stocks (US equities) API** — full coverage of the 11 `/v2/globalstocks/*` endpoints, previously absent:
|
|
32
|
+
- Resources: `Resources::GlobalStocks::Orders` (place/modify/cancel/find/all), `Trades` (all + by security id), `Holdings`, `Funds`, `MarginCalculator` (margin + charge estimate), `MarketStatus`.
|
|
33
|
+
- Models: `Models::GlobalStocks::Order`, `Trade`, `Holding`, `Funds`, `Margin`, `OrderEstimate`, `MarketStatus`.
|
|
34
|
+
- Contracts: `GlobalStocksPlaceOrderContract`, `GlobalStocksModifyOrderContract`, `GlobalStocksEstimatorContract`.
|
|
35
|
+
- Namespaced under `GlobalStocks` so a USD position can never be mistaken for an INR one.
|
|
36
|
+
- Handles the ways this book differs from domestic trading: no exchange segment / product type / validity on orders, float quantities (fractional shares), and the `AMOUNT` notional order type. `MarginCalculator` renders `price`/`quantity` as the strings the API documents while validating them as numbers.
|
|
37
|
+
- Writes are gated by `LIVE_TRADING` and audit-logged like any other order path. `Risk::Pipeline` is deliberately not applied — its checks resolve instruments from the Indian scrip master and encode NSE/BSE rules.
|
|
38
|
+
- **Multi Order (basket) endpoint** — `POST /v2/alerts/multi/orders` via `Resources::MultiOrders` and `Models::MultiOrder`, with `MultiOrderContract` enforcing the documented 15-leg cap, unique `sequence` values, and per-leg price/trigger rules. `MultiOrder` exposes `order_ids`, `accepted`, `rejected`, `all_accepted?`, `partially_accepted?` and `for_sequence` so partial acceptance is visible. Previously undocumented as missing — earlier feature tables claimed it was covered.
|
|
39
|
+
- **SDK-level dry-run mode** — `config.dry_run` / `DHAN_DRY_RUN=true`. Suppresses every state-changing request (orders, position exits, kill switch, P&L exit, EDIS, IP setup), logs the full payload as `DHAN_DRY_RUN`, and answers order placements with a simulated `DRYRUN-…` order id so caller code paths run to completion. Reads — including read-only POSTs like the option chain, market feed, charts and margin calculators — still hit the API, so a full strategy can be rehearsed against live prices. Complements `Agent::OrderPreview`, which covers a single order.
|
|
40
|
+
- **Correlation-id reconciliation** — `config.auto_correlation_id` / `DHAN_AUTO_CORRELATION_ID=true` fills in a `correlationId` (`dhq-<hex>`) on order placements that lack one, so a timed-out placement can be resolved through `GET /v2/orders/external/{correlation-id}`. Off by default because it changes the request body; an explicit correlation id is always preserved.
|
|
41
|
+
- **WebSocket lifecycle hooks** — `WS::Client#on` now emits `:open`, `:reconnect`, `:close` and `:error` in addition to `:tick`. `:reconnect` carries `{ attempt:, resubscribed: }` so callers can re-seed derived state (candle builders, caches) after a gap and see which instruments were restored. `WS::Connection` reports transitions through a new `on_event:` callable.
|
|
42
|
+
- **WebSocket health checks** — `WS::Client#last_message_at`, `#seconds_since_last_message`, `#healthy?(stale_after: 45)`, `#connected_at`, `#reconnect_count`, `#subscriptions` and `#health`, for monitoring long-running daemons. Frame arrival is tracked separately from socket state because a feed can stay connected while the server stops publishing.
|
|
43
|
+
- **9 new agent/MCP tools** (32 total) — `dhan_global_holdings`, `dhan_global_funds`, `dhan_global_orders`, `dhan_global_trades`, `dhan_global_market_status`, `dhan_global_order_estimate` (combines charges and margin in one call), `dhan_global_place_order`, `dhan_global_cancel_order`, `dhan_multi_order`. Each gated by `Agent::Policy` on the same scopes and risk levels as the domestic equivalents.
|
|
44
|
+
- **Global Stocks constants** — `Constants::ExchangeSegment::INX_EQ` (kept out of `ALL` so it can never satisfy a domestic order contract) plus `Constants::GlobalStocks` with the `AMOUNT` order type, feed limits (segment code 14, 100 instruments per frame, 5,000 per connection, 5 connections per client), market-status values and feed message codes. `Urls::WS_GLOBAL_STOCKS_FEED` added.
|
|
45
|
+
- **Mutating-path constants** — `Constants::MUTATING_PATH_PREFIXES` and `ORDER_PLACEMENT_PATH_PREFIXES` distinguish real writes from the API's several read-only POSTs, driving both dry-run and retry gating.
|
|
46
|
+
- **`DhanHQ::WritePaths`** — path taxonomy (`mutating?`, `order_placement?`, `multi_order?`) extracted out of `Client`, which no longer needs to know it.
|
|
47
|
+
- **`DhanHQ::DryRun::Simulator` and `::Ledger`** — the dry-run concern extracted out of `Client` as collaborators. The ledger retains up to 500 simulated orders per process, evicting oldest first.
|
|
48
|
+
- **`AttributeHelper#deep_camelize_keys`** — recursive key camelization for the endpoints whose bodies nest objects (basket order legs, alert conditions).
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
|
|
52
|
+
- **The published gem shrank from 52.5 MB to 1.15 MB.** Four tracked files nobody intended to ship were being published: `core` (a 36.5 MB ELF core dump from an unrelated `light-locker` process), `diagram.html` (17.3 MB), `TAGS`, and `watchlist.csv` — 51.3 MB of the payload, so every `gem install DhanHQ` downloaded a stranger's crash dump. They entered via `f46fde1 "Squashed commit"` and `.gitignore` matched none of them. Removed from tracking, `.gitignore` patterns added, and `spec.files` switched from a reject-list to an explicit allowlist so an unanticipated artifact cannot leak into a release again. All 217 `lib/` files and both executables are unchanged. (The blobs remain in git history; shrinking clones would need a separate history rewrite.)
|
|
53
|
+
- **Non-idempotent writes are no longer auto-retried** (behaviour change). `Client#with_transient_retry` previously retried order placement, modification and cancellation on 429s, 5xxs and network timeouts. The DhanHQ API has no idempotency key, so a `POST /v2/orders` that timed out may already have reached the exchange — retrying it could place a second, real order. Transient failures on mutating paths now raise to the caller, who can reconcile before resubmitting. Reads and read-only POSTs keep their exponential backoff unchanged. Set `config.retry_non_idempotent_writes = true` (or `DHAN_RETRY_WRITES=true`) to restore the old behaviour, accepting the duplicate risk.
|
|
54
|
+
- `WS::Connection#initialize` accepts an optional `on_event:` keyword; the socket-open handling moved into a `handle_open` method.
|
|
55
|
+
- `Configuration` reads boolean env vars through a shared helper, so an empty value is treated as unset rather than false.
|
|
56
|
+
- README, ARCHITECTURE.md and CLAUDE.md document the Global Stocks book, basket orders, dry-run mode, write-path safety, and the WebSocket hooks and health API. The "Indian markets only" rule is restated as "DhanHQ v2 API only" — Delta Exchange and other brokers stay out, while DhanHQ's own Global Stocks surface is in scope.
|
|
57
|
+
- **`ToolCatalogue.tool` takes keyword arguments.** It had nine positional-and-keyword parameters, so a transposed `scope` and `risk` would have produced a silently mis-gated tool, and it needed an inline `rubocop:disable Metrics/ParameterLists`. Now keyword-only with a `**attributes` splat: an unknown attribute raises rather than half-building a tool, the repeated `version: "1.0.0"` is gone from all 21 definitions in favour of a default, and the inline suppression is removed. Verified by diffing the full 32-tool metadata dump before and after — byte-for-byte identical.
|
|
58
|
+
- `.rubocop_todo.yml` excludes `constants.rb` from `Metrics/ModuleLength` (a pure enumeration module whose line count tracks API coverage) and adds `models/global_stocks/order.rb` to the existing `Naming/PredicateMethod` exclusions, matching the domestic `Models::Order`.
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- **Basket order legs were sent in snake_case.** `BaseAPI` camelizes only the top level of a payload, so the nested `orders` entries reached the API as `transaction_type`/`exchange_segment`/`security_id` instead of `transactionType`/`exchangeSegment`/`securityId` — every basket order placed through the wrapper would have been rejected. Added `AttributeHelper#deep_camelize_keys` and applied it to the legs. Caught in review by Codex; the original spec only asserted top-level keys, which is why it passed.
|
|
63
|
+
- **The same bug pre-existed in alert orders.** `Models::AlertOrder.create` and `.modify` shallow-camelized a payload whose `condition` and `orders` are nested objects, so `AlertCondition` fields (`comparisonType`, `securityId`, `timeFrame`, …) went out snake_cased. Now uses `deep_camelize_keys`.
|
|
64
|
+
- **Dry run did not work through the model APIs.** `Models::Order.place` and `GlobalStocks::Order.place` re-fetch after placing, so a simulated `DRYRUN-…` id was carried into a live `GET /v2/orders/{id}` — a real network call and a guaranteed miss. `MultiOrder.place` received no `orders` array and returned an `ErrorObject`. Dry run therefore only worked for direct `Client#request` calls, contradicting the "rehearse a full strategy" claim. Fixed by extracting `DryRun::Simulator` and `DryRun::Ledger`: placements are recorded, reads for a `DRYRUN-` id are replayed locally, and the basket endpoint gets a per-leg response. Caught in review by Codex.
|
|
65
|
+
- **`dhanClientId` was never injected for alert orders.** `PAYLOAD_REQUIRES_DHAN_CLIENT_ID_PREFIXES` listed `/alerts/orders`, but `Resources::AlertOrders` uses `HTTP_PATH = "/v2/alerts/orders"`, so the `start_with?` check never matched and the field the API documents as **required** was omitted from every conditional-order payload unless the caller passed it explicitly. Fixed by adding the versioned `/v2/alerts` prefix, which also covers the new multi-order path.
|
|
66
|
+
- `DhanHQ::NetworkError` no longer reports "Request failed after 0 retries" when retries are disabled.
|
|
67
|
+
|
|
68
|
+
### Tests
|
|
69
|
+
|
|
70
|
+
- 1,052 examples, 0 failures (up from 833); RuboCop clean.
|
|
71
|
+
- New specs for the review fixes: nested leg camelization on the wire, dry run completing through `Order.place` / `GlobalStocks::Order.place` / `MultiOrder.place` with zero network calls, `DryRun::Simulator` response shaping and read replay, `DryRun::Ledger` eviction, and `WritePaths` classification (including that read-only POSTs are not treated as writes).
|
|
72
|
+
- New specs: Global Stocks resources (orders, holdings, funds, trades, market status, margin calculator) and models (order, holding, funds, trade, market status, margin, order estimate); `MultiOrder`; `Client` dry-run, retry gating and correlation-id behaviour; `WS::Client` lifecycle hooks and health tracking; `WS::Connection` subscription replay and reconnect reporting; agent tool registration and policy enforcement for the new tools; constants and configuration coverage for the new flags and path lists.
|
|
73
|
+
|
|
1
74
|
## [3.1.0] - 2026-07-20
|
|
2
75
|
|
|
3
76
|
### Added
|
data/README.md
CHANGED
|
@@ -117,7 +117,11 @@ For the full dependency flow and extension pattern, see [ARCHITECTURE.md](ARCHIT
|
|
|
117
117
|
- **Instrument convenience methods** — `.ltp`, `.ohlc`, `.option_chain` directly on instruments
|
|
118
118
|
- **Order audit logging** — every order attempt logs machine, IP, environment, and correlation ID as structured JSON
|
|
119
119
|
- **Live trading guard** — prevents accidental order placement unless `ENV["LIVE_TRADING"]="true"`
|
|
120
|
-
- **
|
|
120
|
+
- **Dry-run mode** — `config.dry_run = true` validates and logs every write while reads stay live, so a full strategy can be rehearsed against real prices
|
|
121
|
+
- **No duplicate orders on retry** — transient failures on order writes are surfaced, not blindly retried
|
|
122
|
+
- **Global Stocks (US equities)** — separate order book, holdings, trades, USD funds, market status, charge estimator and margin calculator
|
|
123
|
+
- **Basket orders** — up to 15 orders in a single request, with per-leg acceptance results
|
|
124
|
+
- **Full REST coverage** — Orders, Trades, Forever Orders, Super Orders, Multi (basket) Orders, Positions, Holdings, Funds, HistoricalData, OptionChain, MarketFeed, EDIS, Kill Switch, P&L Exit, Alert Orders, Margin Calculator, IP Setup, Global Stocks
|
|
121
125
|
- **P&L Based Exit** — automatic position exit on profit/loss thresholds
|
|
122
126
|
- **Postback parser** — parse Dhan webhook payloads with `Postback.parse` and status predicates
|
|
123
127
|
- **EDIS model** — ORM-style T-PIN, form, and status inquiry for delivery instruction slips
|
|
@@ -127,10 +131,12 @@ For the full dependency flow and extension pattern, see [ARCHITECTURE.md](ARCHIT
|
|
|
127
131
|
## Reliability & Safety
|
|
128
132
|
|
|
129
133
|
- retry-on-401 with token refresh
|
|
130
|
-
- WebSocket auto-reconnect and
|
|
134
|
+
- WebSocket auto-reconnect, backoff, and automatic re-subscription
|
|
131
135
|
- 429 rate-limit protection
|
|
132
136
|
- live trading guard via `LIVE_TRADING=true`
|
|
133
137
|
- structured order audit logs
|
|
138
|
+
- dry-run mode via `DHAN_DRY_RUN=true`
|
|
139
|
+
- order writes are never silently retried, so a timeout cannot become two orders
|
|
134
140
|
|
|
135
141
|
See [ARCHITECTURE.md](ARCHITECTURE.md), [docs/TESTING_GUIDE.md](docs/TESTING_GUIDE.md), and [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) for the deeper implementation details.
|
|
136
142
|
|
|
@@ -219,6 +225,95 @@ LIVE_TRADING=false # or simply omit
|
|
|
219
225
|
|
|
220
226
|
Attempting to place an order without `LIVE_TRADING=true` raises `DhanHQ::LiveTradingDisabledError`.
|
|
221
227
|
|
|
228
|
+
### Dry-Run Mode
|
|
229
|
+
|
|
230
|
+
`dry_run` suppresses every request that would change account state — order placement,
|
|
231
|
+
modification, cancellation, position exits, kill switch, P&L exit — while letting reads
|
|
232
|
+
through untouched. Market data, the option chain, positions and holdings stay live, so a
|
|
233
|
+
strategy can be rehearsed end-to-end against real prices without any risk of an order
|
|
234
|
+
reaching the exchange.
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
DhanHQ.configure do |config|
|
|
238
|
+
config.dry_run = true # or DHAN_DRY_RUN=true
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
order = DhanHQ::Models::Order.place(
|
|
242
|
+
transaction_type: DhanHQ::Constants::TransactionType::BUY,
|
|
243
|
+
exchange_segment: DhanHQ::Constants::ExchangeSegment::NSE_EQ,
|
|
244
|
+
product_type: DhanHQ::Constants::ProductType::INTRADAY,
|
|
245
|
+
order_type: DhanHQ::Constants::OrderType::LIMIT,
|
|
246
|
+
validity: DhanHQ::Constants::Validity::DAY,
|
|
247
|
+
security_id: "11536", quantity: 5, price: 1500.0
|
|
248
|
+
)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Suppressed writes are logged at WARN level with the full payload, and order placements come
|
|
252
|
+
back with a simulated `orderId` so downstream code paths run to completion:
|
|
253
|
+
|
|
254
|
+
```json
|
|
255
|
+
{ "event": "DHAN_DRY_RUN", "method": "POST", "path": "/v2/orders", "payload": { "…": "…" } }
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Note that a POST is not automatically a write on this API: the option chain, market feed,
|
|
259
|
+
historical charts and the margin calculators are read-only POSTs, and dry-run leaves them
|
|
260
|
+
alone.
|
|
261
|
+
|
|
262
|
+
This works through the high-level models too, not just `Client#request`. Because
|
|
263
|
+
`Order.place` re-fetches after placing, the SDK records each simulated order and replays reads
|
|
264
|
+
for its `DRYRUN-…` id locally — so a rehearsal issues no request for a fake order, and the
|
|
265
|
+
model you get back carries the fields you submitted:
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
order.order_id # => "DRYRUN-9F7BEB55D0F9"
|
|
269
|
+
order.security_id # => "11536"
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`dry_run` complements `Agent::OrderPreview` — preview validates a single order and returns a
|
|
273
|
+
summary, while `dry_run` covers every write path in the SDK, including skills and the MCP
|
|
274
|
+
tools.
|
|
275
|
+
|
|
276
|
+
### Order Retries and Duplicate Protection
|
|
277
|
+
|
|
278
|
+
The DhanHQ API has no idempotency key. A `POST /v2/orders` that times out may well have
|
|
279
|
+
reached the exchange, so retrying it can place a second, real order. By default this SDK
|
|
280
|
+
therefore **does not** retry non-idempotent writes — the transient error is raised to you:
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
begin
|
|
284
|
+
DhanHQ::Models::Order.place(...)
|
|
285
|
+
rescue DhanHQ::NetworkError
|
|
286
|
+
# The order may or may not have been accepted. Reconcile before resubmitting.
|
|
287
|
+
end
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Reads, and read-only POSTs like the option chain, are still retried with exponential backoff.
|
|
291
|
+
|
|
292
|
+
To reconcile after a timeout, place orders with a correlation id and look the order up by it:
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
DhanHQ.configure do |config|
|
|
296
|
+
config.auto_correlation_id = true # or DHAN_AUTO_CORRELATION_ID=true
|
|
297
|
+
end
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
With this on, any order placement missing a `correlationId` gets one (`dhq-<hex>`), which you
|
|
301
|
+
can then resolve via `GET /v2/orders/external/{correlation-id}`:
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
DhanHQ::Resources::Orders.new.by_correlation("dhq-2f9c1a…")
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
It is off by default because it changes the request body, which would break callers that match
|
|
308
|
+
recorded fixtures on the exact payload. An explicit `correlation_id` is always preserved.
|
|
309
|
+
|
|
310
|
+
If you would rather have the old auto-retry behaviour, opt back in — accepting the duplicate
|
|
311
|
+
risk:
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
DhanHQ.configure { |config| config.retry_non_idempotent_writes = true }
|
|
315
|
+
```
|
|
316
|
+
|
|
222
317
|
### Order Audit Logging
|
|
223
318
|
|
|
224
319
|
Every order attempt (place, modify, slice) automatically logs a structured JSON line at WARN level:
|
|
@@ -342,6 +437,45 @@ DhanHQ::WS::MarketDepth.connect(symbols: [
|
|
|
342
437
|
end
|
|
343
438
|
```
|
|
344
439
|
|
|
440
|
+
### Lifecycle Hooks and Health Checks
|
|
441
|
+
|
|
442
|
+
Subscriptions are restored automatically on reconnect — the server keeps no subscription state
|
|
443
|
+
across connections, so the client replays the desired set on every new session. Hook `:reconnect`
|
|
444
|
+
when you need to re-seed derived state (candle builders, caches) after a gap:
|
|
445
|
+
|
|
446
|
+
```ruby
|
|
447
|
+
client = DhanHQ::WS.connect(mode: :ticker) { |tick| handle(tick) }
|
|
448
|
+
|
|
449
|
+
client.on(:open) { |info| logger.info "feed open, #{info[:resubscribed].size} restored" }
|
|
450
|
+
client.on(:reconnect) { |info| logger.warn "reconnect ##{info[:attempt]}"; candles.reset! }
|
|
451
|
+
client.on(:close) { |info| logger.warn "closed #{info[:code]} #{info[:reason]}" }
|
|
452
|
+
client.on(:error) { |message| logger.error message }
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
A feed can be connected and still be dead — the socket stays open while the server stops
|
|
456
|
+
publishing. For long-running daemons, watch frame arrival rather than the socket alone:
|
|
457
|
+
|
|
458
|
+
```ruby
|
|
459
|
+
client.connected? # socket state
|
|
460
|
+
client.healthy? # connected AND delivering frames (default: within 45s)
|
|
461
|
+
client.healthy?(stale_after: 20) # tighter threshold
|
|
462
|
+
client.last_message_at # Time of the most recent frame
|
|
463
|
+
client.seconds_since_last_message
|
|
464
|
+
client.reconnect_count
|
|
465
|
+
client.subscriptions # ["NSE_EQ:11536", …] — survives reconnects
|
|
466
|
+
|
|
467
|
+
client.health
|
|
468
|
+
# => { mode: :ticker, started: true, connected: true, healthy: true,
|
|
469
|
+
# connected_at: …, last_message_at: …, seconds_since_last_message: 0.4,
|
|
470
|
+
# reconnect_count: 2, subscription_count: 37 }
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
`health` is shaped for a monitoring endpoint or a periodic log line.
|
|
474
|
+
|
|
475
|
+
> **Connection limit:** Dhan allows **5 concurrent WebSocket connections per client id**, with
|
|
476
|
+
> 5,000 instruments per connection and 100 instruments per subscribe frame. Running several
|
|
477
|
+
> strategies in separate processes counts against the same limit — a 6th connection is refused.
|
|
478
|
+
|
|
345
479
|
### Cleanup
|
|
346
480
|
|
|
347
481
|
```ruby
|
|
@@ -373,6 +507,110 @@ DhanHQ::Models::SuperOrder.create(
|
|
|
373
507
|
|
|
374
508
|
---
|
|
375
509
|
|
|
510
|
+
## Basket Orders (Multi Order)
|
|
511
|
+
|
|
512
|
+
Place up to 15 unconditional orders in a single request. Each leg carries a `sequence` that
|
|
513
|
+
identifies it in the response, so partial acceptance is visible:
|
|
514
|
+
|
|
515
|
+
```ruby
|
|
516
|
+
result = DhanHQ::Models::MultiOrder.place([
|
|
517
|
+
{ sequence: "1", transaction_type: "BUY", exchange_segment: "NSE_EQ", product_type: "CNC",
|
|
518
|
+
order_type: "LIMIT", validity: "DAY", security_id: "11536", quantity: 1, price: 1500.0 },
|
|
519
|
+
{ sequence: "2", transaction_type: "BUY", exchange_segment: "NSE_EQ", product_type: "CNC",
|
|
520
|
+
order_type: "MARKET", validity: "DAY", security_id: "1333", quantity: 2 }
|
|
521
|
+
])
|
|
522
|
+
|
|
523
|
+
result.all_accepted? # => true
|
|
524
|
+
result.order_ids # => ["112111182198", "112111182199"]
|
|
525
|
+
result.rejected # legs the exchange refused
|
|
526
|
+
result.partially_accepted? # some in, some out
|
|
527
|
+
result.for_sequence("1").order_id # look a leg up by its sequence
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
Every leg is validated before anything is sent, and each one gets its own audit log line.
|
|
531
|
+
Requires `LIVE_TRADING=true` like any other order path.
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## Global Stocks (US Equities)
|
|
536
|
+
|
|
537
|
+
US stocks are a **separate book** from domestic NSE/BSE trading: their own order book,
|
|
538
|
+
holdings, trades, and a USD fund limit. They are namespaced under `GlobalStocks` so a USD
|
|
539
|
+
position can never be mistaken for an INR one.
|
|
540
|
+
|
|
541
|
+
```ruby
|
|
542
|
+
# Is the US market open?
|
|
543
|
+
DhanHQ::Models::GlobalStocks::MarketStatus.fetch.open?
|
|
544
|
+
|
|
545
|
+
# USD balance and portfolio
|
|
546
|
+
DhanHQ::Models::GlobalStocks::Funds.fetch.available_cash
|
|
547
|
+
DhanHQ::Models::GlobalStocks::Holding.all.each do |h|
|
|
548
|
+
puts "#{h.trading_symbol}: #{h.quantity} @ $#{h.avg_cost_price} (#{h.gain_percentage}%)"
|
|
549
|
+
end
|
|
550
|
+
DhanHQ::Models::GlobalStocks::Holding.total_current_value
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### Placing US orders
|
|
554
|
+
|
|
555
|
+
Global Stocks orders carry **no exchange segment, product type, or validity** — and quantity
|
|
556
|
+
is a float, because fractional shares are supported:
|
|
557
|
+
|
|
558
|
+
```ruby
|
|
559
|
+
DhanHQ::Models::GlobalStocks::Order.place(
|
|
560
|
+
transaction_type: DhanHQ::Constants::TransactionType::BUY,
|
|
561
|
+
order_type: DhanHQ::Constants::GlobalStocks::OrderType::LIMIT,
|
|
562
|
+
security_id: "AAPL",
|
|
563
|
+
quantity: 0.5, # fractional shares
|
|
564
|
+
price: 180.0
|
|
565
|
+
)
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
`AMOUNT` is a notional order type unique to this book — you name a dollar value instead of a
|
|
569
|
+
share count:
|
|
570
|
+
|
|
571
|
+
```ruby
|
|
572
|
+
DhanHQ::Models::GlobalStocks::Order.place(
|
|
573
|
+
transaction_type: DhanHQ::Constants::TransactionType::BUY,
|
|
574
|
+
order_type: DhanHQ::Constants::GlobalStocks::OrderType::AMOUNT,
|
|
575
|
+
security_id: "AAPL",
|
|
576
|
+
amount: 500.0 # buy $500 worth
|
|
577
|
+
)
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### Pre-trade checks
|
|
581
|
+
|
|
582
|
+
```ruby
|
|
583
|
+
params = { security_id: "AAPL", transaction_type: "BUY", price: 180.0, quantity: 10 }
|
|
584
|
+
|
|
585
|
+
estimate = DhanHQ::Models::GlobalStocks::OrderEstimate.calculate(params)
|
|
586
|
+
estimate.total_charges # brokerage + exchange + GST + turnover + other
|
|
587
|
+
|
|
588
|
+
margin = DhanHQ::Models::GlobalStocks::Margin.calculate(params)
|
|
589
|
+
margin.sufficient? # false when the account is short
|
|
590
|
+
margin.total_margin
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### Order and trade books
|
|
594
|
+
|
|
595
|
+
```ruby
|
|
596
|
+
DhanHQ::Models::GlobalStocks::Order.all # today's US order book
|
|
597
|
+
DhanHQ::Models::GlobalStocks::Order.find(order_id)
|
|
598
|
+
DhanHQ::Models::GlobalStocks::Trade.all # today's US fills
|
|
599
|
+
DhanHQ::Models::GlobalStocks::Trade.find_by_security_id("AAPL")
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Writes go through the same `LIVE_TRADING` gate, audit logging, and dry-run handling as
|
|
603
|
+
domestic orders. The pre-trade `Risk::Pipeline` is deliberately **not** applied: its checks
|
|
604
|
+
resolve instruments from the Indian scrip master and encode NSE/BSE rules (lot sizes, ASM/GSM
|
|
605
|
+
surveillance, F&O product support, IST market hours), none of which apply to US equities. Use
|
|
606
|
+
`Margin#sufficient?` and `OrderEstimate` as the pre-trade gate here instead.
|
|
607
|
+
|
|
608
|
+
The Global Stocks live feed (`wss://global-stocks-api-feed.dhan.co`, segment `INX_EQ`, code
|
|
609
|
+
`14`) is documented in `DhanHQ::Constants::GlobalStocks`; its binary packet decoder is not yet
|
|
610
|
+
implemented.
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
376
614
|
## Real-World Example: NIFTY Trend Monitor
|
|
377
615
|
|
|
378
616
|
```ruby
|
|
@@ -512,7 +750,7 @@ Claude Desktop config (`claude_desktop_config.json`):
|
|
|
512
750
|
|
|
513
751
|
| Feature | Description |
|
|
514
752
|
| ------- | ----------- |
|
|
515
|
-
| **Tools** | 12
|
|
753
|
+
| **Tools** | 32 total: 12 domestic primitives (profile, funds, holdings, positions, order history, order preview/place/cancel, instruments, market feed) + 9 Global Stocks and basket tools (`dhan_global_*`, `dhan_multi_order`) + 11 skill-derived tools (`dhan_skill_*` — one per builtin strategy in [Skills System](#skills-system) below) |
|
|
516
754
|
| **Resources** | 6 URI-addressable data endpoints: `dhanhq://account/profile`, `dhanhq://account/funds`, `dhanhq://account/holdings`, `dhanhq://account/positions`, `dhanhq://account/orders`, `dhanhq://market/capabilities` |
|
|
517
755
|
| **Prompts** | 5 pre-built AI prompts: `portfolio_summary`, `market_analysis`, `risk_report`, `order_preview`, `suggest_strategy` |
|
|
518
756
|
|
data/docs/AUTHENTICATION.md
CHANGED
|
@@ -54,7 +54,7 @@ If the token was **generated from Dhan Web** (not API key flow), you can refresh
|
|
|
54
54
|
|
|
55
55
|
```ruby
|
|
56
56
|
# Returns a hash with API response (e.g. accessToken, expiryTime). Use the new token for subsequent requests.
|
|
57
|
-
response = DhanHQ::Auth.renew_token(current_access_token, client_id)
|
|
57
|
+
response = DhanHQ::Auth.renew_token(access_token: current_access_token, client_id: client_id)
|
|
58
58
|
new_token = response["accessToken"] || response[:accessToken]
|
|
59
59
|
# Optional: response may include "expiryTime"
|
|
60
60
|
```
|
|
@@ -68,7 +68,7 @@ Example: refresh in provider and cache the result until near expiry:
|
|
|
68
68
|
config.access_token_provider = lambda do
|
|
69
69
|
stored = YourTokenStore.current
|
|
70
70
|
if stored.nil? || stored.expired_soon?
|
|
71
|
-
response = DhanHQ::Auth.renew_token(stored&.access_token || ENV["DHAN_ACCESS_TOKEN"], config.client_id)
|
|
71
|
+
response = DhanHQ::Auth.renew_token(access_token: stored&.access_token || ENV["DHAN_ACCESS_TOKEN"], client_id: config.client_id)
|
|
72
72
|
YourTokenStore.update!(response["accessToken"], response["expiryTime"])
|
|
73
73
|
stored = YourTokenStore.current
|
|
74
74
|
end
|
|
@@ -219,6 +219,4 @@ Central service: configure_from_token_endpoint
|
|
|
219
219
|
|
|
220
220
|
Fully automated: TOTP generate (Auth / Client#generate_access_token)
|
|
221
221
|
|
|
222
|
-
Production-grade automation: enable_auto_token_management! (generate + renew)
|
|
223
|
-
|
|
224
|
-
If you tell me your exact deployment style (single user box, multi-user SaaS, on-prem, etc.), I can tell you which one you should actually use and what to delete as overkill.
|
|
222
|
+
Production-grade automation: enable_auto_token_management! (generate + renew)
|
data/docs/CONFIGURATION.md
CHANGED
|
@@ -9,7 +9,7 @@ require 'dhan_hq'
|
|
|
9
9
|
DhanHQ.configure_with_env
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
`configure_with_env` reads `DHAN_CLIENT_ID` and `DHAN_ACCESS_TOKEN` from `ENV
|
|
12
|
+
`configure_with_env` reads `DHAN_CLIENT_ID` and `DHAN_ACCESS_TOKEN` from `ENV`. It does **not** raise if either is missing — they're simply left `nil`, and a `DhanHQ::AuthenticationError` (or similar) surfaces later, only when a request actually needs the token.
|
|
13
13
|
|
|
14
14
|
## Required Environment Variables
|
|
15
15
|
|
|
@@ -25,7 +25,8 @@ Set these _before_ calling `configure_with_env` to override defaults:
|
|
|
25
25
|
| Variable | Default | Description |
|
|
26
26
|
| -------------------------------- | ---------- | ------------------------------------------------------- |
|
|
27
27
|
| `DHAN_LOG_LEVEL` | `INFO` | Logger verbosity (`DEBUG`, `INFO`, `WARN`, `ERROR`) |
|
|
28
|
-
| `
|
|
28
|
+
| `DHAN_SANDBOX` | `false` | Set `"true"` to route REST calls to `https://sandbox.dhan.co/v2` instead of production. 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_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
30
|
| `DHAN_WS_VERSION` | latest | Pin WebSocket connections to a specific API version |
|
|
30
31
|
| `DHAN_WS_ORDER_URL` | Dhan prod | Override the order update WebSocket endpoint |
|
|
31
32
|
| `DHAN_WS_USER_TYPE` | `SELF` | Switch between `SELF` and `PARTNER` streaming modes |
|
data/docs/CONSTANTS_REFERENCE.md
CHANGED
|
@@ -17,8 +17,9 @@ Exchange segments for different markets and instruments.
|
|
|
17
17
|
| `NSE_EQ` | `"NSE_EQ"` | NSE - Equity Cash | 1 |
|
|
18
18
|
| `NSE_FNO` | `"NSE_FNO"` | NSE - Futures & Options | 2 |
|
|
19
19
|
| `NSE_CURRENCY` | `"NSE_CURRENCY"` | NSE - Currency | 3 |
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
20
|
+
| `NSE_COMM` | `"NSE_COMM"` | NSE - Commodity | 4 |
|
|
21
|
+
| `BSE_EQ` | `"BSE_EQ"` | BSE - Equity Cash | 5 |
|
|
22
|
+
| `MCX_COMM` | `"MCX_COMM"` | MCX - Commodity | 6 |
|
|
22
23
|
| `BSE_CURRENCY` | `"BSE_CURRENCY"` | BSE - Currency | 7 |
|
|
23
24
|
| `BSE_FNO` | `"BSE_FNO"` | BSE - Futures & Options | 8 |
|
|
24
25
|
|
|
@@ -431,7 +432,7 @@ end
|
|
|
431
432
|
|
|
432
433
|
### Validation Helpers
|
|
433
434
|
|
|
434
|
-
|
|
435
|
+
Already implemented in `lib/DhanHQ/constants.rb`:
|
|
435
436
|
|
|
436
437
|
```ruby
|
|
437
438
|
module DhanHQ
|
|
@@ -439,7 +440,7 @@ module DhanHQ
|
|
|
439
440
|
def self.valid?(module_name, value)
|
|
440
441
|
const_get(module_name)::ALL.include?(value)
|
|
441
442
|
end
|
|
442
|
-
|
|
443
|
+
|
|
443
444
|
def self.all_for(module_name)
|
|
444
445
|
const_get(module_name)::ALL
|
|
445
446
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Sandbox behavior
|
|
4
4
|
|
|
5
|
+
- **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.
|
|
5
6
|
- **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
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 `sandbox` setting. There are no sandbox WebSocket endpoints in the Dhan v2 API; do not rely on sandbox for real-time streams.
|
|
7
8
|
- **Auth endpoints** (`DhanHQ::Auth`) do **not** use sandbox. They always call:
|
data/docs/LIVE_ORDER_UPDATES.md
CHANGED
|
@@ -320,24 +320,19 @@ This ensures safe usage in multi-threaded applications.
|
|
|
320
320
|
|
|
321
321
|
## Testing
|
|
322
322
|
|
|
323
|
-
For comprehensive testing examples
|
|
323
|
+
For comprehensive testing examples, see the [Testing Guide](TESTING_GUIDE.md). It covers:
|
|
324
324
|
|
|
325
325
|
- **Order Update WebSocket Testing**: Complete examples for all order update features
|
|
326
326
|
- **Event Handler Testing**: Examples for all event types
|
|
327
327
|
- **Order State Management**: Testing order tracking and querying
|
|
328
|
-
- **Interactive Console Helpers**: Load `bin/test_helpers.rb` for quick test functions
|
|
329
328
|
|
|
330
329
|
**Quick start in console:**
|
|
331
330
|
```ruby
|
|
332
331
|
# Start console
|
|
333
332
|
bin/console
|
|
334
333
|
|
|
335
|
-
#
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
test_order_websocket(10) # Test order updates for 10 seconds
|
|
340
|
-
|
|
341
|
-
# Monitor specific order
|
|
342
|
-
monitor_order("112111182045") # Monitor order by ID
|
|
334
|
+
# Test order WebSocket for 10 seconds
|
|
335
|
+
client = DhanHQ::WS::Orders.connect { |update| puts update.inspect }
|
|
336
|
+
sleep 10
|
|
337
|
+
client.stop
|
|
343
338
|
```
|
data/docs/RAILS_INTEGRATION.md
CHANGED
|
@@ -122,7 +122,7 @@ end
|
|
|
122
122
|
|
|
123
123
|
When the API returns 401 or token-expired (error code 807) and `access_token_provider` is set, the client retries the request **once** with a fresh token from the provider. `on_token_expired` is called before that retry so you can refresh your store if needed.
|
|
124
124
|
|
|
125
|
-
**RenewToken (web-generated tokens only):** If the token was generated from Dhan Web (24h validity), you can refresh it with `DhanHQ::Auth.renew_token(access_token, client_id)` and store the result; use it in your provider or call it from `on_token_expired`. The gem does **not** implement API key/secret or Partner consent flows—implement those in your app and pass the token to the gem. See [docs/AUTHENTICATION.md](AUTHENTICATION.md).
|
|
125
|
+
**RenewToken (web-generated tokens only):** If the token was generated from Dhan Web (24h validity), you can refresh it with `DhanHQ::Auth.renew_token(access_token: access_token, client_id: client_id)` (keyword arguments) and store the result; use it in your provider or call it from `on_token_expired`. The gem does **not** implement API key/secret or Partner consent flows—implement those in your app and pass the token to the gem. See [docs/AUTHENTICATION.md](AUTHENTICATION.md).
|
|
126
126
|
|
|
127
127
|
## 3. Build service objects for REST flows
|
|
128
128
|
|