cotcube-helpers 0.2.1.1 → 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -1
- data/Gemfile +1 -0
- data/VERSION +1 -1
- data/cotcube-helpers.gemspec +1 -1
- data/lib/cotcube-helpers/cache_client.rb +52 -0
- data/lib/cotcube-helpers/constants.rb +2 -0
- data/lib/cotcube-helpers/data_client.rb +139 -86
- data/lib/cotcube-helpers/deep_decode_datetime.rb +25 -0
- data/lib/cotcube-helpers/expiration.rb +31 -0
- data/lib/cotcube-helpers/hash_ext.rb +5 -5
- data/lib/cotcube-helpers/init.rb +1 -1
- data/lib/cotcube-helpers/josch_client.rb +115 -0
- data/lib/cotcube-helpers/order_client.rb +131 -0
- data/lib/cotcube-helpers/orderclient.rb +203 -0
- data/lib/cotcube-helpers.rb +5 -1
- data/scripts/collect_market_depth +103 -0
- data/scripts/cron_ruby_wrapper.sh +2 -1
- metadata +11 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1d08ed5fa831283b0934d2ce7f86f9da0f1be5430d0d3164ee503b0aa7bac74f
|
4
|
+
data.tar.gz: ac59b5558432299e6906c8a93b4ddab8b7a57a7cac4b2c11239a8f41a8c05f4e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8b3ce7871cbc2068523994caa155a2291512c0cce8c72398f51de275e56c6e1a75a94402327118f27ae0376ccbf70b148be3fb17d0f66326e019d21a639ae485
|
7
|
+
data.tar.gz: e17fe32e700a3441e5e54a4b521c0c3d20acedbfb45606cd8f3da6edb5041a7cbcab522729da6d9d0e784f739730972af1ad1210b5542e06cd879f70587d7b1e
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,36 @@
|
|
1
|
+
## 0.2.3 (December 30, 2021)
|
2
|
+
- merging conflict
|
3
|
+
- added bare josch_ and order_client s
|
4
|
+
- added bare js_ and order_client s
|
5
|
+
|
6
|
+
## 0.2.2.5 (December 23, 2021)
|
7
|
+
- hash_ext: reworked keys_to_sym!
|
8
|
+
- minor changes
|
9
|
+
- deep_decode_datetime: added helper for conversion of timestrings in nested arrays and hashed
|
10
|
+
- cache_client: rewritten as class
|
11
|
+
- gemspec: raised activesupport to version 7
|
12
|
+
|
13
|
+
## 0.2.2.4 (December 07, 2021)
|
14
|
+
- introduced cache_client as client to readcache
|
15
|
+
|
16
|
+
## 0.2.2.3 (November 28, 2021)
|
17
|
+
- data_client: fixing troublesome output in .command
|
18
|
+
|
19
|
+
## 0.2.2.2 (November 28, 2021)
|
20
|
+
- dataclient fixing = for false ==
|
21
|
+
- solving merge conflicts
|
22
|
+
- Bump version to 0.2.2.
|
23
|
+
|
24
|
+
## 0.2.2.1 (November 28, 2021)
|
25
|
+
- Bump version to 0.2.2.1
|
26
|
+
|
27
|
+
## 0.2.2 (November 13, 2021)
|
28
|
+
- some further improvements to DataClient
|
29
|
+
- some fixes related to ib_contracts
|
30
|
+
- some fixes related to DataClient
|
31
|
+
|
1
32
|
## 0.2.1.1 (November 10, 2021)
|
2
33
|
- Bump version to 0.2.1.
|
3
|
-
- Bump version to 0.2.1.
|
4
34
|
|
5
35
|
## 0.2.1 (November 10, 2021)
|
6
36
|
- added new class 'dataclient' for communication with dataproxy
|
data/Gemfile
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.2.
|
1
|
+
0.2.3
|
data/cotcube-helpers.gemspec
CHANGED
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
|
|
26
26
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
27
27
|
spec.require_paths = ['lib']
|
28
28
|
|
29
|
-
spec.add_dependency 'activesupport', '~>
|
29
|
+
spec.add_dependency 'activesupport', '~> 7'
|
30
30
|
spec.add_dependency 'colorize', '~> 0.8'
|
31
31
|
|
32
32
|
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Cotcube
|
5
|
+
module Helpers
|
6
|
+
class CacheClient
|
7
|
+
|
8
|
+
def initialize(query='keys', timezone: Cotcube::Helpers::CHICAGO, debug: false, deflate: false, update: false)
|
9
|
+
raise ArgumentError, "Query must not be empty." if [ nil, '' ].include? query
|
10
|
+
raise ArgumentError, "Query '#{query}' is garbage." if query.split('/').size > 2 or not query.match? /\A[a-zA-Z0-9?=\/]+\Z/
|
11
|
+
@update = update ? '?update=true' : ''
|
12
|
+
@request_headers = {}
|
13
|
+
@request_headers['Accept-Encoding'] = 'deflate' if deflate
|
14
|
+
@query = query
|
15
|
+
@result = JSON.parse(HTTParty.get("http://100.100.0.14:8081/#{query}#{@update}").body, headers: @request_headers, symbolize_names: true) rescue { error: 1, msg: "Could not parse response for query '#{query}'." }
|
16
|
+
retry_once if has_errors?
|
17
|
+
end
|
18
|
+
|
19
|
+
def retry_once
|
20
|
+
sleep 2
|
21
|
+
raw = HTTParty.get("http://100.100.0.14:8081/#{query}#{update}")
|
22
|
+
@result = JSON.parse(raw.body, symbolize_names: true) rescue { error: 1, msg: "Could not parse response for query '#{query}'." }
|
23
|
+
if has_errors?
|
24
|
+
puts "ERROR in parsing response: #{raw[..300]}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def has_errors?
|
29
|
+
result[:error].nil? or result[:error] > 0
|
30
|
+
end
|
31
|
+
|
32
|
+
def warnings
|
33
|
+
result[:warnings]
|
34
|
+
end
|
35
|
+
|
36
|
+
def payload
|
37
|
+
has_errors? ? false : @result[:payload]
|
38
|
+
end
|
39
|
+
|
40
|
+
def entity
|
41
|
+
query.split('/').first
|
42
|
+
end
|
43
|
+
|
44
|
+
def asset
|
45
|
+
entity, asset = query.split('/')
|
46
|
+
asset
|
47
|
+
end
|
48
|
+
|
49
|
+
attr_reader :query, :result, :update
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -1,160 +1,213 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
2
4
|
require 'bunny'
|
3
5
|
require 'json'
|
4
6
|
|
5
7
|
module Cotcube
|
6
8
|
module Helpers
|
7
9
|
class DataClient
|
10
|
+
SECRETS_DEFAULT = {
|
11
|
+
'dataproxy_mq_proto' => 'http',
|
12
|
+
'dataproxy_mq_user' => 'guest',
|
13
|
+
'dataproxy_mq_password' => 'guest',
|
14
|
+
'dataproxy_mq_host' => 'localhost',
|
15
|
+
'dataproxy_mq_port' => '15672',
|
16
|
+
'dataproxy_mq_vhost' => '%2F'
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
SECRETS = SECRETS_DEFAULT.merge(
|
20
|
+
lambda {
|
21
|
+
begin
|
22
|
+
YAML.safe_load(File.read(Cotcube::Helpers.init[:secrets_file]))
|
23
|
+
rescue StandardError
|
24
|
+
{}
|
25
|
+
end
|
26
|
+
}.call
|
27
|
+
)
|
8
28
|
|
9
29
|
def initialize
|
10
|
-
@connection = Bunny.new(
|
30
|
+
@connection = Bunny.new(user: SECRETS['dataproxy_mq_user'],
|
31
|
+
password: SECRETS['dataproxy_mq_password'],
|
32
|
+
vhost: SECRETS['dataproxy_mq_vhost'])
|
11
33
|
@connection.start
|
12
34
|
|
13
|
-
@
|
14
|
-
@exchange
|
15
|
-
@requests
|
35
|
+
@commands = connection.create_channel
|
36
|
+
@exchange = commands.direct('dataproxy_commands')
|
37
|
+
@requests = {}
|
16
38
|
@persistent = { depth: {}, realtimebars: {}, ticks: {} }
|
17
|
-
@
|
18
|
-
|
39
|
+
@debug = false
|
19
40
|
setup_reply_queue
|
20
41
|
end
|
21
42
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
"\t client.stop_realtimebars(request_id: )\n"\
|
28
|
-
"\trequest_id = client.start_ticks(contract: )\n"\
|
29
|
-
"\t client.stop_ticks(request_id: )\n"
|
30
|
-
end
|
31
|
-
|
32
|
-
def send_command(command, timeout: 5)
|
43
|
+
# command acts a synchronizer: it sends the command and waits for the response
|
44
|
+
# otherwise times out --- the counterpart here is the subscription within
|
45
|
+
# setup_reply_queue
|
46
|
+
#
|
47
|
+
def command(command, timeout: 10)
|
33
48
|
command = { command: command.to_s } unless command.is_a? Hash
|
34
49
|
command[:timestamp] ||= (Time.now.to_f * 1000).to_i
|
35
50
|
request_id = Digest::SHA256.hexdigest(command.to_json)[..6]
|
36
|
-
requests[request_id] = {
|
51
|
+
requests[request_id] = {
|
52
|
+
request: command,
|
53
|
+
id: request_id,
|
54
|
+
lock: Mutex.new,
|
55
|
+
condition: ConditionVariable.new
|
56
|
+
}
|
37
57
|
|
38
58
|
exchange.publish(command.to_json,
|
39
|
-
content_type: 'application/json',
|
40
59
|
routing_key: 'dataproxy_commands',
|
41
60
|
correlation_id: request_id,
|
42
61
|
reply_to: reply_queue.name)
|
43
62
|
|
44
63
|
# wait for the signal to continue the execution
|
45
|
-
|
46
|
-
|
47
|
-
|
64
|
+
#
|
65
|
+
requests[request_id][:lock].synchronize do
|
66
|
+
requests[request_id][:condition].wait(requests[request_id][:lock], timeout)
|
67
|
+
end
|
48
68
|
|
69
|
+
# if we reached timeout, we will return nil, just for explicity
|
70
|
+
response = requests[request_id][:response].dup
|
71
|
+
requests.delete(request_id)
|
49
72
|
response
|
50
73
|
end
|
51
74
|
|
75
|
+
alias_method :send_command, :command
|
76
|
+
|
52
77
|
def stop
|
53
|
-
|
78
|
+
%i[depth ticks realtimebars].each do |type|
|
79
|
+
persistent[type].each do |local_key, obj|
|
80
|
+
puts "Cancelling #{local_key}"
|
81
|
+
obj[:subscription].cancel
|
82
|
+
end
|
83
|
+
end
|
84
|
+
commands.close
|
54
85
|
connection.close
|
55
86
|
end
|
56
87
|
|
57
|
-
|
58
|
-
|
59
|
-
send_command( { command: :get_contracts, symbol: symbol } )
|
88
|
+
def get_contracts(symbol:)
|
89
|
+
send_command({ command: :get_contracts, symbol: symbol })
|
60
90
|
end
|
61
91
|
|
62
92
|
def get_historical(contract:, interval:, duration: nil, before: nil, rth_only: false, based_on: :trades)
|
63
93
|
# rth.true? means data outside of rth is skipped
|
64
94
|
rth_only = rth_only ? 1 : 0
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
95
|
+
|
96
|
+
# interval most probably is given as ActiveSupport::Duration
|
97
|
+
if interval.is_a? ActiveSupport::Duration
|
98
|
+
interval = case interval
|
99
|
+
when 1; :sec1
|
100
|
+
when 5; :sec5
|
101
|
+
when 15; :sec15
|
102
|
+
when 30; :sec30
|
103
|
+
when 60; :min1
|
104
|
+
when 120; :min2
|
105
|
+
when 300; :min5
|
106
|
+
when 900; :min15
|
107
|
+
when 1800; :min30
|
108
|
+
when 3600; :hour1
|
109
|
+
when 86400; :day1
|
110
|
+
else; interval
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
default_durations = { sec1: '30_M', sec5: '2_H', sec15: '6_H', sec30: '12_H',
|
115
|
+
min1: '1_D', min2: '2_D', min5: '5_D', min15: '1_W',
|
116
|
+
min30: '1_W', hour1: '1_W', day1: '1_Y' }
|
117
|
+
|
118
|
+
unless default_durations.keys.include? interval
|
119
|
+
raise "Invalid interval '#{interval}', should be in '#{default_durations.keys}'."
|
120
|
+
end
|
121
|
+
|
79
122
|
# TODO: Check for valid duration specification
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
123
|
+
puts 'WARNING in get_historical: param :before ignored' unless before.nil?
|
124
|
+
duration ||= default_durations[interval]
|
125
|
+
send_command({
|
126
|
+
command: :historical,
|
127
|
+
contract: contract,
|
128
|
+
interval: interval,
|
129
|
+
duration: duration,
|
130
|
+
based_on: based_on.to_s.upcase,
|
131
|
+
rth_only: rth_only,
|
132
|
+
before: nil
|
133
|
+
}, timeout: 20)
|
90
134
|
end
|
91
135
|
|
92
|
-
def start_persistent(contract:, type: :realtimebars, &block)
|
93
|
-
unless %i[
|
136
|
+
def start_persistent(contract:, type: :realtimebars, local_id: 0, &block)
|
137
|
+
unless %i[depth ticks realtimebars].include? type.to_sym
|
94
138
|
puts "ERROR: Inappropriate type in stop_realtimebars with #{type}"
|
95
139
|
return false
|
96
140
|
end
|
97
141
|
|
98
|
-
|
99
|
-
|
100
|
-
|
142
|
+
local_key = "#{contract}_#{local_id}"
|
143
|
+
|
144
|
+
channel = connection.create_channel
|
145
|
+
exchange = channel.fanout("dataproxy_#{type}_#{contract}")
|
146
|
+
queue = channel.queue('', exclusive: true, auto_delete: true)
|
101
147
|
queue.bind(exchange)
|
102
|
-
|
103
|
-
|
148
|
+
|
149
|
+
ib_contract = Cotcube::Helpers.get_ib_contract(contract)
|
150
|
+
|
151
|
+
command = { command: type, contract: contract, con_id: ib_contract[:con_id],
|
152
|
+
delivery: queue.name, exchange: exchange.name }
|
153
|
+
|
154
|
+
block ||= ->(bar) { puts bar.to_s }
|
155
|
+
|
156
|
+
subscription = queue.subscribe do |_delivery_info, _properties, payload|
|
104
157
|
block.call(JSON.parse(payload, symbolize_names: true))
|
105
158
|
end
|
106
|
-
|
107
|
-
persistent[type][queue
|
108
|
-
persistent[type][
|
159
|
+
persistent[type][local_key] = command.dup
|
160
|
+
persistent[type][local_key][:queue] = queue
|
161
|
+
persistent[type][local_key][:subscription] = subscription
|
162
|
+
persistent[type][local_key][:channel] = channel
|
109
163
|
send_command(command)
|
110
164
|
end
|
111
165
|
|
112
|
-
def stop_persistent(contract:, type: :realtimebars
|
113
|
-
unless %i[
|
166
|
+
def stop_persistent(contract:, local_id: 0, type: :realtimebars)
|
167
|
+
unless %i[depth ticks realtimebars].include? type.to_sym
|
114
168
|
puts "ERROR: Inappropriate type in stop_realtimebars with #{type}"
|
115
169
|
return false
|
116
170
|
end
|
117
|
-
|
118
|
-
|
119
|
-
|
171
|
+
local_key = "#{contract}_#{local_id}"
|
172
|
+
p persistent[type][local_key][:subscription].cancel
|
173
|
+
p persistent[type][local_key][:channel].close
|
174
|
+
persistent[type].delete(local_key)
|
120
175
|
end
|
121
176
|
|
122
177
|
attr_accessor :response
|
123
178
|
attr_reader :lock, :condition
|
124
179
|
|
125
180
|
private
|
126
|
-
attr_reader :call_id, :connection, :requests, :persistent,
|
127
|
-
:channel, :server_queue_name, :reply_queue, :exchange
|
128
181
|
|
182
|
+
attr_reader :call_id, :connection, :requests, :persistent,
|
183
|
+
:commands, :server_queue_name, :reply_queue, :exchange
|
129
184
|
|
130
185
|
def setup_reply_queue
|
131
|
-
@
|
132
|
-
@
|
133
|
-
that = self
|
134
|
-
@reply_queue = channel.queue('', exclusive: true, auto_delete: true)
|
135
|
-
@reply_queue.bind(channel.exchange('dataproxy_replies', auto_delete: true), routing_key: @reply_queue.name)
|
186
|
+
@reply_queue = commands.queue('', exclusive: true, auto_delete: true)
|
187
|
+
@reply_queue.bind(commands.exchange('dataproxy_replies'), routing_key: @reply_queue.name)
|
136
188
|
|
137
189
|
reply_queue.subscribe do |delivery_info, properties, payload|
|
138
|
-
|
139
|
-
__id__ = properties[:correlation_id]
|
190
|
+
__id__ = properties[:correlation_id]
|
140
191
|
|
141
192
|
if __id__.nil?
|
142
|
-
puts "Received without __id__: #{delivery_info.map{|k,v| "#{k}\t#{v}"}.join("\n")
|
143
|
-
|
144
|
-
|
193
|
+
puts "Received without __id__: #{delivery_info.map { |k, v| "#{k}\t#{v}" }.join("\n")
|
194
|
+
}\n\n#{properties.map { |k, v| "#{k}\t#{v}" }.join("\n")
|
195
|
+
}\n\n#{JSON.parse(payload).map { |k, v| "#{k}\t#{v}" }.join("\n")}" if @debug
|
145
196
|
|
146
197
|
elsif requests[__id__].nil?
|
147
|
-
puts "Received non-matching response: \n\n#{
|
148
|
-
else
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
198
|
+
puts "Received non-matching response, maybe previously timed out: \n\n#{delivery_info}\n\n#{properties}\n\n#{payload}\n."[..620].scan(/.{1,120}/).join(' '*30 + "\n") if @debug
|
199
|
+
else
|
200
|
+
# save the payload and send the signal to continue the execution of #command
|
201
|
+
# need to rescue the rare case, where lock and condition are destroyed right in parallel by timeout
|
202
|
+
begin
|
203
|
+
puts "Received result for #{__id__}" if @debug
|
204
|
+
requests[__id__][:response] = payload
|
205
|
+
requests[__id__][:lock].synchronize { requests[__id__][:condition].signal }
|
206
|
+
rescue nil
|
207
|
+
end
|
154
208
|
end
|
155
209
|
end
|
156
210
|
end
|
157
|
-
|
158
211
|
end
|
159
212
|
end
|
160
213
|
end
|
@@ -174,4 +227,4 @@ begin
|
|
174
227
|
}
|
175
228
|
ensure
|
176
229
|
client.stop
|
177
|
-
|
230
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Cotcube
|
2
|
+
module Helpers
|
3
|
+
VALID_DATETIME_STRING = lambda {|str| str.is_a?(String) and [10,25,29].include?(str.length) and str.count("^0-9:TZ+-= ").zero? }
|
4
|
+
|
5
|
+
def deep_decode_datetime(data, zone: DateTime)
|
6
|
+
case data
|
7
|
+
when nil; nil
|
8
|
+
when VALID_DATETIME_STRING
|
9
|
+
res = nil
|
10
|
+
begin
|
11
|
+
res = zone.parse(data)
|
12
|
+
rescue ArgumentError
|
13
|
+
data
|
14
|
+
end
|
15
|
+
[ DateTime, ActiveSupport::TimeWithZone ].include?(res.class) ? res : data
|
16
|
+
when Array; data.map! { |d| deep_decode_datetime(d, zone: zone) }
|
17
|
+
when Hash; data.transform_values! { |v| deep_decode_datetime(v, zone: zone) }
|
18
|
+
else; data
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module_function :deep_decode_datetime
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Cotcube
|
2
|
+
module Helpers
|
3
|
+
|
4
|
+
class ExpirationMonth
|
5
|
+
attr_accessor *%i[ asset month year holidays stencil ]
|
6
|
+
def initialize( contract: )
|
7
|
+
a,b,c,d,e = contract.chars
|
8
|
+
@asset = [ a, b ].join
|
9
|
+
if %w[ GG DL BJ GE VI ]
|
10
|
+
puts "Denying to calculate expiration for #{@asset}".light_red
|
11
|
+
return
|
12
|
+
end
|
13
|
+
@month = MONTHS[c] + offset
|
14
|
+
@month -= 1 if %w[ CL HO NG RB SB].include? @asset
|
15
|
+
@month += 1 if %w[ ].include? @asset
|
16
|
+
@month += 12 if month < 1
|
17
|
+
@month -= 12 if month > 12
|
18
|
+
@year = [ d, e ].join.to_i
|
19
|
+
@year += year > 61 ? 1900 : 2000
|
20
|
+
@holidays = CSV.read("/var/cotcube/bardata/holidays.csv").map{|x| DateTime.parse(x[0]).to_date}.select{|x| x.year == @year }
|
21
|
+
@stencil = [ Date.new(@year, @month, 1) ]
|
22
|
+
end_date = Date.new(@year, @month + 1, 1 )
|
23
|
+
while (next_date = @stencil.last + 1) < end_date
|
24
|
+
@stencil << next_date
|
25
|
+
end
|
26
|
+
@stencil.reject!{|x| [0,6].include?(x.wday) or @holidays.include? x}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -2,15 +2,15 @@
|
|
2
2
|
|
3
3
|
# Monkey patching the Ruby Core class Hash
|
4
4
|
class Hash
|
5
|
-
def keys_to_sym
|
6
|
-
|
5
|
+
def keys_to_sym!
|
6
|
+
self.keys.each do |key|
|
7
7
|
case self[key].class.to_s
|
8
8
|
when 'Hash'
|
9
|
-
self[key].keys_to_sym
|
9
|
+
self[key].keys_to_sym!
|
10
10
|
when 'Array'
|
11
|
-
self[key].map { |el| el.is_a?(Hash) ? el.keys_to_sym : el }
|
11
|
+
self[key].map { |el| el.is_a?(Hash) ? el.keys_to_sym! : el }
|
12
12
|
end
|
13
|
-
self[key.to_sym] = delete(key)
|
13
|
+
self["#{key}".to_sym] = delete(key)
|
14
14
|
end
|
15
15
|
self
|
16
16
|
end
|
data/lib/cotcube-helpers/init.rb
CHANGED
@@ -0,0 +1,115 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bunny'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module Cotcube
|
8
|
+
module Helpers
|
9
|
+
class JoSchClient
|
10
|
+
SECRETS_DEFAULT = {
|
11
|
+
'josch_mq_proto' => 'http',
|
12
|
+
'josch_mq_user' => 'guest',
|
13
|
+
'josch_mq_password' => 'guest',
|
14
|
+
'josch_mq_host' => 'localhost',
|
15
|
+
'josch_mq_port' => '15672',
|
16
|
+
'josch_mq_vhost' => '%2F'
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
SECRETS = SECRETS_DEFAULT.merge(
|
20
|
+
lambda {
|
21
|
+
begin
|
22
|
+
YAML.safe_load(File.read(Cotcube::Helpers.init[:secrets_file]))
|
23
|
+
rescue StandardError
|
24
|
+
{}
|
25
|
+
end
|
26
|
+
}.call
|
27
|
+
)
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
@connection = Bunny.new(user: SECRETS['josch_mq_user'],
|
31
|
+
password: SECRETS['josch_mq_password'],
|
32
|
+
vhost: SECRETS['josch_mq_vhost'])
|
33
|
+
@connection.start
|
34
|
+
|
35
|
+
@commands = connection.create_channel
|
36
|
+
@exchange = commands.direct('josch_commands')
|
37
|
+
@requests = {}
|
38
|
+
@debug = false
|
39
|
+
setup_reply_queue
|
40
|
+
end
|
41
|
+
|
42
|
+
# command acts a synchronizer: it sends the command and waits for the response
|
43
|
+
# otherwise times out --- the counterpart here is the subscription within
|
44
|
+
# setup_reply_queue
|
45
|
+
#
|
46
|
+
def command(command, timeout: 10)
|
47
|
+
command = { command: command.to_s } unless command.is_a? Hash
|
48
|
+
command[:timestamp] ||= (Time.now.to_f * 1000).to_i
|
49
|
+
request_id = Digest::SHA256.hexdigest(command.to_json)[..6]
|
50
|
+
requests[request_id] = {
|
51
|
+
request: command,
|
52
|
+
id: request_id,
|
53
|
+
lock: Mutex.new,
|
54
|
+
condition: ConditionVariable.new
|
55
|
+
}
|
56
|
+
|
57
|
+
exchange.publish(command.to_json,
|
58
|
+
routing_key: 'josch_commands',
|
59
|
+
correlation_id: request_id,
|
60
|
+
reply_to: reply_queue.name)
|
61
|
+
|
62
|
+
# wait for the signal to continue the execution
|
63
|
+
#
|
64
|
+
requests[request_id][:lock].synchronize do
|
65
|
+
requests[request_id][:condition].wait(requests[request_id][:lock], timeout)
|
66
|
+
end
|
67
|
+
|
68
|
+
# if we reached timeout, we will return nil, just for explicity
|
69
|
+
response = requests[request_id][:response].dup
|
70
|
+
requests.delete(request_id)
|
71
|
+
response
|
72
|
+
end
|
73
|
+
|
74
|
+
alias_method :send_command, :command
|
75
|
+
|
76
|
+
attr_accessor :response
|
77
|
+
attr_reader :lock, :condition
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
attr_reader :call_id, :connection, :requests, :persistent,
|
82
|
+
:commands, :server_queue_name, :reply_queue, :exchange
|
83
|
+
|
84
|
+
def setup_reply_queue
|
85
|
+
@reply_queue = commands.queue('', exclusive: true, auto_delete: true)
|
86
|
+
@reply_queue.bind(commands.exchange('josch_replies'), routing_key: @reply_queue.name)
|
87
|
+
|
88
|
+
reply_queue.subscribe do |delivery_info, properties, payload|
|
89
|
+
__id__ = properties[:correlation_id]
|
90
|
+
|
91
|
+
if __id__.nil?
|
92
|
+
puts "Received without __id__: #{delivery_info.map { |k, v| "#{k}\t#{v}" }.join("\n")
|
93
|
+
}\n\n#{properties.map { |k, v| "#{k}\t#{v}" }.join("\n")
|
94
|
+
}\n\n#{JSON.parse(payload).map { |k, v| "#{k}\t#{v}" }.join("\n")}" if @debug
|
95
|
+
|
96
|
+
elsif requests[__id__].nil?
|
97
|
+
puts "Received non-matching response, maybe previously timed out: \n\n#{delivery_info}\n\n#{properties}\n\n#{payload}\n."[..620].scan(/.{1,120}/).join(' '*30 + "\n") if @debug
|
98
|
+
else
|
99
|
+
# save the payload and send the signal to continue the execution of #command
|
100
|
+
# need to rescue the rare case, where lock and condition are destroyed right in parallel by timeout
|
101
|
+
begin
|
102
|
+
puts "Received result for #{__id__}" if @debug
|
103
|
+
requests[__id__][:response] = payload
|
104
|
+
requests[__id__][:lock].synchronize { requests[__id__][:condition].signal }
|
105
|
+
rescue nil
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
JoschClient = JoSchClient
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
__END__
|
@@ -0,0 +1,131 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bunny'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
module Cotcube
|
8
|
+
module Helpers
|
9
|
+
class OrderClient
|
10
|
+
SECRETS_DEFAULT = {
|
11
|
+
'orderproxy_mq_proto' => 'http',
|
12
|
+
'orderproxy_mq_user' => 'guest',
|
13
|
+
'orderproxy_mq_password' => 'guest',
|
14
|
+
'orderproxy_mq_host' => 'localhost',
|
15
|
+
'orderproxy_mq_port' => '15672',
|
16
|
+
'orderproxy_mq_vhost' => '%2F'
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
SECRETS = SECRETS_DEFAULT.merge(
|
20
|
+
lambda {
|
21
|
+
begin
|
22
|
+
YAML.safe_load(File.read(Cotcube::Helpers.init[:secrets_file]))
|
23
|
+
rescue StandardError
|
24
|
+
{}
|
25
|
+
end
|
26
|
+
}.call
|
27
|
+
)
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
@connection = Bunny.new(user: SECRETS['orderproxy_mq_user'],
|
31
|
+
password: SECRETS['orderproxy_mq_password'],
|
32
|
+
vhost: SECRETS['orderproxy_mq_vhost'])
|
33
|
+
@connection.start
|
34
|
+
|
35
|
+
@commands = connection.create_channel
|
36
|
+
@exchange = commands.direct('orderproxy_commands')
|
37
|
+
@requests = {}
|
38
|
+
@persistent = { depth: {}, realtimebars: {}, ticks: {} }
|
39
|
+
@debug = false
|
40
|
+
setup_reply_queue
|
41
|
+
end
|
42
|
+
|
43
|
+
# command acts a synchronizer: it sends the command and waits for the response
|
44
|
+
# otherwise times out --- the counterpart here is the subscription within
|
45
|
+
# setup_reply_queue
|
46
|
+
#
|
47
|
+
def command(command, timeout: 10)
|
48
|
+
command = { command: command.to_s } unless command.is_a? Hash
|
49
|
+
command[:timestamp] ||= (Time.now.to_f * 1000).to_i
|
50
|
+
request_id = Digest::SHA256.hexdigest(command.to_json)[..6]
|
51
|
+
requests[request_id] = {
|
52
|
+
request: command,
|
53
|
+
id: request_id,
|
54
|
+
lock: Mutex.new,
|
55
|
+
condition: ConditionVariable.new
|
56
|
+
}
|
57
|
+
|
58
|
+
exchange.publish(command.to_json,
|
59
|
+
routing_key: 'orderproxy_commands',
|
60
|
+
correlation_id: request_id,
|
61
|
+
reply_to: reply_queue.name)
|
62
|
+
|
63
|
+
# wait for the signal to continue the execution
|
64
|
+
#
|
65
|
+
requests[request_id][:lock].synchronize do
|
66
|
+
requests[request_id][:condition].wait(requests[request_id][:lock], timeout)
|
67
|
+
end
|
68
|
+
|
69
|
+
# if we reached timeout, we will return nil, just for explicity
|
70
|
+
response = requests[request_id][:response].dup
|
71
|
+
requests.delete(request_id)
|
72
|
+
response
|
73
|
+
end
|
74
|
+
|
75
|
+
alias_method :send_command, :command
|
76
|
+
|
77
|
+
def stop
|
78
|
+
commands.close
|
79
|
+
connection.close
|
80
|
+
end
|
81
|
+
|
82
|
+
def get_contracts(symbol:)
|
83
|
+
send_command({ command: :get_contracts, symbol: symbol })
|
84
|
+
end
|
85
|
+
|
86
|
+
attr_accessor :response
|
87
|
+
attr_reader :lock, :condition
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
attr_reader :call_id, :connection, :requests, :persistent,
|
92
|
+
:commands, :server_queue_name, :reply_queue, :exchange
|
93
|
+
|
94
|
+
def setup_reply_queue
|
95
|
+
@reply_queue = commands.queue('', exclusive: true, auto_delete: true)
|
96
|
+
@reply_queue.bind(commands.exchange('orderproxy_replies'), routing_key: @reply_queue.name)
|
97
|
+
|
98
|
+
reply_queue.subscribe do |delivery_info, properties, payload|
|
99
|
+
__id__ = properties[:correlation_id]
|
100
|
+
|
101
|
+
if __id__.nil?
|
102
|
+
puts "Received without __id__: #{delivery_info.map { |k, v| "#{k}\t#{v}" }.join("\n")
|
103
|
+
}\n\n#{properties.map { |k, v| "#{k}\t#{v}" }.join("\n")
|
104
|
+
}\n\n#{JSON.parse(payload).map { |k, v| "#{k}\t#{v}" }.join("\n")}" if @debug
|
105
|
+
|
106
|
+
elsif requests[__id__].nil?
|
107
|
+
puts "Received non-matching response, maybe previously timed out: \n\n#{delivery_info}\n\n#{properties}\n\n#{payload}\n."[..620].scan(/.{1,120}/).join(' '*30 + "\n") if @debug
|
108
|
+
else
|
109
|
+
# save the payload and send the signal to continue the execution of #command
|
110
|
+
# need to rescue the rare case, where lock and condition are destroyed right in parallel by timeout
|
111
|
+
begin
|
112
|
+
puts "Received result for #{__id__}" if @debug
|
113
|
+
requests[__id__][:response] = payload
|
114
|
+
requests[__id__][:lock].synchronize { requests[__id__][:condition].signal }
|
115
|
+
rescue nil
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
__END__
|
125
|
+
begin
|
126
|
+
client = OrderClient.new
|
127
|
+
reply = client.send_command( { command: 'ping' } )
|
128
|
+
puts reply.nil? ? 'nil' : JSON.parse(reply)
|
129
|
+
ensure
|
130
|
+
client.stop
|
131
|
+
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
4
|
+
|
5
|
+
module Cotcube
|
6
|
+
module Helpers
|
7
|
+
# A proxyclient is a wrapper that allows communication with cotcube-orderproxy and cotcube-dataproxy. It fulfills
|
8
|
+
# registration and provides the opportunity to implement the logic to respond do events.
|
9
|
+
# (orderproxy and dataproxy are separate gems creating a layer between tws/ib-ruby and cotcube-based
|
10
|
+
# applications)
|
11
|
+
#
|
12
|
+
# NOTE: Whats here is a provisionally version
|
13
|
+
#
|
14
|
+
class DataClient # rubocop:disable Metrics/ClassLength
|
15
|
+
attr_reader :power, :ticksize, :multiplier, :average, :account
|
16
|
+
|
17
|
+
# The constructor takes a lot of arguments:
|
18
|
+
def initialize(
|
19
|
+
debug: false,
|
20
|
+
contract: ,
|
21
|
+
serverport: 24001,
|
22
|
+
serveraddr: '127.0.0.1',
|
23
|
+
client:,
|
24
|
+
bars: true,
|
25
|
+
ticks: false,
|
26
|
+
bar_size: 5,
|
27
|
+
spawn_timeout: 15
|
28
|
+
)
|
29
|
+
require 'json' unless Hash.new.respond_to? :to_json
|
30
|
+
require 'socket' unless defined? TCPSocket
|
31
|
+
|
32
|
+
puts 'PROXYCLIENT: Debug enabled' if @debug
|
33
|
+
|
34
|
+
@contract = contract.upcase
|
35
|
+
%w[debug serverport serveraddr client bars ticks bar_size].each {|var| eval("@#{var} = #{var}")}
|
36
|
+
|
37
|
+
@position = 0
|
38
|
+
@account = 0
|
39
|
+
@average = 0
|
40
|
+
|
41
|
+
exit_on_startup(':client must be in range 24001..24999') if @client.nil? || (@client / 1000 != 24) || (@client == 24_000)
|
42
|
+
|
43
|
+
res = send_command({ command: 'register', contract: @contract, date: @date,
|
44
|
+
ticks: @ticks, bars: @bars, bar_size: @bar_size })
|
45
|
+
|
46
|
+
# spawn_server has to be called separately after initialization.
|
47
|
+
print "Waiting #{spawn_timeout} seconds on server_thread to spawn..."
|
48
|
+
Thread.new do
|
49
|
+
begin
|
50
|
+
Timeout.timeout(spawn_timeout) { sleep(0.1) while @server_thread.nil? }
|
51
|
+
rescue Timeout::Error
|
52
|
+
puts 'Could not get server_thread, has :spawn_server been called?'
|
53
|
+
shutdown
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
unless res['error'].zero?
|
58
|
+
exit_on_startup("Unable to register on orderproxy, exiting")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def exit_on_startup(msg = '')
|
63
|
+
puts "Cannot startup client, exiting during startup: '#{msg}'"
|
64
|
+
shutdown
|
65
|
+
defined?(::IRB) ? (raise) : (exit 1)
|
66
|
+
end
|
67
|
+
|
68
|
+
def send_command(req)
|
69
|
+
req[:client_id] = @client
|
70
|
+
res = nil
|
71
|
+
puts "Connecting to #{@serveraddr}:#{@serverport} to send '#{req}'." if @debug
|
72
|
+
|
73
|
+
TCPSocket.open(@serveraddr, @serverport) do |proxy|
|
74
|
+
proxy.puts(req.to_json)
|
75
|
+
raw = proxy.gets
|
76
|
+
begin
|
77
|
+
res = JSON.parse(raw)
|
78
|
+
rescue StandardError
|
79
|
+
puts 'Error while parsing response'
|
80
|
+
return false
|
81
|
+
end
|
82
|
+
if @debug
|
83
|
+
# rubocop:disable Style/FormatStringToken, Style/FormatString
|
84
|
+
res.each do |k, v|
|
85
|
+
case v
|
86
|
+
when Array
|
87
|
+
(v.size < 2) ? puts(format '%-15s', "#{k}:") : print(format '%-15s', "#{k}:")
|
88
|
+
v.each_with_index { |x, i| i.zero? ? (puts x) : (puts " #{x}") }
|
89
|
+
else
|
90
|
+
puts "#{format '%-15s', "#{k}:"}#{v}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
# rubocop:enable Style/FormatStringToken, Style/FormatString
|
94
|
+
end
|
95
|
+
puts "ERROR on command: #{res['msg']}" unless res['error'].nil? or res['error'].zero?
|
96
|
+
end
|
97
|
+
puts res.to_s if @debug
|
98
|
+
res
|
99
|
+
end
|
100
|
+
|
101
|
+
# #shutdown ends the @server_thread and --if :close is set-- closes the current position attached to the client
|
102
|
+
def shutdown(close: true)
|
103
|
+
return if @shutdown
|
104
|
+
@shutdown = true
|
105
|
+
|
106
|
+
if @position.abs.positive? && close
|
107
|
+
send_command({ command: 'order', action: 'create', type: 'market',
|
108
|
+
side: (@position.positive? ? 'sell' : 'buy'), size: @position.abs })
|
109
|
+
end
|
110
|
+
sleep 3
|
111
|
+
result = send_command({ command: 'unregister' })
|
112
|
+
puts "FINAL ACCOUNT: #{@account}"
|
113
|
+
result['executions']&.each do |x|
|
114
|
+
x.delete('msg_type')
|
115
|
+
puts x.to_s
|
116
|
+
end
|
117
|
+
@server_thread.exit if @server_thread.respond_to? :exit
|
118
|
+
end
|
119
|
+
|
120
|
+
def spawn_server(
|
121
|
+
execution_proc: nil,
|
122
|
+
orderstate_proc: nil,
|
123
|
+
tick_proc: nil,
|
124
|
+
depth_proc: nil,
|
125
|
+
order_proc: nil,
|
126
|
+
bars_proc: nil
|
127
|
+
) # rubocop:disable Metrics/MethodLength
|
128
|
+
|
129
|
+
%w[execution_proc orderstate_proc tick_proc depth_proc order_proc bars_proc].each {|var| eval("@#{var} = #{var}") }
|
130
|
+
|
131
|
+
if @bars
|
132
|
+
@bars_proc ||= lambda {|msg| puts msg.inspect }
|
133
|
+
end
|
134
|
+
|
135
|
+
if @ticks
|
136
|
+
@ticks_proc ||= lambda {|msg| puts msg.inspect }
|
137
|
+
end
|
138
|
+
|
139
|
+
if @shutdown
|
140
|
+
puts "Cannot spawn server on proxyclient that has been already shut down."
|
141
|
+
return
|
142
|
+
end
|
143
|
+
if @server_thread
|
144
|
+
puts "Cannot spawn server more than once."
|
145
|
+
return
|
146
|
+
end
|
147
|
+
|
148
|
+
@server_thread = Thread.new do # rubocop:disable Metrics/BlockLength
|
149
|
+
puts 'Spawning RECEIVER'
|
150
|
+
server = TCPServer.open(@serveraddr, @client)
|
151
|
+
loop do # rubocop:disable Metrics/BlockLength
|
152
|
+
Thread.start(server.accept) do |client| # rubocop:disable Metrics/BlockLength
|
153
|
+
while (response = client.gets)
|
154
|
+
response = JSON.parse(response)
|
155
|
+
|
156
|
+
case response['msg_type']
|
157
|
+
|
158
|
+
when 'alert'
|
159
|
+
case response['code']
|
160
|
+
when 2104
|
161
|
+
puts 'ALERT: data farm connection resumed __ignored__'.light_black
|
162
|
+
when 2108
|
163
|
+
puts 'ALERT: data farm connection suspended __ignored__'.light_black
|
164
|
+
when 2109
|
165
|
+
# Order Event Warning:Attribute 'Outside Regular Trading Hours' is ignored
|
166
|
+
# based on the order type and destination. PlaceOrder is now being processed.
|
167
|
+
puts 'ALERT: outside_rth __ignored__'.light_black
|
168
|
+
when 2100
|
169
|
+
puts 'ALERT: Account_info unsubscribed __ignored__'.light_black
|
170
|
+
when 202
|
171
|
+
puts 'ALERT: order cancelled'
|
172
|
+
else
|
173
|
+
puts '-------------ALERT------------------------------'
|
174
|
+
puts response.to_s
|
175
|
+
puts '------------------------------------------------'
|
176
|
+
end
|
177
|
+
|
178
|
+
when 'tick'
|
179
|
+
@tick_proc&.call(response)
|
180
|
+
|
181
|
+
when 'depth'
|
182
|
+
@depth_proc&.call(response)
|
183
|
+
|
184
|
+
when 'realtimebar'
|
185
|
+
@bars_proc&.call(response)
|
186
|
+
|
187
|
+
else
|
188
|
+
puts "ERROR: #{response}"
|
189
|
+
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
rescue StandardError => e
|
194
|
+
backtrace = e.backtrace.join("\r\n")
|
195
|
+
puts "======= ERROR: '#{e.class}', MESSAGE: '#{e.message}'\n#{backtrace}"
|
196
|
+
end
|
197
|
+
end
|
198
|
+
puts '@server_thread spawned'
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
data/lib/cotcube-helpers.rb
CHANGED
@@ -21,6 +21,7 @@ require_relative 'cotcube-helpers/subpattern'
|
|
21
21
|
require_relative 'cotcube-helpers/parallelize'
|
22
22
|
require_relative 'cotcube-helpers/simple_output'
|
23
23
|
require_relative 'cotcube-helpers/simple_series_stats'
|
24
|
+
require_relative 'cotcube-helpers/deep_decode_datetime'
|
24
25
|
require_relative 'cotcube-helpers/constants'
|
25
26
|
require_relative 'cotcube-helpers/input'
|
26
27
|
require_relative 'cotcube-helpers/output'
|
@@ -30,7 +31,6 @@ require_relative 'cotcube-helpers/init'
|
|
30
31
|
require_relative 'cotcube-helpers/get_id_set'
|
31
32
|
require_relative 'cotcube-helpers/ib_contracts'
|
32
33
|
require_relative 'cotcube-helpers/recognition'
|
33
|
-
require_relative 'cotcube-helpers/data_client'
|
34
34
|
|
35
35
|
module Cotcube
|
36
36
|
module Helpers
|
@@ -52,3 +52,7 @@ module Cotcube
|
|
52
52
|
# please not that module_functions of source provided in private files must be published there
|
53
53
|
end
|
54
54
|
end
|
55
|
+
|
56
|
+
%w[ data cache order josch ].each do |part|
|
57
|
+
require_relative "cotcube-helpers/#{part}_client"
|
58
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler'
|
4
|
+
Bundler.require
|
5
|
+
require 'parallel'
|
6
|
+
require_relative '../lib/cotcube-helpers'
|
7
|
+
|
8
|
+
dc = Cotcube::Helpers::DataClient.new
|
9
|
+
|
10
|
+
dc.start_persistent(type: :depth, contract: 'GCZ21') {|msg| p msg.size; p msg.map{|z| z[:size]}.reduce(&:+) }
|
11
|
+
|
12
|
+
loop do
|
13
|
+
sleep 1
|
14
|
+
end
|
15
|
+
|
16
|
+
__END__
|
17
|
+
depthThread = Thread.new do
|
18
|
+
begin
|
19
|
+
loop do
|
20
|
+
sleep 0.025 while depthQueue.empty?
|
21
|
+
while not depthQueue.empty?
|
22
|
+
msg = depthQueue.pop
|
23
|
+
if msg.data[:operation] == 2
|
24
|
+
data = [ (Time.now.to_f % 100000).round(3), :d, msg.data.values_at(:operation, :side, :position) ].flatten
|
25
|
+
else
|
26
|
+
data = [ (Time.now.to_f % 100000).round(3), :d, msg.data.values_at(:operation, :side, :position, :size, :price) ].flatten
|
27
|
+
end
|
28
|
+
puts "#{data}" if data[2]!=1 or data[4] == 0
|
29
|
+
next
|
30
|
+
writeQueue << data
|
31
|
+
side = msg.data[:side]
|
32
|
+
price = msg.data[:price]
|
33
|
+
size = msg.data[:size]
|
34
|
+
pos = msg.data[:position]
|
35
|
+
case msg.data[:operation]
|
36
|
+
when 0 # insert
|
37
|
+
orderbook[side].insert(pos, { price: price.to_f, size: size })
|
38
|
+
when 1 # update
|
39
|
+
orderbook[side][pos] = { price: price.to_f, size: size }
|
40
|
+
when 2 # remove
|
41
|
+
orderbook[side].delete_at(pos)
|
42
|
+
end
|
43
|
+
orderbook[1].shift while orderbook[1].size > DEPTH
|
44
|
+
orderbook[0].shift while orderbook[0].size > DEPTH
|
45
|
+
a = orderbook[0].size
|
46
|
+
a.times do |n|
|
47
|
+
s = a - 1
|
48
|
+
ask = orderbook[0][s-n]
|
49
|
+
next if ask.nil?
|
50
|
+
allasks = orderbook[0][0..s-n].map{|x| x[:size]}.reduce(:+)
|
51
|
+
asksacc = orderbook[0][0..s-n].map{|x| x[:size] * x[:price]}.reduce(:+) / allasks
|
52
|
+
puts "\t\t\t\t#{format % ask[:price]} x #{'% 5d' % ask[:size]}\t#{'% 4d' % allasks}\t#{format % asksacc}"
|
53
|
+
end
|
54
|
+
allasks = orderbook[0].compact.map{|x| x[:size]}.reduce(:+)
|
55
|
+
asksacc = orderbook[0].compact.map{|x| x[:size] * x[:price]}.reduce(:+) / allasks unless orderbook[0].empty?
|
56
|
+
allbids = orderbook[1].compact.map{|x| x[:size]}.reduce(:+)
|
57
|
+
bidsacc = orderbook[1].compact.map{|x| x[:size] * x[:price]}.reduce(:+) / allbids unless orderbook[0].empty?
|
58
|
+
puts "#{(format % bidsacc) unless bidsacc.nil?}\t".light_red +
|
59
|
+
"#{('% 4d' % allbids) unless allbids.nil?}\t\t\t\t\t".light_red +
|
60
|
+
"#{"#{'% 5d' % allasks}" unless allasks.nil?}\t".light_red +
|
61
|
+
"#{"#{format % asksacc}" unless asksacc.nil?}".light_red
|
62
|
+
b = orderbook[1].size
|
63
|
+
b.times do |n|
|
64
|
+
bid = orderbook[1][n]
|
65
|
+
next if bid.nil?
|
66
|
+
allbids = orderbook[1][0..n].map{|x| x[:size]}.reduce(:+)
|
67
|
+
bidsacc = orderbook[1][0..n].map{|x| x[:size] * x[:price]}.reduce(:+) / allbids
|
68
|
+
puts "#{format % bidsacc}\t#{'% 4d' % allbids}\t#{'%5d' % bid[:size]} x #{format % bid[:price]}"
|
69
|
+
end
|
70
|
+
puts "="*50
|
71
|
+
end
|
72
|
+
end
|
73
|
+
rescue
|
74
|
+
puts "RESCUE in depthThread".light_red
|
75
|
+
puts "#{orderbook}"
|
76
|
+
raise
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
sleep 0.01 while Time.now.to_i % WRITE_INTERVAL != 0
|
81
|
+
loop do
|
82
|
+
t = Time.now.to_f
|
83
|
+
unless writeQueue.empty?
|
84
|
+
data = []
|
85
|
+
data << writeQueue.pop while not writeQueue.empty?
|
86
|
+
CSV.open(OUTFILE, "a+") { |csv| data.each {|x| csv << x } }
|
87
|
+
end
|
88
|
+
begin
|
89
|
+
sleep WRITE_INTERVAL - (Time.now.to_f - t)
|
90
|
+
rescue
|
91
|
+
sleep 3
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
ensure
|
97
|
+
ib.send_message :CancelMarketDepth, id: ID
|
98
|
+
ib.send_message :CancelMarketData, id: ID
|
99
|
+
tickThread.kill
|
100
|
+
depthThread.kill
|
101
|
+
end
|
102
|
+
sleep 1
|
103
|
+
puts "Done."
|
@@ -1,6 +1,6 @@
|
|
1
1
|
#!/bin/bash
|
2
2
|
|
3
|
-
export rubyenv=/home/pepe/.rvm/environments/
|
3
|
+
export rubyenv=/home/pepe/.rvm/environments/ruby-2.7.5
|
4
4
|
|
5
5
|
. $rubyenv
|
6
6
|
cd /home/pepe/GEMS/${1}
|
@@ -10,6 +10,7 @@ ruby ${2} ${3} ${4} ${5} ${6}
|
|
10
10
|
|
11
11
|
|
12
12
|
exit
|
13
|
+
|
13
14
|
for testing run
|
14
15
|
env - `cat /home/pepe/bin/cron_ruby_wrapper.sh | tail -n 6` /bin/bash
|
15
16
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cotcube-helpers
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Benjamin L. Tischendorf
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-12-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '7'
|
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: '7'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: colorize
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -96,16 +96,22 @@ files:
|
|
96
96
|
- cotcube-helpers.gemspec
|
97
97
|
- lib/cotcube-helpers.rb
|
98
98
|
- lib/cotcube-helpers/array_ext.rb
|
99
|
+
- lib/cotcube-helpers/cache_client.rb
|
99
100
|
- lib/cotcube-helpers/constants.rb
|
100
101
|
- lib/cotcube-helpers/data_client.rb
|
101
102
|
- lib/cotcube-helpers/datetime_ext.rb
|
103
|
+
- lib/cotcube-helpers/deep_decode_datetime.rb
|
102
104
|
- lib/cotcube-helpers/enum_ext.rb
|
105
|
+
- lib/cotcube-helpers/expiration.rb
|
103
106
|
- lib/cotcube-helpers/get_id_set.rb
|
104
107
|
- lib/cotcube-helpers/hash_ext.rb
|
105
108
|
- lib/cotcube-helpers/ib_contracts.rb
|
106
109
|
- lib/cotcube-helpers/init.rb
|
107
110
|
- lib/cotcube-helpers/input.rb
|
111
|
+
- lib/cotcube-helpers/josch_client.rb
|
108
112
|
- lib/cotcube-helpers/numeric_ext.rb
|
113
|
+
- lib/cotcube-helpers/order_client.rb
|
114
|
+
- lib/cotcube-helpers/orderclient.rb
|
109
115
|
- lib/cotcube-helpers/output.rb
|
110
116
|
- lib/cotcube-helpers/parallelize.rb
|
111
117
|
- lib/cotcube-helpers/range_ext.rb
|
@@ -119,6 +125,7 @@ files:
|
|
119
125
|
- lib/cotcube-helpers/swig/fill_x.rb
|
120
126
|
- lib/cotcube-helpers/swig/recognition.rb
|
121
127
|
- lib/cotcube-helpers/symbols.rb
|
128
|
+
- scripts/collect_market_depth
|
122
129
|
- scripts/cron_ruby_wrapper.sh
|
123
130
|
- scripts/symbols
|
124
131
|
homepage: https://github.com/donkeybridge/cotcube-helpers
|