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 the function registerCurrency()
:
>>> EUR = registerCurrency('EUR')
... HKD = registerCurrency('HKD')
... TND = registerCurrency('TND')
... USD = registerCurrency('USD')
>>> EUR, HKD, TND, USD
(Currency(u'EUR'), Currency(u'HKD'), Currency(u'TND'), Currency(u'USD'))
The function 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 the 3-character ISO 4217 code) and a name. In addition, it
holds the smallest fraction defined for amounts in this currency:
>>> TND.symbol
u'TND'
>>> TND.name
u'Tunisian Dinar'
>>> TND.smallestFraction
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(u'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(u'EUR'))
>>> Money(41.1783, TND)
Money(Decimal('41.178'), Currency(u'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(u'USD'))
>>> 3.18 ^ USD
Money(Decimal('3.18'), Currency(u'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(u'EUR'))
>>> Money(30, EUR) + Money(3.18, USD)
IncompatibleUnitsError: Can't convert 'US Dollar' to 'Euro'
Resulting values are always quantized to the smallest fraction defined with the currency:
>>> Money('3.20 USD') / 3
Money(Decimal('1.07'), Currency(u'USD'))
>>> Money('3.20 TND') / 3
Money(Decimal('1.067'), Currency(u'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(u'EUR'), Decimal(1), Currency(u'HKD'), Decimal('8.395804'))
unitMultiple and termAmount 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(u'TND'), Decimal(100), Currency(u'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(u'TND'), Currency(u'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.inverseRate
Fraction(50000000, 82073)
>>> fxTND2EUR.inverseQuotation
(Currency(u'EUR'), Currency(u'TND'), Fraction(50000000, 82073))
The inverse ExchangeRate can be created by calling the method
ExchangeRate.inverted()
:
>>> fxEUR2TND = fxTND2EUR.inverted()
>>> fxEUR2TND
ExchangeRate(Currency(u'EUR'), Decimal(1), Currency(u'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(u'TND'), Decimal(10), Currency(u'HKD'), Decimal('0.137814'))
>>> fxEUR2HKD / fxEUR2TND
ExchangeRate(Currency(u'TND'), Decimal(10), Currency(u'HKD'), Decimal('0.137814'))
>>> fxEUR2TND / fxEUR2HKD
ExchangeRate(Currency(u'HKD'), Decimal(1), Currency(u'TND'), Decimal('72.561693'))
>>> fxHKD2EUR = fxEUR2HKD.inverted()
>>> fxTND2EUR / fxHKD2EUR
ExchangeRate(Currency(u'TND'), Decimal(10), Currency(u'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(u'HKD'))
>>> mEUR * fxEUR2TND
Money(Decimal('3210.556'), Currency(u'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(u'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:
>>> rates_2015_11 = [(USD, Decimal('1.1073'), 1),
... (HKD, Decimal('8.7812'), 1)]
>>> conv.update((2015, 11), rates_2015_11)
>>> rates_2015_12 = [(USD, Decimal('1.0943'), 1),
... (HKD, Decimal('8.4813'), 1)]
>>> conv.update((2015, 12), rates_2015_12)
Exchange rates can be retrieved by calling MoneyConverter.getRate()
. 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:
>>> # assuming today is a date in December 2015
>>> conv.getRate(EUR, USD)
ExchangeRate(Currency(u'EUR'), Decimal(1), Currency(u'USD'), Decimal('1.0943', 6))
>>> conv.getRate(HKD, EUR, date(2015, 11, 3))
ExchangeRate(Currency(u'HKD'), Decimal(1), Currency(u'EUR'), Decimal('0.11388', 6))
>>> conv.getRate(USD, EUR)
ExchangeRate(Currency(u'USD'), Decimal(1), Currency(u'EUR'), Decimal('0.913826'))
>>> conv.getRate(HKD, USD)
ExchangeRate(Currency(u'HKD'), Decimal(1), Currency(u'USD'), Decimal('0.129025'))
A money converter can be registered with the class Currency
in order
to support implicit conversion of money amounts from one currency into
another (using the default reference date, see below):
>>> Currency.registerConverter(conv)
>>> USD(2 ^ EUR)
Decimal('2.1886', 8)
>>> twoEUR = 2 ^ EUR
>>> twoEUR.convert(USD)
Money(Decimal('2.19'), Currency(u'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
getRate()
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.getRate(EUR, USD)
ExchangeRate(Currency(u'EUR'), Decimal(1), Currency(u'USD'), Decimal('1.0917', 6))
>>> conv = MoneyConverter(EUR, getDfltEffectiveDate=yesterday)
>>> conv.update(yesterday(), [(USD, Decimal('1.0943'), 1)])
>>> conv.update(datetime.date.today(), [(USD, Decimal('1.0917'), 1)])
>>> conv.getRate(EUR, USD)
ExchangeRate(Currency(u'EUR'), Decimal(1), Currency(u'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:
>>> class PricePerMass(Quantity):
... defineAs = Money / Mass
Because Money
has no reference unit, there is no reference unit
created for the derived quantity …:
>>> list(PricePerMass.Unit.registeredUnits())
[]
… instead, units must be explicitly defined:
>>> EURpKG = PricePerMass.Unit(defineAs=EUR/KILOGRAM)
>>> list(PricePerMass.Unit.registeredUnits())
[PricePerMass.Unit(u'EUR/kg')]
As with other derived quantities, the function generateUnits()
can be used to create all units from the cross-product of units of the base
quantities.
Instances of the derived quantity can be created and used just like those of other quantities:
>>> p = 17.45 ^ EURpKG
>>> p * Decimal('1.05')
PricePerMass(Decimal('18.354', 4), PricePerMass.Unit(u'EUR/kg'))
>>> m = 530 ^ GRAM
>>> m * p
Money(Decimal('9.26'), Currency(u'EUR'))
Note that instances of the derived class are not automatically quantized to the quantum defined for the currency:
>>> PricePerMass.getQuantum(EURpKG) 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
QuantityError: Resulting unit not defined: HKD/kg.
>>> HKDpKG = PricePerMass.Unit(defineAs=HKD/KILOGRAM)
>>> p * fxEUR2HKD
PricePerMass(Decimal('146.75865392'), PricePerMass.Unit(u'HKD/kg'))
4.2. Classes¶
-
class
quantity.money.
Currency
¶ Represents a currency, i.e. a money unit.
Parameters: - isoCode (string) – ISO 4217 3-character code
- name (string) – name of the currency
- minorUnit (Integral) – amount of minor unit (as exponent to 10), optional, defaults to precision of smallest fraction, if that is given, otherwise to 2
- smallestFraction (number) – smallest fraction available for the currency, optional, defaults to Decimal(10) ** -minorUnit
smallestFraction can also be given as a string, as long as it is convertable to a Decimal.
Returns: Currency
instanceRaises: TypeError
– given isoCode is not a stringValueError
– no isoCode was givenTypeError
– given minorUnit is not an Integral numberValueError
– given minorUnit < 0ValueError
– given smallestFraction can not be converted to a DecimalValueError
– given smallestFraction not > 0ValueError
– 1 is not an integer multiple of given smallestFractionValueError
– given smallestFraction does not fit given minorUnit
-
isoCode
¶ ISO 4217 3-character code.
-
name
¶ Name of this currency.
-
smallestFraction
¶ The smallest fraction available for this currency.
-
class
quantity.money.
Money
¶ 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 (number) – money amount (gets rounded to a Decimal according to smallest fraction of currency)
- currency (Currency) – money unit
amount must convertable to a decimalfp.Decimal, it can also be given as a string.
Returns: Money
instanceRaises: TypeError
– amount can not be converted to a Decimal numberValueError
– no currency given
2. Form
Parameters: - mStr (string) – 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
instanceRaises: TypeError
– amount given in mStr can not be converted to a Decimal numberValueError
– no currency givenTypeError
– a byte string is given that can not be decoded using the standard encodingValueError
– given string does not represent a Money amount:exp:`~quantity.IncompatibleUnitsError`: the currency derived from –
the symbol given in mStr can not be converted to given currency
-
currency
¶ The money’s currency, i.e. its unit.
-
class
quantity.money.
ExchangeRate
(unitCurrency, unitMultiple, termCurrency, termAmount)¶ Basic representation of a conversion factor between two currencies.
Parameters: unitCurrency and termCurrency can also be given as 3-character ISO 4217 codes of already registered currencies.
unitMultiple must be > 1. It can also be given as a string, as long as it is convertable to an Integral.
termAmount 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’)
Returns: ExchangeRate
instanceunitMultiple and termAmount 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
– unknown ISO 4217 code given for a currencyTypeError
– value of type other than Currency or string given for a currencyValueError
– currencies given are identicalValueError
– unit multiple is not an Integral or is not >= 1ValueError
– term amount is not >= 0.000001ValueError
– unit multiple or term amount can not be converted to a Decimal
-
unitCurrency
¶ Currency to be converted from, aka base currency.
-
termCurrency
¶ Currency to be converted to, aka price currency.
-
rate
¶ Relative value of termCurrency to unitCurrency.
-
inverseRate
¶ Inverted rate, i.e. relative value of unitCurrency to termCurrency.
-
quotation
¶ Tuple of unitCurrency, termCurrency and rate.
-
inverseQuotation
¶ Tuple of termCurrency, unitCurrency and inverseRate.
-
inverted
()¶ Return inverted exchange rate.
-
__hash__
()¶ hash(self)
-
__eq__
(other)¶ self == other
Parameters: other (object) – object to compare with Returns: True if other is an instance of ExchangeRate and self.quotation == other.quotation, False otherwise
-
__mul__
(other)¶ self * other
1. Form
Parameters: other ( Money
) – money amount to multiply withReturns: Money
instance: equivalent of other in term currencyRaises: ValueError
– currency of other is not equal to unit currency2. Form
Parameters: other ( ExchangeRate
) – exchange rate to multiply withReturns: ExchangeRate
instance: “triangulated” exchange rateRaises: ValueError
– unit currency of one multiplicant does not equal the term currency of the other multiplicant3. Form
Parameters: other ( Quantity
sub-class) – quantity to multiply withThe type of other must be a sub-class of
Quantity
derived fromMoney
divided by some other sub-class ofQuantity
.Returns: Quantity
sub-class instance: equivalent of other in term currencyRaises: ValueError
– resulting unit is not defined
-
__div__
(other)¶ self / other
Parameters: other ( ExchangeRate
) – exchange rate to divide withReturns: ExchangeRate
instance: “triangulated” exchange rateRaises: ValueError
– unit currencies of operands not equal and term currencies of operands not equal
-
__rdiv__
(other)¶ other / self
1. Form
Parameters: other ( Money
) – money amount to divideReturns: Money
instance: equivalent of other in unit currencyRaises: ValueError
– currency of other is not equal to term currency2. Form
Parameters: other ( Quantity
sub-class) – quantity to divideThe type of other must be a sub-class of
Quantity
derived fromMoney
divided by some other sub-class ofQuantity
.Returns: Quantity
sub-class instance: equivalent of other in unit currencyRaises: :class:`~quantity.QuantityError`: resulting unit is not defined –
-
class
quantity.money.
MoneyConverter
(baseCurrency, getDfltEffectiveDate=None)¶ Converter for money amounts.
Money converters can be used to hold different exchange rates. They can be registered with the class
Currency
in order to support implicit conversion of money amounts from one currency into another.Parameters: - baseCurrency (
Currency
) – currency used as reference currency - getDfltEffectiveDate (callable) – a callable without parameters that
must return a date which is then used as default effective date in
MoneyConverter.getRate()
(default: datetime.date.today)
-
__call__
(moneyAmnt, toCurrency, effectiveDate=None)¶ Convert a money amount in one currency to the equivalent amount for another currency.
Parameters: If effectiveDate is not given, the return value of the callable given as getDfltEffectiveDate to
MoneyConverter
is used as reference (default: today).Returns: amount equiv so that equiv ^ toCurrency == moneyAmnt Return type: number Raises: UnitConversionError
– exchange rate not available
-
baseCurrency
¶ The currency used as reference currency.
-
update
(validity, rateSpecs)¶ Update the exchange rate dictionary used by the converter.
Parameters: - validity (see below) – specifies the validity period of the given exchange rates
- rateSpecs (iterable) – 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 rateSpecs must be comprised of the following elements:
- termCurrency (
Currency
): currency of equivalent amount, aka price currency - termAmount (number): equivalent amount of term currency
- unitMultiple (Integral): amount of base currency
validity and termCurrency are used together as the key for the internal exchange rate dictionary.
Raises: ValueError
– invalid date given for validityValueError
– invalid year / month given for validityValueError
– invalid year given for validityValueError
– unknown value given for validityValueError
– different types of validity period given in subsequent calls
-
getRate
(unitCurrency, termCurrency, effectiveDate=None)¶ Return exchange rate from unitCurrency to termCurrency that is effective for effectiveDate.
Parameters: If effectiveDate is not given, the return value of the callable given as getDfltEffectiveDate to
MoneyConverter
is used as reference (default: today).Returns: ExchangeRate
: exchange rate from unitCurrency to termCurrency that is effective for effectiveDate, None if there is no such rate
-
__enter__
()¶ Register self as converter in class Currency.
-
__exit__
(*args)¶ Unregister self as converter in class Currency.
- baseCurrency (
4.3. Functions¶
-
quantity.money.
getCurrencyInfo
(isoCode)¶ Return infos from ISO 4217 currency database.
Parameters: isoCode (string) – ISO 4217 3-character code for the currency to be looked-up Returns: 3-character code, numerical code, name, minorUnit and list of countries which use the currency as functional currency Return type: tuple Raises: ValueError
– currency with code isoCode not in databaseNote
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.