DhanHQ 2.1.5 → 2.1.6
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 +7 -0
- data/GUIDE.md +215 -73
- data/README.md +416 -132
- data/README1.md +267 -26
- data/docs/live_order_updates.md +319 -0
- data/docs/rails_websocket_integration.md +847 -0
- data/docs/standalone_ruby_websocket_integration.md +1588 -0
- data/docs/websocket_integration.md +871 -0
- data/examples/comprehensive_websocket_examples.rb +148 -0
- data/examples/instrument_finder_test.rb +195 -0
- data/examples/live_order_updates.rb +118 -0
- data/examples/market_depth_example.rb +144 -0
- data/examples/market_feed_example.rb +81 -0
- data/examples/order_update_example.rb +105 -0
- data/examples/trading_fields_example.rb +215 -0
- data/lib/DhanHQ/configuration.rb +16 -1
- data/lib/DhanHQ/contracts/expired_options_data_contract.rb +103 -0
- data/lib/DhanHQ/contracts/trade_contract.rb +70 -0
- data/lib/DhanHQ/errors.rb +2 -0
- data/lib/DhanHQ/models/expired_options_data.rb +331 -0
- data/lib/DhanHQ/models/instrument.rb +96 -2
- data/lib/DhanHQ/models/order_update.rb +235 -0
- data/lib/DhanHQ/models/trade.rb +118 -31
- data/lib/DhanHQ/resources/expired_options_data.rb +22 -0
- data/lib/DhanHQ/version.rb +1 -1
- data/lib/DhanHQ/ws/base_connection.rb +249 -0
- data/lib/DhanHQ/ws/connection.rb +2 -2
- data/lib/DhanHQ/ws/decoder.rb +3 -3
- data/lib/DhanHQ/ws/market_depth/client.rb +376 -0
- data/lib/DhanHQ/ws/market_depth/decoder.rb +131 -0
- data/lib/DhanHQ/ws/market_depth.rb +74 -0
- data/lib/DhanHQ/ws/orders/client.rb +175 -11
- data/lib/DhanHQ/ws/orders/connection.rb +40 -81
- data/lib/DhanHQ/ws/orders.rb +28 -0
- data/lib/DhanHQ/ws/segments.rb +18 -2
- data/lib/DhanHQ/ws.rb +3 -2
- data/lib/dhan_hq.rb +5 -0
- metadata +35 -1
@@ -0,0 +1,376 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "concurrent"
|
4
|
+
require_relative "../base_connection"
|
5
|
+
require_relative "decoder"
|
6
|
+
require_relative "../../models/instrument"
|
7
|
+
require_relative "../../constants"
|
8
|
+
|
9
|
+
module DhanHQ
|
10
|
+
module WS
|
11
|
+
module MarketDepth
|
12
|
+
##
|
13
|
+
# WebSocket client for Full Market Depth data
|
14
|
+
# Provides real-time market depth (bid/ask levels) for specified symbols
|
15
|
+
class Client < BaseConnection
|
16
|
+
SUBSCRIBE_REQUEST_CODE = 23
|
17
|
+
UNSUBSCRIBE_REQUEST_CODE = 12
|
18
|
+
|
19
|
+
##
|
20
|
+
# Initialize Market Depth WebSocket client
|
21
|
+
# @param symbols [Array<String, Hash>] List of symbols (or metadata hashes) to subscribe to
|
22
|
+
# @param options [Hash] Connection options
|
23
|
+
def initialize(symbols: [], **options)
|
24
|
+
cfg = DhanHQ.configuration
|
25
|
+
url = options[:url] || build_market_depth_url(cfg)
|
26
|
+
super(url: url, **options)
|
27
|
+
|
28
|
+
@symbols = Array(symbols)
|
29
|
+
@subscriptions = Concurrent::Map.new
|
30
|
+
@instrument_cache = Concurrent::Map.new
|
31
|
+
@decoder = Decoder.new
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Start the Market Depth WebSocket connection
|
36
|
+
# @return [Client] self for method chaining
|
37
|
+
def start
|
38
|
+
super
|
39
|
+
subscribe_to_symbols(@symbols) if @symbols.any?
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Subscribe to market depth for specific symbols
|
45
|
+
# @param symbols [Array<String, Hash>] Symbols (or metadata hashes) to subscribe to
|
46
|
+
# @return [Client] self for method chaining
|
47
|
+
def subscribe(symbols)
|
48
|
+
subscribe_to_symbols(Array(symbols))
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Unsubscribe from market depth for specific symbols
|
54
|
+
# @param symbols [Array<String, Hash>] Symbols (or metadata hashes) to unsubscribe from
|
55
|
+
# @return [Client] self for method chaining
|
56
|
+
def unsubscribe(symbols)
|
57
|
+
unsubscribe_from_symbols(Array(symbols))
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# Get current subscriptions
|
63
|
+
# @return [Array<String>] Currently subscribed symbol labels
|
64
|
+
def subscriptions
|
65
|
+
@subscriptions.values.map { |meta| meta[:original_label] }
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Check if subscribed to a symbol
|
70
|
+
# @param symbol [String, Hash] Symbol or metadata hash to check
|
71
|
+
# @return [Boolean] true if subscribed
|
72
|
+
def subscribed?(symbol)
|
73
|
+
@subscriptions.key?(normalize_label(symbol))
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
##
|
79
|
+
# Build Market Depth WebSocket URL
|
80
|
+
# @param config [Configuration] DhanHQ configuration
|
81
|
+
# @return [String] WebSocket URL
|
82
|
+
def build_market_depth_url(config)
|
83
|
+
token = config.access_token or raise "DhanHQ.access_token not set"
|
84
|
+
cid = config.client_id or raise "DhanHQ.client_id not set"
|
85
|
+
depth_level = config.market_depth_level || 20 # Default to 20 level depth
|
86
|
+
|
87
|
+
if depth_level == 200
|
88
|
+
"wss://full-depth-api.dhan.co/twohundreddepth?token=#{token}&clientId=#{cid}&authType=2"
|
89
|
+
else
|
90
|
+
"wss://depth-api-feed.dhan.co/twentydepth?token=#{token}&clientId=#{cid}&authType=2"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
##
|
95
|
+
# Run WebSocket session for Market Depth
|
96
|
+
# @return [Array<Boolean>] [failed, got_429]
|
97
|
+
def run_session
|
98
|
+
failed = false
|
99
|
+
got_429 = false
|
100
|
+
|
101
|
+
EM.run do
|
102
|
+
@ws = Faye::WebSocket::Client.new(@url, nil, headers: default_headers)
|
103
|
+
|
104
|
+
@ws.on :open do |_|
|
105
|
+
handle_open
|
106
|
+
end
|
107
|
+
|
108
|
+
@ws.on :message do |ev|
|
109
|
+
handle_message(ev)
|
110
|
+
end
|
111
|
+
|
112
|
+
@ws.on :close do |ev|
|
113
|
+
failed, got_429 = handle_close(ev)
|
114
|
+
EM.stop
|
115
|
+
end
|
116
|
+
|
117
|
+
@ws.on :error do |ev|
|
118
|
+
failed, got_429 = handle_error(ev)
|
119
|
+
EM.stop
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
[failed, got_429]
|
124
|
+
end
|
125
|
+
|
126
|
+
##
|
127
|
+
# Process incoming WebSocket message
|
128
|
+
# @param data [String] Raw message data
|
129
|
+
def process_message(data)
|
130
|
+
depth_data = @decoder.decode(data)
|
131
|
+
return unless depth_data
|
132
|
+
|
133
|
+
emit(:depth_update, depth_data)
|
134
|
+
emit(:raw_depth, data) # For debugging
|
135
|
+
rescue StandardError => e
|
136
|
+
DhanHQ.logger&.error("[DhanHQ::WS::MarketDepth] Error processing message: #{e.class} #{e.message}")
|
137
|
+
emit(:error, e)
|
138
|
+
end
|
139
|
+
|
140
|
+
##
|
141
|
+
# Subscribe to symbols
|
142
|
+
# @param symbols [Array<String, Hash>] Symbols to subscribe to
|
143
|
+
def subscribe_to_symbols(symbols)
|
144
|
+
symbols.each do |symbol|
|
145
|
+
resolution = resolve_symbol(symbol)
|
146
|
+
next unless resolution
|
147
|
+
|
148
|
+
label = resolution[:label]
|
149
|
+
next if @subscriptions.key?(label)
|
150
|
+
|
151
|
+
subscription_message = {
|
152
|
+
"RequestCode" => SUBSCRIBE_REQUEST_CODE,
|
153
|
+
"InstrumentCount" => 1,
|
154
|
+
"InstrumentList" => [
|
155
|
+
{
|
156
|
+
"ExchangeSegment" => resolution[:exchange_segment],
|
157
|
+
"SecurityId" => resolution[:security_id]
|
158
|
+
}
|
159
|
+
]
|
160
|
+
}
|
161
|
+
|
162
|
+
send_message(subscription_message)
|
163
|
+
@subscriptions[label] = resolution
|
164
|
+
DhanHQ.logger&.info("[DhanHQ::WS::MarketDepth] Subscribed to #{resolution[:original_label]} (#{resolution[:exchange_segment]}:#{resolution[:security_id]})")
|
165
|
+
rescue StandardError => e
|
166
|
+
DhanHQ.logger&.error("[DhanHQ::WS::MarketDepth] Subscription error for #{symbol.inspect}: #{e.class} #{e.message}")
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
##
|
171
|
+
# Unsubscribe from symbols
|
172
|
+
# @param symbols [Array<String, Hash>] Symbols to unsubscribe from
|
173
|
+
def unsubscribe_from_symbols(symbols)
|
174
|
+
symbols.each do |symbol|
|
175
|
+
label = normalize_label(symbol)
|
176
|
+
security_data = @subscriptions[label]
|
177
|
+
next unless security_data
|
178
|
+
|
179
|
+
unsubscribe_message = {
|
180
|
+
"RequestCode" => UNSUBSCRIBE_REQUEST_CODE,
|
181
|
+
"InstrumentCount" => 1,
|
182
|
+
"InstrumentList" => [
|
183
|
+
{
|
184
|
+
"ExchangeSegment" => security_data[:exchange_segment],
|
185
|
+
"SecurityId" => security_data[:security_id]
|
186
|
+
}
|
187
|
+
]
|
188
|
+
}
|
189
|
+
|
190
|
+
send_message(unsubscribe_message)
|
191
|
+
@subscriptions.delete(label)
|
192
|
+
DhanHQ.logger&.info("[DhanHQ::WS::MarketDepth] Unsubscribed from #{security_data[:original_label]} (#{security_data[:exchange_segment]}:#{security_data[:security_id]})")
|
193
|
+
rescue StandardError => e
|
194
|
+
DhanHQ.logger&.error("[DhanHQ::WS::MarketDepth] Unsubscribe error for #{symbol.inspect}: #{e.class} #{e.message}")
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
##
|
199
|
+
# Resolve symbol to security_id and exchange_segment using the instrument API
|
200
|
+
# @param symbol [String, Hash] Trading symbol or metadata hash
|
201
|
+
# @return [Hash, nil] Resolved instrument metadata
|
202
|
+
def resolve_symbol(symbol)
|
203
|
+
label = normalize_label(symbol)
|
204
|
+
|
205
|
+
return @subscriptions[label] if @subscriptions.key?(label)
|
206
|
+
|
207
|
+
from_hash = resolve_hash_symbol(symbol, label)
|
208
|
+
return from_hash if from_hash
|
209
|
+
|
210
|
+
symbol_str = symbol.to_s
|
211
|
+
segment_hint, symbol_code = extract_segment_and_symbol(symbol_str)
|
212
|
+
return nil unless symbol_code && !symbol_code.strip.empty?
|
213
|
+
|
214
|
+
instrument = find_instrument(symbol_code, segment_hint)
|
215
|
+
unless instrument
|
216
|
+
DhanHQ.logger&.warn("[DhanHQ::WS::MarketDepth] Unable to locate instrument for #{symbol_code} (segment hint: #{segment_hint || 'AUTO'})")
|
217
|
+
return nil
|
218
|
+
end
|
219
|
+
|
220
|
+
build_resolution(instrument, label, symbol_str)
|
221
|
+
end
|
222
|
+
|
223
|
+
def resolve_hash_symbol(symbol, label)
|
224
|
+
return nil unless symbol.is_a?(Hash)
|
225
|
+
|
226
|
+
exchange_segment = symbol[:exchange_segment] || symbol[:segment] || symbol[:exchange]
|
227
|
+
security_id = symbol[:security_id] || symbol[:token]
|
228
|
+
normalized_segment = exchange_segment&.to_s&.strip&.upcase
|
229
|
+
|
230
|
+
if security_id && normalized_segment
|
231
|
+
original_label = symbol[:symbol] || symbol[:symbol_name] || symbol[:name] || security_id
|
232
|
+
return {
|
233
|
+
label: label,
|
234
|
+
original_label: original_label.to_s,
|
235
|
+
exchange_segment: normalized_segment,
|
236
|
+
security_id: security_id.to_s,
|
237
|
+
display_name: symbol[:display_name],
|
238
|
+
symbol: original_label.to_s,
|
239
|
+
original_input: symbol
|
240
|
+
}
|
241
|
+
end
|
242
|
+
|
243
|
+
if security_id
|
244
|
+
instrument = find_instrument(security_id.to_s, normalized_segment)
|
245
|
+
return build_resolution(instrument, label, security_id) if instrument
|
246
|
+
end
|
247
|
+
|
248
|
+
symbol_code = symbol[:symbol] || symbol[:symbol_name] || symbol[:name]
|
249
|
+
return nil unless symbol_code
|
250
|
+
|
251
|
+
instrument = find_instrument(symbol_code.to_s, normalized_segment)
|
252
|
+
build_resolution(instrument, label, symbol_code) if instrument
|
253
|
+
end
|
254
|
+
|
255
|
+
def build_resolution(instrument, label, original_label)
|
256
|
+
return nil unless instrument
|
257
|
+
|
258
|
+
fallback_label = original_label.to_s.strip.empty? ? instrument.symbol_name.to_s : original_label.to_s
|
259
|
+
|
260
|
+
{
|
261
|
+
label: label,
|
262
|
+
original_label: fallback_label,
|
263
|
+
exchange_segment: instrument.exchange_segment.to_s,
|
264
|
+
security_id: instrument.security_id.to_s,
|
265
|
+
display_name: instrument.display_name,
|
266
|
+
symbol: instrument.symbol_name,
|
267
|
+
original_input: original_label
|
268
|
+
}
|
269
|
+
end
|
270
|
+
|
271
|
+
def extract_segment_and_symbol(symbol)
|
272
|
+
parts = symbol.to_s.split(":", 2).map(&:strip)
|
273
|
+
if parts.size == 2
|
274
|
+
[parts[0].upcase, parts[1]]
|
275
|
+
else
|
276
|
+
[nil, symbol.strip]
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def normalize_label(symbol)
|
281
|
+
case symbol
|
282
|
+
when Hash
|
283
|
+
symbol_label_from_hash(symbol)
|
284
|
+
else
|
285
|
+
symbol.to_s.strip.upcase
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def symbol_label_from_hash(symbol)
|
290
|
+
seg = symbol[:exchange_segment] || symbol[:segment] || symbol[:exchange]
|
291
|
+
sym = symbol[:symbol] || symbol[:symbol_name] || symbol[:name] || symbol[:security_id]
|
292
|
+
base_label = sym ? sym.to_s : symbol[:security_id].to_s
|
293
|
+
label = seg ? "#{seg}:#{base_label}" : base_label
|
294
|
+
label.strip.upcase
|
295
|
+
end
|
296
|
+
|
297
|
+
def find_instrument(symbol_code, segment_hint)
|
298
|
+
candidates = if segment_hint
|
299
|
+
[segment_hint.to_s.strip.upcase]
|
300
|
+
else
|
301
|
+
segment_priority
|
302
|
+
end
|
303
|
+
|
304
|
+
normalized_code = symbol_code.to_s.strip.upcase
|
305
|
+
candidates.each do |segment|
|
306
|
+
next if segment.nil? || segment.empty?
|
307
|
+
|
308
|
+
instrument = instrument_index(segment)[normalized_code]
|
309
|
+
return instrument if instrument
|
310
|
+
|
311
|
+
alt_code = normalized_code.gsub(/\s+/, " ")
|
312
|
+
instrument = instrument_index(segment)[alt_code]
|
313
|
+
return instrument if instrument
|
314
|
+
end
|
315
|
+
|
316
|
+
nil
|
317
|
+
end
|
318
|
+
|
319
|
+
def instrument_index(segment)
|
320
|
+
@instrument_cache.compute_if_absent(segment) do
|
321
|
+
build_instrument_index(segment)
|
322
|
+
end
|
323
|
+
rescue NoMethodError
|
324
|
+
# Concurrent::Map#compute_if_absent is available on recent versions.
|
325
|
+
# Fallback to fetch-or-store semantics if absent.
|
326
|
+
@instrument_cache[segment] ||= build_instrument_index(segment)
|
327
|
+
rescue StandardError => e
|
328
|
+
DhanHQ.logger&.error("[DhanHQ::WS::MarketDepth] Instrument index error for #{segment}: #{e.class} #{e.message}")
|
329
|
+
{}
|
330
|
+
end
|
331
|
+
|
332
|
+
def build_instrument_index(segment)
|
333
|
+
records = DhanHQ::Models::Instrument.by_segment(segment)
|
334
|
+
index = {}
|
335
|
+
records.each do |instrument|
|
336
|
+
keys_for(instrument).each do |key|
|
337
|
+
index[key] ||= instrument
|
338
|
+
end
|
339
|
+
end
|
340
|
+
index
|
341
|
+
rescue StandardError => e
|
342
|
+
DhanHQ.logger&.error("[DhanHQ::WS::MarketDepth] Failed to download instruments for #{segment}: #{e.class} #{e.message}")
|
343
|
+
{}
|
344
|
+
end
|
345
|
+
|
346
|
+
def keys_for(instrument)
|
347
|
+
keys = []
|
348
|
+
symbol_name = instrument.symbol_name.to_s.strip.upcase
|
349
|
+
display_name = instrument.display_name.to_s.strip.upcase
|
350
|
+
security_id = instrument.security_id.to_s.strip.upcase
|
351
|
+
keys << symbol_name unless symbol_name.empty?
|
352
|
+
keys << display_name unless display_name.empty? || display_name == symbol_name
|
353
|
+
keys << security_id unless security_id.empty?
|
354
|
+
if instrument.respond_to?(:series) && instrument.series
|
355
|
+
series_key = "#{symbol_name}:#{instrument.series}".strip.upcase
|
356
|
+
keys << series_key unless series_key.empty?
|
357
|
+
end
|
358
|
+
keys
|
359
|
+
end
|
360
|
+
|
361
|
+
def segment_priority
|
362
|
+
@segment_priority ||= begin
|
363
|
+
preferred = [
|
364
|
+
DhanHQ::Constants::NSE,
|
365
|
+
DhanHQ::Constants::BSE,
|
366
|
+
DhanHQ::Constants::NSE_FNO,
|
367
|
+
DhanHQ::Constants::BSE_FNO,
|
368
|
+
DhanHQ::Constants::INDEX
|
369
|
+
]
|
370
|
+
(preferred + DhanHQ::Constants::EXCHANGE_SEGMENTS).compact.map(&:to_s).map(&:upcase).uniq
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DhanHQ
|
4
|
+
module WS
|
5
|
+
module MarketDepth
|
6
|
+
##
|
7
|
+
# Decoder for Market Depth WebSocket messages
|
8
|
+
# Handles parsing of market depth data (bid/ask levels)
|
9
|
+
class Decoder
|
10
|
+
##
|
11
|
+
# Decode raw WebSocket message to market depth data
|
12
|
+
# @param data [String] Raw WebSocket message
|
13
|
+
# @return [Hash, nil] Parsed market depth data or nil if invalid
|
14
|
+
def decode(data)
|
15
|
+
return nil if data.nil? || data.empty?
|
16
|
+
|
17
|
+
begin
|
18
|
+
# Parse JSON message
|
19
|
+
message = JSON.parse(data)
|
20
|
+
|
21
|
+
# Handle different message types
|
22
|
+
case message["Type"]
|
23
|
+
when "depth_update"
|
24
|
+
parse_depth_update(message["Data"])
|
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
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
##
|
43
|
+
# Parse depth update message
|
44
|
+
# @param data [Hash] Message data
|
45
|
+
# @return [Hash] Parsed depth update
|
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
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Parse depth snapshot message
|
65
|
+
# @param data [Hash] Message data
|
66
|
+
# @return [Hash] Parsed depth snapshot
|
67
|
+
def parse_depth_snapshot(data)
|
68
|
+
{
|
69
|
+
type: :depth_snapshot,
|
70
|
+
symbol: data["Symbol"],
|
71
|
+
exchange_segment: data["ExchangeSegment"],
|
72
|
+
security_id: data["SecurityId"],
|
73
|
+
timestamp: data["Timestamp"],
|
74
|
+
bids: parse_bid_levels(data["Bids"]),
|
75
|
+
asks: parse_ask_levels(data["Asks"]),
|
76
|
+
best_bid: data["BestBid"],
|
77
|
+
best_ask: data["BestAsk"],
|
78
|
+
spread: calculate_spread(data["BestBid"], data["BestAsk"]),
|
79
|
+
total_bid_qty: data["TotalBidQty"],
|
80
|
+
total_ask_qty: data["TotalAskQty"]
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# Parse bid levels
|
86
|
+
# @param bids [Array] Raw bid levels
|
87
|
+
# @return [Array<Hash>] Parsed bid levels
|
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
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# Parse ask levels
|
102
|
+
# @param asks [Array] Raw ask levels
|
103
|
+
# @return [Array<Hash>] Parsed ask levels
|
104
|
+
def parse_ask_levels(asks)
|
105
|
+
return [] unless asks.is_a?(Array)
|
106
|
+
|
107
|
+
asks.map do |ask|
|
108
|
+
{
|
109
|
+
price: ask["Price"].to_f,
|
110
|
+
quantity: ask["Quantity"].to_i,
|
111
|
+
orders: ask["Orders"] || 1
|
112
|
+
}
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
##
|
117
|
+
# Calculate spread between best bid and ask
|
118
|
+
# @param best_bid [Float, String] Best bid price
|
119
|
+
# @param best_ask [Float, String] Best ask price
|
120
|
+
# @return [Float] Spread amount
|
121
|
+
def calculate_spread(best_bid, best_ask)
|
122
|
+
bid = best_bid.to_f
|
123
|
+
ask = best_ask.to_f
|
124
|
+
return 0.0 if bid.zero? || ask.zero?
|
125
|
+
|
126
|
+
ask - bid
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "market_depth/client"
|
4
|
+
|
5
|
+
module DhanHQ
|
6
|
+
module WS
|
7
|
+
module MarketDepth
|
8
|
+
##
|
9
|
+
# Market Depth WebSocket module for real-time market depth data
|
10
|
+
# Provides access to bid/ask levels and order book depth
|
11
|
+
|
12
|
+
module_function
|
13
|
+
|
14
|
+
##
|
15
|
+
# Connect to Market Depth WebSocket with a simple callback
|
16
|
+
# @param symbols [Array<String>] Symbols to subscribe to
|
17
|
+
# @param options [Hash] Connection options
|
18
|
+
# @param block [Proc] Callback for depth updates
|
19
|
+
# @return [Client] WebSocket client instance
|
20
|
+
def connect(symbols: [], **options, &block)
|
21
|
+
client = Client.new(symbols: symbols, **options)
|
22
|
+
client.on(:depth_update, &block) if block_given?
|
23
|
+
client.start
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# Create a new Market Depth client with advanced features
|
28
|
+
# @param symbols [Array<String>] Symbols to subscribe to
|
29
|
+
# @param options [Hash] Connection options
|
30
|
+
# @return [Client] New client instance
|
31
|
+
def client(symbols: [], **options)
|
32
|
+
Client.new(symbols: symbols, **options)
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Quick connection with multiple event handlers
|
37
|
+
# @param symbols [Array<String>] Symbols to subscribe to
|
38
|
+
# @param handlers [Hash] Event handlers
|
39
|
+
# @param options [Hash] Connection options
|
40
|
+
# @return [Client] Started client instance
|
41
|
+
def connect_with_handlers(symbols: [], handlers: {}, **options)
|
42
|
+
client = Client.new(symbols: symbols, **options).start
|
43
|
+
|
44
|
+
handlers.each do |event, handler|
|
45
|
+
client.on(event, &handler)
|
46
|
+
end
|
47
|
+
|
48
|
+
client
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Subscribe to market depth for specific symbols
|
53
|
+
# @param symbols [Array<String>] Symbols to subscribe to
|
54
|
+
# @param options [Hash] Connection options
|
55
|
+
# @param block [Proc] Callback for depth updates
|
56
|
+
# @return [Client] Started client instance
|
57
|
+
def subscribe(symbols:, **options, &block)
|
58
|
+
connect(symbols: symbols, **options, &block)
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# Get market depth snapshot for symbols
|
63
|
+
# @param symbols [Array<String>] Symbols to get snapshot for
|
64
|
+
# @param options [Hash] Connection options
|
65
|
+
# @param block [Proc] Callback for snapshot data
|
66
|
+
# @return [Client] Started client instance
|
67
|
+
def snapshot(symbols:, **options, &block)
|
68
|
+
client = Client.new(symbols: symbols, **options)
|
69
|
+
client.on(:depth_snapshot, &block) if block_given?
|
70
|
+
client.start
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|