DhanHQ 2.6.2 → 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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -3
  3. data/ARCHITECTURE.md +113 -0
  4. data/CHANGELOG.md +31 -0
  5. data/README.md +2 -0
  6. data/docs/API_VERIFICATION.md +10 -8
  7. data/docs/ENDPOINTS_AND_SANDBOX.md +12 -0
  8. data/lib/DhanHQ/auth.rb +2 -2
  9. data/lib/DhanHQ/client.rb +42 -34
  10. data/lib/DhanHQ/configuration.rb +5 -6
  11. data/lib/DhanHQ/constants.rb +67 -7
  12. data/lib/DhanHQ/contracts/alert_order_contract.rb +23 -16
  13. data/lib/DhanHQ/contracts/expired_options_data_contract.rb +4 -2
  14. data/lib/DhanHQ/contracts/forever_order_contract.rb +55 -0
  15. data/lib/DhanHQ/contracts/historical_data_contract.rb +17 -19
  16. data/lib/DhanHQ/contracts/intraday_historical_data_contract.rb +12 -0
  17. data/lib/DhanHQ/contracts/margin_calculator_contract.rb +19 -17
  18. data/lib/DhanHQ/contracts/market_feed_contract.rb +42 -0
  19. data/lib/DhanHQ/contracts/multi_scrip_margin_calc_request_contract.rb +8 -5
  20. data/lib/DhanHQ/contracts/option_chain_contract.rb +17 -19
  21. data/lib/DhanHQ/contracts/pnl_based_exit_contract.rb +1 -1
  22. data/lib/DhanHQ/contracts/slice_order_contract.rb +10 -10
  23. data/lib/DhanHQ/core/auth_api.rb +1 -1
  24. data/lib/DhanHQ/core/base_api.rb +9 -9
  25. data/lib/DhanHQ/core/base_model.rb +4 -1
  26. data/lib/DhanHQ/core/error_handler.rb +2 -2
  27. data/lib/DhanHQ/errors.rb +14 -2
  28. data/lib/DhanHQ/helpers/request_helper.rb +11 -2
  29. data/lib/DhanHQ/helpers/response_helper.rb +48 -19
  30. data/lib/DhanHQ/helpers/validation_helper.rb +4 -2
  31. data/lib/DhanHQ/models/alert_order.rb +6 -2
  32. data/lib/DhanHQ/models/edis.rb +20 -13
  33. data/lib/DhanHQ/models/expired_options_data.rb +54 -44
  34. data/lib/DhanHQ/models/forever_order.rb +16 -7
  35. data/lib/DhanHQ/models/historical_data.rb +40 -6
  36. data/lib/DhanHQ/models/instrument_helpers.rb +2 -1
  37. data/lib/DhanHQ/models/margin.rb +62 -82
  38. data/lib/DhanHQ/models/market_feed.rb +14 -3
  39. data/lib/DhanHQ/models/option_chain.rb +50 -150
  40. data/lib/DhanHQ/models/order.rb +19 -4
  41. data/lib/DhanHQ/resources/alert_orders.rb +1 -1
  42. data/lib/DhanHQ/resources/edis.rb +4 -3
  43. data/lib/DhanHQ/resources/instruments.rb +3 -2
  44. data/lib/DhanHQ/resources/ip_setup.rb +4 -1
  45. data/lib/DhanHQ/resources/kill_switch.rb +7 -1
  46. data/lib/DhanHQ/resources/orders.rb +1 -1
  47. data/lib/DhanHQ/resources/super_orders.rb +8 -2
  48. data/lib/DhanHQ/resources/trader_control.rb +13 -4
  49. data/lib/DhanHQ/version.rb +1 -1
  50. data/lib/DhanHQ/ws/base_connection.rb +1 -1
  51. data/lib/DhanHQ/ws/market_depth/client.rb +11 -4
  52. data/lib/dhan_hq.rb +17 -20
  53. data/lib/ta/indicators.rb +15 -18
  54. metadata +6 -9
  55. data/CODE_REVIEW_ISSUES.md +0 -397
  56. data/FIXES_APPLIED.md +0 -373
  57. data/RELEASING.md +0 -60
  58. data/REVIEW_SUMMARY.md +0 -120
  59. data/VERSION_UPDATE.md +0 -82
  60. data/diagram.md +0 -34
  61. data/docs/ARCHIVE_README.md +0 -784
@@ -8,7 +8,7 @@ module DhanHQ
8
8
  class AlertOrder < BaseModel
