intrinio-realtime 1.0.0 → 2.2.1

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 +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: []