intrinio-realtime 4.1.2 → 5.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/intrinio-realtime.rb +114 -55
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c3beedb23f93b49b061ba476daa8520cb3f09cfde888eaddbd08fec263a39de3
|
4
|
+
data.tar.gz: e81ce551b2ce00694e0d7ef238b6b63d60b5deb365e20dc3583c8b7819a9eb7a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c09c353f980bcd5bd4cd36687bcbe0cd87661b5f3b428c98299374f69d19e01c6408d107842e2b46c84d26f2dc429bd6278e04abc5b2890de444e33590361467
|
7
|
+
data.tar.gz: f7d2c9141ee70a4cff900669152d1e2cda68ffba54d5360c4f013e88866bea9a829a49e79f3ce0f82444bf2b3c49fb592c8285edf2da0bb3be88516345da1927
|
data/lib/intrinio-realtime.rb
CHANGED
@@ -7,14 +7,25 @@ require 'websocket-client-simple'
|
|
7
7
|
|
8
8
|
module Intrinio
|
9
9
|
module Realtime
|
10
|
-
HEARTBEAT_TIME = 3
|
11
10
|
SELF_HEAL_BACKOFFS = [0, 100, 500, 1000, 2000, 5000].freeze
|
12
11
|
REALTIME = "REALTIME".freeze
|
13
12
|
MANUAL = "MANUAL".freeze
|
14
13
|
DELAYED_SIP = "DELAYED_SIP".freeze
|
15
|
-
|
14
|
+
NASDAQ_BASIC = "NASDAQ_BASIC".freeze
|
15
|
+
NO_SUBPROVIDER = "NONE".freeze
|
16
|
+
CTA_A = "CTA_A".freeze
|
17
|
+
CTA_B = "CTA_B".freeze
|
18
|
+
UTP = "UTP".freeze
|
19
|
+
OTC = "OTC".freeze
|
20
|
+
IEX = "IEX".freeze
|
21
|
+
PROVIDERS = [REALTIME, MANUAL, DELAYED_SIP, NASDAQ_BASIC].freeze
|
22
|
+
SUBPROVIDERS = [NO_SUBPROVIDER, CTA_A, CTA_B, UTP, OTC, NASDAQ_BASIC, IEX].freeze
|
16
23
|
ASK = "Ask".freeze
|
17
24
|
BID = "Bid".freeze
|
25
|
+
CLIENT_INFO_HEADER_KEY = "Client-Information".freeze
|
26
|
+
CLIENT_INFO_HEADER_VALUE = "IntrinioRealtimeRubySDKv5.0".freeze
|
27
|
+
MESSAGE_VERSION_HEADER_KEY = "UseNewEquitiesFormat".freeze
|
28
|
+
MESSAGE_VERSION_HEADER_VALUE = "v2".freeze
|
18
29
|
|
19
30
|
def self.connect(options, on_trade, on_quote)
|
20
31
|
EM.run do
|
@@ -24,12 +35,15 @@ module Intrinio
|
|
24
35
|
end
|
25
36
|
|
26
37
|
class Trade
|
27
|
-
def initialize(symbol, price, size, timestamp, total_volume)
|
38
|
+
def initialize(symbol, price, size, timestamp, total_volume, subprovider, market_center, condition)
|
28
39
|
@symbol = symbol
|
29
40
|
@price = price
|
30
41
|
@size = size
|
31
42
|
@timestamp = timestamp
|
32
43
|
@total_volume = total_volume
|
44
|
+
@subprovider = subprovider
|
45
|
+
@market_center = market_center
|
46
|
+
@condition = condition
|
33
47
|
end
|
34
48
|
|
35
49
|
def symbol
|
@@ -52,18 +66,33 @@ module Intrinio
|
|
52
66
|
@total_volume
|
53
67
|
end
|
54
68
|
|
69
|
+
def subprovider
|
70
|
+
@subprovider
|
71
|
+
end
|
72
|
+
|
73
|
+
def market_center
|
74
|
+
@market_center
|
75
|
+
end
|
76
|
+
|
77
|
+
def condition
|
78
|
+
@condition
|
79
|
+
end
|
80
|
+
|
55
81
|
def to_s
|
56
|
-
[@symbol, @price, @size, @timestamp, @total_volume].join(",")
|
82
|
+
[@symbol, @price, @size, @timestamp, @total_volume, @subprovider, @market_center, @condition].join(",")
|
57
83
|
end
|
58
84
|
end
|
59
85
|
|
60
86
|
class Quote
|
61
|
-
def initialize(type, symbol, price, size, timestamp)
|
87
|
+
def initialize(type, symbol, price, size, timestamp, subprovider, market_center, condition)
|
62
88
|
@type = type
|
63
89
|
@symbol = symbol
|
64
90
|
@price = price
|
65
91
|
@size = size
|
66
92
|
@timestamp = timestamp
|
93
|
+
@subprovider = subprovider
|
94
|
+
@market_center = market_center
|
95
|
+
@condition = condition
|
67
96
|
end
|
68
97
|
|
69
98
|
def type
|
@@ -86,8 +115,20 @@ module Intrinio
|
|
86
115
|
@timestamp
|
87
116
|
end
|
88
117
|
|
118
|
+
def subprovider
|
119
|
+
@subprovider
|
120
|
+
end
|
121
|
+
|
122
|
+
def market_center
|
123
|
+
@market_center
|
124
|
+
end
|
125
|
+
|
126
|
+
def condition
|
127
|
+
@condition
|
128
|
+
end
|
129
|
+
|
89
130
|
def to_s
|
90
|
-
[@symbol, @type, @price, @size, @timestamp].join(",")
|
131
|
+
[@symbol, @type, @price, @size, @timestamp, @subprovider, @market_center, @condition].join(",")
|
91
132
|
end
|
92
133
|
end
|
93
134
|
|
@@ -114,7 +155,7 @@ module Intrinio
|
|
114
155
|
unless @provider
|
115
156
|
@provider = REALTIME
|
116
157
|
end
|
117
|
-
raise "Provider must be 'REALTIME' or 'MANUAL'" unless PROVIDERS.include?(@provider)
|
158
|
+
raise "Provider must be 'REALTIME', 'DELAYED_SIP', 'NASDAQ_BASIC', or 'MANUAL'" unless PROVIDERS.include?(@provider)
|
118
159
|
|
119
160
|
@ip_address = options[:ip_address]
|
120
161
|
raise "Missing option ip_address while in MANUAL mode." if @provider == MANUAL and (@ip_address.nil? || @ip_address.empty?)
|
@@ -147,7 +188,6 @@ module Intrinio
|
|
147
188
|
|
148
189
|
@ready = false
|
149
190
|
@joined_channels = []
|
150
|
-
@heartbeat_timer = nil
|
151
191
|
@selfheal_timer = nil
|
152
192
|
@selfheal_backoffs = Array.new(SELF_HEAL_BACKOFFS)
|
153
193
|
@ws = nil
|
@@ -205,7 +245,6 @@ module Intrinio
|
|
205
245
|
end
|
206
246
|
|
207
247
|
def disconnect
|
208
|
-
EM.cancel_timer(@heartbeat_timer) if @heartbeat_timer
|
209
248
|
EM.cancel_timer(@selfheal_timer) if @selfheal_timer
|
210
249
|
@ready = false
|
211
250
|
@closing = true
|
@@ -256,38 +295,71 @@ module Intrinio
|
|
256
295
|
data.map { |i| [sprintf('%02x',i)].pack('H2') }.join.unpack('e').first
|
257
296
|
end
|
258
297
|
|
259
|
-
def
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
298
|
+
def parse_subprovider(byte)
|
299
|
+
case byte
|
300
|
+
when 0
|
301
|
+
NO_SUBPROVIDER
|
302
|
+
when 1
|
303
|
+
CTA_A
|
304
|
+
when 2
|
305
|
+
CTA_B
|
306
|
+
when 3
|
307
|
+
UTP
|
308
|
+
when 4
|
309
|
+
OTC
|
310
|
+
when 5
|
311
|
+
NASDAQ_BASIC
|
312
|
+
when 6
|
313
|
+
IEX
|
314
|
+
else
|
315
|
+
IEX
|
316
|
+
end
|
266
317
|
end
|
267
318
|
|
268
|
-
def
|
319
|
+
def parse_trade(data, start_index)
|
320
|
+
symbol_length = data[start_index + 2]
|
321
|
+
condition_length = data[start_index + 26 + symbol_length]
|
322
|
+
symbol = data[start_index + 3, symbol_length].map!{|c| c.chr}.join
|
323
|
+
price = parse_float32(data[start_index + 6 + symbol_length, 4])
|
324
|
+
size = parse_uint32(data[start_index + 10 + symbol_length, 4])
|
325
|
+
timestamp = parse_uint64(data[start_index + 14 + symbol_length, 8])
|
326
|
+
total_volume = parse_uint32(data[start_index + 22 + symbol_length, 4])
|
327
|
+
subprovider = parse_subprovider(data[start_index + 3 + symbol_length])
|
328
|
+
market_center = data[start_index + 4 + symbol_length, 2].pack("C*").encode!('UTF-8', 'UTF-16LE')
|
329
|
+
condition = if condition_length > 0 then data[start_index + 27 + symbol_length, condition_length].map!{|c| c.chr}.join else "" end
|
330
|
+
|
331
|
+
return Trade.new(symbol, price, size, timestamp, total_volume, subprovider, market_center, condition)
|
332
|
+
end
|
333
|
+
|
334
|
+
def parse_quote(data, start_index, msg_type)
|
335
|
+
symbol_length = data[start_index + 2]
|
336
|
+
condition_length = data[start_index + 22 + symbol_length]
|
269
337
|
type = case when msg_type == 1 then ASK when msg_type == 2 then BID end
|
270
|
-
symbol = data[start_index +
|
271
|
-
price = parse_float32(data[start_index +
|
272
|
-
size = parse_uint32(data[start_index +
|
273
|
-
timestamp = parse_uint64(data[start_index +
|
274
|
-
|
338
|
+
symbol = data[start_index + 3, symbol_length].map!{|c| c.chr}.join
|
339
|
+
price = parse_float32(data[start_index + 6 + symbol_length, 4])
|
340
|
+
size = parse_uint32(data[start_index + 10 + symbol_length, 4])
|
341
|
+
timestamp = parse_uint64(data[start_index + 14 + symbol_length, 8])
|
342
|
+
subprovider = parse_subprovider(data[start_index + 3 + symbol_length])
|
343
|
+
market_center = data[start_index + 4 + symbol_length, 2].pack("C*").encode!('UTF-8', 'UTF-16LE')
|
344
|
+
condition = if condition_length > 0 then data[start_index + 23 + symbol_length, condition_length].map!{|c| c.chr}.join else "" end
|
345
|
+
|
346
|
+
return Quote.new(type, symbol, price, size, timestamp, subprovider, market_center, condition)
|
275
347
|
end
|
276
348
|
|
277
349
|
def handle_message(data, start_index)
|
278
350
|
msg_type = data[start_index]
|
279
|
-
|
351
|
+
msg_length = data[start_index + 1]
|
280
352
|
case msg_type
|
281
353
|
when 0 then
|
282
|
-
trade = parse_trade(data, start_index
|
354
|
+
trade = parse_trade(data, start_index)
|
283
355
|
@on_trade.call(trade)
|
284
|
-
return start_index +
|
356
|
+
return start_index + msg_length
|
285
357
|
when 1 || 2 then
|
286
|
-
quote = parse_quote(data, start_index,
|
358
|
+
quote = parse_quote(data, start_index, msg_type)
|
287
359
|
@on_quote.call(quote)
|
288
|
-
return start_index +
|
360
|
+
return start_index + msg_length
|
289
361
|
end
|
290
|
-
return start_index
|
362
|
+
return start_index + msg_length
|
291
363
|
end
|
292
364
|
|
293
365
|
def handle_data
|
@@ -331,7 +403,7 @@ module Intrinio
|
|
331
403
|
http.use_ssl = true if (auth_url.include?("https"))
|
332
404
|
http.start
|
333
405
|
request = Net::HTTP::Get.new(uri.request_uri)
|
334
|
-
request.add_field(
|
406
|
+
request.add_field(CLIENT_INFO_HEADER_KEY, CLIENT_INFO_HEADER_VALUE)
|
335
407
|
|
336
408
|
unless @api_key
|
337
409
|
request.basic_auth(@username, @password)
|
@@ -352,6 +424,7 @@ module Intrinio
|
|
352
424
|
case @provider
|
353
425
|
when REALTIME then url = "https://realtime-mx.intrinio.com/auth"
|
354
426
|
when DELAYED_SIP then url = "https://realtime-delayed-sip.intrinio.com/auth"
|
427
|
+
when NASDAQ_BASIC then url = "https://realtime-nasdaq-basic.intrinio.com/auth"
|
355
428
|
when MANUAL then url = "http://" + @ip_address + "/auth"
|
356
429
|
end
|
357
430
|
|
@@ -372,9 +445,10 @@ module Intrinio
|
|
372
445
|
|
373
446
|
def socket_url
|
374
447
|
case @provider
|
375
|
-
when REALTIME then "wss://realtime-mx.intrinio.com/socket/websocket?vsn=1.0.0&token=#{@token}"
|
376
|
-
when DELAYED_SIP then "wss://realtime-delayed-sip.intrinio.com/socket/websocket?vsn=1.0.0&token=#{@token}"
|
377
|
-
|
448
|
+
when REALTIME then "wss://realtime-mx.intrinio.com/socket/websocket?vsn=1.0.0&token=#{@token}&#{CLIENT_INFO_HEADER_KEY}=#{CLIENT_INFO_HEADER_VALUE}&#{MESSAGE_VERSION_HEADER_KEY}=#{MESSAGE_VERSION_HEADER_VALUE}"
|
449
|
+
when DELAYED_SIP then "wss://realtime-delayed-sip.intrinio.com/socket/websocket?vsn=1.0.0&token=#{@token}&#{CLIENT_INFO_HEADER_KEY}=#{CLIENT_INFO_HEADER_VALUE}&#{MESSAGE_VERSION_HEADER_KEY}=#{MESSAGE_VERSION_HEADER_VALUE}"
|
450
|
+
when NASDAQ_BASIC then "wss://realtime-nasdaq-basic.intrinio.com/socket/websocket?vsn=1.0.0&token=#{@token}&#{CLIENT_INFO_HEADER_KEY}=#{CLIENT_INFO_HEADER_VALUE}&#{MESSAGE_VERSION_HEADER_KEY}=#{MESSAGE_VERSION_HEADER_VALUE}"
|
451
|
+
when MANUAL then "ws://" + @ip_address + "/socket/websocket?vsn=1.0.0&token=#{@token}&#{CLIENT_INFO_HEADER_KEY}=#{CLIENT_INFO_HEADER_VALUE}&#{MESSAGE_VERSION_HEADER_KEY}=#{MESSAGE_VERSION_HEADER_VALUE}"
|
378
452
|
end
|
379
453
|
end
|
380
454
|
|
@@ -397,17 +471,21 @@ module Intrinio
|
|
397
471
|
@threads = []
|
398
472
|
@stop = false
|
399
473
|
@thread_quantity.times {@threads << Thread.new{handle_data}}
|
400
|
-
|
401
|
-
|
474
|
+
|
475
|
+
headers = {}
|
476
|
+
headers[:headers] = {}
|
477
|
+
headers[CLIENT_INFO_HEADER_KEY] = CLIENT_INFO_HEADER_VALUE
|
478
|
+
headers[MESSAGE_VERSION_HEADER_KEY] = MESSAGE_VERSION_HEADER_VALUE
|
479
|
+
@ws = ws = WebSocket::Client::Simple.connect(socket_url, headers)
|
480
|
+
|
402
481
|
me.send :info, "Connection opening"
|
403
482
|
|
404
483
|
ws.on :open do
|
405
484
|
me.send :info, "Connection established"
|
406
485
|
me.send :ready, true
|
407
|
-
if
|
486
|
+
if PROVIDERS.include?(me.send(:provider))
|
408
487
|
me.send :refresh_channels
|
409
488
|
end
|
410
|
-
me.send :start_heartbeat
|
411
489
|
me.send :stop_self_heal
|
412
490
|
end
|
413
491
|
|
@@ -465,24 +543,6 @@ module Intrinio
|
|
465
543
|
debug "Current channels: #{@channels}"
|
466
544
|
end
|
467
545
|
|
468
|
-
def start_heartbeat
|
469
|
-
EM.cancel_timer(@heartbeat_timer) if @heartbeat_timer
|
470
|
-
@heartbeat_timer = EM.add_periodic_timer(HEARTBEAT_TIME) do
|
471
|
-
if msg = heartbeat_msg()
|
472
|
-
@ws.send(msg)
|
473
|
-
debug "Heartbeat #{msg}"
|
474
|
-
end
|
475
|
-
end
|
476
|
-
end
|
477
|
-
|
478
|
-
def heartbeat_msg
|
479
|
-
""
|
480
|
-
end
|
481
|
-
|
482
|
-
def stop_heartbeat
|
483
|
-
EM.cancel_timer(@heartbeat_timer) if @heartbeat_timer
|
484
|
-
end
|
485
|
-
|
486
546
|
def try_self_heal
|
487
547
|
return if @closing
|
488
548
|
debug "Attempting to self-heal"
|
@@ -490,7 +550,6 @@ module Intrinio
|
|
490
550
|
time = @selfheal_backoffs.first
|
491
551
|
@selfheal_backoffs.delete_at(0) if @selfheal_backoffs.count > 1
|
492
552
|
|
493
|
-
EM.cancel_timer(@heartbeat_timer) if @heartbeat_timer
|
494
553
|
EM.cancel_timer(@selfheal_timer) if @selfheal_timer
|
495
554
|
|
496
555
|
@selfheal_timer = EM.add_timer(time/1000) do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: intrinio-realtime
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 5.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Intrinio
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-06-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: eventmachine
|
@@ -78,7 +78,7 @@ homepage: https://github.com/intrinio/intrinio-realtime-ruby-sdk
|
|
78
78
|
licenses:
|
79
79
|
- GPL-3.0
|
80
80
|
metadata: {}
|
81
|
-
post_install_message:
|
81
|
+
post_install_message:
|
82
82
|
rdoc_options: []
|
83
83
|
require_paths:
|
84
84
|
- lib
|
@@ -94,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
94
94
|
version: '0'
|
95
95
|
requirements: []
|
96
96
|
rubygems_version: 3.1.6
|
97
|
-
signing_key:
|
97
|
+
signing_key:
|
98
98
|
specification_version: 4
|
99
99
|
summary: Intrinio provides real-time stock prices from its Multi-Exchange feed, via
|
100
100
|
a two-way WebSocket connection.
|