DhanHQ 2.6.1 → 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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -3
  3. data/ARCHITECTURE.md +113 -0
  4. data/CHANGELOG.md +55 -0
  5. data/README.md +2 -0
  6. data/Rakefile +3 -1
  7. data/docs/API_VERIFICATION.md +10 -8
  8. data/docs/ENDPOINTS_AND_SANDBOX.md +115 -0
  9. data/lib/DhanHQ/auth.rb +2 -2
  10. data/lib/DhanHQ/client.rb +72 -51
  11. data/lib/DhanHQ/configuration.rb +45 -11
  12. data/lib/DhanHQ/constants.rb +68 -4
  13. data/lib/DhanHQ/contracts/alert_order_contract.rb +23 -16
  14. data/lib/DhanHQ/contracts/expired_options_data_contract.rb +4 -2
  15. data/lib/DhanHQ/contracts/forever_order_contract.rb +55 -0
  16. data/lib/DhanHQ/contracts/historical_data_contract.rb +17 -19
  17. data/lib/DhanHQ/contracts/intraday_historical_data_contract.rb +12 -0
  18. data/lib/DhanHQ/contracts/margin_calculator_contract.rb +19 -17
  19. data/lib/DhanHQ/contracts/market_feed_contract.rb +42 -0
  20. data/lib/DhanHQ/contracts/multi_scrip_margin_calc_request_contract.rb +8 -5
  21. data/lib/DhanHQ/contracts/option_chain_contract.rb +17 -19
  22. data/lib/DhanHQ/contracts/pnl_based_exit_contract.rb +1 -1
  23. data/lib/DhanHQ/contracts/slice_order_contract.rb +10 -10
  24. data/lib/DhanHQ/core/auth_api.rb +1 -1
  25. data/lib/DhanHQ/core/base_api.rb +10 -9
  26. data/lib/DhanHQ/core/base_model.rb +4 -1
  27. data/lib/DhanHQ/core/error_handler.rb +2 -2
  28. data/lib/DhanHQ/errors.rb +14 -2
  29. data/lib/DhanHQ/helpers/request_helper.rb +27 -5
  30. data/lib/DhanHQ/helpers/response_helper.rb +48 -19
  31. data/lib/DhanHQ/helpers/validation_helper.rb +4 -2
  32. data/lib/DhanHQ/models/alert_order.rb +6 -2
  33. data/lib/DhanHQ/models/edis.rb +20 -13
  34. data/lib/DhanHQ/models/expired_options_data.rb +54 -44
  35. data/lib/DhanHQ/models/forever_order.rb +17 -7
  36. data/lib/DhanHQ/models/historical_data.rb +40 -6
  37. data/lib/DhanHQ/models/instrument_helpers.rb +2 -1
  38. data/lib/DhanHQ/models/margin.rb +62 -82
  39. data/lib/DhanHQ/models/market_feed.rb +14 -3
  40. data/lib/DhanHQ/models/option_chain.rb +50 -150
  41. data/lib/DhanHQ/models/order.rb +19 -4
  42. data/lib/DhanHQ/models/super_order.rb +2 -2
  43. data/lib/DhanHQ/resources/alert_orders.rb +1 -1
  44. data/lib/DhanHQ/resources/edis.rb +4 -3
  45. data/lib/DhanHQ/resources/instruments.rb +3 -2
  46. data/lib/DhanHQ/resources/ip_setup.rb +4 -1
  47. data/lib/DhanHQ/resources/kill_switch.rb +7 -1
  48. data/lib/DhanHQ/resources/orders.rb +1 -1
  49. data/lib/DhanHQ/resources/super_orders.rb +8 -2
  50. data/lib/DhanHQ/resources/trader_control.rb +13 -4
  51. data/lib/DhanHQ/version.rb +1 -1
  52. data/lib/DhanHQ/ws/base_connection.rb +1 -1
  53. data/lib/DhanHQ/ws/client.rb +2 -1
  54. data/lib/DhanHQ/ws/market_depth/client.rb +16 -8
  55. data/lib/dhan_hq.rb +37 -32
  56. data/lib/ta/indicators.rb +15 -18
  57. metadata +7 -9
  58. data/CODE_REVIEW_ISSUES.md +0 -397
  59. data/FIXES_APPLIED.md +0 -373
  60. data/RELEASING.md +0 -60
  61. data/REVIEW_SUMMARY.md +0 -120
  62. data/VERSION_UPDATE.md +0 -82
  63. data/diagram.md +0 -34
  64. data/docs/ARCHIVE_README.md +0 -784
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b6e949dc3b147daabce77b5d808d5dcdeb4953e9751322eb8cd671d70201c57
4
- data.tar.gz: 12327daca50d50836d724f4eeb5cf664abee766767c7bdec31f51b07b78ddc00
3
+ metadata.gz: ef2616285615327c5e0ad08a85440d91617ea66818add0a55e4cd6c5e65512aa
4
+ data.tar.gz: 4232cfcd4c9626683e3bf993f75aa0a8add2f8c83702435c66edb2c58a3e252e
5
5
  SHA512:
