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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -1
  3. data/CHANGELOG.md +78 -6
  4. data/GUIDE.md +57 -39
  5. data/README.md +24 -23
  6. data/docs/API_DOCS_GAPS.md +128 -0
  7. data/docs/API_VERIFICATION.md +10 -11
  8. data/docs/ARCHIVE_README.md +16 -16
  9. data/docs/AUTHENTICATION.md +1 -1
  10. data/docs/CONSTANTS_REFERENCE.md +477 -0
  11. data/docs/DATA_API_PARAMETERS.md +7 -7
  12. data/docs/{rails_websocket_integration.md → RAILS_WEBSOCKET_INTEGRATION.md} +10 -10
  13. data/docs/{standalone_ruby_websocket_integration.md → STANDALONE_RUBY_WEBSOCKET_INTEGRATION.md} +32 -32
  14. data/docs/{technical_analysis.md → TECHNICAL_ANALYSIS.md} +3 -3
  15. data/docs/TESTING_GUIDE.md +84 -82
  16. data/docs/{websocket_integration.md → WEBSOCKET_INTEGRATION.md} +19 -19
  17. data/docs/WEBSOCKET_PROTOCOL.md +2 -2
  18. data/lib/DhanHQ/constants.rb +456 -151
  19. data/lib/DhanHQ/contracts/alert_order_contract.rb +37 -10
  20. data/lib/DhanHQ/contracts/base_contract.rb +22 -0
  21. data/lib/DhanHQ/contracts/edis_contract.rb +25 -0
  22. data/lib/DhanHQ/contracts/margin_calculator_contract.rb +27 -4
  23. data/lib/DhanHQ/contracts/modify_order_contract.rb +65 -12
  24. data/lib/DhanHQ/contracts/multi_scrip_margin_calc_request_contract.rb +23 -0
  25. data/lib/DhanHQ/contracts/order_contract.rb +171 -39
  26. data/lib/DhanHQ/contracts/place_order_contract.rb +14 -141
  27. data/lib/DhanHQ/contracts/pnl_based_exit_contract.rb +20 -0
  28. data/lib/DhanHQ/contracts/position_conversion_contract.rb +15 -3
  29. data/lib/DhanHQ/contracts/slice_order_contract.rb +10 -1
  30. data/lib/DhanHQ/contracts/user_ip_contract.rb +14 -0
  31. data/lib/DhanHQ/core/base_model.rb +13 -4
  32. data/lib/DhanHQ/helpers/response_helper.rb +2 -2
  33. data/lib/DhanHQ/helpers/validation_helper.rb +1 -1
  34. data/lib/DhanHQ/models/alert_order.rb +7 -11
  35. data/lib/DhanHQ/models/concerns/api_response_handler.rb +46 -0
  36. data/lib/DhanHQ/models/edis.rb +0 -9
  37. data/lib/DhanHQ/models/expired_options_data.rb +6 -12
  38. data/lib/DhanHQ/models/forever_order.rb +8 -5
  39. data/lib/DhanHQ/models/historical_data.rb +0 -8
  40. data/lib/DhanHQ/models/instrument.rb +1 -7
  41. data/lib/DhanHQ/models/instrument_helpers.rb +4 -4
  42. data/lib/DhanHQ/models/kill_switch.rb +1 -11
  43. data/lib/DhanHQ/models/margin.rb +2 -2
  44. data/lib/DhanHQ/models/order.rb +107 -126
  45. data/lib/DhanHQ/models/order_update.rb +7 -13
  46. data/lib/DhanHQ/models/pnl_exit.rb +1 -9
  47. data/lib/DhanHQ/models/position.rb +1 -1
  48. data/lib/DhanHQ/models/postback.rb +4 -13
  49. data/lib/DhanHQ/models/profile.rb +0 -10
  50. data/lib/DhanHQ/models/super_order.rb +13 -3
  51. data/lib/DhanHQ/models/trade.rb +11 -23
  52. data/lib/DhanHQ/resources/ip_setup.rb +16 -5
  53. data/lib/DhanHQ/resources/kill_switch.rb +9 -7
  54. data/lib/DhanHQ/resources/orders.rb +41 -41
  55. data/lib/DhanHQ/version.rb +1 -1
  56. data/lib/DhanHQ/ws/cmd_bus.rb +1 -1
  57. data/lib/DhanHQ/ws/orders/client.rb +6 -6
  58. data/lib/DhanHQ/ws/singleton_lock.rb +2 -1
  59. data/lib/dhanhq/analysis/options_buying_advisor.rb +2 -2
  60. data/lib/rubocop/cop/dhanhq/use_constants.rb +171 -0
  61. metadata +20 -23
  62. data/TODO-1.md +0 -14
  63. data/TODO.md +0 -127
  64. data/app/services/live/order_update_guard_support.rb +0 -75
  65. data/app/services/live/order_update_hub.rb +0 -76
  66. data/app/services/live/order_update_persistence_support.rb +0 -68
  67. data/docs/PR_2.2.0.md +0 -48
  68. data/examples/comprehensive_websocket_examples.rb +0 -148
  69. data/examples/instrument_finder_test.rb +0 -195
  70. data/examples/live_order_updates.rb +0 -118
  71. data/examples/market_depth_example.rb +0 -144
  72. data/examples/market_feed_example.rb +0 -81
  73. data/examples/order_update_example.rb +0 -105
  74. data/examples/trading_fields_example.rb +0 -215
  75. /data/docs/{live_order_updates.md → LIVE_ORDER_UPDATES.md} +0 -0
  76. /data/docs/{rails_integration.md → RAILS_INTEGRATION.md} +0 -0
