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
fxRevaluation(balances ×1, rates ×1, GBP)→ lines ×1, net gain loss -£300.00 a dollar receivable revalued down is a lossfxRevaluation(balances ×1, rates ×1, GBP)→ lines ×1, net gain loss £300.00 the same dollars owed to a supplier are a gainfxRevaluation(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.
export function fxRevaluation(balances: readonly ForeignBalance[], rates: readonly ExchangeRate[], functionalCurrency: string): FxRevaluation
| balances | ForeignBalance[] | monetary balances held in foreign currencies |
| rates | ExchangeRate[] | the period-end (closing) rates, each from a foreign currency into the functional currency |
| functionalCurrency | string | the currency the books are kept in |
| returns | FxRevaluation |
The types it declares, generated into your project
/** A balance held in a foreign currency, and what the books carry it at. */
export interface ForeignBalance {
readonly account: string;
/** in the foreign currency; negative for a liability (a credit balance) */
readonly balance: Money;
/** carrying amount in the functional currency before revaluation */
readonly bookedValue: Money;
}
/** One balance at the closing rate, and the adjustment to post. */
export interface RevaluedBalance {
readonly account: string;
readonly balance: Money;
readonly bookedValue: Money;
/** the balance at the closing rate, rounded half-up */
readonly revaluedValue: Money;
/** revalued less booked: positive is a gain, negative a loss */
readonly gainLoss: Money;
}
export interface FxRevaluation {
/** in input order */
readonly lines: readonly RevaluedBalance[];
/** the total unrealised gain (positive) or loss (negative) */
readonly netGainLoss: Money;
}
Your code names it in one line, in the file that uses it
import { fxRevaluation } from "#fune/finance.fx-revaluation@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { addMoney, subtractMoney } from "./money_add.ts"; ← from money.add ^1.0.0 · built alongside by fune
import { money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { convertMoney } from "./money_convert.ts"; ← from money.convert ^1.0.0 · built alongside by fune
import { type ExchangeRate } from "./money_convert_types.ts";
import { type ForeignBalance, type FxRevaluation, type RevaluedBalance } from "./finance_fx_revaluation_types.ts";
/**
* Revalue foreign-currency balances at the closing rates and report the
* unrealised gain (positive) or loss (negative) on each and in total.
*/
export function fxRevaluation(
balances: readonly ForeignBalance[],
rates: readonly ExchangeRate[],
functionalCurrency: string,
): FxRevaluation {
let netGainLoss = money(0, functionalCurrency);
const lines: RevaluedBalance[] = [];
for (const item of balances) {
const foreign = item.balance.currency;
if (foreign === functionalCurrency) {
throw new RangeError(`account ${item.account} is already in ${functionalCurrency}`);
}
if (item.bookedValue.currency !== functionalCurrency) {
throw new RangeError(`booked value of account ${item.account} must be in ${functionalCurrency}`);
}
const matching = rates.filter((r) => r.base === foreign && r.quote === functionalCurrency);
if (matching.length === 0) {
throw new RangeError(`no period-end rate from ${foreign} to ${functionalCurrency}`);
}
if (matching.length > 1) {
throw new RangeError(`more than one period-end rate from ${foreign} to ${functionalCurrency}`);
}
const revaluedValue = convertMoney(item.balance, matching[0], "half-up");
const gainLoss = subtractMoney(revaluedValue, item.bookedValue);
lines.push({ account: item.account, balance: item.balance, bookedValue: item.bookedValue, revaluedValue, gainLoss });
netGainLoss = addMoney(netGainLoss, gainLoss);
}
return { lines, netGainLoss };
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 3 dependencies, pins them in fune.lock, downloads only the TypeScript 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
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./finance.fx-revaluation-1.0.0-typescript.fune, or fetch it from a terminal with fune pull finance.fx-revaluation@1.0.0:typescript.
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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| README.md | 1,787 |
| impl/python.py | 1,916 |
| impl/rust.rs | 3,569 |
| impl/typescript.ts | 1,833 |
| vectors.json | 7,541 |