DhanHQ 2.6.2 → 2.7.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -3
  3. data/AGENTS.md +23 -0
  4. data/ARCHITECTURE.md +115 -0
  5. data/CHANGELOG.md +65 -0
  6. data/README.md +55 -0
  7. data/docs/API_VERIFICATION.md +10 -8
  8. data/docs/ENDPOINTS_AND_SANDBOX.md +12 -0
  9. data/lib/DhanHQ/auth.rb +2 -2
  10. data/lib/DhanHQ/client.rb +42 -34
  11. data/lib/DhanHQ/concerns/order_audit.rb +69 -0
  12. data/lib/DhanHQ/configuration.rb +5 -6
  13. data/lib/DhanHQ/constants.rb +67 -7
  14. data/lib/DhanHQ/contracts/alert_order_contract.rb +23 -16
  15. data/lib/DhanHQ/contracts/expired_options_data_contract.rb +4 -2
  16. data/lib/DhanHQ/contracts/forever_order_contract.rb +55 -0
  17. data/lib/DhanHQ/contracts/historical_data_contract.rb +17 -19
  18. data/lib/DhanHQ/contracts/intraday_historical_data_contract.rb +12 -0
  19. data/lib/DhanHQ/contracts/margin_calculator_contract.rb +19 -17
  20. data/lib/DhanHQ/contracts/market_feed_contract.rb +42 -0
  21. data/lib/DhanHQ/contracts/multi_scrip_margin_calc_request_contract.rb +8 -5
  22. data/lib/DhanHQ/contracts/option_chain_contract.rb +17 -19
  23. data/lib/DhanHQ/contracts/pnl_based_exit_contract.rb +1 -1
  24. data/lib/DhanHQ/contracts/slice_order_contract.rb +10 -10
  25. data/lib/DhanHQ/core/auth_api.rb +1 -1
  26. data/lib/DhanHQ/core/base_api.rb +9 -9
  27. data/lib/DhanHQ/core/base_model.rb +4 -1
  28. data/lib/DhanHQ/core/error_handler.rb +2 -2
  29. data/lib/DhanHQ/errors.rb +16 -2
  30. data/lib/DhanHQ/helpers/request_helper.rb +11 -2
  31. data/lib/DhanHQ/helpers/response_helper.rb +48 -19
  32. data/lib/DhanHQ/helpers/validation_helper.rb +4 -2
  33. data/lib/DhanHQ/models/alert_order.rb +6 -2
  34. data/lib/DhanHQ/models/edis.rb +20 -13
  35. data/lib/DhanHQ/models/expired_options_data.rb +54 -44
  36. data/lib/DhanHQ/models/forever_order.rb +16 -7
  37. data/lib/DhanHQ/models/historical_data.rb +40 -6
  38. data/lib/DhanHQ/models/instrument_helpers.rb +2 -1
  39. data/lib/DhanHQ/models/margin.rb +62 -82
  40. data/lib/DhanHQ/models/market_feed.rb +14 -3
  41. data/lib/DhanHQ/models/option_chain.rb +50 -150
  42. data/lib/DhanHQ/models/order.rb +19 -4
  43. data/lib/DhanHQ/resources/alert_orders.rb +23 -1
  44. data/lib/DhanHQ/resources/edis.rb +4 -3
  45. data/lib/DhanHQ/resources/forever_orders.rb +10 -0
  46. data/lib/DhanHQ/resources/instruments.rb +3 -2
  47. data/lib/DhanHQ/resources/ip_setup.rb +4 -1
  48. data/lib/DhanHQ/resources/kill_switch.rb +7 -1
  49. data/lib/DhanHQ/resources/orders.rb +13 -1
  50. data/lib/DhanHQ/resources/pnl_exit.rb +8 -0
  51. data/lib/DhanHQ/resources/super_orders.rb +21 -2
  52. data/lib/DhanHQ/resources/trader_control.rb +13 -4
  53. data/lib/DhanHQ/utils/network_inspector.rb +71 -0
  54. data/lib/DhanHQ/version.rb +1 -1
  55. data/lib/DhanHQ/ws/base_connection.rb +1 -1
  56. data/lib/DhanHQ/ws/market_depth/client.rb +11 -4
  57. data/lib/dhan_hq.rb +17 -20
  58. data/lib/ta/indicators.rb +15 -18
  59. metadata +9 -9
  60. data/CODE_REVIEW_ISSUES.md +0 -397
  61. data/FIXES_APPLIED.md +0 -373
  62. data/RELEASING.md +0 -60
  63. data/REVIEW_SUMMARY.md +0 -120
  64. data/VERSION_UPDATE.md +0 -82
  65. data/diagram.md +0 -34
  66. data/docs/ARCHIVE_README.md +0 -784
