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
|
@@ -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
|
##
|
|
@@ -132,20 +132,32 @@ module DhanHQ
|
|
|
132
132
|
def deactivate
|
|
133
133
|
update("DEACTIVATE")
|
|
134
134
|
end
|
|
135
|
+
|
|
136
|
+
##
|
|
137
|
+
# Fetches the current kill switch status for your account.
|
|
138
|
+
#
|
|
139
|
+
# Checks whether the kill switch is currently active or inactive for
|
|
140
|
+
# the current trading day.
|
|
141
|
+
#
|
|
142
|
+
# @return [Hash{Symbol => String}] Response hash containing kill switch status.
|
|
143
|
+
# - **:dhan_client_id** [String] User-specific identification generated by Dhan
|
|
144
|
+
# - **:kill_switch_status** [String] Current status: "ACTIVATE" or "DEACTIVATE"
|
|
145
|
+
#
|
|
146
|
+
# @example Check if kill switch is active
|
|
147
|
+
# response = DhanHQ::Models::KillSwitch.status
|
|
148
|
+
# if response[:kill_switch_status] == "ACTIVATE"
|
|
149
|
+
# puts "Kill switch is active — trading disabled"
|
|
150
|
+
# else
|
|
151
|
+
# puts "Kill switch is inactive — trading enabled"
|
|
152
|
+
# end
|
|
153
|
+
#
|
|
154
|
+
def status
|
|
155
|
+
resource.status
|
|
156
|
+
end
|
|
135
157
|
end
|
|
136
158
|
|
|
137
159
|
##
|
|
138
160
|
# No explicit validation contract is required for kill switch updates.
|
|
139
|
-
#
|
|
140
|
-
# Kill switch operations are simple status updates that don't require complex validation.
|
|
141
|
-
# The API handles validation server-side.
|
|
142
|
-
#
|
|
143
|
-
# @return [nil] Always returns nil as no validation contract is needed
|
|
144
|
-
#
|
|
145
|
-
# @api private
|
|
146
|
-
def validation_contract
|
|
147
|
-
nil
|
|
148
|
-
end
|
|
149
161
|
end
|
|
150
162
|
end
|
|
151
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
|
##
|
|
@@ -139,6 +139,55 @@ module DhanHQ
|
|
|
139
139
|
response = resource.calculate(formatted_params)
|
|
140
140
|
new(response, skip_validation: true)
|
|
141
141
|
end
|
|
142
|
+
|
|
143
|
+
##
|
|
144
|
+
# Calculates margin requirements for multiple scripts in a single request.
|
|
145
|
+
#
|
|
146
|
+
# Provides combined margin calculation including hedge benefit across multiple
|
|
147
|
+
# instruments. Useful for portfolio-level margin analysis.
|
|
148
|
+
#
|
|
149
|
+
# @param params [Hash{Symbol => Object}] Request parameters
|
|
150
|
+
# @option params [Boolean] :include_position Whether to include existing positions
|
|
151
|
+
# @option params [Boolean] :include_order Whether to include existing orders
|
|
152
|
+
# @option params [String] :dhan_client_id User-specific identification
|
|
153
|
+
# @option params [Array<Hash>] :scrip_list Array of instrument margin params, each with:
|
|
154
|
+
# - :exchange_segment [String]
|
|
155
|
+
# - :transaction_type [String]
|
|
156
|
+
# - :quantity [Integer]
|
|
157
|
+
# - :product_type [String]
|
|
158
|
+
# - :security_id [String]
|
|
159
|
+
# - :price [Float]
|
|
160
|
+
# - :trigger_price [Float] (optional)
|
|
161
|
+
#
|
|
162
|
+
# @return [Hash{Symbol => String}] Response hash containing:
|
|
163
|
+
# - **:total_margin** [String] Total margin required
|
|
164
|
+
# - **:span_margin** [String] SPAN margin
|
|
165
|
+
# - **:exposure_margin** [String] Exposure margin
|
|
166
|
+
# - **:equity_margin** [String] Equity margin
|
|
167
|
+
# - **:fo_margin** [String] F&O margin
|
|
168
|
+
# - **:commodity_margin** [String] Commodity margin
|
|
169
|
+
# - **:currency** [String] Currency (e.g., "INR")
|
|
170
|
+
# - **:hedge_benefit** [String] Hedge benefit amount
|
|
171
|
+
#
|
|
172
|
+
# @example Calculate margin for multiple scripts
|
|
173
|
+
# result = DhanHQ::Models::Margin.calculate_multi(
|
|
174
|
+
# include_position: true,
|
|
175
|
+
# include_order: true,
|
|
176
|
+
# dhan_client_id: "1000000132",
|
|
177
|
+
# scrip_list: [
|
|
178
|
+
# { exchange_segment: "NSE_EQ", transaction_type: "BUY",
|
|
179
|
+
# quantity: 100, product_type: "CNC", security_id: "1333", price: 1428.0 },
|
|
180
|
+
# { exchange_segment: "NSE_FNO", transaction_type: "SELL",
|
|
181
|
+
# quantity: 50, product_type: "INTRADAY", security_id: "43492", price: 200.0 }
|
|
182
|
+
# ]
|
|
183
|
+
# )
|
|
184
|
+
# puts "Total margin: #{result[:total_margin]}"
|
|
185
|
+
# puts "Hedge benefit: #{result[:hedge_benefit]}"
|
|
186
|
+
#
|
|
187
|
+
def calculate_multi(params)
|
|
188
|
+
formatted_params = camelize_keys(params)
|
|
189
|
+
resource.calculate_multi(formatted_params)
|
|
190
|
+
end
|
|
142
191
|
end
|
|
143
192
|
|
|
144
193
|
##
|
data/lib/DhanHQ/models/order.rb
CHANGED
|
@@ -46,6 +46,8 @@ module DhanHQ
|
|
|
46
46
|
# puts "Pending orders: #{pending_orders.count}"
|
|
47
47
|
#
|
|
48
48
|
class Order < BaseModel
|
|
49
|
+
include Concerns::ApiResponseHandler
|
|
50
|
+
|
|
49
51
|
# Attributes eligible for modification requests.
|
|
50
52
|
MODIFIABLE_FIELDS = %i[
|
|
51
53
|
dhan_client_id
|
|
@@ -57,10 +59,10 @@ module DhanHQ
|
|
|
57
59
|
disclosed_quantity
|
|
58
60
|
validity
|
|
59
61
|
leg_name
|
|
62
|
+
bo_profit_value
|
|
63
|
+
bo_stop_loss_value
|
|
60
64
|
].freeze
|
|
61
65
|
|
|
62
|
-
attr_reader :order_id, :order_status
|
|
63
|
-
|
|
64
66
|
# Define attributes that are part of an order
|
|
65
67
|
attributes :dhan_client_id, :order_id, :correlation_id, :order_status,
|
|
66
68
|
:transaction_type, :exchange_segment, :product_type, :order_type,
|
|
@@ -168,9 +170,10 @@ module DhanHQ
|
|
|
168
170
|
#
|
|
169
171
|
def find(order_id)
|
|
170
172
|
response = resource.find(order_id)
|
|
171
|
-
|
|
173
|
+
is_array = response.is_a?(Array)
|
|
174
|
+
return nil unless response.is_a?(Hash) || (is_array && response.any?)
|
|
172
175
|
|
|
173
|
-
order_data =
|
|
176
|
+
order_data = is_array ? response.first : response
|
|
174
177
|
new(order_data, skip_validation: true)
|
|
175
178
|
end
|
|
176
179
|
|
|
@@ -286,15 +289,19 @@ module DhanHQ
|
|
|
286
289
|
# @raise [DhanHQ::ValidationError] If validation fails for any parameter
|
|
287
290
|
def place(params)
|
|
288
291
|
normalized_params = snake_case(params)
|
|
292
|
+
config = DhanHQ.configuration
|
|
289
293
|
# Auto-inject dhan_client_id from configuration if not provided
|
|
290
|
-
normalized_params[:dhan_client_id] ||=
|
|
294
|
+
normalized_params[:dhan_client_id] ||= config.client_id if config&.client_id
|
|
291
295
|
validate_params!(normalized_params, DhanHQ::Contracts::PlaceOrderContract)
|
|
292
296
|
|
|
293
297
|
response = resource.create(camelize_keys(normalized_params))
|
|
294
|
-
return nil unless response.is_a?(Hash)
|
|
298
|
+
return nil unless response.is_a?(Hash)
|
|
299
|
+
|
|
300
|
+
order_id = response["orderId"]
|
|
301
|
+
return nil unless order_id
|
|
295
302
|
|
|
296
303
|
# Fetch the complete order details
|
|
297
|
-
find(
|
|
304
|
+
find(order_id)
|
|
298
305
|
end
|
|
299
306
|
|
|
300
307
|
##
|
|
@@ -366,25 +373,12 @@ module DhanHQ
|
|
|
366
373
|
def modify(new_params)
|
|
367
374
|
raise "Order ID is required to modify an order" unless id
|
|
368
375
|
|
|
369
|
-
|
|
370
|
-
# This maintains backward compatibility - API will return appropriate error
|
|
371
|
-
if order_status && %w[TRADED CANCELLED EXPIRED CLOSED].include?(order_status)
|
|
372
|
-
DhanHQ.logger&.warn("[DhanHQ::Models::Order] Attempting to modify order #{id} in #{order_status} state - API will reject")
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
base_payload = attributes.merge(new_params)
|
|
376
|
-
normalized_payload = snake_case(base_payload).merge(order_id: id)
|
|
377
|
-
filtered_payload = normalized_payload.each_with_object({}) do |(key, value), memo|
|
|
378
|
-
symbolized_key = key.respond_to?(:to_sym) ? key.to_sym : key
|
|
379
|
-
memo[symbolized_key] = value if MODIFIABLE_FIELDS.include?(symbolized_key)
|
|
380
|
-
end
|
|
381
|
-
filtered_payload[:order_id] ||= id
|
|
382
|
-
filtered_payload[:dhan_client_id] ||= attributes[:dhan_client_id] || DhanHQ.configuration&.client_id
|
|
376
|
+
warn_invalid_state if order_status_invalid_for_modification?
|
|
383
377
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
validate_params!(formatted_payload, DhanHQ::Contracts::ModifyOrderContract)
|
|
378
|
+
filtered_payload = prepare_modify_payload(new_params)
|
|
379
|
+
validate_params!(filtered_payload, DhanHQ::Contracts::ModifyOrderContract)
|
|
387
380
|
|
|
381
|
+
formatted_payload = camelize_keys(filtered_payload)
|
|
388
382
|
response = self.class.resource.update(id, formatted_payload)
|
|
389
383
|
response = response.with_indifferent_access if response.respond_to?(:with_indifferent_access)
|
|
390
384
|
|
|
@@ -415,8 +409,15 @@ module DhanHQ
|
|
|
415
409
|
def cancel
|
|
416
410
|
raise "Order ID is required to cancel an order" unless id
|
|
417
411
|
|
|
412
|
+
DhanHQ.logger&.info("[DhanHQ::Models::Order] Cancelling order #{id}")
|
|
418
413
|
response = self.class.resource.cancel(id)
|
|
419
|
-
response["orderStatus"] ==
|
|
414
|
+
if response["orderStatus"] == DhanHQ::Constants::OrderStatus::CANCELLED
|
|
415
|
+
DhanHQ.logger&.info("[DhanHQ::Models::Order] Order #{id} cancelled successfully")
|
|
416
|
+
true
|
|
417
|
+
else
|
|
418
|
+
DhanHQ.logger&.error("[DhanHQ::Models::Order] Cancel failed for order #{id}: #{response.inspect}")
|
|
419
|
+
false
|
|
420
|
+
end
|
|
420
421
|
end
|
|
421
422
|
|
|
422
423
|
##
|
|
@@ -453,6 +454,44 @@ module DhanHQ
|
|
|
453
454
|
order_id.nil? || order_id.to_s.empty?
|
|
454
455
|
end
|
|
455
456
|
|
|
457
|
+
def destroy
|
|
458
|
+
return false if new_record?
|
|
459
|
+
|
|
460
|
+
DhanHQ.logger&.info("[DhanHQ::Models::Order] Destroying order #{id}")
|
|
461
|
+
response = self.class.resource.delete(id)
|
|
462
|
+
if success_response?(response) && response["orderStatus"] == DhanHQ::Constants::OrderStatus::CANCELLED
|
|
463
|
+
@attributes[:order_status] = DhanHQ::Constants::OrderStatus::CANCELLED
|
|
464
|
+
DhanHQ.logger&.info("[DhanHQ::Models::Order] Order #{id} destroyed successfully")
|
|
465
|
+
true
|
|
466
|
+
else
|
|
467
|
+
DhanHQ.logger&.error("[DhanHQ::Models::Order] Destroy failed for order #{id}")
|
|
468
|
+
false
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
alias delete destroy
|
|
472
|
+
|
|
473
|
+
def slice_order(params)
|
|
474
|
+
raise "Order ID is required to slice an order" unless id
|
|
475
|
+
|
|
476
|
+
normalized = snake_case(params)
|
|
477
|
+
normalized[:dhan_client_id] ||= DhanHQ.configuration&.client_id
|
|
478
|
+
base_payload = normalized.merge(order_id: id)
|
|
479
|
+
formatted_payload = camelize_keys(base_payload)
|
|
480
|
+
|
|
481
|
+
validate_params!(formatted_payload, DhanHQ::Contracts::SliceOrderContract)
|
|
482
|
+
|
|
483
|
+
self.class.resource.slicing(formatted_payload)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def validation_contract
|
|
487
|
+
order_id_val = @attributes[:order_id] || @attributes[:orderId]
|
|
488
|
+
if order_id_val.nil? || order_id_val.to_s.empty?
|
|
489
|
+
DhanHQ::Contracts::PlaceOrderContract.new
|
|
490
|
+
else
|
|
491
|
+
DhanHQ::Contracts::ModifyOrderContract.new
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
|
|
456
495
|
##
|
|
457
496
|
# Returns the order ID used for resource calls.
|
|
458
497
|
#
|
|
@@ -495,40 +534,53 @@ module DhanHQ
|
|
|
495
534
|
return false unless valid?
|
|
496
535
|
|
|
497
536
|
if new_record?
|
|
498
|
-
|
|
499
|
-
DhanHQ.logger&.info("[DhanHQ::Models::Order] Placing order: #{attributes.slice(:transaction_type,
|
|
500
|
-
:exchange_segment, :security_id, :quantity, :price).inspect}")
|
|
501
|
-
response = self.class.resource.create(to_request_params)
|
|
502
|
-
if success_response?(response) && response["orderId"]
|
|
503
|
-
@attributes.merge!(normalize_keys(response))
|
|
504
|
-
assign_attributes
|
|
505
|
-
DhanHQ.logger&.info("[DhanHQ::Models::Order] Order placed successfully: #{response["orderId"]}")
|
|
506
|
-
true
|
|
507
|
-
else
|
|
508
|
-
error_msg = response.is_a?(Hash) ? response[:errorMessage] || response[:message] || "Unknown error" : "Invalid response format"
|
|
509
|
-
DhanHQ.logger&.error("[DhanHQ::Models::Order] Order placement failed: #{error_msg}")
|
|
510
|
-
@errors = response if response.is_a?(Hash)
|
|
511
|
-
false
|
|
512
|
-
end
|
|
537
|
+
save_new_order
|
|
513
538
|
else
|
|
514
|
-
|
|
515
|
-
DhanHQ.logger&.info("[DhanHQ::Models::Order] Modifying order #{id}: #{attributes.slice(:price, :quantity,
|
|
516
|
-
:order_type).inspect}")
|
|
517
|
-
response = self.class.resource.update(id, to_request_params)
|
|
518
|
-
if success_response?(response) && response["orderStatus"]
|
|
519
|
-
@attributes.merge!(normalize_keys(response))
|
|
520
|
-
assign_attributes
|
|
521
|
-
DhanHQ.logger&.info("[DhanHQ::Models::Order] Order modified successfully: #{id}")
|
|
522
|
-
true
|
|
523
|
-
else
|
|
524
|
-
error_msg = response.is_a?(Hash) ? response[:errorMessage] || response[:message] || "Unknown error" : "Invalid response format"
|
|
525
|
-
DhanHQ.logger&.error("[DhanHQ::Models::Order] Order modification failed for #{id}: #{error_msg}")
|
|
526
|
-
@errors = response if response.is_a?(Hash)
|
|
527
|
-
false
|
|
528
|
-
end
|
|
539
|
+
modify_existing_order
|
|
529
540
|
end
|
|
530
541
|
end
|
|
531
542
|
|
|
543
|
+
private
|
|
544
|
+
|
|
545
|
+
def save_new_order
|
|
546
|
+
DhanHQ.logger&.info("[DhanHQ::Models::Order] Placing order: #{attributes.slice(:transaction_type, :exchange_segment, :security_id, :quantity, :price).inspect}")
|
|
547
|
+
response = self.class.resource.create(to_request_params)
|
|
548
|
+
handle_api_response(response, success_key: "orderId", context: "[DhanHQ::Models::Order] Order placement")
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def modify_existing_order
|
|
552
|
+
DhanHQ.logger&.info("[DhanHQ::Models::Order] Modifying order #{id}: #{attributes.slice(:price, :quantity, :order_type).inspect}")
|
|
553
|
+
response = self.class.resource.update(id, to_request_params)
|
|
554
|
+
handle_api_response(response, success_key: "orderStatus", context: "[DhanHQ::Models::Order] Order modification")
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def order_status_invalid_for_modification?
|
|
558
|
+
order_status && %w[TRADED CANCELLED EXPIRED CLOSED].include?(order_status)
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def warn_invalid_state
|
|
562
|
+
DhanHQ.logger&.warn("[DhanHQ::Models::Order] Attempting to modify order #{id} in #{order_status} state - API will reject")
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def prepare_modify_payload(new_params)
|
|
566
|
+
base_payload = attributes.merge(new_params)
|
|
567
|
+
normalized_payload = snake_case(base_payload).merge(order_id: id)
|
|
568
|
+
|
|
569
|
+
filtered_payload = normalized_payload.each_with_object({}) do |(key, value), memo|
|
|
570
|
+
symbolized_key = key.respond_to?(:to_sym) ? key.to_sym : key
|
|
571
|
+
memo[symbolized_key] = value if MODIFIABLE_FIELDS.include?(symbolized_key)
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
filtered_payload[:order_id] ||= id
|
|
575
|
+
filtered_payload[:dhan_client_id] ||= attributes[:dhan_client_id] || DhanHQ.configuration&.client_id
|
|
576
|
+
|
|
577
|
+
# Don't send trigger_price when it's 0 for non–stop-loss orders (API default; avoids validation noise).
|
|
578
|
+
order_type = filtered_payload[:order_type].to_s
|
|
579
|
+
filtered_payload.delete(:trigger_price) if !%w[STOP_LOSS STOP_LOSS_MARKET].include?(order_type) && filtered_payload[:trigger_price].to_f.zero?
|
|
580
|
+
|
|
581
|
+
filtered_payload.compact
|
|
582
|
+
end
|
|
583
|
+
|
|
532
584
|
##
|
|
533
585
|
# Cancels (destroys) the order.
|
|
534
586
|
#
|
|
@@ -545,77 +597,6 @@ module DhanHQ
|
|
|
545
597
|
# end
|
|
546
598
|
#
|
|
547
599
|
# @note This method does nothing for new (unsaved) orders.
|
|
548
|
-
def destroy
|
|
549
|
-
return false if new_record?
|
|
550
|
-
|
|
551
|
-
response = self.class.resource.delete(id)
|
|
552
|
-
if success_response?(response) && response["orderStatus"] == "CANCELLED"
|
|
553
|
-
@attributes[:order_status] = "CANCELLED"
|
|
554
|
-
true
|
|
555
|
-
else
|
|
556
|
-
false
|
|
557
|
-
end
|
|
558
|
-
end
|
|
559
|
-
alias delete destroy
|
|
560
|
-
|
|
561
|
-
##
|
|
562
|
-
# Slices an order into multiple legs to place orders over freeze limit quantity.
|
|
563
|
-
#
|
|
564
|
-
# This API helps you slice your order request into multiple orders to allow you
|
|
565
|
-
# to place over freeze limit quantity for F&O instruments. Returns an array of
|
|
566
|
-
# orders created from the slice operation.
|
|
567
|
-
#
|
|
568
|
-
# @param params [Hash{Symbol => String, Integer, Float, Boolean}] Order parameters for slicing.
|
|
569
|
-
# Same parameters as {place}, but quantity can exceed freeze limits as it will be
|
|
570
|
-
# automatically split into multiple orders.
|
|
571
|
-
#
|
|
572
|
-
# @return [Array<Hash>] Array of order objects created from the slice operation.
|
|
573
|
-
# Each hash contains:
|
|
574
|
-
# - **:order_id** [String] Order-specific identification generated by Dhan
|
|
575
|
-
# - **:order_status** [String] Order status. Valid values: "TRANSIT", "PENDING",
|
|
576
|
-
# "REJECTED", "CANCELLED", "TRADED", "EXPIRED", "CONFIRM"
|
|
577
|
-
#
|
|
578
|
-
# @example Slice a large order
|
|
579
|
-
# order = DhanHQ::Models::Order.find("112111182045")
|
|
580
|
-
# sliced_orders = order.slice_order(
|
|
581
|
-
# dhan_client_id: "1000000003",
|
|
582
|
-
# transaction_type: "BUY",
|
|
583
|
-
# exchange_segment: "NSE_FNO",
|
|
584
|
-
# product_type: "INTRADAY",
|
|
585
|
-
# order_type: "MARKET",
|
|
586
|
-
# validity: "DAY",
|
|
587
|
-
# security_id: "49081",
|
|
588
|
-
# quantity: 50000 # Will be split into multiple orders
|
|
589
|
-
# )
|
|
590
|
-
# puts "Created #{sliced_orders.count} orders"
|
|
591
|
-
#
|
|
592
|
-
# @raise [RuntimeError] If order ID is missing
|
|
593
|
-
# @raise [DhanHQ::ValidationError] If validation fails for any parameter
|
|
594
|
-
def slice_order(params)
|
|
595
|
-
raise "Order ID is required to slice an order" unless id
|
|
596
|
-
|
|
597
|
-
base_payload = params.merge(order_id: id)
|
|
598
|
-
formatted_payload = camelize_keys(base_payload)
|
|
599
|
-
|
|
600
|
-
validate_params!(formatted_payload, DhanHQ::Contracts::SliceOrderContract)
|
|
601
|
-
|
|
602
|
-
self.class.resource.slicing(formatted_payload)
|
|
603
|
-
end
|
|
604
|
-
|
|
605
|
-
##
|
|
606
|
-
# Returns the appropriate validation contract based on order state.
|
|
607
|
-
#
|
|
608
|
-
# For new records, returns PlaceOrderContract. For existing records, returns
|
|
609
|
-
# ModifyOrderContract. This allows the same validation logic to be used for
|
|
610
|
-
# both creating and modifying orders.
|
|
611
|
-
#
|
|
612
|
-
# @return [DhanHQ::Contracts::PlaceOrderContract, DhanHQ::Contracts::ModifyOrderContract]
|
|
613
|
-
# The appropriate validation contract based on whether this is a new or existing order
|
|
614
|
-
#
|
|
615
|
-
# @api private
|
|
616
|
-
def validation_contract
|
|
617
|
-
new_record? ? DhanHQ::Contracts::PlaceOrderContract.new : DhanHQ::Contracts::ModifyOrderContract.new
|
|
618
|
-
end
|
|
619
600
|
end
|
|
620
601
|
end
|
|
621
602
|
end
|
|
@@ -31,12 +31,6 @@ module DhanHQ
|
|
|
31
31
|
new(data, skip_validation: true)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
##
|
|
35
|
-
# OrderUpdate objects are read-only, so no validation contract needed
|
|
36
|
-
def validation_contract
|
|
37
|
-
nil
|
|
38
|
-
end
|
|
39
|
-
|
|
40
34
|
##
|
|
41
35
|
# Helper methods for transaction type
|
|
42
36
|
def buy?
|
|
@@ -94,33 +88,33 @@ module DhanHQ
|
|
|
94
88
|
##
|
|
95
89
|
# Helper methods for order status
|
|
96
90
|
def transit?
|
|
97
|
-
status ==
|
|
91
|
+
status == DhanHQ::Constants::OrderStatus::TRANSIT
|
|
98
92
|
end
|
|
99
93
|
|
|
100
94
|
def pending?
|
|
101
|
-
status ==
|
|
95
|
+
status == DhanHQ::Constants::OrderStatus::PENDING
|
|
102
96
|
end
|
|
103
97
|
|
|
104
98
|
def rejected?
|
|
105
|
-
status ==
|
|
99
|
+
status == DhanHQ::Constants::OrderStatus::REJECTED
|
|
106
100
|
end
|
|
107
101
|
|
|
108
102
|
def cancelled?
|
|
109
|
-
status ==
|
|
103
|
+
status == DhanHQ::Constants::OrderStatus::CANCELLED
|
|
110
104
|
end
|
|
111
105
|
|
|
112
106
|
def traded?
|
|
113
|
-
status ==
|
|
107
|
+
status == DhanHQ::Constants::OrderStatus::TRADED
|
|
114
108
|
end
|
|
115
109
|
|
|
116
110
|
def expired?
|
|
117
|
-
status ==
|
|
111
|
+
status == DhanHQ::Constants::OrderStatus::EXPIRED
|
|
118
112
|
end
|
|
119
113
|
|
|
120
114
|
##
|
|
121
115
|
# Helper methods for instrument type
|
|
122
116
|
def equity?
|
|
123
|
-
instrument ==
|
|
117
|
+
instrument == DhanHQ::Constants::InstrumentType::EQUITY
|
|
124
118
|
end
|
|
125
119
|
|
|
126
120
|
def derivative?
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DhanHQ
|
|
4
|
+
module Models
|
|
5
|
+
##
|
|
6
|
+
# Model for managing P&L-based automatic position exit.
|
|
7
|
+
#
|
|
8
|
+
# The P&L Based Exit API allows users to configure automatic exit rules based on
|
|
9
|
+
# cumulative profit or loss thresholds. When the defined limits are breached, all
|
|
10
|
+
# applicable positions are exited automatically.
|
|
11
|
+
#
|
|
12
|
+
# @note The configured P&L based exit remains active for the current day and is
|
|
13
|
+
# reset at the end of the trading session.
|
|
14
|
+
#
|
|
15
|
+
# @example Configure P&L-based exit
|
|
16
|
+
# response = DhanHQ::Models::PnlExit.configure(
|
|
17
|
+
# profit_value: 1500.0,
|
|
18
|
+
# loss_value: 500.0,
|
|
19
|
+
# product_type: ["INTRADAY", "DELIVERY"],
|
|
20
|
+
# enable_kill_switch: true
|
|
21
|
+
# )
|
|
22
|
+
# puts response[:pnl_exit_status] # => "ACTIVE"
|
|
23
|
+
#
|
|
24
|
+
# @example Check current P&L exit configuration
|
|
25
|
+
# config = DhanHQ::Models::PnlExit.status
|
|
26
|
+
# puts "Status: #{config.pnl_exit_status}"
|
|
27
|
+
# puts "Profit threshold: ₹#{config.profit}"
|
|
28
|
+
# puts "Loss threshold: ₹#{config.loss}"
|
|
29
|
+
#
|
|
30
|
+
# @example Stop P&L-based exit
|
|
31
|
+
# response = DhanHQ::Models::PnlExit.stop
|
|
32
|
+
# puts response[:pnl_exit_status] # => "DISABLED"
|
|
33
|
+
#
|
|
34
|
+
class PnlExit < BaseModel
|
|
35
|
+
HTTP_PATH = "/v2/pnlExit"
|
|
36
|
+
|
|
37
|
+
attributes :pnl_exit_status, :profit, :loss, :segments, :enable_kill_switch
|
|
38
|
+
|
|
39
|
+
class << self
|
|
40
|
+
##
|
|
41
|
+
# Provides a shared instance of the PnlExit resource.
|
|
42
|
+
#
|
|
43
|
+
# @return [DhanHQ::Resources::PnlExit] The PnlExit resource client instance
|
|
44
|
+
def resource
|
|
45
|
+
@resource ||= DhanHQ::Resources::PnlExit.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
##
|
|
49
|
+
# Configure automatic P&L-based position exit.
|
|
50
|
+
#
|
|
51
|
+
# When the defined profit or loss thresholds are breached during the trading day,
|
|
52
|
+
# all applicable positions are exited automatically.
|
|
53
|
+
#
|
|
54
|
+
# @param profit_value [Float] Profit threshold that triggers exit (e.g., 1500.0)
|
|
55
|
+
# @param loss_value [Float] Loss threshold that triggers exit (e.g., 500.0)
|
|
56
|
+
# @param product_type [Array<String>] Product types to apply. e.g., ["INTRADAY", "DELIVERY"]
|
|
57
|
+
# @param enable_kill_switch [Boolean] Whether to activate kill switch after exit
|
|
58
|
+
#
|
|
59
|
+
# @return [Hash{Symbol => String}] Response hash containing:
|
|
60
|
+
# - **:pnl_exit_status** [String] "ACTIVE" on success
|
|
61
|
+
# - **:message** [String] Confirmation message
|
|
62
|
+
#
|
|
63
|
+
# @example Configure with kill switch
|
|
64
|
+
# DhanHQ::Models::PnlExit.configure(
|
|
65
|
+
# profit_value: 2000.0,
|
|
66
|
+
# loss_value: 1000.0,
|
|
67
|
+
# product_type: ["INTRADAY"],
|
|
68
|
+
# enable_kill_switch: true
|
|
69
|
+
# )
|
|
70
|
+
#
|
|
71
|
+
def configure(profit_value:, loss_value:, product_type:, enable_kill_switch: false)
|
|
72
|
+
params = {
|
|
73
|
+
profitValue: profit_value.to_s,
|
|
74
|
+
lossValue: loss_value.to_s,
|
|
75
|
+
productType: product_type,
|
|
76
|
+
enableKillSwitch: enable_kill_switch
|
|
77
|
+
}
|
|
78
|
+
params[:dhanClientId] = DhanHQ.configuration.client_id if DhanHQ.configuration&.client_id.to_s != ""
|
|
79
|
+
resource.configure(params)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
##
|
|
83
|
+
# Stop/disable the active P&L-based exit configuration.
|
|
84
|
+
#
|
|
85
|
+
# @return [Hash{Symbol => String}] Response hash containing:
|
|
86
|
+
# - **:pnl_exit_status** [String] "DISABLED"
|
|
87
|
+
# - **:message** [String] Confirmation message
|
|
88
|
+
#
|
|
89
|
+
# @example Disable P&L exit
|
|
90
|
+
# response = DhanHQ::Models::PnlExit.stop
|
|
91
|
+
# puts response[:pnl_exit_status] # => "DISABLED"
|
|
92
|
+
#
|
|
93
|
+
def stop
|
|
94
|
+
resource.stop
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
##
|
|
98
|
+
# Fetch the currently active P&L-based exit configuration.
|
|
99
|
+
#
|
|
100
|
+
# @return [PnlExit] PnlExit object with current configuration.
|
|
101
|
+
# - **:pnl_exit_status** [String] "ACTIVE" or "DISABLED"
|
|
102
|
+
# - **:profit** [String] Configured profit threshold
|
|
103
|
+
# - **:loss** [String] Configured loss threshold
|
|
104
|
+
# - **:segments** [Array<String>] Active product types
|
|
105
|
+
# - **:enable_kill_switch** [Boolean] Whether kill switch is enabled
|
|
106
|
+
#
|
|
107
|
+
# @example Check configuration
|
|
108
|
+
# config = DhanHQ::Models::PnlExit.status
|
|
109
|
+
# if config.pnl_exit_status == "ACTIVE"
|
|
110
|
+
# puts "P&L exit active: profit=₹#{config.profit}, loss=₹#{config.loss}"
|
|
111
|
+
# end
|
|
112
|
+
#
|
|
113
|
+
def status
|
|
114
|
+
response = resource.status
|
|
115
|
+
return nil unless response.is_a?(Hash)
|
|
116
|
+
|
|
117
|
+
new(response, skip_validation: true)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|