DhanHQ 2.1.8 → 2.2.0

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/.rubocop_todo.yml +143 -118
  4. data/CHANGELOG.md +177 -0
  5. data/CODE_REVIEW_ISSUES.md +397 -0
  6. data/FIXES_APPLIED.md +373 -0
  7. data/GUIDE.md +41 -0
  8. data/README.md +55 -0
  9. data/RELEASING.md +60 -0
  10. data/REVIEW_SUMMARY.md +120 -0
  11. data/VERSION_UPDATE.md +82 -0
  12. data/core +0 -0
  13. data/docs/AUTHENTICATION.md +63 -0
  14. data/docs/DATA_API_PARAMETERS.md +278 -0
  15. data/docs/PR_2.2.0.md +48 -0
  16. data/docs/RELEASE_GUIDE.md +492 -0
  17. data/docs/TESTING_GUIDE.md +1514 -0
  18. data/docs/live_order_updates.md +25 -1
  19. data/docs/rails_integration.md +29 -0
  20. data/docs/websocket_integration.md +22 -0
  21. data/lib/DhanHQ/client.rb +65 -9
  22. data/lib/DhanHQ/configuration.rb +26 -0
  23. data/lib/DhanHQ/constants.rb +1 -1
  24. data/lib/DhanHQ/contracts/expired_options_data_contract.rb +6 -6
  25. data/lib/DhanHQ/contracts/place_order_contract.rb +51 -0
  26. data/lib/DhanHQ/core/base_model.rb +26 -11
  27. data/lib/DhanHQ/errors.rb +4 -0
  28. data/lib/DhanHQ/helpers/request_helper.rb +17 -2
  29. data/lib/DhanHQ/helpers/response_helper.rb +34 -13
  30. data/lib/DhanHQ/models/edis.rb +150 -14
  31. data/lib/DhanHQ/models/expired_options_data.rb +307 -88
  32. data/lib/DhanHQ/models/forever_order.rb +261 -22
  33. data/lib/DhanHQ/models/funds.rb +76 -10
  34. data/lib/DhanHQ/models/historical_data.rb +148 -31
  35. data/lib/DhanHQ/models/holding.rb +82 -6
  36. data/lib/DhanHQ/models/instrument_helpers.rb +4 -4
  37. data/lib/DhanHQ/models/kill_switch.rb +113 -11
  38. data/lib/DhanHQ/models/ledger_entry.rb +101 -13
  39. data/lib/DhanHQ/models/margin.rb +133 -8
  40. data/lib/DhanHQ/models/market_feed.rb +181 -17
  41. data/lib/DhanHQ/models/option_chain.rb +184 -12
  42. data/lib/DhanHQ/models/order.rb +418 -36
  43. data/lib/DhanHQ/models/order_update.rb +0 -4
  44. data/lib/DhanHQ/models/position.rb +161 -10
  45. data/lib/DhanHQ/models/profile.rb +103 -7
  46. data/lib/DhanHQ/models/super_order.rb +275 -15
  47. data/lib/DhanHQ/models/trade.rb +279 -26
  48. data/lib/DhanHQ/rate_limiter.rb +40 -6
  49. data/lib/DhanHQ/resources/expired_options_data.rb +1 -1
  50. data/lib/DhanHQ/version.rb +1 -1
  51. data/lib/DhanHQ/ws/client.rb +11 -5
  52. data/lib/DhanHQ/ws/connection.rb +16 -2
  53. data/lib/DhanHQ/ws/market_depth/client.rb +2 -1
  54. data/lib/DhanHQ/ws/market_depth.rb +12 -12
  55. data/lib/DhanHQ/ws/orders/client.rb +78 -12
  56. data/lib/DhanHQ/ws/orders/connection.rb +2 -1
  57. data/lib/DhanHQ/ws/orders.rb +2 -1
  58. metadata +18 -5
  59. data/lib/DhanHQ/contracts/modify_order_contract_copy.rb +0 -100
@@ -3,11 +3,35 @@
3
3
  module DhanHQ
