DhanHQ 3.1.0 → 3.2.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.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/ARCHITECTURE.md +47 -1
  3. data/CHANGELOG.md +47 -0
  4. data/README.md +241 -3
  5. data/docs/AUTHENTICATION.md +3 -5
  6. data/docs/CONFIGURATION.md +3 -2
  7. data/docs/CONSTANTS_REFERENCE.md +5 -4
  8. data/docs/ENDPOINTS_AND_SANDBOX.md +1 -0
  9. data/docs/LIVE_ORDER_UPDATES.md +5 -10
  10. data/docs/RAILS_INTEGRATION.md +1 -1
  11. data/docs/RELEASE_GUIDE.md +13 -2
  12. data/docs/STANDALONE_RUBY_WEBSOCKET_INTEGRATION.md +2 -2
  13. data/docs/WEBSOCKET_INTEGRATION.md +13 -20
  14. data/docs/WEBSOCKET_PROTOCOL.md +7 -3
  15. data/lib/DhanHQ/agent/key_coercion.rb +36 -0
  16. data/lib/DhanHQ/agent/tool.rb +37 -0
  17. data/lib/DhanHQ/agent/tool_catalogue.rb +180 -0
  18. data/lib/DhanHQ/agent/tool_handlers.rb +116 -0
  19. data/lib/DhanHQ/agent/tool_registry.rb +17 -261
  20. data/lib/DhanHQ/agent/tool_schemas.rb +160 -0
  21. data/lib/DhanHQ/client.rb +58 -2
  22. data/lib/DhanHQ/concerns/order_audit.rb +35 -4
  23. data/lib/DhanHQ/configuration.rb +70 -1
  24. data/lib/DhanHQ/constants.rb +101 -0
  25. data/lib/DhanHQ/contracts/expired_options_data_contract.rb +1 -9
  26. data/lib/DhanHQ/contracts/global_stocks_estimator_contract.rb +28 -0
  27. data/lib/DhanHQ/contracts/global_stocks_modify_order_contract.rb +26 -0
  28. data/lib/DhanHQ/contracts/global_stocks_order_contract.rb +33 -0
  29. data/lib/DhanHQ/contracts/global_stocks_place_order_contract.rb +75 -0
  30. data/lib/DhanHQ/contracts/historical_data_contract.rb +1 -21
  31. data/lib/DhanHQ/contracts/multi_order_contract.rb +74 -0
  32. data/lib/DhanHQ/contracts/trade_history_contract.rb +1 -8
  33. data/lib/DhanHQ/dry_run/ledger.rb +71 -0
  34. data/lib/DhanHQ/dry_run/simulator.rb +140 -0
  35. data/lib/DhanHQ/helpers/attribute_helper.rb +23 -0
  36. data/lib/DhanHQ/models/alert_order.rb +5 -2
  37. data/lib/DhanHQ/models/global_stocks/funds.rb +56 -0
  38. data/lib/DhanHQ/models/global_stocks/holding.rb +101 -0
  39. data/lib/DhanHQ/models/global_stocks/margin.rb +54 -0
  40. data/lib/DhanHQ/models/global_stocks/market_status.rb +63 -0
  41. data/lib/DhanHQ/models/global_stocks/order.rb +189 -0
  42. data/lib/DhanHQ/models/global_stocks/order_estimate.rb +61 -0
  43. data/lib/DhanHQ/models/global_stocks/trade.rb +74 -0
  44. data/lib/DhanHQ/models/margin.rb +5 -1
  45. data/lib/DhanHQ/models/multi_order.rb +130 -0
  46. data/lib/DhanHQ/resources/global_stocks/funds.rb +25 -0
  47. data/lib/DhanHQ/resources/global_stocks/holdings.rb +22 -0
  48. data/lib/DhanHQ/resources/global_stocks/margin_calculator.rb +70 -0
  49. data/lib/DhanHQ/resources/global_stocks/market_status.rb +22 -0
  50. data/lib/DhanHQ/resources/global_stocks/orders.rb +112 -0
  51. data/lib/DhanHQ/resources/global_stocks/trades.rb +31 -0
  52. data/lib/DhanHQ/resources/multi_orders.rb +58 -0
  53. data/lib/DhanHQ/version.rb +1 -1
  54. data/lib/DhanHQ/write_paths.rb +57 -0
  55. data/lib/DhanHQ/ws/client.rb +117 -2
  56. data/lib/DhanHQ/ws/connection.rb +40 -11
  57. data/lib/DhanHQ/ws/sub_state.rb +14 -0
  58. data/lib/dhanhq/analysis/options_buying_advisor.rb +11 -10
  59. metadata +30 -40
  60. data/.rspec +0 -3
  61. data/.rubocop.yml +0 -50
  62. data/.rubocop_todo.yml +0 -217
  63. data/AGENTS.md +0 -23
  64. data/CODE_OF_CONDUCT.md +0 -132
  65. data/Rakefile +0 -14
  66. data/TAGS +0 -10
  67. data/core +0 -0
  68. data/diagram.html +0 -184
  69. data/skills/dhanhq-ruby/SKILL.md +0 -207
  70. data/skills/dhanhq-ruby/examples/fetch_option_chain.rb +0 -54
  71. data/skills/dhanhq-ruby/examples/gtt_forever_order.rb +0 -65
  72. data/skills/dhanhq-ruby/examples/historical_data_analysis.rb +0 -89
  73. data/skills/dhanhq-ruby/examples/iron_condor.rb +0 -137
  74. data/skills/dhanhq-ruby/examples/live_feed_setup.rb +0 -43
  75. data/skills/dhanhq-ruby/examples/margin_check.rb +0 -42
  76. data/skills/dhanhq-ruby/examples/order_management.rb +0 -105
  77. data/skills/dhanhq-ruby/examples/place_equity_order.rb +0 -36
  78. data/skills/dhanhq-ruby/examples/place_fno_order.rb +0 -76
  79. data/skills/dhanhq-ruby/examples/portfolio_summary.rb +0 -74
  80. data/skills/dhanhq-ruby/examples/super_order_with_sl.rb +0 -57
  81. data/skills/dhanhq-ruby/references/backtesting-with-dhan.md +0 -65
  82. data/skills/dhanhq-ruby/references/common-workflows.md +0 -76
  83. data/skills/dhanhq-ruby/references/error-codes.md +0 -50
  84. data/skills/dhanhq-ruby/references/funds.md +0 -67
  85. data/skills/dhanhq-ruby/references/instruments.md +0 -85
  86. data/skills/dhanhq-ruby/references/live-feed.md +0 -83
  87. data/skills/dhanhq-ruby/references/market-data.md +0 -119
  88. data/skills/dhanhq-ruby/references/option-chain.md +0 -71
  89. data/skills/dhanhq-ruby/references/options-analysis-patterns.md +0 -76
  90. data/skills/dhanhq-ruby/references/orders.md +0 -201
  91. data/skills/dhanhq-ruby/references/portfolio.md +0 -93
  92. data/skills/dhanhq-ruby/references/scanx-data.md +0 -62
  93. data/skills/dhanhq-ruby/scripts/dhan_helpers.rb +0 -323
  94. data/skills/dhanhq-ruby/scripts/resolve_security.rb +0 -168
  95. data/skills/dhanhq-ruby/scripts/trade_logger.rb +0 -131
  96. data/skills/dhanhq-ruby/scripts/validate_order.rb +0 -169
  97. data/watchlist.csv +0 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aa2811b4faf1f45f3ddc18b5f0b1ae03f481aa30fe48f1b1be3b3a928f87c581
