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
@@ -370,9 +370,17 @@ module DhanHQ
370
370
  #
371
371
  # @raise [RuntimeError] If order ID is missing
372
372
  # @raise [DhanHQ::ValidationError] If validation fails for any parameter
373
+ # @raise [DhanHQ::ModificationLimitError] If this instance has already been modified 25 times (Dhan API cap)
374
+ # @note Count is per Order instance in this process; a fresh find() resets it.
373
375
  def modify(new_params)
374
376
  raise "Order ID is required to modify an order" unless id
375
377
 
378
+ count = @modification_count || 0
379
+ if count >= Constants::RateLimit::ORDER_MODIFICATIONS_PER_ORDER
380
+ raise ModificationLimitError,
381
+ "Order modification limit reached (#{Constants::RateLimit::ORDER_MODIFICATIONS_PER_ORDER} per order)"
382
+ end
383
+
376
384
  warn_invalid_state if order_status_invalid_for_modification?
377
385
 
378
386
  filtered_payload = prepare_modify_payload(new_params)
@@ -384,6 +392,7 @@ module DhanHQ
384
392
 
385
393
  return DhanHQ::ErrorObject.new(response) unless success_response?(response)
386
394
 
395
+ @modification_count = count + 1
387
396
  @attributes.merge!(normalize_keys(response))
388
397
  assign_attributes
389
398
  self
@@ -543,13 +552,15 @@ module DhanHQ
543
552
  private
544
553
 
545
554
  def save_new_order
546
- DhanHQ.logger&.info("[DhanHQ::Models::Order] Placing order: #{attributes.slice(:transaction_type, :exchange_segment, :security_id, :quantity, :price).inspect}")
555
+ slice_attrs = attributes.slice(:transaction_type, :exchange_segment, :security_id, :quantity, :price)
556
+ DhanHQ.logger&.info("[DhanHQ::Models::Order] Placing order: #{slice_attrs.inspect}")
547
557
  response = self.class.resource.create(to_request_params)
548
558
  handle_api_response(response, success_key: "orderId", context: "[DhanHQ::Models::Order] Order placement")
549
559
  end
550
560
 
551
561
  def modify_existing_order
552
- DhanHQ.logger&.info("[DhanHQ::Models::Order] Modifying order #{id}: #{attributes.slice(:price, :quantity, :order_type).inspect}")
562
+ slice_attrs = attributes.slice(:price, :quantity, :order_type)
563
+ DhanHQ.logger&.info("[DhanHQ::Models::Order] Modifying order #{id}: #{slice_attrs.inspect}")
553
564
  response = self.class.resource.update(id, to_request_params)
554
565
  handle_api_response(response, success_key: "orderStatus", context: "[DhanHQ::Models::Order] Order modification")
555
566
  end
@@ -559,7 +570,9 @@ module DhanHQ
559
570
  end
560
571
 
561
572
  def warn_invalid_state
562
- DhanHQ.logger&.warn("[DhanHQ::Models::Order] Attempting to modify order #{id} in #{order_status} state - API will reject")
573
+ DhanHQ.logger&.warn(
574
+ "[DhanHQ::Models::Order] Attempting to modify order #{id} in #{order_status} state - API will reject"
575
+ )
563
576
  end
564
577
 
565
578
  def prepare_modify_payload(new_params)
@@ -576,7 +589,9 @@ module DhanHQ
576
589
 
577
590
  # Don't send trigger_price when it's 0 for non–stop-loss orders (API default; avoids validation noise).
578
591
  order_type = filtered_payload[:order_type].to_s
579
- filtered_payload.delete(:trigger_price) if !%w[STOP_LOSS STOP_LOSS_MARKET].include?(order_type) && filtered_payload[:trigger_price].to_f.zero?
592
+ trigger_zero = filtered_payload[:trigger_price].to_f.zero?
593
+ drop_trigger = !%w[STOP_LOSS STOP_LOSS_MARKET].include?(order_type) && trigger_zero
594
+ filtered_payload.delete(:trigger_price) if drop_trigger
580
595
 
581
596
  filtered_payload.compact
582
597
  end
