cotcube-helpers 0.1.10 → 0.2.2.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -0
- data/Gemfile +3 -0
- data/VERSION +1 -1
- data/lib/cotcube-helpers/cache_client.rb +28 -0
- data/lib/cotcube-helpers/constants.rb +10 -4
- data/lib/cotcube-helpers/data_client.rb +230 -0
- data/lib/cotcube-helpers/expiration.rb +31 -0
- data/lib/cotcube-helpers/get_id_set.rb +3 -2
- data/lib/cotcube-helpers/ib_contracts.rb +69 -0
- data/lib/cotcube-helpers/orderclient.rb +203 -0
- data/lib/cotcube-helpers/output.rb +20 -0
- data/lib/cotcube-helpers/recognition.rb +509 -0
- data/lib/cotcube-helpers/symbols.rb +50 -18
- data/lib/cotcube-helpers.rb +11 -0
- data/scripts/collect_market_depth +103 -0
- data/scripts/symbols +13 -0
- metadata +12 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2e416bde4f3c3a8dbba8290c74fa4a54d8f0b5f84f61eb9af8e7bd669bab5a43
|
4
|
+
data.tar.gz: abb4a2842d5b11f2fcb38bbede59bd693b18117547c94d9eb11a2342d1f76665
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
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
|