Module: Cotcube::Level

Defined in:
lib/cotcube-level.rb,
lib/cotcube-level/helpers.rb,
lib/cotcube-level/eod_stencil.rb,
lib/cotcube-level/detect_slope.rb,
lib/cotcube-level/tritangulate.rb,
lib/cotcube-level/intraday_stencil.rb

Defined Under Namespace

Classes: EOD_Stencil, Intraday_Stencil

Constant Summary collapse

PRECISION =
16
INTERVALS =
%i[ daily continuous hours halfs ]
SWAPTYPES =
%i[ full ]
TIMEZONES =
{ 'CT' => Time.find_zone('America/Chicago'),
'DE' => Time.find_zone('Europe/Berlin')    }
GLOBAL_SOW =
{ 'CT' => '0000-1700' }
GLOBAL_EOW =
{ 'CT' => '1700-0000' }
GLOBAL_EOD =
{ 'CT' => '1600-1700' }

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.check_exceedanceObject

.deg2radObject

.detect_slopeObject

.get_jsonl_nameObject

.load_swapsObject

.mark_exceededObject

.mark_ignoredObject

.member_to_humanObject

.puts_swapObject

.rad2degObject

.save_swapsObject

.shear_to_degObject

.shear_to_radObject

.tritangulateObject

Instance Method Details

#check_exceedance(swaps:, zero:, stencil:, contract:, sym:, debug: false) ⇒ Object

:swaps is an array of swaps :zero is the current interval (ohlc) :stencil is the according current stencil (eod or intraday)



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# File 'lib/cotcube-level/helpers.rb', line 243

def check_exceedance(swaps:, zero:, stencil:, contract:, sym:, debug: false)
  swaps.map do |swap|
    # swaps cannot exceed the day they are found (or if they are found in the future)
    next if  swap[:datetime] >= zero[:datetime] or swap[:empty]
    update = stencil.use with: swap, sym: sym, zero: zero
    if update[:exceeded]
      to_save = {
        datetime: zero[:datetime],
        ref:      swap[:digest],
        side:     swap[:side],
        exceeded: update[:exceeded]
      }
      save_swaps to_save, interval: swap[:interval], swap_type: swap[:swap_type], contract: contract, sym: sym, quiet: (not debug)
      swap[:exceeded] = update[:exceeded]
    end
    %i[ current_change current_value current_diff current_dist alert].map{|key| swap[key] = update[key] }
    swap
  end.compact
end

#deg2rad(rad) ⇒ Object



8
# File 'lib/cotcube-level/helpers.rb', line 8

def deg2rad(rad); rad * Math::PI / 180; end

#detect_slope(base:, max: 90, debug: false, format: '% 5.2f', calculus: false, ticksize: nil, max_dev: 200) ⇒ Object

Raises:

  • (ArgumentError)


3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/cotcube-level/detect_slope.rb', line 3

