4. Money

Currency-safe computations with money amounts.

4.1. Usage

4.1.1. Registering a currency

A currency must explicitly be registered as a unit for further use. The easiest way to do this is to call Money.register_currency():

>>> from quantity.money import Money
>>> EUR = Money.register_currency('EUR')
>>> HKD = Money.register_currency('HKD')
>>> TND = Money.register_currency('TND')
>>> USD = Money.register_currency('USD')
>>> EUR, HKD, TND, USD
(Currency('EUR'), Currency('HKD'), Currency('TND'), Currency('USD'))

The method is backed by a database of currencies defined in ISO 4217. It takes the 3-character ISO 4217 code as parameter.

Currency derives from Unit. Each instance has a symbol (which is usually the 3-character ISO 4217 code) and a name. In addition, it holds the smallest fraction defined for amounts in this currency:

>>> TND.symbol
'TND'
>>> TND.name
'Tunisian Dinar'
>>> TND.smallest_fraction
Decimal('0.001')

4.1.2. Instantiating a money amount

As Money derives from Quantity, an instance can simply be created by giving an amount and a unit:

>>> Money(30, EUR)
Money(Decimal(30, 2), Currency('EUR'))

All amounts of money are rounded according to the smallest fraction defined for the currency:

>>> Money(3.128, EUR)
Money(Decimal('3.13'), Currency('EUR'))
>>> Money(41.1783, TND)
Money(Decimal('41.178'), Currency('TND'))

As with other quantities, money amounts can also be derived from a string or build using the operator *:

>>> Money('3.18 USD')
Money(Decimal('3.18'), Currency('USD'))
>>> 3.18 * USD
Money(Decimal('3.18'), Currency('USD'))

4.1.3. Computing with money amounts

Money derives from Quantity, so all operations on quantities can also be applied to instances of Money. But because there is no fixed relation between currencies, there is no implicit conversion between money amounts of different currencies:

>>> Money(30, EUR) + Money(3.18, EUR)
Money(Decimal('33.18'), Currency('EUR'))
>>> Money(30, EUR) + Money(3.18, USD)
Traceback (most recent call last):
UnitConversionError: Can't convert 'USD' to 'EUR'

Resulting values are always quantized to the smallest fraction defined with the currency:

>>> Money('3.20 USD') / 3
Money(Decimal('1.07'), Currency('USD'))
>>> Money('3.20 TND') / 3
Money(Decimal('1.067'), Currency('TND'))

4.1.4. Converting between different currencies

4.1.4.1. Exchange rates

A conversion factor between two currencies can be defined by using the ExchangeRate. It is given a unit currency (aka base currency), a unit multiple, a term currency (aka price currency) and a term amount, i.e. the amount in term currency equivalent to unit multiple in unit currency:

>>> fxEUR2HKD = ExchangeRate(EUR, 1, HKD, Decimal('8.395804'))
>>> fxEUR2HKD
ExchangeRate(Currency('EUR'), Decimal(1), Currency('HKD'), Decimal('8.395804'))

unit_multiple and term_amount will always be adjusted so that the resulting unit multiple is a power to 10 and the resulting term amounts magnitude is >= -1. The latter will always be rounded to 6 decimal digits:

>>> fxTND2EUR = ExchangeRate(TND, 5, EUR, Decimal('0.0082073'))
>>> fxTND2EUR
ExchangeRate(Currency('TND'), Decimal(100), Currency('EUR'), Decimal('0.164146'))

The resulting rate for an amount of 1 unit currency in term currency can be obtained via the property ExchangeRate.rate:

>>> fxTND2EUR.rate
Decimal('0.00164146')

The property ExchangeRate.quotation gives a tuple of unit currency, term currency and rate:

>>> fxTND2EUR.quotation
(Currency('TND'), Currency('EUR'), Decimal('0.00164146'))

