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