DhanHQ 3.4.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +61 -1
  3. data/GUIDE.md +40 -0
  4. data/README.md +103 -0
  5. data/docs/CONFIGURATION.md +29 -2
  6. data/docs/ENDPOINTS_AND_SANDBOX.md +83 -38
  7. data/lib/DhanHQ/client.rb +101 -26
  8. data/lib/DhanHQ/concerns/order_audit.rb +26 -3
  9. data/lib/DhanHQ/configuration.rb +178 -27
  10. data/lib/DhanHQ/constants.rb +60 -1
  11. data/lib/DhanHQ/contracts/company_info_contract.rb +24 -0
  12. data/lib/DhanHQ/contracts/market_movers_contract.rb +57 -0
  13. data/lib/DhanHQ/contracts/news_headline_contract.rb +21 -0
  14. data/lib/DhanHQ/contracts/technical_data_contract.rb +25 -0
  15. data/lib/DhanHQ/core/base_api.rb +3 -17
  16. data/lib/DhanHQ/core/base_model.rb +32 -12
  17. data/lib/DhanHQ/dry_run/simulator.rb +3 -2
  18. data/lib/DhanHQ/helpers/attribute_helper.rb +0 -22
  19. data/lib/DhanHQ/helpers/request_helper.rb +13 -3
  20. data/lib/DhanHQ/models/option_chain.rb +7 -0
  21. data/lib/DhanHQ/models/order.rb +7 -7
  22. data/lib/DhanHQ/rate_limiter.rb +34 -40
  23. data/lib/DhanHQ/resources/global_stocks/funds.rb +1 -1
  24. data/lib/DhanHQ/resources/global_stocks/holdings.rb +1 -1
  25. data/lib/DhanHQ/resources/global_stocks/margin_calculator.rb +1 -1
  26. data/lib/DhanHQ/resources/global_stocks/market_status.rb +1 -1
  27. data/lib/DhanHQ/resources/global_stocks/orders.rb +1 -1
  28. data/lib/DhanHQ/resources/global_stocks/trades.rb +1 -1
  29. data/lib/DhanHQ/resources/market_feed.rb +1 -1
  30. data/lib/DhanHQ/resources/scanx.rb +98 -0
  31. data/lib/DhanHQ/version.rb +1 -1
  32. data/lib/DhanHQ/ws/base_connection.rb +2 -1
  33. data/lib/DhanHQ/ws/client.rb +10 -13
  34. data/lib/DhanHQ/ws/connection.rb +13 -15
  35. data/lib/DhanHQ/ws/market_depth/client.rb +9 -4
  36. data/lib/DhanHQ/ws/orders/client.rb +4 -4
  37. data/lib/DhanHQ/ws/orders/connection.rb +1 -1
  38. data/lib/DhanHQ/ws/registry.rb +10 -6
  39. data/lib/dhan_hq.rb +81 -48
  40. metadata +27 -22
@@ -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,16 @@ 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
39
+ raise DhanHQ::AuthenticationError, "client_id is not set (configure DhanHQ.client_id)" if cid.nil? || cid.to_s.empty?
40
+
41
+ ver = (@config.respond_to?(:ws_version) && @config.ws_version) || 2
42
+ base = url || @config.ws_market_feed_url
39
43
  @url = base.include?("?") ? base : "#{base}?version=#{ver}&token=#{token}&clientId=#{cid}&authType=2"
40
44
  end
41
45
 
@@ -52,7 +56,7 @@ module DhanHQ
52
56
  emit(:tick, tick) if tick
53
57
  end
54
58
  Registry.register(self)
55
- install_at_exit_once!
59
+ self.class.install_at_exit_hook!
56
60
  @conn.start
57
61
  self
58
62
  end
@@ -293,13 +297,6 @@ module DhanHQ
293
297
  rescue StandardError => e
294
298
  DhanHQ.logger&.error("[DhanHQ::WS::Client] Error in event handler for #{event}: #{e.class} #{e.message}")
295
299
  end
296
-
297
- def install_at_exit_once!
298
- return if defined?(@at_exit_installed) && @at_exit_installed
299
-
300
- @at_exit_installed = true
301
- at_exit { Registry.stop_all }
302
- end
303
300
  end
304
301
  end
305
302
  end
