cotcube-level 0.2.0 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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'