Functional Weave
Code in Python

finance.fx-revaluation

Unrealised foreign exchange gain or loss on foreign-currency balances revalued at a period-end rate.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 14 tests, run in TypeScript, Python and Rust.

What it does

At a period end, monetary balances held in a foreign currency (bank accounts, receivables, payables, loans) are restated at the closing rate, and the difference from what the books carried them at is an unrealised exchange gain or loss (IAS 21 paragraph 23 and FRS 102 section 30 require monetary items to be translated at the closing rate). This works out that difference, one balance at a time, and the net total to post.

Sign convention: an asset is a positive balance and a liability a negative one, in both currencies. `gainLoss` is the revalued value less the booked value, so it is positive for a gain and negative for a loss whichever side the balance is on. A USD 10,000.00 receivable booked at GBP 7,800.00 and revalued at 0.75 is GBP 7,500.00, a loss of 300.00; the same amount owed to a supplier (-10,000.00 booked at -7,800.00) is a gain of 300.00, because the pounds needed to settle it fell.

For example

  • fx_revaluation(balances ×1, rates ×1, GBP) → lines ×1, net gain loss -£300.00 a dollar receivable revalued down is a loss
  • fx_revaluation(balances ×1, rates ×1, GBP) → lines ×1, net gain loss £300.00 the same dollars owed to a supplier are a gain
  • fx_revaluation(balances ×2, rates ×1, GBP) → lines ×2, net gain loss £0.00 a receivable and a payable in the same currency net to nothing

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

def fx_revaluation(balances: Sequence[ForeignBalance], rates: Sequence[ExchangeRate], functional_currency: str) -> FxRevaluation
balancesForeignBalance[]monetary balances held in foreign currencies
ratesExchangeRate[]the period-end (closing) rates, each from a foreign currency into the functional currency
functional_currencystringthe currency the books are kept in
returnsFxRevaluation

The types it declares, generated into your project

@dataclass(frozen=True)
class ForeignBalance:
    """A balance held in a foreign currency, and what the books carry it at."""

    account: str
    #: in the foreign currency; negative for a liability (a credit balance)
    balance: Money
    #: carrying amount in the functional currency before revaluation
    booked_value: Money

@dataclass(frozen=True)
class RevaluedBalance:
    """One balance at the closing rate, and the adjustment to post."""

    account: str
    balance: Money
    booked_value: Money
    #: the balance at the closing rate, rounded half-up
    revalued_value: Money
    #: revalued less booked: positive is a gain, negative a loss
    gain_loss: Money

@dataclass(frozen=True)
class FxRevaluation:
    #: in input order
    lines: List[RevaluedBalance]
    #: the total unrealised gain (positive) or loss (negative)
    net_gain_loss: Money

Your code names it in one line, in the file that uses it

from fune.finance.fx_revaluation import fx_revaluation  # finance.fx-revaluation@^1
impl/python.py · 41 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

from typing import List, Sequence

from .finance_fx_revaluation_types import ForeignBalance, FxRevaluation, RevaluedBalance
from .money_add import add_money, subtract_money  ← from money.add ^1.0.0 · built alongside by fune
from .money_amount import money  ← from money.amount ^1.0.0 · built alongside by fune
from .money_convert import convert_money  ← from money.convert ^1.0.0 · built alongside by fune
from .money_convert_types import ExchangeRate


def fx_revaluation(
    balances: Sequence[ForeignBalance], rates: Sequence[ExchangeRate], functional_currency: str
) -> FxRevaluation:
    """Revalue foreign-currency balances at the closing rates and report the
    unrealised gain (positive) or loss (negative) on each and in total.
    """
    net = money(0, functional_currency)
    lines: List[RevaluedBalance] = []
    for item in balances:
        foreign = item.balance.currency
        if foreign == functional_currency:
            raise ValueError("account %s is already in %s" % (item.account, functional_currency))
        if item.booked_value.currency != functional_currency:
            raise ValueError("booked value of account %s must be in %s" % (item.account, functional_currency))
        matching = [r for r in rates if r.base == foreign and r.quote == functional_currency]
        if len(matching) == 0:
            raise ValueError("no period-end rate from %s to %s" % (foreign, functional_currency))
        if len(matching) > 1:
            raise ValueError("more than one period-end rate from %s to %s" % (foreign, functional_currency))
        revalued = convert_money(item.balance, matching[0], "half-up")
        gain_loss = subtract_money(revalued, item.booked_value)
        lines.append(
            RevaluedBalance(
                account=item.account,
                balance=item.balance,
                booked_value=item.booked_value,
                revalued_value=revalued,
                gain_loss=gain_loss,
            )
        )
        net = add_money(net, gain_loss)
    return FxRevaluation(lines=lines, net_gain_loss=net)

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 3 dependencies, pins them in fune.lock, downloads only the Python package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add finance.fx-revaluation
Download for Python finance.fx-revaluation-1.0.0-python.fune · 15,959 bytes sha256 ee32c0e370c1fe4a1a39ccf9ad3b3fc3e5335cc37d8083ea5443bd5a8017ac83

The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./finance.fx-revaluation-1.0.0-python.fune, or fetch it from a terminal with fune pull finance.fx-revaluation@1.0.0:python.

The whole function, every language, is one file too: finance.fx-revaluation-1.0.0.fune, 21,558 bytes, sha256 6cb7b3e1212b7ea10c4c3a460e0c1c058a94db0fd0cee1683a75fe666d6eeb01. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

# fune: before finance.fx-revaluation

after — your function gets the result and the arguments, and returns the final result.

# fune: after finance.fx-revaluation

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

# fune: replace money.add in finance.fx-revaluation
# fune: replace money.amount in finance.fx-revaluation
# fune: replace money.convert in finance.fx-revaluation

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show finance.fx-revaluation --steps.

# fune: step finance.fx-revaluation after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
a dollar receivable revalued down is a loss balances ×1, rates ×1, GBP → lines ×1, net gain loss -£300.00
the same dollars owed to a supplier are a gain balances ×1, rates ×1, GBP → lines ×1, net gain loss £300.00
a receivable and a payable in the same currency net to nothing balances ×2, rates ×1, GBP → lines ×2, net gain loss £0.00
a euro bank balance at a four-decimal rate, rounded half-up once balances ×1, rates ×1, GBP → lines ×1, net gain loss £7.65
yen have no minor unit: 1,000,000 yen at 0.0052 balances ×1, rates ×1, GBP → lines ×1, net gain loss -£100.00
half a penny rounds away from zero on an asset balances ×1, rates ×1, GBP → lines ×1, net gain loss £0.01
and on a liability, symmetrically balances ×1, rates ×1, GBP → lines ×1, net gain loss -£0.01
several currencies, rates listed in any order, unused rates ignored balances ×2, rates ×3, GBP → lines ×2, net gain loss £1.00
no balances: a zero net in the functional currency , rates ×1, GBP → lines , net gain loss £0.00
a currency with no closing rate is refused balances ×1, rates ×1, GBP → error: no period-end rate from EUR to GBP
Show the other 4 tests
CaseArgumentsExpected
a rate into another currency does not count balances ×1, rates ×1, GBP → error: no period-end rate from EUR to GBP
two closing rates for one currency are refused balances ×1, rates ×2, GBP → error: more than one period-end rate from USD to GBP
a balance already in the functional currency is refused balances ×1, rates ×1, GBP → error: account 1200 is already in GBP
a booked value in the foreign currency is refused balances ×1, rates ×1, GBP → error: booked value of account 1210 must be in GBP

More from the author

Conversion is money.convert: the rate is an exact fraction in major units, the decimal places of each currency are looked up (yen has none), and the one rounding to the functional currency's minor unit is half-up, symmetric for negative balances. Each line is rounded on its own, and the net is the sum of the rounded lines, so it agrees with the postings.

Each foreign currency needs exactly one rate into the functional currency; a missing or duplicated rate is an error rather than a guess, and so is a balance already in the functional currency or a booked value that is not. Choosing the closing rate (which source, which day) is the caller's decision.

Non-monetary items (stock, fixed assets, prepayments) stay at historical rates and must not be passed here. After posting, the revalued value becomes the new booked value for the next period.

Files

PathBytes
README.md1,787
impl/python.py1,916
impl/rust.rs3,569
impl/typescript.ts1,833
vectors.json7,541