Currency Utilities¶
Overview¶
Hordak features multi currency support. Each account in Hordak can support one or more currencies. Hordak does provide currency conversion functionality, but should be as part of the display logic only. It is also a good idea to make it clear to users that you are showing converted values.
The preference for Hordak internals is to always store & process values in the intended currency. This is because currency conversion is an inherently lossy process. Exchange rates vary over time, and rounding errors mean that currency conversions are not reversible without data loss (e.g. ¥176.51 -> $1.54 -> ¥176.20).
Classes¶
Money
instances:
The
Money
class is provided by moneyd and combines both an amount and a currency into a single value. Hordak uses these these as the core unit of monetary value.
Balance
instances (see below for more details):
An account can hold multiple currencies, and a Balance instance is how we represent this.
A Balance may contain one or more
Money
objects. There will be precisely oneMoney
object for each currency which the account holds.Balance objects may be added, subtracted etc. This will produce a new Balance object containing a union of all the currencies involved in the calculation, even where the result was zero.
Accounts with
is_bank_account=True
may only support a single currency.
Caching¶
Currency conversion makes use of Django’s cache. It is therefore recommended that you setup your Django cache to something other than the default in-memory store.
Currency Exchange¶
The currency_exchange()
helper function is provided to assist in creating
currency conversion Transactions.
- hordak.utilities.currency.currency_exchange(source, source_amount, destination, destination_amount, trading_account, fee_destination=None, fee_amount=None, date=None, description=None)¶
Exchange funds from one currency to another
Use this method to represent a real world currency transfer. Note this process doesn’t care about exchange rates, only about the value of currency going in and out of the transaction.
You can also record any exchange fees by syphoning off funds to
fee_account
of amountfee_amount
.Examples
For example, imagine our Canadian bank has obligingly transferred 120 CAD into our US bank account. We sent CAD 120, and received USD 100. We were also charged 1.50 CAD in fees.
We can represent this exchange in Hordak as follows:
from hordak.utilities.currency import currency_exchange currency_exchange( # Source account and amount source=cad_cash, source_amount=Money(120, 'CAD'), # Destination account and amount destination=usd_cash, destination_amount=Money(100, 'USD'), # Trading account the exchange will be done through trading_account=trading, # We also incur some fees fee_destination=banking_fees, fee_amount=Money(1.50, 'CAD') )
We should now find that:
cad_cash.get_balance()
has decreased byCAD 120
usd_cash.get_balance()
has increased byUSD 100
banking_fees.get_balance()
isCAD 1.50
trading_account.get_balance()
isUSD 100, CAD -120
You can perform
trading_account.normalise()
to discover your unrealised gains/losses on currency traded through that account.- Parameters:
source (Account) – The account the funds will be taken from
source_amount (Money) – A
Money
instance containing the inbound amount and currency.destination (Account) – The account the funds will be placed into
destination_amount (Money) – A
Money
instance containing the outbound amount and currencytrading_account (Account) – The trading account to be used. The normalised balance of this account will indicate gains/losses you have made as part of your activity via this account. Note that the normalised balance fluctuates with the current exchange rate.
fee_destination (Account) – Your exchange may incur fees. Specifying this will move incurred fees into this account (optional).
fee_amount (Money) – The amount and currency of any incurred fees (optional).
description (str) – Description for the transaction. Will default to describing funds in/out & fees (optional).
date (datetime.date) – The date on which the transaction took place. Defaults to today (optional).
- Returns:
The transaction created
- Return type:
See also
You can see the above example in practice in
CurrencyExchangeTestCase.test_fees
in test_currency.py.
Balance¶
- class hordak.utilities.currency.Balance(_money_obs=None, *args)¶
An account balance
Accounts may have multiple currencies. This class represents these multi-currency balances and provides math functionality. Balances can be added, subtracted, multiplied, divided, absolute’ed, and have their sign changed.
Examples
Example use:
Balance([Money(100, 'USD'), Money(200, 'EUR')]) # Or in short form Balance(100, 'USD', 200, 'EUR')
Important
Balances can also be compared, but note that this requires a currency conversion step. Therefore it is possible that balances will compare differently as exchange rates change over time.
- monies()¶
Get a list of the underlying
Money
instances- Returns:
A list of zero or money money instances. Currencies will be unique.
- Return type:
([Money])
- currencies()¶
Get all currencies with non-zero values
Exchange Rate Backends¶
- class hordak.utilities.currency.BaseBackend¶
Top-level exchange rate backend
This should be extended to hook into your preferred exchange rate service. The primary method which needs defining is
_get_rate()
.- cache_rate(currency, date, rate)¶
Cache a rate for future use
- get_rate(currency, date)¶
Get the exchange rate for
currency
against_INTERNAL_CURRENCY
If implementing your own backend, you should probably override
_get_rate()
rather than this.
- _get_rate(currency, date)¶
Get the exchange rate for
currency
againstINTERNAL_CURRENCY
You should implement this in any custom backend. For each rate you should call
cache_rate()
.Normally you will only need to call
cache_rate()
once. However, some services provide multiple exchange rates in a single response, in which it will likely be expedient to cache them all.Important
Not calling
cache_rate()
will result in your backend service being called for every currency conversion. This could be very slow and may result in your software being rate limited (or, if you pay for your exchange rates, you may get a big bill).
- class hordak.utilities.currency.FixerBackend¶
Use fixer.io for currency conversions