cotcube-helpers 0.1.10 → 0.2.2.4

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: e00fe82276d1be8a2f034759240a0bf820574f6c99de4ec6126bf436bf6a149b
4
- data.tar.gz: e512de0546fd46dcdb356a55265bb8bc874999447c98df67c77b1289a8e2cee4
3
+ metadata.gz: 2e416bde4f3c3a8dbba8290c74fa4a54d8f0b5f84f61eb9af8e7bd669bab5a43
4
+ data.tar.gz: abb4a2842d5b11f2fcb38bbede59bd693b18117547c94d9eb11a2342d1f76665
5
5
  SHA512:
6
- metadata.gz: 5c0474d0638810ecf7af38f662c80c9a91ac531e5d38e71ff03e7872b559a4add577437c91c435ace0a8b1afc2adb1c1b686272426b6404d138d9d7b3fdfa8af
7
- data.tar.gz: 94b42e991e539ac38e672c0fea5aba4a052e9c5a2b3899bc48d3e855ce23541f04f3b8a8640bb9bb76bd144476e4110d226e4a28298c66ad45480b3d80031a51
6
+ metadata.gz: 60f1bc2438ef256bc3d772dec73e22a12711e4088a8ffa07463976c0931e149d471f9d7ea93c6930aff3b96fe06d15537cf96319946ed0fb4cbbbdf70573295a
7
+ data.tar.gz: b372776b0d180c6007b0c652d1eeb056d1845ac12775a8853e6e9e9ba9de128cf411527c4912174c722d5c6c4705d4524240665e917928ab50bb9ca0a3fe53cf
data/CHANGELOG.md CHANGED
@@ -1,3 +1,36 @@
1
+ ## 0.2.2.4 (December 07, 2021)
2
+ - introduced cache_client as client to readcache
3
+
4
+ ## 0.2.2.3 (November 28, 2021)
5
+ - data_client: fixing troublesome output in .command
6
+
7
+ ## 0.2.2.2 (November 28, 2021)
8
+ - dataclient fixing = for false ==
9
+ - solving merge conflicts
10
+ - Bump version to 0.2.2.
11
+
12
+ ## 0.2.2.1 (November 28, 2021)
13
+ - Bump version to 0.2.2.1
14
+
15
+ ## 0.2.2 (November 13, 2021)
16
+ - some further improvements to DataClient
17
+ - some fixes related to ib_contracts
18
+ - some fixes related to DataClient
19
+
20
+ ## 0.2.1.1 (November 10, 2021)
21
+ - Bump version to 0.2.1.
22
+
23
+ ## 0.2.1 (November 10, 2021)
24
+ - added new class 'dataclient' for communication with dataproxy
25
+ - added .translate_ib_contract
26
+
27
+ ## 0.2.0 (November 07, 2021)
28
+ - added module Candlestick_Recognition
29
+ - added instance_inspect method to 'scan' objects for contents of instance variables
30
+ - symbols: made selection of symbols more versatile by key
31
+ - added headers (:ib_symbol, :internal, :exchange, :currency) to symbol headers as well as symbol examples
32
+ - added scripts/symbols to list (and filter) symbols from command line (put to PATH!)
33
+
1
34
  ## 0.1.10 (October 28, 2021)
2
35
  - added script cron_ruby_wrapper.sh (linkable as /usr/local/bin/cruw.sh)
3
36
  - added numeric ext .with_delimiter to support printing like 123_456_789.00121
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.1.10
1
+ 0.2.2.4
@@ -0,0 +1,28 @@
1
+ require 'httparty'
2
+ require 'json'
3
+
4
+ module Cotcube
5
+ module Helpers
6
+ def cached(query, timezone: Cotcube::Helpers::CHICAGO, debug: false, deflate: false)
7
+ # TODO: set param to enable deflate on transmission via HTTPARRTY Header
8
+ request_headers = {}
9
+ request_headers['Accept-Encoding' => 'deflate'] if deflate
10
+ res = JSON.parse(HTTParty.get("http://100.100.0.14:8081/#{query}").parsed_response, symbolize_names: true) rescue { error: 1, msg: "Could not parse response for query '#{query}'." }
11
+ unless res[:error] and res[:error].zero?
12
+ puts "ERROR: #{res}"
13
+ return false
14
+ end
15
+ #res[:valid_until] = timezone.parse(res[:valid_until])
16
+ #res[:modified] = timezone.parse(res[:modified_at])
17
+ if debug
18
+ puts "Warnings: #{res[:warnings]}"
19
+ puts "Modified: #{res[:modified]}"
20
+ puts "Valid_un: #{res[:valid_until]}"
21
+ puts "payload: #{res[:payload].to_s.size}"
22
+ end
23
+ res[:payload]
24
+ end
25
+
26
+ module_function :cached
27
+ end
28
+ end
@@ -3,13 +3,13 @@
3
3
  module Cotcube
