DhanHQ 2.3.0 → 2.5.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -1
  3. data/CODE_REVIEW_ISSUES.md +2 -2
  4. data/GUIDE.md +2 -2
  5. data/README.md +194 -741
  6. data/REVIEW_SUMMARY.md +2 -2
  7. data/{README1.md → docs/ARCHIVE_README.md} +4 -4
  8. data/docs/AUTHENTICATION.md +116 -2
  9. data/docs/CONFIGURATION.md +109 -0
  10. data/docs/SUPER_ORDERS.md +284 -0
  11. data/docs/TESTING_GUIDE.md +8 -8
  12. data/docs/TROUBLESHOOTING.md +117 -0
  13. data/docs/WEBSOCKET_PROTOCOL.md +154 -0
  14. data/docs/live_order_updates.md +2 -2
  15. data/docs/rails_integration.md +7 -7
  16. data/docs/standalone_ruby_websocket_integration.md +24 -24
  17. data/docs/technical_analysis.md +1 -1
  18. data/docs/websocket_integration.md +4 -4
  19. data/examples/comprehensive_websocket_examples.rb +2 -2
  20. data/examples/instrument_finder_test.rb +2 -2
  21. data/examples/market_depth_example.rb +2 -2
  22. data/examples/market_feed_example.rb +2 -2
  23. data/examples/order_update_example.rb +2 -2
  24. data/examples/trading_fields_example.rb +2 -2
  25. data/lib/DhanHQ/auth/token_generator.rb +33 -0
  26. data/lib/DhanHQ/auth/token_manager.rb +88 -0
  27. data/lib/DhanHQ/auth/token_renewal.rb +25 -0
  28. data/lib/DhanHQ/auth.rb +91 -31
  29. data/lib/DhanHQ/client.rb +42 -2
  30. data/lib/DhanHQ/configuration.rb +2 -2
  31. data/lib/DhanHQ/contracts/order_contract.rb +0 -23
  32. data/lib/DhanHQ/contracts/trade_by_order_id_contract.rb +12 -0
  33. data/lib/DhanHQ/contracts/trade_contract.rb +0 -65
  34. data/lib/DhanHQ/contracts/trade_history_contract.rb +52 -0
  35. data/lib/DhanHQ/core/auth_api.rb +21 -0
  36. data/lib/DhanHQ/helpers/request_helper.rb +1 -1
  37. data/lib/DhanHQ/models/alert_order.rb +22 -0
  38. data/lib/DhanHQ/models/edis.rb +110 -0
  39. data/lib/DhanHQ/models/kill_switch.rb +22 -0
  40. data/lib/DhanHQ/models/margin.rb +49 -0
  41. data/lib/DhanHQ/models/pnl_exit.rb +130 -0
  42. data/lib/DhanHQ/models/position.rb +22 -0
  43. data/lib/DhanHQ/models/postback.rb +123 -0
  44. data/lib/DhanHQ/models/token_response.rb +88 -0
  45. data/lib/DhanHQ/resources/kill_switch.rb +8 -0
  46. data/lib/DhanHQ/resources/margin_calculator.rb +9 -0
  47. data/lib/DhanHQ/resources/pnl_exit.rb +37 -0
  48. data/lib/DhanHQ/resources/positions.rb +8 -0
  49. data/lib/DhanHQ/version.rb +1 -1
  50. data/lib/dhan_hq.rb +31 -81
  51. metadata +46 -4
  52. data/lib/DhanHQ/config.rb +0 -33
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Auth
5
+ # Backward-compatible wrapper for token renewal.
6
+ # Delegates to module-level Auth.renew_token.
7
+ class TokenRenewal
8
+ def renew
9
+ config = DhanHQ.configuration
10
+ raise Errors::AuthenticationError, "Missing configuration" unless config
11
+
12
+ access_token = config.resolved_access_token
13
+ dhan_client_id = config.client_id
14
+ raise Errors::AuthenticationError, "Missing dhanClientId (client_id)" if dhan_client_id.to_s.strip.empty?
15
+
16
+ response = Auth.renew_token(
17
+ access_token: access_token,
18
+ client_id: dhan_client_id
19
+ )
20
+
21
+ Models::TokenResponse.new(response)
22
+ end
23
+ end
24
+ end
25
+ end
data/lib/DhanHQ/auth.rb CHANGED
@@ -2,52 +2,112 @@
2
2
 
