DhanHQ 2.1.7 → 2.1.8
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/CHANGELOG.md +8 -0
- data/examples/comprehensive_websocket_examples.rb +0 -0
- data/examples/instrument_finder_test.rb +0 -0
- data/examples/live_order_updates.rb +1 -1
- data/examples/market_depth_example.rb +2 -2
- data/examples/order_update_example.rb +0 -0
- data/examples/trading_fields_example.rb +1 -1
- data/lib/DhanHQ/client.rb +2 -1
- data/lib/DhanHQ/models/instrument_helpers.rb +39 -5
- data/lib/DhanHQ/rate_limiter.rb +78 -23
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/market_depth/client.rb +3 -1
- data/lib/DhanHQ/ws/orders/connection.rb +0 -3
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a414cb1f2ba6e22e0c4014ea9ca5656607611787d7f4f18e26c50069a8c3811
|
|
4
|
+
data.tar.gz: 05bbdf626ae74c2a64b963bccbff2ff35bd64972a7ea825491c42ae2069377f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b9cb7044f23b775360cc999bec4c3475278d923f8f11d083fc3874827f3565c19dbe8bc1f201da6619b9bd6c1d6911f2774ee0d8e329b0c8f2f7e66155f161d0
|
|
7
|
+
data.tar.gz: c81c3c6685511cfef78b1083c49e5d4ea4ec692f494d36b2ee6a74580c1de4655aa9e9139c7b63dd3c40a075c02e07bd7137a30b81e6062b83f4ec7000498fc5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [2.1.8] - 2025-10-30
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- Correctly map `underlying_seg` for option chain APIs:
|
|
7
|
+
- Index instruments use `IDX_I`.
|
|
8
|
+
- Stocks map to `NSE_FNO` or `BSE_FNO` based on the instrument's exchange.
|
|
9
|
+
- Implemented via `underlying_segment_for_options` in `DhanHQ::Models::InstrumentHelpers` and applied to `expiry_list` and `option_chain`.
|
|
10
|
+
|
|
3
11
|
## [2.1.7] - 2025-01-28
|
|
4
12
|
|
|
5
13
|
### Added
|
|
File without changes
|
|
File without changes
|
|
@@ -74,14 +74,14 @@ depth_client = DhanHQ::WS::MarketDepth.connect(symbols: symbols) do |depth_data|
|
|
|
74
74
|
puts " Ask Levels: #{depth_data[:asks].size}"
|
|
75
75
|
|
|
76
76
|
# Show top 3 bid/ask levels if available
|
|
77
|
-
if depth_data[:bids]
|
|
77
|
+
if depth_data[:bids]&.size&.positive?
|
|
78
78
|
puts " Top Bids:"
|
|
79
79
|
depth_data[:bids].first(3).each_with_index do |bid, i|
|
|
80
80
|
puts " #{i + 1}. Price: #{bid[:price]}, Qty: #{bid[:quantity]}"
|
|
81
81
|
end
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
-
if depth_data[:asks]
|
|
84
|
+
if depth_data[:asks]&.size&.positive?
|
|
85
85
|
puts " Top Asks:"
|
|
86
86
|
depth_data[:asks].first(3).each_with_index do |ask, i|
|
|
87
87
|
puts " #{i + 1}. Price: #{ask[:price]}, Qty: #{ask[:quantity]}"
|
|
File without changes
|
|
@@ -109,7 +109,7 @@ def calculate_margin_requirements(instrument, name, quantity, price)
|
|
|
109
109
|
puts " Sell CO Margin Required: ₹#{sell_margin}"
|
|
110
110
|
|
|
111
111
|
# Calculate MTF leverage
|
|
112
|
-
if instrument.mtf_leverage
|
|
112
|
+
if instrument.mtf_leverage.positive?
|
|
113
113
|
mtf_value = total_value * instrument.mtf_leverage
|
|
114
114
|
puts " MTF Leverage: #{instrument.mtf_leverage}x"
|
|
115
115
|
puts " MTF Value: ₹#{mtf_value}"
|
data/lib/DhanHQ/client.rb
CHANGED
|
@@ -38,7 +38,8 @@ module DhanHQ
|
|
|
38
38
|
# @return [DhanHQ::Client] A new client instance.
|
|
39
39
|
def initialize(api_type:)
|
|
40
40
|
DhanHQ.configure_with_env if ENV.fetch("CLIENT_ID", nil)
|
|
41
|
-
|
|
41
|
+
# Use shared rate limiter instance per API type to ensure proper coordination
|
|
42
|
+
@rate_limiter = RateLimiter.for(api_type)
|
|
42
43
|
|
|
43
44
|
raise "RateLimiter initialization failed" unless @rate_limiter
|
|
44
45
|
|
|
@@ -8,13 +8,26 @@ module DhanHQ
|
|
|
8
8
|
##
|
|
9
9
|
# Fetches last traded price (LTP) for this instrument.
|
|
10
10
|
#
|
|
11
|
-
# @return [
|
|
11
|
+
# @return [Float, nil] Last traded price value, or nil if not available
|
|
12
12
|
# @example
|
|
13
13
|
# instrument = DhanHQ::Models::Instrument.find("IDX_I", "NIFTY")
|
|
14
|
-
# instrument.ltp
|
|
14
|
+
# instrument.ltp # => 26053.9
|
|
15
15
|
def ltp
|
|
16
16
|
params = build_market_feed_params
|
|
17
|
-
DhanHQ::Models::MarketFeed.ltp(params)
|
|
17
|
+
response = DhanHQ::Models::MarketFeed.ltp(params)
|
|
18
|
+
|
|
19
|
+
# Extract last_price from nested response structure
|
|
20
|
+
# Response format: {"data" => {"EXCHANGE_SEGMENT" => {"security_id" => {"last_price" => value}}}, "status" => "success"}
|
|
21
|
+
data = response[:data] || response["data"]
|
|
22
|
+
return nil unless data
|
|
23
|
+
|
|
24
|
+
segment_data = data[exchange_segment] || data[exchange_segment.to_sym]
|
|
25
|
+
return nil unless segment_data
|
|
26
|
+
|
|
27
|
+
security_data = segment_data[security_id] || segment_data[security_id.to_s]
|
|
28
|
+
return nil unless security_data
|
|
29
|
+
|
|
30
|
+
security_data[:last_price] || security_data["last_price"]
|
|
18
31
|
end
|
|
19
32
|
|
|
20
33
|
##
|
|
@@ -82,7 +95,7 @@ module DhanHQ
|
|
|
82
95
|
def expiry_list
|
|
83
96
|
params = {
|
|
84
97
|
underlying_scrip: security_id.to_i,
|
|
85
|
-
underlying_seg:
|
|
98
|
+
underlying_seg: underlying_segment_for_options
|
|
86
99
|
}
|
|
87
100
|
DhanHQ::Models::OptionChain.fetch_expiry_list(params)
|
|
88
101
|
end
|
|
@@ -98,7 +111,7 @@ module DhanHQ
|
|
|
98
111
|
def option_chain(expiry:)
|
|
99
112
|
params = {
|
|
100
113
|
underlying_scrip: security_id.to_i,
|
|
101
|
-
underlying_seg:
|
|
114
|
+
underlying_seg: underlying_segment_for_options,
|
|
102
115
|
expiry: expiry
|
|
103
116
|
}
|
|
104
117
|
DhanHQ::Models::OptionChain.fetch(params)
|
|
@@ -136,6 +149,27 @@ module DhanHQ
|
|
|
136
149
|
|
|
137
150
|
params
|
|
138
151
|
end
|
|
152
|
+
|
|
153
|
+
##
|
|
154
|
+
# Determines the correct underlying segment for option chain APIs.
|
|
155
|
+
# Index uses IDX_I; stocks map to NSE_FNO or BSE_FNO by exchange.
|
|
156
|
+
def underlying_segment_for_options
|
|
157
|
+
seg = exchange_segment.to_s
|
|
158
|
+
ins = instrument.to_s
|
|
159
|
+
|
|
160
|
+
# If already a valid underlying seg, return as-is
|
|
161
|
+
return seg if %w[IDX_I NSE_FNO BSE_FNO MCX_FO].include?(seg)
|
|
162
|
+
|
|
163
|
+
# Index detection by instrument kind or segment
|
|
164
|
+
return "IDX_I" if ins == "INDEX" || seg == "IDX_I"
|
|
165
|
+
|
|
166
|
+
# Map equities/stock-related segments to respective FNO
|
|
167
|
+
return "NSE_FNO" if seg.start_with?("NSE")
|
|
168
|
+
return "BSE_FNO" if seg.start_with?("BSE")
|
|
169
|
+
|
|
170
|
+
# Fallback to IDX_I to avoid contract rejection
|
|
171
|
+
"IDX_I"
|
|
172
|
+
end
|
|
139
173
|
end
|
|
140
174
|
end
|
|
141
175
|
end
|
data/lib/DhanHQ/rate_limiter.rb
CHANGED
|
@@ -15,13 +15,31 @@ module DhanHQ
|
|
|
15
15
|
per_day: Float::INFINITY }
|
|
16
16
|
}.freeze
|
|
17
17
|
|
|
18
|
+
# Thread-safe shared rate limiters per API type
|
|
19
|
+
@shared_limiters = Concurrent::Map.new
|
|
20
|
+
@mutexes = Concurrent::Map.new
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
attr_reader :shared_limiters, :mutexes
|
|
24
|
+
|
|
25
|
+
# Get or create a shared rate limiter instance for the given API type
|
|
26
|
+
def for(api_type)
|
|
27
|
+
@shared_limiters[api_type] ||= new(api_type)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
18
31
|
# Creates a rate limiter for a given API type.
|
|
32
|
+
# Note: For proper rate limiting coordination, use RateLimiter.for(api_type) instead
|
|
33
|
+
# of RateLimiter.new(api_type) to get a shared instance.
|
|
19
34
|
#
|
|
20
35
|
# @param api_type [Symbol] One of the keys from {RATE_LIMITS}.
|
|
21
36
|
def initialize(api_type)
|
|
22
37
|
@api_type = api_type
|
|
23
38
|
@buckets = Concurrent::Hash.new
|
|
24
39
|
@buckets[:last_request_time] = Time.at(0) if api_type == :option_chain
|
|
40
|
+
# Track request timestamps for per-second limiting
|
|
41
|
+
@request_times = []
|
|
42
|
+
@window_start = Time.now
|
|
25
43
|
initialize_buckets
|
|
26
44
|
start_cleanup_threads
|
|
27
45
|
end
|
|
@@ -30,34 +48,70 @@ module DhanHQ
|
|
|
30
48
|
#
|
|
31
49
|
# @return [void]
|
|
32
50
|
def throttle!
|
|
33
|
-
|
|
34
|
-
|
|
51
|
+
mutex.synchronize do
|
|
52
|
+
if @api_type == :option_chain
|
|
53
|
+
last_request_time = @buckets[:last_request_time]
|
|
35
54
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
55
|
+
sleep_time = 3 - (Time.now - last_request_time)
|
|
56
|
+
if sleep_time.positive?
|
|
57
|
+
if ENV["DHAN_DEBUG"] == "true"
|
|
58
|
+
puts "Sleeping for #{sleep_time.round(2)} seconds due to option_chain rate limit"
|
|
59
|
+
end
|
|
60
|
+
sleep(sleep_time)
|
|
40
61
|
end
|
|
41
|
-
|
|
62
|
+
|
|
63
|
+
@buckets[:last_request_time] = Time.now
|
|
64
|
+
return
|
|
42
65
|
end
|
|
43
66
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
67
|
+
# For per-second limits, use timestamp-based sliding window
|
|
68
|
+
per_second_limit = RATE_LIMITS[@api_type][:per_second]
|
|
69
|
+
if per_second_limit && per_second_limit != Float::INFINITY
|
|
70
|
+
now = Time.now
|
|
71
|
+
# Remove requests older than 1 second
|
|
72
|
+
@request_times.reject! { |t| now - t >= 1.0 }
|
|
73
|
+
|
|
74
|
+
# Check if we've hit the per-second limit
|
|
75
|
+
if @request_times.size >= per_second_limit
|
|
76
|
+
# Calculate how long to wait until the oldest request is 1 second old
|
|
77
|
+
oldest_time = @request_times.min
|
|
78
|
+
wait_time = 1.0 - (now - oldest_time)
|
|
79
|
+
|
|
80
|
+
if wait_time.positive?
|
|
81
|
+
sleep(wait_time)
|
|
82
|
+
# Recalculate after sleep
|
|
83
|
+
now = Time.now
|
|
84
|
+
@request_times.reject! { |t| now - t >= 1.0 }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
47
87
|
|
|
48
|
-
|
|
49
|
-
|
|
88
|
+
# Record this request time
|
|
89
|
+
@request_times << Time.now
|
|
90
|
+
end
|
|
50
91
|
|
|
51
|
-
|
|
92
|
+
# Check other limits (per_minute, per_hour, per_day)
|
|
93
|
+
loop do
|
|
94
|
+
break if allow_request?
|
|
95
|
+
|
|
96
|
+
sleep(0.1)
|
|
97
|
+
end
|
|
98
|
+
record_request
|
|
52
99
|
end
|
|
53
|
-
record_request
|
|
54
100
|
end
|
|
55
101
|
|
|
56
102
|
private
|
|
57
103
|
|
|
104
|
+
# Gets or creates a mutex for this API type for thread-safe throttling
|
|
105
|
+
def mutex
|
|
106
|
+
self.class.mutexes[@api_type] ||= Mutex.new
|
|
107
|
+
end
|
|
108
|
+
|
|
58
109
|
# Prepares the counters used for each interval in {RATE_LIMITS}.
|
|
59
110
|
def initialize_buckets
|
|
60
111
|
RATE_LIMITS[@api_type].each_key do |interval|
|
|
112
|
+
# Skip per_second as we handle it with timestamps
|
|
113
|
+
next if interval == :per_second
|
|
114
|
+
|
|
61
115
|
@buckets[interval] = Concurrent::AtomicFixnum.new(0)
|
|
62
116
|
end
|
|
63
117
|
end
|
|
@@ -67,6 +121,9 @@ module DhanHQ
|
|
|
67
121
|
# @return [Boolean]
|
|
68
122
|
def allow_request?
|
|
69
123
|
RATE_LIMITS[@api_type].all? do |interval, limit|
|
|
124
|
+
# Skip per_second check as it's handled in throttle! with timestamps
|
|
125
|
+
next true if interval == :per_second
|
|
126
|
+
|
|
70
127
|
@buckets[interval].value < limit
|
|
71
128
|
end
|
|
72
129
|
end
|
|
@@ -74,34 +131,32 @@ module DhanHQ
|
|
|
74
131
|
# Increments the counters for each time window once a request is made.
|
|
75
132
|
def record_request
|
|
76
133
|
RATE_LIMITS[@api_type].each_key do |interval|
|
|
134
|
+
# Skip per_second as it's handled with timestamps
|
|
135
|
+
next if interval == :per_second
|
|
136
|
+
|
|
77
137
|
@buckets[interval].increment
|
|
78
138
|
end
|
|
79
139
|
end
|
|
80
140
|
|
|
81
141
|
# Spawns background threads to reset counters after each interval elapses.
|
|
82
142
|
def start_cleanup_threads
|
|
83
|
-
|
|
84
|
-
loop do
|
|
85
|
-
sleep(1)
|
|
86
|
-
@buckets[:per_second].value = 0
|
|
87
|
-
end
|
|
88
|
-
end
|
|
143
|
+
# Don't create per_second cleanup thread - we handle it with timestamps
|
|
89
144
|
Thread.new do
|
|
90
145
|
loop do
|
|
91
146
|
sleep(60)
|
|
92
|
-
@buckets[:per_minute]
|
|
147
|
+
@buckets[:per_minute]&.value = 0
|
|
93
148
|
end
|
|
94
149
|
end
|
|
95
150
|
Thread.new do
|
|
96
151
|
loop do
|
|
97
152
|
sleep(3600)
|
|
98
|
-
@buckets[:per_hour]
|
|
153
|
+
@buckets[:per_hour]&.value = 0
|
|
99
154
|
end
|
|
100
155
|
end
|
|
101
156
|
Thread.new do
|
|
102
157
|
loop do
|
|
103
158
|
sleep(86_400)
|
|
104
|
-
@buckets[:per_day]
|
|
159
|
+
@buckets[:per_day]&.value = 0
|
|
105
160
|
end
|
|
106
161
|
end
|
|
107
162
|
end
|
data/lib/DhanHQ/version.rb
CHANGED
|
@@ -213,7 +213,9 @@ module DhanHQ
|
|
|
213
213
|
|
|
214
214
|
instrument = find_instrument(symbol_code, segment_hint)
|
|
215
215
|
unless instrument
|
|
216
|
-
DhanHQ.logger&.warn(
|
|
216
|
+
DhanHQ.logger&.warn(
|
|
217
|
+
"[DhanHQ::WS::MarketDepth] Unable to locate instrument for #{symbol_code} (segment hint: #{segment_hint || "AUTO"})"
|
|
218
|
+
)
|
|
217
219
|
return nil
|
|
218
220
|
end
|
|
219
221
|
|