@@ -9,9 +9,14 @@ module DhanHQ
9
9
  # Low-level wrapper responsible for establishing and maintaining the raw
10
10
  # WebSocket connection to the streaming API.
11
11
  class Connection
12
- SUB_CODES = { ticker: 15, quote: 15, full: 15 }.freeze # All use RequestCode 15 per official API
13
- # Request codes used when unsubscribing from feeds.
14
- UNSUB_CODES = { ticker: 12, quote: 12, full: 12 }.freeze # Use disconnect code 12 for unsubscribe
12
+ # Feed request codes per the official Annexure "Feed Request Code" table
13
+ # (dhanhq.co/docs/v2/annexure#feed-request-code): 15/16 ticker, 17/18 quote,
14
+ # 21/22 full. RequestCode 12 is the whole-connection Disconnect Feed frame
15
+ # and is reserved for #send_disconnect.
16
+ SUB_CODES = { ticker: 15, quote: 17, full: 21 }.freeze
17
+ # Unsubscribe codes per the same table: 16/18/22. Never 12 — that is the
18
+ # whole-connection disconnect, not a per-instrument unsubscribe.
19
+ UNSUB_CODES = { ticker: 16, quote: 18, full: 22 }.freeze
15
20
 
16
21
  COOL_OFF_429 = 60 # seconds to cool off on 429
17
22
  MAX_BACKOFF = 90 # cap exponential backoff
@@ -219,7 +224,7 @@ module DhanHQ
219
224
  subs, unsubs = cmds.partition { |c| c.op == :sub }
220
225
 
221
226
  unless subs.empty?
222
- list = uniq(flatten(subs.map(&:payload)))
227
+ list = uniq(subs.map(&:payload).flatten)
223
228
  new_only = @state.want_sub(list)
224
229
  unless new_only.empty?
225
230
  send_sub(new_only)
@@ -229,7 +234,7 @@ module DhanHQ
229
234
 
230
235
  return if unsubs.empty?
231
236
 
232
- list = uniq(flatten(unsubs.map(&:payload)))
237
+ list = uniq(unsubs.map(&:payload).flatten)
233
238
  exist_only = @state.want_unsub(list)
234
239
  return if exist_only.empty?
235
240
 
@@ -267,17 +272,10 @@ module DhanHQ
267
272
  DhanHQ.logger&.debug("[DhanHQ::WS] send_disconnect error #{e.class}: #{e.message}")
268
273
  end
269
274
 
270
- def flatten(a) = a.flatten
271
-
272
275
  def uniq(list)
273
- seen = {}
274
- list.each_with_object([]) do |i, out|
275
- k = "#{i[:ExchangeSegment]}:#{i[:SecurityId]}"
276
- next if seen[k]
277
-
278
- out << i
279
- seen[k] = true
280
- end
276
+ # Composite key: the same instrument can arrive via repeated subscribe
277
+ # calls; Array#uniq with a block keeps the first occurrence.
278
+ list.uniq { |i| [i[:ExchangeSegment], i[:SecurityId]] }
281
279
  end
282
280
  end
283
281
  end
@@ -13,17 +13,22 @@ module DhanHQ
13
13
  # WebSocket client for Full Market Depth data
14
14
  # Provides real-time market depth (bid/ask levels) for specified symbols
15
15
  class Client < BaseConnection
16
+ # Feed request codes per the official Annexure "Feed Request Code" table
17
+ # (dhanhq.co/docs/v2/annexure#feed-request-code): 23 = Subscribe - Full
18
+ # Market Depth, 24 = Unsubscribe - Full Market Depth. Never 12 - that is
19
+ # the whole-connection disconnect, not a per-instrument unsubscribe.
16
20
  SUBSCRIBE_REQUEST_CODE = 23
17
- UNSUBSCRIBE_REQUEST_CODE = 12
21
+ UNSUBSCRIBE_REQUEST_CODE = 24
18
22
 
19
23
  ##
20
24
  # Initialize Market Depth WebSocket client
21
25
  # @param symbols [Array<String, Hash>] List of symbols (or metadata hashes) to subscribe to
26
+ # @param config [DhanHQ::Configuration, nil] Optional configuration instance
22
27
  # @param options [Hash] Connection options
