DhanHQ 2.1.3 → 2.1.6

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/.rubocop_todo.yml +185 -0
  4. data/CHANGELOG.md +31 -0
  5. data/GUIDE.md +173 -31
  6. data/README.md +437 -133
  7. data/README1.md +267 -26
  8. data/docs/live_order_updates.md +319 -0
  9. data/docs/rails_integration.md +1 -1
  10. data/docs/rails_websocket_integration.md +847 -0
  11. data/docs/standalone_ruby_websocket_integration.md +1588 -0
  12. data/docs/technical_analysis.md +1 -0
  13. data/docs/websocket_integration.md +871 -0
  14. data/examples/comprehensive_websocket_examples.rb +148 -0
  15. data/examples/instrument_finder_test.rb +195 -0
  16. data/examples/live_order_updates.rb +118 -0
  17. data/examples/market_depth_example.rb +144 -0
  18. data/examples/market_feed_example.rb +81 -0
  19. data/examples/order_update_example.rb +105 -0
  20. data/examples/trading_fields_example.rb +215 -0
  21. data/lib/DhanHQ/config.rb +1 -0
  22. data/lib/DhanHQ/configuration.rb +16 -1
  23. data/lib/DhanHQ/contracts/expired_options_data_contract.rb +103 -0
  24. data/lib/DhanHQ/contracts/modify_order_contract.rb +1 -0
  25. data/lib/DhanHQ/contracts/option_chain_contract.rb +11 -1
  26. data/lib/DhanHQ/contracts/trade_contract.rb +70 -0
  27. data/lib/DhanHQ/errors.rb +2 -0
  28. data/lib/DhanHQ/models/expired_options_data.rb +331 -0
  29. data/lib/DhanHQ/models/instrument.rb +96 -2
  30. data/lib/DhanHQ/models/option_chain.rb +2 -0
  31. data/lib/DhanHQ/models/order_update.rb +235 -0
  32. data/lib/DhanHQ/models/trade.rb +118 -31
  33. data/lib/DhanHQ/rate_limiter.rb +4 -2
  34. data/lib/DhanHQ/resources/expired_options_data.rb +22 -0
  35. data/lib/DhanHQ/version.rb +1 -1
  36. data/lib/DhanHQ/ws/base_connection.rb +249 -0
  37. data/lib/DhanHQ/ws/client.rb +1 -1
  38. data/lib/DhanHQ/ws/connection.rb +3 -3
  39. data/lib/DhanHQ/ws/decoder.rb +3 -3
  40. data/lib/DhanHQ/ws/market_depth/client.rb +376 -0
  41. data/lib/DhanHQ/ws/market_depth/decoder.rb +131 -0
  42. data/lib/DhanHQ/ws/market_depth.rb +74 -0
  43. data/lib/DhanHQ/ws/orders/client.rb +177 -10
  44. data/lib/DhanHQ/ws/orders/connection.rb +41 -83
  45. data/lib/DhanHQ/ws/orders.rb +31 -2
  46. data/lib/DhanHQ/ws/registry.rb +1 -0
  47. data/lib/DhanHQ/ws/segments.rb +21 -5
  48. data/lib/DhanHQ/ws/sub_state.rb +1 -1
  49. data/lib/DhanHQ/ws.rb +3 -2
  50. data/lib/{DhanHQ.rb → dhan_hq.rb} +5 -0
  51. data/lib/dhanhq/analysis/helpers/bias_aggregator.rb +18 -18
  52. data/lib/dhanhq/analysis/helpers/moneyness_helper.rb +1 -0
  53. data/lib/dhanhq/analysis/multi_timeframe_analyzer.rb +2 -0
  54. data/lib/dhanhq/analysis/options_buying_advisor.rb +4 -3
  55. data/lib/dhanhq/contracts/options_buying_advisor_contract.rb +1 -0
  56. data/lib/ta/candles.rb +1 -0
  57. data/lib/ta/fetcher.rb +1 -0
  58. data/lib/ta/indicators.rb +2 -1
  59. data/lib/ta/market_calendar.rb +4 -3
  60. data/lib/ta/technical_analysis.rb +3 -2
  61. metadata +38 -4
  62. data/lib/DhanHQ/ws/errors.rb +0 -0
  63. /data/lib/DhanHQ/contracts/{modify_order_contract copy.rb → modify_order_contract_copy.rb} +0 -0
@@ -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
@@ -7,7 +7,10 @@ module DhanHQ
7
7
  # Model wrapper for fetching instruments by exchange segment.
8
8
  class Instrument < BaseModel
9
9
  attributes :security_id, :symbol_name, :display_name, :exchange_segment, :instrument, :series,