4
- data.tar.gz: cd4829e129d27e6b939bedaac18828f44237674ef5c84135ce9a647c4af37bcc
3
+ metadata.gz: 474564a108e478d3e2cc7bb0ad2b313a885b265ca3ece5091bd04dc1bb31b014
4
+ data.tar.gz: ab2a03010eb2ca03f7e6c9a01011b1b4b84f805611f22c55f7c2c07af1a7e063
5
5
  SHA512:
6
- metadata.gz: 2b8fedf28da427de98161f6d2288230dc6e34a112bbfb56bda85d6373f0d2489cdfa8c2427facf0dc98e729790b4ee2023f534888f1b84989885993e9272545a
7
- data.tar.gz: 11500ff76de75b2f9b9205e7f5af1210940291fe05b5c53b5f40d4f91e1a8d1c56346e8adb65c1fb2e2a36509b0c80f198f9264d402be8ced0622057fb288d64
6
+ metadata.gz: 531c7d4fba8e753c2371569a47562ed32cd026c8a2828e28bb7b617f5f342bec870ace4fac6f95b4e122b360c164c42a65a34a90899e2e100b18e7cc594ffea0
7
+ data.tar.gz: 82332825d24456beeb67b61836472c05047ff323c3826f2f8324ac06ae036c10e9324788a43e1e7dbdcc74fb60cdbc385aaf373d99d851bb6732c51f6bf7638b
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. Documented in code and specs; not expanded here.
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,50 @@
1
+ ## [3.2.0] - 2026-07-25
2
+
3
+ ### Added
4
+
5
+ - **Global Stocks (US equities) API** — full coverage of the 11 `/v2/globalstocks/*` endpoints, previously absent:
6
+ - Resources: `Resources::GlobalStocks::Orders` (place/modify/cancel/find/all), `Trades` (all + by security id), `Holdings`, `Funds`, `MarginCalculator` (margin + charge estimate), `MarketStatus`.
7
+ - Models: `Models::GlobalStocks::Order`, `Trade`, `Holding`, `Funds`, `Margin`, `OrderEstimate`, `MarketStatus`.
8
+ - Contracts: `GlobalStocksPlaceOrderContract`, `GlobalStocksModifyOrderContract`, `GlobalStocksEstimatorContract`.
9
+ - Namespaced under `GlobalStocks` so a USD position can never be mistaken for an INR one.
10
+ - 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.
11
+ - 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.
12
+ - **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.
13
+ - **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.
14
+ - **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.
15
+ - **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.
16
+ - **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.
17
+ - **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.
18
+ - **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.
19
+ - **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.
20
+ - **`DhanHQ::WritePaths`** — path taxonomy (`mutating?`, `order_placement?`, `multi_order?`) extracted out of `Client`, which no longer needs to know it.
21
+ - **`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.
22
+ - **`AttributeHelper#deep_camelize_keys`** — recursive key camelization for the endpoints whose bodies nest objects (basket order legs, alert conditions).
23
+
24
+ ### Changed
25
+
26
+ - **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.)
27
+ - **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.
28
+ - `WS::Connection#initialize` accepts an optional `on_event:` keyword; the socket-open handling moved into a `handle_open` method.
29
+ - `Configuration` reads boolean env vars through a shared helper, so an empty value is treated as unset rather than false.
30
+ - 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.
31
+ - **`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.
32
+ - `.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`.
33
+
34
+ ### Fixed
35
+
36
+ - **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.
37
+ - **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`.
38
+ - **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.
39
+ - **`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.
40
+ - `DhanHQ::NetworkError` no longer reports "Request failed after 0 retries" when retries are disabled.
41
+
42
+ ### Tests
43
+
44
+ - 1,052 examples, 0 failures (up from 833); RuboCop clean.
45
+ - 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).
46
+ - 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.
47
+
1
48
  ## [3.1.0] - 2026-07-20
2
49
 
3
50
  ### 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
- - **Full REST coverage** — Orders, Trades, Forever Orders, Super Orders, Positions, Holdings, Funds, HistoricalData, OptionChain, MarketFeed, EDIS, Kill Switch, P&L Exit, Alert Orders, Margin Calculator
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 backoff
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 primitive tools (profile, funds, holdings, positions, order history, order preview/place/cancel, instruments, market feed) + 11 skill-derived tools (`dhan_skill_*` — one per builtin strategy in [Skills System](#skills-system) below) |
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
 
@@ -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)
@@ -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` and raises if either is missing.
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
- | `DHAN_BASE_URL` | Dhan prod | Point REST calls to a different API hostname |
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 |
@@ -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
- | `BSE_EQ` | `"BSE_EQ"` | BSE - Equity Cash | 4 |
21
- | `MCX_COMM` | `"MCX_COMM"` | MCX - Commodity | 5 |
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
- Consider adding validation helpers:
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:
@@ -320,24 +320,19 @@ This ensures safe usage in multi-threaded applications.
320
320
 
