cotcube-level 0.2.0 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,332 @@
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, # 1st criteria: swaps having a lower rating are discarded
13
+ min_length: 8, # 2nd criteria: shorter swaps are discared
14
+ min_ratio: # 3rd criteria: the ratio between rating and length (if true, swap is discarded)
15
+ lambda {|r,l| r < l / 4.0 },
16
+ save: true, # allow saving of results
17
+ cached: true, # allow loading of cached results
18
+ interval: , # interval (currently) is one of %i[ daily continuous halfs ]
19
+ swap_type: nil, # if not given, a warning is printed and swaps won't be saved or loaded
20
+ with_flaws: 0, # the maximum amount of consecutive bars that would actually break the current swap
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
23
+ deviation: 2 # the maximum shift of :x-values of found members
24
+ )
25
+
26
+ raise ArgumentError, "'0 < max < 90, but got '#{max}'" unless max.is_a? Numeric and 0 < max and max <= 90
27
+ raise ArgumentError, 'need :side either :upper or :lower for dots' unless [:upper, :lower].include? side
28
+
29
+ ###########################################################################################################################
30
+ # init some helpers
31
+ #
32
+ high = side == :upper
33
+ first = base.to_a.find{|x| not x[:high].nil? }
34
+ zero = base.select{|x| x[:x].zero? }
35
+ raise ArgumentError, "Inappropriate base, it should contain ONE :x.zero, but contains #{zero.size}." unless zero.size==1
36
+ zero = zero.first
37
+
38
+ contract ||= zero[:contract]
39
+ sym ||= Cotcube::Helpers.get_id_set(contract: contract)
40
+
41
+
42
+ if cached
43
+ if interval.nil? or swap_type.nil?
44
+ puts "Warning: Cannot use cache as both :interval and :swap_type must be given".light_yellow
45
+ else
46
+ cache = load_swaps(interval: interval, swap_type: swap_type, contract: contract, sym: sym, datetime: zero[:datetime])
47
+ # if the current datetime was already processed but nothing has been found,
48
+ # an 'empty' value is saved.
49
+ # that means, if neither a swap (or more) nor :empty is found, the datetime has not been processed yet
50
+ selected = cache.select{|sw| sw[:datetime] == zero[:datetime] and sw[:side] == side }
51
+ unless selected.empty?
52
+ puts 'cache_hit'.light_white if debug
53
+ return (selected.first[:empty] ? [] : selected )
54
+ end
55
+ end
56
+ end
57
+
58
+ ###########################################################################################################################
59
+ # prepare base (i.e. dupe the original, create proper :y, and reject unneeded items)
60
+ #
61
+ base = base.
62
+ map { |x|
63
+ y = x.dup
64
+ y[:y] = (high ?
65
+ (y[:high] - zero[:high]).round(8) :
66
+ (zero[:low] - y[:low]).round(8)
67
+ ) unless y[:high].nil?
68
+ y
69
+ }.
70
+ reject{|b| b.nil? or b[:datetime] < first[:datetime] or b[:x] < 0 or b[:y].nil?}[range]
71
+
72
+ # abs_peak is the absolute high / low of the base. the shearing operation ends there,
73
+ # but results might be influenced when abs_peak becomes affected by :with_flaws
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
78
+
79
+ ###########################################################################################################################z
80
+ # only if (and only if) the range portion above change the underlying base
81
+ # the offset has to be fixed for :x and :y
82
+
83
+ unless range == (0..-1)
84
+ puts "adjusting range to '#{range}'".light_yellow if debug
85
+ offset_x = base.last[:x]
86
+ offset_y = base.last[:y]
87
+ base.map!{|b| b[:x] -= offset_x; b[:y] -= offset_y ; b}
88
+ end
89
+
90
+ ###########################################################################################################################
91
+ # introducing :i to the base, which provides the negative index of the :base Array of the current element
92
+ # this simplifies handling during the, where I can use the members array,
93
+ # that will carry just the index of the original base, regardless how many array_members have be already dropped
94
+ base.each_index.map{|i| base[i][:i] = -base.size + i }
95
+
96
+
97
+ ###########################################################################################################################
98
+ # LAMBDA no1: simplifying DEBUG output
99
+ #
100
+ present = lambda {|z| z.slice(*%i[datetime high low x y i yy dx dev near miss dev]) }
101
+
102
+
103
+ ###########################################################################################################################
104
+ # LAMBDA no2: all members except the pivot itself now most probably are too far to the left
105
+ # finalizing tries to get the proper dx value for them
106
+ #
107
+ finalize = lambda do |results|
108
+ results.map do |result|
109
+ result[:members].each do |member|
110
+ next if member[:yy].nil? or member[:yy].round(PRECISION-5).zero?
111
+
112
+ diff = (member[:x] - member[:dx]).abs / 2.0
113
+ member[:dx] = member[:x] + diff
114
+ # it employs another binary-search
115
+ while member[:yy].round(PRECISION-5) != 0.0
116
+ member[:yy] = shear_to_deg(deg: result[:deg], base: [ member ] ).first[:yy]
117
+ diff /= 2.0
118
+ if member[:yy] > 0
119
+ member[:dx] += diff
120
+ else
121
+ member[:dx] -= diff
122
+ end
123
+ end
124
+ member[:yy] = member[:yy].abs.round(PRECISION-5)
125
+ end
126
+
127
+ puts 'done!'.magenta if debug
128
+ result[:members].each {|member| puts "finalizing #{member}".magenta } if debug
129
+ result
130
+ end
131
+ end
132
+
133
+ ###########################################################################################################################
134
+ # LAMDBA no3: the actual 'function' to retrieve the slope
135
+ #
136
+ # the idea implemented is based on the fact, that we don't know in which exact time of the interval the value
137
+ # was created. even further we know that the stencil might be incorrect. so after shearing the :x value of the
138
+ # recently found new member(s) is shifted by :deviation and shearing is repeated. this is done as long as new members
139
+ # are found.
140
+ get_slope = lambda do |b|
141
+ if debug
142
+ puts "in get_slope ... SETTING BASE: ".light_green
143
+ puts "Last: \t#{present.call b.last }".light_green
144
+ puts "First:\t#{present.call b.first}".light_green
145
+ end
146
+ members = [ b.last[:i] ]
147
+ loop do
148
+ current_slope = detect_slope(base: b, ticksize: sym[:ticksize], format: sym[:format], debug: debug)
149
+ if debug
150
+ puts "CURR: #{current_slope[:deg]} "
151
+ current_slope[:members].each {|x| puts "CURR: #{present.call(x)}" }
152
+ end
153
+ current_members = current_slope[:members].map{|dot| dot[:i]}
154
+ new_members = current_members - members
155
+ puts "New members: #{new_members} (as of #{current_members} - #{members})" if debug
156
+ # the return condition is if no new members are found in slope
157
+ # except lowest members are neighbours, what (recursively) causes re-run until the
158
+ # first member is solitary
159
+ if new_members.empty?
160
+ mem_sorted=members.sort
161
+ if mem_sorted[1] == mem_sorted[0] + 1 and not manual
162
+ b2 = b[mem_sorted[1]..mem_sorted[-1]].map{|x| x.dup; x[:dx] = nil; x}
163
+ puts 'starting recursive rerun'.light_red if debug
164
+ alternative_slope = get_slope.call(b2)
165
+ alternative = alternative_slope[:members].map{|bar| bar[:i]}
166
+ # the alternative won't be used if it misses out a member that would have
167
+ # been in the 'original' slope
168
+ if (mem_sorted[1..-1] - alternative).empty?
169
+ current_slope = alternative_slope
170
+ members = alternative
171
+ end
172
+ end
173
+
174
+ current_slope[:raw] = members.map{|i| base[i][:x]}
175
+
176
+ members.sort_by{|i| -i}.each_with_index do |i, index|
177
+
178
+ puts "#{index}\t#{range}\t#{present.call b[i]}".light_yellow if debug
179
+
180
+ current_slope[:members] << b[i] unless current_slope[:members].map{|x| x[:datetime]}.include? b[i][:datetime]
181
+ current_slope[:members].sort_by!{|x| x[:datetime]}
182
+ end
183
+ return current_slope
184
+
185
+ end
186
+ # all new members found in current iteration have now receive their new :x value, depending on their distance to
187
+ # to the origin. when exploring near distance, it is assumned, that the actual :y value might have an
188
+ # additional distance of 1, further distant points can be distant even :deviation, what defaults to 2
189
+ # covering e.g. a holiday when using a daily base
190
+ new_members.each do |mem|
191
+ current_deviation = (0.1 * b[mem][:x])
192
+ current_deviation = 1 if current_deviation < 1
193
+ current_deviation = deviation if current_deviation > deviation
194
+ b[mem][:dx] = b[mem][:x] + current_deviation
195
+ end
196
+ members += new_members
197
+ end
198
+ end # of lambda
199
+
200
+ ###########################################################################################################################
201
+ # Lambda no. 4: analyzing the slope, adding near misses and characteristics
202
+ #
203
+ # near misses are treated as full members, as for example stacked orders within a swap architecture might impede that the
204
+ # peak runs to the maximum expansion
205
+ #
206
+ # first, the swap_base is created by shearing the entire base to current :deg
207
+ # then all base members are selected that fit the desired :y range.
208
+ # please note that here also the processing of :with_flaws takes place
209
+ #
210
+ # the key :dev is introduced, which is actually a ticksize-based variant of :yy
211
+
212
+ analyze = lambda do |swaps|
213
+ swaps.each do |swap|
214
+
215
+ swap_base = base.map{|y|
216
+ x = y.slice(*%i[ datetime high low dist x y i yy dx ])
217
+ current_member = swap[:members].find{|z| z[:datetime] == x[:datetime] }
218
+ x[:dx] = current_member[:dx] if current_member
219
+ x
220
+ }
221
+ swap_base = shear_to_deg(base: swap_base, deg: swap[:deg])
222
+ swap_base.map!{|x| x[:dev] = (x[:yy] / sym[:ticksize].to_f); x[:dev] = -( x[:dev] > 0 ? x[:dev].floor : x[:dev].ceil); x}
223
+ invalids = swap_base.select{|x| x[:dev] < 0 }
224
+ with_flaws = 0 unless with_flaws # support legacy versions, where with_flaws was boolean
225
+ if with_flaws > 0
226
+ # TODO: this behaves only as expected when with_flaws == 2
227
+ last_invalid = invalids[(invalids[-2][:i] + 1 == invalids[-1][:i] ) ? -3 : -2] rescue nil
228
+ else
229
+ last_invalid = invalids.last
230
+ end
231
+
232
+ # the 'near' members are all base members found, that fit
233
+ # 1. being positive (as being zero means that they are original members)
234
+ # 2. match a valid :dev
235
+ # 3. appeared later than :last_invalid
236
+ near = swap_base.select{|x|
237
+ x[:dev] <= [ 5, (x[:x] / 100)+2 ].min and
238
+ x[:dev].positive? and
239
+ (last_invalid.nil? ? true : x[:datetime] > last_invalid[:datetime])
240
+ }.map{|x| x[:near] = x[:dev]; x}
241
+
242
+ # these then are added to the swap[:members] and for further processing swap_base is cleaned
243
+ swap[:members] = (swap[:members] + near).sort_by{|x| x[:datetime] }
244
+ swap_base.select!{|x| x[:datetime] >= swap[:members].first[:datetime]}
245
+
246
+ ########################################################################33
247
+ # now swap characteristics are calculated
248
+ #
249
+ # avg_dev: the average distance of high or low to the swap_line
250
+ 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
251
+ # depth: the maximum distance to the swap line
252
+ swap[:depth] = swap_base.max_by{|x| x[:dev]}[:dev]
253
+ swap[:interval] = interval
254
+ swap[:swap_type] = swap_type
255
+ swap[:raw] = swap[:members].map{|x| x[:x]}.reverse
256
+ swap[:size] = swap[:members].size
257
+ swap[:length] = swap[:raw][-1] - swap[:raw][0]
258
+ # rating: the maximum distance of the 'most middle' point of the swap to the nearer end
259
+ swap[:rating] = swap[:raw][1..-2].map{ |dot| [ dot - swap[:raw][0], swap[:raw][-1] - dot].min }.max || 0
260
+ swap[:datetime] = swap[:members].last[:datetime]
261
+ swap[:side] = side
262
+ rat = swap[:rating]
263
+ # color: to simplify human readability a standard set of colors for intraday and eod based swaps
264
+ swap[:color] = (rat > 75) ? :light_blue : (rat > 30) ? :magenta : (rat > 15) ? :light_magenta : (rat > 7) ? (high ? :light_green : :light_red) : high ? :green : :red
265
+ unless %i[ daily continuous ].include? interval
266
+ swap[:color] = ((rat > 150) ? :light_blue : (rat > 80) ? :magenta : (rat > 30) ? :light_magenta : (rat > 15) ? :light_yellow : high ? :green : :red)
267
+ end
268
+ swap[:diff] = (swap[:members].last[ high ? :high : :low ] - swap[:members].first[ high ? :high : :low ]).round(8)
269
+ swap[:ticks] = (swap[:diff] / sym[:ticksize]).to_i
270
+ # tpi: ticks per interval, how many ticks are passed each :interval
271
+ swap[:tpi] = (swap[:ticks].to_f / swap[:length]).round(3)
272
+ # ppi: power per interval, how many $dollar value is passed each :interval
273
+ swap[:ppi] = (swap[:tpi] * sym[:power]).round(3)
274
+ end # swap
275
+ end # lambda
276
+
277
+ ###########################################################################################################################
278
+ # after declaring lambdas, the rest is quite few code
279
+ #
280
+ # starting with the full range, a valid slope is searched. the found slope defines an interval of the
281
+ # base array, in which again a (lower) slope can be uncovered.
282
+ #
283
+ # this process is repeated while the interval to be processed is large enough (:min_length)
284
+ current_range = (0..-1) # RANGE set
285
+ current_slope = { members: [] } # SLOPE reset
286
+ current_base = base[current_range].map{|z| z.slice(*%i[datetime high low x y i ])} # BASE set
287
+ current_results = [ ] # RESULTS reset
288
+ binding.irb if debug
289
+ while current_base.size >= min_length # LOOP
290
+
291
+ puts '-------------------------------------------------------------------------------------' if debug
292
+
293
+ while current_base.size >= min_length and current_slope[:members].size < 2
294
+
295
+ puts "---- #{current_base.size} #{current_range.to_s.light_yellow} ------" if debug
296
+
297
+ # get new slope
298
+ current_slope = get_slope.call(current_base) # SLOPE call
299
+
300
+ # define new range and base
301
+ next_i = current_slope[:members].select{|z| z[:miss].nil? and z[:near].nil?}[-2]
302
+ current_range = ((next_i.nil? ? -2 : next_i[:i])+1..-1) # RANGE adjust
303
+ current_base = base[current_range].map{|z| z.slice(*%i[datetime high low x y i ])} # BASE adjust
304
+ end
305
+ puts "Current slope: ".light_yellow + "#{current_slope}" if debug
306
+ current_results << current_slope if current_slope # RESULTS add
307
+ current_slope = { members: [] } # SLOPE reset
308
+ end
309
+
310
+ finalize.call(current_results)
311
+ analyze.call(current_results)
312
+ binding.irb if debug
313
+
314
+ # reject all results that do not suffice
315
+ current_results.reject!{|swap| swap[:rating] < min_rating or swap[:length] < min_length or min_ratio.call(swap[:rating],swap[:length])}
316
+
317
+ #####################################################################################################################3
318
+ # finally save results for caching and return them
319
+ if save
320
+ if interval.nil? or swap_type.nil?
321
+ puts "WARNING: Cannot save swaps, as both :interval and :swap_type must be given".colorize(:light_yellow)
322
+ else
323
+ 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 }
324
+ to_save = current_results.empty? ? [ { datetime: zero[:datetime], side: side, empty: true, interval: interval, swap_type: swap_type } ] : current_results
325
+ save_swaps(to_save, interval: interval, swap_type: swap_type, contract: contract, sym: sym)
326
+ end
327
+ end
328
+ current_results
329
+ end
330
+ end
331
+ end
332
+
data/lib/cotcube-level.rb CHANGED
@@ -12,34 +12,37 @@ 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
- :puts_swaps,
38
+ :puts_swap,
36
39
  :save_swaps,