9
9
  include Concerns::ApiResponseHandler
10
10
 
11
- HTTP_PATH = "/alerts/orders"
11
+ HTTP_PATH = "/v2/alerts/orders"
12
12
 
13
13
  attributes :alert_id, :exchange_segment, :security_id, :condition,
14
14
  :trigger_price, :order_type, :transaction_type, :quantity,
@@ -37,6 +37,8 @@ module DhanHQ
37
37
  return nil unless response.is_a?(Hash) || (response.is_a?(Array) && response.any?)
38
38
 
39
39
  payload = response.is_a?(Array) ? response.first : response
40
+ return nil if payload.is_a?(Hash) && payload.empty?
41
+
40
42
  new(payload, skip_validation: true)
41
43
  end
42
44
 
@@ -65,7 +67,9 @@ module DhanHQ
65
67
  #
66
68
  def modify(alert_id, params)
67
69
  normalized = snake_case(params)
68
- response = resource.update(alert_id, camelize_keys(normalized))
70
+ validate_params!(normalized, DhanHQ::Contracts::AlertOrderContract)
71
+ payload = normalized.merge(alert_id: alert_id)
72
+ response = resource.update(alert_id, camelize_keys(payload))
69
73
  return nil unless success_response?(response)
70
74
 
71
75
  find(alert_id)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../contracts/edis_contract"
4
+
3
5
  module DhanHQ
4
6
  module Models
5
7
  ##
@@ -16,15 +18,15 @@ module DhanHQ
16
18
  # isin: "INE155A01022",
17
19
  # qty: 10,
18
20
  # exchange: "NSE",
19
- # segment: "E",
21
+ # segment: "EQ",
20
22
  # bulk: false
21
23
  # )
22
24
  #
23
- # @example Check EDIS status for a security
25
+ # @example Check EDIS status for a security (or "ALL" for all holdings)
24
26
  # status = DhanHQ::Models::Edis.inquire(isin: "INE155A01022")
25
27
  #
26
28
  class Edis < BaseModel
27
- HTTP_PATH = "/edis"
29
+ HTTP_PATH = "/v2/edis"
28
30
 
29
31
  class << self
30
32
  ##
@@ -52,24 +54,26 @@ module DhanHQ
52
54
  ##
53
55
  # Generate an eDIS form for authorizing sale of holdings.
54
56
  #
55
- # @param isin [String] ISIN of the security (e.g., "INE155A01022")
57
+ # @param isin [String] ISIN of the security (e.g. "INE733E01010")
56
58
  # @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)
59
+ # @param exchange [String] Exchange: "NSE" or "BSE"
60
+ # @param segment [String] Segment: "EQ", "COMM", or "FNO"
61
+ # @param bulk [Boolean] If true, mark eDIS for all stocks in portfolio (default: false)
60
62
  #
61
- # @return [Hash] API response containing the eDIS form data
63
+ # @return [Hash] API response with dhanClientId and edisFormHtml (escaped HTML to render)
62
64
  #
63
65
  # @example Authorize sale of 10 shares
64
66
  # DhanHQ::Models::Edis.generate_form(
65
67
  # isin: "INE155A01022",
66
68
  # qty: 10,
67
69
  # exchange: "NSE",
68
- # segment: "E"
70
+ # segment: "EQ"
69
71
  # )
70
72
  #
71
73
  def generate_form(isin:, qty:, exchange:, segment:, bulk: false)
72
- resource.form({ isin: isin, qty: qty, exchange: exchange, segment: segment, bulk: bulk })
74
+ params = { isin: isin, qty: qty, exchange: exchange, segment: segment, bulk: bulk }
75
+ validate_params!(params, DhanHQ::Contracts::EdisFormContract)
76
+ resource.form(params)
73
77
  end
74
78
 
75
79
  ##
@@ -85,13 +89,16 @@ module DhanHQ
85
89
  ##
86
90
  # Check EDIS authorization status for a security.
87
91
  #
88
- # @param isin [String] ISIN of the security to check
92
+ # @param isin [String] ISIN of the security, or "ALL" for all holdings
89
93
  #
90
- # @return [Hash] API response containing authorization status
94
+ # @return [Hash] API response with clientId, isin, totalQty, aprvdQty, status, remarks
91
95
  #
92
- # @example Check if EDIS is authorized
96
+ # @example Check if EDIS is authorized for one security
93
97
  # status = DhanHQ::Models::Edis.inquire(isin: "INE155A01022")
94
98
  #
