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
- .check_exceedance ⇒ Object
- .deg2rad ⇒ Object
- .detect_slope ⇒ Object
- .get_jsonl_name ⇒ Object
- .load_swaps ⇒ Object
- .mark_exceeded ⇒ Object
- .mark_ignored ⇒ Object
- .member_to_human ⇒ Object
- .puts_swap ⇒ Object
- .rad2deg ⇒ Object
- .save_swaps ⇒ Object
- .shear_to_deg ⇒ Object
- .shear_to_rad ⇒ Object
- .tritangulate ⇒ Object
Instance Method Summary collapse
-
#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).
- #deg2rad(rad) ⇒ Object
- #detect_slope(base:, max: 90, debug: false, format: '% 5.2f', calculus: false, ticksize: nil, max_dev: 200) ⇒ Object
-
#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.
-
#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.
- #mark_exceeded(swap:, datetime:, debug: false, sym: nil) ⇒ Object
- #mark_ignored(swap:, datetime: DateTime.now, sym:, debug: true) ⇒ Object
-
#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.
-
#puts_swap(swap, format:, short: true, notice: nil, hash: 3, tws: false) ⇒ Object
human readable output format: e.g.
-
#rad2deg(deg) ⇒ Object
3 simple, self-explaining helpers.
-
#save_swaps(swaps, interval:, swap_type:, contract:, sym: nil, quiet: false) ⇒ Object
the name says it all.
- #shear_to_deg(base:, deg:) ⇒ Object
-
#shear_to_rad(base:, rad:) ⇒ Object
the actual shearing takes place here.
- #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
Class Method Details
.check_exceedance ⇒ Object
.deg2rad ⇒ Object
.detect_slope ⇒ Object
.get_jsonl_name ⇒ Object
.load_swaps ⇒ Object
.mark_exceeded ⇒ Object
.mark_ignored ⇒ Object
.member_to_human ⇒ Object
.puts_swap ⇒ Object
.rad2deg ⇒ Object
.save_swaps ⇒ Object
.shear_to_deg ⇒ Object
.shear_to_rad ⇒ Object
.tritangulate ⇒ Object
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
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:
-
a swap
-
an ‘empty’ information, referring to an interval that has been processed but no swaps were found
-
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
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{|| [: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] < 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 |