DhanHQ 2.5.0 → 2.6.1
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 +78 -6
- data/GUIDE.md +57 -39
- data/README.md +24 -23
- data/docs/API_DOCS_GAPS.md +128 -0
- data/docs/API_VERIFICATION.md +10 -11
- data/docs/ARCHIVE_README.md +16 -16
- data/docs/AUTHENTICATION.md +1 -1
- 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/{technical_analysis.md → TECHNICAL_ANALYSIS.md} +3 -3
- data/docs/TESTING_GUIDE.md +84 -82
- data/docs/{websocket_integration.md → WEBSOCKET_INTEGRATION.md} +19 -19
- data/docs/WEBSOCKET_PROTOCOL.md +2 -2
- 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 +7 -11
- data/lib/DhanHQ/models/concerns/api_response_handler.rb +46 -0
- data/lib/DhanHQ/models/edis.rb +0 -9
- 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 +1 -11
- data/lib/DhanHQ/models/margin.rb +2 -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 +1 -9
- data/lib/DhanHQ/models/position.rb +1 -1
- data/lib/DhanHQ/models/postback.rb +4 -13
- 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 +9 -7
- data/lib/DhanHQ/resources/orders.rb +41 -41
- 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 +20 -23
- 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)
|
|
@@ -76,21 +76,17 @@ module DhanHQ
|
|
|
76
76
|
alert_id&.to_s
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
-
def save
|
|
79
|
+
def save
|
|
80
80
|
return false unless valid?
|
|
81
81
|
|
|
82
82
|
payload = to_request_params
|
|
83
83
|
response = if new_record?
|
|
84
|
-
self.class.resource.create(
|
|
84
|
+
self.class.resource.create(payload)
|
|
85
85
|
else
|
|
86
|
-
self.class.resource.update(id,
|
|
86
|
+
self.class.resource.update(id, payload)
|
|
87
87
|
end
|
|
88
|
-
return false if new_record? && !(response.is_a?(Hash) && response["alertId"])
|
|
89
|
-
return false if !new_record? && !success_response?(response)
|
|
90
88
|
|
|
91
|
-
|
|
92
|
-
assign_attributes
|
|
93
|
-
true
|
|
89
|
+
handle_api_response(response, success_key: new_record? ? "alertId" : nil)
|
|
94
90
|
end
|
|
95
91
|
|
|
96
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
|
data/lib/DhanHQ/models/edis.rb
CHANGED
|
@@ -96,15 +96,6 @@ module DhanHQ
|
|
|
96
96
|
resource.inquire(isin)
|
|
97
97
|
end
|
|
98
98
|
end
|
|
99
|
-
|
|
100
|
-
##
|
|
101
|
-
# No validation contract needed — EDIS operations are simple API calls.
|
|
102
|
-
#
|
|
103
|
-
# @return [nil]
|
|
104
|
-
# @api private
|
|
105
|
-
def validation_contract
|
|
106
|
-
nil
|
|
107
|
-
end
|
|
108
99
|
end
|
|
109
100
|
end
|
|
110
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
|
|
@@ -181,14 +181,6 @@ module DhanHQ
|
|
|
181
181
|
resource.intraday(params)
|
|
182
182
|
end
|
|
183
183
|
end
|
|
184
|
-
|
|
185
|
-
##
|
|
186
|
-
# HistoricalData objects are read-only, so no validation contract is applied.
|
|
187
|
-
#
|
|
188
|
-
# @return [nil] No validation contract needed for read-only data
|
|
189
|
-
def validation_contract
|
|
190
|
-
nil
|
|
191
|
-
end
|
|
192
184
|
end
|
|
193
185
|
end
|
|
194
186
|
end
|
|
@@ -69,7 +69,7 @@ module DhanHQ
|
|
|
69
69
|
|
|
70
70
|
instruments.find do |instrument|
|
|
71
71
|
# For equity instruments, prefer underlying_symbol over symbol_name
|
|
72
|
-
instrument_symbol = if instrument.instrument ==
|
|
72
|
+
instrument_symbol = if instrument.instrument == DhanHQ::Constants::InstrumentType::EQUITY && instrument.underlying_symbol
|
|
73
73
|
case_sensitive ? instrument.underlying_symbol : instrument.underlying_symbol.upcase
|
|
74
74
|
else
|
|
75
75
|
case_sensitive ? instrument.symbol_name : instrument.symbol_name.upcase
|
|
@@ -155,12 +155,6 @@ module DhanHQ
|
|
|
155
155
|
}
|
|
156
156
|
end
|
|
157
157
|
end
|
|
158
|
-
|
|
159
|
-
private
|
|
160
|
-
|
|
161
|
-
def validation_contract
|
|
162
|
-
nil
|
|
163
|
-
end
|
|
164
158
|
end
|
|
165
159
|
end
|
|
166
160
|
end
|
|
@@ -161,14 +161,14 @@ module DhanHQ
|
|
|
161
161
|
return seg if %w[IDX_I NSE_FNO BSE_FNO MCX_FO].include?(seg)
|
|
162
162
|
|
|
163
163
|
# Index detection by instrument kind or segment
|
|
164
|
-
return
|
|
164
|
+
return DhanHQ::Constants::ExchangeSegment::IDX_I if ins == DhanHQ::Constants::InstrumentType::INDEX || seg == DhanHQ::Constants::ExchangeSegment::IDX_I
|
|
165
165
|
|
|
166
166
|
# Map equities/stock-related segments to respective FNO
|
|
167
|
-
return
|
|
168
|
-
return
|
|
167
|
+
return DhanHQ::Constants::ExchangeSegment::NSE_FNO if seg.start_with?("NSE")
|
|
168
|
+
return DhanHQ::Constants::ExchangeSegment::BSE_FNO if seg.start_with?("BSE")
|
|
169
169
|
|
|
170
170
|
# Fallback to IDX_I to avoid contract rejection
|
|
171
|
-
|
|
171
|
+
DhanHQ::Constants::ExchangeSegment::IDX_I
|
|
172
172
|
end
|
|
173
173
|
end
|
|
174
174
|
end
|
|
@@ -68,7 +68,7 @@ module DhanHQ
|
|
|
68
68
|
# end
|
|
69
69
|
#
|
|
70
70
|
def update(status)
|
|
71
|
-
resource.update(
|
|
71
|
+
resource.update(status)
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
##
|
|
@@ -158,16 +158,6 @@ module DhanHQ
|
|
|
158
158
|
|
|
159
159
|
##
|
|
160
160
|
# No explicit validation contract is required for kill switch updates.
|
|
161
|
-
#
|
|
162
|
-
# Kill switch operations are simple status updates that don't require complex validation.
|
|
163
|
-
# The API handles validation server-side.
|
|
164
|
-
#
|
|
165
|
-
# @return [nil] Always returns nil as no validation contract is needed
|
|
166
|
-
#
|
|
167
|
-
# @api private
|
|
168
|
-
def validation_contract
|
|
169
|
-
nil
|
|
170
|
-
end
|
|
171
161
|
end
|
|
172
162
|
end
|
|
173
163
|
end
|
data/lib/DhanHQ/models/margin.rb
CHANGED
|
@@ -47,8 +47,8 @@ module DhanHQ
|
|
|
47
47
|
# Base path used to invoke the calculator.
|
|
48
48
|
HTTP_PATH = "/v2/margincalculator"
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
attributes :total_margin, :span_margin, :exposure_margin, :available_balance,
|
|
51
|
+
:variable_margin, :insufficient_balance, :brokerage, :leverage
|
|
52
52
|
|
|
53
53
|
class << self
|
|
54
54
|
##
|