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,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
@@ -4,12 +4,14 @@ module DhanHQ
4
4
  module Models
5
5
  ##
6
6
  # Represents a single trade.
7
- # The API docs show an array of trades from GET /v2/trades/{from-date}/{to-date}/{page}
7
+ # Supports three main API endpoints:
8
+ # 1. GET /v2/trades - Current day trades
9
+ # 2. GET /v2/trades/{order-id} - Trades for specific order
10
+ # 3. GET /v2/trades/{from-date}/{to-date}/{page} - Historical trades
8
11
  class Trade < BaseModel
9
- # No explicit HTTP_PATH if we rely on the statements resource
10
- # but we can define it if needed
11
12
  HTTP_PATH = "/v2/trades"
12
13
 
14
+ # All trade attributes as per API documentation
13
15
  attributes :dhan_client_id, :order_id, :exchange_order_id, :exchange_trade_id,
14
16
  :transaction_type, :exchange_segment, :product_type, :order_type,
15
17
  :trading_symbol, :custom_symbol, :security_id, :traded_quantity,
@@ -19,14 +21,6 @@ module DhanHQ
19
21
  :drv_option_type, :drv_strike_price
20
22
 
21
23
  class << self
22
- ##
23
- # Provide a **shared instance** of the `Statements` resource,
24
- # where we have `trade_history(from_date:, to_date:, page:)`.
25
- # used for fetching historical trades.
26
- def resource
27
- @resource ||= DhanHQ::Resources::Statements.new
28
- end
29
-
30
24
  ##
31
25
  # Resource for current day tradebook APIs
32
26
  def tradebook_resource
@@ -34,46 +28,139 @@ module DhanHQ
34
28
  end
35
29
 
36
30
  ##
37
- # Fetch trades within the given date range and page.
38
- # GET /v2/trades/{from-date}/{to-date}/{page}
39
- #
40
- # @param from_date [String]
41
- # @param to_date [String]
42
- # @param page [Integer] Default 0
43
- # @return [Array<Trade>]
44
- # Retrieve historical trades
45
- def history(from_date:, to_date:, page: 0)
46
- # The resource call returns an Array<Hash>.
47
- response = resource.trade_history(from_date: from_date, to_date: to_date, page: page)
48
- return [] unless response.is_a?(Array)
49
-
50
- response.map { |t| new(t, skip_validation: true) }
31
+ # Resource for historical trade data
32
+ def statements_resource
33
+ @statements_resource ||= DhanHQ::Resources::Statements.new
51
34
  end
52
35
 
53
- alias all history
54
-
55
- # Retrieve current day trades
36
+ ##
37
+ # Fetch current day trades
38
+ # GET /v2/trades
39
+ #
40
+ # @return [Array<Trade>] Array of trades executed today
56
41
  def today
57
42
  response = tradebook_resource.all
58
43
  return [] unless response.is_a?(Array)
59
44
 
60
- response.map { |t| new(t, skip_validation: true) }
45
+ response.map { |trade_data| new(trade_data, skip_validation: true) }
61
46
  end
62
47
 
63
- # Fetch trades for a specific order id for the current day
48
+ ##
49
+ # Fetch trades for a specific order ID (current day)
50
+ # GET /v2/trades/{order-id}
51
+ #
52
+ # @param order_id [String] The order ID to fetch trades for
53
+ # @return [Trade, nil] Trade object or nil if not found
64
54
  def find_by_order_id(order_id)
55
+ # Validate input
56
+ contract = DhanHQ::Contracts::TradeByOrderIdContract.new
57
+ validation_result = contract.call(order_id: order_id)
58
+
59
+ unless validation_result.success?
60
+ raise DhanHQ::ValidationError, "Invalid order_id: #{validation_result.errors.to_h}"
61
+ end
62
+
65
63
  response = tradebook_resource.find(order_id)
66
64
  return nil unless response.is_a?(Hash) || (response.is_a?(Array) && response.any?)
67
65
 
68
66
  data = response.is_a?(Array) ? response.first : response
69
67
  new(data, skip_validation: true)
70
68
  end