@@ -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
@@ -28,12 +28,12 @@ module DhanHQ
28
28
  # expiry: "2024-10-31"
29
29
  # )
30
30
  # puts "Underlying LTP: ₹#{chain[:last_price]}"
31
- # nifty_25000 = chain[:oc]["25000.000000"]
32
- # puts "CE LTP: ₹#{nifty_25000['ce'][:last_price]}"
33
- # puts "CE OI: #{nifty_25000['ce'][:oi]}"
31
+ # nifty_first_strike = chain[:strikes].first
32
+ # puts "Strike: #{nifty_first_strike[:strike]}"
33
+ # puts "Call LTP: ₹#{nifty_first_strike[:call][:last_price]}"
34
34
  #
35
35
  # @example Fetch expiry list for an underlying
36
- # expiries = DhanHQ::Models::OptionChain.fetch_expiry_list(
36
+ # expiries = DhanHQ::Models::OptionChain.expiry_list(
37
37
  # underlying_scrip: 13,
38
38
  # underlying_seg: "IDX_I"
39
39
  # )
@@ -45,15 +45,14 @@ module DhanHQ
45
45
  # underlying_seg: "NSE_FNO",
46
46
  # expiry: "2024-12-26"
47
47
  # )
48
- # strike_data = chain[:oc]["25000.000000"]
49
- # ce_greeks = strike_data['ce'][:greeks]
48
+ # strike_data = chain[:strikes].find { |s| s[:strike] == 25000.0 }
49
+ # ce_greeks = strike_data[:call][:greeks]
50
50
  # puts "Delta: #{ce_greeks[:delta]}"
51
- # puts "Gamma: #{ce_greeks[:gamma]}"
52
- # puts "Theta: #{ce_greeks[:theta]}"
53
- # puts "Vega: #{ce_greeks[:vega]}"
54
51
  #
55
52
  class OptionChain < BaseModel
56
- attr_reader :underlying_scrip, :underlying_seg, :expiry, :last_price, :option_data
53
+ def validation_contract
54
+ self.class.validation_contract
55
+ end
57
56
 
58
57
  class << self
59
58
  ##
@@ -64,6 +63,10 @@ module DhanHQ
64
63
  @resource ||= DhanHQ::Resources::OptionChain.new
65
64
  end
66
65
 
66
+ def validation_contract
67
+ @validation_contract ||= DhanHQ::Contracts::OptionChainContract.new
68
+ end
69
+
67
70
  ##
68
71
  # Fetches the entire option chain for a specified underlying instrument and expiry.
69
72
  #
@@ -73,129 +76,36 @@ module DhanHQ
73
76
  # both Call (CE) and Put (PE) options at each strike price.
74
77
  #
75
78
  # @param params [Hash{Symbol => Integer, String}] Request parameters for option chain
