cotcube-helpers 0.1.9.1 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb81f4056cbca5f2a1630a3ddc3a63070bcf00030b78cad54163e9fca057aaaa
4
- data.tar.gz: 80cfdd92008406204cabb9add25987d49c1c3ba28b32db5c935e743e1d276470
3
+ metadata.gz: 1228015a21f3920ec5b1d87e3c5918e3c69e0a577746b6a18579ae51e1528ce2
4
+ data.tar.gz: 431798647c5d4e9d5a7a32816636bd768c4e0e9c70d789dd491c374fde75f768
5
5
  SHA512:
6
- metadata.gz: bedbd01706f4f4896acf7dc7258371c31ace12f81b8c65c6f0c057129b824d1b3087f806fe48bd8a38d0a2894c1028ec5113fa6a1f9abbd2036ea156a86ca266
7
- data.tar.gz: c17bbb49e51ced51f8ef8edefa6a781dc54598fb8ed7912df41434b6702096567f495f0cd0c873efbe020e36132e9402ac9f336e4607b151d1a34ae4600a2c3f
6
+ metadata.gz: b931c2dad6f54a1f2de64590e703c1be03ed2330691a299c2e4d4a64f5cb5d20d3be9ba44584866db66116160c2d01c87cd17f7d84f5ae82ae2668f5a8cf0bae
7
+ data.tar.gz: 18b50ba7237d373dec19d3144814ad55c5ec3d36274213c5749041bbf66ce008461d2be89cda0240245529e29ed383110352d945af4a82cb761d53a6d702f09a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,33 @@
1
+ ## 0.2.1.1 (November 10, 2021)
2
+ - Bump version to 0.2.1.
3
+ - Bump version to 0.2.1.
4
+
5
+ ## 0.2.1 (November 10, 2021)
6
+ - added new class 'dataclient' for communication with dataproxy
7
+ - added .translate_ib_contract
8
+
9
+ ## 0.2.0 (November 07, 2021)
10
+ - added module Candlestick_Recognition
11
+ - added instance_inspect method to 'scan' objects for contents of instance variables
12
+ - symbols: made selection of symbols more versatile by key
13
+ - added headers (:ib_symbol, :internal, :exchange, :currency) to symbol headers as well as symbol examples
14
+ - added scripts/symbols to list (and filter) symbols from command line (put to PATH!)
15
+
16
+ ## 0.1.10 (October 28, 2021)
17
+ - added script cron_ruby_wrapper.sh (linkable as /usr/local/bin/cruw.sh)
18
+ - added numeric ext .with_delimiter to support printing like 123_456_789.00121
19
+ - added micros to module
20
+ - added Helpers.micros to symbols.rb
21
+ - subpattern: excaping regex pattern to avoid ESC errors
22
+ - minor change
23
+
24
+ ## 0.1.9.2 (July 24, 2021)
25
+ - added missing module_functions
26
+ - init: minor fix
27
+ - datetime_ext: added warning comment / TODO, as switch from to daylight time will produce erroneous results
28
+ - constants: minor fix (typo)
29
+ - array_ext: added param to provide a default return value if result is an empty array
30
+
1
31
  ## 0.1.9.1 (May 07, 2021)
2
32
  - moved 'get_id_set' to Cotcube::Helpers
3
33
  - minor fix to suppress some warning during build
data/Gemfile CHANGED
@@ -4,3 +4,5 @@ source 'https://rubygems.org'
4
4
 
5
5
  # Specify your gem's dependencies in bitangent.gemspec
6
6
  gemspec
7
+ gem 'parallel'
8
+ gem 'bunny'
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.9.1
1
+ 0.2.1.1
@@ -29,10 +29,10 @@ class Array
29
29
  # This method iterates over an Array by calling the given block on all 2 consecutive elements
30
30
  # it returns a Array of self.size - 1
31
31
  #
32
- def pairwise(ret=nil, &block)
32
+ def pairwise(ret=nil, empty: nil, &block)
33
33
  raise ArgumentError, 'Array.one_by_one needs an arity of 2 (i.e. |a, b|)' unless block.arity == 2
34
- raise ArgumentError, 'Each element of Array should respond to []=, at least the last one fails.' unless self.last.respond_to?(:[]=)
35
- return [] if size <= 1
34
+ raise ArgumentError, 'Each element of Array should respond to []=, at least the last one fails.' if not ret.nil? and not self.last.respond_to?(:[]=)
35
+ return empty ||= [] if size <= 1
36
36
 
37
37
  each_index.map do |i|
38
38
  next if i.zero?
@@ -47,7 +47,7 @@ class Array
47
47
  # same as pairwise, but with arity of three
48
48
  def triplewise(ret=nil, &block)
49
49
  raise ArgumentError, 'Array.triplewise needs an arity of 3 (i.e. |a, b, c|)' unless block.arity == 3
50
- raise ArgumentError, 'Each element of Array should respond to []=, at least the last one fails.' unless self.last.respond_to?(:[]=)
50
+ raise ArgumentError, 'Each element of Array should respond to []=, at least the last one fails.' if not ret.nil? and not self.last.respond_to?(:[]=)
51
51
  return [] if size <= 2
52
52
 
53
53
  each_index.map do |i|
@@ -1,11 +1,15 @@
1
-
2
- zen_string_literal: true
1
+ #frozen_string_literal: true
3
2
 
4
3
  module Cotcube
5
- module SwapSeeker
4
+ module Helpers
6
5
  SYMBOL_EXAMPLES = [
7
- { id: '13874U', symbol: 'ET', ticksize: 0.25, power: 1.25, months: 'HMUZ', bcf: 1.0, reports: 'LF', format: '8.2f', name: 'S&P 500 MICRO' },
8
- { id: '209747', symbol: 'NM', ticksize: 0.25, power: 0.5, monhts: 'HMUZ', bcf: 1.0, reports: 'LF', format: '8.2f', name: 'NASDAQ 100 MICRO' }
6
+ { id: '13874U', symbol: 'ES', ib_symbol: 'ES', internal: 'ES', exchange: 'GLOBEX', currency: 'USD', ticksize: 0.25, power: 12.5, months: 'HMUZ', bcf: 1.0, reports: 'LF', format: '8.2f', name: 'S&P 500 MICRO' },
7
+ { id: '209747', symbol: 'NQ', ib_symbol: 'NQ', internal: 'NQ', exchange: 'GLOBEx', currency: 'USD', ticksize: 0.25, power: 5.0, monhts: 'HMUZ', bcf: 1.0, reports: 'LF', format: '8.2f', name: 'NASDAQ 100 MICRO' }
8
+ ].freeze
9
+
10
+ MICRO_EXAMPLES = [
11
+ { id: '13874U', symbol: 'ET', ib_symbol: 'MES', internal: 'MES', exchange: 'GLOBEX', currency: 'USD', ticksize: 0.25, power: 1.25, months: 'HMUZ', bcf: 1.0, reports: 'LF', format: '8.2f', name: 'MICRO S&P 500 MICRO' },
12
+ { id: '209747', symbol: 'NM', ib_symbol: 'MNQ', internal: 'MNQ', exchange: 'GLOBEX', currency: 'USD', ticksize: 0.25, power: 0.5, monhts: 'HMUZ', bcf: 1.0, reports: 'LF', format: '8.2f', name: 'MICRO NASDAQ 100 MICRO' }
9
13
  ].freeze
10
14
 
11
15
  COLORS = %i[light_red light_yellow light_green red yellow green cyan magenta blue].freeze
@@ -19,9 +23,9 @@ module Cotcube
19
23
  'J' => 4, 'K' => 5, 'M' => 6,
20
24
  'N' => 7, 'Q' => 8, 'U' => 9,
21
25
  'V' => 10, 'X' => 11, 'Z' => 12,
22
- 1 => 'F', 2 => 'G', 3 => 'H',
23
- 4 => 'J', 5 => 'K', 6 => 'M',
24
- 7 => 'N', 8 => 'Q', 9 => 'U',
26
+ 1 => 'F', 2 => 'G', 3 => 'H',
27
+ 4 => 'J', 5 => 'K', 6 => 'M',
28
+ 7 => 'N', 8 => 'Q', 9 => 'U',
25
29
  10 => 'V', 11 => 'X', 12 => 'Z' }.freeze
26
30
 