10
- :lot_size, :tick_size, :expiry_date, :strike_price, :option_type
10
+ :lot_size, :tick_size, :expiry_date, :strike_price, :option_type, :underlying_symbol,
11
+ :isin, :instrument_type, :expiry_flag, :bracket_flag, :cover_flag, :asm_gsm_flag,
12
+ :asm_gsm_category, :buy_sell_indicator, :buy_co_min_margin_per, :sell_co_min_margin_per,
13
+ :mtf_leverage
11
14
 
12
15
  class << self
13
16
  # @return [DhanHQ::Resources::Instruments]
@@ -29,6 +32,85 @@ module DhanHQ
29
32
  rows.map { |r| new(normalize_csv_row(r), skip_validation: true) }
30
33
  end
31
34
 
35
+ # Find a specific instrument by exchange segment and symbol.
36
+ # @param exchange_segment [String] The exchange segment (e.g., "NSE_EQ", "IDX_I")
37
+ # @param symbol [String] The symbol name to search for
38
+ # @param options [Hash] Additional search options
39
+ # @option options [Boolean] :exact_match Whether to perform exact symbol matching (default: false)
40
+ # @option options [Boolean] :case_sensitive Whether the search should be case sensitive (default: false)
41
+ # @return [Instrument, nil] The found instrument or nil if not found
42
+ # @example
43
+ # # Find RELIANCE in NSE_EQ (uses underlying_symbol for equity)
44
+ # instrument = DhanHQ::Models::Instrument.find("NSE_EQ", "RELIANCE")
45
+ # puts instrument.security_id # => "2885"
46
+ #
47
+ # # Find NIFTY in IDX_I (uses symbol_name for indices)
48
+ # instrument = DhanHQ::Models::Instrument.find("IDX_I", "NIFTY")
49
+ # puts instrument.security_id # => "13"
50
+ #
51
+ # # Exact match search
52
+ # instrument = DhanHQ::Models::Instrument.find("NSE_EQ", "RELIANCE", exact_match: true)
53
+ #
54
+ # # Case sensitive search
55
+ # instrument = DhanHQ::Models::Instrument.find("NSE_EQ", "reliance", case_sensitive: true)
56
+ def find(exchange_segment, symbol, options = { exact_match: true, case_sensitive: false })
57
+ validate_params!({ exchange_segment: exchange_segment, symbol: symbol }, DhanHQ::Contracts::InstrumentListContract)
58
+
59
+ exact_match = options[:exact_match] || false
60
+ case_sensitive = options[:case_sensitive] || false
61
+
62
+ instruments = by_segment(exchange_segment)
63
+ return nil if instruments.empty?
64
+
65
+ search_symbol = case_sensitive ? symbol : symbol.upcase
66
+
67
+ instruments.find do |instrument|
68
+ # For equity instruments, prefer underlying_symbol over symbol_name
69
+ instrument_symbol = if instrument.instrument == "EQUITY" && instrument.underlying_symbol
70
+ case_sensitive ? instrument.underlying_symbol : instrument.underlying_symbol.upcase
71
+ else
72
+ case_sensitive ? instrument.symbol_name : instrument.symbol_name.upcase
73
+ end
74
+
75
+ if exact_match
76
+ instrument_symbol == search_symbol
77
+ else
78
+ instrument_symbol.include?(search_symbol)
79
+ end
80
+ end
81
+ end
82
+
83
+ # Find a specific instrument across all exchange segments.
84
+ # @param symbol [String] The symbol name to search for
85
+ # @param options [Hash] Additional search options
86
+ # @option options [Boolean] :exact_match Whether to perform exact symbol matching (default: false)
87
+ # @option options [Boolean] :case_sensitive Whether the search should be case sensitive (default: false)
88
+ # @option options [Array<String>] :segments Specific segments to search in (default: all common segments)
89
+ # @return [Instrument, nil] The found instrument or nil if not found
90
+ # @example
91
+ # # Find RELIANCE across all segments
92
+ # instrument = DhanHQ::Models::Instrument.find_anywhere("RELIANCE")
93
+ # puts "#{instrument.exchange_segment}:#{instrument.security_id}" # => "NSE_EQ:2885"
94
+ #
95
+ # # Find NIFTY across all segments
96
+ # instrument = DhanHQ::Models::Instrument.find_anywhere("NIFTY")
97
+ # puts "#{instrument.exchange_segment}:#{instrument.security_id}" # => "IDX_I:13"
98
+ #
99
+ # # Search only in specific segments
100
+ # instrument = DhanHQ::Models::Instrument.find_anywhere("RELIANCE", segments: ["NSE_EQ", "BSE_EQ"])
101
+ def find_anywhere(symbol, options = {})
102
+ exact_match = options[:exact_match] || false
103
+ case_sensitive = options[:case_sensitive] || false
104
+ segments = options[:segments] || %w[NSE_EQ BSE_EQ IDX_I NSE_FNO NSE_CURRENCY]
105
+
106
+ segments.each do |segment|
107
+ instrument = find(segment, symbol, exact_match: exact_match, case_sensitive: case_sensitive)
108
+ return instrument if instrument
109
+ end
110
+
111
+ nil
112
+ end
113
+
32
114
  def normalize_csv_row(row)
