Module: Timeprice::Compare Private

Defined in:
lib/timeprice/compare.rb,
lib/timeprice/compare/series.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.

Compare combines FX and inflation across two (currency, date) points.

CONVENTION (critical): convert at SOURCE date first, then inflate in destination currency. See README.md “Compare semantics” section.

This preserves purchasing-power equivalence in the destination economy. The naive alternative (inflate in source currency first, then convert at destination date) double-counts source-country inflation because nominal FX rates already absorb relative inflation between the two currencies.

If a future refactor flips the order, the regression test in spec/timeprice/compare_spec.rb will fail.

The supported public entry point is compare. Direct references will move to ‘Timeprice::Internal::Compare` in a future release.

Defined Under Namespace

Modules: Series

Class Method Summary collapse

Class Method Details

.forecast_hash(cpi_fwd:, converted:, source_cpi:) ⇒ 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.



164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/timeprice/compare.rb', line 164

def forecast_hash(cpi_fwd:, converted:, source_cpi:)
  {
    basis_kind: cpi_fwd.basis_kind,
    projection_method: cpi_fwd.projection_method,
    window_years: cpi_fwd.window_years,
    sigma_pct: cpi_fwd.sigma_pct,
    last_known_date: cpi_fwd.last_known_date,
    horizon_months: cpi_fwd.horizon_months,
    low: converted * (cpi_fwd.low / source_cpi),
    high: converted * (cpi_fwd.high / source_cpi),
    warnings: cpi_fwd.warnings,
  }
end

.future_target?(to_point, to_country) ⇒ 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.

Returns true when to_point.date is past the destination country’s last bundled CPI date.

Returns:

  • (Boolean)


134
135
136
137
138
139
# File 'lib/timeprice/compare.rb', line 134

def future_target?(to_point, to_country)
  data   = DataLoader.load_cpi(to_country)
  series = Forecast::CpiForecaster.pick_series(data)
  last   = series.keys.max_by { |k| Forecast::Cagr.parse(k) }
  Forecast::Cagr.parse(to_point.date.to_s) > Forecast::Cagr.parse(last)
end

.fx_only_result(amount:, from_point:, to_point:, to_country:, fx_result:) ⇒ 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.

Same-date branch: no time-elapsed inflation, so the FX leg alone is the answer. Builds a CompareResult with cpi_ratio=1.0.



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/timeprice/compare.rb', line 103

def fx_only_result(amount:, from_point:, to_point:, to_country:, fx_result:)
  CompareResult.new(
    amount: fx_result.amount,
    original_amount: amount.to_f,
    from_currency: from_point.currency,
    from_date: from_point.date.to_s,
    to_currency: to_point.currency,
    to_date: to_point.date.to_s,
    country: to_country,
    fx_rate: fx_result.rate,
    cpi_ratio: 1.0,
    converted_amount: fx_result.amount,
    granularity: fx_result.granularity,
    forecast: nil
  )
end

.measured_result(amount:, from_point:, to_point:, to_country:, fx_result:) ⇒ 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.



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/timeprice/compare.rb', line 77

def measured_result(amount:, from_point:, to_point:, to_country:, fx_result:)
  infl = Inflation.adjust(
    amount: fx_result.amount,
    from: from_point.date.to_s,
    to: to_point.date.to_s,
    country: to_country
  )

  CompareResult.new(
    amount: infl.amount,
    original_amount: amount.to_f,
    from_currency: from_point.currency,
    from_date: from_point.date.to_s,
    to_currency: to_point.currency,
    to_date: to_point.date.to_s,
    country: to_country,
    fx_rate: fx_result.rate,
    cpi_ratio: infl.to_index.to_f / infl.from_index,
    converted_amount: fx_result.amount,
    granularity: Granularity.merge(fx_result.granularity, infl.granularity),
    forecast: nil
  )
end

.resolve_points(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.

Coerce both points and resolve to_country.



121
122
123
124
125
126
127
128
129
130
# File 'lib/timeprice/compare.rb', line 121

def resolve_points(from, to)
  from_point = Point.coerce(from)
  to_point   = Point.coerce(to)
  fail UnsupportedCurrency, from_point.currency unless Supported.country_for_currency(from_point.currency)

  to_country = Supported.country_for_currency(to_point.currency)
  fail UnsupportedCurrency, to_point.currency unless to_country

  [from_point, to_point, to_country]
end

.run(amount:, from:, to:, forecast: false) ⇒ CompareResult

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.

Compare an amount across two (currency, date) points.

Parameters:

  • amount (Numeric)
  • from (Timeprice::Point, Array(String, String))

    source point; accepts a Point or a 2-tuple like ‘[“USD”, “2010”]` or `[“USD”, “2010-06”]`

  • to (Timeprice::Point, Array(String, String))

    destination point

Returns:

Raises:



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/timeprice/compare.rb', line 50

def run(amount:, from:, to:, forecast: false)
  from_point, to_point, to_country = resolve_points(from, to)

  if forecast && future_target?(to_point, to_country)
    return run_with_forecast(
      amount: amount, from_point: from_point, to_point: to_point, to_country: to_country
    )
  end

  fx_result = Exchange.convert(
    amount: amount, from: from_point.currency,
    to: to_point.currency, date: from_point.fx_anchor_date
  )

  if from_point.date == to_point.date
    return fx_only_result(
      amount: amount, from_point: from_point, to_point: to_point,
      to_country: to_country, fx_result: fx_result
    )
  end

  measured_result(
    amount: amount, from_point: from_point, to_point: to_point,
    to_country: to_country, fx_result: fx_result
  )
end

.run_with_forecast(amount:, from_point:, to_point:, to_country:) ⇒ 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.



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/timeprice/compare.rb', line 141

def run_with_forecast(amount:, from_point:, to_point:, to_country:)
  fx_result = Exchange.convert(
    amount: amount, from: from_point.currency,
    to: to_point.currency, date: from_point.fx_anchor_date
  )
  cpi_fwd          = Forecast::CpiForecaster.project(country: to_country, target: to_point.date.to_s)
  source_cpi_value = source_index(to_country, from_point.date.to_s)
  inflation_ratio  = cpi_fwd.value / source_cpi_value

  CompareResult.new(
    amount: fx_result.amount * inflation_ratio,
    original_amount: amount.to_f,
    from_currency: from_point.currency, from_date: from_point.date.to_s,
    to_currency: to_point.currency,     to_date: to_point.date.to_s,
    country: to_country,
    fx_rate: fx_result.rate,
    cpi_ratio: inflation_ratio,
    converted_amount: fx_result.amount,
    granularity: :forecast,
    forecast: forecast_hash(cpi_fwd: cpi_fwd, converted: fx_result.amount, source_cpi: source_cpi_value)
  )
end

.series_forObject

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.



116
117
118
# File 'lib/timeprice/compare/series.rb', line 116

def self.series_for(**)
  Series.for(**).compact
end

.source_index(country, date) ⇒ 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.

Resolve a measured CPI index for the source date (which must be in range).



179
180
181
# File 'lib/timeprice/compare.rb', line 179

def source_index(country, date)
  CpiLookup.new(DataLoader.load_cpi(country)).at(date).value.to_f
end