intrinio-realtime 4.2.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/intrinio-realtime.rb +111 -55
  3. metadata +5 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5999d98e1df1a97e8d4a6f217a645b2a4cc83173df769f831b92a0e2fdaa03f0
4
- data.tar.gz: 5c49201fd0ea6140c1c8e538a6d9ac1b9ddcb53ba980493c3e1bffd0b3739866
3
+ metadata.gz: c3beedb23f93b49b061ba476daa8520cb3f09cfde888eaddbd08fec263a39de3
4
+ data.tar.gz: e81ce551b2ce00694e0d7ef238b6b63d60b5deb365e20dc3583c8b7819a9eb7a
5
5
  SHA512:
6
- metadata.gz: 114ca0120fe8b58520217f933679519a79160014facb39b9a1668bc05682c9bd69aaab1cd4d8b53360152430b85cbb52057f724a83b78cbc3bd0ed85ea38655e
7
- data.tar.gz: 3d37a004b8603e5ed8ba2346f26520781098504fd265bb90ccd0cda7d638dec6114b9c114283950d1716170ce27952c7ce7e51b7b0aedf174cf5abf74fbcea3a
6
+ metadata.gz: c09c353f980bcd5bd4cd36687bcbe0cd87661b5f3b428c98299374f69d19e01c6408d107842e2b46c84d26f2dc429bd6278e04abc5b2890de444e33590361467
7
+ data.tar.gz: f7d2c9141ee70a4cff900669152d1e2cda68ffba54d5360c4f013e88866bea9a829a49e79f3ce0f82444bf2b3c49fb592c8285edf2da0bb3be88516345da1927
@@ -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,33 @@ 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
+
56
81
  def to_s
57
- [@symbol, @price, @size, @timestamp, @total_volume].join(",")
82
+ [@symbol, @price, @size, @timestamp, @total_volume, @subprovider, @market_center, @condition].join(",")
58
83
  end
59
84
  end
60
85
 
61
86
  class Quote
62
- def initialize(type, symbol, price, size, timestamp)
87
+ def initialize(type, symbol, price, size, timestamp, subprovider, market_center, condition)
63
88
  @type = type
64
89
  @symbol = symbol
65
90
  @price = price
66
91
  @size = size
67
92
  @timestamp = timestamp
93
+ @subprovider = subprovider
94
+ @market_center = market_center
95
+ @condition = condition
68
96
  end
69
97
 
70
98
  def type
@@ -87,8 +115,20 @@ module Intrinio
87
115
  @timestamp
88
116
  end
89
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
+
90
130
  def to_s
91
- [@symbol, @type, @price, @size, @timestamp].join(",")
131
+ [@symbol, @type, @price, @size, @timestamp, @subprovider, @market_center, @condition].join(",")
92
132
  end
93
133
  end
94
134
 
@@ -115,7 +155,7 @@ module Intrinio
115
155
  unless @provider
116
156
  @provider = REALTIME
117
157
  end
118
- 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)
119
159
 
120
160
  @ip_address = options[:ip_address]
121
161
  raise "Missing option ip_address while in MANUAL mode." if @provider == MANUAL and (@ip_address.nil? || @ip_address.empty?)
@@ -148,7 +188,6 @@ module Intrinio
148
188
 
149
189
  @ready = false
150
190
  @joined_channels = []
151
- @heartbeat_timer = nil
152
191
  @selfheal_timer = nil
153
192
  @selfheal_backoffs = Array.new(SELF_HEAL_BACKOFFS)
154
193
  @ws = nil
@@ -206,7 +245,6 @@ module Intrinio
206
245
  end
207
246
 
208
247
  def disconnect
209
- EM.cancel_timer(@heartbeat_timer) if @heartbeat_timer
210
248
  EM.cancel_timer(@selfheal_timer) if @selfheal_timer
211
249
  @ready = false
212
250
  @closing = true
@@ -257,38 +295,71 @@ module Intrinio
257
295
  data.map { |i| [sprintf('%02x',i)].pack('H2') }.join.unpack('e').first
258
296
  end
259
297
 