33
115
  {
34
116
  security_id: row["SECURITY_ID"].to_s,
@@ -41,7 +123,19 @@ module DhanHQ
41
123
  tick_size: row["TICK_SIZE"]&.to_f,
42
124
  expiry_date: row["SM_EXPIRY_DATE"],
43
125
  strike_price: row["STRIKE_PRICE"]&.to_f,
44
- option_type: row["OPTION_TYPE"]
126
+ option_type: row["OPTION_TYPE"],
127
+ underlying_symbol: row["UNDERLYING_SYMBOL"],
128
+ isin: row["ISIN"],
129
+ instrument_type: row["INSTRUMENT_TYPE"],
130
+ expiry_flag: row["EXPIRY_FLAG"],
131
+ bracket_flag: row["BRACKET_FLAG"],
132
+ cover_flag: row["COVER_FLAG"],
133
+ asm_gsm_flag: row["ASM_GSM_FLAG"],
134
+ asm_gsm_category: row["ASM_GSM_CATEGORY"],
135
+ buy_sell_indicator: row["BUY_SELL_INDICATOR"],
136
+ buy_co_min_margin_per: row["BUY_CO_MIN_MARGIN_PER"]&.to_f,
137
+ sell_co_min_margin_per: row["SELL_CO_MIN_MARGIN_PER"]&.to_f,
138
+ mtf_leverage: row["MTF_LEVERAGE"]&.to_f
45
139
  }
46
140
  end
47
141
  end
@@ -34,6 +34,8 @@ module DhanHQ
34
34
  # @param params [Hash] The request parameters (snake_case format)
35
35
  # @return [Array<String>] The list of expiry dates
36
36
  def fetch_expiry_list(params)
37
+ validate_params!(params, DhanHQ::Contracts::OptionChainExpiryListContract)
38
+
37
39
  response = resource.expirylist(params)
38
40
  response[:status] == "success" ? response[:data] : []
