cotcube-level 0.2.0

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