intrinio-realtime 1.0.0 → 2.2.1

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.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/intrinio-realtime.rb +188 -47
  3. metadata +6 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d5bfe9d4ae794df6de84bdc415554e2562da3369
4
- data.tar.gz: 43a51886bdf1fe2603b5af0420919945325a58d1
3
+ metadata.gz: c6566e800ad6cb471773564b491f07464cb6e12e
4
+ data.tar.gz: c19f2fc41783cff36f947d9116160464585f45f3
5
5
  SHA512:
6
- metadata.gz: 3d356ae56916d72fb0cc0243e58354ec925b08e598fd97aa121aaeb80fb07fdb1be61cc60e78e95a1c1c120635acaf70db0ea5d1ba6a46c546051d034d62950f
7
- data.tar.gz: 2a210412771750e5ed2ac3cfc17fd210c498dd2a88537f15981ded4c3c7b4e73ad8d4e1d2740289ec6dda1aeb3a6a988f22f518f33c3f26bc868c3257452583d
6
+ metadata.gz: 9fb548a774267738be2077bba70ce7dc26a6eb2c97a5f08373fec257f4114aec7897e74e539e9cdcd6c496fd935eadbe2d5c764ef013f1cc4a4df420eff1ee05
7
+ data.tar.gz: 6c27f127033719ab64f7ab681089fc540e8d1ed053307b7e217d702c248da806f750f9593c09c40900e524216678657fd72f35f6e460b69c6062deca8724a518
@@ -6,12 +6,13 @@ require 'websocket-client-simple'
6
6
 
7
7
  module Intrinio
8
8
  module Realtime
9
- AUTH_URL = "https://realtime.intrinio.com/auth"
10
- SOCKET_URL = "wss://realtime.intrinio.com/socket/websocket"
11
- HEARTBEAT_TIME = 1
12
- HEARTBEAT_MSG = {topic: 'phoenix', event: 'heartbeat', payload: {}, ref: nil}.to_json
13
- SELF_HEAL_BACKOFFS = [0,100,500,1000,2000,5000]
14
- DEFAULT_POOL_SIZE = 100
9
+ HEARTBEAT_TIME = 3
10
+ SELF_HEAL_BACKOFFS = [0, 100, 500, 1000, 2000, 5000].freeze
11
+ IEX = "iex".freeze
12
+ QUODD = "quodd".freeze
13
+ CRYPTOQUOTE = "cryptoquote".freeze
14
+ FXCM = "fxcm".freeze
15
+ PROVIDERS = [IEX, QUODD, CRYPTOQUOTE, FXCM].freeze
15
16
 
16
17
  def self.connect(options, &b)
17
18
  EM.run do
@@ -22,13 +23,22 @@ module Intrinio
22
23
  end
23
24
 
24
25
  class Client
26
+
25
27
  def initialize(options)
26
28
  raise "Options parameter is required" if options.nil? || !options.is_a?(Hash)
27
-
28
- @username = options[:username]
29
- @password = options[:password]
30
- raise "Username and password are required" if @username.nil? || @username.empty? || @password.nil? || @password.empty?
31
-
29
+
30
+ @api_key = options[:api_key]
31
+ raise "API Key was formatted invalidly." if @api_key && !valid_api_key?(@api_key)
32
+
33
+ unless @api_key
34
+ @username = options[:username]
35
+ @password = options[:password]
36
+ raise "API Key or Username and password are required" if @username.nil? || @username.empty? || @password.nil? || @password.empty?
37
+ end
38
+
39
+ @provider = options[:provider]
40
+ raise "Provider must be 'CRYPTOQUOTE', 'FXCM', 'IEX', or 'QUODD'" unless PROVIDERS.include?(@provider)
41
+
32
42
  @channels = []
33
43
  @channels = parse_channels(options[:channels]) if options[:channels]
34
44
  bad_channels = @channels.select{|x| !x.is_a?(String)}
@@ -42,7 +52,7 @@ module Intrinio
42
52
  @logger = Logger.new($stdout)
43
53
  @logger.level = Logger::INFO
44
54
  end
45
-
55
+
46
56
  @quotes = EventMachine::Channel.new
47
57
  @ready = false
48
58
  @joined_channels = []
@@ -51,6 +61,10 @@ module Intrinio
51
61
  @selfheal_backoffs = Array.new(SELF_HEAL_BACKOFFS)
52
62
  @ws = nil
53
63
  end
64
+
65
+ def provider
66
+ @provider
67
+ end
54
68
 
55
69
  def join(*channels)
56
70
  channels = parse_channels(channels)
@@ -97,7 +111,7 @@ module Intrinio
97
111
  refresh_token()
98
112
  refresh_websocket()
99
113
  rescue StandardError => e
100
- error("Connection error: #{e}")
114
+ error("Connection error: #{e} \n#{e.backtrace.join("\n")}")
101
115
  try_self_heal()
102
116
  end
103
117
  end
@@ -118,33 +132,71 @@ module Intrinio
118
132
 