76
- # @option params [Integer] :underlying_scrip (required) Security ID of the underlying
77
- # instrument. Can be found via the Instruments API.
78
- # @option params [String] :underlying_seg (required) Exchange and segment of underlying
79
- # for which data is to be fetched.
80
- # Valid values: "IDX_I" (Index), "NSE_FNO" (NSE F&O), "BSE_FNO" (BSE F&O), "MCX_FO" (MCX)
81
- # @option params [String] :expiry (required) Expiry date of the option contract for
82
- # which the option chain is requested. Must be in "YYYY-MM-DD" format.
83
- # List of active expiries can be fetched using {fetch_expiry_list}.
79
+ # @option params [Integer] :underlying_scrip (required) Security ID of the underlying instrument.
80
+ # @option params [String] :underlying_seg (required) Exchange and segment of underlying.
81
+ # @option params [String] :expiry (required) Expiry date in "YYYY-MM-DD" format.
84
82
  #
85
- # @return [HashWithIndifferentAccess] Filtered option chain data.
83
+ # @return [HashWithIndifferentAccess] Normalized option chain data.
86
84
  # Response structure:
87
85
  # - **:last_price** [Float] Last Traded Price (LTP) of the underlying instrument
88
- # - **:oc** [Hash{String => Hash}] Option chain data organized by strike price.
89
- # Strike prices are stored as string keys (e.g., "25000.000000").
90
- # Each strike contains:
91
- # - **"ce"** [Hash{Symbol => Float, Integer, Hash}] Call Option data for this strike:
92
- # - **:greeks** [Hash{Symbol => Float}] Option Greeks:
93
- # - **:delta** [Float] Measures the change of option's premium based on
94
- # every 1 rupee change in underlying
95
- # - **:theta** [Float] Measures how quickly an option's value decreases over time
96
- # - **:gamma** [Float] Rate of change in an option's delta in relation to the
97
- # price of the underlying asset
98
- # - **:vega** [Float] Measures the change of option's premium in response to
99
- # a 1% change in implied volatility
100
- # - **:implied_volatility** [Float] Value of expected volatility of a stock
101
- # over the life of the option
102
- # - **:last_price** [Float] Last Traded Price of the Call Option Instrument
103
- # - **:oi** [Integer] Open Interest of the Call Option Instrument
104
- # - **:previous_close_price** [Float] Previous day close price
105
- # - **:previous_oi** [Integer] Previous day Open Interest
106
- # - **:previous_volume** [Integer] Previous day volume
107
- # - **:top_ask_price** [Float] Current best ask price available
108
- # - **:top_ask_quantity** [Integer] Quantity available at current best ask price
109
- # - **:top_bid_price** [Float] Current best bid price available
110
- # - **:top_bid_quantity** [Integer] Quantity available at current best bid price
111
- # - **:volume** [Integer] Day volume for Call Option Instrument
112
- # - **"pe"** [Hash{Symbol => Float, Integer, Hash}] Put Option data for this strike.
113
- # Contains the same fields as "ce" (Call Option data).
114
- #
115
- # @note Strikes where both CE and PE have zero `last_price` are automatically filtered out.
116
- # This keeps the payload compact and focused on actively traded strikes.
117
- #
118
- # @example Fetch option chain for NIFTY index options
119
- # chain = DhanHQ::Models::OptionChain.fetch(
120
- # underlying_scrip: 13,
121
- # underlying_seg: "IDX_I",
122
- # expiry: "2024-10-31"
123
- # )
124
- # puts "NIFTY LTP: ₹#{chain[:last_price]}"
86
+ # - **:strikes** [Array<Hash>] Sorted array of strike data:
87
+ # - **:strike** [Float] The strike price
88
+ # - **:call** [Hash] Call Option (CE) data for this strike
89
+ # - **:put** [Hash] Put Option (PE) data for this strike
125
90
  #
