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
@@ -9,12 +9,20 @@ module DhanHQ
9
9
  ##
10
10
  # Enhanced WebSocket client for real-time order updates
11
11
  # Provides comprehensive order state tracking and event handling
12
- # rubocop:disable Metrics/ClassLength
13
12
  class Client
13
+ # Maximum number of orders to keep in tracker (default: 10,000)
14
+ MAX_TRACKED_ORDERS = ENV.fetch("DHAN_WS_MAX_TRACKED_ORDERS", 10_000).to_i
15
+
16
+ # Maximum age of orders in tracker in seconds (default: 7 days)
17
+ MAX_ORDER_AGE = ENV.fetch("DHAN_WS_MAX_ORDER_AGE", 604_800).to_i
18
+
14
19
  def initialize(url: nil, **options)
15
20
  @callbacks = Concurrent::Map.new { |h, k| h[k] = [] }
16
21
  @started = Concurrent::AtomicBoolean.new(false)
17
22
  @order_tracker = Concurrent::Map.new
23
+ @order_timestamps = Concurrent::Map.new
24
+ @cleanup_mutex = Mutex.new
25
+ @cleanup_thread = nil
18
26
  cfg = DhanHQ.configuration
19
27
  @url = url || cfg.ws_order_url
20
28
  @connection_options = options
@@ -33,6 +41,7 @@ module DhanHQ
33
41
  @conn.on(:error) { |error| emit(:error, error) }
34
42
  @conn.on(:message) { |msg| handle_message(msg) }
35
43
  @conn.start
44
+ start_cleanup_thread
36
45
  DhanHQ::WS::Registry.register(self) if defined?(DhanHQ::WS::Registry)
37
46
  self
38
47
  end
@@ -44,6 +53,7 @@ module DhanHQ
44
53
  return unless @started.true?
45
54
 
46
55
  @started.make_false
56
+ stop_cleanup_thread
47
57
  @conn&.stop
48
58
  emit(:close, true)
49
59
  DhanHQ::WS::Registry.unregister(self) if defined?(DhanHQ::WS::Registry)
@@ -147,13 +157,16 @@ module DhanHQ
147
157
  ##
148
158
  # Handle order update and track state changes
149
159
  # @param order_update [OrderUpdate] Parsed order update
150
- # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
151
160
  def handle_order_update(order_update)
152
161
  order_no = order_update.order_no
153
162
  previous_state = @order_tracker[order_no]
154
163
 
155
- # Update order tracker
164
+ # Update order tracker with timestamp
156
165
  @order_tracker[order_no] = order_update
166
+ @order_timestamps[order_no] = Time.now
167
+
168
+ # Cleanup if tracker exceeds max size
169
+ cleanup_old_orders if @order_tracker.size > MAX_TRACKED_ORDERS
157
170
 
158
171
  # Emit update event
159
172
  emit(:update, order_update)
@@ -180,13 +193,12 @@ module DhanHQ
180
193
  # Emit specific status events
181
194
  emit_status_specific_events(order_update, previous_state)
182
195
  end
183
- # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
184
196
 
185
197
  ##
186
198
  # Emit status-specific events
199
+ #
187
200
  # @param order_update [OrderUpdate] Current order update
188
- # @param previous_state [OrderUpdate, nil] Previous order state
189
- # rubocop:disable Metrics/MethodLength
201
+ # @param _previous_state [OrderUpdate, nil] Previous order state (unused parameter)
190
202
  def emit_status_specific_events(order_update, _previous_state)
191
203
  case order_update.status
192
204
  when "TRANSIT"
@@ -203,24 +215,78 @@ module DhanHQ
203
215
  emit(:order_expired, order_update)
204
216
  end
205
217
  end
206
- # rubocop:enable Metrics/MethodLength
207
218
 
208
219
  ##
209
220
  # Emit events to registered callbacks
210
221
  # @param event [Symbol] Event type