@@ -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?
@@ -75,6 +75,7 @@ module DhanHQ
75
75
  productType: product_type,
76
76
  enableKillSwitch: enable_kill_switch
77
77
  }
78
+ params[:dhanClientId] = DhanHQ.configuration.client_id if DhanHQ.configuration&.client_id.to_s != ""
78
79
  resource.configure(params)
79
80
  end
80
81
 
@@ -116,15 +117,6 @@ module DhanHQ
116
117
  new(response, skip_validation: true)
117
118
  end
118
119
  end
119
-
120
- ##
121
- # No validation contract needed — server-side validation handles it.
122
- #
123
- # @return [nil]
124
- # @api private
125
- def validation_contract
126
- nil
127
- end
128
120
  end
129
121
  end
130
122
  end
@@ -139,7 +139,7 @@ module DhanHQ
139
139
  # puts "Total open position value: ₹#{total_value}"
140
140
  #
141
141
  def active
142
- all.reject { |position| position.position_type == "CLOSED" }
142
+ all.reject { |position| position.position_type == DhanHQ::Constants::OrderStatus::CLOSED }
143
143
  end
144
144
 
145
145
  ##
@@ -83,7 +83,7 @@ module DhanHQ
83
83
  #
84
84
  # @return [Boolean]
85
85
  def traded?
86
- order_status == "TRADED"
86
+ order_status == DhanHQ::Constants::OrderStatus::TRADED
87
87
  end
88
88
 
89
89
  ##
@@ -91,7 +91,7 @@ module DhanHQ
91
91
  #
92
92
  # @return [Boolean]
93
93
  def rejected?
94
- order_status == "REJECTED"
94
+ order_status == DhanHQ::Constants::OrderStatus::REJECTED
95
95
  end
96
96
 
97
97
  ##
@@ -99,7 +99,7 @@ module DhanHQ
99
99
  #
100
100
  # @return [Boolean]
101
101
  def pending?
102
- order_status == "PENDING"
102
+ order_status == DhanHQ::Constants::OrderStatus::PENDING
103
103
  end
104
104
 
105
105
  ##
@@ -107,16 +107,7 @@ module DhanHQ
107
107
  #
108
108
  # @return [Boolean]
109
109
  def cancelled?
110
- order_status == "CANCELLED"
111
- end
112
-
113
- ##
114
- # No validation contract — postback payloads are parsed as-is.
115
- #
116
- # @return [nil]
117
- # @api private
118
- def validation_contract
119
- nil
110
+ order_status == DhanHQ::Constants::OrderStatus::CANCELLED
120
111
  end
121
112
  end
122
113
  end
@@ -125,16 +125,6 @@ module DhanHQ
125
125
 
126
126
  ##
127
127
  # Profile responses are informational and not validated locally.
128
- #
129
- # Since profile data is read-only account metadata, no validation contract
130
- # is needed. The API response is trusted as-is.
131
- #
132
- # @return [nil] Always returns nil as profiles are read-only and informational
133
- #
134
- # @api private
135
- def validation_contract
136
- nil
137
- end
138
128
  end
139
129
  end
140
130
  end
@@ -48,6 +48,8 @@ module DhanHQ
48
48
  # order.cancel("STOP_LOSS_LEG")
49
49
  #
50
50
  class SuperOrder < BaseModel
51
+ include Concerns::ApiResponseHandler
52
+
51
53
  attributes :dhan_client_id, :order_id, :correlation_id, :order_status,
