cotcube-helpers 0.2.0 → 0.2.2.5

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: 38df15256f996b15dfeb1755f7464e932a98541e62aa43efda0a264be58bafb2
4
- data.tar.gz: cd336b6a91442a6d3bbace66ec8a25049ae979ab78eef03d1c8b44a548da8023
3
+ metadata.gz: 7496f5774fbcb9b661dac0f9315081a4b5f4d30a8c89334ef13d0639b789ba73
4
+ data.tar.gz: 596ab3ed0cc578c6321beb1b07b6400e109f4537d91cf0903a0c0542b9a962df
5
5
  SHA512:
6
- metadata.gz: bcfc562ab118939e7c23dc37b89fee881871552b6b7b17ec7066cb4a2028fa2661e50cd5512725da28332dd1b983d6817d416d31ba05059dd15d824a0e101f7c
7
- data.tar.gz: 9ca3305120eb630283e303206b764cf9d8dd533ceaf0a2aa044f702fa2f2a01055ff70a4331ec865d854580b1dcf367f365c5addbb988605537c0ee9b9b6fad9
6
+ metadata.gz: 7447d4f4d2297c60eb37a815b8cd0c91ddd70d1c4af0d2f8bab89ada65c2ac0c328da6e6dd2e99f46d39d75cc765ee4d724598c3d9fdbf42cd15b6a7f30a22fc
7
+ data.tar.gz: 348426b27122a2be123f93f4db63b7d57e032cf2451f6822a10fce94124d9d0f2cf6c9d3621b920daa651e80ec1b69a5e777d4d94a169cb03f6067f13d1d693f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,36 @@
1
+ ## 0.2.2.5 (December 23, 2021)
2
+ - hash_ext: reworked keys_to_sym!
3
+ - minor changes
4
+ - deep_decode_datetime: added helper for conversion of timestrings in nested arrays and hashed
5
+ - cache_client: rewritten as class
6
+ - gemspec: raised activesupport to version 7
7
+
8
+ ## 0.2.2.4 (December 07, 2021)
9
+ - introduced cache_client as client to readcache
10
+
11
+ ## 0.2.2.3 (November 28, 2021)
12
+ - data_client: fixing troublesome output in .command
13
+
14
+ ## 0.2.2.2 (November 28, 2021)
15
+ - dataclient fixing = for false ==
16
+ - solving merge conflicts
17
+ - Bump version to 0.2.2.
18
+
19
+ ## 0.2.2.1 (November 28, 2021)
20
+ - Bump version to 0.2.2.1
21
+
22
+ ## 0.2.2 (November 13, 2021)
23
+ - some further improvements to DataClient
24
+ - some fixes related to ib_contracts
25
+ - some fixes related to DataClient
26
+
27
+ ## 0.2.1.1 (November 10, 2021)
28
+ - Bump version to 0.2.1.
29
+
30
+ ## 0.2.1 (November 10, 2021)
31
+ - added new class 'dataclient' for communication with dataproxy
32
+ - added .translate_ib_contract
33
+
1
34
  ## 0.2.0 (November 07, 2021)
2
35
  - added module Candlestick_Recognition
3
36
  - added instance_inspect method to 'scan' objects for contents of instance variables
data/Gemfile CHANGED
@@ -4,3 +4,6 @@ source 'https://rubygems.org'
4
4
 
5
5
  # Specify your gem's dependencies in bitangent.gemspec
6
6
  gemspec
7
+ gem 'parallel'
8
+ gem 'bunny'
9
+ gem 'httparty'
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.0
1
+ 0.2.2.5
@@ -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,9 +30,17 @@ 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
 
38
+ # Simple mapper to get from MONTH to LETTER
39
+ LETTERS = { "JAN"=> "F", "FEB"=> "G", "MAR"=> "H",
40
+ "APR"=> "J", "MAY"=> "K", "JUN"=> "M",
41
+ "JUL"=> "N", "AUG"=> "Q", "SEP"=> "U",
42
+ "OCT"=> "V", "NOV"=> "X", "DEC"=> "Z" }
43
+
36
44
  end
37
45
  end
38
46
 