@@ -207,9 +207,9 @@ module DhanHQ
207
207
  # )
208
208
  #
209
209
  def create(params)
210
- # Normalize params and auto-inject dhan_client_id from configuration if not provided
211
210
  normalized_params = snake_case(params)
212
- normalized_params[:dhan_client_id] ||= DhanHQ.configuration.client_id if DhanHQ.configuration.client_id
211
+ config = DhanHQ.configuration
212
+ normalized_params[:dhan_client_id] ||= config.client_id if config&.client_id
213
213
  response = resource.create(normalized_params)
214
214
  return nil unless response.is_a?(Hash) && response["orderId"]
215
215
 
@@ -5,7 +5,7 @@ module DhanHQ
5
5
  # Resource for alert/conditional orders per API docs: /alerts/orders (GET/POST/PUT/DELETE).
6
6
  class AlertOrders < BaseResource
7
7
  API_TYPE = :order_api
8
- HTTP_PATH = "/alerts/orders"
8
+ HTTP_PATH = "/v2/alerts/orders"
9
9
  end
10
10
  end
11
11
  end
@@ -3,11 +3,12 @@
3
3
  module DhanHQ
4
4
  module Resources
5
5
  # Resource for EDIS per https://dhanhq.co/docs/v2/edis/
6
- # GET /edis/tpin, POST /edis/form (body: isin, qty, exchange, segment, bulk),
7
- # POST /edis/bulkform, GET /edis/inquire/{isin}.
6
+ # GET /edis/tpin (generate T-PIN, 202 Accepted), POST /edis/form (body: isin, qty, exchange, segment, bulk),
7
+ # POST /edis/bulkform, GET /edis/inquire/{isin} (or "ALL").
8
+ # Form response: dhanClientId, edisFormHtml. Inquire response: clientId, isin, totalQty, aprvdQty, status, remarks.
8
9
  class Edis < BaseAPI
9
10
  API_TYPE = :order_api
10
- HTTP_PATH = "/edis"
11
+ HTTP_PATH = "/v2/edis"
11
12
 
12
13
  def form(params)
13
14
  post("/form", params: params)
@@ -19,9 +19,10 @@ module DhanHQ
19
19
  resp = client.connection.get(path)
20
20
  if resp.status.between?(300, 399) && resp.headers["location"]
21
21
  redirect_url = resp.headers["location"]
22
- return Faraday.get(redirect_url).body
22
+ Faraday.get(redirect_url).body
23
+ else
24
+ resp.body
23
25
  end
24
- resp.body
25
26
  end
26
27
  end
27
28
  end
@@ -4,9 +4,12 @@ module DhanHQ
4
4
  module Resources
5
5
  # Resource for IP whitelist per API docs: GET /v2/ip/getIP, POST /v2/ip/setIP, PUT /v2/ip/modifyIP.
6
6
  # Set/Modify require dhanClientId, ip, ipFlag (PRIMARY | SECONDARY). See dhanhq.co/docs/v2/authentication/#setup-static-ip
7
+ #
8
+ # GET /v2/ip/getIP response: modifyDateSecondary, secondaryIP, modifyDatePrimary, primaryIP
9
+ # (dates are YYYY-MM-DD from which the IP can be modified; IPs are IPv4 or IPv6).
7
10
  class IPSetup < BaseAPI
8
11
  API_TYPE = :order_api
9
- HTTP_PATH = "/ip"
12
+ HTTP_PATH = "/v2/ip"
10
13
 
11
14
  def current
12
15
  get("/getIP")
@@ -10,12 +10,18 @@ module DhanHQ
10
10
  API_TYPE = :order_api
11
11
  HTTP_PATH = "/v2/killswitch"
12
12
 
13
+ KILL_SWITCH_STATUSES = %w[ACTIVATE DEACTIVATE].freeze
14
+
13
15
  # Enables or disables the kill switch via query parameter (doc: no body).
14
16
  #
15
17
  # @param status [String] "ACTIVATE" or "DEACTIVATE"
16
18
  # @return [Hash]
19
+ # @raise [DhanHQ::ValidationError] if status is not ACTIVATE or DEACTIVATE
17
20
  def update(status)