3
3
  require "faraday"
4
4
  require "json"
5
+ require "rotp"
6
+ require_relative "errors"
5
7
 
6
8
  module DhanHQ
7
- # Helpers for Dhan authentication APIs.
8
- # The gem does not implement API key/secret consent flows or Partner consent;
9
- # use Dhan Web or your own OAuth flow to obtain tokens, then pass them via
10
- # configuration. This module supports refreshing web-generated tokens only.
9
+ # Module-level helpers for Dhan authentication APIs.
10
+ #
11
+ # Supports:
12
+ # - Generating access tokens via TOTP (for automated systems)
13
+ # - Renewing web-generated tokens (web-only tokens)
14
+ #
15
+ # @example Generate token with TOTP
16
+ # totp = DhanHQ::Auth.generate_totp(ENV["DHAN_TOTP_SECRET"])
17
+ # response = DhanHQ::Auth.generate_access_token(
18
+ # dhan_client_id: ENV["DHAN_CLIENT_ID"],
19
+ # pin: ENV["DHAN_PIN"],
20
+ # totp: totp
21
+ # )
22
+ # token = response["accessToken"]
23
+ #
24
+ # @example Renew web token
25
+ # response = DhanHQ::Auth.renew_token(
26
+ # access_token: current_token,
27
+ # client_id: ENV["DHAN_CLIENT_ID"]
28
+ # )
11
29
  module Auth
12
- # Refreshes a web-generated access token (24h validity).
13
- # Calls POST /v2/RenewToken; expires the current token and returns a new one.
14
- # Only valid for tokens generated from Dhan Web (not API key flow).
30
+ AUTH_BASE_URL = "https://auth.dhan.co"
31
+ API_BASE_URL = "https://api.dhan.co/v2"
32
+
33
+ # Generates an access token using TOTP authentication.
15
34
  #
16
- # @param access_token [String] Current JWT from Dhan Web
17
- # @param client_id [String] Dhan client ID (dhanClientId)
18
- # @param base_url [String, nil] API base URL (default: DhanHQ.configuration.base_url)
19
- # @return [HashWithIndifferentAccess] Response with :access_token and :expiry_time (if present)
20
- # @raise [DhanHQ::Error] On HTTP or API error
21
- def self.renew_token(access_token, client_id, base_url: nil)
22
- base_url ||= DhanHQ.configuration&.base_url || Configuration::BASE_URL
23
- url = "#{base_url.chomp("/")}/RenewToken"
24
-
25
- conn = Faraday.new(url: url) do |c|
26
- c.request :json
27
- c.response :json, content_type: /\bjson$/
28
- c.adapter Faraday.default_adapter
35
+ # POST https://auth.dhan.co/app/generateAccessToken
36
+ #
37
+ # @param dhan_client_id [String] Your Dhan client ID
38
+ # @param pin [String] Your 6-digit Dhan PIN
39
+ # @param totp [String] 6-digit TOTP code (or use generate_totp helper)
40
+ # @return [Hash] Response hash with accessToken, expiryTime, etc.
41
+ # @raise [DhanHQ::InvalidAuthenticationError] On authentication failure
42
+ def self.generate_access_token(dhan_client_id:, pin:, totp:)
43
+ conn = build_connection(AUTH_BASE_URL)
44
+
45
+ response = conn.post("/app/generateAccessToken") do |req|
46
+ req.headers["Accept"] = "application/json"
47
+
48
+ req.params = {
49
+ dhanClientId: dhan_client_id,
50
+ pin: pin,
51
+ totp: totp
52
+ }
29
53
  end
30
54
 
31
- response = conn.get("") do |req|
55
+ handle_response(response, context: "GenerateAccessToken")
56
+ end
57
+
58
+ # Renews a web-generated access token (24h validity).
59
+ #
60
+ # POST https://api.dhan.co/v2/RenewToken
61
+ #
62
+ # ⚠️ Works ONLY for tokens generated from Dhan Web dashboard.
63
+ # For TOTP-generated tokens, regenerate instead of renewing.
64
+ #
65
+ # @param access_token [String] Current JWT access token
66
+ # @param client_id [String] Dhan client ID (dhanClientId)
67
+ # @return [Hash] Response hash with new accessToken and expiryTime
68
+ # @raise [DhanHQ::InvalidAuthenticationError] On renewal failure
69
+ def self.renew_token(access_token:, client_id:)
70
+ conn = build_connection(API_BASE_URL)
71
+
72
+ response = conn.post("/RenewToken") do |req|
32
73
  req.headers["access-token"] = access_token