@@ -0,0 +1,230 @@
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 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
+ )
28
+
29
+ def initialize
30
+ @connection = Bunny.new(user: SECRETS['dataproxy_mq_user'],
31
+ password: SECRETS['dataproxy_mq_password'],
32
+ vhost: SECRETS['dataproxy_mq_vhost'])
33
+ @connection.start
34
+
35
+ @commands = connection.create_channel
36
+ @exchange = commands.direct('dataproxy_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: 'dataproxy_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
+ %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
85
+ connection.close
86
+ end
87
+
88
+ def get_contracts(symbol:)
89
+ send_command({ command: :get_contracts, symbol: symbol })
90
+ end
91
+
92
+ def get_historical(contract:, interval:, duration: nil, before: nil, rth_only: false, based_on: :trades)
93
+ # rth.true? means data outside of rth is skipped
94
+ rth_only = rth_only ? 1 : 0
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
+
122
+ # TODO: Check for valid duration specification
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)
134
+ end
135
+
136
+ def start_persistent(contract:, type: :realtimebars, local_id: 0, &block)
137
+ unless %i[depth ticks realtimebars].include? type.to_sym
138
+ puts "ERROR: Inappropriate type in stop_realtimebars with #{type}"
139
+ return false
140
+ end
141
+
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)
147
+ queue.bind(exchange)
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|
157
+ block.call(JSON.parse(payload, symbolize_names: true))
158
+ end
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
163
+ send_command(command)
164
+ end
165
+
166
+ def stop_persistent(contract:, local_id: 0, type: :realtimebars)
167
+ unless %i[depth ticks realtimebars].include? type.to_sym
168
+ puts "ERROR: Inappropriate type in stop_realtimebars with #{type}"
169
+ return false
170
+ end
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)
175
+ end
176
+
177
+ attr_accessor :response
178
+ attr_reader :lock, :condition
179
+
180
+ private
181
+
182
+ attr_reader :call_id, :connection, :requests, :persistent,
183
+ :commands, :server_queue_name, :reply_queue, :exchange
184
+
185
+ def setup_reply_queue
186
+ @reply_queue = commands.queue('', exclusive: true, auto_delete: true)
187
+ @reply_queue.bind(commands.exchange('dataproxy_replies'), routing_key: @reply_queue.name)
188
+
189
+ reply_queue.subscribe do |delivery_info, properties, payload|
190
+ __id__ = properties[:correlation_id]
191
+
192
+ if __id__.nil?
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
196
+
197
+ elsif requests[__id__].nil?
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
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+
215
+ __END__
216
+ begin
217
+ client = DataClient.new
218
+ reply = client.send_command( { command: 'ping' } ) #{ command: :hist, contract: 'A6Z21', con_id: 259130514, interval: :min15 } )
219
+ puts reply.nil? ? 'nil' : JSON.parse(reply)
220
+ reply = client.get_historical( contract: 'A6Z21', con_id: 259130514, interval: :min15 , rth_only: false)
221
+ JSON.parse(reply, symbolize_names: true)[:result].map{|z|
222
+ z[:datetime] = Cotcube::Helpers::CHICAGO.parse(z[:time]).strftime('%Y-%m-%d %H:%M:%S')
223
+ z.delete(:created_at)
224
+ z.delete(:time)
225
+ p z.slice(*%i[datetime open high low close volume]).values
226
+
227
+ }
228
+ ensure
229
+ client.stop
230
+ e,nd
@@ -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
@@ -20,10 +20,10 @@ module Cotcube
20
20
  end
21
21
 
22
22
  unless symbol.nil?
23
- sym = symbols(symbol: symbol)
23
+ sym = symbols(symbol: symbol).presence || micros(symbol: symbol)
24
24
  if sym.nil? || sym[:id].nil?
25
25
  raise ArgumentError,
26
- "Could not find match in #{config[:symbols_file]} for given symbol #{symbol}"
26
+ "Could not find match in #{config[:symbols_file]} or #{config[:micros_file]} for given symbol #{symbol}"
27
27
  end
28
28
  raise ArgumentError, "Mismatching symbol #{symbol} and given id #{id}" if (not id.nil?) && (sym[:id] != id)
29
29
 
@@ -39,6 +39,7 @@ module Cotcube
39
39
  end
40
40
  raise ArgumentError, 'Need :id, :symbol or valid :contract '
41
41
  end
42
+
42
43
  end
43
44
  end
44
45
 