27
31
 
@@ -29,6 +33,12 @@ module Cotcube
29
33
 
30
34
  DATE_FMT = '%Y-%m-%d'
31
35
 
36
+ # Simple mapper to get from MONTH to LETTER
37
+ LETTERS = { "JAN"=> "F", "FEB"=> "G", "MAR"=> "H",
38
+ "APR"=> "J", "MAY"=> "K", "JUN"=> "M",
39
+ "JUL"=> "N", "AUG"=> "Q", "SEP"=> "U",
40
+ "OCT"=> "V", "NOV"=> "X", "DEC"=> "Z" }
41
+
32
42
  end
33
43
  end
34
44
 
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env ruby
2
+ require 'bunny'
3
+ require 'json'
4
+
5
+ module Cotcube
6
+ module Helpers
7
+ class DataClient
8
+
9
+ def initialize
10
+ @connection = Bunny.new(automatically_recover: true)
11
+ @connection.start
12
+
13
+ @channel = connection.create_channel
14
+ @exchange = channel.direct('dataproxy_commands', auto_delete: true)
15
+ @requests = {}
16
+ @persistent = { depth: {}, realtimebars: {}, ticks: {} }
17
+ @response = nil
18
+
19
+ setup_reply_queue
20
+ end
21
+
22
+ def help
23
+ puts "The following commands are available:\n\n"\
24
+ "\tcontracts = client.get_contracts(symbol:)\n"\
25
+ "\tbars = client.get_historical(contract:, duration:, interval:, before: nil)"\
26
+ "\trequest_id = client.start_realtimebars(contract: )\n"\
27
+ "\t client.stop_realtimebars(request_id: )\n"\
28
+ "\trequest_id = client.start_ticks(contract: )\n"\
29
+ "\t client.stop_ticks(request_id: )\n"
30
+ end
31
+
32
+ def send_command(command, timeout: 5)
33
+ command = { command: command.to_s } unless command.is_a? Hash
34
+ command[:timestamp] ||= (Time.now.to_f * 1000).to_i
35
+ request_id = Digest::SHA256.hexdigest(command.to_json)[..6]
36
+ requests[request_id] = { request: command, id: request_id }
37
+
38
+ exchange.publish(command.to_json,
39
+ content_type: 'application/json',
40
+ routing_key: 'dataproxy_commands',
41
+ correlation_id: request_id,
42
+ reply_to: reply_queue.name)
43
+
44
+ # wait for the signal to continue the execution
45
+ lock.synchronize {
46
+ condition.wait(lock, timeout)
47
+ }
48
+
49
+ response
50
+ end
51
+
52
+ def stop
53
+ channel.close
54
+ connection.close
55
+ end
56
+
57
+
58
+ def get_contracts(symbol: )
59
+ send_command( { command: :get_contracts, symbol: symbol } )
60
+ end
61
+
62
+ def get_historical(contract:, interval:, duration: nil, before: nil, rth_only: false, based_on: :trades)
63
+ # rth.true? means data outside of rth is skipped
64
+ rth_only = rth_only ? 1 : 0
65
+ default_durations = {
66
+ sec1: '30_M',
67
+ sec5: '2_H',
68
+ sec15: '6_H',
69
+ sec30: '12_H',
70
+ min1: '1_D',
71
+ min2: '2_D',
72
+ min5: '5_D',
73
+ min15: '1_W',
74
+ min30: '1_W',
75
+ hour1: '1_W',
76
+ day1: '1_Y'
77
+ }
78
+ raise "Invalid interval '#{interval}', should be in '#{default_durations.keys}'." unless default_durations.keys.include? interval
79
+ # TODO: Check for valid duration specification
80
+ duration ||= default_durations[interval]
81
+ send_command( {
82
+ command: :historical,
83
+ contract: contract,
84
+ interval: interval,
85
+ duration: duration,
86
+ based_on: based_on.to_s.upcase,
87
+ rth_only: rth_only,
88
+ before: nil
89
+ }, timeout: 20 )
90
+ end
91
+
92
+ def start_persistent(contract:, type: :realtimebars, &block)
93
+ unless %i[ depth ticks realtimebars].include? type.to_sym
94
+ puts "ERROR: Inappropriate type in stop_realtimebars with #{type}"
95
+ return false
96
+ end
97
+
98
+ ib_contract = Cotcube::Helpers.get_ib_contract(contract)
99
+ exchange = channel.fanout( "dataproxy_#{type.to_s}_#{contract}", auto_delete: true)
100
+ queue = channel.queue('', exclusive: true, auto_delete: true)
101
+ queue.bind(exchange)
102
+ block ||= ->(bar){ puts "#{bar}" }
103
+ queue.subscribe do |_delivery_info, properties, payload|
104
+ block.call(JSON.parse(payload, symbolize_names: true))
105
+ end
106
+ command = { command: type, contract: contract, con_id: ib_contract[:con_id], delivery: queue.name, exchange: exchange.name }
107
+ persistent[type][queue.name] = command
108
+ persistent[type][queue.name][:queue] = queue
109
+ send_command(command)
110
+ end
111
+
112
+ def stop_persistent(contract:, type: :realtimebars )
113
+ unless %i[ depth ticks realtimebars].include? type.to_sym
114
+ puts "ERROR: Inappropriate type in stop_realtimebars with #{type}"
115
+ return false
116
+ end
117
+ ib_contract = Cotcube::Helpers.get_ib_contract(contract)
118
+ command = { command: "stop_#{type}", contract: contract, con_id: ib_contract[:con_id] }
119
+ send_command(command)
120
+ end
121
+
122
+ attr_accessor :response
123
+ attr_reader :lock, :condition
124
+
125
+ private
126
+ attr_reader :call_id, :connection, :requests, :persistent,
127
+ :channel, :server_queue_name, :reply_queue, :exchange
128
+
129
+
130
+ def setup_reply_queue
131
+ @lock = Mutex.new
132
+ @condition = ConditionVariable.new
133
+ that = self
134
+ @reply_queue = channel.queue('', exclusive: true, auto_delete: true)
135
+ @reply_queue.bind(channel.exchange('dataproxy_replies', auto_delete: true), routing_key: @reply_queue.name)
136
+
137
+ reply_queue.subscribe do |delivery_info, properties, payload|
138
+
139
+ __id__ = properties[:correlation_id]
140
+
141
+ if __id__.nil?
142
+ puts "Received without __id__: #{delivery_info.map{|k,v| "#{k}\t#{v}"}.join("\n")
143
+ }\n\n#{properties .map{|k,v| "#{k}\t#{v}"}.join("\n")
144
+ }\n\n#{JSON.parse(payload).map{|k,v| "#{k}\t#{v}"}.join("\n")}"
145
+
146
+ elsif requests[__id__].nil?
147
+ puts "Received non-matching response: \n\n#{_delivery_info}\n\n#{properties}\n\n#{payload}\n."
148
+ else
149
+ that.response = payload
150
+
151
+ # sends the signal to continue the execution of #call
152
+ requests.delete(__id__)
153
+ that.lock.synchronize { that.condition.signal }
154
+ end
155
+ end
156
+ end
157
+
158
+ end
159
+ end
160
+ end
161
+
162
+ __END__
163
+ begin
164
+ client = DataClient.new
165
+ reply = client.send_command( { command: 'ping' } ) #{ command: :hist, contract: 'A6Z21', con_id: 259130514, interval: :min15 } )
166
+ puts reply.nil? ? 'nil' : JSON.parse(reply)
167
+ reply = client.get_historical( contract: 'A6Z21', con_id: 259130514, interval: :min15 , rth_only: false)
168
+ JSON.parse(reply, symbolize_names: true)[:result].map{|z|
169
+ z[:datetime] = Cotcube::Helpers::CHICAGO.parse(z[:time]).strftime('%Y-%m-%d %H:%M:%S')
170
+ z.delete(:created_at)
171
+ z.delete(:time)
172
+ p z.slice(*%i[datetime open high low close volume]).values
173
+
174
+ }
175
+ ensure
176
+ client.stop
177
+ e,nd
@@ -4,6 +4,9 @@
4
4
  class DateTime
5
5
  # based on the fact that sunday is 'wday 0' plus that trading week starts
6
6
  # sunday 0:00 (as trading starts sunday 5pm CT to fit tokyo monday morning)
