DhanHQ 2.3.0 → 2.5.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 +50 -1
- data/CODE_REVIEW_ISSUES.md +2 -2
- data/GUIDE.md +2 -2
- data/README.md +194 -741
- data/REVIEW_SUMMARY.md +2 -2
- data/{README1.md → docs/ARCHIVE_README.md} +4 -4
- data/docs/AUTHENTICATION.md +116 -2
- data/docs/CONFIGURATION.md +109 -0
- data/docs/SUPER_ORDERS.md +284 -0
- data/docs/TESTING_GUIDE.md +8 -8
- data/docs/TROUBLESHOOTING.md +117 -0
- data/docs/WEBSOCKET_PROTOCOL.md +154 -0
- data/docs/live_order_updates.md +2 -2
- data/docs/rails_integration.md +7 -7
- data/docs/standalone_ruby_websocket_integration.md +24 -24
- data/docs/technical_analysis.md +1 -1
- data/docs/websocket_integration.md +4 -4
- data/examples/comprehensive_websocket_examples.rb +2 -2
- data/examples/instrument_finder_test.rb +2 -2
- data/examples/market_depth_example.rb +2 -2
- data/examples/market_feed_example.rb +2 -2
- data/examples/order_update_example.rb +2 -2
- data/examples/trading_fields_example.rb +2 -2
- data/lib/DhanHQ/auth/token_generator.rb +33 -0
- data/lib/DhanHQ/auth/token_manager.rb +88 -0
- data/lib/DhanHQ/auth/token_renewal.rb +25 -0
- data/lib/DhanHQ/auth.rb +91 -31
- data/lib/DhanHQ/client.rb +42 -2
- data/lib/DhanHQ/configuration.rb +2 -2
- data/lib/DhanHQ/contracts/order_contract.rb +0 -23
- data/lib/DhanHQ/contracts/trade_by_order_id_contract.rb +12 -0
- data/lib/DhanHQ/contracts/trade_contract.rb +0 -65
- data/lib/DhanHQ/contracts/trade_history_contract.rb +52 -0
- data/lib/DhanHQ/core/auth_api.rb +21 -0
- data/lib/DhanHQ/helpers/request_helper.rb +1 -1
- data/lib/DhanHQ/models/alert_order.rb +22 -0
- data/lib/DhanHQ/models/edis.rb +110 -0
- data/lib/DhanHQ/models/kill_switch.rb +22 -0
- data/lib/DhanHQ/models/margin.rb +49 -0
- data/lib/DhanHQ/models/pnl_exit.rb +130 -0
- data/lib/DhanHQ/models/position.rb +22 -0
- data/lib/DhanHQ/models/postback.rb +123 -0
- data/lib/DhanHQ/models/token_response.rb +88 -0
- data/lib/DhanHQ/resources/kill_switch.rb +8 -0
- data/lib/DhanHQ/resources/margin_calculator.rb +9 -0
- data/lib/DhanHQ/resources/pnl_exit.rb +37 -0
- data/lib/DhanHQ/resources/positions.rb +8 -0
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/dhan_hq.rb +31 -81
- metadata +46 -4
- data/lib/DhanHQ/config.rb +0 -33
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Auth
|
|
5
|
+
# Backward-compatible wrapper for token renewal.
|
|
6
|
+
# Delegates to module-level Auth.renew_token.
|
|
7
|
+
class TokenRenewal
|
|
8
|
+
def renew
|
|
9
|
+
config = DhanHQ.configuration
|
|
10
|
+
raise Errors::AuthenticationError, "Missing configuration" unless config
|
|
11
|
+
|
|
12
|
+
access_token = config.resolved_access_token
|
|
13
|
+
dhan_client_id = config.client_id
|
|
14
|
+
raise Errors::AuthenticationError, "Missing dhanClientId (client_id)" if dhan_client_id.to_s.strip.empty?
|
|
15
|
+
|
|
16
|
+
response = Auth.renew_token(
|
|
17
|
+
access_token: access_token,
|
|
18
|
+
client_id: dhan_client_id
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
Models::TokenResponse.new(response)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/DhanHQ/auth.rb
CHANGED
|
@@ -2,52 +2,112 @@
|
|
|
2
2
|
|
|
3
3
|
require "faraday"
|
|
4
4
|
require "json"
|
|
5
|
+
require "rotp"
|
|
6
|
+
require_relative "errors"
|
|
5
7
|
|
|
6
8
|
module DhanHQ
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
9
|
+
# Module-level helpers for Dhan authentication APIs.
|
|
10
|
+
#
|
|
11
|
+
# Supports:
|
|
12
|
+
# - Generating access tokens via TOTP (for automated systems)
|
|
13
|
+
# - Renewing web-generated tokens (web-only tokens)
|
|
14
|
+
#
|
|
15
|
+
# @example Generate token with TOTP
|
|
16
|
+
# totp = DhanHQ::Auth.generate_totp(ENV["DHAN_TOTP_SECRET"])
|
|
17
|
+
# response = DhanHQ::Auth.generate_access_token(
|
|
18
|
+
# dhan_client_id: ENV["DHAN_CLIENT_ID"],
|
|
19
|
+
# pin: ENV["DHAN_PIN"],
|
|
20
|
+
# totp: totp
|
|
21
|
+
# )
|
|
22
|
+
# token = response["accessToken"]
|
|
23
|
+
#
|
|
24
|
+
# @example Renew web token
|
|
25
|
+
# response = DhanHQ::Auth.renew_token(
|
|
26
|
+
# access_token: current_token,
|
|
27
|
+
# client_id: ENV["DHAN_CLIENT_ID"]
|
|
28
|
+
# )
|
|
11
29
|
module Auth
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
30
|
+
AUTH_BASE_URL = "https://auth.dhan.co"
|
|
31
|
+
API_BASE_URL = "https://api.dhan.co/v2"
|
|
32
|
+
|
|
33
|
+
# Generates an access token using TOTP authentication.
|
|
15
34
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# @param
|
|
19
|
-
# @
|
|
20
|
-
# @
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
# POST https://auth.dhan.co/app/generateAccessToken
|
|
36
|
+
#
|
|
37
|
+
# @param dhan_client_id [String] Your Dhan client ID
|
|
38
|
+
# @param pin [String] Your 6-digit Dhan PIN
|
|
39
|
+
# @param totp [String] 6-digit TOTP code (or use generate_totp helper)
|
|
40
|
+
# @return [Hash] Response hash with accessToken, expiryTime, etc.
|
|
41
|
+
# @raise [DhanHQ::InvalidAuthenticationError] On authentication failure
|
|
42
|
+
def self.generate_access_token(dhan_client_id:, pin:, totp:)
|
|
43
|
+
conn = build_connection(AUTH_BASE_URL)
|
|
44
|
+
|
|
45
|
+
response = conn.post("/app/generateAccessToken") do |req|
|
|
46
|
+
req.headers["Accept"] = "application/json"
|
|
47
|
+
|
|
48
|
+
req.params = {
|
|
49
|
+
dhanClientId: dhan_client_id,
|
|
50
|
+
pin: pin,
|
|
51
|
+
totp: totp
|
|
52
|
+
}
|
|
29
53
|
end
|
|
30
54
|
|
|
31
|
-
response
|
|
55
|
+
handle_response(response, context: "GenerateAccessToken")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Renews a web-generated access token (24h validity).
|
|
59
|
+
#
|
|
60
|
+
# POST https://api.dhan.co/v2/RenewToken
|
|
61
|
+
#
|
|
62
|
+
# ⚠️ Works ONLY for tokens generated from Dhan Web dashboard.
|
|
63
|
+
# For TOTP-generated tokens, regenerate instead of renewing.
|
|
64
|
+
#
|
|
65
|
+
# @param access_token [String] Current JWT access token
|
|
66
|
+
# @param client_id [String] Dhan client ID (dhanClientId)
|
|
67
|
+
# @return [Hash] Response hash with new accessToken and expiryTime
|
|
68
|
+
# @raise [DhanHQ::InvalidAuthenticationError] On renewal failure
|
|
69
|
+
def self.renew_token(access_token:, client_id:)
|
|
70
|
+
conn = build_connection(API_BASE_URL)
|
|
71
|
+
|
|
72
|
+
response = conn.post("/RenewToken") do |req|
|
|
32
73
|
req.headers["access-token"] = access_token
|
|
33
74
|
req.headers["dhanClientId"] = client_id
|
|
34
75
|
req.headers["Accept"] = "application/json"
|
|
35
76
|
end
|
|
36
77
|
|
|
78
|
+
handle_response(response, context: "RenewToken")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Generates a 6-digit TOTP code from a secret.
|
|
82
|
+
#
|
|
83
|
+
# @param secret [String] TOTP secret (from authenticator app setup)
|
|
84
|
+
# @return [String] 6-digit TOTP code
|
|
85
|
+
def self.generate_totp(secret)
|
|
86
|
+
ROTP::TOTP.new(secret).now
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def self.build_connection(base_url)
|
|
92
|
+
Faraday.new(url: base_url) do |c|
|
|
93
|
+
c.request :url_encoded
|
|
94
|
+
c.response :json, content_type: /\bjson$/
|
|
95
|
+
c.adapter Faraday.default_adapter
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
private_class_method :build_connection
|
|
99
|
+
|
|
100
|
+
def self.handle_response(response, context:)
|
|
37
101
|
unless response.success?
|
|
38
|
-
body =
|
|
39
|
-
JSON.parse(response.body.to_s)
|
|
40
|
-
rescue JSON::ParserError
|
|
41
|
-
{}
|
|
42
|
-
end
|
|
102
|
+
body = response.body.is_a?(Hash) ? response.body : {}
|
|
43
103
|
error_message = body["errorMessage"] || body["message"] || response.body.to_s
|
|
44
|
-
|
|
104
|
+
|
|
105
|
+
raise DhanHQ::InvalidAuthenticationError,
|
|
106
|
+
"#{context} failed: #{response.status} #{error_message}"
|
|
45
107
|
end
|
|
46
108
|
|
|
47
|
-
|
|
48
|
-
data = JSON.parse(data) if data.is_a?(String)
|
|
49
|
-
result = data.is_a?(Hash) ? data : {}
|
|
50
|
-
result.with_indifferent_access
|
|
109
|
+
response.body.is_a?(Hash) ? response.body : {}
|
|
51
110
|
end
|
|
111
|
+
private_class_method :handle_response
|
|
52
112
|
end
|
|
53
113
|
end
|
data/lib/DhanHQ/client.rb
CHANGED
|
@@ -29,6 +29,8 @@ module DhanHQ
|
|
|
29
29
|
# @return [Faraday::Connection] The connection instance used for API requests.
|
|
30
30
|
attr_reader :connection
|
|
31
31
|
|
|
32
|
+
attr_reader :token_manager
|
|
33
|
+
|
|
32
34
|
# Initializes a new DhanHQ Client instance with a Faraday connection.
|
|
33
35
|
#
|
|
34
36
|
# @example Create a new client:
|
|
@@ -38,9 +40,9 @@ module DhanHQ
|
|
|
38
40
|
# @return [DhanHQ::Client] A new client instance.
|
|
39
41
|
# @raise [DhanHQ::Error] If configuration is invalid or rate limiter initialization fails
|
|
40
42
|
def initialize(api_type:)
|
|
41
|
-
# Configure from ENV if
|
|
43
|
+
# Configure from ENV if DHAN_CLIENT_ID is present (backward compatible behavior)
|
|
42
44
|
# Validation happens at request time in build_headers, not here
|
|
43
|
-
DhanHQ.configure_with_env if ENV.fetch("
|
|
45
|
+
DhanHQ.configure_with_env if ENV.fetch("DHAN_CLIENT_ID", nil)
|
|
44
46
|
|
|
45
47
|
# Use shared rate limiter instance per API type to ensure proper coordination
|
|
46
48
|
@rate_limiter = RateLimiter.for(api_type)
|
|
@@ -72,6 +74,7 @@ module DhanHQ
|
|
|
72
74
|
# @return [HashWithIndifferentAccess, Array<HashWithIndifferentAccess>] Parsed JSON response.
|
|
73
75
|
# @raise [DhanHQ::Error] If an HTTP error occurs.
|
|
74
76
|
def request(method, path, payload, retries: 3)
|
|
77
|
+
@token_manager&.ensure_valid_token!
|
|
75
78
|
@rate_limiter.throttle! # **Ensure we don't hit rate limit before calling API**
|
|
76
79
|
|
|
77
80
|
attempt = 0
|
|
@@ -159,6 +162,43 @@ module DhanHQ
|
|
|
159
162
|
request(:delete, path, params)
|
|
160
163
|
end
|
|
161
164
|
|
|
165
|
+
def generate_access_token(dhan_client_id:, pin:, totp: nil, totp_secret: nil)
|
|
166
|
+
token = Auth::TokenGenerator.new.generate(
|
|
167
|
+
dhan_client_id: dhan_client_id,
|
|
168
|
+
pin: pin,
|
|
169
|
+
totp: totp,
|
|
170
|
+
totp_secret: totp_secret
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
DhanHQ.configure do |config|
|
|
174
|
+
config.access_token = token.access_token
|
|
175
|
+
config.client_id = token.client_id if token.client_id.to_s.strip != ""
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
token
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def renew_access_token
|
|
182
|
+
token = Auth::TokenRenewal.new.renew
|
|
183
|
+
|
|
184
|
+
DhanHQ.configure do |config|
|
|
185
|
+
config.access_token = token.access_token
|
|
186
|
+
config.client_id = token.client_id if token.client_id.to_s.strip != ""
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
token
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def enable_auto_token_management!(dhan_client_id:, pin:, totp_secret:)
|
|
193
|
+
@token_manager = Auth::TokenManager.new(
|
|
194
|
+
dhan_client_id: dhan_client_id,
|
|
195
|
+
pin: pin,
|
|
196
|
+
totp_secret: totp_secret
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
@token_manager.generate!
|
|
200
|
+
end
|
|
201
|
+
|
|
162
202
|
private
|
|
163
203
|
|
|
164
204
|
# Calculates exponential backoff time
|
data/lib/DhanHQ/configuration.rb
CHANGED
|
@@ -98,8 +98,8 @@ module DhanHQ
|
|
|
98
98
|
# config.client_id = "your_client_id"
|
|
99
99
|
# config.access_token = "your_access_token"
|
|
100
100
|
def initialize
|
|
101
|
-
@client_id = ENV.fetch("
|
|
102
|
-
@access_token = ENV.fetch("
|
|
101
|
+
@client_id = ENV.fetch("DHAN_CLIENT_ID", nil)
|
|
102
|
+
@access_token = ENV.fetch("DHAN_ACCESS_TOKEN", nil)
|
|
103
103
|
@base_url = ENV.fetch("DHAN_BASE_URL", "https://api.dhan.co/v2")
|
|
104
104
|
@ws_version = ENV.fetch("DHAN_WS_VERSION", 2).to_i
|
|
105
105
|
@ws_order_url = ENV.fetch("DHAN_WS_ORDER_URL", "wss://api-order-update.dhan.co")
|
|
@@ -75,28 +75,5 @@ module DhanHQ
|
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
end
|
|
78
|
-
|
|
79
|
-
# Contract enforcing additional requirements for new order placement.
|
|
80
|
-
class PlaceOrderContract < OrderContract
|
|
81
|
-
# Additional placement specific rules
|
|
82
|
-
rule(:after_market_order) do
|
|
83
|
-
key.failure("amo_time required for after market orders") if value == true && !values[:amo_time]
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Contract extending {OrderContract} for order modification payloads.
|
|
88
|
-
class ModifyOrderContract < OrderContract
|
|
89
|
-
# Modification specific requirements
|
|
90
|
-
params do
|
|
91
|
-
required(:order_id).filled(:string)
|
|
92
|
-
optional(:quantity).maybe(:integer, gt?: 0)
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
rule do
|
|
96
|
-
if !values[:price] && !values[:quantity] && !values[:trigger_price]
|
|
97
|
-
key.failure("at least one modification field required")
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
78
|
end
|
|
102
79
|
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Contracts
|
|
5
|
+
# Validation contract for trade-by-order-id requests.
|
|
6
|
+
class TradeByOrderIdContract < BaseContract
|
|
7
|
+
params do
|
|
8
|
+
required(:order_id).filled(:string, min_size?: 1)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "date"
|
|
4
|
-
|
|
5
3
|
module DhanHQ
|
|
6
4
|
module Contracts
|
|
7
5
|
##
|
|
@@ -10,68 +8,5 @@ module DhanHQ
|
|
|
10
8
|
# No input validation needed for GET requests
|
|
11
9
|
# These contracts are mainly for documentation and future extensibility
|
|
12
10
|
end
|
|
13
|
-
|
|
14
|
-
##
|
|
15
|
-
# Validation contract for trade history requests
|
|
16
|
-
class TradeHistoryContract < BaseContract
|
|
17
|
-
params do
|
|
18
|
-
required(:from_date).filled(:string)
|
|
19
|
-
required(:to_date).filled(:string)
|
|
20
|
-
optional(:page).filled(:integer, gteq?: 0)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
rule(:from_date) do
|
|
24
|
-
key.failure("must be in YYYY-MM-DD format (e.g., 2024-01-15)") unless valid_date_format?(value)
|
|
25
|
-
key.failure("must be a valid trading date (no weekend dates)") if valid_date_format?(value) && !trading_day?(Date.parse(value))
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
rule(:to_date) do
|
|
29
|
-
key.failure("must be in YYYY-MM-DD format (e.g., 2024-01-15)") unless valid_date_format?(value)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
rule(:from_date, :to_date) do
|
|
33
|
-
from_date_valid = valid_date_format?(values[:from_date])
|
|
34
|
-
to_date_valid = valid_date_format?(values[:to_date])
|
|
35
|
-
|
|
36
|
-
if values[:from_date] && values[:to_date] && from_date_valid && to_date_valid
|
|
37
|
-
begin
|
|
38
|
-
from_date = Date.parse(values[:from_date])
|
|
39
|
-
to_date = Date.parse(values[:to_date])
|
|
40
|
-
|
|
41
|
-
key.failure("from_date must be before to_date") if from_date >= to_date
|
|
42
|
-
rescue Date::Error
|
|
43
|
-
key.failure("invalid date format")
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
private
|
|
49
|
-
|
|
50
|
-
def valid_date_format?(date_string)
|
|
51
|
-
return false unless date_string.is_a?(String)
|
|
52
|
-
return false unless date_string.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
53
|
-
|
|
54
|
-
begin
|
|
55
|
-
Date.parse(date_string)
|
|
56
|
-
true
|
|
57
|
-
rescue Date::Error
|
|
58
|
-
false
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def trading_day?(date)
|
|
63
|
-
return false unless date.is_a?(Date)
|
|
64
|
-
|
|
65
|
-
(1..5).cover?(date.wday)
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
##
|
|
70
|
-
# Validation contract for trade by order ID requests
|
|
71
|
-
class TradeByOrderIdContract < BaseContract
|
|
72
|
-
params do
|
|
73
|
-
required(:order_id).filled(:string, min_size?: 1)
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
11
|
end
|
|
77
12
|
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module DhanHQ
|
|
6
|
+
module Contracts
|
|
7
|
+
# Validation contract for trade history requests.
|
|
8
|
+
class TradeHistoryContract < BaseContract
|
|
9
|
+
params do
|
|
10
|
+
required(:from_date).filled(:string)
|
|
11
|
+
required(:to_date).filled(:string)
|
|
12
|
+
optional(:page).filled(:integer, gteq?: 0)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
rule(:from_date) do
|
|
16
|
+
key.failure("must be in YYYY-MM-DD format (e.g., 2024-01-15)") unless valid_date_format?(value)
|
|
17
|
+
next unless valid_date_format?(value)
|
|
18
|
+
|
|
19
|
+
key.failure("must be a valid trading date (no weekend dates)") unless trading_day?(Date.parse(value))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
rule(:to_date) do
|
|
23
|
+
key.failure("must be in YYYY-MM-DD format (e.g., 2024-01-15)") unless valid_date_format?(value)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
rule(:from_date, :to_date) do
|
|
27
|
+
from = values[:from_date]
|
|
28
|
+
to = values[:to_date]
|
|
29
|
+
next unless valid_date_format?(from) && valid_date_format?(to)
|
|
30
|
+
|
|
31
|
+
key.failure("from_date must be before to_date") if Date.parse(from) >= Date.parse(to)
|
|
32
|
+
rescue Date::Error
|
|
33
|
+
key.failure("invalid date format")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def valid_date_format?(date_string)
|
|
39
|
+
return false unless date_string.is_a?(String) && date_string.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
40
|
+
|
|
41
|
+
Date.parse(date_string)
|
|
42
|
+
true
|
|
43
|
+
rescue Date::Error
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def trading_day?(date)
|
|
48
|
+
date.is_a?(Date) && (1..5).cover?(date.wday)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
|
|
5
|
+
module DhanHQ
|
|
6
|
+
# Faraday client for Dhan auth endpoints.
|
|
7
|
+
#
|
|
8
|
+
# This class intentionally lives at the top-level namespace so it autoloads
|
|
9
|
+
# cleanly from `lib/DhanHQ/core/auth_api.rb` with Zeitwerk `collapse`.
|
|
10
|
+
class AuthAPI
|
|
11
|
+
BASE_URL = "https://auth.dhan.co"
|
|
12
|
+
|
|
13
|
+
def connection
|
|
14
|
+
@connection ||= Faraday.new(url: BASE_URL) do |faraday|
|
|
15
|
+
faraday.request :url_encoded
|
|
16
|
+
faraday.response :json, content_type: /\bjson$/
|
|
17
|
+
faraday.adapter Faraday.default_adapter
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -44,7 +44,7 @@ module DhanHQ
|
|
|
44
44
|
client_id = DhanHQ.configuration&.client_id
|
|
45
45
|
unless client_id
|
|
46
46
|
raise DhanHQ::InvalidAuthenticationError,
|
|
47
|
-
"client_id is required for DATA APIs but not set. Please configure DhanHQ with
|
|
47
|
+
"client_id is required for DATA APIs but not set. Please configure DhanHQ with DHAN_CLIENT_ID."
|
|
48
48
|
end
|
|
49
49
|
headers["client-id"] = client_id
|
|
50
50
|
end
|
|
@@ -48,6 +48,28 @@ module DhanHQ
|
|
|
48
48
|
|
|
49
49
|
find(response["alertId"])
|
|
50
50
|
end
|
|
51
|
+
|
|
52
|
+
##
|
|
53
|
+
# Modify an existing conditional trigger/alert order.
|
|
54
|
+
#
|
|
55
|
+
# @param alert_id [String] The alert ID to modify
|
|
56
|
+
# @param params [Hash] Updated parameters (condition, orders, etc.)
|
|
57
|
+
#
|
|
58
|
+
# @return [AlertOrder, nil] Updated AlertOrder instance, or nil on failure
|
|
59
|
+
#
|
|
60
|
+
# @example Modify an alert order's condition
|
|
61
|
+
# updated = DhanHQ::Models::AlertOrder.modify("12345",
|
|
62
|
+
# condition: { comparing_value: 300 },
|
|
63
|
+
# orders: [{ quantity: 20 }]
|
|
64
|
+
# )
|
|
65
|
+
#
|
|
66
|
+
def modify(alert_id, params)
|
|
67
|
+
normalized = snake_case(params)
|
|
68
|
+
response = resource.update(alert_id, camelize_keys(normalized))
|
|
69
|
+
return nil unless success_response?(response)
|
|
70
|
+
|
|
71
|
+
find(alert_id)
|
|
72
|
+
end
|
|
51
73
|
end
|
|
52
74
|
|
|
53
75
|
def id
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Models
|
|
5
|
+
##
|
|
6
|
+
# Model for EDIS (Electronic Delivery Instruction Slip) operations.
|
|
7
|
+
#
|
|
8
|
+
# EDIS is used for selling holdings from your demat account. The API provides
|
|
9
|
+
# endpoints to generate T-PIN, create eDIS forms, and check authorization status.
|
|
10
|
+
#
|
|
11
|
+
# @example Generate T-PIN
|
|
12
|
+
# DhanHQ::Models::Edis.generate_tpin
|
|
13
|
+
#
|
|
14
|
+
# @example Generate eDIS form
|
|
15
|
+
# response = DhanHQ::Models::Edis.generate_form(
|
|
16
|
+
# isin: "INE155A01022",
|
|
17
|
+
# qty: 10,
|
|
18
|
+
# exchange: "NSE",
|
|
19
|
+
# segment: "E",
|
|
20
|
+
# bulk: false
|
|
21
|
+
# )
|
|
22
|
+
#
|
|
23
|
+
# @example Check EDIS status for a security
|
|
24
|
+
# status = DhanHQ::Models::Edis.inquire(isin: "INE155A01022")
|
|
25
|
+
#
|
|
26
|
+
class Edis < BaseModel
|
|
27
|
+
HTTP_PATH = "/edis"
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
##
|
|
31
|
+
# Provides a shared instance of the Edis resource.
|
|
32
|
+
#
|
|
33
|
+
# @return [DhanHQ::Resources::Edis] The Edis resource client instance
|
|
34
|
+
def resource
|
|
35
|
+
@resource ||= DhanHQ::Resources::Edis.new
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# Generate T-PIN for eDIS authorization.
|
|
40
|
+
#
|
|
41
|
+
# Triggers T-PIN generation which is sent to the user's registered mobile/email.
|
|
42
|
+
#
|
|
43
|
+
# @return [Hash] API response
|
|
44
|
+
#
|
|
45
|
+
# @example Generate T-PIN before selling holdings
|
|
46
|
+
# DhanHQ::Models::Edis.generate_tpin
|
|
47
|
+
#
|
|
48
|
+
def generate_tpin
|
|
49
|
+
resource.tpin
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
##
|
|
53
|
+
# Generate an eDIS form for authorizing sale of holdings.
|
|
54
|
+
#
|
|
55
|
+
# @param isin [String] ISIN of the security (e.g., "INE155A01022")
|
|
56
|
+
# @param qty [Integer] Quantity to authorize for sale
|
|
57
|
+
# @param exchange [String] Exchange name (e.g., "NSE", "BSE")
|
|
58
|
+
# @param segment [String] Segment identifier (e.g., "E")
|
|
59
|
+
# @param bulk [Boolean] Whether this is a bulk authorization (default: false)
|
|
60
|
+
#
|
|
61
|
+
# @return [Hash] API response containing the eDIS form data
|
|
62
|
+
#
|
|
63
|
+
# @example Authorize sale of 10 shares
|
|
64
|
+
# DhanHQ::Models::Edis.generate_form(
|
|
65
|
+
# isin: "INE155A01022",
|
|
66
|
+
# qty: 10,
|
|
67
|
+
# exchange: "NSE",
|
|
68
|
+
# segment: "E"
|
|
69
|
+
# )
|
|
70
|
+
#
|
|
71
|
+
def generate_form(isin:, qty:, exchange:, segment:, bulk: false)
|
|
72
|
+
resource.form({ isin: isin, qty: qty, exchange: exchange, segment: segment, bulk: bulk })
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
##
|
|
76
|
+
# Generate a bulk eDIS form for multiple securities.
|
|
77
|
+
#
|
|
78
|
+
# @param params [Hash] Bulk form parameters
|
|
79
|
+
# @return [Hash] API response
|
|
80
|
+
#
|
|
81
|
+
def generate_bulk_form(params)
|
|
82
|
+
resource.bulk_form(params)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
##
|
|
86
|
+
# Check EDIS authorization status for a security.
|
|
87
|
+
#
|
|
88
|
+
# @param isin [String] ISIN of the security to check
|
|
89
|
+
#
|
|
90
|
+
# @return [Hash] API response containing authorization status
|
|
91
|
+
#
|
|
92
|
+
# @example Check if EDIS is authorized
|
|
93
|
+
# status = DhanHQ::Models::Edis.inquire(isin: "INE155A01022")
|
|
94
|
+
#
|
|
95
|
+
def inquire(isin:)
|
|
96
|
+
resource.inquire(isin)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
##
|
|
101
|
+
# No validation contract needed — EDIS operations are simple API calls.
|
|
102
|
+
#
|
|
103
|
+
# @return [nil]
|
|
104
|
+
# @api private
|
|
105
|
+
def validation_contract
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -132,6 +132,28 @@ module DhanHQ
|
|
|
132
132
|
def deactivate
|
|
133
133
|
update("DEACTIVATE")
|
|
134
134
|
end
|
|
135
|
+
|
|
136
|
+
##
|
|
137
|
+
# Fetches the current kill switch status for your account.
|
|
138
|
+
#
|
|
139
|
+
# Checks whether the kill switch is currently active or inactive for
|
|
140
|
+
# the current trading day.
|
|
141
|
+
#
|
|
142
|
+
# @return [Hash{Symbol => String}] Response hash containing kill switch status.
|
|
143
|
+
# - **:dhan_client_id** [String] User-specific identification generated by Dhan
|
|
144
|
+
# - **:kill_switch_status** [String] Current status: "ACTIVATE" or "DEACTIVATE"
|
|
145
|
+
#
|
|
146
|
+
# @example Check if kill switch is active
|
|
147
|
+
# response = DhanHQ::Models::KillSwitch.status
|
|
148
|
+
# if response[:kill_switch_status] == "ACTIVATE"
|
|
149
|
+
# puts "Kill switch is active — trading disabled"
|
|
150
|
+
# else
|
|
151
|
+
# puts "Kill switch is inactive — trading enabled"
|
|
152
|
+
# end
|
|
153
|
+
#
|
|
154
|
+
def status
|
|
155
|
+
resource.status
|
|
156
|
+
end
|
|
135
157
|
end
|
|
136
158
|
|
|
137
159
|
##
|
data/lib/DhanHQ/models/margin.rb
CHANGED
|
@@ -139,6 +139,55 @@ module DhanHQ
|
|
|
139
139
|
response = resource.calculate(formatted_params)
|
|
140
140
|
new(response, skip_validation: true)
|
|
141
141
|
end
|
|
142
|
+
|
|
143
|
+
##
|
|
144
|
+
# Calculates margin requirements for multiple scripts in a single request.
|
|
145
|
+
#
|
|
146
|
+
# Provides combined margin calculation including hedge benefit across multiple
|
|
147
|
+
# instruments. Useful for portfolio-level margin analysis.
|
|
148
|
+
#
|
|
149
|
+
# @param params [Hash{Symbol => Object}] Request parameters
|
|
150
|
+
# @option params [Boolean] :include_position Whether to include existing positions
|
|
151
|
+
# @option params [Boolean] :include_order Whether to include existing orders
|
|
152
|
+
# @option params [String] :dhan_client_id User-specific identification
|
|
153
|
+
# @option params [Array<Hash>] :scrip_list Array of instrument margin params, each with:
|
|
154
|
+
# - :exchange_segment [String]
|
|
155
|
+
# - :transaction_type [String]
|
|
156
|
+
# - :quantity [Integer]
|
|
157
|
+
# - :product_type [String]
|
|
158
|
+
# - :security_id [String]
|
|
159
|
+
# - :price [Float]
|
|
160
|
+
# - :trigger_price [Float] (optional)
|
|
161
|
+
#
|
|
162
|
+
# @return [Hash{Symbol => String}] Response hash containing:
|
|
163
|
+
# - **:total_margin** [String] Total margin required
|
|
164
|
+
# - **:span_margin** [String] SPAN margin
|
|
165
|
+
# - **:exposure_margin** [String] Exposure margin
|
|
166
|
+
# - **:equity_margin** [String] Equity margin
|
|
167
|
+
# - **:fo_margin** [String] F&O margin
|
|
168
|
+
# - **:commodity_margin** [String] Commodity margin
|
|
169
|
+
# - **:currency** [String] Currency (e.g., "INR")
|
|
170
|
+
# - **:hedge_benefit** [String] Hedge benefit amount
|
|
171
|
+
#
|
|
172
|
+
# @example Calculate margin for multiple scripts
|
|
173
|
+
# result = DhanHQ::Models::Margin.calculate_multi(
|
|
174
|
+
# include_position: true,
|
|
175
|
+
# include_order: true,
|
|
176
|
+
# dhan_client_id: "1000000132",
|
|
177
|
+
# scrip_list: [
|
|
178
|
+
# { exchange_segment: "NSE_EQ", transaction_type: "BUY",
|
|
179
|
+
# quantity: 100, product_type: "CNC", security_id: "1333", price: 1428.0 },
|
|
180
|
+
# { exchange_segment: "NSE_FNO", transaction_type: "SELL",
|
|
181
|
+
# quantity: 50, product_type: "INTRADAY", security_id: "43492", price: 200.0 }
|
|
182
|
+
# ]
|
|
183
|
+
# )
|
|
184
|
+
# puts "Total margin: #{result[:total_margin]}"
|
|
185
|
+
# puts "Hedge benefit: #{result[:hedge_benefit]}"
|
|
186
|
+
#
|
|
187
|
+
def calculate_multi(params)
|
|
188
|
+
formatted_params = camelize_keys(params)
|
|
189
|
+
resource.calculate_multi(formatted_params)
|
|
190
|
+
end
|
|
142
191
|
end
|
|
143
192
|
|
|
144
193
|
##
|