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 +7 -0
- data/.irbrc.rb +12 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile +5 -0
- data/README.md +49 -0
- data/VERSION +1 -0
- data/cotcube-level.gemspec +37 -0
- data/lib/cotcube-level.rb +45 -0
- data/lib/cotcube-level/detect_slope.rb +117 -0
- data/lib/cotcube-level/helpers.rb +100 -0
- data/lib/cotcube-level/stencil.rb +150 -0
- data/lib/cotcube-level/triangulate.rb +238 -0
- metadata +156 -0
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
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: []
|