7
+ #
8
+ # TODO: there is a slight flaw, that 1 sunday per year is 1 hour too short and another is 1 hour too long
9
+ #
7
10
  def to_seconds_since_sunday_morning
8
11
  wday * 86_400 + hour * 3600 + min * 60 + sec
9
12
  end
@@ -3,12 +3,13 @@
3
3
  module Cotcube
4
4
  module Helpers
5
5
 
6
- def get_id_set(symbol: nil, id: nil, contract: nil, config: init)
6
+ def get_id_set(symbol: nil, id: nil, contract: nil, config: init, mini: false, micro: false)
7
+ micro = mini || micro
7
8
  contract = contract.to_s.upcase if contract.is_a? Symbol
8
9
  id = id.to_s.upcase if id.is_a? Symbol
9
10
  symbol = symbol.to_s.upcase if symbol.is_a? Symbol
10
11
 
11
- if contract.is_a?(String) && (contract.length == 5)
12
+ if contract.is_a?(String) && ([2,3,4,5].include? contract.length)
12
13
  c_symbol = contract[0..1]
13
14
  if (not symbol.nil?) && (symbol != c_symbol)
14
15
  raise ArgumentError,
@@ -19,25 +20,26 @@ module Cotcube
19
20
  end
20
21
 
21
22
  unless symbol.nil?
22
- sym = symbols.select { |s| s[:symbol] == symbol.to_s.upcase }.first
23
+ sym = symbols(symbol: symbol).presence || micros(symbol: symbol)
23
24
  if sym.nil? || sym[:id].nil?
24
25
  raise ArgumentError,
25
- "Could not find match in #{config[:symbols_file]} for given symbol #{symbol}"
26
+ "Could not find match in #{config[:symbols_file]} or #{config[:micros_file]} for given symbol #{symbol}"
26
27
  end
27
28
  raise ArgumentError, "Mismatching symbol #{symbol} and given id #{id}" if (not id.nil?) && (sym[:id] != id)
28
29
 
29
- return sym
30
+ return micro ? micros(id: sym[:id]) : sym
30
31
  end
31
32
  unless id.nil?
32
- sym = symbols.select { |s| s[:id] == id.to_s }.first
33
+ sym = symbols(id: id)
33
34
  if sym.nil? || sym[:id].nil?
34
35
  raise ArgumentError,
35
36
  "Could not find match in #{config[:symbols_file]} for given id #{id}"
36
37
  end
37
- return sym
38
+ return micro ? micros(id: sym[:id]) : sym
38
39
  end
39
40
  raise ArgumentError, 'Need :id, :symbol or valid :contract '
40
41
  end
42
+
41
43
  end
42
44
  end
43
45
 
@@ -0,0 +1,69 @@
1
+ module Cotcube
2
+ module Helpers
3
+ def get_ib_contract(contract)
4
+ symbol = contract[..1]
5
+ # TODO consider file location to be found in configfile
6
+ filepath = '/etc/cotcube/ibsymbols/'
7
+ result = YAML.load(File.read( "#{filepath}/#{symbol}.yml"))[contract].transform_keys(&:to_sym) rescue nil
8
+ result.nil? ? update_ib_contracts(symbol: contract[..1]) : (return result)
9
+ YAML.load(File.read( "#{filepath}/#{symbol}.yml"))[contract].transform_keys(&:to_sym) rescue nil
10
+ end
11
+
12
+ def update_ib_contracts(symbol: nil)
13
+ begin
14
+ client = DataClient.new
15
+ (Cotcube::Helpers.symbols + Cotcube::Helpers.micros).each do |sym|
16
+
17
+ # TODO: consider file location to be located in config
18
+ file = "/etc/cotcube/ibsymbols/#{sym[:symbol]}.yml"
19
+
20
+ # TODO: VI publishes weekly options which dont match, the 3 others need multiplier enabled to work
21
+ next if %w[ DY TM SI VI ].include? sym[:symbol]
22
+ next if symbol and sym[:symbol] != symbol
23
+ begin
24
+ if File.exist? file
25
+ next if Time.now - File.mtime(file) < 5.days
26
+ data = nil
27
+ data = YAML.load(File.read(file))
28
+ else
29
+ data = {}
30
+ end
31
+ p file
32
+ %w[ symbol sec_type exchange multiplier ticksize power internal ].each {|z| data.delete z}
33
+ raw = client.get_contracts(symbol: sym[:symbol])
34
+ reply = JSON.parse(raw)['result']
35
+ reply.each do |set|
36
+ contract = translate_ib_contract set['local_symbol']
37
+ data[contract] ||= set
38
+ end
39
+ keys = data.keys.sort_by{|z| z[2]}.sort_by{|z| z[-2..] }.select{|z| z[..1] == sym[:symbol] }
40
+ data = data.slice(*keys)
41
+ File.open(file, 'w'){|f| f.write(data.to_yaml) }
42
+ rescue Exception => e
43
+ puts e.full_message
44
+ p sym
45
+ binding.irb
46
+ end
47
+ end
48
+ ensure
49
+ client.stop
50
+ true
51
+ end
52
+ end
53
+
54
+ def translate_ib_contract(contract)
55
+ short = contract.split(" ").size == 1
56
+ sym_a = contract.split(short ? '' : ' ')
57
+ year = sym_a.pop.to_i + (short ? 20 : 0)
58
+ if short and sym_a[-1].to_i > 0
59
+ year = year - 20 + sym_a.pop.to_i * 10
60
+ end
61
+ month = short ? sym_a.pop : LETTERS[sym_a.pop]
62
+ sym = Cotcube::Helpers.symbols(internal: sym_a.join)[:symbol] rescue nil
63
+ sym ||= Cotcube::Helpers.micros(internal: sym_a.join)[:symbol] rescue nil
64
+ sym.nil? ? false : "#{sym}#{month}#{year}"
65
+ end
66
+
67
+ end
68
+ end
69
+
@@ -23,10 +23,11 @@ module Cotcube
23
23
  gem_name: nil,
24
24
  debug: false)
25
25
  gem_name ||= self.ancestors.first.to_s
26
- config_file_name = "#{gem_name.down_case}.yml"
26
+ config_file_name = "#{gem_name.downcase.split('::').last}.yml"
27
27
  config_file = config_path + "/#{config_file_name}"
28
28
 
29
29
  if File.exist?(config_file)
30
+ require 'yaml'
30
31
  config = YAML.load(File.read config_file).transform_keys(&:to_sym)
31
32
  else
32
33
  config = {}
