cotcube-helpers 0.2.0 → 0.2.2.5

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: 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