File: currencies.rst

package info (click to toggle)
python-pint 0.25.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 1,940 kB
  • sloc: python: 20,487; makefile: 149
file content (145 lines) | stat: -rw-r--r-- 5,346 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
.. _currencies:

Using Pint for currency conversions
===================================
Currency conversion tends to be substantially more complex than physical units.
The exact exchange rate between two currencies:

- changes every minute,
- changes depending on the place,
- changes depending on who you are and who changes the money,
- may be not reversible, e.g. EUR->USD may not be the same as 1/(USD->EUR),
- different rates may apply to different amounts of money, e.g. in a BUY/SELL ledger,
- frequently involves fees, whose calculation can be more or less sophisticated.
  For example, a typical credit card contract may state that the bank will charge you a
  fee on all purchases in foreign currency of 1 USD or 2%, whichever is higher, for all
  amounts less than 1000 USD, and then 1.5% for anything in excess.

You may implement currencies in two ways, both of which require you to be familiar
with :ref:`contexts`.

Simplified model
----------------

This model implies a few strong assumptions:

- There are no conversion fees
- All exchange rates are reversible
- Any amount of money can be exchanged at the same rate
- All exchanges can happen at the same time, between the same actors.

In this simplified scenario, you can perform any round-trip across currencies
and always come back with the original money; e.g.
1 USD -> EUR -> JPY -> GBP -> USD will always give you 1 USD.

In reality, these assumptions almost never happen but can be a reasonable approximation,
for example in the case of large financial institutions, which can use interbank
exchange rates and have nearly-limitless liquidity and sub-second trading systems.

This can be implemented by putting all currencies on the same dimension, with a
default conversion rate of NaN, and then setting the rate within contexts::

    USD = [currency]
    EUR = nan USD
    JPY = nan USD
    GBP = nan USD

    @context FX
        EUR = 1.11254 USD
        GBP = 1.16956 EUR
    @end

Note how, in the example above:

- USD is our *base currency*. It is arbitrary, only matters for the purpose
  of invoking ``to_base_units()``, and can be changed with :ref:`systems`.
- We did not set a value for JPY - maybe because the trader has no availability, or
  because the data source was for some reason missing up-to-date data.
  Any conversion involving JPY will return NaN.
- We redefined GBP to be a function of EUR instead of USD. This is fine as long as there
  is a path between two currencies.

Full model
----------

If any of the assumptions of the simplified model fails, one can resort to putting each
currency on its own dimension, and then implement transformations::

    EUR = [currency_EUR]
    GBP = [currency_GBP]

    @context FX
        GBP -> EUR: value * 1.11108 EUR/GBP
        EUR -> GBP: value * 0.81227 GBP/EUR
    @end

.. code-block:: python

    >>> q = ureg.Quantity("1 EUR")
    >>> with ureg.context("FX"):
    ... q = q.to("GBP").to("EUR")
    >>> q
    0.9024969516 EUR

More sophisticated formulas, e.g. dealing with flat fees and thresholds, can be
implemented with arbitrary python code by programmatically defining a context (see
:ref:`contexts`).

Currency Symbols
----------------

Many common currency symbols are not supported by the pint parser. A preprocessor can be used as a workaround:

.. doctest::

   >>> import pint
   >>> ureg = pint.UnitRegistry(preprocessors = [lambda s: s.replace("€", "EUR")])
   >>> ureg.define("euro = [currency] = € = EUR")
   >>> print(ureg.Quantity("1 €"))
   1 euro

Example using Currency Converter
-------------------------------

The following example demonstrates how to use `currency_converter`_ with Pint to perform currency conversions with historical rates.

.. doctest::

   >>> import itertools
   >>> import pint
   >>> import currency_converter
   >>> from datetime import date
   >>>
   >>> # create a currency converter instance to load all of the data
   >>> cc = currency_converter.CurrencyConverter(fallback_on_missing_rate=True)
   >>>
   >>> # load custom units and instantiate Quantity base class that is used everywhere
   >>> ureg = pint.UnitRegistry()
   >>> for c in cc.currencies:
   ...     ureg.define(f"{c} = [currency_{c}]")  # i.e. USD = [currency_USD]
   >>>
   >>> # add programmatic context for currency conversion
   >>> currency_context = pint.Context("FX", defaults={"date": None})
   >>> for a, b in itertools.combinations(list(cc.currencies), 2):
   ...     def a2b(_ureg, x, date=None, a=a, b=b):
   ...         return cc.convert(x.magnitude, a, b, date=date) * _ureg(b)
   ...     def b2a(_ureg, x, date=None, a=a, b=b):
   ...         return cc.convert(x.magnitude, b, a, date=date) * _ureg(a)
   ...     currency_context.add_transformation(f"[currency_{a}]", f"[currency_{b}]", a2b)
   ...     currency_context.add_transformation(f"[currency_{b}]", f"[currency_{a}]", b2a)
   >>> ureg.add_context(currency_context)
   >>>
   >>> # Example usage
   >>> q = ureg.Quantity("1 EUR")
   >>> with ureg.context("FX", date=date(2010, 11, 21)):
   ...     q = q.to("USD")
   >>> print(q)
   1.3656 USD

Note that the `currency_converter`_ package is not a dependency of Pint, so you will need to install it separately:

.. code-block:: bash

    pip install currencyconverter

.. _`currency_converter`: https://alexprengere.github.io/currencyconverter/