6
- metadata.gz: 0ba1d351594f83a0824961dff0e8cfb28ce9b6fd6e65daa9124bf0ceab651c49aaefd00674cdbfdd78a48f9e7c2032395b4981b0b613bfccbee49b7c46ba319d
7
- data.tar.gz: 2e5ca5a81905c9e4fa861296f247eaa2411bf548f84cd94e348c51a079b406280ae9247838c013173552e6564c625ded0fd4504284d097b27892b55ecb9380ca
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,42 @@
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
+
32
+ ## [2.6.2] - 2026-03-07
33
+
34
+ ### Changed
35
+
36
+ - **Release from main** after merging add-sandbox-support. Includes sandbox REST base URL, `ensure_configuration!`, payload non-mutation, Rakefile/VCR fixes, and docs. Full feature list is under [2.6.0](#260---2026-03-06); 2.6.0 and 2.6.1 are already on RubyGems.
37
+
38
+ ---
39
+
1
40
  ## [2.6.1] - 2026-03-07
2
41
 
3
42
  ### Changed
@@ -8,6 +47,22 @@
8
47
 
9
48
  ## [2.6.0] - 2026-03-06
10
49
 
50
+ ### Sandbox & configuration
51
+
52
+ - **Sandbox environment**: `DhanHQ.configuration.sandbox` (or `ENV["DHAN_SANDBOX"]=true`) switches REST base URL to `https://sandbox.dhan.co/v2`. Only `GET /v2/profile` and `GET /v2/fundlimit` are verified on sandbox; other REST endpoints are unverified. See `docs/ENDPOINTS_AND_SANDBOX.md`.
53
+ - **WebSocket**: Sandbox does **not** support WebSocket. Order updates, market feed, and market depth always use production URLs regardless of `sandbox`; no sandbox WS URLs are published.
54
+ - **Env-only bootstrap**: `DhanHQ.ensure_configuration!` ensures configuration exists (from ENV when nil). Called automatically in `Client#initialize` so apps using only `DHAN_CLIENT_ID` / `DHAN_ACCESS_TOKEN` work without calling `configure_with_env`.
55
+
56
+ ### Fixed
57
+
58
+ - **Payload mutation**: `prepare_payload` no longer mutates the caller's hash when injecting `dhanClientId` for DATA APIs; uses a duplicate so frozen or reused hashes are safe.
59
+ - **VCR**: Removed erroneous `/v2/v2/` market feed cassette entries (404 responses).
60
+ - **Rakefile**: Single RuboCop task; removed redundant `rubocop:fix` / `rubocop:fix_all` and deprecated `--auto-correct-all` flag.
61
+
62
+ ### Added
63
+
64
+ - **docs/ENDPOINTS_AND_SANDBOX.md**: Lists all REST/WebSocket endpoints, sandbox behavior, and endpoints verified vs not supported on sandbox.
65
+
11
66
  ### Fixed (API docs alignment)
12
67
 
13
68
  - **Kill Switch**: Manage API now uses query parameter per [dhanhq.co/docs/v2/traders-control](https://dhanhq.co/docs/v2/traders-control/). `Resources::KillSwitch#update(status)` sends `POST /v2/killswitch?killSwitchStatus=ACTIVATE` (or `DEACTIVATE`) with no body. `Models::KillSwitch.update("ACTIVATE")` / `.activate` / `.deactivate` unchanged.
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:
data/Rakefile CHANGED
@@ -7,6 +7,8 @@ RSpec::Core::RakeTask.new(:spec)
7
7
 
8
8
  require "rubocop/rake_task"
9
9
 
10
- RuboCop::RakeTask.new
10
+ # Single RuboCop task; the gem also registers rubocop:autocorrect and rubocop:autocorrect_all.
11
+ desc "Run RuboCop"
12
+ RuboCop::RakeTask.new(:rubocop)
11
13
 
12
14
  task default: %i[spec rubocop]
@@ -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
 
@@ -0,0 +1,115 @@
1
+ # DhanHQ gem — Endpoints and sandbox support
2
+
3
+ ## Sandbox behavior
4
+
5
+ - **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
+ - **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
+ - **Auth endpoints** (`DhanHQ::Auth`) do **not** use sandbox. They always call:
8
+ - `https://auth.dhan.co` — token generation
9
+ - `https://api.dhan.co/v2` — token renewal
10
+ So token generation/renewal always hit production; only data/order REST calls follow the sandbox flag.
11
+
12
+ ---
13
+
14
+ ## Sandbox: verified vs not working / unverified
15
+
16
+ | Status | Endpoints |
17
+ |-----------|-----------|
18
+ | **Verified on sandbox** (gem specs) | `GET /v2/profile`, `GET /v2/fundlimit` |
19
+ | **Sandbox connectivity spec** | `spec/dhan_hq/sandbox_connectivity_spec.rb` — uses VCR `record: :new_episodes`. In CI, use committed cassettes or skip without sandbox credentials; locally, run with `VCR_RECORD=new_episodes` and sandbox credentials to record. |
20
+ | **Not supported in sandbox** | All WebSocket endpoints (order updates, market feed, market depth). Use production only. |
21
+ | **Not verified on sandbox** | All other REST endpoints below. They may fail, return differently, or be unavailable in sandbox. Use Dhan documentation or manual testing before relying on them in sandbox. |
22
+
23
+ **REST endpoints not verified / may not work on sandbox** (only profile and funds are verified):
24
+
25
+ - `/v2/ledger`, `/v2/trades/{from}/{to}/{page}` (statements)
26
+ - `/v2/orders` (all order CRUD, slicing, external)
27
+ - `/v2/positions`, `/v2/positions/convert`
28
+ - `/v2/holdings`
29
+ - `/v2/trades`, `/v2/trades/{order_id}`
30
+ - `/v2/forever/orders` (all)
31
+ - `/v2/super/orders` (all)
32
+ - `/v2/killswitch`
33
+ - `/trader-control`
34
+ - `/ip/getIP`, `/ip/setIP`, `/ip/modifyIP`
35
+ - `/edis/tpin`, `/edis/form`, `/edis/bulkform`, `/edis/inquire/{isin}`
36
+ - `/alerts/orders`
37
+ - `/v2/pnlExit`
38
+ - `/v2/margincalculator`, `/v2/margincalculator/multi`
39
+ - `/v2/instrument/{segment}`
40
+ - `/v2/marketfeed/ltp`, `/v2/marketfeed/ohlc`, `/v2/marketfeed/quote`
41
+ - `/v2/optionchain`, `/v2/optionchain/expirylist`
42
+ - `/v2/charts/historical`, `/v2/charts/intraday`, `/v2/charts/rollingoption`
43
+
44
+ ---
45
+
46
+ ## REST endpoints integrated in the gem
47
+
48
+ Paths are as built by the gem (HTTP_PATH + endpoint). Base URL is either `https://api.dhan.co/v2` (production) or `https://sandbox.dhan.co/v2` (sandbox).
49
+
50
+ | Resource / model | Path(s) | Methods | API type |
51
+ |---------------------------|----------------------------------------------|-----------|-----------------|
52
+ | **Profile** | `/v2/profile` | GET | non_trading_api |
53
+ | **Funds** | `/v2/fundlimit` | GET | non_trading_api |
54
+ | **Statements** | `/v2/ledger`, `/v2/trades/{from}/{to}/{page}`| GET | non_trading_api |
55
+ | **Orders** | `/v2/orders`, `/v2/orders/{id}`, `/v2/orders/external/{correlation_id}`, `/v2/orders/slicing` | GET, POST, PUT, DELETE | order_api |
56
+ | **Positions** | `/v2/positions`, `/v2/positions/convert` | GET, POST, DELETE | order_api |
57
+ | **Holdings** | `/v2/holdings` | GET | order_api |
58
+ | **Trades** | `/v2/trades`, `/v2/trades/{order_id}` | GET | order_api |
59
+ | **Forever orders** | `/v2/forever/orders`, `/v2/forever/orders/{id}` | GET, POST, PUT, DELETE | order_api |
60
+ | **Super orders** | `/v2/super/orders`, `/v2/super/orders/{id}`, leg delete | GET, POST, PUT, DELETE | order_api |
61
+ | **Kill switch** | `/v2/killswitch` | GET, POST | order_api |
62
+ | **Trader control** | `/trader-control` | GET, POST | order_api |
63
+ | **IP setup** | `/ip/getIP`, `/ip/setIP`, `/ip/modifyIP` | GET, POST, PUT | order_api |
64
+ | **EDIS** | `/edis/tpin`, `/edis/form`, `/edis/bulkform`, `/edis/inquire/{isin}` | GET, POST | order_api |
65
+ | **Alert orders** | `/alerts/orders` | GET, POST, PUT, DELETE | order_api |
66
+ | **PnL exit** | `/v2/pnlExit` | GET, POST, DELETE | order_api |
67
+ | **Margin calculator** | `/v2/margincalculator`, `/v2/margincalculator/multi` | POST | order_api |
68
+ | **Instruments** | `/v2/instrument/{segment}` (redirect to CSV) | GET | data_api |
69
+ | **Market feed** | `/v2/marketfeed/ltp`, `/v2/marketfeed/ohlc`, `/v2/marketfeed/quote` | POST | data_api / quote_api |
70
+ | **Option chain** | `/v2/optionchain`, `/v2/optionchain/expirylist` | POST | data_api |
71
+ | **Historical data** | `/v2/charts/historical`, `/v2/charts/intraday` | POST | data_api |
72
+ | **Expired options data** | `/v2/charts/rollingoption` | POST | data_api |
73
+
74
+ ---
75
+
76
+ ## Auth (outside main client base URL)
77
+
78
+ | Purpose | URL / path | Method |
79
+ |-------------------|--------------------------------|--------|
80
+ | Generate token | `https://auth.dhan.co/app/generateAccessToken` | POST |
81
+ | Renew token | `https://api.dhan.co/v2/RenewToken` | POST |
82
+
83
+ These are **not** sandbox-aware; they always use the URLs above.
84
+
85
+ ---
86
+
87
+ ## WebSocket endpoints (production only; sandbox not supported)
88
+
89
+ | Purpose | URL |
90
+ |----------------|-----|
91
+ | Order updates | `wss://api-order-update.dhan.co` |
92
+ | Market feed | `wss://api-feed.dhan.co` |
93
+ | Market depth | `wss://depth-api-feed.dhan.co/twentydepth` |
94
+
95
+ **Sandbox:** Dhan sandbox does **not** provide WebSocket services. These endpoints are production-only. The gem never switches WS URLs based on `sandbox`; you can still override via `DhanHQ.configuration.ws_order_url`, `ws_market_feed_url`, `ws_market_depth_url`, or env vars `DHAN_WS_ORDER_URL`, `DHAN_WS_MARKET_FEED_URL`, `DHAN_WS_MARKET_DEPTH_URL` if you have a different production URL.
96
+
97
+ ---
98
+
99
+ ## Summary
100
+
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
+ - **Auth:** Token generation and renewal always use production hosts.
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
@@ -40,29 +40,15 @@ module DhanHQ
40
40
  # @return [DhanHQ::Client] A new client instance.
41
41
  # @raise [DhanHQ::Error] If configuration is invalid or rate limiter initialization fails
42
42
  def initialize(api_type:)
43
- # Configure from ENV if DHAN_CLIENT_ID is present (backward compatible behavior)
44
- # Validation happens at request time in build_headers, not here
45
- DhanHQ.configure_with_env if ENV.fetch("DHAN_CLIENT_ID", nil)
46
-
43
+ DhanHQ.ensure_configuration!
47
44
  # Use shared rate limiter instance per API type to ensure proper coordination
48
45
  @rate_limiter = RateLimiter.for(api_type)
49
46
 
50
47
  raise DhanHQ::Error, "RateLimiter initialization failed" unless @rate_limiter
51
48
 
52
- # Get timeout values from configuration or environment, with sensible defaults
53
- connect_timeout = ENV.fetch("DHAN_CONNECT_TIMEOUT", 10).to_i
54
- read_timeout = ENV.fetch("DHAN_READ_TIMEOUT", 30).to_i
55
- write_timeout = ENV.fetch("DHAN_WRITE_TIMEOUT", 30).to_i
56
-
57
- @connection = Faraday.new(url: DhanHQ.configuration.base_url) do |conn|
58
- conn.request :json, parser_options: { symbolize_names: true }
59
- conn.response :json, content_type: /\bjson$/
60
- conn.response :logger if ENV["DHAN_DEBUG"] == "true"
61
- conn.options.timeout = read_timeout
62
- conn.options.open_timeout = connect_timeout
63
- conn.options.write_timeout = write_timeout
64
- conn.adapter Faraday.default_adapter
65
- end
49
+ # Store initial URL to detect changes
50
+ @last_base_url = DhanHQ.configuration.base_url
51
+ @connection = build_connection(@last_base_url)
66
52
  end
67
53
 
68
54
  # Sends an HTTP request to the API with automatic retry for transient errors.
@@ -75,46 +61,56 @@ module DhanHQ
75
61
  # @raise [DhanHQ::Error] If an HTTP error occurs.
76
62
  def request(method, path, payload, retries: 3)
77
63
  @token_manager&.ensure_valid_token!
78
- @rate_limiter.throttle! # **Ensure we don't hit rate limit before calling API**
64
+ @rate_limiter.throttle!
65
+ refresh_connection!
66
+
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)
74
+ end
75
+ end
76
+ end
79
77
 
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:)
80
91
  attempt = 0