4
4
  module Models
5
5
  ##
6
- # Represents a single trade.
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
6
+ # Model for retrieving trade execution details.
7
+ #
8
+ # The Trade Book API lets you retrieve an array of all trades executed in a day.
9
+ # You can also fetch trade details for a specific order ID, which is useful during
10
+ # partial trades or Bracket/Cover Orders. Additionally, you can retrieve detailed
11
+ # trade history for all orders within a particular time frame.
12
+ #
13
+ # @example Fetch all trades for today
14
+ # trades = DhanHQ::Models::Trade.today
15
+ # trades.each do |trade|
16
+ # puts "#{trade.trading_symbol}: #{trade.traded_quantity} @ ₹#{trade.traded_price}"
17
+ # puts "Total Value: ₹#{trade.total_value}, Charges: ₹#{trade.total_charges}"
18
+ # end
19
+ #
20
+ # @example Fetch trades for a specific order
21
+ # trade = DhanHQ::Models::Trade.find_by_order_id("112111182045")
22
+ # if trade
23
+ # puts "Traded: #{trade.traded_quantity} @ ₹#{trade.traded_price}"
24
+ # end
25
+ #
26
+ # @example Fetch historical trades
27
+ # trades = DhanHQ::Models::Trade.history(
28
+ # from_date: "2022-12-01",
29
+ # to_date: "2022-12-31",
30
+ # page: 0
31
+ # )
32
+ # total_value = trades.sum(&:total_value)
33
+ # puts "Total traded value: ₹#{total_value}"
34
+ #
11
35
  class Trade < BaseModel
12
36
  HTTP_PATH = "/v2/trades"
13
37
 
@@ -22,22 +46,66 @@ module DhanHQ
22
46
 
23
47
  class << self
24
48
  ##
25
- # Resource for current day tradebook APIs
49
+ # Provides a shared instance of the Trades resource for current day tradebook APIs.
50
+ #
51
+ # @return [DhanHQ::Resources::Trades] The Trades resource client instance
26
52
  def tradebook_resource
27
53
  @tradebook_resource ||= DhanHQ::Resources::Trades.new
28
54
  end
29
55
 
30
56
  ##
31
- # Resource for historical trade data
57
+ # Provides a shared instance of the Statements resource for historical trade data.
58
+ #
59
+ # @return [DhanHQ::Resources::Statements] The Statements resource client instance
32
60
  def statements_resource
33
61
  @statements_resource ||= DhanHQ::Resources::Statements.new
34
62
  end
35
63
 
36
64
  ##
