cotcube-level 0.2.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +14 -6
- data/VERSION +1 -1
- data/cotcube-level.gemspec +1 -1
- data/lib/cotcube-level/detect_slope.rb +79 -98
- data/lib/cotcube-level/{stencil.rb → eod_stencil.rb} +10 -13
- data/lib/cotcube-level/helpers.rb +84 -75
- data/lib/cotcube-level/intraday_stencil.rb +179 -0
- data/lib/cotcube-level/tritangulate.rb +323 -0
- data/lib/cotcube-level.rb +15 -13
- metadata +6 -5
- data/lib/cotcube-level/triangulate.rb +0 -238
@@ -0,0 +1,179 @@
|
|
1
|
+
module Cotcube
|
2
|
+
module Level
|
3
|
+
# TODO: Missing documentation of shiftsets, swap_types and stencil_types
|
4
|
+
class Intraday_Stencil
|
5
|
+
|
6
|
+
GLOBAL_SOW = { 'CT' => '0000-1700' }
|
7
|
+
GLOBAL_EOW = { 'CT' => '1700-0000' }
|
8
|
+
GLOBAL_EOD = { 'CT' => '1600-1700' }
|
9
|
+
|
10
|
+
|
11
|
+
def self.shiftset(asset:, sym: nil)
|
12
|
+
shiftset_file = '/var/cotcube/level/stencils/shiftsets.csv'
|
13
|
+
headers = %i[nr tz sod pre mpre rth post mpost rth5 mpost5 symbols]
|
14
|
+
shiftsets = CSV.read(shiftset_file, headers: headers).
|
15
|
+
map{|x| x.to_h}
|
16
|
+
current_set = shiftsets.find{|s| s[:symbols] =~ /#{asset}/ }
|
17
|
+
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?
|
18
|
+
sym ||= Cotcube::Helpers.get_id_set(symbol: asset)
|
19
|
+
current_set = shiftsets.find{|s| s[:symbols] =~ /#{sym[:type]}/ }
|
20
|
+
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!"
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :base, :shiftset, :timezone, :datetime, :zero, :index
|
25
|
+
|
26
|
+
# asset: the asset the stencil will be applied to--or :full, if default stencil is desired
|
27
|
+
# datetime: the datetime that will become 'zero'.
|
28
|
+
# it will be calculated to the beginning of the previous interval
|
29
|
+
# it must match the timezone of the asset
|
30
|
+
# interval: the interval as minutes
|
31
|
+
# weeks: the amount of weeks before the beginning of the current week
|
32
|
+
# future: the amount of weeks after the beginning of the current week
|
33
|
+
def initialize(asset:, sym: nil, datetime:, interval:, weeks:, future: 1, debug: false, type:, base: )
|
34
|
+
@shiftset = Intraday_Stencils.shiftset(asset: asset)
|
35
|
+
@timezone = TIMEZONES[@shiftset[:tz]]
|
36
|
+
@debug = debug
|
37
|
+
datetime = @timezone.at(datetime.to_i) unless datetime.is_a? ActiveSupport::TimeWithZone
|
38
|
+
# slight flaw, as datetime does not carry the actuall timezone information but just the abbr. timezone name (like CDT or CEST)
|
39
|
+
raise "Zone mismatch: Timezone of asset is #{@timezone.now.zone} but datetime given is #{dateime.zone}" unless @timezone.now.zone == datetime.zone
|
40
|
+
@datetime = datetime.beginning_of_day
|
41
|
+
@datetime += interval while @datetime <= datetime - interval
|
42
|
+
@datetime -= interval
|
43
|
+
const = "RAW_INTRA_STENCIL_#{@shiftset[:nr]}_#{interval.in_minutes.to_i}".to_sym
|
44
|
+
if Object.const_defined? const
|
45
|
+
@base = (Object.const_get const).map{|z| z.dup}
|
46
|
+
else
|
47
|
+
|
48
|
+
start_time = lambda {|x| @shiftset[x].split('-').first rescue '' }
|
49
|
+
start_hours = lambda {|x| @shiftset[x].split('-').first[ 0.. 1].to_i.send(:hours) rescue 0 }
|
50
|
+
start_minutes = lambda {|x| @shiftset[x].split('-').first[-2..-1].to_i.send(:minutes) rescue 0 }
|
51
|
+
end_time = lambda {|x| @shiftset[x].split('-').last rescue '' }
|
52
|
+
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 }
|
54
|
+
|
55
|
+
runner = (@datetime -
|
56
|
+
weeks * 7.days).beginning_of_week(:sunday)
|
57
|
+
tm_runner = lambda { runner.strftime('%H%M') }
|
58
|
+
@base = []
|
59
|
+
(weeks+future).times do
|
60
|
+
while tm_runner.call < GLOBAL_SOW[@shiftset[:tz]].split('-').last
|
61
|
+
# if daylight is switched, this phase will be shorter or longer
|
62
|
+
@base << { datetime: runner, type: :sow }
|
63
|
+
runner += interval
|
64
|
+
end
|
65
|
+
end_of_week = runner + 6.days + 7.hours
|
66
|
+
|
67
|
+
5.times do |i|
|
68
|
+
# TODO: mark holidays as such
|
69
|
+
[:sod, :pre, :mpre, (i<4 ? :rth : :rth5), :post, (i<4 ? :mpost : :mpost5)].each do |phase|
|
70
|
+
yet_rth = false
|
71
|
+
unless start_time.call(phase).empty?
|
72
|
+
eophase = end_time.call(phase)
|
73
|
+
sophase = start_time.call(phase)
|
74
|
+
phase = :rth if phase == :rth5
|
75
|
+
phase = :mpost if phase == :mpost5
|
76
|
+
if %i[ pre rth ].include? phase and tm_runner.call > sophase
|
77
|
+
# fix previous interval
|
78
|
+
@base.last[:type] = phase
|
79
|
+
if phase == :rth and not yet_rth
|
80
|
+
@base.last[:block] = true
|
81
|
+
yet_rth = true
|
82
|
+
end
|
83
|
+
end
|
84
|
+
while ((sophase > eophase) ? (tm_runner.call >= sophase or tm_runner.call < eophase) : (tm_runner.call < eophase))
|
85
|
+
current = { datetime: runner, type: phase }
|
86
|
+
if phase == :rth and not yet_rth
|
87
|
+
current[:block] = true
|
88
|
+
yet_rth = true
|
89
|
+
end
|
90
|
+
@base << current
|
91
|
+
runner += interval
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
while tm_runner.call < GLOBAL_EOD[@shiftset[:tz]].split('-').last
|
96
|
+
@base << { datetime: runner, type: :eod }
|
97
|
+
runner += interval
|
98
|
+
end
|
99
|
+
end # 5.times
|
100
|
+
while runner < end_of_week
|
101
|
+
@base << { datetime: runner, type: :eow }
|
102
|
+
runner += interval
|
103
|
+
end
|
104
|
+
end
|
105
|
+
Object.const_set(const, @base.map{|z| z.dup})
|
106
|
+
end
|
107
|
+
self.apply to: base, type: type
|
108
|
+
@index = @base.index{|x| x[:datetime] == @datetime }
|
109
|
+
@index -= 1 while %i[sow sod mpre mpost eod eow].include? @base[@index][:type]
|
110
|
+
@datetime = @base[@index][:datetime]
|
111
|
+
@zero = @base[@index]
|
112
|
+
counter = 0
|
113
|
+
while @base[@index - counter] and @index - counter >= 0
|
114
|
+
@base[@index - counter][:x] = counter
|
115
|
+
counter += 1
|
116
|
+
end
|
117
|
+
counter = 0
|
118
|
+
while @base[@index + counter] and @index + counter < @base.length
|
119
|
+
@base[@index + counter][:x] = -counter
|
120
|
+
counter += 1
|
121
|
+
end
|
122
|
+
@base.select!{|z| z[:x] <= 0 or z[:high]}
|
123
|
+
end
|
124
|
+
|
125
|
+
def apply!(to:, type:)
|
126
|
+
apply(to: to, type: type, force: true)
|
127
|
+
end
|
128
|
+
|
129
|
+
# :force will apply values to each bar regardless of existing ones
|
130
|
+
def apply(to:, type:, force: false, debug: false)
|
131
|
+
offset = 0
|
132
|
+
to.each_index do |i|
|
133
|
+
begin
|
134
|
+
offset += 1 while @base[i+offset][:datetime] < to[i][:datetime]
|
135
|
+
puts "#{i}\t#{offset}\t#{@base[i+offset][:datetime]} < #{to[i][:datetime]}" if debug
|
136
|
+
rescue
|
137
|
+
# appending
|
138
|
+
puts "appending #{i}\t#{offset}\t#{@base[i+offset][:datetime]} < #{to[i][:datetime]}" if debug
|
139
|
+
@base << to[i]
|
140
|
+
next
|
141
|
+
end
|
142
|
+
if @base[i+offset][:datetime] > to[i][:datetime]
|
143
|
+
# skipping
|
144
|
+
puts "skipping #{i}\t#{offset}\t#{@base[i+offset][:datetime]} < #{to[i][:datetime]}" if debug
|
145
|
+
offset -= 1
|
146
|
+
next
|
147
|
+
end
|
148
|
+
# merging
|
149
|
+
j = i + offset
|
150
|
+
@base[j]=@base[j].merge(to[i]) if force or (@base[j][:high].nil? and @base[j][:low].nil?)
|
151
|
+
puts "MERGED:\t#{i}\t#{offset}\t#{@base[j]}" if debug
|
152
|
+
end
|
153
|
+
# finally remove all bars that do not belong to the stencil (i.e. holidays)
|
154
|
+
case type
|
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}
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
|
177
|
+
Intraday_Stencils = Intraday_Stencil
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,323 @@
|
|
1
|
+
module Cotcube
|
2
|
+
module Level
|
3
|
+
def tritangulate(
|
4
|
+
contract: nil, # contract actually isnt needed for tritangulation, but allows much more convenient output
|
5
|
+
# on some occasion here can also be given a :symbol, but this requires :sym to be set
|
6
|
+
sym: nil, # sym is the id set provided by Cotcube::Helper.get_id_set
|
7
|
+
side:, # :upper or :lower
|
8
|
+
base:, # the base of a readily injected stencil
|
9
|
+
range: (0..-1), # range is relative to base
|
10
|
+
max: 90, # the range which to scan for swaps goes from deg 0 to max
|
11
|
+
debug: false,
|
12
|
+
min_rating: 3, # swaps having a lower rating are discarded
|
13
|
+
min_length: 8, # shorter swaps are discared
|
14
|
+
save: true, # allow saving of results
|
15
|
+
cached: true, # allow loading of cached results
|
16
|
+
interval: , # interval (currently) is one of %i[ daily continuous halfs ]
|
17
|
+
swap_type: nil, # if not given, a warning is printed and swaps won't be saved or loaded
|
18
|
+
with_flaws: 0, # the maximum amount of consecutive bars that would actually break the current swap
|
19
|
+
# should be set to 0 for dailies and I suggest no more than 3 for intraday
|
20
|
+
deviation: 2) # the maximum shift of :x-values of found members
|
21
|
+
|
22
|
+
raise ArgumentError, "'0 < max < 90, but got '#{max}'" unless max.is_a? Numeric and 0 < max and max <= 90
|
23
|
+
raise ArgumentError, 'need :side either :upper or :lower for dots' unless [:upper, :lower].include? side
|
24
|
+
|
25
|
+
###########################################################################################################################
|
26
|
+
# init some helpers
|
27
|
+
#
|
28
|
+
high = side == :upper
|
29
|
+
# DELETE first = base.to_a.find{|x| not x[:high].nil? }
|
30
|
+
zero = base.select{|x| x[:x].zero? }
|
31
|
+
raise ArgumentError, "Inappropriate base, it should contain ONE :x.zero, but contains #{zero.size}." unless zero.size==1
|
32
|
+
zero = zero.first
|
33
|
+
|
34
|
+
contract ||= zero[:contract]
|
35
|
+
sym ||= Cotcube::Helpers.get_id_set(contract: contract)
|
36
|
+
|
37
|
+
if cached
|
38
|
+
if interval.nil? or swap_type.nil?
|
39
|
+
puts "Warning: Cannot use cache as both :interval and :swap_type must be given".light_yellow
|
40
|
+
else
|
41
|
+
cache = load_swaps(interval: interval, swap_type: swap_type, contract: contract, sym: sym)
|
42
|
+
# if the current datetime has been yet processed but nothing has been found,
|
43
|
+
# an 'empty' value is saved.
|
44
|
+
# that means, if neither an array of swaps nor :empty is found, the datetime has not been processed yet
|
45
|
+
selected = cache.select{|sw| sw[:datetime] == zero[:datetime] and sw[:side] == side}
|
46
|
+
unless selected.empty?
|
47
|
+
puts 'cache_hit'.light_white if debug
|
48
|
+
return (selected.first[:empty] ? [] : selected )
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
###########################################################################################################################
|
54
|
+
# prepare base (i.e. dupe the original, create proper :y, and reject unneeded items)
|
55
|
+
#
|
56
|
+
base = base.
|
57
|
+
map { |x|
|
58
|
+
y = x.dup
|
59
|
+
y[:y] = (high ?
|
60
|
+
(y[:high] - zero[:high]).round(8) :
|
61
|
+
(zero[:low] - y[:low]).round(8)
|
62
|
+
) unless y[:high].nil?
|
63
|
+
y
|
64
|
+
}.
|
65
|
+
reject{|b| b.nil? or b[:datetime] < first[:datetime] or b[:x] < 0 or b[:y].nil?}[range]
|
66
|
+
|
67
|
+
# abs_peak is the absolute high / low of the base. the shearing operation ends there,
|
68
|
+
# but results might be influenced when abs_peak becomes affected by :with_flaws
|
69
|
+
abs_peak = base.send(high ? :max_by : :min_by){|x| x[high ? :high : :low] }[:datetime]
|
70
|
+
base.reject!{|x| x[:datetime] < abs_peak}
|
71
|
+
|
72
|
+
###########################################################################################################################z
|
73
|
+
# only if (and only if) the range portion above change the underlying base
|
74
|
+
# the offset has to be fixed for :x and :y
|
75
|
+
|
76
|
+
unless range == (0..-1)
|
77
|
+
puts "adjusting range to '#{range}'".light_yellow if debug
|
78
|
+
offset_x = base.last[:x]
|
79
|
+
offset_y = base.last[:y]
|
80
|
+
base.map!{|b| b[:x] -= offset_x; b[:y] -= offset_y ; b}
|
81
|
+
end
|
82
|
+
|
83
|
+
###########################################################################################################################
|
84
|
+
# introducing :i to the base, which provides the negative index of the :base Array of the current element
|
85
|
+
# this simplifies handling during the, where I can use the members array,
|
86
|
+
# that will carry just the index of the original base, regardless how many array_members have be already dropped
|
87
|
+
base.each_index.map{|i| base[i][:i] = -base.size + i }
|
88
|
+
|
89
|
+
|
90
|
+
###########################################################################################################################
|
91
|
+
# LAMBDA no1: simplifying DEBUG output
|
92
|
+
#
|
93
|
+
present = lambda {|z| z.slice(*%i[datetime high low x y i yy dx dev near miss dev]) }
|
94
|
+
|
95
|
+
|
96
|
+
###########################################################################################################################
|
97
|
+
# LAMBDA no2: all members except the pivot itself now most probably are too far to the left
|
98
|
+
# finalizing tries to get the proper dx value for them
|
99
|
+
#
|
100
|
+
finalize = lambda do |results|
|
101
|
+
results.map do |result|
|
102
|
+
result[:members].each do |member|
|
103
|
+
next if member[:yy].nil? or member[:yy].zero?
|
104
|
+
|
105
|
+
diff = (member[:x] - member[:dx]).abs / 2.0
|
106
|
+
member[:dx] = member[:x] + diff
|
107
|
+
# it employs another binary-search
|
108
|
+
while member[:yy].round(PRECISION) != 0
|
109
|
+
print '.' if debug
|
110
|
+
member[:yy] = shear_to_deg(deg: result[:deg], base: [ member ] ).first[:yy]
|
111
|
+
diff /= 2.0
|
112
|
+
if member[:yy] > 0
|
113
|
+
member[:dx] += diff
|
114
|
+
else
|
115
|
+
member[:dx] -= diff
|
116
|
+
end
|
117
|
+
end
|
118
|
+
member[:yy] = member[:yy].abs.round(8)
|
119
|
+
end
|
120
|
+
|
121
|
+
puts 'done!'.magenta if debug
|
122
|
+
result[:members].each {|member| puts "finalizing #{member}".magenta } if debug
|
123
|
+
result
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
###########################################################################################################################
|
128
|
+
# LAMDBA no3: the actual 'function' to retrieve the slope
|
129
|
+
#
|
130
|
+
# the idea implemented is based on the fact, that we don't know in which exact time of the interval the value
|
131
|
+
# was created. even further we know that the stencil might be incorrect. so after shearing the :x value of the
|
132
|
+
# recently found new member(s) is shifted by :deviation and shearing is repeated. this is done as long as new members
|
133
|
+
# are found.
|
134
|
+
get_slope = lambda do |b|
|
135
|
+
if debug
|
136
|
+
puts "in get_slope ... SETTING BASE: ".light_green
|
137
|
+
puts "Last: \t#{present.call b.last }".light_green
|
138
|
+
puts "First:\t#{present.call b.first}".light_green
|
139
|
+
end
|
140
|
+
members = [ b.last[:i] ]
|
141
|
+
loop do
|
142
|
+
current_slope = detect_slope(base: b, ticksize: sym[:ticksize], format: sym[:format], debug: debug)
|
143
|
+
if debug
|
144
|
+
puts "CURR: #{current_slope[:deg]} "
|
145
|
+
current_slope[:members].each {|x| puts "CURR: #{present.call(x)}" }
|
146
|
+
end
|
147
|
+
current_members = current_slope[:members].map{|dot| dot[:i]}
|
148
|
+
new_members = current_members - members
|
149
|
+
puts "New members: #{new_members} (as of #{current_members} - #{members})" if debug
|
150
|
+
# the return condition is if no new members are found in slope
|
151
|
+
# except lowest members are neighbours, what (recursively) causes re-run until the
|
152
|
+
# first member is solitary
|
153
|
+
if new_members.empty?
|
154
|
+
mem_sorted=members.sort
|
155
|
+
if mem_sorted[1] == mem_sorted[0] + 1
|
156
|
+
b2 = b[mem_sorted[1]..mem_sorted[-1]].map{|x| x.dup; x[:dx] = nil; x}
|
157
|
+
puts 'starting recursive rerun'.light_red if debug
|
158
|
+
alternative_slope = get_slope.call(b2)
|
159
|
+
alternative = alternative_slope[:members].map{|bar| bar[:i]}
|
160
|
+
# the alternative won't be used if it misses out a member that would have
|
161
|
+
# been in the 'original' slope
|
162
|
+
if (mem_sorted[1..-1] - alternative).empty?
|
163
|
+
current_slope = alternative_slope
|
164
|
+
members = alternative
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
current_slope[:raw] = members.map{|i| base[i][:x]}
|
169
|
+
|
170
|
+
members.sort_by{|i| -i}.each_with_index do |i, index|
|
171
|
+
|
172
|
+
puts "#{index}\t#{range}\t#{present.call b[i]}".light_yellow if debug
|
173
|
+
|
174
|
+
current_slope[:members] << b[i] unless current_slope[:members].map{|x| x[:datetime]}.include? b[i][:datetime]
|
175
|
+
current_slope[:members].sort_by!{|x| x[:datetime]}
|
176
|
+
end
|
177
|
+
current_slope
|
178
|
+
|
179
|
+
end
|
180
|
+
# all new members found in current iteration have now receive their new :x value, depending on their distance to
|
181
|
+
# to the origin. when exploring near distance, it is assumned, that the actual :y value might have an
|
182
|
+
# additional distance of 1, further distant points can be distant even :deviation, what defaults to 2
|
183
|
+
# covering e.g. a holiday when using a daily base
|
184
|
+
new_members.each do |mem|
|
185
|
+
current_deviation = (0.1 * b[mem][:x])
|
186
|
+
current_deviation = 1 if current_deviation < 1
|
187
|
+
current_deviation = deviation if current_deviation > deviation
|
188
|
+
b[mem][:dx] = b[mem][:x] + current_deviation
|
189
|
+
end
|
190
|
+
members += new_members
|
191
|
+
end
|
192
|
+
end # of lambda
|
193
|
+
|
194
|
+
###########################################################################################################################
|
195
|
+
# Lambda no. 4: analyzing the slope, adding near misses and characteristics
|
196
|
+
#
|
197
|
+
# near misses are treated as full members, as for example stacked orders within a swap architecture might impede that the
|
198
|
+
# peak runs to the maximum expansion
|
199
|
+
#
|
200
|
+
# first, the swap_base is created by shearing the entire base to current :deg
|
201
|
+
# then all base members are selected that fit the desired :y range.
|
202
|
+
# please note that here also the processing of :with_flaws takes place
|
203
|
+
#
|
204
|
+
# the key :dev is introduced, which is actually a ticksize-based variant of :yy
|
205
|
+
|
206
|
+
analyze = lambda do |swaps|
|
207
|
+
swaps.each do |swap|
|
208
|
+
|
209
|
+
swap_base = base.map{|y|
|
210
|
+
x = y.slice(*%i[ datetime high low dist x y i yy dx ])
|
211
|
+
current_member = swap[:members].find{|z| z[:datetime] == x[:datetime] }
|
212
|
+
x[:dx] = current_member[:dx] if current_member
|
213
|
+
x
|
214
|
+
}
|
215
|
+
swap_base = shear_to_deg(base: swap_base, deg: swap[:deg])
|
216
|
+
swap_base.map!{|x| x[:dev] = (x[:yy] / sym[:ticksize].to_f); x[:dev] = -( x[:dev] > 0 ? x[:dev].floor : x[:dev].ceil); x}
|
217
|
+
invalids = swap_base.select{|x| x[:dev] < 0 }
|
218
|
+
if with_flaws > 0
|
219
|
+
# TODO: this behaves only as expected when with_flaws == 2
|
220
|
+
last_invalid = invalids[(invalids[-2][:i] + 1 == invalids[-1][:i] ) ? -3 : -2] rescue nil
|
221
|
+
else
|
222
|
+
last_invalid = invalids.last
|
223
|
+
end
|
224
|
+
|
225
|
+
# the 'near' members are all base members found, that fit
|
226
|
+
# 1. being positive (as being zero means that they are original members)
|
227
|
+
# 2. match a valid :dev
|
228
|
+
# 3. appeared later than :last_invalid
|
229
|
+
near = swap_base.select{|x|
|
230
|
+
x[:dev] <= [ 5, (x[:x] / 100)+2 ].min and
|
231
|
+
x[:dev].positive? and
|
232
|
+
(last_invalid.nil? ? true : x[:datetime] > last_invalid[:datetime])
|
233
|
+
}.map{|x| x[:near] = x[:dev]; x}
|
234
|
+
|
235
|
+
# these then are added to the swap[:members] and for further processing swap_base is cleaned
|
236
|
+
swap[:members] = (swap[:members] + near).sort_by{|x| x[:datetime] }
|
237
|
+
swap_base.select!{|x| x[:datetime] >= swap[:members].first[:datetime]}
|
238
|
+
|
239
|
+
########################################################################33
|
240
|
+
# now swap characteristics are calculated
|
241
|
+
#
|
242
|
+
# avg_dev: the average distance of high or low to the swap_line
|
243
|
+
swap[:avg_dev] = (swap_base.reject{|x| x[:dev].zero?}.map{|x| x[:dev].abs}.reduce(:+) / (swap_base.size - swap[:members].size).to_f).ceil rescue 0
|
244
|
+
# depth: the maximum distance to the swap line
|
245
|
+
swap[:depth] = swap_base.max_by{|x| x[:dev]}[:dev]
|
246
|
+
swap[:raw] = swap[:members].map{|x| x[:x]}.reverse
|
247
|
+
swap[:size] = swap[:members].size
|
248
|
+
swap[:length] = swap[:raw][-1] - swap[:raw][0]
|
249
|
+
# rating: the maximum distance of the 'most middle' point of the swap to the nearer end
|
250
|
+
swap[:rating] = swap[:raw][1..-2].map{ |dot| [ dot - swap[:raw][0], swap[:raw][-1] - dot].min }.max || 0
|
251
|
+
swap[:datetime] = swap[:members].last[:datetime]
|
252
|
+
swap[:side] = side
|
253
|
+
rat = swap[:rating]
|
254
|
+
# color: to simplify human readability a standard set of colors for intraday and eod based swaps
|
255
|
+
swap[:color] = (rat > 75) ? :light_blue : (rat > 30) ? :magenta : (rat > 15) ? :light_magenta : (rat > 7) ? (high ? :light_green : :light_red) : high ? :green : :red
|
256
|
+
unless %i[ daily continuous ].include? interval
|
257
|
+
swap[:color] = ((rat > 150) ? :light_blue : (rat > 80) ? :magenta : (rat > 30) ? :light_magenta : (rat > 15) ? :light_yellow : high ? :green : :red)
|
258
|
+
end
|
259
|
+
swap[:diff] = swap[:members].last[ high ? :high : :low ] - swap[:members].first[ high ? :high : :low ]
|
260
|
+
swap[:ticks] = (swap[:diff] / sym[:ticksize]).to_i
|
261
|
+
# tpi: ticks per interval, how many ticks are passed each :interval
|
262
|
+
swap[:tpi] = (swap[:ticks].to_f / swap[:length]).round(3)
|
263
|
+
# ppi: power per interval, how many dollar value is passed each :interval
|
264
|
+
swap[:ppi] = (swap[:tpi] * sym[:power]).round(3)
|
265
|
+
end # swap
|
266
|
+
end # lambda
|
267
|
+
|
268
|
+
###########################################################################################################################
|
269
|
+
# after declaring lambdas, the rest is quite few code
|
270
|
+
#
|
271
|
+
# starting with the full range, a valid slope is searched. the found slope defines an interval of the
|
272
|
+
# base array, in which again a (lower) slope can be uncovered.
|
273
|
+
#
|
274
|
+
# this process is repeated while the interval to be processed is large enough (:min_length)
|
275
|
+
current_range = (0..-1) # RANGE set
|
276
|
+
current_slope = { members: [] } # SLOPE reset
|
277
|
+
current_base = base[current_range].map{|z| z.slice(*%i[datetime high low x y i ])} # BASE set
|
278
|
+
current_results = [ ] # RESULTS reset
|
279
|
+
binding.irb if debug
|
280
|
+
while current_base.size >= min_length # LOOP
|
281
|
+
|
282
|
+
puts '-------------------------------------------------------------------------------------' if debug
|
283
|
+
|
284
|
+
while current_base.size >= min_length and current_slope[:members].size < 2
|
285
|
+
|
286
|
+
puts "---- #{current_base.size} #{current_range.to_s.light_yellow} ------" if debug
|
287
|
+
|
288
|
+
# get new slope
|
289
|
+
current_slope = get_slope.call(current_base) # SLOPE call
|
290
|
+
|
291
|
+
# define new range and base
|
292
|
+
next_i = current_slope[:members].select{|z| z[:miss].nil? and z[:near].nil?}[-2]
|
293
|
+
current_range = ((next_i.nil? ? -2 : next_i[:i])+1..-1) # RANGE adjust
|
294
|
+
current_base = base[current_range].map{|z| z.slice(*%i[datetime high low x y i ])} # BASE adjust
|
295
|
+
end
|
296
|
+
puts "Current slope: ".light_yellow + "#{current_slope}" if debug
|
297
|
+
current_results << current_slope if current_slope # RESULTS add
|
298
|
+
current_slope = { members: [] } # SLOPE reset
|
299
|
+
end
|
300
|
+
|
301
|
+
finalize.call(current_results)
|
302
|
+
analyze.call(current_results)
|
303
|
+
binding.irb if debug
|
304
|
+
|
305
|
+
# reject all results that do not suffice
|
306
|
+
current_results.reject!{|swap| swap[:rating] < min_rating or swap[:length] < min_length or swap[:rating] < swap[:length] / 4.to_f}
|
307
|
+
|
308
|
+
#####################################################################################################################3
|
309
|
+
# finally save results for caching and return them
|
310
|
+
if save
|
311
|
+
if interval.nil? or swap_type.nil?
|
312
|
+
puts "WARNING: Cannot save swaps, as both :interval and :swap_type must be given".colorize(:light_yellow)
|
313
|
+
else
|
314
|
+
current_results.map{|sw| mem = sw[:members]; sw[:slope] = (mem.last[:y] - mem.first[:y]) / (mem.last[mem.last[:dx].nil? ? :x : :dx] - mem.first[mem.first[:dx].nil? ? :x : :dx]).to_f }
|
315
|
+
to_save = current_results.empty? ? [ { datetime: zero[:datetime], side: side, empty: true } ] : current_results
|
316
|
+
save_swaps(to_save, interval: interval, swap_type: swap_type, contract: contract, sym: sym)
|
317
|
+
end
|
318
|
+
end
|
319
|
+
current_results
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
data/lib/cotcube-level.rb
CHANGED
@@ -12,34 +12,36 @@ require 'json' unless defined?(JSON)
|
|
12
12
|
require 'digest' unless defined?(Digest)
|
13
13
|
require 'cotcube-helpers'
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
%w[ stencil detect_slope triangulate helpers].each do |part|
|
15
|
+
%w[ eod_stencil intraday_stencil detect_slope tritangulate helpers].each do |part|
|
18
16
|
require_relative "cotcube-level/#{part}"
|
19
17
|
end
|
20
18
|
|
21
|
-
|
22
|
-
|
23
19
|
module Cotcube
|
24
20
|
module Level
|
25
21
|
|
26
|
-
PRECISION =
|
27
|
-
INTERVALS = %i[ daily ]
|
22
|
+
PRECISION = 16
|
23
|
+
INTERVALS = %i[ daily continuous hours halfs ]
|
28
24
|
SWAPTYPES = %i[ full ]
|
25
|
+
TIMEZONES = { 'CT' => Time.find_zone('America/Chicago'),
|
26
|
+
'DE' => Time.find_zone('Europe/Berlin') }
|
27
|
+
GLOBAL_SOW = { 'CT' => '0000-1700' }
|
28
|
+
GLOBAL_EOW = { 'CT' => '1700-0000' }
|
29
|
+
GLOBAL_EOD = { 'CT' => '1600-1700' }
|
30
|
+
|
29
31
|
#module_function :init, # checks whether environment is prepared and returns the config hash
|
30
|
-
module_function :detect_slope,
|
31
|
-
:
|
32
|
-
:
|
32
|
+
module_function :detect_slope, # in detect_slope.rb
|
33
|
+
:tritangulate, # in tritangulate.rb
|
34
|
+
:shear_to_deg, # in helpers.rb
|
35
|
+
:shear_to_rad, # same all below
|
33
36
|
:rad2deg,
|
34
37
|
:deg2rad,
|
35
38
|
:puts_swaps,
|
36
39
|
:save_swaps,
|
37
40
|
:get_jsonl_name,
|
38
41
|
:load_swaps,
|
39
|
-
:member_to_human
|
40
|
-
:triangulate
|
42
|
+
:member_to_human
|
41
43
|
|
42
|
-
# please note that module_functions of sources provided in
|
44
|
+
# please note that module_functions of sources provided in non-public files must slso be published within these
|
43
45
|
end
|
44
46
|
end
|
45
47
|
|
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.
|
4
|
+
version: 0.3.1
|
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-08-
|
11
|
+
date: 2021-08-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -108,7 +108,7 @@ dependencies:
|
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0.9'
|
111
|
-
description: A gem to shear a time series, basically serving as a yet
|
111
|
+
description: A gem to shear a time series, basically serving as a yet unseen class
|
112
112
|
of indicators.
|
113
113
|
email:
|
114
114
|
- donkeybridge@jtown.eu
|
@@ -124,9 +124,10 @@ files:
|
|
124
124
|
- cotcube-level.gemspec
|
125
125
|
- lib/cotcube-level.rb
|
126
126
|
- lib/cotcube-level/detect_slope.rb
|
127
|
+
- lib/cotcube-level/eod_stencil.rb
|
127
128
|
- lib/cotcube-level/helpers.rb
|
128
|
-
- lib/cotcube-level/
|
129
|
-
- lib/cotcube-level/
|
129
|
+
- lib/cotcube-level/intraday_stencil.rb
|
130
|
+
- lib/cotcube-level/tritangulate.rb
|
130
131
|
homepage: https://github.com/donkeybridge/cotcube-level
|
131
132
|
licenses:
|
132
133
|
- BSD-3-Clause
|