The properties ExchangeRate.inverseRate and ExchangeRate.inverseQuotation give the rate and the quotation in the opposite direction (but do not round the rate!):

>>> fxTND2EUR.inverse_rate
Fraction(50000000, 82073)
>>> fxTND2EUR.inverse_quotation
(Currency('EUR'), Currency('TND'), Fraction(50000000, 82073))

The inverse ExchangeRate can be created by calling the method ExchangeRate.inverted():

>>> fxEUR2TND = fxTND2EUR.inverted()
>>> fxEUR2TND
ExchangeRate(Currency('EUR'), Decimal(1), Currency('TND'), Decimal('609.213749'))

An exchange rate can be derived from two other exchange rates, provided that they have one currency in common (“triangulation”). If the unit currency of one exchange rate is equal to the term currency of the other, the two exchange rates can be multiplied with each other. If either the unit currencies or the term currencies are equal, the two exchange rates can be divided:

>>> fxEUR2HKD * fxTND2EUR
ExchangeRate(Currency('TND'), Decimal(10), Currency('HKD'), Decimal('0.137814'))
>>> fxEUR2HKD / fxEUR2TND
ExchangeRate(Currency('TND'), Decimal(10), Currency('HKD'), Decimal('0.137814'))
>>> fxEUR2TND / fxEUR2HKD
ExchangeRate(Currency('HKD'), Decimal(1), Currency('TND'), Decimal('72.561693'))
>>> fxHKD2EUR = fxEUR2HKD.inverted()
>>> fxTND2EUR / fxHKD2EUR
ExchangeRate(Currency('TND'), Decimal(10), Currency('HKD'), Decimal('0.137814'))

4.1.4.2. Converting money amounts using exchange rates

Multiplying an amount in some currency with an exchange rate with the same currency as unit currency results in the equivalent amount in term currency:

>>> mEUR = 5.27 * EUR
>>> mEUR * fxEUR2HKD
Money(Decimal('44.25'), Currency('HKD'))
>>> mEUR * fxEUR2TND
Money(Decimal('3210.556'), Currency('TND'))

Likewise, dividing an amount in some currency with an exchange rate with the same currency as term currency results in the equivalent amount in unit currency:

>>> fxHKD2EUR = fxEUR2HKD.inverted()
>>> mEUR / fxHKD2EUR
Money(Decimal('44.25'), Currency('HKD'))

4.1.4.3. Using money converters

Money converters can be used to hold different exchange rates, each of them linked to a period of validity. The type of period must be the same for all exchange rates held by a money converter.

A money converter is created by calling MoneyConverter, giving the base currency used by this converter:

>>> conv = MoneyConverter(EUR)

The method MoneyConverter.update() is then used to feed exchange rates into the converter.

For example, a money converter with monthly rates can be created like this:

>>> import datetime
>>> today = datetime.date.today()
>>> year, month, day = today.timetuple()[:3]
>>> prev_month = month - 1
>>> rates = [(USD, Decimal('1.1073'), 1),
...          (HKD, Decimal('8.7812'), 1)]
>>> conv.update((year, prev_month), rates)
>>> rates = [(USD, Decimal('1.0943'), 1),
...          (HKD, Decimal('8.4813'), 1)]
>>> conv.update((year, month), rates)

Exchange rates can be retrieved by calling MoneyConverter.get_rate(). If no reference date is given, the current date is used (unless a callable returning a different date is given when the converter is created, see below). The method returns not only rates directly given to the converter, but also inverted rates and rates calculated by triangulation:

>>> conv.get_rate(EUR, USD)
ExchangeRate(Currency('EUR'), Decimal(1), Currency('USD'), Decimal('1.0943', 6))
>>> conv.get_rate(HKD, EUR, date(year, prev_month, 3))
ExchangeRate(Currency('HKD'), Decimal(1), Currency('EUR'), Decimal('0.11388', 6))
>>> conv.get_rate(USD, EUR)
ExchangeRate(Currency('USD'), Decimal(1), Currency('EUR'), Decimal('0.913826'))
>>> conv.get_rate(HKD, USD)
ExchangeRate(Currency('HKD'), Decimal(1), Currency('USD'), Decimal('0.129025'))