126
- # @example Access Call and Put data for a specific strike
127
- # chain = DhanHQ::Models::OptionChain.fetch(
128
- # underlying_scrip: 13,
129
- # underlying_seg: "IDX_I",
130
- # expiry: "2024-10-31"
131
- # )
132
- # strike_25000 = chain[:oc]["25000.000000"]
133
- # ce_data = strike_25000["ce"]
134
- # pe_data = strike_25000["pe"]
135
- # puts "CE LTP: ₹#{ce_data[:last_price]}, OI: #{ce_data[:oi]}"
136
- # puts "PE LTP: ₹#{pe_data[:last_price]}, OI: #{pe_data[:oi]}"
137
- #
138
- # @example Calculate OI change and analyze Greeks
139
- # chain = DhanHQ::Models::OptionChain.fetch(
140
- # underlying_scrip: 1333,
141
- # underlying_seg: "NSE_FNO",
142
- # expiry: "2024-12-26"
143
- # )
144
- # strike_data = chain[:oc]["25000.000000"]
145
- # ce = strike_data["ce"]
146
- # oi_change = ce[:oi] - ce[:previous_oi]
147
- # puts "OI Change: #{oi_change}"
148
- # puts "Delta: #{ce[:greeks][:delta]}"
149
- # puts "IV: #{ce[:implied_volatility]}%"
150
- #
151
- # @raise [DhanHQ::ValidationError] If validation fails for any parameter or date format
91
+ # @raise [DhanHQ::ValidationError] If validation fails for any parameter
152
92
  def fetch(params)
153
93
  validate_params!(params, DhanHQ::Contracts::OptionChainContract)
154
94
 
155
95
  response = resource.fetch(params)
156
96
  return {}.with_indifferent_access unless response[:status] == "success"
157
97
 
158
- filter_valid_strikes(response[:data]).with_indifferent_access
98
+ normalize_chain(response[:data]).with_indifferent_access
159
99
  end
160
100
 
161
101
  ##
162
102
  # Fetches the list of active expiry dates for an underlying instrument.
163
103
  #
164
- # Retrieves all expiry dates for which option instruments are active for the given
165
- # underlying. This list is useful for selecting valid expiry dates when fetching
166
- # option chains.
167
- #
168
104
  # @param params [Hash{Symbol => Integer, String}] Request parameters for expiry list
169
- # @option params [Integer] :underlying_scrip (required) Security ID of the underlying
170
- # instrument. Can be found via the Instruments API.
171
- # @option params [String] :underlying_seg (required) Exchange and segment of underlying
172
- # for which expiry list is to be fetched.
173
- # Valid values: "IDX_I" (Index), "NSE_FNO" (NSE F&O), "BSE_FNO" (BSE F&O), "MCX_FO" (MCX)
105
+ # @option params [Integer] :underlying_scrip (required) Security ID of the underlying instrument.
106
+ # @option params [String] :underlying_seg (required) Exchange and segment of underlying.
174
107
  #
175
108
  # @return [Array<String>] Array of expiry dates in "YYYY-MM-DD" format.
176
- # Returns empty array if the API response status is not "success" or if no expiries are found.
177
- #
178
- # @example Fetch expiry list for NIFTY index
179
- # expiries = DhanHQ::Models::OptionChain.fetch_expiry_list(
180
- # underlying_scrip: 13,
181
- # underlying_seg: "IDX_I"
182
- # )
183
- # puts "Available expiries:"
184
- # expiries.each { |expiry| puts " #{expiry}" }
185
- #
186
- # @example Use expiry list to fetch option chains
187
- # expiries = DhanHQ::Models::OptionChain.fetch_expiry_list(
188
- # underlying_scrip: 1333,
189
- # underlying_seg: "NSE_FNO"
190
- # )
191
- # nearest_expiry = expiries.first
192
- # chain = DhanHQ::Models::OptionChain.fetch(
193
- # underlying_scrip: 1333,
194
- # underlying_seg: "NSE_FNO",
195
- # expiry: nearest_expiry
196
- # )
197
- #
198
- # @raise [DhanHQ::ValidationError] If validation fails for any parameter
199
109
  def fetch_expiry_list(params)
200
110
  validate_params!(params, DhanHQ::Contracts::OptionChainExpiryListContract)
201
111
 
@@ -203,51 +113,41 @@ module DhanHQ
203
113
  response[:status] == "success" ? response[:data] : []
204
114
  end
205
115
 
116
+ alias expiry_list fetch_expiry_list
117
+
206
118
  private
207
119
 
208
120
  ##