52
54
  :transaction_type, :exchange_segment, :product_type, :order_type,
53
55
  :validity, :trading_symbol, :security_id, :quantity,
@@ -284,8 +286,12 @@ module DhanHQ
284
286
  def modify(new_params)
285
287
  raise "Order ID is required to modify a super order" unless id
286
288
 
289
+ DhanHQ.logger&.info("[DhanHQ::Models::SuperOrder] Modifying super order #{id}")
287
290
  response = self.class.resource.update(id, new_params)
288
- response["orderId"] == id
291
+ return false unless response.is_a?(Hash) && response["orderId"] == id
292
+
293
+ DhanHQ.logger&.info("[DhanHQ::Models::SuperOrder] Super order #{id} modified successfully")
294
+ true
289
295
  end
290
296
 
291
297
  ##
@@ -318,11 +324,15 @@ module DhanHQ
318
324
  # order.cancel("TARGET_LEG")
319
325
  #
320
326
  # @raise [RuntimeError] If order ID is missing
321
- def cancel(leg_name = "ENTRY_LEG")
327
+ def cancel(leg_name = DhanHQ::Constants::LegName::ENTRY_LEG)
322
328
  raise "Order ID is required to cancel a super order" unless id
323
329
 
330
+ DhanHQ.logger&.info("[DhanHQ::Models::SuperOrder] Cancelling super order #{id} leg #{leg_name}")
324
331
  response = self.class.resource.cancel(id, leg_name)
325
- response["orderStatus"] == "CANCELLED"
332
+ return false unless response.is_a?(Hash) && response["orderStatus"] == DhanHQ::Constants::OrderStatus::CANCELLED
333
+
334
+ DhanHQ.logger&.info("[DhanHQ::Models::SuperOrder] Super order #{id} leg #{leg_name} cancelled successfully")
335
+ true
326
336
  end
327
337
  end
328
338
  end
@@ -49,8 +49,8 @@ module DhanHQ
49
49
  # Provides a shared instance of the Trades resource for current day tradebook APIs.
50
50
  #
51
51
  # @return [DhanHQ::Resources::Trades] The Trades resource client instance
52
- def tradebook_resource
53
- @tradebook_resource ||= DhanHQ::Resources::Trades.new
52
+ def resource
53
+ @resource ||= DhanHQ::Resources::Trades.new
54
54
  end
55
55
 
56
56
  ##
@@ -107,10 +107,8 @@ module DhanHQ
107
107
  # puts "P&L: ₹#{pnl}"
108
108
  #
109
109
  def today
110
- response = tradebook_resource.all
111
- return [] unless response.is_a?(Array)
112
-
113
- response.map { |trade_data| new(trade_data, skip_validation: true) }
110
+ response = resource.all
111
+ parse_collection_response(response)
114
112
  end
115
113
 
116
114
  ##
@@ -145,7 +143,7 @@ module DhanHQ
145
143
  raise DhanHQ::ValidationError, "Invalid order_id: #{validation_result.errors.to_h}"
146
144
  end
147
145
 
148
- response = tradebook_resource.find(order_id)
146
+ response = resource.find(order_id)
149
147
  return nil unless response.is_a?(Hash) || (response.is_a?(Array) && response.any?)
150
148
 
151
149
  data = response.is_a?(Array) ? response.first : response
@@ -248,16 +246,6 @@ module DhanHQ
248
246
  alias all history
249
247
  end
250
248
 
251
- ##
252
- # Trade objects are read-only, so no validation contract needed.
253
- #
254
- # @return [nil] Always returns nil as trades are read-only
255
- #
256
- # @api private
257
- def validation_contract
258
- nil
259
- end
260
-
261
249
  ##
262
250
  # Checks if the trade is a BUY transaction.
263
251
  #
@@ -270,7 +258,7 @@ module DhanHQ
270
258
  # end
271
259
  #
272
260
  def buy?
273
- transaction_type == "BUY"
261
+ transaction_type == DhanHQ::Constants::TransactionType::BUY
274
262
  end
275
263
 
276
264
  ##
@@ -285,7 +273,7 @@ module DhanHQ
285
273
  # end
286
274
  #
287
275
  def sell?
288
- transaction_type == "SELL"
276
+ transaction_type == DhanHQ::Constants::TransactionType::SELL
289
277
  end
290
278
 
291
279
  ##
@@ -299,7 +287,7 @@ module DhanHQ
299
287
  # puts "Equity trades: #{equity_trades.count}"
300
288
  #
301
289
  def equity?
302
- instrument == "EQUITY"
290
+ instrument == DhanHQ::Constants::InstrumentType::EQUITY
303
291
  end