18
- query = "?killSwitchStatus=#{CGI.escape(status.to_s)}"
21
+ normalized = status.to_s.upcase.strip
22
+ raise DhanHQ::ValidationError, "killSwitchStatus must be one of: #{KILL_SWITCH_STATUSES.join(", ")}" unless KILL_SWITCH_STATUSES.include?(normalized)
23
+
24
+ query = "?killSwitchStatus=#{CGI.escape(normalized)}"
19
25
  handle_response(client.post(build_path(query), {}))
20
26
  end
21
27
 
@@ -63,7 +63,7 @@ module DhanHQ
63
63
  end
64
64
 
65
65
  def raise_validation_error!(result)
66
- raise DhanHQ::Error, "Validation Error: #{result.errors.to_h}"
66
+ raise DhanHQ::ValidationError, "Invalid parameters: #{result.errors.to_h}"
67
67
  end
68
68
  end
69
69
  end
@@ -33,13 +33,19 @@ module DhanHQ
33
33
  put("/#{order_id}", params: params)
34
34
  end
35
35
 
36
+ SUPER_ORDER_LEGS = %w[ENTRY_LEG STOP_LOSS_LEG TARGET_LEG].freeze
37
+
36
38
  # Cancels a specific leg from a super order.
37
39
  #
38
40
  # @param order_id [String]
39
- # @param leg_name [String]
41
+ # @param leg_name [String] One of ENTRY_LEG, STOP_LOSS_LEG, TARGET_LEG (per API path enum)
40
42
  # @return [Hash]
43
+ # @raise [DhanHQ::ValidationError] if leg_name is not a valid leg
41
44
  def cancel(order_id, leg_name)
42
- delete("/#{order_id}/#{leg_name}")
45
+ normalized = leg_name.to_s.upcase.strip
46
+ raise DhanHQ::ValidationError, "leg_name must be one of: #{SUPER_ORDER_LEGS.join(", ")}" unless SUPER_ORDER_LEGS.include?(normalized)
47
+
48
+ delete("/#{order_id}/#{normalized}")
43
49
  end
44
50
  end
45
51
  end
@@ -2,21 +2,30 @@
2
2
 
3
3
  module DhanHQ
4
4
  module Resources
5
- # Resource for trader control (kill switch): status (GET), enable/disable (POST /trader-control).
5
+ # The path /trader-control is not part of the Dhan v2 API (https://dhanhq.co/docs/v2).
6
+ # Trader's Control in the docs is implemented via:
7
+ # - Kill Switch: GET/POST /v2/killswitch → use DhanHQ::Models::KillSwitch or DhanHQ::Resources::KillSwitch
8
+ # - P&L Exit: GET/POST/DELETE /v2/pnlExit → use DhanHQ::Models::PnlExit
9
+ #
10
+ # This class is kept for backward compatibility but raises when used.
6
11
  class TraderControl < BaseAPI
7
12
  API_TYPE = :order_api
8
13
  HTTP_PATH = "/trader-control"
9
14
 
15
+ MSG = "The /trader-control endpoint is not part of the Dhan v2 API. " \
16
+ "Use DhanHQ::Models::KillSwitch or DhanHQ::Resources::KillSwitch for kill switch " \
17
+ "(GET/POST /v2/killswitch). See https://dhanhq.co/docs/v2"
18
+
10
19
  def status
11
- get("")
20
+ raise DhanHQ::Error, MSG
12
21
  end
13
22
 
14
23
  def enable
15
- post("", params: { action: "ENABLE" })
24
+ raise DhanHQ::Error, MSG
16
25
  end
17
26
 
18
27
  def disable
19
- post("", params: { action: "DISABLE" })
28
+ raise DhanHQ::Error, MSG
20
29
  end
21
30
  end
22
31
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module DhanHQ
4
4
  # Semantic version of the DhanHQ client gem.
5
- VERSION = "2.6.1"
5
+ VERSION = "2.6.3"
6
6
  end
@@ -173,7 +173,7 @@ module DhanHQ
173
173
  def default_headers
