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