cotcube-dataproxy 0.1.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 +7 -0
- data/CHANGELOG.md +18 -0
- data/Gemfile +11 -0
- data/VERSION +1 -0
- data/bin/dataproxy +24 -0
- data/bin/test_client.rb +24 -0
- data/cotcube-dataproxy.gemspec +42 -0
- data/lib/cotcube-dataproxy/3rd_clients.rb +65 -0
- data/lib/cotcube-dataproxy/client_response.rb +46 -0
- data/lib/cotcube-dataproxy/commserver.rb +185 -0
- data/lib/cotcube-dataproxy/gc.rb +157 -0
- data/lib/cotcube-dataproxy/init.rb +69 -0
- data/lib/cotcube-dataproxy/subscribers.rb +115 -0
- data/lib/cotcube-dataproxy.rb +36 -0
- metadata +229 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9edcf5e59cffe3a7dbb7a03c8896da6cd98ecb288dfc648297e06c64bd0b3314
|
4
|
+
data.tar.gz: 3701e4dc24d822c7349015ff4d3e5bcd28dea57d126905016706c276ee088136
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cf474143f34f1d81f148f1a72986d1c575f3407a19e4494c9fa6e0e08be18b90bf5057bf4f248667a6bcbd9812e32d9cb6e75a75343e05c765dd7ab9cc5c6ddf
|
7
|
+
data.tar.gz: 14599b72e780bcf9a942f70448ed1fdaf5ee9aad649b30c3701f056802fb3ea44275d6c47dee5e05da94eb8ed23620d393e14ec96c8ed347b33ad54c2f8db640
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
## 0.1.1 (November 13, 2021)
|
2
|
+
- added handy test_script in bin/test_client.rb
|
3
|
+
- added httparty to Gem requirements
|
4
|
+
- added handy startup script to bin/dataproxy
|
5
|
+
- removed superflouus lines in commserver.rb'
|
6
|
+
- gc.rb: the garbage collector. containing gc, gc_start/_stop, api() (uses management api to get a list), get_mq ( for listings) and delete_mq (for API based deletion)
|
7
|
+
- commserver.rb, handling incoming commands from clients. containing commserver_start/_stop and subscribe_persistent
|
8
|
+
- subscribers.rb, handling of incoming IB messages. PLUS __int2hex__, just a tiny helper to translate IDs
|
9
|
+
- client_response.rb, containing client_success and client_fail
|
10
|
+
- init.rb: containing initialize, shutdown, recover and log
|
11
|
+
- 3rd_clients: class level helpers to create IB and RabbitMQ client objects
|
12
|
+
- central startup file, containing bundler, requires and constants
|
13
|
+
- added startup script
|
14
|
+
- initial commit. adding Gemfile and gemspec
|
15
|
+
|
16
|
+
## 0.1.0 (November 10, 2021)
|
17
|
+
|
18
|
+
|
data/Gemfile
ADDED
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.1
|
data/bin/dataproxy
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../lib/cotcube-dataproxy'
|
4
|
+
|
5
|
+
Signal.trap('TERM') { local_interrupt }
|
6
|
+
Signal.trap('INT') { local_interrupt }
|
7
|
+
|
8
|
+
# TODO: Prepare reload config on SIGHUP
|
9
|
+
Signal.trap('HUP') { puts 'TODO: Reload config' }
|
10
|
+
exiting = false
|
11
|
+
|
12
|
+
dataproxy = Cotcube::DataProxy.new
|
13
|
+
|
14
|
+
define_method :local_interrupt do
|
15
|
+
dataproxy.send(:log, "DATAPROXY Received termination request...")
|
16
|
+
exiting = true
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
begin
|
21
|
+
loop { exit if exiting; sleep 0.5 }
|
22
|
+
ensure
|
23
|
+
dataproxy.shutdown
|
24
|
+
end
|
data/bin/test_client.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'cotcube-helpers'
|
3
|
+
|
4
|
+
s = Cotcube::Helpers::DataClient.new
|
5
|
+
|
6
|
+
print "Ping => "
|
7
|
+
puts "#{s.send_command({ command: 'ping' })}"
|
8
|
+
|
9
|
+
raw = s.get_historical(contract: 'GCZ21', interval: :min15)
|
10
|
+
|
11
|
+
result = JSON.parse(raw, symbolize_names: true)
|
12
|
+
if result[:error].zero?
|
13
|
+
result[:result][-20..].each {|z| p z.slice(*%i[time open high low close volume]) }
|
14
|
+
else
|
15
|
+
puts "Some ERROR occured: #{result}"
|
16
|
+
end
|
17
|
+
|
18
|
+
# Please test this during business hours
|
19
|
+
bars = [ ]
|
20
|
+
id = s.start_persistent(contract: 'GCZ21', type: :realtimebars) {|bar| puts "Got #{bar}"; bars << bar }
|
21
|
+
sleep 20
|
22
|
+
s.stop_persistent(contract: 'GCZ21', type: :realtimebars)
|
23
|
+
|
24
|
+
puts "Received #{bars.count} bars. Exiting now..."
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = 'cotcube-dataproxy'
|
5
|
+
spec.version = File.read("#{__dir__}/VERSION")
|
6
|
+
spec.authors = ['Benjamin L. Tischendorf']
|
7
|
+
spec.email = ['donkeybridge@jtown.eu']
|
8
|
+
|
9
|
+
spec.summary = 'An AMQ based proxy to retrieve price and contract data from IBKR TWS.'
|
10
|
+
spec.description = 'An AMQ based proxy to retrieve price and contract data from IBKR TWS. The AMQ is provided by rabbitMQ with the gem Bunny, the API to TWS/ibGateway with the gem ib-api'
|
11
|
+
|
12
|
+
spec.homepage = 'https://github.com/donkeybridge/' + spec.name
|
13
|
+
spec.license = 'BSD-3-Clause'
|
14
|
+
spec.required_ruby_version = Gem::Requirement.new('~> 2.7')
|
15
|
+
|
16
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
17
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
18
|
+
spec.metadata['changelog_uri'] = spec.homepage + '/CHANGELOG.md'
|
19
|
+
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
21
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
22
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
23
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
24
|
+
end
|
25
|
+
spec.bindir = 'bin'
|
26
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
|
+
spec.require_paths = ['lib']
|
28
|
+
|
29
|
+
spec.add_dependency 'activesupport', '~> 6'
|
30
|
+
spec.add_dependency 'ib-api', '~> 972'
|
31
|
+
spec.add_dependency 'colorize', '~> 0.8'
|
32
|
+
spec.add_dependency 'cotcube-helpers', '~> 0.2.1'
|
33
|
+
spec.add_dependency 'outputhandler', '~> 0.2'
|
34
|
+
spec.add_dependency 'bunny', '~> 2'
|
35
|
+
spec.add_dependency 'httparty' '~> 0.20'
|
36
|
+
spec.add_dependency 'yaml', '~> 0.1'
|
37
|
+
spec.add_dependency 'json', '~> 2'
|
38
|
+
|
39
|
+
spec.add_development_dependency 'rake', '~> 13'
|
40
|
+
spec.add_development_dependency 'rspec', '~>3.6'
|
41
|
+
spec.add_development_dependency 'yard', '~>0.9'
|
42
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Cotcube
|
2
|
+
class DataProxy
|
3
|
+
|
4
|
+
# Create a connection to the locally running
|
5
|
+
def self.get_ib_client(host: 'localhost', port: 4002, id: 5, client_id: 5)
|
6
|
+
obj = {
|
7
|
+
id: id,
|
8
|
+
client_id: client_id,
|
9
|
+
port: port,
|
10
|
+
host: host
|
11
|
+
}
|
12
|
+
begin
|
13
|
+
obj[:ib] = IB::Connection.new(
|
14
|
+
id: id,
|
15
|
+
client_id: client_id,
|
16
|
+
port: port,
|
17
|
+
host: host
|
18
|
+
) do |provider|
|
19
|
+
obj[:alert] = provider.subscribe(:Alert) { true }
|
20
|
+
obj[:managed_accounts] = provider.subscribe(:ManagedAccounts) { true }
|
21
|
+
end
|
22
|
+
obj[:error] = 0
|
23
|
+
rescue Exception => e
|
24
|
+
obj[:error] = 1
|
25
|
+
obj[:message] = e.message
|
26
|
+
obj[:full_message] = e.full_message
|
27
|
+
end
|
28
|
+
obj
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
def self.get_mq_client(client_id: 5)
|
33
|
+
obj = {
|
34
|
+
client_id: client_id,
|
35
|
+
}
|
36
|
+
begin
|
37
|
+
# for more info on connection parameters see http://rubybunny.info/articles/connecting.html
|
38
|
+
#
|
39
|
+
obj[:connection] = Bunny.new(
|
40
|
+
host: 'localhost',
|
41
|
+
port: 5672,
|
42
|
+
user: SECRETS['dataproxy_mq_user'],
|
43
|
+
password: SECRETS['dataproxy_mq_password'],
|
44
|
+
vhost: SECRETS['dataproxy_mq_vhost']
|
45
|
+
)
|
46
|
+
obj[:connection].start
|
47
|
+
obj[:commands] = obj[:connection].create_channel
|
48
|
+
obj[:channel] = obj[:connection].create_channel
|
49
|
+
obj[:request_queue] = obj[:commands].queue('', exclusive: true, auto_delete: true)
|
50
|
+
obj[:request_exch] = obj[:commands].direct('dataproxy_commands', exclusive: true, auto_delete: true)
|
51
|
+
obj[:replies_exch] = obj[:commands].direct('dataproxy_replies', auto_delete: true)
|
52
|
+
%w[ dataproxy_commands ].each do |key|
|
53
|
+
obj[:request_queue].bind(obj[:request_exch], routing_key: key )
|
54
|
+
end
|
55
|
+
obj[:error] = 0
|
56
|
+
rescue Exception => e
|
57
|
+
obj[:error] = 1
|
58
|
+
obj[:message] = e.message
|
59
|
+
obj[:full_message] = e.full_message
|
60
|
+
end
|
61
|
+
obj
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Cotcube
|
2
|
+
class DataProxy
|
3
|
+
|
4
|
+
def client_success(request, id: nil, to: nil, exchange: :replies_exch, &block)
|
5
|
+
client_response( request, id: nil, to: nil, err: 0, exchange: exchange, &block)
|
6
|
+
end
|
7
|
+
|
8
|
+
def client_fail(request, id: nil, to: nil, exchange: :replies_exch, &block)
|
9
|
+
client_response(request, id: nil, to: nil, err: 1, exchange: exchange, &block)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def client_response(request, id: nil, to: nil, err:, exchange: :replies_exch)
|
15
|
+
__id__ = id.presence || request.delete(:__id__)
|
16
|
+
__to__ = to.presence || request.delete(:__to__)
|
17
|
+
msg = yield
|
18
|
+
case msg
|
19
|
+
when String
|
20
|
+
response = { error: err, msg: msg }
|
21
|
+
when Hash
|
22
|
+
response = { error: err }
|
23
|
+
msg.each { |k, v| response[k] = v }
|
24
|
+
when Array
|
25
|
+
response = { error: err, result: msg }
|
26
|
+
else
|
27
|
+
response = { error: 1, msg: "Processing failed for '#{msg.inspect}' after '#{request}'." }
|
28
|
+
end
|
29
|
+
if response[:error] == 1
|
30
|
+
log "CLIENT #{id} FAILIURE: #{response.inspect}.".colorize(:light_red)
|
31
|
+
elsif response[:result].is_a?(Array)
|
32
|
+
log "CLIENT #{id} SUCCESS: sent #{response[:result].size} datasets."
|
33
|
+
else
|
34
|
+
log "CLIENT #{id} SUCCESS: #{response.to_s.scan(/.{1,120}/).join(' '*30 + "\n")}"
|
35
|
+
end
|
36
|
+
mq[exchange].publish(
|
37
|
+
response.to_json,
|
38
|
+
content_type: 'application/json',
|
39
|
+
priority: 7,
|
40
|
+
correlation_id: __id__,
|
41
|
+
routing_key: __to__,
|
42
|
+
reply_to: __id__
|
43
|
+
)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cotcube
|
4
|
+
class DataProxy
|
5
|
+
|
6
|
+
def commserver_start
|
7
|
+
mq[:request_subscription] = mq[:request_queue].subscribe do |delivery_info, properties, payload|
|
8
|
+
|
9
|
+
################################################################################################
|
10
|
+
# the request will be JSON decoded. The generic command 'failed' will be set, if decoding raises.
|
11
|
+
# furthermore, __id__ and __to__ are extracted and added to the request-hash
|
12
|
+
#
|
13
|
+
request = JSON.parse(payload, symbolize_names: true) rescue { command: 'failed' }
|
14
|
+
request[:command] ||= 'nil'
|
15
|
+
|
16
|
+
request[:__id__] = properties[:correlation_id]
|
17
|
+
request[:__to__] = properties[:reply_to]
|
18
|
+
|
19
|
+
if request[:debug]
|
20
|
+
log "Received \t#{delivery_info.map{|k,v| "#{k}\t#{v}"}.join("\n")
|
21
|
+
}\n\n#{properties .map{|k,v| "#{k}\t#{v}"}.join("\n")
|
22
|
+
}\n\n#{request .map{|k,v| "#{k}\t#{v}"}.join("\n")}" if request[:debug]
|
23
|
+
else
|
24
|
+
log "Received\t#{request}"
|
25
|
+
end
|
26
|
+
|
27
|
+
###############################################################################################
|
28
|
+
# the entire set of command processing,
|
29
|
+
# starting with the (generic) 'failed' command, that just answers with the failure notice
|
30
|
+
# and with another failure notice upon a missing command section in the request
|
31
|
+
# ending with another failure notice, if an unknown command was issued
|
32
|
+
#
|
33
|
+
log "Processing #{request[:command]}:"
|
34
|
+
case request[:command].downcase
|
35
|
+
when 'failed'
|
36
|
+
client_fail(request) { "Failed to parse payload: '#{payload}'." }
|
37
|
+
|
38
|
+
when 'nil'
|
39
|
+
client_fail(request) { "missing :command in request: '#{request}'." }
|
40
|
+
|
41
|
+
##############################################################################################
|
42
|
+
# ping -> pong, just for testing
|
43
|
+
#
|
44
|
+
when 'ping'
|
45
|
+
client_success(request) { "pong" }
|
46
|
+
|
47
|
+
##############################################################################################
|
48
|
+
# the get_contracts command tries to resolve a list of available contracts related to a
|
49
|
+
# specific symbol based on a set of characteristics retrieved via Herlpers::get_id_set
|
50
|
+
#
|
51
|
+
# the reply of the message is processed asynchroniously upon reception of
|
52
|
+
# the IB Message 'ContractDataEnd' in the message subscribers section
|
53
|
+
#
|
54
|
+
when 'get_contract', 'get_contracts', Cotcube::Helpers.sub(minimum: 3) { 'contracts' }
|
55
|
+
if request[:symbol].nil?
|
56
|
+
client_fail(request) { "Cannot requets contracts without :symbol (in '#{request}')." }
|
57
|
+
next
|
58
|
+
end
|
59
|
+
sym = Cotcube::Helpers.get_id_set(symbol: request[:symbol])
|
60
|
+
if [nil, false].include? sym
|
61
|
+
client_fail(request) { "Unknown symbol '#{request[:symbol]}' in '#{request}'." }
|
62
|
+
next
|
63
|
+
end
|
64
|
+
request[:result] = []
|
65
|
+
req_mon.synchronize { requests[request[:__id__]] = request }
|
66
|
+
ib_contract = IB::Contract.new symbol: sym[:ib_symbol], exchange: sym[:exchange], currency: sym[:currency], sec_type: (request[:sec_type] || 'FUT')
|
67
|
+
ib.send_message :RequestContractData, contract: ib_contract, request_id: request[:__id__].to_i(16)
|
68
|
+
|
69
|
+
|
70
|
+
##############################################################################################
|
71
|
+
# the historical command retrieves a list of bars as provided by TWS
|
72
|
+
# the minimum requirement is :contract parameter issued.
|
73
|
+
#
|
74
|
+
# the reply to this message is processed asynchroniously upon reception of
|
75
|
+
# the IB message 'HistoricalData' in message subscribers section
|
76
|
+
#
|
77
|
+
when Cotcube::Helpers.sub(minimum: 3) {'historical'}
|
78
|
+
con_id = request[:con_id] || Cotcube::Helpers.get_ib_contract(request[:contract])[:con_id] rescue nil
|
79
|
+
if con_id.nil? or request[:contract].nil?
|
80
|
+
client_fail(request) { "Cannot get :con_id for contract:'#{request[:contract]}' in '#{request}'." }
|
81
|
+
next
|
82
|
+
end
|
83
|
+
sym = Cotcube::Helpers.get_id_set(contract: request[:contract])
|
84
|
+
before = Time.at(request[:before]).to_ib rescue Time.now.to_ib
|
85
|
+
ib_contract = IB::Contract.new(con_id: con_id, exchange: sym[:exchange])
|
86
|
+
req = {
|
87
|
+
request_id: request[:__id__].to_i(16),
|
88
|
+
contract: ib_contract,
|
89
|
+
end_date_time: before,
|
90
|
+
what_to_show: (request[:based_on] || :trades),
|
91
|
+
use_rth: (request[:rth_only] || 1),
|
92
|
+
keep_up_to_date: 0,
|
93
|
+
duration: (request[:duration].gsub('_', ' ') || '1 D'),
|
94
|
+
bar_size: (request[:interval].to_sym || :min15)
|
95
|
+
}
|
96
|
+
req_mon.synchronize { requests[request[:__id__]] = request }
|
97
|
+
begin
|
98
|
+
Timeout.timeout(2) { ib.send_message(IB::Messages::Outgoing::RequestHistoricalData.new(req)) }
|
99
|
+
rescue Timeout::Error, IB::Error
|
100
|
+
client_fail(request) { 'Could not request historical data. Is ib_client running?' }
|
101
|
+
req_mon.synchronize { requests.delete(request[:__id__]) }
|
102
|
+
next
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
# ********************************************************************************************
|
107
|
+
#
|
108
|
+
# REQUESTS BELOW ARE BASED ON A cONTINUOUS IB SUBSCRIPTION AND MUST BE CONSIDERED
|
109
|
+
# GARBAGE COLLECTION ( instance.gc ) --- SUBSCRIPTION DATA IS PERSISTET IN @persistent
|
110
|
+
#
|
111
|
+
# ********************************************************************************************
|
112
|
+
|
113
|
+
|
114
|
+
###############################################################################################
|
115
|
+
# the start_realtimebars initiates the IBKR realtime (5s) bars delivery for a specific contract
|
116
|
+
# and feeds them into a fanout exchange dedicated to that contract
|
117
|
+
# delivery continues as long as there are queues bound to that exchange
|
118
|
+
#
|
119
|
+
when Cotcube::Helpers.sub(minimum:4){'realtimebars'}
|
120
|
+
subscribe_persistent(request, type: :realtimebars)
|
121
|
+
next
|
122
|
+
|
123
|
+
when 'ticks'
|
124
|
+
subscribe_persistent(request, type: :realtimebars)
|
125
|
+
next
|
126
|
+
|
127
|
+
when 'depth'
|
128
|
+
subscribe_persistent(request, type: :depth)
|
129
|
+
next
|
130
|
+
|
131
|
+
else
|
132
|
+
client_fail(request) { "Unknown :command '#{request[:command]}' in '#{request}'." }
|
133
|
+
end
|
134
|
+
end
|
135
|
+
log "Started commserver listening on #{mq[:request_queue]}"
|
136
|
+
end
|
137
|
+
|
138
|
+
def commserver_stop
|
139
|
+
mq[:request_subscription].cancel
|
140
|
+
log "Stopped commserver ..."
|
141
|
+
end
|
142
|
+
|
143
|
+
def subscribe_persistent(request, type:)
|
144
|
+
sym = Cotcube::Helpers.get_id_set(contract: request[:contract])
|
145
|
+
con_id = request[:con_id] || Cotcube::Helpers.get_ib_contract(request[:contract])[:con_id] rescue nil
|
146
|
+
if sym.nil? or con_id.nil?
|
147
|
+
client_fail(request) { "Invalid contract '#{request[:contract]}'." }
|
148
|
+
return
|
149
|
+
end
|
150
|
+
if persistent[type][con_id].nil?
|
151
|
+
per_mon.synchronize {
|
152
|
+
persistent[type][con_id] = { con_id: con_id,
|
153
|
+
contract: request[:contract],
|
154
|
+
exchange: mq[:channel].fanout(request[:exchange]) }
|
155
|
+
}
|
156
|
+
if type == :depth
|
157
|
+
bufferthread = Thread.new do
|
158
|
+
sleep 5.0 - (Time.now.to_f % 5)
|
159
|
+
loop do
|
160
|
+
begin
|
161
|
+
@block_depth_queue = true
|
162
|
+
sleep 0.025
|
163
|
+
con = persistent[:depth][con_id]
|
164
|
+
con[:exchange].publish(con[:buffer].to_json)
|
165
|
+
con[:buffer] = []
|
166
|
+
@block_depth_queue = false
|
167
|
+
end
|
168
|
+
sleep 5.0 - (Time.now.to_f % 5)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
per_mon.synchronize { persistent[:depth][con_id][:bufferthread] = bufferthread }
|
172
|
+
end
|
173
|
+
ib_contract = IB::Contract.new(con_id: con_id, exchange: sym[:exchange])
|
174
|
+
ib.send_message(REQUEST_TYPES[type], id: con_id, contract: ib_contract, data_type: :trades, use_rth: false)
|
175
|
+
client_success(request) { "Delivery of #{type} of #{request[:contract]} started." }
|
176
|
+
elsif persistent[type][con_id][:on_cancel]
|
177
|
+
client_fail(request) { { reason: :on_cancel, message: "Exchange '#{requst[:exchange]}' is marked for cancel, retry in a few seconds to recreate" } }
|
178
|
+
else
|
179
|
+
client_success(request) { "Delivery of #{type} of #{request[:contract]} attached to existing." }
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# about garbage collection, i.e. periodically check for unused artifacts and clean them
|
2
|
+
#
|
3
|
+
module Cotcube
|
4
|
+
class DataProxy
|
5
|
+
|
6
|
+
def gc_start
|
7
|
+
if gc_thread.nil?
|
8
|
+
@gc_thread = Thread.new do
|
9
|
+
loop do
|
10
|
+
sleep 30 + 2 * Random.rand
|
11
|
+
gc
|
12
|
+
end
|
13
|
+
end
|
14
|
+
log 'GC_INFO: GC spawned.'
|
15
|
+
else
|
16
|
+
log 'GC_ERROR: Cannot start GC_THREAD more than once.'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def gc_stop
|
21
|
+
if gc_thread.nil?
|
22
|
+
log 'GC_ERROR: Cannot stop nonexisting GC_THREAD.'
|
23
|
+
false
|
24
|
+
else
|
25
|
+
gc_thread.kill
|
26
|
+
gc_thread = nil
|
27
|
+
log 'GC_INFO: GC stopped.'
|
28
|
+
true
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def gc
|
33
|
+
get_mq(list: false).each do |item_type, items|
|
34
|
+
items.each do |key, item|
|
35
|
+
case item_type
|
36
|
+
when :bindings
|
37
|
+
# NOTE if might be considerable to unbind unused queues from exchanges before removing either
|
38
|
+
# as the bound value of the exchange will decrease it might be removed on next run of GC
|
39
|
+
when :queues
|
40
|
+
next if item[:consumers].nil? or item[:consumers].positive?
|
41
|
+
delete_mq(type: item_type, instance: item[:name])
|
42
|
+
log "GC_INFO: Deleted unsed queue: #{item}."
|
43
|
+
# sadly we don't know about unsed queues. basically this means that some client did not declare its queue as auto_delete.
|
44
|
+
# the dateproxy itself has only 1 queue -- the command queue, everything else is send out to exchanges
|
45
|
+
when :exchanges
|
46
|
+
if item[:name].empty? or item[:name] =~ /^amq\./ or item[:bound].count.positive? or
|
47
|
+
%w[ dataproxy_commands dataproxy_replies ].include? item[:name]
|
48
|
+
next
|
49
|
+
end
|
50
|
+
#log "GC_INFO: found superfluous exchange '#{item[:name]}'"
|
51
|
+
|
52
|
+
_, subscription_type, contract = item[:name].split('_')
|
53
|
+
unless %w[ ticks depth realtimebars ].include? subscription_type.downcase
|
54
|
+
puts "GC_WARNING: Unknown subscription_type '#{subscription_type}', skipping..."
|
55
|
+
next
|
56
|
+
end
|
57
|
+
con_id = Cotcube::Helpers.get_ib_contract(contract)[:con_id] rescue 0
|
58
|
+
if con_id.zero?
|
59
|
+
puts "GC_WARNING: No con_id found for contract '#{contract}', skipping..."
|
60
|
+
next
|
61
|
+
end
|
62
|
+
if persistent[subscription_type.to_sym][con_id].nil?
|
63
|
+
puts "GC_WARNING: No record for subscription '#{subscription_type}_#{contract}' with #{con_id} found, deleting anyway..."
|
64
|
+
end
|
65
|
+
Thread.new do
|
66
|
+
per_mon.synchronize {
|
67
|
+
persistent[subscription_type.to_sym][con_id][:on_cancel] = true if persistent.dig(subscription_type.to_sym, con_id)
|
68
|
+
}
|
69
|
+
log "GC_INFO: Sending cancel for #{subscription_type}::#{contract}::#{con_id}."
|
70
|
+
message_type = case subscription_type;
|
71
|
+
when 'ticks'; :CancelMarketData
|
72
|
+
when 'depth'; :CancelMarketDepth
|
73
|
+
else; :CancelRealTimeBars
|
74
|
+
end
|
75
|
+
if ib.send_message( message_type, id: con_id )
|
76
|
+
sleep 0.75 + Random.rand
|
77
|
+
res = delete_mq(type: item_type, instance: item[:name])
|
78
|
+
log "GC_SUCCESS: exchange '#{item[:name]}' with #{con_id} has been deleted ('#{res}')."
|
79
|
+
per_mon.synchronize { persistent[subscription_type.to_sym].delete(con_id) }
|
80
|
+
else
|
81
|
+
log "GC_FAILED: something went wrong unsubscribing '#{subscription_type}_#{contract}' with #{con_id}."
|
82
|
+
end
|
83
|
+
end
|
84
|
+
else
|
85
|
+
log "GC_ERROR: Unexpected type '#{item_type}' in GarbageCollector"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def get_mq(list: true)
|
92
|
+
bindings = api(type: :bindings)
|
93
|
+
results = {}
|
94
|
+
%i[ queues exchanges ].each do |type|
|
95
|
+
results[type] = {}
|
96
|
+
items = api type: type
|
97
|
+
items.each do |item|
|
98
|
+
query_name = item[:name].empty? ? 'amq.default' : item[:name]
|
99
|
+
results[type][item[:name]] = api type: type, instance: query_name
|
100
|
+
results[type][item[:name]][:bound] = bindings.select{|z| z[:source] == item[:name] }.map{|z| z[:destination]} if type == :exchanges
|
101
|
+
end
|
102
|
+
end
|
103
|
+
results.each do |type, items|
|
104
|
+
items.each do |key,item|
|
105
|
+
if item.is_a? Array
|
106
|
+
puts "#{key}\t\t#{item}"
|
107
|
+
next
|
108
|
+
end
|
109
|
+
puts "#{format '%12s', type.to_s.upcase} #{
|
110
|
+
case type
|
111
|
+
when :queues
|
112
|
+
"Key: #{format '%-30s', key} Cons: #{item[:consumers]}"
|
113
|
+
when :exchanges
|
114
|
+
"Key: #{format '%-30s', key} Type: #{format '%10s', item[:type]} Bound: #{item[:bound].presence || 'None.'}"
|
115
|
+
else
|
116
|
+
"Unknown details for #{type}"
|
117
|
+
end
|
118
|
+
}"
|
119
|
+
end
|
120
|
+
end if list
|
121
|
+
results[:bindings] = bindings
|
122
|
+
results
|
123
|
+
end
|
124
|
+
|
125
|
+
def delete_mq(type:, instance: )
|
126
|
+
allowed_types = %i[ queues exchanges ]
|
127
|
+
raise ArgumentError, "Type must be in '#{allowed_types}', but is '#{type}'" unless allowed_types.include? type
|
128
|
+
result = HTTParty.delete("#{SECRETS['dataproxy_mq_proto']
|
129
|
+
}://#{SECRETS['dataproxy_mq_user']
|
130
|
+
}:#{SECRETS['dataproxy_mq_password']
|
131
|
+
}@#{SECRETS['dataproxy_mq_host']
|
132
|
+
}:#{SECRETS['dataproxy_mq_port']
|
133
|
+
}/api/#{type.to_s
|
134
|
+
}/dp/#{instance}", query: { 'if-unused' => true })
|
135
|
+
if result.code == 204
|
136
|
+
result = { error: 0}
|
137
|
+
else
|
138
|
+
result = JSON.parse(result.body, symbolize_names: true)
|
139
|
+
result[:error] = 1
|
140
|
+
end
|
141
|
+
result
|
142
|
+
end
|
143
|
+
|
144
|
+
def api(type:, instance: nil)
|
145
|
+
allowed_types = %i[ queues exchanges bindings ] # other types need different API sepc: channels connections definitions
|
146
|
+
raise ArgumentError, "Type must be in '#{allowed_types}', but is '#{type}'" unless allowed_types.include? type
|
147
|
+
req = "#{type.to_s}/#{SECRETS['dataproxy_mq_vhost']}#{instance.nil? ? '' : "/#{instance}"}"
|
148
|
+
JSON.parse(HTTParty.get("#{SECRETS['dataproxy_mq_proto']
|
149
|
+
}://#{SECRETS['dataproxy_mq_user']
|
150
|
+
}:#{SECRETS['dataproxy_mq_password']
|
151
|
+
}@#{SECRETS['dataproxy_mq_host']
|
152
|
+
}:#{SECRETS['dataproxy_mq_port']
|
153
|
+
}/api/#{req}").body, symbolize_names: true)
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Cotcube
|
4
|
+
# top-level class documentation comment
|
5
|
+
class DataProxy
|
6
|
+
|
7
|
+
def initialize(
|
8
|
+
outputhandler: OutputHandler.new(
|
9
|
+
location: "/var/cotcube/log/dataproxy"
|
10
|
+
)
|
11
|
+
)
|
12
|
+
@output = outputhandler
|
13
|
+
@client = DataProxy.get_ib_client
|
14
|
+
@mq = DataProxy.get_mq_client
|
15
|
+
@ib = @client[:ib]
|
16
|
+
raise 'Could not connect to IB' unless @ib
|
17
|
+
raise 'Could not connect to RabbitMQ' if %i[ request_exch replies_exch request_queue ].map{|z| mq[z].nil? }.reduce(:|)
|
18
|
+
@requests = {}
|
19
|
+
@req_mon = Monitor.new
|
20
|
+
@persistent = { ticks: {}, depth: {}, realtimebars: {} }
|
21
|
+
@per_mon = Monitor.new
|
22
|
+
@gc_thread = nil
|
23
|
+
spawn_message_subscribers
|
24
|
+
commserver_start
|
25
|
+
recover
|
26
|
+
gc_start
|
27
|
+
end
|
28
|
+
|
29
|
+
def shutdown
|
30
|
+
puts "Shutting down dataproxy."
|
31
|
+
commserver_stop
|
32
|
+
gc_stop
|
33
|
+
mq[:commands].close
|
34
|
+
mq[:channel].close
|
35
|
+
mq[:connection].close
|
36
|
+
persistent.each do |type, items|
|
37
|
+
items.each do |con_id, item|
|
38
|
+
log "sending #{ CANCEL_TYPES[type.to_sym]} #{con_id} (for #{item[:contract]})"
|
39
|
+
ib.send_message CANCEL_TYPES[type.to_sym], id: con_id
|
40
|
+
end
|
41
|
+
end
|
42
|
+
sleep 1
|
43
|
+
gc
|
44
|
+
sleep 1
|
45
|
+
ib.close
|
46
|
+
puts "... done."
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
attr_reader :client, :clients, :ib, :mq, :requests, :req_mon, :persistent, :per_mon, :gc_thread
|
51
|
+
|
52
|
+
def recover
|
53
|
+
get_mq(list: false)[:exchanges].keys.select{|z| z.split('_').size == 3 }.each do |exch|
|
54
|
+
src, type, contract = exch.split('_')
|
55
|
+
next unless src == 'dataproxy'
|
56
|
+
next unless %w[ ticks depth realtimebars ].include? type.downcase
|
57
|
+
puts "Found #{exch} to recover."
|
58
|
+
subscribe_persistent( { contract: contract, exchange: exch }, type: type.to_sym )
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def log(msg)
|
63
|
+
@output.puts "#{DateTime.now.strftime('%Y%m%d-%H:%M:%S: ')}#{msg.to_s.scan(/.{1,120}/).join("\n" + ' ' * 20)}"
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
__END__
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module Cotcube
|
2
|
+
class DataProxy
|
3
|
+
|
4
|
+
def __int2hex__(id)
|
5
|
+
tmp = id.to_s(16) rescue nil
|
6
|
+
return nil if tmp.nil?
|
7
|
+
tmp.prepend('0') while tmp.length < 7
|
8
|
+
return tmp
|
9
|
+
end
|
10
|
+
|
11
|
+
def spawn_message_subscribers
|
12
|
+
@msg_queue = Queue.new
|
13
|
+
@depth_queue = Queue.new
|
14
|
+
|
15
|
+
ib.subscribe(:MarketDataType, :TickRequestParameters){|msg| log "#{msg.class}\t#{msg.data.inspect}".colorize(:yellow) }
|
16
|
+
|
17
|
+
@msg_subscriber = ib.subscribe(
|
18
|
+
:Alert,
|
19
|
+
:ContractData, :ContractDataEnd, :BondContractData,
|
20
|
+
:TickGeneric, :TickString, :TickPrice, :TickSize,
|
21
|
+
:HistoricalData,
|
22
|
+
:RealTimeBar
|
23
|
+
) do |msg|
|
24
|
+
|
25
|
+
@msg_queue << msg
|
26
|
+
end
|
27
|
+
|
28
|
+
@depth_subscriber = ib.subscribe( :MarketDepth ) {|msg| @depth_queue << msg}
|
29
|
+
|
30
|
+
@msg_subscriber_thread = Thread.new do
|
31
|
+
loop do
|
32
|
+
msg = @msg_queue.pop
|
33
|
+
|
34
|
+
data = msg.data
|
35
|
+
data[:time] = msg.created_at.strftime('%H:%M:%S')
|
36
|
+
data[:timestamp] = (msg.created_at.to_time.to_f * 1000).to_i
|
37
|
+
__id__ = __int2hex__(data[:request_id])
|
38
|
+
|
39
|
+
case msg
|
40
|
+
|
41
|
+
when IB::Messages::Incoming::HistoricalData
|
42
|
+
client_success(requests[__id__]) { msg.results }
|
43
|
+
req_mon.synchronize { requests.delete(__id__) }
|
44
|
+
|
45
|
+
when IB::Messages::Incoming::Alert # Alert
|
46
|
+
__id__ = __int2hex__(data[:error_id])
|
47
|
+
case data[:code]
|
48
|
+
when 162
|
49
|
+
log("ALERT 162:".light_red + ' MISSING MARKET DATA PERMISSION')
|
50
|
+
when 201
|
51
|
+
log("ALERT 201:".light_red + ' DUPLICATE OCA_GROUP')
|
52
|
+
else
|
53
|
+
log("ALERT #{data[:code]}:".light_red + " #{data[:message]}")
|
54
|
+
end
|
55
|
+
data[:msg_type] = 'alert'
|
56
|
+
client_fail(requests[__id__]) {data} unless requests[__id__].nil?
|
57
|
+
log data
|
58
|
+
|
59
|
+
when IB::Messages::Incoming::ContractData
|
60
|
+
req_mon.synchronize do
|
61
|
+
requests[__id__][:result] << data[:contract].slice(:local_symbol, :last_trading_day, :con_id)
|
62
|
+
end
|
63
|
+
|
64
|
+
when IB::Messages::Incoming::ContractDataEnd
|
65
|
+
sleep 0.25
|
66
|
+
client_success(requests[__id__]) { requests[__id__][:result] }
|
67
|
+
req_mon.synchronize { requests.delete(__id__) }
|
68
|
+
|
69
|
+
when IB::Messages::Incoming::RealTimeBar
|
70
|
+
con_id = data[:request_id]
|
71
|
+
bar = data[:bar]
|
72
|
+
exchange = persistent[:realtimebars][con_id][:exchange]
|
73
|
+
begin
|
74
|
+
exchange.publish(bar.slice(*%i[time open high low close volume trades wap]).to_json)
|
75
|
+
rescue Bunny::ChannelAlreadyClosed
|
76
|
+
ib.send_message :CancelRealTimeBars, id: con_id
|
77
|
+
log "Delivery for #{persistent[:realtimebars][con_id][:contract] rescue 'unknown contract'
|
78
|
+
} with con_id #{con_id} has been stopped."
|
79
|
+
Thread.new{ sleep 5; per_mon.synchronize { persistent[:realtimebars].delete(con_id) } }
|
80
|
+
end
|
81
|
+
|
82
|
+
when IB::Messages::Incoming::TickSize,
|
83
|
+
IB::Messages::Incoming::TickPrice,
|
84
|
+
IB::Messages::Incoming::TickGeneric,
|
85
|
+
IB::Messages::Incoming::TickString
|
86
|
+
con_id = data[:ticker_id]
|
87
|
+
contract = persistent[:ticks][con_id][:contract]
|
88
|
+
exchange = persistent[:ticks][con_id][:exchange]
|
89
|
+
begin
|
90
|
+
exchange.publish(data.inspect.to_json)
|
91
|
+
rescue Bunny::ChannelAlreadyClosed
|
92
|
+
ib.send_message :CancelMarketData, id: con_id
|
93
|
+
log "Delivery for #{persistent[:ticks][con_id][:contract]} with con_id #{con_id} has been stopped."
|
94
|
+
Thread.new{ sleep 0.25; per_mon.synchronize { persistent[:ticks].delete(con_id) } }
|
95
|
+
end
|
96
|
+
|
97
|
+
else
|
98
|
+
log("WARNING".light_red + "\tUnknown messagetype: #{msg.inspect}")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
log "SPAWN_SUBSCRIBERS\tSubscribers attached to IB" if @debug
|
102
|
+
end
|
103
|
+
@depth_subscriber_thread = Thread.new do
|
104
|
+
loop do
|
105
|
+
sleep 0.025 while @block_depth_queue
|
106
|
+
msg = @depth_queue.pop
|
107
|
+
con_id = msg.data[:request_id]
|
108
|
+
msg[:contract] = persistent[:depth][con_id][:contract]
|
109
|
+
persistent[:depth][con_id][:buffer] << msg.data.slice(*%i[ contract position operation side price size ])
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler'
|
4
|
+
Bundler.require
|
5
|
+
require_relative './cotcube-dataproxy/3rd_clients'
|
6
|
+
require_relative './cotcube-dataproxy/init'
|
7
|
+
require_relative './cotcube-dataproxy/client_response'
|
8
|
+
require_relative './cotcube-dataproxy/subscribers'
|
9
|
+
require_relative './cotcube-dataproxy/commserver'
|
10
|
+
require_relative './cotcube-dataproxy/gc'
|
11
|
+
|
12
|
+
SECRETS_DEFAULT = {
|
13
|
+
'dataproxy_mq_proto' => 'http',
|
14
|
+
'dataproxy_mq_user' => 'guest',
|
15
|
+
'dataproxy_mq_password' => 'guest',
|
16
|
+
'dataproxy_mq_host' => 'localhost',
|
17
|
+
'dataproxy_mq_port' => '15672',
|
18
|
+
'dataproxy_mq_vhost' => '%2F'
|
19
|
+
}
|
20
|
+
|
21
|
+
# Load a yaml file containing actual parameter and merge those with current
|
22
|
+
# TODO use better config file location
|
23
|
+
SECRETS = SECRETS_DEFAULT.merge( -> {YAML.load(File.read("#{__FILE__.split('/')[..-2].join('/')}/../secrets.yml")) rescue {} }.call)
|
24
|
+
|
25
|
+
CANCEL_TYPES = {
|
26
|
+
ticks: :CancelMarketData,
|
27
|
+
depth: :CancelMarketDepth,
|
28
|
+
realtimebars: :CancelRealTimeBars
|
29
|
+
}
|
30
|
+
|
31
|
+
REQUEST_TYPES = {
|
32
|
+
ticks: :RequestMarketData,
|
33
|
+
depth: :RequestMarketDepth,
|
34
|
+
realtimebars: :RequestRealTimeBars
|
35
|
+
}
|
36
|
+
|
metadata
ADDED
@@ -0,0 +1,229 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cotcube-dataproxy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Benjamin L. Tischendorf
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-11-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: ib-api
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '972'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '972'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: colorize
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.8'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.8'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: cotcube-helpers
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.2.1
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.2.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: outputhandler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.2'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.2'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: bunny
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: httparty~> 0.20
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: yaml
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0.1'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0.1'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: json
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '2'
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '2'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rake
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '13'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '13'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rspec
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '3.6'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '3.6'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: yard
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - "~>"
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0.9'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - "~>"
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0.9'
|
181
|
+
description: An AMQ based proxy to retrieve price and contract data from IBKR TWS.
|
182
|
+
The AMQ is provided by rabbitMQ with the gem Bunny, the API to TWS/ibGateway with
|
183
|
+
the gem ib-api
|
184
|
+
email:
|
185
|
+
- donkeybridge@jtown.eu
|
186
|
+
executables: []
|
187
|
+
extensions: []
|
188
|
+
extra_rdoc_files: []
|
189
|
+
files:
|
190
|
+
- CHANGELOG.md
|
191
|
+
- Gemfile
|
192
|
+
- VERSION
|
193
|
+
- bin/dataproxy
|
194
|
+
- bin/test_client.rb
|
195
|
+
- cotcube-dataproxy.gemspec
|
196
|
+
- lib/cotcube-dataproxy.rb
|
197
|
+
- lib/cotcube-dataproxy/3rd_clients.rb
|
198
|
+
- lib/cotcube-dataproxy/client_response.rb
|
199
|
+
- lib/cotcube-dataproxy/commserver.rb
|
200
|
+
- lib/cotcube-dataproxy/gc.rb
|
201
|
+
- lib/cotcube-dataproxy/init.rb
|
202
|
+
- lib/cotcube-dataproxy/subscribers.rb
|
203
|
+
homepage: https://github.com/donkeybridge/cotcube-dataproxy
|
204
|
+
licenses:
|
205
|
+
- BSD-3-Clause
|
206
|
+
metadata:
|
207
|
+
homepage_uri: https://github.com/donkeybridge/cotcube-dataproxy
|
208
|
+
source_code_uri: https://github.com/donkeybridge/cotcube-dataproxy
|
209
|
+
changelog_uri: https://github.com/donkeybridge/cotcube-dataproxy/CHANGELOG.md
|
210
|
+
post_install_message:
|
211
|
+
rdoc_options: []
|
212
|
+
require_paths:
|
213
|
+
- lib
|
214
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
215
|
+
requirements:
|
216
|
+
- - "~>"
|
217
|
+
- !ruby/object:Gem::Version
|
218
|
+
version: '2.7'
|
219
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
220
|
+
requirements:
|
221
|
+
- - ">="
|
222
|
+
- !ruby/object:Gem::Version
|
223
|
+
version: '0'
|
224
|
+
requirements: []
|
225
|
+
rubygems_version: 3.1.6
|
226
|
+
signing_key:
|
227
|
+
specification_version: 4
|
228
|
+
summary: An AMQ based proxy to retrieve price and contract data from IBKR TWS.
|
229
|
+
test_files: []
|