211
222
  # @param payload [Object] Event payload
212
223
  def emit(event, payload)
213
- list = begin
214
- @callbacks[event]
224
+ # Create a snapshot of callbacks to avoid modification during iteration
225
+ callbacks_snapshot = begin
226
+ @callbacks[event].dup.freeze
215
227
  rescue StandardError
216
- []
228
+ [].freeze
217
229
  end
218
- list.each { |cb| cb.call(payload) }
230
+
231
+ callbacks_snapshot.each { |cb| cb.call(payload) }
219
232
  rescue StandardError => e
220
233
  DhanHQ.logger&.error("[DhanHQ::WS::Orders] Error in event handler: #{e.class} #{e.message}")
221
234
  end
235
+
236
+ ##
237
+ # Start cleanup thread to periodically remove old orders
238
+ def start_cleanup_thread
239
+ return if @cleanup_thread&.alive?
240
+
241
+ @cleanup_thread = Thread.new do
242
+ loop do
243
+ break unless @started.true?
244
+
245
+ sleep(3600) # Run cleanup every hour
246
+ break unless @started.true?
247
+
248
+ cleanup_old_orders
249
+ end
250
+ end
251
+ end
252
+
253
+ ##
254
+ # Stop cleanup thread
255
+ def stop_cleanup_thread
256
+ return unless @cleanup_thread&.alive?
257
+
258
+ @cleanup_thread.wakeup
259
+ @cleanup_thread.join(5) # Wait up to 5 seconds
260
+ @cleanup_thread = nil
261
+ end
262
+
263
+ ##
264
+ # Clean up old orders from tracker
265
+ def cleanup_old_orders
266
+ @cleanup_mutex.synchronize do
267
+ now = Time.now
268
+ orders_to_remove = []
269
+
270
+ # Find orders to remove (too old or if tracker is too large)
271
+ @order_timestamps.each do |order_no, timestamp|
272
+ age = now - timestamp
273
+ if age > MAX_ORDER_AGE || (@order_tracker.size > MAX_TRACKED_ORDERS && orders_to_remove.size < @order_tracker.size - MAX_TRACKED_ORDERS)
274
+ orders_to_remove << order_no
275
+ end
276
+ end
277
+
278
+ # Remove old orders
279
+ orders_to_remove.each do |order_no|
280
+ @order_tracker.delete(order_no)
281
+ @order_timestamps.delete(order_no)
282
+ end
283
+
284
+ if orders_to_remove.any?
285
+ DhanHQ.logger&.debug("[DhanHQ::WS::Orders] Cleaned up #{orders_to_remove.size} old orders")
286
+ end
287
+ end
288
+ end
222
289
  end
223
- # rubocop:enable Metrics/ClassLength
224
290
  end
225
291
  end
226
292
  end
@@ -86,7 +86,8 @@ module DhanHQ
86
86
  Secret: cfg.partner_secret
87
87
  }
88
88
  else
89
- token = cfg.access_token or raise "DhanHQ.access_token not set"
89
+ token = cfg.resolved_access_token
90
+ raise DhanHQ::AuthenticationError, "Missing access token" if token.nil? || token.empty?
90
91
  cid = cfg.client_id or raise "DhanHQ.client_id not set"