321
321
  ## Testing
322
322
 
323
- For comprehensive testing examples and interactive console helpers, see the [Testing Guide](TESTING_GUIDE.md). The guide includes:
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
- # Load test helpers
336
- load 'bin/test_helpers.rb'
337
-
338
- # Test order WebSocket
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
  ```
@@ -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
 
@@ -148,8 +148,9 @@ When you push a tag (e.g., `v2.1.12`), the **Release** workflow (`.github/workfl
148
148
  2. ✅ **Generate OTP code** from secret `RUBYGEMS_OTP_SECRET`
149
149
  3. ✅ **Build gem** using `gem build DhanHQ.gemspec`
150
150
  4. ✅ **Push to RubyGems** with OTP using `GEM_HOST_API_KEY` (from secret `RUBYGEMS_API_KEY`)
151
+ 5. ✅ **Attach gem to GitHub Release** — uploads `DhanHQ-<version>.gem` as a downloadable asset on the GitHub Release for the tag, creating the release (with auto-generated notes) if it doesn't already exist, or uploading to it if it does
151
152
 
152
- Tests run on push/PR via the main CI workflow; the release job does not run tests. No GitHub Release is created (tag-only publish, same pattern as ollama-client).
153
+ Tests run on push/PR via the main CI workflow; the release job does not run tests. A GitHub Release page is created/updated with the built `.gem` attached as a downloadable asset (verified working on the real v3.1.0 release: https://github.com/shubhamtaywade82/dhanhq-client/releases/tag/v3.1.0).
153
154
 
154
155
  **No manual intervention needed!** ⚡
155
156
 
@@ -286,6 +287,16 @@ on:
286
287
  gem push "DhanHQ-${gem_version}.gem" --otp "$otp_code"
287
288
  ```
288
289
 
290
+ 5. **Attach gem to GitHub Release**
291
+ ```bash
292
+ # Creates the release (with --generate-notes) if it doesn't exist yet, else uploads to it
293
+ if gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
294
+ gh release upload "$tag" "$gem_file" --repo "$GITHUB_REPOSITORY" --clobber
295
+ else
296
+ gh release create "$tag" "$gem_file" --repo "$GITHUB_REPOSITORY" --title "$tag" --generate-notes
297
+ fi
298
+ ```
299
+
289
300
  ### Security Features
290
301
 
291
302
  - 🔒 **MFA Required** - `rubygems_mfa_required = "true"` in gemspec
@@ -476,7 +487,7 @@ The old workflow (`.github/workflows/main.yml`):
476
487
  ✅ **MFA secured:** OTP authentication on every release
477
488
  ✅ **Production ready:** Battle-tested release workflow
478
489
  ✅ **Tests integrated:** Ensures quality before release
479
- ✅ **Tag-only release:** Same pattern as ollama-client; push tag `v*` to publish
490
+ ✅ **Tag-triggered release:** push tag `v*` to publish to RubyGems and attach the built gem to a GitHub Release
480
491
 
481
492
  Questions or issues? Check the [Troubleshooting](#troubleshooting) section above.
482
493
 
@@ -996,8 +996,8 @@ class MarketDataCLI
996
996
  volume: full[:vol],
997
997
  day_high: full[:day_high],
998
998
  day_low: full[:day_low],
999
- open: full[:open],
1000
- close: full[:close],
999
+ open: full[:day_open],
1000
+ close: full[:day_close],
1001
1001
  timestamp: timestamp.iso8601
1002
1002
  }
1003
1003