cotcube-bardata 0.1.2 → 0.1.7

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.
@@ -1,80 +1,77 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cotcube
4
+ # Missing top level documentation comment
4
5
  module Bardata
5
-
6
-
7
- def symbols(config: init)
6
+ def symbols(config: init, type: nil, symbol: nil)
8
7
  if config[:symbols_file].nil?
9
8
  SYMBOL_EXAMPLES
10
9
  else
11
10
  CSV
12
- .read(config[:symbols_file], headers: %i{ id symbol ticksize power months type bcf reports name})
13
- .map{|row| row.to_h }
14
- .map{|row| [ :ticksize, :power, :bcf ].each {|z| row[z] = row[z].to_f}; row }
15
- .reject{|row| row[:id].nil? }
11
+ .read(config[:symbols_file], headers: %i[id symbol ticksize power months type bcf reports name])
12
+ .map(&:to_h)
13
+ .map { |row| %i[ticksize power bcf].each { |z| row[z] = row[z].to_f }; row } # rubocop:disable Style/Semicolon
14
+ .reject { |row| row[:id].nil? }
15
+ .tap { |all| all.select! { |x| x[:type] == type } unless type.nil? }
16
+ .tap { |all| all.select! { |x| x[:symbol] == symbol } unless symbol.nil? }
16
17
  end
17
18
  end
18
19
 
19
20
  def config_prefix
20
- os = Gem::Platform.local.os
21
+ os = Gem::Platform.local.os
21
22
  case os
22
23
  when 'linux'
23
24
  ''
24
25
  when 'freebsd'
25
26
  '/usr/local'
26
27
  else
27
- raise RuntimeError, 'unknown architecture'
28
+ raise 'unknown architecture'
28
29
  end
29
30
  end
30
31
 
31
32
  def config_path
32
- config_prefix + '/etc/cotcube'
33
+ "#{config_prefix}/etc/cotcube"
33
34
  end
34
35
 
35
- def init(config_file_name: 'bardata.yml', debug: false)
36
+ def init(config_file_name: 'bardata.yml')
36
37
  name = 'bardata'
37
38
  config_file = config_path + "/#{config_file_name}"
38
39
 
39
- if File.exist?(config_file)
40
- config = YAML.load(File.read config_file).transform_keys(&:to_sym)
41
- else
42
- config = {}
43
- end
40
+ config = if File.exist?(config_file)
41
+ YAML.safe_load(File.read(config_file)).transform_keys(&:to_sym)
42
+ else
43
+ {}
44
+ end
44
45
 
