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 +4 -4
- data/CHANGELOG.md +33 -0
- data/Gemfile +3 -0
- data/VERSION +1 -1
- data/cotcube-helpers.gemspec +1 -1
- data/lib/cotcube-helpers/cache_client.rb +52 -0
- data/lib/cotcube-helpers/constants.rb +8 -0
- data/lib/cotcube-helpers/data_client.rb +230 -0
- data/lib/cotcube-helpers/deep_decode_datetime.rb +25 -0
- data/lib/cotcube-helpers/expiration.rb +31 -0
- data/lib/cotcube-helpers/get_id_set.rb +3 -2
- data/lib/cotcube-helpers/hash_ext.rb +5 -5
- data/lib/cotcube-helpers/ib_contracts.rb +69 -0
- data/lib/cotcube-helpers/init.rb +1 -1
- data/lib/cotcube-helpers/orderclient.rb +203 -0
- data/lib/cotcube-helpers/symbols.rb +5 -3
- data/lib/cotcube-helpers.rb +10 -0
- data/scripts/collect_market_depth +103 -0
- data/scripts/cron_ruby_wrapper.sh +2 -1
- metadata +11 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7496f5774fbcb9b661dac0f9315081a4b5f4d30a8c89334ef13d0639b789ba73
|
4
|
+
data.tar.gz: 596ab3ed0cc578c6321beb1b07b6400e109f4537d91cf0903a0c0542b9a962df
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.2.
|
1
|
+
0.2.2.5
|
data/cotcube-helpers.gemspec
CHANGED
@@ -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', '~>
|
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
|
-
|
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
|
+
|
data/lib/cotcube-helpers/init.rb
CHANGED
@@ -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[:
|
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,
|
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.
|
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
|
data/lib/cotcube-helpers.rb
CHANGED
@@ -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/
|
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.
|
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
|
+
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: '
|
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: '
|
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
|