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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1228015a21f3920ec5b1d87e3c5918e3c69e0a577746b6a18579ae51e1528ce2
4
- data.tar.gz: 431798647c5d4e9d5a7a32816636bd768c4e0e9c70d789dd491c374fde75f768
3
+ metadata.gz: 1d08ed5fa831283b0934d2ce7f86f9da0f1be5430d0d3164ee503b0aa7bac74f
4
+ data.tar.gz: ac59b5558432299e6906c8a93b4ddab8b7a57a7cac4b2c11239a8f41a8c05f4e
5
5
  SHA512:
6
- metadata.gz: b931c2dad6f54a1f2de64590e703c1be03ed2330691a299c2e4d4a64f5cb5d20d3be9ba44584866db66116160c2d01c87cd17f7d84f5ae82ae2668f5a8cf0bae
7
- data.tar.gz: 18b50ba7237d373dec19d3144814ad55c5ec3d36274213c5749041bbf66ce008461d2be89cda0240245529e29ed383110352d945af4a82cb761d53a6d702f09a
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
@@ -6,3 +6,4 @@ source 'https://rubygems.org'
6
6
  gemspec
7
7
  gem 'parallel'
8
8
  gem 'bunny'
9
+ gem 'httparty'
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.1.1
1
+ 0.2.3
@@ -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', '~> 6'
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
@@ -30,6 +30,8 @@ module Cotcube
30
30
 
31
31
 
32
32
  CHICAGO = Time.find_zone('America/Chicago')
33
+ NEW_YORK = Time.find_zone('America/New_York')
34
+ BERLIN = Time.find_zone('Europe/Berlin')
33
35
 
34
36
  DATE_FMT = '%Y-%m-%d'
35
37
 
@@ -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: 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] = { 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
@@ -174,4 +227,4 @@ begin
174
227
  }
175
228
  ensure
176
229
  client.stop
177
- e,nd
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
- each_key do |key|
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
@@ -34,7 +34,7 @@ module Cotcube
34
34
  end
35
35
 
36
36
  defaults = {
37
- data_path: '/var/cotcube/' + name,
37
+ data_path: '/var/cotcube/' + name.split('::').last.downcase,
38
38
  }
39
39
 
40
40
  config = defaults.merge(config)
@@ -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
@@ -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/default
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.1.1
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-11 00:00:00.000000000 Z
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: '6'
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: '6'
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