33
74
  req.headers["dhanClientId"] = client_id
34
75
  req.headers["Accept"] = "application/json"
35
76
  end
36
77
 
78
+ handle_response(response, context: "RenewToken")
79
+ end
80
+
81
+ # Generates a 6-digit TOTP code from a secret.
82
+ #
83
+ # @param secret [String] TOTP secret (from authenticator app setup)
84
+ # @return [String] 6-digit TOTP code
85
+ def self.generate_totp(secret)
86
+ ROTP::TOTP.new(secret).now
87
+ end
88
+
89
+ private
90
+
91
+ def self.build_connection(base_url)
92
+ Faraday.new(url: base_url) do |c|
93
+ c.request :url_encoded
94
+ c.response :json, content_type: /\bjson$/
95
+ c.adapter Faraday.default_adapter
96
+ end
97
+ end
98
+ private_class_method :build_connection
99
+
100
+ def self.handle_response(response, context:)
37
101
  unless response.success?
38
- body = begin
39
- JSON.parse(response.body.to_s)
40
- rescue JSON::ParserError
41
- {}
42
- end
102
+ body = response.body.is_a?(Hash) ? response.body : {}
43
103
  error_message = body["errorMessage"] || body["message"] || response.body.to_s
44
- raise DhanHQ::InvalidAuthenticationError, "RenewToken failed: #{response.status} #{error_message}"
104
+
105
+ raise DhanHQ::InvalidAuthenticationError,
106
+ "#{context} failed: #{response.status} #{error_message}"
45
107
  end
46
108
 
47
- data = response.body
48
- data = JSON.parse(data) if data.is_a?(String)
49
- result = data.is_a?(Hash) ? data : {}
50
- result.with_indifferent_access
109
+ response.body.is_a?(Hash) ? response.body : {}
51
110
  end
111
+ private_class_method :handle_response
52
112
  end
53
113
  end
data/lib/DhanHQ/client.rb CHANGED
@@ -29,6 +29,8 @@ module DhanHQ
29
29
  # @return [Faraday::Connection] The connection instance used for API requests.
30
30
  attr_reader :connection
31
31
 
32
+ attr_reader :token_manager
33
+
32
34
  # Initializes a new DhanHQ Client instance with a Faraday connection.
33
35
  #
34
36
  # @example Create a new client:
@@ -38,9 +40,9 @@ module DhanHQ
38
40
  # @return [DhanHQ::Client] A new client instance.
39
41
  # @raise [DhanHQ::Error] If configuration is invalid or rate limiter initialization fails
40
42
  def initialize(api_type:)
41
- # Configure from ENV if CLIENT_ID is present (backward compatible behavior)
43
+ # Configure from ENV if DHAN_CLIENT_ID is present (backward compatible behavior)
42
44
  # Validation happens at request time in build_headers, not here
43
- DhanHQ.configure_with_env if ENV.fetch("CLIENT_ID", nil)
45
+ DhanHQ.configure_with_env if ENV.fetch("DHAN_CLIENT_ID", nil)
44
46
 
45
47
  # Use shared rate limiter instance per API type to ensure proper coordination
46
48
  @rate_limiter = RateLimiter.for(api_type)
@@ -72,6 +74,7 @@ module DhanHQ
72
74
  # @return [HashWithIndifferentAccess, Array<HashWithIndifferentAccess>] Parsed JSON response.
73
75
  # @raise [DhanHQ::Error] If an HTTP error occurs.
74
76
  def request(method, path, payload, retries: 3)
77
+ @token_manager&.ensure_valid_token!
75
78
  @rate_limiter.throttle! # **Ensure we don't hit rate limit before calling API**
76
79
 
77
80
  attempt = 0
@@ -159,6 +162,43 @@ module DhanHQ
159
162
  request(:delete, path, params)
160
163
  end
161
164
 
