cotcube-helpers 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 819dae89b6428b3cec4226d40f4afa3b790108b2e7a0f8ff9784b49532a894e9
4
+ data.tar.gz: 2e4823aa3a4f86efa8d94ff07be306cb991a7bd08504cae0209025127e67d472
5
+ SHA512:
6
+ metadata.gz: 46647ed87173997191a22b05072e0ce4b53bba9be4a222d86a92605bf35d9c3d4d33ed0381c6ab3e46106e21851d10f4b9b3b178136bf780bdcf89ce055cc469
7
+ data.tar.gz: 7acdf173d676c7db61e6e2a1aa3111b22ae00c55768611d7355b4f13216c225f9e5ef9204a23796694b95d21d955e7a53e72d3b62b390e3735786aceb0b67801
@@ -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-helpers'
@@ -0,0 +1,3 @@
1
+ ## 0.1.1 (December 21, 2020)
2
+
3
+
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/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'cotcube-helpers'
5
+ spec.version = File.read("#{__dir__}/VERSION")
6
+ spec.authors = ['Benjamin L. Tischendorf']
7
+ spec.email = ['donkeybridge@jtown.eu']
8
+
9
+ spec.summary = 'Some helpers and core extensions as part of the Cotcube Suite.'
10
+ spec.description = 'Some helpers and core extensions as part of the Cotcube Suite...'
11
+
12
+ spec.homepage = 'https://github.com/donkeybridge/'+ spec.name
13
+ spec.license = 'BSD-4-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'
30
+
31
+
32
+ spec.add_development_dependency 'rake'
33
+ spec.add_development_dependency 'rspec', '~>3.6'
34
+ spec.add_development_dependency 'yard', '~>0.9'
35
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/core_ext/time'
5
+ require 'active_support/core_ext/numeric'
6
+ require 'parallel'
7
+
8
+ require_relative 'cotcube-helpers/array_ext'
9
+ require_relative 'cotcube-helpers/enum_ext'
10
+ require_relative 'cotcube-helpers/hash_ext'
11
+ require_relative 'cotcube-helpers/range_ext'
12
+ require_relative 'cotcube-helpers/string_ext'
13
+ require_relative 'cotcube-helpers/subpattern.rb'
14
+ require_relative 'cotcube-helpers/parallelize'
15
+ require_relative 'cotcube-helpers/simple_output'
16
+ require_relative 'cotcube-helpers/input'
17
+
18
+
19
+ module Cotcube
20
+ module Helpers
21
+
22
+ module_function :sub,
23
+ :parallelize,
24
+ :keystroke
25
+
26
+
27
+
28
+ # please not that module_functions of source provided in private files must be published there
29
+ end
30
+ end
31
+
@@ -0,0 +1,53 @@
1
+ class Array
2
+
3
+ # returns nil if the compacted array is empty, otherwise returns the compacted array
4
+ def compact_or_nil(*args, &block)
5
+ return nil if self.compact == []
6
+ yield self.compact
7
+ end
8
+
9
+ # sorts by a given attribute and then returns groups of where this attribute is equal
10
+ # .... seems like some_array.group_by(&attr).values
11
+ def split_by(attrib)
12
+ res = []
13
+ sub = []
14
+ self.sort_by(&attrib).each do |elem|
15
+ if sub.empty? or sub.last[attrib] == elem[attrib]
16
+ sub << elem
17
+ else
18
+ res << sub
19
+ sub = [ elem ]
20
+ end
21
+ end
22
+ res << sub
23
+ res
24
+ end
25
+
26
+ # This method iterates over an Array by calling the given block on all 2 consecutive elements
27
+ # it returns a Array of self.size - 1
28
+ #
29
+ def pairwise(&block)
30
+ raise ArgumentError, 'Array.one_by_one needs an arity of 2 (i.e. |a, b|)' unless block.arity == 2
31
+ return [] if size <= 1
32
+
33
+ each_with_index.map do |_, i|
34
+ next if i.zero?
35
+
36
+ block.call(self[i - 1], self[i])
37
+ end.compact
38
+ end
39
+
40
+ alias_method :one_by_one, :pairwise
41
+
42
+ # same as pairwise, but with arity of three
43
+ def triplewise(&block)
44
+ raise ArgumentError, 'Array.triplewise needs an arity of 3 (i.e. |a, b, c|)' unless block.arity == 3
45
+ return [] if size <= 2
46
+
47
+ each_with_index.map do |_, i|
48
+ next if i < 2
49
+
50
+ block.call(self[i - 2], self[i-1], self[i])
51
+ end.compact
52
+ end
53
+ end
@@ -0,0 +1,11 @@
1
+ class Enumerator
2
+ def shy_peek
3
+ begin
4
+ ret = self.peek
5
+ rescue
6
+ ret = nil
7
+ end
8
+ ret
9
+ end
10
+ end
11
+
@@ -0,0 +1,15 @@
1
+ class Hash
2
+
3
+ def keys_to_sym
4
+ self.keys.each do |key|
5
+ case self[key].class.to_s
6
+ when "Hash"
7
+ self[key].keys_to_sym
8
+ when "Array"
9
+ self[key].map {|el| el.is_a?(Hash) ? el.keys_to_sym : el}
10
+ end
11
+ self[key.to_sym] = self.delete(key)
12
+ end
13
+ self
14
+ end
15
+ end
@@ -0,0 +1,44 @@
1
+ module Cotcube
2
+ module Helpers
3
+ def keystroke(quit: false)
4
+ begin
5
+ # save previous state of stty
6
+ old_state = `stty -g`
7
+ # disable echoing and enable raw (not having to press enter)
8
+ system "stty raw -echo"
9
+ c = STDIN.getc.chr
10
+ # gather next two characters of special keys
11
+ if(c=="\e")
12
+ extra_thread = Thread.new{
13
+ c = c + STDIN.getc.chr
14
+ c = c + STDIN.getc.chr
15
+ }
16
+ # wait just long enough for special keys to get swallowed
17
+ extra_thread.join(0.00001)
18
+ # kill thread so not-so-long special keys don't wait on getc
19
+ extra_thread.kill
20
+ end
21
+ rescue => ex
22
+ puts "#{ex.class}: #{ex.message}"
23
+ puts ex.backtrace
24
+ ensure
25
+ # restore previous state of stty
26
+ system "stty #{old_state}"
27
+ end
28
+ c.each_byte do |x|
29
+ case x
30
+ when 3
31
+ puts "Strg-C captured, exiting..."
32
+ quit ? exit : (return true)
33
+ when 13
34
+ return "_return_"
35
+ when 27
36
+ puts "ESCAPE gathered"
37
+ return "_esc_"
38
+ else
39
+ return c
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,33 @@
1
+ module Cotcube
2
+ module Helpers
3
+
4
+ def parallelize(ary, opts = {}, &block)
5
+ processes = opts[:processes].nil? ? 1 : opts[:processes]
6
+ threads_per_process = opts[:threads ].nil? ? 1 : opts[:threads]
7
+ progress = opts[:progress ].nil? ? "" : opts[:progress]
8
+ chunks = []
9
+ if processes == 0 or processes == 1
10
+ r = Parallel.map(ary, in_threads: threads_per_process) {|u| v = yield(u); v}
11
+ elsif [0,1].include?(threads_per_process)
12
+ r = Parallel.map(ary, in_processes: processes) {|u| v = yield(u)}
13
+ else
14
+ ary.each_slice(threads_per_process) {|chunk| chunks << chunk }
15
+ if progress == ""
16
+ r = Parallel.map(chunks, :in_processes => processes) do |chunk|
17
+ Parallel.map(chunk, in_threads: threads_per_process) do |unit|
18
+ yield(unit)
19
+ end
20
+ end
21
+ else
22
+ r = Parallel.map(ary, :progress => progress, :in_processes => processes) do |chunk|
23
+ Parallel.map(chunk, in_threads: threads_per_process) do |unit|
24
+ yield(unit)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ return r
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,52 @@
1
+ class Range
2
+ def to_time_intervals(timezone: Time.find_zone('America/Chicago'), step:, ranges: nil)
3
+
4
+ raise ArgumentError, ":step must be a 'ActiveSupport::Duration', like '15.minutes', but '#{step}' is a '#{step.class}'" unless step.is_a? ActiveSupport::Duration
5
+
6
+ valid_classes = [ ActiveSupport::TimeWithZone, Time, Date, DateTime ]
7
+ raise "Expecting 'ActiveSupport::TimeZone' for :timezone, got '#{timezone.class}" unless timezone.is_a? ActiveSupport::TimeZone
8
+ starting = self.begin
9
+ ending = self.end
10
+ starting = timezone.parse(starting) if starting.is_a? String
11
+ ending = timezone.parse(ending) if ending.is_a? String
12
+ raise ArgumentError, ":self.begin seems not to be proper time value: #{starting} is a #{starting.class}" unless valid_classes.include? starting.class
13
+ raise ArgumentError, ":self.end seems not to be proper time value: #{ending} is a #{ending.class}" unless valid_classes.include? ending.class
14
+
15
+ ##### The following is the actual big magic line:
16
+ #
17
+ result = (starting.to_time.to_i..ending.to_time.to_i).step(step).to_a.map{|x| timezone.at(x)}
18
+ #
19
+ ####################<3
20
+
21
+
22
+ # with step.to_i >= 86400 we are risking stuff like 25.hours to return bogus
23
+ # also notice: When using this with swaps, you will loose 1 hour (#f**k_it)
24
+ #
25
+ # eventually, for dailies and above, return M-F default, return S-S when forced by empty ranges
26
+ return result.select{|x| (not ranges.nil? and ranges.empty?) ? true : (not [6,0].include?(x.wday)) } if step.to_i >= 86400
27
+
28
+ # sub-day is checked for DST and filtered along provided ranges
29
+ starting_with_dst = result.first.dst?
30
+ seconds_since_sunday_morning = lambda {|x| x.wday * 86400 + x.hour * 3600 + x.min * 60 + x.sec}
31
+ ranges ||= [
32
+ 61200..143999,
33
+ 147600..230399,
34
+ 234000..316799,
35
+ 320400..403199,
36
+ 406800..489599
37
+ ]
38
+
39
+ # if there was a change towards daylight saving time, substract 1 hour, otherwise add 1 hour
40
+ result.map! do |time|
41
+ if not starting_with_dst and time.dst?
42
+ time - 3600
43
+ elsif starting_with_dst and not time.dst?
44
+ time + 3600
45
+ else
46
+ time
47
+ end
48
+ end
49
+ return result if ranges.empty?
50
+ result.select{|x| ranges.map{|r| r.include? seconds_since_sunday_morning.call(x)}.reduce(:|) }
51
+ end
52
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cotcube
4
+ module Helpers
5
+ # SimpleOutput is a very basic outputhandler, which is actually only there to mock output handling until
6
+ # a more sophisticated solution is available (e.g. OutPutHandler gem is reworked and tested )
7
+ class SimpleOutput
8
+ # Aliasing puts and print, as they are included / inherited (?) from IO
9
+ alias_method :superputs, :puts # rubocop:disable Style/Alias
10
+ # Aliasing puts and print, as they are included / inherited (?) from IO
11
+ alias_method :superprint, :print # rubocop:disable Style/Alias
12
+
13
+ # ...
14
+ def puts(msg)
15
+ superputs msg
16
+ end
17
+
18
+ # ...
19
+ def print(msg)
20
+ superprint msg
21
+ end
22
+
23
+ # The source expects methods with exclamation mark (for unbuffered output) -- although it makes no sense
24
+ # here, we need to provide the syntax for later.
25
+ alias puts! puts
26
+ alias print! print
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,9 @@
1
+ class String
2
+ def is_valid_json?
3
+ JSON.parse(self)
4
+ return true
5
+ rescue JSON::ParserError => e
6
+ return false
7
+ end
8
+ end
9
+
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cotcube
4
+ module Helpers
5
+ # sub (should be 'subpattern', but too long) is for use in case / when statements
6
+ # it returns a lambda, that checks the case'd expression for matching subpattern
7
+ # based on the the giving minimum. E.g. 'a', 'ab' .. 'abcd' will match sub(1){'abcd'}
8
+ # but only 'abc' and 'abcd' will match sub(3){'abcd'}
9
+ #
10
+ # The recommended use within evaluating user input, where abbreviation of incoming commands
11
+ # is desirable (h for hoover and hyper, what will translate to sub(2){'hoover'} and sub(2){hyper})
12
+ #
13
+ # To extend functionality even more, it is possible to send a group of patterns to, like
14
+ # sub(2){[:hyper,:mega]}, what will respond truthy to "hy" and "meg" but not to "m" or "hypo"
15
+ def sub(minimum = 1)
16
+ pattern = yield
17
+ case pattern
18
+ when String, Symbol, NilClass
19
+ pattern = pattern.to_s
20
+ lambda do |x|
21
+ return false if x.nil? || (x.size < minimum)
22
+
23
+ return ((pattern =~ /^#{x}/i).nil? ? false : true)
24
+ end
25
+ when Array
26
+ pattern.map do |x|
27
+ unless [String, Symbol, NilClass].include? x.class
28
+ raise TypeError, "Unsupported class '#{x.class}' for '#{x}'in pattern '#{pattern}'."
29
+ end
30
+ end
31
+ lambda do |x|
32
+ pattern.each do |sub|
33
+ sub = sub.to_s
34
+ return false if x.size < minimum
35
+
36
+ result = ((sub =~ /^#{x}/i).nil? ? false : true)
37
+ return true if result
38
+ end
39
+ return false
40
+ end
41
+ else
42
+ raise TypeError, "Unsupported class #{pattern.class} in Cotcube::Core::sub"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,66 @@
1
+ class Time
2
+ def to_centi
3
+ (self.to_f * 1000.0).to_i
4
+ end
5
+
6
+ def to_part(x=100)
7
+ (self.to_f * x.to_f).to_i % x
8
+ end
9
+ end
10
+
11
+
12
+ class Date
13
+ def abbr_dayname
14
+ ABBR_DAYNAMES[self.wday]
15
+ end
16
+
17
+ def to_string
18
+ self.strftime("%Y-%m-%d")
19
+ end
20
+
21
+ def to_timestring
22
+ self.strftime("%Y-%m-%d-%H-%M-%S")
23
+ end
24
+
25
+ end
26
+
27
+ class String
28
+ def to_date
29
+ Date::strptime(self, "%Y-%m-%d")
30
+ end
31
+
32
+ def to_time
33
+ DateTime::strptime(self, "%s")
34
+ end
35
+ end
36
+
37
+ class Integer
38
+ def to_tz
39
+ return "+0000" if self == 0
40
+ sign = self > 0 ? "+" : "-"
41
+ value = self.abs
42
+ hours = (value / 3600.to_f).floor
43
+ minutes = ((value - 3600 * hours) / 60).floor
44
+ "#{sign}#{hours>9 ? "" : "0"}#{hours}#{minutes<10 ? "0" : "" }#{minutes}"
45
+ end
46
+
47
+ def to_tod(zone="UTC")
48
+ self.to_time
49
+ #in_time_zone(zone).
50
+ #to_time_of_day
51
+ end
52
+
53
+ def to_tod_i
54
+ (self / 100).to_time.strftime("%H%M%S").to_i
55
+ end
56
+
57
+ def to_date
58
+ Date::strptime(self.to_s, "%s")
59
+ end
60
+
61
+ def to_time
62
+ DateTime.strptime(self.to_s, "%s")
63
+ end
64
+
65
+ end
66
+
@@ -0,0 +1,57 @@
1
+ module Tritangent
2
+ module Helpers
3
+ include ::Tod
4
+ #Notice the first use of keyword arguments here ;)
5
+ def fill_x (base:, discretion:, time:, attr:, maintenance: nil, zone: nil)
6
+ zone ||= case base[0][time].zone
7
+ when 'CDT','CT','CST'
8
+ "America/Chicago"
9
+ when 'EDT', 'ET', 'EST'
10
+ "AMerica/New_York"
11
+ else
12
+ "UTC"
13
+ end
14
+
15
+ maintenance ||= case zone
16
+ when "America/Chicago"
17
+ { 0 => Shift.new(Tod::TimeOfDay.new( 0), Tod::TimeOfDay.new(17), true),
18
+ 1 => Tod::Shift.new(Tod::TimeOfDay.new(16), Tod::TimeOfDay.new(17), true),
19
+ 2 => Tod::Shift.new(Tod::TimeOfDay.new(16), Tod::TimeOfDay.new(17), true),
20
+ 3 => Tod::Shift.new(Tod::TimeOfDay.new(16), Tod::TimeOfDay.new(17), true),
21
+ 4 => Tod::Shift.new(Tod::TimeOfDay.new(16), Tod::TimeOfDay.new(17), true),
22
+ 5 => Tod::Shift.new(Tod::TimeOfDay.new(16), Tod::TimeOfDay.new( 0), true)
23
+ }
24
+ when "America/New_York"
25
+ { 0 => Tod::Shift.new(Tod::TimeOfDay.new( 0), Tod::TimeOfDay.new(17), true),
26
+ 1 => Tod::Shift.new(Tod::TimeOfDay.new(16), Tod::TimeOfDay.new(17), true),
27
+ 2 => Tod::Shift.new(Tod::TimeOfDay.new(16), Tod::TimeOfDay.new(17), true),
28
+ 3 => Tod::Shift.new(Tod::TimeOfDay.new(16), Tod::TimeOfDay.new(17), true),
29
+ 4 => Tod::Shift.new(Tod::TimeOfDay.new(16), Tod::TimeOfDay.new(17), true),
30
+ 5 => Tod::Shift.new(Tod::TimeOfDay.new(16), Tod::TimeOfDay.new( 0), true)
31
+ }
32
+ else
33
+ raise "Tritangent::Helpers.fill_x needs :maintenance or :zone"
34
+ end
35
+
36
+ tz = Time.find_zone zone
37
+ exchange_open = lambda do |t0|
38
+ current_week_day = t0.wday
39
+ return false if current_week_day == 6
40
+ not maintenance[current_week_day].include?(Tod::TimeOfDay(t0.in_time_zone(zone)))
41
+ end
42
+ result = [] # [ { time => base.first[time], attr => base.first[attr] } ]
43
+ i = 0
44
+ timer = base.first[time]
45
+ while not base[i].nil?
46
+ if timer == base[i][time]
47
+ result << { time => timer, attr => base[i][attr] }
48
+ i += 1
49
+ else
50
+ result << { time => timer } if exchange_open.call(timer)
51
+ end
52
+ timer += discretion
53
+ end
54
+ result
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,330 @@
1
+ module Candlestick_Recognition
2
+
3
+ extend self
4
+
5
+ def candles(candles, debug = false)
6
+ candles.each_with_index do |bar, i|
7
+ # rel simply sets a grace limit based on the full height of the bar, so we won't need to use the hard limit of zero
8
+ begin
9
+ rel = (bar[:high] - bar[:low]) * 0.1
10
+ rescue
11
+ puts "Warning, found inappropriate bar".light_white + " #{bar}"
12
+ raise
13
+ end
14
+ bar[:rel] = rel
15
+
16
+ bar[:upper] = [bar[:open], bar[:close]].max
17
+ bar[:lower] = [bar[:open], bar[:close]].min
18
+ bar[:bar_size] = (bar[:high] - bar[:low])
19
+ bar[:body_size] = (bar[:open] - bar[:close]).abs
20
+ bar[:lower_wick] = (bar[:lower] - bar[:low])
21
+ bar[:upper_wick] = (bar[:high] - bar[:upper])
22
+ bar.each{|k,v| bar[k] = v.round(8) if v.is_a? Float}
23
+
24
+ # a doji's open and close are same (or only differ by rel)
25
+ bar[:doji] = true if bar[:body_size] <= rel and bar[:dist] >= 3
26
+ bar[:tiny] = true if bar[:dist] <= 5
27
+
28
+ next if bar[:tiny]
29
+
30
+ bar[:bullish] = true if not bar[:doji] and bar[:close] > bar[:open]
31
+ bar[:bearish] = true if not bar[:doji] and bar[:close] < bar[:open]
32
+
33
+ bar[:spinning_top] = true if bar[:body_size] <= bar[:bar_size] / 4 and
34
+ bar[:lower_wick] >= bar[:bar_size] / 4 and
35
+ bar[:upper_wick] >= bar[:bar_size] / 4
36
+
37
+ # a marubozu open at high or low and closes at low or high
38
+ bar[:marubozu] = true if bar[:upper_wick] < rel and bar[:lower_wick] < rel
39
+
40
+ # a bar is considered bearish if it has at least a dist of 5 ticks and it's close it near high (low resp)
41
+ bar[:bullish_close] = true if (bar[:high] - bar[:close]) <= rel and not bar[:marubozu]
42
+ bar[:bearish_close] = true if (bar[:close] - bar[:low]) <= rel and not bar[:marubozu]
43
+
44
+ # the distribution of main volume is shown in 5 segments, like [0|0|0|4|5] shows that most volume concentrated at the bottom, [0|0|3|0|0] is heavily centered
45
+ # TODO
46
+
47
+ end
48
+ candles
49
+ end
50
+
51
+ def comparebars(prev, curr)
52
+ bullishscore = 0
53
+ bearishscore = 0
54
+ bullishscore += 1 if prev[:high] <= curr[:high]
55
+ bullishscore += 1 if prev[:low] <= curr[:low]
56
+ bullishscore += 1 if prev[:close] <= curr[:close]
57
+ bearishscore += 1 if prev[:close] >= curr[:close]
58
+ bearishscore += 1 if prev[:low] >= curr[:low]
59
+ bearishscore += 1 if prev[:high] >= curr[:high]
60
+ r = {}
61
+ r[:bullish] = true if bullishscore >= 2
62
+ r[:bearish] = true if bearishscore >= 2
63
+ return r
64
+ end
65
+
66
+
67
+ def patterns(candles, debug = false)
68
+ candles.each_with_index do |bar, i|
69
+ if i.zero?
70
+ bar[:slope] = 0
71
+ next
72
+ end
73
+ pprev= candles[i-2]
74
+ prev = candles[i-1]
75
+ succ = candles[i+1]
76
+
77
+ bar[:tr] = [ bar[:high], prev[:close] ].max - [bar[:low], prev[:close]].min
78
+ bar[:ranges] = prev[:ranges].nil? ? [ bar[:tr] ] : prev[:ranges] + [ bar[:tr] ]
79
+ bar[:ranges].shift while bar[:ranges].size > 5
80
+ bar[:atr] = (bar[:ranges].reduce(:+) / bar[:ranges].size.to_f).round(8) if bar[:ranges].size > 0
81
+
82
+ bar[:vranges] = prev[:vranges].nil? ? [ bar[:volume] ] : prev[:vranges] + [ bar[:volume] ]
83
+ bar[:vranges].shift while bar[:vranges].size > 5
84
+ bar[:vavg] = (bar[:vranges].reduce(:+) / bar[:vranges].size.to_f).round if bar[:vranges].compact.size > 0
85
+
86
+
87
+
88
+ # VOLUME
89
+ if bar[:volume] > bar[:vavg] * 1.3
90
+ bar[:BREAKN_volume] = true
91
+ elsif bar[:volume] >= bar[:vavg] * 1.1
92
+ bar[:RISING_volume] = true
93
+ elsif bar[:volume] < bar[:vavg] * 0.7
94
+ bar[:FAINTN_volume] = true
95
+ elsif bar[:volume] <= bar[:vavg] * 0.9
96
+ bar[:FALLIN_volume] = true
97
+ else
98
+ bar[:STABLE_volume] = true
99
+ end
100
+
101
+ # GAPS
102
+ bar[:bodygap] = true if bar[:lower] > prev[:upper] or bar[:upper] < prev[:lower]
103
+ bar[:gap] = true if bar[:low] > prev[:high] or bar[:high] < prev[:low]
104
+
105
+
106
+ bar[:slope] = slopescore(pprev, prev, bar, debug)
107
+
108
+ # UPPER_PIVOTs define by having higher highs and higher lows than their neighor
109
+ bar[:UPPER_ISOPIVOT] = true if succ and prev[:high] < bar[:high] and prev[:low] <= bar[:low] and succ[:high] < bar[:high] and succ[:low] <= bar[:low] and bar[:lower] >= [prev[:upper], succ[:upper]].max
110
+ bar[:LOWER_ISOPIVOT] = true if succ and prev[:high] >= bar[:high] and prev[:low] > bar[:low] and succ[:high] >= bar[:high] and succ[:low] > bar[:low] and bar[:upper] <= [prev[:lower], succ[:lower]].min
111
+ bar[:UPPER_PIVOT] = true if succ and prev[:high] < bar[:high] and prev[:low] <= bar[:low] and succ[:high] < bar[:high] and succ[:low] <= bar[:low] and not bar[:UPPER_ISOPIVOT]
112
+ bar[:LOWER_PIVOT] = true if succ and prev[:high] >= bar[:high] and prev[:low] > bar[:low] and succ[:high] >= bar[:high] and succ[:low] > bar[:low] and not bar[:LOWER_ISOPIVOT]
113
+
114
+ # stopping volume is defined as high volume candle during downtrend then closes above mid candle (i.e. lower_wick > body_size)
115
+ bar[:stopping_volume] = true if bar[:BREAKN_volume] and prev[:slope] < -5 and bar[:lower_wick] >= bar[:body_size]
116
+ bar[:stopping_volume] = true if bar[:BREAKN_volume] and prev[:slope] > 5 and bar[:upper_wick] >= bar[:body_size]
117
+ bar[:volume_lower_wick] = true if bar[:vhigh] and (bar[:vol_i].nil? or bar[:vol_i] >= 2) and bar[:vhigh] <= bar[:lower] and not bar[:FAINTN_volume] and not bar[:FALLIN_volume]
118
+ bar[:volume_upper_wick] = true if bar[:vlow] and (bar[:vol_i].nil? or bar[:vol_i] >= 2) and bar[:vlow] >= bar[:upper] and not bar[:FAINTN_volume] and not bar[:FALLIN_volume]
119
+
120
+
121
+ ###################################
122
+ # SINGLE CANDLE PATTERNS
123
+ ###################################
124
+
125
+ # a hammer is a bar, whose open or close is at the high and whose body is lte 1/3 of the size, found on falling slope, preferrably gapping away
126
+ bar[:HAMMER] = true if bar[:upper_wick] <= bar[:rel] and bar[:body_size] <= bar[:bar_size] / 3 and bar[ :slope] <= -6
127
+ # same shape, but found at a raising slope without the need to gap away
128
+ bar[:HANGING_MAN] = true if bar[:upper_wick] <= bar[:rel] and bar[:body_size] <= bar[:bar_size] / 3 and prev[:slope] >= 6
129
+
130
+ # a shooting star is the inverse of the hammer, while the inverted hammer is the inverse of the hanging man
131
+ bar[:SHOOTING_STAR] = true if bar[:lower_wick] <= bar[:rel] and bar[:body_size] <= bar[:bar_size] / 2.5 and bar[ :slope] >= 6
132
+ bar[:INVERTED_HAMMER] = true if bar[:lower_wick] <= bar[:rel] and bar[:body_size] <= bar[:bar_size] / 3 and prev[:slope] <= -6
133
+
134
+ # a star is simply gapping away the preceding slope
135
+ bar[:STAR] = true if ((bar[:lower] >= prev[:upper] and bar[:slope] >= 6) or (bar[:upper] <= prev[:lower] and bar[:slope] <= -6)) and not bar[:doji]
136
+ bar[:DOJI_STAR] = true if ((bar[:lower] >= prev[:upper] and bar[:slope] >= 6) or (bar[:upper] <= prev[:lower] and bar[:slope] <= -6)) and bar[:doji]
137
+
138
+ # a belthold is has a gap in the open, but reverses strong
139
+ bar[:BULLISH_BELTHOLD] = true if bar[:lower_wick] <= bar[:rel] and bar[:body_size] >= bar[:bar_size] / 2 and
140
+ prev[:slope] <= -4 and bar[:lower] <= prev[:low ] and bar[:bullish] and not prev[:bullish] and bar[:bar_size] >= prev[:bar_size]
141
+ bar[:BEARISH_BELTHOLD] = true if bar[:upper_wick] <= bar[:rel] and bar[:body_size] >= bar[:bar_size] / 2 and
142
+ prev[:slope] >= -4 and bar[:upper] <= prev[:high] and bar[:bearish] and not prev[:bearish] and bar[:bar_size] >= prev[:bar_size]
143
+
144
+
145
+ ###################################
146
+ # DUAL CANDLE PATTERNS
147
+ ###################################
148
+
149
+
150
+ # ENGULFINGS
151
+ bar[:BULLISH_ENGULFING] = true if bar[:bullish] and prev[:bearish] and bar[:lower] <= prev[:lower] and bar[:upper] > prev[:upper] and prev[:slope] <= -6
152
+ bar[:BEARISH_ENGULFING] = true if bar[:bearish] and prev[:bullish] and bar[:lower] < prev[:lower] and bar[:upper] >= prev[:upper] and prev[:slope] >= 6
153
+
154
+
155
+ # DARK-CLOUD-COVER / PIERCING-LINE (on-neck / in-neck / thrusting / piercing / PDF pg 63)
156
+ bar[:DARK_CLOUD_COVER] = true if bar[:slope] > 5 and prev[:bullish] and bar[:open] > prev[:high] and bar[:close] < prev[:upper] - prev[:body_size] * 0.5 and
157
+ not bar[:BEARISH_ENGULFING]
158
+ bar[:PIERCING_LINE] = true if bar[:slope] < -5 and prev[:bearish] and bar[:open] < prev[:low ] and bar[:close] > prev[:lower] + prev[:body_size] * 0.5 and
159
+ not bar[:BULLISH_ENGULFING]
160
+ bar[:SMALL_CLOUD_COVER] = true if bar[:slope] > 5 and prev[:bullish] and bar[:open] > prev[:high] and bar[:close] < prev[:upper] - prev[:body_size] * 0.25 and
161
+ not bar[:BEARISH_ENGULFING] and not bar[:DARK_CLOUD_COVER]
162
+ bar[:THRUSTING_LINE] = true if bar[:slope] < -5 and prev[:bearish] and bar[:open] < prev[:low ] and bar[:close] > prev[:lower] + prev[:body_size] * 0.25 and
163
+ not bar[:BULLISH_ENGULFING] and not bar[:PIERCING_LINE]
164
+
165
+
166
+ # COUNTER ATTACKS are like piercings / cloud covers, but insist on a large reverse while only reaching the preceding close
167
+ bar[:BULLISH_COUNTERATTACK] = true if bar[:slope] < 6 and prev[:bearish] and bar[:bar_size] > bar[:atr] * 0.66 and (bar[:close] - prev[:close]).abs < 2 * bar[:rel] and
168
+ bar[:body_size] >= bar[:bar_size] * 0.5 and bar[:bullish]
169
+ bar[:BEARISH_COUNTERATTACK] = true if bar[:slope] > 6 and prev[:bullish] and bar[:bar_size] > bar[:atr] * 0.66 and (bar[:close] - prev[:close]).abs < 2 * bar[:rel] and
170
+ bar[:body_size] >= bar[:bar_size] * 0.5 and bar[:bearish]
171
+
172
+
173
+ # HARAMIs are an unusual long body embedding the following small body
174
+ bar[:HARAMI] = true if bar[:body_size] < prev[:body_size] / 2.5 and prev[:bar_size] >= bar[:atr] and
175
+ prev[:upper] > bar[:upper] and prev[:lower] < bar[:lower] and not bar[:doji]
176
+ bar[:HARAMI_CROSS] = true if bar[:body_size] < prev[:body_size] / 2.5 and prev[:bar_size] >= bar[:atr] and
177
+ prev[:upper] > bar[:upper] and prev[:lower] < bar[:lower] and bar[:doji]
178
+ if bar[:HARAMI] or bar[:HARAMI_CROSS]
179
+ puts [ :date, :open, :high, :low, :close, :upper, :lower ].map{|x| prev[x]}.join("\t") if debug
180
+ puts [ :date, :open, :high, :low, :close, :upper, :lower ].map{|x| bar[ x]}.join("\t") if debug
181
+ puts "" if debug
182
+ end
183
+
184
+ # TODO TWEEZER_TOP and TWEEZER_BOTTOM
185
+ # actually being a double top / bottom, this dual candle pattern has to be unfolded. It is valid on daily or weekly charts,
186
+ # and valid if
187
+ # 1 it has an according
188
+
189
+
190
+ ###################################
191
+ # TRIPLE CANDLE PATTERNS
192
+ ###################################
193
+
194
+ # morning star, morning doji star
195
+ next unless prev and pprev
196
+ bar[:MORNING_STAR] = true if prev[:STAR] and bar[:bullish] and bar[:close] >= pprev[:lower] and prev[:slope] < -6
197
+ bar[:MORNING_DOJI_STAR] = true if prev[:DOJI_STAR] and bar[:bullish] and bar[:close] >= pprev[:lower] and prev[:slope] < -6
198
+ bar[:EVENING_STAR] = true if prev[:STAR] and bar[:bearish] and bar[:close] <= pprev[:upper] and prev[:slope] > 6
199
+ bar[:EVENING_DOJI_STAR] = true if prev[:DOJI_STAR] and bar[:bearish] and bar[:close] <= pprev[:upper] and prev[:slope] > 6
200
+
201
+ # the abandoned baby escalates above stars by gapping the inner star candle to both framing it
202
+ bar[:ABANDONED_BABY] = true if (bar[:MORNING_STAR] or bar[:MORNING_DOJI_STAR]) and prev[:high] <= [ pprev[:low ], bar[:low ] ].min
203
+ bar[:ABANDONED_BABY] = true if (bar[:EVENING_STAR] or bar[:EVENING_DOJI_STAR]) and prev[:low ] >= [ pprev[:high], bar[:high] ].max
204
+
205
+ # UPSIDEGAP_TWO_CROWS
206
+ bar[:UPSIDEGAP_TWO_CROWS] = true if (prev[:STAR] or prev[:DOJI_STAR]) and prev[:slope] > 4 and bar[:bearish] and prev[:bearish] and bar[:close] > pprev[:close]
207
+ bar[:DOWNGAP_TWO_RIVERS] = true if (prev[:STAR] or prev[:DOJI_STAR]) and prev[:slope] < 4 and bar[:bullish] and prev[:bullish] and bar[:close] < pprev[:close]
208
+
209
+ # THREE BLACK CROWS / THREE WHITE SOLDIERS
210
+ bar[:THREE_BLACK_CROWS] = true if [ bar, prev, pprev ].map{|x| x[:bearish] and x[:bar_size] > 0.5 * bar[:atr] }.reduce(:&) and
211
+ pprev[:close] - prev[ :close] > bar[:atr] * 0.2 and
212
+ prev[ :close] - bar[ :close] > bar[:atr] * 0.2
213
+ bar[:THREE_WHITE_SOLDIERS] = true if [ bar, prev, pprev ].map{|x| x[:bullish] and x[:bar_size] > 0.5 * bar[:atr] }.reduce(:&) and
214
+ prev[:close] - pprev[:close] > bar[:atr] * 0.2 and
215
+ bar[ :close] - prev[ :close] > bar[:atr] * 0.2
216
+ end
217
+ end
218
+
219
+
220
+ # SLOPE SCORE
221
+ def slopescore(pprev, prev, bar, debug = false)
222
+ # the slope between to bars is considered bullish, if 2 of three points match
223
+ # - higher high
224
+ # - higher close
225
+ # - higher low
226
+ # the opposite counts for bearish
227
+ #
228
+ # this comparison is done between the current bar and previous bar
229
+ # - if it confirms the score of the previous bar, the new slope score is prev + curr
230
+ # - otherwise the is compared to score of the pprevious bar
231
+ # - if it confirms there, the new slope score is pprev + curr
232
+ # - otherwise the trend is destroyed and tne new score is solely curr
233
+
234
+ if bar[:bullish]
235
+ curr = 1
236
+ curr += 1 if bar[:bullish_close]
237
+ elsif bar[:bearish]
238
+ curr = -1
239
+ curr -= 1 if bar[:bearish_close]
240
+ else
241
+ curr = 0
242
+ end
243
+ puts "curr set to #{curr} @ #{bar[:date]}".yellow if debug
244
+ if prev.nil?
245
+ puts "no prev found, score == curr: #{curr}" if debug
246
+ score = curr
247
+ else
248
+ comp = comparebars(prev, bar)
249
+
250
+ puts prev.select{|k,v| [:high,:low,:close,:score].include?(k)} if debug
251
+ puts bar if debug
252
+ puts "COMPARISON 1: #{comp}" if debug
253
+
254
+ if prev[:slope] >= 0 and comp[:bullish] # bullish slope confirmed
255
+ score = prev[:slope]
256
+ score += curr if curr > 0
257
+ [ :gap, :bodygap ] .each {|x| score += 0.5 if bar[x] }
258
+ score += 1 if bar[:RISING_volume]
259
+ score += 2 if bar[:BREAKN_volume]
260
+ puts "found bullish slope confirmed, new score #{score}" if debug
261
+ elsif prev[:slope] <= 0 and comp[:bearish] # bearish slope confirmed
262
+ score = prev[:slope]
263
+ score += curr if curr < 0
264
+ [ :gap, :bodygap ] .each {|x| score -= 0.5 if bar[x] }
265
+ score -= 1 if bar[:RISING_volume]
266
+ score -= 2 if bar[:BREAKN_volume]
267
+ puts "found bearish slope confirmed, new score #{score} (including #{curr} and #{bar[:bodygap]} and #{bar[:gap]}" if debug
268
+ else #if prev[:slope] > 0 # slopes failed
269
+ puts "confirmation failed: " if debug
270
+ if pprev.nil?
271
+ score = curr
272
+ else
273
+ comp2 = comparebars(pprev, bar)
274
+ puts "\t\tCOMPARISON 2: #{comp2}" if debug
275
+ if pprev[:slope] >= 0 and comp2[:bullish] # bullish slope confirmed on pprev
276
+ score = pprev[:slope]
277
+ score += curr if curr > 0
278
+ [ :gap, :bodygap ] .each {|x| score += 0.5 if bar[x] }
279
+ puts "\t\tfound bulliish slope confirmed, new score #{score}" if debug
280
+ score += 1 if bar[:RISING_volume]
281
+ score += 2 if bar[:BREAKN_volume]
282
+ elsif pprev[:slope] <= 0 and comp2[:bearish] # bearish slope confirmed
283
+ score = pprev[:slope]
284
+ score += curr if curr < 0
285
+ [ :gap, :bodygap ] .each {|x| score -= 0.5 if bar[x] }
286
+ score -= 1 if bar[:RISING_volume]
287
+ score -= 2 if bar[:BREAKN_volume]
288
+ puts "\t\tfound bearish slope confirmed, new score #{score}" if debug
289
+ else #slope confirmation finally failed
290
+ comp3 = comparebars(pprev, prev)
291
+ if prev[:slope] > 0 # was bullish, turning bearish now
292
+ score = curr
293
+ score -= 1 if comp3[:bearish]
294
+ score -= 1 if comp[:bearish]
295
+ score -= 1 if prev[:bearish]
296
+ score -= 1 if prev[:RISING_volume] and comp3[:bearish]
297
+ score -= 2 if prev[:BREAKN_volume] and comp3[:bearish]
298
+ score -= 1 if bar[:RISING_volume] and comp[:bearish]
299
+ score -= 2 if bar[:BREAKN_volume] and comp[:bearish]
300
+ score -= 1 if bar[:RISING_volume] and comp[:bearish]
301
+ score -= 2 if bar[:BREAKN_volume] and comp[:bearish]
302
+ [ :gap, :bodygap ] .each {|x| score += 0.5 if bar[x] }
303
+ puts "\t\tfinally gave up, turning bearish now, new score #{score}" if debug
304
+ elsif prev[:slope] < 0
305
+ score = curr
306
+ score += 1 if comp3[:bullish]
307
+ score += 1 if comp[:bullish]
308
+ score += 1 if prev[:bullish]
309
+ score += 1 if prev[:RISING_volume] and comp3[:bullish]
310
+ score += 2 if prev[:BREAKN_volume] and comp3[:bullish]
311
+ score += 1 if bar[:RISING_volume] and comp[:bullish]
312
+ score += 2 if bar[:BREAKN_volume] and comp[:bullish]
313
+ score += 1 if bar[:RISING_volume] and comp[:bullish]
314
+ score += 2 if bar[:BREAKN_volume] and comp[:bullish]
315
+ [ :gap, :bodygap ] .each {|x| score -= 0.5 if bar[x] } if curr < 0
316
+ puts "\t\tfinally gave up, turning bullish now, new score #{score}" if debug
317
+ else
318
+ score = 0
319
+ end
320
+ end
321
+ end
322
+ end
323
+ end
324
+ puts "" if debug
325
+ score
326
+ end
327
+
328
+ end
329
+
330
+ CR = Candlestick_Recognition
metadata ADDED
@@ -0,0 +1,120 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cotcube-helpers
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Benjamin L. Tischendorf
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-12-21 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: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.6'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: yard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.9'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.9'
69
+ description: Some helpers and core extensions as part of the Cotcube Suite...
70
+ email:
71
+ - donkeybridge@jtown.eu
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".irbrc.rb"
77
+ - CHANGELOG.md
78
+ - Gemfile
79
+ - VERSION
80
+ - cotcube-helpers.gemspec
81
+ - lib/cotcube-helpers.rb
82
+ - lib/cotcube-helpers/array_ext.rb
83
+ - lib/cotcube-helpers/enum_ext.rb
84
+ - lib/cotcube-helpers/hash_ext.rb
85
+ - lib/cotcube-helpers/input.rb
86
+ - lib/cotcube-helpers/parallelize.rb
87
+ - lib/cotcube-helpers/range_ext.rb
88
+ - lib/cotcube-helpers/simple_output.rb
89
+ - lib/cotcube-helpers/string_ext.rb
90
+ - lib/cotcube-helpers/subpattern.rb
91
+ - lib/cotcube-helpers/swig/date.rb
92
+ - lib/cotcube-helpers/swig/fill_x.rb
93
+ - lib/cotcube-helpers/swig/recognition.rb
94
+ homepage: https://github.com/donkeybridge/cotcube-helpers
95
+ licenses:
96
+ - BSD-4-Clause
97
+ metadata:
98
+ homepage_uri: https://github.com/donkeybridge/cotcube-helpers
99
+ source_code_uri: https://github.com/donkeybridge/cotcube-helpers
100
+ changelog_uri: https://github.com/donkeybridge/cotcube-helpers/CHANGELOG.md
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '2.7'
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubygems_version: 3.1.2
117
+ signing_key:
118
+ specification_version: 4
119
+ summary: Some helpers and core extensions as part of the Cotcube Suite.
120
+ test_files: []