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
@@ -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
  ##
@@ -205,6 +205,28 @@ module DhanHQ
205
205
  response = resource.convert(formatted_params)
206
206
  success_response?(response) ? response : DhanHQ::ErrorObject.new(response)
207
207
  end
208
+
209
+ ##
210
+ # Exits all active positions and cancels all open orders for the current trading day.
211
+ #
212
+ # This is a safety endpoint for emergency position closure. It sends a DELETE request
213
+ # to close all positions and cancel all pending orders in one call.
214
+ #
215
+ # @return [Hash{Symbol => String}] Response hash containing operation result.
216
+ # - **:status** [String] "SUCCESS" or "ERROR"
217
+ # - **:message** [String] Description of the result
218
+ #
219
+ # @example Emergency exit all positions
220
+ # response = DhanHQ::Models::Position.exit_all!
221
+ # if response[:status] == "SUCCESS"
222
+ # puts "✓ All positions exited and orders cancelled"
223
+ # else
224
+ # puts "✗ Failed: #{response[:message]}"
225
+ # end
226
+ #
227
+ def exit_all!
228
+ resource.exit_all
229
+ end
208
230
  end
209
231
  end
210
232
  end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Models
5
+ ##
6
+ # Utility model for parsing Dhan postback (webhook) payloads.
7
+ #
8
+ # Postback is a webhook mechanism where Dhan pushes order status updates to your
9
+ # configured URL. This model provides a convenient way to parse the incoming JSON
10
+ # payload into a typed, attribute-accessible object.
11
+ #
12
+ # @note Postback URL is configured in the Dhan web console when generating an access
13
+ # token. It will NOT work with localhost or 127.0.0.1.
14
+ #
15
+ # @example Parse postback payload in a Rails controller
16
+ # class DhanWebhooksController < ApplicationController
17
+ # skip_before_action :verify_authenticity_token
18
+ #
19
+ # def create
20
+ # postback = DhanHQ::Models::Postback.parse(request.body.read)
21
+ # case postback.order_status
22
+ # when "TRADED"
23
+ # handle_fill(postback)
24
+ # when "REJECTED"
25
+ # handle_rejection(postback)
26
+ # end
27
+ # head :ok
28
+ # end
29
+ # end
30
+ #
31
+ # @example Parse postback payload from a hash
32
+ # postback = DhanHQ::Models::Postback.parse(params)
33
+ # puts "Order #{postback.order_id} is now #{postback.order_status}"
34
+ # puts "Filled: #{postback.filled_qty}/#{postback.quantity}"
35
+ #
36
+ class Postback < BaseModel
37
+ HTTP_PATH = nil # No API endpoint — postback is pushed to the user
38
+
39
+ attributes :dhan_client_id, :order_id, :correlation_id, :order_status,
40
+ :transaction_type, :exchange_segment, :product_type, :order_type,
41
+ :validity, :trading_symbol, :security_id, :quantity,
42
+ :disclosed_quantity, :price, :trigger_price, :after_market_order,
43
+ :bo_profit_value, :bo_stop_loss_value, :leg_name,
44
+ :create_time, :update_time, :exchange_time,
45
+ :drv_expiry_date, :drv_option_type, :drv_strike_price,
46
+ :oms_error_code, :oms_error_description, :filled_qty, :algo_id
47
+
48
+ class << self
49
+ ##
50
+ # Parse a postback webhook payload into a Postback model instance.
51
+ #
52
+ # Accepts either a JSON string (from request body) or a Hash (from parsed params).
53
+ # Keys are normalized to snake_case automatically.
54
+ #
55
+ # @param payload [String, Hash] Raw JSON string or Hash from the webhook
56
+ #
57
+ # @return [Postback] Parsed Postback object with typed attributes
58
+ #
59
+ # @example From raw JSON string
60
+ # postback = DhanHQ::Models::Postback.parse('{"orderId":"123","orderStatus":"TRADED"}')
61
+ # puts postback.order_status # => "TRADED"
62
+ #
63
+ # @example From a hash
64
+ # postback = DhanHQ::Models::Postback.parse(order_id: "123", order_status: "TRADED")
65
+ # puts postback.order_id # => "123"
66
+ #
67
+ def parse(payload)
68
+ data = case payload
69
+ when String
70
+ JSON.parse(payload)
71
+ when Hash
72
+ payload
73
+ else
74
+ raise ArgumentError, "Expected String or Hash, got #{payload.class}"
75
+ end
76
+
77
+ new(data, skip_validation: true)
78
+ end
79
+ end
80
+
81
+ ##
82
+ # Whether the order has been fully traded.
83
+ #
84
+ # @return [Boolean]
85
+ def traded?
86
+ order_status == DhanHQ::Constants::OrderStatus::TRADED
87
+ end
88
+
89
+ ##
90
+ # Whether the order was rejected.
91
+ #
92
+ # @return [Boolean]
93
+ def rejected?
94
+ order_status == DhanHQ::Constants::OrderStatus::REJECTED
95
+ end
96
+
97
+ ##
98
+ # Whether the order is still pending.
99
+ #
100
+ # @return [Boolean]
101
+ def pending?
102
+ order_status == DhanHQ::Constants::OrderStatus::PENDING
103
+ end
104
+
105
+ ##
106
+ # Whether the order was cancelled.
107
+ #
108
+ # @return [Boolean]
109
+ def cancelled?
110
+ order_status == DhanHQ::Constants::OrderStatus::CANCELLED
111
+ end
112
+ end
113
+ end
114
+ 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,30 @@
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), {}))
20
+ end
21
+
22
+ ##
23
+ # Fetches the current kill switch status.
24
+ #
25
+ # @return [Hash] API response containing dhan_client_id and kill_switch_status.
26
+ def status
27
+ get("")
18
28
  end
