intrinio-realtime 4.2.0 → 5.1.0
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/lib/intrinio-realtime.rb +115 -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: aa4f4dd35aeb0249722b61a5781edc9c7f621b0edfffb32b0ee7be771a3e3d2d
|
4
|
+
data.tar.gz: dcea71654614604c02f3194009386b09857637c6faa463aca3f6c507df625553
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5a4180e823e6bd20357105dc3e40ad2753d81a109b5c661c76ac36d989f9de1b266c64188f6796ba5e345d33985bb503aab98a0798e7af3e31093477b4dbd3f2
|
7
|
+
data.tar.gz: e35bacfcda00abbbf89f369f1a9c8d100f658b29955987d76fadc47ac150a02776b68de2f9b0628378a30b3cb916072ddebce45169516842f1b8f8d2619bfe24
|
data/lib/intrinio-realtime.rb
CHANGED
@@ -7,15 +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
|
16
21
|
PROVIDERS = [REALTIME, MANUAL, DELAYED_SIP, NASDAQ_BASIC].freeze
|
22
|
+
SUBPROVIDERS = [NO_SUBPROVIDER, CTA_A, CTA_B, UTP, OTC, NASDAQ_BASIC, IEX].freeze
|
17
23
|
ASK = "Ask".freeze
|
18
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
|
19
29
|
|
20
30
|
def self.connect(options, on_trade, on_quote)
|
21
31
|
EM.run do
|
@@ -25,12 +35,15 @@ module Intrinio
|
|
25
35
|
end
|
26
36
|
|
27
37
|
class Trade
|
28
|
-
def initialize(symbol, price, size, timestamp, total_volume)
|
38
|
+
def initialize(symbol, price, size, timestamp, total_volume, subprovider, market_center, condition)
|
29
39
|
@symbol = symbol
|
30
40
|
@price = price
|
31
41
|
@size = size
|
32
42
|
@timestamp = timestamp
|
33
43
|
@total_volume = total_volume
|
44
|
+
@subprovider = subprovider
|
45
|
+
@market_center = market_center
|
46
|
+
@condition = condition
|
34
47
|
end
|
35
48
|
|
36
49
|
def symbol
|
@@ -53,18 +66,37 @@ module Intrinio
|
|
53
66
|
@total_volume
|
54
67
|
end
|
55
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
|
+
|
81
|
+
def is_darkpool
|
82
|
+
@market_center == nil || @market_center == 'D' || @market_center == 'E' || @market_center == "\0" || @market_center.empty?
|
83
|
+
end
|
84
|
+
|
56
85
|
def to_s
|
57
|
-
[@symbol, @price, @size, @timestamp, @total_volume].join(",")
|
86
|
+
[@symbol, @price, @size, @timestamp, @total_volume, @subprovider, @market_center, @condition].join(",")
|
58
87
|
end
|
59
88
|
end
|
60
89
|
|
61
90
|
class Quote
|
62
|
-
def initialize(type, symbol, price, size, timestamp)
|
91
|
+
def initialize(type, symbol, price, size, timestamp, subprovider, market_center, condition)
|
63
92
|
@type = type
|
64
93
|
@symbol = symbol
|
65
94
|
@price = price
|
66
95
|
@size = size
|
67
96
|
@timestamp = timestamp
|
97
|
+
@subprovider = subprovider
|
98
|
+
@market_center = market_center
|
99
|
+
@condition = condition
|
68
100
|
end
|
69
101
|
|
70
102
|
def type
|
@@ -87,8 +119,20 @@ module Intrinio
|
|
87
119
|
@timestamp
|
88
120
|
end
|
89
121
|
|
122
|
+
def subprovider
|
123
|
+
@subprovider
|
124
|
+
end
|
125
|
+
|
126
|
+
def market_center
|
127
|
+
@market_center
|
128
|
+
end
|
129
|
+
|
130
|
+
def condition
|
131
|
+
@condition
|
132
|
+
end
|
133
|
+
|
90
134
|
def to_s
|
91
|
-
[@symbol, @type, @price, @size, @timestamp].join(",")
|
135
|
+
[@symbol, @type, @price, @size, @timestamp, @subprovider, @market_center, @condition].join(",")
|
92
136
|
end
|
93
137
|
end
|
94
138
|
|
@@ -115,7 +159,7 @@ module Intrinio
|
|
115
159
|
unless @provider
|
116
160
|
@provider = REALTIME
|
117
161
|
end
|
118
|
-
raise "Provider must be 'REALTIME' or 'MANUAL'" unless PROVIDERS.include?(@provider)
|
162
|
+
raise "Provider must be 'REALTIME', 'DELAYED_SIP', 'NASDAQ_BASIC', or 'MANUAL'" unless PROVIDERS.include?(@provider)
|
119
163
|
|
120
164
|
@ip_address = options[:ip_address]
|
121
165
|
raise "Missing option ip_address while in MANUAL mode." if @provider == MANUAL and (@ip_address.nil? || @ip_address.empty?)
|
@@ -148,7 +192,6 @@ module Intrinio
|
|
148
192
|
|
149
193
|
@ready = false
|
150
194
|
@joined_channels = []
|
151
|
-
@heartbeat_timer = nil
|
152
195
|
@selfheal_timer = nil
|
153
196
|
@selfheal_backoffs = Array.new(SELF_HEAL_BACKOFFS)
|
154
197
|
@ws = nil
|
@@ -206,7 +249,6 @@ module Intrinio
|
|
206
249
|
end
|
207
250
|
|
208
251
|
def disconnect
|
209
|
-
EM.cancel_timer(@heartbeat_timer) if @heartbeat_timer
|
210
252
|
EM.cancel_timer(@selfheal_timer) if @selfheal_timer
|
211
253
|
@ready = false
|
212
254
|
@closing = true
|
@@ -257,38 +299,71 @@ module Intrinio
|
|
257
299
|
data.map { |i| [sprintf('%02x',i)].pack('H2') }.join.unpack('e').first
|
258
300
|
end
|
259
301
|
|
260
|
-
def
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
302
|
+
def parse_subprovider(byte)
|
303
|
+
case byte
|
304
|
+
when 0
|
305
|
+
NO_SUBPROVIDER
|
306
|
+
when 1
|
307
|
+
CTA_A
|
308
|
+
when 2
|
309
|
+
CTA_B
|
310
|
+
when 3
|
311
|
+
UTP
|
312
|
+
when 4
|
313
|
+
OTC
|
314
|
+
when 5
|
315
|
+
NASDAQ_BASIC
|
316
|
+
when 6
|
317
|
+
IEX
|
318
|
+
else
|
319
|
+
IEX
|
320
|
+
end
|
267
321
|
end
|
268
322
|
|
269
|
-
def
|
323
|
+
def parse_trade(data, start_index)
|
324
|
+
symbol_length = data[start_index + 2]
|
325
|
+
condition_length = data[start_index + 26 + symbol_length]
|
326
|
+
symbol = data[start_index + 3, symbol_length].map!{|c| c.chr}.join
|
327
|
+
price = parse_float32(data[start_index + 6 + symbol_length, 4])
|
328
|
+
size = parse_uint32(data[start_index + 10 + symbol_length, 4])
|
329
|
+
timestamp = parse_uint64(data[start_index + 14 + symbol_length, 8])
|
330
|
+
total_volume = parse_uint32(data[start_index + 22 + symbol_length, 4])
|
331
|
+
subprovider = parse_subprovider(data[start_index + 3 + symbol_length])
|
332
|
+
market_center = data[start_index + 4 + symbol_length, 2].pack("C*").encode!('UTF-8', 'UTF-16LE')
|
333
|
+
condition = if condition_length > 0 then data[start_index + 27 + symbol_length, condition_length].map!{|c| c.chr}.join else "" end
|
334
|
+
|
335
|
+
return Trade.new(symbol, price, size, timestamp, total_volume, subprovider, market_center, condition)
|
336
|
+
end
|
337
|
+
|
338
|
+
def parse_quote(data, start_index, msg_type)
|
339
|
+
symbol_length = data[start_index + 2]
|
340
|
+
condition_length = data[start_index + 22 + symbol_length]
|
270
341
|
type = case when msg_type == 1 then ASK when msg_type == 2 then BID end
|
271
|
-
symbol = data[start_index +
|
272
|
-
price = parse_float32(data[start_index +
|
273
|
-
size = parse_uint32(data[start_index +
|
274
|
-
timestamp = parse_uint64(data[start_index +
|
275
|
-
|
342
|
+
symbol = data[start_index + 3, symbol_length].map!{|c| c.chr}.join
|
343
|
+
price = parse_float32(data[start_index + 6 + symbol_length, 4])
|
344
|
+
size = parse_uint32(data[start_index + 10 + symbol_length, 4])
|
345
|
+
timestamp = parse_uint64(data[start_index + 14 + symbol_length, 8])
|
346
|
+
subprovider = parse_subprovider(data[start_index + 3 + symbol_length])
|
347
|
+
market_center = data[start_index + 4 + symbol_length, 2].pack("C*").encode!('UTF-8', 'UTF-16LE')
|
348
|
+
condition = if condition_length > 0 then data[start_index + 23 + symbol_length, condition_length].map!{|c| c.chr}.join else "" end
|
349
|
+
|
350
|
+
return Quote.new(type, symbol, price, size, timestamp, subprovider, market_center, condition)
|
276
351
|
end
|
277
352
|
|
278
353
|
def handle_message(data, start_index)
|
279
354
|
msg_type = data[start_index]
|
280
|
-
|
355
|
+
msg_length = data[start_index + 1]
|
281
356
|
case msg_type
|
282
357
|
when 0 then
|
283
|
-
trade = parse_trade(data, start_index
|
358
|
+
trade = parse_trade(data, start_index)
|
284
359
|
@on_trade.call(trade)
|
285
|
-
return start_index +
|
360
|
+
return start_index + msg_length
|
286
361
|
when 1 || 2 then
|
287
|
-
quote = parse_quote(data, start_index,
|
362
|
+
quote = parse_quote(data, start_index, msg_type)
|
288
363
|
@on_quote.call(quote)
|
289
|
-
return start_index +
|
364
|
+
return start_index + msg_length
|
290
365
|
end
|
291
|
-
return start_index
|
366
|
+
return start_index + msg_length
|
292
367
|
end
|
293
368
|
|
294
369
|
def handle_data
|
@@ -332,7 +407,7 @@ module Intrinio
|
|
332
407
|
http.use_ssl = true if (auth_url.include?("https"))
|
333
408
|
http.start
|
334
409
|
request = Net::HTTP::Get.new(uri.request_uri)
|
335
|
-
request.add_field(
|
410
|
+
request.add_field(CLIENT_INFO_HEADER_KEY, CLIENT_INFO_HEADER_VALUE)
|
336
411
|
|
337
412
|
unless @api_key
|
338
413
|
request.basic_auth(@username, @password)
|
@@ -374,10 +449,10 @@ module Intrinio
|
|
374
449
|
|
375
450
|
def socket_url
|
376
451
|
case @provider
|
377
|
-
when REALTIME then "wss://realtime-mx.intrinio.com/socket/websocket?vsn=1.0.0&token=#{@token}"
|
378
|
-
when DELAYED_SIP then "wss://realtime-delayed-sip.intrinio.com/socket/websocket?vsn=1.0.0&token=#{@token}"
|
379
|
-
when NASDAQ_BASIC then "wss://realtime-nasdaq-basic.intrinio.com/socket/websocket?vsn=1.0.0&token=#{@token}"
|
380
|
-
|
452
|
+
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}"
|
453
|
+
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}"
|
454
|
+
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}"
|
455
|
+
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}"
|
381
456
|
end
|
382
457
|
end
|
383
458
|
|
@@ -400,17 +475,21 @@ module Intrinio
|
|
400
475
|
@threads = []
|
401
476
|
@stop = false
|
402
477
|
@thread_quantity.times {@threads << Thread.new{handle_data}}
|
403
|
-
|
404
|
-
|
478
|
+
|
479
|
+
headers = {}
|
480
|
+
headers[:headers] = {}
|
481
|
+
headers[CLIENT_INFO_HEADER_KEY] = CLIENT_INFO_HEADER_VALUE
|
482
|
+
headers[MESSAGE_VERSION_HEADER_KEY] = MESSAGE_VERSION_HEADER_VALUE
|
483
|
+
@ws = ws = WebSocket::Client::Simple.connect(socket_url, headers)
|
484
|
+
|
405
485
|
me.send :info, "Connection opening"
|
406
486
|
|
407
487
|
ws.on :open do
|
408
488
|
me.send :info, "Connection established"
|
409
489
|
me.send :ready, true
|
410
|
-
if
|
490
|
+
if PROVIDERS.include?(me.send(:provider))
|
411
491
|
me.send :refresh_channels
|
412
492
|
end
|
413
|
-
me.send :start_heartbeat
|
414
493
|
me.send :stop_self_heal
|
415
494
|
end
|
416
495
|
|
@@ -468,24 +547,6 @@ module Intrinio
|
|
468
547
|
debug "Current channels: #{@channels}"
|
469
548
|
end
|
470
549
|
|
471
|
-
def start_heartbeat
|
472
|
-
EM.cancel_timer(@heartbeat_timer) if @heartbeat_timer
|
473
|
-
@heartbeat_timer = EM.add_periodic_timer(HEARTBEAT_TIME) do
|
474
|
-
if msg = heartbeat_msg()
|
475
|
-
@ws.send(msg)
|
476
|
-
debug "Heartbeat #{msg}"
|
477
|
-
end
|
478
|
-
end
|
479
|
-
end
|
480
|
-
|
481
|
-
def heartbeat_msg
|
482
|
-
""
|
483
|
-
end
|
484
|
-
|
485
|
-
def stop_heartbeat
|
486
|
-
EM.cancel_timer(@heartbeat_timer) if @heartbeat_timer
|
487
|
-
end
|
488
|
-
|
489
550
|
def try_self_heal
|
490
551
|
return if @closing
|
491
552
|
debug "Attempting to self-heal"
|
@@ -493,7 +554,6 @@ module Intrinio
|
|
493
554
|
time = @selfheal_backoffs.first
|
494
555
|
@selfheal_backoffs.delete_at(0) if @selfheal_backoffs.count > 1
|
495
556
|
|
496
|
-
EM.cancel_timer(@heartbeat_timer) if @heartbeat_timer
|
497
557
|
EM.cancel_timer(@selfheal_timer) if @selfheal_timer
|
498
558
|
|
499
559
|
@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.1.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:
|
11
|
+
date: 2024-03-14 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.
|