DhanHQ 2.4.0 → 2.5.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.
@@ -0,0 +1,154 @@
1
+ # WebSocket Protocol Reference
2
+
3
+ Low-level protocol details for the DhanHQ WebSocket market feed. For high-level usage, see the [WebSocket Integration Guide](websocket_integration.md).
4
+
5
+ ---
6
+
7
+ ## Subscription Modes
8
+
9
+ | Mode | What you get | Best for |
10
+ | --------- | ----------------------------------------- | ------------------------------- |
11
+ | `:ticker` | LTP + LTT | Lightweight price monitoring |
12
+ | `:quote` | LTP + LTT + OHLCV + totals | Most trading strategies |
13
+ | `:full` | Quote + OI + best-5 depth (bid/ask) | Order book analysis, depth-based strategies |
14
+
15
+ ---
16
+
17
+ ## Request Codes
18
+
19
+ Per Dhan documentation:
20
+
21
+ | Action | Ticker | Quote | Full |
22
+ | ------------ | ------ | ----- | ---- |
23
+ | Subscribe | 15 | 17 | 21 |
24
+ | Unsubscribe | 16 | 18 | 22 |
25
+ | Disconnect | 12 | 12 | 12 |
26
+
27
+ ---
28
+
29
+ ## Packet Parsing
30
+
31
+ ### Response Header (8 bytes)
32
+
33
+ | Field | Size | Encoding | Description |
34
+ | -------------------- | ------ | -------- | ----------------------------- |
35
+ | `feed_response_code` | 1 byte | u8, BE | Identifies the packet type |
36
+ | `message_length` | 2 bytes| u16, BE | Total message length in bytes |
37
+ | `exchange_segment` | 1 byte | u8, BE | Exchange segment identifier |
38
+ | `security_id` | 4 bytes| i32, LE | Security identifier |
39
+
40
+ ### Packet Types
41
+
42
+ | Code | Type | Fields |
43
+ | ---- | ------------- | ------------------------------------------------------------- |
44
+ | 1 | Index | Surfaced as raw/misc unless documented |
45
+ | 2 | Ticker | `ltp`, `ltt` |
46
+ | 4 | Quote | `ltp`, `ltt`, `atp`, `volume`, totals, `day_*` |
47
+ | 5 | OI | `open_interest` |
48
+ | 6 | Prev Close | `prev_close`, `oi_prev` |
49
+ | 7 | Market Status | Raw/misc unless documented |
50
+ | 8 | Full | Quote fields + `open_interest` + 5× depth (bid/ask) |
51
+ | 50 | Disconnect | Reason code |
52
+
53
+ ---
54
+
55
+ ## Normalized Tick Schema
56
+
57
+ All ticks are delivered as a Ruby Hash with consistent keys:
58
+
59
+ ```ruby
60
+ {
61
+ kind: :quote, # :ticker | :quote | :full | :oi | :prev_close | :misc
62
+ segment: "NSE_FNO", # string enum
63
+ security_id: "12345",
64
+ ltp: 101.5,
65
+ ts: 1723791300, # LTT epoch (sec) if present
66
+ vol: 123456, # quote/full only
67
+ atp: 100.9, # quote/full only
68
+ day_open: 100.1,
69
+ day_high: 102.4,
70
+ day_low: 99.5,
71
+ day_close: nil,
72
+ oi: 987654, # full or OI packet
73
+ bid: 101.45, # from depth (mode :full)
74
+ ask: 101.55 # from depth (mode :full)
75
+ }
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Connection Limits & Behavior
81
+
82
+ ### Limits
83
+
84
+ - **100 instruments** per subscribe/unsubscribe frame (auto-chunked by the client)
85
+ - **5 WebSocket connections** per user (per Dhan)
86
+
87
+ ### Backoff & 429 Cool-Off
88
+
89
+ - Exponential backoff with jitter on connection failure
90
+ - Handshake **429** triggers a **60-second cool-off** before retry
91
+ - The client handles this automatically — avoid manual rapid reconnect loops
92
+
93
+ ### Reconnect & Resubscribe
94
+
95
+ - On reconnect, the client resends the **current subscription snapshot** (idempotent)
96
+ - No manual re-subscribe needed after automatic reconnection
97
+
98
+ ### Graceful Shutdown
99
+
100
+ - `ws.disconnect!` — sends broker disconnect code 12, prevents reconnects
101
+ - `ws.stop` — hard stop (no broker message, just closes and halts loop)
102
+ - `DhanHQ::WS.disconnect_all_local!` — kills all registered WS clients
103
+ - An `at_exit` hook stops all registered clients to avoid leaked sockets
104
+
105
+ ---
106
+
107
+ ## Exchange Segment Enums
108
+
109
+ Use these string enums in WebSocket `subscribe_*` calls and REST parameters:
110
+
111
+ | Enum | Exchange | Segment |
112
+ | -------------- | -------- | ----------------- |
113
+ | `IDX_I` | Index | Index Value |
114
+ | `NSE_EQ` | NSE | Equity Cash |
115
+ | `NSE_FNO` | NSE | Futures & Options |
116
+ | `NSE_CURRENCY` | NSE | Currency |
117
+ | `BSE_EQ` | BSE | Equity Cash |
118
+ | `BSE_FNO` | BSE | Futures & Options |
119
+ | `BSE_CURRENCY` | BSE | Currency |
120
+ | `MCX_COMM` | MCX | Commodity |
121
+
122
+ ---
123
+
124
+ ## Tick Access Patterns
125
+
126
+ ### Direct Handler
127
+
128
+ ```ruby
129
+ ws.on(:tick) { |t| do_something_fast(t) } # avoid heavy work here
130
+ ```
131
+
132
+ ### Shared TickCache (Recommended)
133
+
134
+ ```ruby
135
+ # app/services/live/tick_cache.rb
136
+ class TickCache
137
+ MAP = Concurrent::Map.new
138
+ def self.put(t) = MAP["#{t[:segment]}:#{t[:security_id]}"] = t
139
+ def self.get(seg, sid) = MAP["#{seg}:#{sid}"]
140
+ def self.ltp(seg, sid) = get(seg, sid)&.dig(:ltp)
141
+ end
142
+
143
+ ws.on(:tick) { |t| TickCache.put(t) }
144
+ ltp = TickCache.ltp("NSE_FNO", "12345")
145
+ ```
146
+
147
+ ### Filtered Callback
148
+
149
+ ```ruby
150
+ def on_tick_for(ws, segment:, security_id:, &blk)
151
+ key = "#{segment}:#{security_id}"
152
+ ws.on(:tick) { |t| blk.call(t) if "#{t[:segment]}:#{t[:security_id]}" == key }
153
+ end
154
+ ```
@@ -48,6 +48,28 @@ module DhanHQ
48
48
 
49
49
  find(response["alertId"])
50
50
  end
51
+
52
+ ##
53
+ # Modify an existing conditional trigger/alert order.
54
+ #
55
+ # @param alert_id [String] The alert ID to modify
56
+ # @param params [Hash] Updated parameters (condition, orders, etc.)
57
+ #
58
+ # @return [AlertOrder, nil] Updated AlertOrder instance, or nil on failure
59
+ #
60
+ # @example Modify an alert order's condition
61
+ # updated = DhanHQ::Models::AlertOrder.modify("12345",
62
+ # condition: { comparing_value: 300 },
63
+ # orders: [{ quantity: 20 }]
64
+ # )
65
+ #
66
+ def modify(alert_id, params)
67
+ normalized = snake_case(params)
68
+ response = resource.update(alert_id, camelize_keys(normalized))
69
+ return nil unless success_response?(response)
70
+
71
+ find(alert_id)
72
+ end
51
73
  end
52
74
 
53
75
  def id
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DhanHQ
4
+ module Models
5
+ ##
6
+ # Model for EDIS (Electronic Delivery Instruction Slip) operations.
7
+ #
8
+ # EDIS is used for selling holdings from your demat account. The API provides
9
+ # endpoints to generate T-PIN, create eDIS forms, and check authorization status.
10
+ #
11
+ # @example Generate T-PIN
12
+ # DhanHQ::Models::Edis.generate_tpin
13
+ #
14
+ # @example Generate eDIS form
15
+ # response = DhanHQ::Models::Edis.generate_form(
16
+ # isin: "INE155A01022",
17
+ # qty: 10,
18
+ # exchange: "NSE",
19
+ # segment: "E",
20
+ # bulk: false
21
+ # )
22
+ #
23
+ # @example Check EDIS status for a security
24
+ # status = DhanHQ::Models::Edis.inquire(isin: "INE155A01022")
25
+ #
26
+ class Edis < BaseModel
27
+ HTTP_PATH = "/edis"
28
+
29
+ class << self
30
+ ##
31
+ # Provides a shared instance of the Edis resource.
32
+ #
33
+ # @return [DhanHQ::Resources::Edis] The Edis resource client instance
34
+ def resource
35
+ @resource ||= DhanHQ::Resources::Edis.new
36
+ end
37
+
38
+ ##
39
+ # Generate T-PIN for eDIS authorization.
40
+ #
41
+ # Triggers T-PIN generation which is sent to the user's registered mobile/email.
42
+ #
43
+ # @return [Hash] API response
44
+ #
45
+ # @example Generate T-PIN before selling holdings
46
+ # DhanHQ::Models::Edis.generate_tpin
47
+ #
48
+ def generate_tpin
49
+ resource.tpin
50
+ end
51
+
52
+ ##
53
+ # Generate an eDIS form for authorizing sale of holdings.
54
+ #
55
+ # @param isin [String] ISIN of the security (e.g., "INE155A01022")
56
+ # @param qty [Integer] Quantity to authorize for sale
57
+ # @param exchange [String] Exchange name (e.g., "NSE", "BSE")
58
+ # @param segment [String] Segment identifier (e.g., "E")
59
+ # @param bulk [Boolean] Whether this is a bulk authorization (default: false)
60
+ #
61
+ # @return [Hash] API response containing the eDIS form data
62
+ #
63
+ # @example Authorize sale of 10 shares
64
+ # DhanHQ::Models::Edis.generate_form(
65
+ # isin: "INE155A01022",
66
+ # qty: 10,
67
+ # exchange: "NSE",
68
+ # segment: "E"
69
+ # )
70
+ #
71
+ def generate_form(isin:, qty:, exchange:, segment:, bulk: false)
72
+ resource.form({ isin: isin, qty: qty, exchange: exchange, segment: segment, bulk: bulk })
73
+ end
74
+
75
+ ##
76
+ # Generate a bulk eDIS form for multiple securities.
77
+ #
78
+ # @param params [Hash] Bulk form parameters
79
+ # @return [Hash] API response
80
+ #
81
+ def generate_bulk_form(params)
82
+ resource.bulk_form(params)
83
+ end
84
+
85
+ ##
86
+ # Check EDIS authorization status for a security.
87
+ #
88
+ # @param isin [String] ISIN of the security to check
89
+ #
90
+ # @return [Hash] API response containing authorization status
91
+ #
92
+ # @example Check if EDIS is authorized
93
+ # status = DhanHQ::Models::Edis.inquire(isin: "INE155A01022")
94
+ #
95
+ def inquire(isin:)
96
+ resource.inquire(isin)
97
+ end
98
+ end
99
+
100
+ ##
101
+ # No validation contract needed — EDIS operations are simple API calls.
102
+ #
103
+ # @return [nil]
104
+ # @api private
105
+ def validation_contract
106
+ nil
107
+ end
108
+ end
109
+ end
110
+ end
@@ -132,6 +132,28 @@ 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
  ##
@@ -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
  ##
@@ -0,0 +1,130 @@
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
+ resource.configure(params)
79
+ end
80
+
81
+ ##
82
+ # Stop/disable the active P&L-based exit configuration.
83
+ #
84
+ # @return [Hash{Symbol => String}] Response hash containing:
85
+ # - **:pnl_exit_status** [String] "DISABLED"
86
+ # - **:message** [String] Confirmation message
87
+ #
88
+ # @example Disable P&L exit
89
+ # response = DhanHQ::Models::PnlExit.stop
90
+ # puts response[:pnl_exit_status] # => "DISABLED"
91
+ #
92
+ def stop
93
+ resource.stop
94
+ end
95
+
96
+ ##
97
+ # Fetch the currently active P&L-based exit configuration.
98
+ #
99
+ # @return [PnlExit] PnlExit object with current configuration.
100
+ # - **:pnl_exit_status** [String] "ACTIVE" or "DISABLED"
101
+ # - **:profit** [String] Configured profit threshold
102
+ # - **:loss** [String] Configured loss threshold
103
+ # - **:segments** [Array<String>] Active product types
104
+ # - **:enable_kill_switch** [Boolean] Whether kill switch is enabled
105
+ #
106
+ # @example Check configuration
107
+ # config = DhanHQ::Models::PnlExit.status
108
+ # if config.pnl_exit_status == "ACTIVE"
109
+ # puts "P&L exit active: profit=₹#{config.profit}, loss=₹#{config.loss}"
110
+ # end
111
+ #
112
+ def status
113
+ response = resource.status
114
+ return nil unless response.is_a?(Hash)
115
+
116
+ new(response, skip_validation: true)
117
+ end
118
+ 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
+ end
129
+ end
130
+ end
@@ -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,123 @@
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 == "TRADED"
87
+ end
88
+
89
+ ##
90
+ # Whether the order was rejected.
91
+ #
92
+ # @return [Boolean]
93
+ def rejected?
94
+ order_status == "REJECTED"
95
+ end
96
+
97
+ ##
98
+ # Whether the order is still pending.
99
+ #
100
+ # @return [Boolean]
101
+ def pending?
102
+ order_status == "PENDING"
103
+ end
104
+
105
+ ##
106
+ # Whether the order was cancelled.
107
+ #
108
+ # @return [Boolean]
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
120
+ end
121
+ end
122
+ end
123
+ end
@@ -16,6 +16,14 @@ module DhanHQ
16
16
  def update(params)
17
17
  post("", params: params)
18
18
  end
19
+
20
+ ##
21
+ # Fetches the current kill switch status.
22
+ #
23
+ # @return [Hash] API response containing dhan_client_id and kill_switch_status.
24
+ def status
25
+ get("")
26
+ end
19
27
  end
20
28
  end
21
29
  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