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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/GUIDE.md +215 -73
  4. data/README.md +416 -132
  5. data/README1.md +267 -26
  6. data/docs/live_order_updates.md +319 -0
  7. data/docs/rails_websocket_integration.md +847 -0
  8. data/docs/standalone_ruby_websocket_integration.md +1588 -0
  9. data/docs/websocket_integration.md +871 -0
  10. data/examples/comprehensive_websocket_examples.rb +148 -0
  11. data/examples/instrument_finder_test.rb +195 -0
  12. data/examples/live_order_updates.rb +118 -0
  13. data/examples/market_depth_example.rb +144 -0
  14. data/examples/market_feed_example.rb +81 -0
  15. data/examples/order_update_example.rb +105 -0
  16. data/examples/trading_fields_example.rb +215 -0
  17. data/lib/DhanHQ/configuration.rb +16 -1
  18. data/lib/DhanHQ/contracts/expired_options_data_contract.rb +103 -0
  19. data/lib/DhanHQ/contracts/trade_contract.rb +70 -0
  20. data/lib/DhanHQ/errors.rb +2 -0
  21. data/lib/DhanHQ/models/expired_options_data.rb +331 -0
  22. data/lib/DhanHQ/models/instrument.rb +96 -2
  23. data/lib/DhanHQ/models/order_update.rb +235 -0
  24. data/lib/DhanHQ/models/trade.rb +118 -31
  25. data/lib/DhanHQ/resources/expired_options_data.rb +22 -0
  26. data/lib/DhanHQ/version.rb +1 -1
  27. data/lib/DhanHQ/ws/base_connection.rb +249 -0
  28. data/lib/DhanHQ/ws/connection.rb +2 -2
  29. data/lib/DhanHQ/ws/decoder.rb +3 -3
  30. data/lib/DhanHQ/ws/market_depth/client.rb +376 -0
  31. data/lib/DhanHQ/ws/market_depth/decoder.rb +131 -0
  32. data/lib/DhanHQ/ws/market_depth.rb +74 -0
  33. data/lib/DhanHQ/ws/orders/client.rb +175 -11
  34. data/lib/DhanHQ/ws/orders/connection.rb +40 -81
  35. data/lib/DhanHQ/ws/orders.rb +28 -0
  36. data/lib/DhanHQ/ws/segments.rb +18 -2
  37. data/lib/DhanHQ/ws.rb +3 -2
  38. data/lib/dhan_hq.rb +5 -0
  39. metadata +35 -1
