DhanHQ 3.4.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d97f7473f5bb7c4968750c3e21e5721a9c9ba1261e6e00d450818613edb74ec
4
- data.tar.gz: 49464e4909ac0f52947fc86735b6b43bef6877f8ba0a1f796eb3b1c58c60bfde
3
+ metadata.gz: 8c2f0784210b7f48ccb056b8cc486ce5c3ef655f69a4bd2834a150b1d04a8bc7
4
+ data.tar.gz: a6628fc83e8ec61bd4ca4b0eb6147a67232156f98ab49196c0a81850f9b4ac59
5
5
  SHA512:
6
- metadata.gz: 5a70be1d7aa2af222df63e8188af1b60e110431d73ae36317244693d974e46a3fcb84a9a49d9276fbadeea105c1124753129aa1c057d089924f0b71c7075f481
7
- data.tar.gz: cb8a910ad46ae08b570eb9b19ac3a553de9a050e1b2a409d1414f3eb18d7eb762cb2fc62d6f2a5a72ad9014f726a1195541fa29ea80d749ba4c1dc21d6b0903f
6
+ metadata.gz: d0a4147017f7722d9a74cec3105fb57e9b8710142abda6c04d5948caa7500b21ce8c31958cc501e99643a31afffe5e64846b5a73ff239f1a8fb87074e57a6856
7
+ data.tar.gz: 508f55621273349e9060e941cc9bf8b7cef7fb28befb8026b57b977ba0954760f783eb5dbb26c19593ac113232c12fa74884fcd57a81b61dd21dbc3d66c7b9e9
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## [4.0.0] - 2026-08-31
2
+
3
+ ### Added
4
+
5
+ - **Multi-Instance Client Isolation (`DhanHQ::Client.new`)** — run multiple isolated client instances in the same process with separate configurations, credentials, sandbox environments, and rate limiters. Ideal for multi-tenant applications, managing multiple trading accounts concurrently, or comparing sandbox vs. production environments side-by-side.
6
+ ```ruby
7
+ client_a = DhanHQ::Client.new(client_id: "ACC_A", access_token: "TOKEN_A")
8
+ client_b = DhanHQ::Client.new(client_id: "ACC_B", access_token: "TOKEN_B", sandbox: true)
9
+
10
+ orders_a = client_a.orders.all
11
+ orders_b = client_b.orders.all
12
+ ```
13
+ - **Instance-bound Resource Accessors** — direct accessors on `DhanHQ::Client` instances (`client.orders`, `client.positions`, `client.holdings`, `client.funds`, `client.trades`, `client.market_feed`, `client.option_chain`, `client.historical_data`, `client.forever_orders`, `client.alert_orders`, `client.iceberg_orders`, `client.super_orders`, `client.twap_orders`, `client.statements`, `client.kill_switch`, `client.pnl_exit`, `client.trader_control`, `client.ip_setup`, `client.edis`) bound to that client's configuration and routed to matching rate-limiting tiers (`client_for(API_TYPE)`).
14
+ - **Instance-bound WebSocket Helpers** — `client.ws_market_feed`, `client.ws_order_update`, and `client.ws_market_depth` configured automatically with the client instance's credentials and URLs.
15
+ - **Keyword Argument Initialization** — `DhanHQ::Client.new` accepts keyword configuration parameters (`dry_run: true`, `sandbox: true`, `client_id: "..."`, `access_token: "..."`) or a `Configuration` instance.
16
+ - **Per-Instance Dry-Run Mode** — `DhanHQ::Client.new(dry_run: true)` simulates state-changing writes locally for that instance regardless of the global configuration.
17
+ - **Dynamic Model Resource Invalidation** — `DhanHQ.reset_model_resources!` automatically refreshes class-level model caches (`Models::Order.resource`, etc.) whenever `DhanHQ.configure`, `configure_with_env`, `configuration=`, or `reset_configuration!` is invoked.
18
+ - **Per-Client Auto Token Management** — `client.enable_auto_token_management!(dhan_client_id: ..., pin: ..., totp_secret: ...)` for automated TOTP-based token lifecycle management per client instance.
19
+
1
20
  ## [3.4.0] - 2026-08-15
2
21
 
3
22
  ### Added
@@ -19,7 +38,9 @@
19
38
  ### Fixed
20
39
 
21
40
  - **`docs/RAILS_INTEGRATION.md`'s Sidekiq examples (§5, §6) called `client.wait!`, `client.subscribe(array)`, and `DhanHQ::WS::Client.new(kind: :order_updates)`** — none of which exist. `DhanHQ::WS::Client`/`DhanHQ::WS::Orders::Client` have no blocking wait method at all; `Client#start` spawns a background thread and returns immediately. Replaced with the real API (`DhanHQ::WS.connect`/`DhanHQ::WS::Orders.connect` plus a `connected?`-based liveness loop), guarded by a new spec that locks down the method names these examples call by name.