37
- # Fetch current day trades
38
- # GET /v2/trades
65
+ # Retrieves all trades executed during the current trading day.
66
+ #
67
+ # Fetches an array of all trades executed in the day. This is useful for
68
+ # tracking daily execution activity and analyzing trade performance.
69
+ #
70
+ # @return [Array<Trade>] Array of Trade objects. Returns empty array if no trades exist.
71
+ # Each Trade object contains (keys normalized to snake_case):
72
+ # - **:dhan_client_id** [String] User-specific identification generated by Dhan
73
+ # - **:order_id** [String] Order-specific identification generated by Dhan
74
+ # - **:exchange_order_id** [String] Order-specific identification generated by exchange
75
+ # - **:exchange_trade_id** [String] Trade-specific identification generated by exchange
76
+ # - **:transaction_type** [String] The trading side of transaction. "BUY" or "SELL"
77
+ # - **:exchange_segment** [String] Exchange segment of instrument
78
+ # - **:product_type** [String] Product type. Valid values: "CNC", "INTRADAY",
79
+ # "MARGIN", "MTF", "CO", "BO"
80
+ # - **:order_type** [String] Order type. Valid values: "LIMIT", "MARKET",
81
+ # "STOP_LOSS", "STOP_LOSS_MARKET"
82
+ # - **:trading_symbol** [String] Trading symbol of the instrument
83
+ # - **:security_id** [String] Exchange standard ID for each scrip
84
+ # - **:traded_quantity** [Integer] Number of shares executed
85
+ # - **:traded_price** [Float] Price at which trade is executed
86
+ # - **:create_time** [String] Time at which the order is created
87
+ # - **:update_time** [String] Time at which the last activity happened
88
+ # - **:exchange_time** [String] Time at which order reached at exchange
89
+ # - **:drv_expiry_date** [String, nil] For F&O, expiry date of contract
90
+ # - **:drv_option_type** [String, nil] Type of Option. "CALL" or "PUT"
91
+ # - **:drv_strike_price** [Float] For Options, Strike Price
92
+ #
93
+ # @example Fetch and analyze today's trades
94
+ # trades = DhanHQ::Models::Trade.today
95
+ # buy_trades = trades.select(&:buy?)
96
+ # sell_trades = trades.select(&:sell?)
97
+ # puts "Buy trades: #{buy_trades.count}, Sell trades: #{sell_trades.count}"
98
+ # total_value = trades.sum(&:total_value)
99
+ # puts "Total traded value: ₹#{total_value}"
100
+ #
101
+ # @example Calculate P&L for today
102
+ # trades = DhanHQ::Models::Trade.today
103
+ # buy_value = trades.select(&:buy?).sum(&:total_value)
104
+ # sell_value = trades.select(&:sell?).sum(&:total_value)
105
+ # total_charges = trades.sum(&:total_charges)
106
+ # pnl = sell_value - buy_value - total_charges
107
+ # puts "P&L: ₹#{pnl}"
39
108
  #
40
- # @return [Array<Trade>] Array of trades executed today
41
109
  def today
42
110
  response = tradebook_resource.all
43
111
  return [] unless response.is_a?(Array)
@@ -46,11 +114,28 @@ module DhanHQ
46
114
  end
47
115
 
48
116
  ##
49
- # Fetch trades for a specific order ID (current day)
50
- # GET /v2/trades/{order-id}
117
+ # Retrieves trade details for a specific order ID (current day).
118
+ #
119
+ # Fetches all trades generated for a particular order ID. This is especially useful
120
+ # during partial trades or Bracket/Cover Orders where traders may get confused
121
+ # reading trades from the tradebook. The response includes all trades generated
122
+ # for the specified order ID.
123
+ #
124
+ # @param order_id [String] Order-specific identification generated by Dhan
51
125
  #
52
- # @param order_id [String] The order ID to fetch trades for
53
- # @return [Trade, nil] Trade object or nil if not found
126
+ # @return [Trade, nil] Trade object with trade details if found, nil otherwise.
127
+ # Response structure is the same as {today}.
128
+ #
129
+ # @example Fetch trade for an order
130
+ # trade = DhanHQ::Models::Trade.find_by_order_id("112111182045")
131
+ # if trade
132
+ # puts "Order: #{trade.order_id}"
133
+ # puts "Traded: #{trade.traded_quantity} @ ₹#{trade.traded_price}"
134
+ # puts "Symbol: #{trade.trading_symbol}"
135
+ # puts "Time: #{trade.exchange_time}"
136
+ # end
137
+ #
138
+ # @raise [DhanHQ::ValidationError] If order_id validation fails
54
139
  def find_by_order_id(order_id)
55
140
  # Validate input
56
141
  contract = DhanHQ::Contracts::TradeByOrderIdContract.new
@@ -68,13 +153,61 @@ module DhanHQ
68
153
  end
69
154
 
70
155
  ##
