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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -1
  3. data/CHANGELOG.md +103 -7
  4. data/GUIDE.md +57 -39
  5. data/README.md +198 -755
  6. data/docs/API_DOCS_GAPS.md +128 -0
  7. data/docs/API_VERIFICATION.md +10 -11
  8. data/{README1.md → docs/ARCHIVE_README.md} +16 -16
  9. data/docs/AUTHENTICATION.md +72 -10
  10. data/docs/CONFIGURATION.md +109 -0
  11. data/docs/CONSTANTS_REFERENCE.md +477 -0
  12. data/docs/DATA_API_PARAMETERS.md +7 -7
  13. data/docs/{rails_websocket_integration.md → RAILS_WEBSOCKET_INTEGRATION.md} +10 -10
  14. data/docs/{standalone_ruby_websocket_integration.md → STANDALONE_RUBY_WEBSOCKET_INTEGRATION.md} +32 -32
  15. data/docs/SUPER_ORDERS.md +284 -0
  16. data/docs/{technical_analysis.md → TECHNICAL_ANALYSIS.md} +3 -3
  17. data/docs/TESTING_GUIDE.md +84 -82
  18. data/docs/TROUBLESHOOTING.md +117 -0
  19. data/docs/{websocket_integration.md → WEBSOCKET_INTEGRATION.md} +19 -19
  20. data/docs/WEBSOCKET_PROTOCOL.md +154 -0
  21. data/lib/DhanHQ/constants.rb +456 -151
  22. data/lib/DhanHQ/contracts/alert_order_contract.rb +37 -10
  23. data/lib/DhanHQ/contracts/base_contract.rb +22 -0
  24. data/lib/DhanHQ/contracts/edis_contract.rb +25 -0
  25. data/lib/DhanHQ/contracts/margin_calculator_contract.rb +27 -4
  26. data/lib/DhanHQ/contracts/modify_order_contract.rb +65 -12
  27. data/lib/DhanHQ/contracts/multi_scrip_margin_calc_request_contract.rb +23 -0
  28. data/lib/DhanHQ/contracts/order_contract.rb +171 -39
  29. data/lib/DhanHQ/contracts/place_order_contract.rb +14 -141
  30. data/lib/DhanHQ/contracts/pnl_based_exit_contract.rb +20 -0
  31. data/lib/DhanHQ/contracts/position_conversion_contract.rb +15 -3
  32. data/lib/DhanHQ/contracts/slice_order_contract.rb +10 -1
  33. data/lib/DhanHQ/contracts/user_ip_contract.rb +14 -0
  34. data/lib/DhanHQ/core/base_model.rb +13 -4
  35. data/lib/DhanHQ/helpers/response_helper.rb +2 -2
  36. data/lib/DhanHQ/helpers/validation_helper.rb +1 -1
  37. data/lib/DhanHQ/models/alert_order.rb +29 -11
  38. data/lib/DhanHQ/models/concerns/api_response_handler.rb +46 -0
  39. data/lib/DhanHQ/models/edis.rb +101 -0
  40. data/lib/DhanHQ/models/expired_options_data.rb +6 -12
  41. data/lib/DhanHQ/models/forever_order.rb +8 -5
  42. data/lib/DhanHQ/models/historical_data.rb +0 -8
  43. data/lib/DhanHQ/models/instrument.rb +1 -7
  44. data/lib/DhanHQ/models/instrument_helpers.rb +4 -4
  45. data/lib/DhanHQ/models/kill_switch.rb +23 -11
  46. data/lib/DhanHQ/models/margin.rb +51 -2
  47. data/lib/DhanHQ/models/order.rb +107 -126
  48. data/lib/DhanHQ/models/order_update.rb +7 -13
  49. data/lib/DhanHQ/models/pnl_exit.rb +122 -0
  50. data/lib/DhanHQ/models/position.rb +23 -1
  51. data/lib/DhanHQ/models/postback.rb +114 -0
  52. data/lib/DhanHQ/models/profile.rb +0 -10
  53. data/lib/DhanHQ/models/super_order.rb +13 -3
  54. data/lib/DhanHQ/models/trade.rb +11 -23
  55. data/lib/DhanHQ/resources/ip_setup.rb +16 -5
  56. data/lib/DhanHQ/resources/kill_switch.rb +17 -7
  57. data/lib/DhanHQ/resources/margin_calculator.rb +9 -0
  58. data/lib/DhanHQ/resources/orders.rb +41 -41
  59. data/lib/DhanHQ/resources/pnl_exit.rb +37 -0
  60. data/lib/DhanHQ/resources/positions.rb +8 -0
  61. data/lib/DhanHQ/version.rb +1 -1
  62. data/lib/DhanHQ/ws/cmd_bus.rb +1 -1
  63. data/lib/DhanHQ/ws/orders/client.rb +6 -6
  64. data/lib/DhanHQ/ws/singleton_lock.rb +2 -1
  65. data/lib/dhanhq/analysis/options_buying_advisor.rb +2 -2
  66. data/lib/rubocop/cop/dhanhq/use_constants.rb +171 -0
  67. metadata +29 -24
  68. data/TODO-1.md +0 -14
  69. data/TODO.md +0 -127
  70. data/app/services/live/order_update_guard_support.rb +0 -75
  71. data/app/services/live/order_update_hub.rb +0 -76
  72. data/app/services/live/order_update_persistence_support.rb +0 -68
  73. data/docs/PR_2.2.0.md +0 -48
  74. data/examples/comprehensive_websocket_examples.rb +0 -148
  75. data/examples/instrument_finder_test.rb +0 -195
  76. data/examples/live_order_updates.rb +0 -118
  77. data/examples/market_depth_example.rb +0 -144
  78. data/examples/market_feed_example.rb +0 -81
  79. data/examples/order_update_example.rb +0 -105
  80. data/examples/trading_fields_example.rb +0 -215
  81. /data/docs/{live_order_updates.md → LIVE_ORDER_UPDATES.md} +0 -0
  82. /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 == "EQUITY" && instrument.underlying_symbol
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 "IDX_I" if ins == "INDEX" || seg == "IDX_I"
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 "NSE_FNO" if seg.start_with?("NSE")
168
- return "BSE_FNO" if seg.start_with?("BSE")
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
- "IDX_I"
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(kill_switch_status: status)
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
@@ -47,8 +47,8 @@ module DhanHQ
47
47
  # Base path used to invoke the calculator.
