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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ffcb72b2997f7841ef6c872ad71748c36f4305f9e746a5d4d8f2a3b9429c8da
4
- data.tar.gz: 5a72519746d9ca58f6cb295855f9065ccc3b9059c2684347b58c3e998ad8668b
3
+ metadata.gz: 9a414cb1f2ba6e22e0c4014ea9ca5656607611787d7f4f18e26c50069a8c3811
4
+ data.tar.gz: 05bbdf626ae74c2a64b963bccbff2ff35bd64972a7ea825491c42ae2069377f3
5
5
  SHA512:
6
- metadata.gz: 397d51627f7049470332e23926ca7d00d78aad94d88eb0939d25164dc74330d68014a5ad6593453dfedb2da51c4f2a810ecc7e971d5f1735b3b21e96d3d068df
7
- data.tar.gz: 2092c00ab1903f18d7718bd27125236251d2129cd536a071b506c3a53d2826a66eb5c9ba737ab89eff52b125e864138b507e577616ffc9e0a034da0a4eb8e37a
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
@@ -101,7 +101,7 @@ summary_timer = Thread.new do
101
101
  end
102
102
  end
103
103
 
104
- puts "\n" + ("=" * 50)
104
+ puts "\n#{"=" * 50}"
105
105
  end
106
106
  end
107
107
 
@@ -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] && depth_data[:bids].size > 0
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] && depth_data[:asks].size > 0
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 > 0
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
- @rate_limiter = RateLimiter.new(api_type)
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 [Hash] Market feed LTP response
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: exchange_segment
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: exchange_segment,
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
@@ -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
- if @api_type == :option_chain
34
- last_request_time = @buckets[:last_request_time]
51
+ mutex.synchronize do
52
+ if @api_type == :option_chain
53
+ last_request_time = @buckets[:last_request_time]
35
54
 
36
- sleep_time = 3 - (Time.now - last_request_time)
37
- if sleep_time.positive?
38
- if ENV["DHAN_DEBUG"] == "true"
39
- puts "Sleeping for #{sleep_time.round(2)} seconds due to option_chain rate limit"
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
- sleep(sleep_time)
62
+
63
+ @buckets[:last_request_time] = Time.now
64
+ return
42
65
  end
43
66
 
44
- @buckets[:last_request_time] = Time.now
45
- return
46
- end
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
- loop do
49
- break if allow_request?
88
+ # Record this request time
89
+ @request_times << Time.now
90
+ end
50
91
 
51
- sleep(0.1)
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
- Thread.new do
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].value = 0
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].value = 0
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].value = 0
159
+ @buckets[:per_day]&.value = 0
105
160
  end
106
161
  end
107
162
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module DhanHQ
4
4
  # Semantic version of the DhanHQ client gem.
5
- VERSION = "2.1.7"
5
+ VERSION = "2.1.8"
6
6
  end
@@ -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("[DhanHQ::WS::MarketDepth] Unable to locate instrument for #{symbol_code} (segment hint: #{segment_hint || 'AUTO'})")
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
 
@@ -13,9 +13,6 @@ module DhanHQ
13
13
  # Initialize Orders WebSocket connection
14
14
  # @param url [String] WebSocket endpoint URL
15
15
  # @param options [Hash] Connection options
16
- def initialize(url:, **options)
17
- super
18
- end
19
16
 
20
17
  private
21
18
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: DhanHQ
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.7
4
+ version: 2.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shubham Taywade