A money converter can be registered with the class Money in order to support implicit conversion of money amounts from one currency into another (using the default reference date, see below):

>>> Money.register_converter(conv)
>>> twoEUR = 2 * EUR
>>> twoEUR.convert(USD)
Money(Decimal('2.19'), Currency('USD'))

A money converter can also be registered and unregistered by using it as context manager in a with statement.

In order to use a default reference date other than the current date, a callable can be given to MoneyConverter. It must be callable without arguments and return a date. It is then used by get_rate() to get the default reference date:

>>> yesterday = lambda: datetime.date.today() - datetime.timedelta(1)
>>> conv = MoneyConverter(EUR)      # uses today as default
>>> conv.update(yesterday(), [(USD, Decimal('1.0943'), 1)])
>>> conv.update(datetime.date.today(), [(USD, Decimal('1.0917'), 1)])
>>> conv.get_rate(EUR, USD)
ExchangeRate(Currency('EUR'), Decimal(1), Currency('USD'), Decimal('1.0917', 6))
>>> conv = MoneyConverter(EUR, get_dflt_effective_date=yesterday)
>>> conv.update(yesterday(), [(USD, Decimal('1.0943'), 1)])
>>> conv.update(datetime.date.today(), [(USD, Decimal('1.0917'), 1)])
>>> conv.get_rate(EUR, USD)
ExchangeRate(Currency('EUR'), Decimal(1), Currency('USD'), Decimal('1.0943', 6))

As other quantity converters, a MoneyConverter instance can be called to convert a money amount into the equivalent amount in another currency. But note that the amount is not adjusted to the smallest fraction of that currency:

>>> conv(twoEUR, USD)
Decimal('2.1886', 8)
>>> conv(twoEUR, USD, datetime.date.today())
Decimal('2.1834', 8)

4.1.5. Combining Money with other quantities

As Money derives from Quantity, it can be combined with other quantities in order to define a new quantity. This is, for example, useful for defining prices per quantum:

>>> from quantity import Quantity
>>> class Mass(Quantity,
...            ref_unit_name='Kilogram',
...            ref_unit_symbol='kg'):
...     pass
>>> KILOGRAM = Mass.ref_unit
>>> class PricePerMass(Quantity, define_as=Money / Mass):
...     pass

Because Money has no reference unit, there is no reference unit created for the derived quantity …:

>>> PricePerMass.units()
()

… instead, units must be explicitly defined:

>>> EURpKG = PricePerMass.derive_unit_from(EUR, KILOGRAM)
>>> PricePerMass.units()
(Unit('EUR/kg'),)

Instances of the derived quantity can be created and used just like those of other quantities:

>>> from decimalfp import Decimal
>>> p = Decimal("17.45") * EURpKG
>>> p * Decimal("1.05")
PricePerMass(Decimal('18.3225'), Unit('EUR/kg'))
>>> GRAM = Mass.new_unit('g', 'Gram', Decimal("0.001") * KILOGRAM)
>>> m = 530 * GRAM
>>> m * p
Money(Decimal('9.25'), Currency('EUR'))

Note that instances of the derived class are not automatically quantized to the quantum defined for the currency:

>>> EURpKG.quantum is None
True

Instances of such a “money per quantum” class can also be converted using exchange rates, as long as the resulting unit is defined:

>>> p * fxEUR2HKD
Traceback (most recent call last):
QuantityError: Resulting unit not defined: HKD/kg.
>>> HKDpKG = PricePerMass.derive_unit_from(HKD, KILOGRAM)
>>> p * fxEUR2HKD
PricePerMass(Decimal('146.5067798', 8), Unit('HKD/kg'))