304
292
 
305
293
  ##
@@ -327,7 +315,7 @@ module DhanHQ
327
315
  # puts "Option trades: #{option_trades.count}"
328
316
  #
329
317
  def option?
330
- %w[CALL PUT].include?(drv_option_type)
318
+ [DhanHQ::Constants::OptionType::CALL, DhanHQ::Constants::OptionType::PUT].include?(drv_option_type)
331
319
  end
332
320
 
333
321
  ##
@@ -341,7 +329,7 @@ module DhanHQ
341
329
  # puts "Call option trades: #{call_trades.count}"
342
330
  #
343
331
  def call_option?
344
- drv_option_type == "CALL"
332
+ drv_option_type == DhanHQ::Constants::OptionType::CALL
345
333
  end
346
334
 
347
335
  ##
@@ -355,7 +343,7 @@ module DhanHQ
355
343
  # puts "Put option trades: #{put_trades.count}"
356
344
  #
357
345
  def put_option?
358
- drv_option_type == "PUT"
346
+ drv_option_type == DhanHQ::Constants::OptionType::PUT
359
347
  end
360
348
 
361
349
  ##
@@ -2,7 +2,8 @@
2
2
 
3
3
  module DhanHQ
4
4
  module Resources
5
- # Resource for IP whitelist per API docs: GET /ip/getIP, POST /ip/setIP, PUT /ip/modifyIP.
5
+ # Resource for IP whitelist per API docs: GET /v2/ip/getIP, POST /v2/ip/setIP, PUT /v2/ip/modifyIP.
6
+ # Set/Modify require dhanClientId, ip, ipFlag (PRIMARY | SECONDARY). See dhanhq.co/docs/v2/authentication/#setup-static-ip
6
7
  class IPSetup < BaseAPI
7
8
  API_TYPE = :order_api
8
9
  HTTP_PATH = "/ip"
@@ -11,12 +12,22 @@ module DhanHQ
11
12
  get("/getIP")
12
13
  end
13
14
 
14
- def set(ip:)
15
- post("/setIP", params: { ip: ip })
15
+ # @param ip [String] Static IP (IPv4 or IPv6)
16
+ # @param ip_flag [String] "PRIMARY" or "SECONDARY"
17
+ # @param dhan_client_id [String, nil] Defaults to DhanHQ.configuration.client_id when nil
18
+ def set(ip:, ip_flag: "PRIMARY", dhan_client_id: nil)
19
+ params = { ip: ip, ip_flag: ip_flag }
20
+ params[:dhan_client_id] = dhan_client_id || DhanHQ.configuration&.client_id
21
+ post("/setIP", params: params)
16
22
  end
17
23
 
18
- def update(ip:)
19
- put("/modifyIP", params: { ip: ip })
24
+ # @param ip [String] Static IP (IPv4 or IPv6)
25
+ # @param ip_flag [String] "PRIMARY" or "SECONDARY"
26
+ # @param dhan_client_id [String, nil] Defaults to DhanHQ.configuration.client_id when nil
27
+ def update(ip:, ip_flag: "PRIMARY", dhan_client_id: nil)
28
+ params = { ip: ip, ip_flag: ip_flag }
29
+ params[:dhan_client_id] = dhan_client_id || DhanHQ.configuration&.client_id
30
+ put("/modifyIP", params: params)
20
31
  end
21
32
  end
22
33
  end
@@ -1,20 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "cgi"
4
+
3
5
  module DhanHQ
4
6
  module Resources
5
7
  # Resource client to control the trading kill switch feature.
8
+ # API expects killSwitchStatus as query parameter (no body). See dhanhq.co/docs/v2/traders-control/
6
9
  class KillSwitch < BaseAPI
7
- # Kill switch operations execute on the trading API tier.
8
- API_TYPE = :order_api
9
- # Base path for kill switch operations.
10
+ API_TYPE = :order_api
10
11
  HTTP_PATH = "/v2/killswitch"
11
12
 
12
- # Enables or disables the kill switch.
13
+ # Enables or disables the kill switch via query parameter (doc: no body).
13
14
  #
14
- # @param params [Hash]
15
+ # @param status [String] "ACTIVATE" or "DEACTIVATE"
15
16
  # @return [Hash]
16
- def update(params)
17
- post("", params: params)
17
+ def update(status)
18
+ query = "?killSwitchStatus=#{CGI.escape(status.to_s)}"
19
+ handle_response(client.post(build_path(query), {}))
18
20
  end
19
21
 
20
22
  ##