99
+ # @example Check EDIS status for all holdings
100
+ # status = DhanHQ::Models::Edis.inquire(isin: "ALL")
101
+ #
95
102
  def inquire(isin:)
96
103
  resource.inquire(isin)
97
104
  end
@@ -42,7 +42,12 @@ module DhanHQ
42
42
  # call_data = data.call_data
43
43
  # put_data = data.put_data
44
44
  #
45
+ # @example Normalize to candles
46
+ # candles = data.to_candles
47
+ #
45
48
  class ExpiredOptionsData < BaseModel
49
+ OHLC_FIELDS = %i[open high low close iv volume strike spot oi open_interest].freeze
50
+
46
51
  # All expired options data attributes
47
52
  attributes :exchange_segment, :interval, :security_id, :instrument,
48
53
  :expiry_flag, :expiry_code, :strike, :drv_option_type,
@@ -56,63 +61,29 @@ module DhanHQ
56
61
  # 31 days in a single request. Historical data is available for up to the last 5 years.
57
62
  #
58
63
  # @param params [Hash{Symbol => String, Integer, Array<String>}] Request parameters
64
+ # @option params [String, Integer] :security_id (required) Underlying exchange standard ID for each scrip
59
65
  # @option params [String] :exchange_segment (required) Exchange and segment identifier.
60
- # Valid values: "NSE_FNO", "BSE_FNO", "NSE_EQ", "BSE_EQ"
61
- # @option params [String] :interval (required) Minute intervals for the timeframe.
62
- # Valid values: "1", "5", "15", "25", "60"
63
- # @option params [Integer] :security_id (required) Underlying exchange standard ID for each scrip
66
+ # Valid values: "NSE_FNO", "IDX_I", "NSE_EQ", "BSE_EQ"
64
67
  # @option params [String] :instrument (required) Instrument type of the scrip.
65
68
  # Valid values: "OPTIDX" (Index Options), "OPTSTK" (Stock Options)
69
+ # @option params [String, Integer] :interval (required) Minute intervals for the timeframe.
70
+ # Valid values: "1", "5", "15", "25", "60"
66
71
  # @option params [String] :expiry_flag (required) Expiry interval of the instrument.
67
72
  # Valid values: "WEEK", "MONTH"
68
73
  # @option params [Integer] :expiry_code (required) Expiry code for the instrument
69
74
  # @option params [String] :strike (required) Strike price specification.
70
75
  # Format: "ATM" for At The Money, "ATM+X" or "ATM-X" for offset strikes.
71
- # For Index Options (near expiry): Up to ATM+10 / ATM-10
72
- # For all other contracts: Up to ATM+3 / ATM-3
73
- # @option params [String] :drv_option_type (required) Option type.
74
- # Valid values: "CALL", "PUT"
76
+ # @option params [String] :option_type (required) Option type ("CALL" or "PUT").
75
77
  # @option params [Array<String>] :required_data (required) Array of required data fields.
76
- # Valid values: "open", "high", "low", "close", "iv", "volume", "strike", "oi", "spot"
77
- # @option params [String] :from_date (required) Start date of the desired range in YYYY-MM-DD format.
78
- # Cannot be more than 5 years ago. Same-day ranges are allowed.
79
- # @option params [String] :to_date (required) End date of the desired range (non-inclusive) in YYYY-MM-DD format.
80
- # Date range cannot exceed 31 days from from_date (to_date is non-inclusive). Same-day `from_date`/`to_date` is valid.
78
+ # @option params [String] :from_date (required) Start date in YYYY-MM-DD format.
79
+ # @option params [String] :to_date (required) End date in YYYY-MM-DD format.
81
80
  #
82
81
  # @return [ExpiredOptionsData] Expired options data object with fetched data
83
- #
84
- # @example Fetch NIFTY index options data
85
- # data = DhanHQ::Models::ExpiredOptionsData.fetch(
86
- # exchange_segment: "NSE_FNO",
87
- # interval: "1",
88
- # security_id: 13,
89
- # instrument: "OPTIDX",
90
- # expiry_flag: "MONTH",
91
- # expiry_code: 1,
92
- # strike: "ATM",
93
- # drv_option_type: "CALL",
94
- # required_data: ["open", "high", "low", "close", "volume", "iv", "oi", "spot"],
95
- # from_date: "2021-08-01",
96
- # to_date: "2021-09-01"
97
- # )
98
- #
99
- # @example Fetch stock options data for ATM+2 strike
100
- # data = DhanHQ::Models::ExpiredOptionsData.fetch(
101
- # exchange_segment: "NSE_FNO",
102
- # interval: "15",
103
- # security_id: 11536,
104
- # instrument: "OPTSTK",
105
- # expiry_flag: "WEEK",
106
- # expiry_code: 0,
107
- # strike: "ATM+2",
108
- # drv_option_type: "PUT",
109
- # required_data: ["open", "high", "low", "close", "volume"],
110
- # from_date: "2024-01-01",
111
- # to_date: "2024-01-31"
112
- # )
113
- #
114
82
  # @raise [DhanHQ::ValidationError] If validation fails for any parameter