23
- def initialize(symbols: [], **options)
24
- cfg = DhanHQ.configuration
28
+ def initialize(symbols: [], config: nil, **options)
29
+ cfg = config || DhanHQ.configuration || DhanHQ.ensure_configuration!
25
30
  url = options[:url] || build_market_depth_url(cfg)
26
- super(url: url, **options)
31
+ super(url: url, config: cfg, **options)
27
32
 
28
33
  @symbols = Array(symbols)
29
34
  @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 = {
@@ -9,13 +9,14 @@ module DhanHQ
9
9
  # disconnected when required.
10
10
  class Registry
11
11
  @clients = []
12
+ @mutex = Mutex.new
12
13
  class << self
13
14
  # Registers a client instance with the registry.
14
15
  #
15
16
  # @param client [DhanHQ::WS::Client]
16
17
  # @return [void]
17
18
  def register(client)
18
- @clients << client unless @clients.include?(client)
19
+ @mutex.synchronize { @clients << client unless @clients.include?(client) }
19
20
  end
20
21
 
21
22
  # Removes a client from the registry.
@@ -23,18 +24,21 @@ module DhanHQ
23
24
  # @param client [DhanHQ::WS::Client]
24
25
  # @return [void]
25
26
  def unregister(client)
26
- @clients.delete(client)
27
+ @mutex.synchronize { @clients.delete(client) }
27
28
  end
28
29
 
29
30
  # Stops and removes all registered clients.
30
31
  #
31
32
  # @return [void]
32
33
  def stop_all
33
- @clients.dup.each do |c|
34
- c.stop
35
- rescue StandardError
34
+ @mutex.synchronize do
35
+ @clients.dup.each do |c|
36
+ c.stop
37
+ rescue StandardError => e
38
+ DhanHQ.logger&.debug("[DhanHQ::WS::Registry] stop failed: #{e.class} #{e.message}")
39
+ end
40
+ @clients.clear
36
41
  end
37
- @clients.clear
38
42
  end
39
43
  end
40
44
  end
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
@@ -40,6 +44,9 @@ module DhanHQ
40
44
  # "dhan_hq"` — it only worked via exe/dhanhq-mcp and lib/dhan_hq/mcp.rb, which
41
45
  # require_relative the file directly instead of going through the autoloader.
42
46
  "mcp" => "MCP",
47
+ # resources/scanx.rb defines DhanHQ::Resources::ScanX; without this Zeitwerk
48
+ # looks for DhanHQ::Resources::Scanx and fails on the first reference.
49
+ "scanx" => "ScanX",
43
50
  "ws" => "WS"
44
51
  )
45
52
  LOADER.push_dir(File.join(__dir__, "DhanHQ"), namespace: self)
@@ -80,17 +87,19 @@ module DhanHQ
80
87
  require_relative "DhanHQ/skills/builtin/market_data_summarizer"
81
88
  DhanHQ::Skills::Registry.load_builtins
82
89
 
83
- class Error < StandardError; end
84
-
85
90
  class << self
86
91
  # Default REST API host used when no custom base URL is provided.
87
92
  #
88
93
  # @return [String]
89
94
  BASE_URL = Constants::Urls::REST_API_BASE
90
95
  # The current configuration instance.
91
- #
92
- # @return [DhanHQ::Configuration, nil] The current configuration or `nil` if not set.
93
- attr_accessor :configuration
96
+ attr_reader :configuration
97
+
98
+ def configuration=(config)
99
+ @configuration = config
100
+ @default_client = nil
101
+ reset_model_resources!
102
+ end
94
103
 
95
104
  attr_writer :logger
96
105
 
@@ -106,8 +115,10 @@ module DhanHQ
106
115
  # @return [void]
107
116
  def configure
108
117
  self.configuration ||= Configuration.new
109
- yield(configuration)
118
+ yield(configuration) if block_given?
110
119
  self.logger ||= Logger.new($stdout, level: Logger::INFO)
120
+ @default_client = Client.new(config: configuration)
121
+ reset_model_resources!
111
122
  end
112
123
 
113
124
  # default logger so calls like DhanHQ.logger&.info never explode
@@ -118,6 +129,18 @@ module DhanHQ
118
129
  @logger ||= Logger.new($stdout, level: Logger::INFO)
119
130
  end
120
131
 