19
29
  end
20
30
  end
@@ -17,6 +17,15 @@ module DhanHQ
17
17
  def calculate(params)
18
18
  post("", params: params)
19
19
  end
20
+
21
+ ##
22
+ # Calculate margin requirements for multiple scripts in one request.
23
+ #
24
+ # @param params [Hash] Request parameters including scripList, includePosition, includeOrder.
25
+ # @return [Hash] API response containing combined margin details with hedge benefit.
26
+ def calculate_multi(params)
27
+ post("/multi", params: params)
28
+ end
20
29
  end
21
30
  end
22
31
  end
@@ -1,70 +1,70 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DhanHQ
4
- # REST API wrappers grouped by resource type.
5
4
  module Resources
6
- # Resource client for managing equity and F&O orders.
5
+ # Handles order placement, modification, and cancellation
7
6
  class Orders < BaseAPI
8
- # Orders are routed through the trading API tier.
9
7
  API_TYPE = :order_api
10
- # Base path for order endpoints.
11
8
  HTTP_PATH = "/v2/orders"
12
9
 
13
- # Retrieve all orders for the current trading day.
14
- #
15
- # @return [Array<Hash>]
16
- def all
17
- get("")
18
- end
10
+ # --------------------------------------------------
11
+ # PUBLIC API
12
+ # --------------------------------------------------
19
13
 
20
- # Places a new order using the provided payload.
21
- #
22
- # @param params [Hash]
23
- # @return [Hash]
24
14
  def create(params)
15
+ validate_place_order!(params)
25
16
  post("", params: params)
26
17
  end
27
18
 
28
- # Fetches a single order by broker order id.
29
- #
30
- # @param order_id [String]
31
- # @return [Hash]
32
- def find(order_id)
33
- get("/#{order_id}")
34
- end
35
-
36
- # Modifies an existing order.
37
- #
38
- # @param order_id [String]
39
- # @param params [Hash]
40
- # @return [Hash]
41
19
  def update(order_id, params)
20
+ validate_modify_order!(params.merge(order_id: order_id))
42
21
  put("/#{order_id}", params: params)
43
22
  end
44
23
 
45
- # Cancels an existing order.
46
- #
47
- # @param order_id [String]
48
- # @return [Hash]
24
+ def slicing(params)
25
+ validate_place_order!(params)
26
+ post("/slicing", params: params)
27
+ end
28
+
49
29
  def cancel(order_id)
50
30
  delete("/#{order_id}")
51
31
  end
52
32
 
53
- # Places a slicing order request.
54
- #
55
- # @param params [Hash]
56
- # @return [Hash]
57
- def slicing(params)
58
- post("/slicing", params: params)
33
+ def all
34
+ get("")
35
+ end
36
+
37
+ def find(order_id)
38
+ get("/#{order_id}")
59
39
  end
60
40
 
61
- # Retrieve an order by client-supplied correlation id.
62
- #
63
- # @param correlation_id [String]
64
- # @return [Hash]
65
41
  def by_correlation(correlation_id)
66
42
  get("/external/#{correlation_id}")
67
43
  end
44
+
45
+ # --------------------------------------------------
46
+ # VALIDATION LAYER
47
+ # --------------------------------------------------
48
+
49
+ private
50
+
51
+ def validate_place_order!(params)
52
+ result = Contracts::PlaceOrderContract.new.call(normalize_keys_for_validation(params))
53
+ raise_validation_error!(result) unless result.success?
54
+ end
55
+
56
+ def validate_modify_order!(params)
57
+ result = Contracts::ModifyOrderContract.new.call(normalize_keys_for_validation(params))
58
+ raise_validation_error!(result) unless result.success?
59
+ end
60
+
61
+ def normalize_keys_for_validation(params)
62
+ snake_case(params)
63
+ end
64
+
65
+ def raise_validation_error!(result)
66
+ raise DhanHQ::Error, "Validation Error: #{result.errors.to_h}"
67
+ end
68
68
  end
69
69
  end