209
- # Filters valid strikes where at least one of CE or PE has a non-zero last_price.
210
- #
211
- # Removes strikes from the option chain where both Call (CE) and Put (PE) options
212
- # have zero `last_price`, keeping only actively traded strikes. This keeps the
213
- # payload compact and focused on relevant data.
214
- #
215
- # @param data [Hash] The API response data containing option chain information
216
- # @return [Hash] The filtered option chain data with original strike price keys preserved
217
- #
218
- # @api private
219
- def filter_valid_strikes(data)
121
+ # Normalizes the raw API option chain data.
122
+ # - Converts strike keys to numeric
123
+ # - Renames 'ce' to 'call' and 'pe' to 'put'
124
+ # - Filters strikes with zero prices
125
+ # - Sorts strikes ascending
126
+ #
127
+ # @param data [Hash] The raw API response data
128
+ # @return [Hash] Normalized option chain
129
+ def normalize_chain(data)
220
130
  return {} unless data.is_a?(Hash) && data.key?(:oc)
221
131
 
222
- filtered_oc = data[:oc].each_with_object({}) do |(strike_price, strike_data), result|
223
- ce_last_price = strike_data.dig("ce", "last_price").to_f
224
- pe_last_price = strike_data.dig("pe", "last_price").to_f
132
+ strikes = data[:oc].filter_map do |strike_price, strike_data|
133
+ ce = strike_data["ce"] || strike_data[:ce]
134
+ pe = strike_data["pe"] || strike_data[:pe]
225
135
 
226
- # Only keep strikes where at least one of CE or PE has a valid last_price
227
- result[strike_price] = strike_data if ce_last_price.positive? || pe_last_price.positive?
228
- end
136
+ next if ce["last_price"].to_f.zero? && pe["last_price"].to_f.zero?
229
137
 
230
- data.merge(oc: filtered_oc)
231
- end
138
+ {
139
+ strike: strike_price.to_f,
140
+ call: ce,
141
+ put: pe
142
+ }
143
+ end
232
144
 
233
- # Validation contract for option chain
234
- #
235
- # @return [DhanHQ::Contracts::OptionChainContract]
236
- # @api private
237
- def validation_contract
238
- DhanHQ::Contracts::OptionChainContract.new
145
+ {
146
+ last_price: data[:last_price],
147
+ strikes: strikes.sort_by { |s| s[:strike] }
148
+ }
239
149
  end
240
150
  end
241
-
242
- private
243
-
244
- # Validation contract for option chain
245
- #
246
- # @return [DhanHQ::Contracts::OptionChainContract]
247
- # @api private
248
- def validation_contract
249
- DhanHQ::Contracts::OptionChainContract.new
250
- end
251
151
  end
252
152
  end
253
153
  end
@@ -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
@@ -1,11 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../concerns/order_audit"
4
+
3
5
  module DhanHQ
4
6
  module Resources
5
7
  # Resource for alert/conditional orders per API docs: /alerts/orders (GET/POST/PUT/DELETE).
6
8
  class AlertOrders < BaseResource
9
+ include DhanHQ::Concerns::OrderAudit
10
+
7
11
  API_TYPE = :order_api
8
- HTTP_PATH = "/alerts/orders"
12
+ HTTP_PATH = "/v2/alerts/orders"
13
+
14
+ def create(params)
15
+ ensure_live_trading!
16
+ log_order_context("DHAN_ALERT_ORDER_ATTEMPT", params)
17
+ super
18
+ end
19
+
20
+ def update(id, params)
21
+ ensure_live_trading!
22
+ log_order_context("DHAN_ALERT_ORDER_MODIFY_ATTEMPT", params.merge(alert_id: id))
23
+ super
24
+ end
25
+
26
+ def delete(id)
27
+ ensure_live_trading!
28
+ log_order_context("DHAN_ALERT_ORDER_DELETE_ATTEMPT", { order_id: id })
29
+ super
30
+ end
9
31
  end
10
32
  end
11
33
  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)