119
133
  def refresh_token
120
134
  @token = nil
121
-
122
- response = HTTP.basic_auth(:user => @username, :pass => @password).get(AUTH_URL)
135
+
136
+ if @api_key
137
+ response = HTTP.get(auth_url)
138
+ else
139
+ response = HTTP.basic_auth(:user => @username, :pass => @password).get(auth_url)
140
+ end
141
+
123
142
  return fatal("Unable to authorize") if response.status == 401
124
143
  return fatal("Could not get auth token") if response.status != 200
125
-
144
+
126
145
  @token = response.body
127
146
  debug "Token refreshed"
128
147
  end
129
148
 
149
+ def auth_url
150
+ url = ""
151
+
152
+ case @provider
153
+ when IEX then url = "https://realtime.intrinio.com/auth"
154
+ when QUODD then url = "https://api.intrinio.com/token?type=QUODD"
155
+ when CRYPTOQUOTE then url = "https://crypto.intrinio.com/auth"
156
+ when FXCM then url = "https://fxcm.intrinio.com/auth"
157
+ end
158
+
159
+ url = api_auth_url(url) if @api_key
160
+
161
+ url
162
+ end
163
+
164
+ def api_auth_url(url)
165
+ if url.include? "?"
166
+ url = "#{url}&"
167
+ else
168
+ url = "#{url}?"
169
+ end
170
+
171
+ "#{url}api_key=#{@api_key}"
172
+ end
173
+
130
174
  def socket_url
131
- URI.escape(SOCKET_URL + "?vsn=1.0.0&token=#{@token}")
175
+ case @provider
176
+ when IEX then URI.escape("wss://realtime.intrinio.com/socket/websocket?vsn=1.0.0&token=#{@token}")
177
+ when QUODD then URI.escape("wss://www5.quodd.com/websocket/webStreamer/intrinio/#{@token}")
178
+ when CRYPTOQUOTE then URI.escape("wss://crypto.intrinio.com/socket/websocket?vsn=1.0.0&token=#{@token}")
179
+ when FXCM then URI.escape("wss://fxcm.intrinio.com/socket/websocket?vsn=1.0.0&token=#{@token}")
180
+ end
132
181
  end
133
-
182
+
134
183
  def refresh_websocket
135
184
  me = self
136
-
185
+
137
186
  @ws.close() unless @ws.nil?
138
187
  @ready = false
139
188
  @joined_channels = []
140
189
 
141
190
  @ws = ws = WebSocket::Client::Simple.connect(socket_url)
191
+ me.send :info, "Connection opening"
142
192
 
143
193
  ws.on :open do
144
- me.send :ready, true
145
194
  me.send :info, "Connection established"
195
+ me.send :ready, true
196
+ if [IEX, CRYPTOQUOTE, FXCM].include?(me.send(:provider))
197
+ me.send :refresh_channels
198
+ end
146
199
  me.send :start_heartbeat
147
- me.send :refresh_channels
148
200
  me.send :stop_self_heal
149
201
  end
150
202
 
@@ -154,14 +206,44 @@ module Intrinio
154
206
 
155
207
  begin
156
208
  json = JSON.parse(message)
157
- if json["event"] == "quote"
158
- quote = json["payload"]
209
+
210
+ if json["event"] == "phx_reply" && json["payload"]["status"] == "error"
211
+ me.send :error, json["payload"]["response"]
212
+ end
213
+
214
+ quote =
215
+ case me.send(:provider)
216
+ when IEX
217
+ if json["event"] == "quote"
218
+ json["payload"]
219
+ end
220
+ when QUODD
221
+ if json["event"] == "info" && json["data"]["message"] == "Connected"
222
+ me.send :refresh_channels
223
+ elsif json["event"] == "quote" || json["event"] == "trade"
224
+ json["data"]
225
+ end
226
+ when CRYPTOQUOTE
227
+ if json["event"] == "book_update" || json["event"] == "ticker" || json["event"] == "trade"
228
+ json["payload"]
229
+ end
230
+ when FXCM
231
+ if json["event"] == "price_update"
232
+ json["payload"]
233
+ end
234
+ end
235
+
236
+ if quote && quote.is_a?(Hash)
159
237
  me.send :process_quote, quote
160
238
  end
161
239
  rescue StandardError => e
162
240
  me.send :error, "Could not parse message: #{message} #{e}"
163
241
  end
164
242
  end
243
+
244
+ ws.on :close do |e|
245
+ me.send :disconnect
246
+ end
165
247
 
166
248
  ws.on :error do |e|
167
249
  me.send :ready, false
@@ -177,28 +259,16 @@ module Intrinio
177
259
  # Join new channels
178
260
  new_channels = @channels - @joined_channels
179
261
  new_channels.each do |channel|
180
- msg = {
181
- topic: parse_topic(channel),
182
- event: "phx_join",
183
- payload: {},
184
- ref: nil
185
- }.to_json
186
-
187
- @ws.send(msg)
262
+ msg = join_message(channel)
263
+ @ws.send(msg.to_json)
188
264
  info "Joined #{channel}"
