DhanHQ 2.6.2 → 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 +31 -0
- data/README.md +2 -0
- data/docs/API_VERIFICATION.md +10 -8
- data/docs/ENDPOINTS_AND_SANDBOX.md +12 -0
- data/lib/DhanHQ/auth.rb +2 -2
- data/lib/DhanHQ/client.rb +42 -34
- data/lib/DhanHQ/configuration.rb +5 -6
- data/lib/DhanHQ/constants.rb +67 -7
- 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 +9 -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 +11 -2
- 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 +16 -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/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/market_depth/client.rb +11 -4
- data/lib/dhan_hq.rb +17 -20
- data/lib/ta/indicators.rb +15 -18
- metadata +6 -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
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Contracts
|
|
5
|
+
# Validates request for POST /v2/forever/orders (create Forever / GTT order).
|
|
6
|
+
# orderFlag: SINGLE | OCO. productType: CNC | MTF. For OCO, price1, triggerPrice1, quantity1 required.
|
|
7
|
+
class ForeverOrderCreateContract < BaseContract
|
|
8
|
+
params do
|
|
9
|
+
required(:dhan_client_id).filled(:string)
|
|
10
|
+
required(:order_flag).filled(:string, included_in?: OrderFlag::ALL)
|
|
11
|
+
required(:transaction_type).filled(:string, included_in?: TRANSACTION_TYPES)
|
|
12
|
+
required(:exchange_segment).filled(:string, included_in?: FOREVER_ORDER_SEGMENTS)
|
|
13
|
+
required(:product_type).filled(:string, included_in?: FOREVER_ORDER_PRODUCT_TYPES)
|
|
14
|
+
required(:order_type).filled(:string, included_in?: %w[LIMIT MARKET])
|
|
15
|
+
required(:validity).filled(:string, included_in?: VALIDITY_TYPES)
|
|
16
|
+
required(:security_id).filled(:string)
|
|
17
|
+
required(:quantity).filled(:integer, gt?: 0)
|
|
18
|
+
required(:price).filled(:float, gt?: 0)
|
|
19
|
+
required(:trigger_price).filled(:float)
|
|
20
|
+
optional(:correlation_id).maybe(:string, max_size?: 30, format?: /\A[a-zA-Z0-9 _-]*\z/)
|
|
21
|
+
optional(:disclosed_quantity).maybe(:integer, gteq?: 0)
|
|
22
|
+
optional(:price1).maybe(:float, gt?: 0)
|
|
23
|
+
optional(:trigger_price1).maybe(:float)
|
|
24
|
+
optional(:quantity1).maybe(:integer, gt?: 0)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
rule(:order_flag) do
|
|
28
|
+
next unless value == DhanHQ::Constants::OrderFlag::OCO
|
|
29
|
+
|
|
30
|
+
missing = []
|
|
31
|
+
missing << "price1" if values[:price1].nil? || values[:price1].to_f <= 0
|
|
32
|
+
missing << "trigger_price1" if values[:trigger_price1].nil?
|
|
33
|
+
missing << "quantity1" if values[:quantity1].nil? || values[:quantity1].to_i < 1
|
|
34
|
+
key.failure("required for OCO: #{missing.join(", ")}") if missing.any?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Validates request for PUT /v2/forever/orders/{order-id} (modify Forever order).
|
|
39
|
+
# orderType: LIMIT | MARKET | STOP_LOSS | STOP_LOSS_MARKET. legName: TARGET_LEG | STOP_LOSS_LEG.
|
|
40
|
+
class ForeverOrderModifyContract < BaseContract
|
|
41
|
+
params do
|
|
42
|
+
required(:dhan_client_id).filled(:string)
|
|
43
|
+
required(:order_id).filled(:string)
|
|
44
|
+
required(:order_flag).filled(:string, included_in?: OrderFlag::ALL)
|
|
45
|
+
required(:order_type).filled(:string, included_in?: ORDER_TYPES)
|
|
46
|
+
required(:leg_name).filled(:string, included_in?: %w[TARGET_LEG STOP_LOSS_LEG])
|
|
47
|
+
required(:quantity).filled(:integer, gt?: 0)
|
|
48
|
+
required(:price).filled(:float, gt?: 0)
|
|
49
|
+
required(:trigger_price).filled(:float)
|
|
50
|
+
required(:validity).filled(:string, included_in?: VALIDITY_TYPES)
|
|
51
|
+
optional(:disclosed_quantity).maybe(:integer, gteq?: 0)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -4,40 +4,38 @@ require "date"
|
|
|
4
4
|
|
|
5
5
|
module DhanHQ
|
|
6
6
|
module Contracts
|
|
7
|
-
# Validates payloads for
|
|
7
|
+
# Validates payloads for POST /v2/charts/historical (daily OHLC). No interval.
|
|
8
8
|
class HistoricalDataContract < BaseContract
|
|
9
9
|
params do
|
|
10
|
-
# Common required fields
|
|
11
10
|
required(:security_id).filled(:string)
|
|
12
|
-
required(:exchange_segment).filled(:string, included_in?:
|
|
11
|
+
required(:exchange_segment).filled(:string, included_in?: CHART_EXCHANGE_SEGMENTS)
|
|
13
12
|
required(:instrument).filled(:string, included_in?: INSTRUMENTS)
|
|
13
|
+
required(:from_date).filled(:string, format?: /\A\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?\z/)
|
|
14
|
+
required(:to_date).filled(:string, format?: /\A\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?\z/)
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# Optional fields
|
|
20
|
-
optional(:expiry_code).maybe(:integer, included_in?: [0, 1, 2])
|
|
21
|
-
|
|
22
|
-
# For intraday, the user can supply an "interval"
|
|
23
|
-
# (valid: 1, 5, 15, 25, 60)
|
|
24
|
-
optional(:interval).maybe(:string, included_in?: %w[1 5 15 25 60])
|
|
16
|
+
optional(:expiry_code).maybe(:integer, included_in?: ExpiryCode::ALL)
|
|
17
|
+
optional(:interval).maybe(:string, included_in?: CHART_INTERVALS)
|
|
18
|
+
optional(:oi).maybe(:bool)
|
|
25
19
|
end
|
|
26
20
|
|
|
27
21
|
rule(:from_date) do
|
|
28
|
-
next unless value.is_a?(String) && value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
22
|
+
next unless value.is_a?(String) && value.match?(/\A\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?\z/)
|
|
29
23
|
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
# Only validate weekend for pure date strings (YYYY-MM-DD)
|
|
25
|
+
if value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
26
|
+
d = Date.parse(value)
|
|
27
|
+
key.failure("must be a valid trading date (no weekend dates)") unless trading_day?(d)
|
|
28
|
+
end
|
|
32
29
|
rescue Date::Error
|
|
33
30
|
key.failure("invalid date format")
|
|
34
31
|
end
|
|
35
32
|
|
|
36
33
|
rule(:from_date, :to_date) do
|
|
37
|
-
next unless values[:from_date].match?(/\A\d{4}-\d{2}-\d{2}
|
|
34
|
+
next unless values[:from_date].match?(/\A\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?\z/) &&
|
|
35
|
+
values[:to_date].match?(/\A\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?\z/)
|
|
38
36
|
|
|
39
|
-
from_date =
|
|
40
|
-
to_date =
|
|
37
|
+
from_date = DateTime.parse(values[:from_date])
|
|
38
|
+
to_date = DateTime.parse(values[:to_date])
|
|
41
39
|
key.failure("from_date must be before to_date") if from_date >= to_date
|
|
42
40
|
rescue Date::Error
|
|
43
41
|
key.failure("invalid date format")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Contracts
|
|
5
|
+
# Validates payloads for POST /v2/charts/intraday (minute OHLC). Requires interval.
|
|
6
|
+
class IntradayHistoricalDataContract < HistoricalDataContract
|
|
7
|
+
params do
|
|
8
|
+
required(:interval).filled(:string, included_in?: CHART_INTERVALS)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -2,39 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
module DhanHQ
|
|
4
4
|
module Contracts
|
|
5
|
-
# Validates
|
|
6
|
-
|
|
5
|
+
# Validates request for POST /v2/margincalculator (single order).
|
|
6
|
+
# dhanClientId, exchangeSegment (NSE_EQ|NSE_FNO|BSE_EQ|BSE_FNO|MCX_COMM), transactionType, quantity,
|
|
7
|
+
# productType (CNC|INTRADAY|MARGIN|MTF), securityId, price (required);
|
|
8
|
+
# orderType (optional, but required by some accounts); triggerPrice (optional, for SL-M/SL-L).
|
|
9
|
+
class MarginCalculatorContract < BaseContract
|
|
7
10
|
params do
|
|
8
11
|
required(:dhanClientId).filled(:string)
|
|
9
|
-
required(:exchangeSegment).filled(:string, included_in?:
|
|
10
|
-
required(:transactionType).filled(:string, included_in?:
|
|
12
|
+
required(:exchangeSegment).filled(:string, included_in?: MARGIN_CALCULATOR_SEGMENTS)
|
|
13
|
+
required(:transactionType).filled(:string, included_in?: TRANSACTION_TYPES)
|
|
11
14
|
required(:quantity).filled(:integer, gt?: 0)
|
|
12
|
-
required(:productType).filled(:string, included_in?:
|
|
15
|
+
required(:productType).filled(:string, included_in?: MARGIN_PRODUCT_TYPES)
|
|
16
|
+
optional(:orderType).maybe(:string, included_in?: ORDER_TYPES)
|
|
13
17
|
required(:securityId).filled(:string)
|
|
14
|
-
|
|
18
|
+
required(:price).filled(:float, gt?: 0)
|
|
15
19
|
optional(:triggerPrice).maybe(:float)
|
|
16
20
|
end
|
|
21
|
+
|
|
17
22
|
rule(:price) do
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
elsif values[:price].is_a?(Float) && (values[:price].nan? || values[:price].infinite?)
|
|
22
|
-
key(:price).failure("must be a finite number")
|
|
23
|
-
end
|
|
24
|
-
end
|
|
23
|
+
next unless values[:price].is_a?(Float)
|
|
24
|
+
|
|
25
|
+
key.failure("must be a finite number") if values[:price].nan? || values[:price].infinite?
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
rule(:triggerPrice) do
|
|
28
|
-
|
|
29
|
+
next unless values[:triggerPrice].is_a?(Float)
|
|
30
|
+
|
|
31
|
+
key.failure("must be a finite number") if values[:triggerPrice].nan? || values[:triggerPrice].infinite?
|
|
29
32
|
end
|
|
30
33
|
|
|
31
|
-
# Segment-Based Product Restrictions for margin calculations
|
|
32
34
|
rule(:productType, :exchangeSegment) do
|
|
33
35
|
case values[:productType]
|
|
34
36
|
when DhanHQ::Constants::ProductType::CNC, DhanHQ::Constants::ProductType::MTF
|
|
35
|
-
key
|
|
37
|
+
key.failure("is only allowed for Equity segments (NSE_EQ, BSE_EQ)") unless values[:exchangeSegment].to_s.end_with?("_EQ")
|
|
36
38
|
when DhanHQ::Constants::ProductType::MARGIN
|
|
37
|
-
key
|
|
39
|
+
key.failure("is not allowed for Equity Cash segments; use CNC or INTRADAY") if values[:exchangeSegment].to_s.end_with?("_EQ")
|
|
38
40
|
end
|
|
39
41
|
end
|
|
40
42
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Contracts
|
|
5
|
+
# Validates request payloads for Market Feed endpoints (LTP, OHLC, Quote).
|
|
6
|
+
#
|
|
7
|
+
# The Market Feed API expects a payload where keys are Exchange Segments
|
|
8
|
+
# and values are Arrays of security IDs (Integers).
|
|
9
|
+
#
|
|
10
|
+
# @example Valid payload:
|
|
11
|
+
# {
|
|
12
|
+
# "NSE_EQ": [11536, 3456],
|
|
13
|
+
# "NSE_FNO": [49081, 49082]
|
|
14
|
+
# }
|
|
15
|
+
#
|
|
16
|
+
class MarketFeedContract < BaseContract
|
|
17
|
+
params do
|
|
18
|
+
config.validate_keys = true
|
|
19
|
+
|
|
20
|
+
# Dynamically define all valid exchange segments as optional keys.
|
|
21
|
+
# Each must be an array of integers.
|
|
22
|
+
EXCHANGE_SEGMENTS.each do |segment|
|
|
23
|
+
optional(segment.to_sym).array(:integer)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
rule do
|
|
28
|
+
base.failure("must provide at least one exchange segment and security ID") if values.to_h.empty?
|
|
29
|
+
|
|
30
|
+
total_instruments = 0
|
|
31
|
+
values.to_h.each do |key, value|
|
|
32
|
+
if value.is_a?(Array)
|
|
33
|
+
key(key).failure("must not be empty") if value.empty?
|
|
34
|
+
total_instruments += value.size
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
base.failure("cannot fetch more than 1000 instruments in a single request (found #{total_instruments})") if total_instruments > 1000
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -2,20 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
module DhanHQ
|
|
4
4
|
module Contracts
|
|
5
|
-
# Validates
|
|
5
|
+
# Validates request for POST /v2/margincalculator/multi.
|
|
6
|
+
# Top-level: includePosition, includeOrder, dhanClientId, scripList.
|
|
7
|
+
# Each scrip: exchangeSegment, transactionType, quantity, productType, securityId, price; triggerPrice optional.
|
|
6
8
|
class MultiScripMarginCalcRequestContract < BaseContract
|
|
7
9
|
params do
|
|
8
10
|
optional(:dhanClientId).maybe(:string)
|
|
9
11
|
optional(:includePosition).maybe(:bool)
|
|
10
12
|
optional(:includeOrder).maybe(:bool)
|
|
11
13
|
required(:scripList).array(:hash) do
|
|
12
|
-
required(:exchangeSegment).filled(:string, included_in?:
|
|
14
|
+
required(:exchangeSegment).filled(:string, included_in?: MARGIN_CALCULATOR_SEGMENTS)
|
|
13
15
|
required(:transactionType).filled(:string, included_in?: TRANSACTION_TYPES)
|
|
14
16
|
required(:quantity).filled(:integer, gt?: 0)
|
|
15
|
-
required(:productType).filled(:string, included_in?:
|
|
17
|
+
required(:productType).filled(:string, included_in?: MARGIN_PRODUCT_TYPES)
|
|
18
|
+
optional(:orderType).maybe(:string, included_in?: ORDER_TYPES)
|
|
16
19
|
required(:securityId).filled(:string)
|
|
17
|
-
|
|
18
|
-
optional(:triggerPrice).maybe(:float
|
|
20
|
+
required(:price).filled(:float, gt?: 0)
|
|
21
|
+
optional(:triggerPrice).maybe(:float)
|
|
19
22
|
end
|
|
20
23
|
end
|
|
21
24
|
end
|
|
@@ -1,40 +1,38 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require "date"
|
|
4
4
|
|
|
5
5
|
module DhanHQ
|
|
6
6
|
module Contracts
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# Validates request parameters for fetching option chains.
|
|
7
|
+
# Validates request for POST /v2/optionchain (option chain by underlying and expiry).
|
|
8
|
+
# UnderlyingScrip (int), UnderlyingSeg (enum), Expiry (YYYY-MM-DD). Rate limit: 1 request per 3 seconds.
|
|
10
9
|
class OptionChainContract < BaseContract
|
|
11
10
|
params do
|
|
12
|
-
required(:underlying_scrip).filled(:integer)
|
|
13
|
-
required(:underlying_seg).filled(:string, included_in?:
|
|
11
|
+
required(:underlying_scrip).filled(:integer)
|
|
12
|
+
required(:underlying_seg).filled(:string, included_in?: OPTION_CHAIN_UNDERLYING_SEGMENTS)
|
|
14
13
|
required(:expiry).filled(:string)
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
rule(:expiry) do
|
|
18
|
-
|
|
19
|
-
key.failure("must be in 'YYYY-MM-DD' format") unless value.match?(/^\d{4}-\d{2}-\d{2}$/)
|
|
17
|
+
next unless value.is_a?(String)
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
key.failure("must be a valid date") unless parsed_date.to_s == value
|
|
25
|
-
rescue ArgumentError
|
|
26
|
-
key.failure("is not a valid date")
|
|
19
|
+
unless value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
20
|
+
key.failure("must be in YYYY-MM-DD format")
|
|
21
|
+
next
|
|
27
22
|
end
|
|
23
|
+
|
|
24
|
+
Date.parse(value)
|
|
25
|
+
rescue StandardError
|
|
26
|
+
key.failure("must be a valid date")
|
|
28
27
|
end
|
|
29
28
|
end
|
|
30
29
|
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
# Validates request parameters for fetching expiry lists (expiry not required).
|
|
30
|
+
# Validates request for POST /v2/optionchain/expirylist (expiry list for an underlying).
|
|
31
|
+
# UnderlyingScrip (int), UnderlyingSeg (enum). No Expiry.
|
|
34
32
|
class OptionChainExpiryListContract < BaseContract
|
|
35
33
|
params do
|
|
36
|
-
required(:underlying_scrip).filled(:integer)
|
|
37
|
-
required(:underlying_seg).filled(:string, included_in?:
|
|
34
|
+
required(:underlying_scrip).filled(:integer)
|
|
35
|
+
required(:underlying_seg).filled(:string, included_in?: OPTION_CHAIN_UNDERLYING_SEGMENTS)
|
|
38
36
|
end
|
|
39
37
|
end
|
|
40
38
|
end
|
|
@@ -13,7 +13,7 @@ module DhanHQ
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
rule(:profitValue, :lossValue) do
|
|
16
|
-
key.failure("at least one of profitValue or lossValue must be provided")
|
|
16
|
+
key.failure("at least one of profitValue or lossValue must be provided") unless values[:profitValue] || values[:lossValue]
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
end
|
|
@@ -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,21 +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)
|
|
92
|
-
return params if
|
|
93
|
+
return params if params.empty?
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
param_formatter_for(full_path).call(params)
|
|
95
96
|
end
|
|
96
97
|
|
|
97
|
-
#
|
|
98
|
-
def
|
|
99
|
-
|
|
100
|
-
|
|
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")
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
endpoint.include?("/marketfeed")
|
|
103
|
+
->(p) { camelize_keys(p) }
|
|
104
104
|
end
|
|
105
105
|
end
|
|
106
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
|
|
|
@@ -79,9 +79,10 @@ module DhanHQ
|
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
out = payload
|
|
82
|
-
if path &&
|
|
82
|
+
if path && %i[post put patch].include?(method)
|
|
83
83
|
client_id = DhanHQ.configuration&.client_id
|
|
84
|
-
|
|
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")
|
|
85
86
|
out = payload.dup
|
|
86
87
|
if out.keys.any?(String)
|
|
87
88
|
out["dhanClientId"] = client_id
|
|
@@ -97,5 +98,13 @@ module DhanHQ
|
|
|
97
98
|
else req.body = out.to_json
|
|
98
99
|
end
|
|
99
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
|
|
100
109
|
end
|
|
101
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
|