115
83
  def fetch(params)
84
+ # Map option_type to drv_option_type if provided
85
+ params[:drv_option_type] ||= params[:option_type] if params.key?(:option_type)
86
+
116
87
  normalized = normalize_params(params)
117
88
  validate_params(normalized)
118
89
 
@@ -120,6 +91,8 @@ module DhanHQ
120
91
  new(response.merge(normalized), skip_validation: true)
121
92
  end
122
93
 
94
+ alias rolling fetch
95
+
123
96
  private
124
97
 
125
98
  def expired_options_resource
@@ -180,6 +153,43 @@ module DhanHQ
180
153
  end
181
154
  end
182
155
 
156
+ ##
157
+ # Normalizes the columnar response into an array of candle hashes.
158
+ #
159
+ # @param option_type [String, nil] Option type to retrieve ("CALL" or "PUT").
160
+ # If nil, uses the {#drv_option_type} from the request.
161
+ # @return [Array<Hash>] Normalized array of candles.
162
+ def to_candles(option_type = nil)
163
+ option_type ||= drv_option_type
164
+ opt_data = data_for_type(option_type)
165
+ return [] unless opt_data.is_a?(Hash)
166
+
167
+ # Standardize keys to symbols
168
+ opt_data = opt_data.transform_keys(&:to_sym)
169
+ ts_arr = opt_data[:timestamp]
170
+ return [] unless ts_arr.is_a?(Array)
171
+
172
+ type_sym = option_type.to_s.downcase.to_sym
173
+
174
+ ts_arr.each_with_index.map do |ts, i|
175
+ candle = {
176
+ option_type: type_sym,
177
+ timestamp: ts.is_a?(Numeric) ? Time.at(ts) : ts
178
+ }
179
+
180
+ # Map requested fields
181
+ OHLC_FIELDS.each do |field|
182
+ val_arr = opt_data[field]
183
+ next unless val_arr.is_a?(Array)
184
+
185
+ # Map 'oi' to 'open_interest' if requested
186
+ target_field = field == :oi ? :open_interest : field
187
+ candle[target_field] = val_arr[i]
188
+ end
189
+ candle
190
+ end
191
+ end
192
+
183
193
  ##
184
194
  # Gets call option data from the response.
185
195
  #
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../contracts/forever_order_contract"
4
+
3
5
  module DhanHQ
4
6
  module Models
5
7
  ##
@@ -154,9 +156,9 @@ module DhanHQ
154
156
  # @option params [String] :transaction_type (required) The trading side of transaction.
155
157
  # Valid values: "BUY", "SELL"
156
158
  # @option params [String] :exchange_segment (required) Exchange and segment identifier.
157
- # Valid values: "NSE_EQ", "NSE_FNO", "BSE_EQ", "BSE_FNO"
159
+ # Valid values: See {DhanHQ::Constants::FOREVER_ORDER_SEGMENTS} (NSE_EQ, NSE_FNO, BSE_EQ, BSE_FNO, MCX_COMM)
158
160
  # @option params [String] :product_type (required) Product type.
159
- # Valid values: "CNC", "MTF"
161
+ # Valid values: See {DhanHQ::Constants::FOREVER_ORDER_PRODUCT_TYPES} (CNC, MTF)
160
162
  # @option params [String] :order_type (required) Order type.
161
163
  # Valid values: "LIMIT", "MARKET"
162
164
  # @option params [String] :validity (required) Validity of order for execution.
@@ -216,11 +218,12 @@ module DhanHQ
216
218
  #
217
219
  # @note Order placement APIs require Static IP whitelisting
218
220
  def create(params)
219
- # Normalize params and auto-inject dhan_client_id from configuration if not provided
220
- normalized_params = snake_case(params)
221
+ normalized = snake_case(params)
221
222
  config = DhanHQ.configuration
