intrinio-realtime 4.2.0 → 5.1.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 +115 -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: aa4f4dd35aeb0249722b61a5781edc9c7f621b0edfffb32b0ee7be771a3e3d2d
4
+ data.tar.gz: dcea71654614604c02f3194009386b09857637c6faa463aca3f6c507df625553
5
5
  SHA512:
6
- metadata.gz: 114ca0120fe8b58520217f933679519a79160014facb39b9a1668bc05682c9bd69aaab1cd4d8b53360152430b85cbb52057f724a83b78cbc3bd0ed85ea38655e
7
- data.tar.gz: 3d37a004b8603e5ed8ba2346f26520781098504fd265bb90ccd0cda7d638dec6114b9c114283950d1716170ce27952c7ce7e51b7b0aedf174cf5abf74fbcea3a
6
+ metadata.gz: 5a4180e823e6bd20357105dc3e40ad2753d81a109b5c661c76ac36d989f9de1b266c64188f6796ba5e345d33985bb503aab98a0798e7af3e31093477b4dbd3f2
7
+ data.tar.gz: e35bacfcda00abbbf89f369f1a9c8d100f658b29955987d76fadc47ac150a02776b68de2f9b0628378a30b3cb916072ddebce45169516842f1b8f8d2619bfe24
@@ -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 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)
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 parse_quote(data, start_index, symbol_length, msg_type)
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 + 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)
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
- symbol_length = data[start_index + 1]
355
+ msg_length = data[start_index + 1]
281
356
  case msg_type
282
357
  when 0 then
283
- trade = parse_trade(data, start_index, symbol_length)
358
+ trade = parse_trade(data, start_index)
284
359
  @on_trade.call(trade)
285
- return start_index + 22 + symbol_length
360
+ return start_index + msg_length
286
361
  when 1 || 2 then
287
- quote = parse_quote(data, start_index, symbol_length, msg_type)
362
+ quote = parse_quote(data, start_index, msg_type)
288
363
  @on_quote.call(quote)
289
- return start_index + 18 + symbol_length
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("Client-Information", "IntrinioRealtimeRubySDKv4.2")
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
- when MANUAL then "ws://" + @ip_address + "/socket/websocket?vsn=1.0.0&token=#{@token}"
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
- @ws = ws = WebSocket::Client::Simple.connect(socket_url)
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 [REALTIME, MANUAL].include?(me.send(:provider))
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.2.0
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: 2023-04-19 00:00:00.000000000 Z
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.