def detect_slope(base:, max: 90, debug: false, format: '% 5.2f', calculus: false, ticksize: nil, max_dev: 200)
  raise ArgumentError, "'0 < max < 90, but got '#{max}'" unless max.is_a? Numeric and 0 < max and max <= 90
  #
  # this method processes a 'well prepared' stencil in a way, that :y values are sheared around stencil.zero,
  #     resulting to a temporary :yy value for each point. this process is iterated until no more :yy
  #     values are above the abscissa ( yy > 0 ) but at least one other values is on (yy == 0)
  #
  # the entire process initially aimed to find slopes that contain 3 or more members. the current version 
  #     is confident with 2 members--or even one member, which results in an even slope.
  #
  # it works by running a binary search, whereon each iteration,
  #   - :part is halved and added or substracted based on current success
  #   - if more than the mandatory result is found, all negative results are removed and degrees are increased by part
  #   - if no results are found, the process is repeated with the same current base after degrees are decreased by part
  #
  raise ArgumentError, 'detect_slope needs param Array :base' unless base.is_a? Array

  # from given base, choose non-negative stencil containing values
  old_base = base.dup.select{|b| b[:x] >= 0 and not b[:y].nil? }

  # set initial shearing angle if not given as param. This is a prepared functionality, that is not yet used.
  # when implemented to use in tritangulate, it would speed up the binary search process, but initial part 
  # has to be set in a different way
  deg ||= -max / 2.0

  # create first shearing. please note how selection works with d[:yy]
  new_base = shear_to_deg(base: old_base, deg: deg).select { |d| d[:yy] >= 0 }

  # debug output
  puts "Iterating slope:\t#{format '% 7.5f',deg
                       }\t\t#{new_base.size
                       } || #{new_base.values_at(*[0]).map{|f| "'#{f[:x]
                                                                } | #{format format,f[:y]
                                                                } | #{format format,f[:yy]}'"}.join(" || ") }" if debug
  # set initial part to deg
  part = deg.abs
  #
  # the loop, that runs until either
  #   - only two points are left on the slope
  #   - the slope has even angle
  #   - several points are found on the slope in quite a good approximation ('round(PRECISION)')
  #
  until deg.round(PRECISION).zero? || part.round(PRECISION).zero? ||
      ((new_base.size >= 2) && (new_base.map { |f| f[:yy].round(PRECISION / 2).zero? }.uniq.size == 1))

    part /= 2.0
    if new_base.size == 1
      # the graph was sheared too far, reuse old_base
      deg += part
    else
      # the graph was sheared too short, continue with new base
      deg -= part
      old_base = new_base.dup unless deg.round(PRECISION).zero?
    end

    # the actual shearing operation
    # this basically maps old_base with yy = y + (dx||x * tan(deg) )
    #
    new_base = shear_to_deg(base: old_base, deg: deg).select { |d| d[:yy] >= 0 }
    new_base.last[:dx] = 0.0

    # debug output is reduced by appr. 70%
    if debug and Random.rand < 0.3
      print " #{format '% 18.15f',deg}\t"
      puts "Iterating slope:\t#{format '% 18.10f',deg
                           }\t#{new_base.size
                         } || #{new_base.values_at(*[0]).map{|f| "'#{f[:x]
                                                               } | #{format '%4.5f', part
                                                               } | #{format format,f[:y]
                                                      } | #{format format,f[:yy]}'"}.join(" || ") }"
    end
  end
  ### Sheering ends here

  # define the approximited result as (also) 0.0
  new_base.each{|x| x[:yy] = 0.0}

  #####################################################################################
  # Calculate the slope based on the angle that resulted above
  #     y = m x + n -->
  #                     m = delta-y / delta-x
  #                     n = y0 - m * x0
  #
  slope     = deg.zero? ? 0 : (new_base.first[:y] - new_base.last[:y]) / (
    (new_base.first[:dx].nil? ? new_base.first[:x] : new_base.first[:dx]).to_f -
    (new_base. last[:dx].nil? ? new_base. last[:x] : new_base. last[:dx]).to_f
  )
  # the result
  {
    deg:       deg,
    slope:     slope,
    members:   new_base.map { |x| x.dup }
  }
end

#get_jsonl_name(interval:, swap_type:, contract:, sym: nil) ⇒ Object

create a standardized name for the cache files and, on-the-fly, create these files plus their directory



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/cotcube-level/helpers.rb', line 115

def get_jsonl_name(interval:, swap_type:, contract:, sym: nil)
  raise "Interval #{interval } is not supported, please choose from #{INTERVALS}" unless INTERVALS.include?(interval) || interval.is_a?(Integer)
  raise "Swaptype #{swap_type} is not supported, please choose from #{SWAPTYPES}" unless SWAPTYPES.include? swap_type
  sym ||= Cotcube::Helpers.get_id_set(contract: contract)
  root = '/var/cotcube/level'
  dir     = "#{root}/#{sym[:id]}"
  symlink = "#{root}/#{sym[:symbol]}"
  `mkdir -p #{dir}`         unless File.exist?(dir)
  `ln -s #{dir} #{symlink}` unless File.exist?(symlink)
  file = "#{dir}/#{contract}_#{interval.to_s}_#{swap_type.to_s}.jsonl"
  unless File.exist? file
    `touch #{file}`
  end
  file
end

#load_swaps(interval:, swap_type:, contract:, sym: nil, datetime: nil, recent: false, digest: nil, quiet: false, exceed: false, keep_ignored: false) ⇒ Object

loading of swaps is also straight forward it takes few more efforts to normalize the values to their expected format

it is not too nice that some actual interactive process is done here in the load section



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/cotcube-level/helpers.rb', line 165