132
+ # Global default client (used for backward compatibility with class-level APIs)
133
+ #
134
+ # @return [DhanHQ::Client]
135
+ def default_client
136
+ @default_client ||= Client.new(config: ensure_configuration!)
137
+ end
138
+
139
+ # Resets the global default client
140
+ def reset_default_client!
141
+ @default_client = nil
142
+ end
143
+
121
144
  # Configures the DhanHQ client using environment variables.
122
145
  #
123
146
  # When credentials are injected via `DHAN_ACCESS_TOKEN` and `DHAN_CLIENT_ID` this helper
@@ -129,6 +152,8 @@ module DhanHQ
129
152
  # @return [void]
130
153
  def configure_with_env
131
154
  self.configuration = Configuration.new
155
+ @default_client = Client.new(config: configuration)
156
+ reset_model_resources!
132
157
  end
133
158
 
134
159
  # Ensures configuration exists, bootstrapping from ENV when nil.
@@ -140,9 +165,32 @@ module DhanHQ
140
165
  self.configuration ||= Configuration.new
141
166
  end
142
167
 
143
- # Resets the configuration to nil.
168
+ # Resets the configuration and default client to nil.
144
169
  def reset_configuration!
145
170
  self.configuration = nil
171
+ @default_client = nil
172
+ reset_model_resources!
173
+ end
174
+
175
+ # Clears memoized class-level resource and API instances across all models.
176
+ def reset_model_resources!
177
+ models = [
178
+ BaseModel,
179
+ Models::Order, Models::Holding, Models::Position, Models::HistoricalData,
180
+ Models::Trade, Models::OptionChain, Models::Funds, Models::AlertOrder,
181
+ Models::ForeverOrder, Models::IcebergOrder, Models::SuperOrder, Models::TwapOrder,
182
+ Models::LedgerEntry, Models::Instrument, Models::Edis, Models::Profile,
183
+ Models::Margin, Models::MultiOrder, Models::KillSwitch, Models::PnlExit,
184
+ Models::MarketFeed, Models::ExpiredOptionsData
185
+ ]
186
+
187
+ models.each do |klass|
188
+ next unless klass.is_a?(Class)
189
+
190
+ klass.instance_variables.each do |var|
191
+ klass.instance_variable_set(var, nil) if var.to_s.end_with?("resource", "api")
192
+ end
193
+ end
146
194
  end
147
195
 
148
196
  # Configures the DhanHQ client by fetching credentials from a token endpoint.
@@ -164,39 +212,14 @@ module DhanHQ
164
212
  def configure_from_token_endpoint(base_url: nil, bearer_token: nil)
165
213
  base_url ||= ENV.fetch("DHAN_TOKEN_ENDPOINT_BASE_URL", nil)
166
214
  bearer_token ||= ENV.fetch("DHAN_TOKEN_ENDPOINT_BEARER", nil)
167
-
168
215
  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
216
 
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]
217
+ data = fetch_token_endpoint("#{base_url.to_s.chomp("/")}/auth/dhan/token", bearer_token, "Token endpoint")
218
+ access_token = data["access_token"]
219
+ client_id = data["client_id"]
192
220
  raise DhanHQ::TokenEndpointError, "Token endpoint response missing access_token or client_id" if access_token.to_s.empty? || client_id.to_s.empty?
193
221
 
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
222
+ apply_token_config(access_token, client_id, data["base_url"])
200
223
  end
201
224
 
202
225
  # Configures the DhanHQ client by fetching credentials from a dashboard API.
@@ -208,8 +231,24 @@ module DhanHQ
208
231
  def configure_from_dashboard(bearer_token:, url: "http://localhost:3011/api/dhan_access_token")
209
232
  raise DhanHQ::TokenEndpointError, "bearer_token is required" if bearer_token.to_s.empty?
210
233
 
234
+ data = fetch_token_endpoint(url, bearer_token, "Dashboard")
235
+ access_token = data["access_token"] || data["accessToken"] || data["dhan_access_token"] || data["dhanaccesstoken"]
236
+ client_id = data["client_id"] || data["clientId"] || data["dhan_client_id"] || data["dhanClientId"]
237
+ client_id ||= configuration&.client_id || ENV.fetch("DHAN_CLIENT_ID", nil)
238
+
239
+ raise DhanHQ::TokenEndpointError, "Dashboard response missing access_token (tried access_token, dhan_access_token, dhanaccesstoken)" if access_token.to_s.empty?
240
+ 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?
241
+
242
+ apply_token_config(access_token, client_id, data["base_url"] || data["baseUrl"])
243
+ end
244
+
245
+ def fetch_token_endpoint(url, bearer_token, label)
211
246
  conn = ::Faraday.new(url: url) do |c|
