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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +143 -118
- data/CHANGELOG.md +177 -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/expired_options_data_contract.rb +6 -6
- data/lib/DhanHQ/contracts/place_order_contract.rb +51 -0
- data/lib/DhanHQ/core/base_model.rb +26 -11
- 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/edis.rb +150 -14
- data/lib/DhanHQ/models/expired_options_data.rb +307 -88
- data/lib/DhanHQ/models/forever_order.rb +261 -22
- data/lib/DhanHQ/models/funds.rb +76 -10
- data/lib/DhanHQ/models/historical_data.rb +148 -31
- data/lib/DhanHQ/models/holding.rb +82 -6
- data/lib/DhanHQ/models/instrument_helpers.rb +4 -4
- data/lib/DhanHQ/models/kill_switch.rb +113 -11
- data/lib/DhanHQ/models/ledger_entry.rb +101 -13
- data/lib/DhanHQ/models/margin.rb +133 -8
- data/lib/DhanHQ/models/market_feed.rb +181 -17
- data/lib/DhanHQ/models/option_chain.rb +184 -12
- data/lib/DhanHQ/models/order.rb +418 -36
- data/lib/DhanHQ/models/order_update.rb +0 -4
- data/lib/DhanHQ/models/position.rb +161 -10
- data/lib/DhanHQ/models/profile.rb +103 -7
- data/lib/DhanHQ/models/super_order.rb +275 -15
- data/lib/DhanHQ/models/trade.rb +279 -26
- data/lib/DhanHQ/rate_limiter.rb +40 -6
- data/lib/DhanHQ/resources/expired_options_data.rb +1 -1
- 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 +78 -12
- data/lib/DhanHQ/ws/orders/connection.rb +2 -1
- data/lib/DhanHQ/ws/orders.rb +2 -1
- metadata +18 -5
- 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
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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.
|
|
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 },
|
data/lib/DhanHQ/ws/orders.rb
CHANGED
|
@@ -10,7 +10,8 @@ module DhanHQ
|
|
|
10
10
|
module Orders
|
|
11
11
|
##
|
|
12
12
|
# Connect to order updates WebSocket with a simple callback
|
|
13
|
-
#
|
|
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.
|
|
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
|