Module: Timeprice::Forecast::Cagr Private
- Defined in:
- lib/timeprice/forecast/cagr.rb
Overview
This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.
Pure math: trailing CAGR and σ of year-over-year changes.
The series is a hash mapping date strings (‘“YYYY”` or `“YYYY-MM”`) to numeric values. The trailing window is anchored on `last_date` and extends `window_years` backward. CAGR is the annualized geometric return between the first and last samples in the window. Sigma is the sample stdev of 1-year-spaced returns within the window.
No I/O, no DataLoader. Pure function — call from anywhere.
Class Method Summary collapse
- .annualised_return(sorted) ⇒ Object private
-
.compute(series:, last_date:, window_years:) ⇒ Hash
private
{ cagr: Float, sigma_yoy: Float, window_start: String, window_end: String, samples: Integer }.
- .parse(s) ⇒ Object private
- .shift_years(date, years) ⇒ Object private
-
.stdev_of_yoy(sorted) ⇒ Object
private
Stdev of simple (arithmetic) 1-year-spaced returns within the window.
- .within?(key, start_date, end_date) ⇒ Boolean private
Class Method Details
.annualised_return(sorted) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/timeprice/forecast/cagr.rb', line 47 def annualised_return(sorted) first_v = sorted.first.last.to_f last_v = sorted.last.last.to_f fail ArgumentError, "first window value must be positive (got #{first_v})" unless first_v.positive? fail ArgumentError, "last window value must be positive (got #{last_v})" unless last_v.positive? years_elapsed = (parse(sorted.last.first) - parse(sorted.first.first)) / 365.2425 fail ArgumentError, "window has zero elapsed time" unless years_elapsed.positive? ((last_v / first_v)**(1.0 / years_elapsed)) - 1.0 end |
.compute(series:, last_date:, window_years:) ⇒ Hash
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns { cagr: Float, sigma_yoy: Float, window_start: String, window_end: String, samples: Integer }.
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# File 'lib/timeprice/forecast/cagr.rb', line 26 def compute(series:, last_date:, window_years:) end_date = parse(last_date) start_date = shift_years(end_date, -window_years) sorted = series .select { |k, _| within?(k, start_date, end_date) } .sort_by { |k, _| parse(k) } fail ArgumentError, "need at least 2 points in window" if sorted.size < 2 cagr = annualised_return(sorted) { cagr: cagr, sigma_yoy: stdev_of_yoy(sorted), window_start: sorted.first.first, window_end: sorted.last.first, samples: sorted.size, } end |
.parse(s) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
60 61 62 63 64 65 66 |
# File 'lib/timeprice/forecast/cagr.rb', line 60 def parse(s) s = s.to_s return ::Date.new(s.to_i, 1, 1) if s.length == 4 y, m = s.split("-").map(&:to_i) ::Date.new(y, m, 1) end |
.shift_years(date, years) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
68 69 70 |
# File 'lib/timeprice/forecast/cagr.rb', line 68 def shift_years(date, years) ::Date.new(date.year + years, date.month, 1) end |
.stdev_of_yoy(sorted) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Stdev of simple (arithmetic) 1-year-spaced returns within the window. Returns 0.0 when fewer than 2 paired samples exist.
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/timeprice/forecast/cagr.rb', line 79 def stdev_of_yoy(sorted) by_date = sorted.to_h returns = sorted.filter_map do |key, value| prior_key = shift_years(parse(key), -1).strftime(key.length == 4 ? "%Y" : "%Y-%m") prior = by_date[prior_key] next unless prior&.positive? (value.to_f / prior) - 1.0 end return 0.0 if returns.size < 2 mean = returns.sum / returns.size variance = returns.sum { |r| (r - mean)**2 } / (returns.size - 1) Math.sqrt(variance) end |
.within?(key, start_date, end_date) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
72 73 74 75 |
# File 'lib/timeprice/forecast/cagr.rb', line 72 def within?(key, start_date, end_date) d = parse(key) d.between?(start_date, end_date) end |