174
174
  {
175
175
  "User-Agent" => "DhanHQ-Ruby-Client/#{DhanHQ::VERSION}",
176
- "Origin" => "https://dhanhq.co"
176
+ "Origin" => Constants::Urls::ORIGIN
177
177
  }
178
178
  end
179
179
 
@@ -32,7 +32,8 @@ module DhanHQ
32
32
 
33
33
  cid = DhanHQ.configuration.client_id or raise "DhanHQ.client_id not set"
34
34
  ver = (DhanHQ.configuration.respond_to?(:ws_version) && DhanHQ.configuration.ws_version) || 2
35
- @url = url || "wss://api-feed.dhan.co?version=#{ver}&token=#{token}&clientId=#{cid}&authType=2"
35
+ base = url || DhanHQ.configuration.ws_market_feed_url
36
+ @url = base.include?("?") ? base : "#{base}?version=#{ver}&token=#{token}&clientId=#{cid}&authType=2"
36
37
  end
37
38
 
38
39
  # Starts the WebSocket connection and event loop.
@@ -86,11 +86,12 @@ module DhanHQ
86
86
  cid = config.client_id or raise "DhanHQ.client_id not set"
87
87
  depth_level = config.market_depth_level || 20 # Default to 20 level depth
88
88
 
89
- if depth_level == 200
90
- "wss://full-depth-api.dhan.co/twohundreddepth?token=#{token}&clientId=#{cid}&authType=2"
91
- else
92
- "wss://depth-api-feed.dhan.co/twentydepth?token=#{token}&clientId=#{cid}&authType=2"
93
- end
89
+ base = if depth_level == 200
90
+ Constants::Urls::WS_DEPTH_200
91
+ else
92
+ config.ws_market_depth_url
93
+ end
94
+ base.include?("?") ? base : "#{base}?token=#{token}&clientId=#{cid}&authType=2"
94
95
  end
95
96
 
96
97
  ##
@@ -163,7 +164,10 @@ module DhanHQ
163
164
 
164
165
  send_message(subscription_message)
165
166
  @subscriptions[label] = resolution
166
- DhanHQ.logger&.info("[DhanHQ::WS::MarketDepth] Subscribed to #{resolution[:original_label]} (#{resolution[:exchange_segment]}:#{resolution[:security_id]})")
167
+ DhanHQ.logger&.info(
168
+ "[DhanHQ::WS::MarketDepth] Subscribed to #{resolution[:original_label]} " \
169
+ "(#{resolution[:exchange_segment]}:#{resolution[:security_id]})"
170
+ )
167
171
  rescue StandardError => e
168
172
  DhanHQ.logger&.error("[DhanHQ::WS::MarketDepth] Subscription error for #{symbol.inspect}: #{e.class} #{e.message}")
169
173
  end
@@ -191,7 +195,10 @@ module DhanHQ
191
195
 
192
196
  send_message(unsubscribe_message)
193
197
  @subscriptions.delete(label)
194
- DhanHQ.logger&.info("[DhanHQ::WS::MarketDepth] Unsubscribed from #{security_data[:original_label]} (#{security_data[:exchange_segment]}:#{security_data[:security_id]})")
198
+ DhanHQ.logger&.info(
199
+ "[DhanHQ::WS::MarketDepth] Unsubscribed from #{security_data[:original_label]} " \
200
+ "(#{security_data[:exchange_segment]}:#{security_data[:security_id]})"
201
+ )
195
202
  rescue StandardError => e
196
203
  DhanHQ.logger&.error("[DhanHQ::WS::MarketDepth] Unsubscribe error for #{symbol.inspect}: #{e.class} #{e.message}")
197
204
  end
@@ -216,7 +223,8 @@ module DhanHQ
216
223
  instrument = find_instrument(symbol_code, segment_hint)
217
224
  unless instrument
218
225
  DhanHQ.logger&.warn(
219
- "[DhanHQ::WS::MarketDepth] Unable to locate instrument for #{symbol_code} (segment hint: #{segment_hint || "AUTO"})"
226
+ "[DhanHQ::WS::MarketDepth] Unable to locate instrument for #{symbol_code} " \
227
+ "(segment hint: #{segment_hint || "AUTO"})"
220
228
  )
