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
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
@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
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
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,
        "HMRC": get_rate_via_hmrc,
    }
    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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
@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_from_base(of_rate, to_rate) cached

Gets the rate when it is known that the of and to rate are relative to a single currency in the base rate.

Parameters:

Name Type Description Default
of_rate Decimal

The of rate.

required
to_rate Decimal

The to rate.

required

Returns:

Name Type Description
Decimal Decimal

The rate to use for conversion.

Source code in src/pycountant/services/fx.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@cache
def get_rate_from_base(of_rate: Decimal, to_rate: Decimal) -> Decimal:
    """
    Gets the rate when it is known that the of and to rate are
    relative to a single currency in the base rate.

    Args:
        of_rate (Decimal): The of rate.
        to_rate (Decimal): The to rate.

    Returns:
        Decimal: The rate to use for conversion.
    """
    base_rate = Decimal("1.0")
    return (base_rate / of_rate) / (base_rate / to_rate)

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
 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
@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",
        )
    return get_rate_from_base(of_rate=of_rate, to_rate=to_rate)

get_rate_via_hmrc(of, to, on) cached

Get FX rate via HMRC.

Note

The rates obtained are the rate for that month and are not specific to the particular day.

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
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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
@cache
def get_rate_via_hmrc(of: ISO4217, to: ISO4217, on: PastDate) -> Decimal:
    """
    Get FX rate via HMRC.

    !!! note

    The rates obtained are the rate for that month and are not specific to the
    particular day.

    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.
    """
    # https://api.trade-tariff.service.gov.uk/reference.html#get-exchange-rates-year-month
    url = (
        f"https://www.trade-tariff.service.gov.uk/"
        f"uk/api/exchange_rates/{on.year}-{on.month}"
        f"?filter[type]=monthly"
    )
    response = httpx.get(url=url)
    content = response.json()
    of_rate = None if of != "GBP" else Decimal("1.0")
    to_rate = None if to != "GBP" else Decimal("1.0")
    if not to_rate:
        to_rate_items = [
            item.get("attributes").get("rate")
            for item in content.get("included")
            if item.get("attributes").get("currency_code") == to
        ]
        if to_rate_items:
            to_rate = to_rate_items[0]
    if not of_rate:
        of_rate_items = [
            item.get("attributes").get("rate")
            for item in content.get("included")
            if item.get("attributes").get("currency_code") == of
        ]
        if of_rate_items:
            of_rate = of_rate_items[0]
    if of_rate is None:
        raise ValueError(
            f"No rate found for '{of}' on {on.isoformat()} using HMRC",
        )
    if to_rate is None:
        raise ValueError(
            f"No rate found for '{to}' on {on.isoformat()} using HMRC",
        )
    return get_rate_from_base(of_rate=Decimal(of_rate), to_rate=Decimal(to_rate))