81
- auth_retry_done = false
82
92
  begin
83
- response = connection.send(method) do |req|
84
- req.url path
85
- req.headers.merge!(build_headers(path))
86
- prepare_payload(req, payload, method)
87
- end
88
-
89
- handle_response(response)
90
- rescue DhanHQ::InvalidAuthenticationError, DhanHQ::InvalidTokenError,
91
- DhanHQ::TokenExpiredError, DhanHQ::AuthenticationFailedError => e
92
- config = DhanHQ.configuration
93
- if !auth_retry_done && config&.access_token_provider
94
- auth_retry_done = true
95
- config.on_token_expired&.call(e)
96
- DhanHQ.logger&.warn("[DhanHQ::Client] Auth failure (#{e.class}), retrying once with fresh token")
97
- retry
98
- end
99
- raise
93
+ yield
100
94
  rescue DhanHQ::RateLimitError, DhanHQ::InternalServerError, DhanHQ::NetworkError => e
101
95
  attempt += 1
102
- if attempt <= retries
103
- backoff_time = calculate_backoff(attempt)
104
- DhanHQ.logger&.warn("[DhanHQ::Client] Transient error (#{e.class}), retrying in #{backoff_time}s (attempt #{attempt}/#{retries})")
105
- sleep(backoff_time)
106
- retry
107
- end
108
- 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
109
104
  rescue Faraday::TimeoutError, Faraday::ConnectionFailed => e