221
229
  return nil
222
230
  end
data/lib/dhan_hq.rb CHANGED
@@ -4,6 +4,7 @@ require "json"
4
4
  require "logger"
5
5
  require "zeitwerk"
6
6
  require "dotenv/load"
7
+ require "faraday"
7
8
  # Minimal eager requires for backward-compatible constants.
8
9
  # These are widely referenced (e.g. `DhanHQ::BaseAPI`) and should not depend on
9
10
  # the autoloader being fully configured.
@@ -11,6 +12,8 @@ require_relative "DhanHQ/helpers/api_helper"
11
12
  require_relative "DhanHQ/helpers/attribute_helper"
12
13
  require_relative "DhanHQ/helpers/validation_helper"
13
14
  require_relative "DhanHQ/helpers/request_helper"
15
+ require_relative "DhanHQ/errors"
16
+ require_relative "DhanHQ/version"
14
17
  require_relative "DhanHQ/helpers/response_helper"
15
18
  require_relative "DhanHQ/core/base_api"
16
19
  require_relative "DhanHQ/core/base_model"
@@ -47,7 +50,7 @@ module DhanHQ
47
50
  # Default REST API host used when no custom base URL is provided.
48
51
  #
49
52
  # @return [String]
50
- BASE_URL = "https://api.dhan.co/v2"
53
+ BASE_URL = Constants::Urls::REST_API_BASE
51
54
  # The current configuration instance.
52
55
  #
53
56
  # @return [DhanHQ::Configuration, nil] The current configuration or `nil` if not set.
@@ -89,15 +92,21 @@ module DhanHQ
89
92
  #
90
93
  # @return [void]
91
94
  def configure_with_env
95
+ self.configuration = Configuration.new
96
+ end
97
+
98
+ # Ensures configuration exists, bootstrapping from ENV when nil.
99
+ # Called automatically when building a Client so env-only integrations work without
100
+ # an explicit configure/configure_with_env call. Idempotent when configuration is already set.
101
+ #
102
+ # @return [DhanHQ::Configuration]
103
+ def ensure_configuration!
92
104
  self.configuration ||= Configuration.new
93
- configuration.access_token = ENV.fetch("DHAN_ACCESS_TOKEN", nil)
94
- configuration.client_id = ENV.fetch("DHAN_CLIENT_ID", nil)
95
- configuration.base_url = ENV.fetch("DHAN_BASE_URL", BASE_URL)
96
- configuration.ws_version = ENV.fetch("DHAN_WS_VERSION", configuration.ws_version || 2).to_i
97
- configuration.ws_order_url = ENV.fetch("DHAN_WS_ORDER_URL", configuration.ws_order_url)
98
- configuration.ws_user_type = ENV.fetch("DHAN_WS_USER_TYPE", configuration.ws_user_type)
99
- configuration.partner_id = ENV.fetch("DHAN_PARTNER_ID", configuration.partner_id)
100
- configuration.partner_secret = ENV.fetch("DHAN_PARTNER_SECRET", configuration.partner_secret)
105
+ end
106
+
107
+ # Resets the configuration to nil.
108
+ def reset_configuration!
109
+ self.configuration = nil
101
110
  end
102
111
 
103
112
  # Configures the DhanHQ client by fetching credentials from a token endpoint.
@@ -120,12 +129,12 @@ module DhanHQ
120
129
  base_url ||= ENV.fetch("DHAN_TOKEN_ENDPOINT_BASE_URL", nil)
121
130
  bearer_token ||= ENV.fetch("DHAN_TOKEN_ENDPOINT_BEARER", nil)
122
131
 
123
- raise TokenEndpointError, "base_url and bearer_token (or ENV DHAN_TOKEN_ENDPOINT_*) are required" if base_url.to_s.empty? || bearer_token.to_s.empty?
132
+ 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?
124
133
 
125
134
  url = "#{base_url.to_s.chomp("/")}/auth/dhan/token"
126
- conn = Faraday.new(url: url) do |c|
135
+ conn = ::Faraday.new(url: url) do |c|
127
136
  c.response :json, content_type: /\bjson$/