@@ -0,0 +1,8 @@
1
+ class Numeric
2
+ def with_delimiter(deli=nil)
3
+ raise ArgumentError, "Param delimiter can't be nil" if deli.nil?
4
+ pre, post = self.to_s.split('.')
5
+ pre = pre.chars.to_a.reverse.each_slice(3).map(&:join).join(deli).reverse
6
+ post.nil? ? pre : [pre,post].join('.')
7
+ end
8
+ end
@@ -0,0 +1,20 @@
1
+ module Cotcube
2
+ module Helpers
3
+
4
+ def instance_inspect(obj, keylength: 20, &block)
5
+ obj.instance_variables.map do |var|
6
+ if block_given?
7
+ block.call(var, obj.instance_variable_get(var))
8
+ else
9
+ puts "#{format "%-#{keylength}s", var
10
+ }: #{obj.instance_variable_get(var).inspect.scan(/.{1,120}/).join( "\n" + ' '*(keylength+2))
11
+ }"
12
+ end
13
+ end
14
+ end
15
+
16
+ module_function :instance_inspect
17
+
18
+ end
19
+ end
20
+
@@ -0,0 +1,509 @@
1
+ module Cotcube
2
+ module Helpers
3
+ module Candlestick_Recognition
4
+
5
+ SUPERFLUOUS = %i[wap datetime prev_slope upper lower bar_size body_size lower_wick upper_wick ranges vranges vavg trades type bullish bearish doji contract]
6
+ COMMON = [:symbol, :timestamp, :size, :rth, :time_end,
7
+ :open, :high, :vhigh, :vlow, :low, :close,
8
+ :interval, :offset, :appeared, :datetime, :volume, :dist, :vperd, :vol_i, :contract, :date,
9
+ :bar_size, :upper_wick, :lower_wick, :body_size, :upper, :lower, :slope, :rel, :tr, :atr, :ranges, :vranges, :vavg]
10
+
11
+ # recognize serves as interface
12
+ def recognize(contract:, interval: :quarters, short: true, base:, return_as_string: false, sym: )
13
+ s = bas
14
+ CR::candles s, ticksize: sym[:ticksize]
15
+ CR::patterns s, contract: contract, ticksize: sym[:ticksize]
16
+ s.map{|x| x[:datetime] += 7.hours } if s and %w[ GG DY ].include?(contract[..1]) and interval == :quarters
17
+ s.map{|x| x[:datetime] += 1.hour } if s and %w[ GC PA PL SI HG NG HO RB CL ].include?(contract[..1]) and interval == :quarters
18
+ make_string = lambda {|c| "#{contract
19
+ }\t#{c[:datetime].strftime( interval == :quarters ? '%Y-%m-%d %H:%M' : '%Y-%m-%d' )}".colorize(:light_white) +
20
+ "\t#{print_bar(bar: c, format: sym[:format], power: sym[:power], short: short)
21
+ } #{"\n\n" if c[:datetime].wday == 5 and interval == :days}" }
22
+ return_as_string ? s[-count..].map{|candle| make_string.call(candle) }.join("\n") : s
23
+ end
24
+
25
+
26
+ def print_bar(bar:, format:, power:, short: false)
27
+ x = bar.dup
28
+ dir = x[:bullish] ? :bullish : x[:bearish] ? :bearish : x[:doji] ? :doji : :none
29
+ contract = bar[:contract]
30
+ SUPERFLUOUS.map{|s| x.delete(s)}
31
+ %i[ UPPER_PIVOT LOWER_PIVOT UPPER_ISOPIVOT LOWER_ISOPIVOT ].map{|s| x.delete(s)}
32
+
33
+
34
+ vol = x.keys.select{|x| x.to_s =~ /_volume/}[0]
35
+ x.delete vol
36
+
37
+ vol = vol.to_s.split("_")[0]
38
+ col = case dir
39
+ when :bullish
40
+ :light_green
41
+ when :bearish
42
+ :light_red
43
+ when :doji
44
+ :light_blue
45
+ else:light_black
46
+ end
47
+
48
+ special = lambda do |s|
49
+ case s
50
+ when *%i[ THRUSTING_LINE SHOOTING_STAR HANGING_MAN EVENING_STAR EVENING_DOJI_STAR UPSIDEGAP_TWO_CROWS UMKEHRSTAB_BEARISH ]
51
+ s.to_s.colorize(:light_red)
52
+ when *%i[ PIERCING_LINE INVERTED_HAMMER HAMMER MORNING_STAR MORNING_DOJI_STAR DOWNGAP_TWO_RIVERS UMKEHRSTAB_BULLISH ]
53
+ s.to_s.colorize(:light_green)
54
+ else
55
+ s.to_s.colorize(:light_cyan)
56
+ end
57
+ end
58
+
59
+
60
+
61
+ "#{ format '%12s', (format % x[:open ]) }" +
62
+ " #{format '%12s', (format % x[:high ])}".colorize( (x.keys.include?(:UPPER_PIVOT) or x.keys.include?(:UPPER_ISOPIVOT)) ? :light_blue : :white ) +
63
+ " #{format '%12s', (format % x[:low ])}".colorize( (x.keys.include?(:LOWER_PIVOT) or x.keys.include?(:LOWER_ISOPIVOT)) ? :light_blue : :white ) +
64
+ " #{format '%12s', ( format % x[:close ])}" +
65
+ (short ? "" : " #{"%10s" % ("[ % -4.1f ]" % x[:slope])}".colorize( if x[:slope].abs < 3; :yellow; elsif x[:slope] >= 3; :green; else; :red; end)) +
66
+ (short ? "" : " #{"% 8d" % x[:volume]}") +
67
+ " #{vol}".colorize( case vol; when *["BREAKN", "FAINTN"]; :light_blue; when *["RISING","FALLIN"]; :cyan; else; :white; end) +
68
+ (short ? "" : " D:#{"%5d" % x[:dist]}" ) +
69
+ (short ? "" : " P:#{"%10.2f" % (x[:dist] * power)} ".cyan ) +
70
+ format('%10s', dir.to_s).colorize(col) +
71
+ (short ? "" : format('%-22s', " >>> #{x.keys.map{|v| (COMMON.include?(v) or v.upcase == v)? nil : v}.compact.join(" ")}").colorize( col )) +
72
+ "\t#{x.keys.map{|v| (COMMON.include?(v) or v.upcase != v)? nil : special.call(v)}.compact.join(" ")}"
73
+ end
74
+
75
+ def candles(candles, debug: false, sym: )
76
+ ticksize = sym[:ticksize]
77
+
78
+ candles.each_with_index do |bar, i|
79
+ # 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
80
+ begin
81
+ rel = ((bar[:high] - bar[:low]) * 0.05).round(8)
82
+ rel = 2 * ticksize if rel < 2 * ticksize
83
+ rescue
84
+ puts "Warning, found inappropriate bar".light_white + " #{bar}"
85
+ raise
86
+ end
87
+ bar[:rel] = rel
88
+ bar[:dist] ||= ((bar[:high] - bar[:low])/ticksize).round(8)
89
+
90
+ bar[:upper] = [bar[:open], bar[:close]].max
91
+ bar[:lower] = [bar[:open], bar[:close]].min
92
+ bar[:bar_size] = (bar[:high] - bar[:low])
93
+ bar[:body_size] = (bar[:open] - bar[:close]).abs
94
+ bar[:lower_wick] = (bar[:lower] - bar[:low])
95
+ bar[:upper_wick] = (bar[:high] - bar[:upper])
96
+ bar.each{|k,v| bar[k] = v.round(8) if v.is_a? Float}
97
+
98
+ # a doji's open and close are same (or only differ by rel)
99
+ bar[:doji] = true if bar[:body_size] <= rel and bar[:dist] >= 3
100
+ bar[:tiny] = true if bar[:dist] <= 5
101
+
102
+ next if bar[:tiny]
103
+
104
+ bar[:bullish] = true if not bar[:doji] and bar[:close] > bar[:open]
105
+ bar[:bearish] = true if not bar[:doji] and bar[:close] < bar[:open]
106
+
107
+ bar[:spinning_top] = true if bar[:body_size] <= bar[:bar_size] / 4 and
108
+ bar[:lower_wick] >= bar[:bar_size] / 4 and
109
+ bar[:upper_wick] >= bar[:bar_size] / 4
110
+
111
+ # a marubozu open at high or low and closes at low or high
112
+ bar[:marubozu] = true if bar[:upper_wick] < rel and bar[:lower_wick] < rel
113
+
114
+ # a bar is considered bearish if it has at least a dist of 5 ticks and it's close it near high (low resp)
115
+ bar[:bullish_close] = true if (bar[:high] - bar[:close]) <= rel and not bar[:marubozu]
116
+ bar[:bearish_close] = true if (bar[:close] - bar[:low]) <= rel and not bar[:marubozu]
117
+
118
+ # 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
119
+ # TODO
120
+
121
+ end
122
+ candles
123
+ end
124
+
125
+ def comparebars(prev, curr)
126
+ bullishscore = 0
127
+ bearishscore = 0
128
+ bullishscore += 1 if prev[:high] <= curr[:high]
129
+ bullishscore += 1 if prev[:low] <= curr[:low]
130
+ bullishscore += 1 if prev[:close] <= curr[:close]
131
+ bearishscore += 1 if prev[:close] >= curr[:close]
132
+ bearishscore += 1 if prev[:low] >= curr[:low]
133
+ bearishscore += 1 if prev[:high] >= curr[:high]
134
+ r = {}
135
+ r[:bullish] = true if bullishscore >= 2
136
+ r[:bearish] = true if bearishscore >= 2
137
+ return r
138
+ end
139
+
140
+
141
+ def patterns(candles, debug: false, size: 5, contract:, ticksize: nil )
142
+ candles.each_with_index do |bar, i|
143
+ preceeding = candles.select{|x| x[:datetime] <= bar[:datetime] }
144
+ if i.zero?
145
+ bar[:slope] = 0
146
+ next
147
+ end
148
+ ppprev= candles[i-3]
149
+ pprev= candles[i-2]
150
+ prev = candles[i-1]
151
+ succ = candles[i+1]
152
+
153
+ bar[:huge] = true if bar[:true_range] >= bar[:atr] * 1.5
154
+ bar[:small] = true if bar[:true_range] <= bar[:atr5] * 0.6666
155
+
156
+ bar[:vavg] = (bar[:vranges].reduce(:+) / bar[:vranges].size.to_f).round if bar[:vranges] and bar[:vranges].compact.size > 0
157
+ bar[:vranges] = prev[:vranges].nil? ? [ bar[:volume] ] : prev[:vranges] + [ bar[:volume] ]
158
+ bar[:vranges].shift while bar[:vranges].size > size
159
+ bar[:vavg] ||= (bar[:vranges].reduce(:+) / bar[:vranges].size.to_f).round if bar[:vranges] and bar[:vranges].compact.size > 0
160
+
161
+
162
+
163
+ # VOLUME
164
+ if bar[:volume] > bar[:vavg] * 1.3
165
+ bar[:BREAKN_volume] = true
166
+ elsif bar[:volume] >= bar[:vavg] * 1.1
167
+ bar[:RISING_volume] = true
168
+ elsif bar[:volume] < bar[:vavg] * 0.7
169
+ bar[:FAINTN_volume] = true
170
+ elsif bar[:volume] <= bar[:vavg] * 0.9
171
+ bar[:FALLIN_volume] = true
172
+ else
173
+ bar[:STABLE_volume] = true
174
+ end
175
+
176
+ # GAPS
177
+ bar[:bodygap] = true if bar[:lower] > prev[:upper] or bar[:upper] < prev[:lower]
178
+ bar[:gap] = true if bar[:low] > prev[:high] or bar[:high] < prev[:low]
179
+
180
+
181
+ bar[:slope] = slopescore(pprev, prev, bar, debug)
182
+ bar[:prev_slope] = prev[:slope]
183
+
184
+ # UPPER_PIVOTs define by having higher highs and higher lows than their neighor
185
+ 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
186
+ 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
187
+ 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]
188
+ 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]
189
+
190
+ # stopping volume is defined as high volume candle during downtrend then closes above mid candle (i.e. lower_wick > body_size)
191
+ bar[:stopping_volume] = true if (bar[:BREAKN_volume] or bar[:RISING_volume]) and prev[:slope] < -5 and bar[:lower_wick] >= bar[:body_size]
192
+ bar[:stopping_volume] = true if (bar[:BREAKN_volume] or bar[:RISING_volume]) and prev[:slope] > 5 and bar[:upper_wick] >= bar[:body_size]
193
+ 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]
194
+ 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]
195
+
196
+
197
+ ###################################
198
+ # SINGLE CANDLE PATTERNS
199
+ ###################################
200
+
201
+ # 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
202
+ bar[:HAMMER] = true if bar[:upper_wick] <= bar[:rel] and bar[:body_size] <= bar[:bar_size] / 3 and bar[ :slope] <= -6
203
+ # same shape, but found at a raising slope without the need to gap away
204
+ bar[:HANGING_MAN] = true if bar[:upper_wick] <= bar[:rel] and bar[:body_size] <= bar[:bar_size] / 3 and prev[:slope] >= 6
205
+
206
+ # a shooting star is the inverse of the hammer, while the inverted hammer is the inverse of the hanging man
207
+ bar[:SHOOTING_STAR] = true if bar[:lower_wick] <= bar[:rel] and bar[:body_size] <= bar[:bar_size] / 2.5 and bar[ :slope] >= 6
208
+ bar[:INVERTED_HAMMER] = true if bar[:lower_wick] <= bar[:rel] and bar[:body_size] <= bar[:bar_size] / 3 and prev[:slope] <= -6
209
+
210
+ # a star is simply gapping away the preceding slope
211
+ if ((bar[:lower] > prev[:upper] and bar[:slope] >= 6 and bar[:high] >= prev[:high]) or
212
+ (bar[:upper] < prev[:lower] and bar[:slope] <= -6) and bar[:low] <= prev[:low])
213
+ bar[:doji] ? bar[:DOJI_STAR] = true : bar[:STAR] = true
214
+ end
215
+
216
+
217
+ # a belthold is has a gap in the open, but reverses strong
218
+ bar[:BULLISH_BELTHOLD] = true if bar[:lower_wick] <= bar[:rel] and bar[:body_size] >= bar[:bar_size] / 2 and
219
+ prev[:slope] <= -4 and bar[:lower] <= prev[:low ] and bar[:bullish] and not prev[:bullish] and bar[:bar_size] >= prev[:bar_size]
220
+ bar[:BEARISH_BELTHOLD] = true if bar[:upper_wick] <= bar[:rel] and bar[:body_size] >= bar[:bar_size] / 2 and
221
+ prev[:slope] >= -4 and bar[:upper] <= prev[:high] and bar[:bearish] and not prev[:bearish] and bar[:bar_size] >= prev[:bar_size]
222
+
223
+
224
+ ###################################
225
+ # DUAL CANDLE PATTERNS
226
+ ###################################
227
+
228
+
229
+ # ENGULFINGS
230
+ bar[:BULLISH_ENGULFING] = true if bar[:bullish] and prev[:bearish] and bar[:lower] <= prev[:lower] and bar[:upper] > prev[:upper] and prev[:slope] <= -6
231
+ bar[:BEARISH_ENGULFING] = true if bar[:bearish] and prev[:bullish] and bar[:lower] < prev[:lower] and bar[:upper] >= prev[:upper] and prev[:slope] >= 6
232
+
233
+
234
+ # DARK-CLOUD-COVER / PIERCING-LINE (on-neck / in-neck / thrusting / piercing / PDF pg 63)
235
+ 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
236
+ not bar[:BEARISH_ENGULFING]
237
+ 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
238
+ not bar[:BULLISH_ENGULFING]
239
+ 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
240
+ not bar[:BEARISH_ENGULFING] and not bar[:DARK_CLOUD_COVER]
241
+ 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
242
+ not bar[:BULLISH_ENGULFING] and not bar[:PIERCING_LINE]
243
+
244
+
245
+ # COUNTER ATTACKS are like piercings / cloud covers, but insist on a large reverse while only reaching the preceding close
246
+ 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
247
+ bar[:body_size] >= bar[:bar_size] * 0.5 and bar[:bullish]
248
+ 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
249
+ bar[:body_size] >= bar[:bar_size] * 0.5 and bar[:bearish]
250
+
251
+
252
+ # HARAMIs are an unusual long body embedding the following small body
253
+ bar[:HARAMI] = true if bar[:body_size] < prev[:body_size] / 2.5 and prev[:bar_size] >= bar[:atr] and
254
+ prev[:upper] > bar[:upper] and prev[:lower] < bar[:lower] and not bar[:doji]
255
+ bar[:HARAMI_CROSS] = true if bar[:body_size] < prev[:body_size] / 2.5 and prev[:bar_size] >= bar[:atr] and
256
+ prev[:upper] > bar[:upper] and prev[:lower] < bar[:lower] and bar[:doji]
257
+ if bar[:HARAMI] or bar[:HARAMI_CROSS]
258
+ puts [ :date, :open, :high, :low, :close, :upper, :lower ].map{|x| prev[x]}.join("\t") if debug
259
+ puts [ :date, :open, :high, :low, :close, :upper, :lower ].map{|x| bar[ x]}.join("\t") if debug
260
+ puts "" if debug
261
+ end
262
+
263
+ # TODO TWEEZER_TOP and TWEEZER_BOTTOM
264
+ # actually being a double top / bottom, this dual candle pattern has to be unfolded. It is valid on daily or weekly charts,
265
+ # and valid if
266
+ # 1 it has an according
267
+
268
+
269
+ ###################################
270
+ # TRIPLE CANDLE PATTERNS
271
+ ###################################
272
+
273
+ # morning star, morning doji star
274
+ next unless prev and pprev
275
+ bar[:MORNING_STAR] = true if prev[:STAR] and bar[:bullish] and bar[:close] >= pprev[:lower] and prev[:slope] < -6
276
+ bar[:MORNING_DOJI_STAR] = true if prev[:DOJI_STAR] and bar[:bullish] and bar[:close] >= pprev[:lower] and prev[:slope] < -6
277
+ bar[:EVENING_STAR] = true if prev[:STAR] and bar[:bearish] and bar[:close] <= pprev[:upper] and prev[:slope] > 6
278
+ bar[:EVENING_DOJI_STAR] = true if prev[:DOJI_STAR] and bar[:bearish] and bar[:close] <= pprev[:upper] and prev[:slope] > 6
279
+
280
+ # the abandoned baby escalates above stars by gapping the inner star candle to both framing it
281
+ bar[:ABANDONED_BABY] = true if (bar[:MORNING_STAR] or bar[:MORNING_DOJI_STAR]) and prev[:high] <= [ pprev[:low ], bar[:low ] ].min
282
+ bar[:ABANDONED_BABY] = true if (bar[:EVENING_STAR] or bar[:EVENING_DOJI_STAR]) and prev[:low ] >= [ pprev[:high], bar[:high] ].max
283
+
284
+ # UPSIDEGAP_TWO_CROWS
285
+ 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]
286
+ 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]
287
+
288
+ # THREE BLACK CROWS / THREE WHITE SOLDIERS
289
+ bar[:THREE_BLACK_CROWS] = true if [ bar, prev, pprev ].map{|x| x[:bearish] and x[:bar_size] > 0.5 * bar[:atr] }.reduce(:&) and
290
+ pprev[:close] - prev[ :close] > bar[:atr] * 0.2 and
291
+ prev[ :close] - bar[ :close] > bar[:atr] * 0.2
292
+ bar[:THREE_WHITE_SOLDIERS] = true if [ bar, prev, pprev ].map{|x| x[:bullish] and x[:bar_size] > 0.5 * bar[:atr] }.reduce(:&) and
293
+ prev[:close] - pprev[:close] > bar[:atr] * 0.2 and
294
+ bar[ :close] - prev[ :close] > bar[:atr] * 0.2
295
+
296
+ #### MARKTTECHNIK ####
297
+
298
+ # Umkehrstäbe
299
+ # Ein Umkehrstab bullish ist ein Candle, der zumindest einen Downtrend aus 3 Kerzen beenden könnte.
300
+ # dazu muss der stab selber ein niedrigers tief haben als seine beiden vorgänger,
301
+ # der TR muss above average sein und der vorgänger muss einen slopescore von < -5 haben
302
+ #
303
+ if pprev[:low] > prev[:low] and prev[:low] > bar[:low] and
304
+ ( bar[:tr] > bar[:atr] * 1.25 or bar[:BREAKN_volume] ) and
305
+ prev[:slope] <= -5 and
306
+ bar[:close] - bar[:low] >= (bar[:bar_size]) * 0.7
307
+ #bar[:close] >= (bar[:high] + bar[:low]) / 2.0
308
+ bar[:UMKEHRSTAB_BULLISH] = true
309
+ bar[:CLASSIC] = true
310
+ end
311
+
312
+ if pprev[:high] < prev[:high] and prev[:high] < bar[:high] and
313
+ ( bar[:tr] > bar[:atr] * 1.25 or bar[:BREAKN_volume] ) and
314
+ prev[:slope] >= 5 and
315
+ bar[:high] - bar[:close] >= (bar[:bar_size]) * 0.7
316
+ #bar[:close] <= (bar[:high] + bar[:low]) / 2.0
317
+ bar[:UMKEHRSTAB_BEARISH] = true
318
+ bar[:CLASSIC] = true
319
+ end
320
+
321
+ # TINY REVERSALS only work for short periods of time (i.e. lte 5 min)
322
+
323
+ if bar[:datetime] and bar[:datetime].respond_to?(:-) and bar[:datetime] - prev[:datetime] <= 5*60
324
+ if pprev[:low] > prev[:low] and prev[:low] > bar[:low] and
325
+ ((not bar[:tiny] and bar[:bar_size] >= 0.75 * bar[:atr]) or bar[:BREAKN_volume]) and
326
+ bar[:high] - bar[:close] < bar[:bar_size] / 2.5
327
+ bar[:UMKEHRSTAB_BULLISH] = true
328
+ bar[:TINY] = true
329
+ end
330
+ if pprev[:high] < prev[:high] and prev[:high] < bar[:high] and
331
+ ((not bar[:tiny] and bar[:bar_size] >= 0.75 * bar[:atr]) or bar[:BREAKN_volume]) and
332
+ bar[:close] - bar[:low] < bar[:bar_size] / 2.5
333
+ bar[:UMKEHRSTAB_BEARISH] = true
334
+ bar[:TINY] = true
335
+ end
336
+ end
337
+
338
+ # GEM reversals are just another set of criteria
339
+ if prev[:low] > bar[:low] and prev[:high] > bar[:high] and
340
+ (bar[:bar_size] >= bar[:atr] or bar[:BREAKN_volume]) and
341
+ bar[:high] - bar[:close] < bar[:bar_size] / 4.0
342
+ bar[:UMKEHRSTAB_BULLISH] = true
343
+ bar[:GEM] = true
344
+ end
345
+ if prev[:low] < bar[:low] and prev[:high] < bar[:high] and
346
+ (bar[:bar_size] >= bar[:atr] or bar[:BREAKN_volume]) and
347
+ bar[:close] - bar[:low] < bar[:bar_size] / 4.0
348
+ bar[:UMKEHRSTAB_BEARISH] = true
349
+ bar[:GEM] = true
350
+ end
351
+
352
+ # DOUBLEBAR reversals are reversals, that span over 2 bars instead of one
353
+ unless ppprev.nil? or pprev[:slope].nil?
354
+ pot = {
355
+ open: prev[:open],
356
+ high: [prev[:high], bar[:high]].max,
357
+ low: [prev[:low], bar[:low]].min,
358
+ close: bar[:close]
359
+ }
360
+ pot[:size] = (pot[:high] - pot[:low]).round(8)
361
+ if (pprev[:slope] >=8 or (prev[:low] > pprev[:low] and pprev[:low] > ppprev[:low])) and pot[:high] > pprev[:high] and
362
+ pot[:high] - pot[:close] >= pot[:size] * 0.7
363
+ bar[:UMKEHRSTAB_BEARISH] = true
364
+ bar[:DOUBLE] = true
365
+ end
366
+ if (pprev[:slope] <=-8 or (prev[:high] < pprev[:high] and pprev[:high] < ppprev[:high])) and
367
+ pot[:low] < pprev[:low] and
368
+ pot[:close] - pot[:low] >= pot[:size] * 0.7
369
+ bar[:UMKEHRSTAB_BULLISH] = true
370
+ bar[:DOUBLE] = true
371
+ end
372
+ end
373
+
374
+ # BULLISH_MOVE und BEARISH_MOVE
375
+
376
+ unless pprev[:atr].nil?
377
+ if pprev[:high] > prev[:high] and prev[:high] > bar[:high] and
378
+ pprev[:low] > prev[:low] and prev[:low] > bar[:low] and
379
+ bar[:bar_size] >= pprev[:atr] and pprev[:bar_size] >= pprev[:atr] and prev[:bar_size] >= pprev[:atr] and
380
+ bar[:close] - bar[:low] < bar[:bar_size] / 3
381
+ bar[:BEARISH_MOVE] = prev[:BEARISH_MOVE].nil? ? (pprev[:BEARISH_MOVE].nil? ? 1 : pprev[:BEARISH_MOVE] + 2) : prev[:BEARISH_MOVE] + 1
382
+ end
383
+ if pprev[:high] < prev[:high] and prev[:high] < bar[:high] and
384
+ pprev[:low] < prev[:low] and prev[:low] < bar[:low] and
385
+ bar[:bar_size] >= pprev[:atr] and pprev[:bar_size] >= pprev[:atr] and prev[:bar_size] >= pprev[:atr] and
386
+ bar[:high] - bar[:close] < bar[:bar_size] / 3
387
+ bar[:BULLISH_MOVE] = prev[:BULLISH_MOVE].nil? ? (pprev[:BULLISH_MOVE].nil? ? 1 : pprev[:BULLISH_MOVE] + 2) : prev[:BULLISH_MOVE] + 1
388
+ end
389
+ end
390
+
391
+ # support and resistance
392
+
393
+ end
394
+ end
395
+
396
+
397
+ # SLOPE SCORE
398
+ def slopescore(pprev, prev, bar, debug = false)
399
+ # the slope between to bars is considered bullish, if 2 of three points match
400
+ # - higher high
401
+ # - higher close
402
+ # - higher low
403
+ # the opposite counts for bearish
404
+ #
405
+ # this comparison is done between the current bar and previous bar
406
+ # - if it confirms the score of the previous bar, the new slope score is prev + curr
407
+ # - otherwise the is compared to score of the pprevious bar
408
+ # - if it confirms there, the new slope score is pprev + curr
409
+ # - otherwise the trend is destroyed and tne new score is solely curr
410
+
411
+ if bar[:bullish]
412
+ curr = 1
413
+ curr += 1 if bar[:bullish_close]
414
+ elsif bar[:bearish]
415
+ curr = -1
416
+ curr -= 1 if bar[:bearish_close]
417
+ else
418
+ curr = 0
419
+ end
420
+ puts "curr set to #{curr} @ #{bar[:date]}".yellow if debug
421
+ if prev.nil?
422
+ puts "no prev found, score == curr: #{curr}" if debug
423
+ score = curr
424
+ else
425
+ comp = comparebars(prev, bar)
426
+
427
+ puts prev.select{|k,v| [:high,:low,:close,:score].include?(k)} if debug
428
+ puts bar if debug
429
+ puts "COMPARISON 1: #{comp}" if debug
430
+
431
+ if prev[:slope] >= 0 and comp[:bullish] # bullish slope confirmed
432
+ score = prev[:slope]
433
+ score += curr if curr > 0
434
+ [ :gap, :bodygap ] .each {|x| score += 0.5 if bar[x] }
435
+ score += 1 if bar[:RISING_volume]
436
+ score += 2 if bar[:BREAKN_volume]
437
+ puts "found bullish slope confirmed, new score #{score}" if debug
438
+ elsif prev[:slope] <= 0 and comp[:bearish] # bearish slope confirmed
439
+ score = prev[:slope]
440
+ score += curr if curr < 0
441
+ [ :gap, :bodygap ] .each {|x| score -= 0.5 if bar[x] }
442
+ score -= 1 if bar[:RISING_volume]
443
+ score -= 2 if bar[:BREAKN_volume]
444
+ puts "found bearish slope confirmed, new score #{score} (including #{curr} and #{bar[:bodygap]} and #{bar[:gap]}" if debug
445
+ else #if prev[:slope] > 0 # slopes failed
446
+ puts "confirmation failed: " if debug
447
+ if pprev.nil?
448
+ score = curr
449
+ else
450
+ comp2 = comparebars(pprev, bar)
451
+ puts "\t\tCOMPARISON 2: #{comp2}" if debug
452
+ if pprev[:slope] >= 0 and comp2[:bullish] # bullish slope confirmed on pprev
453
+ score = pprev[:slope]
454
+ score += curr if curr > 0
455
+ [ :gap, :bodygap ] .each {|x| score += 0.5 if bar[x] }
456
+ puts "\t\tfound bullish slope confirmed, new score #{score}" if debug
457
+ score += 1 if bar[:RISING_volume]
458
+ score += 2 if bar[:BREAKN_volume]
459
+ elsif pprev[:slope] <= 0 and comp2[:bearish] # bearish slope confirmed
460
+ score = pprev[:slope]
461
+ score += curr if curr < 0
462
+ [ :gap, :bodygap ] .each {|x| score -= 0.5 if bar[x] }
463
+ score -= 1 if bar[:RISING_volume]
464
+ score -= 2 if bar[:BREAKN_volume]
465
+ puts "\t\tfound bearish slope confirmed, new score #{score}" if debug
466
+ else #slope confirmation finally failed
467
+ comp3 = comparebars(pprev, prev)
468
+ if prev[:slope] > 0 # was bullish, turning bearish now
469
+ score = curr
470
+ score -= 1 if comp3[:bearish]
471
+ score -= 1 if comp[:bearish]
472
+ score -= 1 if prev[:bearish]
473
+ score -= 1 if prev[:RISING_volume] and comp3[:bearish]
474
+ score -= 2 if prev[:BREAKN_volume] and comp3[:bearish]
475
+ score -= 1 if bar[:RISING_volume] and comp[:bearish]
476
+ score -= 2 if bar[:BREAKN_volume] and comp[:bearish]
477
+ score -= 1 if bar[:RISING_volume] and comp[:bearish]
478
+ score -= 2 if bar[:BREAKN_volume] and comp[:bearish]
479
+ [ :gap, :bodygap ] .each {|x| score += 0.5 if bar[x] }
480
+ puts "\t\tfinally gave up, turning bearish now, new score #{score}" if debug
481
+ elsif prev[:slope] < 0
482
+ score = curr
483
+ score += 1 if comp3[:bullish]
484
+ score += 1 if comp[:bullish]
485
+ score += 1 if prev[:bullish]
486
+ score += 1 if prev[:RISING_volume] and comp3[:bullish]
487
+ score += 2 if prev[:BREAKN_volume] and comp3[:bullish]
488
+ score += 1 if bar[:RISING_volume] and comp[:bullish]
489
+ score += 2 if bar[:BREAKN_volume] and comp[:bullish]
490
+ score += 1 if bar[:RISING_volume] and comp[:bullish]
491
+ score += 2 if bar[:BREAKN_volume] and comp[:bullish]
492
+ [ :gap, :bodygap ] .each {|x| score -= 0.5 if bar[x] } if curr < 0
493
+ puts "\t\tfinally gave up, turning bullish now, new score #{score}" if debug
494
+ else
495
+ score = 0
496
+ end
497
+ end
498
+ end
499
+ end
500
+ end
501
+ puts "" if debug
502
+ score
503
+ end
504
+
505
+ end
506
+
507
+ CR = Candlestick_Recognition
508
+ end
509
+ end
@@ -22,12 +22,12 @@ module Cotcube
22
22
  lambda do |x|
