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,1588 @@
|
|
1
|
+
# Standalone Ruby WebSocket Integration Guide
|
2
|
+
|
3
|
+
This guide provides comprehensive instructions for integrating DhanHQ WebSocket connections into standalone Ruby applications, including scripts, daemons, and command-line tools.
|
4
|
+
|
5
|
+
## Table of Contents
|
6
|
+
|
7
|
+
1. [Quick Start](#quick-start)
|
8
|
+
2. [Configuration](#configuration)
|
9
|
+
3. [Script Patterns](#script-patterns)
|
10
|
+
4. [Daemon Integration](#daemon-integration)
|
11
|
+
5. [Command-Line Tools](#command-line-tools)
|
12
|
+
6. [Error Handling](#error-handling)
|
13
|
+
7. [Production Considerations](#production-considerations)
|
14
|
+
8. [Best Practices](#best-practices)
|
15
|
+
|
16
|
+
## Quick Start
|
17
|
+
|
18
|
+
### 1. Install the Gem
|
19
|
+
|
20
|
+
```bash
|
21
|
+
gem install dhan_hq
|
22
|
+
```
|
23
|
+
|
24
|
+
### 2. Basic Configuration
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
#!/usr/bin/env ruby
|
28
|
+
# frozen_string_literal: true
|
29
|
+
|
30
|
+
require 'dhan_hq'
|
31
|
+
|
32
|
+
# Configure DhanHQ
|
33
|
+
DhanHQ.configure do |config|
|
34
|
+
config.client_id = ENV["CLIENT_ID"] || "your_client_id"
|
35
|
+
config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
|
36
|
+
config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
|
37
|
+
end
|
38
|
+
|
39
|
+
# Market Feed WebSocket
|
40
|
+
market_client = DhanHQ::WS.connect(mode: :ticker) do |tick|
|
41
|
+
timestamp = tick[:ts] ? Time.at(tick[:ts]) : Time.now
|
42
|
+
puts "Market Data: #{tick[:segment]}:#{tick[:security_id]} = #{tick[:ltp]} at #{timestamp}"
|
43
|
+
end
|
44
|
+
|
45
|
+
# Subscribe to major indices
|
46
|
+
market_client.subscribe_one(segment: "IDX_I", security_id: "13") # NIFTY
|
47
|
+
market_client.subscribe_one(segment: "IDX_I", security_id: "25") # BANKNIFTY
|
48
|
+
market_client.subscribe_one(segment: "IDX_I", security_id: "29") # NIFTYIT
|
49
|
+
market_client.subscribe_one(segment: "IDX_I", security_id: "51") # SENSEX
|
50
|
+
|
51
|
+
# Wait for data
|
52
|
+
sleep(30)
|
53
|
+
|
54
|
+
# Clean shutdown
|
55
|
+
market_client.stop
|
56
|
+
```
|
57
|
+
|
58
|
+
### 3. Run the Script
|
59
|
+
|
60
|
+
```bash
|
61
|
+
# Set environment variables
|
62
|
+
export CLIENT_ID="your_client_id"
|
63
|
+
export ACCESS_TOKEN="your_access_token"
|
64
|
+
|
65
|
+
# Run the script
|
66
|
+
ruby market_feed_script.rb
|
67
|
+
```
|
68
|
+
|
69
|
+
## Configuration
|
70
|
+
|
71
|
+
### Environment Variables
|
72
|
+
|
73
|
+
```bash
|
74
|
+
# Required
|
75
|
+
export CLIENT_ID="your_client_id"
|
76
|
+
export ACCESS_TOKEN="your_access_token"
|
77
|
+
|
78
|
+
# Optional
|
79
|
+
export DHAN_WS_USER_TYPE="SELF" # or "PARTNER"
|
80
|
+
export DHAN_PARTNER_ID="your_partner_id" # if using PARTNER
|
81
|
+
export DHAN_PARTNER_SECRET="your_partner_secret" # if using PARTNER
|
82
|
+
```
|
83
|
+
|
84
|
+
### Configuration File
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
# config/dhanhq.yml
|
88
|
+
development:
|
89
|
+
client_id: "your_dev_client_id"
|
90
|
+
access_token: "your_dev_access_token"
|
91
|
+
ws_user_type: "SELF"
|
92
|
+
|
93
|
+
production:
|
94
|
+
client_id: "your_prod_client_id"
|
95
|
+
access_token: "your_prod_access_token"
|
96
|
+
ws_user_type: "SELF"
|
97
|
+
```
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
# config/configuration.rb
|
101
|
+
require 'yaml'
|
102
|
+
|
103
|
+
class Configuration
|
104
|
+
def self.load
|
105
|
+
config = YAML.load_file('config/dhanhq.yml')
|
106
|
+
env = ENV['RACK_ENV'] || 'development'
|
107
|
+
config[env]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Usage
|
112
|
+
config = Configuration.load
|
113
|
+
DhanHQ.configure do |c|
|
114
|
+
c.client_id = config['client_id']
|
115
|
+
c.access_token = config['access_token']
|
116
|
+
c.ws_user_type = config['ws_user_type']
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
## Script Patterns
|
121
|
+
|
122
|
+
### Market Feed Script
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
#!/usr/bin/env ruby
|
126
|
+
# frozen_string_literal: true
|
127
|
+
|
128
|
+
require 'dhan_hq'
|
129
|
+
require 'json'
|
130
|
+
|
131
|
+
# Configure DhanHQ
|
132
|
+
DhanHQ.configure do |config|
|
133
|
+
config.client_id = ENV["CLIENT_ID"] || "your_client_id"
|
134
|
+
config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
|
135
|
+
config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
|
136
|
+
end
|
137
|
+
|
138
|
+
class MarketFeedScript
|
139
|
+
def initialize
|
140
|
+
@market_client = nil
|
141
|
+
@running = false
|
142
|
+
end
|
143
|
+
|
144
|
+
def start
|
145
|
+
puts "Starting Market Feed WebSocket..."
|
146
|
+
@running = true
|
147
|
+
|
148
|
+
@market_client = DhanHQ::WS.connect(mode: :ticker) do |tick|
|
149
|
+
process_market_data(tick)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Add error handling
|
153
|
+
@market_client.on(:error) do |error|
|
154
|
+
puts "ā WebSocket Error: #{error}"
|
155
|
+
@running = false
|
156
|
+
end
|
157
|
+
|
158
|
+
@market_client.on(:close) do |close_info|
|
159
|
+
puts "š WebSocket Closed: #{close_info[:code]} - #{close_info[:reason]}"
|
160
|
+
@running = false
|
161
|
+
end
|
162
|
+
|
163
|
+
# Subscribe to indices
|
164
|
+
subscribe_to_indices
|
165
|
+
|
166
|
+
# Wait for data
|
167
|
+
wait_for_data
|
168
|
+
end
|
169
|
+
|
170
|
+
def stop
|
171
|
+
puts "Stopping Market Feed WebSocket..."
|
172
|
+
@running = false
|
173
|
+
@market_client&.stop
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
|
178
|
+
def subscribe_to_indices
|
179
|
+
indices = [
|
180
|
+
{ segment: "IDX_I", security_id: "13", name: "NIFTY" },
|
181
|
+
{ segment: "IDX_I", security_id: "25", name: "BANKNIFTY" },
|
182
|
+
{ segment: "IDX_I", security_id: "29", name: "NIFTYIT" },
|
183
|
+
{ segment: "IDX_I", security_id: "51", name: "SENSEX" }
|
184
|
+
]
|
185
|
+
|
186
|
+
indices.each do |index|
|
187
|
+
@market_client.subscribe_one(
|
188
|
+
segment: index[:segment],
|
189
|
+
security_id: index[:security_id]
|
190
|
+
)
|
191
|
+
puts "ā
Subscribed to #{index[:name]} (#{index[:segment]}:#{index[:security_id]})"
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def process_market_data(tick)
|
196
|
+
timestamp = tick[:ts] ? Time.at(tick[:ts]) : Time.now
|
197
|
+
data = {
|
198
|
+
segment: tick[:segment],
|
199
|
+
security_id: tick[:security_id],
|
200
|
+
ltp: tick[:ltp],
|
201
|
+
timestamp: timestamp.iso8601,
|
202
|
+
name: get_index_name(tick[:segment], tick[:security_id])
|
203
|
+
}
|
204
|
+
|
205
|
+
# Display data
|
206
|
+
puts "š Market Data: #{data[:name]} = #{data[:ltp]} at #{data[:timestamp]}"
|
207
|
+
|
208
|
+
# Save to file (optional)
|
209
|
+
save_to_file(data)
|
210
|
+
|
211
|
+
# Send to external service (optional)
|
212
|
+
send_to_external_service(data)
|
213
|
+
end
|
214
|
+
|
215
|
+
def get_index_name(segment, security_id)
|
216
|
+
case "#{segment}:#{security_id}"
|
217
|
+
when "IDX_I:13" then "NIFTY"
|
218
|
+
when "IDX_I:25" then "BANKNIFTY"
|
219
|
+
when "IDX_I:29" then "NIFTYIT"
|
220
|
+
when "IDX_I:51" then "SENSEX"
|
221
|
+
else "#{segment}:#{security_id}"
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def save_to_file(data)
|
226
|
+
File.open("market_data.json", "a") do |file|
|
227
|
+
file.puts(data.to_json)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def send_to_external_service(data)
|
232
|
+
# Example: Send to external API
|
233
|
+
# HTTP.post("https://api.example.com/market-data", data.to_json)
|
234
|
+
end
|
235
|
+
|
236
|
+
def wait_for_data
|
237
|
+
puts "Waiting for market data... Press Ctrl+C to stop"
|
238
|
+
begin
|
239
|
+
while @running
|
240
|
+
sleep(1)
|
241
|
+
end
|
242
|
+
rescue Interrupt
|
243
|
+
puts "\nReceived interrupt signal, shutting down..."
|
244
|
+
stop
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# Run the script
|
250
|
+
script = MarketFeedScript.new
|
251
|
+
script.start
|
252
|
+
```
|
253
|
+
|
254
|
+
### Order Update Script
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
#!/usr/bin/env ruby
|
258
|
+
# frozen_string_literal: true
|
259
|
+
|
260
|
+
require 'dhan_hq'
|
261
|
+
require 'json'
|
262
|
+
|
263
|
+
# Configure DhanHQ
|
264
|
+
DhanHQ.configure do |config|
|
265
|
+
config.client_id = ENV["CLIENT_ID"] || "your_client_id"
|
266
|
+
config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
|
267
|
+
config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
|
268
|
+
end
|
269
|
+
|
270
|
+
class OrderUpdateScript
|
271
|
+
def initialize
|
272
|
+
@orders_client = nil
|
273
|
+
@running = false
|
274
|
+
end
|
275
|
+
|
276
|
+
def start
|
277
|
+
puts "Starting Order Update WebSocket..."
|
278
|
+
@running = true
|
279
|
+
|
280
|
+
@orders_client = DhanHQ::WS::Orders.connect do |order_update|
|
281
|
+
process_order_update(order_update)
|
282
|
+
end
|
283
|
+
|
284
|
+
# Add comprehensive event handling
|
285
|
+
@orders_client.on(:update) do |order_update|
|
286
|
+
puts "š Order Updated: #{order_update.order_no}"
|
287
|
+
end
|
288
|
+
|
289
|
+
@orders_client.on(:status_change) do |change_data|
|
290
|
+
puts "š Status Changed: #{change_data[:previous_status]} -> #{change_data[:new_status]}"
|
291
|
+
end
|
292
|
+
|
293
|
+
@orders_client.on(:execution) do |execution_data|
|
294
|
+
puts "ā
Execution: #{execution_data[:new_traded_qty]} shares executed"
|
295
|
+
end
|
296
|
+
|
297
|
+
@orders_client.on(:order_traded) do |order_update|
|
298
|
+
puts "š° Order Traded: #{order_update.order_no} - #{order_update.symbol}"
|
299
|
+
end
|
300
|
+
|
301
|
+
@orders_client.on(:order_rejected) do |order_update|
|
302
|
+
puts "ā Order Rejected: #{order_update.order_no} - #{order_update.reason_description}"
|
303
|
+
end
|
304
|
+
|
305
|
+
@orders_client.on(:error) do |error|
|
306
|
+
puts "ā ļø WebSocket Error: #{error}"
|
307
|
+
@running = false
|
308
|
+
end
|
309
|
+
|
310
|
+
@orders_client.on(:close) do |close_info|
|
311
|
+
puts "š WebSocket Closed: #{close_info[:code]} - #{close_info[:reason]}"
|
312
|
+
@running = false
|
313
|
+
end
|
314
|
+
|
315
|
+
# Wait for updates
|
316
|
+
wait_for_updates
|
317
|
+
end
|
318
|
+
|
319
|
+
def stop
|
320
|
+
puts "Stopping Order Update WebSocket..."
|
321
|
+
@running = false
|
322
|
+
@orders_client&.stop
|
323
|
+
end
|
324
|
+
|
325
|
+
private
|
326
|
+
|
327
|
+
def process_order_update(order_update)
|
328
|
+
data = {
|
329
|
+
order_no: order_update.order_no,
|
330
|
+
status: order_update.status,
|
331
|
+
symbol: order_update.symbol,
|
332
|
+
quantity: order_update.quantity,
|
333
|
+
traded_qty: order_update.traded_qty,
|
334
|
+
price: order_update.price,
|
335
|
+
execution_percentage: order_update.execution_percentage,
|
336
|
+
timestamp: Time.current.iso8601
|
337
|
+
}
|
338
|
+
|
339
|
+
# Display data
|
340
|
+
puts "š Order Update: #{data[:order_no]} - #{data[:status]}"
|
341
|
+
puts " Symbol: #{data[:symbol]}"
|
342
|
+
puts " Quantity: #{data[:quantity]}"
|
343
|
+
puts " Traded Qty: #{data[:traded_qty]}"
|
344
|
+
puts " Price: #{data[:price]}"
|
345
|
+
puts " Execution: #{data[:execution_percentage]}%"
|
346
|
+
puts " ---"
|
347
|
+
|
348
|
+
# Save to file (optional)
|
349
|
+
save_to_file(data)
|
350
|
+
|
351
|
+
# Send to external service (optional)
|
352
|
+
send_to_external_service(data)
|
353
|
+
end
|
354
|
+
|
355
|
+
def save_to_file(data)
|
356
|
+
File.open("order_updates.json", "a") do |file|
|
357
|
+
file.puts(data.to_json)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
def send_to_external_service(data)
|
362
|
+
# Example: Send to external API
|
363
|
+
# HTTP.post("https://api.example.com/order-updates", data.to_json)
|
364
|
+
end
|
365
|
+
|
366
|
+
def wait_for_updates
|
367
|
+
puts "Waiting for order updates... Press Ctrl+C to stop"
|
368
|
+
begin
|
369
|
+
while @running
|
370
|
+
sleep(1)
|
371
|
+
end
|
372
|
+
rescue Interrupt
|
373
|
+
puts "\nReceived interrupt signal, shutting down..."
|
374
|
+
stop
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
# Run the script
|
380
|
+
script = OrderUpdateScript.new
|
381
|
+
script.start
|
382
|
+
```
|
383
|
+
|
384
|
+
### Market Depth Script
|
385
|
+
|
386
|
+
```ruby
|
387
|
+
#!/usr/bin/env ruby
|
388
|
+
# frozen_string_literal: true
|
389
|
+
|
390
|
+
require 'dhan_hq'
|
391
|
+
require 'json'
|
392
|
+
|
393
|
+
# Configure DhanHQ
|
394
|
+
DhanHQ.configure do |config|
|
395
|
+
config.client_id = ENV["CLIENT_ID"] || "your_client_id"
|
396
|
+
config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
|
397
|
+
config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
|
398
|
+
end
|
399
|
+
|
400
|
+
class MarketDepthScript
|
401
|
+
def initialize
|
402
|
+
@depth_client = nil
|
403
|
+
@running = false
|
404
|
+
end
|
405
|
+
|
406
|
+
def start
|
407
|
+
puts "Starting Market Depth WebSocket..."
|
408
|
+
@running = true
|
409
|
+
|
410
|
+
# Define symbols with correct exchange segments and security IDs
|
411
|
+
symbols = [
|
412
|
+
{ symbol: "RELIANCE", exchange_segment: "NSE_EQ", security_id: "2885" },
|
413
|
+
{ symbol: "TCS", exchange_segment: "NSE_EQ", security_id: "11536" }
|
414
|
+
]
|
415
|
+
|
416
|
+
@depth_client = DhanHQ::WS::MarketDepth.connect(symbols: symbols) do |depth_data|
|
417
|
+
process_market_depth(depth_data)
|
418
|
+
end
|
419
|
+
|
420
|
+
# Add event handlers
|
421
|
+
@depth_client.on(:depth_update) do |update_data|
|
422
|
+
puts "š Depth Update: #{update_data[:symbol]} - #{update_data[:side]} side updated"
|
423
|
+
end
|
424
|
+
|
425
|
+
@depth_client.on(:depth_snapshot) do |snapshot_data|
|
426
|
+
puts "šø Depth Snapshot: #{snapshot_data[:symbol]} - Full order book received"
|
427
|
+
end
|
428
|
+
|
429
|
+
@depth_client.on(:error) do |error|
|
430
|
+
puts "ā ļø WebSocket Error: #{error}"
|
431
|
+
@running = false
|
432
|
+
end
|
433
|
+
|
434
|
+
@depth_client.on(:close) do |close_info|
|
435
|
+
puts "š WebSocket Closed: #{close_info[:code]} - #{close_info[:reason]}"
|
436
|
+
@running = false
|
437
|
+
end
|
438
|
+
|
439
|
+
# Wait for data
|
440
|
+
wait_for_data
|
441
|
+
end
|
442
|
+
|
443
|
+
def stop
|
444
|
+
puts "Stopping Market Depth WebSocket..."
|
445
|
+
@running = false
|
446
|
+
@depth_client&.stop
|
447
|
+
end
|
448
|
+
|
449
|
+
private
|
450
|
+
|
451
|
+
def process_market_depth(depth_data)
|
452
|
+
data = {
|
453
|
+
symbol: depth_data[:symbol],
|
454
|
+
exchange_segment: depth_data[:exchange_segment],
|
455
|
+
security_id: depth_data[:security_id],
|
456
|
+
best_bid: depth_data[:best_bid],
|
457
|
+
best_ask: depth_data[:best_ask],
|
458
|
+
spread: depth_data[:spread],
|
459
|
+
bid_levels: depth_data[:bids],
|
460
|
+
ask_levels: depth_data[:asks],
|
461
|
+
timestamp: Time.current.iso8601
|
462
|
+
}
|
463
|
+
|
464
|
+
# Display data
|
465
|
+
puts "š Market Depth: #{data[:symbol]}"
|
466
|
+
puts " Best Bid: #{data[:best_bid]}"
|
467
|
+
puts " Best Ask: #{data[:best_ask]}"
|
468
|
+
puts " Spread: #{data[:spread]}"
|
469
|
+
puts " Bid Levels: #{data[:bid_levels].size}"
|
470
|
+
puts " Ask Levels: #{data[:ask_levels].size}"
|
471
|
+
|
472
|
+
# Show top 3 bid/ask levels
|
473
|
+
if data[:bid_levels] && data[:bid_levels].size > 0
|
474
|
+
puts " Top Bids:"
|
475
|
+
data[:bid_levels].first(3).each_with_index do |bid, i|
|
476
|
+
puts " #{i+1}. Price: #{bid[:price]}, Qty: #{bid[:quantity]}"
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
if data[:ask_levels] && data[:ask_levels].size > 0
|
481
|
+
puts " Top Asks:"
|
482
|
+
data[:ask_levels].first(3).each_with_index do |ask, i|
|
483
|
+
puts " #{i+1}. Price: #{ask[:price]}, Qty: #{ask[:quantity]}"
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
puts " ---"
|
488
|
+
|
489
|
+
# Save to file (optional)
|
490
|
+
save_to_file(data)
|
491
|
+
|
492
|
+
# Send to external service (optional)
|
493
|
+
send_to_external_service(data)
|
494
|
+
end
|
495
|
+
|
496
|
+
def save_to_file(data)
|
497
|
+
File.open("market_depth.json", "a") do |file|
|
498
|
+
file.puts(data.to_json)
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
def send_to_external_service(data)
|
503
|
+
# Example: Send to external API
|
504
|
+
# HTTP.post("https://api.example.com/market-depth", data.to_json)
|
505
|
+
end
|
506
|
+
|
507
|
+
def wait_for_data
|
508
|
+
puts "Waiting for market depth data... Press Ctrl+C to stop"
|
509
|
+
begin
|
510
|
+
while @running
|
511
|
+
sleep(1)
|
512
|
+
end
|
513
|
+
rescue Interrupt
|
514
|
+
puts "\nReceived interrupt signal, shutting down..."
|
515
|
+
stop
|
516
|
+
end
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
# Run the script
|
521
|
+
script = MarketDepthScript.new
|
522
|
+
script.start
|
523
|
+
```
|
524
|
+
|
525
|
+
## Daemon Integration
|
526
|
+
|
527
|
+
### Systemd Service
|
528
|
+
|
529
|
+
```ini
|
530
|
+
# /etc/systemd/system/dhanhq-market-feed.service
|
531
|
+
[Unit]
|
532
|
+
Description=DhanHQ Market Feed Daemon
|
533
|
+
After=network.target
|
534
|
+
|
535
|
+
[Service]
|
536
|
+
Type=simple
|
537
|
+
User=dhanhq
|
538
|
+
Group=dhanhq
|
539
|
+
WorkingDirectory=/opt/dhanhq-market-feed
|
540
|
+
ExecStart=/usr/bin/ruby market_feed_daemon.rb
|
541
|
+
Restart=always
|
542
|
+
RestartSec=10
|
543
|
+
Environment=CLIENT_ID=your_client_id
|
544
|
+
Environment=ACCESS_TOKEN=your_access_token
|
545
|
+
Environment=DHAN_WS_USER_TYPE=SELF
|
546
|
+
|
547
|
+
[Install]
|
548
|
+
WantedBy=multi-user.target
|
549
|
+
```
|
550
|
+
|
551
|
+
### Daemon Script
|
552
|
+
|
553
|
+
```ruby
|
554
|
+
#!/usr/bin/env ruby
|
555
|
+
# frozen_string_literal: true
|
556
|
+
|
557
|
+
require 'dhan_hq'
|
558
|
+
require 'json'
|
559
|
+
require 'fileutils'
|
560
|
+
|
561
|
+
# Configure DhanHQ
|
562
|
+
DhanHQ.configure do |config|
|
563
|
+
config.client_id = ENV["CLIENT_ID"] || "your_client_id"
|
564
|
+
config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
|
565
|
+
config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
|
566
|
+
end
|
567
|
+
|
568
|
+
class MarketFeedDaemon
|
569
|
+
def initialize
|
570
|
+
@market_client = nil
|
571
|
+
@running = false
|
572
|
+
@pid_file = "/tmp/dhanhq-market-feed.pid"
|
573
|
+
@log_file = "/var/log/dhanhq-market-feed.log"
|
574
|
+
end
|
575
|
+
|
576
|
+
def start
|
577
|
+
if running?
|
578
|
+
puts "Daemon is already running (PID: #{pid})"
|
579
|
+
return
|
580
|
+
end
|
581
|
+
|
582
|
+
puts "Starting DhanHQ Market Feed Daemon..."
|
583
|
+
@running = true
|
584
|
+
|
585
|
+
# Create PID file
|
586
|
+
File.write(@pid_file, Process.pid)
|
587
|
+
|
588
|
+
# Set up signal handlers
|
589
|
+
setup_signal_handlers
|
590
|
+
|
591
|
+
# Start WebSocket connection
|
592
|
+
start_websocket
|
593
|
+
|
594
|
+
# Main loop
|
595
|
+
main_loop
|
596
|
+
end
|
597
|
+
|
598
|
+
def stop
|
599
|
+
puts "Stopping DhanHQ Market Feed Daemon..."
|
600
|
+
@running = false
|
601
|
+
@market_client&.stop
|
602
|
+
File.delete(@pid_file) if File.exist?(@pid_file)
|
603
|
+
end
|
604
|
+
|
605
|
+
def status
|
606
|
+
if running?
|
607
|
+
puts "Daemon is running (PID: #{pid})"
|
608
|
+
else
|
609
|
+
puts "Daemon is not running"
|
610
|
+
end
|
611
|
+
end
|
612
|
+
|
613
|
+
private
|
614
|
+
|
615
|
+
def running?
|
616
|
+
return false unless File.exist?(@pid_file)
|
617
|
+
|
618
|
+
pid = File.read(@pid_file).to_i
|
619
|
+
Process.kill(0, pid)
|
620
|
+
true
|
621
|
+
rescue Errno::ESRCH, Errno::ENOENT
|
622
|
+
false
|
623
|
+
end
|
624
|
+
|
625
|
+
def pid
|
626
|
+
File.read(@pid_file).to_i if File.exist?(@pid_file)
|
627
|
+
end
|
628
|
+
|
629
|
+
def setup_signal_handlers
|
630
|
+
Signal.trap("TERM") do
|
631
|
+
puts "Received TERM signal, shutting down gracefully..."
|
632
|
+
stop
|
633
|
+
exit(0)
|
634
|
+
end
|
635
|
+
|
636
|
+
Signal.trap("INT") do
|
637
|
+
puts "Received INT signal, shutting down gracefully..."
|
638
|
+
stop
|
639
|
+
exit(0)
|
640
|
+
end
|
641
|
+
end
|
642
|
+
|
643
|
+
def start_websocket
|
644
|
+
@market_client = DhanHQ::WS.connect(mode: :ticker) do |tick|
|
645
|
+
process_market_data(tick)
|
646
|
+
end
|
647
|
+
|
648
|
+
# Add error handling
|
649
|
+
@market_client.on(:error) do |error|
|
650
|
+
log_error("WebSocket Error: #{error}")
|
651
|
+
@running = false
|
652
|
+
end
|
653
|
+
|
654
|
+
@market_client.on(:close) do |close_info|
|
655
|
+
log_warning("WebSocket Closed: #{close_info[:code]} - #{close_info[:reason]}")
|
656
|
+
@running = false
|
657
|
+
end
|
658
|
+
|
659
|
+
# Subscribe to indices
|
660
|
+
subscribe_to_indices
|
661
|
+
end
|
662
|
+
|
663
|
+
def subscribe_to_indices
|
664
|
+
indices = [
|
665
|
+
{ segment: "IDX_I", security_id: "13", name: "NIFTY" },
|
666
|
+
{ segment: "IDX_I", security_id: "25", name: "BANKNIFTY" },
|
667
|
+
{ segment: "IDX_I", security_id: "29", name: "NIFTYIT" },
|
668
|
+
{ segment: "IDX_I", security_id: "51", name: "SENSEX" }
|
669
|
+
]
|
670
|
+
|
671
|
+
indices.each do |index|
|
672
|
+
@market_client.subscribe_one(
|
673
|
+
segment: index[:segment],
|
674
|
+
security_id: index[:security_id]
|
675
|
+
)
|
676
|
+
log_info("Subscribed to #{index[:name]} (#{index[:segment]}:#{index[:security_id]})")
|
677
|
+
end
|
678
|
+
end
|
679
|
+
|
680
|
+
def process_market_data(tick)
|
681
|
+
timestamp = tick[:ts] ? Time.at(tick[:ts]) : Time.now
|
682
|
+
data = {
|
683
|
+
segment: tick[:segment],
|
684
|
+
security_id: tick[:security_id],
|
685
|
+
ltp: tick[:ltp],
|
686
|
+
timestamp: timestamp.iso8601,
|
687
|
+
name: get_index_name(tick[:segment], tick[:security_id])
|
688
|
+
}
|
689
|
+
|
690
|
+
# Log data
|
691
|
+
log_info("Market Data: #{data[:name]} = #{data[:ltp]} at #{data[:timestamp]}")
|
692
|
+
|
693
|
+
# Save to file
|
694
|
+
save_to_file(data)
|
695
|
+
|
696
|
+
# Send to external service
|
697
|
+
send_to_external_service(data)
|
698
|
+
end
|
699
|
+
|
700
|
+
def get_index_name(segment, security_id)
|
701
|
+
case "#{segment}:#{security_id}"
|
702
|
+
when "IDX_I:13" then "NIFTY"
|
703
|
+
when "IDX_I:25" then "BANKNIFTY"
|
704
|
+
when "IDX_I:29" then "NIFTYIT"
|
705
|
+
when "IDX_I:51" then "SENSEX"
|
706
|
+
else "#{segment}:#{security_id}"
|
707
|
+
end
|
708
|
+
end
|
709
|
+
|
710
|
+
def save_to_file(data)
|
711
|
+
File.open("/var/log/dhanhq-market-data.json", "a") do |file|
|
712
|
+
file.puts(data.to_json)
|
713
|
+
end
|
714
|
+
end
|
715
|
+
|
716
|
+
def send_to_external_service(data)
|
717
|
+
# Example: Send to external API
|
718
|
+
# HTTP.post("https://api.example.com/market-data", data.to_json)
|
719
|
+
end
|
720
|
+
|
721
|
+
def main_loop
|
722
|
+
log_info("Daemon started and running...")
|
723
|
+
while @running
|
724
|
+
sleep(1)
|
725
|
+
end
|
726
|
+
end
|
727
|
+
|
728
|
+
def log_info(message)
|
729
|
+
log("INFO", message)
|
730
|
+
end
|
731
|
+
|
732
|
+
def log_warning(message)
|
733
|
+
log("WARN", message)
|
734
|
+
end
|
735
|
+
|
736
|
+
def log_error(message)
|
737
|
+
log("ERROR", message)
|
738
|
+
end
|
739
|
+
|
740
|
+
def log(level, message)
|
741
|
+
timestamp = Time.current.iso8601
|
742
|
+
log_entry = "[#{timestamp}] #{level}: #{message}\n"
|
743
|
+
|
744
|
+
File.open(@log_file, "a") do |file|
|
745
|
+
file.write(log_entry)
|
746
|
+
end
|
747
|
+
|
748
|
+
puts log_entry.strip
|
749
|
+
end
|
750
|
+
end
|
751
|
+
|
752
|
+
# Command line interface
|
753
|
+
case ARGV[0]
|
754
|
+
when "start"
|
755
|
+
MarketFeedDaemon.new.start
|
756
|
+
when "stop"
|
757
|
+
MarketFeedDaemon.new.stop
|
758
|
+
when "status"
|
759
|
+
MarketFeedDaemon.new.status
|
760
|
+
else
|
761
|
+
puts "Usage: #{$0} {start|stop|status}"
|
762
|
+
exit(1)
|
763
|
+
end
|
764
|
+
```
|
765
|
+
|
766
|
+
## Command-Line Tools
|
767
|
+
|
768
|
+
### Market Data CLI
|
769
|
+
|
770
|
+
```ruby
|
771
|
+
#!/usr/bin/env ruby
|
772
|
+
# frozen_string_literal: true
|
773
|
+
|
774
|
+
require 'dhan_hq'
|
775
|
+
require 'optparse'
|
776
|
+
require 'json'
|
777
|
+
|
778
|
+
# Configure DhanHQ
|
779
|
+
DhanHQ.configure do |config|
|
780
|
+
config.client_id = ENV["CLIENT_ID"] || "your_client_id"
|
781
|
+
config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
|
782
|
+
config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
|
783
|
+
end
|
784
|
+
|
785
|
+
class MarketDataCLI
|
786
|
+
def initialize
|
787
|
+
@options = {}
|
788
|
+
@parser = OptionParser.new do |opts|
|
789
|
+
opts.banner = "Usage: #{$0} [options]"
|
790
|
+
|
791
|
+
opts.on("-m", "--mode MODE", "WebSocket mode (ticker, quote, full)") do |mode|
|
792
|
+
@options[:mode] = mode.to_sym
|
793
|
+
end
|
794
|
+
|
795
|
+
opts.on("-s", "--symbols SYMBOLS", "Comma-separated list of symbols") do |symbols|
|
796
|
+
@options[:symbols] = symbols.split(",")
|
797
|
+
end
|
798
|
+
|
799
|
+
opts.on("-o", "--output FILE", "Output file for data") do |file|
|
800
|
+
@options[:output] = file
|
801
|
+
end
|
802
|
+
|
803
|
+
opts.on("-d", "--duration SECONDS", Integer, "Duration to run (seconds)") do |duration|
|
804
|
+
@options[:duration] = duration
|
805
|
+
end
|
806
|
+
|
807
|
+
opts.on("-v", "--verbose", "Verbose output") do
|
808
|
+
@options[:verbose] = true
|
809
|
+
end
|
810
|
+
|
811
|
+
opts.on("-h", "--help", "Show this help") do
|
812
|
+
puts opts
|
813
|
+
exit
|
814
|
+
end
|
815
|
+
end
|
816
|
+
end
|
817
|
+
|
818
|
+
def run
|
819
|
+
@parser.parse!
|
820
|
+
@options[:mode] ||= :ticker
|
821
|
+
@options[:duration] ||= 30
|
822
|
+
|
823
|
+
case @options[:mode]
|
824
|
+
when :ticker
|
825
|
+
run_ticker_mode
|
826
|
+
when :quote
|
827
|
+
run_quote_mode
|
828
|
+
when :full
|
829
|
+
run_full_mode
|
830
|
+
else
|
831
|
+
puts "Invalid mode: #{@options[:mode]}"
|
832
|
+
puts "Valid modes: ticker, quote, full"
|
833
|
+
exit(1)
|
834
|
+
end
|
835
|
+
end
|
836
|
+
|
837
|
+
private
|
838
|
+
|
839
|
+
def run_ticker_mode
|
840
|
+
puts "Starting Market Feed WebSocket (Ticker Mode)..."
|
841
|
+
|
842
|
+
market_client = DhanHQ::WS.connect(mode: :ticker) do |tick|
|
843
|
+
process_ticker_data(tick)
|
844
|
+
end
|
845
|
+
|
846
|
+
# Subscribe to symbols or default indices
|
847
|
+
if @options[:symbols]
|
848
|
+
subscribe_to_symbols(market_client)
|
849
|
+
else
|
850
|
+
subscribe_to_default_indices(market_client)
|
851
|
+
end
|
852
|
+
|
853
|
+
# Wait for data
|
854
|
+
sleep(@options[:duration])
|
855
|
+
|
856
|
+
# Clean shutdown
|
857
|
+
market_client.stop
|
858
|
+
puts "Market Feed WebSocket stopped."
|
859
|
+
end
|
860
|
+
|
861
|
+
def run_quote_mode
|
862
|
+
puts "Starting Market Feed WebSocket (Quote Mode)..."
|
863
|
+
|
864
|
+
market_client = DhanHQ::WS.connect(mode: :quote) do |quote|
|
865
|
+
process_quote_data(quote)
|
866
|
+
end
|
867
|
+
|
868
|
+
# Subscribe to symbols or default indices
|
869
|
+
if @options[:symbols]
|
870
|
+
subscribe_to_symbols(market_client)
|
871
|
+
else
|
872
|
+
subscribe_to_default_indices(market_client)
|
873
|
+
end
|
874
|
+
|
875
|
+
# Wait for data
|
876
|
+
sleep(@options[:duration])
|
877
|
+
|
878
|
+
# Clean shutdown
|
879
|
+
market_client.stop
|
880
|
+
puts "Market Feed WebSocket stopped."
|
881
|
+
end
|
882
|
+
|
883
|
+
def run_full_mode
|
884
|
+
puts "Starting Market Feed WebSocket (Full Mode)..."
|
885
|
+
|
886
|
+
market_client = DhanHQ::WS.connect(mode: :full) do |full|
|
887
|
+
process_full_data(full)
|
888
|
+
end
|
889
|
+
|
890
|
+
# Subscribe to symbols or default indices
|
891
|
+
if @options[:symbols]
|
892
|
+
subscribe_to_symbols(market_client)
|
893
|
+
else
|
894
|
+
subscribe_to_default_indices(market_client)
|
895
|
+
end
|
896
|
+
|
897
|
+
# Wait for data
|
898
|
+
sleep(@options[:duration])
|
899
|
+
|
900
|
+
# Clean shutdown
|
901
|
+
market_client.stop
|
902
|
+
puts "Market Feed WebSocket stopped."
|
903
|
+
end
|
904
|
+
|
905
|
+
def subscribe_to_default_indices(client)
|
906
|
+
indices = [
|
907
|
+
{ segment: "IDX_I", security_id: "13", name: "NIFTY" },
|
908
|
+
{ segment: "IDX_I", security_id: "25", name: "BANKNIFTY" },
|
909
|
+
{ segment: "IDX_I", security_id: "29", name: "NIFTYIT" },
|
910
|
+
{ segment: "IDX_I", security_id: "51", name: "SENSEX" }
|
911
|
+
]
|
912
|
+
|
913
|
+
indices.each do |index|
|
914
|
+
client.subscribe_one(
|
915
|
+
segment: index[:segment],
|
916
|
+
security_id: index[:security_id]
|
917
|
+
)
|
918
|
+
puts "ā
Subscribed to #{index[:name]} (#{index[:segment]}:#{index[:security_id]})"
|
919
|
+
end
|
920
|
+
end
|
921
|
+
|
922
|
+
def subscribe_to_symbols(client)
|
923
|
+
@options[:symbols].each do |symbol|
|
924
|
+
# Find the correct segment and security ID
|
925
|
+
segment, security_id = find_symbol_info(symbol)
|
926
|
+
if segment && security_id
|
927
|
+
client.subscribe_one(segment: segment, security_id: security_id)
|
928
|
+
puts "ā
Subscribed to #{symbol} (#{segment}:#{security_id})"
|
929
|
+
else
|
930
|
+
puts "ā Could not find symbol: #{symbol}"
|
931
|
+
end
|
932
|
+
end
|
933
|
+
end
|
934
|
+
|
935
|
+
def find_symbol_info(symbol)
|
936
|
+
# Search in different segments
|
937
|
+
segments = ["NSE_EQ", "BSE_EQ", "IDX_I"]
|
938
|
+
|
939
|
+
segments.each do |segment|
|
940
|
+
instruments = DhanHQ::Models::Instrument.by_segment(segment)
|
941
|
+
instrument = instruments.find { |i| i.symbol_name.upcase.include?(symbol.upcase) }
|
942
|
+
return [segment, instrument.security_id] if instrument
|
943
|
+
end
|
944
|
+
|
945
|
+
nil
|
946
|
+
end
|
947
|
+
|
948
|
+
def process_ticker_data(tick)
|
949
|
+
timestamp = tick[:ts] ? Time.at(tick[:ts]) : Time.now
|
950
|
+
data = {
|
951
|
+
segment: tick[:segment],
|
952
|
+
security_id: tick[:security_id],
|
953
|
+
ltp: tick[:ltp],
|
954
|
+
timestamp: timestamp.iso8601
|
955
|
+
}
|
956
|
+
|
957
|
+
if @options[:verbose]
|
958
|
+
puts "š Market Data: #{data[:segment]}:#{data[:security_id]} = #{data[:ltp]} at #{data[:timestamp]}"
|
959
|
+
end
|
960
|
+
|
961
|
+
save_to_file(data) if @options[:output]
|
962
|
+
end
|
963
|
+
|
964
|
+
def process_quote_data(quote)
|
965
|
+
timestamp = quote[:ts] ? Time.at(quote[:ts]) : Time.now
|
966
|
+
data = {
|
967
|
+
segment: quote[:segment],
|
968
|
+
security_id: quote[:security_id],
|
969
|
+
ltp: quote[:ltp],
|
970
|
+
volume: quote[:vol],
|
971
|
+
day_high: quote[:day_high],
|
972
|
+
day_low: quote[:day_low],
|
973
|
+
timestamp: timestamp.iso8601
|
974
|
+
}
|
975
|
+
|
976
|
+
if @options[:verbose]
|
977
|
+
puts "š Quote Data: #{data[:segment]}:#{data[:security_id]}"
|
978
|
+
puts " LTP: #{data[:ltp]}"
|
979
|
+
puts " Volume: #{data[:volume]}"
|
980
|
+
puts " Day High: #{data[:day_high]}"
|
981
|
+
puts " Day Low: #{data[:day_low]}"
|
982
|
+
puts " Timestamp: #{data[:timestamp]}"
|
983
|
+
end
|
984
|
+
|
985
|
+
save_to_file(data) if @options[:output]
|
986
|
+
end
|
987
|
+
|
988
|
+
def process_full_data(full)
|
989
|
+
timestamp = full[:ts] ? Time.at(full[:ts]) : Time.now
|
990
|
+
data = {
|
991
|
+
segment: full[:segment],
|
992
|
+
security_id: full[:security_id],
|
993
|
+
ltp: full[:ltp],
|
994
|
+
volume: full[:vol],
|
995
|
+
day_high: full[:day_high],
|
996
|
+
day_low: full[:day_low],
|
997
|
+
open: full[:open],
|
998
|
+
close: full[:close],
|
999
|
+
timestamp: timestamp.iso8601
|
1000
|
+
}
|
1001
|
+
|
1002
|
+
if @options[:verbose]
|
1003
|
+
puts "š Full Data: #{data[:segment]}:#{data[:security_id]}"
|
1004
|
+
puts " LTP: #{data[:ltp]}"
|
1005
|
+
puts " Volume: #{data[:volume]}"
|
1006
|
+
puts " Open: #{data[:open]}"
|
1007
|
+
puts " High: #{data[:day_high]}"
|
1008
|
+
puts " Low: #{data[:day_low]}"
|
1009
|
+
puts " Close: #{data[:close]}"
|
1010
|
+
puts " Timestamp: #{data[:timestamp]}"
|
1011
|
+
end
|
1012
|
+
|
1013
|
+
save_to_file(data) if @options[:output]
|
1014
|
+
end
|
1015
|
+
|
1016
|
+
def save_to_file(data)
|
1017
|
+
File.open(@options[:output], "a") do |file|
|
1018
|
+
file.puts(data.to_json)
|
1019
|
+
end
|
1020
|
+
end
|
1021
|
+
end
|
1022
|
+
|
1023
|
+
# Run the CLI
|
1024
|
+
MarketDataCLI.new.run
|
1025
|
+
```
|
1026
|
+
|
1027
|
+
### Usage Examples
|
1028
|
+
|
1029
|
+
```bash
|
1030
|
+
# Basic usage with default indices
|
1031
|
+
ruby market_data_cli.rb
|
1032
|
+
|
1033
|
+
# Ticker mode for 60 seconds
|
1034
|
+
ruby market_data_cli.rb -m ticker -d 60
|
1035
|
+
|
1036
|
+
# Quote mode with verbose output
|
1037
|
+
ruby market_data_cli.rb -m quote -v
|
1038
|
+
|
1039
|
+
# Full mode with output file
|
1040
|
+
ruby market_data_cli.rb -m full -o market_data.json
|
1041
|
+
|
1042
|
+
# Subscribe to specific symbols
|
1043
|
+
ruby market_data_cli.rb -s "RELIANCE,TCS" -v
|
1044
|
+
|
1045
|
+
# Help
|
1046
|
+
ruby market_data_cli.rb -h
|
1047
|
+
```
|
1048
|
+
|
1049
|
+
## Error Handling
|
1050
|
+
|
1051
|
+
### Comprehensive Error Handling
|
1052
|
+
|
1053
|
+
```ruby
|
1054
|
+
#!/usr/bin/env ruby
|
1055
|
+
# frozen_string_literal: true
|
1056
|
+
|
1057
|
+
require 'dhan_hq'
|
1058
|
+
require 'json'
|
1059
|
+
|
1060
|
+
# Configure DhanHQ
|
1061
|
+
DhanHQ.configure do |config|
|
1062
|
+
config.client_id = ENV["CLIENT_ID"] || "your_client_id"
|
1063
|
+
config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
|
1064
|
+
config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
|
1065
|
+
end
|
1066
|
+
|
1067
|
+
class RobustWebSocketClient
|
1068
|
+
def initialize
|
1069
|
+
@client = nil
|
1070
|
+
@running = false
|
1071
|
+
@retry_count = 0
|
1072
|
+
@max_retries = 5
|
1073
|
+
@retry_delay = 5
|
1074
|
+
end
|
1075
|
+
|
1076
|
+
def start
|
1077
|
+
@running = true
|
1078
|
+
connect_with_retry
|
1079
|
+
end
|
1080
|
+
|
1081
|
+
def stop
|
1082
|
+
@running = false
|
1083
|
+
@client&.stop
|
1084
|
+
end
|
1085
|
+
|
1086
|
+
private
|
1087
|
+
|
1088
|
+
def connect_with_retry
|
1089
|
+
while @running && @retry_count < @max_retries
|
1090
|
+
begin
|
1091
|
+
connect
|
1092
|
+
@retry_count = 0 # Reset retry count on successful connection
|
1093
|
+
wait_for_connection
|
1094
|
+
rescue StandardError => e
|
1095
|
+
handle_connection_error(e)
|
1096
|
+
end
|
1097
|
+
end
|
1098
|
+
|
1099
|
+
if @retry_count >= @max_retries
|
1100
|
+
puts "ā Maximum retry attempts reached. Giving up."
|
1101
|
+
@running = false
|
1102
|
+
end
|
1103
|
+
end
|
1104
|
+
|
1105
|
+
def connect
|
1106
|
+
puts "š Attempting to connect (attempt #{@retry_count + 1}/#{@max_retries})..."
|
1107
|
+
|
1108
|
+
@client = DhanHQ::WS.connect(mode: :ticker) do |tick|
|
1109
|
+
process_data(tick)
|
1110
|
+
end
|
1111
|
+
|
1112
|
+
# Add comprehensive error handling
|
1113
|
+
@client.on(:error) do |error|
|
1114
|
+
puts "ā WebSocket Error: #{error}"
|
1115
|
+
handle_websocket_error(error)
|
1116
|
+
end
|
1117
|
+
|
1118
|
+
@client.on(:close) do |close_info|
|
1119
|
+
puts "š WebSocket Closed: #{close_info[:code]} - #{close_info[:reason]}"
|
1120
|
+
handle_websocket_close(close_info)
|
1121
|
+
end
|
1122
|
+
|
1123
|
+
# Subscribe to indices
|
1124
|
+
subscribe_to_indices
|
1125
|
+
end
|
1126
|
+
|
1127
|
+
def subscribe_to_indices
|
1128
|
+
indices = [
|
1129
|
+
{ segment: "IDX_I", security_id: "13", name: "NIFTY" },
|
1130
|
+
{ segment: "IDX_I", security_id: "25", name: "BANKNIFTY" },
|
1131
|
+
{ segment: "IDX_I", security_id: "29", name: "NIFTYIT" },
|
1132
|
+
{ segment: "IDX_I", security_id: "51", name: "SENSEX" }
|
1133
|
+
]
|
1134
|
+
|
1135
|
+
indices.each do |index|
|
1136
|
+
@client.subscribe_one(
|
1137
|
+
segment: index[:segment],
|
1138
|
+
security_id: index[:security_id]
|
1139
|
+
)
|
1140
|
+
puts "ā
Subscribed to #{index[:name]} (#{index[:segment]}:#{index[:security_id]})"
|
1141
|
+
end
|
1142
|
+
end
|
1143
|
+
|
1144
|
+
def process_data(tick)
|
1145
|
+
timestamp = tick[:ts] ? Time.at(tick[:ts]) : Time.now
|
1146
|
+
data = {
|
1147
|
+
segment: tick[:segment],
|
1148
|
+
security_id: tick[:security_id],
|
1149
|
+
ltp: tick[:ltp],
|
1150
|
+
timestamp: timestamp.iso8601
|
1151
|
+
}
|
1152
|
+
|
1153
|
+
puts "š Market Data: #{data[:segment]}:#{data[:security_id]} = #{data[:ltp]} at #{data[:timestamp]}"
|
1154
|
+
end
|
1155
|
+
|
1156
|
+
def wait_for_connection
|
1157
|
+
puts "ā
Connected successfully. Waiting for data..."
|
1158
|
+
while @running
|
1159
|
+
sleep(1)
|
1160
|
+
end
|
1161
|
+
end
|
1162
|
+
|
1163
|
+
def handle_connection_error(error)
|
1164
|
+
@retry_count += 1
|
1165
|
+
puts "ā Connection error: #{error.class} - #{error.message}"
|
1166
|
+
puts "š Retrying in #{@retry_delay} seconds..."
|
1167
|
+
sleep(@retry_delay)
|
1168
|
+
end
|
1169
|
+
|
1170
|
+
def handle_websocket_error(error)
|
1171
|
+
puts "ā WebSocket error: #{error}"
|
1172
|
+
# Don't increment retry count for WebSocket errors
|
1173
|
+
# The connection will be closed and we'll retry
|
1174
|
+
end
|
1175
|
+
|
1176
|
+
def handle_websocket_close(close_info)
|
1177
|
+
puts "š WebSocket closed: #{close_info[:code]} - #{close_info[:reason]}"
|
1178
|
+
|
1179
|
+
# Handle specific close codes
|
1180
|
+
case close_info[:code]
|
1181
|
+
when 1006
|
1182
|
+
puts "š Connection lost, will retry..."
|
1183
|
+
when 1000
|
1184
|
+
puts "ā
Normal closure"
|
1185
|
+
@running = false
|
1186
|
+
when 1001
|
1187
|
+
puts "š Going away, will retry..."
|
1188
|
+
when 1002
|
1189
|
+
puts "ā Protocol error, will retry..."
|
1190
|
+
when 1003
|
1191
|
+
puts "ā Unsupported data, will retry..."
|
1192
|
+
when 1004
|
1193
|
+
puts "ā Reserved, will retry..."
|
1194
|
+
when 1005
|
1195
|
+
puts "ā No status received, will retry..."
|
1196
|
+
when 1007
|
1197
|
+
puts "ā Invalid frame payload data, will retry..."
|
1198
|
+
when 1008
|
1199
|
+
puts "ā Policy violation, will retry..."
|
1200
|
+
when 1009
|
1201
|
+
puts "ā Message too big, will retry..."
|
1202
|
+
when 1010
|
1203
|
+
puts "ā Missing extension, will retry..."
|
1204
|
+
when 1011
|
1205
|
+
puts "ā Internal error, will retry..."
|
1206
|
+
when 1012
|
1207
|
+
puts "š Service restart, will retry..."
|
1208
|
+
when 1013
|
1209
|
+
puts "š Try again later, will retry..."
|
1210
|
+
when 1014
|
1211
|
+
puts "ā Bad gateway, will retry..."
|
1212
|
+
when 1015
|
1213
|
+
puts "ā TLS handshake, will retry..."
|
1214
|
+
else
|
1215
|
+
puts "ā Unknown close code: #{close_info[:code]}, will retry..."
|
1216
|
+
end
|
1217
|
+
end
|
1218
|
+
end
|
1219
|
+
|
1220
|
+
# Run the robust client
|
1221
|
+
client = RobustWebSocketClient.new
|
1222
|
+
|
1223
|
+
# Handle interrupt signals
|
1224
|
+
Signal.trap("INT") do
|
1225
|
+
puts "\nš Received interrupt signal, shutting down..."
|
1226
|
+
client.stop
|
1227
|
+
exit(0)
|
1228
|
+
end
|
1229
|
+
|
1230
|
+
Signal.trap("TERM") do
|
1231
|
+
puts "\nš Received terminate signal, shutting down..."
|
1232
|
+
client.stop
|
1233
|
+
exit(0)
|
1234
|
+
end
|
1235
|
+
|
1236
|
+
client.start
|
1237
|
+
```
|
1238
|
+
|
1239
|
+
## Production Considerations
|
1240
|
+
|
1241
|
+
### Logging
|
1242
|
+
|
1243
|
+
```ruby
|
1244
|
+
#!/usr/bin/env ruby
|
1245
|
+
# frozen_string_literal: true
|
1246
|
+
|
1247
|
+
require 'dhan_hq'
|
1248
|
+
require 'logger'
|
1249
|
+
require 'json'
|
1250
|
+
|
1251
|
+
# Configure logging
|
1252
|
+
logger = Logger.new(STDOUT)
|
1253
|
+
logger.level = Logger::INFO
|
1254
|
+
logger.formatter = proc do |severity, datetime, progname, msg|
|
1255
|
+
{
|
1256
|
+
timestamp: datetime.iso8601,
|
1257
|
+
level: severity,
|
1258
|
+
message: msg,
|
1259
|
+
service: 'dhanhq-websocket'
|
1260
|
+
}.to_json + "\n"
|
1261
|
+
end
|
1262
|
+
|
1263
|
+
# Configure DhanHQ
|
1264
|
+
DhanHQ.configure do |config|
|
1265
|
+
config.client_id = ENV["CLIENT_ID"] || "your_client_id"
|
1266
|
+
config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
|
1267
|
+
config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
|
1268
|
+
config.logger = logger
|
1269
|
+
end
|
1270
|
+
|
1271
|
+
class ProductionWebSocketClient
|
1272
|
+
def initialize
|
1273
|
+
@client = nil
|
1274
|
+
@running = false
|
1275
|
+
@logger = logger
|
1276
|
+
end
|
1277
|
+
|
1278
|
+
def start
|
1279
|
+
@running = true
|
1280
|
+
@logger.info("Starting WebSocket client")
|
1281
|
+
|
1282
|
+
@client = DhanHQ::WS.connect(mode: :ticker) do |tick|
|
1283
|
+
process_data(tick)
|
1284
|
+
end
|
1285
|
+
|
1286
|
+
# Add error handling
|
1287
|
+
@client.on(:error) do |error|
|
1288
|
+
@logger.error("WebSocket error: #{error}")
|
1289
|
+
end
|
1290
|
+
|
1291
|
+
@client.on(:close) do |close_info|
|
1292
|
+
@logger.warn("WebSocket closed: #{close_info[:code]} - #{close_info[:reason]}")
|
1293
|
+
end
|
1294
|
+
|
1295
|
+
# Subscribe to indices
|
1296
|
+
subscribe_to_indices
|
1297
|
+
|
1298
|
+
# Wait for data
|
1299
|
+
wait_for_data
|
1300
|
+
end
|
1301
|
+
|
1302
|
+
def stop
|
1303
|
+
@running = false
|
1304
|
+
@client&.stop
|
1305
|
+
@logger.info("WebSocket client stopped")
|
1306
|
+
end
|
1307
|
+
|
1308
|
+
private
|
1309
|
+
|
1310
|
+
def subscribe_to_indices
|
1311
|
+
indices = [
|
1312
|
+
{ segment: "IDX_I", security_id: "13", name: "NIFTY" },
|
1313
|
+
{ segment: "IDX_I", security_id: "25", name: "BANKNIFTY" },
|
1314
|
+
{ segment: "IDX_I", security_id: "29", name: "NIFTYIT" },
|
1315
|
+
{ segment: "IDX_I", security_id: "51", name: "SENSEX" }
|
1316
|
+
]
|
1317
|
+
|
1318
|
+
indices.each do |index|
|
1319
|
+
@client.subscribe_one(
|
1320
|
+
segment: index[:segment],
|
1321
|
+
security_id: index[:security_id]
|
1322
|
+
)
|
1323
|
+
@logger.info("Subscribed to #{index[:name]} (#{index[:segment]}:#{index[:security_id]})")
|
1324
|
+
end
|
1325
|
+
end
|
1326
|
+
|
1327
|
+
def process_data(tick)
|
1328
|
+
timestamp = tick[:ts] ? Time.at(tick[:ts]) : Time.now
|
1329
|
+
data = {
|
1330
|
+
segment: tick[:segment],
|
1331
|
+
security_id: tick[:security_id],
|
1332
|
+
ltp: tick[:ltp],
|
1333
|
+
timestamp: timestamp.iso8601
|
1334
|
+
}
|
1335
|
+
|
1336
|
+
@logger.info("Market data received: #{data[:segment]}:#{data[:security_id]} = #{data[:ltp]}")
|
1337
|
+
|
1338
|
+
# Save to file
|
1339
|
+
save_to_file(data)
|
1340
|
+
end
|
1341
|
+
|
1342
|
+
def save_to_file(data)
|
1343
|
+
File.open("/var/log/dhanhq-market-data.json", "a") do |file|
|
1344
|
+
file.puts(data.to_json)
|
1345
|
+
end
|
1346
|
+
end
|
1347
|
+
|
1348
|
+
def wait_for_data
|
1349
|
+
@logger.info("Waiting for market data...")
|
1350
|
+
while @running
|
1351
|
+
sleep(1)
|
1352
|
+
end
|
1353
|
+
end
|
1354
|
+
end
|
1355
|
+
|
1356
|
+
# Run the production client
|
1357
|
+
client = ProductionWebSocketClient.new
|
1358
|
+
|
1359
|
+
# Handle interrupt signals
|
1360
|
+
Signal.trap("INT") do
|
1361
|
+
client.stop
|
1362
|
+
exit(0)
|
1363
|
+
end
|
1364
|
+
|
1365
|
+
Signal.trap("TERM") do
|
1366
|
+
client.stop
|
1367
|
+
exit(0)
|
1368
|
+
end
|
1369
|
+
|
1370
|
+
client.start
|
1371
|
+
```
|
1372
|
+
|
1373
|
+
### Monitoring
|
1374
|
+
|
1375
|
+
```ruby
|
1376
|
+
#!/usr/bin/env ruby
|
1377
|
+
# frozen_string_literal: true
|
1378
|
+
|
1379
|
+
require 'dhan_hq'
|
1380
|
+
require 'json'
|
1381
|
+
|
1382
|
+
# Configure DhanHQ
|
1383
|
+
DhanHQ.configure do |config|
|
1384
|
+
config.client_id = ENV["CLIENT_ID"] || "your_client_id"
|
1385
|
+
config.access_token = ENV["ACCESS_TOKEN"] || "your_access_token"
|
1386
|
+
config.ws_user_type = ENV["DHAN_WS_USER_TYPE"] || "SELF"
|
1387
|
+
end
|
1388
|
+
|
1389
|
+
class WebSocketMonitor
|
1390
|
+
def initialize
|
1391
|
+
@clients = {}
|
1392
|
+
@stats = {
|
1393
|
+
total_messages: 0,
|
1394
|
+
errors: 0,
|
1395
|
+
reconnections: 0,
|
1396
|
+
start_time: Time.current
|
1397
|
+
}
|
1398
|
+
end
|
1399
|
+
|
1400
|
+
def start_monitoring
|
1401
|
+
puts "š Starting WebSocket monitoring..."
|
1402
|
+
|
1403
|
+
# Start market feed client
|
1404
|
+
start_market_feed_client
|
1405
|
+
|
1406
|
+
# Start order update client
|
1407
|
+
start_order_update_client
|
1408
|
+
|
1409
|
+
# Start market depth client
|
1410
|
+
start_market_depth_client
|
1411
|
+
|
1412
|
+
# Monitor loop
|
1413
|
+
monitor_loop
|
1414
|
+
end
|
1415
|
+
|
1416
|
+
def stop_monitoring
|
1417
|
+
puts "š Stopping WebSocket monitoring..."
|
1418
|
+
@clients.each_value(&:stop)
|
1419
|
+
end
|
1420
|
+
|
1421
|
+
private
|
1422
|
+
|
1423
|
+
def start_market_feed_client
|
1424
|
+
@clients[:market_feed] = DhanHQ::WS.connect(mode: :ticker) do |tick|
|
1425
|
+
@stats[:total_messages] += 1
|
1426
|
+
process_market_data(tick)
|
1427
|
+
end
|
1428
|
+
|
1429
|
+
@clients[:market_feed].on(:error) do |error|
|
1430
|
+
@stats[:errors] += 1
|
1431
|
+
puts "ā Market Feed Error: #{error}"
|
1432
|
+
end
|
1433
|
+
|
1434
|
+
@clients[:market_feed].on(:close) do |close_info|
|
1435
|
+
@stats[:reconnections] += 1
|
1436
|
+
puts "š Market Feed Closed: #{close_info[:code]}"
|
1437
|
+
end
|
1438
|
+
|
1439
|
+
# Subscribe to indices
|
1440
|
+
subscribe_to_indices(@clients[:market_feed])
|
1441
|
+
end
|
1442
|
+
|
1443
|
+
def start_order_update_client
|
1444
|
+
@clients[:order_updates] = DhanHQ::WS::Orders.connect do |order_update|
|
1445
|
+
@stats[:total_messages] += 1
|
1446
|
+
process_order_update(order_update)
|
1447
|
+
end
|
1448
|
+
|
1449
|
+
@clients[:order_updates].on(:error) do |error|
|
1450
|
+
@stats[:errors] += 1
|
1451
|
+
puts "ā Order Update Error: #{error}"
|
1452
|
+
end
|
1453
|
+
|
1454
|
+
@clients[:order_updates].on(:close) do |close_info|
|
1455
|
+
@stats[:reconnections] += 1
|
1456
|
+
puts "š Order Update Closed: #{close_info[:code]}"
|
1457
|
+
end
|
1458
|
+
end
|
1459
|
+
|
1460
|
+
def start_market_depth_client
|
1461
|
+
symbols = [
|
1462
|
+
{ symbol: "RELIANCE", exchange_segment: "NSE_EQ", security_id: "2885" },
|
1463
|
+
{ symbol: "TCS", exchange_segment: "NSE_EQ", security_id: "11536" }
|
1464
|
+
]
|
1465
|
+
|
1466
|
+
@clients[:market_depth] = DhanHQ::WS::MarketDepth.connect(symbols: symbols) do |depth_data|
|
1467
|
+
@stats[:total_messages] += 1
|
1468
|
+
process_market_depth(depth_data)
|
1469
|
+
end
|
1470
|
+
|
1471
|
+
@clients[:market_depth].on(:error) do |error|
|
1472
|
+
@stats[:errors] += 1
|
1473
|
+
puts "ā Market Depth Error: #{error}"
|
1474
|
+
end
|
1475
|
+
|
1476
|
+
@clients[:market_depth].on(:close) do |close_info|
|
1477
|
+
@stats[:reconnections] += 1
|
1478
|
+
puts "š Market Depth Closed: #{close_info[:code]}"
|
1479
|
+
end
|
1480
|
+
end
|
1481
|
+
|
1482
|
+
def subscribe_to_indices(client)
|
1483
|
+
indices = [
|
1484
|
+
{ segment: "IDX_I", security_id: "13", name: "NIFTY" },
|
1485
|
+
{ segment: "IDX_I", security_id: "25", name: "BANKNIFTY" },
|
1486
|
+
{ segment: "IDX_I", security_id: "29", name: "NIFTYIT" },
|
1487
|
+
{ segment: "IDX_I", security_id: "51", name: "SENSEX" }
|
1488
|
+
]
|
1489
|
+
|
1490
|
+
indices.each do |index|
|
1491
|
+
client.subscribe_one(
|
1492
|
+
segment: index[:segment],
|
1493
|
+
security_id: index[:security_id]
|
1494
|
+
)
|
1495
|
+
puts "ā
Subscribed to #{index[:name]} (#{index[:segment]}:#{index[:security_id]})"
|
1496
|
+
end
|
1497
|
+
end
|
1498
|
+
|
1499
|
+
def process_market_data(tick)
|
1500
|
+
timestamp = tick[:ts] ? Time.at(tick[:ts]) : Time.now
|
1501
|
+
puts "š Market Data: #{tick[:segment]}:#{tick[:security_id]} = #{tick[:ltp]} at #{timestamp}"
|
1502
|
+
end
|
1503
|
+
|
1504
|
+
def process_order_update(order_update)
|
1505
|
+
puts "š Order Update: #{order_update.order_no} - #{order_update.status}"
|
1506
|
+
end
|
1507
|
+
|
1508
|
+
def process_market_depth(depth_data)
|
1509
|
+
puts "š Market Depth: #{depth_data[:symbol]} - Bid: #{depth_data[:best_bid]}, Ask: #{depth_data[:best_ask]}"
|
1510
|
+
end
|
1511
|
+
|
1512
|
+
def monitor_loop
|
1513
|
+
puts "š Monitoring WebSocket connections..."
|
1514
|
+
puts "Press Ctrl+C to stop"
|
1515
|
+
|
1516
|
+
begin
|
1517
|
+
while true
|
1518
|
+
sleep(30) # Print stats every 30 seconds
|
1519
|
+
print_stats
|
1520
|
+
end
|
1521
|
+
rescue Interrupt
|
1522
|
+
puts "\nš Received interrupt signal, shutting down..."
|
1523
|
+
stop_monitoring
|
1524
|
+
end
|
1525
|
+
end
|
1526
|
+
|
1527
|
+
def print_stats
|
1528
|
+
uptime = Time.current - @stats[:start_time]
|
1529
|
+
puts "\nš WebSocket Statistics:"
|
1530
|
+
puts " Uptime: #{uptime.round(2)} seconds"
|
1531
|
+
puts " Total Messages: #{@stats[:total_messages]}"
|
1532
|
+
puts " Errors: #{@stats[:errors]}"
|
1533
|
+
puts " Reconnections: #{@stats[:reconnections]}"
|
1534
|
+
puts " Messages/sec: #{(@stats[:total_messages] / uptime).round(2)}"
|
1535
|
+
puts " Error Rate: #{(@stats[:errors].to_f / @stats[:total_messages] * 100).round(2)}%"
|
1536
|
+
puts " ---"
|
1537
|
+
end
|
1538
|
+
end
|
1539
|
+
|
1540
|
+
# Run the monitor
|
1541
|
+
monitor = WebSocketMonitor.new
|
1542
|
+
monitor.start_monitoring
|
1543
|
+
```
|
1544
|
+
|
1545
|
+
## Best Practices
|
1546
|
+
|
1547
|
+
### 1. Configuration Management
|
1548
|
+
|
1549
|
+
- Use environment variables for credentials
|
1550
|
+
- Implement configuration validation
|
1551
|
+
- Use different configurations for different environments
|
1552
|
+
|
1553
|
+
### 2. Error Handling
|
1554
|
+
|
1555
|
+
- Implement comprehensive error handling
|
1556
|
+
- Log all errors with context
|
1557
|
+
- Implement retry logic with exponential backoff
|
1558
|
+
- Handle connection failures gracefully
|
1559
|
+
|
1560
|
+
### 3. Resource Management
|
1561
|
+
|
1562
|
+
- Always clean up WebSocket connections
|
1563
|
+
- Implement proper signal handling
|
1564
|
+
- Monitor memory usage
|
1565
|
+
- Clean up old data regularly
|
1566
|
+
|
1567
|
+
### 4. Performance
|
1568
|
+
|
1569
|
+
- Use background processing for heavy operations
|
1570
|
+
- Implement caching for frequently accessed data
|
1571
|
+
- Monitor connection count and rate limits
|
1572
|
+
- Optimize data processing
|
1573
|
+
|
1574
|
+
### 5. Security
|
1575
|
+
|
1576
|
+
- Never log sensitive information
|
1577
|
+
- Use secure credential storage
|
1578
|
+
- Implement proper authentication
|
1579
|
+
- Monitor for suspicious activity
|
1580
|
+
|
1581
|
+
### 6. Monitoring
|
1582
|
+
|
1583
|
+
- Implement health checks
|
1584
|
+
- Monitor connection status
|
1585
|
+
- Track error rates and reconnections
|
1586
|
+
- Set up alerts for critical issues
|
1587
|
+
|
1588
|
+
This comprehensive standalone Ruby integration guide provides everything needed to integrate DhanHQ WebSocket connections into standalone Ruby applications with production-ready patterns and best practices.
|