70
70
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Resources
5
+ # Resource for P&L Based Exit endpoints per https://dhanhq.co/docs/v2/traders-control/
6
+ # POST /v2/pnlExit — configure, DELETE /v2/pnlExit — stop, GET /v2/pnlExit — status.
7
+ class PnlExit < BaseAPI
8
+ API_TYPE = :order_api
9
+ HTTP_PATH = "/v2/pnlExit"
10
+
11
+ ##
12
+ # Configure automatic P&L-based position exit.
13
+ #
14
+ # @param params [Hash] Request body with profitValue, lossValue, productType, enableKillSwitch.
15
+ # @return [Hash] API response containing pnlExitStatus and message.
16
+ def configure(params)
17
+ post("", params: params)
18
+ end
19
+
20
+ ##
21
+ # Stop/disable the active P&L-based exit configuration.
22
+ #
23
+ # @return [Hash] API response containing pnlExitStatus and message.
24
+ def stop
25
+ delete("")
26
+ end
27
+
28
+ ##
29
+ # Fetch the currently active P&L-based exit configuration.
30
+ #
31
+ # @return [Hash] API response containing pnlExitStatus, profit, loss, segments, enable_kill_switch.
32
+ def status
33
+ get("")
34
+ end
35
+ end
36
+ end
37
+ end
@@ -24,6 +24,14 @@ module DhanHQ
24
24
  def convert(params)
25
25
  post("/convert", params: params)
26
26
  end
27
+
28
+ ##
29
+ # Exit all active positions and cancel all open orders.
30
+ #
31
+ # @return [Hash] API response containing status and message.
32
+ def exit_all
33
+ delete("")
34
+ end
27
35
  end
28
36
  end
29
37
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module DhanHQ
4
4
  # Semantic version of the DhanHQ client gem.
5
- VERSION = "2.4.0"
5
+ VERSION = "2.6.0"
6
6
  end
@@ -6,7 +6,7 @@ module DhanHQ
6
6
  # connection is ready to send them.
7
7
  class CmdBus
8
8
  # Represents a subscription command queued for execution.
9
- Command = Struct.new(:op, :payload, keyword_init: true)
9
+ Command = Struct.new(:op, :payload)
10
10
 
11
11
  def initialize
12
12
  @q = Queue.new
@@ -201,17 +201,17 @@ module DhanHQ
201
201
  # @param _previous_state [OrderUpdate, nil] Previous order state (unused parameter)
202
202
  def emit_status_specific_events(order_update, _previous_state)
203
203
  case order_update.status
204
- when "TRANSIT"
204
+ when DhanHQ::Constants::OrderStatus::TRANSIT
205
205
  emit(:order_transit, order_update)
206
- when "PENDING"
206
+ when DhanHQ::Constants::OrderStatus::PENDING
207
207
  emit(:order_pending, order_update)
208
- when "REJECTED"
208
+ when DhanHQ::Constants::OrderStatus::REJECTED
209
209
  emit(:order_rejected, order_update)
210
- when "CANCELLED"
210
+ when DhanHQ::Constants::OrderStatus::CANCELLED
211
211
  emit(:order_cancelled, order_update)
212
- when "TRADED"
212
+ when DhanHQ::Constants::OrderStatus::TRADED
213
213
  emit(:order_traded, order_update)
214
- when "EXPIRED"
214
+ when DhanHQ::Constants::OrderStatus::EXPIRED
215
215
  emit(:order_expired, order_update)
216
216
  end
217
217
  end
@@ -14,7 +14,8 @@ module DhanHQ
14
14
  key = Digest::SHA256.hexdigest("#{client_id}:#{token}")[0, 12]
15
15
  @path = File.expand_path("tmp/dhanhq_ws_#{key}.lock", Dir.pwd)
16
16
  FileUtils.mkdir_p(File.dirname(@path))
17
- @fh = File.open(@path, File::RDWR | File::CREAT, 0o644)
17
+ # Lock file must stay open until release!; block form would close it and release the lock.
18
+ @fh = File.open(@path, File::RDWR | File::CREAT, 0o644) # rubocop:disable Style/FileOpen
18
19
  end
19
20
 
20
21
  # Attempts to acquire the lock for the current process.
@@ -91,7 +91,7 @@ module DhanHQ
91
91
 
92
92
  # Use OptionChain model: pick nearest/next expiry and fetch chain
93
93
  sid = @data.dig(:meta, :security_id)
94
- seg = @data.dig(:meta, :exchange_segment) || "IDX_I"
94
+ seg = @data.dig(:meta, :exchange_segment) || DhanHQ::Constants::ExchangeSegment::IDX_I
95
95
  return unless sid && seg
96
96
 
97
97
  expiries = DhanHQ::Models::OptionChain.fetch_expiry_list(underlying_scrip: sid.to_i, underlying_seg: seg)
@@ -128,7 +128,7 @@ module DhanHQ
128
128
  end
129
129
 
130
130
  def index_instrument?(meta)
131
- meta[:instrument].to_s == "INDEX" || meta[:exchange_segment].to_s == "IDX_I"
131
+ meta[:instrument].to_s == DhanHQ::Constants::InstrumentType::INDEX || meta[:exchange_segment].to_s == DhanHQ::Constants::ExchangeSegment::IDX_I
132
132
  end
133
133
 
134
134
  def select_strike(side:, moneyness:)