@@ -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
@@ -0,0 +1,69 @@
1
+ module Cotcube
2
+ module Helpers
3
+ def get_ib_contract(contract)
4
+ symbol = contract[..1]
5
+ # TODO consider file location to be found in configfile
6
+ filepath = '/etc/cotcube/ibsymbols/'
7
+ result = YAML.load(File.read( "#{filepath}/#{symbol}.yml"))[contract].transform_keys(&:to_sym) rescue nil
8
+ result.nil? ? update_ib_contracts(symbol: contract[..1]) : (return result)
9
+ YAML.load(File.read( "#{filepath}/#{symbol}.yml"))[contract].transform_keys(&:to_sym) rescue nil
10
+ end
11
+
12
+ def update_ib_contracts(symbol: nil)
13
+ begin
14
+ client = DataClient.new
15
+ (Cotcube::Helpers.symbols + Cotcube::Helpers.micros).each do |sym|
16
+
17
+ # TODO: consider file location to be located in config
18
+ file = "/etc/cotcube/ibsymbols/#{sym[:symbol]}.yml"
19
+
20
+ # TODO: VI publishes weekly options which dont match, the 3 others need multiplier enabled to work
21
+ next if %w[ DY TM SI VI ].include? sym[:symbol]
22
+ next if symbol and sym[:symbol] != symbol
23
+ begin
24
+ if File.exist? file
25
+ next if Time.now - File.mtime(file) < 5.days
26
+ data = nil
27
+ data = YAML.load(File.read(file))
28
+ else
29
+ data = {}
30
+ end
31
+ p file
32
+ %w[ symbol sec_type exchange multiplier ticksize power internal ].each {|z| data.delete z}
33
+ raw = client.get_contracts(symbol: sym[:symbol])
34
+ reply = JSON.parse(raw)['result']
35
+ reply.each do |set|
36
+ contract = translate_ib_contract set['local_symbol']
37
+ data[contract] ||= set
38
+ end
39
+ keys = data.keys.sort_by{|z| z[2]}.sort_by{|z| z[-2..] }.select{|z| z[..1] == sym[:symbol] }
40
+ data = data.slice(*keys)
41
+ File.open(file, 'w'){|f| f.write(data.to_yaml) }
42
+ rescue Exception => e
43
+ puts e.full_message
44
+ p sym
45
+ binding.irb
46
+ end
47
+ end
48
+ ensure
49
+ client.stop
50
+ true
51
+ end
52
+ end
53
+
54
+ def translate_ib_contract(contract)
55
+ short = contract.split(" ").size == 1
56
+ sym_a = contract.split(short ? '' : ' ')
57
+ year = sym_a.pop.to_i + (short ? 20 : 0)
58
+ if short and sym_a[-1].to_i > 0
59
+ year = year - 20 + sym_a.pop.to_i * 10
60
+ end
61
+ month = short ? sym_a.pop : LETTERS[sym_a.pop]
62
+ sym = Cotcube::Helpers.symbols(internal: sym_a.join)[:symbol] rescue nil
63
+ sym ||= Cotcube::Helpers.micros(internal: sym_a.join)[:symbol] rescue nil
64
+ sym.nil? ? false : "#{sym}#{month}#{year}"
65
+ end
66
+
67
+ end
68
+ end
69
+
@@ -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,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
@@ -17,7 +17,7 @@ module Cotcube
17
17
  [ :ticksize, :power, :bcf ].each {|z| row[z] = row[z].to_f}
18
18
  row[:format] = "%#{row[:format]}f"
19
19
  row[:currency] ||= 'USD'
20
- row[:multiplier] = (row[:ticksize] / row[:power]).round(8)
20
+ row[:multiplier] = (row[:power] / row[:ticksize]).round(8)
21
21
  row
22
22
  }
23
23
  .reject{|row| row[:id].nil? }
@@ -35,7 +35,7 @@ module Cotcube
35
35
  end
36
36
  end
37
37
 
38
- def micros(config: init, symbol: nil, id: nil)
38
+ def micros(config: init, **args)
39
39
  if config[:micros_file].nil?
40
40
  MICRO_EXAMPLES
41
41
  else
@@ -46,13 +46,14 @@ module Cotcube
46
46
  [ :ticksize, :power, :bcf ].each {|z| row[z] = row[z].to_f }
47
47
  row[:format] = "%#{row[:format]}f"
48
48
  row[:currency] ||= 'USD'
49
+ row[:multiplier] = (row[:power] / row[:ticksize]).round(8)
49
50
  row