48
48
  HTTP_PATH = "/v2/margincalculator"
49
49
 
50
- attr_reader :total_margin, :span_margin, :exposure_margin, :available_balance,
51
- :variable_margin, :insufficient_balance, :brokerage, :leverage
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
  ##
@@ -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
- return nil unless response.is_a?(Hash) || (response.is_a?(Array) && response.any?)
173
+ is_array = response.is_a?(Array)
174
+ return nil unless response.is_a?(Hash) || (is_array && response.any?)
172
175
 
173
- order_data = response.is_a?(Array) ? response.first : response
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] ||= DhanHQ.configuration.client_id if DhanHQ.configuration.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) && response["orderId"]
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(response["orderId"])
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
- # Log warning for invalid states but still attempt API call (let API handle validation)
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
- cleaned_payload = filtered_payload.compact
385
- formatted_payload = camelize_keys(cleaned_payload)
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"] == "CANCELLED"
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
- # PLACE ORDER
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
- # MODIFY ORDER
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 == "TRANSIT"
91
+ status == DhanHQ::Constants::OrderStatus::TRANSIT
98
92
  end
99
93
 
100
94
  def pending?
101
- status == "PENDING"
95
+ status == DhanHQ::Constants::OrderStatus::PENDING
102
96
  end
103
97
 
104
98
  def rejected?
105
- status == "REJECTED"
99
+ status == DhanHQ::Constants::OrderStatus::REJECTED
106
100
  end
107
101
 
108
102
  def cancelled?
109
- status == "CANCELLED"
103
+ status == DhanHQ::Constants::OrderStatus::CANCELLED
110
104
  end
111
105
 
112
106
  def traded?
113
- status == "TRADED"
107
+ status == DhanHQ::Constants::OrderStatus::TRADED
114
108
  end
115
109
 
116
110
  def expired?
117
- status == "EXPIRED"
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 == "EQUITY"
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