cotcube-level 0.3.2 → 0.3.4.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/VERSION +1 -1
- data/bin/iswaps.rb +45 -0
- data/bin/swaps.rb +45 -0
- data/lib/cotcube-level/eod_stencil.rb +18 -11
- data/lib/cotcube-level/helpers.rb +152 -44
- data/lib/cotcube-level/intraday_stencil.rb +125 -71
- data/lib/cotcube-level/tritangulate.rb +6 -3
- data/lib/cotcube-level.rb +2 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4bbf4fc1b2edf64ffee60a29ffe7d72a71a4c0f8fb99b889026dce9dbca7399f
|
4
|
+
data.tar.gz: 59469c2431a20e9e1f07a99e0af61a4fed59ca276a02d654a2f2bd28a8543c04
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 78a4cb1a3bc80de5086befc5e927cafc86c38ef62410c4fc9d61e716e929f3847ad4a6ac870d30b94168e10dd72d22267aba8d4d4ae2f52a5e7170454a6864f5
|
7
|
+
data.tar.gz: 79ed8087a1936cef86257e24c9909f8923902be09e43868ce29deb5b178d03c2d3bbb2700f2bb76f67b2a58a51df930b3bdd41c4134922ed90bd6b2a1f0990ad
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,24 @@
|
|
1
|
+
## 0.3.4.2 (November 28, 2021)
|
2
|
+
- added 2 executables to bin to display eod- and intraday-swaps
|
3
|
+
- helpers: adding ignorance to load_swaps and other, adding .mark_ignored
|
4
|
+
- intraday_stencil: allowing absence of :zero in #use
|
5
|
+
|
6
|
+
## 0.3.4.1 (October 09, 2021)
|
7
|
+
- intraday_stencil: fixing @index, that did not work when used outside active hours
|
8
|
+
|
9
|
+
## 0.3.4 (October 06, 2021)
|
10
|
+
- intraday_stencil: major rework resp. rebuild ... now beta-ready
|
11
|
+
- helpers: preparing puts_swap and get_jsonl_name to work intraday
|
12
|
+
- puts_swap: changed output scheme to provide exceedance as NOTE
|
13
|
+
|
14
|
+
## 0.3.3 (October 05, 2021)
|
15
|
+
- helpers::load_swaps: added :exceed to allow 1(sic) swap to be exceeded while loading
|
16
|
+
- tritangulate: added :manual for feature of manual swap creation with base of 2 members
|
17
|
+
- helpers::load_swap added :digest to filter for swaps starting with pattern
|
18
|
+
- helpers: minor readability improvements
|
19
|
+
- eod_stencil: minor readability improvements
|
20
|
+
- helpers: few optimizations
|
21
|
+
|
1
22
|
## 0.3.2 (August 29, 2021)
|
2
23
|
- tritangulate: fixing 'finalize', as Integer zero won't comparte to Float zero
|
3
24
|
- cotcube-level.rb: added :check_exceedance
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.3.2
|
1
|
+
0.3.4.2
|
data/bin/iswaps.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../lib/cotcube-level.rb'
|
4
|
+
|
5
|
+
HELP = <<HEREDOC
|
6
|
+
Display current intraday swaps.
|
7
|
+
> USAGE: iswaps.rb <contract> [json]
|
8
|
+
> contract a contract known to the system
|
9
|
+
> json switch to toggle json output instead of human readable
|
10
|
+
HEREDOC
|
11
|
+
if ARGV.empty?
|
12
|
+
puts HELP
|
13
|
+
exit
|
14
|
+
end
|
15
|
+
|
16
|
+
contract = ARGV[0].nil? ? nil : ARGV[0].upcase
|
17
|
+
json = ARGV.include? 'json'
|
18
|
+
|
19
|
+
sym = Cotcube::Helpers.get_id_set(contract: contract) rescue "Could not determine contract #{contract}"
|
20
|
+
if sym.is_a? Sring; puts sym; puts HELP; exit 1; end
|
21
|
+
|
22
|
+
swaps = Cotcube::Level::load_swaps(interval: 30.minutes, swap_type: :full, contract: contract, sym: sym).
|
23
|
+
select{|swap| not(swap[:empty]) and
|
24
|
+
not(swap[:ignored]) and
|
25
|
+
not(swap[:exceeded].presence ? (swap[:exceeded] < DateTime.now - 2.days) : false)
|
26
|
+
}
|
27
|
+
stencil = Cotcube::Level::Intraday_Stencil.new( interval: 30.minutes, swap_type: :full, asset: contract[..1])
|
28
|
+
swaps.map!{|swap| stencil.use with: swap, sym: sym}
|
29
|
+
|
30
|
+
if json
|
31
|
+
puts swaps.to_json
|
32
|
+
else
|
33
|
+
puts '<none>' if swaps.empty?
|
34
|
+
swaps.each {|swap|
|
35
|
+
notice = if swap[:exceeded]
|
36
|
+
"EXCEEDED #{swap[:exceeded]}"
|
37
|
+
elsif swap[:ignored]
|
38
|
+
'IGNORED'
|
39
|
+
else
|
40
|
+
"Current: #{format sym[:format], swap[:current_value]}"
|
41
|
+
end
|
42
|
+
Cotcube::Level.puts_swap(swap, format: sym[:format], notice: notice)
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
data/bin/swaps.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../lib/cotcube-level.rb'
|
4
|
+
|
5
|
+
HELP = <<HEREDOC
|
6
|
+
swaps.rb: Display current eod swaps.
|
7
|
+
> USAGE: swaps.rb <contract> [json]
|
8
|
+
> contract a contract known to the system
|
9
|
+
> json switch to toggle json output instead of human readable
|
10
|
+
HEREDOC
|
11
|
+
if ARGV.empty?
|
12
|
+
puts HELP
|
13
|
+
exit
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
contract = ARGV[0].nil? ? nil : ARGV[0].upcase
|
18
|
+
json = ARGV.include? 'json'
|
19
|
+
|
20
|
+
sym = Cotcube::Helpers.get_id_set(contract: contract) rescue "ERROR: Could not determine contract '#{contract}'."
|
21
|
+
|
22
|
+
if sym.is_a? String; puts sym; puts HELP; exit 1; end
|
23
|
+
|
24
|
+
swaps = Cotcube::Level::load_swaps(interval: :daily, swap_type: :full, contract: contract, quiet: true).
|
25
|
+
select{|swap| not(swap[:empty]) and
|
26
|
+
not(swap[:ignored]) and
|
27
|
+
not(swap[:exceeded].presence ? (swap[:exceeded] < DateTime.now - 2.days) : false)
|
28
|
+
}
|
29
|
+
stencil = Cotcube::Level::EOD_Stencil.new( interval: :daily, swap_type: :full)
|
30
|
+
swaps.map!{|swap| stencil.use with: swap, sym: sym}
|
31
|
+
if json
|
32
|
+
puts swaps.to_json
|
33
|
+
else
|
34
|
+
puts '<none>' if swaps.empty?
|
35
|
+
swaps.each {|swap|
|
36
|
+
notice = if swap[:exceeded]
|
37
|
+
"EXCEEDED #{swap[:exceeded].strftime('%Y-%m-%d')}"
|
38
|
+
elsif swap[:ignored]
|
39
|
+
'IGNORED'
|
40
|
+
else
|
41
|
+
"Current: #{format sym[:format], swap[:current_value]}"
|
42
|
+
end
|
43
|
+
Cotcube::Level.puts_swap(swap, format: sym[:format], notice: notice)
|
44
|
+
}
|
45
|
+
end
|
@@ -13,7 +13,7 @@ module Cotcube
|
|
13
13
|
#
|
14
14
|
# Current daily stencils contain dates from 2020-01-01 to 2023-12-31
|
15
15
|
#
|
16
|
-
def self.provide_raw_stencil(type:, interval: :daily, version: nil)
|
16
|
+
def self.provide_raw_stencil(type:, interval: :daily, version: nil, timezone: Cotcube::Helpers::CHICAGO)
|
17
17
|
loading = lambda do |typ|
|
18
18
|
file_base = "/var/cotcube/level/stencils/stencil_#{interval.to_s}_#{typ.to_s}.csv_"
|
19
19
|
if Dir["#{file_base}?*"].empty?
|
@@ -27,7 +27,7 @@ module Cotcube
|
|
27
27
|
raise ArgumentError, "Cannot open stencil from non-existant file #{file}."
|
28
28
|
end
|
29
29
|
end
|
30
|
-
CSV.read(file).map{|x| { datetime:
|
30
|
+
CSV.read(file).map{|x| { datetime: timezone.parse(x.first).freeze, x: x.last.to_i.freeze } }
|
31
31
|
end
|
32
32
|
unless const_defined? :RAW_STENCILS
|
33
33
|
const_set :RAW_STENCILS, { daily:
|
@@ -80,7 +80,7 @@ module Cotcube
|
|
80
80
|
raise ArgumentError, "Each stencil members should contain at least :datetime and :x" unless stencil.nil? or
|
81
81
|
stencil.map{|x| ([:datetime, :x] - x.keys).empty? and [ActiveSupport::TimeWithZone, Day].include?( x[:datetime] ) and x[:x].is_a?(Integer)}.reduce(:&)
|
82
82
|
|
83
|
-
base = stencil || EOD_Stencil.provide_raw_stencil(type: stencil_type, interval: :daily, version: version)
|
83
|
+
base = stencil || EOD_Stencil.provide_raw_stencil(type: stencil_type, interval: :daily, version: version, timezone: timezone)
|
84
84
|
|
85
85
|
# fast rewind to previous trading day
|
86
86
|
date = timezone.parse(date) unless [NilClass, Date, ActiveSupport::TimeWithZone].include? date.class
|
@@ -111,7 +111,12 @@ module Cotcube
|
|
111
111
|
end
|
112
112
|
|
113
113
|
def zero
|
114
|
-
|
114
|
+
index(0)
|
115
|
+
end
|
116
|
+
|
117
|
+
def index(offset = 0)
|
118
|
+
@index ||= @base.index{|b| b[:x].zero? }
|
119
|
+
@base[@index + offset]
|
115
120
|
end
|
116
121
|
|
117
122
|
def apply(to: )
|
@@ -136,7 +141,7 @@ module Cotcube
|
|
136
141
|
to.reject!{|x| x[:x].nil? }
|
137
142
|
end
|
138
143
|
|
139
|
-
def use(with:, sym:, zero
|
144
|
+
def use(with:, sym:, zero: nil, grace: -2)
|
140
145
|
# todo: validate with (check if vslid swap
|
141
146
|
# sym (check keys)
|
142
147
|
# zero (ohlc with x.zero?)
|
@@ -146,15 +151,17 @@ module Cotcube
|
|
146
151
|
ohlc = high ? :high : :low
|
147
152
|
start = base.find{|x| swap[:datetime] == x[:datetime]}
|
148
153
|
swap[:current_change] = (swap[:tpi] * start[:x]).round(8)
|
149
|
-
swap[:current_value] =
|
150
|
-
|
151
|
-
|
152
|
-
|
154
|
+
swap[:current_value] = swap[:members].last[ ohlc ] + swap[:current_change] * sym[:ticksize]
|
155
|
+
unless zero.nil?
|
156
|
+
swap[:current_diff] = (swap[:current_value] - zero[ohlc]) * (high ? 1 : -1 )
|
157
|
+
swap[:current_dist] = (swap[:current_diff] / sym[:ticksize]).to_i
|
158
|
+
swap[:exceeded] = zero[:datetime] if swap[:current_dist] < grace
|
159
|
+
end
|
153
160
|
swap
|
154
161
|
end
|
155
|
-
|
162
|
+
end
|
156
163
|
|
157
|
-
|
164
|
+
end
|
158
165
|
|
159
166
|
end
|
160
167
|
|
@@ -23,15 +23,15 @@ module Cotcube
|
|
23
23
|
|
24
24
|
# human readable output
|
25
25
|
# please note the format must be given, that should be taken from :sym
|
26
|
-
def member_to_human(member,side: ,format:, daily: false)
|
27
|
-
high = side == :upper
|
28
|
-
"#{member[:datetime].strftime("%a, %Y-%m-%d#{daily ? "" :" %I:%M%p"}")
|
29
|
-
} x: #{format '%-4d',
|
26
|
+
def member_to_human(member,side: ,format:, daily: false, tws: false)
|
27
|
+
high = (side == :upper)
|
28
|
+
"#{ member[:datetime].strftime("%a, %Y-%m-%d#{daily ? "" :" %I:%M%p"}")
|
29
|
+
} x: #{format '%-4d', member[:x]
|
30
30
|
} dx: #{format '%-8.3f', (member[:dx].nil? ? member[:x] : member[:dx].round(3))
|
31
31
|
} #{high ? "high" : "low"
|
32
|
-
}: #{format format,
|
33
|
-
} i: #{(format '%4d',
|
34
|
-
} d: #{format '%6.2f',
|
32
|
+
}: #{format format, member[high ? :high : :low]
|
33
|
+
} i: #{(format '%4d', member[:i]) unless member[:i].nil?
|
34
|
+
} d: #{format '%6.2f', member[:dev] unless member[:dev].nil?
|
35
35
|
} #{member[:near].nil? ? '' : "near: #{member[:near]}"
|
36
36
|
}"
|
37
37
|
end
|
@@ -40,36 +40,80 @@ module Cotcube
|
|
40
40
|
# format: e.g. sym[:format]
|
41
41
|
# short: print one line / less verbose
|
42
42
|
# notice: add this to output as well
|
43
|
-
def puts_swap(swap, format: , short:
|
44
|
-
return if swap[:empty]
|
45
|
-
|
46
|
-
|
43
|
+
def puts_swap(swap, format: , short: true, notice: nil, hash: 3, tws: false)
|
44
|
+
return '' if swap[:empty]
|
45
|
+
|
46
|
+
# if presenting swaps from json, the datetimes need to be parsed first
|
47
|
+
swap[:datetime] = DateTime.parse(swap[:datetime]) if swap[:datetime].is_a? String
|
48
|
+
swap[:exceeded] = DateTime.parse(swap[:exceeded]) if swap[:exceeded].is_a? String
|
49
|
+
swap[:ignored] = DateTime.parse(swap[:ignored]) if swap[:ignored ].is_a? String
|
50
|
+
swap[:side] = swap[:side].to_sym
|
51
|
+
swap[:members].each do |mem|
|
52
|
+
mem[:datetime] = DateTime.parse(mem[:datetime]) if mem[:datetime].is_a? String
|
53
|
+
end
|
54
|
+
|
55
|
+
# TODO: create config-entry to contain 1.hour -- set of contracts ; 7.hours -- set of contracts [...]
|
56
|
+
# instead of hard-coding in here
|
57
|
+
# TODO: add also to :member_to_human
|
58
|
+
# then commit
|
59
|
+
if tws
|
60
|
+
case swap[:contract][0...2]
|
61
|
+
when *%w[ GC SI PL PA HG NG CL HO RB ]
|
62
|
+
delta_datetime = 1.hour
|
63
|
+
when *%w[ GG DX ]
|
64
|
+
delta_datetime = 7.hours
|
65
|
+
else
|
66
|
+
delta_datetime = 0
|
67
|
+
end
|
68
|
+
else
|
69
|
+
delta_datetime = 0
|
70
|
+
end
|
71
|
+
daily = %i[ continuous daily ].include?(swap[:interval].to_sym) rescue false
|
72
|
+
datetime_format = daily ? '%Y-%m-%d' : '%Y-%m-%d %I:%M %p'
|
47
73
|
high = swap[:side] == :high
|
48
74
|
ohlc = high ? :high : :low
|
75
|
+
if notice.nil? and swap[:exceeded]
|
76
|
+
notice = "exceeded #{(swap[:exceeded] + delta_datetime).strftime(datetime_format)}"
|
77
|
+
end
|
78
|
+
if swap[:ignored]
|
79
|
+
notice += " IGNORED"
|
80
|
+
end
|
49
81
|
if short
|
50
|
-
|
51
|
-
}
|
52
|
-
}
|
53
|
-
}
|
54
|
-
|
55
|
-
}
|
56
|
-
}
|
57
|
-
|
58
|
-
|
59
|
-
|
82
|
+
res ="#{format '%7s', swap[:digest][...hash]
|
83
|
+
} #{ swap[:contract]
|
84
|
+
} #{ swap[:side].to_s
|
85
|
+
}".colorize( swap[:side] == :upper ? :light_green : :light_red ) +
|
86
|
+
" (#{ format '%4d', swap[:length]
|
87
|
+
},#{ format '%4d', swap[:rating]
|
88
|
+
},#{ format '%4d', swap[:depth]
|
89
|
+
}) P: #{ format '%6s', (format '%4.2f', swap[:ppi])
|
90
|
+
} #{
|
91
|
+
if swap[:current_value].nil?
|
92
|
+
"I: #{ format '%8s', (format format, swap[:members].last[ ohlc ]) }"
|
93
|
+
else
|
94
|
+
"C: #{ format '%8s', (format format, swap[:current_value]) } "
|
95
|
+
end
|
96
|
+
} [#{ (swap[:members].first[:datetime] + delta_datetime).strftime(datetime_format)
|
97
|
+
} - #{ (swap[:members].last[:datetime] + delta_datetime).strftime(datetime_format)
|
98
|
+
}]#{" NOTE: #{notice}" unless notice.nil?
|
99
|
+
}".colorize(swap[:color] || :white )
|
100
|
+
puts res
|
60
101
|
else
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
swap[:members].each {|x|
|
102
|
+
res = ["side: #{swap[:side] }\tlen: #{swap[:length]} \trating: #{swap[:rating]}".colorize(swap[:color] || :white )]
|
103
|
+
res << "diff: #{swap[:ticks]}\tdif: #{swap[:diff].round(7)}\tdepth: #{swap[:depth]}".colorize(swap[:color] || :white )
|
104
|
+
res << "tpi: #{swap[:tpi] }\tppi: #{swap[:ppi]}".colorize(swap[:color] || :white )
|
105
|
+
res << "NOTE: #{notice}".colorize(:light_white) unless notice.nil?
|
106
|
+
swap[:members].each {|x| res << member_to_human(x, side: swap[:side], format: format, daily: daily) }
|
107
|
+
res = res.join("\n")
|
108
|
+
puts res
|
66
109
|
end
|
110
|
+
res
|
67
111
|
end
|
68
112
|
|
69
113
|
# create a standardized name for the cache files
|
70
114
|
# and, on-the-fly, create these files plus their directory
|
71
115
|
def get_jsonl_name(interval:, swap_type:, contract:, sym: nil)
|
72
|
-
raise "Interval #{interval } is not supported, please choose from #{INTERVALS}" unless INTERVALS.include? interval
|
116
|
+
raise "Interval #{interval } is not supported, please choose from #{INTERVALS}" unless INTERVALS.include?(interval) || interval.is_a?(Integer)
|
73
117
|
raise "Swaptype #{swap_type} is not supported, please choose from #{SWAPTYPES}" unless SWAPTYPES.include? swap_type
|
74
118
|
sym ||= Cotcube::Helpers.get_id_set(contract: contract)
|
75
119
|
root = '/var/cotcube/level'
|
@@ -78,7 +122,9 @@ module Cotcube
|
|
78
122
|
`mkdir -p #{dir}` unless File.exist?(dir)
|
79
123
|
`ln -s #{dir} #{symlink}` unless File.exist?(symlink)
|
80
124
|
file = "#{dir}/#{contract}_#{interval.to_s}_#{swap_type.to_s}.jsonl"
|
81
|
-
|
125
|
+
unless File.exist? file
|
126
|
+
`touch #{file}`
|
127
|
+
end
|
82
128
|
file
|
83
129
|
end
|
84
130
|
|
@@ -114,7 +160,9 @@ module Cotcube
|
|
114
160
|
|
115
161
|
# loading of swaps is also straight forward
|
116
162
|
# it takes few more efforts to normalize the values to their expected format
|
117
|
-
|
163
|
+
#
|
164
|
+
# it is not too nice that some actual interactive process is done here in the load section
|
165
|
+
def load_swaps(interval:, swap_type:, contract:, sym: nil, datetime: nil, recent: false, digest: nil, quiet: false, exceed: false, keep_ignored: false)
|
118
166
|
file = get_jsonl_name(interval: interval, swap_type: swap_type, contract: contract, sym: sym)
|
119
167
|
jsonl = File.read(file)
|
120
168
|
data = jsonl.
|
@@ -123,32 +171,69 @@ module Cotcube
|
|
123
171
|
JSON.parse(x).
|
124
172
|
deep_transform_keys(&:to_sym).
|
125
173
|
tap do |sw|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
sw[:
|
134
|
-
|
135
|
-
|
174
|
+
sw[:datetime] = DateTime.parse(sw[:datetime]) rescue nil
|
175
|
+
(sw[:exceeded] = DateTime.parse(sw[:exceeded]) rescue nil) if sw[:exceeded]
|
176
|
+
(sw[:ignored] = DateTime.parse(sw[:ignored]) rescue nil) if sw[:ignored]
|
177
|
+
sw[:interval] = interval
|
178
|
+
sw[:swap_type] = swap_type
|
179
|
+
sw[:contract] = contract
|
180
|
+
%i[ side ].each {|key| sw[key] = sw[key].to_sym rescue false }
|
181
|
+
unless sw[:empty] or sw[:exceeded] or sw[:ignored]
|
182
|
+
sw[:color] = sw[:color].to_sym
|
183
|
+
sw[:members].map{|mem| mem[:datetime] = DateTime.parse(mem[:datetime]) }
|
184
|
+
end
|
136
185
|
end
|
137
186
|
end
|
138
187
|
# assign exceedance data to actual swaps
|
139
188
|
data.select{|swap| swap[:exceeded] }.each do |exc|
|
140
189
|
swap = data.find{|ref| ref[:digest] == exc[:ref]}
|
141
|
-
raise RuntimeError, "
|
190
|
+
raise RuntimeError, "Inconsistent history for '#{exc}'. Origin not found." if swap.nil?
|
142
191
|
swap[:exceeded] = exc[:exceeded]
|
143
192
|
end
|
193
|
+
# assign ignorance data to actual swaps
|
194
|
+
data.select{|swap| swap[:ignored] }.each do |ign|
|
195
|
+
swap = data.find{|ref| ref[:digest] == ign[:ref]}
|
196
|
+
raise RuntimeError, "Inconsistent history for '#{ign}'. Origin not found." if swap.nil?
|
197
|
+
swap[:ignored] = ign[:ignored]
|
198
|
+
end
|
144
199
|
# do not return bare exceedance information
|
145
|
-
data.reject!{|swap| swap[:exceeded] and swap[:members].nil? }
|
200
|
+
data.reject!{|swap| (swap[:ignored] or swap[:exceeded]) and swap[:members].nil? }
|
146
201
|
# do not return swaps that are found 'later'
|
147
202
|
data.reject!{|swap| swap[:datetime] > datetime } unless datetime.nil?
|
148
203
|
# do not return exceeded swaps, that are exceeded in the past
|
149
|
-
|
204
|
+
recent = 7.days if recent.is_a? TrueClass
|
205
|
+
recent += 5.hours if recent
|
206
|
+
data.reject!{|swap| swap[:ignored] } unless keep_ignored
|
207
|
+
data.reject!{|swap| swap[:exceeded] and swap[:exceeded] < datetime - (recent ? recent : 0) } unless datetime.nil?
|
150
208
|
# remove exceedance information that is found 'later'
|
151
|
-
data.map{|swap| swap.delete(:exceeded) if swap[:exceeded] and swap[:exceeded] > datetime}
|
209
|
+
data.map{|swap| swap.delete(:exceeded) if swap[:exceeded] and swap[:exceeded] > datetime} unless datetime.nil?
|
210
|
+
unless digest.nil?
|
211
|
+
data.select! do |z|
|
212
|
+
(Cotcube::Helpers.sub(minimum: digest.length){ z[:digest] } === digest) and
|
213
|
+
not z[:empty]
|
214
|
+
end
|
215
|
+
case data.size
|
216
|
+
when 0
|
217
|
+
puts "No swaps found for digest '#{digest}'." unless quiet
|
218
|
+
when 1
|
219
|
+
sym ||= Cotcube::Helpers.get_id_set(contract: contract)
|
220
|
+
if not quiet or exceed
|
221
|
+
puts "Found 1 digest: "
|
222
|
+
data.each {|d| puts_swap( d, format: sym[:format], short: true, hash: digest.size + 2) }
|
223
|
+
if exceed
|
224
|
+
exceed = DateTime.now if exceed.is_a? TrueClass
|
225
|
+
mark_exceeded(swap: data.first, datetime: exceed)
|
226
|
+
puts "Swap marked exceeded."
|
227
|
+
end
|
228
|
+
end
|
229
|
+
else
|
230
|
+
sym ||= Cotcube::Helpers.get_id_set(contract: contract)
|
231
|
+
unless quiet
|
232
|
+
puts "Too many digests found for digest '#{digest}', please consider sending more figures: "
|
233
|
+
data.each {|d| puts_swap( d, format: sym[:format], short: true, hash: digest.size + 3)}
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
152
237
|
data
|
153
238
|
end
|
154
239
|
|
@@ -170,11 +255,34 @@ module Cotcube
|
|
170
255
|
save_swaps to_save, interval: swap[:interval], swap_type: swap[:swap_type], contract: contract, sym: sym, quiet: (not debug)
|
171
256
|
swap[:exceeded] = update[:exceeded]
|
172
257
|
end
|
173
|
-
%i[ current_change current_value current_diff current_dist ].map{|key| swap[key] = update[key] }
|
258
|
+
%i[ current_change current_value current_diff current_dist alert].map{|key| swap[key] = update[key] }
|
174
259
|
swap
|
175
260
|
end.compact
|
176
261
|
end
|
177
262
|
|
263
|
+
def mark_exceeded(swap:, datetime:, debug: false, sym: nil)
|
264
|
+
to_save = {
|
265
|
+
datetime: datetime,
|
266
|
+
ref: swap[:digest],
|
267
|
+
side: swap[:side],
|
268
|
+
exceeded: datetime
|
269
|
+
}
|
270
|
+
sym ||= Cotcube::Helpers.get_id_set(contract: swap[:contract])
|
271
|
+
save_swaps to_save, interval: swap[:interval], swap_type: swap[:swap_type], sym: sym, contract: swap[:contract], quiet: (not debug)
|
272
|
+
swap
|
273
|
+
end
|
274
|
+
|
275
|
+
def mark_ignored(swap:, datetime: DateTime.now, sym: , debug: true)
|
276
|
+
to_save = {
|
277
|
+
datetime: datetime,
|
278
|
+
ref: swap[:digest],
|
279
|
+
side: swap[:side],
|
280
|
+
ignored: datetime
|
281
|
+
}
|
282
|
+
save_swaps to_save, interval: swap[:interval], swap_type: swap[:swap_type], sym: sym, contract: swap[:contract], quiet: (not debug)
|
283
|
+
swap
|
284
|
+
end
|
285
|
+
|
178
286
|
end
|
179
287
|
end
|
180
288
|
|
@@ -1,12 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Cotcube
|
2
4
|
module Level
|
3
|
-
|
5
|
+
|
4
6
|
class Intraday_Stencil
|
5
7
|
|
6
|
-
GLOBAL_SOW = { 'CT' => '0000-1700' }
|
7
|
-
GLOBAL_EOW = { 'CT' => '1700-0000' }
|
8
|
-
GLOBAL_EOD = { 'CT' => '1600-1700' }
|
9
8
|
|
9
|
+
# Class method that loads the (latest) shiftset for given asset
|
10
|
+
# These raw stencils are located in /var/cotcube/level/stencils/shiftsets.csv
|
11
|
+
#
|
10
12
|
|
11
13
|
def self.shiftset(asset:, sym: nil)
|
12
14
|
shiftset_file = '/var/cotcube/level/stencils/shiftsets.csv'
|
@@ -18,45 +20,66 @@ module Cotcube
|
|
18
20
|
sym ||= Cotcube::Helpers.get_id_set(symbol: asset)
|
19
21
|
current_set = shiftsets.find{|s| s[:symbols] =~ /#{sym[:type]}/ }
|
20
22
|
return current_set.tap{|s| headers.map{|h| s[h] = nil if s[h] == '---------' }; s[:rth5] ||= s[:rth]; s[:mpost5] ||= s[:mpost] } unless current_set.nil?
|
21
|
-
raise "Cannot get shiftset for #{sym[:type]}: #{asset}, please prepare #{shiftset_file} before!"
|
23
|
+
raise "Cannot get shiftset for #{sym[:type]}: #{asset}, please prepare #{shiftset_file} before!"
|
22
24
|
end
|
23
25
|
|
24
26
|
attr_reader :base, :shiftset, :timezone, :datetime, :zero, :index
|
25
27
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
@
|
42
|
-
@
|
43
|
-
|
28
|
+
|
29
|
+
|
30
|
+
def initialize(
|
31
|
+
asset:,
|
32
|
+
interval: 30.minutes,
|
33
|
+
swap_type: :full,
|
34
|
+
datetime: nil,
|
35
|
+
debug: false,
|
36
|
+
weeks: 6,
|
37
|
+
future: 2,
|
38
|
+
measuring: false,
|
39
|
+
version: nil, # when referring to a specicic version of the stencil
|
40
|
+
stencil: nil, # instead of preparing, use this one if set
|
41
|
+
warnings: true # be more quiet
|
42
|
+
)
|
43
|
+
@shiftset = Intraday_Stencil.shiftset(asset: asset)
|
44
|
+
@timezone = Cotcube::Level::TIMEZONES[@shiftset[:tz]]
|
45
|
+
@debug = debug
|
46
|
+
@interval = interval
|
47
|
+
@swap_type = swap_type
|
48
|
+
@warnings = warnings
|
49
|
+
datetime ||= DateTime.now
|
50
|
+
datetime = @timezone.at(datetime.to_i) unless datetime.is_a? ActiveSupport::TimeWithZone
|
51
|
+
@datetime = datetime.beginning_of_day
|
52
|
+
@datetime += interval while @datetime <= datetime - interval
|
53
|
+
@datetime -= interval
|
54
|
+
|
55
|
+
now = DateTime.now
|
56
|
+
measure = lambda {|x| puts "\nMeasured #{(Time.now - now).to_f.round(2)}: ".colorize(:light_yellow) + x + "\n\n" if measuring }
|
57
|
+
|
58
|
+
measure.call "Starting initialization for asset '#{asset}' "
|
59
|
+
|
60
|
+
const = "RAW_INTRA_STENCIL_#{@shiftset[:nr]}_#{interval.in_minutes.to_i}_#{weeks}_#{future}".to_sym
|
61
|
+
cachefile = "/var/cotcube/level/stencils/cache/#{swap_type.to_s}_#{interval}_#{@datetime.strftime('%Y-%m-%d-%H-%M')}.json"
|
62
|
+
|
44
63
|
if Object.const_defined? const
|
64
|
+
measure.call 'getting cached base from memory'
|
45
65
|
@base = (Object.const_get const).map{|z| z.dup}
|
66
|
+
elsif File.exist? cachefile
|
67
|
+
measure.call 'getting cached base from file'
|
68
|
+
@base = JSON.parse(File.read(cachefile), symbolize_names: true).map{|z| z[:datetime] = DateTime.parse(z[:datetime]); z[:type] = z[:type].to_sym; z }
|
46
69
|
else
|
47
|
-
|
70
|
+
measure.call 'creating base from shiftset'
|
48
71
|
start_time = lambda {|x| @shiftset[x].split('-').first rescue '' }
|
49
72
|
start_hours = lambda {|x| @shiftset[x].split('-').first[ 0.. 1].to_i.send(:hours) rescue 0 }
|
50
73
|
start_minutes = lambda {|x| @shiftset[x].split('-').first[-2..-1].to_i.send(:minutes) rescue 0 }
|
51
74
|
end_time = lambda {|x| @shiftset[x].split('-').last rescue '' }
|
52
75
|
end_hours = lambda {|x| @shiftset[x].split('-').last [ 0.. 1].to_i.send(:hours) rescue 0 }
|
53
|
-
end_minutes = lambda {|x| @shiftset[x].split('-').last [-2..-1].to_i.send(:minutes) rescue 0 }
|
76
|
+
end_minutes = lambda {|x| @shiftset[x].split('-').last [-2..-1].to_i.send(:minutes) rescue 0 }
|
54
77
|
|
55
|
-
runner = (@datetime -
|
78
|
+
runner = (@datetime -
|
56
79
|
weeks * 7.days).beginning_of_week(:sunday)
|
57
80
|
tm_runner = lambda { runner.strftime('%H%M') }
|
58
|
-
@base = []
|
59
|
-
(weeks+future).times do
|
81
|
+
@base = []
|
82
|
+
(weeks+future).times do
|
60
83
|
while tm_runner.call < GLOBAL_SOW[@shiftset[:tz]].split('-').last
|
61
84
|
# if daylight is switched, this phase will be shorter or longer
|
62
85
|
@base << { datetime: runner, type: :sow }
|
@@ -81,10 +104,10 @@ module Cotcube
|
|
81
104
|
yet_rth = true
|
82
105
|
end
|
83
106
|
end
|
84
|
-
while ((sophase > eophase) ? (tm_runner.call >= sophase or tm_runner.call < eophase) : (tm_runner.call < eophase))
|
107
|
+
while ((sophase > eophase) ? (tm_runner.call >= sophase or tm_runner.call < eophase) : (tm_runner.call < eophase))
|
85
108
|
current = { datetime: runner, type: phase }
|
86
109
|
if phase == :rth and not yet_rth
|
87
|
-
current[:block] = true
|
110
|
+
current[:block] = true
|
88
111
|
yet_rth = true
|
89
112
|
end
|
90
113
|
@base << current
|
@@ -100,80 +123,111 @@ module Cotcube
|
|
100
123
|
while runner < end_of_week
|
101
124
|
@base << { datetime: runner, type: :eow }
|
102
125
|
runner += interval
|
103
|
-
end
|
126
|
+
end
|
104
127
|
end
|
105
128
|
Object.const_set(const, @base.map{|z| z.dup})
|
129
|
+
File.open(cachefile, 'w'){|f| f.write(@base.to_json)}
|
130
|
+
end
|
131
|
+
measure.call "base created with #{@base.size} records"
|
132
|
+
|
133
|
+
case swap_type
|
134
|
+
when :full
|
135
|
+
@base.select!{|x| %i[ pre rth post ].include?(x[:type]) }
|
136
|
+
when :rth
|
137
|
+
@base.select!{|x| x[:type] == :rth }
|
138
|
+
when :flow
|
139
|
+
@base.reject!{|x| %i[ sow eow mpost mpost ].include?(x[:type]) }
|
140
|
+
@base.
|
141
|
+
map{ |x|
|
142
|
+
[:high, :low, :volume].map{|z| x[z] = nil} unless x[:type] == :rth
|
143
|
+
}
|
144
|
+
when :run
|
145
|
+
@base.select!{|x| %i[ pre rth post ].include? x[:type]}
|
146
|
+
else
|
147
|
+
raise ArgumentError, "Unknown stencil/swap type '#{swap_type}'"
|
106
148
|
end
|
107
|
-
|
108
|
-
@
|
109
|
-
|
149
|
+
measure.call "swaptype #{swap_type} applied"
|
150
|
+
@base.map!{|z| z.dup}
|
151
|
+
measure.call 'base dupe\'d'
|
152
|
+
|
153
|
+
# zero is, were either x[:datetime] == @datetime (when we are intraday)
|
154
|
+
# or otherwise {x[:datetime] <= @datetime}.last (when on maintenance)
|
155
|
+
selector = @base.select{|y| y[:datetime] <= @datetime }.last
|
156
|
+
@index = @base.index{|x| x == selector }
|
157
|
+
measure.call 'index selected'
|
158
|
+
@index -= 1 while %i[sow sod mpre mpost eod eow].include? @base[@index][:type]
|
159
|
+
measure.call 'index adjusted'
|
110
160
|
@datetime = @base[@index][:datetime]
|
111
161
|
@zero = @base[@index]
|
112
|
-
counter = 0
|
162
|
+
counter = 0
|
163
|
+
measure.call "Applying counter to past"
|
113
164
|
while @base[@index - counter] and @index - counter >= 0
|
114
165
|
@base[@index - counter][:x] = counter
|
115
166
|
counter += 1
|
116
167
|
end
|
117
|
-
counter = 0
|
168
|
+
counter = 0
|
169
|
+
measure.call "Applying counter to future"
|
118
170
|
while @base[@index + counter] and @index + counter < @base.length
|
119
171
|
@base[@index + counter][:x] = -counter
|
120
172
|
counter += 1
|
121
173
|
end
|
122
|
-
|
174
|
+
measure.call 'initialization finished'
|
175
|
+
end
|
176
|
+
|
177
|
+
def zero
|
178
|
+
@zero ||= @base.find{|b| b[:x].zero? }
|
123
179
|
end
|
124
180
|
|
125
|
-
def
|
126
|
-
|
181
|
+
def index(offset = 0)
|
182
|
+
@index ||= @base.index{|b| b[:x].zero? }
|
183
|
+
@base[@index + offset]
|
127
184
|
end
|
128
185
|
|
129
|
-
|
130
|
-
def apply(to
|
186
|
+
|
187
|
+
def apply(to: )
|
131
188
|
offset = 0
|
132
|
-
|
189
|
+
@base.each_index do |i|
|
133
190
|
begin
|
134
|
-
offset += 1 while
|
135
|
-
puts "#{i}\t#{offset}\t#{@base[i+offset][:datetime]} < #{to[i][:datetime]}" if debug
|
191
|
+
offset += 1 while to[i+offset][:datetime] < @base[i][:datetime]
|
136
192
|
rescue
|
137
193
|
# appending
|
138
|
-
|
139
|
-
@base << to[i]
|
194
|
+
to << @base[i]
|
140
195
|
next
|
141
196
|
end
|
142
|
-
if
|
197
|
+
if to[i+offset][:datetime] > @base[i][:datetime]
|
143
198
|
# skipping
|
144
|
-
puts "skipping #{i}\t#{offset}\t#{@base[i+offset][:datetime]} < #{to[i][:datetime]}" if debug
|
145
199
|
offset -= 1
|
146
200
|
next
|
147
201
|
end
|
148
202
|
# merging
|
149
|
-
|
150
|
-
|
151
|
-
puts "MERGED:\t#{i}\t#{offset}\t#{@base[j]}" if debug
|
203
|
+
to[i+offset][:x] = @base[i][:x]
|
204
|
+
to[i+offset][:type] = @base[i][:type]
|
152
205
|
end
|
153
206
|
# finally remove all bars that do not belong to the stencil (i.e. holidays)
|
154
|
-
|
155
|
-
when :full
|
156
|
-
@base.select!{|x| %i[ pre rth post ].include?(x[:type]) }
|
157
|
-
when :rth
|
158
|
-
@base.select!{|x| x[:type] == :rth }
|
159
|
-
# to.map{ |x| [:high, :low, :volume].map{|z| x[z] = nil} if x[:block] }
|
160
|
-
when :flow
|
161
|
-
@base.reject!{|x| %i[ meow postmm postmm5 ].include?(x[:type]) }
|
162
|
-
@base.
|
163
|
-
map{ |x|
|
164
|
-
[:high, :low, :volume].map{|z| x[z] = nil} unless x[:type] == :rth
|
165
|
-
# [:high, :low, :volume].map{|z| x[z] = nil} if x[:block]
|
166
|
-
}
|
167
|
-
when :run
|
168
|
-
@base.select!{|x| %i[ premarket rth postmarket ].include? x[:type]}
|
169
|
-
else
|
170
|
-
raise ArgumentError, "Unknown stencil/swap type '#{type}'"
|
171
|
-
end
|
172
|
-
@base.map!{|z| z.dup}
|
207
|
+
to.reject!{|x| x[:x].nil? }
|
173
208
|
end
|
174
209
|
|
210
|
+
def use(with:, sym:, zero: nil, grace: -2)
|
211
|
+
# todo: validate with (check if vslid swap
|
212
|
+
# sym (check keys)
|
213
|
+
# zero (ohlc with x.zero?)
|
214
|
+
# side ( upper or lower)
|
215
|
+
swap = with.dup
|
216
|
+
high = swap[:side] == :upper
|
217
|
+
ohlc = high ? :high : :low
|
218
|
+
start = base.find{|x| swap[:datetime] == x[:datetime]}
|
219
|
+
swap[:current_change] = (swap[:tpi] * start[:x]).round(8)
|
220
|
+
swap[:current_value] = swap[:members].last[ ohlc ] + swap[:current_change] * sym[:ticksize]
|
221
|
+
unless zero.nil?
|
222
|
+
swap[:current_diff] = (swap[:current_value] - zero[ohlc]) * (high ? 1 : -1 )
|
223
|
+
swap[:current_dist] = (swap[:current_diff] / sym[:ticksize]).to_i
|
224
|
+
swap[:alert] = (swap[:current_diff] / zero[:atr5]).round(2)
|
225
|
+
swap[:exceeded] = zero[:datetime] if swap[:current_dist] < grace
|
226
|
+
end
|
227
|
+
swap
|
228
|
+
end
|
175
229
|
end
|
176
230
|
|
177
|
-
Intraday_Stencils = Intraday_Stencil
|
178
231
|
end
|
232
|
+
|
179
233
|
end
|
@@ -19,6 +19,7 @@ module Cotcube
|
|
19
19
|
swap_type: nil, # if not given, a warning is printed and swaps won't be saved or loaded
|
20
20
|
with_flaws: 0, # the maximum amount of consecutive bars that would actually break the current swap
|
21
21
|
# should be set to 0 for dailies and I suggest no more than 3 for intraday
|
22
|
+
manual: false, # some triggers must be set differently when manual entry is used
|
22
23
|
deviation: 2 # the maximum shift of :x-values of found members
|
23
24
|
)
|
24
25
|
|
@@ -70,8 +71,10 @@ module Cotcube
|
|
70
71
|
|
71
72
|
# abs_peak is the absolute high / low of the base. the shearing operation ends there,
|
72
73
|
# but results might be influenced when abs_peak becomes affected by :with_flaws
|
73
|
-
|
74
|
-
|
74
|
+
unless manual
|
75
|
+
abs_peak = base.send(high ? :max_by : :min_by){|x| x[high ? :high : :low] }[:datetime]
|
76
|
+
base.reject!{|x| x[:datetime] < abs_peak}
|
77
|
+
end
|
75
78
|
|
76
79
|
###########################################################################################################################z
|
77
80
|
# only if (and only if) the range portion above change the underlying base
|
@@ -155,7 +158,7 @@ module Cotcube
|
|
155
158
|
# first member is solitary
|
156
159
|
if new_members.empty?
|
157
160
|
mem_sorted=members.sort
|
158
|
-
if mem_sorted[1] == mem_sorted[0] + 1
|
161
|
+
if mem_sorted[1] == mem_sorted[0] + 1 and not manual
|
159
162
|
b2 = b[mem_sorted[1]..mem_sorted[-1]].map{|x| x.dup; x[:dx] = nil; x}
|
160
163
|
puts 'starting recursive rerun'.light_red if debug
|
161
164
|
alternative_slope = get_slope.call(b2)
|
data/lib/cotcube-level.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cotcube-level
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.2
|
4
|
+
version: 0.3.4.2
|
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-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -120,6 +120,8 @@ files:
|
|
120
120
|
- Gemfile
|
121
121
|
- README.md
|
122
122
|
- VERSION
|
123
|
+
- bin/iswaps.rb
|
124
|
+
- bin/swaps.rb
|
123
125
|
- cotcube-level.gemspec
|
124
126
|
- lib/cotcube-level.rb
|
125
127
|
- lib/cotcube-level/detect_slope.rb
|
@@ -149,7 +151,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
149
151
|
- !ruby/object:Gem::Version
|
150
152
|
version: '0'
|
151
153
|
requirements: []
|
152
|
-
rubygems_version: 3.1.
|
154
|
+
rubygems_version: 3.1.6
|
153
155
|
signing_key:
|
154
156
|
specification_version: 4
|
155
157
|
summary: A gem to shear a time series
|