222
- normalized_params[:dhan_client_id] ||= config.client_id if config&.client_id
223
- response = resource.create(normalized_params)
223
+ normalized[:dhan_client_id] ||= config.client_id if config&.client_id
224
+ validate_params!(normalized, DhanHQ::Contracts::ForeverOrderCreateContract)
225
+ formatted = camelize_keys(normalized)
226
+ response = resource.create(formatted)
224
227
  return nil unless response.is_a?(Hash) && response["orderId"]
225
228
 
226
229
  find(response["orderId"])
@@ -292,7 +295,13 @@ module DhanHQ
292
295
  raise "Order ID is required to modify a forever order" unless order_id
293
296
 
294
297
  DhanHQ.logger&.info("[DhanHQ::Models::ForeverOrder] Modifying order #{order_id}")
295
- response = self.class.resource.update(order_id, new_params)
298
+ full_params = snake_case(new_params)
299
+ config = DhanHQ.configuration
300
+ full_params[:dhan_client_id] ||= config.client_id if config&.client_id
301
+ full_params[:order_id] = order_id
302
+ validate_params!(full_params, DhanHQ::Contracts::ForeverOrderModifyContract)
303
+ formatted = camelize_keys(full_params)
304
+ response = self.class.resource.update(order_id, formatted)
296
305
  ctx = "[DhanHQ::Models::ForeverOrder] Modification"
297
306
  success = handle_api_response(response, success_key: "orderId", context: ctx)
298
307
  return self.class.find(order_id) if success
@@ -56,11 +56,11 @@ module DhanHQ
56
56
  # @param params [Hash{Symbol => String, Integer, Boolean}] Request parameters
57
57
  # @option params [String] :security_id (required) Exchange standard ID for each scrip
58
58
  # @option params [String] :exchange_segment (required) Exchange and segment for which data is to be fetched.
59
- # Valid values: See {DhanHQ::Constants::EXCHANGE_SEGMENTS}
59
+ # Valid values: See {DhanHQ::Constants::CHART_EXCHANGE_SEGMENTS}
60
60
  # @option params [String] :instrument (required) Instrument type of the scrip.
61
61
  # Valid values: See {DhanHQ::Constants::INSTRUMENTS}
62
62
  # @option params [Integer] :expiry_code (optional) Expiry of the instruments in case of derivatives.
63
- # Valid values: 0, 1, 2
63
+ # Valid values: See {DhanHQ::Constants::ExpiryCode::ALL} (0, 1, 2)
64
64
  # @option params [Boolean] :oi (optional) Include Open Interest data for Futures & Options.
65
65
  # Default: false
66
66
  # @option params [String] :from_date (required) Start date of the desired range in YYYY-MM-DD format
@@ -100,8 +100,9 @@ module DhanHQ
100
100
  #
101
101
  # @raise [DhanHQ::ValidationError] If validation fails for any parameter
102
102
  def daily(params)
103
- validate_params!(params, DhanHQ::Contracts::HistoricalDataContract)
104
- resource.daily(params)
103
+ validated_params = validate_params!(params, DhanHQ::Contracts::HistoricalDataContract)
104
+ response = resource.daily(validated_params)
105
+ normalize(response)
105
106
  end
106
107
 
107
108
  ##
@@ -177,8 +178,41 @@ module DhanHQ
177
178
  # make multiple requests or store data locally for analysis.
178
179
  # @raise [DhanHQ::ValidationError] If validation fails for any parameter
179
180
  def intraday(params)
180
- validate_params!(params, DhanHQ::Contracts::HistoricalDataContract)
181
- resource.intraday(params)
181
+ validated_params = validate_params!(params, DhanHQ::Contracts::IntradayHistoricalDataContract)
182
+ response = resource.intraday(validated_params)
183
+ normalize(response)
184
+ end
185
+
186
+ private
187
+
188
+ # Normalizes the columnar API response into an array of candle hashes.
189
+ #
190
+ # @param response [Hash] The raw API response
191
+ # @return [Array<Hash>, Hash] Normalized array of candles, or raw response if structure unexpected
192
+ def normalize(response)
193
+ # Use symbols or strings depending on HashWithIndifferentAccess behavior
194
+ close = response[:close] || response["close"]
195
+ return response unless response.is_a?(Hash) && close.is_a?(Array)
196
+
197
+ ts = response[:timestamp] || response["timestamp"]
198
+ open = response[:open] || response["open"]
199
+ high = response[:high] || response["high"]
200
+ low = response[:low] || response["low"]
201
+ volume = response[:volume] || response["volume"]
202
+ oi = response[:open_interest] || response["open_interest"]
203
+
204
+ (0...close.size).map do |i|
205
+ candle = {
206
+ timestamp: ts[i].is_a?(Numeric) ? Time.at(ts[i]) : ts[i],
207
+ open: open[i],
208
+ high: high[i],
209
+ low: low[i],
210
+ close: close[i],
211
+ volume: volume[i]
212
+ }
213
+ candle[:open_interest] = oi[i] if oi && oi[i]
214
+ candle
215
+ end
182
216
  end