71
- # Fetch historical trades within the given date range and page
72
- # GET /v2/trades/{from-date}/{to-date}/{page}
156
+ # Retrieves detailed trade history for all orders within a specified time frame.
73
157
  #
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
158
+ # Fetches paginated trade history data with comprehensive charge breakdowns.
159
+ # The response includes detailed information about SEBI tax, STT, brokerage charges,
160
+ # service tax, exchange transaction charges, and stamp duty for each trade.
161
+ #
162
+ # @param from_date [String] (required) Start date in "YYYY-MM-DD" format
163
+ # @param to_date [String] (required) End date in "YYYY-MM-DD" format
164
+ # @param page [Integer] (default: 0) Page number for paginated results. Pass 0 for first page
165
+ #
166
+ # @return [Array<Trade>] Array of historical Trade objects. Returns empty array if no trades found.
167
+ # Each Trade object contains all fields from {today} plus additional charge details:
168
+ # - **:custom_symbol** [String] Trading Symbol as per Dhan
169
+ # - **:isin** [String] Universal standard ID for each scrip (International Securities Identification Number)
170
+ # - **:instrument** [String] Type of Instrument. "EQUITY" or "DERIVATIVES"
171
+ # - **:sebi_tax** [String] SEBI Turnover Charges
172
+ # - **:stt** [String] Securities Transactions Tax
173
+ # - **:brokerage_charges** [String] Brokerage charges by Dhan
174
+ # - **:service_tax** [String] Applicable Service Tax
175
+ # - **:exchange_transaction_charges** [String] Exchange Transaction Charge
176
+ # - **:stamp_duty** [String] Stamp Duty Charges
177
+ #
178
+ # @example Fetch historical trades for a month
179
+ # trades = DhanHQ::Models::Trade.history(
180
+ # from_date: "2022-12-01",
181
+ # to_date: "2022-12-31",
182
+ # page: 0
183
+ # )
184
+ # puts "Total trades: #{trades.count}"
185
+ #
186
+ # @example Calculate total charges for historical period
187
+ # trades = DhanHQ::Models::Trade.history(
188
+ # from_date: "2022-12-01",
189
+ # to_date: "2022-12-31"
190
+ # )
191
+ # total_charges = trades.sum(&:total_charges)
192
+ # total_brokerage = trades.sum { |t| t.brokerage_charges.to_f }
193
+ # puts "Total Charges: ₹#{total_charges}"
194
+ # puts "Total Brokerage: ₹#{total_brokerage}"
195
+ #
196
+ # @example Paginate through historical trades
197
+ # page = 0
198
+ # loop do
199
+ # trades = DhanHQ::Models::Trade.history(
200
+ # from_date: "2022-12-01",
201
+ # to_date: "2022-12-31",
202
+ # page: page
203
+ # )
204
+ # break if trades.empty?
205
+ #
206
+ # puts "Page #{page}: #{trades.count} trades"
207
+ # page += 1
208
+ # end
209
+ #
210
+ # @raise [DhanHQ::ValidationError] If date format or page validation fails
78
211
  def history(from_date:, to_date:, page: 0)
79
212
  validate_history_params(from_date, to_date, page)
80
213
 
@@ -91,6 +224,16 @@ module DhanHQ
91
224
 
92
225
  private
93
226
 
227
+ ##
228
+ # Validates parameters for historical trade queries.
229
+ #
230
+ # @param from_date [String] Start date in YYYY-MM-DD format
231
+ # @param to_date [String] End date in YYYY-MM-DD format
232
+ # @param page [Integer] Page number
233
+ #
234
+ # @raise [DhanHQ::ValidationError] If validation fails
235
+ #
236
+ # @api private
94
237
  def validate_history_params(from_date, to_date, page)
95
238
  contract = DhanHQ::Contracts::TradeHistoryContract.new
96
239
  validation_result = contract.call(from_date: from_date, to_date: to_date, page: page)
@@ -100,48 +243,131 @@ module DhanHQ
100
243
  raise DhanHQ::ValidationError, "Invalid parameters: #{validation_result.errors.to_h}"
101
244
  end
102
245
 
103
- # Alias for backward compatibility
246
+ # Alias for backward compatibility with historical trades
247
+ # @api private
104
248
  alias all history
105
249
  end
106
250
 
107
251
  ##
108
- # Trade objects are read-only, so no validation contract needed
252
+ # Trade objects are read-only, so no validation contract needed.
253
+ #
254
+ # @return [nil] Always returns nil as trades are read-only
255
+ #
256
+ # @api private
109
257
  def validation_contract