91
92
  payload = {
92
93
  LoginReq: { MsgCode: 42, ClientId: cid, Token: token },
@@ -10,7 +10,8 @@ module DhanHQ
10
10
  module Orders
11
11
  ##
12
12
  # Connect to order updates WebSocket with a simple callback
13
- # @param block [Proc] Callback for order updates
13
+ #
14
+ # @yield [OrderUpdate] Yields order update objects when received
14
15
  # @return [Client] WebSocket client instance
15
16
  def self.connect(&)
16
17
  Client.new.start.on(:update, &)
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: DhanHQ
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.8
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shubham Taywade
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-01-31 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: activesupport
@@ -162,20 +163,31 @@ files:
162
163
  - ".rubocop_todo.yml"
163
164
  - CHANGELOG.md
164
165
  - CODE_OF_CONDUCT.md
166
+ - CODE_REVIEW_ISSUES.md
167
+ - FIXES_APPLIED.md
165
168
  - GUIDE.md
166
169
  - LICENSE.txt
167
170
  - README.md
168
171
  - README1.md
172
+ - RELEASING.md
173
+ - REVIEW_SUMMARY.md
169
174
  - Rakefile
170
175
  - TAGS
171
176
  - TODO-1.md
172
177
  - TODO.md
178
+ - VERSION_UPDATE.md
173
179
  - app/services/live/order_update_guard_support.rb
174
180
  - app/services/live/order_update_hub.rb
175
181
  - app/services/live/order_update_persistence_support.rb
176
182
  - config/initializers/order_update_hub.rb
183
+ - core
177
184
  - diagram.html
178
185
  - diagram.md
186
+ - docs/AUTHENTICATION.md
187
+ - docs/DATA_API_PARAMETERS.md
188
+ - docs/PR_2.2.0.md
189
+ - docs/RELEASE_GUIDE.md
190
+ - docs/TESTING_GUIDE.md
179
191
  - docs/live_order_updates.md
180
192
  - docs/rails_integration.md
181
193
  - docs/rails_websocket_integration.md
@@ -200,7 +212,6 @@ files:
200
212
  - lib/DhanHQ/contracts/instrument_list_contract.rb
201
213
  - lib/DhanHQ/contracts/margin_calculator_contract.rb
202
214
  - lib/DhanHQ/contracts/modify_order_contract.rb
203
- - lib/DhanHQ/contracts/modify_order_contract_copy.rb
204
215
  - lib/DhanHQ/contracts/option_chain_contract.rb
205
216
  - lib/DhanHQ/contracts/order_contract.rb
206
217
  - lib/DhanHQ/contracts/place_order_contract.rb
@@ -312,6 +323,7 @@ metadata:
312
323
  source_code_uri: https://github.com/shubhamtaywade82/dhanhq-client
313
324
  changelog_uri: https://github.com/shubhamtaywade82/dhanhq-client/blob/main/CHANGELOG.md
314
325
  rubygems_mfa_required: 'true'
326
+ post_install_message:
315
327
  rdoc_options: []
316
328
  require_paths:
317
329
  - lib
@@ -319,14 +331,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
319
331
  requirements:
320
332
  - - ">="
321
333
  - !ruby/object:Gem::Version
322
- version: 3.1.0
334
+ version: 3.2.0
323
335
  required_rubygems_version: !ruby/object:Gem::Requirement
324
336
  requirements:
325
337
  - - ">="
326
338
  - !ruby/object:Gem::Version
327
339
  version: '0'
328
340
  requirements: []
329
- rubygems_version: 3.6.9
341
+ rubygems_version: 3.5.11
342
+ signing_key:
330
343
  specification_version: 4
331
344
  summary: DhanHQ is a simple CLI for DhanHQ API.
332
345
  test_files: []
@@ -1,100 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DhanHQ
4
- module Contracts
5
- # Validation contract for modifying an existing order via Dhanhq's API.
6
- #
7
- # This contract validates input parameters for the Modify Order API,
8
- # ensuring that all required fields are provided and optional fields follow
9
- # the correct constraints. It also applies custom validation rules based on
10
- # the type of order.
11
- #
12
- # Example usage:
13
- # contract = Dhanhq::Contracts::ModifyOrderContract.new
14
- # result = contract.call(
15
- # dhanClientId: "123456",
16
- # orderId: "1001",
17
- # orderType: "STOP_LOSS",
18
- # legName: "ENTRY_LEG",
19
- # quantity: 10,
20
- # price: 150.0,
21
- # triggerPrice: 140.0,
22
- # validity: "DAY"
23
- # )
24
- # result.success? # => true or false
25
- #
26
- # @see https://dhanhq.co/docs/v2/ Dhanhq API Documentation
27
- class ModifyOrderContract < BaseContract
28
- # Parameters and validation rules for the Modify Order request.
29
- #
30
- # @!attribute [r] orderId
31
- # @return [String] Required. Unique identifier for the order to be modified.
32
- # @!attribute [r] orderType
33
- # @return [String] Required. Type of the order.
34
- # Must be one of: LIMIT, MARKET, STOP_LOSS, STOP_LOSS_MARKET.
35
- # @!attribute [r] legName
36
- # @return [String] Optional. Leg name for complex orders.
37
- # Must be one of: ENTRY_LEG, TARGET_LEG, STOP_LOSS_LEG, NA.
38
- # @!attribute [r] quantity
39
- # @return [Integer] Required. Quantity to be modified, must be greater than 0.
40
- # @!attribute [r] price
41
- # @return [Float] Optional. Price to be modified, must be greater than 0 if provided.
42
- # @!attribute [r] disclosedQuantity
43
- # @return [Integer] Optional. Disclosed quantity, must be >= 0 if provided.
44
- # @!attribute [r] triggerPrice
45
- # @return [Float] Optional. Trigger price for stop-loss orders, must be greater than 0 if provided.
46
- # @!attribute [r] validity
47
- # @return [String] Required. Validity of the order.
48
- # Must be one of: DAY, IOC, GTC, GTD.
49
- params do
50
- required(:orderId).filled(:string)
51
- required(:orderType).filled(:string, included_in?: %w[LIMIT MARKET STOP_LOSS STOP_LOSS_MARKET])
52
- optional(:legName).maybe(:string, included_in?: %w[ENTRY_LEG TARGET_LEG STOP_LOSS_LEG NA])
53
- required(:quantity).filled(:integer, gt?: 0)
54
- optional(:price).maybe(:float, gt?: 0)
55
- optional(:disclosedQuantity).maybe(:integer, gteq?: 0)
56
- optional(:triggerPrice).maybe(:float, gt?: 0)
57
- required(:validity).filled(:string, included_in?: %w[DAY IOC GTC GTD])
58
- end
59
-
60
- # Custom validation to ensure a trigger price is provided for stop-loss orders.
61
- #
62
- # @example Invalid stop-loss order:
63
- # orderType: "STOP_LOSS", triggerPrice: nil
64
- # => Adds failure message "is required for orderType STOP_LOSS or STOP_LOSS_MARKET".
65
- #
66
- # @param triggerPrice [Float] The price at which the order will be triggered.
67
- # @param orderType [String] The type of the order.
68
- rule(:triggerPrice, :orderType) do
69
- if values[:orderType].start_with?("STOP_LOSS") && !values[:triggerPrice]
70
- key(:triggerPrice).failure("is required for orderType STOP_LOSS or STOP_LOSS_MARKET")
71
- end
72
- end
73
-
74
- # Custom validation to ensure a leg name is provided for CO or BO order types.
75
- #
76
- # @example Invalid CO order:
77
- # orderType: "CO", legName: nil
78
- # => Adds failure message "is required for orderType CO or BO".
79
- #
80
- # @param legName [String] The leg name of the order.
81
- # @param orderType [String] The type of the order.
82
- rule(:legName, :orderType) do
83
- if %w[CO BO].include?(values[:orderType]) && !values[:legName]
84
- key(:legName).failure("is required for orderType CO or BO")
85
- end
86
- end
87
-
88
- # Custom validation to ensure the price is valid if provided.
89
- #
90
- # @example Invalid price:
91
- # price: 0
92
- # => Adds failure message "must be greater than 0 if provided".
93
- #
94
- # @param price [Float] The price of the order.
95
- rule(:price) do
96
- key(:price).failure("must be greater than 0 if provided") if values[:price].nil? || values[:price] <= 0
97
- end
98
- end
99
- end
100
- end