110
105
  attempt += 1
111
- if attempt <= retries
112
- backoff_time = calculate_backoff(attempt)
113
- DhanHQ.logger&.warn("[DhanHQ::Client] Network error (#{e.class}), retrying in #{backoff_time}s (attempt #{attempt}/#{retries})")
114
- sleep(backoff_time)
115
- retry
116
- end
117
- 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
118
114
  end
119
115
  end
120
116
 
@@ -201,6 +197,31 @@ module DhanHQ
201
197
 
202
198
  private
203
199
 
200
+ def refresh_connection!
201
+ current_url = DhanHQ.configuration.base_url
202
+ return if @last_base_url == current_url
203
+
204
+ @last_base_url = current_url
205
+ @connection = build_connection(current_url)
206
+ end
207
+
208
+ def build_connection(url)
209
+ # Get timeout values from configuration or environment, with sensible defaults
210
+ connect_timeout = ENV.fetch("DHAN_CONNECT_TIMEOUT", 10).to_i
211
+ read_timeout = ENV.fetch("DHAN_READ_TIMEOUT", 30).to_i
212
+ write_timeout = ENV.fetch("DHAN_WRITE_TIMEOUT", 30).to_i
213
+
214
+ Faraday.new(url: url) do |conn|
215
+ conn.request :json, parser_options: { symbolize_names: true }
216
+ conn.response :json, content_type: /\bjson$/
217
+ conn.response :logger if ENV["DHAN_DEBUG"] == "true"
218
+ conn.options.timeout = read_timeout
219
+ conn.options.open_timeout = connect_timeout
220
+ conn.options.write_timeout = write_timeout
221
+ conn.adapter Faraday.default_adapter
222
+ end
223
+ end
224
+
204
225
  # Calculates exponential backoff time
205
226
  #
206
227
  # @param attempt [Integer] Current attempt number (1-based)