165
+ def generate_access_token(dhan_client_id:, pin:, totp: nil, totp_secret: nil)
166
+ token = Auth::TokenGenerator.new.generate(
167
+ dhan_client_id: dhan_client_id,
168
+ pin: pin,
169
+ totp: totp,
170
+ totp_secret: totp_secret
171
+ )
172
+
173
+ DhanHQ.configure do |config|
174
+ config.access_token = token.access_token
175
+ config.client_id = token.client_id if token.client_id.to_s.strip != ""
176
+ end
177
+
178
+ token
179
+ end
180
+
181
+ def renew_access_token
182
+ token = Auth::TokenRenewal.new.renew
183
+
184
+ DhanHQ.configure do |config|
185
+ config.access_token = token.access_token
186
+ config.client_id = token.client_id if token.client_id.to_s.strip != ""
187
+ end
188
+
189
+ token
190
+ end
191
+
192
+ def enable_auto_token_management!(dhan_client_id:, pin:, totp_secret:)
193
+ @token_manager = Auth::TokenManager.new(
194
+ dhan_client_id: dhan_client_id,
195
+ pin: pin,
196
+ totp_secret: totp_secret
197
+ )
198
+
199
+ @token_manager.generate!
200
+ end
201
+
162
202
  private
163
203
 
164
204
  # Calculates exponential backoff time
@@ -98,8 +98,8 @@ module DhanHQ
98
98
  # config.client_id = "your_client_id"
99
99
  # config.access_token = "your_access_token"
100
100
  def initialize
101
- @client_id = ENV.fetch("CLIENT_ID", nil)
102
- @access_token = ENV.fetch("ACCESS_TOKEN", nil)
101
+ @client_id = ENV.fetch("DHAN_CLIENT_ID", nil)
102
+ @access_token = ENV.fetch("DHAN_ACCESS_TOKEN", nil)
103
103
  @base_url = ENV.fetch("DHAN_BASE_URL", "https://api.dhan.co/v2")
104
104
  @ws_version = ENV.fetch("DHAN_WS_VERSION", 2).to_i
105
105
  @ws_order_url = ENV.fetch("DHAN_WS_ORDER_URL", "wss://api-order-update.dhan.co")
@@ -75,28 +75,5 @@ module DhanHQ
75
75
  end
76
76
  end
77
77
  end
78
-
79
- # Contract enforcing additional requirements for new order placement.
80
- class PlaceOrderContract < OrderContract
81
- # Additional placement specific rules
82
- rule(:after_market_order) do
83
- key.failure("amo_time required for after market orders") if value == true && !values[:amo_time]
84
- end
85
- end
86
-
87
- # Contract extending {OrderContract} for order modification payloads.
88
- class ModifyOrderContract < OrderContract
89
- # Modification specific requirements
90
- params do
91
- required(:order_id).filled(:string)
92
- optional(:quantity).maybe(:integer, gt?: 0)
93
- end
94
-
95
- rule do
96
- if !values[:price] && !values[:quantity] && !values[:trigger_price]
97
- key.failure("at least one modification field required")
98
- end
99
- end
100
- end
101
78
  end
102
79
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Contracts
5
+ # Validation contract for trade-by-order-id requests.
6
+ class TradeByOrderIdContract < BaseContract
7
+ params do
8
+ required(:order_id).filled(:string, min_size?: 1)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "date"
4
-
5
3
  module DhanHQ
6
4
  module Contracts
7
5
  ##
@@ -10,68 +8,5 @@ module DhanHQ
10
8
  # No input validation needed for GET requests
11
9
  # These contracts are mainly for documentation and future extensibility
12
10
  end
