Skip to content

fx

Foreign exchange services.

convert(*, value, of, to, on, using='European Central Bank') cached

Convert a value from one currency to another.

Parameters:

Name Type Description Default
value Decimal

The value to convert.

required
of ISO4217

Currency code to convert from.

required
to ISO4217

Currency code to convert to.

required
on PastDate

Date to get the rate for.

required
using FxProviderStr

The FX provider to use. Defaults to "European Central Bank"

'European Central Bank'

Raises:

Type Description
NotImplementedError

When the given FX provider is not supported.

Returns:

Name Type Description
Decimal Decimal

The converted value.

Examples:

>>> from pycountant.services.fx import convert
>>> print(
...     convert(value=Decimal("12340"), of="USD", to="EUR", on=date(2023, 1, 5))
... )
11640.41128195453259126497500

Warning

If the on PastDate does not fall on a weekday (Monday to Friday), then it will not be possible to obtain a rate for conversion.

Failure

If there is no internet connection, it will not be possible to make the relevant API call to obtain the rate for conversion.

Source code in src/pycountant/services/fx.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
@cache
def convert(
    *,
    value: Decimal,
    of: ISO4217,
    to: ISO4217,
    on: PastDate,
    using: FxProviderStr = "European Central Bank",
) -> Decimal:
    """
    Convert a value from one currency to another.

    Args:
        value (Decimal): The value to convert.
        of (ISO4217): Currency code to convert from.
        to (ISO4217): Currency code to convert to.
        on (PastDate): Date to get the rate for.
        using (FxProviderStr): The FX provider to use. Defaults to "European Central Bank"

    Raises:
        NotImplementedError: When the given FX provider is not supported.

    Returns:
        Decimal: The converted value.

    Examples:

        >>> from pycountant.services.fx import convert
        >>> print(
        ...     convert(value=Decimal("12340"), of="USD", to="EUR", on=date(2023, 1, 5))
        ... )
        11640.41128195453259126497500

    !!! warning

        If the *on* PastDate does not fall on a weekday (Monday to Friday),
        then it will not be possible to obtain a rate for conversion.

    !!! failure

        If there is no internet connection, it will not be possible to make
        the relevant API call to obtain the rate for conversion.

    """
    rate = get_rate(of=of, to=to, on=on, using=using)
    return value * rate

get_conversion_strategy(*, using)

Get the conversion strategy for a particular provider.

Parameters:

Name Type Description Default
using FxProviderStr

The provider.

required

Raises:

Type Description
NotImplementedError

Error raised if no strategy exists for that provider.

Returns:

Type Description
Callable[[ISO4217, ISO4217, date], Decimal]

Callable[[ISO4217, ISO4217, date], Decimal]: A strategy to obtain the rate for conversion with.

Source code in src/pycountant/services/fx.py
 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
def get_conversion_strategy(
    *,
    using: FxProviderStr,
) -> Callable[[ISO4217, ISO4217, date], Decimal]:
    """
    Get the conversion strategy for a particular provider.

    Args:
        using (FxProviderStr): The provider.

    Raises:
        NotImplementedError: Error raised if no strategy exists for that provider.

    Returns:
        Callable[[ISO4217, ISO4217, date], Decimal]: A strategy to obtain the rate for conversion with.
    """
    strategies: dict[
        FxProviderStr,
        Callable[[ISO4217, ISO4217, date], Decimal],
    ] = {
        "European Central Bank": get_rate_via_ecb,
    }
    strategy = strategies.get(using)
    if strategy is None:
        raise NotImplementedError(
            f"Currently only '{','.join(strategies)}' is supported, not '{using}'",
        )
    return strategy

get_rate(*, of, to, on, using='European Central Bank') cached

Get the rate to convert one currency to another on a past date.

Parameters:

Name Type Description Default
of ISO4217

Currency code to convert from.

required
to ISO4217

Currency code to convert to.

required
on PastDate

