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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Contracts
|
|
5
|
+
# Validates request for POST /v2/data/newsheadline (ScanX live news).
|
|
6
|
+
#
|
|
7
|
+
# categories[] (ALL or any news category), limit (1-50), optional universe
|
|
8
|
+
# (PORTFOLIO / WATCHLIST) and stock_list[] of security ids. dhanClientId is
|
|
9
|
+
# injected by the client layer and is not validated here.
|
|
10
|
+
class NewsHeadlineContract < BaseContract
|
|
11
|
+
params do
|
|
12
|
+
required(:categories).filled(:array, min_size?: 1) do
|
|
13
|
+
each(:str?, included_in?: Constants::ScanX::NEWS_CATEGORIES)
|
|
14
|
+
end
|
|
15
|
+
required(:limit).filled(:integer, included_in?: 1..50)
|
|
16
|
+
optional(:universe).filled(:string, included_in?: Constants::ScanX::NEWS_UNIVERSES)
|
|
17
|
+
optional(:stock_list).filled(:array) { each(:str?) }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Contracts
|
|
5
|
+
# Validates request for POST /v2/data/technical (ScanX technical indicators).
|
|
6
|
+
#
|
|
7
|
+
# securityId (digits), exchangeSegment (NSE_EQ/IDX_I), instrument
|
|
8
|
+
# (INDEX/EQUITY), timeframe (1/5/15/D), indicators[] (e.g. SMA_20, RSI_14).
|
|
9
|
+
class TechnicalDataContract < BaseContract
|
|
10
|
+
params do
|
|
11
|
+
required(:security_id).filled(:string)
|
|
12
|
+
required(:exchange_segment).filled(:string, included_in?: Constants::ScanX::TECHNICAL_SEGMENTS)
|
|
13
|
+
required(:instrument).filled(:string, included_in?: Constants::ScanX::TECHNICAL_INSTRUMENTS)
|
|
14
|
+
required(:timeframe).filled(:string, included_in?: Constants::ScanX::TECHNICAL_TIMEFRAMES)
|
|
15
|
+
required(:indicators).filled(:array, min_size?: 1) do
|
|
16
|
+
each(:str?, included_in?: Constants::ScanX::TECHNICAL_INDICATORS)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
rule(:security_id) do
|
|
21
|
+
key.failure("must be a numeric security id string") unless value.to_s.match?(/\A\d+\z/)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/DhanHQ/core/base_api.rb
CHANGED
|
@@ -17,9 +17,10 @@ module DhanHQ
|
|
|
17
17
|
|
|
18
18
|
# Initializes the BaseAPI with the appropriate Client instance
|
|
19
19
|
#
|
|
20
|
+
# @param client [DhanHQ::Client, nil] Optional injected client instance
|
|
20
21
|
# @param api_type [Symbol] API type (`:order_api`, `:data_api`, `:non_trading_api`)
|
|
21
|
-
def initialize(api_type: self.class::API_TYPE)
|
|
22
|
-
@client = DhanHQ::Client.new(api_type: api_type)
|
|
22
|
+
def initialize(client: nil, api_type: self.class::API_TYPE)
|
|
23
|
+
@client = client || DhanHQ::Client.new(api_type: api_type)
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
# Perform a GET request via `Client`
|
|
@@ -63,21 +64,6 @@ module DhanHQ
|
|
|
63
64
|
|
|
64
65
|
private
|
|
65
66
|
|
|
66
|
-
# Performs an API request.
|
|
67
|
-
#
|
|
68
|
-
# @param method [Symbol] HTTP method (:get, :post, :put, :delete)
|
|
69
|
-
# @param endpoint [String] API endpoint
|
|
70
|
-
# @param params [Hash] Request parameters
|
|
71
|
-
# @return [Hash, Array] The parsed API response
|
|
72
|
-
# @raise [DhanHQ::Error] If an API error occurs.
|
|
73
|
-
def request(method, endpoint, params: {})
|
|
74
|
-
formatted_params = format_params(endpoint, params)
|
|
75
|
-
|
|
76
|
-
response = client.request(method, build_path(endpoint), formatted_params)
|
|
77
|
-
|
|
78
|
-
handle_response(response)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
67
|
# Construct the complete API URL
|
|
82
68
|
#
|
|
83
69
|
# @param endpoint [String] API endpoint
|
|
@@ -40,14 +40,31 @@ module DhanHQ
|
|
|
40
40
|
class << self
|
|
41
41
|
attr_reader :defined_attributes
|
|
42
42
|
|
|
43
|
-
# Registers the set of attributes for this model
|
|
43
|
+
# Registers the set of attributes for this model. Also defines the
|
|
44
|
+
# snake_case and camelCase readers once per class, so instances stop
|
|
45
|
+
# allocating per-record singleton methods.
|
|
44
46
|
#
|
|
45
47
|
# @param args [Array<Symbol, String>] A list of attribute names
|
|
46
48
|
def attributes(*args)
|
|
47
49
|
@defined_attributes ||= []
|
|
48
|
-
|
|
50
|
+
new_attrs = args.map(&:to_s)
|
|
51
|
+
@defined_attributes.concat(new_attrs)
|
|
52
|
+
define_accessors(new_attrs)
|
|
53
|
+
@defined_attributes
|
|
49
54
|
end
|
|
50
55
|
|
|
56
|
+
# Defines one snake_case and one camelCase reader per attribute, reading
|
|
57
|
+
# through @attributes so every instance stays in sync. Called from
|
|
58
|
+
# {attributes}; private because it is a declaration helper.
|
|
59
|
+
def define_accessors(attrs)
|
|
60
|
+
attrs.each do |attr|
|
|
61
|
+
camel = attr.to_s.camelize(:lower)
|
|
62
|
+
define_method(attr) { @attributes[attr] }
|
|
63
|
+
define_method(camel) { @attributes[attr] }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
private :define_accessors
|
|
67
|
+
|
|
51
68
|
# Provide a default API type, can be overridden by child classes
|
|
52
69
|
#
|
|
53
70
|
# e.g., def self.api_type; :data_api; end
|
|
@@ -64,6 +81,13 @@ module DhanHQ
|
|
|
64
81
|
@api ||= BaseAPI.new(api_type: api_type)
|
|
65
82
|
end
|
|
66
83
|
|
|
84
|
+
# Parameter key style used by {#to_request_params}: :camel (camelCase)
|
|
85
|
+
# by default. Models whose API takes TitleCase keys (the option chain)
|
|
86
|
+
# override this to :title.
|
|
87
|
+
def param_key_style
|
|
88
|
+
:camel
|
|
89
|
+
end
|
|
90
|
+
|
|
67
91
|
##
|
|
68
92
|
# Returns the API resource used by collection methods.
|
|
69
93
|
#
|
|
@@ -91,6 +115,8 @@ module DhanHQ
|
|
|
91
115
|
# Validate attributes before creating a new instance
|
|
92
116
|
def validate_attributes(attributes)
|
|
93
117
|
contract = validation_contract
|
|
118
|
+
return true if contract.nil?
|
|
119
|
+
|
|
94
120
|
result = contract.call(attributes)
|
|
95
121
|
|
|
96
122
|
raise ArgumentError, "Validation failed: #{result.errors.to_h}" if result.failure?
|
|
@@ -132,8 +158,6 @@ module DhanHQ
|
|
|
132
158
|
# @param attributes [Hash] The attributes of the resource
|
|
133
159
|
# @return [DhanHQ::BaseModel, DhanHQ::ErrorObject] The resource or error object
|
|
134
160
|
def create(attributes)
|
|
135
|
-
# validate_params!(attributes, validation_contract)
|
|
136
|
-
|
|
137
161
|
response = resource.post("", params: attributes)
|
|
138
162
|
build_from_response(response)
|
|
139
163
|
end
|
|
@@ -242,7 +266,7 @@ module DhanHQ
|
|
|
242
266
|
if self.class.defined_attributes&.include?("dhan_client_id") && !attrs[:dhan_client_id] && DhanHQ.configuration.client_id
|
|
243
267
|
attrs[:dhan_client_id] = DhanHQ.configuration.client_id
|
|
244
268
|
end
|
|
245
|
-
|
|
269
|
+
self.class.param_key_style == :title ? titleize_keys(attrs) : camelize_keys(attrs)
|
|
246
270
|
end
|
|
247
271
|
|
|
248
272
|
# Identifier inferred from the loaded attributes.
|
|
@@ -254,19 +278,15 @@ module DhanHQ
|
|
|
254
278
|
id_value&.to_s
|
|
255
279
|
end
|
|
256
280
|
|
|
257
|
-
#
|
|
281
|
+
# Assigns each declared attribute to an instance variable. The accessor
|
|
282
|
+
# methods themselves are defined once per class (see .attributes); the
|
|
283
|
+
# ivars exist for {AttributeHelper#inspect} and debugging.
|
|
258
284
|
def assign_attributes
|
|
259
285
|
self.class.defined_attributes&.each do |attr|
|
|
260
286
|
instance_variable_set(:"@#{attr}", @attributes[attr])
|
|
261
|
-
define_singleton_method(attr) { instance_variable_get(:"@#{attr}") }
|
|
262
|
-
define_singleton_method(attr.to_s.camelize(:lower)) { instance_variable_get(:"@#{attr}") }
|
|
263
287
|
end
|
|
264
288
|
end
|
|
265
289
|
|
|
266
|
-
def optionchain_api?
|
|
267
|
-
self.class.name.include?("OptionChain")
|
|
268
|
-
end
|
|
269
|
-
|
|
270
290
|
# Validate attributes using contract
|
|
271
291
|
def valid?
|
|
272
292
|
contract_class = validation_contract || self.class.validation_contract
|
|
@@ -25,8 +25,9 @@ module DhanHQ
|
|
|
25
25
|
# Extracts a fabricated id out of a request path.
|
|
26
26
|
ID_PATTERN = /#{Regexp.escape(ID_PREFIX)}[A-Za-z0-9]+/
|
|
27
27
|
|
|
28
|
-
def initialize(ledger: Ledger.instance)
|
|
28
|
+
def initialize(ledger: Ledger.instance, config: nil)
|
|
29
29
|
@ledger = ledger
|
|
30
|
+
@config = config
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
# Whether this request should be answered locally.
|
|
@@ -35,7 +36,7 @@ module DhanHQ
|
|
|
35
36
|
# @param path [String, nil] Request path
|
|
36
37
|
# @return [Boolean]
|
|
37
38
|
def simulates?(method, path)
|
|
38
|
-
return false unless DhanHQ.configuration&.dry_run?
|
|
39
|
+
return false unless (@config || DhanHQ.configuration)&.dry_run?
|
|
39
40
|
|
|
40
41
|
WritePaths.mutating?(method, path) || replayable_read?(method, path)
|
|
41
42
|
end
|
|
@@ -69,27 +69,5 @@ module DhanHQ
|
|
|
69
69
|
instance_vars = self.class.defined_attributes.map { |attr| "#{attr}: #{instance_variable_get(:"@#{attr}")}" }
|
|
70
70
|
"#<#{self.class.name} #{instance_vars.join(", ")}>"
|
|
71
71
|
end
|
|
72
|
-
|
|
73
|
-
# def format_params(path, params)
|
|
74
|
-
# return params unless params.is_a?(Hash)
|
|
75
|
-
|
|
76
|
-
# if optionchain_api?(path)
|
|
77
|
-
# titleize_keys(params)
|
|
78
|
-
# else
|
|
79
|
-
# camelize_keys(params)
|
|
80
|
-
# end
|
|
81
|
-
# end
|
|
82
|
-
|
|
83
|
-
# def camelize_keys(hash)
|
|
84
|
-
# hash.transform_keys { |key| key.to_s.camelize(:lower) }
|
|
85
|
-
# end
|
|
86
|
-
|
|
87
|
-
# def titleize_keys(hash)
|
|
88
|
-
# hash.transform_keys { |key| key.to_s.titleize.delete(" ") }
|
|
89
|
-
# end
|
|
90
|
-
|
|
91
|
-
# def optionchain_api?(path)
|
|
92
|
-
# path.include?("/optionchain")
|
|
93
|
-
# end
|
|
94
72
|
end
|
|
95
73
|
end
|
|
@@ -21,6 +21,16 @@ module DhanHQ
|
|
|
21
21
|
|
|
22
22
|
private
|
|
23
23
|
|
|
24
|
+
def current_config
|
|
25
|
+
if respond_to?(:config) && config
|
|
26
|
+
config
|
|
27
|
+
elsif respond_to?(:client) && client.respond_to?(:config) && client.config
|
|
28
|
+
client.config
|
|
29
|
+
else
|
|
30
|
+
DhanHQ.configuration
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
24
34
|
# Dynamically builds headers for each request.
|
|
25
35
|
#
|
|
26
36
|
# @param path [String] The API endpoint path.
|
|
@@ -41,7 +51,7 @@ module DhanHQ
|
|
|
41
51
|
|
|
42
52
|
# Add client-id for DATA APIs (now including sandbox profile/funds)
|
|
43
53
|
if data_api?(path)
|
|
44
|
-
client_id =
|
|
54
|
+
client_id = current_config&.client_id
|
|
45
55
|
unless client_id
|
|
46
56
|
raise DhanHQ::InvalidAuthenticationError,
|
|
47
57
|
"client_id is required for DATA APIs but not set. Please configure DhanHQ with DHAN_CLIENT_ID."
|
|
@@ -53,7 +63,7 @@ module DhanHQ
|
|
|
53
63
|
end
|
|
54
64
|
|
|
55
65
|
def resolved_access_token
|
|
56
|
-
|
|
66
|
+
current_config&.resolved_access_token
|
|
57
67
|
end
|
|
58
68
|
|
|
59
69
|
# Determines if the API path requires a `client-id` header.
|
|
@@ -80,7 +90,7 @@ module DhanHQ
|
|
|
80
90
|
|
|
81
91
|
out = payload
|
|
82
92
|
if path && %i[post put patch].include?(method)
|
|
83
|
-
client_id =
|
|
93
|
+
client_id = current_config&.client_id
|
|
84
94
|
needs_client_id = data_api?(path) || payload_requires_dhan_client_id?(path)
|
|
85
95
|
if client_id && needs_client_id && !payload.key?(:dhanClientId) && !payload.key?("dhanClientId")
|
|
86
96
|
out = payload.dup
|
|
@@ -55,6 +55,13 @@ module DhanHQ
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
class << self
|
|
58
|
+
##
|
|
59
|
+
# The option chain API takes TitleCase keys (e.g. "UnderlyingScripValue");
|
|
60
|
+
# see BaseModel.param_key_style.
|
|
61
|
+
def param_key_style
|
|
62
|
+
:title
|
|
63
|
+
end
|
|
64
|
+
|
|
58
65
|
##
|
|
59
66
|
# Provides a shared instance of the OptionChain resource.
|
|
60
67
|
#
|
data/lib/DhanHQ/models/order.rb
CHANGED
|
@@ -491,12 +491,12 @@ module DhanHQ
|
|
|
491
491
|
# order = DhanHQ::Models::Order.find("112111182045")
|
|
492
492
|
# order.modify(validity: "IOC")
|
|
493
493
|
#
|
|
494
|
-
# @raise [
|
|
494
|
+
# @raise [DhanHQ::OrderError] If order ID is missing
|
|
495
495
|
# @raise [DhanHQ::ValidationError] If validation fails for any parameter
|
|
496
496
|
# @raise [DhanHQ::ModificationLimitError] If this instance has already been modified 25 times (Dhan API cap)
|
|
497
497
|
# @note Count is per Order instance in this process; a fresh find() resets it.
|
|
498
498
|
def modify(new_params)
|
|
499
|
-
raise "Order ID is required to modify an order" unless id
|
|
499
|
+
raise DhanHQ::OrderError, "Order ID is required to modify an order" unless id
|
|
500
500
|
|
|
501
501
|
count = @modification_count || 0
|
|
502
502
|
if count >= Constants::RateLimit::ORDER_MODIFICATIONS_PER_ORDER
|
|
@@ -537,9 +537,9 @@ module DhanHQ
|
|
|
537
537
|
# puts "Failed to cancel order"
|
|
538
538
|
# end
|
|
539
539
|
#
|
|
540
|
-
# @raise [
|
|
540
|
+
# @raise [DhanHQ::OrderError] If order ID is missing
|
|
541
541
|
def cancel
|
|
542
|
-
raise "Order ID is required to cancel an order" unless id
|
|
542
|
+
raise DhanHQ::OrderError, "Order ID is required to cancel an order" unless id
|
|
543
543
|
|
|
544
544
|
DhanHQ.logger&.info("[DhanHQ::Models::Order] Cancelling order #{id}")
|
|
545
545
|
response = self.class.resource.cancel(id)
|
|
@@ -566,9 +566,9 @@ module DhanHQ
|
|
|
566
566
|
# puts "Updated status: #{order.order_status}"
|
|
567
567
|
# puts "Filled: #{order.filled_qty}/#{order.quantity}"
|
|
568
568
|
#
|
|
569
|
-
# @raise [
|
|
569
|
+
# @raise [DhanHQ::OrderError] If order ID is missing
|
|
570
570
|
def refresh
|
|
571
|
-
raise "Order ID is required to refresh an order" unless id
|
|
571
|
+
raise DhanHQ::OrderError, "Order ID is required to refresh an order" unless id
|
|
572
572
|
|
|
573
573
|
self.class.find(id)
|
|
574
574
|
end
|
|
@@ -603,7 +603,7 @@ module DhanHQ
|
|
|
603
603
|
alias delete destroy
|
|
604
604
|
|
|
605
605
|
def slice_order(params)
|
|
606
|
-
raise "Order ID is required to slice an order" unless id
|
|
606
|
+
raise DhanHQ::OrderError, "Order ID is required to slice an order" unless id
|
|
607
607
|
|
|
608
608
|
normalized = snake_case(params)
|
|
609
609
|
normalized[:dhan_client_id] ||= DhanHQ.configuration&.client_id
|
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
|
@@ -23,8 +23,9 @@ module DhanHQ
|
|
|
23
23
|
# @param options [Hash] Connection options
|
|
24
24
|
# @option options [Integer] :max_backoff Maximum backoff time (default: 90)
|
|
25
25
|
# @option options [Integer] :cool_off_429 Cool off time for 429 errors (default: 60)
|
|
26
|
-
def initialize(url:, **options)
|
|
26
|
+
def initialize(url:, config: nil, **options)
|
|
27
27
|
@url = url
|
|
28
|
+
@config = config || DhanHQ.configuration || DhanHQ.ensure_configuration!
|
|
28
29
|
@callbacks = Concurrent::Map.new { |h, k| h[k] = [] }
|
|
29
30
|
@started = Concurrent::AtomicBoolean.new(false)
|
|
30
31
|
@stop = false
|