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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1228015a21f3920ec5b1d87e3c5918e3c69e0a577746b6a18579ae51e1528ce2
4
- data.tar.gz: 431798647c5d4e9d5a7a32816636bd768c4e0e9c70d789dd491c374fde75f768
3
+ metadata.gz: 54c97a5746e0e03b971b9421be0636eb2c259beb90840ff34404f8049114019b
4
+ data.tar.gz: 6d565f00ed620b2458122ad686b1b6c60bb26e815f248474f746b38c10902dc6
5
5
  SHA512:
6
- metadata.gz: b931c2dad6f54a1f2de64590e703c1be03ed2330691a299c2e4d4a64f5cb5d20d3be9ba44584866db66116160c2d01c87cd17f7d84f5ae82ae2668f5a8cf0bae
7
- data.tar.gz: 18b50ba7237d373dec19d3144814ad55c5ec3d36274213c5749041bbf66ce008461d2be89cda0240245529e29ed383110352d945af4a82cb761d53a6d702f09a
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.1
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(automatically_recover: true)
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
- @channel = connection.create_channel
14
- @exchange = channel.direct('dataproxy_commands', auto_delete: true)
15
- @requests = {}
35
+ @commands = connection.create_channel
36
+ @exchange = commands.direct('dataproxy_commands')
37
+ @requests = {}
16
38
  @persistent = { depth: {}, realtimebars: {}, ticks: {} }
17
- @response = nil
18
-
39
+ @debug = false
19
40
  setup_reply_queue
20
41
  end
21
42
 
22
- def help
23
- puts "The following commands are available:\n\n"\
24
- "\tcontracts = client.get_contracts(symbol:)\n"\
25
- "\tbars = client.get_historical(contract:, duration:, interval:, before: nil)"\
26
- "\trequest_id = client.start_realtimebars(contract: )\n"\
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] = { request: command, id: 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
- lock.synchronize {
46
- condition.wait(lock, timeout)
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
- channel.close
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
- def get_contracts(symbol: )
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
- default_durations = {
66
- sec1: '30_M',
67
- sec5: '2_H',
68
- sec15: '6_H',
69
- sec30: '12_H',
70
- min1: '1_D',
71
- min2: '2_D',
72
- min5: '5_D',
73
- min15: '1_W',
74
- min30: '1_W',
75
- hour1: '1_W',
76
- day1: '1_Y'
77
- }
78
- raise "Invalid interval '#{interval}', should be in '#{default_durations.keys}'." unless default_durations.keys.include? interval
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
- duration ||= default_durations[interval]
81
- send_command( {
82
- command: :historical,
83
- contract: contract,
84
- interval: interval,
85
- duration: duration,
86
- based_on: based_on.to_s.upcase,
87
- rth_only: rth_only,
88
- before: nil
89
- }, timeout: 20 )
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[ depth ticks realtimebars].include? type.to_sym
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
- ib_contract = Cotcube::Helpers.get_ib_contract(contract)
99
- exchange = channel.fanout( "dataproxy_#{type.to_s}_#{contract}", auto_delete: true)
100
- queue = channel.queue('', exclusive: true, auto_delete: true)
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
- block ||= ->(bar){ puts "#{bar}" }
103
- queue.subscribe do |_delivery_info, properties, payload|
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
- command = { command: type, contract: contract, con_id: ib_contract[:con_id], delivery: queue.name, exchange: exchange.name }
107
- persistent[type][queue.name] = command
108
- persistent[type][queue.name][:queue] = queue
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[ depth ticks realtimebars].include? type.to_sym
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
- ib_contract = Cotcube::Helpers.get_ib_contract(contract)
118
- command = { command: "stop_#{type}", contract: contract, con_id: ib_contract[:con_id] }
119
- send_command(command)
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
- @lock = Mutex.new
132
- @condition = ConditionVariable.new
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
- }\n\n#{properties .map{|k,v| "#{k}\t#{v}"}.join("\n")
144
- }\n\n#{JSON.parse(payload).map{|k,v| "#{k}\t#{v}"}.join("\n")}"
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#{_delivery_info}\n\n#{properties}\n\n#{payload}\n."
148
- else
149
- that.response = payload
150
-
151
- # sends the signal to continue the execution of #call
152
- requests.delete(__id__)
153
- that.lock.synchronize { that.condition.signal }
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
@@ -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.1.1
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 00:00:00.000000000 Z
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