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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -1
- data/README.md +194 -752
- data/docs/AUTHENTICATION.md +71 -9
- data/docs/CONFIGURATION.md +109 -0
- data/docs/SUPER_ORDERS.md +284 -0
- data/docs/TROUBLESHOOTING.md +117 -0
- data/docs/WEBSOCKET_PROTOCOL.md +154 -0
- data/lib/DhanHQ/models/alert_order.rb +22 -0
- data/lib/DhanHQ/models/edis.rb +110 -0
- data/lib/DhanHQ/models/kill_switch.rb +22 -0
- data/lib/DhanHQ/models/margin.rb +49 -0
- data/lib/DhanHQ/models/pnl_exit.rb +130 -0
- data/lib/DhanHQ/models/position.rb +22 -0
- data/lib/DhanHQ/models/postback.rb +123 -0
- data/lib/DhanHQ/resources/kill_switch.rb +8 -0
- data/lib/DhanHQ/resources/margin_calculator.rb +9 -0
- data/lib/DhanHQ/resources/pnl_exit.rb +37 -0
- data/lib/DhanHQ/resources/positions.rb +8 -0
- data/lib/DhanHQ/version.rb +1 -1
- metadata +11 -3
- /data/{README1.md → docs/ARCHIVE_README.md} +0 -0
|
@@ -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
|
##
|
data/lib/DhanHQ/models/margin.rb
CHANGED
|
@@ -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
|