cotcube-level 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c1c9fc6eb075ccc486c29c13d9989e52b5e659330cba2b900bdd4f420d085bb8
4
+ data.tar.gz: 6303babcc58fd8def53c747174c0feaa669c848787c0c32eaa811e0f76aad61f
5
+ SHA512:
6
+ metadata.gz: a5bc9e406caecd100ad2aebaf5934f9c2b6f0b3c43dc51aa76b63575bb52bd77fec7b06f5b183801149f6b35fb367462261e93bb2c174ef8b10b29bb61ee2370
7
+ data.tar.gz: b1de063e02b133b76aea5ffe01b357fad9b9ed99461225cf8780b7cc592ea00bd043afef6d957a3a2f78db02e75291bfac775aa2c56c3cd36886378fbd7f7767
data/.irbrc.rb ADDED
@@ -0,0 +1,12 @@
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'
data/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ ## 0.2.0 (August 17, 2021)
2
+ - cotcube-level: added new module_functions, added restrictive constants for intervals and swaptypes
3
+ - adding new features to so-called 'test suite'
4
+ - triangulate: added saving/caching, added rejection of base data older than abs_peak, utilized new output helpers, added analyzation lambda
5
+ - helpers: added output helpers and save / load
6
+ - fixed License definition
7
+ - added current stuff to main module loader 'cotcube-level.rb'
8
+ - added (copied from legacy swapseeker) and adapted module functions for swap detection (shearing, detect_slope, triangulate)
9
+ - added (copied from legacy swapseeker) and adapted stencil model
10
+ - added trivial testsuite for days, which contains loading of example data, stencil generation and slope detection
11
+
12
+ ## 0.1.0 (May 07, 2021)
13
+ - initial commit
14
+
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in bitangent.gemspec
4
+ gemspec
5
+
data/README.md ADDED
@@ -0,0 +1,49 @@
1
+ ## Cotcube::Level
2
+
3
+ When naming this gem, I had in mind the German level a.k.a spirit level. But naming was not quite in the beginning
4
+ of the development process. All in the beginning was the wish to create an algorithm to automatically draw trend
5
+ lines. So for like 2 year I was whirling my head on how to put an algorithmic ruler on a chart and rotate it until
6
+ one or a set of trend lines are concise outpout.
7
+
8
+ As most hard do accomplish thing turn out to be much easier when you put them up side down, same happend to me in this
9
+ matter. I found rotating the ruler is too hard for, but instead transforming (shearing) the chart itself while keeping
10
+ the ruler at its _level_ is much more eligible.
11
+
12
+ The idea and the development of the algorithm had taken place within another Cotcube project named 'SwapSeeker'. At
13
+ some point the SwapSeeker has become too complex so I decided to decouple the Level and the Stencil as independent
14
+ functional unit, that can be used on arbitrary time series.
15
+
16
+ ### The shear mapping
17
+
18
+ There is really no magic in it. The timeseries (or basically an interval of a timeseries) needs to be prepared to locate
19
+ in Cartesian Quadrant I with fitting x==0 to y==0, and then a binary search on shearing angles determines the resulting
20
+ muiltitangent of which as the origin ('now') one point already is given. As limitation only shearing between 0 and 90
21
+ deg is supported.
22
+
23
+ The result contains
24
+
25
+ - only the origin, if it is the absolute high (resp. low) within the provided interval of the time series. This happens
26
+ to happen if _deg -> 0_.
27
+ - the origin and one arbitrary point, the usual result.
28
+ - the origin and two or more points residing on the same level--what is the desired result showing mathematical accuracy where
29
+ human eye cannot detect it in time.
30
+
31
+ ### The stencil
32
+
33
+ The shearing transformation is based on _x_ and _y_ values, where y obviously are the values of the series while _x_
34
+ refers to the time. It turned out a much bigger challenge to create an expedient mapping from DateTime to Integer,
35
+ where, as you foresee, 'now' should result in _x.zero?_.
36
+
37
+ ## Usage
38
+
39
+ TODO: Write usage instructions here
40
+
41
+ ## Development
42
+
43
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
44
+
45
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
46
+
47
+ ## License
48
+
49
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/BSD-3-Clause).
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'cotcube-level'
5
+ spec.version = File.read("#{__dir__}/VERSION")
6
+ spec.authors = ['Benjamin L. Tischendorf']
7
+ spec.email = ['donkeybridge@jtown.eu']
8
+
9
+ spec.summary = 'A gem to shear a time series '
10
+ spec.description = 'A gem to shear a time series, basically serving as a yet unssen class of indicators.'
11
+
12
+ spec.homepage = 'https://github.com/donkeybridge/' + spec.name
13
+ spec.license = 'BSD-3-Clause'
14
+ spec.required_ruby_version = Gem::Requirement.new('~> 2.7')
15
+
16
+ spec.metadata['homepage_uri'] = spec.homepage
17
+ spec.metadata['source_code_uri'] = spec.homepage
18
+ spec.metadata['changelog_uri'] = spec.homepage + '/CHANGELOG.md'
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ end
25
+ spec.bindir = 'bin'
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ['lib']
28
+
29
+ spec.add_dependency 'activesupport', '~> 6'
30
+ spec.add_dependency 'colorize', '~> 0.8'
31
+ spec.add_dependency 'cotcube-helpers', '~> 0.1'
32
+ spec.add_dependency 'yaml', '~> 0.1'
33
+
34
+ spec.add_development_dependency 'rake', '~> 13'
35
+ spec.add_development_dependency 'rspec', '~>3.6'
36
+ spec.add_development_dependency 'yard', '~>0.9'
37
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+ #
3
+
4
+ require 'active_support'
5
+ require 'active_support/core_ext/time'
6
+ require 'active_support/core_ext/numeric'
7
+ require 'colorize'
8
+ require 'date' unless defined?(DateTime)
9
+ require 'csv' unless defined?(CSV)
10
+ require 'yaml' unless defined?(YAML)
11
+ require 'json' unless defined?(JSON)
12
+ require 'digest' unless defined?(Digest)
13
+ require 'cotcube-helpers'
14
+
15
+ # require_relative 'cotcube-level/filename
16
+
17
+ %w[ stencil detect_slope triangulate helpers].each do |part|
18
+ require_relative "cotcube-level/#{part}"
19
+ end
20
+
21
+
22
+
23
+ module Cotcube
24
+ module Level
25
+
26
+ PRECISION = 7
27
+ INTERVALS = %i[ daily ]
28
+ SWAPTYPES = %i[ full ]
29
+ #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,
33
+ :rad2deg,
34
+ :deg2rad,
35
+ :puts_swaps,
36
+ :save_swaps,
37
+ :get_jsonl_name,
38
+ :load_swaps,
39
+ :member_to_human,
40
+ :triangulate
41
+
42
+ # please note that module_functions of sources provided in private files must slso be published within these
43
+ end
44
+ end
45
+
@@ -0,0 +1,117 @@
1
+ module Cotcube
2
+ module Level
3
+ #
4
+ # TODO: add support for slopes not only exactly matching but also allow 'dip' of n x ticksize
5
+ def detect_slope(base:, max: 90, debug: false, format: '% 5.2f', calculus: false, ticksize: nil, max_dev: 200)
6
+ raise ArgumentError, "'0 < max < 90, but got '#{max}'" unless max.is_a? Numeric and 0 < max and max <= 90
7
+ #
8
+ # aiming for a shearing angle, all but those in a line below the abscissa
9
+ #
10
+ # doing a binary search starting at part = 45 degrees
11
+ # on each iteration,
12
+ # part is halved and added or substracted based on current success
13
+ # if more than the mandatory result is found, all negative results are removed and degrees are increased by part
14
+ #
15
+ raise ArgumentError, 'detect_slope needs param Array :base' unless base.is_a? Array
16
+
17
+ # from given base, choose non-negative stencil containing values
18
+ old_base = base.dup.select{|b| b[:x] >= 0 and not b[:y].nil? }
19
+
20
+ # some debug output
21
+ old_base.each {|x| p x} if old_base.size < 50 and debug
22
+
23
+ # set initial shearing angle if not given as param
24
+ deg ||= -max / 2.0
25
+
26
+ # create first sheering. please note how selection working with d[:yy]
27
+ new_base = shear_to_deg(base: old_base, deg: deg).select { |d| d[:yy] >= 0 } #-ticksize }
28
+
29
+ # debug output
30
+ puts "Iterating slope:\t#{format '% 7.5f',deg
31
+ }\t\t#{new_base.size
32
+ } || #{new_base.values_at(*[0]).map{|f| "'#{f[:x]
33
+ } | #{format format,f[:y]
34
+ } | #{format format,f[:yy]}'"}.join(" || ") }" if debug
35
+ # set initial part to deg
36
+ part = deg.abs
37
+ #
38
+ # the loop, that runs until either
39
+ # - only two points are left on the slope
40
+ # - the slope has even angle
41
+ # - several points are on the slope in quite a good approximation ('round(7)')
42
+ #
43
+ until deg.round(PRECISION).zero? || part.round(PRECISION).zero? ||
44
+ ((new_base.size >= 2) && (new_base.map { |f| f[:yy].round(PRECISION).zero? }.uniq.size == 1))
45
+
46
+ part /= 2.0
47
+ if new_base.size == 1
48
+ # the graph was sheared too far, reuse old_base
49
+ deg = deg + part
50
+ else
51
+ # the graph was sheared too short, continue with new base
52
+ deg = deg - part
53
+ old_base = new_base.dup unless deg.round(PRECISION).zero?
54
+ end
55
+
56
+ # the actual sheering operation
57
+ # note that this basically maps old_base with yy = y + (dx||x * tan(deg) )
58
+ #
59
+ new_base = shear_to_deg(base: old_base, deg: deg).select { |d| d[:yy] >= 0 } #-ticksize }
60
+ new_base.last[:dx] = 0.0
61
+ if debug
62
+ print " #{format '% 8.5f',deg}"
63
+ puts "Iterating slope:\t#{format '% 8.5f',deg
64
+ }\t#{new_base.size
65
+ } || #{new_base.values_at(*[0]).map{|f| "'#{f[:x]
66
+ } | #{format '%4.5f', part
67
+ } | #{format format,f[:y]
68
+ } | #{format format,f[:yy]}'"}.join(" || ") }"
69
+ end
70
+ end
71
+ puts ' done.' if debug
72
+
73
+ ### Sheering ends here
74
+
75
+ # define the approximited result as (also) 0.0
76
+ new_base.each{|x| x[:yy] = 0.0}
77
+ if debug
78
+ puts "RESULT: #{deg} #{deg2rad(deg)}"
79
+ new_base.each {|f| puts "\t#{f.inspect}" }
80
+ end
81
+ # there is speacial treatment for even slopes
82
+ if deg.round(PRECISION).zero?
83
+ #puts "found even slope"
84
+ # this is intentionally voided as evenness is calculated somewhere else
85
+ # even_base = base.dup.select{|b| b[:x] >= 0 and not b[:y].nil? }[-2..-1].map{|x| x.dup}
86
+ # last_barrier is the last bar, that exceeds
87
+ #binding.irb
88
+ #last_barrier = even_base.select{|bar| (bar[:y] - even_base.last[:y]).abs > evenness * ticksize}.last
89
+ #even_base.select!{|bar| (bar[:y] - even_base.last[:y]).abs <= evenness * ticksize}
90
+ # could be, that no last barrier exists, when there is a top or bottom plateau
91
+ #even_base.select!{|bar| bar[:x] < last_barrier[:x]} unless last_barrier.nil?
92
+ # TODO
93
+ return { deg: 0, slope: 0, members: [] } #, members: even_base.map { |x| xx = x.dup; %i[y yy].map { |z| xx[z]=nil }; xx } })
94
+ end
95
+
96
+
97
+ #####################################################################################
98
+ # Calculate the slope bsaed on the angle that resulted above
99
+ # y = m x + n -->
100
+ # m = delta-y / delta-x
101
+ # n = y0 - m * x0
102
+ #
103
+ slope = (new_base.first[:y] - new_base.last[:y]) / (
104
+ (new_base.first[:dx].nil? ? new_base.first[:x] : new_base.first[:dx]).to_f -
105
+ (new_base. last[:dx].nil? ? new_base. last[:x] : new_base. last[:dx]).to_f
106
+ )
107
+ # the result
108
+ {
109
+ deg: deg,
110
+ slope: slope,
111
+ members: new_base.map { |x| x.dup }
112
+ }
113
+ end
114
+ end
115
+ end
116
+
117
+
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cotcube
4
+ module Level
5
+ def rad2deg(deg)
6
+ deg * 180 / Math::PI
7
+ end
8
+
9
+ def deg2rad(rad)
10
+ rad * Math::PI / 180
11
+ end
12
+
13
+ def shear_to_deg(base:, deg:)
14
+ shear_to_rad(base: base, rad: deg2rad(deg))
15
+ end
16
+
17
+ def shear_to_rad(base: , rad:)
18
+ tan = Math.tan(rad)
19
+ base.map { |bar|
20
+ # separating lines for easier debugging
21
+ bar[:yy] =
22
+ bar[:y] +
23
+ (bar[:dx].nil? ? bar[:x] : bar[:dx]) * tan
24
+ bar
25
+ }
26
+ end
27
+
28
+ def member_to_human(member,side: ,format:)
29
+ high = side == :high
30
+ "#{member[:datetime].strftime("%a, %Y-%m-%d %H:%M")
31
+ } x: #{format '%-4d', member[:x]
32
+ } dx: #{format '%-8.3f', (member[:dx].nil? ? member[:x] : member[:dx].round(3))
33
+ } #{high ? "high" : "low"
34
+ }: #{format format, member[high ? :high : :low]
35
+ } i: #{(format '%4d', member[:i]) unless member[:i].nil?
36
+ } #{member[:miss].nil? ? '' : "miss: #{member[:miss]}" }"
37
+ end
38
+
39
+ def puts_swaps(swaps, format: )
40
+ swaps = [ swaps ] unless swaps.is_a? Array
41
+ swaps.each do |swap|
42
+ puts "side: #{swap[:side] }\tlen: #{swap[:length]} \trating: #{swap[:rating]}".colorize(swap[:color] || :white )
43
+ puts "diff: #{swap[:ticks]}\tdif: #{swap[:diff].round(7)}\tdepth: #{swap[:depth]}".colorize(swap[:color] || :white )
44
+ puts "tpi: #{swap[:tpi] }\tppi: #{swap[:ppi]}".colorize(swap[:color] || :white )
45
+ swap[:members].each {|x| puts member_to_human(x, side: swap[:side], format: format) }
46
+ end
47
+ end
48
+
49
+ def get_jsonl_name(interval:, swap_type:, contract:, sym: nil)
50
+ raise "Interval #{interval } is not supported, please choose from #{INTERVALS}" unless INTERVALS.include? interval
51
+ raise "Swaptype #{swap_type} is not supported, please choose from #{SWAPTYPES}" unless SWAPTYPES.include? swap_type
52
+ sym ||= Cotcube::Helpers.get_id_set(contract: contract)
53
+ root = '/var/cotcube/level'
54
+ dir = "#{root}/#{sym[:id]}"
55
+ symlink = "#{root}/#{sym[:symbol]}"
56
+ `mkdir -p #{dir}` unless File.exist?(dir)
57
+ `ln -s #{dir} #{symlink}` unless File.exist?(symlink)
58
+ file = "#{dir}/#{contract}_#{interval.to_s}_#{swap_type.to_s}.jsonl"
59
+ `touch #{file}`
60
+ file
61
+ end
62
+
63
+ def save_swaps(swaps, interval:, swap_type:, contract:, sym: nil)
64
+ file = get_jsonl_name(interval: interval, swap_type: swap_type, contract: contract, sym: sym)
65
+ swaps.each do |swap|
66
+ swap_json = swap.to_json
67
+ digest = Digest::SHA256.hexdigest swap_json
68
+ res = `cat #{file} | grep #{digest}`.strip
69
+ unless res.empty?
70
+ puts "Cannot save swap, it is already in #{file}:"
71
+ p swap
72
+ else
73
+ swap[:digest] = digest
74
+ File.open(file, 'a+'){|f| f.write(swap.to_json + "\n") }
75
+ end
76
+ end
77
+ end
78
+
79
+ def load_swaps(interval:, swap_type:, contract:, sym: nil)
80
+ file = get_jsonl_name(interval: interval, swap_type: swap_type, contract: contract, sym: sym)
81
+ jsonl = File.read(file)
82
+ jsonl.
83
+ each_line.
84
+ map do |x|
85
+ JSON.parse(x).
86
+ deep_transform_keys(&:to_sym).
87
+ tap do |sw|
88
+ sw[:datetime] = DateTime.parse(sw[:datetime]) rescue nil
89
+ sw[:side] = sw[:side].to_sym
90
+ unless sw[:empty]
91
+ sw[:color] = sw[:color].to_sym
92
+ sw[:members].map{|mem| mem[:datetime] = DateTime.parse(mem[:datetime]) }
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ end
100
+
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cotcube
4
+ module Level
5
+
6
+ class Stencil
7
+ attr_accessor :base
8
+ attr_reader :interval
9
+
10
+
11
+ # Class method that loads the (latest) obligatory stencil for given interval and type.
12
+ # These raw stencils are located in /var/cotcube/level/stencils
13
+ #
14
+ # Current daily stencils contain dates from 2020-01-01 to 2023-12-31
15
+ #
16
+ def self.provide_raw_stencil(type:, interval: :daily, version: nil)
17
+ loading = lambda do |typ|
18
+ file_base = "/var/cotcube/level/stencils/stencil_#{interval.to_s}_#{typ.to_s}.csv_"
19
+ if Dir["#{file_base}?*"].empty?
20
+ raise ArgumentError, "Could not find any stencil matching interval #{interval} and type #{typ}. Check #{file_base} manually!"
21
+ end
22
+ if version.nil? # use latest available version if not given
23
+ file = Dir["#{file_base}?*"].sort.last
24
+ else
25
+ file = "#{file_base}#{version}"
26
+ unless File.exist? file
27
+ raise ArgumentError, "Cannot open stencil from non-existant file #{file}."
28
+ end
29
+ end
30
+ CSV.read(file).map{|x| { datetime: CHICAGO.parse(x.first).freeze, x: x.last.to_i.freeze } }
31
+ end
32
+ unless const_defined? :RAW_STENCILS
33
+ const_set :RAW_STENCILS, { daily:
34
+ { full: loading.call( :full).freeze,
35
+ rtc: loading.call( :rtc).freeze
36
+ }.freeze
37
+ }.freeze
38
+ end
39
+ RAW_STENCILS[interval][type].map{|x| x.dup}
40
+ end
41
+
42
+ def initialize(
43
+ range: nil, # used to shrink the stencil size, accepts String or Date
44
+ interval:,
45
+ swap_type:,
46
+ ranges: nil, # currently not used, prepared to be used in connection intraday
47
+ contract: nil,
48
+ date: nil,
49
+ debug: false,
50
+ version: nil, # when referring to a specicic version of the stencil
51
+ timezone: CHICAGO,
52
+ stencil: nil, # instead of loading, use this data
53
+ #config: init,
54
+ warnings: true
55
+ )
56
+ @debug = debug
57
+ @interval = interval
58
+ @swap_type = swap_type
59
+ @swaps = []
60
+ @contract = contract
61
+ @warnings = warnings
62
+ step = case @interval
63
+ when :hours, :hour; 1.hour
64
+ when :quarters, :quarter; 15.minutes
65
+ else; 1.day
66
+ end
67
+
68
+ case @interval
69
+ when :day, :days, :daily, :dailies, :synth, :synthetic #, :week, :weeks, :month, :months
70
+ unless range.nil?
71
+ starter = range.begin.is_a?(String) ? timezone.parse(range.begin) : range.begin
72
+ ender = range. end.is_a?(String) ? timezone.parse(range. end) : range. end
73
+ end
74
+
75
+ stencil_type = case swap_type
76
+ when :rth
77
+ :full
78
+ when :rthc
79
+ :rtc
80
+ else
81
+ swap_type
82
+ end
83
+ # TODO: Check / warn / raise whether stencil (if provided) is a proper data type
84
+ raise ArgumentError, "Stencil should be nil or Array" unless [NilClass, Array].include? stencil.class
85
+ raise ArgumentError, "Each stencil members should contain at least :datetime and :x" unless stencil.nil? or
86
+ stencil.map{|x| ([:datetime, :x] - x.keys).empty? and [ActiveSupport::TimeWithZone, Day].include?( x[:datetime] ) and x[:x].is_a?(Integer)}.reduce(:&)
87
+
88
+ base = stencil || Stencil.provide_raw_stencil(type: stencil_type, interval: :daily, version: version)
89
+
90
+ # fast forward to prev trading day
91
+ date = CHICAGO.parse(date) unless [NilClass, Date, ActiveSupport::TimeWithZone].include? date.class
92
+ @date = date || Date.today
93
+ best_match = base.select{|x| x[:datetime].to_date <= @date}.last[:datetime]
94
+ @date = best_match
95
+
96
+ offset = base.map{|x| x[:datetime]}.index(@date)
97
+
98
+ # apply offset to stencil, so zero will match today (or what was provided as 'date')
99
+ @base = base.map.
100
+ each_with_index{|d,i| d[:x] = (offset - i).freeze; d }
101
+ # if range was given, shrink stencil to specified range
102
+ @base.select!{|d| (d[:datetime] >= starter and d[:datetime] <= ender) } unless range.nil?
103
+ else
104
+ raise RuntimeError, "'interval: #{interval}' was provided, what does not match anything this tool can handle (currently :days, :dailies, :synthetic)."
105
+ end
106
+ end
107
+
108
+ def dup
109
+ Stencil.new(
110
+ debug: @debug,
111
+ interval: @interval,
112
+ swap_type: @swap_type,
113
+ date: @date,
114
+ contract: @contract,
115
+ stencil: @base.map{|x| x.dup}
116
+ )
117
+ end
118
+
119
+ def zero
120
+ @zero ||= @base.find{|b| b[:x].zero? }
121
+ end
122
+
123
+ def apply(to: )
124
+ offset = 0
125
+ @base.each_index do |i|
126
+ begin
127
+ offset += 1 while to[i+offset][:datetime].to_date < @base[i][:datetime].to_date
128
+ rescue
129
+ # appending
130
+ to << @base[i]
131
+ next
132
+ end
133
+ if to[i+offset][:datetime].to_date > @base[i][:datetime].to_date
134
+ # skipping
135
+ offset -= 1
136
+ next
137
+ end
138
+ # merging
139
+ to[i+offset][:x] = @base[i][:x]
140
+ end
141
+ # finally remove all bars that do not belong to the stencil (i.e. holidays)
142
+ to.reject!{|x| x[:x].nil? }
143
+ end
144
+
145
+ end
146
+
147
+ end
148
+
149
+ end
150
+
@@ -0,0 +1,238 @@
1
+ module Cotcube
2
+ module Level
3
+ def triangulate(
4
+ contract: nil, # contract actually isnt needed to triangulation, but allows much more convenient output
5
+ side:, # :upper or :lower
6
+ base:, # the base of a readily injected stencil
7
+ range: (0..-1), # range is relative to base
8
+ max: 90, # the range which to scan for swaps goes from deg 0 to max
9
+ debug: false,
10
+ format: '% 5.2f',
11
+ min_members: 3, # this param should not be changed manually, it is used for the guess operation
12
+ min_rating: 3, # swaps having a lower rating are discarded
13
+ allow_sub: true, # this param determines whether guess can be called or not
14
+ save: true, # allow saving of results
15
+ cached: true, # allow loading of yet cached intervals
16
+ interval: nil, # interval and swap_type are only needed if saving / caching of swaps is desired
17
+ swap_type: nil, # if not given, a warning is printed and swaps are not saved
18
+ deviation: 2)
19
+
20
+ raise ArgumentError, "'0 < max < 90, but got '#{max}'" unless max.is_a? Numeric and 0 < max and max <= 90
21
+ raise ArgumentError, 'need :side either :upper or :lower for dots' unless [:upper, :lower].include? side
22
+
23
+ ###########################################################################################################################
24
+ # init some helpers
25
+ #
26
+ high = side == :upper
27
+ first = base.to_a.find{|x| not x[:high].nil? }
28
+ zero = base.select{|x| x[:x].zero? }
29
+ raise ArgumentError, "Inappropriate base, it should contain ONE :x.zero, but contains #{zero.size}." unless zero.size==1
30
+ zero = zero.first
31
+ contract ||= zero.contract
32
+ sym = Cotcube::Helpers.get_id_set(contract: contract)
33
+
34
+ if cached
35
+ if interval.nil? or swap_type.nil?
36
+ puts "Warning: Cannot use cache as both :interval and :swap_type must be given".light_yellow
37
+ else
38
+ cache = load_swaps(interval: interval, swap_type: swap_type, contract: contract, sym: sym)
39
+ selected = cache.select{|sw| sw[:datetime] == zero[:datetime] and sw[:side] == side}
40
+ unless selected.empty?
41
+ puts 'cache_hit'.light_white if debug
42
+ return (selected.first[:empty] ? [] : selected )
43
+ end
44
+ end
45
+ end
46
+ ticksize = sym[:ticksize] / sym[:bcf] # need to adjust, as we are working on barchart data, not on exchange data !!
47
+
48
+
49
+ ###########################################################################################################################
50
+ # prepare base (i.e. dupe the original, create proper :y, and reject unneeded items)
51
+ #
52
+ base = base.
53
+ map { |x|
54
+ y = x.dup
55
+ y[:y] = (high ?
56
+ (y[:high] - zero[:high]).round(8) :
57
+ (zero[:low] - y[:low]).round(8)
58
+ ) unless y[:high].nil?
59
+ y
60
+ }.
61
+ reject{|b| b.nil? or b[:datetime] < first[:datetime] or b[:x] < 0 or b[:y].nil?}[range]
62
+ abs_peak = base.send(high ? :max_by : :min_by){|x| x[high ? :high : :low] }[:datetime]
63
+
64
+ base.reject!{|x| x[:datetime] < abs_peak}
65
+
66
+ ###########################################################################################################################z
67
+ # only if (and only if) the range portion above change the underlying base
68
+ # the offset has to be fixed for :x and :y
69
+
70
+ unless range == (0..-1)
71
+ puts "adjusting range to '#{range}'".light_yellow if debug
72
+ offset_x = base.last[:x]
73
+ offset_y = base.last[:y]
74
+ base.map!{|b| b[:x] -= offset_x; b[:y] -= offset_y ; b}
75
+ end
76
+ base.each_index.map{|i| base[i][:i] = -base.size + i }
77
+
78
+
79
+ ###########################################################################################################################
80
+ # LAMBDA no1: simplifying DEBUG output
81
+ #
82
+ present = lambda {|z| swap_to_human(z) }
83
+
84
+
85
+
86
+ ###########################################################################################################################
87
+ # LAMBDA no2: all members except the pivot itself now most probably are too far to the left
88
+ # finalizing tries to get the proper dx value for them
89
+ #
90
+ finalize = lambda do |res|
91
+ res.map do |r|
92
+ r[:members].each do |m|
93
+ next if m[:yy].nil? or m[:yy].zero?
94
+
95
+ diff = (m[:x] - m[:dx]).abs / 2.0
96
+ m[:dx] = m[:x] + diff
97
+ # it employs another binary-search
98
+ while m[:yy].round(PRECISION) != 0 # or m[:yy].round(PRECISION) < -ticksize
99
+ print '.' if debug
100
+ m[:yy] = shear_to_deg(deg: r[:deg], base: [ m ] ).first[:yy]
101
+ diff /= 2.0
102
+ if m[:yy] > 0
103
+ m[:dx] += diff
104
+ else
105
+ m[:dx] -= diff
106
+ end
107
+ end
108
+ m[:yy] = m[:yy].abs.round(8)
109
+ end # r.members
110
+ puts 'done!'.light_yellow if debug
111
+
112
+ r[:members].each {|x| puts "finalizing #{x}".magenta } if debug
113
+ ## The transforming part
114
+
115
+ r
116
+ end # res
117
+ end # lambda
118
+
119
+ ###########################################################################################################################
120
+ # LAMDBA no3: the actual 'function' to retrieve the slope
121
+ #
122
+ get_slope = lambda do |b|
123
+ if debug
124
+ puts "in get_slope ... SETTING BASE: ".light_green
125
+ puts "Last:\t#{present.call b.last}"
126
+ puts "First:\t#{present.call b.first}"
127
+ end
128
+ members = [ b.last[:i] ]
129
+ loop do
130
+ current_slope = detect_slope(base: b, ticksize: ticksize, format: sym[:format], debug: debug)
131
+ current_members = current_slope[:members]
132
+ .map{|dot| dot[:i]}
133
+ new_members = current_members - members
134
+ puts "New members: #{new_members} as of #{current_members} - #{members}" if debug
135
+ # the return condition is if no new members are found in slope
136
+ # except lowest members are neighbours, what causes a re-run
137
+ if new_members.empty?
138
+ mem_sorted=members.sort
139
+ if mem_sorted[1] == mem_sorted[0] + 1
140
+ b2 = b[mem_sorted[1]..mem_sorted[-1]].map{|x| x.dup; x[:dx] = nil; x}
141
+ puts 'starting rerun' if debug
142
+ alternative_slope = get_slope.call(b2)
143
+ alternative = alternative_slope[:members].map{|bar| bar[:i]}
144
+ if (mem_sorted[1..-1] - alternative).empty?
145
+ current_slope = alternative_slope
146
+ members = alternative
147
+ end
148
+ end
149
+ if min_members >= 3 and members.size >= 3
150
+ current_slope[:raw] = members.map{|x| x.abs }.sort
151
+ current_slope[:length] = current_slope[:raw][-1] - current_slope[:raw][0]
152
+ current_slope[:rating] = current_slope[:raw][1..-2].map{|dot| [ dot - current_slope[:raw][0], current_slope[:raw][-1] - dot].min }.max
153
+ end
154
+ members.sort_by{|i| -i}.each do |x|
155
+ puts "#{range}\t#{present.call(b[x])}" if debug
156
+ current_slope[:members] << b[x] unless current_slope[:members].map{|x| x[:datetime]}.include? b[x][:datetime]
157
+ current_slope[:members].sort_by!{|x| x[:datetime]}
158
+ end
159
+ return current_slope
160
+
161
+ end
162
+ new_members.each do |mem|
163
+ current_deviation = (0.1 * b[mem][:x])
164
+ current_deviation = 1 if current_deviation < 1
165
+ current_deviation = deviation if current_deviation > deviation
166
+ b[mem][:dx] = b[mem][:x] + current_deviation
167
+ end
168
+ members += new_members
169
+ end
170
+ end # of lambda
171
+
172
+ analyze = lambda do |swaps|
173
+ swaps.each do |swap|
174
+ swap[:datetime] = swap[:members].last[:datetime]
175
+ swap[:side] = side
176
+ rat = swap[:rating]
177
+ swap[:color ] = (rat > 75) ? :light_blue : (rat > 30) ? :magenta : (rat > 15) ? :light_magenta : (rat > 7) ? (high ? :light_green : :light_red) : high ? :green : :red
178
+ swap[:diff] = swap[:members].last[ high ? :high : :low ] - swap[:members].first[ high ? :high : :low ]
179
+ swap[:ticks] = (swap[:diff] / sym[:ticksize]).to_i
180
+ swap[:tpi] = (swap[:ticks].to_f / swap[:length]).round(3)
181
+ swap[:ppi] = (swap[:tpi] * sym[:power]).round(3)
182
+ swap_base = shear_to_deg(base: base[swap[:members].first[:i]..], deg: swap[:deg]).map{|x| x[:dev] = (x[:yy] / sym[:ticksize]).abs.floor; x}
183
+ swap[:depth] = swap_base.max_by{|x| x[:dev]}[:dev]
184
+ swap[:avg_dev]= (swap_base.reject{|x| x[:dev].zero?}.map{|x| x[:dev]}.reduce(:+) / (swap_base.size - swap[:members].size).to_f).ceil rescue 0
185
+ # a miss is considered a point that is less than 10% of the average deviation away of the slope
186
+ unless swap[:avg_dev].zero?
187
+ misses = swap_base.select{|x| x[:dev] <= swap[:avg_dev] / 10.to_f and x[:dev] > 0}.map{|x| x[:miss] = x[:dev]; x}
188
+ # misses are sorted among members, but stay marked
189
+ swap[:members]= (swap[:members] + misses).sort_by{|x| x[:datetime] }
190
+ end
191
+ end # swap
192
+ end # of lambda
193
+
194
+ ###########################################################################################################################
195
+ # after declaring lambdas, the rest is quite few code
196
+ #
197
+ current_range = (0..-1) # RANGE set
198
+ current_slope = { members: [] } # SLOPE reset
199
+ current_base = base[current_range] # BASE set
200
+ current_results = [ ] # RESULTS reset
201
+ while current_base.size >= 5 # LOOP
202
+
203
+ puts '-------------------------------------------------------------------------------------' if debug
204
+
205
+ while current_base.size >= 5 and current_slope[:members].size < min_members
206
+ puts "---- #{current_base.size} #{current_range.to_s.light_yellow} ------" if debug
207
+ current_slope = get_slope.call(current_base) # SLOPE call
208
+ next_i = current_slope[:members][-2]
209
+ current_range = ((next_i.nil? ? -2 : next_i[:i])+1..-1) # RANGE adjust
210
+ current_base = base[current_range] # BASE adjust
211
+ if debug
212
+ print 'Hit <enter> to continue...'
213
+ STDIN.gets
214
+ end
215
+ end
216
+ puts "Current slope: ".light_yellow + "#{current_slope}" if debug
217
+ current_results << current_slope if current_slope # RESULTS add
218
+ current_slope = { members: [] } # SLOPE reset
219
+ end
220
+ current_results.select!{|x| x[:members].size >= min_members }
221
+
222
+ # Adjust all members (except pivot) to fit the actual dx-value
223
+ finalize.call(current_results)
224
+ analyze.call(current_results)
225
+ current_results.reject!{|swap| swap[:rating] < min_rating}
226
+ if save
227
+ if interval.nil? or swap_type.nil?
228
+ puts "WARNING: Cannot save swaps, as both :interval and :swap_type must be given".colorize(:light_yellow)
229
+ else
230
+ to_save = current_results.empty? ? [ { datetime: zero[:datetime], side: side, empty: true } ] : current_results
231
+ save_swaps(to_save, interval: interval, swap_type: swap_type, contract: contract, sym: sym)
232
+ end
233
+ end
234
+ current_results
235
+ end
236
+ end
237
+ end
238
+
metadata ADDED
@@ -0,0 +1,156 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cotcube-level
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Benjamin L. Tischendorf
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-08-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: colorize
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.8'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.8'
41
+ - !ruby/object:Gem::Dependency
42
+ name: cotcube-helpers
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.1'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: yaml
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '13'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '13'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.6'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.6'
97
+ - !ruby/object:Gem::Dependency
98
+ name: yard
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.9'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.9'
111
+ description: A gem to shear a time series, basically serving as a yet unssen class
112
+ of indicators.
113
+ email:
114
+ - donkeybridge@jtown.eu
115
+ executables: []
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - ".irbrc.rb"
120
+ - CHANGELOG.md
121
+ - Gemfile
122
+ - README.md
123
+ - VERSION
124
+ - cotcube-level.gemspec
125
+ - lib/cotcube-level.rb
126
+ - lib/cotcube-level/detect_slope.rb
127
+ - lib/cotcube-level/helpers.rb
128
+ - lib/cotcube-level/stencil.rb
129
+ - lib/cotcube-level/triangulate.rb
130
+ homepage: https://github.com/donkeybridge/cotcube-level
131
+ licenses:
132
+ - BSD-3-Clause
133
+ metadata:
134
+ homepage_uri: https://github.com/donkeybridge/cotcube-level
135
+ source_code_uri: https://github.com/donkeybridge/cotcube-level
136
+ changelog_uri: https://github.com/donkeybridge/cotcube-level/CHANGELOG.md
137
+ post_install_message:
138
+ rdoc_options: []
139
+ require_paths:
140
+ - lib
141
+ required_ruby_version: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '2.7'
146
+ required_rubygems_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ requirements: []
152
+ rubygems_version: 3.1.2
153
+ signing_key:
154
+ specification_version: 4
155
+ summary: A gem to shear a time series
156
+ test_files: []