50
51
  }
51
52
  .reject{|row| row[:id].nil? }
52
53
  .tap{ |all|
53
54
  args.keys.each { |header|
54
55
  unless SYMBOL_HEADERS.include? header
55
- puts "WARNING in Cotcube::Helpers.symbols: '#{header}' is not a valid symbol header. Skipping..."
56
+ puts "WARNING in Cotcube::Helpers.micros: '#{header}' is not a valid symbol header. Skipping..."
56
57
  next
57
58
  end
58
59
  all.select!{|x| x[header] == args[header]} unless args[header].nil?
@@ -62,6 +63,7 @@ module Cotcube
62
63
  }
63
64
  end
64
65
  end
66
+
65
67
  end
66
68
 
67
69
  end
@@ -7,6 +7,8 @@ require 'active_support/core_ext/time'
7
7
  require 'active_support/core_ext/numeric'
8
8
  require 'parallel'
9
9
  require 'csv'
10
+ require 'yaml'
11
+ require 'json'
10
12
 
11
13
  require_relative 'cotcube-helpers/array_ext'
12
14
  require_relative 'cotcube-helpers/enum_ext'
@@ -19,6 +21,7 @@ require_relative 'cotcube-helpers/subpattern'
19
21
  require_relative 'cotcube-helpers/parallelize'
20
22
  require_relative 'cotcube-helpers/simple_output'
21
23
  require_relative 'cotcube-helpers/simple_series_stats'
24
+ require_relative 'cotcube-helpers/deep_decode_datetime'
22
25
  require_relative 'cotcube-helpers/constants'
23
26
  require_relative 'cotcube-helpers/input'
24
27
  require_relative 'cotcube-helpers/output'
@@ -26,6 +29,7 @@ require_relative 'cotcube-helpers/reduce'
26
29
  require_relative 'cotcube-helpers/symbols'
27
30
  require_relative 'cotcube-helpers/init'
28
31
  require_relative 'cotcube-helpers/get_id_set'
32
+ require_relative 'cotcube-helpers/ib_contracts'
29
33
  require_relative 'cotcube-helpers/recognition'
30
34
 
31
35
  module Cotcube
@@ -40,8 +44,14 @@ module Cotcube
40
44
  :symbols,
41
45
  :micros,
42
46
  :get_id_set,
47
+ :get_ib_contract,
48
+ :update_ib_contracts,
49
+ :translate_ib_contract,
43
50
  :init
44
51
 
45
52
  # please not that module_functions of source provided in private files must be published there
46
53
  end
47
54
  end
55
+
56
+ require_relative 'cotcube-helpers/data_client'
57
+ require_relative 'cotcube-helpers/cache_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."
@@ -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.0
4
+ version: 0.2.2.5
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-07 00:00:00.000000000 Z
11
+ date: 2021-12-23 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,14 +96,20 @@ 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
101
+ - lib/cotcube-helpers/data_client.rb
100
102
  - lib/cotcube-helpers/datetime_ext.rb
103
+ - lib/cotcube-helpers/deep_decode_datetime.rb
101
104
  - lib/cotcube-helpers/enum_ext.rb
105
+ - lib/cotcube-helpers/expiration.rb
102
106
  - lib/cotcube-helpers/get_id_set.rb
103
107
  - lib/cotcube-helpers/hash_ext.rb
108
+ - lib/cotcube-helpers/ib_contracts.rb
104
109
  - lib/cotcube-helpers/init.rb
105
110
  - lib/cotcube-helpers/input.rb
106
111
  - lib/cotcube-helpers/numeric_ext.rb
112
+ - lib/cotcube-helpers/orderclient.rb
107
113
  - lib/cotcube-helpers/output.rb
108
114
  - lib/cotcube-helpers/parallelize.rb
109
115
  - lib/cotcube-helpers/range_ext.rb
@@ -117,6 +123,7 @@ files:
117
123
  - lib/cotcube-helpers/swig/fill_x.rb
118
124
  - lib/cotcube-helpers/swig/recognition.rb
119
125
  - lib/cotcube-helpers/symbols.rb
126
+ - scripts/collect_market_depth
120
127
  - scripts/cron_ruby_wrapper.sh
121
128
  - scripts/symbols
122
129
  homepage: https://github.com/donkeybridge/cotcube-helpers