45
- defaults = {
46
- data_path: config_prefix + '/var/cotcube/' + name,
46
+ defaults = {
47
+ data_path: "#{config_prefix}/var/cotcube/#{name}"
47
48
  }
48
49
 
49
-
50
50
  config = defaults.merge(config)
51
51
 
52
-
53
52
  # part 2 of init process: Prepare directories
54
53
 
55
54
  save_create_directory = lambda do |directory_name|
56
55
  unless Dir.exist?(directory_name)
57
56
  begin
58
57
  `mkdir -p #{directory_name}`
59
- unless $?.exitstatus.zero?
58
+ unless $CHILD_STATUS.exitstatus.zero?
60
59
  puts "Missing permissions to create or access '#{directory_name}', please clarify manually"
61
60
  exit 1 unless defined?(IRB)
62
61
  end
63
- rescue
62
+ rescue StandardError
64
63
  puts "Missing permissions to create or access '#{directory_name}', please clarify manually"
65
64
  exit 1 unless defined?(IRB)
66
65
  end
67
66
  end
68
67
  end
69
- ['',:daily,:quarters].each do |path|
70
- dir = "#{config[:data_path]}#{path == '' ? '' : '/'}#{path.to_s}"
68
+ ['', :daily, :quarters, :eods, :trading_hours, :cached].each do |path|
69
+ dir = "#{config[:data_path]}#{path == '' ? '' : '/'}#{path}"
71
70
  save_create_directory.call(dir)
72
71
  end
73
72
 
74
73
  # eventually return config
75
74
  config
76
75
  end
77
-
78
76
  end
79
77
  end
80
-
@@ -1,23 +1,48 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cotcube
4
+ # Missing top level documentation comment
4
5
  module Bardata
6
+ def provide(contract:, # rubocop:disable Metrics/ParameterLists
7
+ # Can be like ("2020-12-01 12:00"..."2020-12-14 11:00")
8
+ range: nil,
9
+ symbol: nil, id: nil,
10
+ config: init,
11
+ # supported types are :quarters, :hours, :days, :rth, :dailies, :weeks, :months
12
+ interval: :days,
13
+ # supported filters are :full and :rth (and custom named, if provided as file)
14
+ filter: :full,
15
+ # TODO: for future compatibility and suggestion: planning to include a function to update
16
+ # with live data from broker
17
+ force_recent: false)
5
18
 
6
- def provide(symbol: nil, id: nil, contract:, config: init, date: Date.today - 1, type: nil, fill: :none)
7
- case type
8
- when :eod, :eods
9
- provide_eods(symbol: symbol, id: id, contract: contract, date: date)
10
- when :quarters
11
- print :quarters
12
- when :hours
13
- print :hours
14
- when :daily, :dailies
15
- print :dailies
19
+ sym = get_id_set(symbol: symbol, id: id, contract: contract, config: config)
20
+
21
+ case interval
22
+ when :quarters, :hours, :quarter, :hour
23
+ base = provide_quarters(contract: contract, symbol: symbol, id: id, config: config)
24
+ base = extended_select_for_range(range: range, base: base) if range
25
+ requested_set = trading_hours(symbol: sym[:symbol], filter: filter)
26
+
27
+ base = base.select_within(ranges: requested_set, attr: :datetime) { |x| x.to_datetime.to_sssm }
28
+ return base if %i[quarters quarter].include? interval
29
+
30
+ Cotcube::Helpers.reduce(bars: base, to: :hours) do |c, b|
31
+ c[:day] == b[:day] and c[:datetime].hour == b[:datetime].hour
32
+ end
33
+
34
+ when :days, :weeks, :months
35
+ base = provide_cached contract: contract, symbol: symbol, id: id, config: config, filter: filter,
36
+ range: range, force_recent: force_recent
37
+ return base if %i[day days].include? interval
38
+
39
+ # TODO: Missing implementation to reduce cached days to weeks or months
40
+ raise 'Missing implementation to reduce cached days to weeks or months'
41
+ when :dailies, :daily
42
+ provide_daily contract: contract, symbol: symbol, id: id, config: config, range: range
16
43
  else
17
- puts "WARNING: Using provide without :type is for legacy support pointing to .provide_daily".light_yellow
18
- provide_daily(symbol: symbol, id: id, contract: contract, config: config)
44
+ raise ArgumentError, "Unsupported or unknown interval '#{interval}' in Bardata.provide"
19
45
  end
20
46
  end
21
47
  end
22
-
23
48
  end
@@ -1,80 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cotcube
4
+ # Missing top level documentation
4
5
  module Bardata
6
+ # the following method loads the quarterly bars (15-min bars) from the directory tree
7
+ # also note that a former version of this method allowed to provide range or date parameters. this has been moved
8
+ # to #provide itself.
9
+ def provide_quarters(contract:, # rubocop:disable Metrics/ParameterLists
10
+ symbol: nil, id: nil,
11
+ timezone: Time.find_zone('America/Chicago'),
12
+ config: init,
13
+ keep_marker: false)
5
14
 
6
- def provide_quarters(
7
- symbol: nil, id: nil,
8
- contract:,
9
- as: :quarters,
10
- range: nil, date: nil,
11
- timezone: Time.find_zone('America/Chicago'),
12
- config: init,
13
- quiet: false
14
- )
15
- date = timezone.parse(date) if date.is_a? String
16
- raise ArgumentError, ":range and :date are mutually exclusive" if range and date
17
- raise ArgumentError, "Contract '#{contract}' is bogus, should be like 'M21' or 'ESM21'" unless contract.is_a? String and [3,5].include? contract.size
18
- if contract.to_s.size == 5
19
- symbol = contract[0..1]
20
- contract = contract[2..4]
15
+ unless contract.is_a?(String) && [3, 5].include?(contract.size)
16
+ raise ArgumentError, "Contract '#{contract}' is bogus, should be like 'M21' or 'ESM21'"
21
17
  end
22
- unless symbol.nil?
23
- symbol_id = symbols.select{|s| s[:symbol] == symbol.to_s.upcase}.first[:id]
24
- raise ArgumentError, "Could not find match in #{config[:symbols_file]} for given symbol #{symbol}" if symbol_id.nil?
25
- raise ArgumentError, "Mismatching symbol #{symbol} and given id #{id}" if not id.nil? and symbol_id != id
26
- id = symbol_id
27
- end
28
- raise ArgumentError, ":as can only be in [:quarters, :hours, :days]" unless %i[quarters hours days].include?(as)
29
- raise ArgumentError, "Could not guess :id or :symbol from 'contract: #{contract}', please clarify." if id.nil?
18
+
19
+ sym = get_id_set(symbol: symbol, id: id, contract: contract)
20
+
21
+ contract = contract[2..4] if contract.to_s.size == 5
22
+ id = sym[:id]
23
+ symbol = sym[:symbol]
24
+
30
25
  id_path = "#{config[:data_path]}/quarters/#{id}"
31
26
  data_file = "#{id_path}/#{contract}.csv"
32
- raise RuntimeError, "No data found for requested :id (#{id_path} does not exist)" unless Dir.exist?(id_path)
33
- raise RuntimeError, "No data found for requested contract #{symbol}:#{contract} in #{id_path}." unless File.exist?(data_file)
34
- data = CSV.read(data_file, headers: %i[contract datetime day open high low close volume]).map do |row|
27
+ raise "No data found for requested :id (#{id_path} does not exist)" unless Dir.exist?(id_path)
28
+
29
+ raise "No data found for requested contract #{symbol}:#{contract} in #{id_path}." unless File.exist?(data_file)
30
+
31
+ data = CSV.read(data_file, headers: %i[contract datetime day open high low close volume]).map do |row|
35
32
  row = row.to_h
36
- %i[open high low close].map{|x| row[x] = row[x].to_f}
37
- %i[volume day].map{|x| row[x] = row[x].to_i}
33
+ %i[open high low close].map { |x| row[x] = row[x].to_f }
34
+ %i[volume day].map { |x| row[x] = row[x].to_i }
38
35
  row[:datetime] = timezone.parse(row[:datetime])
36
+ row[:type] = :quarter
39
37
  row
40
38
  end
41
- select_specific_date = lambda do |specific_date|
42
- data.select{|d| d[:day] == specific_date.day and specific_date.year == d[:datetime].year and (
43
- if specific_date.day > 1
44
- specific_date.month == d[:datetime].month
45
- else
46
- ((specific_date.month == d[:datetime].month and d[:datetime].day == 1) or
47
- (specific_date.month == d[:datetime].month + 1 and d[:datetime].day > 25) )
48
- end
49
- )}
50
- end
51
- if range
52
- starting = range.begin
53
- starting = timezone.parse(starting) if starting.is_a? String
54
- ending = range.end
55
- ending = timezone.parse( ending) if ending.is_a? String
56
- if starting.hour == 0 and starting.min == 0 and ending.hour == 0 and ending.min == 0
57
- puts "WARNING: When sending midnight, full trading day is assumed (starting 5 pm yesterday, ending 4 pm today)".light_yellow unless quiet
58
- result = select_specific_date.call(starting)
59
- result += data.select{|d| d[:datetime] > starting and d[:datetime] < ending.to_date }
60
- result += select_specific_date.call(ending)
61
- result.uniq!
62
- else
63
- result = data.select{|x| x[:datetime] >= starting and x[:datetime] < ending }
64
- end
65
- elsif date
66
- result = select_specific_date.call(date)
67
- else
68
- result = data
69
- end
70
- return case as
71
- when :hours
72
- Cotcube::Helpers.reduce(bars: result, to: 1.hour){|c,b| c[:day] == b[:day] and c[:datetime].hour == b[:datetime].hour }
73
- when :days
74
- Cotcube::Helpers.reduce(bars: result, to: 1.day ){|c,b| c[:day] == b[:day] }
75
- else
76
- result
77
- end
39
+ data.pop if data.last[:high].zero? && (not keep_marker)
40
+ data
78
41
  end
79
42
  end
80
43
  end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cotcube
4
+ # Missing top level documentation
5
+ module Bardata
6
+ # this is an analysis tool to investigate actual ranges of an underlying symbol
7
+ # it is in particular no true range or average true range, as a 'true range' can only be applied to
8
+ # a steady series, what changing contracts definitely aren't
9
+ #
10
+ # The result printed / returned is a table, containing a matrix of rows:
11
+ # 1. size: the amount of values evaluated
12
+ # 2. avg:
13
+ # 3. lower: like median, but not at 1/2 but 1/4
14
+ # 4. median:
15
+ # 5. upper: like median, bot not at 1/2 but 3/4
16
+ # 6. max:
17
+ # and columns:
18
+ # 1.a) all days os the series
19
+ # 1.b) all days of the series, diminished by 2* :dim*100% extreme values (i.e. at both ends)
20
+ # 1.c) the last 200 days
21
+ # 2.a-c) same with days reduced to weeks (c: 52 weeks)
22
+ # 3.a-c) same with days reduced to months (c: 12 months)
23
+ def range_matrix(symbol: nil, id: nil, print: false, dim: 0.05)
24
+ # rubocop:disable Style/MultilineBlockChain
25
+ sym = get_id_set(symbol: symbol, id: id)
26
+ source = {}
27
+ target = {}
28
+ source[:days] = Cotcube::Bardata.continuous_actual_ml symbol: symbol
29
+ source[:weeks] = Cotcube::Helpers.reduce bars: source[:days], to: :weeks
30
+ source[:months] = Cotcube::Helpers.reduce bars: source[:days], to: :months
31
+
32
+ %i[days weeks months].each do |period|
33
+ source[period].map! do |x|
34
+ x[:range] = ((x[:high] - x[:low]) / sym[:ticksize]).round
35
+ x
36
+ end
37
+ target[period] = {}
38
+ target[period][:all_size] = source[period].size
39
+ target[period][:all_avg] = (source[period].map { |x| x[:range] }.reduce(:+) / source[period].size).round
40
+ target[period][:all_lower] = source[period].sort_by do |x|
41
+ x[:range]
42
+ end.map { |x| x[:range] }[ (source[period].size * 1 / 4).round ]
43
+ target[period][:all_median] = source[period].sort_by do |x|
44
+ x[:range]
45
+ end.map { |x| x[:range] }[ (source[period].size * 2 / 4).round ]
46
+ target[period][:all_upper] = source[period].sort_by do |x|
47
+ x[:range]
48
+ end.map { |x| x[:range] }[ (source[period].size * 3 / 4).round ]
49
+ target[period][:all_max] = source[period].map { |x| x[:range] }.max
50
+ target[period][:all_records] = source[period].sort_by do |x|
51
+ -x[:range]
52
+ end.map { |x| { contract: x[:contract], range: x[:range] } }.take(5)
53
+
54
+ tenth = (source[period].size * dim).round
55
+ custom = source[period].sort_by { |x| x[:range] }[tenth..source[period].size - tenth]
56
+ target[period][:dim_size] = custom.size
57
+ target[period][:dim_avg] = (custom.map { |x| x[:range] }.reduce(:+) / custom.size).round
58
+ target[period][:dim_lower] = custom.sort_by do |x|
59
+ x[:range]
60
+ end.map { |x| x[:range] }[ (custom.size * 1 / 4).round ]
61
+ target[period][:dim_median] = custom.sort_by do |x|
62
+ x[:range]
63
+ end.map { |x| x[:range] }[ (custom.size * 2 / 4).round ]
64
+ target[period][:dim_upper] = custom.sort_by do |x|
65
+ x[:range]
66
+ end.map { |x| x[:range] }[ (custom.size * 3 / 4).round ]
67
+ target[period][:dim_max] = custom.map { |x| x[:range] }.max
68
+ target[period][:dim_records] = custom.sort_by do |x|
69
+ -x[:range]
70
+ end.map { |x| { contract: x[:contract], range: x[:range] } }.take(5)
71
+
72
+ range = case period
73
+ when :months
74
+ -13..-2
75
+ when :weeks
76
+ -53..-2
77
+ when :days
78
+ -200..-1
79
+ else
80
+ raise ArgumentError, "Unsupported period: '#{period}'"
81
+ end
82
+ custom = source[period][range]
83
+ target[period][:rec_size] = custom.size
84
+ target[period][:rec_avg] = (custom.map { |x| x[:range] }.reduce(:+) / custom.size).round
85
+ target[period][:rec_lower] = custom.sort_by do |x|
86
+ x[:range]
87
+ end.map { |x| x[:range] }[ (custom.size * 1 / 4).round ]
88
+ target[period][:rec_median] = custom.sort_by do |x|
89
+ x[:range]
90
+ end.map { |x| x[:range] }[ (custom.size * 2 / 4).round ]
91
+ target[period][:rec_upper] = custom.sort_by do |x|
92
+ x[:range]
93
+ end.map { |x| x[:range] }[ (custom.size * 3 / 4).round ]
94
+ target[period][:rec_max] = custom.map { |x| x[:range] }.max
95
+ target[period][:rec_records] = custom.sort_by do |x|
96
+ -x[:range]
97
+ end.map { |x| { contract: x[:contract], range: x[:range] } }.take(5)
98
+ end
99
+
100
+ if print
101
+ %w[size avg lower median upper max].each do |a|
102
+ print "#{'%10s' % a} | " # rubocop:disable Style/FormatString
103
+ %i[days weeks months].each do |b|
104
+ %w[all dim rec].each do |c|
105
+ print ('%8d' % target[b]["#{c}_#{a}".to_sym]).to_s # rubocop:disable Style/FormatString
106
+ end
107
+ print ' | '
108
+ end
109
+ puts ''
110
+ end
111
+ end
112
+
113
+ target
114
+ # rubocop:enable Style/MultilineBlockChain
115
+ end
116
+ end
117
+ end
@@ -1,14 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cotcube
4
+ # missing top level documentation
4
5
  module Bardata
5
-
6
- # fetching official tradedates from CME
6
+ # fetching official trade dates from CME
7
+ # it returns the current trade date or, if today isn't a trading day, the last trade date.
7
8
  def last_trade_date
8
- uri = "https://www.cmegroup.com/CmeWS/mvc/Volume/TradeDates?exchange=CME"
9
- res = nil
10
- res = HTTParty.get(uri).parsed_response
11
- res.map{|x| a = x["tradeDate"].chars.each_slice(2).map(&:join); "#{a[0]}#{a[1]}-#{a[2]}-#{a[3]}"}.first
9
+ uri = 'https://www.cmegroup.com/CmeWS/mvc/Volume/TradeDates?exchange=CME'
10
+ begin
11
+ HTTParty.get(uri)
12
+ .parsed_response
13
+ .map do |x|
14
+ a = x['tradeDate'].chars.each_slice(2).map(&:join)
15
+ "#{a[0]}#{a[1]}-#{a[2]}-#{a[3]}"
16
+ end
17
+ .first
18
+ rescue StandardError
19
+ nil
20
+ end
21
+ end
22
+
23
+ def holidays(config: init)
24
+ CSV.read("#{config[:data_path]}/holidays.csv").map{|x| DateTime.parse(x[0])}
12
25
  end
13
26
 
14
27
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cotcube
4
+ # Missing top level comment
5
+ module Bardata
6
+ # returns an Array of ranges containing a week of trading hours, specified by seconds since monday morning
7
+ # (as sunday is wday:0)
8
+ # according files are located in config[:data_path]/trading_hours and picked either
9
+ # by the symbol itself or by the assigned type
10
+ # commonly there are two filter for each symbol: :full and :rth, exceptions are e.g. meats
11
+ def trading_hours(symbol: nil, id: nil, # rubocop:disable Metrics/ParameterLists
12
+ filter: ,
13
+ force_filter: false, # with force_filter one would avoid falling back
14
+ # to the contract_type based range set
15
+ config: init, debug: false)
16
+ return (0...24 * 7 * 3600) if filter.to_s =~ /24x7/
17
+
18
+ prepare = lambda do |f|
19
+ CSV.read(f, converters: :numeric)
20
+ .map(&:to_a)
21
+ .tap { |x| x.shift unless x.first.first.is_a?(Numeric) }
22
+ .map { |x| (x.first...x.last) }
23
+ end
24
+
25
+ sym = get_id_set(symbol: symbol, id: id)
26
+
27
+ file = "#{config[:data_path]}/trading_hours/#{sym[:symbol]}_#{filter}.csv"
28
+ puts "Trying to use #{file} for #{symbol} + #{filter}" if debug
29
+ return prepare.call(file) if File.exist? file
30
+
31
+ file = "#{config[:data_path]}/trading_hours/#{sym[:symbol]}_full.csv"
32
+ puts "Failed. Trying to use #{file} now" if debug
33
+ return prepare.call(file) if File.exist?(file) && (not force_filter)
34
+
35
+ file = "#{config[:data_path]}/trading_hours/#{sym[:type]}_#{filter}.csv"
36
+ puts "Failed. Trying to use #{file} now." if debug
37
+ return prepare.call(file) if File.exist? file
38
+
39
+ file = "#{config[:data_path]}/trading_hours/#{sym[:type]}_full.csv"
40
+ puts "Failed. Trying to use #{file} now." if debug
41
+ return prepare.call(file) if File.exist?(file) && (not force_filter)
42
+
43
+ puts "Finally failed to find range filter for #{symbol} + #{filter}, returning 24x7".colorize(:light_yellow)
44
+ (0...24 * 7 * 3600)
45
+ end
46
+ end
47
+ end