13
-
14
- ##
15
- # Validation contract for trade history requests
16
- class TradeHistoryContract < BaseContract
17
- params do
18
- required(:from_date).filled(:string)
19
- required(:to_date).filled(:string)
20
- optional(:page).filled(:integer, gteq?: 0)
21
- end
22
-
23
- rule(:from_date) do
24
- key.failure("must be in YYYY-MM-DD format (e.g., 2024-01-15)") unless valid_date_format?(value)
25
- key.failure("must be a valid trading date (no weekend dates)") if valid_date_format?(value) && !trading_day?(Date.parse(value))
26
- end
27
-
28
- rule(:to_date) do
29
- key.failure("must be in YYYY-MM-DD format (e.g., 2024-01-15)") unless valid_date_format?(value)
30
- end
31
-
32
- rule(:from_date, :to_date) do
33
- from_date_valid = valid_date_format?(values[:from_date])
34
- to_date_valid = valid_date_format?(values[:to_date])
35
-
36
- if values[:from_date] && values[:to_date] && from_date_valid && to_date_valid
37
- begin
38
- from_date = Date.parse(values[:from_date])
39
- to_date = Date.parse(values[:to_date])
40
-
41
- key.failure("from_date must be before to_date") if from_date >= to_date
42
- rescue Date::Error
43
- key.failure("invalid date format")
44
- end
45
- end
46
- end
47
-
48
- private
49
-
50
- def valid_date_format?(date_string)
51
- return false unless date_string.is_a?(String)
52
- return false unless date_string.match?(/\A\d{4}-\d{2}-\d{2}\z/)
53
-
54
- begin
55
- Date.parse(date_string)
56
- true
57
- rescue Date::Error
58
- false
59
- end
60
- end
61
-
62
- def trading_day?(date)
63
- return false unless date.is_a?(Date)
64
-
65
- (1..5).cover?(date.wday)
66
- end
67
- end
68
-
69
- ##
70
- # Validation contract for trade by order ID requests
71
- class TradeByOrderIdContract < BaseContract
72
- params do
73
- required(:order_id).filled(:string, min_size?: 1)
74
- end
75
- end
76
11
  end
77
12
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module DhanHQ
6
+ module Contracts
7
+ # Validation contract for trade history requests.
8
+ class TradeHistoryContract < BaseContract
9
+ params do
10
+ required(:from_date).filled(:string)
11
+ required(:to_date).filled(:string)
12
+ optional(:page).filled(:integer, gteq?: 0)
13
+ end
14
+
15
+ rule(:from_date) do
16
+ key.failure("must be in YYYY-MM-DD format (e.g., 2024-01-15)") unless valid_date_format?(value)
17
+ next unless valid_date_format?(value)
18
+
19
+ key.failure("must be a valid trading date (no weekend dates)") unless trading_day?(Date.parse(value))
20
+ end
21
+
22
+ rule(:to_date) do
23
+ key.failure("must be in YYYY-MM-DD format (e.g., 2024-01-15)") unless valid_date_format?(value)
24
+ end
25
+
26
+ rule(:from_date, :to_date) do
27
+ from = values[:from_date]
28
+ to = values[:to_date]
29
+ next unless valid_date_format?(from) && valid_date_format?(to)
30
+
31
+ key.failure("from_date must be before to_date") if Date.parse(from) >= Date.parse(to)
32
+ rescue Date::Error
33
+ key.failure("invalid date format")
34
+ end
35
+
36
+ private
37
+
38
+ def valid_date_format?(date_string)
39
+ return false unless date_string.is_a?(String) && date_string.match?(/\A\d{4}-\d{2}-\d{2}\z/)
40
+
41
+ Date.parse(date_string)
42
+ true
43
+ rescue Date::Error
44
+ false
45
+ end
46
+
47
+ def trading_day?(date)
48
+ date.is_a?(Date) && (1..5).cover?(date.wday)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+
5
+ module DhanHQ
6
+ # Faraday client for Dhan auth endpoints.
7
+ #
8
+ # This class intentionally lives at the top-level namespace so it autoloads
9
+ # cleanly from `lib/DhanHQ/core/auth_api.rb` with Zeitwerk `collapse`.
10
+ class AuthAPI
11
+ BASE_URL = "https://auth.dhan.co"
12
+
13
+ def connection
14
+ @connection ||= Faraday.new(url: BASE_URL) do |faraday|
15
+ faraday.request :url_encoded
16
+ faraday.response :json, content_type: /\bjson$/
17
+ faraday.adapter Faraday.default_adapter
18
+ end
19
+ end
20
+ end
21
+ end
@@ -44,7 +44,7 @@ module DhanHQ
44
44
  client_id = DhanHQ.configuration&.client_id
45
45
  unless client_id
46
46
  raise DhanHQ::InvalidAuthenticationError,
47
- "client_id is required for DATA APIs but not set. Please configure DhanHQ with CLIENT_ID."
47
+ "client_id is required for DATA APIs but not set. Please configure DhanHQ with DHAN_CLIENT_ID."
48
48
  end
49
49
  headers["client-id"] = client_id
50
50
  end
@@ -48,6 +48,28 @@ module DhanHQ
48
48
 
49
49
  find(response["alertId"])
50
50
  end