37
40
  :get_jsonl_name,
38
41
  :load_swaps,
39
- :member_to_human,
40
- :triangulate
42
+ :check_exceedance,
43
+ :member_to_human
41
44
 
42
- # please note that module_functions of sources provided in private files must slso be published within these
45
+ # please note that module_functions of sources provided in non-public files must slso be published within these
43
46
  end
44
47
  end
45
48
 
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.4
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-10-06 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
@@ -116,7 +116,6 @@ executables: []
116
116
  extensions: []
117
117
  extra_rdoc_files: []
118
118
  files:
119
- - ".irbrc.rb"
120
119
  - CHANGELOG.md
121
120
  - Gemfile
122
121
  - README.md
@@ -124,9 +123,10 @@ files:
124
123
  - cotcube-level.gemspec
125
124
  - lib/cotcube-level.rb
126
125
  - lib/cotcube-level/detect_slope.rb
126
+ - lib/cotcube-level/eod_stencil.rb
127
127
  - lib/cotcube-level/helpers.rb
128
- - lib/cotcube-level/stencil.rb
129
- - lib/cotcube-level/triangulate.rb
128
+ - lib/cotcube-level/intraday_stencil.rb
129
+ - lib/cotcube-level/tritangulate.rb
130
130
  homepage: https://github.com/donkeybridge/cotcube-level
131
131
  licenses:
132
132
  - BSD-3-Clause
data/.irbrc.rb DELETED
@@ -1,12 +0,0 @@
1
- def verbose_toggle
2
- irb_context.echo ? irb_context.echo = false : irb_context.echo = true
3
- end
4
-
5
- alias vt verbose_toggle
6
-
7
- $debug = true
8
- IRB.conf[:USE_MULTILINE] = false
9
- # require 'bundler'
10
- # Bundler.require
11
-
12
- require_relative 'lib/cotcube-level'