4
4
  module Helpers
5
5
  SYMBOL_EXAMPLES = [
6
- { id: '13874U', symbol: 'ES', ticksize: 0.25, power: 12.5, months: 'HMUZ', bcf: 1.0, reports: 'LF', format: '8.2f', name: 'S&P 500 MICRO' },
7
- { id: '209747', symbol: 'NQ', ticksize: 0.25, power: 5.0, monhts: 'HMUZ', bcf: 1.0, reports: 'LF', format: '8.2f', name: 'NASDAQ 100 MICRO' }
6
+ { id: '13874U', symbol: 'ES', ib_symbol: 'ES', internal: 'ES', exchange: 'GLOBEX', currency: 'USD', ticksize: 0.25, power: 12.5, months: 'HMUZ', bcf: 1.0, reports: 'LF', format: '8.2f', name: 'S&P 500 MICRO' },
7
+ { id: '209747', symbol: 'NQ', ib_symbol: 'NQ', internal: 'NQ', exchange: 'GLOBEx', currency: 'USD', ticksize: 0.25, power: 5.0, monhts: 'HMUZ', bcf: 1.0, reports: 'LF', format: '8.2f', name: 'NASDAQ 100 MICRO' }
8
8
  ].freeze
9
9
 
10
10
  MICRO_EXAMPLES = [
11
- { id: '13874U', symbol: 'ET', ticksize: 0.25, power: 1.25, months: 'HMUZ', bcf: 1.0, reports: 'LF', format: '8.2f', name: 'MICRO S&P 500 MICRO' },
12
- { id: '209747', symbol: 'NM', ticksize: 0.25, power: 0.5, monhts: 'HMUZ', bcf: 1.0, reports: 'LF', format: '8.2f', name: 'MICRO NASDAQ 100 MICRO' }
11
+ { id: '13874U', symbol: 'ET', ib_symbol: 'MES', internal: 'MES', exchange: 'GLOBEX', currency: 'USD', ticksize: 0.25, power: 1.25, months: 'HMUZ', bcf: 1.0, reports: 'LF', format: '8.2f', name: 'MICRO S&P 500 MICRO' },
12
+ { id: '209747', symbol: 'NM', ib_symbol: 'MNQ', internal: 'MNQ', exchange: 'GLOBEX', currency: 'USD', ticksize: 0.25, power: 0.5, monhts: 'HMUZ', bcf: 1.0, reports: 'LF', format: '8.2f', name: 'MICRO NASDAQ 100 MICRO' }
13
13
  ].freeze
14
14
 
15
15
  COLORS = %i[light_red light_yellow light_green red yellow green cyan magenta blue].freeze
@@ -33,6 +33,12 @@ module Cotcube
33
33
 
34
34
  DATE_FMT = '%Y-%m-%d'
35
35
 
36
+ # Simple mapper to get from MONTH to LETTER
37
+ LETTERS = { "JAN"=> "F", "FEB"=> "G", "MAR"=> "H",
38
+ "APR"=> "J", "MAY"=> "K", "JUN"=> "M",
39
+ "JUL"=> "N", "AUG"=> "Q", "SEP"=> "U",
40
+ "OCT"=> "V", "NOV"=> "X", "DEC"=> "Z" }
41
+
36
42
  end
37
43
  end
38
44
 
@@ -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: 5)
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,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
 
@@ -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
+
@@ -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