DhanHQ 2.1.5 → 2.1.7
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/CHANGELOG.md +21 -0
- data/GUIDE.md +221 -31
- data/README.md +453 -126
- data/README1.md +293 -30
- data/docs/live_order_updates.md +319 -0
- data/docs/rails_websocket_integration.md +847 -0
- data/docs/standalone_ruby_websocket_integration.md +1588 -0
- data/docs/websocket_integration.md +918 -0
- data/examples/comprehensive_websocket_examples.rb +148 -0
- data/examples/instrument_finder_test.rb +195 -0
- data/examples/live_order_updates.rb +118 -0
- data/examples/market_depth_example.rb +144 -0
- data/examples/market_feed_example.rb +81 -0
- data/examples/order_update_example.rb +105 -0
- data/examples/trading_fields_example.rb +215 -0
- data/lib/DhanHQ/configuration.rb +16 -1
- data/lib/DhanHQ/constants.rb +16 -0
- data/lib/DhanHQ/contracts/expired_options_data_contract.rb +103 -0
- data/lib/DhanHQ/contracts/trade_contract.rb +70 -0
- data/lib/DhanHQ/errors.rb +2 -0
- data/lib/DhanHQ/models/expired_options_data.rb +331 -0
- data/lib/DhanHQ/models/instrument.rb +114 -4
- data/lib/DhanHQ/models/instrument_helpers.rb +141 -0
- data/lib/DhanHQ/models/order_update.rb +235 -0
- data/lib/DhanHQ/models/trade.rb +118 -31
- data/lib/DhanHQ/resources/expired_options_data.rb +22 -0
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/base_connection.rb +249 -0
- data/lib/DhanHQ/ws/connection.rb +2 -2
- data/lib/DhanHQ/ws/decoder.rb +3 -3
- data/lib/DhanHQ/ws/market_depth/client.rb +376 -0
- data/lib/DhanHQ/ws/market_depth/decoder.rb +131 -0
- data/lib/DhanHQ/ws/market_depth.rb +74 -0
- data/lib/DhanHQ/ws/orders/client.rb +175 -11
- data/lib/DhanHQ/ws/orders/connection.rb +40 -81
- data/lib/DhanHQ/ws/orders.rb +28 -0
- data/lib/DhanHQ/ws/segments.rb +18 -2
- data/lib/DhanHQ/ws.rb +3 -2
- data/lib/dhan_hq.rb +5 -0
- metadata +36 -1
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Models
|
|
5
|
+
##
|
|
6
|
+
# Represents expired options data for rolling contracts
|
|
7
|
+
# Provides access to OHLC, volume, open interest, implied volatility, and spot data
|
|
8
|
+
# rubocop:disable Metrics/ClassLength
|
|
9
|
+
class ExpiredOptionsData < BaseModel
|
|
10
|
+
# All expired options data attributes
|
|
11
|
+
attributes :exchange_segment, :interval, :security_id, :instrument,
|
|
12
|
+
:expiry_flag, :expiry_code, :strike, :drv_option_type,
|
|
13
|
+
:required_data, :from_date, :to_date, :data
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
##
|
|
17
|
+
# Fetch expired options data for rolling contracts
|
|
18
|
+
# POST /charts/rollingoption
|
|
19
|
+
#
|
|
20
|
+
# @param params [Hash] Parameters for the request
|
|
21
|
+
# @option params [String] :exchange_segment Exchange segment (e.g., "NSE_FNO")
|
|
22
|
+
# @option params [Integer] :interval Minute interval (1, 5, 15, 25, 60)
|
|
23
|
+
# @option params [String] :security_id Security ID for the underlying
|
|
24
|
+
# @option params [String] :instrument Instrument type ("OPTIDX" or "OPTSTK")
|
|
25
|
+
# @option params [String] :expiry_flag Expiry interval ("WEEK" or "MONTH")
|
|
26
|
+
# @option params [Integer] :expiry_code Expiry code
|
|
27
|
+
# @option params [String] :strike Strike price ("ATM", "ATM+1", "ATM-1", etc.)
|
|
28
|
+
# @option params [String] :drv_option_type Option type ("CALL" or "PUT")
|
|
29
|
+
# @option params [Array<String>] :required_data Required data fields
|
|
30
|
+
# @option params [String] :from_date Start date (YYYY-MM-DD)
|
|
31
|
+
# @option params [String] :to_date End date (YYYY-MM-DD)
|
|
32
|
+
# @return [ExpiredOptionsData] Expired options data object
|
|
33
|
+
def fetch(params)
|
|
34
|
+
validate_params(params)
|
|
35
|
+
|
|
36
|
+
response = expired_options_resource.fetch(params)
|
|
37
|
+
new(response.merge(params), skip_validation: true)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def expired_options_resource
|
|
43
|
+
@expired_options_resource ||= DhanHQ::Resources::ExpiredOptionsData.new
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def validate_params(params)
|
|
47
|
+
contract = DhanHQ::Contracts::ExpiredOptionsDataContract.new
|
|
48
|
+
validation_result = contract.call(params)
|
|
49
|
+
|
|
50
|
+
return if validation_result.success?
|
|
51
|
+
|
|
52
|
+
raise DhanHQ::ValidationError, "Invalid parameters: #{validation_result.errors.to_h}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
# ExpiredOptionsData objects are read-only, so no validation contract needed
|
|
58
|
+
def validation_contract
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
##
|
|
63
|
+
# Get call option data
|
|
64
|
+
# @return [Hash, nil] Call option data or nil if not available
|
|
65
|
+
def call_data
|
|
66
|
+
return nil unless data.is_a?(Hash)
|
|
67
|
+
|
|
68
|
+
data["ce"] || data[:ce]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
##
|
|
72
|
+
# Get put option data
|
|
73
|
+
# @return [Hash, nil] Put option data or nil if not available
|
|
74
|
+
def put_data
|
|
75
|
+
return nil unless data.is_a?(Hash)
|
|
76
|
+
|
|
77
|
+
data["pe"] || data[:pe]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
##
|
|
81
|
+
# Get data for the specified option type
|
|
82
|
+
# @param option_type [String] "CALL" or "PUT"
|
|
83
|
+
# @return [Hash, nil] Option data or nil if not available
|
|
84
|
+
def data_for_type(option_type)
|
|
85
|
+
case option_type.upcase
|
|
86
|
+
when "CALL"
|
|
87
|
+
call_data
|
|
88
|
+
when "PUT"
|
|
89
|
+
put_data
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
##
|
|
94
|
+
# Get OHLC data for the specified option type
|
|
95
|
+
# @param option_type [String] "CALL" or "PUT"
|
|
96
|
+
# @return [Hash] OHLC data with open, high, low, close arrays
|
|
97
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
98
|
+
def ohlc_data(option_type = nil)
|
|
99
|
+
option_type ||= drv_option_type
|
|
100
|
+
option_data = data_for_type(option_type)
|
|
101
|
+
return {} unless option_data
|
|
102
|
+
|
|
103
|
+
{
|
|
104
|
+
open: option_data["open"] || option_data[:open] || [],
|
|
105
|
+
high: option_data["high"] || option_data[:high] || [],
|
|
106
|
+
low: option_data["low"] || option_data[:low] || [],
|
|
107
|
+
close: option_data["close"] || option_data[:close] || []
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
111
|
+
|
|
112
|
+
##
|
|
113
|
+
# Get volume data for the specified option type
|
|
114
|
+
# @param option_type [String] "CALL" or "PUT"
|
|
115
|
+
# @return [Array<Integer>] Volume data array
|
|
116
|
+
def volume_data(option_type = nil)
|
|
117
|
+
option_type ||= drv_option_type
|
|
118
|
+
option_data = data_for_type(option_type)
|
|
119
|
+
return [] unless option_data
|
|
120
|
+
|
|
121
|
+
option_data["volume"] || option_data[:volume] || []
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
##
|
|
125
|
+
# Get open interest data for the specified option type
|
|
126
|
+
# @param option_type [String] "CALL" or "PUT"
|
|
127
|
+
# @return [Array<Float>] Open interest data array
|
|
128
|
+
def open_interest_data(option_type = nil)
|
|
129
|
+
option_type ||= drv_option_type
|
|
130
|
+
option_data = data_for_type(option_type)
|
|
131
|
+
return [] unless option_data
|
|
132
|
+
|
|
133
|
+
option_data["oi"] || option_data[:oi] || []
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
##
|
|
137
|
+
# Get implied volatility data for the specified option type
|
|
138
|
+
# @param option_type [String] "CALL" or "PUT"
|
|
139
|
+
# @return [Array<Float>] Implied volatility data array
|
|
140
|
+
def implied_volatility_data(option_type = nil)
|
|
141
|
+
option_type ||= drv_option_type
|
|
142
|
+
option_data = data_for_type(option_type)
|
|
143
|
+
return [] unless option_data
|
|
144
|
+
|
|
145
|
+
option_data["iv"] || option_data[:iv] || []
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
##
|
|
149
|
+
# Get strike price data for the specified option type
|
|
150
|
+
# @param option_type [String] "CALL" or "PUT"
|
|
151
|
+
# @return [Array<Float>] Strike price data array
|
|
152
|
+
def strike_data(option_type = nil)
|
|
153
|
+
option_type ||= drv_option_type
|
|
154
|
+
option_data = data_for_type(option_type)
|
|
155
|
+
return [] unless option_data
|
|
156
|
+
|
|
157
|
+
option_data["strike"] || option_data[:strike] || []
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
##
|
|
161
|
+
# Get spot price data for the specified option type
|
|
162
|
+
# @param option_type [String] "CALL" or "PUT"
|
|
163
|
+
# @return [Array<Float>] Spot price data array
|
|
164
|
+
def spot_data(option_type = nil)
|
|
165
|
+
option_type ||= drv_option_type
|
|
166
|
+
option_data = data_for_type(option_type)
|
|
167
|
+
return [] unless option_data
|
|
168
|
+
|
|
169
|
+
option_data["spot"] || option_data[:spot] || []
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
##
|
|
173
|
+
# Get timestamp data for the specified option type
|
|
174
|
+
# @param option_type [String] "CALL" or "PUT"
|
|
175
|
+
# @return [Array<Integer>] Timestamp data array (epoch)
|
|
176
|
+
def timestamp_data(option_type = nil)
|
|
177
|
+
option_type ||= drv_option_type
|
|
178
|
+
option_data = data_for_type(option_type)
|
|
179
|
+
return [] unless option_data
|
|
180
|
+
|
|
181
|
+
option_data["timestamp"] || option_data[:timestamp] || []
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
##
|
|
185
|
+
# Get data points count for the specified option type
|
|
186
|
+
# @param option_type [String] "CALL" or "PUT"
|
|
187
|
+
# @return [Integer] Number of data points
|
|
188
|
+
def data_points_count(option_type = nil)
|
|
189
|
+
timestamps = timestamp_data(option_type)
|
|
190
|
+
timestamps.size
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
##
|
|
194
|
+
# Get average volume for the specified option type
|
|
195
|
+
# @param option_type [String] "CALL" or "PUT"
|
|
196
|
+
# @return [Float] Average volume
|
|
197
|
+
def average_volume(option_type = nil)
|
|
198
|
+
volumes = volume_data(option_type)
|
|
199
|
+
return 0.0 if volumes.empty?
|
|
200
|
+
|
|
201
|
+
volumes.sum.to_f / volumes.size
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
##
|
|
205
|
+
# Get average open interest for the specified option type
|
|
206
|
+
# @param option_type [String] "CALL" or "PUT"
|
|
207
|
+
# @return [Float] Average open interest
|
|
208
|
+
def average_open_interest(option_type = nil)
|
|
209
|
+
oi_data = open_interest_data(option_type)
|
|
210
|
+
return 0.0 if oi_data.empty?
|
|
211
|
+
|
|
212
|
+
oi_data.sum.to_f / oi_data.size
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
##
|
|
216
|
+
# Get average implied volatility for the specified option type
|
|
217
|
+
# @param option_type [String] "CALL" or "PUT"
|
|
218
|
+
# @return [Float] Average implied volatility
|
|
219
|
+
def average_implied_volatility(option_type = nil)
|
|
220
|
+
iv_data = implied_volatility_data(option_type)
|
|
221
|
+
return 0.0 if iv_data.empty?
|
|
222
|
+
|
|
223
|
+
iv_data.sum.to_f / iv_data.size
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
##
|
|
227
|
+
# Get price range (high - low) for the specified option type
|
|
228
|
+
# @param option_type [String] "CALL" or "PUT"
|
|
229
|
+
# @return [Array<Float>] Price range for each data point
|
|
230
|
+
def price_ranges(option_type = nil)
|
|
231
|
+
ohlc = ohlc_data(option_type)
|
|
232
|
+
highs = ohlc[:high]
|
|
233
|
+
lows = ohlc[:low]
|
|
234
|
+
|
|
235
|
+
return [] if highs.empty? || lows.empty?
|
|
236
|
+
|
|
237
|
+
highs.zip(lows).map { |high, low| high - low }
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
##
|
|
241
|
+
# Get summary statistics for the specified option type
|
|
242
|
+
# @param option_type [String] "CALL" or "PUT"
|
|
243
|
+
# @return [Hash] Summary statistics
|
|
244
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
245
|
+
def summary_stats(option_type = nil)
|
|
246
|
+
option_type ||= drv_option_type
|
|
247
|
+
ohlc = ohlc_data(option_type)
|
|
248
|
+
volumes = volume_data(option_type)
|
|
249
|
+
oi_data = open_interest_data(option_type)
|
|
250
|
+
iv_data = implied_volatility_data(option_type)
|
|
251
|
+
|
|
252
|
+
{
|
|
253
|
+
data_points: data_points_count(option_type),
|
|
254
|
+
avg_volume: average_volume(option_type),
|
|
255
|
+
avg_open_interest: average_open_interest(option_type),
|
|
256
|
+
avg_implied_volatility: average_implied_volatility(option_type),
|
|
257
|
+
price_ranges: price_ranges(option_type),
|
|
258
|
+
has_ohlc: !ohlc[:open].empty?,
|
|
259
|
+
has_volume: !volumes.empty?,
|
|
260
|
+
has_open_interest: !oi_data.empty?,
|
|
261
|
+
has_implied_volatility: !iv_data.empty?
|
|
262
|
+
}
|
|
263
|
+
end
|
|
264
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
265
|
+
|
|
266
|
+
##
|
|
267
|
+
# Check if this is index options data
|
|
268
|
+
# @return [Boolean] true if instrument is OPTIDX
|
|
269
|
+
def index_options?
|
|
270
|
+
instrument == "OPTIDX"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
##
|
|
274
|
+
# Check if this is stock options data
|
|
275
|
+
# @return [Boolean] true if instrument is OPTSTK
|
|
276
|
+
def stock_options?
|
|
277
|
+
instrument == "OPTSTK"
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
##
|
|
281
|
+
# Check if this is weekly expiry
|
|
282
|
+
# @return [Boolean] true if expiry_flag is WEEK
|
|
283
|
+
def weekly_expiry?
|
|
284
|
+
expiry_flag == "WEEK"
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
##
|
|
288
|
+
# Check if this is monthly expiry
|
|
289
|
+
# @return [Boolean] true if expiry_flag is MONTH
|
|
290
|
+
def monthly_expiry?
|
|
291
|
+
expiry_flag == "MONTH"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
##
|
|
295
|
+
# Check if this is call option data
|
|
296
|
+
# @return [Boolean] true if drv_option_type is CALL
|
|
297
|
+
def call_option?
|
|
298
|
+
drv_option_type == "CALL"
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
##
|
|
302
|
+
# Check if this is put option data
|
|
303
|
+
# @return [Boolean] true if drv_option_type is PUT
|
|
304
|
+
def put_option?
|
|
305
|
+
drv_option_type == "PUT"
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
##
|
|
309
|
+
# Check if strike is at the money
|
|
310
|
+
# @return [Boolean] true if strike is ATM
|
|
311
|
+
def at_the_money?
|
|
312
|
+
strike == "ATM"
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
##
|
|
316
|
+
# Get strike offset from ATM
|
|
317
|
+
# @return [Integer] Strike offset (0 for ATM, positive for ATM+X, negative for ATM-X)
|
|
318
|
+
def strike_offset
|
|
319
|
+
return 0 if at_the_money?
|
|
320
|
+
|
|
321
|
+
match = strike.match(/\AATM(\+|-)?(\d+)\z/)
|
|
322
|
+
return 0 unless match
|
|
323
|
+
|
|
324
|
+
sign = match[1] == "-" ? -1 : 1
|
|
325
|
+
offset = match[2].to_i
|
|
326
|
+
sign * offset
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
# rubocop:enable Metrics/ClassLength
|
|
330
|
+
end
|
|
331
|
+
end
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../contracts/instrument_list_contract"
|
|
4
|
+
require_relative "instrument_helpers"
|
|
4
5
|
|
|
5
6
|
module DhanHQ
|
|
6
7
|
module Models
|
|
7
8
|
# Model wrapper for fetching instruments by exchange segment.
|
|
8
9
|
class Instrument < BaseModel
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
include InstrumentHelpers
|
|
11
|
+
|
|
12
|
+
attributes :security_id, :symbol_name, :display_name, :exchange, :segment, :exchange_segment, :instrument, :series,
|
|
13
|
+
:lot_size, :tick_size, :expiry_date, :strike_price, :option_type, :underlying_symbol,
|
|
14
|
+
:isin, :instrument_type, :expiry_flag, :bracket_flag, :cover_flag, :asm_gsm_flag,
|
|
15
|
+
:asm_gsm_category, :buy_sell_indicator, :buy_co_min_margin_per, :sell_co_min_margin_per,
|
|
16
|
+
:mtf_leverage
|
|
11
17
|
|
|
12
18
|
class << self
|
|
13
19
|
# @return [DhanHQ::Resources::Instruments]
|
|
@@ -29,19 +35,123 @@ module DhanHQ
|
|
|
29
35
|
rows.map { |r| new(normalize_csv_row(r), skip_validation: true) }
|
|
30
36
|
end
|
|
31
37
|
|
|
38
|
+
# Find a specific instrument by exchange segment and symbol.
|
|
39
|
+
# @param exchange_segment [String] The exchange segment (e.g., "NSE_EQ", "IDX_I")
|
|
40
|
+
# @param symbol [String] The symbol name to search for
|
|
41
|
+
# @param options [Hash] Additional search options
|
|
42
|
+
# @option options [Boolean] :exact_match Whether to perform exact symbol matching (default: false)
|
|
43
|
+
# @option options [Boolean] :case_sensitive Whether the search should be case sensitive (default: false)
|
|
44
|
+
# @return [Instrument, nil] The found instrument or nil if not found
|
|
45
|
+
# @example
|
|
46
|
+
# # Find RELIANCE in NSE_EQ (uses underlying_symbol for equity)
|
|
47
|
+
# instrument = DhanHQ::Models::Instrument.find("NSE_EQ", "RELIANCE")
|
|
48
|
+
# puts instrument.security_id # => "2885"
|
|
49
|
+
#
|
|
50
|
+
# # Find NIFTY in IDX_I (uses symbol_name for indices)
|
|
51
|
+
# instrument = DhanHQ::Models::Instrument.find("IDX_I", "NIFTY")
|
|
52
|
+
# puts instrument.security_id # => "13"
|
|
53
|
+
#
|
|
54
|
+
# # Exact match search
|
|
55
|
+
# instrument = DhanHQ::Models::Instrument.find("NSE_EQ", "RELIANCE", exact_match: true)
|
|
56
|
+
#
|
|
57
|
+
# # Case sensitive search
|
|
58
|
+
# instrument = DhanHQ::Models::Instrument.find("NSE_EQ", "reliance", case_sensitive: true)
|
|
59
|
+
def find(exchange_segment, symbol, options = { exact_match: true, case_sensitive: false })
|
|
60
|
+
validate_params!({ exchange_segment: exchange_segment, symbol: symbol }, DhanHQ::Contracts::InstrumentListContract)
|
|
61
|
+
|
|
62
|
+
exact_match = options[:exact_match] || false
|
|
63
|
+
case_sensitive = options[:case_sensitive] || false
|
|
64
|
+
|
|
65
|
+
instruments = by_segment(exchange_segment)
|
|
66
|
+
return nil if instruments.empty?
|
|
67
|
+
|
|
68
|
+
search_symbol = case_sensitive ? symbol : symbol.upcase
|
|
69
|
+
|
|
70
|
+
instruments.find do |instrument|
|
|
71
|
+
# For equity instruments, prefer underlying_symbol over symbol_name
|
|
72
|
+
instrument_symbol = if instrument.instrument == "EQUITY" && instrument.underlying_symbol
|
|
73
|
+
case_sensitive ? instrument.underlying_symbol : instrument.underlying_symbol.upcase
|
|
74
|
+
else
|
|
75
|
+
case_sensitive ? instrument.symbol_name : instrument.symbol_name.upcase
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if exact_match
|
|
79
|
+
instrument_symbol == search_symbol
|
|
80
|
+
else
|
|
81
|
+
instrument_symbol.include?(search_symbol)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Find a specific instrument across all exchange segments.
|
|
87
|
+
# @param symbol [String] The symbol name to search for
|
|
88
|
+
# @param options [Hash] Additional search options
|
|
89
|
+
# @option options [Boolean] :exact_match Whether to perform exact symbol matching (default: false)
|
|
90
|
+
# @option options [Boolean] :case_sensitive Whether the search should be case sensitive (default: false)
|
|
91
|
+
# @option options [Array<String>] :segments Specific segments to search in (default: all common segments)
|
|
92
|
+
# @return [Instrument, nil] The found instrument or nil if not found
|
|
93
|
+
# @example
|
|
94
|
+
# # Find RELIANCE across all segments
|
|
95
|
+
# instrument = DhanHQ::Models::Instrument.find_anywhere("RELIANCE")
|
|
96
|
+
# puts "#{instrument.exchange_segment}:#{instrument.security_id}" # => "NSE_EQ:2885"
|
|
97
|
+
#
|
|
98
|
+
# # Find NIFTY across all segments
|
|
99
|
+
# instrument = DhanHQ::Models::Instrument.find_anywhere("NIFTY")
|
|
100
|
+
# puts "#{instrument.exchange_segment}:#{instrument.security_id}" # => "IDX_I:13"
|
|
101
|
+
#
|
|
102
|
+
# # Search only in specific segments
|
|
103
|
+
# instrument = DhanHQ::Models::Instrument.find_anywhere("RELIANCE", segments: ["NSE_EQ", "BSE_EQ"])
|
|
104
|
+
def find_anywhere(symbol, options = {})
|
|
105
|
+
exact_match = options[:exact_match] || false
|
|
106
|
+
case_sensitive = options[:case_sensitive] || false
|
|
107
|
+
segments = options[:segments] || %w[NSE_EQ BSE_EQ IDX_I NSE_FNO NSE_CURRENCY]
|
|
108
|
+
|
|
109
|
+
segments.each do |segment|
|
|
110
|
+
instrument = find(segment, symbol, exact_match: exact_match, case_sensitive: case_sensitive)
|
|
111
|
+
return instrument if instrument
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
nil
|
|
115
|
+
end
|
|
116
|
+
|
|
32
117
|
def normalize_csv_row(row)
|
|
118
|
+
# Extract exchange and segment from CSV
|
|
119
|
+
exchange_id = row["EXCH_ID"] || row["EXCHANGE"]
|
|
120
|
+
segment_code = row["SEGMENT"]
|
|
121
|
+
|
|
122
|
+
# Calculate exchange_segment using SEGMENT_MAP from Constants
|
|
123
|
+
exchange_segment = if exchange_id && segment_code
|
|
124
|
+
DhanHQ::Constants::SEGMENT_MAP[[exchange_id, segment_code]]
|
|
125
|
+
else
|
|
126
|
+
row["EXCH_ID"] # Fallback to original value
|
|
127
|
+
end
|
|
128
|
+
|
|
33
129
|
{
|
|
34
130
|
security_id: row["SECURITY_ID"].to_s,
|
|
35
131
|
symbol_name: row["SYMBOL_NAME"],
|
|
36
132
|
display_name: row["DISPLAY_NAME"],
|
|
37
|
-
|
|
133
|
+
exchange: exchange_id,
|
|
134
|
+
segment: segment_code,
|
|
135
|
+
exchange_segment: exchange_segment,
|
|
38
136
|
instrument: row["INSTRUMENT"],
|
|
39
137
|
series: row["SERIES"],
|
|
40
138
|
lot_size: row["LOT_SIZE"]&.to_f,
|
|
41
139
|
tick_size: row["TICK_SIZE"]&.to_f,
|
|
42
140
|
expiry_date: row["SM_EXPIRY_DATE"],
|
|
43
141
|
strike_price: row["STRIKE_PRICE"]&.to_f,
|
|
44
|
-
option_type: row["OPTION_TYPE"]
|
|
142
|
+
option_type: row["OPTION_TYPE"],
|
|
143
|
+
underlying_symbol: row["UNDERLYING_SYMBOL"],
|
|
144
|
+
isin: row["ISIN"],
|
|
145
|
+
instrument_type: row["INSTRUMENT_TYPE"],
|
|
146
|
+
expiry_flag: row["EXPIRY_FLAG"],
|
|
147
|
+
bracket_flag: row["BRACKET_FLAG"],
|
|
148
|
+
cover_flag: row["COVER_FLAG"],
|
|
149
|
+
asm_gsm_flag: row["ASM_GSM_FLAG"],
|
|
150
|
+
asm_gsm_category: row["ASM_GSM_CATEGORY"],
|
|
151
|
+
buy_sell_indicator: row["BUY_SELL_INDICATOR"],
|
|
152
|
+
buy_co_min_margin_per: row["BUY_CO_MIN_MARGIN_PER"]&.to_f,
|
|
153
|
+
sell_co_min_margin_per: row["SELL_CO_MIN_MARGIN_PER"]&.to_f,
|
|
154
|
+
mtf_leverage: row["MTF_LEVERAGE"]&.to_f
|
|
45
155
|
}
|
|
46
156
|
end
|
|
47
157
|
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Models
|
|
5
|
+
# Helper module providing instance methods for Instrument objects
|
|
6
|
+
# to access market feed, historical data, and option chain data.
|
|
7
|
+
module InstrumentHelpers
|
|
8
|
+
##
|
|
9
|
+
# Fetches last traded price (LTP) for this instrument.
|
|
10
|
+
#
|
|
11
|
+
# @return [Hash] Market feed LTP response
|
|
12
|
+
# @example
|
|
13
|
+
# instrument = DhanHQ::Models::Instrument.find("IDX_I", "NIFTY")
|
|
14
|
+
# instrument.ltp
|
|
15
|
+
def ltp
|
|
16
|
+
params = build_market_feed_params
|
|
17
|
+
DhanHQ::Models::MarketFeed.ltp(params)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# Fetches OHLC (Open-High-Low-Close) data for this instrument.
|
|
22
|
+
#
|
|
23
|
+
# @return [Hash] Market feed OHLC response
|
|
24
|
+
# @example
|
|
25
|
+
# instrument = DhanHQ::Models::Instrument.find("IDX_I", "NIFTY")
|
|
26
|
+
# instrument.ohlc
|
|
27
|
+
def ohlc
|
|
28
|
+
params = build_market_feed_params
|
|
29
|
+
DhanHQ::Models::MarketFeed.ohlc(params)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# Fetches full quote depth and analytics for this instrument.
|
|
34
|
+
#
|
|
35
|
+
# @return [Hash] Market feed quote response
|
|
36
|
+
# @example
|
|
37
|
+
# instrument = DhanHQ::Models::Instrument.find("NSE_FNO", "RELIANCE")
|
|
38
|
+
# instrument.quote
|
|
39
|
+
def quote
|
|
40
|
+
params = build_market_feed_params
|
|
41
|
+
DhanHQ::Models::MarketFeed.quote(params)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
##
|
|
45
|
+
# Fetches daily historical data for this instrument.
|
|
46
|
+
#
|
|
47
|
+
# @param from_date [String] Start date in YYYY-MM-DD format
|
|
48
|
+
# @param to_date [String] End date in YYYY-MM-DD format
|
|
49
|
+
# @param options [Hash] Additional options (e.g., expiry_code)
|
|
50
|
+
# @return [Hash] Historical data with open, high, low, close, volume, timestamp arrays
|
|
51
|
+
# @example
|
|
52
|
+
# instrument = DhanHQ::Models::Instrument.find("NSE_EQ", "RELIANCE")
|
|
53
|
+
# instrument.daily(from_date: "2024-01-01", to_date: "2024-01-31")
|
|
54
|
+
def daily(from_date:, to_date:, **options)
|
|
55
|
+
params = build_historical_data_params(from_date: from_date, to_date: to_date, **options)
|
|
56
|
+
DhanHQ::Models::HistoricalData.daily(params)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
##
|
|
60
|
+
# Fetches intraday historical data for this instrument.
|
|
61
|
+
#
|
|
62
|
+
# @param from_date [String] Start date in YYYY-MM-DD format
|
|
63
|
+
# @param to_date [String] End date in YYYY-MM-DD format
|
|
64
|
+
# @param interval [String] Time interval in minutes (1, 5, 15, 25, 60)
|
|
65
|
+
# @param options [Hash] Additional options
|
|
66
|
+
# @return [Hash] Historical data with open, high, low, close, volume, timestamp arrays
|
|
67
|
+
# @example
|
|
68
|
+
# instrument = DhanHQ::Models::Instrument.find("IDX_I", "NIFTY")
|
|
69
|
+
# instrument.intraday(from_date: "2024-09-11", to_date: "2024-09-15", interval: "15")
|
|
70
|
+
def intraday(from_date:, to_date:, interval:, **options)
|
|
71
|
+
params = build_historical_data_params(from_date: from_date, to_date: to_date, interval: interval, **options)
|
|
72
|
+
DhanHQ::Models::HistoricalData.intraday(params)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
##
|
|
76
|
+
# Fetches the expiry list for this instrument (option chain).
|
|
77
|
+
#
|
|
78
|
+
# @return [Array<String>] List of expiry dates in YYYY-MM-DD format
|
|
79
|
+
# @example
|
|
80
|
+
# instrument = DhanHQ::Models::Instrument.find("NSE_FNO", "NIFTY")
|
|
81
|
+
# expiries = instrument.expiry_list
|
|
82
|
+
def expiry_list
|
|
83
|
+
params = {
|
|
84
|
+
underlying_scrip: security_id.to_i,
|
|
85
|
+
underlying_seg: exchange_segment
|
|
86
|
+
}
|
|
87
|
+
DhanHQ::Models::OptionChain.fetch_expiry_list(params)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
##
|
|
91
|
+
# Fetches the option chain for this instrument.
|
|
92
|
+
#
|
|
93
|
+
# @param expiry [String] Expiry date in YYYY-MM-DD format
|
|
94
|
+
# @return [Hash] Option chain data
|
|
95
|
+
# @example
|
|
96
|
+
# instrument = DhanHQ::Models::Instrument.find("NSE_FNO", "NIFTY")
|
|
97
|
+
# chain = instrument.option_chain(expiry: "2024-02-29")
|
|
98
|
+
def option_chain(expiry:)
|
|
99
|
+
params = {
|
|
100
|
+
underlying_scrip: security_id.to_i,
|
|
101
|
+
underlying_seg: exchange_segment,
|
|
102
|
+
expiry: expiry
|
|
103
|
+
}
|
|
104
|
+
DhanHQ::Models::OptionChain.fetch(params)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
##
|
|
110
|
+
# Builds market feed params from instrument attributes.
|
|
111
|
+
#
|
|
112
|
+
# @return [Hash] Market feed params in format { "EXCHANGE_SEGMENT": [security_id] }
|
|
113
|
+
def build_market_feed_params
|
|
114
|
+
{ exchange_segment => [security_id.to_i] }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
##
|
|
118
|
+
# Builds historical data params from instrument attributes.
|
|
119
|
+
#
|
|
120
|
+
# @param from_date [String] Start date
|
|
121
|
+
# @param to_date [String] End date
|
|
122
|
+
# @param interval [String, nil] Time interval for intraday
|
|
123
|
+
# @param options [Hash] Additional options
|
|
124
|
+
# @return [Hash] Historical data params
|
|
125
|
+
def build_historical_data_params(from_date:, to_date:, interval: nil, **options)
|
|
126
|
+
params = {
|
|
127
|
+
security_id: security_id,
|
|
128
|
+
exchange_segment: exchange_segment,
|
|
129
|
+
instrument: instrument,
|
|
130
|
+
from_date: from_date,
|
|
131
|
+
to_date: to_date
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
params[:interval] = interval if interval
|
|
135
|
+
params.merge!(options) if options.any?
|
|
136
|
+
|
|
137
|
+
params
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|