DhanHQ 2.6.2 → 2.6.3

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -3
  3. data/ARCHITECTURE.md +113 -0
  4. data/CHANGELOG.md +31 -0
  5. data/README.md +2 -0
  6. data/docs/API_VERIFICATION.md +10 -8
  7. data/docs/ENDPOINTS_AND_SANDBOX.md +12 -0
  8. data/lib/DhanHQ/auth.rb +2 -2
  9. data/lib/DhanHQ/client.rb +42 -34
  10. data/lib/DhanHQ/configuration.rb +5 -6
  11. data/lib/DhanHQ/constants.rb +67 -7
  12. data/lib/DhanHQ/contracts/alert_order_contract.rb +23 -16
  13. data/lib/DhanHQ/contracts/expired_options_data_contract.rb +4 -2
  14. data/lib/DhanHQ/contracts/forever_order_contract.rb +55 -0
  15. data/lib/DhanHQ/contracts/historical_data_contract.rb +17 -19
  16. data/lib/DhanHQ/contracts/intraday_historical_data_contract.rb +12 -0
  17. data/lib/DhanHQ/contracts/margin_calculator_contract.rb +19 -17
  18. data/lib/DhanHQ/contracts/market_feed_contract.rb +42 -0
  19. data/lib/DhanHQ/contracts/multi_scrip_margin_calc_request_contract.rb +8 -5
  20. data/lib/DhanHQ/contracts/option_chain_contract.rb +17 -19
  21. data/lib/DhanHQ/contracts/pnl_based_exit_contract.rb +1 -1
  22. data/lib/DhanHQ/contracts/slice_order_contract.rb +10 -10
  23. data/lib/DhanHQ/core/auth_api.rb +1 -1
  24. data/lib/DhanHQ/core/base_api.rb +9 -9
  25. data/lib/DhanHQ/core/base_model.rb +4 -1
  26. data/lib/DhanHQ/core/error_handler.rb +2 -2
  27. data/lib/DhanHQ/errors.rb +14 -2
  28. data/lib/DhanHQ/helpers/request_helper.rb +11 -2
  29. data/lib/DhanHQ/helpers/response_helper.rb +48 -19
  30. data/lib/DhanHQ/helpers/validation_helper.rb +4 -2
  31. data/lib/DhanHQ/models/alert_order.rb +6 -2
  32. data/lib/DhanHQ/models/edis.rb +20 -13
  33. data/lib/DhanHQ/models/expired_options_data.rb +54 -44
  34. data/lib/DhanHQ/models/forever_order.rb +16 -7
  35. data/lib/DhanHQ/models/historical_data.rb +40 -6
  36. data/lib/DhanHQ/models/instrument_helpers.rb +2 -1
  37. data/lib/DhanHQ/models/margin.rb +62 -82
  38. data/lib/DhanHQ/models/market_feed.rb +14 -3
  39. data/lib/DhanHQ/models/option_chain.rb +50 -150
  40. data/lib/DhanHQ/models/order.rb +19 -4
  41. data/lib/DhanHQ/resources/alert_orders.rb +1 -1
  42. data/lib/DhanHQ/resources/edis.rb +4 -3
  43. data/lib/DhanHQ/resources/instruments.rb +3 -2
  44. data/lib/DhanHQ/resources/ip_setup.rb +4 -1
  45. data/lib/DhanHQ/resources/kill_switch.rb +7 -1
  46. data/lib/DhanHQ/resources/orders.rb +1 -1
  47. data/lib/DhanHQ/resources/super_orders.rb +8 -2
  48. data/lib/DhanHQ/resources/trader_control.rb +13 -4
  49. data/lib/DhanHQ/version.rb +1 -1
  50. data/lib/DhanHQ/ws/base_connection.rb +1 -1
  51. data/lib/DhanHQ/ws/market_depth/client.rb +11 -4
  52. data/lib/dhan_hq.rb +17 -20
  53. data/lib/ta/indicators.rb +15 -18
  54. metadata +6 -9
  55. data/CODE_REVIEW_ISSUES.md +0 -397
  56. data/FIXES_APPLIED.md +0 -373
  57. data/RELEASING.md +0 -60
  58. data/REVIEW_SUMMARY.md +0 -120
  59. data/VERSION_UPDATE.md +0 -82
  60. data/diagram.md +0 -34
  61. data/docs/ARCHIVE_README.md +0 -784
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4469f87602b1825aa53ac5d53ef3493fb3a6fd40b00fd58199f23ba3c4a3e129
4
- data.tar.gz: 7b5c65ab06cf2f945f488af2ab63499eb9a5fe2cea6ec04579db12a227ac121e
3
+ metadata.gz: ef2616285615327c5e0ad08a85440d91617ea66818add0a55e4cd6c5e65512aa
4
+ data.tar.gz: 4232cfcd4c9626683e3bf993f75aa0a8add2f8c83702435c66edb2c58a3e252e
5
5
  SHA512:
6
- metadata.gz: 13683dc64df357c014a4427bbf3ee9981f3c52ad12795cd7f08d02e3d2cab3597efdb3668f63521f3bd2d43d616ac5ba577b11e104b304738c1b1c0769438bd2
7
- data.tar.gz: cc2992611f9d3d4c785be89edc8bb3c33bffb6170a2575a20c532c8ff4ee9838bc1bf0021cdec6327349ff008fa7ae278b8e56d7cc2286310df3d0f4cd604292
6
+ metadata.gz: c0ba9cd2119899ed6c9eaefe27601b142e75abbc541538f241058712b82bccdc5fab6af888489c5ac01423c1bfb387e42cdfcee22487bbafce6d32c7ce6f54b2
7
+ data.tar.gz: 4107fc9f9a5ced70692469c21a88f2ed6979739af7e3ef5444d1506e5ba574ba4124f2f7f264747a147157164610606ff22408dcd3612649cb19b9f1e558ca8a
data/.rubocop.yml CHANGED
@@ -28,9 +28,21 @@ RSpec/ExampleLength:
28
28
  RSpec/MultipleMemoizedHelpers:
29
29
  Max: 10
30
30
 
31
+ Lint/ScriptPermission:
32
+ Exclude:
33
+ - "bin/**/*"
34
+
35
+ Metrics/AbcSize:
36
+ Exclude:
37
+ - "bin/call_all_endpoints.rb"
38
+
39
+ Layout/LineLength:
40
+ Exclude:
41
+ - "bin/call_all_endpoints.rb"
42
+
31
43
  DhanHQ/UseConstants:
32
44
  Enabled: true
33
45
  Exclude:
34
- - 'lib/DhanHQ/constants.rb'
35
- - 'lib/DhanHQ/ws/segments.rb'
36
- - 'spec/**/*'
46
+ - "lib/DhanHQ/constants.rb"
47
+ - "lib/DhanHQ/ws/segments.rb"
48
+ - "spec/**/*"
data/ARCHITECTURE.md ADDED
@@ -0,0 +1,113 @@
1
+ # dhanhq-client Architecture
2
+
3
+ This document describes the architecture of the DhanHQ v2 API client gem: layers, dependencies, and design patterns in use.
4
+
5
+ ## Guiding principles
6
+
7
+ - **Dependency rule**: High-level policy (models, domain) does not depend on low-level details (HTTP, JSON). Infrastructure (Client, BaseAPI) depends on configuration and helpers; models depend on resources (abstractions) and contracts.
8
+ - **Single responsibility**: Each layer has one reason to change. Models own domain behavior; resources own HTTP; contracts own validation rules.
9
+ - **Open/closed**: New endpoints are added by adding new Model + Resource + Contract pairs without modifying BaseAPI or BaseModel core.
10
+ - **Don’t force patterns**: Patterns (Strategy, Factory Method, Facade) emerged from refactoring; we avoid speculative abstraction.
11
+
12
+ ---
13
+
14
+ ## Layer overview
15
+
16
+ ```
17
+ ┌─────────────────────────────────────────────────────────────────┐
18
+ │ Entry & configuration (lib/dhan_hq.rb, Configuration) │
19
+ ├─────────────────────────────────────────────────────────────────┤
20
+ │ Domain / facade layer (Models) │
21
+ │ Order, Position, MarketFeed, OptionChain, ExpiredOptionsData… │
22
+ │ → validate via Contracts, delegate HTTP to Resources │
23
+ ├─────────────────────────────────────────────────────────────────┤
24
+ │ REST / HTTP layer (Resources, BaseAPI, BaseResource) │
25
+ │ → build path, format params, call Client │
26
+ ├─────────────────────────────────────────────────────────────────┤
27
+ │ Transport layer (Client, RateLimiter, RequestHelper, │
28
+ │ ResponseHelper) │
29
+ │ → Faraday, headers, retries, error mapping │
30
+ ├─────────────────────────────────────────────────────────────────┤
31
+ │ Validation (Contracts) │
32
+ │ → Dry::Validation, shared macros in BaseContract │
33
+ ├─────────────────────────────────────────────────────────────────┤
34
+ │ WebSocket (WS::*) — separate subsystem │
35
+ └─────────────────────────────────────────────────────────────────┘
36
+ ```
37
+
38
+ ---
39
+
40
+ ## Directory structure and roles
41
+
42
+ | Path | Role | Responsibility |
43
+ |------|------|----------------|
44
+ | `lib/dhan_hq.rb` | Entry point | Zeitwerk setup, eager load of core/helpers/errors, `DhanHQ.configure` |
45
+ | `core/` | Base abstractions | BaseAPI (HTTP verbs, path building, param formatting), BaseModel (attributes, resource, validation, CRUD helpers), BaseResource (CRUD on BaseAPI), AuthAPI, ErrorHandler |
46
+ | `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
+ | `models/` | Domain / facade | Typed wrappers (Order, Position, Holding, etc.). Define `resource`, optional `validation_contract`, and domain methods. Validate then delegate to resource. |
48
+ | `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
+ | `contracts/` | Request/response validation | Dry::Validation contracts (PlaceOrderContract, ModifyOrderContract, OptionChainContract, etc.). BaseContract provides shared macros (e.g. lot_size, tick_size). |
50
+ | `auth/` | Token lifecycle | Token generator/renewal/manager for dynamic tokens. |
51
+ | `ws/` | WebSocket | Connection, packets, decoder, market depth, orders client — isolated from REST. |
52
+
53
+ ---
54
+
55
+ ## Dependency flow
56
+
57
+ - **Configuration** is global (`DhanHQ.configuration`). Client and helpers read it (access_token, client_id, base_url).
58
+ - **Models** depend on:
59
+ - **Resources** (Factory Method: `resource` returns the right BaseAPI subclass)
60
+ - **Contracts** (for validate_params!)
61
+ - **Helpers** (via BaseModel: ValidationHelper, RequestHelper, ResponseHelper, AttributeHelper, APIHelper)
62
+ - **Resources** depend on:
63
+ - **Client** (injected via BaseAPI: `DhanHQ::Client.new(api_type)`)
64
+ - **Helpers** (BaseAPI includes APIHelper, AttributeHelper; Client uses RequestHelper, ResponseHelper)
65
+ - **Client** depends on Configuration, RateLimiter, and helpers. No dependency on Models or Resources.
66
+ - **Contracts** depend on Constants (and optional instrument_meta). No dependency on Models or HTTP.
67
+
68
+ So: **Models → Resources → Client**; **Contracts** are used by Models (and optionally Resources); **Helpers** are used by Client, BaseAPI, and BaseModel.
69
+
70
+ ---
71
+
72
+ ## Design patterns in use
73
+
74
+ | Pattern | Where | Purpose |
75
+ |--------|--------|--------|
76
+ | **Facade** | Models (e.g. `Order.place`, `MarketFeed.ltp`) | Single entry point for “place order” or “get LTP”; hide validation, normalization, and resource call. |
77
+ | **Factory Method** | BaseModel `resource` | Subclasses override `resource` to return the correct REST wrapper (e.g. Orders, OptionChain) without callers knowing the class. |
78
+ | **Strategy** | BaseAPI `param_formatter_for(full_path)` | Choose how to format params by path: pass-through (marketfeed), titleize (optionchain), default camelize. Encapsulated in lambdas. |
79
+ | **Template Method** | BaseModel `save` | `save` calls `new_record? ? create : update`; subclasses override `create`/`update` or collection methods. |
80
+ | **Adapter** | RequestHelper / ResponseHelper | Adapt external API (headers, JSON, status codes) to internal hashes and error classes. |
81
+ | **Singleton (per API type)** | RateLimiter.for(api_type) | One rate limiter per API type so all clients share limits. |
82
+
83
+ ---
84
+
85
+ ## Error handling
86
+
87
+ - **Validation failures**: Raised as `DhanHQ::ValidationError` with a message that includes contract errors (e.g. `"Invalid parameters: #{result.errors.to_h}"`). Used by ValidationHelper, ExpiredOptionsData, Trade, and any code that validates via contracts.
88
+ - **HTTP / API errors**: Mapped in ResponseHelper (e.g. 401 → InvalidAuthenticationError, 807 → TokenExpiredError) and raised as appropriate `DhanHQ::*` subclasses.
89
+ - **Client** retries on auth failures (once, with token refresh) and on transient/network errors (with backoff).
90
+
91
+ ---
92
+
93
+ ## Configuration
94
+
95
+ - Credentials and URLs live in `DhanHQ::Configuration` (access_token, client_id, base_url, sandbox, optional access_token_provider).
96
+ - `DhanHQ.configure { }`, `configure_with_env`, and `configure_from_token_endpoint` set configuration. Never hardcode credentials; use env vars or token endpoint.
97
+
98
+ ---
99
+
100
+ ## WebSocket (WS)
101
+
102
+ - Separate subsystem under `DhanHQ::WS`: own connection, packet types, decoder, market depth, orders client.
103
+ - Shares configuration (e.g. access token) but not the REST Client or Resources. Documented in code and specs; not expanded here.
104
+
105
+ ---
106
+
107
+ ## Adding a new API surface
108
+
109
+ 1. **Contract** (optional): Add a Dry::Validation contract under `contracts/` if the endpoint has structured input.
110
+ 2. **Resource**: Add a class under `resources/` inheriting BaseAPI (or BaseResource). Set `HTTP_PATH`, `API_TYPE`; implement methods that call `get`/`post`/`put`/`delete`.
111
+ 3. **Model**: Add a class under `models/` inheriting BaseModel. Override `resource` to return the new resource; override `validation_contract` if needed; implement class/instance methods that call `validate_params!` then `resource.*`.
112
+
113
+ This keeps the dependency rule and keeps each layer focused on one concern.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,34 @@
1
+ ## [2.6.3] - 2026-03-14
2
+
3
+ ### Added
4
+
5
+ - **Constants::Urls**: All canonical Dhan API/auth/WebSocket URLs in one place (`REST_API_BASE`, `SANDBOX_API_BASE`, `AUTH_BASE`, `WS_MARKET_FEED`, `WS_ORDER_UPDATE`, `WS_DEPTH_20`, `WS_DEPTH_200`, `INSTRUMENT_CSV_*`, `DOCS`, `ORIGIN`). Configuration, Auth, and WS defaults now use these constants.
6
+ - **Order modification limit enforcement**: `Order#modify` enforces Dhan’s 25-modifications-per-order cap per instance; the 26th modify raises `DhanHQ::ModificationLimitError` and does not call the API. Count is in-process only (fresh `find` resets it).
7
+ - **DhanHQ::ModificationLimitError**: New error class for the 25-per-order limit (rescuable).
8
+ - **API error payload on exceptions**: Raised API errors now expose the full response body as `error.response_body` (e.g. `errorCode`, `errorMessage`, and any future keys like `errors` or `details`). Useful for logging and debugging.
9
+ - **DH-905 message hint**: For `InputExceptionError` (DH-905), the exception message now includes a note that the Dhan API does not return which field failed, and to check required params and value types for the endpoint.
10
+ - **Kill switch status validation**: `Resources::KillSwitch#update(status)` and `Models::KillSwitch.update(status)` now validate that `status` is `ACTIVATE` or `DEACTIVATE` (case-insensitive); invalid values raise `DhanHQ::ValidationError` before the request.
11
+ - **Super order cancel leg validation**: `Resources::SuperOrders#cancel(order_id, leg_name)` validates `leg_name` against the API path enum (`ENTRY_LEG`, `STOP_LOSS_LEG`, `TARGET_LEG`); invalid values raise `DhanHQ::ValidationError`.
12
+
13
+ ### Changed
14
+
15
+ - **SliceOrderContract**: Aligned with Dhan v2 orders doc — `amoTime` now allows `PRE_OPEN`; validity restricted to `DAY`/`IOC`; product type restricted to `CNC`/`INTRADAY`/`MARGIN`/`MTF`; `correlationId` max length 30 (was 25).
16
+ - **Order#modify** YARD: Documented modification limit and `ModificationLimitError`.
17
+ - **Error#initialize**: `DhanHQ::Error` now accepts an optional `response_body:` keyword argument so API-raised errors can carry the parsed response. Subclasses unchanged; `raise ErrorClass, "msg"` still works.
18
+ - **ResponseHelper**: When the API returns extra keys (`errors`, `details`, `validationErrors`), they are appended to the exception message. DH-905 errors include the endpoint hint above.
19
+ - **Margin contracts**: `MarginCalculatorContract` and `MultiScripMarginCalcRequestContract` accept optional `orderType` (per OpenAPI; some accounts require it). `Margin` model and `bin/test_all` margin payloads send `order_type: LIMIT` when `DHAN_TEST_MARGIN=true`.
20
+ - **bin/test_all**: Fixed `ArgumentError: wrong number of arguments (given 1, expected 0)` by invoking endpoint lambdas with no arguments inside `Timeout.timeout` (the timeout block receives the duration). Refactored `endpoint_list` for RuboCop Metrics/AbcSize; optional read endpoints (forever order by id, PnL exit, margin) are skipped unless `DHAN_TEST_FOREVER_ORDER_ID`, `DHAN_TEST_PNL_EXIT=true`, or `DHAN_TEST_MARGIN=true` are set.
21
+
22
+ ### Fixed
23
+
24
+ - **bin/test_all**: All 30 read endpoints now run successfully by default (26 called, 4 optional); margin/PnL/forever-by-id no longer fail when fixtures are missing.
25
+
26
+ ### Removed
27
+
28
+ - **docs/DHAN_V2_GAPS.md**: Removed; path/behavior alignment remains in `docs/API_VERIFICATION.md`.
29
+
30
+ ---
31
+
1
32
  ## [2.6.2] - 2026-03-07
