cotcube-helpers 0.1.9.1 → 0.2.1.1

Sign up to get free protection for your applications and to get access to all the features.
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.