Skip to content

Instantly share code, notes, and snippets.

@ep-wac
Created May 31, 2012 13:50
Show Gist options
  • Save ep-wac/2843533 to your computer and use it in GitHub Desktop.
Save ep-wac/2843533 to your computer and use it in GitHub Desktop.
Ramblings on exchange rates pertaining to the Money, Nordea and MoneyRails Gem

A money field supports/depends upon a currency and that introduces the question of what exchange_rate to use!

The simple answer is 1.00000 meaning that the currency gets 'traded' on par - which will satisfy the 'one currency' use case (commonly exchange_rates are listed with 5 decimals - but some currencies requires 7 or more decimals to allowing an accurate calculation, like the Afghani, currently traded against the USD at 0.0206954). One currency use cases could be set using a simple configuration option - like config.money_rails.one_currency_only = true

A somewhat more complex use case is the 'two currencies' application where all money fields are listed (and calculated) in a 'preferred' currency but has to be presented in a secondary currency as well. Some EU-countries have not implemented the Euro as their national currency, like Poland. Their national currency has an almost fixed exchange_rate towards the Euro - it sees fluctuations but only marginally. Money fields are rarely converted but even though, should display in two currencies. This use case will require an exchange rate (exchange quote to be exact) - which could be cached and used when displaying money fields in the alternate currency (a view helper could be provided an extra argument to display the amount using the alternate currency exchange rate).

Increasingly complex, the 'multilateral' use case will persist money in a number of different currencies - and each transaction will see a (slightly) different exchange rate. Amounts are converted using sell/buy rates - depending on whether the participating bank is selling the amount to the user or buying it from the user! The difference usually is neglectable - except when amounts get big - or the number of transactions are considerable (loosing half-a-cent two million times a day still sets you back 10K a day!!). This use case has two options - use a means exchange rate (which will usually be the case - eg. the nordea gem provides such a means exchange rate ) or go with both a sell and a buy rate. Both options requires at least one accurate exchange rate which adds yet another challenge: time

Digressing on the subject of time: money amounts are registered at time t1 (at exchange rate x1) but rarely are executed at that exact point in time. Hence the user will see the conversion taking place at some later point in time - say t2 - at rate x2, and once concluded, the amount will have transformed from, say euro's, to dollars with a margin (and usually some surcharge which we will have to leave for the developer to implement ways of handling). Businesses operating in multiple currencies solve this in one of two ways 1) have a number of accounts, one in each currency regularly being used or 2) hedges exposure to risks through either defensively insuring or aggressively trading currencies. Either way they will 'know' the exchange rate at the time (or within the hour) of registering money fields and the exchange_rates table will hold adjusted conversion rates.

On a totally different note, money fields persists amounts and a currency - but not the purpose :) This will inhibit the accuracy of the exchange rate - as banks sets different rates on commercial transactions versus cash buy/sell - but again, that will have to abstracted away IMHO

Summing up:

Supporting exchange rates above the obvious one currency and alternate currency use cases would require a configuration which by popular convention methodology could utilise the nordea gem entirely - it converts amounts to other currencies using a cached exchange rate as provided by Nordea, but configurable to allow for persisted exchange rates in an exchange_rateables:

exchange_rateable: (ID +) CURRENCY_ID + EXCHANGE_RATE + EXCHANGE_RATEABLE_ID + EXCHANGE_RATEABLE_TYPE + EXCHANGE_RATEABLE_FIELDNAME + CREATED_AT

This table would record the currency used at the time of registering the amount in some table/field and the exchange_rate used - it could be a cached exchange rate or the present rate - depending on the implementation -

Persisting the exchange_rates could be in this table:

exchange_rates: ID + CURRENCY_ID + MIDDLE_RATE + SELL_RATE + BUY_RATE + QUOTE_AT + CREATED_AT

Now, using this could look a little (the following is part pipe dream part pseudo code, and please excuse my mixing it all together in ways it should not, hoping that you get the general idea) like

# config/initializers/money_rails.rb
require 'nordea'
Money.default_bank =  Nordea::Bank.new
MoneyRails.setup do |config|
  config.one_currency = false
  config.persist_exchange_quotes = :table # :file | :cache 
  config.exchange_rate = :middle # :sell | :buy | :day_mean | :week_mean | :month_mean
end

# lib/money_rails.rb
require 'delayed_job'
require 'delayed_job_active_record'
require 'delayed_job_croned'			# non-existing CRON-like 'runner' - every so often
class MoneyRails

  def week_mean(wednesday=nil)
    wednesday ||= 1.week.ago.at_beginning_of_week  + 2.days
    rates = exchange_rates.where{ (quote_at >= wednesday - 2.days) & (quote_at <= wednesday + 2.days)  }
    rates.sum(exchange_rate) / rates.count
  end

  Cron.add_job('Money.default_bank.update_rates',[ '09:00:00','12:00:00','16:30:00']) if %w{ table cache file }.include? config.persist_exchange_quotes.to_s

end

# lib/nordea.rb
module Nordea
  class Bank

    # the default_bank #update_rates should be 'changed/implemented' to use the config.exchange_rate 
    def update_rates
    end

    def exchange_with_rate( to_currency, rate)
	  me=self
      rate = ExchangeRateable.where{ exchangeable_id == me.id & exchange_rateable_type == me.class.to_s }[0] || e_w_r(to_currency, rate) 
      rate = rate.exchange_rate if rate.class == ExchangeRateable
	  Money.new((from.cents * rate).round, to_currency)
    end

    def exchange_with_rate_locked( to_currency, rate )
      me = self
      rate = exchange_with_rate( to_currency, rate )
      ExchangeRateable.create( currency: to_currency, exchange_rate: rate, exchangeable_id: me.id, exchange_rateable_type = me.class.to_s )
      rate
    end

    private
      # exchange_with_rate should return a specific rate
      def e_w_r(to_currency, rate) 
        case rate.class
        when Symbol
          rates = ExchangeRate.joins(:currencies).where{ currencies.iso_code==to_currency }.order{ quote_at desc}
          return 1.0 if rates.empty?
          case rate
          when :sell
            rates[0].sell_rate
          end
        when String
          rate.to_f
        end
      end    

  end

end

# app/models/account.rb
class Account
  monetize :amount_cents
end

irb >> acc=Account.new( amount: 100 )
irb >> acc.amount.exchange_to("SEK")  						# use what_ever exchange_rate is 'in stock'
irb >> acc.amount.exchange_with_rate("SEK", :week_mean)		# use the :week_mean to convert the amount
irb >> acc.amount.exchange_with_rate("SEK", :sell)			# use the present or latest :sell rate to convert the amount
irb >> acc.amount.exchange_with_rate("SEK", 1.25416)		# use this exchange rate
irb >> acc.amount.exchange_with_rate_locked("SEK", :buy)	# use the present or latest :buy rate to convert the amount - and lock it for future use!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment