DhanHQ 2.1.10 → 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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +143 -118
- data/CHANGELOG.md +127 -0
- data/CODE_REVIEW_ISSUES.md +397 -0
- data/FIXES_APPLIED.md +373 -0
- data/GUIDE.md +41 -0
- data/README.md +55 -0
- data/RELEASING.md +60 -0
- data/REVIEW_SUMMARY.md +120 -0
- data/VERSION_UPDATE.md +82 -0
- data/core +0 -0
- data/docs/AUTHENTICATION.md +63 -0
- data/docs/DATA_API_PARAMETERS.md +278 -0
- data/docs/PR_2.2.0.md +48 -0
- data/docs/RELEASE_GUIDE.md +492 -0
- data/docs/TESTING_GUIDE.md +1514 -0
- data/docs/live_order_updates.md +25 -1
- data/docs/rails_integration.md +29 -0
- data/docs/websocket_integration.md +22 -0
- data/lib/DhanHQ/client.rb +65 -9
- data/lib/DhanHQ/configuration.rb +26 -0
- data/lib/DhanHQ/constants.rb +1 -1
- data/lib/DhanHQ/contracts/place_order_contract.rb +51 -0
- data/lib/DhanHQ/core/base_model.rb +17 -10
- data/lib/DhanHQ/errors.rb +4 -0
- data/lib/DhanHQ/helpers/request_helper.rb +17 -2
- data/lib/DhanHQ/helpers/response_helper.rb +34 -13
- data/lib/DhanHQ/models/expired_options_data.rb +0 -6
- data/lib/DhanHQ/models/instrument_helpers.rb +4 -4
- data/lib/DhanHQ/models/order.rb +19 -2
- data/lib/DhanHQ/models/order_update.rb +0 -4
- data/lib/DhanHQ/rate_limiter.rb +40 -6
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/client.rb +11 -5
- data/lib/DhanHQ/ws/connection.rb +16 -2
- data/lib/DhanHQ/ws/market_depth/client.rb +2 -1
- data/lib/DhanHQ/ws/market_depth.rb +12 -12
- data/lib/DhanHQ/ws/orders/client.rb +76 -11
- data/lib/DhanHQ/ws/orders/connection.rb +2 -1
- metadata +18 -5
- 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
|
data/lib/DhanHQ/rate_limiter.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
159
|
+
|
|
160
|
+
@cleanup_threads << Thread.new do
|
|
151
161
|
loop do
|
|
162
|
+
break if @shutdown.true?
|
|
163
|
+
|
|
152
164
|
sleep(3600)
|
|
153
|
-
|
|
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
|
-
|
|
172
|
+
|
|
173
|
+
@cleanup_threads << Thread.new do
|
|
157
174
|
loop do
|
|
175
|
+
break if @shutdown.true?
|
|
176
|
+
|
|
158
177
|
sleep(86_400)
|
|
159
|
-
|
|
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
|
data/lib/DhanHQ/version.rb
CHANGED
data/lib/DhanHQ/ws/client.rb
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
168
|
-
|
|
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
|
|
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!
|
data/lib/DhanHQ/ws/connection.rb
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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: [],
|
|
21
|
-
client = Client.new(symbols: symbols, **
|
|
22
|
-
client.on(:depth_update, &
|
|
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: [], **
|
|
32
|
-
Client.new(symbols: symbols, **
|
|
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: {}, **
|
|
42
|
-
client = Client.new(symbols: symbols, **
|
|
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:,
|
|
58
|
-
connect(symbols: symbols,
|
|
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:,
|
|
68
|
-
client = Client.new(symbols: symbols, **
|
|
69
|
-
client.on(:depth_snapshot, &
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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,8 @@ module DhanHQ
|
|
|
86
86
|
Secret: cfg.partner_secret
|
|
87
87
|
}
|
|
88
88
|
else
|
|
89
|
-
token = cfg.
|
|
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 },
|
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.
|
|
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:
|
|
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.
|
|
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.
|
|
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
|