2
33
 
3
34
  ### Changed
data/README.md CHANGED
@@ -75,6 +75,8 @@ gem install DhanHQ
75
75
 
76
76
  > **Bleeding edge?** Use `gem 'DhanHQ', git: 'https://github.com/shubhamtaywade82/dhanhq-client.git', branch: 'main'` only if you need unreleased features.
77
77
 
78
+ **`bundle update` / `bundle install` warnings** — If you see "Local specification for rexml-3.2.8 has different dependencies" or "Unresolved or ambiguous specs during Gem::Specification.reset: psych", the bundle still completes successfully. To clear the rexml warning once, run: `gem cleanup rexml`. The psych message is a known Bundler quirk and can be ignored.
79
+
78
80
  ### ⚠️ Breaking Change (v2.1.5+)
79
81
 
80
82
  The require statement changed:
@@ -1,13 +1,12 @@
1
1
  # API Verification (Dhan v2)
2
2
 
3
- This document records how the gem’s implementation aligns with the official Dhan API v2 docs.
3
+ Path/behavior alignment with the official Dhan API v2 docs.
4
4
 
5
5
  **Sources:**
6
6
 
7
7
  - [dhanhq.co/docs/v2](https://dhanhq.co/docs/v2/) – main docs
8
8
  - [dhanhq.co/docs/v2/edis](https://dhanhq.co/docs/v2/edis/) – EDIS
9
9
  - [api.dhan.co/v2](https://api.dhan.co/v2/#/) – Developer Kit (when available)
10
- - In-repo: `CODE_REVIEW_ISSUES.md` (Alert Orders, IP Setup paths)
11
10
 
12
11
  ---
13
12
 
@@ -32,7 +31,7 @@ This document records how the gem’s implementation aligns with the official Dh
32
31
 
33
32
  | Doc path | Gem resource | Path used |
34
33
  |---------------------------|---------------------------|------------------|
35
- | `/alerts/orders` | `Resources::AlertOrders` | `HTTP_PATH = "/alerts/orders"` |
34
+ | `/alerts/orders` | `Resources::AlertOrders` | `HTTP_PATH = "/v2/alerts/orders"` |
36
35
  | GET/POST `/alerts/orders` | `#all`, `#create` | BaseResource |
37
36
  | GET/PUT/DELETE `/alerts/orders/{trigger-id}` | `#find`, `#update`, `#delete` | `/{id}` |
38
37
 
@@ -46,16 +45,19 @@ Model: `Models::AlertOrder`. Condition must include `exchange_segment`, `exp_dat
46
45
 
47
46
  | Doc path | Gem method | Path / body |
48
47
  |-----------------|----------------|-------------|
49
- | GET /ip/getIP | `#current` | `get("/getIP")` |
50
- | POST /ip/setIP | `#set(ip:, ip_flag: "PRIMARY", dhan_client_id: nil)` | `post("/setIP", params: { ip:, ip_flag:, dhan_client_id: })`; `dhan_client_id` defaults from config |
51
- | PUT /ip/modifyIP| `#update(ip:, ip_flag: "PRIMARY", dhan_client_id: nil)` | `put("/modifyIP", params: { ip:, ip_flag:, dhan_client_id: })` |
48
+ | GET /v2/ip/getIP | `#current` | `get("/getIP")` (HTTP_PATH = "/v2/ip") |
49
+ | POST /v2/ip/setIP | `#set(ip:, ip_flag: "PRIMARY", dhan_client_id: nil)` | `post("/setIP", params: { ip:, ip_flag:, dhan_client_id: })`; `dhan_client_id` defaults from config |
50
+ | PUT /v2/ip/modifyIP| `#update(ip:, ip_flag: "PRIMARY", dhan_client_id: nil)` | `put("/modifyIP", params: { ip:, ip_flag:, dhan_client_id: })` |
52
51
 
53
52
  ---
54
53
 
55
54
  ## Trader Control / Kill Switch
56
55
 
57
- - **Kill Switch:** `Resources::KillSwitch`, `Models::KillSwitch` – path `/v2/killswitch`. Manage (activate/deactivate) uses **query parameter** per [traders-control](https://dhanhq.co/docs/v2/traders-control/): `POST /v2/killswitch?killSwitchStatus=ACTIVATE` (or `DEACTIVATE`) with no body. `#status` is GET.
58
- - **Trader Control:** `Resources::TraderControl` – path `/trader-control`; `#status`, `#enable`, `#disable`. Not found on the public docs; kept for compatibility.
56
+ **Doc:** [dhanhq.co/docs/v2](https://dhanhq.co/docs/v2/) Trading APIs Trader's Control.
57
+
58
+ - **Kill Switch:** `Resources::KillSwitch`, `Models::KillSwitch` – path `/v2/killswitch`. Manage (activate/deactivate) uses **query parameter**: `POST /v2/killswitch?killSwitchStatus=ACTIVATE` (or `DEACTIVATE`) with no body. `#status` is GET.
59
+ - **P&L Exit:** `Models::PnlExit` – path `/v2/pnlExit`. GET status, POST configure, DELETE stop.
60
+ - **TraderControl:** `Resources::TraderControl` – path `/trader-control` is **not** in the Dhan v2 API. The class is kept for backward compatibility but raises `DhanHQ::Error` when any method is called; use KillSwitch and PnlExit instead.
59
61
 
60
62
  ---
61
63
 
@@ -101,3 +101,15 @@ These are **not** sandbox-aware; they always use the URLs above.
101
101
  - **REST:** When `sandbox` is true, all REST calls go to `https://sandbox.dhan.co/v2`. Only `GET /v2/profile` and `GET /v2/fundlimit` are verified working on sandbox; other REST endpoints are not verified — see "Sandbox: verified vs not working / unverified" above.
102
102
  - **Auth:** Token generation and renewal always use production hosts.
103
103
  - **WebSockets:** Sandbox does **not** support WebSocket. Order updates, market feed, and market depth always use production URLs; the gem does not publish or use any sandbox WebSocket URLs.
104
+
105
+ ## Call-all-endpoints script
106
+
107
+ `bin/call_all_endpoints.rb` invokes every REST endpoint exposed by the gem (read-only by default; use `--all` to include write/destructive calls). Useful for connectivity checks or sandbox verification.
108
+
109
+ ```bash
110
+ bin/call_all_endpoints.rb # read-only
111
+ bin/call_all_endpoints.rb --list # print endpoint list
112
+ bin/call_all_endpoints.rb --all # include POST/PUT/DELETE
113
+ ```
114
+
115
+ Requires `DHAN_CLIENT_ID` and `DHAN_ACCESS_TOKEN`. Optional: `DHAN_SANDBOX=true`, `DHAN_TEST_SECURITY_ID`, `DHAN_TEST_ORDER_ID`, `DHAN_TEST_ISIN`, `DHAN_TEST_EXPIRY`. When not using `--skip-unavailable`, the script creates a temporary alert for GET/PUT/DELETE alert endpoints and deletes it at exit.
data/lib/DhanHQ/auth.rb CHANGED
@@ -27,8 +27,8 @@ module DhanHQ
27
27
  # client_id: ENV["DHAN_CLIENT_ID"]
28
28
  # )
29
29
  module Auth
30
- AUTH_BASE_URL = "https://auth.dhan.co"
31
- API_BASE_URL = "https://api.dhan.co/v2"
30
+ AUTH_BASE_URL = Constants::Urls::AUTH_BASE
31
+ API_BASE_URL = Constants::Urls::REST_API_BASE
32
32
 
33
33
  # Generates an access token using TOTP authentication.
34
34
  #
data/lib/DhanHQ/client.rb CHANGED
@@ -61,48 +61,56 @@ module DhanHQ
61
61
  # @raise [DhanHQ::Error] If an HTTP error occurs.
62
62
  def request(method, path, payload, retries: 3)
63
63
  @token_manager&.ensure_valid_token!
64
- @rate_limiter.throttle! # **Ensure we don't hit rate limit before calling API**
65
-
66
- # Ensure connection matches current configuration (e.g. sandbox toggle)
64
+ @rate_limiter.throttle!
67
65
  refresh_connection!
68
66
 
69
- attempt = 0
70
- auth_retry_done = false
71
- begin
72
- response = connection.send(method, path) do |req|
73
- req.headers.merge!(build_headers(path))
74
- prepare_payload(req, payload, method, path)
67
+ with_auth_retry do
68
+ with_transient_retry(retries: retries) do
69
+ response = connection.send(method, path) do |req|
70
+ req.headers.merge!(build_headers(path))
71
+ prepare_payload(req, payload, method, path)
72
+ end
73
+ handle_response(response)
75
74
  end
75
+ end
76
+ end
76
77
 
77
- handle_response(response)
78
- rescue DhanHQ::InvalidAuthenticationError, DhanHQ::InvalidTokenError,
79
- DhanHQ::TokenExpiredError, DhanHQ::AuthenticationFailedError => e
80
- config = DhanHQ.configuration
81
- if !auth_retry_done && config&.access_token_provider
82
- auth_retry_done = true
83
- config.on_token_expired&.call(e)
84
- DhanHQ.logger&.warn("[DhanHQ::Client] Auth failure (#{e.class}), retrying once with fresh token")
85
- retry
86
- end
87
- raise
78
+ def with_auth_retry
79
+ yield
80
+ rescue DhanHQ::InvalidAuthenticationError, DhanHQ::InvalidTokenError,
81
+ DhanHQ::TokenExpiredError, DhanHQ::AuthenticationFailedError => e
82
+ config = DhanHQ.configuration
83
+ raise unless config&.access_token_provider
84
+
85
+ config.on_token_expired&.call(e)
86
+ DhanHQ.logger&.warn("[DhanHQ::Client] Auth failure (#{e.class}), retrying once with fresh token")
87
+ yield
88
+ end
89
+
90
+ def with_transient_retry(retries:)
91
+ attempt = 0
92
+ begin
93
+ yield
88
94
  rescue DhanHQ::RateLimitError, DhanHQ::InternalServerError, DhanHQ::NetworkError => e
89
95
  attempt += 1
90
- if attempt <= retries
91
- backoff_time = calculate_backoff(attempt)
92
- DhanHQ.logger&.warn("[DhanHQ::Client] Transient error (#{e.class}), retrying in #{backoff_time}s (attempt #{attempt}/#{retries})")
93
- sleep(backoff_time)
94
- retry
95
- end
96
- raise
96
+ raise if attempt > retries
97
+
98
+ backoff = calculate_backoff(attempt)
99
+ DhanHQ.logger&.warn(
100
+ "[DhanHQ::Client] Transient error (#{e.class}), retrying in #{backoff}s (attempt #{attempt}/#{retries})"
101
+ )
102
+ sleep(backoff)
103
+ retry
97
104
  rescue Faraday::TimeoutError, Faraday::ConnectionFailed => e
98
105
  attempt += 1
99
- if attempt <= retries
100
- backoff_time = calculate_backoff(attempt)
101
- DhanHQ.logger&.warn("[DhanHQ::Client] Network error (#{e.class}), retrying in #{backoff_time}s (attempt #{attempt}/#{retries})")
102
- sleep(backoff_time)
103
- retry
104
- end
105
- raise DhanHQ::NetworkError, "Request failed after #{retries} retries: #{e.message}"
106
+ raise DhanHQ::NetworkError, "Request failed after #{retries} retries: #{e.message}" if attempt > retries
107
+
108
+ backoff = calculate_backoff(attempt)
109
+ DhanHQ.logger&.warn(
110
+ "[DhanHQ::Client] Network error (#{e.class}), retrying in #{backoff}s (attempt #{attempt}/#{retries})"
111
+ )
112
+ sleep(backoff)
113
+ retry
106
114
  end
107
115
  end
108
116
 
@@ -9,13 +9,12 @@ module DhanHQ
9
9
  # @see https://dhanhq.co/docs/v2/ DhanHQ API Documentation
10
10
  class Configuration
11
11
  # Default REST API host used when the base URL is not overridden.
12
- #
13
12
  # @return [String]
14
- BASE_URL = "https://api.dhan.co/v2"
13
+ BASE_URL = Constants::Urls::REST_API_BASE
15
14
 
16
15
  # Default Sandbox API host.
17
16
  # @return [String]
18
- SANDBOX_URL = "https://sandbox.dhan.co/v2"
17
+ SANDBOX_URL = Constants::Urls::SANDBOX_API_BASE
19
18
  # The client ID for API authentication.
20
19
  # @return [String, nil] The client ID or `nil` if not set.
21
20
  attr_accessor :client_id
@@ -58,21 +57,21 @@ module DhanHQ
58
57
  # Sandbox does not support WebSocket; always returns production URL unless overridden.
59
58
  # @return [String]
60
59
  def ws_order_url
61
- @ws_order_url || "wss://api-order-update.dhan.co"
60
+ @ws_order_url || Constants::Urls::WS_ORDER_UPDATE
62
61
  end
63
62
 
64
63
  # Websocket market feed endpoint.
65
64
  # Sandbox does not support WebSocket; always returns production URL unless overridden.
66
65
  # @return [String]
67
66
  def ws_market_feed_url
68
- @ws_market_feed_url || "wss://api-feed.dhan.co"
67
+ @ws_market_feed_url || Constants::Urls::WS_MARKET_FEED
69
68
  end
70
69
 
71
70
  # Websocket market depth endpoint.
72
71
  # Sandbox does not support WebSocket; always returns production URL unless overridden.
73
72
  # @return [String]
74
73
  def ws_market_depth_url
75
- @ws_market_depth_url || "wss://depth-api-feed.dhan.co/twentydepth"
74
+ @ws_market_depth_url || Constants::Urls::WS_DEPTH_20
76
75
  end
77
76
 
78
77
  # Market depth level (20 or 200).
@@ -18,6 +18,14 @@ module DhanHQ
18
18
  BSE_FNO = "BSE_FNO"
19
19
 
20
20
  ALL = [IDX_I, NSE_EQ, NSE_FNO, NSE_CURRENCY, NSE_COMM, BSE_EQ, MCX_COMM, BSE_CURRENCY, BSE_FNO].freeze
21
+ # Segments allowed by POST /v2/margincalculator (single and multi).
22
+ MARGIN_CALC_ALL = [NSE_EQ, NSE_FNO, BSE_EQ, BSE_FNO, MCX_COMM].freeze
23
+ # Segments allowed by POST /v2/forever/orders (create).
24
+ FOREVER_ORDER_ALL = [NSE_EQ, NSE_FNO, BSE_EQ, BSE_FNO, MCX_COMM].freeze
25
+ # Segments for conditional trigger (equities and indices only). POST/PUT /v2/alerts/orders.
26
+ ALERT_CONDITION_ALL = [NSE_EQ, BSE_EQ, IDX_I].freeze
27
+ # Segments allowed by POST /v2/charts/historical and POST /v2/charts/intraday (excludes NSE_COMM).
28
+ CHART_ALL = [IDX_I, NSE_EQ, NSE_FNO, NSE_CURRENCY, BSE_EQ, BSE_FNO, BSE_CURRENCY, MCX_COMM].freeze
21
29
  end
22
30
 
23
31
  # Product types for order placement.
@@ -30,6 +38,10 @@ module DhanHQ
30
38
  BO = "BO"
31
39
 
32
40
  ALL = [CNC, INTRADAY, MARGIN, MTF, CO, BO].freeze
41
+ # Product types allowed by POST /v2/margincalculator (single and multi).
42
+ MARGIN_CALC_ALL = [CNC, INTRADAY, MARGIN, MTF].freeze
43
+ # Product types allowed by POST /v2/forever/orders (create). Only CNC and MTF.
44
+ FOREVER_ORDER_ALL = [CNC, MTF].freeze
33
45
  end
34
46
 
35
47
  # Buy/Sell transaction types.
@@ -112,6 +124,17 @@ module DhanHQ
112
124
  # Backward-compatible alias kept for existing SDK usage.
113
125
  Instrument = InstrumentType
114
126
 
127
+ # Minute intervals allowed by POST /v2/charts/intraday (charts annexure).
128
+ module ChartInterval
129
+ ONE = "1"
130
+ FIVE = "5"
131
+ FIFTEEN = "15"
132
+ TWENTY_FIVE = "25"
133
+ SIXTY = "60"
134
+
135
+ ALL = [ONE, FIVE, FIFTEEN, TWENTY_FIVE, SIXTY].freeze
136
+ end
137
+
115
138
  # Option types for derivatives trading.
116
139
  module OptionType
117
140
  CALL = "CALL"
@@ -365,10 +388,33 @@ module DhanHQ
365
388
  ORDER_MODIFICATIONS_PER_ORDER = 25
366
389
  end
367
390
 
391
+ # Canonical Dhan API, auth, WebSocket and instrument URLs (see https://dhanhq.co/docs/v2/).
392
+ # Configuration and Auth use these as defaults; ENV overrides apply at runtime.
393
+ module Urls
394
+ REST_API_BASE = "https://api.dhan.co/v2"
395
+ SANDBOX_API_BASE = "https://sandbox.dhan.co/v2"
396
+ AUTH_BASE = "https://auth.dhan.co"
397
+ WS_MARKET_FEED = "wss://api-feed.dhan.co"
398
+ WS_ORDER_UPDATE = "wss://api-order-update.dhan.co"
399
+ WS_DEPTH_20 = "wss://depth-api-feed.dhan.co/twentydepth"
400
+ WS_DEPTH_200 = "wss://full-depth-api.dhan.co/twohundreddepth"
401
+ INSTRUMENT_CSV_COMPACT = "https://images.dhan.co/api-data/api-scrip-master.csv"
402
+ INSTRUMENT_CSV_DETAILED = "https://images.dhan.co/api-data/api-scrip-master-detailed.csv"
403
+ DOCS = "https://dhanhq.co/docs/v2"
404
+ # Origin header value for WebSocket connections (Dhan main site).
405
+ ORIGIN = "https://dhanhq.co"
406
+ end
407
+
368
408
  # Backward-compatible arrays used across existing validations.
369
409
  TRANSACTION_TYPES = TransactionType::ALL
370
410
  EXCHANGE_SEGMENTS = ExchangeSegment::ALL
411
+ CHART_EXCHANGE_SEGMENTS = ExchangeSegment::CHART_ALL
371
412
  INSTRUMENTS = InstrumentType::ALL
413
+ CHART_INTERVALS = ChartInterval::ALL
414
+ MARGIN_CALCULATOR_SEGMENTS = ExchangeSegment::MARGIN_CALC_ALL
415
+ MARGIN_PRODUCT_TYPES = ProductType::MARGIN_CALC_ALL
416
+ FOREVER_ORDER_SEGMENTS = ExchangeSegment::FOREVER_ORDER_ALL
417
+ FOREVER_ORDER_PRODUCT_TYPES = ProductType::FOREVER_ORDER_ALL
372
418
  PRODUCT_TYPES = ProductType::ALL
373
419
  ORDER_TYPES = OrderType::ALL
374
420
  VALIDITY_TYPES = Validity::ALL
@@ -376,6 +422,8 @@ module DhanHQ
376
422
  ORDER_STATUSES = OrderStatus::ALL
377
423
  COMPARISON_TYPES = ComparisonType::ALL
378
424
  OPERATORS = Operator::ALL
425
+ ALERT_CONDITION_SEGMENTS = ExchangeSegment::ALERT_CONDITION_ALL
426
+ ALERT_TIMEFRAMES = %w[DATE ONE_MIN FIVE_MIN FIFTEEN_MIN DAY].freeze
379
427
 
380
428
  # Exchange aliases used when building subscription payloads.
381
429
  NSE = ExchangeSegment::NSE_EQ
@@ -387,7 +435,8 @@ module DhanHQ
387
435
  BSE_FNO = ExchangeSegment::BSE_FNO
388
436
  INDEX = ExchangeSegment::IDX_I
389
437
 
390
- OPTION_SEGMENTS = [NSE, BSE, CUR, MCX, FNO, NSE_FNO, BSE_FNO, INDEX].freeze
438
+ # Underlying segments accepted by POST /v2/optionchain and POST /v2/optionchain/expirylist.
439
+ OPTION_CHAIN_UNDERLYING_SEGMENTS = %w[IDX_I NSE_FNO BSE_FNO MCX_FO].freeze
391
440
 
392
441
  # Canonical labels kept for compatibility with previous SDK versions.
393
442
  BUY = TransactionType::BUY
@@ -409,19 +458,30 @@ module DhanHQ
409
458
  IOC = Validity::IOC
410
459
 
411
460
  # Download URL for the compact instrument master CSV.
412
- COMPACT_CSV_URL = "https://images.dhan.co/api-data/api-scrip-master.csv"
461
+ COMPACT_CSV_URL = Urls::INSTRUMENT_CSV_COMPACT
413
462
  # Download URL for the detailed instrument master CSV.
414
- DETAILED_CSV_URL = "https://images.dhan.co/api-data/api-scrip-master-detailed.csv"
463
+ DETAILED_CSV_URL = Urls::INSTRUMENT_CSV_DETAILED
415
464
 
416
465
  # API route prefixes that require a `client-id` header in addition to the access token.
417
466
  DATA_API_PREFIXES = [
418
467
  "/v2/marketfeed/",
419
468
  "/v2/optionchain",
420
469
  "/v2/instrument/",
421
- "/v2/charts",
422
- "/v2/margincalculator",
423
- "/v2/profile",
424
- "/v2/fundlimit"
470
+ "/v2/charts"
471
+ ].freeze
472
+
473
+ # Path prefixes for which the request body (POST/PUT/PATCH) must include dhanClientId.
474
+ # Injection is done in the client layer when building the payload.
475
+ PAYLOAD_REQUIRES_DHAN_CLIENT_ID_PREFIXES = %w[
476
+ /alerts/orders
477
+ /v2/orders
478
+ /v2/forever
479
+ /v2/super/orders
480
+ /v2/positions
481
+ /v2/pnlExit
482
+ /v2/margincalculator
483
+ /v2/killswitch
484
+ /v2/ip
425
485
  ].freeze
426
486
 
427
487
  # Mapping of exchange and segment combinations to canonical exchange segment names.
@@ -2,45 +2,52 @@
2
2
 
3
3
  module DhanHQ
4
4
  module Contracts
5
- # Validates alert order payloads for create/update per dhanhq.co/docs/v2/conditional-trigger/
6
- # Condition requires exchange_segment, exp_date, frequency; time_frame required for TECHNICAL_* comparison types.
5
+ # Validates Conditional Trigger (alert order) payloads for POST /v2/alerts/orders and PUT /v2/alerts/orders/{alertId}.
6
+ # Condition: exchangeSegment (NSE_EQ|BSE_EQ|IDX_I), timeframe (required), comparisonType, operator, expDate, frequency;
7
+ # indicatorName/time_frame required for TECHNICAL_* comparison types.
8
+ # Orders: transactionType, exchangeSegment, productType (CNC|INTRADAY|MARGIN|MTF), orderType, securityId,
9
+ # quantity, validity, price (required); discQuantity, triggerPrice optional.
7
10
  class AlertOrderContract < BaseContract
8
11
  params do
9
12
  required(:condition).hash do
10
13
  required(:security_id).filled(:string, max_size?: 20)
11
- required(:exchange_segment).filled(:string, included_in?: EXCHANGE_SEGMENTS)
14
+ required(:exchange_segment).filled(:string, included_in?: ALERT_CONDITION_SEGMENTS)
12
15
  required(:comparison_type).filled(:string, included_in?: COMPARISON_TYPES)
13
- optional(:indicator_name).maybe(:string)
14
- optional(:time_frame).maybe(:string)
16
+ required(:time_frame).filled(:string, included_in?: ALERT_TIMEFRAMES)
15
17
  required(:operator).filled(:string, included_in?: OPERATORS)
18
+ required(:exp_date).filled(:string, format?: /\A\d{4}-\d{2}-\d{2}\z/)
19
+ required(:frequency).filled(:string)
20
+ optional(:indicator_name).maybe(:string)
16
21
  optional(:comparing_value).maybe(:float)
17
22
  optional(:comparing_indicator_name).maybe(:string)
18
- required(:exp_date).filled(:string)
19
- required(:frequency).filled(:string)
23
+ optional(:user_note).maybe(:string)
20
24
  end
21
25
  required(:orders).array(:hash) do
22
26
  required(:transaction_type).filled(:string, included_in?: TRANSACTION_TYPES)
23
27
  required(:exchange_segment).filled(:string, included_in?: EXCHANGE_SEGMENTS)
24
- required(:product_type).filled(:string, included_in?: PRODUCT_TYPES)
28
+ required(:product_type).filled(:string, included_in?: MARGIN_PRODUCT_TYPES)
25
29
  required(:order_type).filled(:string, included_in?: ORDER_TYPES)
26
30
  required(:security_id).filled(:string, max_size?: 20)
27
31
  required(:quantity).filled(:integer, gt?: 0)
28
32
  required(:validity).filled(:string, included_in?: VALIDITY_TYPES)
29
- optional(:price).maybe(:float)
30
- optional(:trigger_price).maybe(:float)
33
+ required(:price).filled # string or number; API expects string, coerce in serialization
34
+ optional(:disc_quantity).maybe(:string)
35
+ optional(:trigger_price).maybe(:string)
31
36
  end
32
37
  end
33
38
 
34
39
  rule(condition: :indicator_name) do
35
- if values[:condition] && values[:condition][:comparison_type].to_s.start_with?("TECHNICAL") && !value
36
- key(condition: :indicator_name).failure("is required for technical comparisons")
37
- end
40
+ next unless values.dig(:condition, :comparison_type).to_s.start_with?("TECHNICAL")
41
+ next if value && !value.to_s.strip.empty?
42
+
43
+ key(%i[condition indicator_name]).failure("is required for technical comparisons")
38
44
  end
39
45
 
40
46
  rule(condition: :time_frame) do
41
- if values[:condition] && values[:condition][:comparison_type].to_s.start_with?("TECHNICAL") && !value
42
- key(condition: :time_frame).failure("is required for technical comparisons")
43
- end
47
+ next unless values.dig(:condition, :comparison_type).to_s.start_with?("TECHNICAL")
48
+ next if value && !value.to_s.strip.empty?
49
+
50
+ key(%i[condition time_frame]).failure("is required for technical comparisons")
44
51
  end
45
52
  end
46
53
  end
@@ -23,7 +23,9 @@ module DhanHQ
23
23
  end
24
24
 
25
25
  rule(:exchange_segment) do
26
- valid_segments = %w[NSE_FNO BSE_FNO NSE_EQ BSE_EQ]
26
+ # IDX_I for index options, NSE_EQ/BSE_EQ for equity options,
27
+ # plus NSE_FNO/BSE_FNO for derivatives.
28
+ valid_segments = %w[IDX_I NSE_EQ BSE_EQ NSE_FNO BSE_FNO]
27
29
  key.failure("must be one of: #{valid_segments.join(", ")}") unless valid_segments.include?(value)
28
30
  end
29
31
 
@@ -43,7 +45,7 @@ module DhanHQ
43
45
  end
44
46
 
45
47
  rule(:strike) do
46
- unless value.match?(/\AATM(\+|-)?\d*\z/) || value == "ATM"
48
+ unless value.match?(/\AATM(\+|-)?\d+\z/) || value == "ATM"
47
49
  key.failure("must be in format ATM, ATM+1, ATM-1, etc. " \
48
50
  "(up to ATM+10/ATM-10 for Index Options, ATM+3/ATM-3 for others)")
49
51
  end