DhanHQ 2.1.10 → 2.2.1

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/.rubocop_todo.yml +143 -118
  4. data/CHANGELOG.md +127 -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 +27 -0
  23. data/lib/DhanHQ/constants.rb +1 -1
  24. data/lib/DhanHQ/contracts/place_order_contract.rb +51 -0
  25. data/lib/DhanHQ/core/base_model.rb +17 -10
  26. data/lib/DhanHQ/errors.rb +4 -0
  27. data/lib/DhanHQ/helpers/request_helper.rb +17 -2
  28. data/lib/DhanHQ/helpers/response_helper.rb +34 -13
  29. data/lib/DhanHQ/models/expired_options_data.rb +0 -6
  30. data/lib/DhanHQ/models/instrument_helpers.rb +4 -4
  31. data/lib/DhanHQ/models/order.rb +19 -2
  32. data/lib/DhanHQ/models/order_update.rb +0 -4
  33. data/lib/DhanHQ/rate_limiter.rb +40 -6
  34. data/lib/DhanHQ/version.rb +1 -1
  35. data/lib/DhanHQ/ws/client.rb +12 -5
  36. data/lib/DhanHQ/ws/connection.rb +16 -2
  37. data/lib/DhanHQ/ws/market_depth/client.rb +3 -1
  38. data/lib/DhanHQ/ws/market_depth.rb +12 -12
  39. data/lib/DhanHQ/ws/orders/client.rb +76 -11
  40. data/lib/DhanHQ/ws/orders/connection.rb +3 -1
  41. metadata +18 -5
  42. data/lib/DhanHQ/contracts/modify_order_contract_copy.rb +0 -100
@@ -5,7 +5,6 @@ module DhanHQ
5
5
  ##
6
6
  # Represents a real-time order update received via WebSocket
7
7
  # Parses and provides access to all order update fields as per DhanHQ API documentation
8
- # rubocop:disable Metrics/ClassLength
9
8
  class OrderUpdate < BaseModel
10
9
  # All order update attributes as per API documentation
11
10
  attributes :exchange, :segment, :source, :security_id, :client_id,
@@ -206,7 +205,6 @@ module DhanHQ
206
205
 
207
206
  ##
208
207
  # Status summary for logging/debugging
209
- # rubocop:disable Metrics/MethodLength
210
208
  def status_summary
211
209
  {
212
210
  order_no: order_no,
@@ -222,7 +220,6 @@ module DhanHQ
222
220
  super_order: super_order?
223
221
  }
224
222
  end
225
- # rubocop:enable Metrics/MethodLength
226
223
 
227
224
  ##
228
225
  # Convert to hash for serialization
@@ -230,6 +227,5 @@ module DhanHQ
230
227
  @attributes.dup
231
228
  end
232
229
  end
233
- # rubocop:enable Metrics/ClassLength
234
230
  end
235
231
  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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module DhanHQ
4
4
  # Semantic version of the DhanHQ client gem.
5
- VERSION = "2.1.10"
5
+ VERSION = "2.2.1"
6
6
  end
@@ -27,7 +27,9 @@ 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?
32
+
31
33
  cid = DhanHQ.configuration.client_id or raise "DhanHQ.client_id not set"
32
34
  ver = (DhanHQ.configuration.respond_to?(:ws_version) && DhanHQ.configuration.ws_version) || 2
33
35
  @url = url || "wss://api-feed.dhan.co?version=#{ver}&token=#{token}&clientId=#{cid}&authType=2"
@@ -164,11 +166,16 @@ module DhanHQ
164
166
  def prune(hash) = { ExchangeSegment: hash[:ExchangeSegment], SecurityId: hash[:SecurityId] }
165
167
 
166
168
  def emit(event, payload)
167
- begin
168
- @callbacks[event].dup
169
+ # Create a frozen snapshot of callbacks to avoid modification during iteration
170
+ callbacks_snapshot = begin
171
+ @callbacks[event].dup.freeze
169
172
  rescue StandardError
170
- []
171
- end.each { |cb| cb.call(payload) }
173
+ [].freeze
174
+ end
175
+
176
+ callbacks_snapshot.each { |cb| cb.call(payload) }
177
+ rescue StandardError => e
178
+ DhanHQ.logger&.error("[DhanHQ::WS::Client] Error in event handler for #{event}: #{e.class} #{e.message}")
172
179
  end
173
180
 
174
181
  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,9 @@ 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?
85
+
84
86
  cid = config.client_id or raise "DhanHQ.client_id not set"
85
87
  depth_level = config.market_depth_level || 20 # Default to 20 level depth
86
88
 
@@ -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
@@ -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,14 +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
187
199
  #
188
200
  # @param order_update [OrderUpdate] Current order update
189
201
  # @param _previous_state [OrderUpdate, nil] Previous order state (unused parameter)
190
- # rubocop:disable Metrics/MethodLength
191
202
  def emit_status_specific_events(order_update, _previous_state)
192
203
  case order_update.status
193
204
  when "TRANSIT"
@@ -204,24 +215,78 @@ module DhanHQ
204
215
  emit(:order_expired, order_update)
205
216
  end
206
217
  end
207
- # rubocop:enable Metrics/MethodLength
208
218
 
209
219
  ##
210
220
  # Emit events to registered callbacks
211
221
  # @param event [Symbol] Event type
212
222
  # @param payload [Object] Event payload
213
223
  def emit(event, payload)
214
- list = begin
215
- @callbacks[event]
224
+ # Create a snapshot of callbacks to avoid modification during iteration
225
+ callbacks_snapshot = begin
226
+ @callbacks[event].dup.freeze
216
227
  rescue StandardError
217
- []
228
+ [].freeze
218
229
  end
219
- list.each { |cb| cb.call(payload) }
230
+
231
+ callbacks_snapshot.each { |cb| cb.call(payload) }
220
232
  rescue StandardError => e
221
233
  DhanHQ.logger&.error("[DhanHQ::WS::Orders] Error in event handler: #{e.class} #{e.message}")
222
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
223
289
  end
224
- # rubocop:enable Metrics/ClassLength
225
290
  end
226
291
  end
227
292
  end
@@ -86,7 +86,9 @@ 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?
91
+
90
92
  cid = cfg.client_id or raise "DhanHQ.client_id not set"
91
93
  payload = {
92
94
  LoginReq: { MsgCode: 42, ClientId: cid, Token: token },
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.10
4
+ version: 2.2.1
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