69
+
70
+ ##
71
+ # Fetch historical trades within the given date range and page
72
+ # GET /v2/trades/{from-date}/{to-date}/{page}
73
+ #
74
+ # @param from_date [String] Start date in YYYY-MM-DD format
75
+ # @param to_date [String] End date in YYYY-MM-DD format
76
+ # @param page [Integer] Page number (default: 0)
77
+ # @return [Array<Trade>] Array of historical trades
78
+ def history(from_date:, to_date:, page: 0)
79
+ validate_history_params(from_date, to_date, page)
80
+
81
+ response = statements_resource.trade_history(
82
+ from_date: from_date,
83
+ to_date: to_date,
84
+ page: page
85
+ )
86
+
87
+ return [] unless response.is_a?(Array)
88
+
89
+ response.map { |trade_data| new(trade_data, skip_validation: true) }
90
+ end
91
+
92
+ private
93
+
94
+ def validate_history_params(from_date, to_date, page)
95
+ contract = DhanHQ::Contracts::TradeHistoryContract.new
96
+ validation_result = contract.call(from_date: from_date, to_date: to_date, page: page)
97
+
98
+ return if validation_result.success?
99
+
100
+ raise DhanHQ::ValidationError, "Invalid parameters: #{validation_result.errors.to_h}"
101
+ end
102
+
103
+ # Alias for backward compatibility
104
+ alias all history
71
105
  end
72
106
 
73
- # If you want custom validations, you'd set a contract or skip for read-only
107
+ ##
108
+ # Trade objects are read-only, so no validation contract needed
74
109
  def validation_contract
75
110
  nil
76
111
  end
112
+
113
+ ##
114
+ # Helper methods for trade data
115
+ def buy?
116
+ transaction_type == "BUY"
117
+ end
118
+
119
+ def sell?
120
+ transaction_type == "SELL"
121
+ end
122
+
123
+ def equity?
124
+ instrument == "EQUITY"
125
+ end
126
+
127
+ def derivative?
128
+ instrument == "DERIVATIVES"
129
+ end
130
+
131
+ def option?
132
+ %w[CALL PUT].include?(drv_option_type)
133
+ end
134
+
135
+ def call_option?
136
+ drv_option_type == "CALL"
137
+ end
138
+
139
+ def put_option?
140
+ drv_option_type == "PUT"
141
+ end
142
+
143
+ ##
144
+ # Calculate total trade value
145
+ def total_value
146
+ return 0 unless traded_quantity && traded_price
147
+
148
+ traded_quantity * traded_price
149
+ end
150
+
151
+ ##
152
+ # Calculate total charges
153
+ def total_charges
154
+ charges = [sebi_tax, stt, brokerage_charges, service_tax,
155
+ exchange_transaction_charges, stamp_duty].compact
156
+ charges.sum(&:to_f)
157
+ end
158
+
159
+ ##
160
+ # Net trade value after charges
161
+ def net_value
162
+ total_value - total_charges
163
+ end
77
164
  end
78
165
  end
79
166
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Resources
5
+ ##
6
+ # Resource for expired options data API endpoints
7
+ class ExpiredOptionsData < BaseAPI
8
+ API_TYPE = :data_api
9
+ HTTP_PATH = "/charts"
10
+
11
+ ##
12
+ # Fetch expired options data for rolling contracts
13
+ # POST /charts/rollingoption
14
+ #
15
+ # @param params [Hash] Parameters for the request
16
+ # @return [Hash] API response with expired options data
17
+ def fetch(params)
18
+ post("/rollingoption", params: params)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module DhanHQ
4
4
  # Semantic version of the DhanHQ client gem.
