DhanHQ 4.0.0 → 4.1.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/CHANGELOG.md +39 -0
- data/GUIDE.md +24 -0
- data/README.md +75 -0
- data/docs/CONFIGURATION.md +4 -2
- data/docs/ENDPOINTS_AND_SANDBOX.md +83 -38
- data/lib/DhanHQ/client.rb +30 -5
- data/lib/DhanHQ/concerns/order_audit.rb +26 -3
- data/lib/DhanHQ/configuration.rb +152 -3
- data/lib/DhanHQ/constants.rb +60 -1
- data/lib/DhanHQ/contracts/company_info_contract.rb +24 -0
- data/lib/DhanHQ/contracts/market_movers_contract.rb +57 -0
- data/lib/DhanHQ/contracts/news_headline_contract.rb +21 -0
- data/lib/DhanHQ/contracts/technical_data_contract.rb +25 -0
- data/lib/DhanHQ/core/base_api.rb +0 -15
- data/lib/DhanHQ/core/base_model.rb +32 -12
- data/lib/DhanHQ/helpers/attribute_helper.rb +0 -22
- data/lib/DhanHQ/models/option_chain.rb +7 -0
- data/lib/DhanHQ/models/order.rb +7 -7
- data/lib/DhanHQ/rate_limiter.rb +34 -40
- data/lib/DhanHQ/resources/global_stocks/funds.rb +1 -1
- data/lib/DhanHQ/resources/global_stocks/holdings.rb +1 -1
- data/lib/DhanHQ/resources/global_stocks/margin_calculator.rb +1 -1
- data/lib/DhanHQ/resources/global_stocks/market_status.rb +1 -1
- data/lib/DhanHQ/resources/global_stocks/orders.rb +1 -1
- data/lib/DhanHQ/resources/global_stocks/trades.rb +1 -1
- data/lib/DhanHQ/resources/scanx.rb +98 -0
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/client.rb +5 -10
- data/lib/DhanHQ/ws/connection.rb +13 -15
- data/lib/DhanHQ/ws/market_depth/client.rb +5 -1
- data/lib/DhanHQ/ws/registry.rb +10 -6
- data/lib/dhan_hq.rb +7 -2
- metadata +6 -1
data/lib/DhanHQ/rate_limiter.rb
CHANGED
|
@@ -5,11 +5,20 @@ require "concurrent"
|
|
|
5
5
|
module DhanHQ
|
|
6
6
|
# Coarse-grained in-memory throttler matching the platform rate limits.
|
|
7
7
|
class RateLimiter
|
|
8
|
-
# Per-interval thresholds keyed by API type
|
|
9
|
-
#
|
|
10
|
-
#
|
|
8
|
+
# Per-interval thresholds keyed by API type.
|
|
9
|
+
#
|
|
10
|
+
# Values are deliberately conservative client-side budgets: they follow this
|
|
11
|
+
# repo's own documented operational limits (docs/DATA_API_PARAMETERS.md — market
|
|
12
|
+
# feed 1/s, option chain 1 per 3s; skills/dhanhq-ruby/references/option-chain.md),
|
|
13
|
+
# NOT the platform's published table, so the SDK stays well inside whatever
|
|
14
|
+
# per-account enforcement the broker applies. Per-minute caps are not enforced
|
|
15
|
+
# yet (all per_minute values are infinite except option chain); see the 4.1.0
|
|
16
|
+
# review notes for the open question.
|
|
17
|
+
# Global Stocks shares the order-API limits on its own tier so it can be
|
|
18
|
+
# routed independently (it always stays on the production host).
|
|
11
19
|
RATE_LIMITS = {
|
|
12
20
|
order_api: { per_second: 10, per_minute: Float::INFINITY, per_hour: Float::INFINITY, per_day: 100_000 },
|
|
21
|
+
global_stocks_api: { per_second: 10, per_minute: Float::INFINITY, per_hour: Float::INFINITY, per_day: 100_000 },
|
|
13
22
|
data_api: { per_second: 5, per_minute: Float::INFINITY, per_hour: Float::INFINITY, per_day: 7_000 },
|
|
14
23
|
quote_api: { per_second: 1, per_minute: Float::INFINITY, per_hour: Float::INFINITY, per_day: Float::INFINITY },
|
|
15
24
|
option_chain: { per_second: 1.0 / 3, per_minute: 20, per_hour: 600, per_day: 4800 },
|
|
@@ -17,6 +26,9 @@ module DhanHQ
|
|
|
17
26
|
per_day: Float::INFINITY }
|
|
18
27
|
}.freeze
|
|
19
28
|
|
|
29
|
+
# Seconds to wait between counter resets, per interval.
|
|
30
|
+
RESET_INTERVALS = { per_minute: 60, per_hour: 3600, per_day: 86_400 }.freeze
|
|
31
|
+
|
|
20
32
|
# Thread-safe shared rate limiters per API type
|
|
21
33
|
@shared_limiters = Concurrent::Map.new
|
|
22
34
|
@mutexes = Concurrent::Map.new
|
|
@@ -56,9 +68,7 @@ module DhanHQ
|
|
|
56
68
|
|
|
57
69
|
sleep_time = 3 - (Time.now - last_request_time)
|
|
58
70
|
if sleep_time.positive?
|
|
59
|
-
|
|
60
|
-
puts "Sleeping for #{sleep_time.round(2)} seconds due to option_chain rate limit"
|
|
61
|
-
end
|
|
71
|
+
DhanHQ.logger&.debug("[DhanHQ::RateLimiter] Sleeping #{sleep_time.round(2)}s (option_chain limit)")
|
|
62
72
|
sleep(sleep_time)
|
|
63
73
|
end
|
|
64
74
|
|
|
@@ -101,6 +111,9 @@ module DhanHQ
|
|
|
101
111
|
end
|
|
102
112
|
end
|
|
103
113
|
|
|
114
|
+
# NOTE: @request_times is only touched inside #throttle!, which holds the
|
|
115
|
+
# per-tier mutex, so the plain Array needs no synchronization of its own.
|
|
116
|
+
|
|
104
117
|
private
|
|
105
118
|
|
|
106
119
|
# Gets or creates a mutex for this API type for thread-safe throttling
|
|
@@ -140,47 +153,28 @@ module DhanHQ
|
|
|
140
153
|
end
|
|
141
154
|
end
|
|
142
155
|
|
|
143
|
-
# Spawns
|
|
156
|
+
# Spawns one reset thread per interval whose limit is finite. Intervals whose
|
|
157
|
+
# limit is Float::INFINITY never block, so resetting them is a no-op and no
|
|
158
|
+
# thread is created (quote and non-trading tiers end up with zero threads).
|
|
159
|
+
# Threads die with the process; #shutdown exists for explicit teardown.
|
|
144
160
|
def start_cleanup_threads
|
|
145
161
|
@cleanup_threads = []
|
|
146
162
|
@shutdown = Concurrent::AtomicBoolean.new(false)
|
|
147
163
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
break if @shutdown.true?
|
|
152
|
-
|
|
153
|
-
sleep(60)
|
|
154
|
-
break if @shutdown.true?
|
|
164
|
+
RESET_INTERVALS.each do |interval, seconds|
|
|
165
|
+
limit = RATE_LIMITS[@api_type][interval]
|
|
166
|
+
next if limit.nil? || limit >= Float::INFINITY
|
|
155
167
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
end
|
|
160
|
-
end
|
|
168
|
+
@cleanup_threads << Thread.new do
|
|
169
|
+
loop do
|
|
170
|
+
break if @shutdown.true?
|
|
161
171
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
break if @shutdown.true?
|
|
172
|
+
sleep(seconds)
|
|
173
|
+
break if @shutdown.true?
|
|
165
174
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
mutex.synchronize do
|
|
170
|
-
@buckets[:per_hour]&.value = 0
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
@cleanup_threads << Thread.new do
|
|
176
|
-
loop do
|
|
177
|
-
break if @shutdown.true?
|
|
178
|
-
|
|
179
|
-
sleep(86_400)
|
|
180
|
-
break if @shutdown.true?
|
|
181
|
-
|
|
182
|
-
mutex.synchronize do
|
|
183
|
-
@buckets[:per_day]&.value = 0
|
|
175
|
+
mutex.synchronize do
|
|
176
|
+
@buckets[interval]&.value = 0
|
|
177
|
+
end
|
|
184
178
|
end
|
|
185
179
|
end
|
|
186
180
|
end
|
|
@@ -10,7 +10,7 @@ module DhanHQ
|
|
|
10
10
|
# Global Stocks funds are held in USD and are reported separately from the
|
|
11
11
|
# domestic INR fund limit exposed by {DhanHQ::Resources::Funds}.
|
|
12
12
|
class Funds < BaseAPI
|
|
13
|
-
API_TYPE = :
|
|
13
|
+
API_TYPE = :global_stocks_api
|
|
14
14
|
HTTP_PATH = "/v2/globalstocks/fundlimit"
|
|
15
15
|
|
|
16
16
|
# Retrieves the authenticated user's US stock fund limit details.
|
|
@@ -11,7 +11,7 @@ module DhanHQ
|
|
|
11
11
|
# Both endpoints take the same request body. The API documents +price+ and
|
|
12
12
|
# +quantity+ as strings, so numeric input is stringified before sending.
|
|
13
13
|
class MarginCalculator < BaseAPI
|
|
14
|
-
API_TYPE = :
|
|
14
|
+
API_TYPE = :global_stocks_api
|
|
15
15
|
HTTP_PATH = "/v2/globalstocks"
|
|
16
16
|
|
|
17
17
|
# Calculates the margin required for a Global Stocks order.
|
|
@@ -7,7 +7,7 @@ module DhanHQ
|
|
|
7
7
|
#
|
|
8
8
|
# GET /v2/globalstocks/marketstatus
|
|
9
9
|
class MarketStatus < BaseAPI
|
|
10
|
-
API_TYPE = :
|
|
10
|
+
API_TYPE = :global_stocks_api
|
|
11
11
|
HTTP_PATH = "/v2/globalstocks/marketstatus"
|
|
12
12
|
|
|
13
13
|
# Retrieves the current US market status and session timings.
|
|
@@ -8,7 +8,7 @@ module DhanHQ
|
|
|
8
8
|
# GET /v2/globalstocks/trades
|
|
9
9
|
# GET /v2/globalstocks/trades/{security-id}
|
|
10
10
|
class Trades < BaseAPI
|
|
11
|
-
API_TYPE = :
|
|
11
|
+
API_TYPE = :global_stocks_api
|
|
12
12
|
HTTP_PATH = "/v2/globalstocks/trades"
|
|
13
13
|
|
|
14
14
|
# Retrieves the current trading day's executed Global Stocks trades.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Resources
|
|
5
|
+
##
|
|
6
|
+
# Resource for the ScanX data API endpoints (POST /v2/data/*).
|
|
7
|
+
#
|
|
8
|
+
# Covers company fundamentals, market movers, live news headlines and
|
|
9
|
+
# server-side technical indicators. These are market-data endpoints, so in
|
|
10
|
+
# hybrid mode they keep hitting the production host.
|
|
11
|
+
class ScanX < BaseAPI
|
|
12
|
+
# ScanX requests hit the data API tier (5 req/sec, 7,000/day).
|
|
13
|
+
API_TYPE = :data_api
|
|
14
|
+
# Root path for the ScanX data endpoints.
|
|
15
|
+
HTTP_PATH = "/v2/data"
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# POST /v2/data/companyinfo
|
|
19
|
+
# Company overview, valuation/profitability ratios and shareholding
|
|
20
|
+
# pattern for a single equity instrument.
|
|
21
|
+
#
|
|
22
|
+
# @param params [Hash] Example:
|
|
23
|
+
# {
|
|
24
|
+
# security_id: "1333",
|
|
25
|
+
# exchange_segment: "NSE_EQ",
|
|
26
|
+
# instrument: "EQUITY",
|
|
27
|
+
# metrics: ["CO", "RATIOS", "SHP"]
|
|
28
|
+
# }
|
|
29
|
+
# @return [HashWithIndifferentAccess]
|
|
30
|
+
def company_info(params)
|
|
31
|
+
validate_with!(Contracts::CompanyInfoContract, params)
|
|
32
|
+
post("/companyinfo", params: params)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
##
|
|
36
|
+
# POST /v2/data/marketmovers
|
|
37
|
+
# Top options, futures and stocks ranked by open interest, volume or
|
|
38
|
+
# price movement.
|
|
39
|
+
#
|
|
40
|
+
# @param params [Hash] Example:
|
|
41
|
+
# {
|
|
42
|
+
# exchange_segment: "NSE_FNO",
|
|
43
|
+
# instrument: ["OPTIDX", "OPTSTK"],
|
|
44
|
+
# category: "HIGHEST_OI",
|
|
45
|
+
# expiry: "2026-10-29",
|
|
46
|
+
# limit: 10
|
|
47
|
+
# }
|
|
48
|
+
# For EQUITY rankings pass +universe:+ (e.g. "NIFTY_50") instead of expiry.
|
|
49
|
+
# @return [HashWithIndifferentAccess]
|
|
50
|
+
def market_movers(params)
|
|
51
|
+
validate_with!(Contracts::MarketMoversContract, params)
|
|
52
|
+
post("/marketmovers", params: params)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
##
|
|
56
|
+
# POST /v2/data/newsheadline
|
|
57
|
+
# Latest news headlines, filterable by category and stock (max 50 items).
|
|
58
|
+
# +dhanClientId+ is injected automatically from the configuration.
|
|
59
|
+
#
|
|
60
|
+
# @param params [Hash] Example:
|
|
61
|
+
# { categories: ["ALL"], limit: 10, stock_list: ["1333"] }
|
|
62
|
+
# @return [HashWithIndifferentAccess]
|
|
63
|
+
def news_headlines(params)
|
|
64
|
+
validate_with!(Contracts::NewsHeadlineContract, params)
|
|
65
|
+
post("/newsheadline", params: params)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
##
|
|
69
|
+
# POST /v2/data/technical
|
|
70
|
+
# Server-side technical indicator values for an instrument. Only the
|
|
71
|
+
# requested indicators are computed and returned.
|
|
72
|
+
#
|
|
73
|
+
# @param params [Hash] Example:
|
|
74
|
+
# {
|
|
75
|
+
# security_id: "1333",
|
|
76
|
+
# exchange_segment: "NSE_EQ",
|
|
77
|
+
# instrument: "EQUITY",
|
|
78
|
+
# timeframe: "D",
|
|
79
|
+
# indicators: ["SMA_20", "RSI_14", "MACD_HIST"]
|
|
80
|
+
# }
|
|
81
|
+
# @return [HashWithIndifferentAccess]
|
|
82
|
+
def technical_data(params)
|
|
83
|
+
validate_with!(Contracts::TechnicalDataContract, params)
|
|
84
|
+
post("/technical", params: params)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
# Runs the given contract and raises DhanHQ::ValidationError on failure.
|
|
90
|
+
def validate_with!(contract_class, params)
|
|
91
|
+
result = contract_class.new.call(snake_case(params))
|
|
92
|
+
return if result.success?
|
|
93
|
+
|
|
94
|
+
raise DhanHQ::ValidationError, "Invalid parameters: #{result.errors.to_h}"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
data/lib/DhanHQ/version.rb
CHANGED
data/lib/DhanHQ/ws/client.rb
CHANGED
|
@@ -35,8 +35,10 @@ module DhanHQ
|
|
|
35
35
|
token = @config.resolved_access_token
|
|
36
36
|
raise DhanHQ::AuthenticationError, "Missing access token" if token.nil? || token.empty?
|
|
37
37
|
|
|
38
|
-
cid
|
|
39
|
-
|
|
38
|
+
cid = @config.client_id
|
|
39
|
+
raise DhanHQ::AuthenticationError, "client_id is not set (configure DhanHQ.client_id)" if cid.nil? || cid.to_s.empty?
|
|
40
|
+
|
|
41
|
+
ver = (@config.respond_to?(:ws_version) && @config.ws_version) || 2
|
|
40
42
|
base = url || @config.ws_market_feed_url
|
|
41
43
|
@url = base.include?("?") ? base : "#{base}?version=#{ver}&token=#{token}&clientId=#{cid}&authType=2"
|
|
42
44
|
end
|
|
@@ -54,7 +56,7 @@ module DhanHQ
|
|
|
54
56
|
emit(:tick, tick) if tick
|
|
55
57
|
end
|
|
56
58
|
Registry.register(self)
|
|
57
|
-
|
|
59
|
+
self.class.install_at_exit_hook!
|
|
58
60
|
@conn.start
|
|
59
61
|
self
|
|
60
62
|
end
|
|
@@ -295,13 +297,6 @@ module DhanHQ
|
|
|
295
297
|
rescue StandardError => e
|
|
296
298
|
DhanHQ.logger&.error("[DhanHQ::WS::Client] Error in event handler for #{event}: #{e.class} #{e.message}")
|
|
297
299
|
end
|
|
298
|
-
|
|
299
|
-
def install_at_exit_once!
|
|
300
|
-
return if defined?(@at_exit_installed) && @at_exit_installed
|
|
301
|
-
|
|
302
|
-
@at_exit_installed = true
|
|
303
|
-
at_exit { Registry.stop_all }
|
|
304
|
-
end
|
|
305
300
|
end
|
|
306
301
|
end
|
|
307
302
|
end
|
data/lib/DhanHQ/ws/connection.rb
CHANGED
|
@@ -9,9 +9,14 @@ module DhanHQ
|
|
|
9
9
|
# Low-level wrapper responsible for establishing and maintaining the raw
|
|
10
10
|
# WebSocket connection to the streaming API.
|
|
11
11
|
class Connection
|
|
12
|
-
|
|
13
|
-
#
|
|
14
|
-
|
|
12
|
+
# Feed request codes per the official Annexure "Feed Request Code" table
|
|
13
|
+
# (dhanhq.co/docs/v2/annexure#feed-request-code): 15/16 ticker, 17/18 quote,
|
|
14
|
+
# 21/22 full. RequestCode 12 is the whole-connection Disconnect Feed frame
|
|
15
|
+
# and is reserved for #send_disconnect.
|
|
16
|
+
SUB_CODES = { ticker: 15, quote: 17, full: 21 }.freeze
|
|
17
|
+
# Unsubscribe codes per the same table: 16/18/22. Never 12 — that is the
|
|
18
|
+
# whole-connection disconnect, not a per-instrument unsubscribe.
|
|
19
|
+
UNSUB_CODES = { ticker: 16, quote: 18, full: 22 }.freeze
|
|
15
20
|
|
|
16
21
|
COOL_OFF_429 = 60 # seconds to cool off on 429
|
|
17
22
|
MAX_BACKOFF = 90 # cap exponential backoff
|
|
@@ -219,7 +224,7 @@ module DhanHQ
|
|
|
219
224
|
subs, unsubs = cmds.partition { |c| c.op == :sub }
|
|
220
225
|
|
|
221
226
|
unless subs.empty?
|
|
222
|
-
list = uniq(
|
|
227
|
+
list = uniq(subs.map(&:payload).flatten)
|
|
223
228
|
new_only = @state.want_sub(list)
|
|
224
229
|
unless new_only.empty?
|
|
225
230
|
send_sub(new_only)
|
|
@@ -229,7 +234,7 @@ module DhanHQ
|
|
|
229
234
|
|
|
230
235
|
return if unsubs.empty?
|
|
231
236
|
|
|
232
|
-
list = uniq(
|
|
237
|
+
list = uniq(unsubs.map(&:payload).flatten)
|
|
233
238
|
exist_only = @state.want_unsub(list)
|
|
234
239
|
return if exist_only.empty?
|
|
235
240
|
|
|
@@ -267,17 +272,10 @@ module DhanHQ
|
|
|
267
272
|
DhanHQ.logger&.debug("[DhanHQ::WS] send_disconnect error #{e.class}: #{e.message}")
|
|
268
273
|
end
|
|
269
274
|
|
|
270
|
-
def flatten(a) = a.flatten
|
|
271
|
-
|
|
272
275
|
def uniq(list)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
next if seen[k]
|
|
277
|
-
|
|
278
|
-
out << i
|
|
279
|
-
seen[k] = true
|
|
280
|
-
end
|
|
276
|
+
# Composite key: the same instrument can arrive via repeated subscribe
|
|
277
|
+
# calls; Array#uniq with a block keeps the first occurrence.
|
|
278
|
+
list.uniq { |i| [i[:ExchangeSegment], i[:SecurityId]] }
|
|
281
279
|
end
|
|
282
280
|
end
|
|
283
281
|
end
|
|
@@ -13,8 +13,12 @@ module DhanHQ
|
|
|
13
13
|
# WebSocket client for Full Market Depth data
|
|
14
14
|
# Provides real-time market depth (bid/ask levels) for specified symbols
|
|
15
15
|
class Client < BaseConnection
|
|
16
|
+
# Feed request codes per the official Annexure "Feed Request Code" table
|
|
17
|
+
# (dhanhq.co/docs/v2/annexure#feed-request-code): 23 = Subscribe - Full
|
|
18
|
+
# Market Depth, 24 = Unsubscribe - Full Market Depth. Never 12 - that is
|
|
19
|
+
# the whole-connection disconnect, not a per-instrument unsubscribe.
|
|
16
20
|
SUBSCRIBE_REQUEST_CODE = 23
|
|
17
|
-
UNSUBSCRIBE_REQUEST_CODE =
|
|
21
|
+
UNSUBSCRIBE_REQUEST_CODE = 24
|
|
18
22
|
|
|
19
23
|
##
|
|
20
24
|
# Initialize Market Depth WebSocket client
|
data/lib/DhanHQ/ws/registry.rb
CHANGED
|
@@ -9,13 +9,14 @@ module DhanHQ
|
|
|
9
9
|
# disconnected when required.
|
|
10
10
|
class Registry
|
|
11
11
|
@clients = []
|
|
12
|
+
@mutex = Mutex.new
|
|
12
13
|
class << self
|
|
13
14
|
# Registers a client instance with the registry.
|
|
14
15
|
#
|
|
15
16
|
# @param client [DhanHQ::WS::Client]
|
|
16
17
|
# @return [void]
|
|
17
18
|
def register(client)
|
|
18
|
-
@clients << client unless @clients.include?(client)
|
|
19
|
+
@mutex.synchronize { @clients << client unless @clients.include?(client) }
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
# Removes a client from the registry.
|
|
@@ -23,18 +24,21 @@ module DhanHQ
|
|
|
23
24
|
# @param client [DhanHQ::WS::Client]
|
|
24
25
|
# @return [void]
|
|
25
26
|
def unregister(client)
|
|
26
|
-
@clients.delete(client)
|
|
27
|
+
@mutex.synchronize { @clients.delete(client) }
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
# Stops and removes all registered clients.
|
|
30
31
|
#
|
|
31
32
|
# @return [void]
|
|
32
33
|
def stop_all
|
|
33
|
-
@
|
|
34
|
-
c
|
|
35
|
-
|
|
34
|
+
@mutex.synchronize do
|
|
35
|
+
@clients.dup.each do |c|
|
|
36
|
+
c.stop
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
DhanHQ.logger&.debug("[DhanHQ::WS::Registry] stop failed: #{e.class} #{e.message}")
|
|
39
|
+
end
|
|
40
|
+
@clients.clear
|
|
36
41
|
end
|
|
37
|
-
@clients.clear
|
|
38
42
|
end
|
|
39
43
|
end
|
|
40
44
|
end
|
data/lib/dhan_hq.rb
CHANGED
|
@@ -44,6 +44,9 @@ module DhanHQ
|
|
|
44
44
|
# "dhan_hq"` — it only worked via exe/dhanhq-mcp and lib/dhan_hq/mcp.rb, which
|
|
45
45
|
# require_relative the file directly instead of going through the autoloader.
|
|
46
46
|
"mcp" => "MCP",
|
|
47
|
+
# resources/scanx.rb defines DhanHQ::Resources::ScanX; without this Zeitwerk
|
|
48
|
+
# looks for DhanHQ::Resources::Scanx and fails on the first reference.
|
|
49
|
+
"scanx" => "ScanX",
|
|
47
50
|
"ws" => "WS"
|
|
48
51
|
)
|
|
49
52
|
LOADER.push_dir(File.join(__dir__, "DhanHQ"), namespace: self)
|
|
@@ -84,8 +87,6 @@ module DhanHQ
|
|
|
84
87
|
require_relative "DhanHQ/skills/builtin/market_data_summarizer"
|
|
85
88
|
DhanHQ::Skills::Registry.load_builtins
|
|
86
89
|
|
|
87
|
-
class Error < StandardError; end
|
|
88
|
-
|
|
89
90
|
class << self
|
|
90
91
|
# Default REST API host used when no custom base URL is provided.
|
|
91
92
|
#
|
|
@@ -244,6 +245,10 @@ module DhanHQ
|
|
|
244
245
|
def fetch_token_endpoint(url, bearer_token, label)
|
|
245
246
|
conn = ::Faraday.new(url: url) do |c|
|
|
246
247
|
c.request :url_encoded
|
|
248
|
+
# Bounded timeouts, same defaults as Client#build_connection; without
|
|
249
|
+
# them a stalled token endpoint would hang the caller forever.
|
|
250
|
+
c.options.timeout = ENV.fetch("DHAN_READ_TIMEOUT", 30).to_i
|
|
251
|
+
c.options.open_timeout = ENV.fetch("DHAN_CONNECT_TIMEOUT", 10).to_i
|
|
247
252
|
c.adapter ::Faraday.default_adapter
|
|
248
253
|
end
|
|
249
254
|
|
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: 4.
|
|
4
|
+
version: 4.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shubham Taywade
|
|
@@ -241,6 +241,7 @@ files:
|
|
|
241
241
|
- lib/DhanHQ/constants.rb
|
|
242
242
|
- lib/DhanHQ/contracts/alert_order_contract.rb
|
|
243
243
|
- lib/DhanHQ/contracts/base_contract.rb
|
|
244
|
+
- lib/DhanHQ/contracts/company_info_contract.rb
|
|
244
245
|
- lib/DhanHQ/contracts/edis_contract.rb
|
|
245
246
|
- lib/DhanHQ/contracts/expired_options_data_contract.rb
|
|
246
247
|
- lib/DhanHQ/contracts/forever_order_contract.rb
|
|
@@ -254,15 +255,18 @@ files:
|
|
|
254
255
|
- lib/DhanHQ/contracts/intraday_historical_data_contract.rb
|
|
255
256
|
- lib/DhanHQ/contracts/margin_calculator_contract.rb
|
|
256
257
|
- lib/DhanHQ/contracts/market_feed_contract.rb
|
|
258
|
+
- lib/DhanHQ/contracts/market_movers_contract.rb
|
|
257
259
|
- lib/DhanHQ/contracts/modify_order_contract.rb
|
|
258
260
|
- lib/DhanHQ/contracts/multi_order_contract.rb
|
|
259
261
|
- lib/DhanHQ/contracts/multi_scrip_margin_calc_request_contract.rb
|
|
262
|
+
- lib/DhanHQ/contracts/news_headline_contract.rb
|
|
260
263
|
- lib/DhanHQ/contracts/option_chain_contract.rb
|
|
261
264
|
- lib/DhanHQ/contracts/order_contract.rb
|
|
262
265
|
- lib/DhanHQ/contracts/place_order_contract.rb
|
|
263
266
|
- lib/DhanHQ/contracts/pnl_based_exit_contract.rb
|
|
264
267
|
- lib/DhanHQ/contracts/position_conversion_contract.rb
|
|
265
268
|
- lib/DhanHQ/contracts/slice_order_contract.rb
|
|
269
|
+
- lib/DhanHQ/contracts/technical_data_contract.rb
|
|
266
270
|
- lib/DhanHQ/contracts/trade_by_order_id_contract.rb
|
|
267
271
|
- lib/DhanHQ/contracts/trade_contract.rb
|
|
268
272
|
- lib/DhanHQ/contracts/trade_history_contract.rb
|
|
@@ -363,6 +367,7 @@ files:
|
|
|
363
367
|
- lib/DhanHQ/resources/pnl_exit.rb
|
|
364
368
|
- lib/DhanHQ/resources/positions.rb
|
|
365
369
|
- lib/DhanHQ/resources/profile.rb
|
|
370
|
+
- lib/DhanHQ/resources/scanx.rb
|
|
366
371
|
- lib/DhanHQ/resources/statements.rb
|
|
367
372
|
- lib/DhanHQ/resources/super_orders.rb
|
|
368
373
|
- lib/DhanHQ/resources/trader_control.rb
|