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.
- checksums.yaml +4 -4
- data/.rubocop.yml +15 -3
- data/ARCHITECTURE.md +113 -0
- data/CHANGELOG.md +31 -0
- data/README.md +2 -0
- data/docs/API_VERIFICATION.md +10 -8
- data/docs/ENDPOINTS_AND_SANDBOX.md +12 -0
- data/lib/DhanHQ/auth.rb +2 -2
- data/lib/DhanHQ/client.rb +42 -34
- data/lib/DhanHQ/configuration.rb +5 -6
- data/lib/DhanHQ/constants.rb +67 -7
- data/lib/DhanHQ/contracts/alert_order_contract.rb +23 -16
- data/lib/DhanHQ/contracts/expired_options_data_contract.rb +4 -2
- data/lib/DhanHQ/contracts/forever_order_contract.rb +55 -0
- data/lib/DhanHQ/contracts/historical_data_contract.rb +17 -19
- data/lib/DhanHQ/contracts/intraday_historical_data_contract.rb +12 -0
- data/lib/DhanHQ/contracts/margin_calculator_contract.rb +19 -17
- data/lib/DhanHQ/contracts/market_feed_contract.rb +42 -0
- data/lib/DhanHQ/contracts/multi_scrip_margin_calc_request_contract.rb +8 -5
- data/lib/DhanHQ/contracts/option_chain_contract.rb +17 -19
- data/lib/DhanHQ/contracts/pnl_based_exit_contract.rb +1 -1
- data/lib/DhanHQ/contracts/slice_order_contract.rb +10 -10
- data/lib/DhanHQ/core/auth_api.rb +1 -1
- data/lib/DhanHQ/core/base_api.rb +9 -9
- data/lib/DhanHQ/core/base_model.rb +4 -1
- data/lib/DhanHQ/core/error_handler.rb +2 -2
- data/lib/DhanHQ/errors.rb +14 -2
- data/lib/DhanHQ/helpers/request_helper.rb +11 -2
- data/lib/DhanHQ/helpers/response_helper.rb +48 -19
- data/lib/DhanHQ/helpers/validation_helper.rb +4 -2
- data/lib/DhanHQ/models/alert_order.rb +6 -2
- data/lib/DhanHQ/models/edis.rb +20 -13
- data/lib/DhanHQ/models/expired_options_data.rb +54 -44
- data/lib/DhanHQ/models/forever_order.rb +16 -7
- data/lib/DhanHQ/models/historical_data.rb +40 -6
- data/lib/DhanHQ/models/instrument_helpers.rb +2 -1
- data/lib/DhanHQ/models/margin.rb +62 -82
- data/lib/DhanHQ/models/market_feed.rb +14 -3
- data/lib/DhanHQ/models/option_chain.rb +50 -150
- data/lib/DhanHQ/models/order.rb +19 -4
- data/lib/DhanHQ/resources/alert_orders.rb +1 -1
- data/lib/DhanHQ/resources/edis.rb +4 -3
- data/lib/DhanHQ/resources/instruments.rb +3 -2
- data/lib/DhanHQ/resources/ip_setup.rb +4 -1
- data/lib/DhanHQ/resources/kill_switch.rb +7 -1
- data/lib/DhanHQ/resources/orders.rb +1 -1
- data/lib/DhanHQ/resources/super_orders.rb +8 -2
- data/lib/DhanHQ/resources/trader_control.rb +13 -4
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/base_connection.rb +1 -1
- data/lib/DhanHQ/ws/market_depth/client.rb +11 -4
- data/lib/dhan_hq.rb +17 -20
- data/lib/ta/indicators.rb +15 -18
- metadata +6 -9
- data/CODE_REVIEW_ISSUES.md +0 -397
- data/FIXES_APPLIED.md +0 -373
- data/RELEASING.md +0 -60
- data/REVIEW_SUMMARY.md +0 -120
- data/VERSION_UPDATE.md +0 -82
- data/diagram.md +0 -34
- data/docs/ARCHIVE_README.md +0 -784
|
@@ -28,12 +28,12 @@ module DhanHQ
|
|
|
28
28
|
# expiry: "2024-10-31"
|
|
29
29
|
# )
|
|
30
30
|
# puts "Underlying LTP: ₹#{chain[:last_price]}"
|
|
31
|
-
#
|
|
32
|
-
# puts "
|
|
33
|
-
# puts "
|
|
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.
|
|
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[:
|
|
49
|
-
# ce_greeks = strike_data[
|
|
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
|
-
|
|
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
|
-
#
|
|
78
|
-
# @option params [String] :
|
|
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]
|
|
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
|
-
# - **:
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
# - **
|
|
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
|
-
# @
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
#
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
# @param data [Hash] The API response data
|
|
216
|
-
# @return [Hash]
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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
|
-
|
|
231
|
-
|
|
138
|
+
{
|
|
139
|
+
strike: strike_price.to_f,
|
|
140
|
+
call: ce,
|
|
141
|
+
put: pe
|
|
142
|
+
}
|
|
143
|
+
end
|
|
232
144
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
data/lib/DhanHQ/models/order.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
20
|
+
raise DhanHQ::Error, MSG
|
|
12
21
|
end
|
|
13
22
|
|
|
14
23
|
def enable
|
|
15
|
-
|
|
24
|
+
raise DhanHQ::Error, MSG
|
|
16
25
|
end
|
|
17
26
|
|
|
18
27
|
def disable
|
|
19
|
-
|
|
28
|
+
raise DhanHQ::Error, MSG
|
|
20
29
|
end
|
|
21
30
|
end
|
|
22
31
|
end
|
data/lib/DhanHQ/version.rb
CHANGED
|
@@ -87,7 +87,7 @@ module DhanHQ
|
|
|
87
87
|
depth_level = config.market_depth_level || 20 # Default to 20 level depth
|
|
88
88
|
|
|
89
89
|
base = if depth_level == 200
|
|
90
|
-
|
|
90
|
+
Constants::Urls::WS_DEPTH_200
|
|
91
91
|
else
|
|
92
92
|
config.ws_market_depth_url
|
|
93
93
|
end
|
|
@@ -164,7 +164,10 @@ module DhanHQ
|
|
|
164
164
|
|
|
165
165
|
send_message(subscription_message)
|
|
166
166
|
@subscriptions[label] = resolution
|
|
167
|
-
DhanHQ.logger&.info(
|
|
167
|
+
DhanHQ.logger&.info(
|
|
168
|
+
"[DhanHQ::WS::MarketDepth] Subscribed to #{resolution[:original_label]} " \
|
|
169
|
+
"(#{resolution[:exchange_segment]}:#{resolution[:security_id]})"
|
|
170
|
+
)
|
|
168
171
|
rescue StandardError => e
|
|
169
172
|
DhanHQ.logger&.error("[DhanHQ::WS::MarketDepth] Subscription error for #{symbol.inspect}: #{e.class} #{e.message}")
|
|
170
173
|
end
|
|
@@ -192,7 +195,10 @@ module DhanHQ
|
|
|
192
195
|
|
|
193
196
|
send_message(unsubscribe_message)
|
|
194
197
|
@subscriptions.delete(label)
|
|
195
|
-
DhanHQ.logger&.info(
|
|
198
|
+
DhanHQ.logger&.info(
|
|
199
|
+
"[DhanHQ::WS::MarketDepth] Unsubscribed from #{security_data[:original_label]} " \
|
|
200
|
+
"(#{security_data[:exchange_segment]}:#{security_data[:security_id]})"
|
|
201
|
+
)
|
|
196
202
|
rescue StandardError => e
|
|
197
203
|
DhanHQ.logger&.error("[DhanHQ::WS::MarketDepth] Unsubscribe error for #{symbol.inspect}: #{e.class} #{e.message}")
|
|
198
204
|
end
|
|
@@ -217,7 +223,8 @@ module DhanHQ
|
|
|
217
223
|
instrument = find_instrument(symbol_code, segment_hint)
|
|
218
224
|
unless instrument
|
|
219
225
|
DhanHQ.logger&.warn(
|
|
220
|
-
"[DhanHQ::WS::MarketDepth] Unable to locate instrument for #{symbol_code}
|
|
226
|
+
"[DhanHQ::WS::MarketDepth] Unable to locate instrument for #{symbol_code} " \
|
|
227
|
+
"(segment hint: #{segment_hint || "AUTO"})"
|
|
221
228
|
)
|
|
222
229
|
return nil
|
|
223
230
|
end
|
data/lib/dhan_hq.rb
CHANGED
|
@@ -12,8 +12,9 @@ require_relative "DhanHQ/helpers/api_helper"
|
|
|
12
12
|
require_relative "DhanHQ/helpers/attribute_helper"
|
|
13
13
|
require_relative "DhanHQ/helpers/validation_helper"
|
|
14
14
|
require_relative "DhanHQ/helpers/request_helper"
|
|
15
|
-
require_relative "DhanHQ/helpers/response_helper"
|
|
16
15
|
require_relative "DhanHQ/errors"
|
|
16
|
+
require_relative "DhanHQ/version"
|
|
17
|
+
require_relative "DhanHQ/helpers/response_helper"
|
|
17
18
|
require_relative "DhanHQ/core/base_api"
|
|
18
19
|
require_relative "DhanHQ/core/base_model"
|
|
19
20
|
require_relative "DhanHQ/core/base_resource"
|
|
@@ -49,7 +50,7 @@ module DhanHQ
|
|
|
49
50
|
# Default REST API host used when no custom base URL is provided.
|
|
50
51
|
#
|
|
51
52
|
# @return [String]
|
|
52
|
-
BASE_URL =
|
|
53
|
+
BASE_URL = Constants::Urls::REST_API_BASE
|
|
53
54
|
# The current configuration instance.
|
|
54
55
|
#
|
|
55
56
|
# @return [DhanHQ::Configuration, nil] The current configuration or `nil` if not set.
|
|
@@ -142,28 +143,12 @@ module DhanHQ
|
|
|
142
143
|
end
|
|
143
144
|
|
|
144
145
|
unless response.success?
|
|
145
|
-
body =
|
|
146
|
-
response.body
|
|
147
|
-
else
|
|
148
|
-
begin
|
|
149
|
-
JSON.parse(response.body.to_s)
|
|
150
|
-
rescue StandardError
|
|
151
|
-
{}
|
|
152
|
-
end
|
|
153
|
-
end
|
|
146
|
+
body = parse_json_body(response.body)
|
|
154
147
|
msg = body["error"] || body["message"] || body["errorMessage"] || response.body.to_s
|
|
155
148
|
raise DhanHQ::TokenEndpointError, "Token endpoint returned #{response.status}: #{msg}"
|
|
156
149
|
end
|
|
157
150
|
|
|
158
|
-
data =
|
|
159
|
-
response.body
|
|
160
|
-
else
|
|
161
|
-
begin
|
|
162
|
-
JSON.parse(response.body.to_s)
|
|
163
|
-
rescue StandardError
|
|
164
|
-
{}
|
|
165
|
-
end
|
|
166
|
-
end
|
|
151
|
+
data = parse_json_body(response.body)
|
|
167
152
|
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
168
153
|
|
|
169
154
|
access_token = data["access_token"] || data[:access_token]
|
|
@@ -177,5 +162,17 @@ module DhanHQ
|
|
|
177
162
|
configuration.base_url = dhan_base.to_s if dhan_base.to_s != ""
|
|
178
163
|
configuration
|
|
179
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
|
|
180
177
|
end
|
|
181
178
|
end
|