def load_swaps(interval:, swap_type:, contract:, sym: nil, datetime: nil, recent: false, digest: nil, quiet: false, exceed: false, keep_ignored: false)
  file = get_jsonl_name(interval: interval, swap_type: swap_type, contract: contract, sym: sym)
  jsonl = File.read(file)
  data = jsonl.
    each_line.
    map do |x|
    JSON.parse(x).
      deep_transform_keys(&:to_sym).
      tap do |sw|
        sw[:datetime] = DateTime.parse(sw[:datetime]) rescue nil
        (sw[:exceeded] = DateTime.parse(sw[:exceeded]) rescue nil) if sw[:exceeded]
        (sw[:ignored] = DateTime.parse(sw[:ignored]) rescue nil) if sw[:ignored]
        sw[:interval] = interval
        sw[:swap_type] = swap_type
        sw[:contract] = contract
        %i[ side ].each {|key| sw[key] = sw[key].to_sym rescue false }
        unless sw[:empty] or sw[:exceeded] or sw[:ignored]
          sw[:color]    = sw[:color].to_sym 
          sw[:members].map{|mem| mem[:datetime] = DateTime.parse(mem[:datetime]) }
        end
    end
  end
  # assign exceedance data to actual swaps
  data.select{|swap| swap[:exceeded] }.each do |exc|
    swap = data.find{|ref| ref[:digest] == exc[:ref]}
    raise RuntimeError, "Inconsistent history for '#{exc}'. Origin not found." if swap.nil?
    swap[:exceeded] = exc[:exceeded]
  end
  # assign ignorance data to actual swaps
  data.select{|swap| swap[:ignored] }.each do |ign|
	swap = data.find{|ref| ref[:digest] == ign[:ref]}
    raise RuntimeError, "Inconsistent history for '#{ign}'. Origin not found." if swap.nil?
    swap[:ignored] = ign[:ignored]
  end
  # do not return bare exceedance information
  data.reject!{|swap| (swap[:ignored] or swap[:exceeded]) and swap[:members].nil? }
  # do not return swaps that are found 'later'
  data.reject!{|swap| swap[:datetime] > datetime } unless datetime.nil?
  # do not return exceeded swaps, that are exceeded in the past
  recent  = 7.days  if recent.is_a? TrueClass
  recent += 5.hours if recent
  data.reject!{|swap| swap[:ignored] } unless keep_ignored
  data.reject!{|swap| swap[:exceeded] and swap[:exceeded] < datetime - (recent ? recent : 0) } unless datetime.nil?
  # remove exceedance information that is found 'later'
  data.map{|swap| swap.delete(:exceeded) if swap[:exceeded] and swap[:exceeded] > datetime} unless datetime.nil?
  unless digest.nil?
    data.select! do |z|
      (Cotcube::Helpers.sub(minimum: digest.length){ z[:digest] } === digest) and
      not z[:empty]
    end
    case data.size
    when 0
      puts "No swaps found for digest '#{digest}'." unless quiet
    when 1
      sym ||= Cotcube::Helpers.get_id_set(contract: contract)
      if not quiet or exceed
        puts "Found 1 digest: "
        data.each {|d| puts_swap( d, format: sym[:format], short: true, hash: digest.size + 2) }
        if exceed
          exceed = DateTime.now if exceed.is_a? TrueClass
          mark_exceeded(swap: data.first, datetime: exceed)
          puts "Swap marked exceeded."
        end
      end
    else
      sym ||= Cotcube::Helpers.get_id_set(contract: contract)
      unless quiet
        puts "Too many digests found for digest '#{digest}', please consider sending more figures: "
        data.each {|d| puts_swap( d, format: sym[:format], short: true, hash: digest.size + 3)}
      end
    end
  end
  data
end

#mark_exceeded(swap:, datetime:, debug: false, sym: nil) ⇒ Object



263
264
265
266
267
268
269
270
271
272
273
# File 'lib/cotcube-level/helpers.rb', line 263

def mark_exceeded(swap:, datetime:, debug: false, sym: nil)
  to_save = {
    datetime: datetime,
    ref:      swap[:digest],
    side:     swap[:side],
    exceeded: datetime
  }
  sym ||=  Cotcube::Helpers.get_id_set(contract: swap[:contract])
  save_swaps to_save, interval: swap[:interval], swap_type: swap[:swap_type], sym: sym, contract: swap[:contract], quiet: (not debug)
  swap
end

#mark_ignored(swap:, datetime: DateTime.now, sym:, debug: true) ⇒ Object



275
276
277
278
279
280
281
282
283
284
# File 'lib/cotcube-level/helpers.rb', line 275

def mark_ignored(swap:, datetime: DateTime.now, sym: , debug: true)
  to_save = {
    datetime: datetime,
    ref:      swap[:digest],
    side:     swap[:side],
    ignored:  datetime
  }
  save_swaps to_save, interval: swap[:interval],  swap_type: swap[:swap_type], sym: sym, contract: swap[:contract], quiet: (not debug)
  swap
end

#member_to_human(member, side:, format:, daily: false, tws: false) ⇒ Object