@@ -0,0 +1,847 @@
1
+ # Rails WebSocket Integration Guide
2
+
3
+ This guide provides comprehensive instructions for integrating DhanHQ WebSocket connections into Rails applications, including best practices, service patterns, and production considerations.
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [Quick Start](#quick-start)
8
+ 2. [Configuration](#configuration)
9
+ 3. [Service Patterns](#service-patterns)
10
+ 4. [Background Processing](#background-processing)
11
+ 5. [ActionCable Integration](#actioncable-integration)
12
+ 6. [Production Considerations](#production-considerations)
13
+ 7. [Monitoring & Debugging](#monitoring--debugging)
14
+ 8. [Best Practices](#best-practices)
15
+
16
+ ## Quick Start
17
+
18
+ ### 1. Add to Gemfile
19
+
20
+ ```ruby
21
+ # Gemfile
22
+ gem 'dhan_hq'
23
+ ```
24
+
25
+ ### 2. Configure DhanHQ
26
+
27
+ ```ruby
28
+ # config/initializers/dhan_hq.rb
29
+ DhanHQ.configure do |config|
30
+ config.client_id = Rails.application.credentials.dhanhq[:client_id]
31
+ config.access_token = Rails.application.credentials.dhanhq[:access_token]
32
+ config.ws_user_type = Rails.application.credentials.dhanhq[:ws_user_type] || "SELF"
33
+ end
34
+ ```
35
+
36
+ ### 3. Set up Credentials
37
+
38
+ ```bash
39
+ # Add credentials
40
+ rails credentials:edit
41
+
42
+ # Add the following:
43
+ dhanhq:
44
+ client_id: your_client_id
45
+ access_token: your_access_token
46
+ ws_user_type: SELF
47
+ ```
48
+
49
+ ### 4. Create a Service
50
+
51
+ ```ruby
52
+ # app/services/market_data_service.rb
53
+ class MarketDataService
54
+ def initialize
55
+ @market_client = nil
56
+ end
57
+
58
+ def start_market_feed
59
+ @market_client = DhanHQ::WS.connect(mode: :ticker) do |tick|
60
+ process_market_data(tick)
61
+ end
62
+
63
+ # Subscribe to major indices
64
+ @market_client.subscribe_one(segment: "IDX_I", security_id: "13") # NIFTY
65
+ @market_client.subscribe_one(segment: "IDX_I", security_id: "25") # BANKNIFTY
66
+ @market_client.subscribe_one(segment: "IDX_I", security_id: "29") # NIFTYIT
67
+ @market_client.subscribe_one(segment: "IDX_I", security_id: "51") # SENSEX
68
+ end
69
+
70
+ def stop_market_feed
71
+ @market_client&.stop
72
+ @market_client = nil
73
+ end
74
+
75
+ private
76
+
77
+ def process_market_data(tick)
78
+ # Store in database
79
+ MarketData.create!(
80
+ segment: tick[:segment],
81
+ security_id: tick[:security_id],
82
+ ltp: tick[:ltp],
83
+ timestamp: tick[:ts] ? Time.at(tick[:ts]) : Time.now
84
+ )
85
+
86
+ # Broadcast via ActionCable
87
+ ActionCable.server.broadcast(
88
+ "market_data_#{tick[:segment]}",
89
+ {
90
+ segment: tick[:segment],
91
+ security_id: tick[:security_id],
92
+ ltp: tick[:ltp],
93
+ timestamp: tick[:ts]
94
+ }
95
+ )
96
+ end
97
+ end
98
+ ```
99
+
100
+ ## Configuration
101
+
102
+ ### Environment-Specific Configuration
103
+
104
+ ```ruby
105
+ # config/initializers/dhan_hq.rb
106
+ DhanHQ.configure do |config|
107
+ if Rails.env.production?
108
+ config.client_id = Rails.application.credentials.dhanhq[:client_id]
109
+ config.access_token = Rails.application.credentials.dhanhq[:access_token]
110
+ else
111
+ config.client_id = ENV["DHAN_CLIENT_ID"] || "your_client_id"
112
+ config.access_token = ENV["DHAN_ACCESS_TOKEN"] || "your_access_token"
113
+ end
114
+
115
+ config.ws_user_type = Rails.application.credentials.dhanhq[:ws_user_type] || "SELF"
116
+
117
+ # Use Rails logger
118
+ config.logger = Rails.logger
119
+ end
120
+ ```
121
+
122
+ ### Development Configuration
123
+
124
+ ```ruby
125
+ # config/environments/development.rb
126
+ Rails.application.configure do
127
+ # Enable WebSocket debugging
128
+ config.log_level = :debug
129
+
130
+ # Configure ActionCable for WebSocket testing
131
+ config.action_cable.allowed_request_origins = [
132
+ "http://localhost:3000",
133
+ "http://127.0.0.1:3000"
134
+ ]
135
+ end
136
+ ```
137
+
138
+ ### Production Configuration
139
+
140
+ ```ruby
141
+ # config/environments/production.rb
142
+ Rails.application.configure do
143
+ # Use structured logging
144
+ config.log_level = :info
145
+
146
+ # Configure ActionCable for production
147
+ config.action_cable.allowed_request_origins = [
148
+ "https://yourdomain.com"
149
+ ]
150
+ end
151
+ ```
152
+
153
+ ## Service Patterns
154
+
155
+ ### Market Data Service
156
+
157
+ ```ruby
158
+ # app/services/market_data_service.rb
159
+ class MarketDataService
160
+ include Singleton
161
+
162
+ def initialize
163
+ @market_client = nil
164
+ @running = false
165
+ end
166
+
167
+ def start_market_feed
168
+ return if @running
169
+
170
+ @running = true
171
+ @market_client = DhanHQ::WS.connect(mode: :ticker) do |tick|
172
+ process_market_data(tick)
173
+ end
174
+
175
+ # Add error handling
176
+ @market_client.on(:error) do |error|
177
+ Rails.logger.error "Market WebSocket error: #{error}"
178
+ @running = false
179
+ end
180
+
181
+ @market_client.on(:close) do |close_info|
182
+ Rails.logger.warn "Market WebSocket closed: #{close_info[:code]}"
183
+ @running = false
184
+ end
185
+
186
+ # Subscribe to indices
187
+ subscribe_to_indices
188
+ end
189
+
190
+ def stop_market_feed
191
+ @running = false
192
+ @market_client&.stop
193
+ @market_client = nil
194
+ end
195
+
196
+ def running?
197
+ @running && @market_client&.connected?
198
+ end
199
+
200
+ private
201
+
202
+ def subscribe_to_indices
203
+ indices = [
204
+ { segment: "IDX_I", security_id: "13", name: "NIFTY" },
205
+ { segment: "IDX_I", security_id: "25", name: "BANKNIFTY" },
206
+ { segment: "IDX_I", security_id: "29", name: "NIFTYIT" },
207
+ { segment: "IDX_I", security_id: "51", name: "SENSEX" }
208
+ ]
209
+
210
+ indices.each do |index|
211
+ @market_client.subscribe_one(
212
+ segment: index[:segment],
213
+ security_id: index[:security_id]
214
+ )
215
+ Rails.logger.info "Subscribed to #{index[:name]} (#{index[:segment]}:#{index[:security_id]})"
216
+ end
217
+ end
218
+
219
+ def process_market_data(tick)
220
+ # Store in database
221
+ market_data = MarketData.create!(
222
+ segment: tick[:segment],
223
+ security_id: tick[:security_id],
224
+ ltp: tick[:ltp],
225
+ timestamp: tick[:ts] ? Time.at(tick[:ts]) : Time.now
226
+ )
227
+
228
+ # Update cache
229
+ cache_key = "market_data_#{tick[:segment]}:#{tick[:security_id]}"
230
+ Rails.cache.write(cache_key, market_data, expires_in: 1.minute)
231
+
232
+ # Broadcast via ActionCable
233
+ ActionCable.server.broadcast(
234
+ "market_data_#{tick[:segment]}",
235
+ {
236
+ segment: tick[:segment],
237
+ security_id: tick[:security_id],
238
+ ltp: tick[:ltp],
239
+ timestamp: tick[:ts],
240
+ name: get_index_name(tick[:segment], tick[:security_id])
241
+ }
242
+ )
243
+
244
+ # Trigger background job for additional processing
245
+ ProcessMarketDataJob.perform_later(market_data)
246
+ end
247
+
248
+ def get_index_name(segment, security_id)
249
+ case "#{segment}:#{security_id}"
250
+ when "IDX_I:13" then "NIFTY"
251
+ when "IDX_I:25" then "BANKNIFTY"
252
+ when "IDX_I:29" then "NIFTYIT"
253
+ when "IDX_I:51" then "SENSEX"
254
+ else "#{segment}:#{security_id}"
255
+ end
256
+ end
257
+ end
258
+ ```
259
+
260
+ ### Order Update Service
261
+
262
+ ```ruby
263
+ # app/services/order_update_service.rb
264
+ class OrderUpdateService
265
+ include Singleton
266
+
267
+ def initialize
268
+ @orders_client = nil
269
+ @running = false
270
+ end
271
+
272
+ def start_order_updates
273
+ return if @running
274
+
275
+ @running = true
276
+ @orders_client = DhanHQ::WS::Orders.connect do |order_update|
277
+ process_order_update(order_update)
278
+ end
279
+
280
+ # Add comprehensive event handling
281
+ @orders_client.on(:update) do |order_update|
282
+ Rails.logger.info "Order updated: #{order_update.order_no} - #{order_update.status}"
283
+ end
284
+
285
+ @orders_client.on(:status_change) do |change_data|
286
+ Rails.logger.info "Order status changed: #{change_data[:previous_status]} -> #{change_data[:new_status]}"
287
+ end
288
+
289
+ @orders_client.on(:execution) do |execution_data|
290
+ Rails.logger.info "Order executed: #{execution_data[:new_traded_qty]} shares"
291
+ end
292
+
293
+ @orders_client.on(:order_traded) do |order_update|
294
+ Rails.logger.info "Order traded: #{order_update.order_no} - #{order_update.symbol}"
295
+ end
296
+
297
+ @orders_client.on(:order_rejected) do |order_update|
298
+ Rails.logger.error "Order rejected: #{order_update.order_no} - #{order_update.reason_description}"
299
+ end
300
+
301
+ @orders_client.on(:error) do |error|
302
+ Rails.logger.error "Order WebSocket error: #{error}"
303
+ @running = false
304
+ end
305
+
306
+ @orders_client.on(:close) do |close_info|
307
+ Rails.logger.warn "Order WebSocket closed: #{close_info[:code]}"
308
+ @running = false
309
+ end
310
+ end
311
+
312
+ def stop_order_updates
313
+ @running = false
314
+ @orders_client&.stop
315
+ @orders_client = nil
316
+ end
317
+
318
+ def running?
319
+ @running && @orders_client&.connected?
320
+ end
321
+
322
+ private
323
+
324
+ def process_order_update(order_update)
325
+ # Update database
326
+ order = Order.find_by(order_no: order_update.order_no)
327
+ if order
328
+ order.update!(
329
+ status: order_update.status,
330
+ traded_qty: order_update.traded_qty,
331
+ avg_price: order_update.avg_traded_price,
332
+ execution_percentage: order_update.execution_percentage
333
+ )
334
+
335
+ # Broadcast to user
336
+ ActionCable.server.broadcast(
337
+ "order_updates_#{order.user_id}",
338
+ {
339
+ order_no: order_update.order_no,
340
+ status: order_update.status,
341
+ traded_qty: order_update.traded_qty,
342
+ execution_percentage: order_update.execution_percentage,
343
+ symbol: order_update.symbol,
344
+ price: order_update.price
345
+ }
346
+ )
347
+
348
+ # Trigger background job for additional processing
349
+ ProcessOrderUpdateJob.perform_later(order_update)
350
+ end
351
+ end
352
+ end
353
+ ```
354
+
355
+ ### Market Depth Service
356
+
357
+ ```ruby
358
+ # app/services/market_depth_service.rb
359
+ class MarketDepthService
360
+ include Singleton
361
+
362
+ def initialize
363
+ @depth_client = nil
364
+ @running = false
365
+ end
366
+
367
+ def start_market_depth(symbols = nil)
368
+ return if @running
369
+
370
+ @running = true
371
+ symbols ||= default_symbols
372
+
373
+ @depth_client = DhanHQ::WS::MarketDepth.connect(symbols: symbols) do |depth_data|
374
+ process_market_depth(depth_data)
375
+ end
376
+
377
+ # Add error handling
378
+ @depth_client.on(:error) do |error|
379
+ Rails.logger.error "Market Depth WebSocket error: #{error}"
380
+ @running = false
381
+ end
382
+
383
+ @depth_client.on(:close) do |close_info|
384
+ Rails.logger.warn "Market Depth WebSocket closed: #{close_info[:code]}"
385
+ @running = false
386
+ end
387
+ end
388
+
389
+ def stop_market_depth
390
+ @running = false
391
+ @depth_client&.stop
392
+ @depth_client = nil
393
+ end
394
+
395
+ def running?
396
+ @running && @depth_client&.connected?
397
+ end
398
+
399
+ private
400
+
401
+ def default_symbols
402
+ [
403
+ { symbol: "RELIANCE", exchange_segment: "NSE_EQ", security_id: "2885" },
404
+ { symbol: "TCS", exchange_segment: "NSE_EQ", security_id: "11536" }
405
+ ]
406
+ end
407
+
408
+ def process_market_depth(depth_data)
409
+ # Store in database
410
+ market_depth = MarketDepth.create!(
411
+ symbol: depth_data[:symbol],
412
+ exchange_segment: depth_data[:exchange_segment],
413
+ security_id: depth_data[:security_id],
414
+ best_bid: depth_data[:best_bid],
415
+ best_ask: depth_data[:best_ask],
416
+ spread: depth_data[:spread],
417
+ bid_levels: depth_data[:bids],
418
+ ask_levels: depth_data[:asks],
419
+ timestamp: Time.current
420
+ )
421
+
422
+ # Update cache
423
+ cache_key = "market_depth_#{depth_data[:symbol]}"
424
+ Rails.cache.write(cache_key, market_depth, expires_in: 30.seconds)
425
+
426
+ # Broadcast via ActionCable
427
+ ActionCable.server.broadcast(
428
+ "market_depth_#{depth_data[:symbol]}",
429
+ {
430
+ symbol: depth_data[:symbol],
431
+ best_bid: depth_data[:best_bid],
432
+ best_ask: depth_data[:best_ask],
433
+ spread: depth_data[:spread],
434
+ bid_levels: depth_data[:bids],
435
+ ask_levels: depth_data[:asks],
436
+ timestamp: Time.current
437
+ }
438
+ )
439
+
440
+ # Trigger background job for additional processing
441
+ ProcessMarketDepthJob.perform_later(market_depth)
442
+ end
443
+ end
444
+ ```
445
+
446
+ ## Background Processing
447
+
448
+ ### Market Data Processing Job
449
+
450
+ ```ruby
451
+ # app/jobs/process_market_data_job.rb
452
+ class ProcessMarketDataJob < ApplicationJob
453
+ queue_as :market_data
454
+
455
+ def perform(market_data)
456
+ # Update real-time cache
457
+ Rails.cache.write(
458
+ "realtime_#{market_data.segment}:#{market_data.security_id}",
459
+ market_data,
460
+ expires_in: 1.minute
461
+ )
462
+
463
+ # Update user portfolios if needed
464
+ update_user_portfolios(market_data)
465
+
466
+ # Send notifications for significant price movements
467
+ check_price_alerts(market_data)
468
+ end
469
+
470
+ private
471
+
472
+ def update_user_portfolios(market_data)
473
+ # Update portfolio values for users holding this instrument
474
+ Portfolio.where(segment: market_data.segment, security_id: market_data.security_id)
475
+ .find_each do |portfolio|
476
+ portfolio.update_current_value(market_data.ltp)
477
+ end
478
+ end
479
+
480
+ def check_price_alerts(market_data)
481
+ # Check for price alerts
482
+ PriceAlert.where(segment: market_data.segment, security_id: market_data.security_id)
483
+ .where("target_price <= ?", market_data.ltp)
484
+ .find_each do |alert|
485
+ PriceAlertNotificationJob.perform_later(alert, market_data)
486
+ end
487
+ end
488
+ end
489
+ ```
490
+
491
+ ### Order Update Processing Job
492
+
493
+ ```ruby
494
+ # app/jobs/process_order_update_job.rb
495
+ class ProcessOrderUpdateJob < ApplicationJob
496
+ queue_as :order_updates
497
+
498
+ def perform(order_update)
499
+ # Update order history
500
+ OrderHistory.create!(
501
+ order_no: order_update.order_no,
502
+ status: order_update.status,
503
+ traded_qty: order_update.traded_qty,
504
+ avg_price: order_update.avg_traded_price,
505
+ execution_percentage: order_update.execution_percentage,
506
+ timestamp: Time.current
507
+ )
508
+
509
+ # Update user's trading statistics
510
+ update_trading_stats(order_update)
511
+
512
+ # Send email notifications for completed orders
513
+ send_order_completion_email(order_update) if order_update.fully_executed?
514
+ end
515
+
516
+ private
517
+
518
+ def update_trading_stats(order_update)
519
+ user = User.joins(:orders).find_by(orders: { order_no: order_update.order_no })
520
+ return unless user
521
+
522
+ user.trading_stats.increment!(:total_trades) if order_update.fully_executed?
523
+ user.trading_stats.increment!(:total_volume, order_update.traded_qty)
524
+ end
525
+
526
+ def send_order_completion_email(order_update)
527
+ user = User.joins(:orders).find_by(orders: { order_no: order_update.order_no })
528
+ return unless user
529
+
530
+ OrderCompletionMailer.order_completed(user, order_update).deliver_later
531
+ end
532
+ end
533
+ ```
534
+
535
+ ## ActionCable Integration
536
+
537
+ ### Market Data Channel
538
+
539
+ ```ruby
540
+ # app/channels/market_data_channel.rb
541
+ class MarketDataChannel < ApplicationCable::Channel
542
+ def subscribed
543
+ stream_from "market_data_#{params[:segment]}"
544
+ end
545
+
546
+ def unsubscribed
547
+ # Any cleanup needed when channel is unsubscribed
548
+ end
549
+
550
+ def receive(data)
551
+ # Handle any data sent from the client
552
+ end
553
+ end
554
+ ```
555
+
556
+ ### Order Updates Channel
557
+
558
+ ```ruby
559
+ # app/channels/order_updates_channel.rb
560
+ class OrderUpdatesChannel < ApplicationCable::Channel
561
+ def subscribed
562
+ stream_from "order_updates_#{current_user.id}"
563
+ end
564
+
565
+ def unsubscribed
566
+ # Any cleanup needed when channel is unsubscribed
567
+ end
568
+ end
569
+ ```
570
+
571
+ ### Market Depth Channel
572
+
573
+ ```ruby
574
+ # app/channels/market_depth_channel.rb
575
+ class MarketDepthChannel < ApplicationCable::Channel
576
+ def subscribed
577
+ stream_from "market_depth_#{params[:symbol]}"
578
+ end
579
+
580
+ def unsubscribed
581
+ # Any cleanup needed when channel is unsubscribed
582
+ end
583
+ end
584
+ ```
585
+
586
+ ### JavaScript Client
587
+
588
+ ```javascript
589
+ // app/assets/javascripts/channels/market_data.js
590
+ import consumer from "./consumer"
591
+
592
+ consumer.subscriptions.create("MarketDataChannel", {
593
+ connected() {
594
+ console.log("Connected to market data channel")
595
+ },
596
+
597
+ disconnected() {
598
+ console.log("Disconnected from market data channel")
599
+ },
600
+
601
+ received(data) {
602
+ console.log("Market data received:", data)
603
+ updateMarketDataDisplay(data)
604
+ }
605
+ })
606
+
607
+ function updateMarketDataDisplay(data) {
608
+ const element = document.getElementById(`market-data-${data.segment}-${data.security_id}`)
609
+ if (element) {
610
+ element.textContent = data.ltp
611
+ element.classList.add('updated')
612
+ setTimeout(() => element.classList.remove('updated'), 1000)
613
+ }
614
+ }
615
+ ```
616
+
617
+ ## Production Considerations
618
+
619
+ ### Application Controller Integration
620
+
621
+ ```ruby
622
+ # app/controllers/application_controller.rb
623
+ class ApplicationController < ActionController::Base
624
+ around_action :ensure_websocket_cleanup
625
+
626
+ private
627
+
628
+ def ensure_websocket_cleanup
629
+ yield
630
+ ensure
631
+ # Clean up any stray WebSocket connections
632
+ DhanHQ::WS.disconnect_all_local!
633
+ end
634
+ end
635
+ ```
636
+
637
+ ### Initializer for Production
638
+
639
+ ```ruby
640
+ # config/initializers/websocket_services.rb
641
+ Rails.application.config.after_initialize do
642
+ # Start WebSocket services in production
643
+ if Rails.env.production?
644
+ # Start market data service
645
+ MarketDataService.instance.start_market_feed
646
+
647
+ # Start order update service
648
+ OrderUpdateService.instance.start_order_updates
649
+
650
+ # Start market depth service
651
+ MarketDepthService.instance.start_market_depth
652
+ end
653
+ end
654
+ ```
655
+
656
+ ### Graceful Shutdown
657
+
658
+ ```ruby
659
+ # config/initializers/graceful_shutdown.rb
660
+ Rails.application.config.after_initialize do
661
+ # Handle graceful shutdown
662
+ Signal.trap("TERM") do
663
+ Rails.logger.info "Received TERM signal, shutting down gracefully..."
664
+
665
+ # Stop WebSocket services
666
+ MarketDataService.instance.stop_market_feed
667
+ OrderUpdateService.instance.stop_order_updates
668
+ MarketDepthService.instance.stop_market_depth
669
+
670
+ # Disconnect all WebSocket connections
671
+ DhanHQ::WS.disconnect_all_local!
672
+
673
+ Rails.logger.info "Graceful shutdown completed"
674
+ exit(0)
675
+ end
676
+
677
+ Signal.trap("INT") do
678
+ Rails.logger.info "Received INT signal, shutting down gracefully..."
679
+
680
+ # Stop WebSocket services
681
+ MarketDataService.instance.stop_market_feed
682
+ OrderUpdateService.instance.stop_order_updates
683
+ MarketDepthService.instance.stop_market_depth
684
+
685
+ # Disconnect all WebSocket connections
686
+ DhanHQ::WS.disconnect_all_local!
687
+
688
+ Rails.logger.info "Graceful shutdown completed"
689
+ exit(0)
690
+ end
691
+ end
692
+ ```
693
+
694
+ ### Health Check Endpoint
695
+
696
+ ```ruby
697
+ # app/controllers/health_controller.rb
698
+ class HealthController < ApplicationController
699
+ def websocket_status
700
+ render json: {
701
+ market_data: {
702
+ running: MarketDataService.instance.running?,
703
+ connected: MarketDataService.instance.running?
704
+ },
705
+ order_updates: {
706
+ running: OrderUpdateService.instance.running?,
707
+ connected: OrderUpdateService.instance.running?
708
+ },
709
+ market_depth: {
710
+ running: MarketDepthService.instance.running?,
711
+ connected: MarketDepthService.instance.running?
712
+ }
713
+ }
714
+ end
715
+ end
716
+ ```
717
+
718
+ ## Monitoring & Debugging
719
+
720
+ ### Logging Configuration
721
+
722
+ ```ruby
723
+ # config/environments/production.rb
724
+ Rails.application.configure do
725
+ # Structured logging for WebSocket events
726
+ config.log_formatter = proc do |severity, datetime, progname, msg|
727
+ {
728
+ timestamp: datetime,
729
+ level: severity,
730
+ message: msg,
731
+ service: 'dhanhq-websocket'
732
+ }.to_json + "\n"
733
+ end
734
+ end
735
+ ```
736
+
737
+ ### Custom Logger
738
+
739
+ ```ruby
740
+ # app/services/websocket_logger.rb
741
+ class WebSocketLogger
742
+ def self.log_event(event_type, data)
743
+ Rails.logger.info({
744
+ event: event_type,
745
+ data: data,
746
+ timestamp: Time.current,
747
+ service: 'dhanhq-websocket'
748
+ }.to_json)
749
+ end
750
+ end
751
+ ```
752
+
753
+ ### Monitoring Dashboard
754
+
755
+ ```ruby
756
+ # app/controllers/admin/websocket_monitor_controller.rb
757
+ class Admin::WebsocketMonitorController < ApplicationController
758
+ before_action :authenticate_admin!
759
+
760
+ def index
761
+ @market_data_status = MarketDataService.instance.running?
762
+ @order_updates_status = OrderUpdateService.instance.running?
763
+ @market_depth_status = MarketDepthService.instance.running?
764
+
765
+ @recent_market_data = MarketData.order(created_at: :desc).limit(100)
766
+ @recent_order_updates = OrderHistory.order(created_at: :desc).limit(100)
767
+ end
768
+
769
+ def restart_services
770
+ # Restart WebSocket services
771
+ MarketDataService.instance.stop_market_feed
772
+ MarketDataService.instance.start_market_feed
773
+
774
+ OrderUpdateService.instance.stop_order_updates
775
+ OrderUpdateService.instance.start_order_updates
776
+
777
+ MarketDepthService.instance.stop_market_depth
778
+ MarketDepthService.instance.start_market_depth
779
+
780
+ redirect_to admin_websocket_monitor_index_path, notice: 'WebSocket services restarted'
781
+ end
782
+ end
783
+ ```
784
+
785
+ ## Best Practices
786
+
787
+ ### 1. Service Management
788
+
789
+ - Use singleton pattern for WebSocket services
790
+ - Implement proper start/stop methods
791
+ - Add health check endpoints
792
+ - Monitor service status
793
+
794
+ ### 2. Error Handling
795
+
796
+ - Implement comprehensive error handling
797
+ - Log all WebSocket events
798
+ - Handle connection failures gracefully
799
+ - Implement retry logic
800
+
801
+ ### 3. Performance
802
+
803
+ - Use background jobs for heavy processing
804
+ - Implement caching for frequently accessed data
805
+ - Monitor memory usage
806
+ - Clean up old data regularly
807
+
808
+ ### 4. Security
809
+
810
+ - Use environment variables for credentials
811
+ - Implement proper authentication for channels
812
+ - Sanitize all user inputs
813
+ - Monitor for suspicious activity
814
+
815
+ ### 5. Testing
816
+
817
+ ```ruby
818
+ # spec/services/market_data_service_spec.rb
819
+ RSpec.describe MarketDataService do
820
+ let(:service) { MarketDataService.instance }
821
+
822
+ before do
823
+ service.stop_market_feed
824
+ end
825
+
826
+ after do
827
+ service.stop_market_feed
828
+ end
829
+
830
+ describe '#start_market_feed' do
831
+ it 'starts the market feed successfully' do
832
+ expect { service.start_market_feed }.not_to raise_error
833
+ expect(service.running?).to be true
834
+ end
835
+ end
836
+
837
+ describe '#stop_market_feed' do
838
+ it 'stops the market feed successfully' do
839
+ service.start_market_feed
840
+ service.stop_market_feed
841
+ expect(service.running?).to be false
842
+ end
843
+ end
844
+ end
845
+ ```
846
+
847
+ This comprehensive Rails integration guide provides everything needed to integrate DhanHQ WebSocket connections into Rails applications with production-ready patterns and best practices.