183
217
  end
184
218
  end
@@ -161,7 +161,8 @@ module DhanHQ
161
161
  return seg if %w[IDX_I NSE_FNO BSE_FNO MCX_FO].include?(seg)
162
162
 
163
163
  # Index detection by instrument kind or segment
164
- return DhanHQ::Constants::ExchangeSegment::IDX_I if ins == DhanHQ::Constants::InstrumentType::INDEX || seg == DhanHQ::Constants::ExchangeSegment::IDX_I
164
+ idx_seg = DhanHQ::Constants::ExchangeSegment::IDX_I
165
+ return idx_seg if ins == DhanHQ::Constants::InstrumentType::INDEX || seg == idx_seg
165
166
 
166
167
  # Map equities/stock-related segments to respective FNO
167
168
  return DhanHQ::Constants::ExchangeSegment::NSE_FNO if seg.start_with?("NSE")
@@ -48,7 +48,9 @@ module DhanHQ
48
48
  HTTP_PATH = "/v2/margincalculator"
49
49
 
50
50
  attributes :total_margin, :span_margin, :exposure_margin, :available_balance,
51
- :variable_margin, :insufficient_balance, :brokerage, :leverage
51
+ :variable_margin, :insufficient_balance, :brokerage, :leverage,
52
+ :client_id, :equity_margin, :fo_margin, :commodity_margin,
53
+ :currency, :hedge_benefit, :exposure, :commodity
52
54
 
53
55
  class << self
54
56
  ##
@@ -70,28 +72,18 @@ module DhanHQ
70
72
  # @option params [String] :dhan_client_id (required) User-specific identification generated by Dhan.
71
73
  # Must be explicitly provided in the params hash
72
74
  # @option params [String] :exchange_segment (required) Exchange and segment identifier.
73
- # Valid values: "NSE_EQ", "NSE_FNO", "BSE_EQ", "BSE_FNO", "MCX_COMM"
75
+ # Valid values: See {DhanHQ::Constants::MARGIN_CALCULATOR_SEGMENTS} (NSE_EQ, NSE_FNO, BSE_EQ, BSE_FNO, MCX_COMM)
74
76
  # @option params [String] :transaction_type (required) The trading side of transaction.
75
77
  # Valid values: "BUY", "SELL"
76
78
  # @option params [Integer] :quantity (required) Number of shares for the order. Must be greater than 0
77
79
  # @option params [String] :product_type (required) Product type.
78
- # Valid values: "CNC", "INTRADAY", "MARGIN", "MTF", "CO", "BO"
80
+ # Valid values: See {DhanHQ::Constants::MARGIN_PRODUCT_TYPES} (CNC, INTRADAY, MARGIN, MTF)
79
81
  # @option params [String] :security_id (required) Exchange standard ID for each scrip
80
82
  # @option params [Float] :price (required) Price at which order is placed. Must be greater than 0
81
83
  # @option params [Float] :trigger_price (conditionally required) Price at which the order is triggered.
82
84
  # Required for STOP_LOSS and STOP_LOSS_MARKET order types
83
85
  #
84
86
  # @return [Margin] Margin object containing margin calculation results.
85
- # Response structure (keys normalized to snake_case):
86
- # - **:total_margin** [Float] Total margin required for placing the order successfully
87
- # - **:span_margin** [Float] SPAN margin required
88
- # - **:exposure_margin** [Float] Exposure margin required
89
- # - **:available_balance** [Float] Available amount in trading account
90
- # - **:variable_margin** [Float] VAR or Variable margin required
91
- # - **:insufficient_balance** [Float] Insufficient amount in trading account
92
- # (Available Balance - Total Margin). Negative or zero indicates sufficient margin
93
- # - **:brokerage** [Float] Brokerage charges for executing the order
94
- # - **:leverage** [String] Margin leverage provided for the order as per product type
95
87
  #
96
88
  # @example Calculate margin for CNC equity order
97
89
  # margin = DhanHQ::Models::Margin.calculate(