39
41
  end
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Models
5
+ ##
6
+ # Represents a real-time order update received via WebSocket
7
+ # Parses and provides access to all order update fields as per DhanHQ API documentation
8
+ # rubocop:disable Metrics/ClassLength
9
+ class OrderUpdate < BaseModel
10
+ # All order update attributes as per API documentation
11
+ attributes :exchange, :segment, :source, :security_id, :client_id,
12
+ :exch_order_no, :order_no, :product, :txn_type, :order_type,
13
+ :validity, :disc_quantity, :disc_qty_rem, :remaining_quantity,
14
+ :quantity, :traded_qty, :price, :trigger_price, :traded_price,
15
+ :avg_traded_price, :algo_ord_no, :off_mkt_flag, :order_date_time,
16
+ :exch_order_time, :last_updated_time, :remarks, :mkt_type,
17
+ :reason_description, :leg_no, :instrument, :symbol, :product_name,
18
+ :status, :lot_size, :strike_price, :expiry_date, :opt_type,
19
+ :display_name, :isin, :series, :good_till_days_date, :ref_ltp,
20
+ :tick_size, :algo_id, :multiplier, :correlation_id
21
+
22
+ ##
23
+ # Create OrderUpdate from WebSocket message
24
+ # @param message [Hash] Raw WebSocket message
25
+ # @return [OrderUpdate] Parsed order update
26
+ def self.from_websocket_message(message)
27
+ return nil unless message.is_a?(Hash) && message[:Type] == "order_alert"
28
+ return nil unless message[:Data].is_a?(Hash)
29
+
30
+ # Map the WebSocket message data to our attributes
31
+ data = message[:Data]
32
+ new(data, skip_validation: true)
33
+ end
34
+
35
+ ##
36
+ # OrderUpdate objects are read-only, so no validation contract needed
37
+ def validation_contract
38
+ nil
39
+ end
40
+
41
+ ##
42
+ # Helper methods for transaction type
43
+ def buy?
44
+ txn_type == "B"
45
+ end
46
+
47
+ def sell?
48
+ txn_type == "S"
49
+ end
50
+
51
+ ##
52
+ # Helper methods for order type
53
+ def limit_order?
54
+ order_type == "LMT"
55
+ end
56
+
57
+ def market_order?
58
+ order_type == "MKT"
59
+ end
60
+
61
+ def stop_loss_order?
62
+ order_type == "SL"
63
+ end
64
+
65
+ def stop_loss_market_order?
66
+ order_type == "SLM"
67
+ end
68
+
69
+ ##
70
+ # Helper methods for product type
71
+ def cnc_product?
72
+ product == "C"
73
+ end
74
+
75
+ def intraday_product?
76
+ product == "I"
77
+ end
78
+
79
+ def margin_product?
80
+ product == "M"
81
+ end
82
+
83
+ def mtf_product?
84
+ product == "F"
85
+ end
86
+
87
+ def cover_order?
88
+ product == "V"
89
+ end
90
+
91
+ def bracket_order?
92
+ product == "B"
93
+ end
94
+
95
+ ##
96
+ # Helper methods for order status
97
+ def transit?
98
+ status == "TRANSIT"
99
+ end
100
+
101
+ def pending?
102
+ status == "PENDING"
103
+ end
104
+
105
+ def rejected?
106
+ status == "REJECTED"
107
+ end
108
+
109
+ def cancelled?
110
+ status == "CANCELLED"
111
+ end
112
+
113
+ def traded?
114
+ status == "TRADED"
115
+ end
116
+
117
+ def expired?
118
+ status == "EXPIRED"
119
+ end
120
+
121
+ ##
122
+ # Helper methods for instrument type
123
+ def equity?
124
+ instrument == "EQUITY"
125
+ end
126
+
127
+ def derivative?
128
+ instrument == "DERIVATIVES"
129
+ end
130
+
131
+ def option?
132
+ %w[CE PE].include?(opt_type)
133
+ end
134
+
135
+ def call_option?
136
+ opt_type == "CE"
137
+ end
138
+
139
+ def put_option?
140
+ opt_type == "PE"
141
+ end
142
+
143
+ ##
144
+ # Helper methods for order leg (for Super Orders)
145
+ def entry_leg?
146
+ leg_no == 1
147
+ end
148
+
149
+ def stop_loss_leg?
150
+ leg_no == 2
151
+ end
152
+
153
+ def target_leg?
154
+ leg_no == 3
155
+ end
156
+
157
+ ##
158
+ # Helper methods for market type
159
+ def normal_market?
160
+ mkt_type == "NL"
161
+ end
162
+
163
+ def auction_market?
164
+ %w[AU A1 A2].include?(mkt_type)
165
+ end
166
+
167
+ ##
168
+ # Helper methods for order characteristics
169
+ def amo_order?
170
+ off_mkt_flag == "1"
171
+ end
172
+
173
+ def super_order?
174
+ remarks == "Super Order"
175
+ end
176
+
177
+ def partially_executed?
178
+ traded_qty.positive? && traded_qty < quantity
179
+ end
180
+
181
+ def fully_executed?
182
+ traded_qty == quantity
183
+ end
184
+
185
+ def not_executed?
186
+ traded_qty.zero?
187
+ end
188
+
189
+ ##
190
+ # Calculation methods
191
+ def execution_percentage
192
+ return 0.0 if quantity.zero?
193
+
194
+ (traded_qty.to_f / quantity * 100).round(2)
195
+ end
196
+
197
+ def pending_quantity
198
+ quantity - traded_qty
199
+ end
200
+
201
+ def total_value
202
+ return 0 unless traded_qty && avg_traded_price
203
+
204
+ traded_qty * avg_traded_price
205
+ end
206
+
207
+ ##
208
+ # Status summary for logging/debugging
209
+ # rubocop:disable Metrics/MethodLength
210
+ def status_summary
211
+ {
212
+ order_no: order_no,
213
+ symbol: symbol,
214
+ status: status,
215
+ txn_type: txn_type,
216
+ quantity: quantity,
217
+ traded_qty: traded_qty,
218
+ execution_percentage: execution_percentage,
219
+ price: price,
220
+ avg_traded_price: avg_traded_price,
221
+ leg_no: leg_no,
222
+ super_order: super_order?
223
+ }
224
+ end
225
+ # rubocop:enable Metrics/MethodLength
226
+
227
+ ##
228
+ # Convert to hash for serialization
229
+ def to_hash
230
+ @attributes.dup
231
+ end
232
+ end
233
+ # rubocop:enable Metrics/ClassLength
234
+ end
235
+ end