Module: Cotcube::Helpers

Defined in:
lib/cotcube-helpers/input.rb,
lib/cotcube-helpers.rb,
lib/cotcube-helpers/init.rb,
lib/cotcube-helpers/output.rb,
lib/cotcube-helpers/reduce.rb,
lib/cotcube-helpers/symbols.rb,
lib/cotcube-helpers/constants.rb,
lib/cotcube-helpers/expiration.rb,
lib/cotcube-helpers/get_id_set.rb,
lib/cotcube-helpers/subpattern.rb,
lib/cotcube-helpers/data_client.rb,
lib/cotcube-helpers/orderclient.rb,
lib/cotcube-helpers/parallelize.rb,
lib/cotcube-helpers/recognition.rb,
lib/cotcube-helpers/cache_client.rb,
lib/cotcube-helpers/ib_contracts.rb,
lib/cotcube-helpers/josch_client.rb,
lib/cotcube-helpers/order_client.rb,
lib/cotcube-helpers/simple_output.rb,
lib/cotcube-helpers/simple_series_stats.rb,
lib/cotcube-helpers/deep_decode_datetime.rb

Overview

Missing top level documentation

Defined Under Namespace

Modules: Candlestick_Recognition Classes: CacheClient, DataClient, ExpirationMonth, JoSchClient, OrderClient, SimpleOutput

Constant Summary collapse