51
+
52
+ ##
53
+ # Modify an existing conditional trigger/alert order.
54
+ #
55
+ # @param alert_id [String] The alert ID to modify
56
+ # @param params [Hash] Updated parameters (condition, orders, etc.)
57
+ #
58
+ # @return [AlertOrder, nil] Updated AlertOrder instance, or nil on failure
59
+ #
60
+ # @example Modify an alert order's condition
61
+ # updated = DhanHQ::Models::AlertOrder.modify("12345",
62
+ # condition: { comparing_value: 300 },
63
+ # orders: [{ quantity: 20 }]
64
+ # )
65
+ #
66
+ def modify(alert_id, params)
67
+ normalized = snake_case(params)
68
+ response = resource.update(alert_id, camelize_keys(normalized))
69
+ return nil unless success_response?(response)
70
+
71
+ find(alert_id)
72
+ end
51
73
  end
52
74
 
53
75
  def id
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Models
5
+ ##
6
+ # Model for EDIS (Electronic Delivery Instruction Slip) operations.
7
+ #
8
+ # EDIS is used for selling holdings from your demat account. The API provides
9
+ # endpoints to generate T-PIN, create eDIS forms, and check authorization status.
10
+ #
11
+ # @example Generate T-PIN
12
+ # DhanHQ::Models::Edis.generate_tpin
13
+ #
14
+ # @example Generate eDIS form
15
+ # response = DhanHQ::Models::Edis.generate_form(
16
+ # isin: "INE155A01022",
17
+ # qty: 10,
18
+ # exchange: "NSE",
19
+ # segment: "E",
20
+ # bulk: false
21
+ # )
22
+ #
23
+ # @example Check EDIS status for a security
24
+ # status = DhanHQ::Models::Edis.inquire(isin: "INE155A01022")
25
+ #
26
+ class Edis < BaseModel
27
+ HTTP_PATH = "/edis"
28
+
29
+ class << self
30
+ ##
31
+ # Provides a shared instance of the Edis resource.
32
+ #
33
+ # @return [DhanHQ::Resources::Edis] The Edis resource client instance
34
+ def resource
35
+ @resource ||= DhanHQ::Resources::Edis.new
36
+ end
37
+
38
+ ##
39
+ # Generate T-PIN for eDIS authorization.
40
+ #
41
+ # Triggers T-PIN generation which is sent to the user's registered mobile/email.
42
+ #
43
+ # @return [Hash] API response
44
+ #
45
+ # @example Generate T-PIN before selling holdings
46
+ # DhanHQ::Models::Edis.generate_tpin
47
+ #
48
+ def generate_tpin
49
+ resource.tpin
50
+ end
51
+
52
+ ##
53
+ # Generate an eDIS form for authorizing sale of holdings.
54
+ #
55
+ # @param isin [String] ISIN of the security (e.g., "INE155A01022")
56
+ # @param qty [Integer] Quantity to authorize for sale
57
+ # @param exchange [String] Exchange name (e.g., "NSE", "BSE")
58
+ # @param segment [String] Segment identifier (e.g., "E")
59
+ # @param bulk [Boolean] Whether this is a bulk authorization (default: false)
60
+ #
61
+ # @return [Hash] API response containing the eDIS form data
62
+ #
63
+ # @example Authorize sale of 10 shares
64
+ # DhanHQ::Models::Edis.generate_form(
65
+ # isin: "INE155A01022",
66
+ # qty: 10,
67
+ # exchange: "NSE",
68
+ # segment: "E"
69
+ # )
70
+ #
71
+ def generate_form(isin:, qty:, exchange:, segment:, bulk: false)
72
+ resource.form({ isin: isin, qty: qty, exchange: exchange, segment: segment, bulk: bulk })
73
+ end
74
+
75
+ ##
76
+ # Generate a bulk eDIS form for multiple securities.
77
+ #
78
+ # @param params [Hash] Bulk form parameters
79
+ # @return [Hash] API response
80
+ #
81
+ def generate_bulk_form(params)
82
+ resource.bulk_form(params)
83
+ end
84
+
85
+ ##
86
+ # Check EDIS authorization status for a security.
87
+ #
88
+ # @param isin [String] ISIN of the security to check
89
+ #
90
+ # @return [Hash] API response containing authorization status
91
+ #
92
+ # @example Check if EDIS is authorized
93
+ # status = DhanHQ::Models::Edis.inquire(isin: "INE155A01022")
94
+ #
95
+ def inquire(isin:)
96
+ resource.inquire(isin)
97
+ end
98
+ end
99
+
100
+ ##
101
+ # No validation contract needed — EDIS operations are simple API calls.
102
+ #
103
+ # @return [nil]
104
+ # @api private
105
+ def validation_contract
106
+ nil
107
+ end
108
+ end
109
+ end
110
+ end
@@ -132,6 +132,28 @@ module DhanHQ
132
132
  def deactivate
