DhanHQ 3.4.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 +61 -1
- data/GUIDE.md +40 -0
- data/README.md +103 -0
- data/docs/CONFIGURATION.md +29 -2
- data/docs/ENDPOINTS_AND_SANDBOX.md +83 -38
- data/lib/DhanHQ/client.rb +101 -26
- data/lib/DhanHQ/concerns/order_audit.rb +26 -3
- data/lib/DhanHQ/configuration.rb +178 -27
- 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 +3 -17
- data/lib/DhanHQ/core/base_model.rb +32 -12
- data/lib/DhanHQ/dry_run/simulator.rb +3 -2
- data/lib/DhanHQ/helpers/attribute_helper.rb +0 -22
- data/lib/DhanHQ/helpers/request_helper.rb +13 -3
- 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/market_feed.rb +1 -1
- data/lib/DhanHQ/resources/scanx.rb +98 -0
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/base_connection.rb +2 -1
- data/lib/DhanHQ/ws/client.rb +10 -13
- data/lib/DhanHQ/ws/connection.rb +13 -15
- data/lib/DhanHQ/ws/market_depth/client.rb +9 -4
- data/lib/DhanHQ/ws/orders/client.rb +4 -4
- data/lib/DhanHQ/ws/orders/connection.rb +1 -1
- data/lib/DhanHQ/ws/registry.rb +10 -6
- data/lib/dhan_hq.rb +81 -48
- metadata +27 -22
data/lib/DhanHQ/ws/client.rb
CHANGED
|
@@ -20,7 +20,8 @@ module DhanHQ
|
|
|
20
20
|
class Client
|
|
21
21
|
# @param mode [Symbol] Feed mode (:ticker, :quote, :full).
|
|
22
22
|
# @param url [String, nil] Optional custom WebSocket endpoint.
|
|
23
|
-
|
|
23
|
+
# @param config [DhanHQ::Configuration, nil] Optional configuration instance.
|
|
24
|
+
def initialize(mode: :ticker, url: nil, config: nil)
|
|
24
25
|
@mode = mode # :ticker, :quote, :full (adjust to your API)
|
|
25
26
|
@bus = CmdBus.new
|
|
26
27
|
@state = SubState.new
|
|
@@ -29,13 +30,16 @@ module DhanHQ
|
|
|
29
30
|
@last_message_at = Concurrent::AtomicReference.new(nil)
|
|
30
31
|
@connected_at = Concurrent::AtomicReference.new(nil)
|
|
31
32
|
@reconnects = Concurrent::AtomicFixnum.new(0)
|
|
33
|
+
@config = config || DhanHQ.configuration || DhanHQ.ensure_configuration!
|
|
32
34
|
|
|
33
|
-
token =
|
|
35
|
+
token = @config.resolved_access_token
|
|
34
36
|
raise DhanHQ::AuthenticationError, "Missing access token" if token.nil? || token.empty?
|
|
35
37
|
|
|
36
|
-
cid
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
42
|
+
base = url || @config.ws_market_feed_url
|
|
39
43
|
@url = base.include?("?") ? base : "#{base}?version=#{ver}&token=#{token}&clientId=#{cid}&authType=2"
|
|
40
44
|
end
|
|
41
45
|
|
|
@@ -52,7 +56,7 @@ module DhanHQ
|
|
|
52
56
|
emit(:tick, tick) if tick
|
|
53
57
|
end
|
|
54
58
|
Registry.register(self)
|
|
55
|
-
|
|
59
|
+
self.class.install_at_exit_hook!
|
|
56
60
|
@conn.start
|
|
57
61
|
self
|
|
58
62
|
end
|
|
@@ -293,13 +297,6 @@ module DhanHQ
|
|
|
293
297
|
rescue StandardError => e
|
|
294
298
|
DhanHQ.logger&.error("[DhanHQ::WS::Client] Error in event handler for #{event}: #{e.class} #{e.message}")
|
|
295
299
|
end
|
|
296
|
-
|
|
297
|
-
def install_at_exit_once!
|
|
298
|
-
return if defined?(@at_exit_installed) && @at_exit_installed
|
|
299
|
-
|
|
300
|
-
@at_exit_installed = true
|
|
301
|
-
at_exit { Registry.stop_all }
|
|
302
|
-
end
|
|
303
300
|
end
|
|
304
301
|
end
|
|
305
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,17 +13,22 @@ 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
|
|
21
25
|
# @param symbols [Array<String, Hash>] List of symbols (or metadata hashes) to subscribe to
|
|
26
|
+
# @param config [DhanHQ::Configuration, nil] Optional configuration instance
|
|
22
27
|
# @param options [Hash] Connection options
|
|
23
|
-
def initialize(symbols: [], **options)
|
|
24
|
-
cfg = DhanHQ.configuration
|
|
28
|
+
def initialize(symbols: [], config: nil, **options)
|
|
29
|
+
cfg = config || DhanHQ.configuration || DhanHQ.ensure_configuration!
|
|
25
30
|
url = options[:url] || build_market_depth_url(cfg)
|
|
26
|
-
super(url: url, **options)
|
|
31
|
+
super(url: url, config: cfg, **options)
|
|
27
32
|
|
|
28
33
|
@symbols = Array(symbols)
|
|
29
34
|
@subscriptions = Concurrent::Map.new
|
|
@@ -16,15 +16,15 @@ module DhanHQ
|
|
|
16
16
|
# Maximum age of orders in tracker in seconds (default: 7 days)
|
|
17
17
|
MAX_ORDER_AGE = ENV.fetch("DHAN_WS_MAX_ORDER_AGE", 604_800).to_i
|
|
18
18
|
|
|
19
|
-
def initialize(url: nil, **options)
|
|
19
|
+
def initialize(url: nil, config: nil, **options)
|
|
20
20
|
@callbacks = Concurrent::Map.new { |h, k| h[k] = [] }
|
|
21
21
|
@started = Concurrent::AtomicBoolean.new(false)
|
|
22
22
|
@order_tracker = Concurrent::Map.new
|
|
23
23
|
@order_timestamps = Concurrent::Map.new
|
|
24
24
|
@cleanup_mutex = Mutex.new
|
|
25
25
|
@cleanup_thread = nil
|
|
26
|
-
|
|
27
|
-
@url = url ||
|
|
26
|
+
@config = config || DhanHQ.configuration || DhanHQ.ensure_configuration!
|
|
27
|
+
@url = url || @config.ws_order_url
|
|
28
28
|
@connection_options = options
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -35,7 +35,7 @@ module DhanHQ
|
|
|
35
35
|
return self if @started.true?
|
|
36
36
|
|
|
37
37
|
@started.make_true
|
|
38
|
-
@conn = Connection.new(url: @url, **@connection_options)
|
|
38
|
+
@conn = Connection.new(url: @url, config: @config, **@connection_options)
|
|
39
39
|
@conn.on(:open) { emit(:open, true) }
|
|
40
40
|
@conn.on(:close) { |payload| emit(:close, payload) }
|
|
41
41
|
@conn.on(:error) { |error| emit(:error, error) }
|
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
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "logger"
|
|
5
5
|
require "zeitwerk"
|
|
6
|
-
|
|
6
|
+
begin
|
|
7
|
+
require "dotenv/load"
|
|
8
|
+
rescue LoadError
|
|
9
|
+
# Dotenv is optional in host applications
|
|
10
|
+
end
|
|
7
11
|
require "faraday"
|
|
8
12
|
# Minimal eager requires for backward-compatible constants.
|
|
9
13
|
# These are widely referenced (e.g. `DhanHQ::BaseAPI`) and should not depend on
|
|
@@ -40,6 +44,9 @@ module DhanHQ
|
|
|
40
44
|
# "dhan_hq"` — it only worked via exe/dhanhq-mcp and lib/dhan_hq/mcp.rb, which
|
|
41
45
|
# require_relative the file directly instead of going through the autoloader.
|
|
42
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",
|
|
43
50
|
"ws" => "WS"
|
|
44
51
|
)
|
|
45
52
|
LOADER.push_dir(File.join(__dir__, "DhanHQ"), namespace: self)
|
|
@@ -80,17 +87,19 @@ module DhanHQ
|
|
|
80
87
|
require_relative "DhanHQ/skills/builtin/market_data_summarizer"
|
|
81
88
|
DhanHQ::Skills::Registry.load_builtins
|
|
82
89
|
|
|
83
|
-
class Error < StandardError; end
|
|
84
|
-
|
|
85
90
|
class << self
|
|
86
91
|
# Default REST API host used when no custom base URL is provided.
|
|
87
92
|
#
|
|
88
93
|
# @return [String]
|
|
89
94
|
BASE_URL = Constants::Urls::REST_API_BASE
|
|
90
95
|
# The current configuration instance.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
attr_reader :configuration
|
|
97
|
+
|
|
98
|
+
def configuration=(config)
|
|
99
|
+
@configuration = config
|
|
100
|
+
@default_client = nil
|
|
101
|
+
reset_model_resources!
|
|
102
|
+
end
|
|
94
103
|
|
|
95
104
|
attr_writer :logger
|
|
96
105
|
|
|
@@ -106,8 +115,10 @@ module DhanHQ
|
|
|
106
115
|
# @return [void]
|
|
107
116
|
def configure
|
|
108
117
|
self.configuration ||= Configuration.new
|
|
109
|
-
yield(configuration)
|
|
118
|
+
yield(configuration) if block_given?
|
|
110
119
|
self.logger ||= Logger.new($stdout, level: Logger::INFO)
|
|
120
|
+
@default_client = Client.new(config: configuration)
|
|
121
|
+
reset_model_resources!
|
|
111
122
|
end
|
|
112
123
|
|
|
113
124
|
# default logger so calls like DhanHQ.logger&.info never explode
|
|
@@ -118,6 +129,18 @@ module DhanHQ
|
|
|
118
129
|
@logger ||= Logger.new($stdout, level: Logger::INFO)
|
|
119
130
|
end
|
|
120
131
|
|
|
132
|
+
# Global default client (used for backward compatibility with class-level APIs)
|
|
133
|
+
#
|
|
134
|
+
# @return [DhanHQ::Client]
|
|
135
|
+
def default_client
|
|
136
|
+
@default_client ||= Client.new(config: ensure_configuration!)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Resets the global default client
|
|
140
|
+
def reset_default_client!
|
|
141
|
+
@default_client = nil
|
|
142
|
+
end
|
|
143
|
+
|
|
121
144
|
# Configures the DhanHQ client using environment variables.
|
|
122
145
|
#
|
|
123
146
|
# When credentials are injected via `DHAN_ACCESS_TOKEN` and `DHAN_CLIENT_ID` this helper
|
|
@@ -129,6 +152,8 @@ module DhanHQ
|
|
|
129
152
|
# @return [void]
|
|
130
153
|
def configure_with_env
|
|
131
154
|
self.configuration = Configuration.new
|
|
155
|
+
@default_client = Client.new(config: configuration)
|
|
156
|
+
reset_model_resources!
|
|
132
157
|
end
|
|
133
158
|
|
|
134
159
|
# Ensures configuration exists, bootstrapping from ENV when nil.
|
|
@@ -140,9 +165,32 @@ module DhanHQ
|
|
|
140
165
|
self.configuration ||= Configuration.new
|
|
141
166
|
end
|
|
142
167
|
|
|
143
|
-
# Resets the configuration to nil.
|
|
168
|
+
# Resets the configuration and default client to nil.
|
|
144
169
|
def reset_configuration!
|
|
145
170
|
self.configuration = nil
|
|
171
|
+
@default_client = nil
|
|
172
|
+
reset_model_resources!
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Clears memoized class-level resource and API instances across all models.
|
|
176
|
+
def reset_model_resources!
|
|
177
|
+
models = [
|
|
178
|
+
BaseModel,
|
|
179
|
+
Models::Order, Models::Holding, Models::Position, Models::HistoricalData,
|
|
180
|
+
Models::Trade, Models::OptionChain, Models::Funds, Models::AlertOrder,
|
|
181
|
+
Models::ForeverOrder, Models::IcebergOrder, Models::SuperOrder, Models::TwapOrder,
|
|
182
|
+
Models::LedgerEntry, Models::Instrument, Models::Edis, Models::Profile,
|
|
183
|
+
Models::Margin, Models::MultiOrder, Models::KillSwitch, Models::PnlExit,
|
|
184
|
+
Models::MarketFeed, Models::ExpiredOptionsData
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
models.each do |klass|
|
|
188
|
+
next unless klass.is_a?(Class)
|
|
189
|
+
|
|
190
|
+
klass.instance_variables.each do |var|
|
|
191
|
+
klass.instance_variable_set(var, nil) if var.to_s.end_with?("resource", "api")
|
|
192
|
+
end
|
|
193
|
+
end
|
|
146
194
|
end
|
|
147
195
|
|
|
148
196
|
# Configures the DhanHQ client by fetching credentials from a token endpoint.
|
|
@@ -164,39 +212,14 @@ module DhanHQ
|
|
|
164
212
|
def configure_from_token_endpoint(base_url: nil, bearer_token: nil)
|
|
165
213
|
base_url ||= ENV.fetch("DHAN_TOKEN_ENDPOINT_BASE_URL", nil)
|
|
166
214
|
bearer_token ||= ENV.fetch("DHAN_TOKEN_ENDPOINT_BEARER", nil)
|
|
167
|
-
|
|
168
215
|
raise DhanHQ::TokenEndpointError, "base_url and bearer_token (or ENV DHAN_TOKEN_ENDPOINT_*) are required" if base_url.to_s.empty? || bearer_token.to_s.empty?
|
|
169
216
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
c.adapter ::Faraday.default_adapter
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
response = conn.get("") do |req|
|
|
177
|
-
req.headers["Authorization"] = "Bearer #{bearer_token}"
|
|
178
|
-
req.headers["Accept"] = "application/json"
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
unless response.success?
|
|
182
|
-
body = parse_json_body(response.body)
|
|
183
|
-
msg = body["error"] || body["message"] || body["errorMessage"] || response.body.to_s
|
|
184
|
-
raise DhanHQ::TokenEndpointError, "Token endpoint returned #{response.status}: #{msg}"
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
data = parse_json_body(response.body)
|
|
188
|
-
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
189
|
-
|
|
190
|
-
access_token = data["access_token"] || data[:access_token]
|
|
191
|
-
client_id = data["client_id"] || data[:client_id]
|
|
217
|
+
data = fetch_token_endpoint("#{base_url.to_s.chomp("/")}/auth/dhan/token", bearer_token, "Token endpoint")
|
|
218
|
+
access_token = data["access_token"]
|
|
219
|
+
client_id = data["client_id"]
|
|
192
220
|
raise DhanHQ::TokenEndpointError, "Token endpoint response missing access_token or client_id" if access_token.to_s.empty? || client_id.to_s.empty?
|
|
193
221
|
|
|
194
|
-
|
|
195
|
-
configuration.access_token = access_token.to_s
|
|
196
|
-
configuration.client_id = client_id.to_s
|
|
197
|
-
dhan_base = data["base_url"] || data[:base_url]
|
|
198
|
-
configuration.base_url = dhan_base.to_s if dhan_base.to_s != ""
|
|
199
|
-
configuration
|
|
222
|
+
apply_token_config(access_token, client_id, data["base_url"])
|
|
200
223
|
end
|
|
201
224
|
|
|
202
225
|
# Configures the DhanHQ client by fetching credentials from a dashboard API.
|
|
@@ -208,8 +231,24 @@ module DhanHQ
|
|
|
208
231
|
def configure_from_dashboard(bearer_token:, url: "http://localhost:3011/api/dhan_access_token")
|
|
209
232
|
raise DhanHQ::TokenEndpointError, "bearer_token is required" if bearer_token.to_s.empty?
|
|
210
233
|
|
|
234
|
+
data = fetch_token_endpoint(url, bearer_token, "Dashboard")
|
|
235
|
+
access_token = data["access_token"] || data["accessToken"] || data["dhan_access_token"] || data["dhanaccesstoken"]
|
|
236
|
+
client_id = data["client_id"] || data["clientId"] || data["dhan_client_id"] || data["dhanClientId"]
|
|
237
|
+
client_id ||= configuration&.client_id || ENV.fetch("DHAN_CLIENT_ID", nil)
|
|
238
|
+
|
|
239
|
+
raise DhanHQ::TokenEndpointError, "Dashboard response missing access_token (tried access_token, dhan_access_token, dhanaccesstoken)" if access_token.to_s.empty?
|
|
240
|
+
raise DhanHQ::TokenEndpointError, "Dashboard response missing client_id, and no fallback client_id was found in config or ENV['DHAN_CLIENT_ID']" if client_id.to_s.empty?
|
|
241
|
+
|
|
242
|
+
apply_token_config(access_token, client_id, data["base_url"] || data["baseUrl"])
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def fetch_token_endpoint(url, bearer_token, label)
|
|
211
246
|
conn = ::Faraday.new(url: url) do |c|
|
|
212
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
|
|
213
252
|
c.adapter ::Faraday.default_adapter
|
|
214
253
|
end
|
|
215
254
|
|
|
@@ -221,24 +260,18 @@ module DhanHQ
|
|
|
221
260
|
unless response.success?
|
|
222
261
|
body = parse_json_body(response.body)
|
|
223
262
|
msg = body["error"] || body["message"] || body["errorMessage"] || response.body.to_s
|
|
224
|
-
raise DhanHQ::TokenEndpointError, "
|
|
263
|
+
raise DhanHQ::TokenEndpointError, "#{label} returned #{response.status}: #{msg}"
|
|
225
264
|
end
|
|
226
265
|
|
|
227
266
|
data = parse_json_body(response.body)
|
|
228
|
-
data
|
|
229
|
-
|
|
230
|
-
access_token = data["access_token"] || data["accessToken"] || data["dhan_access_token"] || data["dhanaccesstoken"]
|
|
231
|
-
client_id = data["client_id"] || data["clientId"] || data["dhan_client_id"] || data["dhanClientId"]
|
|
232
|
-
client_id ||= self.configuration&.client_id || ENV.fetch("DHAN_CLIENT_ID", nil)
|
|
233
|
-
|
|
234
|
-
raise DhanHQ::TokenEndpointError, "Dashboard response missing access_token (tried access_token, dhan_access_token, dhanaccesstoken)" if access_token.to_s.empty?
|
|
235
|
-
raise DhanHQ::TokenEndpointError, "Dashboard response missing client_id, and no fallback client_id was found in config or ENV['DHAN_CLIENT_ID']" if client_id.to_s.empty?
|
|
267
|
+
data.is_a?(Hash) ? data.transform_keys(&:to_s) : {}
|
|
268
|
+
end
|
|
236
269
|
|
|
270
|
+
def apply_token_config(access_token, client_id, base_url)
|
|
237
271
|
self.configuration ||= Configuration.new
|
|
238
272
|
configuration.access_token = access_token.to_s
|
|
239
273
|
configuration.client_id = client_id.to_s
|
|
240
|
-
|
|
241
|
-
configuration.base_url = dhan_base.to_s if dhan_base.to_s != ""
|
|
274
|
+
configuration.base_url = base_url.to_s if base_url.to_s != ""
|
|
242
275
|
configuration
|
|
243
276
|
end
|
|
244
277
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: DhanHQ
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shubham Taywade
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -16,98 +16,98 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - ">="
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '0'
|
|
19
|
+
version: '8.0'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '0'
|
|
26
|
+
version: '8.0'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: base64
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
31
|
- - ">="
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '0'
|
|
33
|
+
version: '0.1'
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '0'
|
|
40
|
+
version: '0.1'
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: bindata
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
44
44
|
requirements:
|
|
45
45
|
- - ">="
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: '
|
|
47
|
+
version: '2.4'
|
|
48
48
|
type: :runtime
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
52
|
- - ">="
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: '
|
|
54
|
+
version: '2.4'
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
56
|
name: concurrent-ruby
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
58
58
|
requirements:
|
|
59
59
|
- - ">="
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '
|
|
61
|
+
version: '1.3'
|
|
62
62
|
type: :runtime
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
66
|
- - ">="
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '
|
|
68
|
+
version: '1.3'
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
70
|
name: csv
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
72
72
|
requirements:
|
|
73
73
|
- - ">="
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: '0'
|
|
75
|
+
version: '3.0'
|
|
76
76
|
type: :runtime
|
|
77
77
|
prerelease: false
|
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
79
|
requirements:
|
|
80
80
|
- - ">="
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: '0'
|
|
82
|
+
version: '3.0'
|
|
83
83
|
- !ruby/object:Gem::Dependency
|
|
84
84
|
name: dry-validation
|
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
|
86
86
|
requirements:
|
|
87
87
|
- - "~>"
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: '1.
|
|
89
|
+
version: '1.0'
|
|
90
90
|
type: :runtime
|
|
91
91
|
prerelease: false
|
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
93
|
requirements:
|
|
94
94
|
- - "~>"
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
|
-
version: '1.
|
|
96
|
+
version: '1.0'
|
|
97
97
|
- !ruby/object:Gem::Dependency
|
|
98
98
|
name: eventmachine
|
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
|
100
100
|
requirements:
|
|
101
101
|
- - ">="
|
|
102
102
|
- !ruby/object:Gem::Version
|
|
103
|
-
version: '
|
|
103
|
+
version: '1.2'
|
|
104
104
|
type: :runtime
|
|
105
105
|
prerelease: false
|
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
107
|
requirements:
|
|
108
108
|
- - ">="
|
|
109
109
|
- !ruby/object:Gem::Version
|
|
110
|
-
version: '
|
|
110
|
+
version: '1.2'
|
|
111
111
|
- !ruby/object:Gem::Dependency
|
|
112
112
|
name: faraday
|
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -128,42 +128,42 @@ dependencies:
|
|
|
128
128
|
requirements:
|
|
129
129
|
- - ">="
|
|
130
130
|
- !ruby/object:Gem::Version
|
|
131
|
-
version: '0'
|
|
131
|
+
version: '0.11'
|
|
132
132
|
type: :runtime
|
|
133
133
|
prerelease: false
|
|
134
134
|
version_requirements: !ruby/object:Gem::Requirement
|
|
135
135
|
requirements:
|
|
136
136
|
- - ">="
|
|
137
137
|
- !ruby/object:Gem::Version
|
|
138
|
-
version: '0'
|
|
138
|
+
version: '0.11'
|
|
139
139
|
- !ruby/object:Gem::Dependency
|
|
140
140
|
name: rotp
|
|
141
141
|
requirement: !ruby/object:Gem::Requirement
|
|
142
142
|
requirements:
|
|
143
143
|
- - ">="
|
|
144
144
|
- !ruby/object:Gem::Version
|
|
145
|
-
version: '0'
|
|
145
|
+
version: '6.0'
|
|
146
146
|
type: :runtime
|
|
147
147
|
prerelease: false
|
|
148
148
|
version_requirements: !ruby/object:Gem::Requirement
|
|
149
149
|
requirements:
|
|
150
150
|
- - ">="
|
|
151
151
|
- !ruby/object:Gem::Version
|
|
152
|
-
version: '0'
|
|
152
|
+
version: '6.0'
|
|
153
153
|
- !ruby/object:Gem::Dependency
|
|
154
154
|
name: zeitwerk
|
|
155
155
|
requirement: !ruby/object:Gem::Requirement
|
|
156
156
|
requirements:
|
|
157
157
|
- - ">="
|
|
158
158
|
- !ruby/object:Gem::Version
|
|
159
|
-
version: '
|
|
159
|
+
version: '2.6'
|
|
160
160
|
type: :runtime
|
|
161
161
|
prerelease: false
|
|
162
162
|
version_requirements: !ruby/object:Gem::Requirement
|
|
163
163
|
requirements:
|
|
164
164
|
- - ">="
|
|
165
165
|
- !ruby/object:Gem::Version
|
|
166
|
-
version: '
|
|
166
|
+
version: '2.6'
|
|
167
167
|
description: A production-grade Ruby SDK and Ruby client for Dhan API v2 built for
|
|
168
168
|
algorithmic trading, portfolio monitoring, and live trading systems on NSE, BSE
|
|
169
169
|
and MCX. Provides typed models, token lifecycle management, dry-validation contracts,
|
|
@@ -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
|