DhanHQ 2.1.3 → 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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/.rubocop_todo.yml +185 -0
  4. data/CHANGELOG.md +31 -0
  5. data/GUIDE.md +173 -31
  6. data/README.md +437 -133
  7. data/README1.md +267 -26
  8. data/docs/live_order_updates.md +319 -0
  9. data/docs/rails_integration.md +1 -1
  10. data/docs/rails_websocket_integration.md +847 -0
  11. data/docs/standalone_ruby_websocket_integration.md +1588 -0
  12. data/docs/technical_analysis.md +1 -0
  13. data/docs/websocket_integration.md +871 -0
  14. data/examples/comprehensive_websocket_examples.rb +148 -0
  15. data/examples/instrument_finder_test.rb +195 -0
  16. data/examples/live_order_updates.rb +118 -0
  17. data/examples/market_depth_example.rb +144 -0
  18. data/examples/market_feed_example.rb +81 -0
  19. data/examples/order_update_example.rb +105 -0
  20. data/examples/trading_fields_example.rb +215 -0
  21. data/lib/DhanHQ/config.rb +1 -0
  22. data/lib/DhanHQ/configuration.rb +16 -1
  23. data/lib/DhanHQ/contracts/expired_options_data_contract.rb +103 -0
  24. data/lib/DhanHQ/contracts/modify_order_contract.rb +1 -0
  25. data/lib/DhanHQ/contracts/option_chain_contract.rb +11 -1
  26. data/lib/DhanHQ/contracts/trade_contract.rb +70 -0
  27. data/lib/DhanHQ/errors.rb +2 -0
  28. data/lib/DhanHQ/models/expired_options_data.rb +331 -0
  29. data/lib/DhanHQ/models/instrument.rb +96 -2
  30. data/lib/DhanHQ/models/option_chain.rb +2 -0
  31. data/lib/DhanHQ/models/order_update.rb +235 -0
  32. data/lib/DhanHQ/models/trade.rb +118 -31
  33. data/lib/DhanHQ/rate_limiter.rb +4 -2
  34. data/lib/DhanHQ/resources/expired_options_data.rb +22 -0
  35. data/lib/DhanHQ/version.rb +1 -1
  36. data/lib/DhanHQ/ws/base_connection.rb +249 -0
  37. data/lib/DhanHQ/ws/client.rb +1 -1
  38. data/lib/DhanHQ/ws/connection.rb +3 -3
  39. data/lib/DhanHQ/ws/decoder.rb +3 -3
  40. data/lib/DhanHQ/ws/market_depth/client.rb +376 -0
  41. data/lib/DhanHQ/ws/market_depth/decoder.rb +131 -0
  42. data/lib/DhanHQ/ws/market_depth.rb +74 -0
  43. data/lib/DhanHQ/ws/orders/client.rb +177 -10
  44. data/lib/DhanHQ/ws/orders/connection.rb +41 -83
  45. data/lib/DhanHQ/ws/orders.rb +31 -2
  46. data/lib/DhanHQ/ws/registry.rb +1 -0
  47. data/lib/DhanHQ/ws/segments.rb +21 -5
  48. data/lib/DhanHQ/ws/sub_state.rb +1 -1
  49. data/lib/DhanHQ/ws.rb +3 -2
  50. data/lib/{DhanHQ.rb → dhan_hq.rb} +5 -0
  51. data/lib/dhanhq/analysis/helpers/bias_aggregator.rb +18 -18
  52. data/lib/dhanhq/analysis/helpers/moneyness_helper.rb +1 -0
  53. data/lib/dhanhq/analysis/multi_timeframe_analyzer.rb +2 -0
  54. data/lib/dhanhq/analysis/options_buying_advisor.rb +4 -3
  55. data/lib/dhanhq/contracts/options_buying_advisor_contract.rb +1 -0
  56. data/lib/ta/candles.rb +1 -0
  57. data/lib/ta/fetcher.rb +1 -0
  58. data/lib/ta/indicators.rb +2 -1
  59. data/lib/ta/market_calendar.rb +4 -3
  60. data/lib/ta/technical_analysis.rb +3 -2
  61. metadata +38 -4
  62. data/lib/DhanHQ/ws/errors.rb +0 -0
  63. /data/lib/DhanHQ/contracts/{modify_order_contract copy.rb → modify_order_contract_copy.rb} +0 -0
@@ -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.