human readable output please note the format must be given, that should be taken from :sym



26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/cotcube-level/helpers.rb', line 26

def member_to_human(member,side: ,format:, daily: false, tws: false)
  high = (side == :upper)
  "#{                         member[:datetime].strftime("%a, %Y-%m-%d#{daily ? "" :" %I:%M%p"}")
    }  x: #{format '%-4d',    member[:x]
    } dx: #{format '%-8.3f', (member[:dx].nil? ? member[:x] : member[:dx].round(3))
        } #{high ? "high" : "low"
       }: #{format format,    member[high ? :high : :low]
     } i: #{(format '%4d',    member[:i]) unless member[:i].nil?
     } d: #{format '%6.2f',   member[:dev] unless member[:dev].nil?
        } #{member[:near].nil? ? '' : "near: #{member[:near]}"
     }"
end

#puts_swap(swap, format:, short: true, notice: nil, hash: 3, tws: false) ⇒ Object

human readable output format: e.g. sym short: print one line / less verbose notice: add this to output as well



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/cotcube-level/helpers.rb', line 43

def puts_swap(swap, format: , short: true, notice: nil, hash: 3, tws: false)
  return '' if swap[:empty]

  # if presenting swaps from json, the datetimes need to be parsed first
  swap[:datetime] = DateTime.parse(swap[:datetime]) if swap[:datetime].is_a? String
  swap[:exceeded] = DateTime.parse(swap[:exceeded]) if swap[:exceeded].is_a? String
  swap[:ignored]  = DateTime.parse(swap[:ignored])  if swap[:ignored ].is_a? String
  swap[:side]     = swap[:side].to_sym
  swap[:members].each do |mem|
    mem[:datetime] = DateTime.parse(mem[:datetime]) if mem[:datetime].is_a? String
  end

  # TODO: create config-entry to contain 1.hour -- set of contracts ; 7.hours -- set of contracts [...]
  #       instead of hard-coding in here
  # TODO: add also to :member_to_human
  #       then commit
  if tws
    case swap[:contract][0...2]
    when *%w[ GC SI PL PA HG NG CL HO RB ] 
      delta_datetime = 1.hour
    when *%w[ GG DX ]
      delta_datetime = 7.hours
    else
      delta_datetime = 0
    end
  else
    delta_datetime = 0 
  end
  daily =  %i[ continuous daily ].include?(swap[:interval].to_sym) rescue false
  datetime_format = daily ? '%Y-%m-%d' : '%Y-%m-%d %I:%M %p'
  high = swap[:side] == :high
  ohlc = high ? :high : :low
  if notice.nil? and swap[:exceeded]
    notice = "exceeded #{(swap[:exceeded] + delta_datetime).strftime(datetime_format)}"
  end
  if swap[:ignored] 
    notice += "  IGNORED"
  end
  if short
    res ="#{format '%7s', swap[:digest][...hash]
        } #{   swap[:contract]
        } #{   swap[:side].to_s
        }".colorize( swap[:side] == :upper ? :light_green : :light_red ) +
       " (#{   format '%4d', swap[:length]
        },#{   format '%4d', swap[:rating]
        },#{   format '%4d', swap[:depth]
    }) P: #{   format '%6s', (format '%4.2f', swap[:ppi])
       }  #{
            if swap[:current_value].nil?
          "I: #{   format '%8s', (format format, swap[:members].last[ ohlc ]) }"
            else
          "C: #{   format '%8s', (format format, swap[:current_value]) } "
            end
      } [#{ (swap[:members].first[:datetime] + delta_datetime).strftime(datetime_format)
     } - #{    (swap[:members].last[:datetime] + delta_datetime).strftime(datetime_format)
       }]#{"    NOTE: #{notice}" unless notice.nil?
       }".colorize(swap[:color] || :white )
    puts res
  else
    res = ["side: #{swap[:side] }\tlen: #{swap[:length]}  \trating: #{swap[:rating]}".colorize(swap[:color] || :white )]
    res <<  "diff: #{swap[:ticks]}\tdif: #{swap[:diff].round(7)}\tdepth: #{swap[:depth]}".colorize(swap[:color] || :white )
    res << "tpi:  #{swap[:tpi]  }\tppi: #{swap[:ppi]}".colorize(swap[:color] || :white )
    res << "NOTE: #{notice}".colorize(:light_white) unless notice.nil?
    swap[:members].each {|x| res << member_to_human(x, side: swap[:side], format: format, daily: daily) }
    res = res.join("\n")
    puts res
  end
  res
end

