cotcube-helpers 0.2.1.1 → 0.2.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -1
- data/VERSION +1 -1
- data/lib/cotcube-helpers/data_client.rb +138 -85
- data/lib/cotcube-helpers/expiration.rb +31 -0
- data/lib/cotcube-helpers/orderclient.rb +203 -0
- data/lib/cotcube-helpers.rb +2 -1
- data/scripts/collect_market_depth +103 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 54c97a5746e0e03b971b9421be0636eb2c259beb90840ff34404f8049114019b
|
4
|
+
data.tar.gz: 6d565f00ed620b2458122ad686b1b6c60bb26e815f248474f746b38c10902dc6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f9a71ad40a61b1caf44162851d2c323c1b762501705459fbbca85d38c8a6bf66e261eb920093fa01f3ea6ddd07b48dbe7aa4f46e5010a39e0e5bb3045c9941d1
|
7
|
+
data.tar.gz: a862cb81820c8459a015788d273c1cd159078f11705f2c5f8db2847e355a6e5f6cf54b4532a43efef246804a1679053ca807b76c79c580881a46e3ea09fb0f01
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,21 @@
|
|
1
|
+
## 0.2.2.3 (November 28, 2021)
|
2
|
+
- data_client: fixing troublesome output in .command
|
3
|
+
|
4
|
+
## 0.2.2.2 (November 28, 2021)
|
5
|
+
- dataclient fixing = for false ==
|
6
|
+
- solving merge conflicts
|
7
|
+
- Bump version to 0.2.2.
|
8
|
+
|
9
|
+
## 0.2.2.1 (November 28, 2021)
|
10
|
+
- Bump version to 0.2.2.1
|
11
|
+
|
12
|
+
## 0.2.2 (November 13, 2021)
|
13
|
+
- some further improvements to DataClient
|
14
|
+
- some fixes related to ib_contracts
|
15
|
+
- some fixes related to DataClient
|
16
|
+
|
1
17
|
## 0.2.1.1 (November 10, 2021)
|
2
18
|
- Bump version to 0.2.1.
|
3
|
-
- Bump version to 0.2.1.
|
4
19
|
|
5
20
|
## 0.2.1 (November 10, 2021)
|
6
21
|
- added new class 'dataclient' for communication with dataproxy
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.2.
|
1
|
+
0.2.2.3
|
@@ -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: 5)
|
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
|
@@ -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
|
@@ -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
@@ -30,7 +30,6 @@ require_relative 'cotcube-helpers/init'
|
|
30
30
|
require_relative 'cotcube-helpers/get_id_set'
|
31
31
|
require_relative 'cotcube-helpers/ib_contracts'
|
32
32
|
require_relative 'cotcube-helpers/recognition'
|
33
|
-
require_relative 'cotcube-helpers/data_client'
|
34
33
|
|
35
34
|
module Cotcube
|
36
35
|
module Helpers
|
@@ -52,3 +51,5 @@ module Cotcube
|
|
52
51
|
# please not that module_functions of source provided in private files must be published there
|
53
52
|
end
|
54
53
|
end
|
54
|
+
|
55
|
+
require_relative 'cotcube-helpers/data_client'
|
@@ -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."
|
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.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-
|
11
|
+
date: 2021-11-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -100,12 +100,14 @@ files:
|
|
100
100
|
- lib/cotcube-helpers/data_client.rb
|
101
101
|
- lib/cotcube-helpers/datetime_ext.rb
|
102
102
|
- lib/cotcube-helpers/enum_ext.rb
|
103
|
+
- lib/cotcube-helpers/expiration.rb
|
103
104
|
- lib/cotcube-helpers/get_id_set.rb
|
104
105
|
- lib/cotcube-helpers/hash_ext.rb
|
105
106
|
- lib/cotcube-helpers/ib_contracts.rb
|
106
107
|
- lib/cotcube-helpers/init.rb
|
107
108
|
- lib/cotcube-helpers/input.rb
|
108
109
|
- lib/cotcube-helpers/numeric_ext.rb
|
110
|
+
- lib/cotcube-helpers/orderclient.rb
|
109
111
|
- lib/cotcube-helpers/output.rb
|
110
112
|
- lib/cotcube-helpers/parallelize.rb
|
111
113
|
- lib/cotcube-helpers/range_ext.rb
|
@@ -119,6 +121,7 @@ files:
|
|
119
121
|
- lib/cotcube-helpers/swig/fill_x.rb
|
120
122
|
- lib/cotcube-helpers/swig/recognition.rb
|
121
123
|
- lib/cotcube-helpers/symbols.rb
|
124
|
+
- scripts/collect_market_depth
|
122
125
|
- scripts/cron_ruby_wrapper.sh
|
123
126
|
- scripts/symbols
|
124
127
|
homepage: https://github.com/donkeybridge/cotcube-helpers
|