4.2. Types

MoneyConverterT

Type of money converters

alias of Callable[[Money, Currency, Optional[date]], Rational]

ValidityT

Types used to specify time periods for the validity of exchange rates

alias of Optional[Union[date, int, str, SupportsInt, Tuple[int, int], Tuple[Union[str, SupportsInt], Union[str, SupportsInt]]]]

RateSpecT

Type of tuple to specify an exchange rate

alias of Tuple[Union[Currency, str], Union[Rational, float, str], Rational]

4.3. Classes

class Currency

Represents a currency, i.e. a money unit.

Note

New instances of Currency can not be created directly by calling Currency. Instead, use Money.register_currency or Money.new_unit.

__init__()
static __new__(cls, symbol: str) Unit

Return the Unit registered with symbol symbol.

Parameters:

symbol – symbol of the requested unit

Raises:

ValueEror – no unit with given symbol registered

property iso_code: str

ISO 4217 3-character code.

property name: str

Return the units name.

If the unit was not given a name, its symbol is returned.

property smallest_fraction: Decimal

The smallest fraction available for this currency.

class Money

Bases: Quantity

Represents a money amount, i.e. the combination of a numerical value and a money unit, aka. currency.

Instances of Money can be created in two ways, by providing a numerical amount and a Currency or by providing a string representation of a money amount.

1. Form

Parameters:
  • amount – money amount (gets rounded to a Decimal according to smallest fraction of currency)

  • currency – money unit

amount must convertable to a decimalfp.Decimal, it can also be given as a string.

Raises:
  • TypeErroramount can not be converted to a Decimal number

  • ValueError – no currency given

2. Form

Parameters:
  • mStr – unicode string representation of a money amount (incl. currency symbol)

  • currency – the money’s unit (optional)

mStr must contain a numerical value and a currency symbol, separated atleast by one blank. Any surrounding white space is ignored. If currency is given in addition, the resulting money’s currency is set to this currency and its amount is converted accordingly, if possible.

Returns:

Money instance

Raises:
  • TypeError – amount given in mStr can not be converted to a Decimal number

  • ValueError – no currency given

  • TypeError – a byte string is given that can not be decoded using the standard encoding

  • ValueError – given string does not represent a Money amount

  • IncompatibleUnitsError – the currency derived from the symbol given in mStr can not be converted to given currency

MoneyMeta.register_currency(iso_code: str) Currency

Register the currency with code iso_code from ISO 4217 database.

Parameters:

iso_code – ISO 4217 3-character code for the currency to be registered

Returns:

registered currency

Raises:

ValueError – currency with code iso_code not in database

MoneyMeta.new_unit(symbol: str, name: str | None = None, minor_unit: int | None = None, smallest_fraction: Real | str | None = None) Currency

Create, register and return a new Currency instance.

Parameters:
  • symbol – symbol of the currency (should be a ISO 4217 3-character code, if possible)

  • name – name of the currency

  • minor_unit – amount of minor unit (as exponent to 10), optional, defaults to precision of smallest fraction, if that is given, otherwise to 2

  • smallest_fraction – smallest fraction available for the currency, optional, defaults to Decimal(10) ** -minor_unit. Can also be given as a string, as long as it is convertable to a Decimal.

Raises:
  • TypeError – given symbol is not a string

  • ValueError – no symbol was given

  • TypeError – given minor_unit is not an Integral number

  • ValueError – given minor_unit < 0

  • ValueError – given smallest_fraction can not be converted to a Decimal

  • ValueError – given smallest_fraction not > 0

  • ValueError – 1 is not an integer multiple of given smallest_fraction

  • ValueError – given smallest_fraction does not fit given minor_unit

__init__()
__new__(amount: Real | str, unit: Unit | None = None) <class 'quantity.Quantity'>

Create new Quantity instance.

property currency: Currency

