Module: Timeprice::Forecast::FxForecaster Private
- Defined in:
- lib/timeprice/forecast/fx_forecaster.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.
Project a currency-pair FX rate forward using the same trailing-CAGR mechanism as CpiForecaster, but with FX-appropriate defaults: 5-year window, 2-year horizon cap.
The annual series is reconstructed by reading each bundled year file and pulling the year-end (or annual-average where present) rate. The daily granularity that Exchange works with is too noisy to anchor a multi-year extrapolation — annualizing first is the whole point.
Constant Summary collapse
- DEFAULT_WINDOW_YEARS =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
5- HORIZON_CAP_YEARS =
This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.
2
Class Method Summary collapse
- .build_result(last_key:, last_value:, target:, horizon_months:, window_years:, stats:, warnings:) ⇒ Object private
-
.cross_avg(day_rates_list, from, to) ⇒ Object
private
Average the per-day cross rate (USD->to / USD->from) for dates where both currencies are present.
-
.cross_scalar(usd_from, usd_to) ⇒ Object
private
Compute cross rate from two USD->X scalars; nil if either is nil/zero.
-
.daily_avg(day_rates_list, currency) ⇒ Object
private
Average USD->
currencyvalues across all day_rates hashes. -
.invert_avg(day_rates_list, currency) ⇒ Object
private
Average USD->
currencythen invert to get currency->USD. -
.invert_scalar(val) ⇒ Object
private
Invert a single USD->X scalar to X->USD; nil if zero or nil.
-
.load_annual_series(from, to) ⇒ Object
private
Build an annual => rate series by reading each FX year file and computing a daily average for the requested currency pair.
- .months_between(from_key, to_key) ⇒ Object private
-
.pick_annual_fallback_rate(fallback, year, from, to) ⇒ Object
private
Extract an annual rate for
from->tofrom the _annual.json fallback. -
.pick_year_rate(payload, from, to) ⇒ Object
private
Compute an annual average rate for
from->tofrom a year-file payload. - .project(from:, to:, target:, window_years: DEFAULT_WINDOW_YEARS) ⇒ Object private
Class Method Details
.build_result(last_key:, last_value:, target:, horizon_months:, window_years:, stats:, warnings:) ⇒ 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.
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/timeprice/forecast/fx_forecaster.rb', line 110 def build_result(last_key:, last_value:, target:, horizon_months:, window_years:, stats:, warnings:) years_forward = horizon_months / 12.0 value = last_value * ((1.0 + stats[:cagr])**years_forward) low = last_value * ((1.0 + stats[:cagr] - stats[:sigma_yoy])**years_forward) high = last_value * ((1.0 + stats[:cagr] + stats[:sigma_yoy])**years_forward) Forecast::Result.new( value: value, low: low, high: high, projection_method: "cagr_trailing", window_years: window_years, sigma_pct: stats[:sigma_yoy], last_known_date: last_key, target_date: target, horizon_months: horizon_months, basis_kind: :fx, warnings: warnings.uniq ) end |
.cross_avg(day_rates_list, from, to) ⇒ 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.
Average the per-day cross rate (USD->to / USD->from) for dates where both currencies are present.
147 148 149 150 151 152 153 154 155 156 |
# File 'lib/timeprice/forecast/fx_forecaster.rb', line 147 def cross_avg(day_rates_list, from, to) pairs = day_rates_list.filter_map do |dr| usd_from = dr[from]&.to_f usd_to = dr[to]&.to_f cross_scalar(usd_from, usd_to) end return nil if pairs.empty? pairs.sum / pairs.size end |
.cross_scalar(usd_from, usd_to) ⇒ 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.
Compute cross rate from two USD->X scalars; nil if either is nil/zero.
166 167 168 169 170 |
# File 'lib/timeprice/forecast/fx_forecaster.rb', line 166 def cross_scalar(usd_from, usd_to) return nil unless usd_from&.positive? && usd_to usd_to / usd_from end |
.daily_avg(day_rates_list, currency) ⇒ 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.
Average USD->currency values across all day_rates hashes.
132 133 134 135 136 137 |
# File 'lib/timeprice/forecast/fx_forecaster.rb', line 132 def daily_avg(day_rates_list, currency) vals = day_rates_list.filter_map { |dr| dr[currency]&.to_f } return nil if vals.empty? vals.sum / vals.size end |
.invert_avg(day_rates_list, currency) ⇒ 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.
Average USD->currency then invert to get currency->USD.
140 141 142 143 |
# File 'lib/timeprice/forecast/fx_forecaster.rb', line 140 def invert_avg(day_rates_list, currency) mean = daily_avg(day_rates_list, currency) invert_scalar(mean) end |
.invert_scalar(val) ⇒ 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.
Invert a single USD->X scalar to X->USD; nil if zero or nil.
159 160 161 162 163 |
# File 'lib/timeprice/forecast/fx_forecaster.rb', line 159 def invert_scalar(val) return nil unless val&.positive? 1.0 / val end |
.load_annual_series(from, to) ⇒ 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.
Build an annual => rate series by reading each FX year file and computing a daily average for the requested currency pair. Falls back to the _annual.json file for years where the daily file does not cover the requested pair. Returns {} if no data is found.
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/timeprice/forecast/fx_forecaster.rb', line 54 def load_annual_series(from, to) root = File.join(DataLoader.data_root, "fx", "usd") return {} unless File.directory?(root) fallback = DataLoader.load_fx_annual_fallback years = Dir.children(root).filter_map do |f| Regexp.last_match(1).to_i if f =~ /\A(\d{4})\.json\z/ end.sort years.each_with_object({}) do |year, acc| rate = pick_year_rate(DataLoader.load_fx_year(year), from, to) rate ||= pick_annual_fallback_rate(fallback, year, from, to) if fallback acc[year.to_s] = rate if rate end end |
.months_between(from_key, to_key) ⇒ 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.
104 105 106 107 108 |
# File 'lib/timeprice/forecast/fx_forecaster.rb', line 104 def months_between(from_key, to_key) f = Cagr.parse(from_key) t = Cagr.parse(to_key) ((t.year - f.year) * 12) + (t.month - f.month) end |
.pick_annual_fallback_rate(fallback, year, from, to) ⇒ 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.
Extract an annual rate for from->to from the _annual.json fallback. Fallback stores USD->currency annual averages keyed by year string.
91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/timeprice/forecast/fx_forecaster.rb', line 91 def pick_annual_fallback_rate(fallback, year, from, to) ann = fallback.dig("annual", year.to_s) return nil unless ann.is_a?(Hash) if from == "USD" ann[to]&.to_f elsif to == "USD" invert_scalar(ann[from]&.to_f) else cross_scalar(ann[from]&.to_f, ann[to]&.to_f) end end |
.pick_year_rate(payload, from, to) ⇒ 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.
Compute an annual average rate for from->to from a year-file payload. Year-file rates are stored as USD->currency daily rates.
from == "USD" => average rates[date][to] across all dates
to == "USD" => average rates[date][from], then invert
else (cross) => per-date to/from ratio, then average
76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/timeprice/forecast/fx_forecaster.rb', line 76 def pick_year_rate(payload, from, to) daily = payload["rates"] return nil unless daily.is_a?(Hash) && !daily.empty? if from == "USD" daily_avg(daily.values, to) elsif to == "USD" invert_avg(daily.values, from) else cross_avg(daily.values, from, to) end end |
.project(from:, to:, target:, window_years: DEFAULT_WINDOW_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.
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
# File 'lib/timeprice/forecast/fx_forecaster.rb', line 26 def project(from:, to:, target:, window_years: DEFAULT_WINDOW_YEARS) from = from.to_s.upcase to = to.to_s.upcase series = load_annual_series(from, to) fail DataNotFound, "no FX series for #{from}->#{to}" if series.empty? last_key = series.keys.max_by { |k| Cagr.parse(k) } last_value = series[last_key].to_f horizon_months = months_between(last_key, target) earliest_year = series.keys.map { |k| Cagr.parse(k).year }.min warnings = [] warnings << "insufficient_window" if Cagr.parse(last_key).year - window_years < earliest_year warnings << "horizon_exceeds_cap" if horizon_months > HORIZON_CAP_YEARS * 12 stats = Cagr.compute(series: series, last_date: last_key, window_years: window_years) build_result( last_key: last_key, last_value: last_value, target: target, horizon_months: horizon_months, window_years: window_years, stats: stats, warnings: warnings ) end |