23
23
  return false if x.nil? || (x.size < minimum)
24
24
 
25
- return ((pattern =~ /^#{x}/i).nil? ? false : true)
25
+ return ((pattern =~ /^#{Regexp.escape x}/i).nil? ? false : true)
26
26
  end
27
27
  when Array
28
28
  pattern.map do |x|
29
29
  unless [String, Symbol, NilClass].include? x.class
30
- raise TypeError, "Unsupported class '#{x.class}' for '#{x}'in pattern '#{pattern}'."
30
+ raise TypeError, "Unsupported class '#{x.class}' for '#{x}' in pattern '#{pattern}'."
31
31
  end
32
32
  end
33
33
  lambda do |x|
@@ -35,13 +35,13 @@ module Cotcube
35
35
  sub = sub.to_s
36
36
  return false if x.size < minimum
37
37
 
38
- result = ((sub =~ /^#{x}/i).nil? ? false : true)
38
+ result = ((sub =~ /^#{Regexp.escape x}/i).nil? ? false : true)
39
39
  return true if result
40
40
  end
41
41
  return false
42
42
  end
43
43
  else
44
- raise TypeError, "Unsupported class #{pattern.class} in Cotcube::Core::sub"
44
+ raise TypeError, "Unsupported class #{pattern.class} in Cotcube::Helpers::sub"
45
45
  end
46
46
  end
47
47
  end
@@ -4,18 +4,66 @@ module Cotcube
4
4
  # Missing top level documentation
5
5
  module Helpers
6
6
 
7
- def symbols(config: init, type: nil, symbol: nil)
7
+ SYMBOL_HEADERS = %i[ id symbol ib_symbol internal exchange currency ticksize power months type bcf reports format name ]
8
+
9
+ def symbols(config: init, **args)
8
10
  if config[:symbols_file].nil?
9
11
  SYMBOL_EXAMPLES
10
12
  else
11
13
  CSV
12
- .read(config[:symbols_file], headers: %i{ id symbol ticksize power months type bcf reports format name})
14
+ .read(config[:symbols_file], headers: SYMBOL_HEADERS)
13
15
  .map{|row| row.to_h }
14
- .map{|row| [ :ticksize, :power, :bcf ].each {|z| row[z] = row[z].to_f}; row[:format] = "%#{row[:format]}f"; row }
16
+ .map{|row|
17
+ [ :ticksize, :power, :bcf ].each {|z| row[z] = row[z].to_f}
18
+ row[:format] = "%#{row[:format]}f"
19
+ row[:currency] ||= 'USD'
20
+ row[:multiplier] = (row[:power] / row[:ticksize]).round(8)
21
+ row
22
+ }
15
23
  .reject{|row| row[:id].nil? }
16
- .tap{|all| all.select!{|x| x[:type] == type} unless type.nil? }
17
- .tap { |all| all.select! { |x| x[:symbol] == symbol } unless symbol.nil? }
24
+ .tap{ |all|
25
+ args.keys.each { |header|
26
+ unless SYMBOL_HEADERS.include? header
27
+ puts "WARNING in Cotcube::Helpers.symbols: '#{header}' is not a valid symbol header. Skipping..."
28
+ next
29
+ end
30
+ all.select!{|x| x[header] == args[header]} unless args[header].nil?
31
+ return all.first if all.size == 1
32
+ }
33
+ return all
34
+ }
18
35
  end
19
36
  end
20
37
 
38
+ def micros(config: init, **args)
39
+ if config[:micros_file].nil?
40
+ MICRO_EXAMPLES
41
+ else
42
+ CSV
43
+ .read(config[:micros_file], headers: SYMBOL_HEADERS)
44
+ .map{|row| row.to_h }
45
+ .map{|row|
46
+ [ :ticksize, :power, :bcf ].each {|z| row[z] = row[z].to_f }
47
+ row[:format] = "%#{row[:format]}f"
48
+ row[:currency] ||= 'USD'
49
+ row[:multiplier] = (row[:power] / row[:ticksize]).round(8)
50
+ row
51
+ }
52
+ .reject{|row| row[:id].nil? }
53
+ .tap{ |all|
54
+ args.keys.each { |header|
55
+ unless SYMBOL_HEADERS.include? header
56
+ puts "WARNING in Cotcube::Helpers.micros: '#{header}' is not a valid symbol header. Skipping..."
57
+ next
58
+ end
59
+ all.select!{|x| x[header] == args[header]} unless args[header].nil?
60
+ return all.first if all.size == 1
61
+ }
62
+ return all
63
+ }
64
+ end
65
+ end
66
+
67
+ end
68
+
21
69
  end
@@ -6,6 +6,9 @@ require 'active_support'
6
6
  require 'active_support/core_ext/time'
7
7
  require 'active_support/core_ext/numeric'
8
8
  require 'parallel'
9
+ require 'csv'
10
+ require 'yaml'
11
+ require 'json'
9
12
 
10
13
  require_relative 'cotcube-helpers/array_ext'
11
14
  require_relative 'cotcube-helpers/enum_ext'
@@ -13,26 +16,37 @@ require_relative 'cotcube-helpers/hash_ext'
13
16
  require_relative 'cotcube-helpers/range_ext'
14
17
  require_relative 'cotcube-helpers/string_ext'
15
18
  require_relative 'cotcube-helpers/datetime_ext'
19
+ require_relative 'cotcube-helpers/numeric_ext'
16
20
  require_relative 'cotcube-helpers/subpattern'
17
21
  require_relative 'cotcube-helpers/parallelize'
18
22
  require_relative 'cotcube-helpers/simple_output'
19
23
  require_relative 'cotcube-helpers/simple_series_stats'
24
+ require_relative 'cotcube-helpers/constants'
20
25
  require_relative 'cotcube-helpers/input'
26
+ require_relative 'cotcube-helpers/output'
21
27
  require_relative 'cotcube-helpers/reduce'
22
- require_relative 'cotcube-helpers/constants'
23
28
  require_relative 'cotcube-helpers/symbols'
24
29
  require_relative 'cotcube-helpers/init'
25
30
  require_relative 'cotcube-helpers/get_id_set'
31
+ require_relative 'cotcube-helpers/ib_contracts'
32
+ require_relative 'cotcube-helpers/recognition'
33
+ require_relative 'cotcube-helpers/data_client'
26
34
 
27
35
  module Cotcube
28
36
  module Helpers
29
37
  module_function :sub,
30
38
  :parallelize,
39
+ :config_path,
40
+ :config_prefix,
31
41
  :reduce,
32
42
  :simple_series_stats,
33
43
  :keystroke,
34
44
  :symbols,
45
+ :micros,
35
46
  :get_id_set,
47
+ :get_ib_contract,
48
+ :update_ib_contracts,
49
+ :translate_ib_contract,
36
50
  :init
37
51
 
38
52
  # please not that module_functions of source provided in private files must be published there
@@ -0,0 +1,25 @@
1
+ #!/bin/bash
2
+
3
+ export rubyenv=/home/pepe/.rvm/environments/default
4
+
5
+ . $rubyenv
6
+ cd /home/pepe/GEMS/${1}
7
+ export LC_ALL="en_US.utf8"
8
+
9
+ ruby ${2} ${3} ${4} ${5} ${6}
10
+
11
+
12
+ exit
13
+ for testing run
14
+ env - `cat /home/pepe/bin/cron_ruby_wrapper.sh | tail -n 6` /bin/bash
15
+
16
+ HOME=/home/pepe
17
+ LOGNAME=pepe
18
+ PATH=/usr/bin:/bin
19
+ LANG=en_US.UTF-8
20
+ SHELL=/bin/sh
21
+ PWD=/home/pepe
22
+
23
+
24
+
25
+
data/scripts/symbols ADDED
@@ -0,0 +1,13 @@
1
+ #!/bin/sh
2
+
3
+ option=$1
4
+ headers='ID,Symbol,ticksize,power,months,type,factor,reports,format,name'
5
+
6
+ if [ -z "${option}" ]
7
+ then
8
+ (echo ${headers} && cat /etc/cotcube/symbols.csv /etc/cotcube/symbols_micros.csv) | sed 's/,/ , /g' | column -s, -t
9
+ else
10
+ (echo ${headers} && cat /etc/cotcube/symbols.csv /etc/cotcube/symbols_micros.csv) | grep -i "$option\|reports,format" | sed 's/,/ , /g' | column -s, -t
11
+ fi
12
+
13
+
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cotcube-helpers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.9.1
4
+ version: 0.2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benjamin L. Tischendorf
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-07 00:00:00.000000000 Z
11
+ date: 2021-11-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -97,14 +97,19 @@ files:
97
97
  - lib/cotcube-helpers.rb
98
98
  - lib/cotcube-helpers/array_ext.rb
99
99
  - lib/cotcube-helpers/constants.rb
100
+ - lib/cotcube-helpers/data_client.rb
100
101
  - lib/cotcube-helpers/datetime_ext.rb
101
102
  - lib/cotcube-helpers/enum_ext.rb
102
103
  - lib/cotcube-helpers/get_id_set.rb
103
104
  - lib/cotcube-helpers/hash_ext.rb
105
+ - lib/cotcube-helpers/ib_contracts.rb
104
106
  - lib/cotcube-helpers/init.rb
105
107
  - lib/cotcube-helpers/input.rb
108
+ - lib/cotcube-helpers/numeric_ext.rb
109
+ - lib/cotcube-helpers/output.rb
106
110
  - lib/cotcube-helpers/parallelize.rb
107
111
  - lib/cotcube-helpers/range_ext.rb
112
+ - lib/cotcube-helpers/recognition.rb
108
113
  - lib/cotcube-helpers/reduce.rb
109
114
  - lib/cotcube-helpers/simple_output.rb
110
115
  - lib/cotcube-helpers/simple_series_stats.rb
@@ -114,6 +119,8 @@ files:
114
119
  - lib/cotcube-helpers/swig/fill_x.rb
115
120
  - lib/cotcube-helpers/swig/recognition.rb
116
121
  - lib/cotcube-helpers/symbols.rb
122
+ - scripts/cron_ruby_wrapper.sh
123
+ - scripts/symbols
117
124
  homepage: https://github.com/donkeybridge/cotcube-helpers
118
125
  licenses:
119
126
  - BSD-4-Clause
@@ -136,7 +143,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
136
143
  - !ruby/object:Gem::Version
137
144
  version: '0'
138
145
  requirements: []
139
- rubygems_version: 3.1.2
146
+ rubygems_version: 3.1.6
140
147
  signing_key:
141
148
  specification_version: 4
142
149
  summary: Some helpers and core extensions as part of the Cotcube Suite.