The money’s currency, i.e. its unit.

class ExchangeRate

Basic representation of a conversion factor between two currencies.

Parameters:
  • unit_currency – currency to be converted from, aka base currency

  • unit_multiple – amount of base currency (must be equal to an integer)

  • term_currency – currency to be converted to, aka price currency

  • term_amount – equivalent amount of term currency

unit_currency and term_currency can also be given as 3-character ISO 4217 codes of already registered currencies.

unit_multiple must be > 1. It can also be given as a string, as long as it is convertable to an Integral.

term_amount can also be given as a string, as long as it is convertable to a number.

Example:

1 USD = 0.9683 EUR => ExchangeRate(‘USD’, 1, ‘EUR’, ‘0.9683’)

unit_multiple and term_amount will always be adjusted so that the resulting unit multiple is a power to 10 and the resulting term amounts magnitude is >= -1. The latter will always be rounded to 6 decimal digits.

Raises:
  • ValueError – non-registered / unknown symbol given for a currency

  • TypeError – value of type other than Currency or string given for a currency

  • ValueError – currencies given are identical

  • ValueError – unit multiple is not equal to an integer or is not >= 1

  • ValueError – term amount is not >= 0.000001

  • ValueError – unit multiple or term amount can not be converted to a Decimal

__eq__(other: Any) bool

self == other

Parameters:

other – object to compare with

Returns:

True if other is an instance of ExchangeRate and self.quotation == other.quotation, False otherwise

__hash__() int

hash(self)

__init__(unit_currency: Currency | str, unit_multiple: Rational, term_currency: Currency | str, term_amount: Rational | float | str) None
__mul__(other: <class 'quantity.money.Money'>) <class 'quantity.money.Money'>
__mul__(other: ExchangeRate) ExchangeRate
__mul__(other: <class 'quantity.Quantity'>) <class 'quantity.Quantity'>

self * other

1. Form

Parameters:

other – money amount to multiply with

Returns:

Money equivalent of other in term currency

Raises:

ValueError – currency of other is not equal to unit currency

2. Form

Parameters:

other – exchange rate to multiply with

Returns:

“triangulated” exchange rate

Raises:

ValueError – unit currency of one multiplicant does not equal the term currency of the other multiplicant

3. Form

Parameters:

other – quantity to multiply with

The type of other must be a sub-class of Quantity derived from Money divided by some other sub-class of Quantity.

Returns:

equivalent of other in term currency

Raises:

ValueError – resulting unit is not defined

__rtruediv__(other: <class 'quantity.money.Money'>) <class 'quantity.money.Money'>
__rtruediv__(other: <class 'quantity.Quantity'>) <class 'quantity.Quantity'>

other / self

1. Form

Parameters:

other – money amount to divide

Returns:

equivalent of other in unit currency

Raises:

ValueError – currency of other is not equal to term currency

2. Form

Parameters:

other – quantity to divide

The type of other must be a sub-class of Quantity derived from Money divided by some other sub-class of Quantity.

Returns:

equivalent of other in unit currency

Raises:

QuantityError – resulting unit is not defined

__truediv__(other: ExchangeRate) ExchangeRate

self / other

Parameters:

other – exchange rate to divide with

Returns:

“triangulated” exchange rate

Raises:

ValueError – unit currencies of operands not equal and term currencies of operands not equal

inverted() ExchangeRate

Return inverted exchange rate.

property inverse_quotation: Tuple[Currency, Currency, Rational]

Tuple of term currency, unit currency and inverse rate.

property inverse_rate: Rational

Inverted rate, i.e. relative value of unit currency to term currency.

property quotation: Tuple[Currency, Currency, Rational]

Tuple of unit currency, term currency and rate.

property rate: Rational

Relative value of term currency to unit currency.

property term_currency: Currency

Currency to be converted to, aka price currency.

property unit_currency: Currency

Currency to be converted from, aka base currency.