110
258
  nil
111
259
  end
112
260
 
113
261
  ##
114
- # Helper methods for trade data
262
+ # Checks if the trade is a BUY transaction.
263
+ #
264
+ # @return [Boolean] true if transaction_type is "BUY", false otherwise
265
+ #
266
+ # @example
267
+ # trade = DhanHQ::Models::Trade.today.first
268
+ # if trade.buy?
269
+ # puts "This is a buy trade"
270
+ # end
271
+ #
115
272
  def buy?
116
273
  transaction_type == "BUY"
117
274
  end
118
275
 
276
+ ##
277
+ # Checks if the trade is a SELL transaction.
278
+ #
279
+ # @return [Boolean] true if transaction_type is "SELL", false otherwise
280
+ #
281
+ # @example
282
+ # trade = DhanHQ::Models::Trade.today.first
283
+ # if trade.sell?
284
+ # puts "This is a sell trade"
285
+ # end
286
+ #
119
287
  def sell?
120
288
  transaction_type == "SELL"
121
289
  end
122
290
 
291
+ ##
292
+ # Checks if the trade instrument is EQUITY.
293
+ #
294
+ # @return [Boolean] true if instrument is "EQUITY", false otherwise
295
+ #
296
+ # @example
297
+ # trades = DhanHQ::Models::Trade.today
298
+ # equity_trades = trades.select(&:equity?)
299
+ # puts "Equity trades: #{equity_trades.count}"
300
+ #
123
301
  def equity?
124
302
  instrument == "EQUITY"
125
303
  end
126
304
 
305
+ ##
306
+ # Checks if the trade instrument is DERIVATIVES.
307
+ #
308
+ # @return [Boolean] true if instrument is "DERIVATIVES", false otherwise
309
+ #
310
+ # @example
311
+ # trades = DhanHQ::Models::Trade.today
312
+ # derivative_trades = trades.select(&:derivative?)
313
+ # puts "Derivative trades: #{derivative_trades.count}"
314
+ #
127
315
  def derivative?
128
316
  instrument == "DERIVATIVES"
129
317
  end
130
318
 
319
+ ##
320
+ # Checks if the trade is an option (CALL or PUT).
321
+ #
322
+ # @return [Boolean] true if drv_option_type is "CALL" or "PUT", false otherwise
323
+ #
324
+ # @example
325
+ # trades = DhanHQ::Models::Trade.today
326
+ # option_trades = trades.select(&:option?)
327
+ # puts "Option trades: #{option_trades.count}"
328
+ #
131
329
  def option?
132
330
  %w[CALL PUT].include?(drv_option_type)
133
331
  end
134
332
 
333
+ ##
334
+ # Checks if the trade is a CALL option.
335
+ #
336
+ # @return [Boolean] true if drv_option_type is "CALL", false otherwise
337
+ #
338
+ # @example
339
+ # trades = DhanHQ::Models::Trade.today
340
+ # call_trades = trades.select(&:call_option?)
341
+ # puts "Call option trades: #{call_trades.count}"
342
+ #
135
343
  def call_option?
136
344
  drv_option_type == "CALL"
137
345
  end
138
346
 
347
+ ##
348
+ # Checks if the trade is a PUT option.
349
+ #
350
+ # @return [Boolean] true if drv_option_type is "PUT", false otherwise
351
+ #
352
+ # @example
353
+ # trades = DhanHQ::Models::Trade.today
354
+ # put_trades = trades.select(&:put_option?)
355
+ # puts "Put option trades: #{put_trades.count}"
356
+ #
139
357
  def put_option?
140
358
  drv_option_type == "PUT"
141
359
  end
142
360
 
143
361
  ##
144
- # Calculate total trade value
362
+ # Calculates the total trade value (quantity × price).
363
+ #
364
+ # @return [Float] Total trade value. Returns 0 if traded_quantity or traded_price is missing
365
+ #
366
+ # @example
367
+ # trade = DhanHQ::Models::Trade.today.first
368
+ # puts "Trade Value: ₹#{trade.total_value}"
369
+ # # => Trade Value: ₹133832.0
370
+ #
145
371
  def total_value