189
265
  end
190
266
 
191
267
  # Leave old channels
192
268
  old_channels = @joined_channels - @channels
193
269
  old_channels.each do |channel|
194
- msg = {
195
- topic: parse_topic(channel),
196
- event: 'phx_leave',
197
- payload: {},
198
- ref: nil
199
- }.to_json
200
-
201
- @ws.send(msg)
270
+ msg = leave_message(channel)
271
+ @ws.send(msg.to_json)
202
272
  info "Left #{channel}"
203
273
  end
204
274
 
@@ -210,8 +280,18 @@ module Intrinio
210
280
  def start_heartbeat
211
281
  EM.cancel_timer(@heartbeat_timer) if @heartbeat_timer
212
282
  @heartbeat_timer = EM.add_periodic_timer(HEARTBEAT_TIME) do
213
- debug "Heartbeat"
214
- @ws.send(HEARTBEAT_MSG)
283
+ if msg = heartbeat_msg()
284
+ @ws.send(msg)
285
+ debug "Heartbeat #{msg}"
286
+ end
287
+ end
288
+ end
289
+
290
+ def heartbeat_msg
291
+ case @provider
292
+ when IEX then {topic: 'phoenix', event: 'heartbeat', payload: {}, ref: nil}.to_json
293
+ when QUODD then {event: 'heartbeat', data: {action: 'heartbeat', ticker: (Time.now.to_f * 1000).to_i}}.to_json
294
+ when CRYPTOQUOTE, FXCM then {topic: 'phoenix', event: 'heartbeat', payload: {}, ref: nil}.to_json
215
295
  end
216
296
  end
217
297
 
@@ -273,7 +353,14 @@ module Intrinio
273
353
  nil
274
354
  end
275
355
 
276
- def parse_topic(channel)
356
+ def parse_channels(channels)
357
+ channels.flatten!
358
+ channels.uniq!
359
+ channels.compact!
360
+ channels
361
+ end
362
+
363
+ def parse_iex_topic(channel)
277
364
  case channel
278
365
  when "$lobby"
279
366
  "iex:lobby"
@@ -284,12 +371,66 @@ module Intrinio
284
371
  end
285
372
  end
286
373
 
287
- def parse_channels(channels)
288
- channels.flatten!
289
- channels.uniq!
290
- channels.compact!
291
- channels
374
+ def join_message(channel)
375
+ case @provider
376
+ when IEX
377
+ {
378
+ topic: parse_iex_topic(channel),
379
+ event: "phx_join",
380
+ payload: {},
381
+ ref: nil
382
+ }
383
+ when QUODD
384
+ {
385
+ event: "subscribe",
386
+ data: {
387
+ ticker: channel,
388
+ action: "subscribe"
389
+ }
390
+ }
391
+ when CRYPTOQUOTE, FXCM
392
+ {
393
+ topic: channel,
394
+ event: "phx_join",
395
+ payload: {},
396
+ ref: nil
397
+ }
398
+ end
292
399
  end
400
+
401
+ def leave_message(channel)
402
+ case @provider
403
+ when IEX
404
+ {
405
+ topic: parse_iex_topic(channel),
406
+ event: "phx_leave",
407
+ payload: {},
408
+ ref: nil
409
+ }
410
+ when QUODD
411
+ {
412
+ event: "unsubscribe",
413
+ data: {
414
+ ticker: channel,
415
+ action: "unsubscribe"
416
+ }
417
+ }
418
+ when CRYPTOQUOTE, FXCM
419
+ {
420
+ topic: channel,
421
+ event: "phx_leave",
422
+ payload: {},
423
+ ref: nil
424
+ }
425
+ end
426
+ end
427
+
428
+ def valid_api_key?(api_key)
429
+ return false unless api_key.is_a?(String)
430
+ return false if api_key.empty?
431
+ true
432
+ end
433
+
293
434
  end
294
435
  end
295
436
  end
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: 1.0.0
4
+ version: 2.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Intrinio
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-05-11 00:00:00.000000000 Z
11
+ date: 2020-05-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http
@@ -52,7 +52,7 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0.3'
55
- description: Intrinio Ruby SDK for Real-Time Stock Prices
55
+ description: Intrinio Ruby SDK for Real-Time Stock & Crypto Prices
56
56
  email:
57
57
  - asolo@intrinio.com
58
58
  executables: []
@@ -80,9 +80,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
80
80
  version: '0'
81
81
  requirements: []
82
82
  rubyforge_project:
83
- rubygems_version: 2.5.1
83
+ rubygems_version: 2.5.2.2
84
84
  signing_key:
85
85
  specification_version: 4
86
- summary: Intrinio provides real-time stock prices from the IEX stock exchange, via
87
- a two-way WebSocket connection.
86
+ summary: Intrinio provides real-time stock & crypto prices from the IEX stock exchange,
87
+ via a two-way WebSocket connection.
88
88
  test_files: []