212
247
  c.request :url_encoded
248
+ # Bounded timeouts, same defaults as Client#build_connection; without
249
+ # them a stalled token endpoint would hang the caller forever.
250
+ c.options.timeout = ENV.fetch("DHAN_READ_TIMEOUT", 30).to_i
251
+ c.options.open_timeout = ENV.fetch("DHAN_CONNECT_TIMEOUT", 10).to_i
213
252
  c.adapter ::Faraday.default_adapter
214
253
  end
215
254
 
@@ -221,24 +260,18 @@ module DhanHQ
221
260
  unless response.success?
222
261
  body = parse_json_body(response.body)
223
262
  msg = body["error"] || body["message"] || body["errorMessage"] || response.body.to_s
224
- raise DhanHQ::TokenEndpointError, "Dashboard returned #{response.status}: #{msg}"
263
+ raise DhanHQ::TokenEndpointError, "#{label} returned #{response.status}: #{msg}"
225
264
  end
226
265
 
227
266
  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?
267
+ data.is_a?(Hash) ? data.transform_keys(&:to_s) : {}
268
+ end
236
269
 
270
+ def apply_token_config(access_token, client_id, base_url)
237
271
  self.configuration ||= Configuration.new
238
272
  configuration.access_token = access_token.to_s
239
273
  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 != ""
274
+ configuration.base_url = base_url.to_s if base_url.to_s != ""
242
275
  configuration
243
276
  end
244
277
 
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.1.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,
@@ -241,6 +241,7 @@ files:
241
241
  - lib/DhanHQ/constants.rb
242
242
  - lib/DhanHQ/contracts/alert_order_contract.rb
243
243
  - lib/DhanHQ/contracts/base_contract.rb
244
+ - lib/DhanHQ/contracts/company_info_contract.rb
244
245
  - lib/DhanHQ/contracts/edis_contract.rb
245
246
  - lib/DhanHQ/contracts/expired_options_data_contract.rb
246
247
  - lib/DhanHQ/contracts/forever_order_contract.rb
@@ -254,15 +255,18 @@ files:
254
255
  - lib/DhanHQ/contracts/intraday_historical_data_contract.rb
255
256
  - lib/DhanHQ/contracts/margin_calculator_contract.rb
256
257
  - lib/DhanHQ/contracts/market_feed_contract.rb
258
+ - lib/DhanHQ/contracts/market_movers_contract.rb
257
259
  - lib/DhanHQ/contracts/modify_order_contract.rb
258
260
  - lib/DhanHQ/contracts/multi_order_contract.rb
259
261
  - lib/DhanHQ/contracts/multi_scrip_margin_calc_request_contract.rb
262
+ - lib/DhanHQ/contracts/news_headline_contract.rb
260
263
  - lib/DhanHQ/contracts/option_chain_contract.rb
261
264
  - lib/DhanHQ/contracts/order_contract.rb
262
265
  - lib/DhanHQ/contracts/place_order_contract.rb
263
266
  - lib/DhanHQ/contracts/pnl_based_exit_contract.rb
264
267
  - lib/DhanHQ/contracts/position_conversion_contract.rb
265
268
  - lib/DhanHQ/contracts/slice_order_contract.rb
269
+ - lib/DhanHQ/contracts/technical_data_contract.rb
266
270
  - lib/DhanHQ/contracts/trade_by_order_id_contract.rb
267
271
  - lib/DhanHQ/contracts/trade_contract.rb
268
272
  - lib/DhanHQ/contracts/trade_history_contract.rb
@@ -363,6 +367,7 @@ files:
363
367
  - lib/DhanHQ/resources/pnl_exit.rb
364
368
  - lib/DhanHQ/resources/positions.rb
365
369
  - lib/DhanHQ/resources/profile.rb
370
+ - lib/DhanHQ/resources/scanx.rb
366
371
  - lib/DhanHQ/resources/statements.rb
367
372
  - lib/DhanHQ/resources/super_orders.rb
368
373
  - lib/DhanHQ/resources/trader_control.rb