128
- c.adapter Faraday.default_adapter
137
+ c.adapter ::Faraday.default_adapter
129
138
  end
130
139
 
131
140
  response = conn.get("") do |req|
@@ -134,33 +143,17 @@ module DhanHQ
134
143
  end
135
144
 
136
145
  unless response.success?
137
- body = if response.body.is_a?(Hash)
138
- response.body
139
- else
140
- begin
141
- JSON.parse(response.body.to_s)
142
- rescue StandardError
143
- {}
144
- end
145
- end
146
+ body = parse_json_body(response.body)
146
147
  msg = body["error"] || body["message"] || body["errorMessage"] || response.body.to_s
147
- raise TokenEndpointError, "Token endpoint returned #{response.status}: #{msg}"
148
+ raise DhanHQ::TokenEndpointError, "Token endpoint returned #{response.status}: #{msg}"
148
149
  end
149
150
 
150
- data = if response.body.is_a?(Hash)
151
- response.body
152
- else
153
- begin
154
- JSON.parse(response.body.to_s)
155
- rescue StandardError
156
- {}
157
- end
158
- end
151
+ data = parse_json_body(response.body)
159
152
  data = data.transform_keys(&:to_s) if data.is_a?(Hash)
160
153
 
161
154
  access_token = data["access_token"] || data[:access_token]
162
155
  client_id = data["client_id"] || data[:client_id]
163
- raise TokenEndpointError, "Token endpoint response missing access_token or client_id" if access_token.to_s.empty? || client_id.to_s.empty?
156
+ raise DhanHQ::TokenEndpointError, "Token endpoint response missing access_token or client_id" if access_token.to_s.empty? || client_id.to_s.empty?
164
157
 
165
158
  self.configuration ||= Configuration.new
166
159
  configuration.access_token = access_token.to_s
@@ -169,5 +162,17 @@ module DhanHQ
169
162
  configuration.base_url = dhan_base.to_s if dhan_base.to_s != ""
170
163
  configuration
171
164
  end
165
+
166
+ # @param body [String, Hash] Raw response body
167
+ # @return [Hash] Parsed hash; empty hash on parse failure or empty string
168
+ def parse_json_body(body)
169
+ return {} if body.nil?
170
+ return body if body.is_a?(Hash)
171
+ return {} if body.to_s.strip.empty?
172
+
173
+ JSON.parse(body.to_s)
174
+ rescue StandardError
175
+ {}
176
+ end
172
177
  end
173
178
  end
data/lib/ta/indicators.rb CHANGED
@@ -16,13 +16,12 @@ module TA
16
16
 
17
17
  def rsi(series, period)
18
18
  if defined?(RubyTechnicalAnalysis) && RubyTechnicalAnalysis.const_defined?(:RSI)
19
- return RubyTechnicalAnalysis::RSI.new(series: series, period: period).call
19
+ RubyTechnicalAnalysis::RSI.new(series: series, period: period).call
20
+ elsif defined?(TechnicalAnalysis) && TechnicalAnalysis.respond_to?(:rsi)
21
+ TechnicalAnalysis.rsi(series, period: period)
22
+ else
23
+ simple_rsi(series, period)
20
24
  end
21
- if defined?(TechnicalAnalysis) && TechnicalAnalysis.respond_to?(:rsi)
22
- return TechnicalAnalysis.rsi(series, period: period)
23
- end
24
-
25
- simple_rsi(series, period)
26
25
  end
27
26
 
28
27
  def macd(series, fast, slow, signal)
@@ -56,24 +55,22 @@ module TA
56
55
 
57
56
  def adx(high, low, close, period)
58
57
  if defined?(RubyTechnicalAnalysis) && RubyTechnicalAnalysis.const_defined?(:ADX)
