cotcube-helpers 0.1.1

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.
@@ -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: []