SYMBOL_HEADERS =
%i[ id symbol ib_symbol internal exchange currency ticksize power months type bcf reports format name ]
SYMBOL_EXAMPLES =
[
  { 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' },
  { 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' }
].freeze
MICRO_EXAMPLES =
[
  { 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' },
  { 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' }
].freeze
COLORS =
%i[light_red light_yellow light_green red yellow green cyan magenta blue].freeze
MONTH_COLOURS =
{ 'F' => :cyan,  'G' => :green,   'H' => :light_green,
'J' => :blue,  'K' => :yellow,  'M' => :light_yellow,
'N' => :cyan,  'Q' => :magenta, 'U' => :light_magenta,
'V' => :blue,  'X' => :red,     'Z' => :light_red }.freeze
MONTHS =
{ 'F' => 1,  'G' =>  2, 'H' =>  3,
'J' => 4,  'K' =>  5, 'M' =>  6,
'N' => 7,  'Q' =>  8, 'U' =>  9,
'V' => 10, 'X' => 11, 'Z' => 12,
1 => 'F',  2 => 'G',  3 => 'H',
4 => 'J',  5 => 'K',  6 => 'M',
7 => 'N',  8 => 'Q',  9 => 'U',
10 => 'V', 11 => 'X', 12 => 'Z' }.freeze
CHICAGO =
Time.find_zone('America/Chicago')
NEW_YORK =
Time.find_zone('America/New_York')
BERLIN =
Time.find_zone('Europe/Berlin')
DATE_FMT =
'%Y-%m-%d'
LETTERS =

Simple mapper to get from MONTH to LETTER

{ "JAN"=> "F", "FEB"=> "G", "MAR"=> "H",
"APR"=> "J", "MAY"=> "K", "JUN"=> "M",
"JUL"=> "N", "AUG"=> "Q", "SEP"=> "U",
"OCT"=> "V", "NOV"=> "X", "DEC"=> "Z" }
CR =
Candlestick_Recognition
JoschClient =
JoSchClient
VALID_DATETIME_STRING =
lambda {|str| str.is_a?(String) and [10,25,29].include?(str.length) and str.count("^0-9:TZ+-= ").zero? }

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.config_pathObject

.config_prefixObject

.deep_decode_datetime(data, zone: DateTime) ⇒ Object



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# File 'lib/cotcube-helpers/deep_decode_datetime.rb', line 5

def deep_decode_datetime(data, zone: DateTime)
  case data
  when nil;    nil
  when VALID_DATETIME_STRING
    res = nil
    begin
      res = zone.parse(data)
    rescue ArgumentError
      data
    end
    [ DateTime, ActiveSupport::TimeWithZone ].include?(res.class) ? res : data
  when Array; data.map!              { |d| deep_decode_datetime(d, zone: zone) }
  when Hash;  data.transform_values! { |v| deep_decode_datetime(v, zone: zone) }
  else;       data
  end
end

.get_ib_contractObject

.get_id_setObject

.initObject

.instance_inspect(obj, keylength: 20, &block) ⇒ Object



4
5
6
7
8
9
10
11
12
13
14
# File 'lib/cotcube-helpers/output.rb', line 4

def instance_inspect(obj, keylength: 20, &block)
  obj.instance_variables.map do |var| 
    if block_given?
      block.call(var, obj.instance_variable_get(var))
    else
      puts "#{format "%-#{keylength}s", var
                     }: #{obj.instance_variable_get(var).inspect.scan(/.{1,120}/).join( "\n" + ' '*(keylength+2)) 
                     }" 
    end
  end
end

.keystrokeObject

.microsObject

.parallelizeObject

.reduceObject

.simple_series_statsObject

.subObject

.symbolsObject

.translate_ib_contractObject

.update_ib_contractsObject

Instance Method Details

#config_pathObject



18
19
20
# File 'lib/cotcube-helpers/init.rb', line 18

def config_path
  config_prefix + '/etc/cotcube'
end

#config_prefixObject



6
7
8
9
10
11
12
13
14
15
16
# File 'lib/cotcube-helpers/init.rb', line 6

def config_prefix
  os = Gem::Platform.local.os
  case os
  when 'linux'
    ''
  when 'freebsd'
    '/usr/local'
  else
    raise RuntimeError, "Unsupported architecture: #{os}"
  end
end

#get_ib_contract(contract) ⇒ Object



3
4
5
6
7
8
9
10
# File 'lib/cotcube-helpers/ib_contracts.rb', line 3

def get_ib_contract(contract)
  symbol = contract[..1]
  # TODO consider file location to be found in configfile
  filepath =  '/etc/cotcube/ibsymbols/'
  result = YAML.load(File.read( "#{filepath}/#{symbol}.yml"))[contract].transform_keys(&:to_sym) rescue nil
  result.nil? ? update_ib_contracts(symbol: contract[..1]) : (return result)
  YAML.load(File.read( "#{filepath}/#{symbol}.yml"))[contract].transform_keys(&:to_sym) rescue nil
end

#get_id_set(symbol: nil, id: nil, contract: nil, config: init, mini: false, micro: false) ⇒ Object

Raises:

  • (ArgumentError)


6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/cotcube-helpers/get_id_set.rb', line 6

def get_id_set(symbol: nil, id: nil, contract: nil, config: init, mini: false, micro: false)
  micro = mini || micro
  contract = contract.to_s.upcase if contract.is_a? Symbol
  id       =       id.to_s.upcase if       id.is_a? Symbol
  symbol   =   symbol.to_s.upcase if   symbol.is_a? Symbol

  if contract.is_a?(String) && ([2,3,4,5].include? contract.length)
    c_symbol = contract[0..1]
    if (not symbol.nil?) && (symbol != c_symbol)
      raise ArgumentError,
            "Mismatch between given symbol #{symbol} and contract #{contract}"
    end

    symbol = c_symbol
  end

  unless symbol.nil?
    sym = symbols(symbol: symbol).presence || micros(symbol: symbol)
    if sym.nil? || sym[:id].nil?
      raise ArgumentError,
            "Could not find match in #{config[:symbols_file]} or #{config[:micros_file]} for given symbol #{symbol}"
    end
    raise ArgumentError, "Mismatching symbol #{symbol} and given id #{id}" if (not id.nil?) && (sym[:id] != id)

    return micro ? micros(id: sym[:id])  : sym
  end
  unless id.nil?
    sym = symbols(id: id)
    if sym.nil? || sym[:id].nil?
      raise ArgumentError,
            "Could not find match in #{config[:symbols_file]} for given id #{id}"
    end
    return micro ? micros(id: sym[:id]) : sym
  end
  raise ArgumentError, 'Need :id, :symbol or valid :contract '
end

#init(config_file_name: nil, gem_name: nil, debug: false) ⇒ Object



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/cotcube-helpers/init.rb', line 22

def init(config_file_name: nil, 
         gem_name: nil,
         debug: false)
  gem_name        ||= self.ancestors.first.to_s
  name              = gem_name.split('::').last.downcase
  config_file_name  = "#{name}.yml"
  config_file       = config_path + "/#{config_file_name}"

  if File.exist?(config_file)
    require 'yaml'
    config      = YAML.load(File.read config_file).transform_keys(&:to_sym)
  else
    config      = {} 
  end

  defaults = { 
    data_path: '/var/cotcube/' + name,
    pid_file:  "/var/run/cotcube/#{name}.pid"
  }

  config = defaults.merge(config)
  puts "CONFIG is '#{config}'" if debug

  config
end

#keystroke(quit: false) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/cotcube-helpers/input.rb', line 7

def keystroke(quit: false)
  begin
    # save previous state of stty
    old_state = `stty -g`
    # disable echoing and enable raw (not having to press enter)
    system 'stty raw -echo'
    c = $stdin.getc.chr rescue '_' # rubocop:disable Style/RescueModifier
    # gather next two characters of special keys
    if c == "\e"
      extra_thread = Thread.new do
        c += $stdin.getc.chr
        c += $stdin.getc.chr
      end
      # wait just long enough for special keys to get swallowed
      extra_thread.join(0.00001)
      # kill thread so not-so-long special keys don't wait on getc
      extra_thread.kill
    end
  rescue StandardError => e
    puts "#{e.class}: #{e.message}"
    puts e.backtrace
  ensure
    # restore previous state of stty
    system "stty #{old_state}"
  end
  c.each_byte do |x| # rubocop:disable Lint/UnreachableLoop
    case x
    when 3
      puts 'Strg-C captured, exiting...'
      quit ? exit : (return true)
    when 13
      return '_return_'
    when 27
      puts 'ESCAPE gathered'
      return '_esc_'
    else
      return c
    end
  end
end

#micros(config: init, **args) ⇒ Object



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/cotcube-helpers/symbols.rb', line 38

def micros(config: init, **args)
  if config[:micros_file].nil?
    MICRO_EXAMPLES
  else
    CSV
      .read(config[:micros_file], headers: SYMBOL_HEADERS)
      .map{|row| row.to_h }
      .map{|row|
        [ :ticksize, :power, :bcf ].each {|z| row[z] = row[z].to_f }
        row[:format] = "%#{row[:format]}f"
        row[:currency] ||= 'USD'
        row[:multiplier] = (row[:power] / row[:ticksize]).round(8)
        row
      }
      .reject{|row| row[:id].nil? }
      .tap{ |all|
        args.keys.each { |header|
         unless SYMBOL_HEADERS.include? header
            puts "WARNING in Cotcube::Helpers.micros: '#{header}' is not a valid symbol header. Skipping..."
            next
          end
          all.select!{|x| x[header] == args[header]} unless args[header].nil?
          return all.first if all.size == 1
        }
        return all
      }
  end
end

#parallelize(ary, processes: 1, threads: 1, progress: '', &block) ⇒ Object



7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# File 'lib/cotcube-helpers/parallelize.rb', line 7

def parallelize(ary, processes: 1, threads: 1, progress: '', &block)
  chunks = []
  if [0, 1].include? processes
    result = Parallel.map(ary, in_threads: threads, &block)
  elsif [0, 1].include? threads
    result = Parallel.map(ary, in_processes: processes, &block)
  else
    ary.each_slice(threads) { |chunk| chunks << chunk }
    result = if progress == ''
               Parallel.map(chunks, in_processes: processes) do |chunk|
                 Parallel.map(chunk, in_threads: threads, &block)
               end
             else
               Parallel.map(chunks, progress: progress, in_processes: processes) do |chunk|
                 Parallel.map(chunk, in_threads: threads, &block)
               end
             end
  end
  result
end

#reduce(bars:, to: nil, date_alike: :datetime, &block) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/cotcube-helpers/reduce.rb', line 6

def reduce(bars:, to: nil, date_alike: :datetime, &block)
  case to
  when :days
    terminators = %i[last daily beginning_of_day]
    block = proc { |c, b| c[:day] == b[:day] } unless block_given?
  when :hours
    terminators = %i[first hours beginning_of_hour]
    block = proc { |c, b| c[:day] == b[:day] and c[:datetime].hour == b[:datetime].hour } unless block_given?
  when :weeks
    terminators = %i[first weeks beginning_of_week]
    block = proc { |a, b| a[:datetime].to_datetime.cweek == b[:datetime].to_datetime.cweek } unless block_given?
  when :months
    terminators = %i[first months beginning_of_month]
    block = proc { |a, b| a[:datetime].to_datetime.month == b[:datetime].to_datetime.month } unless block_given?
  else
    raise ArgumentError, 'Currently supported are reductions to :hours, :days, :weeks, :months '
  end
  determine_date_alike = ->(ary) { ary.send(terminators.first)[date_alike].send(terminators.last) }
  make_new_bar = lambda do |ary, _date = nil|
    result = {
      contract: ary.first[:contract],
      symbol: ary.first[:symbol],
      datetime: determine_date_alike.call(ary),
      day: ary.first[:day],
      open: ary.first[:open],
      high: ary.map { |x| x[:high] }.max,
      low: ary.map { |x| x[:low] }.min,
      close: ary.last[:close],
      volume: ary.map { |x| x[:volume] }.reduce(:+),
      type: terminators[1]
    }
    result.map { |k, v| result.delete(k) if v.nil? }
    result
  end
  collector = []
  final     = []
  bars.each do |bar|
    if collector.empty? || block.call(collector.last, bar)
      collector << bar
    else
      new_bar = make_new_bar.call(collector)
      final << new_bar
      collector = [bar]
    end
  end
  new_bar = make_new_bar.call(collector)
  final << new_bar
  final
end

#simple_series_stats(base:, ind: nil, dim: 0, format: '% 6.2f', prefix: '', print: true, &block) ⇒ Object

if given a block, :ind of base is set by block.call() dim reduces the sample size by top n% and least n%, so dim of 0.5 would remove 100% of the sample

Raises:

  • (ArgumentError)


8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/cotcube-helpers/simple_series_stats.rb', line 8

def simple_series_stats(base:, ind: nil, dim: 0, format: '% 6.2f', prefix: '', print: true, &block)
  raise ArgumentError, 'Need :ind of type integer' if base.first.is_a?(Array) and not ind.is_a?(Integer)
  raise ArgumentError, 'Need :ind to evaluate base' if base.first.is_a?(Hash) and ind.nil?

  dim = dim.to_f if dim.is_a? Numeric
  dim = 0 if dim==false

  raise ArgumentError, 'Expecting 0 <= :dim < 0.5' unless dim.is_a?(Float) and dim >= 0  and dim < 0.5
  raise ArgumentError, 'Expecting arity of one if block given' if block_given? and not block.arity==1

  precision = format[-1] == 'f' ? format[..-2].split('.').last.to_i : 0
  worker = base.
    tap {|b| b.map{|x| x[ind] = block.call(x) } if block.is_a? Proc }.
    map {|x| ind.nil? ? x : x[ind] }.
    compact.
    sort
  unless dim.zero?
    reductor = (base.size * dim).round
    worker = worker[reductor..base.size - reductor]
  end
  result = {}

  result[:size]   =  worker.size
  result[:min]    =  worker.first
  result[:avg]    = (worker.reduce(:+) / result[:size]).round(precision+1)
  result[:lower]  =  worker[ (result[:size] * 1 / 4).round ]
  result[:median] =  worker[ (result[:size] * 2 / 4).round ]
  result[:upper]  =  worker[ (result[:size] * 3 / 4).round ]
  result[:max]    =  worker.last

  output = result.
    reject{|k,_| k == :size}.
    map{|k,v| { type: k, value: v, output: "#{k}: #{format(format, v)}".colorize(k==:avg ? :light_yellow : :white) } }.
    sort_by{|x| x[:value]}.
    map{|x| x[:output]}
  result[:output] = "#{format '%20s',(prefix.empty? ? '' : (prefix + ': '))}" + 
                    "[" +
                    " size: #{format '%6d', result[:size]} | ".light_white +
                    output.join(' |  ') +
                    " ]"

  puts result[:output] if print
  result
end

#sub(minimum: 1) ⇒ Object

sub (should be ‘subpattern’, but too long) is for use in case / when statements

it returns a lambda, that checks the case'd expression for matching subpattern
based on the the giving minimum. E.g. 'a', 'ab' .. 'abcd' will match sub(1){'abcd'}
but only 'abc' and 'abcd' will match sub(3){'abcd'}.:

The recommended use within evaluating user input, where abbreviation of incoming commands
is desirable (h for hoover and hyper, what will translate to sub(2){'hoover'} and sub(2){hyper})

To extend functionality even more, it is possible to send a group of patterns to, like
sub(2){[:hyper,:mega]}, what will respond truthy to "hy" and "meg" but not to "m" or "hypo"


17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/cotcube-helpers/subpattern.rb', line 17

def sub(minimum: 1)
  pattern = yield
  case pattern
  when String, Symbol, NilClass
    pattern = pattern.to_s
    lambda do |x|
      return false if x.nil? || (x.size < minimum)

      return ((pattern =~ /^#{Regexp.escape x}/i).nil? ? false : true)
    end
  when Array
    pattern.map do |x|
      unless [String, Symbol, NilClass].include? x.class
        raise TypeError, "Unsupported class '#{x.class}' for '#{x}' in pattern '#{pattern}'."
      end
    end
    lambda do |x|
      pattern.each do |sub|
        sub = sub.to_s
        return false if x.size < minimum

        result = ((sub =~ /^#{Regexp.escape x}/i).nil? ? false : true)
        return true if result
      end
      return false
    end
  else
    raise TypeError, "Unsupported class #{pattern.class} in Cotcube::Helpers::sub"
  end
end

#symbols(config: init, **args) ⇒ Object



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/cotcube-helpers/symbols.rb', line 9

def symbols(config: init, **args)
  if config[:symbols_file].nil?
    SYMBOL_EXAMPLES
  else
    CSV
      .read(config[:symbols_file], headers: SYMBOL_HEADERS)
      .map{|row| row.to_h }
      .map{|row|
        [ :ticksize, :power, :bcf ].each {|z| row[z] = row[z].to_f}
        row[:format] = "%#{row[:format]}f"
        row[:currency] ||= 'USD'
        row[:multiplier] = (row[:power] / row[:ticksize]).round(8)
        row
      }
      .reject{|row| row[:id].nil? }
      .tap{ |all|
        args.keys.each { |header|
          unless SYMBOL_HEADERS.include? header
            puts "WARNING in Cotcube::Helpers.symbols: '#{header}' is not a valid symbol header. Skipping..."
            next
          end
          all.select!{|x| x[header] == args[header]} unless args[header].nil?
          return all.first if all.size == 1
        }
        return all
      }
  end
end

#translate_ib_contract(contract) ⇒ Object



54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/cotcube-helpers/ib_contracts.rb', line 54

def translate_ib_contract(contract)
  short = contract.split(" ").size == 1
  sym_a = contract.split(short ? '' : ' ')
  year  = sym_a.pop.to_i + (short ? 20 : 0)
  if short and sym_a[-1].to_i > 0
    year = year - 20 + sym_a.pop.to_i * 10
  end
  month = short ? sym_a.pop : LETTERS[sym_a.pop]
  sym   = Cotcube::Helpers.symbols(internal: sym_a.join)[:symbol] rescue nil
  sym ||= Cotcube::Helpers.micros(internal: sym_a.join)[:symbol] rescue nil
  sym.nil? ? false : "#{sym}#{month}#{year}"
end

#update_ib_contracts(symbol: nil) ⇒ Object



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/cotcube-helpers/ib_contracts.rb', line 12

def update_ib_contracts(symbol: nil)
  begin
    client = DataClient.new
    (Cotcube::Helpers.symbols + Cotcube::Helpers.micros).each do |sym|

      # TODO: consider file location to be located in config
      file = "/etc/cotcube/ibsymbols/#{sym[:symbol]}.yml"

      # TODO: VI publishes weekly options which dont match, the 3 others need multiplier enabled to work
      next if %w[ DY TM SI VI ].include? sym[:symbol]
      next if symbol and sym[:symbol] != symbol
      begin
        if File.exist? file
          next if Time.now - File.mtime(file) < 5.days
          data = nil
          data = YAML.load(File.read(file))
        else
          data = {}
        end
        p file
        %w[ symbol sec_type exchange multiplier ticksize power internal ].each {|z| data.delete z}
        raw   = client.get_contracts(symbol: sym[:symbol])
        reply = JSON.parse(raw)['result']
        reply.each do |set|
          contract = translate_ib_contract set['local_symbol']
          data[contract] ||= set
        end
        keys = data.keys.sort_by{|z| z[2]}.sort_by{|z| z[-2..] }.select{|z| z[..1] == sym[:symbol] }
        data = data.slice(*keys)
        File.open(file, 'w'){|f| f.write(data.to_yaml) }
      rescue Exception => e
        puts e.full_message
        p sym
        binding.irb
      end
    end
  ensure 
    client.stop
    true
  end
end