146
372
  return 0 unless traded_quantity && traded_price
147
373
 
@@ -149,7 +375,22 @@ module DhanHQ
149
375
  end
150
376
 
151
377
  ##
152
- # Calculate total charges
378
+ # Calculates the total charges for the trade.
379
+ #
380
+ # Sums all applicable charges including SEBI tax, STT, brokerage charges,
381
+ # service tax, exchange transaction charges, and stamp duty.
382
+ #
383
+ # @return [Float] Total charges amount. Returns 0 if no charges are present
384
+ #
385
+ # @example
386
+ # trade = DhanHQ::Models::Trade.history(
387
+ # from_date: "2022-12-01",
388
+ # to_date: "2022-12-31"
389
+ # ).first
390
+ # puts "Total Charges: ₹#{trade.total_charges}"
391
+ # puts "Brokerage: ₹#{trade.brokerage_charges}"
392
+ # puts "STT: ₹#{trade.stt}"
393
+ #
153
394
  def total_charges
154
395
  charges = [sebi_tax, stt, brokerage_charges, service_tax,
155
396
  exchange_transaction_charges, stamp_duty].compact
@@ -157,7 +398,19 @@ module DhanHQ
157
398
  end
158
399
 
159
400
  ##
160
- # Net trade value after charges
401
+ # Calculates the net trade value after deducting all charges.
402
+ #
403
+ # @return [Float] Net trade value (total_value - total_charges)
404
+ #
405
+ # @example
406
+ # trade = DhanHQ::Models::Trade.history(
407
+ # from_date: "2022-12-01",
408
+ # to_date: "2022-12-31"
409
+ # ).first
410
+ # puts "Gross Value: ₹#{trade.total_value}"
411
+ # puts "Charges: ₹#{trade.total_charges}"
412
+ # puts "Net Value: ₹#{trade.net_value}"
413
+ #
161
414
  def net_value
162
415
  total_value - total_charges
163
416
  end
@@ -140,25 +140,59 @@ module DhanHQ
140
140
 
141
141
  # Spawns background threads to reset counters after each interval elapses.
142
142
  def start_cleanup_threads
143
+ @cleanup_threads = []
144
+ @shutdown = Concurrent::AtomicBoolean.new(false)
145
+
143
146
  # Don't create per_second cleanup thread - we handle it with timestamps
144
- Thread.new do
147
+ @cleanup_threads << Thread.new do
145
148
  loop do
149
+ break if @shutdown.true?
150
+
146
151
  sleep(60)
147
- @buckets[:per_minute]&.value = 0
152
+ break if @shutdown.true?
153
+
154
+ mutex.synchronize do
155
+ @buckets[:per_minute]&.value = 0
156
+ end
148
157
  end
149
158
  end
150
- Thread.new do
159
+
160
+ @cleanup_threads << Thread.new do
151
161
  loop do
162
+ break if @shutdown.true?
163
+
152
164
  sleep(3600)
153
- @buckets[:per_hour]&.value = 0
165
+ break if @shutdown.true?
166
+
167
+ mutex.synchronize do
168
+ @buckets[:per_hour]&.value = 0
169
+ end
154
170
  end
155
171
  end
156
- Thread.new do
172
+
173
+ @cleanup_threads << Thread.new do
157
174
  loop do
175
+ break if @shutdown.true?
176
+
158
177
  sleep(86_400)
159
- @buckets[:per_day]&.value = 0
178
+ break if @shutdown.true?
179
+
180
+ mutex.synchronize do
181
+ @buckets[:per_day]&.value = 0
182
+ end
160
183
  end
161
184
  end
162
185
  end
