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