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.
@@ -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
- # require_relative 'cotcube-level/filename
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 = 7
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
- :shear_to_deg,
32
- :shear_to_rad,
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 private files must slso be published within these
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.2.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-17 00:00:00.000000000 Z
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 unssen class
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/stencil.rb
129
- - lib/cotcube-level/triangulate.rb
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