Date to get the rate for.

required
using FxProviderStr

The FX provider to use. Defaults to "European Central Bank"

'European Central Bank'

Raises:

Type Description
NotImplementedError

When the given FX provider is not supported.

Returns:

Name Type Description
Decimal

The rate

Warning

If the on PastDate does not fall on a weekday (Monday to Friday), then it will not be possible to obtain a rate for conversion.

Failure

If there is no internet connection, it will not be possible to make the relevant API call to obtain the rate for conversion.

Source code in src/pycountant/services/fx.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
@cache
def get_rate(
    *,
    of: ISO4217,
    to: ISO4217,
    on: PastDate,
    using: FxProviderStr = "European Central Bank",
):
    """
    Get the rate to convert one currency to another on a past date.

    Args:
        of (ISO4217): Currency code to convert from.
        to (ISO4217): Currency code to convert to.
        on (PastDate): Date to get the rate for.
        using (FxProviderStr): The FX provider to use. Defaults to "European Central Bank"

    Raises:
        NotImplementedError: When the given FX provider is not supported.

    Returns:
        Decimal: The rate

    !!! warning

        If the *on* PastDate does not fall on a weekday (Monday to Friday),
        then it will not be possible to obtain a rate for conversion.

    !!! failure

        If there is no internet connection, it will not be possible to make
        the relevant API call to obtain the rate for conversion.

    """
    strategy = get_conversion_strategy(using=using)
    if isinstance(on, datetime):
        on = on.date()  # Don't need time info
    return strategy(of, to, on)

get_rate_via_ecb(of, to, on) cached

Get FX rate via European Central Bank.

Parameters:

Name Type Description Default
of ISO4217

Currency code to convert from.

required
to ISO4217

Currency code to convert to.

required
on PastDate

Date to get the rate for.

required

Raises:

Type Description
ValueError

When no rate is found for the given currencies on the given date.

Returns:

Name Type Description
Decimal Decimal

The FX rate.

Source code in src/pycountant/services/fx.py
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
@cache
def get_rate_via_ecb(of: ISO4217, to: ISO4217, on: PastDate) -> Decimal:
    """
    Get FX rate via European Central Bank.

    Args:
        of (ISO4217): Currency code to convert from.
        to (ISO4217): Currency code to convert to.
        on (PastDate): Date to get the rate for.

    Raises:
        ValueError: When no rate is found for the given currencies on the given date.

    Returns:
        Decimal: The FX rate.
    """
    content = str(
        httpx.get(
            "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml",
        ).content,
    )
    start = content.find("<Cube>")
    end = content.find("</gesmes:Envelope>")
    tree = ElementTree.fromstring(content[start:end])
    # Filter by date
    of_rate = None if of != "EUR" else Decimal("1.0")
    to_rate = None if to != "EUR" else Decimal("1.0")
    date_cubes = [
        cube
        for cube in tree.findall("Cube")
        if cube.attrib.get("time") == on.isoformat()
    ]  # and cube.attrib.get("currency") == to]
    for date_cube in date_cubes:
        for cube in date_cube.findall("Cube"):
            # Gets rates against EUR
            if cube.attrib.get("currency") == of:
                _of_rate = cube.attrib.get("rate")
                if _of_rate:
                    of_rate = Decimal(_of_rate)
                    continue
            if cube.attrib.get("currency") == to:
                _to_rate = cube.attrib.get("rate")
                if _to_rate:
                    to_rate = Decimal(_to_rate)
                    continue
    if of_rate is None:
        raise ValueError(
            f"No rate found for '{of}' on {on.isoformat()} using European Central Bank",
        )
    if to_rate is None:
        raise ValueError(
            f"No rate found for '{to}' on {on.isoformat()} using European Central Bank",
        )
    eur_to_eur_rate = Decimal("1.0")
    return (eur_to_eur_rate / of_rate) / (eur_to_eur_rate / to_rate)