#rad2deg(deg) ⇒ Object

3 simple, self-explaining helpers



7
# File 'lib/cotcube-level/helpers.rb', line 7

def rad2deg(deg); deg * 180 / Math::PI; end

#save_swaps(swaps, interval:, swap_type:, contract:, sym: nil, quiet: false) ⇒ Object

the name says it all. just note the addition of a digest, that serves to check whether same swap has been yet saved to the cache.

there are actually 3 types of information, that are saved here:

  1. a swap

  2. an ‘empty’ information, referring to an interval that has been processed but no swaps were found

  3. an ‘exceeded’ information, referring to another swap, that has been exceeded



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/cotcube-level/helpers.rb', line 140

def save_swaps(swaps, interval:, swap_type:, contract:, sym: nil, quiet: false)
  file = get_jsonl_name(interval: interval, swap_type: swap_type, contract: contract, sym: sym)
  swaps = [ swaps ] unless swaps.is_a? Array
  swaps.each do |swap|
    raise "Illegal swap info: Must contain keys :datetime, :side... #{swap}" unless (%i[ datetime side ] - swap.keys).empty?
    %i[ interval swap_type ].map {|key| swap.delete(key) }
    sorted_keys = [ :datetime, :side ] + ( swap.keys - [ :datetime, :side ])
    swap_json = swap.slice(*sorted_keys).to_json
    digest = Digest::SHA256.hexdigest swap_json
    res = `cat #{file} | grep '"digest":"#{digest}"'`.strip
    unless res.empty?
      puts "Cannot save swap, it is already in #{file}:".light_red unless quiet
      p swap unless quiet
    else
      swap[:digest] = digest
      sorted_keys += %i[digest]
      File.open(file, 'a+'){|f| f.write(swap.slice(*sorted_keys).to_json + "\n") }
    end
  end
end

#shear_to_deg(base:, deg:) ⇒ Object



9
# File 'lib/cotcube-level/helpers.rb', line 9

def shear_to_deg(base:, deg:); shear_to_rad(base: base, rad: deg2rad(deg)); end

#shear_to_rad(base:, rad:) ⇒ Object

the actual shearing takes place here. please not that shifting of :x takes place

by setting the new :x as :dx. so if :dx is found, it is used, otherwise :x


13
14
15
16
17
18
19
20
21
22
# File 'lib/cotcube-level/helpers.rb', line 13

def shear_to_rad(base: , rad:)
  tan = Math.tan(rad)
  base.map { |member|
    # separating lines for easier debugging
    member[:yy] =
      member[:y] +
      (member[:dx].nil? ? member[:x] : member[:dx]) * tan
    member
  }
end

#tritangulate(contract: nil, sym: nil, side:, base:, range: (0..-1), max: 90, debug: false, min_rating: 3, min_length: 8, min_ratio: lambda {|r,l| r < l / 4.0 }, save: true, cached: true, interval:, swap_type: nil, with_flaws: 0, manual: false, deviation: 2) ⇒ Object

Raises:

  • (ArgumentError)


3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/cotcube-level/tritangulate.rb', line 3