@@ -106,31 +98,6 @@ module DhanHQ
106
98
  # puts "Total Margin: ₹#{margin.total_margin}"
107
99
  # puts "Brokerage: ₹#{margin.brokerage}"
108
100
  #
109
- # @example Calculate margin for intraday order
110
- # margin = DhanHQ::Models::Margin.calculate(
111
- # dhan_client_id: "1000000132",
112
- # exchange_segment: "NSE_EQ",
113
- # transaction_type: "SELL",
114
- # quantity: 10,
115
- # product_type: "INTRADAY",
116
- # security_id: "1333",
117
- # price: 1500.0
118
- # )
119
- # puts "Leverage: #{margin.leverage}x"
120
- # puts "SPAN Margin: ₹#{margin.span_margin}"
121
- #
122
- # @example Calculate margin for stop-loss order
123
- # margin = DhanHQ::Models::Margin.calculate(
124
- # dhan_client_id: "1000000132",
125
- # exchange_segment: "NSE_EQ",
126
- # transaction_type: "BUY",
127
- # quantity: 5,
128
- # product_type: "INTRADAY",
129
- # security_id: "1333",
130
- # price: 1428.0,
131
- # trigger_price: 1427.0
132
- # )
133
- #
134
101
  # @raise [DhanHQ::ValidationError] If validation fails for any parameter
135
102
  def calculate(params)
136
103
  formatted_params = camelize_keys(params)
@@ -148,45 +115,68 @@ module DhanHQ
148
115
  #
149
116
  # @param params [Hash{Symbol => Object}] Request parameters
150
117
  # @option params [Boolean] :include_position Whether to include existing positions
151
- # @option params [Boolean] :include_order Whether to include existing orders
118
+ # @option params [Boolean] :include_orders Whether to include existing orders
152
119
  # @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]
120
+ # @option params [Array<Hash>] :scripts Array of instrument margin params, each with:
121
+ # - :exchange_segment [String] See {DhanHQ::Constants::MARGIN_CALCULATOR_SEGMENTS}
122
+ # - :transaction_type [String] BUY or SELL
156
123
  # - :quantity [Integer]
157
- # - :product_type [String]
124
+ # - :product_type [String] See {DhanHQ::Constants::MARGIN_PRODUCT_TYPES}
158
125
  # - :security_id [String]
159
- # - :price [Float]
126
+ # - :price [Float] (required)
160
127
  # - :trigger_price [Float] (optional)
161
128
  #
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
129
+ # @return [Margin] Margin object containing combined results.
171
130
  #
172
131
  # @example Calculate margin for multiple scripts
173
- # result = DhanHQ::Models::Margin.calculate_multi(
132
+ # margin = DhanHQ::Models::Margin.calculate_multi(
174
133
  # include_position: true,
175
- # include_order: true,
176
- # dhan_client_id: "1000000132",
177
- # scrip_list: [
134
+ # include_orders: true,
135
+ # scripts: [
178
136
  # { exchange_segment: "NSE_EQ", transaction_type: "BUY",
179
137
  # 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 }
138
+ # { exchange_segment: "NSE_EQ", transaction_type: "SELL",
139
+ # quantity: 50, product_type: "INTRADAY", security_id: "11536", price: 3000.0 }
182
140
  # ]
183
141
  # )
184
- # puts "Total margin: #{result[:total_margin]}"
185
- # puts "Hedge benefit: #{result[:hedge_benefit]}"
142
+ # puts "Total margin: #{margin.total_margin}"
186
143
  #
187
144
  def calculate_multi(params)
188
- formatted_params = camelize_keys(params)
189
- resource.calculate_multi(formatted_params)
145
+ # Map scripts to scrip_list and include_orders to include_order if provided
146
+ params[:scrip_list] ||= params[:scripts] if params.key?(:scripts)
147
+ params[:include_order] ||= params[:include_orders] if params.key?(:include_orders)
148
+ params[:dhan_client_id] ||= DhanHQ.configuration.client_id
149
+
150
+ # Filter only keys supported by the API
151
+ filtered_params = {
152
+ includePosition: params[:include_position],
153
+ includeOrder: params[:include_order],
154
+ dhanClientId: params[:dhan_client_id],
155
+ scripList: params[:scrip_list]
156
+ }
157
+
158
+ if filtered_params[:scripList].is_a?(Array)
159
+ filtered_params[:scripList] = filtered_params[:scripList].map do |scrip|
160
+ if scrip.is_a?(Hash)
161
+ {
162
+ exchangeSegment: scrip[:exchange_segment] || scrip[:exchangeSegment],
163
+ transactionType: scrip[:transaction_type] || scrip[:transactionType],
164
+ quantity: scrip[:quantity],
165
+ productType: scrip[:product_type] || scrip[:productType],
166
+ orderType: scrip[:order_type] || scrip[:orderType],
167
+ securityId: scrip[:security_id] || scrip[:securityId],
168
+ price: scrip[:price],
169
+ triggerPrice: scrip[:trigger_price] || scrip[:triggerPrice]
170
+ }.compact
171
+ else
172
+ scrip
173
+ end
174
+ end
175
+ end
176
+
177
+ validate_params!(filtered_params, DhanHQ::Contracts::MultiScripMarginCalcRequestContract)
178
+ response = resource.calculate_multi(filtered_params)
179
+ new(response, skip_validation: true)
190
180
  end