186
+
187
+ # Shuts down cleanup threads gracefully
188
+ def shutdown
189
+ return if @shutdown.true?
190
+
191
+ @shutdown.make_true
192
+ @cleanup_threads&.each do |thread|
193
+ thread&.wakeup if thread&.alive?
194
+ thread&.join(5) # Wait up to 5 seconds for thread to finish
195
+ end
196
+ end
163
197
  end
164
198
  end
@@ -6,7 +6,7 @@ module DhanHQ
6
6
  # Resource for expired options data API endpoints
7
7
  class ExpiredOptionsData < BaseAPI
8
8
  API_TYPE = :data_api
9
- HTTP_PATH = "/charts"
9
+ HTTP_PATH = "/v2/charts"
10
10
 
11
11
  ##
12
12
  # Fetch expired options data for rolling contracts
@@ -2,5 +2,5 @@
2
2
 
3
3
  module DhanHQ
4
4
  # Semantic version of the DhanHQ client gem.
5
- VERSION = "2.1.8"
5
+ VERSION = "2.2.0"
6
6
  end
@@ -27,7 +27,8 @@ module DhanHQ
27
27
  @callbacks = Concurrent::Map.new { |h, k| h[k] = [] }
28
28
  @started = Concurrent::AtomicBoolean.new(false)
29
29
 
30
- token = DhanHQ.configuration.access_token or raise "DhanHQ.access_token not set"
30
+ token = DhanHQ.configuration.resolved_access_token
31
+ raise DhanHQ::AuthenticationError, "Missing access token" if token.nil? || token.empty?
31
32
  cid = DhanHQ.configuration.client_id or raise "DhanHQ.client_id not set"
32
33
  ver = (DhanHQ.configuration.respond_to?(:ws_version) && DhanHQ.configuration.ws_version) || 2
33
34
  @url = url || "wss://api-feed.dhan.co?version=#{ver}&token=#{token}&clientId=#{cid}&authType=2"
@@ -164,11 +165,16 @@ module DhanHQ
164
165
  def prune(hash) = { ExchangeSegment: hash[:ExchangeSegment], SecurityId: hash[:SecurityId] }
165
166
 
166
167
  def emit(event, payload)
167
- begin
168
- @callbacks[event].dup
168
+ # Create a frozen snapshot of callbacks to avoid modification during iteration
169
+ callbacks_snapshot = begin
170
+ @callbacks[event].dup.freeze
169
171
  rescue StandardError
170
- []
171
- end.each { |cb| cb.call(payload) }
172
+ [].freeze
173
+ end
174
+
175
+ callbacks_snapshot.each { |cb| cb.call(payload) }
176
+ rescue StandardError => e
177
+ DhanHQ.logger&.error("[DhanHQ::WS::Client] Error in event handler for #{event}: #{e.class} #{e.message}")
172
178
  end
173
179
 
174
180
  def install_at_exit_once!
@@ -141,8 +141,20 @@ module DhanHQ
141
141
  end
142
142
  rescue StandardError => e
143
143
  DhanHQ.logger&.error("[DhanHQ::WS] crashed #{e.class} #{e.message}")
144
+ DhanHQ.logger&.error("[DhanHQ::WS] backtrace: #{e.backtrace&.first(5)&.join("\n")}")
144
145
  failed = true
146
+ # Reset connection state on error
147
+ @ws = nil
148
+ @timer = nil
145
149
  ensure
150
+ # Clean up EventMachine resources
151
+ begin
152
+ EM.cancel_timer(@timer) if @timer
153
+ @timer = nil
154
+ rescue StandardError => e
155
+ DhanHQ.logger&.debug("[DhanHQ::WS] cleanup error: #{e.message}")
156
+ end
157
+
146
158
  break if @stop
147
159
 
148
160
  if got_429
@@ -154,11 +166,13 @@ module DhanHQ
154
166
  # exponential backoff with jitter
155
167
  sleep_time = [backoff, MAX_BACKOFF].min
156
168
  jitter = rand(0.2 * sleep_time)
