DhanHQ 2.6.1 → 2.6.3
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/.rubocop.yml +15 -3
- data/ARCHITECTURE.md +113 -0
- data/CHANGELOG.md +55 -0
- data/README.md +2 -0
- data/Rakefile +3 -1
- data/docs/API_VERIFICATION.md +10 -8
- data/docs/ENDPOINTS_AND_SANDBOX.md +115 -0
- data/lib/DhanHQ/auth.rb +2 -2
- data/lib/DhanHQ/client.rb +72 -51
- data/lib/DhanHQ/configuration.rb +45 -11
- data/lib/DhanHQ/constants.rb +68 -4
- data/lib/DhanHQ/contracts/alert_order_contract.rb +23 -16
- data/lib/DhanHQ/contracts/expired_options_data_contract.rb +4 -2
- data/lib/DhanHQ/contracts/forever_order_contract.rb +55 -0
- data/lib/DhanHQ/contracts/historical_data_contract.rb +17 -19
- data/lib/DhanHQ/contracts/intraday_historical_data_contract.rb +12 -0
- data/lib/DhanHQ/contracts/margin_calculator_contract.rb +19 -17
- data/lib/DhanHQ/contracts/market_feed_contract.rb +42 -0
- data/lib/DhanHQ/contracts/multi_scrip_margin_calc_request_contract.rb +8 -5
- data/lib/DhanHQ/contracts/option_chain_contract.rb +17 -19
- data/lib/DhanHQ/contracts/pnl_based_exit_contract.rb +1 -1
- data/lib/DhanHQ/contracts/slice_order_contract.rb +10 -10
- data/lib/DhanHQ/core/auth_api.rb +1 -1
- data/lib/DhanHQ/core/base_api.rb +10 -9
- data/lib/DhanHQ/core/base_model.rb +4 -1
- data/lib/DhanHQ/core/error_handler.rb +2 -2
- data/lib/DhanHQ/errors.rb +14 -2
- data/lib/DhanHQ/helpers/request_helper.rb +27 -5
- data/lib/DhanHQ/helpers/response_helper.rb +48 -19
- data/lib/DhanHQ/helpers/validation_helper.rb +4 -2
- data/lib/DhanHQ/models/alert_order.rb +6 -2
- data/lib/DhanHQ/models/edis.rb +20 -13
- data/lib/DhanHQ/models/expired_options_data.rb +54 -44
- data/lib/DhanHQ/models/forever_order.rb +17 -7
- data/lib/DhanHQ/models/historical_data.rb +40 -6
- data/lib/DhanHQ/models/instrument_helpers.rb +2 -1
- data/lib/DhanHQ/models/margin.rb +62 -82
- data/lib/DhanHQ/models/market_feed.rb +14 -3
- data/lib/DhanHQ/models/option_chain.rb +50 -150
- data/lib/DhanHQ/models/order.rb +19 -4
- data/lib/DhanHQ/models/super_order.rb +2 -2
- data/lib/DhanHQ/resources/alert_orders.rb +1 -1
- data/lib/DhanHQ/resources/edis.rb +4 -3
- data/lib/DhanHQ/resources/instruments.rb +3 -2
- data/lib/DhanHQ/resources/ip_setup.rb +4 -1
- data/lib/DhanHQ/resources/kill_switch.rb +7 -1
- data/lib/DhanHQ/resources/orders.rb +1 -1
- data/lib/DhanHQ/resources/super_orders.rb +8 -2
- data/lib/DhanHQ/resources/trader_control.rb +13 -4
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/base_connection.rb +1 -1
- data/lib/DhanHQ/ws/client.rb +2 -1
- data/lib/DhanHQ/ws/market_depth/client.rb +16 -8
- data/lib/dhan_hq.rb +37 -32
- data/lib/ta/indicators.rb +15 -18
- metadata +7 -9
- data/CODE_REVIEW_ISSUES.md +0 -397
- data/FIXES_APPLIED.md +0 -373
- data/RELEASING.md +0 -60
- data/REVIEW_SUMMARY.md +0 -120
- data/VERSION_UPDATE.md +0 -82
- data/diagram.md +0 -34
- data/docs/ARCHIVE_README.md +0 -784
|
@@ -31,7 +31,7 @@ module DhanHQ
|
|
|
31
31
|
# Parameters and validation rules for the slicing order request.
|
|
32
32
|
#
|
|
33
33
|
# @!attribute [r] correlationId
|
|
34
|
-
# @return [String] Optional. Identifier for tracking, max length
|
|
34
|
+
# @return [String] Optional. Identifier for tracking, max length 30 characters (per orders doc).
|
|
35
35
|
# @!attribute [r] transactionType
|
|
36
36
|
# @return [String] Required. BUY or SELL.
|
|
37
37
|
# @!attribute [r] exchangeSegment
|
|
@@ -39,13 +39,13 @@ module DhanHQ
|
|
|
39
39
|
# Must be one of: NSE_EQ, NSE_FNO, NSE_CURRENCY, BSE_EQ, BSE_FNO, BSE_CURRENCY, MCX_COMM.
|
|
40
40
|
# @!attribute [r] productType
|
|
41
41
|
# @return [String] Required. Product type for the order.
|
|
42
|
-
# Must be one of: CNC, INTRADAY, MARGIN, MTF
|
|
42
|
+
# Must be one of: CNC, INTRADAY, MARGIN, MTF (per orders doc).
|
|
43
43
|
# @!attribute [r] orderType
|
|
44
44
|
# @return [String] Required. Type of order.
|
|
45
45
|
# Must be one of: LIMIT, MARKET, STOP_LOSS, STOP_LOSS_MARKET.
|
|
46
46
|
# @!attribute [r] validity
|
|
47
47
|
# @return [String] Required. Validity of the order.
|
|
48
|
-
# Must be one of: DAY, IOC
|
|
48
|
+
# Must be one of: DAY, IOC (per orders doc).
|
|
49
49
|
# @!attribute [r] securityId
|
|
50
50
|
# @return [String] Required. Security identifier for the order.
|
|
51
51
|
# @!attribute [r] quantity
|
|
@@ -59,11 +59,11 @@ module DhanHQ
|
|
|
59
59
|
# @!attribute [r] afterMarketOrder
|
|
60
60
|
# @return [Boolean] Optional. Indicates if this is an after-market order.
|
|
61
61
|
# @!attribute [r] amoTime
|
|
62
|
-
# @return [String] Optional. Time for after-market orders. Must be one of: OPEN, OPEN_30, OPEN_60.
|
|
62
|
+
# @return [String] Optional. Time for after-market orders. Must be one of: PRE_OPEN, OPEN, OPEN_30, OPEN_60.
|
|
63
63
|
# @!attribute [r] boProfitValue
|
|
64
|
-
# @return [Float] Optional. Profit value for Bracket Orders
|
|
64
|
+
# @return [Float] Optional. Profit value for Bracket Orders (not used when productType is CNC/INTRADAY/MARGIN/MTF).
|
|
65
65
|
# @!attribute [r] boStopLossValue
|
|
66
|
-
# @return [Float] Optional. Stop-loss value for Bracket Orders
|
|
66
|
+
# @return [Float] Optional. Stop-loss value for Bracket Orders (not used when productType is CNC/INTRADAY/MARGIN/MTF).
|
|
67
67
|
# @!attribute [r] drvExpiryDate
|
|
68
68
|
# @return [String] Optional. Expiry date for derivative contracts.
|
|
69
69
|
# @!attribute [r] drvOptionType
|
|
@@ -71,21 +71,21 @@ module DhanHQ
|
|
|
71
71
|
# @!attribute [r] drvStrikePrice
|
|
72
72
|
# @return [Float] Optional. Strike price for options, must be > 0 if provided.
|
|
73
73
|
params do
|
|
74
|
-
optional(:correlationId).maybe(:string, max_size?:
|
|
74
|
+
optional(:correlationId).maybe(:string, max_size?: 30)
|
|
75
75
|
required(:transactionType).filled(:string, included_in?: %w[BUY SELL])
|
|
76
76
|
required(:exchangeSegment).filled(:string,
|
|
77
77
|
included_in?: %w[NSE_EQ NSE_FNO NSE_CURRENCY BSE_EQ BSE_FNO BSE_CURRENCY
|
|
78
78
|
MCX_COMM])
|
|
79
|
-
required(:productType).filled(:string, included_in?: %w[CNC INTRADAY MARGIN MTF
|
|
79
|
+
required(:productType).filled(:string, included_in?: %w[CNC INTRADAY MARGIN MTF])
|
|
80
80
|
required(:orderType).filled(:string, included_in?: %w[LIMIT MARKET STOP_LOSS STOP_LOSS_MARKET])
|
|
81
|
-
required(:validity).filled(:string, included_in?: %w[DAY IOC
|
|
81
|
+
required(:validity).filled(:string, included_in?: %w[DAY IOC])
|
|
82
82
|
required(:securityId).filled(:string)
|
|
83
83
|
required(:quantity).filled(:integer, gt?: 0)
|
|
84
84
|
optional(:disclosedQuantity).maybe(:integer, gteq?: 0)
|
|
85
85
|
optional(:price).maybe(:float, gt?: 0)
|
|
86
86
|
optional(:triggerPrice).maybe(:float, gt?: 0)
|
|
87
87
|
optional(:afterMarketOrder).maybe(:bool)
|
|
88
|
-
optional(:amoTime).maybe(:string, included_in?: %w[OPEN OPEN_30 OPEN_60])
|
|
88
|
+
optional(:amoTime).maybe(:string, included_in?: %w[PRE_OPEN OPEN OPEN_30 OPEN_60])
|
|
89
89
|
optional(:boProfitValue).maybe(:float, gt?: 0)
|
|
90
90
|
optional(:boStopLossValue).maybe(:float, gt?: 0)
|
|
91
91
|
optional(:drvExpiryDate).maybe(:string)
|
data/lib/DhanHQ/core/auth_api.rb
CHANGED
|
@@ -8,7 +8,7 @@ module DhanHQ
|
|
|
8
8
|
# This class intentionally lives at the top-level namespace so it autoloads
|
|
9
9
|
# cleanly from `lib/DhanHQ/core/auth_api.rb` with Zeitwerk `collapse`.
|
|
10
10
|
class AuthAPI
|
|
11
|
-
BASE_URL =
|
|
11
|
+
BASE_URL = Constants::Urls::AUTH_BASE
|
|
12
12
|
|
|
13
13
|
def connection
|
|
14
14
|
@connection ||= Faraday.new(url: BASE_URL) do |faraday|
|
data/lib/DhanHQ/core/base_api.rb
CHANGED
|
@@ -86,20 +86,21 @@ module DhanHQ
|
|
|
86
86
|
"#{self.class::HTTP_PATH}#{endpoint}"
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
-
# Format parameters based on API endpoint
|
|
89
|
+
# Format parameters based on API endpoint. Uses path-based strategy (marketfeed: pass-through,
|
|
90
|
+
# optionchain: titleize, default: camelize).
|
|
90
91
|
def format_params(endpoint, params)
|
|
91
|
-
|
|
92
|
+
full_path = build_path(endpoint)
|
|
93
|
+
return params if params.empty?
|
|
92
94
|
|
|
93
|
-
|
|
95
|
+
param_formatter_for(full_path).call(params)
|
|
94
96
|
end
|
|
95
97
|
|
|
96
|
-
#
|
|
97
|
-
def
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
# Returns a callable that formats params for the given path (Strategy).
|
|
99
|
+
def param_formatter_for(full_path)
|
|
100
|
+
return ->(p) { p } if full_path.include?("/marketfeed")
|
|
101
|
+
return ->(p) { titleize_keys(p) } if full_path.include?("/optionchain")
|
|
100
102
|
|
|
101
|
-
|
|
102
|
-
endpoint.include?("/marketfeed")
|
|
103
|
+
->(p) { camelize_keys(p) }
|
|
103
104
|
end
|
|
104
105
|
end
|
|
105
106
|
end
|
|
@@ -145,7 +145,10 @@ module DhanHQ
|
|
|
145
145
|
def parse_collection_response(response)
|
|
146
146
|
# Some endpoints return arrays, others might return a `[:data]` structure
|
|
147
147
|
unless response.is_a?(Array) || (response.is_a?(Hash) && response[:data].is_a?(Array))
|
|
148
|
-
DhanHQ.logger&.warn(
|
|
148
|
+
DhanHQ.logger&.warn(
|
|
149
|
+
"[DhanHQ::BaseModel] Unexpected response format for collection: #{response.class}. " \
|
|
150
|
+
"Expected Array or Hash with :data key."
|
|
151
|
+
)
|
|
149
152
|
return []
|
|
150
153
|
end
|
|
151
154
|
|
|
@@ -10,9 +10,9 @@ module DhanHQ
|
|
|
10
10
|
def self.handle(error)
|
|
11
11
|
case error
|
|
12
12
|
when Dry::Validation::Result
|
|
13
|
-
raise "
|
|
13
|
+
raise DhanHQ::ValidationError, "Invalid parameters: #{error.errors.to_h}"
|
|
14
14
|
else
|
|
15
|
-
raise
|
|
15
|
+
raise DhanHQ::Error, error.message
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
end
|
data/lib/DhanHQ/errors.rb
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module DhanHQ
|
|
4
|
-
# Base error class for all DhanHQ API errors
|
|
5
|
-
|
|
4
|
+
# Base error class for all DhanHQ API errors.
|
|
5
|
+
# When raised from API response handling, {#response_body} holds the parsed error payload.
|
|
6
|
+
class Error < StandardError
|
|
7
|
+
attr_reader :response_body
|
|
8
|
+
|
|
9
|
+
def initialize(message = nil, response_body: nil)
|
|
10
|
+
super(message)
|
|
11
|
+
@response_body = response_body
|
|
12
|
+
end
|
|
13
|
+
end
|
|
6
14
|
|
|
7
15
|
# Authentication and access errors
|
|
8
16
|
# Raised when access token cannot be resolved (missing config or provider returned nil).
|
|
@@ -36,6 +44,10 @@ module DhanHQ
|
|
|
36
44
|
|
|
37
45
|
# Order and market data errors
|
|
38
46
|
class OrderError < Error; end
|
|
47
|
+
|
|
48
|
+
# Raised when the 25-modifications-per-order API cap would be exceeded.
|
|
49
|
+
# Count is tracked per Order instance in this process only (see Order#modify).
|
|
50
|
+
class ModificationLimitError < Error; end
|
|
39
51
|
# Raised when the API signals an issue with the requested data payload.
|
|
40
52
|
class DataError < Error; end
|
|
41
53
|
|
|
@@ -39,7 +39,7 @@ module DhanHQ
|
|
|
39
39
|
"access-token" => token
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
# Add client-id for DATA APIs
|
|
42
|
+
# Add client-id for DATA APIs (now including sandbox profile/funds)
|
|
43
43
|
if data_api?(path)
|
|
44
44
|
client_id = DhanHQ.configuration&.client_id
|
|
45
45
|
unless client_id
|
|
@@ -70,19 +70,41 @@ module DhanHQ
|
|
|
70
70
|
# @param req [Faraday::Request] The request object.
|
|
71
71
|
# @param payload [Hash] The request payload.
|
|
72
72
|
# @param method [Symbol] The HTTP method.
|
|
73
|
-
def prepare_payload(req, payload, method)
|
|
74
|
-
return if payload.nil? || payload.empty?
|
|
73
|
+
def prepare_payload(req, payload, method, path = nil)
|
|
74
|
+
return if payload.nil? || (payload.empty? && (path.nil? || !data_api?(path)))
|
|
75
75
|
|
|
76
76
|
unless payload.is_a?(Hash)
|
|
77
77
|
raise DhanHQ::InputExceptionError,
|
|
78
78
|
"Invalid payload: Expected a Hash, got #{payload.class}"
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
+
out = payload
|
|
82
|
+
if path && %i[post put patch].include?(method)
|
|
83
|
+
client_id = DhanHQ.configuration&.client_id
|
|
84
|
+
needs_client_id = data_api?(path) || payload_requires_dhan_client_id?(path)
|
|
85
|
+
if client_id && needs_client_id && !payload.key?(:dhanClientId) && !payload.key?("dhanClientId")
|
|
86
|
+
out = payload.dup
|
|
87
|
+
if out.keys.any?(String)
|
|
88
|
+
out["dhanClientId"] = client_id
|
|
89
|
+
else
|
|
90
|
+
out[:dhanClientId] = client_id
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
81
95
|
case method
|
|
82
96
|
when :delete then req.params = {}
|
|
83
|
-
when :get then req.params =
|
|
84
|
-
else req.body =
|
|
97
|
+
when :get then req.params = out
|
|
98
|
+
else req.body = out.to_json
|
|
85
99
|
end
|
|
86
100
|
end
|
|
101
|
+
|
|
102
|
+
# True when the path is one where the request body must include dhanClientId (order-api style).
|
|
103
|
+
def payload_requires_dhan_client_id?(path)
|
|
104
|
+
return false if path.nil? || path.empty?
|
|
105
|
+
|
|
106
|
+
prefixes = DhanHQ::Constants::PAYLOAD_REQUIRES_DHAN_CLIENT_ID_PREFIXES
|
|
107
|
+
prefixes.any? { |p| path.start_with?(p) }
|
|
108
|
+
end
|
|
87
109
|
end
|
|
88
110
|
end
|
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
module DhanHQ
|
|
4
4
|
# Helper mixin for normalising API responses and raising mapped errors.
|
|
5
5
|
module ResponseHelper
|
|
6
|
+
STATUS_ERROR_FALLBACK = {
|
|
7
|
+
400 => DhanHQ::InputExceptionError,
|
|
8
|
+
401 => DhanHQ::InvalidAuthenticationError,
|
|
9
|
+
403 => DhanHQ::InvalidAccessError,
|
|
10
|
+
404 => DhanHQ::NotFoundError,
|
|
11
|
+
429 => DhanHQ::RateLimitError
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
6
14
|
private
|
|
7
15
|
|
|
8
16
|
# Determines if the API response indicates success.
|
|
@@ -53,31 +61,52 @@ module DhanHQ
|
|
|
53
61
|
end
|
|
54
62
|
|
|
55
63
|
error_class = DhanHQ::Constants::DHAN_ERROR_MAPPING[error_code]
|
|
56
|
-
|
|
57
64
|
unless error_class
|
|
58
|
-
# Log unmapped error codes for investigation
|
|
59
65
|
DhanHQ.logger&.warn("[DhanHQ] Unmapped error code: #{error_code} (status: #{response.status})")
|
|
66
|
+
error_class = status_fallback_error_class(response.status)
|
|
67
|
+
end
|
|
60
68
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
message = build_error_text(error_code, error_message, body)
|
|
70
|
+
raise error_class.new(message, response_body: body)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def status_fallback_error_class(status)
|
|
74
|
+
STATUS_ERROR_FALLBACK[status] ||
|
|
75
|
+
(status.between?(500, 599) ? DhanHQ::InternalServerError : DhanHQ::OtherError)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def build_error_text(error_code, error_message, body = {})
|
|
79
|
+
text = if error_code == DhanHQ::Constants::TradingErrorCode::NO_HOLDINGS
|
|
80
|
+
"#{error_message} (error code: #{error_code})"
|
|
81
|
+
else
|
|
82
|
+
"#{error_code}: #{error_message}"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
extra = extra_error_detail(body)
|
|
86
|
+
text += " | #{extra}" if extra
|
|
87
|
+
|
|
88
|
+
if error_code == DhanHQ::Constants::TradingErrorCode::INPUT_EXCEPTION
|
|
89
|
+
text += " (API does not return which field failed; check required params and value types for this endpoint.)"
|
|
71
90
|
end
|
|
72
91
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"#{error_message} (error code: #{error_code})"
|
|
76
|
-
else
|
|
77
|
-
"#{error_code}: #{error_message}"
|
|
78
|
-
end
|
|
92
|
+
text
|
|
93
|
+
end
|
|
79
94
|
|
|
80
|
-
|
|
95
|
+
# Returns any additional error detail from the response body (errors array, details, etc.).
|
|
96
|
+
def extra_error_detail(body)
|
|
97
|
+
return nil unless body.is_a?(Hash)
|
|
98
|
+
|
|
99
|
+
parts = []
|
|
100
|
+
if body[:errors].is_a?(Array) && body[:errors].any?
|
|
101
|
+
parts << body[:errors].join("; ")
|
|
102
|
+
end
|
|
103
|
+
if body[:details].is_a?(String) && body[:details].to_s.strip != ""
|
|
104
|
+
parts << body[:details].to_s
|
|
105
|
+
end
|
|
106
|
+
if body[:validationErrors].is_a?(Array) && body[:validationErrors].any?
|
|
107
|
+
parts << body[:validationErrors].map { |e| e.is_a?(Hash) ? e[:message] || e[:field] : e }.join("; ")
|
|
108
|
+
end
|
|
109
|
+
parts.empty? ? nil : parts.join(" ")
|
|
81
110
|
end
|
|
82
111
|
|
|
83
112
|
# Parses JSON response safely. Converts response body to a hash or array with indifferent access.
|
|
@@ -11,7 +11,9 @@ module DhanHQ
|
|
|
11
11
|
contract = contract_class.new
|
|
12
12
|
result = contract.call(params)
|
|
13
13
|
|
|
14
|
-
raise DhanHQ::
|
|
14
|
+
raise DhanHQ::ValidationError, "Invalid parameters: #{result.errors.to_h}" unless result.success?
|
|
15
|
+
|
|
16
|
+
result.to_h
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
# Validate instance attributes using the defined validation contract
|
|
@@ -23,7 +25,7 @@ module DhanHQ
|
|
|
23
25
|
|
|
24
26
|
result = contract.call(@attributes)
|
|
25
27
|
@errors = result.errors.to_h unless result.success?
|
|
26
|
-
raise DhanHQ::
|
|
28
|
+
raise DhanHQ::ValidationError, "Invalid parameters: #{@errors}" unless valid?
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
# Checks if the current instance is valid
|
|
@@ -8,7 +8,7 @@ module DhanHQ
|
|
|
8
8
|
class AlertOrder < BaseModel
|
|
9
9
|
include Concerns::ApiResponseHandler
|
|
10
10
|
|
|
11
|
-
HTTP_PATH = "/alerts/orders"
|
|
11
|
+
HTTP_PATH = "/v2/alerts/orders"
|
|
12
12
|
|
|
13
13
|
attributes :alert_id, :exchange_segment, :security_id, :condition,
|
|
14
14
|
:trigger_price, :order_type, :transaction_type, :quantity,
|
|
@@ -37,6 +37,8 @@ module DhanHQ
|
|
|
37
37
|
return nil unless response.is_a?(Hash) || (response.is_a?(Array) && response.any?)
|
|
38
38
|
|
|
39
39
|
payload = response.is_a?(Array) ? response.first : response
|
|
40
|
+
return nil if payload.is_a?(Hash) && payload.empty?
|
|
41
|
+
|
|
40
42
|
new(payload, skip_validation: true)
|
|
41
43
|
end
|
|
42
44
|
|
|
@@ -65,7 +67,9 @@ module DhanHQ
|
|
|
65
67
|
#
|
|
66
68
|
def modify(alert_id, params)
|
|
67
69
|
normalized = snake_case(params)
|
|
68
|
-
|
|
70
|
+
validate_params!(normalized, DhanHQ::Contracts::AlertOrderContract)
|
|
71
|
+
payload = normalized.merge(alert_id: alert_id)
|
|
72
|
+
response = resource.update(alert_id, camelize_keys(payload))
|
|
69
73
|
return nil unless success_response?(response)
|
|
70
74
|
|
|
71
75
|
find(alert_id)
|
data/lib/DhanHQ/models/edis.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../contracts/edis_contract"
|
|
4
|
+
|
|
3
5
|
module DhanHQ
|
|
4
6
|
module Models
|
|
5
7
|
##
|
|
@@ -16,15 +18,15 @@ module DhanHQ
|
|
|
16
18
|
# isin: "INE155A01022",
|
|
17
19
|
# qty: 10,
|
|
18
20
|
# exchange: "NSE",
|
|
19
|
-
# segment: "
|
|
21
|
+
# segment: "EQ",
|
|
20
22
|
# bulk: false
|
|
21
23
|
# )
|
|
22
24
|
#
|
|
23
|
-
# @example Check EDIS status for a security
|
|
25
|
+
# @example Check EDIS status for a security (or "ALL" for all holdings)
|
|
24
26
|
# status = DhanHQ::Models::Edis.inquire(isin: "INE155A01022")
|
|
25
27
|
#
|
|
26
28
|
class Edis < BaseModel
|
|
27
|
-
HTTP_PATH = "/edis"
|
|
29
|
+
HTTP_PATH = "/v2/edis"
|
|
28
30
|
|
|
29
31
|
class << self
|
|
30
32
|
##
|
|
@@ -52,24 +54,26 @@ module DhanHQ
|
|
|
52
54
|
##
|
|
53
55
|
# Generate an eDIS form for authorizing sale of holdings.
|
|
54
56
|
#
|
|
55
|
-
# @param isin [String] ISIN of the security (e.g
|
|
57
|
+
# @param isin [String] ISIN of the security (e.g. "INE733E01010")
|
|
56
58
|
# @param qty [Integer] Quantity to authorize for sale
|
|
57
|
-
# @param exchange [String] Exchange
|
|
58
|
-
# @param segment [String] Segment
|
|
59
|
-
# @param bulk [Boolean]
|
|
59
|
+
# @param exchange [String] Exchange: "NSE" or "BSE"
|
|
60
|
+
# @param segment [String] Segment: "EQ", "COMM", or "FNO"
|
|
61
|
+
# @param bulk [Boolean] If true, mark eDIS for all stocks in portfolio (default: false)
|
|
60
62
|
#
|
|
61
|
-
# @return [Hash] API response
|
|
63
|
+
# @return [Hash] API response with dhanClientId and edisFormHtml (escaped HTML to render)
|
|
62
64
|
#
|
|
63
65
|
# @example Authorize sale of 10 shares
|
|
64
66
|
# DhanHQ::Models::Edis.generate_form(
|
|
65
67
|
# isin: "INE155A01022",
|
|
66
68
|
# qty: 10,
|
|
67
69
|
# exchange: "NSE",
|
|
68
|
-
# segment: "
|
|
70
|
+
# segment: "EQ"
|
|
69
71
|
# )
|
|
70
72
|
#
|
|
71
73
|
def generate_form(isin:, qty:, exchange:, segment:, bulk: false)
|
|
72
|
-
|
|
74
|
+
params = { isin: isin, qty: qty, exchange: exchange, segment: segment, bulk: bulk }
|
|
75
|
+
validate_params!(params, DhanHQ::Contracts::EdisFormContract)
|
|
76
|
+
resource.form(params)
|
|
73
77
|
end
|
|
74
78
|
|
|
75
79
|
##
|
|
@@ -85,13 +89,16 @@ module DhanHQ
|
|
|
85
89
|
##
|
|
86
90
|
# Check EDIS authorization status for a security.
|
|
87
91
|
#
|
|
88
|
-
# @param isin [String] ISIN of the security
|
|
92
|
+
# @param isin [String] ISIN of the security, or "ALL" for all holdings
|
|
89
93
|
#
|
|
90
|
-
# @return [Hash] API response
|
|
94
|
+
# @return [Hash] API response with clientId, isin, totalQty, aprvdQty, status, remarks
|
|
91
95
|
#
|
|
92
|
-
# @example Check if EDIS is authorized
|
|
96
|
+
# @example Check if EDIS is authorized for one security
|
|
93
97
|
# status = DhanHQ::Models::Edis.inquire(isin: "INE155A01022")
|
|
94
98
|
#
|
|
99
|
+
# @example Check EDIS status for all holdings
|
|
100
|
+
# status = DhanHQ::Models::Edis.inquire(isin: "ALL")
|
|
101
|
+
#
|
|
95
102
|
def inquire(isin:)
|
|
96
103
|
resource.inquire(isin)
|
|
97
104
|
end
|
|
@@ -42,7 +42,12 @@ module DhanHQ
|
|
|
42
42
|
# call_data = data.call_data
|
|
43
43
|
# put_data = data.put_data
|
|
44
44
|
#
|
|
45
|
+
# @example Normalize to candles
|
|
46
|
+
# candles = data.to_candles
|
|
47
|
+
#
|
|
45
48
|
class ExpiredOptionsData < BaseModel
|
|
49
|
+
OHLC_FIELDS = %i[open high low close iv volume strike spot oi open_interest].freeze
|
|
50
|
+
|
|
46
51
|
# All expired options data attributes
|
|
47
52
|
attributes :exchange_segment, :interval, :security_id, :instrument,
|
|
48
53
|
:expiry_flag, :expiry_code, :strike, :drv_option_type,
|
|
@@ -56,63 +61,29 @@ module DhanHQ
|
|
|
56
61
|
# 31 days in a single request. Historical data is available for up to the last 5 years.
|
|
57
62
|
#
|
|
58
63
|
# @param params [Hash{Symbol => String, Integer, Array<String>}] Request parameters
|
|
64
|
+
# @option params [String, Integer] :security_id (required) Underlying exchange standard ID for each scrip
|
|
59
65
|
# @option params [String] :exchange_segment (required) Exchange and segment identifier.
|
|
60
|
-
# Valid values: "NSE_FNO", "
|
|
61
|
-
# @option params [String] :interval (required) Minute intervals for the timeframe.
|
|
62
|
-
# Valid values: "1", "5", "15", "25", "60"
|
|
63
|
-
# @option params [Integer] :security_id (required) Underlying exchange standard ID for each scrip
|
|
66
|
+
# Valid values: "NSE_FNO", "IDX_I", "NSE_EQ", "BSE_EQ"
|
|
64
67
|
# @option params [String] :instrument (required) Instrument type of the scrip.
|
|
65
68
|
# Valid values: "OPTIDX" (Index Options), "OPTSTK" (Stock Options)
|
|
69
|
+
# @option params [String, Integer] :interval (required) Minute intervals for the timeframe.
|
|
70
|
+
# Valid values: "1", "5", "15", "25", "60"
|
|
66
71
|
# @option params [String] :expiry_flag (required) Expiry interval of the instrument.
|
|
67
72
|
# Valid values: "WEEK", "MONTH"
|
|
68
73
|
# @option params [Integer] :expiry_code (required) Expiry code for the instrument
|
|
69
74
|
# @option params [String] :strike (required) Strike price specification.
|
|
70
75
|
# Format: "ATM" for At The Money, "ATM+X" or "ATM-X" for offset strikes.
|
|
71
|
-
#
|
|
72
|
-
# For all other contracts: Up to ATM+3 / ATM-3
|
|
73
|
-
# @option params [String] :drv_option_type (required) Option type.
|
|
74
|
-
# Valid values: "CALL", "PUT"
|
|
76
|
+
# @option params [String] :option_type (required) Option type ("CALL" or "PUT").
|
|
75
77
|
# @option params [Array<String>] :required_data (required) Array of required data fields.
|
|
76
|
-
#
|
|
77
|
-
# @option params [String] :
|
|
78
|
-
# Cannot be more than 5 years ago. Same-day ranges are allowed.
|
|
79
|
-
# @option params [String] :to_date (required) End date of the desired range (non-inclusive) in YYYY-MM-DD format.
|
|
80
|
-
# Date range cannot exceed 31 days from from_date (to_date is non-inclusive). Same-day `from_date`/`to_date` is valid.
|
|
78
|
+
# @option params [String] :from_date (required) Start date in YYYY-MM-DD format.
|
|
79
|
+
# @option params [String] :to_date (required) End date in YYYY-MM-DD format.
|
|
81
80
|
#
|
|
82
81
|
# @return [ExpiredOptionsData] Expired options data object with fetched data
|
|
83
|
-
#
|
|
84
|
-
# @example Fetch NIFTY index options data
|
|
85
|
-
# data = DhanHQ::Models::ExpiredOptionsData.fetch(
|
|
86
|
-
# exchange_segment: "NSE_FNO",
|
|
87
|
-
# interval: "1",
|
|
88
|
-
# security_id: 13,
|
|
89
|
-
# instrument: "OPTIDX",
|
|
90
|
-
# expiry_flag: "MONTH",
|
|
91
|
-
# expiry_code: 1,
|
|
92
|
-
# strike: "ATM",
|
|
93
|
-
# drv_option_type: "CALL",
|
|
94
|
-
# required_data: ["open", "high", "low", "close", "volume", "iv", "oi", "spot"],
|
|
95
|
-
# from_date: "2021-08-01",
|
|
96
|
-
# to_date: "2021-09-01"
|
|
97
|
-
# )
|
|
98
|
-
#
|
|
99
|
-
# @example Fetch stock options data for ATM+2 strike
|
|
100
|
-
# data = DhanHQ::Models::ExpiredOptionsData.fetch(
|
|
101
|
-
# exchange_segment: "NSE_FNO",
|
|
102
|
-
# interval: "15",
|
|
103
|
-
# security_id: 11536,
|
|
104
|
-
# instrument: "OPTSTK",
|
|
105
|
-
# expiry_flag: "WEEK",
|
|
106
|
-
# expiry_code: 0,
|
|
107
|
-
# strike: "ATM+2",
|
|
108
|
-
# drv_option_type: "PUT",
|
|
109
|
-
# required_data: ["open", "high", "low", "close", "volume"],
|
|
110
|
-
# from_date: "2024-01-01",
|
|
111
|
-
# to_date: "2024-01-31"
|
|
112
|
-
# )
|
|
113
|
-
#
|
|
114
82
|
# @raise [DhanHQ::ValidationError] If validation fails for any parameter
|
|
115
83
|
def fetch(params)
|
|
84
|
+
# Map option_type to drv_option_type if provided
|
|
85
|
+
params[:drv_option_type] ||= params[:option_type] if params.key?(:option_type)
|
|
86
|
+
|
|
116
87
|
normalized = normalize_params(params)
|
|
117
88
|
validate_params(normalized)
|
|
118
89
|
|
|
@@ -120,6 +91,8 @@ module DhanHQ
|
|
|
120
91
|
new(response.merge(normalized), skip_validation: true)
|
|
121
92
|
end
|
|
122
93
|
|
|
94
|
+
alias rolling fetch
|
|
95
|
+
|
|
123
96
|
private
|
|
124
97
|
|
|
125
98
|
def expired_options_resource
|
|
@@ -180,6 +153,43 @@ module DhanHQ
|
|
|
180
153
|
end
|
|
181
154
|
end
|
|
182
155
|
|
|
156
|
+
##
|
|
157
|
+
# Normalizes the columnar response into an array of candle hashes.
|
|
158
|
+
#
|
|
159
|
+
# @param option_type [String, nil] Option type to retrieve ("CALL" or "PUT").
|
|
160
|
+
# If nil, uses the {#drv_option_type} from the request.
|
|
161
|
+
# @return [Array<Hash>] Normalized array of candles.
|
|
162
|
+
def to_candles(option_type = nil)
|
|
163
|
+
option_type ||= drv_option_type
|
|
164
|
+
opt_data = data_for_type(option_type)
|
|
165
|
+
return [] unless opt_data.is_a?(Hash)
|
|
166
|
+
|
|
167
|
+
# Standardize keys to symbols
|
|
168
|
+
opt_data = opt_data.transform_keys(&:to_sym)
|
|
169
|
+
ts_arr = opt_data[:timestamp]
|
|
170
|
+
return [] unless ts_arr.is_a?(Array)
|
|
171
|
+
|
|
172
|
+
type_sym = option_type.to_s.downcase.to_sym
|
|
173
|
+
|
|
174
|
+
ts_arr.each_with_index.map do |ts, i|
|
|
175
|
+
candle = {
|
|
176
|
+
option_type: type_sym,
|
|
177
|
+
timestamp: ts.is_a?(Numeric) ? Time.at(ts) : ts
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# Map requested fields
|
|
181
|
+
OHLC_FIELDS.each do |field|
|
|
182
|
+
val_arr = opt_data[field]
|
|
183
|
+
next unless val_arr.is_a?(Array)
|
|
184
|
+
|
|
185
|
+
# Map 'oi' to 'open_interest' if requested
|
|
186
|
+
target_field = field == :oi ? :open_interest : field
|
|
187
|
+
candle[target_field] = val_arr[i]
|
|
188
|
+
end
|
|
189
|
+
candle
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
183
193
|
##
|
|
184
194
|
# Gets call option data from the response.
|
|
185
195
|
#
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../contracts/forever_order_contract"
|
|
4
|
+
|
|
3
5
|
module DhanHQ
|
|
4
6
|
module Models
|
|
5
7
|
##
|
|
@@ -154,9 +156,9 @@ module DhanHQ
|
|
|
154
156
|
# @option params [String] :transaction_type (required) The trading side of transaction.
|
|
155
157
|
# Valid values: "BUY", "SELL"
|
|
156
158
|
# @option params [String] :exchange_segment (required) Exchange and segment identifier.
|
|
157
|
-
# Valid values:
|
|
159
|
+
# Valid values: See {DhanHQ::Constants::FOREVER_ORDER_SEGMENTS} (NSE_EQ, NSE_FNO, BSE_EQ, BSE_FNO, MCX_COMM)
|
|
158
160
|
# @option params [String] :product_type (required) Product type.
|
|
159
|
-
# Valid values:
|
|
161
|
+
# Valid values: See {DhanHQ::Constants::FOREVER_ORDER_PRODUCT_TYPES} (CNC, MTF)
|
|
160
162
|
# @option params [String] :order_type (required) Order type.
|
|
161
163
|
# Valid values: "LIMIT", "MARKET"
|
|
162
164
|
# @option params [String] :validity (required) Validity of order for execution.
|
|
@@ -216,10 +218,12 @@ module DhanHQ
|
|
|
216
218
|
#
|
|
217
219
|
# @note Order placement APIs require Static IP whitelisting
|
|
218
220
|
def create(params)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
normalized = snake_case(params)
|
|
222
|
+
config = DhanHQ.configuration
|
|
223
|
+
normalized[:dhan_client_id] ||= config.client_id if config&.client_id
|
|
224
|
+
validate_params!(normalized, DhanHQ::Contracts::ForeverOrderCreateContract)
|
|
225
|
+
formatted = camelize_keys(normalized)
|
|
226
|
+
response = resource.create(formatted)
|
|
223
227
|
return nil unless response.is_a?(Hash) && response["orderId"]
|
|
224
228
|
|
|
225
229
|
find(response["orderId"])
|
|
@@ -291,7 +295,13 @@ module DhanHQ
|
|
|
291
295
|
raise "Order ID is required to modify a forever order" unless order_id
|
|
292
296
|
|
|
293
297
|
DhanHQ.logger&.info("[DhanHQ::Models::ForeverOrder] Modifying order #{order_id}")
|
|
294
|
-
|
|
298
|
+
full_params = snake_case(new_params)
|
|
299
|
+
config = DhanHQ.configuration
|
|
300
|
+
full_params[:dhan_client_id] ||= config.client_id if config&.client_id
|
|
301
|
+
full_params[:order_id] = order_id
|
|
302
|
+
validate_params!(full_params, DhanHQ::Contracts::ForeverOrderModifyContract)
|
|
303
|
+
formatted = camelize_keys(full_params)
|
|
304
|
+
response = self.class.resource.update(order_id, formatted)
|
|
295
305
|
ctx = "[DhanHQ::Models::ForeverOrder] Modification"
|
|
296
306
|
success = handle_api_response(response, success_key: "orderId", context: ctx)
|
|
297
307
|
return self.class.find(order_id) if success
|