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,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.
|