59
- return RubyTechnicalAnalysis::ADX.new(high: high, low: low, close: close, period: period).call
60
- end
61
- if defined?(TechnicalAnalysis) && TechnicalAnalysis.respond_to?(:adx)
62
- return TechnicalAnalysis.adx(high: high, low: low, close: close, period: period)
58
+ RubyTechnicalAnalysis::ADX.new(high: high, low: low, close: close, period: period).call
59
+ elsif defined?(TechnicalAnalysis) && TechnicalAnalysis.respond_to?(:adx)
60
+ TechnicalAnalysis.adx(high: high, low: low, close: close, period: period)
61
+ else
62
+ simple_adx(high, low, close, period)
63
63
  end
64
-
65
- simple_adx(high, low, close, period)
66
64
  end
67
65
 
68
66
  def atr(high, low, close, period)
69
67
  if defined?(RubyTechnicalAnalysis) && RubyTechnicalAnalysis.const_defined?(:ATR)
70
- return RubyTechnicalAnalysis::ATR.new(high: high, low: low, close: close, period: period).call
71
- end
72
- if defined?(TechnicalAnalysis) && TechnicalAnalysis.respond_to?(:atr)
73
- return TechnicalAnalysis.atr(high: high, low: low, close: close, period: period)
68
+ RubyTechnicalAnalysis::ATR.new(high: high, low: low, close: close, period: period).call
69
+ elsif defined?(TechnicalAnalysis) && TechnicalAnalysis.respond_to?(:atr)
70
+ TechnicalAnalysis.atr(high: high, low: low, close: close, period: period)
71
+ else
72
+ simple_atr(high, low, close, period)
74
73
  end
75
-
76
- simple_atr(high, low, close, period)
77
74
  end
78
75
 
79
76
  def simple_rsi(series, period)
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: 2.6.1
4
+ version: 2.6.3
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-03-07 00:00:00.000000000 Z
11
+ date: 2026-03-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -191,29 +191,24 @@ files:
191
191
  - ".rspec"
192
192
  - ".rubocop.yml"
193
193
  - ".rubocop_todo.yml"
194
+ - ARCHITECTURE.md
194
195
  - CHANGELOG.md
195
196
  - CODE_OF_CONDUCT.md
196
- - CODE_REVIEW_ISSUES.md
197
- - FIXES_APPLIED.md
198
197
  - GUIDE.md
199
198
  - LICENSE.txt
200
199
  - README.md
201
- - RELEASING.md
202
- - REVIEW_SUMMARY.md
203
200
  - Rakefile
204
201
  - TAGS
205
- - VERSION_UPDATE.md
206
202
  - config/initializers/order_update_hub.rb
207
203
  - core
208
204
  - diagram.html
209
- - diagram.md
210
205
  - docs/API_DOCS_GAPS.md
211
206
  - docs/API_VERIFICATION.md
212
- - docs/ARCHIVE_README.md
213
207
  - docs/AUTHENTICATION.md
214
208
  - docs/CONFIGURATION.md
215
209
  - docs/CONSTANTS_REFERENCE.md
216
210
  - docs/DATA_API_PARAMETERS.md
211
+ - docs/ENDPOINTS_AND_SANDBOX.md
217
212
  - docs/LIVE_ORDER_UPDATES.md
218
213
  - docs/RAILS_INTEGRATION.md
219
214
  - docs/RAILS_WEBSOCKET_INTEGRATION.md
@@ -237,9 +232,12 @@ files:
237
232
  - lib/DhanHQ/contracts/base_contract.rb
238
233
  - lib/DhanHQ/contracts/edis_contract.rb
239
234
  - lib/DhanHQ/contracts/expired_options_data_contract.rb
235
+ - lib/DhanHQ/contracts/forever_order_contract.rb
240
236
  - lib/DhanHQ/contracts/historical_data_contract.rb
241
237
  - lib/DhanHQ/contracts/instrument_list_contract.rb
238
+ - lib/DhanHQ/contracts/intraday_historical_data_contract.rb
242
239
  - lib/DhanHQ/contracts/margin_calculator_contract.rb
240
+ - lib/DhanHQ/contracts/market_feed_contract.rb
243
241
  - lib/DhanHQ/contracts/modify_order_contract.rb
244
242
  - lib/DhanHQ/contracts/multi_scrip_margin_calc_request_contract.rb
245
243
  - lib/DhanHQ/contracts/option_chain_contract.rb