class MoneyConverter

Converter for money amounts.

Money converters can be used to hold different exchange rates. They can be registered with the class Money in order to support implicit conversion of money amounts from one currency into another.

Parameters:
  • base_currency – currency used as reference currency

  • get_dflt_effective_date – a callable without parameters that must return a date which is then used as default effective date in MoneyConverter.get_rate() (default: datetime.date.today)

__call__(money_amnt: <class 'quantity.money.Money'>, to_currency: ~quantity.money.Currency, effective_date: ~datetime.date | None = None) Rational

Convert a money amount in one currency to the equivalent amount for another currency.

Parameters:
  • money_amnt – money amount to be converted

  • to_currency – currency for which the equivalent amount is to be returned

  • effective_date – date for which the exchange rate to be used must be effective (default: None)

If effective_date is not given, the return value of the callable given as get_dflt_effective_date to MoneyConverter is used as reference (default: today).

Returns:

amount equiv so that equiv * to_currency == money_amnt

Raises:

UnitConversionError – exchange rate not available

__enter__() MoneyConverter

Register self as converter in Money.

__exit__(*args: Tuple[Any, ...]) None

Unregister self as converter in Money.

__init__(base_currency: Currency, get_dflt_effective_date: Callable[[], date] | None = None) None
get_rate(unit_currency: Currency, term_currency: Currency, effective_date: date | None = None) ExchangeRate | None

Return exchange rate from unit_currency to term_currency that is effective for effective_date.

Parameters:
  • unit_currency – currency to be converted from

  • term_currency – currency to be converted to

  • effective_date – date at which the rate must be effective (default: None)

If effective_date is not given, the return value of the callable given as get_dflt_effective_date to MoneyConverter is used as reference (default: today).

Returns:

exchange rate from unit_currency to term_currency that is

effective for effective_date, None if there is no such rate

update(validity: ValidityT, rate_specs: Iterable[RateSpecT]) None

Update the exchange rate dictionary used by the converter.

Parameters:
  • validity – specifies the validity period of the given exchange rates

  • rate_specs – list of entries to update the converter

validity can be given in different ways:

If None is given, the validity of the given rates is not restricted, i. e. they are used for all times (“constant rates”).

If an int (or a string convertable to an int) is given, it is interpreted as a year and the given rates are treated as valid for that year (“yearly rates”).

If a tuple of two int`s (or two strings convertable to an `int) or a string in the form ‘YYYY-MM’ is given, it is interpreted as a combination of a year and a month, and the given rates are treated as valid for that month (“monthly rates”).

If a date or a string holding a date in ISO format (‘YYYY-MM-DD’) is given, the rates are treated as valid just for that date (“daily rates”).

The type of validity must be the same in recurring updates.

Each entry in rate_specs must be comprised of the following elements:

  • term_currency (Union[Currency, str]): currency of equivalent

    amount, aka price currency

  • term_amount (Union[Rational, float, str]): equivalent amount of

    term currency

  • unit_multiple (Rational): amount of base currency

validity and term_currency are used together as the key for the internal exchange rate dictionary.

Raises:
  • ValueError – invalid date given for validity

  • ValueError – invalid year / month given for validity

  • ValueError – invalid year given for validity

  • ValueError – unknown value given for validity

  • ValueError – different types of validity period given in subsequent calls

property base_currency: Currency

The currency used as reference currency.

4.4. Functions

get_currency_info(iso_code: str) Tuple[str, int, str, int, List[str]]

Return infos from ISO 4217 currency database.

Parameters:

iso_code – ISO 4217 3-character code for the currency to be looked-up

Returns:

3-character code, numerical code, name, minor unit and list of

countries which use the currency as functional currency

Raises:

ValueError – currency with code iso_code not in database

Note

The database available here does only include entries from ISO 4217 which are used as functional currency, not those used for bond markets, noble metals and testing purposes.