Class: RVGP::Journal::Pricer

Inherits:
Object
  • Object
show all
Defined in:
lib/rvgp/journal/pricer.rb

Overview

This class takes a value, denominated in one commodity, and returns the equivalent value, in another commodity. This process is also known as price (or currency) exchange. The basis for exchanges are rates, affixed to a date. These exchange rates are expected to be provided in the same format that ledger and hledger use. Here’s an example: “‘

P 2020-01-01 USD 0.893179 EUR
P 2020-02-01 EUR 1.109275 USD
P 2020-03-01 USD 0.907082 EUR

“‘ It’s typical for these exchange rates to exist in a project’s journals/prices.db, but, the constructor to this class expects the contents of such a file, as a string. The conversion process is fairly smart, in that a specified rate, works ‘both ways’. Meaning that, a price query will resolve based on any stipulation of equivalence between commodities. And, the matter of which code is to the left, or right, of a ratio, is undifferentiated from the inverse arrangement. This behavior, and most all others in this class, mimics the way ledger works, wrt price conversion.

Defined Under Namespace

Classes: NoPriceError, Price

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(prices_content = nil, opts = {}) ⇒ Pricer

Create a Price exchanger, given a prices database

Parameters:

  • prices_content (String) (defaults to: nil)

    The contents of a prices.db file, defining the exchange rates.

  • opts (Hash) (defaults to: {})

    Optional features

Options Hash (opts):

  • before_price_add (Proc<Time,String,String>)

    This option calls the provided Proc with the parameters offered to #add. Mostly, this exists to solve a very specific bug that occurs under certain conditions, in projects where currencies are automatically converted by ledger. If you see the I18n.t(error.missing_entry_in_prices_db) message in your build log, scrolling by - you should almost certainly add that entry to your project’s prices.db. And this option is how that notice was fired.

    This option ‘addresses’ a pernicious bug that will likely affect you. And I don’t have an easy solution, as, I sort of blame ledger for this. The problem will manifest itself in the form of grids that output differently, depending on what grids were built in the process.

    So, If, say, we’re only building 2022 grids. But, a clean build would have built 2021 grids, before instigating the 2022 grid build - then, we would see different outputs in the 2022-only build.

    The reason for this, is that there doesn’t appear to be any way of accounting for all historical currency conversions in ledger’s output. The data coming out of ledger only includes currency conversions in the output date range. This will sometimes cause weird discrepencies in the totals between a 2021-2022 run, vs a 2022-only run.

    The only solution I could think of, at this time, was to burp on any occurence, where, a conversion, wasn’t already in the prices.db That way, an operator (you) can simply add the outputted burp, into the prices.db file. This will ensure consistency in all grids, regardless of the ranges you run them.

    NOTE: This feature is currently unimplemnted in hledger. And, I have no solution planned there at this time. Probably, that means you should only use ledger in your project, if you’re working with multiple currencies, and don’t want to rebuild your project from clean, every time you make non-trivial changes.

    If you have a better idea, or some other way to ensure consistency (A SystemValidation?)… PR’s welcome!



104
105
106
107
# File 'lib/rvgp/journal/pricer.rb', line 104

def initialize(prices_content = nil, opts = {})
  @prices_db = prices_content ? parse(prices_content) : {}
  @before_price_add = opts[:before_price_add] if opts[:before_price_add]
end

Instance Attribute Details

#prices_dbArray<Pricer::Price> (readonly)

A parsed representation of the prices file, based on what was passed in the constructor.

Returns:



24
25
26
# File 'lib/rvgp/journal/pricer.rb', line 24

def prices_db
  @prices_db
end

Instance Method Details

#add(time, from_alpha, to) ⇒ void

This method returns an undefined value.

Add a conversion rate to the database

Parameters:

  • time (Time)

    The time at which this rate was discovered

  • from_alpha (String)

    The three character alphabetic currency code, of the source currency

  • to (RVGP::Journal::Currency)

    A commodity, expressing the quantity and commodity, that one unit of :from_alpha converts to



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/rvgp/journal/pricer.rb', line 173

def add(time, from_alpha, to)
  lcurrency = RVGP::Journal::Currency.from_code_or_symbol from_alpha

  price = Price.new time.to_time,
                    lcurrency ? lcurrency.alphabetic_code : from_alpha,
                    to.alphabetic_code || to.code,
                    to

  key = price.to_key
  if @prices_db.key? key
    i = @prices_db[key].find_index { |p| p.at > price.at.to_time }

    # There's no need to add the price, if there's no difference between
    # what we're adding, and what would have been found, otherwise
    price_before_add = i ? @prices_db[key][i - 1] : @prices_db[key].last

    if price_before_add.amount != price.amount
      @before_price_add&.call time, from_alpha, to

      if i
        @prices_db[key].insert i, price
      else
        @prices_db[key] << price
      end
    end

  else
    @before_price_add&.call time, from_alpha, to
    @prices_db[key] = [price]
  end

  price
end

#convert(at, from_commodity, to_code_or_symbol) ⇒ RVGP::Journal::Commodity

Convert the provided commodity, to another commodity, based on the rate at a given time.

Parameters:

  • at (Time)

    The time at which you want to query for an exchange rate. The most-recently-availble and eligible entry, before this parameter, will be selected.

  • from_commodity (RVGP::Journal::Commodity)

    The commodity you wish to convert

  • to_code_or_symbol (String)

    The three character alphabetic currency code, or symbol, of the destination currency you wish to convert to.

Returns:



158
159
160
161
162
163
164
165
# File 'lib/rvgp/journal/pricer.rb', line 158

def convert(at, from_commodity, to_code_or_symbol)
  rate = price at, from_commodity.code, to_code_or_symbol

  RVGP::Journal::Commodity.from_symbol_and_amount(
    to_code_or_symbol,
    (from_commodity.quantity_as_bigdecimal * rate.quantity_as_bigdecimal).to_s('F')
  )
end

#price(at, from, to) ⇒ RVGP::Journal::Commodity

Retrieve an exchange rate, for a given commodity, to another commodity, at a given time.

Parameters:

  • at (Time)

    The time at which you want to query for an exchange rate. The most-recently-availble and eligible entry, before this parameter, will be selected.

  • from (String)

    The three character alphabetic currency code, of the source currency

  • to (String)

    The three character alphabetic currency code, of the destination currency

Returns:



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/rvgp/journal/pricer.rb', line 115

def price(at, from, to)
  no_price! at, from, to if prices_db.nil? || prices_db.empty?

  lcurrency = RVGP::Journal::Currency.from_code_or_symbol from
  from_alpha = lcurrency ? lcurrency.alphabetic_code : from

  rcurrency = RVGP::Journal::Currency.from_code_or_symbol to
  to_alpha = rcurrency ? rcurrency.alphabetic_code : to

  prices = prices_db[Price.to_key(from_alpha, to_alpha)]

  no_price! at, from, to unless prices && !prices.empty? && at >= prices.first.at

  price = nil

  1.upto(prices.length - 1) do |i|
    if prices[i].at > at
      price = prices[i - 1]
      break
    end
  end

  price = prices.last if price.nil? && prices.last.at <= at

  no_price! at, from, to unless price

  # OK, so we have the price record that applies. But, it may need to be
  # inverted.
  if price.lcode == from_alpha && price.amount.alphabetic_code == to_alpha
    price.amount
  else
    RVGP::Journal::Commodity.from_symbol_and_amount to,
                                                    (1 / price.amount.quantity_as_bigdecimal).round(17).to_s('F')
  end
end