Currency

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 one Money 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 amount fee_amount. Note that the free currency must be the same as the source currency.

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 changed 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:

  1. cad_cash.balance() has decreased by CAD 120
  2. usd_cash.balance() has increased by USD 100
  3. banking_fees.balance() is CAD 1.50
  4. trading_account.balance() is USD 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 currency
  • trading_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:

(Transaction)

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])
normalise(to_currency)

Normalise this balance into a single currency

Parameters:to_currency (str) – Destination currency
Returns:A new balance object containing a single Money value in the specified currency
Return type:(Balance)

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 against INTERNAL_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