cotcube-helpers 0.2.1.1 → 0.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: 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