DhanHQ 2.4.0 → 2.6.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/.rubocop.yml +9 -1
- data/CHANGELOG.md +103 -7
- data/GUIDE.md +57 -39
- data/README.md +198 -755
- data/docs/API_DOCS_GAPS.md +128 -0
- data/docs/API_VERIFICATION.md +10 -11
- data/{README1.md → docs/ARCHIVE_README.md} +16 -16
- data/docs/AUTHENTICATION.md +72 -10
- data/docs/CONFIGURATION.md +109 -0
- data/docs/CONSTANTS_REFERENCE.md +477 -0
- data/docs/DATA_API_PARAMETERS.md +7 -7
- data/docs/{rails_websocket_integration.md → RAILS_WEBSOCKET_INTEGRATION.md} +10 -10
- data/docs/{standalone_ruby_websocket_integration.md → STANDALONE_RUBY_WEBSOCKET_INTEGRATION.md} +32 -32
- data/docs/SUPER_ORDERS.md +284 -0
- data/docs/{technical_analysis.md → TECHNICAL_ANALYSIS.md} +3 -3
- data/docs/TESTING_GUIDE.md +84 -82
- data/docs/TROUBLESHOOTING.md +117 -0
- data/docs/{websocket_integration.md → WEBSOCKET_INTEGRATION.md} +19 -19
- data/docs/WEBSOCKET_PROTOCOL.md +154 -0
- data/lib/DhanHQ/constants.rb +456 -151
- data/lib/DhanHQ/contracts/alert_order_contract.rb +37 -10
- data/lib/DhanHQ/contracts/base_contract.rb +22 -0
- data/lib/DhanHQ/contracts/edis_contract.rb +25 -0
- data/lib/DhanHQ/contracts/margin_calculator_contract.rb +27 -4
- data/lib/DhanHQ/contracts/modify_order_contract.rb +65 -12
- data/lib/DhanHQ/contracts/multi_scrip_margin_calc_request_contract.rb +23 -0
- data/lib/DhanHQ/contracts/order_contract.rb +171 -39
- data/lib/DhanHQ/contracts/place_order_contract.rb +14 -141
- data/lib/DhanHQ/contracts/pnl_based_exit_contract.rb +20 -0
- data/lib/DhanHQ/contracts/position_conversion_contract.rb +15 -3
- data/lib/DhanHQ/contracts/slice_order_contract.rb +10 -1
- data/lib/DhanHQ/contracts/user_ip_contract.rb +14 -0
- data/lib/DhanHQ/core/base_model.rb +13 -4
- data/lib/DhanHQ/helpers/response_helper.rb +2 -2
- data/lib/DhanHQ/helpers/validation_helper.rb +1 -1
- data/lib/DhanHQ/models/alert_order.rb +29 -11
- data/lib/DhanHQ/models/concerns/api_response_handler.rb +46 -0
- data/lib/DhanHQ/models/edis.rb +101 -0
- data/lib/DhanHQ/models/expired_options_data.rb +6 -12
- data/lib/DhanHQ/models/forever_order.rb +8 -5
- data/lib/DhanHQ/models/historical_data.rb +0 -8
- data/lib/DhanHQ/models/instrument.rb +1 -7
- data/lib/DhanHQ/models/instrument_helpers.rb +4 -4
- data/lib/DhanHQ/models/kill_switch.rb +23 -11
- data/lib/DhanHQ/models/margin.rb +51 -2
- data/lib/DhanHQ/models/order.rb +107 -126
- data/lib/DhanHQ/models/order_update.rb +7 -13
- data/lib/DhanHQ/models/pnl_exit.rb +122 -0
- data/lib/DhanHQ/models/position.rb +23 -1
- data/lib/DhanHQ/models/postback.rb +114 -0
- data/lib/DhanHQ/models/profile.rb +0 -10
- data/lib/DhanHQ/models/super_order.rb +13 -3
- data/lib/DhanHQ/models/trade.rb +11 -23
- data/lib/DhanHQ/resources/ip_setup.rb +16 -5
- data/lib/DhanHQ/resources/kill_switch.rb +17 -7
- data/lib/DhanHQ/resources/margin_calculator.rb +9 -0
- data/lib/DhanHQ/resources/orders.rb +41 -41
- 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/DhanHQ/ws/cmd_bus.rb +1 -1
- data/lib/DhanHQ/ws/orders/client.rb +6 -6
- data/lib/DhanHQ/ws/singleton_lock.rb +2 -1
- data/lib/dhanhq/analysis/options_buying_advisor.rb +2 -2
- data/lib/rubocop/cop/dhanhq/use_constants.rb +171 -0
- metadata +29 -24
- data/TODO-1.md +0 -14
- data/TODO.md +0 -127
- data/app/services/live/order_update_guard_support.rb +0 -75
- data/app/services/live/order_update_hub.rb +0 -76
- data/app/services/live/order_update_persistence_support.rb +0 -68
- data/docs/PR_2.2.0.md +0 -48
- data/examples/comprehensive_websocket_examples.rb +0 -148
- data/examples/instrument_finder_test.rb +0 -195
- data/examples/live_order_updates.rb +0 -118
- data/examples/market_depth_example.rb +0 -144
- data/examples/market_feed_example.rb +0 -81
- data/examples/order_update_example.rb +0 -105
- data/examples/trading_fields_example.rb +0 -215
- /data/docs/{live_order_updates.md → LIVE_ORDER_UPDATES.md} +0 -0
- /data/docs/{rails_integration.md → RAILS_INTEGRATION.md} +0 -0
|
@@ -1,168 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "order_contract"
|
|
4
|
+
|
|
3
5
|
module DhanHQ
|
|
4
6
|
module Contracts
|
|
5
7
|
# Validation contract for placing an order via Dhanhq's API.
|
|
6
|
-
|
|
7
|
-
# This contract validates the parameters required to place an order,
|
|
8
|
-
# ensuring the correctness of inputs based on API requirements. It includes:
|
|
9
|
-
# - Mandatory fields for order placement.
|
|
10
|
-
# - Conditional validation for optional fields based on provided values.
|
|
11
|
-
# - Validation of enumerated values using constants for consistency.
|
|
12
|
-
#
|
|
13
|
-
# Example usage:
|
|
14
|
-
# contract = Dhanhq::Contracts::PlaceOrderContract.new
|
|
15
|
-
# result = contract.call(
|
|
16
|
-
# dhanClientId: "123456",
|
|
17
|
-
# transaction_type: "BUY",
|
|
18
|
-
# exchange_segment: "NSE_EQ",
|
|
19
|
-
# product_type: "CNC",
|
|
20
|
-
# order_type: "LIMIT",
|
|
21
|
-
# validity: "DAY",
|
|
22
|
-
# security_id: "1001",
|
|
23
|
-
# quantity: 10,
|
|
24
|
-
# price: 150.0
|
|
25
|
-
# )
|
|
26
|
-
# result.success? # => true or false
|
|
27
|
-
#
|
|
28
|
-
# @see https://dhanhq.co/docs/v2/ Dhanhq API Documentation
|
|
29
|
-
class PlaceOrderContract < BaseContract
|
|
30
|
-
# Parameters and validation rules for the place order request.
|
|
31
|
-
#
|
|
32
|
-
# @!attribute [r] correlation_id
|
|
33
|
-
# @return [String] Optional. Identifier for tracking, max length 25 characters.
|
|
34
|
-
# @!attribute [r] transaction_type
|
|
35
|
-
# @return [String] Required. BUY or SELL.
|
|
36
|
-
# @!attribute [r] exchange_segment
|
|
37
|
-
# @return [String] Required. Exchange segment for the order.
|
|
38
|
-
# Must be one of: `EXCHANGE_SEGMENTS`.
|
|
39
|
-
# @!attribute [r] product_type
|
|
40
|
-
# @return [String] Required. Product type for the order.
|
|
41
|
-
# Must be one of: `PRODUCT_TYPES`.
|
|
42
|
-
# @!attribute [r] order_type
|
|
43
|
-
# @return [String] Required. Type of order.
|
|
44
|
-
# Must be one of: `ORDER_TYPES`.
|
|
45
|
-
# @!attribute [r] validity
|
|
46
|
-
# @return [String] Required. Validity of the order.
|
|
47
|
-
# Must be one of: DAY, IOC.
|
|
48
|
-
# @!attribute [r] trading_symbol
|
|
49
|
-
# @return [String] Optional. Trading symbol of the instrument.
|
|
50
|
-
# @!attribute [r] security_id
|
|
51
|
-
# @return [String] Required. Security identifier for the order.
|
|
52
|
-
# @!attribute [r] quantity
|
|
53
|
-
# @return [Integer] Required. Quantity of the order, must be greater than 0.
|
|
54
|
-
# @!attribute [r] disclosed_quantity
|
|
55
|
-
# @return [Integer] Optional. Disclosed quantity, must be >= 0 if provided.
|
|
56
|
-
# @!attribute [r] price
|
|
57
|
-
# @return [Float] Optional. Price for the order, must be > 0 if provided.
|
|
58
|
-
# @!attribute [r] trigger_price
|
|
59
|
-
# @return [Float] Optional. Trigger price for stop-loss orders, must be > 0 if provided.
|
|
60
|
-
# @!attribute [r] after_market_order
|
|
61
|
-
# @return [Boolean] Optional. Indicates if this is an after-market order.
|
|
62
|
-
# @!attribute [r] amo_time
|
|
63
|
-
# @return [String] Optional. Time for after-market orders. Must be one of: OPEN, OPEN_30, OPEN_60.
|
|
64
|
-
# @!attribute [r] bo_profit_value
|
|
65
|
-
# @return [Float] Optional. Profit value for Bracket Orders, must be > 0 if provided.
|
|
66
|
-
# @!attribute [r] bo_stop_loss_value
|
|
67
|
-
# @return [Float] Optional. Stop-loss value for Bracket Orders, must be > 0 if provided.
|
|
68
|
-
# @!attribute [r] drv_expiry_date
|
|
69
|
-
# @return [String] Optional. Expiry date for derivative contracts.
|
|
70
|
-
# @!attribute [r] drv_option_type
|
|
71
|
-
# @return [String] Optional. Option type for derivatives, must be one of: CALL, PUT, NA.
|
|
72
|
-
# @!attribute [r] drv_strike_price
|
|
73
|
-
# @return [Float] Optional. Strike price for options, must be > 0 if provided.
|
|
8
|
+
class PlaceOrderContract < OrderContract
|
|
74
9
|
params do
|
|
10
|
+
# Common required fields
|
|
75
11
|
required(:transaction_type).filled(:string, included_in?: TRANSACTION_TYPES)
|
|
76
12
|
required(:exchange_segment).filled(:string, included_in?: EXCHANGE_SEGMENTS)
|
|
77
13
|
required(:product_type).filled(:string, included_in?: PRODUCT_TYPES)
|
|
78
14
|
required(:order_type).filled(:string, included_in?: ORDER_TYPES)
|
|
79
15
|
required(:validity).filled(:string, included_in?: VALIDITY_TYPES)
|
|
80
|
-
required(:security_id).filled(:string)
|
|
16
|
+
required(:security_id).filled(:string, max_size?: 20)
|
|
81
17
|
required(:quantity).filled(:integer, gt?: 0)
|
|
18
|
+
|
|
19
|
+
# Optional fields
|
|
20
|
+
optional(:correlation_id).maybe(:string, max_size?: 30, format?: /\A[a-zA-Z0-9 _-]*\z/)
|
|
82
21
|
optional(:disclosed_quantity).maybe(:integer, gteq?: 0)
|
|
83
|
-
optional(:trading_symbol).maybe(:string)
|
|
84
|
-
optional(:correlation_id).maybe(:string, max_size?: 25)
|
|
85
22
|
optional(:price).maybe(:float, gt?: 0)
|
|
86
23
|
optional(:trigger_price).maybe(:float, gt?: 0)
|
|
87
24
|
optional(:after_market_order).maybe(:bool)
|
|
88
|
-
optional(:amo_time).maybe(:string, included_in?:
|
|
25
|
+
optional(:amo_time).maybe(:string, included_in?: AMO_TIMES)
|
|
89
26
|
optional(:bo_profit_value).maybe(:float, gt?: 0)
|
|
90
27
|
optional(:bo_stop_loss_value).maybe(:float, gt?: 0)
|
|
28
|
+
|
|
29
|
+
# Derivative specific fields
|
|
30
|
+
optional(:trading_symbol).maybe(:string)
|
|
91
31
|
optional(:drv_expiry_date).maybe(:string)
|
|
92
32
|
optional(:drv_option_type).maybe(:string, included_in?: %w[CALL PUT NA])
|
|
93
33
|
optional(:drv_strike_price).maybe(:float, gt?: 0)
|
|
94
34
|
end
|
|
95
35
|
|
|
96
|
-
# Validate that float values are finite (not NaN or Infinity) and within reasonable bounds
|
|
97
|
-
rule(:price) do
|
|
98
|
-
if values[:price].is_a?(Float)
|
|
99
|
-
if values[:price].nan? || values[:price].infinite?
|
|
100
|
-
key(:price).failure("must be a finite number")
|
|
101
|
-
elsif values[:price] > 1_000_000_000
|
|
102
|
-
key(:price).failure("must be less than 1,000,000,000")
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
rule(:trigger_price) do
|
|
108
|
-
if values[:trigger_price].is_a?(Float)
|
|
109
|
-
if values[:trigger_price].nan? || values[:trigger_price].infinite?
|
|
110
|
-
key(:trigger_price).failure("must be a finite number")
|
|
111
|
-
elsif values[:trigger_price] > 1_000_000_000
|
|
112
|
-
key(:trigger_price).failure("must be less than 1,000,000,000")
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
rule(:bo_profit_value) do
|
|
118
|
-
if values[:bo_profit_value].is_a?(Float)
|
|
119
|
-
if values[:bo_profit_value].nan? || values[:bo_profit_value].infinite?
|
|
120
|
-
key(:bo_profit_value).failure("must be a finite number")
|
|
121
|
-
elsif values[:bo_profit_value] > 1_000_000_000
|
|
122
|
-
key(:bo_profit_value).failure("must be less than 1,000,000,000")
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
rule(:bo_stop_loss_value) do
|
|
128
|
-
if values[:bo_stop_loss_value].is_a?(Float)
|
|
129
|
-
if values[:bo_stop_loss_value].nan? || values[:bo_stop_loss_value].infinite?
|
|
130
|
-
key(:bo_stop_loss_value).failure("must be a finite number")
|
|
131
|
-
elsif values[:bo_stop_loss_value] > 1_000_000_000
|
|
132
|
-
key(:bo_stop_loss_value).failure("must be less than 1,000,000,000")
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
36
|
rule(:drv_strike_price) do
|
|
138
|
-
if
|
|
139
|
-
|
|
140
|
-
key(:drv_strike_price).failure("must be a finite number")
|
|
141
|
-
elsif values[:drv_strike_price] > 1_000_000_000
|
|
142
|
-
key(:drv_strike_price).failure("must be less than 1,000,000,000")
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# Custom validation for trigger price when the order type is STOP_LOSS or STOP_LOSS_MARKET.
|
|
148
|
-
rule(:trigger_price, :order_type) do
|
|
149
|
-
if values[:order_type] =~ /^STOP_LOSS/ && !values[:trigger_price]
|
|
150
|
-
key(:trigger_price).failure("is required for order_type STOP_LOSS or STOP_LOSS_MARKET")
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
# Custom validation for AMO time when the order is marked as after-market.
|
|
155
|
-
rule(:after_market_order, :amo_time) do
|
|
156
|
-
if values[:after_market_order] == true && !values[:amo_time]
|
|
157
|
-
key(:amo_time).failure("is required when after_market_order is true")
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
# Custom validation for Bracket Order (BO) fields.
|
|
162
|
-
rule(:bo_profit_value, :bo_stop_loss_value, :product_type) do
|
|
163
|
-
if values[:product_type] == "BO" && (!values[:bo_profit_value] || !values[:bo_stop_loss_value])
|
|
164
|
-
key(:bo_profit_value).failure("is required for Bracket Orders")
|
|
165
|
-
key(:bo_stop_loss_value).failure("is required for Bracket Orders")
|
|
37
|
+
if value.is_a?(Float) && (value.nan? || value.infinite?)
|
|
38
|
+
key.failure("must be a finite number")
|
|
166
39
|
end
|
|
167
40
|
end
|
|
168
41
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Contracts
|
|
5
|
+
# Validates requests for P&L based exit configurations.
|
|
6
|
+
class PnlBasedExitContract < BaseContract
|
|
7
|
+
params do
|
|
8
|
+
optional(:dhanClientId).maybe(:string)
|
|
9
|
+
optional(:profitValue).maybe(:float, gt?: 0)
|
|
10
|
+
optional(:lossValue).maybe(:float, gt?: 0)
|
|
11
|
+
optional(:enableKillSwitch).maybe(:bool)
|
|
12
|
+
optional(:productType).array(:string, included_in?: %w[INTRADAY DELIVERY])
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
rule(:profitValue, :lossValue) do
|
|
16
|
+
key.failure("at least one of profitValue or lossValue must be provided") if !values[:profitValue] && !values[:lossValue]
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -7,7 +7,7 @@ module DhanHQ
|
|
|
7
7
|
params do
|
|
8
8
|
required(:dhanClientId).filled(:string)
|
|
9
9
|
required(:fromProductType).filled(:string, included_in?: PRODUCT_TYPES)
|
|
10
|
-
required(:exchangeSegment).filled(:string, included_in?: EXCHANGE_SEGMENTS)
|
|
10
|
+
required(:exchangeSegment).filled(:string, included_in?: EXCHANGE_SEGMENTS - %w[IDX_I NSE_COMM])
|
|
11
11
|
required(:positionType).filled(:string, included_in?: %w[LONG SHORT CLOSED])
|
|
12
12
|
required(:securityId).filled(:string)
|
|
13
13
|
required(:convertQty).filled(:integer, gt?: 0)
|
|
@@ -15,9 +15,21 @@ module DhanHQ
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
rule(:toProductType, :fromProductType) do
|
|
18
|
-
|
|
18
|
+
key(:toProductType).failure("must be different from fromProductType") if values[:toProductType] == values[:fromProductType]
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
if %w[BO CO].include?(values[:toProductType]) || %w[BO CO].include?(values[:fromProductType])
|
|
21
|
+
key(:base).failure("cannot convert to or from Bracket (BO) or Cover (CO) orders")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Segment-Based Product Restrictions for conversion
|
|
26
|
+
rule(:toProductType, :exchangeSegment) do
|
|
27
|
+
case values[:toProductType]
|
|
28
|
+
when DhanHQ::Constants::ProductType::CNC, DhanHQ::Constants::ProductType::MTF
|
|
29
|
+
key(:toProductType).failure("is only allowed for Equity segments (NSE_EQ, BSE_EQ)") unless /_EQ$/.match?(values[:exchangeSegment])
|
|
30
|
+
when DhanHQ::Constants::ProductType::MARGIN
|
|
31
|
+
key(:toProductType).failure("is not allowed for Equity Cash segments; use CNC or INTRADAY") if /_EQ$/.match?(values[:exchangeSegment])
|
|
32
|
+
end
|
|
21
33
|
end
|
|
22
34
|
end
|
|
23
35
|
end
|
|
@@ -93,9 +93,14 @@ module DhanHQ
|
|
|
93
93
|
optional(:drvStrikePrice).maybe(:float, gt?: 0)
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
+
# Institutional macros
|
|
97
|
+
rule(:quantity).validate(:lot_size_multiple)
|
|
98
|
+
rule(:price).validate(:tick_size_multiple)
|
|
99
|
+
rule(:triggerPrice).validate(:tick_size_multiple)
|
|
100
|
+
|
|
96
101
|
# Custom validation for trigger price when the order type is STOP_LOSS or STOP_LOSS_MARKET.
|
|
97
102
|
rule(:triggerPrice, :orderType) do
|
|
98
|
-
if values[:orderType].start_with?(
|
|
103
|
+
if values[:orderType].start_with?(DhanHQ::Constants::OrderType::STOP_LOSS) && !values[:triggerPrice]
|
|
99
104
|
key(:triggerPrice).failure("is required for orderType STOP_LOSS or STOP_LOSS_MARKET")
|
|
100
105
|
end
|
|
101
106
|
end
|
|
@@ -106,6 +111,10 @@ module DhanHQ
|
|
|
106
111
|
key(:amoTime).failure("is required when afterMarketOrder is true")
|
|
107
112
|
end
|
|
108
113
|
end
|
|
114
|
+
|
|
115
|
+
# Validation to ensure slice amounts don't sum to more than something?
|
|
116
|
+
# For SliceOrder, total quantity might be split by exchange limits if it exceeds. Since the API handles splitting,
|
|
117
|
+
# we just need to make sure quantity matches lot sizes.
|
|
109
118
|
end
|
|
110
119
|
end
|
|
111
120
|
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Contracts
|
|
5
|
+
# Validates requests for configuring static IP addresses.
|
|
6
|
+
class UserIpContract < BaseContract
|
|
7
|
+
params do
|
|
8
|
+
optional(:dhanClientId).maybe(:string)
|
|
9
|
+
required(:ip).filled(:string)
|
|
10
|
+
required(:ipFlag).filled(:string, included_in?: %w[PRIMARY SECONDARY])
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -80,11 +80,12 @@ module DhanHQ
|
|
|
80
80
|
self::HTTP_PATH
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
-
#
|
|
83
|
+
# Default class-level validation contract — returns nil (no validation).
|
|
84
|
+
# Override in subclasses to provide a contract for class-level validation.
|
|
84
85
|
#
|
|
85
|
-
# @return [
|
|
86
|
+
# @return [nil]
|
|
86
87
|
def validation_contract
|
|
87
|
-
|
|
88
|
+
nil
|
|
88
89
|
end
|
|
89
90
|
|
|
90
91
|
# Validate attributes before creating a new instance
|
|
@@ -155,6 +156,14 @@ module DhanHQ
|
|
|
155
156
|
|
|
156
157
|
# Instance Methods
|
|
157
158
|
|
|
159
|
+
# Default validation contract — returns nil (no validation).
|
|
160
|
+
# Models that require instance-level validation must override this method.
|
|
161
|
+
#
|
|
162
|
+
# @return [nil]
|
|
163
|
+
def validation_contract
|
|
164
|
+
nil
|
|
165
|
+
end
|
|
166
|
+
|
|
158
167
|
# Update an existing resource
|
|
159
168
|
#
|
|
160
169
|
# @param attributes [Hash] Attributes to update
|
|
@@ -264,7 +273,7 @@ module DhanHQ
|
|
|
264
273
|
|
|
265
274
|
# Validate attributes using contract
|
|
266
275
|
def valid?
|
|
267
|
-
contract_class =
|
|
276
|
+
contract_class = validation_contract || self.class.validation_contract
|
|
268
277
|
return true unless contract_class
|
|
269
278
|
|
|
270
279
|
contract = contract_class.is_a?(Class) ? contract_class.new : contract_class
|
|
@@ -48,7 +48,7 @@ module DhanHQ
|
|
|
48
48
|
|
|
49
49
|
error_code = body[:errorCode] || response.status.to_s
|
|
50
50
|
error_message = body[:errorMessage] || body[:message] || "Unknown error"
|
|
51
|
-
if error_code ==
|
|
51
|
+
if error_code == DhanHQ::Constants::TradingErrorCode::NO_HOLDINGS
|
|
52
52
|
error_message = "No holdings found for this account. Add holdings or wait for them to settle before retrying."
|
|
53
53
|
end
|
|
54
54
|
|
|
@@ -71,7 +71,7 @@ module DhanHQ
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
error_text =
|
|
74
|
-
if error_code ==
|
|
74
|
+
if error_code == DhanHQ::Constants::TradingErrorCode::NO_HOLDINGS
|
|
75
75
|
"#{error_message} (error code: #{error_code})"
|
|
76
76
|
else
|
|
77
77
|
"#{error_code}: #{error_message}"
|
|
@@ -16,7 +16,7 @@ module DhanHQ
|
|
|
16
16
|
|
|
17
17
|
# Validate instance attributes using the defined validation contract
|
|
18
18
|
def validate!
|
|
19
|
-
contract_class =
|
|
19
|
+
contract_class = validation_contract || self.class.validation_contract
|
|
20
20
|
return unless contract_class
|
|
21
21
|
|
|
22
22
|
contract = contract_class.is_a?(Class) ? contract_class.new : contract_class
|
|
@@ -6,6 +6,8 @@ module DhanHQ
|
|
|
6
6
|
module Models
|
|
7
7
|
# Model for alert/conditional orders. CRUD via AlertOrders resource; validated by AlertOrderContract.
|
|
8
8
|
class AlertOrder < BaseModel
|
|
9
|
+
include Concerns::ApiResponseHandler
|
|
10
|
+
|
|
9
11
|
HTTP_PATH = "/alerts/orders"
|
|
10
12
|
|
|
11
13
|
attributes :alert_id, :exchange_segment, :security_id, :condition,
|
|
@@ -27,9 +29,7 @@ module DhanHQ
|
|
|
27
29
|
|
|
28
30
|
def all
|
|
29
31
|
response = resource.all
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
response.map { |attrs| new(attrs, skip_validation: true) }
|
|
32
|
+
parse_collection_response(response)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def find(alert_id)
|
|
@@ -48,27 +48,45 @@ 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
|
|
54
76
|
alert_id&.to_s
|
|
55
77
|
end
|
|
56
78
|
|
|
57
|
-
def save
|
|
79
|
+
def save
|
|
58
80
|
return false unless valid?
|
|
59
81
|
|
|
60
82
|
payload = to_request_params
|
|
61
83
|
response = if new_record?
|
|
62
|
-
self.class.resource.create(
|
|
84
|
+
self.class.resource.create(payload)
|
|
63
85
|
else
|
|
64
|
-
self.class.resource.update(id,
|
|
86
|
+
self.class.resource.update(id, payload)
|
|
65
87
|
end
|
|
66
|
-
return false if new_record? && !(response.is_a?(Hash) && response["alertId"])
|
|
67
|
-
return false if !new_record? && !success_response?(response)
|
|
68
88
|
|
|
69
|
-
|
|
70
|
-
assign_attributes
|
|
71
|
-
true
|
|
89
|
+
handle_api_response(response, success_key: new_record? ? "alertId" : nil)
|
|
72
90
|
end
|
|
73
91
|
|
|
74
92
|
def destroy # rubocop:disable Naming/PredicateMethod
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Models
|
|
5
|
+
module Concerns
|
|
6
|
+
# Shared behavior for handling API responses and error logging in models.
|
|
7
|
+
module ApiResponseHandler
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# Handles a standard API response, merging attributes on success or logging error on failure.
|
|
11
|
+
#
|
|
12
|
+
# @param response [Hash, Array] The raw API response
|
|
13
|
+
# @param success_key [String, nil] Key to check for specific success identifier (e.g., "orderId")
|
|
14
|
+
# @param context [String] Context for logging (e.g., "[DhanHQ::Models::Order] Placement")
|
|
15
|
+
# @param error_target [Symbol] Where to store errors (defaults to :@errors)
|
|
16
|
+
# @return [Boolean] True if successful
|
|
17
|
+
def handle_api_response(response, success_key: nil, context: nil) # rubocop:disable Naming/PredicateMethod
|
|
18
|
+
response = response.with_indifferent_access if response.respond_to?(:with_indifferent_access)
|
|
19
|
+
is_hash = response.is_a?(Hash)
|
|
20
|
+
is_success = success_response?(response) || (is_hash && success_key && response[success_key])
|
|
21
|
+
|
|
22
|
+
if is_success
|
|
23
|
+
@attributes.merge!(normalize_keys(response))
|
|
24
|
+
assign_attributes
|
|
25
|
+
DhanHQ.logger&.info("#{context} successfully: #{identifier_from(response, success_key) || id}") if context
|
|
26
|
+
true
|
|
27
|
+
else
|
|
28
|
+
error_msg = is_hash ? response[:errorMessage] || response[:message] || "Unknown error" : "Invalid response format"
|
|
29
|
+
DhanHQ.logger&.error("#{context} failed: #{error_msg}") if context
|
|
30
|
+
instance_variable_set(error_target_variable, response) if is_hash
|
|
31
|
+
false
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def error_target_variable
|
|
36
|
+
:@errors
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Safely fetches an ID or response identifier
|
|
40
|
+
def identifier_from(response, key)
|
|
41
|
+
response.is_a?(Hash) ? response[key] : nil
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -180,12 +180,6 @@ module DhanHQ
|
|
|
180
180
|
end
|
|
181
181
|
end
|
|
182
182
|
|
|
183
|
-
##
|
|
184
|
-
# ExpiredOptionsData objects are read-only, so no validation contract needed
|
|
185
|
-
def validation_contract
|
|
186
|
-
nil
|
|
187
|
-
end
|
|
188
|
-
|
|
189
183
|
##
|
|
190
184
|
# Gets call option data from the response.
|
|
191
185
|
#
|
|
@@ -238,9 +232,9 @@ module DhanHQ
|
|
|
238
232
|
# See {#call_data} or {#put_data} for structure details.
|
|
239
233
|
def data_for_type(option_type)
|
|
240
234
|
case option_type.upcase
|
|
241
|
-
when
|
|
235
|
+
when DhanHQ::Constants::OptionType::CALL
|
|
242
236
|
call_data
|
|
243
|
-
when
|
|
237
|
+
when DhanHQ::Constants::OptionType::PUT
|
|
244
238
|
put_data
|
|
245
239
|
end
|
|
246
240
|
end
|
|
@@ -466,7 +460,7 @@ module DhanHQ
|
|
|
466
460
|
#
|
|
467
461
|
# @return [Boolean] true if instrument type is "OPTIDX", false otherwise
|
|
468
462
|
def index_options?
|
|
469
|
-
instrument ==
|
|
463
|
+
instrument == DhanHQ::Constants::InstrumentType::OPTIDX
|
|
470
464
|
end
|
|
471
465
|
|
|
472
466
|
##
|
|
@@ -474,7 +468,7 @@ module DhanHQ
|
|
|
474
468
|
#
|
|
475
469
|
# @return [Boolean] true if instrument type is "OPTSTK", false otherwise
|
|
476
470
|
def stock_options?
|
|
477
|
-
instrument ==
|
|
471
|
+
instrument == DhanHQ::Constants::InstrumentType::OPTSTK
|
|
478
472
|
end
|
|
479
473
|
|
|
480
474
|
##
|
|
@@ -498,7 +492,7 @@ module DhanHQ
|
|
|
498
492
|
#
|
|
499
493
|
# @return [Boolean] true if drv_option_type is "CALL", false otherwise
|
|
500
494
|
def call_option?
|
|
501
|
-
drv_option_type ==
|
|
495
|
+
drv_option_type == DhanHQ::Constants::OptionType::CALL
|
|
502
496
|
end
|
|
503
497
|
|
|
504
498
|
##
|
|
@@ -506,7 +500,7 @@ module DhanHQ
|
|
|
506
500
|
#
|
|
507
501
|
# @return [Boolean] true if drv_option_type is "PUT", false otherwise
|
|
508
502
|
def put_option?
|
|
509
|
-
drv_option_type ==
|
|
503
|
+
drv_option_type == DhanHQ::Constants::OptionType::PUT
|
|
510
504
|
end
|
|
511
505
|
|
|
512
506
|
##
|
|
@@ -64,6 +64,8 @@ module DhanHQ
|
|
|
64
64
|
# orders.each { |order| puts order.order_status }
|
|
65
65
|
#
|
|
66
66
|
class ForeverOrder < BaseModel
|
|
67
|
+
include Concerns::ApiResponseHandler
|
|
68
|
+
|
|
67
69
|
attributes :dhan_client_id, :order_id, :correlation_id, :order_status,
|
|
68
70
|
:transaction_type, :exchange_segment, :product_type, :order_flag,
|
|
69
71
|
:order_type, :validity, :trading_symbol, :security_id, :quantity,
|
|
@@ -115,9 +117,7 @@ module DhanHQ
|
|
|
115
117
|
# end
|
|
116
118
|
def all
|
|
117
119
|
response = resource.all
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
response.map { |o| new(o, skip_validation: true) }
|
|
120
|
+
parse_collection_response(response)
|
|
121
121
|
end
|
|
122
122
|
|
|
123
123
|
##
|
|
@@ -290,8 +290,11 @@ module DhanHQ
|
|
|
290
290
|
def modify(new_params)
|
|
291
291
|
raise "Order ID is required to modify a forever order" unless order_id
|
|
292
292
|
|
|
293
|
+
DhanHQ.logger&.info("[DhanHQ::Models::ForeverOrder] Modifying order #{order_id}")
|
|
293
294
|
response = self.class.resource.update(order_id, new_params)
|
|
294
|
-
|
|
295
|
+
ctx = "[DhanHQ::Models::ForeverOrder] Modification"
|
|
296
|
+
success = handle_api_response(response, success_key: "orderId", context: ctx)
|
|
297
|
+
return self.class.find(order_id) if success
|
|
295
298
|
|
|
296
299
|
nil
|
|
297
300
|
end
|
|
@@ -317,7 +320,7 @@ module DhanHQ
|
|
|
317
320
|
raise "Order ID is required to cancel a forever order" unless order_id
|
|
318
321
|
|
|
319
322
|
response = self.class.resource.cancel(order_id)
|
|
320
|
-
response["orderStatus"] ==
|
|
323
|
+
response["orderStatus"] == DhanHQ::Constants::OrderStatus::CANCELLED
|
|
321
324
|
end
|
|
322
325
|
end
|
|
323
326
|
end
|