cotcube-bardata 0.1.2 → 0.1.7

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