260
- def parse_trade(data, start_index, symbol_length)
261
- symbol = data[start_index + 2, symbol_length].map!{|c| c.chr}.join
262
- price = parse_float32(data[start_index + 2 + symbol_length, 4])
263
- size = parse_uint32(data[start_index + 6 + symbol_length, 4])
264
- timestamp = parse_uint64(data[start_index + 10 + symbol_length, 8])
265
- total_volume = parse_uint32(data[start_index + 18 + symbol_length, 4])
266
- return Trade.new(symbol, price, size, timestamp, total_volume)
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
267
317
  end
268
318
 
269
- def parse_quote(data, start_index, symbol_length, msg_type)
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]
270
337
  type = case when msg_type == 1 then ASK when msg_type == 2 then BID end
271
- symbol = data[start_index + 2, symbol_length].map!{|c| c.chr}.join
272
- price = parse_float32(data[start_index + 2 + symbol_length, 4])
273
- size = parse_uint32(data[start_index + 6 + symbol_length, 4])
274
- timestamp = parse_uint64(data[start_index + 10 + symbol_length, 8])
275
- return Quote.new(type, symbol, price, size, timestamp)
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)
276
347
  end
277
348
 
278
349
  def handle_message(data, start_index)
279
350
  msg_type = data[start_index]
280
- symbol_length = data[start_index + 1]
351
+ msg_length = data[start_index + 1]
281
352
  case msg_type
282
353
  when 0 then
283
- trade = parse_trade(data, start_index, symbol_length)
354
+ trade = parse_trade(data, start_index)
284
355
  @on_trade.call(trade)
285
- return start_index + 22 + symbol_length
356
+ return start_index + msg_length
286
357
  when 1 || 2 then
287
- quote = parse_quote(data, start_index, symbol_length, msg_type)
358
+ quote = parse_quote(data, start_index, msg_type)
288
359
  @on_quote.call(quote)
289
- return start_index + 18 + symbol_length
360
+ return start_index + msg_length
290
361
  end
291
- return start_index
362
+ return start_index + msg_length
292
363
  end
293
364
 
294
365
  def handle_data
@@ -332,7 +403,7 @@ module Intrinio
332
403
  http.use_ssl = true if (auth_url.include?("https"))
333
404
  http.start
334
405
  request = Net::HTTP::Get.new(uri.request_uri)
335
- request.add_field("Client-Information", "IntrinioRealtimeRubySDKv4.2")
406
+ request.add_field(CLIENT_INFO_HEADER_KEY, CLIENT_INFO_HEADER_VALUE)
336
407
 
337
408
  unless @api_key
338
409
  request.basic_auth(@username, @password)
@@ -374,10 +445,10 @@ module Intrinio
374
445
 
375
446
  def socket_url
376
447
  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
- when MANUAL then "ws://" + @ip_address + "/socket/websocket?vsn=1.0.0&token=#{@token}"
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}"
381
452
  end
382
453
  end
383
454
 
@@ -400,17 +471,21 @@ module Intrinio
400
471
  @threads = []
401
472
  @stop = false
402
473
  @thread_quantity.times {@threads << Thread.new{handle_data}}
403
-
404
- @ws = ws = WebSocket::Client::Simple.connect(socket_url)
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
+
405
481
  me.send :info, "Connection opening"
406
482
 
407
483
  ws.on :open do
408
484
  me.send :info, "Connection established"
409
485
  me.send :ready, true
410
- if [REALTIME, MANUAL].include?(me.send(:provider))
486
+ if PROVIDERS.include?(me.send(:provider))
411
487
  me.send :refresh_channels
412
488
  end
413
- me.send :start_heartbeat
414
489
  me.send :stop_self_heal
415
490
  end
416
491
 
@@ -468,24 +543,6 @@ module Intrinio
468
543
  debug "Current channels: #{@channels}"
469
544
  end
470
545
 
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
546
  def try_self_heal
490
547
  return if @closing
491
548
  debug "Attempting to self-heal"
@@ -493,7 +550,6 @@ module Intrinio
493
550
  time = @selfheal_backoffs.first
494
551
  @selfheal_backoffs.delete_at(0) if @selfheal_backoffs.count > 1
495
552
 
496
- EM.cancel_timer(@heartbeat_timer) if @heartbeat_timer
497
553
  EM.cancel_timer(@selfheal_timer) if @selfheal_timer
498
554
 
499
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.2.0
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-04-19 00:00:00.000000000 Z
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.