191
181
  end
192
182
 
@@ -195,33 +185,23 @@ module DhanHQ
195
185
  #
196
186
  # Useful for serialization, logging, or passing margin data to other methods.
197
187
  #
198
- # @return [Hash{Symbol => Float, String}] Hash representation of the Margin model containing:
199
- # - **:total_margin** [Float] Total margin required
200
- # - **:span_margin** [Float] SPAN margin
201
- # - **:exposure_margin** [Float] Exposure margin
202
- # - **:available_balance** [Float] Available balance
203
- # - **:variable_margin** [Float] Variable margin
204
- # - **:insufficient_balance** [Float] Insufficient balance amount
205
- # - **:brokerage** [Float] Brokerage charges
206
- # - **:leverage** [String] Leverage as string
207
- #
208
- # @example Convert margin to hash
209
- # margin = DhanHQ::Models::Margin.calculate(params)
210
- # margin_hash = margin.to_h
211
- # puts margin_hash[:total_margin] # => 2800.00
212
- # puts margin_hash[:leverage] # => "4.00"
213
- #
188
+ # @return [Hash{Symbol => Float, String}] Hash representation of the Margin model.
214
189
  def to_h
215
190
  {
216
191
  total_margin: total_margin,
217
192
  span_margin: span_margin,
218
- exposure_margin: exposure_margin,
193
+ exposure_margin: exposure_margin || exposure,
219
194
  available_balance: available_balance,
220
195
  variable_margin: variable_margin,
221
196
  insufficient_balance: insufficient_balance,
222
197
  brokerage: brokerage,
223
- leverage: leverage
224
- }
198
+ leverage: leverage,
199
+ equity_margin: equity_margin,
200
+ fo_margin: fo_margin,
201
+ commodity_margin: commodity_margin || commodity,
202
+ currency: currency,
203
+ hedge_benefit: hedge_benefit
204
+ }.compact
225
205
  end
226
206
  end
227
207
  end
@@ -43,6 +43,14 @@ module DhanHQ
43
43
  #
44
44
  class MarketFeed < BaseModel
45
45
  class << self
46
+ ##
47
+ # Returns the validation contract for MarketFeed requests.
48
+ #
49
+ # @return [Class] The MarketFeedContract class
50
+ def validation_contract
51
+ DhanHQ::Contracts::MarketFeedContract
52
+ end
53
+
46
54
  ##
47
55
  # Provides a shared instance of the MarketFeed resource.
48
56
  #
@@ -84,7 +92,8 @@ module DhanHQ
84
92
  # puts "Last Price: ₹#{data[:last_price]}"
85
93
  #
86
94
  def ltp(params)
87
- resource.ltp(params)
95
+ validated_params = validate_params!(params, validation_contract)
96
+ resource.ltp(validated_params)
88
97
  end
89
98
 
90
99
  ##
@@ -127,7 +136,8 @@ module DhanHQ
127
136
  # puts "LTP: ₹#{tcs_data[:last_price]}"
128
137
  #
129
138
  def ohlc(params)
130
- resource.ohlc(params)
139
+ validated_params = validate_params!(params, validation_contract)
140
+ resource.ohlc(validated_params)
131
141
  end
132
142
 
133
143
  ##
@@ -197,7 +207,8 @@ module DhanHQ
197
207
  # puts "Best Buy Quantity: #{buy_depth[0][:quantity]}"
198
208
  #
199
209
  def quote(params)
200
- resource.quote(params)
210
+ validated_params = validate_params!(params, validation_contract)
211
+ resource.quote(validated_params)
201
212
  end
202
213
  end
203
214
  end