def tritangulate(
  contract: nil,        # contract actually isnt needed for tritangulation, but allows much more convenient output
                        # on some occasion here can also be given a :symbol, but this requires :sym to be set
  sym: nil,             # sym is the id set provided by Cotcube::Helper.get_id_set
  side:,                # :upper or :lower
  base:,                # the base of a readily injected stencil
  range: (0..-1),       # range is relative to base
  max: 90,              # the range which to scan for swaps goes from deg 0 to max
  debug: false,
  min_rating: 3,        # 1st criteria: swaps having a lower rating are discarded
  min_length: 8,        # 2nd criteria: shorter swaps are discared
  min_ratio:            # 3rd criteria: the ratio between rating and length (if true, swap is discarded)
    lambda {|r,l| r < l / 4.0 },
  save: true,           # allow saving  of results
  cached: true,         # allow loading of cached results
  interval: ,           # interval (currently) is one of %i[ daily continuous halfs ]
  swap_type: nil,       # if not given, a warning is printed and swaps won't be saved or loaded
  with_flaws: 0,        # the maximum amount of consecutive bars that would actually break the current swap
                        # should be set to 0 for dailies and I suggest no more than 3 for intraday
  manual: false,        # some triggers must be set differently when manual entry is used
  deviation: 2          # the maximum shift of :x-values of found members
)

  raise ArgumentError, "'0 < max < 90, but got '#{max}'" unless max.is_a? Numeric and 0 < max and max <= 90
  raise ArgumentError, 'need :side either :upper or :lower for dots' unless [:upper, :lower].include? side

  ###########################################################################################################################
  # init some helpers
  #
  high       = side == :upper
  first      = base.to_a.find{|x|  not x[:high].nil? }
  zero       = base.select{|x| x[:x].zero? }
  raise ArgumentError, "Inappropriate base, it should contain ONE :x.zero, but contains #{zero.size}." unless zero.size==1
  zero       = zero.first

  contract ||= zero[:contract]
  sym      ||= Cotcube::Helpers.get_id_set(contract: contract)


  if cached
    if interval.nil? or swap_type.nil?
      puts "Warning: Cannot use cache as both :interval and :swap_type must be given".light_yellow
    else
      cache = load_swaps(interval: interval, swap_type: swap_type, contract: contract, sym: sym, datetime: zero[:datetime])
      # if the current datetime was already processed but nothing has been found,
      # an 'empty' value is saved.
      # that means, if neither a swap (or more) nor :empty is found, the datetime has not been processed yet
      selected = cache.select{|sw| sw[:datetime] == zero[:datetime] and sw[:side] == side }
      unless selected.empty?
        puts 'cache_hit'.light_white if debug
        return (selected.first[:empty] ? [] : selected )
      end
    end
  end

  ###########################################################################################################################
  # prepare base (i.e. dupe the original, create proper :y, and reject unneeded items)
  #
  base = base.
    map { |x|
    y = x.dup
    y[:y] = (high ?
             (y[:high] - zero[:high]).round(8) :
             (zero[:low] - y[:low]).round(8)
            ) unless y[:high].nil?
    y
  }.
  reject{|b| b.nil? or b[:datetime] < first[:datetime] or b[:x] < 0 or b[:y].nil?}[range]

  # abs_peak is the absolute high / low of the base. the shearing operation ends there,
  # but results might be influenced when abs_peak becomes affected by :with_flaws
  unless manual
    abs_peak = base.send(high ? :max_by : :min_by){|x| x[high ? :high : :low] }[:datetime]
    base.reject!{|x| x[:datetime] < abs_peak}
  end

  ###########################################################################################################################z
  # only if (and only if) the range portion above change the underlying base
  #   the offset has to be fixed for :x and :y

  unless range == (0..-1)
    puts "adjusting range to '#{range}'".light_yellow if debug
    offset_x = base.last[:x]
    offset_y = base.last[:y]
    base.map!{|b| b[:x] -= offset_x; b[:y] -= offset_y  ; b}
  end

  ###########################################################################################################################
  # introducing :i to the base, which provides the negative index of the :base Array of the current element
  # this simplifies handling during the, where I can use the members array,
  # that will carry just the index of the original base, regardless how many array_members have be already dropped
  base.each_index.map{|i| base[i][:i] = -base.size + i }


  ###########################################################################################################################
  # LAMBDA no1: simplifying DEBUG output
  #
  present = lambda {|z|  z.slice(*%i[datetime high low x y i yy dx dev near miss dev]) }


  ###########################################################################################################################
  # LAMBDA no2: all members except the pivot itself now most probably are too far to the left
  #             finalizing tries to get the proper dx value for them
  #
  finalize = lambda do |results|
    results.map do |result|
      result[:members].each  do |member|
        next if member[:yy].nil? or member[:yy].round(PRECISION-5).zero?

        diff = (member[:x] - member[:dx]).abs / 2.0
        member[:dx] = member[:x] + diff
        # it employs another binary-search
        while member[:yy].round(PRECISION-5) != 0.0
          member[:yy] = shear_to_deg(deg: result[:deg], base: [ member ] ).first[:yy]
          diff /= 2.0
          if member[:yy] > 0
            member[:dx] += diff
          else
            member[:dx] -= diff
          end
        end
        member[:yy] = member[:yy].abs.round(PRECISION-5)
      end

      puts 'done!'.magenta if debug
      result[:members].each {|member| puts "finalizing #{member}".magenta } if debug
      result
    end
  end

  ###########################################################################################################################
  # LAMDBA no3:  the actual 'function' to retrieve the slope
  #
  # the idea implemented is based on the fact, that we don't know in which exact time of the interval the value
  #     was created. even further we know that the stencil might be incorrect. so after shearing the :x value of the
  #     recently found new member(s) is shifted by :deviation and shearing is repeated. this is done as long as new members
  #     are found.
  get_slope = lambda do |b|
    if debug
      puts "in get_slope ... SETTING BASE: ".light_green
      puts "Last: \t#{present.call b.last }".light_green
      puts "First:\t#{present.call b.first}".light_green
    end
    members = [ b.last[:i] ]
    loop do
      current_slope   = detect_slope(base: b, ticksize: sym[:ticksize], format: sym[:format], debug: debug)
      if debug
        puts "CURR: #{current_slope[:deg]} "
        current_slope[:members].each {|x| puts "CURR: #{present.call(x)}" }
      end
      current_members = current_slope[:members].map{|dot| dot[:i]}
      new_members = current_members - members
      puts "New members: #{new_members} (as of #{current_members} - #{members})" if debug
      # the return condition is if no new members are found in slope
      # except lowest members are neighbours, what (recursively) causes re-run until the
      # first member is solitary
      if new_members.empty?
        mem_sorted=members.sort
        if mem_sorted[1] == mem_sorted[0] + 1 and not manual
          b2 = b[mem_sorted[1]..mem_sorted[-1]].map{|x| x.dup; x[:dx] = nil; x}
          puts 'starting recursive rerun'.light_red if debug
          alternative_slope = get_slope.call(b2)
          alternative = alternative_slope[:members].map{|bar| bar[:i]}
          # the alternative won't be used if it misses out a member that would have
          # been in the 'original' slope
          if (mem_sorted[1..-1] - alternative).empty?
            current_slope = alternative_slope
            members = alternative
          end
        end

        current_slope[:raw]    = members.map{|i| base[i][:x]}

        members.sort_by{|i| -i}.each_with_index do |i, index|

          puts "#{index}\t#{range}\t#{present.call b[i]}".light_yellow if debug

          current_slope[:members] << b[i] unless current_slope[:members].map{|x| x[:datetime]}.include? b[i][:datetime]
          current_slope[:members].sort_by!{|x| x[:datetime]}
        end
        return current_slope

      end
      # all new members found in current iteration have now receive their new :x value, depending on their distance to
      #    to the origin. when exploring near distance, it is assumned, that the actual :y value might have an
      #    additional distance of 1, further distant points can be distant even :deviation, what defaults to 2
      #    covering e.g. a holiday when using a daily base
      new_members.each do |mem|
        current_deviation = (0.1 * b[mem][:x])
        current_deviation =  1                  if current_deviation < 1
        current_deviation =  deviation          if current_deviation > deviation
        b[mem][:dx] = b[mem][:x] + current_deviation
      end
      members += new_members
    end
  end # of lambda

  ###########################################################################################################################
  # Lambda no. 4: analyzing the slope, adding near misses and characteristics
  #
  # near misses are treated as full members, as for example stacked orders within a swap architecture might impede that the
  # peak runs to the maximum expansion
  #
  # first, the swap_base is created by shearing the entire base to current :deg
  # then all base members are selected that fit the desired :y range.
  # please note that here also the processing of :with_flaws takes place
  #
  # the key :dev is introduced, which is actually a ticksize-based variant of :yy

  analyze = lambda do |swaps|
    swaps.each do |swap|

      swap_base      = base.map{|y|
        x = y.slice(*%i[ datetime high low dist x y i yy dx ])
        current_member = swap[:members].find{|z| z[:datetime] == x[:datetime] }
        x[:dx] = current_member[:dx] if current_member
        x
      }
      swap_base      = shear_to_deg(base: swap_base, deg: swap[:deg])
      swap_base.map!{|x| x[:dev] = (x[:yy] / sym[:ticksize].to_f); x[:dev] = -( x[:dev] > 0 ? x[:dev].floor : x[:dev].ceil);  x}
      invalids       = swap_base.select{|x| x[:dev] < 0 }
      with_flaws = 0 unless with_flaws # support legacy versions, where with_flaws was boolean
      if with_flaws > 0
        # TODO: this behaves only as expected when with_flaws == 2
        last_invalid   = invalids[(invalids[-2][:i] + 1 == invalids[-1][:i] ) ? -3 : -2] rescue nil
      else
        last_invalid   = invalids.last
      end

      # the 'near' members are all base members found, that fit
      #  1. being positive (as being zero means that they are original members)
      #  2. match a valid :dev
      #  3. appeared later than :last_invalid
      near           = swap_base.select{|x|
        x[:dev] <= [ 5, (x[:x] / 100)+2 ].min and
          x[:dev].positive? and
          (last_invalid.nil? ? true : x[:datetime] > last_invalid[:datetime])
      }.map{|x| x[:near] = x[:dev]; x}

      # these then are added to the swap[:members] and for further processing swap_base is cleaned
      swap[:members] = (swap[:members] + near).sort_by{|x| x[:datetime] }
      swap_base.select!{|x| x[:datetime] >= swap[:members].first[:datetime]}

      ########################################################################33
      # now swap characteristics are calculated
      #
      # avg_dev: the average distance of high or low to the swap_line
      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
      # depth:   the maximum distance to the swap line
      swap[:depth]     = swap_base.max_by{|x| x[:dev]}[:dev]
      swap[:interval]  = interval
      swap[:swap_type] = swap_type
      swap[:raw]       = swap[:members].map{|x| x[:x]}.reverse
      swap[:size]      = swap[:members].size
      swap[:length]    = swap[:raw][-1] - swap[:raw][0]
      # rating:  the maximum distance of the 'most middle' point of the swap to the nearer end
      swap[:rating]    = swap[:raw][1..-2].map{ |dot| [ dot - swap[:raw][0], swap[:raw][-1] - dot].min }.max || 0
      swap[:datetime]  = swap[:members].last[:datetime]
      swap[:side]      = side
      rat = swap[:rating]
      # color:   to simplify human readability a standard set of colors for intraday and eod based swaps
      swap[:color]     =  (rat > 75)  ? :light_blue : (rat > 30) ? :magenta : (rat > 15) ? :light_magenta : (rat > 7) ? (high ? :light_green : :light_red) : high ? :green : :red
      unless %i[ daily continuous ].include? interval
        swap[:color]     = ((rat > 150) ? :light_blue : (rat > 80) ? :magenta : (rat > 30) ? :light_magenta : (rat > 15) ? :light_yellow : high ? :green : :red)
      end
      swap[:diff]      = (swap[:members].last[ high ? :high : :low ] - swap[:members].first[ high ? :high : :low ]).round(8)
      swap[:ticks]     = (swap[:diff] / sym[:ticksize]).to_i
      # tpi:     ticks per interval, how many ticks are passed each :interval
      swap[:tpi]       = (swap[:ticks].to_f / swap[:length]).round(3)
      # ppi:     power per interval, how many $dollar value is passed each :interval
      swap[:ppi]       = (swap[:tpi] * sym[:power]).round(3)
    end # swap
  end # lambda

  ###########################################################################################################################
  # after declaring lambdas, the rest is quite few code
  #
  # starting with the full range, a valid slope is searched. the found slope defines an interval of the
  # base array, in which again a (lower) slope can be uncovered.
  #
  # this process is repeated while the interval to be processed is large enough (:min_length)
  current_range = (0..-1)                                                                                         # RANGE   set
  current_slope = { members: [] }                                                                                 # SLOPE   reset
  current_base = base[current_range].map{|z| z.slice(*%i[datetime high low x y i ])}                              # BASE    set
  current_results = [ ]                                                                                           # RESULTS reset
  binding.irb if debug
  while current_base.size >= min_length                                                                           # LOOP

    puts '-------------------------------------------------------------------------------------' if debug

    while current_base.size >= min_length and current_slope[:members].size < 2

      puts "---- #{current_base.size} #{current_range.to_s.light_yellow} ------" if debug

      # get new slope
      current_slope = get_slope.call(current_base)                                                                # SLOPE   call

      # define new range and base
      next_i  = current_slope[:members].select{|z| z[:miss].nil? and z[:near].nil?}[-2]
      current_range = ((next_i.nil? ? -2 : next_i[:i])+1..-1)                                                     # RANGE   adjust
      current_base = base[current_range].map{|z| z.slice(*%i[datetime high low x y i ])}                          # BASE    adjust
    end
    puts "Current slope: ".light_yellow + "#{current_slope}" if debug
    current_results << current_slope if current_slope                                                             # RESULTS add
    current_slope = { members: [] }                                                                               # SLOPE   reset
  end

  finalize.call(current_results)
  analyze.call(current_results)
  binding.irb if debug

  # reject all results that do not suffice
  current_results.reject!{|swap| swap[:rating] < min_rating or swap[:length] < min_length or min_ratio.call(swap[:rating],swap[:length])}

  #####################################################################################################################3
  # finally save results for caching and return them
  if save
    if interval.nil? or swap_type.nil?
      puts "WARNING: Cannot save swaps, as both :interval and :swap_type must be given".colorize(:light_yellow)
    else
      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 }
      to_save = current_results.empty? ? [ { datetime: zero[:datetime], side: side, empty: true, interval: interval, swap_type: swap_type } ] : current_results
      save_swaps(to_save, interval: interval, swap_type: swap_type, contract: contract, sym: sym)
    end
  end
  current_results
end