133
133
  update("DEACTIVATE")
134
134
  end
135
+
136
+ ##
137
+ # Fetches the current kill switch status for your account.
138
+ #
139
+ # Checks whether the kill switch is currently active or inactive for
140
+ # the current trading day.
141
+ #
142
+ # @return [Hash{Symbol => String}] Response hash containing kill switch status.
143
+ # - **:dhan_client_id** [String] User-specific identification generated by Dhan
144
+ # - **:kill_switch_status** [String] Current status: "ACTIVATE" or "DEACTIVATE"
145
+ #
146
+ # @example Check if kill switch is active
147
+ # response = DhanHQ::Models::KillSwitch.status
148
+ # if response[:kill_switch_status] == "ACTIVATE"
149
+ # puts "Kill switch is active — trading disabled"
150
+ # else
151
+ # puts "Kill switch is inactive — trading enabled"
152
+ # end
153
+ #
154
+ def status
155
+ resource.status
156
+ end
135
157
  end
136
158
 
137
159
  ##
@@ -139,6 +139,55 @@ module DhanHQ
139
139
  response = resource.calculate(formatted_params)
140
140
  new(response, skip_validation: true)
141
141
  end
142
+
143
+ ##
144
+ # Calculates margin requirements for multiple scripts in a single request.
145
+ #
146
+ # Provides combined margin calculation including hedge benefit across multiple
147
+ # instruments. Useful for portfolio-level margin analysis.
148
+ #
149
+ # @param params [Hash{Symbol => Object}] Request parameters
150
+ # @option params [Boolean] :include_position Whether to include existing positions
151
+ # @option params [Boolean] :include_order Whether to include existing orders
152
+ # @option params [String] :dhan_client_id User-specific identification
153
+ # @option params [Array<Hash>] :scrip_list Array of instrument margin params, each with:
154
+ # - :exchange_segment [String]
155
+ # - :transaction_type [String]
156
+ # - :quantity [Integer]
157
+ # - :product_type [String]
158
+ # - :security_id [String]
159
+ # - :price [Float]
160
+ # - :trigger_price [Float] (optional)
161
+ #
162
+ # @return [Hash{Symbol => String}] Response hash containing:
163
+ # - **:total_margin** [String] Total margin required
164
+ # - **:span_margin** [String] SPAN margin
165
+ # - **:exposure_margin** [String] Exposure margin
166
+ # - **:equity_margin** [String] Equity margin
167
+ # - **:fo_margin** [String] F&O margin
168
+ # - **:commodity_margin** [String] Commodity margin
169
+ # - **:currency** [String] Currency (e.g., "INR")
170
+ # - **:hedge_benefit** [String] Hedge benefit amount
171
+ #
172
+ # @example Calculate margin for multiple scripts
173
+ # result = DhanHQ::Models::Margin.calculate_multi(
174
+ # include_position: true,
175
+ # include_order: true,
176
+ # dhan_client_id: "1000000132",
177
+ # scrip_list: [
178
+ # { exchange_segment: "NSE_EQ", transaction_type: "BUY",
179
+ # quantity: 100, product_type: "CNC", security_id: "1333", price: 1428.0 },
180
+ # { exchange_segment: "NSE_FNO", transaction_type: "SELL",
181
+ # quantity: 50, product_type: "INTRADAY", security_id: "43492", price: 200.0 }
182
+ # ]
183
+ # )
184
+ # puts "Total margin: #{result[:total_margin]}"
185
+ # puts "Hedge benefit: #{result[:hedge_benefit]}"
186
+ #
187
+ def calculate_multi(params)
188
+ formatted_params = camelize_keys(params)
189
+ resource.calculate_multi(formatted_params)
190
+ end
142
191
  end
143
192
 
144
193
  ##