22
- - **`dry-validation` had no version floor in the gemspec.** A fresh `bundle install` could resolve it to `0.4.1` — a 2016-era, pre-`Dry::Validation::Contract` API generation every contract in `lib/DhanHQ/contracts/` is incompatible with. Pinned to `~> 1.11`. Found while investigating whether the Ruby floor could drop to 3.1 (it can't — see below); confirmed by forcing an actual fresh resolve under Ruby 3.1.6, which hit exactly this.
41
+ - **Every runtime dependency except `faraday` had no version floor at all** (`add_dependency "x"`, equivalent to `>= 0` — satisfied by any version ever published). Found while investigating whether the Ruby floor could drop to 3.1 (it can't — see below): forcing a fresh `bundle install` under Ruby 3.1.6 resolved `dry-validation` to `0.4.1`, a 2016-era, pre-`Dry::Validation::Contract` API generation every contract in `lib/DhanHQ/contracts/` is incompatible with — every contract spec failed at `require`-time. Pinning that alone wasn't enough: with `dry-validation` fixed, the same fresh resolve then pulled `activesupport` down to `7.2.3.2` (ActiveSupport 8.x itself requires Ruby ≥ 3.2), and AS 7-vs-8 behavioral differences broke `Agent::ToolRegistry`, `MCP::Server`, and `Risk::Pipeline` — 63 failures with no connection to Ruby version syntax at all.
42
+
43
+ All ten now have an explicit floor matching their currently locked major (`activesupport >= 8.0`, `dry-validation ~> 1.0`, and so on) — closing the class of bug, not just the two instances that happened to surface. The `dry-validation` constraint went through two attempts: the first pass in this same release pinned it to `~> 1.11`, which — because `~>` locks to the *last* segment given — actually meant `>= 1.11, < 1.12` and would have silently blocked every future 1.12+ release. Corrected to `~> 1.0`, which excludes the ancient `0.x` line (the actual problem) without capping legitimate 1.x updates.
23
44
  - **`spec/dhan_hq/contracts/expired_options_data_contract_spec.rb` and `expired_options_data_spec.rb` hardcoded `from_date: "2021-08-02"`**, which is itself now more than 5 years in the past and so failed the contract's own "cannot be more than 5 years ago" rule — the shared fixture failed the exact rule it existed to exercise, cascading into every test merged onto it. Replaced every literal 2021 date with one computed relative to `Date.today`, preserving each test's original intent (span length, ordering, the exact-31-day boundary).
24
45
 
25
46
  ### Investigated, not changed
data/GUIDE.md CHANGED
@@ -77,6 +77,22 @@ For token rotation without restarting the app, set `access_token_provider` (Proc
77
77
 
78
78
  See [docs/AUTHENTICATION.md](docs/AUTHENTICATION.md) and README “Dynamic access token”.
79
79
 
80
+ **Multi-instance clients (multi-account support)**
81
+
82
+ When you need to trade across multiple accounts, run multi-tenant setups, or isolate sandbox/testing environments in a single process, use `DhanHQ::Client.new`:
83
+
84
+ ```ruby
85
+ # Account 1
86
+ client1 = DhanHQ::Client.new(client_id: "ACC_1", access_token: "TOKEN_1")
87
+
88
+ # Account 2 (Sandbox)
89
+ client2 = DhanHQ::Client.new(client_id: "ACC_2", access_token: "TOKEN_2", sandbox: true)
90
+
91
+ # Resource accessors directly bound to client instance:
92
+ orders1 = client1.orders.all
93
+ orders2 = client2.orders.all
94
+ ```
95
+
80
96
  ---
81
97
 
82
98
  ## Working With Models
data/README.md CHANGED
@@ -218,6 +218,34 @@ end
218
218
 
219
219
  When the API returns 401, the client retries **once** with a fresh token from your provider.
220
220
 
221
+ ### Multi-Instance Clients (Multi-Account / Multi-Tenant)
222
+
223
+ For multi-tenant applications, managing multiple trading accounts concurrently, or isolating sandbox from production within the same process, construct explicit `DhanHQ::Client` instances:
224
+
225
+ ```ruby
226
+ # Account 1 (Live Trading)
227
+ client1 = DhanHQ::Client.new(
228
+ client_id: "ACC_1_ID",
229
+ access_token: "ACC_1_TOKEN"
230
+ )
231
+
232
+ # Account 2 (Sandbox / Testing)
233
+ client2 = DhanHQ::Client.new(
234
+ client_id: "ACC_2_ID",
235
+ access_token: "ACC_2_TOKEN",
236
+ sandbox: true,
237
+ dry_run: true
238
+ )
239
+
240
+ # Resource accessors are bound to the client instance:
241
+ positions = client1.positions.all
242
+ orders = client2.orders.all
243
+
244
+ # Instance-bound WebSocket streams:
245
+ ws_stream = client1.ws_market_feed(mode: :ticker) { |tick| puts tick[:ltp] }
246
+ ws_orders = client1.ws_order_update
247
+ ```
248
+
221
249
  > **Full details**: TOTP flows, partner mode, token endpoint bootstrap, auto-management — see [docs/AUTHENTICATION.md](docs/AUTHENTICATION.md).
222
250
 
223
251
  ---
@@ -106,6 +106,31 @@ end
106
106
  - **`access_token_provider`**: Callable (Proc/lambda) returning the token string. Called on every request (no memoization). When set, the gem uses it instead of `access_token`.
107
107
  - **`on_token_expired`**: Optional callable invoked when a 401/token-expired triggers a **single retry** (only when `access_token_provider` is set).
108
108
 
109
+ ## Multi-Instance Client Configuration
110
+
111
+ For multi-account or multi-tenant architectures, construct explicit `DhanHQ::Client` instances without depending on global singleton state:
112
+
113
+ ```ruby
114
+ # Direct configuration via keyword arguments
115
+ client = DhanHQ::Client.new(
116
+ client_id: "YOUR_CLIENT_ID",
117
+ access_token: "YOUR_ACCESS_TOKEN",
118
+ sandbox: false,
119
+ dry_run: false
120
+ )
121
+
122
+ # Or via an explicit Configuration object
123
+ custom_config = DhanHQ::Configuration.new do |c|
124
+ c.client_id = "TENANT_CLIENT_ID"
125
+ c.access_token = "TENANT_ACCESS_TOKEN"
126
+ c.dry_run = true
127
+ end
128
+
129
+ client = DhanHQ::Client.new(config: custom_config)
130
+ ```
131
+
132
+ Each client instance manages its own Faraday connection, isolated configuration, dry-run state, and instance-bound resource accessors (`client.orders`, `client.positions`, `client.market_feed`, etc.).
133
+
109
134
  For detailed authentication flows, see [AUTHENTICATION.md](AUTHENTICATION.md).
110
135
 
111
136
  ## Available Resources
data/lib/DhanHQ/client.rb CHANGED
@@ -42,42 +42,68 @@ module DhanHQ
42
42
  ].freeze
43
43
  private_constant :RETRYABLE_TRANSPORT_ERRORS
44
44
 
45
- attr_reader :token_manager
45
+ attr_reader :config, :token_manager, :api_type
46
46
 
47
- # Initializes a new DhanHQ Client.
48
- #
49
- # Establishes state and checks its one invariant; the HTTP connection is opened
50
- # lazily on first use, so constructing a client does no network setup.
51
- #
52
- # @example Create a new client:
53
- # client = DhanHQ::Client.new(api_type: :order_api)
47
+ # Initializes a new DhanHQ Client with instance-level configuration and rate limiting.
54
48
  #
49
+ # @param config_or_attrs [DhanHQ::Configuration, Hash, nil] Optional configuration or attribute hash
50
+ # @param config [DhanHQ::Configuration, nil] Optional keyword configuration
55
51
  # @param api_type [Symbol] Type of API (`:order_api`, `:data_api`, `:non_trading_api`)
56
- # @return [DhanHQ::Client] A new client instance.
57
- # @raise [DhanHQ::Error] If the rate limiter cannot be resolved for this API type.
58
- def initialize(api_type:)
59
- DhanHQ.ensure_configuration!
60
- # Shared per API type, so separate clients coordinate against one limit.
52
+ # @param attrs [Hash] Additional keyword configuration attributes
53
+ def initialize(config_or_attrs = nil, config: nil, api_type: :order_api, **attrs)
54
+ @config = resolve_config(config_or_attrs, config, attrs)
55
+ @api_type = api_type
61
56
  @rate_limiter = RateLimiter.for(api_type)
62
57
 
63
58
  raise DhanHQ::Error, "RateLimiter initialization failed" unless @rate_limiter
64
59
  end
65
60
 
66
61
  # The Faraday connection used for HTTP requests.
67
- #
68
- # Built on first use and rebuilt whenever the configured base URL changes — which
69
- # happens when sandbox mode is toggled, or when a token endpoint hands back a
70
- # different host mid-process.
62
+ # Built on first use and rebuilt whenever the configured base URL changes.
71
63
  #
72
64
  # @return [Faraday::Connection]
73
65
  def connection
74
- current_url = DhanHQ.configuration.base_url
66
+ current_url = @config.base_url
75
67
  return @connection if @connection && @last_base_url == current_url
76
68
 
77
69
  @last_base_url = current_url
78
70
  @connection = build_connection(current_url)
79
71
  end
80
72
 
73
+ # Resource accessors bound to this client instance. Each resource gets the client
74
+ # matching its own API_TYPE, so e.g. option chain calls throttle at 1 per 3 sec
75
+ # instead of borrowing the order client's 10/sec limiter.
76
+ def orders = @orders ||= Resources::Orders.new(client: client_for(Resources::Orders::API_TYPE))
77
+ def market_feed = @market_feed ||= Resources::MarketFeed.new(client: client_for(Resources::MarketFeed::API_TYPE))
78
+ def positions = @positions ||= Resources::Positions.new(client: client_for(Resources::Positions::API_TYPE))
79
+ def holdings = @holdings ||= Resources::Holdings.new(client: client_for(Resources::Holdings::API_TYPE))
80
+ def historical_data = @historical_data ||= Resources::HistoricalData.new(client: client_for(Resources::HistoricalData::API_TYPE))
81
+ def option_chain = @option_chain ||= Resources::OptionChain.new(client: client_for(Resources::OptionChain::API_TYPE))
82
+ def funds = @funds ||= Resources::Funds.new(client: client_for(Resources::Funds::API_TYPE))
83
+ def trades = @trades ||= Resources::Trades.new(client: client_for(Resources::Trades::API_TYPE))
84
+ def forever_orders = @forever_orders ||= Resources::ForeverOrders.new(client: client_for(Resources::ForeverOrders::API_TYPE))
85
+ def alert_orders = @alert_orders ||= Resources::AlertOrders.new(client: client_for(Resources::AlertOrders::API_TYPE))
86
+ def iceberg_orders = @iceberg_orders ||= Resources::IcebergOrders.new(client: client_for(Resources::IcebergOrders::API_TYPE))
87
+ def super_orders = @super_orders ||= Resources::SuperOrders.new(client: client_for(Resources::SuperOrders::API_TYPE))
88
+ def twap_orders = @twap_orders ||= Resources::TwapOrders.new(client: client_for(Resources::TwapOrders::API_TYPE))
89
+ def statements = @statements ||= Resources::Statements.new(client: client_for(Resources::Statements::API_TYPE))
90
+ def kill_switch = @kill_switch ||= Resources::KillSwitch.new(client: client_for(Resources::KillSwitch::API_TYPE))
91
+ def pnl_exit = @pnl_exit ||= Resources::PnlExit.new(client: client_for(Resources::PnlExit::API_TYPE))
92
+ def trader_control = @trader_control ||= Resources::TraderControl.new(client: client_for(Resources::TraderControl::API_TYPE))
93
+ def ip_setup = @ip_setup ||= Resources::IPSetup.new(client: client_for(Resources::IPSetup::API_TYPE))
94
+ def edis = @edis ||= Resources::Edis.new(client: client_for(Resources::Edis::API_TYPE))
95
+
96
+ # WebSocket streams bound to this client instance configuration
97
+ def ws_market_feed(mode: :ticker, url: nil, &block)
98
+ WS::Client.new(mode: mode, url: url, config: @config).tap do |ws|
99
+ ws.start if block_given?
100
+ ws.on(:tick, &block) if block_given?
101
+ end
102
+ end
103
+
104
+ def ws_order_update = WS::Orders::Client.new(config: @config)
105
+ def ws_market_depth = WS::MarketDepth::Client.new(config: @config)
106
+
81
107
  # Sends an HTTP request to the API with automatic retry for transient errors.
82
108
  #
83
109
  # @param method [Symbol] The HTTP method (`:get`, `:post`, `:put`, `:delete`)
@@ -93,8 +119,6 @@ module DhanHQ
93
119
  @token_manager&.ensure_valid_token!
94
120
  @rate_limiter.throttle!
95
121
 
96
- # A non-idempotent write is not safely retryable: the API has no idempotency
97
- # key, so a request that timed out may already have reached the exchange.
98
122
  effective_retries = retryable_write?(method, path) ? retries : 0
99
123
 
100
124
  with_auth_retry do
@@ -112,10 +136,9 @@ module DhanHQ
112
136
  yield
113
137
  rescue DhanHQ::InvalidAuthenticationError, DhanHQ::InvalidTokenError,
114
138
  DhanHQ::TokenExpiredError, DhanHQ::AuthenticationFailedError => e
115
- config = DhanHQ.configuration
116
- raise unless config&.access_token_provider
139
+ raise unless @config&.access_token_provider
117
140
 
118
- config.on_token_expired&.call(e)
141
+ @config.on_token_expired&.call(e)
119
142
  DhanHQ.logger&.warn("[DhanHQ::Client] Auth failure (#{e.class}), retrying once with fresh token")
120
143
  yield
121
144
  end
@@ -218,20 +241,47 @@ module DhanHQ
218
241
  @token_manager.generate!
219
242
  end
220
243
 
244
+ # A client sharing this one's config but rate-limited for a different API tier.
245
+ # RateLimiter.for(api_type) is a shared instance per type, so sibling clients still
246
+ # throttle against the same counters as any other client using that tier.
247
+ #
248
+ # @param api_type [Symbol] Target API tier (:order_api, :data_api, :non_trading_api, :option_chain)
249
+ # @return [DhanHQ::Client]
250
+ def client_for(api_type)
251
+ return self if api_type == @api_type
252
+
253
+ @sibling_clients ||= {}
254
+ @sibling_clients[api_type] ||= self.class.new(config: @config, api_type: api_type)
255
+ end
256
+
221
257
  private
222
258
 
259
+ def resolve_config(config_or_attrs, keyword_config, attrs = {})
260
+ if config_or_attrs.is_a?(DhanHQ::Configuration)
261
+ config_or_attrs
262
+ elsif config_or_attrs.is_a?(Hash)
263
+ DhanHQ::Configuration.new(config_or_attrs.merge(attrs))
264
+ elsif keyword_config
265
+ keyword_config
266
+ elsif attrs.any?
267
+ DhanHQ::Configuration.new(attrs)
268
+ else
269
+ DhanHQ.ensure_configuration!
270
+ end
271
+ end
272
+
223
273
  # Simulator that answers state-changing requests locally when dry run is on.
224
274
  #
225
275
  # @return [DhanHQ::DryRun::Simulator]
226
276
  def dry_run
227
- @dry_run ||= DhanHQ::DryRun::Simulator.new
277
+ @dry_run ||= DhanHQ::DryRun::Simulator.new(config: @config)
228
278
  end
229
279
 
230
280
  # A write may be auto-retried only when the user has explicitly opted in.
231
281
  def retryable_write?(method, path)
232
282
  return true unless WritePaths.mutating?(method, path)
233
283
 
234
- DhanHQ.configuration&.retry_non_idempotent_writes? || false
284
+ @config&.retry_non_idempotent_writes? || false
235
285
  end
236
286
 
237
287
  # Fills in a +correlationId+ on order placements that lack one, so a caller who
@@ -240,7 +290,7 @@ module DhanHQ
240
290
  def with_correlation_id(method, path, payload)
241
291
  return payload unless WritePaths.order_placement?(method, path)
242
292
  return payload unless payload.is_a?(Hash)
243
- return payload unless DhanHQ.configuration&.auto_correlation_id?
293
+ return payload unless @config&.auto_correlation_id?
244
294
  return payload if correlation_id?(payload)
245
295
 
246
296
  inject_correlation_id(payload)
@@ -212,30 +212,32 @@ module DhanHQ
212
212
  @ws_debug == true
213
213
  end
214
214
 
215
- # Initializes a new configuration instance with default values.
216
- #
217
- # @example
218
- # config = DhanHQ::Configuration.new
219
- # config.client_id = "your_client_id"
220
- # config.access_token = "your_access_token"
221
- def initialize
222
- @client_id = ENV.fetch("DHAN_CLIENT_ID", nil)
223
- @access_token = ENV.fetch("DHAN_ACCESS_TOKEN", nil)
224
- @sandbox = env_flag("DHAN_SANDBOX", default: false)
225
- @dry_run = env_flag("DHAN_DRY_RUN", default: false)
226
- @retry_non_idempotent_writes = env_flag("DHAN_RETRY_WRITES", default: false)
227
- @auto_correlation_id = env_flag("DHAN_AUTO_CORRELATION_ID", default: false)
228
- @warn_on_ambiguous_write_failure = env_flag("DHAN_WARN_AMBIGUOUS_WRITE_FAILURE", default: true)
229
- @base_url = ENV.fetch("DHAN_BASE_URL", nil)
230
- @ws_version = ENV.fetch("DHAN_WS_VERSION", 2).to_i
231
- @ws_order_url = ENV.fetch("DHAN_WS_ORDER_URL", nil)
232
- @ws_market_feed_url = ENV.fetch("DHAN_WS_MARKET_FEED_URL", nil)
233
- @ws_market_depth_url = ENV.fetch("DHAN_WS_MARKET_DEPTH_URL", nil)
234
- @market_depth_level = ENV.fetch("DHAN_MARKET_DEPTH_LEVEL", "20").to_i
235
- @ws_debug = env_flag("DHAN_WS_DEBUG", default: false)
236
- @ws_user_type = ENV.fetch("DHAN_WS_USER_TYPE", "SELF")
237
- @partner_id = ENV.fetch("DHAN_PARTNER_ID", nil)
238
- @partner_secret = ENV.fetch("DHAN_PARTNER_SECRET", nil)
215
+ def initialize(attrs = {})
216
+ @client_id = attrs[:client_id] || ENV.fetch("DHAN_CLIENT_ID", nil)
217
+ @access_token = attrs[:access_token] || ENV.fetch("DHAN_ACCESS_TOKEN", nil)
218
+ @access_token_provider = attrs[:access_token_provider]
219
+ @on_token_expired = attrs[:on_token_expired]
220
+ @sandbox = attrs.key?(:sandbox) ? attrs[:sandbox] : env_flag("DHAN_SANDBOX", default: false)
221
+ @dry_run = attrs.key?(:dry_run) ? attrs[:dry_run] : env_flag("DHAN_DRY_RUN", default: false)
222
+ @retry_non_idempotent_writes = attrs.key?(:retry_non_idempotent_writes) ? attrs[:retry_non_idempotent_writes] : env_flag("DHAN_RETRY_WRITES", default: false)
223
+ @auto_correlation_id = attrs.key?(:auto_correlation_id) ? attrs[:auto_correlation_id] : env_flag("DHAN_AUTO_CORRELATION_ID", default: false)
224
+ @warn_on_ambiguous_write_failure = if attrs.key?(:warn_on_ambiguous_write_failure)
225
+ attrs[:warn_on_ambiguous_write_failure]
226
+ else
227
+ env_flag("DHAN_WARN_AMBIGUOUS_WRITE_FAILURE",
228
+ default: true)
229
+ end
230
+ @base_url = attrs[:base_url] || ENV.fetch("DHAN_BASE_URL", nil)
231
+ @ws_version = (attrs[:ws_version] || ENV.fetch("DHAN_WS_VERSION", 2)).to_i
232
+ @ws_order_url = attrs[:ws_order_url] || ENV.fetch("DHAN_WS_ORDER_URL", nil)
233
+ @ws_market_feed_url = attrs[:ws_market_feed_url] || ENV.fetch("DHAN_WS_MARKET_FEED_URL", nil)
234
+ @ws_market_depth_url = attrs[:ws_market_depth_url] || ENV.fetch("DHAN_WS_MARKET_DEPTH_URL", nil)
235
+ @market_depth_level = (attrs[:market_depth_level] || ENV.fetch("DHAN_MARKET_DEPTH_LEVEL", "20")).to_i
236
+ @ws_debug = attrs.key?(:ws_debug) ? attrs[:ws_debug] : env_flag("DHAN_WS_DEBUG", default: false)
237
+ @ws_user_type = attrs[:ws_user_type] || ENV.fetch("DHAN_WS_USER_TYPE", "SELF")
238
+ @partner_id = attrs[:partner_id] || ENV.fetch("DHAN_PARTNER_ID", nil)
239
+ @partner_secret = attrs[:partner_secret] || ENV.fetch("DHAN_PARTNER_SECRET", nil)
240
+ yield(self) if block_given?
239
241
  end
240
242
 
241
243
  private
@@ -17,9 +17,10 @@ module DhanHQ
17
17
 
18
18
  # Initializes the BaseAPI with the appropriate Client instance
19
19
  #
20
+ # @param client [DhanHQ::Client, nil] Optional injected client instance
20
21
  # @param api_type [Symbol] API type (`:order_api`, `:data_api`, `:non_trading_api`)
21
- def initialize(api_type: self.class::API_TYPE)
22
- @client = DhanHQ::Client.new(api_type: api_type)
22
+ def initialize(client: nil, api_type: self.class::API_TYPE)
23
+ @client = client || DhanHQ::Client.new(api_type: api_type)
23
24
  end
24
25
 
25
26
  # Perform a GET request via `Client`
@@ -25,8 +25,9 @@ module DhanHQ
25
25
  # Extracts a fabricated id out of a request path.
26
26
  ID_PATTERN = /#{Regexp.escape(ID_PREFIX)}[A-Za-z0-9]+/
27
27
 
28
- def initialize(ledger: Ledger.instance)
28
+ def initialize(ledger: Ledger.instance, config: nil)
29
29
  @ledger = ledger
30
+ @config = config
30
31
  end
31
32
 
32
33
  # Whether this request should be answered locally.
@@ -35,7 +36,7 @@ module DhanHQ
35
36
  # @param path [String, nil] Request path
36
37
  # @return [Boolean]
37
38
  def simulates?(method, path)
38
- return false unless DhanHQ.configuration&.dry_run?
39
+ return false unless (@config || DhanHQ.configuration)&.dry_run?
39
40
 
40
41
  WritePaths.mutating?(method, path) || replayable_read?(method, path)
41
42
  end
@@ -21,6 +21,16 @@ module DhanHQ
21
21
 
22
22
  private
23
23
 
24
+ def current_config
25
+ if respond_to?(:config) && config
26
+ config
27
+ elsif respond_to?(:client) && client.respond_to?(:config) && client.config
28
+ client.config
29
+ else
30
+ DhanHQ.configuration
31
+ end
32
+ end
33
+
24
34
  # Dynamically builds headers for each request.
25
35
  #
26
36
  # @param path [String] The API endpoint path.
@@ -41,7 +51,7 @@ module DhanHQ
41
51
 
42
52
  # Add client-id for DATA APIs (now including sandbox profile/funds)
43
53
  if data_api?(path)
44
- client_id = DhanHQ.configuration&.client_id
54
+ client_id = current_config&.client_id
45
55
  unless client_id
46
56
  raise DhanHQ::InvalidAuthenticationError,
47
57
  "client_id is required for DATA APIs but not set. Please configure DhanHQ with DHAN_CLIENT_ID."
@@ -53,7 +63,7 @@ module DhanHQ
53
63
  end
54
64
 
55
65
  def resolved_access_token
56
- DhanHQ.configuration&.resolved_access_token
66
+ current_config&.resolved_access_token
57
67
  end
58
68
 
59
69
  # Determines if the API path requires a `client-id` header.
@@ -80,7 +90,7 @@ module DhanHQ
80
90
 
81
91
  out = payload
82
92
  if path && %i[post put patch].include?(method)
83
- client_id = DhanHQ.configuration&.client_id
93
+ client_id = current_config&.client_id
84
94
  needs_client_id = data_api?(path) || payload_requires_dhan_client_id?(path)
85
95
  if client_id && needs_client_id && !payload.key?(:dhanClientId) && !payload.key?("dhanClientId")
86
96
  out = payload.dup
@@ -49,7 +49,7 @@ module DhanHQ
49
49
  #
50
50
  # @return [MarketFeed]
51
51
  def quote_resource
52
- @quote_resource ||= self.class.new(api_type: :quote_api)
52
+ @quote_resource ||= self.class.new(client: DhanHQ::Client.new(config: client.config, api_type: :quote_api))
53
53
  end
54
54
  end
55
55
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module DhanHQ
4
4
  # Semantic version of the DhanHQ client gem.
5
- VERSION = "3.4.0"
5
+ VERSION = "4.0.0"
6
6
  end
@@ -23,8 +23,9 @@ module DhanHQ
23
23
  # @param options [Hash] Connection options
24
24
  # @option options [Integer] :max_backoff Maximum backoff time (default: 90)
25
25
  # @option options [Integer] :cool_off_429 Cool off time for 429 errors (default: 60)
26
- def initialize(url:, **options)
26
+ def initialize(url:, config: nil, **options)
27
27
  @url = url
28
+ @config = config || DhanHQ.configuration || DhanHQ.ensure_configuration!
28
29
  @callbacks = Concurrent::Map.new { |h, k| h[k] = [] }
29
30
  @started = Concurrent::AtomicBoolean.new(false)
30
31
  @stop = false
@@ -20,7 +20,8 @@ module DhanHQ
20
20
  class Client
21
21
  # @param mode [Symbol] Feed mode (:ticker, :quote, :full).
22
22
  # @param url [String, nil] Optional custom WebSocket endpoint.
23
- def initialize(mode: :ticker, url: nil)
23
+ # @param config [DhanHQ::Configuration, nil] Optional configuration instance.
24
+ def initialize(mode: :ticker, url: nil, config: nil)
24
25
  @mode = mode # :ticker, :quote, :full (adjust to your API)
25
26
  @bus = CmdBus.new
26
27
  @state = SubState.new
@@ -29,13 +30,14 @@ module DhanHQ
29
30
  @last_message_at = Concurrent::AtomicReference.new(nil)
30
31
  @connected_at = Concurrent::AtomicReference.new(nil)
31
32
  @reconnects = Concurrent::AtomicFixnum.new(0)
33
+ @config = config || DhanHQ.configuration || DhanHQ.ensure_configuration!
32
34
 
33
- token = DhanHQ.configuration.resolved_access_token
35
+ token = @config.resolved_access_token
34
36
  raise DhanHQ::AuthenticationError, "Missing access token" if token.nil? || token.empty?
35
37
 
36
- cid = DhanHQ.configuration.client_id or raise "DhanHQ.client_id not set"
37
- ver = (DhanHQ.configuration.respond_to?(:ws_version) && DhanHQ.configuration.ws_version) || 2
38
- base = url || DhanHQ.configuration.ws_market_feed_url
38
+ cid = @config.client_id or raise "DhanHQ.client_id not set"
39
+ ver = (@config.respond_to?(:ws_version) && @config.ws_version) || 2
40
+ base = url || @config.ws_market_feed_url
39
41
  @url = base.include?("?") ? base : "#{base}?version=#{ver}&token=#{token}&clientId=#{cid}&authType=2"
40
42
  end
41
43
 
@@ -19,11 +19,12 @@ module DhanHQ
19
19
  ##
20
20
  # Initialize Market Depth WebSocket client
21
21
  # @param symbols [Array<String, Hash>] List of symbols (or metadata hashes) to subscribe to
22
+ # @param config [DhanHQ::Configuration, nil] Optional configuration instance
22
23
  # @param options [Hash] Connection options
23
- def initialize(symbols: [], **options)
24
- cfg = DhanHQ.configuration
24
+ def initialize(symbols: [], config: nil, **options)
25
+ cfg = config || DhanHQ.configuration || DhanHQ.ensure_configuration!
25
26
  url = options[:url] || build_market_depth_url(cfg)
26
- super(url: url, **options)
27
+ super(url: url, config: cfg, **options)
27
28
 
28
29
  @symbols = Array(symbols)
29
30
  @subscriptions = Concurrent::Map.new
@@ -16,15 +16,15 @@ module DhanHQ
16
16
  # Maximum age of orders in tracker in seconds (default: 7 days)
17
17
  MAX_ORDER_AGE = ENV.fetch("DHAN_WS_MAX_ORDER_AGE", 604_800).to_i
18
18
 
19
- def initialize(url: nil, **options)
19
+ def initialize(url: nil, config: nil, **options)
20
20
  @callbacks = Concurrent::Map.new { |h, k| h[k] = [] }
21
21
  @started = Concurrent::AtomicBoolean.new(false)
22
22
  @order_tracker = Concurrent::Map.new
23
23
  @order_timestamps = Concurrent::Map.new
24
24
  @cleanup_mutex = Mutex.new
25
25
  @cleanup_thread = nil
26
- cfg = DhanHQ.configuration
27
- @url = url || cfg.ws_order_url
26
+ @config = config || DhanHQ.configuration || DhanHQ.ensure_configuration!
27
+ @url = url || @config.ws_order_url
28
28
  @connection_options = options
29
29
  end
30
30
 
@@ -35,7 +35,7 @@ module DhanHQ
35
35
  return self if @started.true?
36
36
 
37
37
  @started.make_true
38
- @conn = Connection.new(url: @url, **@connection_options)
38
+ @conn = Connection.new(url: @url, config: @config, **@connection_options)
39
39
  @conn.on(:open) { emit(:open, true) }
40
40
  @conn.on(:close) { |payload| emit(:close, payload) }
41
41
  @conn.on(:error) { |error| emit(:error, error) }
@@ -78,7 +78,7 @@ module DhanHQ
78
78
  ##
79
79
  # Authenticate with DhanHQ Orders WebSocket
80
80
  def authenticate
81
- cfg = DhanHQ.configuration
81
+ cfg = @config || DhanHQ.configuration
82
82
 
83
83
  if cfg.ws_user_type.to_s.upcase == "PARTNER"
84
84
  payload = {
data/lib/dhan_hq.rb CHANGED
@@ -3,7 +3,11 @@
3
3
  require "json"
4
4
  require "logger"
5
5
  require "zeitwerk"
6
- require "dotenv/load"
6
+ begin
7
+ require "dotenv/load"
8
+ rescue LoadError
9
+ # Dotenv is optional in host applications
10
+ end
7
11
  require "faraday"
8
12
  # Minimal eager requires for backward-compatible constants.
9
13
  # These are widely referenced (e.g. `DhanHQ::BaseAPI`) and should not depend on
@@ -88,9 +92,13 @@ module DhanHQ
88
92
  # @return [String]
89
93
  BASE_URL = Constants::Urls::REST_API_BASE
90
94
  # The current configuration instance.
91
- #
92
- # @return [DhanHQ::Configuration, nil] The current configuration or `nil` if not set.
93
- attr_accessor :configuration
95
+ attr_reader :configuration
96
+
97
+ def configuration=(config)
98
+ @configuration = config
99
+ @default_client = nil
100
+ reset_model_resources!
101
+ end
94
102
 
95
103
  attr_writer :logger
96
104
 
@@ -106,8 +114,10 @@ module DhanHQ
106
114
  # @return [void]
107
115
  def configure
108
116
  self.configuration ||= Configuration.new
109
- yield(configuration)
117
+ yield(configuration) if block_given?
110
118
  self.logger ||= Logger.new($stdout, level: Logger::INFO)
119
+ @default_client = Client.new(config: configuration)
120
+ reset_model_resources!
111
121
  end
112
122
 
113
123
  # default logger so calls like DhanHQ.logger&.info never explode
@@ -118,6 +128,18 @@ module DhanHQ
118
128
  @logger ||= Logger.new($stdout, level: Logger::INFO)
119
129
  end
120
130
 
131
+ # Global default client (used for backward compatibility with class-level APIs)
132
+ #
133
+ # @return [DhanHQ::Client]
134
+ def default_client
135
+ @default_client ||= Client.new(config: ensure_configuration!)
136
+ end
137
+
138
+ # Resets the global default client
139
+ def reset_default_client!
140
+ @default_client = nil
141
+ end
142
+
121
143
  # Configures the DhanHQ client using environment variables.
122
144
  #
123
145
  # When credentials are injected via `DHAN_ACCESS_TOKEN` and `DHAN_CLIENT_ID` this helper
@@ -129,6 +151,8 @@ module DhanHQ
129
151
  # @return [void]
130
152
  def configure_with_env
131
153
  self.configuration = Configuration.new
154
+ @default_client = Client.new(config: configuration)
155
+ reset_model_resources!
132
156
  end
133
157
 
134
158
  # Ensures configuration exists, bootstrapping from ENV when nil.
@@ -140,9 +164,32 @@ module DhanHQ
140
164
  self.configuration ||= Configuration.new
141
165
  end
142
166
 
143
- # Resets the configuration to nil.
167
+ # Resets the configuration and default client to nil.
144
168
  def reset_configuration!
145
169
  self.configuration = nil
170
+ @default_client = nil
171
+ reset_model_resources!
172
+ end
173
+
174
+ # Clears memoized class-level resource and API instances across all models.
175
+ def reset_model_resources!
176
+ models = [
177
+ BaseModel,
178
+ Models::Order, Models::Holding, Models::Position, Models::HistoricalData,
179
+ Models::Trade, Models::OptionChain, Models::Funds, Models::AlertOrder,
180
+ Models::ForeverOrder, Models::IcebergOrder, Models::SuperOrder, Models::TwapOrder,
181
+ Models::LedgerEntry, Models::Instrument, Models::Edis, Models::Profile,
182
+ Models::Margin, Models::MultiOrder, Models::KillSwitch, Models::PnlExit,
183
+ Models::MarketFeed, Models::ExpiredOptionsData
184
+ ]
185
+
186
+ models.each do |klass|
187
+ next unless klass.is_a?(Class)
188
+
189
+ klass.instance_variables.each do |var|
190
+ klass.instance_variable_set(var, nil) if var.to_s.end_with?("resource", "api")
191
+ end
192
+ end
146
193
  end
147
194
 
148
195
  # Configures the DhanHQ client by fetching credentials from a token endpoint.
@@ -164,39 +211,14 @@ module DhanHQ
164
211
  def configure_from_token_endpoint(base_url: nil, bearer_token: nil)
165
212
  base_url ||= ENV.fetch("DHAN_TOKEN_ENDPOINT_BASE_URL", nil)
166
213
  bearer_token ||= ENV.fetch("DHAN_TOKEN_ENDPOINT_BEARER", nil)
167
-
168
214
  raise DhanHQ::TokenEndpointError, "base_url and bearer_token (or ENV DHAN_TOKEN_ENDPOINT_*) are required" if base_url.to_s.empty? || bearer_token.to_s.empty?
169
215
 
170
- url = "#{base_url.to_s.chomp("/")}/auth/dhan/token"
171
- conn = ::Faraday.new(url: url) do |c|
172
- c.request :url_encoded
173
- c.adapter ::Faraday.default_adapter
174
- end
175
-
176
- response = conn.get("") do |req|
177
- req.headers["Authorization"] = "Bearer #{bearer_token}"
178
- req.headers["Accept"] = "application/json"
179
- end
180
-
181
- unless response.success?
182
- body = parse_json_body(response.body)
183
- msg = body["error"] || body["message"] || body["errorMessage"] || response.body.to_s
184
- raise DhanHQ::TokenEndpointError, "Token endpoint returned #{response.status}: #{msg}"
185
- end
186
-
187
- data = parse_json_body(response.body)
188
- data = data.transform_keys(&:to_s) if data.is_a?(Hash)
189
-
190
- access_token = data["access_token"] || data[:access_token]
191
- client_id = data["client_id"] || data[:client_id]
216
+ data = fetch_token_endpoint("#{base_url.to_s.chomp("/")}/auth/dhan/token", bearer_token, "Token endpoint")
217
+ access_token = data["access_token"]
218
+ client_id = data["client_id"]
192
219
  raise DhanHQ::TokenEndpointError, "Token endpoint response missing access_token or client_id" if access_token.to_s.empty? || client_id.to_s.empty?
193
220
 
194
- self.configuration ||= Configuration.new
195
- configuration.access_token = access_token.to_s
196
- configuration.client_id = client_id.to_s
197
- dhan_base = data["base_url"] || data[:base_url]
198
- configuration.base_url = dhan_base.to_s if dhan_base.to_s != ""
199
- configuration
221
+ apply_token_config(access_token, client_id, data["base_url"])
200
222
  end
201
223
 
202
224
  # Configures the DhanHQ client by fetching credentials from a dashboard API.
@@ -208,6 +230,18 @@ module DhanHQ
208
230
  def configure_from_dashboard(bearer_token:, url: "http://localhost:3011/api/dhan_access_token")
209
231
  raise DhanHQ::TokenEndpointError, "bearer_token is required" if bearer_token.to_s.empty?
210
232
 
233
+ data = fetch_token_endpoint(url, bearer_token, "Dashboard")
234
+ access_token = data["access_token"] || data["accessToken"] || data["dhan_access_token"] || data["dhanaccesstoken"]
235
+ client_id = data["client_id"] || data["clientId"] || data["dhan_client_id"] || data["dhanClientId"]
236
+ client_id ||= configuration&.client_id || ENV.fetch("DHAN_CLIENT_ID", nil)
237
+
238
+ raise DhanHQ::TokenEndpointError, "Dashboard response missing access_token (tried access_token, dhan_access_token, dhanaccesstoken)" if access_token.to_s.empty?
239
+ raise DhanHQ::TokenEndpointError, "Dashboard response missing client_id, and no fallback client_id was found in config or ENV['DHAN_CLIENT_ID']" if client_id.to_s.empty?
240
+
241
+ apply_token_config(access_token, client_id, data["base_url"] || data["baseUrl"])
242
+ end
243
+
244
+ def fetch_token_endpoint(url, bearer_token, label)
211
245
  conn = ::Faraday.new(url: url) do |c|
212
246
  c.request :url_encoded
213
247
  c.adapter ::Faraday.default_adapter
@@ -221,24 +255,18 @@ module DhanHQ
221
255
  unless response.success?
222
256
  body = parse_json_body(response.body)
223
257
  msg = body["error"] || body["message"] || body["errorMessage"] || response.body.to_s
224
- raise DhanHQ::TokenEndpointError, "Dashboard returned #{response.status}: #{msg}"
258
+ raise DhanHQ::TokenEndpointError, "#{label} returned #{response.status}: #{msg}"
225
259
  end
226
260
 
227
261
  data = parse_json_body(response.body)
228
- data = data.transform_keys(&:to_s) if data.is_a?(Hash)
229
-
230
- access_token = data["access_token"] || data["accessToken"] || data["dhan_access_token"] || data["dhanaccesstoken"]
231
- client_id = data["client_id"] || data["clientId"] || data["dhan_client_id"] || data["dhanClientId"]
232
- client_id ||= self.configuration&.client_id || ENV.fetch("DHAN_CLIENT_ID", nil)
233
-
234
- raise DhanHQ::TokenEndpointError, "Dashboard response missing access_token (tried access_token, dhan_access_token, dhanaccesstoken)" if access_token.to_s.empty?
235
- raise DhanHQ::TokenEndpointError, "Dashboard response missing client_id, and no fallback client_id was found in config or ENV['DHAN_CLIENT_ID']" if client_id.to_s.empty?
262
+ data.is_a?(Hash) ? data.transform_keys(&:to_s) : {}
263
+ end
236
264
 
265
+ def apply_token_config(access_token, client_id, base_url)
237
266
  self.configuration ||= Configuration.new
238
267
  configuration.access_token = access_token.to_s
239
268
  configuration.client_id = client_id.to_s
240
- dhan_base = data["base_url"] || data["baseUrl"]
241
- configuration.base_url = dhan_base.to_s if dhan_base.to_s != ""
269
+ configuration.base_url = base_url.to_s if base_url.to_s != ""
242
270
  configuration
243
271
  end
244
272
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: DhanHQ
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shubham Taywade
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-15 00:00:00.000000000 Z
11
+ date: 2026-10-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -16,98 +16,98 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '8.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: '8.0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: base64
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: '0.1'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: '0.1'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: bindata
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '0'
47
+ version: '2.4'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '0'
54
+ version: '2.4'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: concurrent-ruby
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: '0'
61
+ version: '1.3'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: '0'
68
+ version: '1.3'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: csv
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
74
74
  - !ruby/object:Gem::Version
75
- version: '0'
75
+ version: '3.0'
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
- version: '0'
82
+ version: '3.0'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: dry-validation
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '1.11'
89
+ version: '1.0'
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '1.11'
96
+ version: '1.0'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: eventmachine
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - ">="
102
102
  - !ruby/object:Gem::Version
103
- version: '0'
103
+ version: '1.2'
104
104
  type: :runtime
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
- version: '0'
110
+ version: '1.2'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: faraday
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -128,42 +128,42 @@ dependencies:
128
128
  requirements:
129
129
  - - ">="
130
130
  - !ruby/object:Gem::Version
131
- version: '0'
131
+ version: '0.11'
132
132
  type: :runtime
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - ">="
137
137
  - !ruby/object:Gem::Version
138
- version: '0'
138
+ version: '0.11'
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: rotp
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
143
  - - ">="
144
144
  - !ruby/object:Gem::Version
145
- version: '0'
145
+ version: '6.0'
146
146
  type: :runtime
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
150
  - - ">="
151
151
  - !ruby/object:Gem::Version
152
- version: '0'
152
+ version: '6.0'
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: zeitwerk
155
155
  requirement: !ruby/object:Gem::Requirement
156
156
  requirements:
157
157
  - - ">="
158
158
  - !ruby/object:Gem::Version
159
- version: '0'
159
+ version: '2.6'
160
160
  type: :runtime
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
- version: '0'
166
+ version: '2.6'
167
167
  description: A production-grade Ruby SDK and Ruby client for Dhan API v2 built for
168
168
  algorithmic trading, portfolio monitoring, and live trading systems on NSE, BSE
169
169
  and MCX. Provides typed models, token lifecycle management, dry-validation contracts,