5
- VERSION = "2.1.5"
5
+ VERSION = "2.1.7"
6
6
  end
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "eventmachine"
4
+ require "faye/websocket"
5
+ require "json"
6
+ require "concurrent"
7
+ require "uri"
8
+
9
+ module DhanHQ
10
+ module WS
11
+ ##
12
+ # Base WebSocket connection class providing common functionality
13
+ # for all DhanHQ WebSocket connections (Orders, Market Feed, Market Depth)
14
+ class BaseConnection
15
+ COOL_OFF_429 = 60 # seconds to cool off on 429
16
+ MAX_BACKOFF = 90 # cap exponential backoff
17
+
18
+ attr_reader :stopping, :url, :callbacks, :started
19
+
20
+ ##
21
+ # Initialize base connection
22
+ # @param url [String] WebSocket endpoint URL
23
+ # @param options [Hash] Connection options
24
+ # @option options [Integer] :max_backoff Maximum backoff time (default: 90)
25
+ # @option options [Integer] :cool_off_429 Cool off time for 429 errors (default: 60)
26
+ def initialize(url:, **options)
27
+ @url = url
28
+ @callbacks = Concurrent::Map.new { |h, k| h[k] = [] }
29
+ @started = Concurrent::AtomicBoolean.new(false)
30
+ @stop = false
31
+ @stopping = false
32
+ @ws = nil
33
+ @timer = nil
34
+ @cooloff_until = nil
35
+ @thr = nil
36
+ @max_backoff = options[:max_backoff] || MAX_BACKOFF
37
+ @cool_off_429 = options[:cool_off_429] || COOL_OFF_429
38
+ end
39
+
40
+ ##
41
+ # Start the WebSocket connection
42
+ # @return [BaseConnection] self for method chaining
43
+ def start
44
+ return self if @started.true?
45
+
46
+ @started.make_true
47
+ @thr = Thread.new { loop_run }
48
+ self
49
+ end
50
+
51
+ ##
52
+ # Stop the WebSocket connection gracefully
53
+ # @return [BaseConnection] self for method chaining
54
+ def stop
55
+ return self unless @started.true?
56
+
57
+ @started.make_false
58
+ @stop = true
59
+ @stopping = true
60
+ @ws&.close
61
+ self
62
+ end
63
+
64
+ ##
65
+ # Force disconnect the WebSocket
66
+ # @return [BaseConnection] self for method chaining
67
+ def disconnect!
68
+ @stop = true
69
+ @stopping = true
70
+ @ws&.close
71
+ self
72
+ end
73
+
74
+ ##
75
+ # Check if connection is open
76
+ # @return [Boolean] true if connected
77
+ def open?
78
+ @ws && @ws.instance_variable_get(:@driver)&.ready_state == 1
79
+ rescue StandardError
80
+ false
81
+ end
82
+
83
+ ##
84
+ # Check if connection is connected (alias for open?)
85
+ # @return [Boolean] true if connected
86
+ def connected?
87
+ open?
88
+ end
89
+
90
+ ##
91
+ # Register event handler
92
+ # @param event [Symbol] Event type
93
+ # @param block [Proc] Event handler
94
+ # @return [BaseConnection] self for method chaining
95
+ def on(event, &block)
96
+ @callbacks[event] << block
97
+ self
98
+ end
99
+
100
+ ##
101
+ # Emit event to registered callbacks
102
+ # @param event [Symbol] Event type
103
+ # @param payload [Object] Event payload
104
+ def emit(event, payload = nil)
105
+ list = @callbacks[event] || []
106
+ list.each { |cb| cb.call(payload) }
107
+ rescue StandardError => e
108
+ DhanHQ.logger&.error("[DhanHQ::WS::BaseConnection] Error in event handler: #{e.class} #{e.message}")
109
+ end
110
+
111
+ ##
112
+ # Send message over WebSocket
113
+ # @param message [String, Hash] Message to send
114
+ def send_message(message)
115
+ return unless @ws && open?
116
+
117
+ data = message.is_a?(Hash) ? message.to_json : message
118
+ @ws.send(data)
119
+ rescue StandardError => e
120
+ DhanHQ.logger&.error("[DhanHQ::WS::BaseConnection] Error sending message: #{e.class} #{e.message}")
121
+ end
122
+
123
+ private
124
+
125
+ ##
126
+ # Main connection loop with reconnection logic
127
+ def loop_run
128
+ backoff = 2.0
129
+ until @stop
130
+ failed = false
131
+ got_429 = false
132
+
133
+ # Respect any active cool-off window
134
+ sleep (@cooloff_until - Time.now).ceil if @cooloff_until && Time.now < @cooloff_until
135
+
136
+ begin
137
+ failed, got_429 = run_session
138
+ rescue StandardError => e
139
+ DhanHQ.logger&.error("[DhanHQ::WS::BaseConnection] Connection crashed: #{e.class} #{e.message}")
140
+ failed = true
141
+ ensure
142
+ break if @stop
143
+
144
+ if got_429
145
+ @cooloff_until = Time.now + @cool_off_429
146
+ DhanHQ.logger&.warn("[DhanHQ::WS::BaseConnection] Cooling off #{@cool_off_429}s due to 429")
147
+ end
148
+
149
+ if failed
150
+ sleep_time = [backoff, @max_backoff].min
151
+ jitter = rand(0.2 * sleep_time)
152
+ DhanHQ.logger&.warn("[DhanHQ::WS::BaseConnection] Reconnecting in #{(sleep_time + jitter).round(1)}s")
153
+ sleep(sleep_time + jitter)
154
+ backoff *= 2.0
155
+ else
156
+ backoff = 2.0
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ ##
163
+ # Run a single WebSocket session
164
+ # Must be implemented by subclasses
165
+ # @return [Array<Boolean>] [failed, got_429]
166
+ def run_session
167
+ raise NotImplementedError, "Subclasses must implement run_session"
168
+ end
169
+
170
+ ##
171
+ # Get default headers for WebSocket connection
172
+ # @return [Hash] Default headers
173
+ def default_headers
174
+ {
175
+ "User-Agent" => "DhanHQ-Ruby-Client/#{DhanHQ::VERSION}",
176
+ "Origin" => "https://dhanhq.co"
177
+ }
178
+ end
179
+
180
+ ##
181
+ # Sanitize URL for logging by removing sensitive parameters
182
+ # @param url [String] Original URL
183
+ # @return [String] Sanitized URL safe for logging
184
+ def sanitize_url(url)
185
+ return url if url.nil? || url.empty?
186
+
187
+ begin
188
+ uri = URI.parse(url)
189
+ # Remove sensitive query parameters
190
+ if uri.query
191
+ params = URI.decode_www_form(uri.query).reject do |key, _|
192
+ %w[token clientId client_id access_token].include?(key.downcase)
193
+ end
194
+ uri.query = URI.encode_www_form(params) unless params.empty?
195
+ end
196
+ uri.to_s
197
+ rescue StandardError
198
+ # If URL parsing fails, return a generic message
199
+ "wss://[sanitized-url]"
200
+ end
201
+ end
202
+
203
+ ##
204
+ # Handle WebSocket open event
205
+ def handle_open
206
+ DhanHQ.logger&.info("[DhanHQ::WS::BaseConnection] Connected to #{sanitize_url(@url)}")
207
+ emit(:open)
208
+ authenticate if respond_to?(:authenticate, true)
209
+ end
210
+
211
+ ##
212
+ # Handle WebSocket message event
213
+ # @param ev [Event] WebSocket message event
214
+ def handle_message(ev)
215
+ emit(:raw, ev.data)
216
+ process_message(ev.data) if respond_to?(:process_message, true)
217
+ end
218
+
219
+ ##
220
+ # Handle WebSocket close event
221
+ # @param ev [Event] WebSocket close event
222
+ def handle_close(ev)
223
+ @timer&.cancel
224
+ @timer = nil
225
+ msg = "[DhanHQ::WS::BaseConnection] Connection closed: #{ev.code} #{ev.reason}"
226
+ DhanHQ.logger&.warn(msg)
227
+
228
+ emit(:close, { code: ev.code, reason: ev.reason })
229
+
230
+ if @stopping
231
+ [false, false]
232
+ else
233
+ failed = (ev.code != 1000)
234
+ got_429 = ev.reason.to_s.include?("429")
235
+ [failed, got_429]
236
+ end
237
+ end
238
+
239
+ ##
240
+ # Handle WebSocket error event
241
+ # @param ev [Event] WebSocket error event
242
+ def handle_error(ev)
243
+ DhanHQ.logger&.error("[DhanHQ::WS::BaseConnection] WebSocket error: #{ev.message}")
244
+ emit(:error, ev.message)
245
+ [true, false]
246
+ end
247
+ end
248
+ end
249
+ end