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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/GUIDE.md +221 -31
  4. data/README.md +453 -126
  5. data/README1.md +293 -30
  6. data/docs/live_order_updates.md +319 -0
  7. data/docs/rails_websocket_integration.md +847 -0
  8. data/docs/standalone_ruby_websocket_integration.md +1588 -0
  9. data/docs/websocket_integration.md +918 -0
  10. data/examples/comprehensive_websocket_examples.rb +148 -0
  11. data/examples/instrument_finder_test.rb +195 -0
  12. data/examples/live_order_updates.rb +118 -0
  13. data/examples/market_depth_example.rb +144 -0
  14. data/examples/market_feed_example.rb +81 -0
  15. data/examples/order_update_example.rb +105 -0
  16. data/examples/trading_fields_example.rb +215 -0
  17. data/lib/DhanHQ/configuration.rb +16 -1
  18. data/lib/DhanHQ/constants.rb +16 -0
  19. data/lib/DhanHQ/contracts/expired_options_data_contract.rb +103 -0
  20. data/lib/DhanHQ/contracts/trade_contract.rb +70 -0
  21. data/lib/DhanHQ/errors.rb +2 -0
  22. data/lib/DhanHQ/models/expired_options_data.rb +331 -0
  23. data/lib/DhanHQ/models/instrument.rb +114 -4
  24. data/lib/DhanHQ/models/instrument_helpers.rb +141 -0
  25. data/lib/DhanHQ/models/order_update.rb +235 -0
  26. data/lib/DhanHQ/models/trade.rb +118 -31
  27. data/lib/DhanHQ/resources/expired_options_data.rb +22 -0
  28. data/lib/DhanHQ/version.rb +1 -1
  29. data/lib/DhanHQ/ws/base_connection.rb +249 -0
  30. data/lib/DhanHQ/ws/connection.rb +2 -2
  31. data/lib/DhanHQ/ws/decoder.rb +3 -3
  32. data/lib/DhanHQ/ws/market_depth/client.rb +376 -0
  33. data/lib/DhanHQ/ws/market_depth/decoder.rb +131 -0
  34. data/lib/DhanHQ/ws/market_depth.rb +74 -0
  35. data/lib/DhanHQ/ws/orders/client.rb +175 -11
  36. data/lib/DhanHQ/ws/orders/connection.rb +40 -81
  37. data/lib/DhanHQ/ws/orders.rb +28 -0
  38. data/lib/DhanHQ/ws/segments.rb +18 -2
  39. data/lib/DhanHQ/ws.rb +3 -2
  40. data/lib/dhan_hq.rb +5 -0
  41. 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
- attributes :security_id, :symbol_name, :display_name, :exchange_segment, :instrument, :series,
10
- :lot_size, :tick_size, :expiry_date, :strike_price, :option_type
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
- exchange_segment: row["EXCH_ID"],
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