DhanHQ 4.1.0 → 4.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +47 -0
- data/GUIDE.md +3 -0
- data/README.md +17 -3
- data/docs/AUTHENTICATION.md +13 -2
- data/docs/CONFIGURATION.md +7 -0
- data/docs/ENDPOINTS_AND_SANDBOX.md +35 -32
- data/lib/DhanHQ/auth.rb +58 -29
- data/lib/DhanHQ/client.rb +25 -0
- data/lib/DhanHQ/concerns/order_audit.rb +13 -1
- data/lib/DhanHQ/configuration.rb +79 -0
- data/lib/DhanHQ/constants.rb +27 -1
- data/lib/DhanHQ/errors.rb +6 -0
- data/lib/DhanHQ/helpers/request_helper.rb +35 -6
- data/lib/DhanHQ/helpers/response_helper.rb +1 -1
- data/lib/DhanHQ/models/iceberg_order.rb +1 -1
- data/lib/DhanHQ/models/super_order.rb +1 -1
- data/lib/DhanHQ/models/twap_order.rb +1 -1
- data/lib/DhanHQ/resources/instruments.rb +30 -0
- data/lib/DhanHQ/resources/margin_calculator.rb +4 -1
- data/lib/DhanHQ/risk/checks/asm_gsm.rb +4 -1
- data/lib/DhanHQ/risk/checks/concentration.rb +80 -11
- data/lib/DhanHQ/risk/checks/max_loss.rb +11 -6
- data/lib/DhanHQ/risk/checks/options.rb +4 -1
- data/lib/DhanHQ/risk/checks/order_type.rb +3 -1
- data/lib/DhanHQ/risk/checks/position_limits.rb +7 -4
- data/lib/DhanHQ/risk/checks/quantity.rb +6 -3
- data/lib/DhanHQ/risk/exposure.rb +48 -0
- data/lib/DhanHQ/risk/limits.rb +75 -0
- data/lib/DhanHQ/risk/pipeline.rb +17 -9
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/market_depth/client.rb +83 -24
- data/lib/DhanHQ/ws/market_depth/decoder.rb +109 -102
- data/lib/dhan_hq.rb +2 -0
- metadata +4 -2
|
@@ -13,12 +13,17 @@ module DhanHQ
|
|
|
13
13
|
# WebSocket client for Full Market Depth data
|
|
14
14
|
# Provides real-time market depth (bid/ask levels) for specified symbols
|
|
15
15
|
class Client < BaseConnection
|
|
16
|
-
# Feed request codes per the
|
|
17
|
-
# (dhanhq.co
|
|
18
|
-
# Market Depth,
|
|
19
|
-
# the whole-connection disconnect, not a
|
|
16
|
+
# Feed request codes per the Annexure "Feed Request Code" table and the
|
|
17
|
+
# Full Market Depth "Feed Disconnect" section (docs.dhanhq.co):
|
|
18
|
+
# 23 = Subscribe - Full Market Depth, 25 = Unsubscribe - Full Market Depth.
|
|
19
|
+
# Never 12 - that is the whole-connection disconnect, not a
|
|
20
|
+
# per-instrument unsubscribe.
|
|
20
21
|
SUBSCRIBE_REQUEST_CODE = 23
|
|
21
|
-
UNSUBSCRIBE_REQUEST_CODE =
|
|
22
|
+
UNSUBSCRIBE_REQUEST_CODE = 25
|
|
23
|
+
|
|
24
|
+
# Instruments one connection may carry: 50 for 20-level depth, exactly
|
|
25
|
+
# one for 200-level depth.
|
|
26
|
+
MAX_INSTRUMENTS = { 20 => 50, 200 => 1 }.freeze
|
|
22
27
|
|
|
23
28
|
##
|
|
24
29
|
# Initialize Market Depth WebSocket client
|
|
@@ -30,10 +35,12 @@ module DhanHQ
|
|
|
30
35
|
url = options[:url] || build_market_depth_url(cfg)
|
|
31
36
|
super(url: url, config: cfg, **options)
|
|
32
37
|
|
|
33
|
-
@symbols =
|
|
38
|
+
@symbols = symbol_list(symbols)
|
|
34
39
|
@subscriptions = Concurrent::Map.new
|
|
35
40
|
@instrument_cache = Concurrent::Map.new
|
|
36
|
-
@
|
|
41
|
+
@depth_level = cfg.market_depth_level.to_i == 200 ? 200 : 20
|
|
42
|
+
@decoder = Decoder.new(depth_level: @depth_level)
|
|
43
|
+
@books = {}
|
|
37
44
|
end
|
|
38
45
|
|
|
39
46
|
##
|
|
@@ -50,7 +57,7 @@ module DhanHQ
|
|
|
50
57
|
# @param symbols [Array<String, Hash>] Symbols (or metadata hashes) to subscribe to
|
|
51
58
|
# @return [Client] self for method chaining
|
|
52
59
|
def subscribe(symbols)
|
|
53
|
-
subscribe_to_symbols(
|
|
60
|
+
subscribe_to_symbols(symbol_list(symbols))
|
|
54
61
|
self
|
|
55
62
|
end
|
|
56
63
|
|
|
@@ -59,7 +66,7 @@ module DhanHQ
|
|
|
59
66
|
# @param symbols [Array<String, Hash>] Symbols (or metadata hashes) to unsubscribe from
|
|
60
67
|
# @return [Client] self for method chaining
|
|
61
68
|
def unsubscribe(symbols)
|
|
62
|
-
unsubscribe_from_symbols(
|
|
69
|
+
unsubscribe_from_symbols(symbol_list(symbols))
|
|
63
70
|
self
|
|
64
71
|
end
|
|
65
72
|
|
|
@@ -135,16 +142,77 @@ module DhanHQ
|
|
|
135
142
|
# Process incoming WebSocket message
|
|
136
143
|
# @param data [String] Raw message data
|
|
137
144
|
def process_message(data)
|
|
138
|
-
|
|
139
|
-
|
|
145
|
+
touched = []
|
|
146
|
+
@decoder.decode(data).each do |packet|
|
|
147
|
+
next handle_disconnect_packet(packet) if packet[:type] == :disconnect
|
|
148
|
+
|
|
149
|
+
key = [packet[:exchange_segment], packet[:security_id]]
|
|
150
|
+
book = (@books[key] ||= { bids: [], asks: [] })
|
|
151
|
+
book[packet[:type] == :bid ? :bids : :asks] = packet[:levels]
|
|
152
|
+
touched << key unless touched.include?(key)
|
|
153
|
+
end
|
|
140
154
|
|
|
141
|
-
emit(:depth_update,
|
|
155
|
+
touched.each { |key| emit(:depth_update, depth_snapshot(*key)) }
|
|
142
156
|
emit(:raw_depth, data) # For debugging
|
|
143
157
|
rescue StandardError => e
|
|
144
158
|
DhanHQ.logger&.error("[DhanHQ::WS::MarketDepth] Error processing message: #{e.class} #{e.message}")
|
|
145
159
|
emit(:error, e)
|
|
146
160
|
end
|
|
147
161
|
|
|
162
|
+
# A single instrument hash is one symbol; Array(hash) would split it into pairs.
|
|
163
|
+
def symbol_list(symbols)
|
|
164
|
+
symbols.is_a?(Hash) ? [symbols] : Array(symbols)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Merged order book for one instrument, combining the latest bid and ask
|
|
168
|
+
# packets (they arrive separately on the wire).
|
|
169
|
+
def depth_snapshot(segment, security_id)
|
|
170
|
+
book = @books.fetch([segment, security_id])
|
|
171
|
+
meta = @subscriptions.values.find { |m| m[:security_id] == security_id && m[:exchange_segment] == segment }
|
|
172
|
+
best_bid = book[:bids].first&.fetch(:price)
|
|
173
|
+
best_ask = book[:asks].first&.fetch(:price)
|
|
174
|
+
|
|
175
|
+
{
|
|
176
|
+
type: :depth_update,
|
|
177
|
+
depth_level: @depth_level,
|
|
178
|
+
symbol: meta&.fetch(:symbol, nil),
|
|
179
|
+
exchange_segment: segment,
|
|
180
|
+
security_id: security_id,
|
|
181
|
+
timestamp: Time.now.to_i,
|
|
182
|
+
bids: book[:bids],
|
|
183
|
+
asks: book[:asks],
|
|
184
|
+
best_bid: best_bid,
|
|
185
|
+
best_ask: best_ask,
|
|
186
|
+
spread: best_bid && best_ask ? best_ask - best_bid : nil,
|
|
187
|
+
total_bid_qty: book[:bids].sum { |level| level[:quantity] },
|
|
188
|
+
total_ask_qty: book[:asks].sum { |level| level[:quantity] }
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def handle_disconnect_packet(packet)
|
|
193
|
+
DhanHQ.logger&.warn("[DhanHQ::WS::MarketDepth] Server disconnect packet, reason code #{packet[:code]}")
|
|
194
|
+
emit(:disconnect, packet)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# 20-level takes an instrument list; 200-level takes one flat instrument
|
|
198
|
+
# (docs.dhanhq.co Full Market Depth, "Adding Instruments").
|
|
199
|
+
def subscription_message(resolution)
|
|
200
|
+
instrument = { "ExchangeSegment" => resolution[:exchange_segment], "SecurityId" => resolution[:security_id] }
|
|
201
|
+
return { "RequestCode" => SUBSCRIBE_REQUEST_CODE }.merge(instrument) if @depth_level == 200
|
|
202
|
+
|
|
203
|
+
{ "RequestCode" => SUBSCRIBE_REQUEST_CODE, "InstrumentCount" => 1, "InstrumentList" => [instrument] }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def capacity_reached?(resolution)
|
|
207
|
+
return false if @subscriptions.size < MAX_INSTRUMENTS.fetch(@depth_level)
|
|
208
|
+
|
|
209
|
+
DhanHQ.logger&.error(
|
|
210
|
+
"[DhanHQ::WS::MarketDepth] Not subscribing #{resolution[:original_label]}: #{@depth_level}-level depth " \
|
|
211
|
+
"allows #{MAX_INSTRUMENTS.fetch(@depth_level)} instrument(s) per connection"
|
|
212
|
+
)
|
|
213
|
+
true
|
|
214
|
+
end
|
|
215
|
+
|
|
148
216
|
##
|
|
149
217
|
# Subscribe to symbols
|
|
150
218
|
# @param symbols [Array<String, Hash>] Symbols to subscribe to
|
|
@@ -155,19 +223,9 @@ module DhanHQ
|
|
|
155
223
|
|
|
156
224
|
label = resolution[:label]
|
|
157
225
|
next if @subscriptions.key?(label)
|
|
226
|
+
next if capacity_reached?(resolution)
|
|
158
227
|
|
|
159
|
-
subscription_message
|
|
160
|
-
"RequestCode" => SUBSCRIBE_REQUEST_CODE,
|
|
161
|
-
"InstrumentCount" => 1,
|
|
162
|
-
"InstrumentList" => [
|
|
163
|
-
{
|
|
164
|
-
"ExchangeSegment" => resolution[:exchange_segment],
|
|
165
|
-
"SecurityId" => resolution[:security_id]
|
|
166
|
-
}
|
|
167
|
-
]
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
send_message(subscription_message)
|
|
228
|
+
send_message(subscription_message(resolution))
|
|
171
229
|
@subscriptions[label] = resolution
|
|
172
230
|
DhanHQ.logger&.info(
|
|
173
231
|
"[DhanHQ::WS::MarketDepth] Subscribed to #{resolution[:original_label]} " \
|
|
@@ -200,6 +258,7 @@ module DhanHQ
|
|
|
200
258
|
|
|
201
259
|
send_message(unsubscribe_message)
|
|
202
260
|
@subscriptions.delete(label)
|
|
261
|
+
@books.delete([security_data[:exchange_segment], security_data[:security_id]])
|
|
203
262
|
DhanHQ.logger&.info(
|
|
204
263
|
"[DhanHQ::WS::MarketDepth] Unsubscribed from #{security_data[:original_label]} " \
|
|
205
264
|
"(#{security_data[:exchange_segment]}:#{security_data[:security_id]})"
|
|
@@ -1,129 +1,136 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../segments"
|
|
4
|
+
|
|
3
5
|
module DhanHQ
|
|
4
6
|
module WS
|
|
5
7
|
module MarketDepth
|
|
6
8
|
##
|
|
7
|
-
# Decoder for Market Depth
|
|
8
|
-
#
|
|
9
|
+
# Decoder for the binary 20/200-level Full Market Depth feed.
|
|
10
|
+
#
|
|
11
|
+
# Wire format (docs.dhanhq.co "Full Market Depth", little-endian, no padding):
|
|
12
|
+
#
|
|
13
|
+
# Header (12 bytes)
|
|
14
|
+
# 0-1 int16 message length of the packet
|
|
15
|
+
# 2 byte feed response code: 41 = bid side, 51 = ask side, 50 = disconnect
|
|
16
|
+
# 3 byte exchange segment (Annexure numeric enum)
|
|
17
|
+
# 4-7 int32 security id
|
|
18
|
+
# 8-11 uint32 20-level: message sequence (ignored) / 200-level: number of rows
|
|
19
|
+
# Rows (16 bytes each; 20 for 20-level, "number of rows" for 200-level)
|
|
20
|
+
# 0-7 float64 price
|
|
21
|
+
# 8-11 uint32 quantity
|
|
22
|
+
# 12-15 uint32 number of orders
|
|
23
|
+
#
|
|
24
|
+
# Bid and ask arrive as separate packets, and several packets (per side,
|
|
25
|
+
# per instrument) are stacked in one WebSocket message, so {#decode}
|
|
26
|
+
# walks the frame packet by packet using each header's length.
|
|
9
27
|
class Decoder
|
|
28
|
+
HEADER_SIZE = 12
|
|
29
|
+
ROW_SIZE = 16
|
|
30
|
+
BID_CODE = 41
|
|
31
|
+
ASK_CODE = 51
|
|
32
|
+
DISCONNECT_CODE = 50
|
|
33
|
+
TWENTY_LEVEL_ROWS = 20
|
|
34
|
+
|
|
35
|
+
# @param depth_level [Integer] 20 or 200; decides how header bytes 8-11 are read.
|
|
36
|
+
def initialize(depth_level: 20)
|
|
37
|
+
@depth_level = depth_level.to_i == 200 ? 200 : 20
|
|
38
|
+
end
|
|
39
|
+
|
|
10
40
|
##
|
|
11
|
-
# Decode
|
|
12
|
-
#
|
|
13
|
-
# @
|
|
41
|
+
# Decode one WebSocket message into its side packets.
|
|
42
|
+
#
|
|
43
|
+
# @param data [String, Array<Integer>] Raw frame (Faye delivers binary frames as byte arrays).
|
|
44
|
+
# @return [Array<Hash>] One entry per packet, in wire order. Side packets are
|
|
45
|
+
# +{ type: :bid | :ask, exchange_segment:, security_id:, levels: [{ price:, quantity:, orders: }] }+;
|
|
46
|
+
# a disconnect packet is +{ type: :disconnect, exchange_segment:, security_id:, code: }+.
|
|
47
|
+
# Malformed trailing bytes are dropped (and logged), never raised.
|
|
14
48
|
def decode(data)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
when "depth_snapshot"
|
|
26
|
-
parse_depth_snapshot(message["Data"])
|
|
27
|
-
else
|
|
28
|
-
# Unknown message type
|
|
29
|
-
nil
|
|
30
|
-
end
|
|
31
|
-
rescue JSON::ParserError => e
|
|
32
|
-
DhanHQ.logger&.error("[DhanHQ::WS::MarketDepth::Decoder] JSON parse error: #{e.message}")
|
|
33
|
-
nil
|
|
34
|
-
rescue StandardError => e
|
|
35
|
-
DhanHQ.logger&.error("[DhanHQ::WS::MarketDepth::Decoder] Decode error: #{e.class} #{e.message}")
|
|
36
|
-
nil
|
|
49
|
+
bytes = to_binary(data)
|
|
50
|
+
packets = []
|
|
51
|
+
offset = 0
|
|
52
|
+
|
|
53
|
+
while bytes.bytesize - offset >= HEADER_SIZE
|
|
54
|
+
packet, consumed = read_packet(bytes, offset)
|
|
55
|
+
break unless packet
|
|
56
|
+
|
|
57
|
+
packets << packet
|
|
58
|
+
offset += consumed
|
|
37
59
|
end
|
|
60
|
+
|
|
61
|
+
log_trailing(bytes.bytesize - offset) if offset < bytes.bytesize
|
|
62
|
+
packets
|
|
63
|
+
rescue StandardError => e
|
|
64
|
+
DhanHQ.logger&.error("[DhanHQ::WS::MarketDepth::Decoder] Decode error: #{e.class} #{e.message}")
|
|
65
|
+
[]
|
|
38
66
|
end
|
|
39
67
|
|
|
40
68
|
private
|
|
41
69
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def parse_depth_update(data)
|
|
47
|
-
{
|
|
48
|
-
type: :depth_update,
|
|
49
|
-
symbol: data["Symbol"],
|
|
50
|
-
exchange_segment: data["ExchangeSegment"],
|
|
51
|
-
security_id: data["SecurityId"],
|
|
52
|
-
timestamp: data["Timestamp"],
|
|
53
|
-
bids: parse_bid_levels(data["Bids"]),
|
|
54
|
-
asks: parse_ask_levels(data["Asks"]),
|
|
55
|
-
best_bid: data["BestBid"],
|
|
56
|
-
best_ask: data["BestAsk"],
|
|
57
|
-
spread: calculate_spread(data["BestBid"], data["BestAsk"]),
|
|
58
|
-
total_bid_qty: data["TotalBidQty"],
|
|
59
|
-
total_ask_qty: data["TotalAskQty"]
|
|
60
|
-
}
|
|
70
|
+
def to_binary(data)
|
|
71
|
+
return "".b if data.nil?
|
|
72
|
+
|
|
73
|
+
data.is_a?(Array) ? data.pack("C*") : data.to_s.b
|
|
61
74
|
end
|
|
62
75
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
# @return [Array(Hash, Integer), nil] The packet and the bytes it spans.
|
|
77
|
+
def read_packet(bytes, offset)
|
|
78
|
+
length, code, segment, security_id, tail = bytes.byteslice(offset, HEADER_SIZE).unpack("s<CCl<L<")
|
|
79
|
+
segment_name = Segments.from_code(segment)
|
|
80
|
+
|
|
81
|
+
return [disconnect(bytes, offset, segment_name, security_id), packet_span(length, HEADER_SIZE + 2)] if code == DISCONNECT_CODE
|
|
82
|
+
|
|
83
|
+
unless [BID_CODE, ASK_CODE].include?(code)
|
|
84
|
+
DhanHQ.logger&.warn("[DhanHQ::WS::MarketDepth::Decoder] Unknown response code #{code}; dropping rest of frame")
|
|
85
|
+
return nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
rows = @depth_level == 200 ? tail : TWENTY_LEVEL_ROWS
|
|
89
|
+
span = side_span(length, rows)
|
|
90
|
+
available = [(bytes.bytesize - offset - HEADER_SIZE) / ROW_SIZE, rows, (span - HEADER_SIZE) / ROW_SIZE].min
|
|
91
|
+
|
|
92
|
+
packet = {
|
|
93
|
+
type: code == BID_CODE ? :bid : :ask,
|
|
94
|
+
exchange_segment: segment_name,
|
|
95
|
+
security_id: security_id.to_s,
|
|
96
|
+
levels: read_levels(bytes, offset + HEADER_SIZE, available)
|
|
81
97
|
}
|
|
98
|
+
[packet, span]
|
|
82
99
|
end
|
|
83
100
|
|
|
84
|
-
|
|
85
|
-
#
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def parse_bid_levels(bids)
|
|
89
|
-
return [] unless bids.is_a?(Array)
|
|
90
|
-
|
|
91
|
-
bids.map do |bid|
|
|
92
|
-
{
|
|
93
|
-
price: bid["Price"].to_f,
|
|
94
|
-
quantity: bid["Quantity"].to_i,
|
|
95
|
-
orders: bid["Orders"] || 1
|
|
96
|
-
}
|
|
97
|
-
end
|
|
101
|
+
# Packet span from the header's length field, falling back to the
|
|
102
|
+
# documented fixed size when the field is absent or implausible.
|
|
103
|
+
def packet_span(length, expected)
|
|
104
|
+
length.to_i >= HEADER_SIZE ? length : expected
|
|
98
105
|
end
|
|
99
106
|
|
|
100
|
-
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
def
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
# The docs call the length field "message length of the entire payload
|
|
108
|
+
# packet" without saying whether the 12-byte header is included. A value
|
|
109
|
+
# equal to exactly the rows (rows * 16) is read as payload-only; anything
|
|
110
|
+
# else as the full packet. Both readings stay aligned on stacked frames.
|
|
111
|
+
def side_span(length, rows)
|
|
112
|
+
payload = rows * ROW_SIZE
|
|
113
|
+
return HEADER_SIZE + payload if length.to_i == payload
|
|
114
|
+
|
|
115
|
+
packet_span(length, HEADER_SIZE + payload)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def read_levels(bytes, start, count)
|
|
119
|
+
levels = Array.new([count, 0].max) do |i|
|
|
120
|
+
price, quantity, orders = bytes.byteslice(start + (i * ROW_SIZE), ROW_SIZE).unpack("EL<L<")
|
|
121
|
+
{ price: price, quantity: quantity, orders: orders }
|
|
113
122
|
end
|
|
123
|
+
# Unfilled rows of a partially populated book are zeroed on the wire.
|
|
124
|
+
levels.reject { |level| level[:quantity].zero? && level[:price].zero? }
|
|
114
125
|
end
|
|
115
126
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def
|
|
122
|
-
|
|
123
|
-
ask = best_ask.to_f
|
|
124
|
-
return 0.0 if bid.zero? || ask.zero?
|
|
125
|
-
|
|
126
|
-
ask - bid
|
|
127
|
+
def disconnect(bytes, offset, segment_name, security_id)
|
|
128
|
+
code = bytes.byteslice(offset + HEADER_SIZE, 2)&.unpack1("s<")
|
|
129
|
+
{ type: :disconnect, exchange_segment: segment_name, security_id: security_id.to_s, code: code }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def log_trailing(count)
|
|
133
|
+
DhanHQ.logger&.warn("[DhanHQ::WS::MarketDepth::Decoder] Ignoring #{count} trailing byte(s) in depth frame")
|
|
127
134
|
end
|
|
128
135
|
end
|
|
129
136
|
end
|
data/lib/dhan_hq.rb
CHANGED
|
@@ -62,6 +62,8 @@ module DhanHQ
|
|
|
62
62
|
# Eager-load risk utilities (PositionSizer, SLCalculator, TrailManager)
|
|
63
63
|
# and the pre-execution risk pipeline so they are available immediately.
|
|
64
64
|
require_relative "DhanHQ/risk"
|
|
65
|
+
require_relative "DhanHQ/risk/limits"
|
|
66
|
+
require_relative "DhanHQ/risk/exposure"
|
|
65
67
|
require_relative "DhanHQ/risk/checks/trading_permission"
|
|
66
68
|
require_relative "DhanHQ/risk/checks/asm_gsm"
|
|
67
69
|
require_relative "DhanHQ/risk/checks/product_support"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: DhanHQ
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.1.
|
|
4
|
+
version: 4.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shubham Taywade
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -384,6 +384,8 @@ files:
|
|
|
384
384
|
- lib/DhanHQ/risk/checks/product_support.rb
|
|
385
385
|
- lib/DhanHQ/risk/checks/quantity.rb
|
|
386
386
|
- lib/DhanHQ/risk/checks/trading_permission.rb
|
|
387
|
+
- lib/DhanHQ/risk/exposure.rb
|
|
388
|
+
- lib/DhanHQ/risk/limits.rb
|
|
387
389
|
- lib/DhanHQ/risk/pipeline.rb
|
|
388
390
|
- lib/DhanHQ/skills.rb
|
|
389
391
|
- lib/DhanHQ/skills/base.rb
|