intrinio-realtime 2.1.0 → 3.1.0
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 +5 -5
- data/lib/intrinio-realtime.rb +566 -424
- metadata +28 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 5133d9c56da7e6d9e8c89dbdda4733dbe3240d996617fb6e1abadeeaabbd1451
|
4
|
+
data.tar.gz: e2e2ef77e3efd159eb3b321f78b93bfd6e84ec8bb59e7058fdc9eb3812375f81
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cd7bca2cb01f65c65414bb58ee45de03456b9c031cd617431410f206a98e9baed436fc968964a569e55007c98dc415cbd490df412679c3766ce1745b11b431d5
|
7
|
+
data.tar.gz: 4a6e3c53d9da10740e75b73aaf482fd9258abcd6fe04c3c983f73dc5ec69150383516fd9a12aeab0abd37a3a3209afe4645f49d57018e17aa57d94c419063bf0
|
data/lib/intrinio-realtime.rb
CHANGED
@@ -1,424 +1,566 @@
|
|
1
|
-
require 'logger'
|
2
|
-
require 'uri'
|
3
|
-
require 'http'
|
4
|
-
require '
|
5
|
-
require '
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
PROVIDERS = [
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
client.
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
def initialize(
|
27
|
-
|
28
|
-
|
29
|
-
@
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
@
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
@
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
@
|
66
|
-
end
|
67
|
-
|
68
|
-
def
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
@
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
end
|
79
|
-
|
80
|
-
def
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
@
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
@
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
@
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
@
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
@
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
when
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
"
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
1
|
+
require 'logger'
|
2
|
+
require 'uri'
|
3
|
+
#require 'http'
|
4
|
+
require 'net/http'
|
5
|
+
require 'eventmachine'
|
6
|
+
require 'websocket-client-simple'
|
7
|
+
|
8
|
+
module Intrinio
|
9
|
+
module Realtime
|
10
|
+
HEARTBEAT_TIME = 3
|
11
|
+
SELF_HEAL_BACKOFFS = [0, 100, 500, 1000, 2000, 5000].freeze
|
12
|
+
REALTIME = "REALTIME".freeze
|
13
|
+
MANUAL = "MANUAL".freeze
|
14
|
+
PROVIDERS = [REALTIME, MANUAL].freeze
|
15
|
+
ASK = "Ask".freeze
|
16
|
+
BID = "Bid".freeze
|
17
|
+
|
18
|
+
def self.connect(options, on_trade, on_quote)
|
19
|
+
EM.run do
|
20
|
+
client = ::Intrinio::Realtime::Client.new(options, on_trade, on_quote)
|
21
|
+
client.connect()
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Trade
|
26
|
+
def initialize(symbol, price, size, timestamp, total_volume)
|
27
|
+
@symbol = symbol
|
28
|
+
@price = price
|
29
|
+
@size = size
|
30
|
+
@timestamp = timestamp
|
31
|
+
@total_volume = total_volume
|
32
|
+
end
|
33
|
+
|
34
|
+
def symbol
|
35
|
+
@symbol
|
36
|
+
end
|
37
|
+
|
38
|
+
def price
|
39
|
+
@price
|
40
|
+
end
|
41
|
+
|
42
|
+
def size
|
43
|
+
@size
|
44
|
+
end
|
45
|
+
|
46
|
+
def timestamp
|
47
|
+
@timestamp
|
48
|
+
end
|
49
|
+
|
50
|
+
def total_volume
|
51
|
+
@total_volume
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_s
|
55
|
+
[@symbol, @price, @size, @timestamp, @total_volume].join(",")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Quote
|
60
|
+
def initialize(type, symbol, price, size, timestamp)
|
61
|
+
@type = type
|
62
|
+
@symbol = symbol
|
63
|
+
@price = price
|
64
|
+
@size = size
|
65
|
+
@timestamp = timestamp
|
66
|
+
end
|
67
|
+
|
68
|
+
def type
|
69
|
+
@type
|
70
|
+
end
|
71
|
+
|
72
|
+
def symbol
|
73
|
+
@symbol
|
74
|
+
end
|
75
|
+
|
76
|
+
def price
|
77
|
+
@price
|
78
|
+
end
|
79
|
+
|
80
|
+
def size
|
81
|
+
@size
|
82
|
+
end
|
83
|
+
|
84
|
+
def timestamp
|
85
|
+
@timestamp
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_s
|
89
|
+
[@symbol, @type, @price, @size, @timestamp].join(",")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class Client
|
94
|
+
|
95
|
+
def initialize(options, on_trade, on_quote)
|
96
|
+
raise "Options parameter is required" if options.nil? || !options.is_a?(Hash)
|
97
|
+
@stop = false
|
98
|
+
@messages = Queue.new
|
99
|
+
raise "Unable to create queue." if @messages.nil?
|
100
|
+
@on_trade = on_trade
|
101
|
+
@on_quote = on_quote
|
102
|
+
|
103
|
+
@api_key = options[:api_key]
|
104
|
+
raise "API Key was formatted invalidly." if @api_key && !valid_api_key?(@api_key)
|
105
|
+
|
106
|
+
unless @api_key
|
107
|
+
@username = options[:username]
|
108
|
+
@password = options[:password]
|
109
|
+
raise "API Key or Username and password are required" if @username.nil? || @username.empty? || @password.nil? || @password.empty?
|
110
|
+
end
|
111
|
+
|
112
|
+
@provider = options[:provider]
|
113
|
+
unless @provider
|
114
|
+
@provider = REALTIME
|
115
|
+
end
|
116
|
+
raise "Provider must be 'REALTIME' or 'MANUAL'" unless PROVIDERS.include?(@provider)
|
117
|
+
|
118
|
+
@ip_address = options[:ip_address]
|
119
|
+
raise "Missing option ip_address while in MANUAL mode." if @provider == MANUAL and (@ip_address.nil? || @ip_address.empty?)
|
120
|
+
|
121
|
+
@trades_only = options[:trades_only]
|
122
|
+
if @trades_only.nil?
|
123
|
+
@trades_only = false
|
124
|
+
end
|
125
|
+
|
126
|
+
@thread_quantity = options[:threads]
|
127
|
+
unless @thread_quantity
|
128
|
+
@thread_quantity = 4
|
129
|
+
end
|
130
|
+
|
131
|
+
@threads = []
|
132
|
+
|
133
|
+
@channels = []
|
134
|
+
@channels = parse_channels(options[:channels]) if options[:channels]
|
135
|
+
bad_channels = @channels.select{|x| !x.is_a?(String)}
|
136
|
+
raise "Invalid channels to join: #{bad_channels}" unless bad_channels.empty?
|
137
|
+
|
138
|
+
if options[:logger] == false
|
139
|
+
@logger = nil
|
140
|
+
elsif !options[:logger].nil?
|
141
|
+
@logger = options[:logger]
|
142
|
+
else
|
143
|
+
@logger = Logger.new($stdout)
|
144
|
+
@logger.level = Logger::INFO
|
145
|
+
end
|
146
|
+
|
147
|
+
@ready = false
|
148
|
+
@joined_channels = []
|
149
|
+
@heartbeat_timer = nil
|
150
|
+
@selfheal_timer = nil
|
151
|
+
@selfheal_backoffs = Array.new(SELF_HEAL_BACKOFFS)
|
152
|
+
@ws = nil
|
153
|
+
end
|
154
|
+
|
155
|
+
def provider
|
156
|
+
@provider
|
157
|
+
end
|
158
|
+
|
159
|
+
def join(*channels)
|
160
|
+
channels = parse_channels(channels)
|
161
|
+
nonconforming = channels.select{|x| !x.is_a?(String)}
|
162
|
+
return error("Invalid channels to join: #{nonconforming}") unless nonconforming.empty?
|
163
|
+
|
164
|
+
@channels.concat(channels)
|
165
|
+
@channels.uniq!
|
166
|
+
debug "Joining channels #{channels}"
|
167
|
+
|
168
|
+
refresh_channels()
|
169
|
+
end
|
170
|
+
|
171
|
+
def leave(*channels)
|
172
|
+
channels = parse_channels(channels)
|
173
|
+
nonconforming = channels.find{|x| !x.is_a?(String)}
|
174
|
+
return error("Invalid channels to leave: #{nonconforming}") unless nonconforming.empty?
|
175
|
+
|
176
|
+
channels.each{|c| @channels.delete(c)}
|
177
|
+
debug "Leaving channels #{channels}"
|
178
|
+
|
179
|
+
refresh_channels()
|
180
|
+
end
|
181
|
+
|
182
|
+
def leave_all
|
183
|
+
@channels = []
|
184
|
+
debug "Leaving all channels"
|
185
|
+
refresh_channels()
|
186
|
+
end
|
187
|
+
|
188
|
+
def connect
|
189
|
+
raise "Must be run from within an EventMachine run loop" unless EM.reactor_running?
|
190
|
+
return warn("Already connected!") if @ready
|
191
|
+
debug "Connecting..."
|
192
|
+
|
193
|
+
catch :fatal do
|
194
|
+
begin
|
195
|
+
@closing = false
|
196
|
+
@ready = false
|
197
|
+
refresh_token()
|
198
|
+
refresh_websocket()
|
199
|
+
rescue StandardError => e
|
200
|
+
error("Connection error: #{e} \n#{e.backtrace.join("\n")}")
|
201
|
+
try_self_heal()
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def disconnect
|
207
|
+
EM.cancel_timer(@heartbeat_timer) if @heartbeat_timer
|
208
|
+
EM.cancel_timer(@selfheal_timer) if @selfheal_timer
|
209
|
+
@ready = false
|
210
|
+
@closing = true
|
211
|
+
@channels = []
|
212
|
+
@joined_channels = []
|
213
|
+
@ws.close() if @ws
|
214
|
+
@stop = true
|
215
|
+
sleep(2)
|
216
|
+
@threads.each { |thread|
|
217
|
+
if !thread.nil? && (!thread.pending_interrupt? || thread.status == "run" || thread.status == "Sleeping")
|
218
|
+
then thread.join(7)
|
219
|
+
elsif !thread.nil?
|
220
|
+
then thread.kill
|
221
|
+
end
|
222
|
+
}
|
223
|
+
@threads = []
|
224
|
+
@stop = false
|
225
|
+
info "Connection closed"
|
226
|
+
end
|
227
|
+
|
228
|
+
def on_trade(on_trade)
|
229
|
+
@on_trade = on_trade
|
230
|
+
end
|
231
|
+
|
232
|
+
def on_quote(on_quote)
|
233
|
+
@on_quote = on_quote
|
234
|
+
end
|
235
|
+
|
236
|
+
private
|
237
|
+
|
238
|
+
def queue_message(message)
|
239
|
+
@messages.enq(message)
|
240
|
+
end
|
241
|
+
|
242
|
+
def parse_uint64(data)
|
243
|
+
data.map { |i| [sprintf('%02x',i)].pack('H2') }.join.unpack('Q<').first
|
244
|
+
end
|
245
|
+
|
246
|
+
def parse_int32(data)
|
247
|
+
data.map { |i| [sprintf('%02x',i)].pack('H2') }.join.unpack('l<').first
|
248
|
+
end
|
249
|
+
|
250
|
+
def parse_uint32(data)
|
251
|
+
data.map { |i| [sprintf('%02x',i)].pack('H2') }.join.unpack('V').first
|
252
|
+
end
|
253
|
+
|
254
|
+
def parse_trade(data, start_index, symbol_length)
|
255
|
+
symbol = data[start_index + 2, symbol_length].map!{|c| c.chr}.join
|
256
|
+
price = parse_int32(data[start_index + 2 + symbol_length, 4]).to_f / 10000.0
|
257
|
+
size = parse_uint32(data[start_index + 6 + symbol_length, 4])
|
258
|
+
timestamp = parse_uint64(data[start_index + 10 + symbol_length, 8])
|
259
|
+
total_volume = parse_uint32(data[start_index + 18 + symbol_length, 4])
|
260
|
+
return Trade.new(symbol, price, size, timestamp, total_volume)
|
261
|
+
end
|
262
|
+
|
263
|
+
def parse_quote(data, start_index, symbol_length, msg_type)
|
264
|
+
type = case when msg_type == 1 then ASK when msg_type == 2 then BID end
|
265
|
+
symbol = data[start_index + 2, symbol_length].map!{|c| c.chr}.join
|
266
|
+
price = parse_int32(data[start_index + 2 + symbol_length, 4]).to_f / 10000.0
|
267
|
+
size = parse_uint32(data[start_index + 6 + symbol_length, 4])
|
268
|
+
timestamp = parse_uint64(data[start_index + 10 + symbol_length, 8])
|
269
|
+
return Quote.new(type, symbol, price, size, timestamp)
|
270
|
+
end
|
271
|
+
|
272
|
+
def handle_message(data, start_index)
|
273
|
+
msg_type = data[start_index]
|
274
|
+
symbol_length = data[start_index + 1]
|
275
|
+
case msg_type
|
276
|
+
when 0 then
|
277
|
+
trade = parse_trade(data, start_index, symbol_length)
|
278
|
+
@on_trade.call(trade)
|
279
|
+
return start_index + 22 + symbol_length
|
280
|
+
when 1 || 2 then
|
281
|
+
quote = parse_quote(data, start_index, symbol_length, msg_type)
|
282
|
+
@on_quote.call(quote)
|
283
|
+
return start_index + 18 + symbol_length
|
284
|
+
end
|
285
|
+
return start_index
|
286
|
+
end
|
287
|
+
|
288
|
+
def handle_data
|
289
|
+
Thread.current.priority -= 1
|
290
|
+
me = self
|
291
|
+
pop = nil
|
292
|
+
until @stop do
|
293
|
+
begin
|
294
|
+
pop = nil
|
295
|
+
data = nil
|
296
|
+
pop = @messages.deq
|
297
|
+
unless pop.nil?
|
298
|
+
begin
|
299
|
+
data = pop.unpack('C*')
|
300
|
+
rescue StandardError => ex
|
301
|
+
me.send :error, "Error unpacking data from queue: #{ex} #{pop}"
|
302
|
+
next
|
303
|
+
end
|
304
|
+
if !data then me.send :error, "Cannot process data. Data is nil. #{pop}" end
|
305
|
+
start_index = 1
|
306
|
+
count = data[0]
|
307
|
+
# These are grouped (many) messages.
|
308
|
+
# The first byte tells us how many there are.
|
309
|
+
# From there, check the type and symbol length at index 0 of each chunk to know how many bytes each message has.
|
310
|
+
count.times {start_index = handle_message(data, start_index)}
|
311
|
+
end
|
312
|
+
if pop.nil? then sleep(0.1) end
|
313
|
+
rescue StandardError => e
|
314
|
+
me.send :error, "Error handling message from queue: #{e} #{pop} : #{data} ; count: #{count} ; start index: #{start_index}"
|
315
|
+
rescue Exception => e
|
316
|
+
#me.send :error, "General error handling message from queue: #{e} #{pop} : #{data} ; count: #{count} ; start index: #{start_index}"
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
def refresh_token
|
322
|
+
@token = nil
|
323
|
+
|
324
|
+
uri = URI.parse(auth_url)
|
325
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
326
|
+
http.start
|
327
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
328
|
+
request.add_field("Client-Information", "IntrinioRealtimeRubySDKv3.1")
|
329
|
+
|
330
|
+
unless @api_key
|
331
|
+
request.basic_auth(@username, @password)
|
332
|
+
end
|
333
|
+
|
334
|
+
response = http.request(request)
|
335
|
+
|
336
|
+
return fatal("Unable to authorize") if response.code == "401"
|
337
|
+
return fatal("Could not get auth token") if response.code != "200"
|
338
|
+
|
339
|
+
@token = response.body
|
340
|
+
debug "Token refreshed"
|
341
|
+
end
|
342
|
+
|
343
|
+
def auth_url
|
344
|
+
url = ""
|
345
|
+
|
346
|
+
case @provider
|
347
|
+
when REALTIME then url = "https://realtime-mx.intrinio.com/auth"
|
348
|
+
when MANUAL then url = "http://" + @ip_address + "/auth"
|
349
|
+
end
|
350
|
+
|
351
|
+
url = api_auth_url(url) if @api_key
|
352
|
+
|
353
|
+
url
|
354
|
+
end
|
355
|
+
|
356
|
+
def api_auth_url(url)
|
357
|
+
if url.include? "?"
|
358
|
+
url = "#{url}&"
|
359
|
+
else
|
360
|
+
url = "#{url}?"
|
361
|
+
end
|
362
|
+
|
363
|
+
"#{url}api_key=#{@api_key}"
|
364
|
+
end
|
365
|
+
|
366
|
+
def socket_url
|
367
|
+
case @provider
|
368
|
+
when REALTIME then URI.escape("wss://realtime-mx.intrinio.com/socket/websocket?vsn=1.0.0&token=#{@token}")
|
369
|
+
when MANUAL then URI.escape("ws://" + @ip_address + "/socket/websocket?vsn=1.0.0&token=#{@token}")
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
def refresh_websocket
|
374
|
+
me = self
|
375
|
+
|
376
|
+
@ws.close() unless @ws.nil?
|
377
|
+
@ready = false
|
378
|
+
@joined_channels = []
|
379
|
+
|
380
|
+
@stop = true
|
381
|
+
sleep(2)
|
382
|
+
@threads.each { |thread|
|
383
|
+
if !thread.nil? && (!thread.pending_interrupt? || thread.status == "run" || thread.status == "Sleeping")
|
384
|
+
then thread.join(7)
|
385
|
+
elsif !thread.nil?
|
386
|
+
then thread.kill
|
387
|
+
end
|
388
|
+
}
|
389
|
+
@threads = []
|
390
|
+
@stop = false
|
391
|
+
@thread_quantity.times {@threads << Thread.new{handle_data}}
|
392
|
+
|
393
|
+
@ws = ws = WebSocket::Client::Simple.connect(socket_url)
|
394
|
+
me.send :info, "Connection opening"
|
395
|
+
|
396
|
+
ws.on :open do
|
397
|
+
me.send :info, "Connection established"
|
398
|
+
me.send :ready, true
|
399
|
+
if [REALTIME, MANUAL].include?(me.send(:provider))
|
400
|
+
me.send :refresh_channels
|
401
|
+
end
|
402
|
+
me.send :start_heartbeat
|
403
|
+
me.send :stop_self_heal
|
404
|
+
end
|
405
|
+
|
406
|
+
ws.on :message do |frame|
|
407
|
+
data_message = frame.data
|
408
|
+
#me.send :debug, "Message: #{data_message}"
|
409
|
+
begin
|
410
|
+
unless data_message.nil?
|
411
|
+
then me.send :queue_message, data_message
|
412
|
+
end
|
413
|
+
rescue StandardError => e
|
414
|
+
me.send :error, "Error adding message to queue: #{data_message} #{e}"
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
ws.on :close do |e|
|
419
|
+
me.send :ready, false
|
420
|
+
me.send :info, "Connection closing...: #{e}"
|
421
|
+
me.send :try_self_heal
|
422
|
+
end
|
423
|
+
|
424
|
+
ws.on :error do |e|
|
425
|
+
me.send :ready, false
|
426
|
+
me.send :error, "Connection error: #{e}"
|
427
|
+
me.send :try_self_heal
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
def refresh_channels
|
432
|
+
return unless @ready
|
433
|
+
debug "Refreshing channels"
|
434
|
+
|
435
|
+
# Join new channels
|
436
|
+
new_channels = @channels - @joined_channels
|
437
|
+
new_channels.each do |channel|
|
438
|
+
#msg = join_message(channel)
|
439
|
+
#@ws.send(msg.to_json)
|
440
|
+
msg = join_binary_message(channel)
|
441
|
+
@ws.send(msg)
|
442
|
+
info "Joined #{channel}"
|
443
|
+
end
|
444
|
+
|
445
|
+
# Leave old channels
|
446
|
+
old_channels = @joined_channels - @channels
|
447
|
+
old_channels.each do |channel|
|
448
|
+
#msg = leave__message(channel)
|
449
|
+
#@ws.send(msg.to_json)
|
450
|
+
msg = leave_binary_message(channel)
|
451
|
+
@ws.send(msg)
|
452
|
+
info "Left #{channel}"
|
453
|
+
end
|
454
|
+
|
455
|
+
@channels.uniq!
|
456
|
+
@joined_channels = Array.new(@channels)
|
457
|
+
debug "Current channels: #{@channels}"
|
458
|
+
end
|
459
|
+
|
460
|
+
def start_heartbeat
|
461
|
+
EM.cancel_timer(@heartbeat_timer) if @heartbeat_timer
|
462
|
+
@heartbeat_timer = EM.add_periodic_timer(HEARTBEAT_TIME) do
|
463
|
+
if msg = heartbeat_msg()
|
464
|
+
@ws.send(msg)
|
465
|
+
debug "Heartbeat #{msg}"
|
466
|
+
end
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
def heartbeat_msg
|
471
|
+
""
|
472
|
+
end
|
473
|
+
|
474
|
+
def stop_heartbeat
|
475
|
+
EM.cancel_timer(@heartbeat_timer) if @heartbeat_timer
|
476
|
+
end
|
477
|
+
|
478
|
+
def try_self_heal
|
479
|
+
return if @closing
|
480
|
+
debug "Attempting to self-heal"
|
481
|
+
|
482
|
+
time = @selfheal_backoffs.first
|
483
|
+
@selfheal_backoffs.delete_at(0) if @selfheal_backoffs.count > 1
|
484
|
+
|
485
|
+
EM.cancel_timer(@heartbeat_timer) if @heartbeat_timer
|
486
|
+
EM.cancel_timer(@selfheal_timer) if @selfheal_timer
|
487
|
+
|
488
|
+
@selfheal_timer = EM.add_timer(time/1000) do
|
489
|
+
connect()
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
def stop_self_heal
|
494
|
+
EM.cancel_timer(@selfheal_timer) if @selfheal_timer
|
495
|
+
@selfheal_backoffs = Array.new(SELF_HEAL_BACKOFFS)
|
496
|
+
end
|
497
|
+
|
498
|
+
def ready(val)
|
499
|
+
@ready = val
|
500
|
+
end
|
501
|
+
|
502
|
+
def debug(message)
|
503
|
+
message = "IntrinioRealtime | #{message}"
|
504
|
+
@logger.debug(message) rescue
|
505
|
+
nil
|
506
|
+
end
|
507
|
+
|
508
|
+
def info(message)
|
509
|
+
message = "IntrinioRealtime | #{message}"
|
510
|
+
@logger.info(message) rescue
|
511
|
+
nil
|
512
|
+
end
|
513
|
+
|
514
|
+
def error(message)
|
515
|
+
message = "IntrinioRealtime | #{message}"
|
516
|
+
@logger.error(message) rescue
|
517
|
+
nil
|
518
|
+
end
|
519
|
+
|
520
|
+
def fatal(message)
|
521
|
+
message = "IntrinioRealtime | #{message}"
|
522
|
+
@logger.fatal(message) rescue
|
523
|
+
EM.stop_event_loop
|
524
|
+
throw :fatal
|
525
|
+
nil
|
526
|
+
end
|
527
|
+
|
528
|
+
def parse_channels(channels)
|
529
|
+
channels.flatten!
|
530
|
+
channels.uniq!
|
531
|
+
channels.compact!
|
532
|
+
channels
|
533
|
+
end
|
534
|
+
|
535
|
+
def join_binary_message(channel)
|
536
|
+
if (channel == "lobby") && (@trades_only == false)
|
537
|
+
return [74, 0, 36, 70, 73, 82, 69, 72, 79, 83, 69].pack('C*') #74, not trades only, "$FIREHOSE"
|
538
|
+
elsif (channel == "lobby") && (@trades_only == true)
|
539
|
+
return [74, 1, 36, 70, 73, 82, 69, 72, 79, 83, 69].pack('C*') #74, trades only, "$FIREHOSE"
|
540
|
+
else
|
541
|
+
bytes = [74, 0]
|
542
|
+
if (@trades_only == true)
|
543
|
+
bytes[1] = 1
|
544
|
+
end
|
545
|
+
return bytes.concat(channel.bytes).pack('C*')
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
def leave_binary_message(channel)
|
550
|
+
if channel == "lobby"
|
551
|
+
return [76, 36, 70, 73, 82, 69, 72, 79, 83, 69].pack('C*') #74, not trades only, "$FIREHOSE"
|
552
|
+
else
|
553
|
+
bytes = [76]
|
554
|
+
return bytes.concat(channel.bytes).pack('C*')
|
555
|
+
end
|
556
|
+
end
|
557
|
+
|
558
|
+
def valid_api_key?(api_key)
|
559
|
+
return false unless api_key.is_a?(String)
|
560
|
+
return false if api_key.empty?
|
561
|
+
true
|
562
|
+
end
|
563
|
+
|
564
|
+
end
|
565
|
+
end
|
566
|
+
end
|
metadata
CHANGED
@@ -1,60 +1,74 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: intrinio-realtime
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.1.0
|
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: 2021-12-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
14
|
+
name: eventmachine
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '1.2'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '1.2'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: websocket-client-simple
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '0.3'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '0.3'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: thread
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
47
|
+
version: 0.2.2
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: 0.2.2
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bigdecimal
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.4.0
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.4.0
|
55
69
|
description: Intrinio Ruby SDK for Real-Time Stock Prices
|
56
70
|
email:
|
57
|
-
-
|
71
|
+
- ssnyder@intrinio.com
|
58
72
|
executables: []
|
59
73
|
extensions: []
|
60
74
|
extra_rdoc_files: []
|
@@ -79,10 +93,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
79
93
|
- !ruby/object:Gem::Version
|
80
94
|
version: '0'
|
81
95
|
requirements: []
|
82
|
-
|
83
|
-
rubygems_version: 2.5.2.2
|
96
|
+
rubygems_version: 3.1.2
|
84
97
|
signing_key:
|
85
98
|
specification_version: 4
|
86
|
-
summary: Intrinio provides real-time stock prices from
|
99
|
+
summary: Intrinio provides real-time stock prices from its Multi-Exchange feed, via
|
87
100
|
a two-way WebSocket connection.
|
88
101
|
test_files: []
|