157
- DhanHQ.logger&.warn("[DhanHQ::WS] reconnecting in #{(sleep_time + jitter).round(1)}s")
169
+ DhanHQ.logger&.warn("[DhanHQ::WS] reconnecting in #{(sleep_time + jitter).round(1)}s (backoff: #{backoff.round(1)}s)")
158
170
  sleep(sleep_time + jitter)
159
171
  backoff *= 2.0
160
172
  else
161
- backoff = 2.0 # reset only after a clean session end
173
+ # Reset backoff only after a clean session end (normal close with code 1000)
174
+ backoff = 2.0
175
+ DhanHQ.logger&.debug("[DhanHQ::WS] backoff reset to 2.0s after clean session end")
162
176
  end
163
177
  end
164
178
  end
@@ -80,7 +80,8 @@ module DhanHQ
80
80
  # @param config [Configuration] DhanHQ configuration
81
81
  # @return [String] WebSocket URL
82
82
  def build_market_depth_url(config)
83
- token = config.access_token or raise "DhanHQ.access_token not set"
83
+ token = config.resolved_access_token
84
+ raise DhanHQ::AuthenticationError, "Missing access token" if token.nil? || token.empty?
84
85
  cid = config.client_id or raise "DhanHQ.client_id not set"
85
86
  depth_level = config.market_depth_level || 20 # Default to 20 level depth
86
87
 
@@ -17,9 +17,9 @@ module DhanHQ
17
17
  # @param options [Hash] Connection options
18
18
  # @param block [Proc] Callback for depth updates
19
19
  # @return [Client] WebSocket client instance
20
- def connect(symbols: [], **options, &block)
21
- client = Client.new(symbols: symbols, **options)
22
- client.on(:depth_update, &block) if block_given?
20
+ def connect(symbols: [], **, &)
21
+ client = Client.new(symbols: symbols, **)
22
+ client.on(:depth_update, &) if block_given?
23
23
  client.start
24
24
  end
25
25
 
@@ -28,8 +28,8 @@ module DhanHQ
28
28
  # @param symbols [Array<String>] Symbols to subscribe to
29
29
  # @param options [Hash] Connection options
30
30
  # @return [Client] New client instance
31
- def client(symbols: [], **options)
32
- Client.new(symbols: symbols, **options)
31
+ def client(symbols: [], **)
32
+ Client.new(symbols: symbols, **)
33
33
  end
34
34
 
35
35
  ##
@@ -38,8 +38,8 @@ module DhanHQ
38
38
  # @param handlers [Hash] Event handlers
39
39
  # @param options [Hash] Connection options
40
40
  # @return [Client] Started client instance
41
- def connect_with_handlers(symbols: [], handlers: {}, **options)
42
- client = Client.new(symbols: symbols, **options).start
41
+ def connect_with_handlers(symbols: [], handlers: {}, **)
42
+ client = Client.new(symbols: symbols, **).start
43
43
 
44
44
  handlers.each do |event, handler|
45
45
  client.on(event, &handler)
@@ -54,8 +54,8 @@ module DhanHQ
54
54
  # @param options [Hash] Connection options
55
55
  # @param block [Proc] Callback for depth updates
56
56
  # @return [Client] Started client instance
57
- def subscribe(symbols:, **options, &block)
58
- connect(symbols: symbols, **options, &block)
57
+ def subscribe(symbols:, **, &)
58
+ connect(symbols: symbols, **, &)
59
59
  end
60
60
 
61
61
  ##
@@ -64,9 +64,9 @@ module DhanHQ
64
64
  # @param options [Hash] Connection options
65
65
  # @param block [Proc] Callback for snapshot data
66
66
  # @return [Client] Started client instance
67
- def snapshot(symbols:, **options, &block)
68
- client = Client.new(symbols: symbols, **options)
69
- client.on(:depth_snapshot, &block) if block_given?
67
+ def snapshot(symbols:, **, &)
68
+ client = Client.new(symbols: symbols, **)
69
+ client.on(:depth_snapshot, &) if block_given?
70
70
  client.start
71
71
  end
72
72
  end