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.
- checksums.yaml +4 -4
- data/lib/intrinio-realtime.rb +188 -47
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c6566e800ad6cb471773564b491f07464cb6e12e
|
4
|
+
data.tar.gz: c19f2fc41783cff36f947d9116160464585f45f3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9fb548a774267738be2077bba70ce7dc26a6eb2c97a5f08373fec257f4114aec7897e74e539e9cdcd6c496fd935eadbe2d5c764ef013f1cc4a4df420eff1ee05
|
7
|
+
data.tar.gz: 6c27f127033719ab64f7ab681089fc540e8d1ed053307b7e217d702c248da806f750f9593c09c40900e524216678657fd72f35f6e460b69c6062deca8724a518
|
data/lib/intrinio-realtime.rb
CHANGED
@@ -6,12 +6,13 @@ require 'websocket-client-simple'
|
|
6
6
|
|
7
7
|
module Intrinio
|
8
8
|
module Realtime
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
@
|
29
|
-
@
|
30
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
158
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
214
|
-
|
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
|
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
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
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:
|
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:
|
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.
|
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,
|
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: []
|