Schedule icon
OutputValues icon
If icon
Fail icon
Download icon
Queries icon
Log icon
UploadFiles icon
Switch icon
Set icon

Revalue open foreign currency balances at month end with ECB rates and rate controls

Month-end FX revaluation in Kestra. ECB rates, triangulation, stale and implausible rate checks, unrealized gain or loss, reversal and exposure.

Categories
BusinessData

Every company that invoices, pays or banks in more than one currency revalues its open balances at month end. The arithmetic is simple. The failures are in the rates: a currency the source did not publish, a fixing from last week, or a yen rate quoted per 100 units. Any of them can quietly book a gain or loss of millions. This flow gets the rates from the ECB, checks them before using them, and books the revaluation with its reversal.

How the rates are built

  • The ECB data API gives daily reference rates of each currency against the euro. No key is needed, and rates_source: EMBEDDED uses the July to September 2026 month-end fixings copied into the flow for offline runs.
  • The closing rate of a currency is its last fixing on or before period end, which covers weekends and holidays.
  • Rates to the functional currency are triangulated through the euro: (functional per EUR) / (currency per EUR), using both fixings of the same day.
  • The previous rate for the move check is the closing rate stored by the last close, or the source's previous month end on a first close.

Rate controls

Control Effect
MISSING_RATE: open items in a currency with no rate Blocks
STALE_RATE: last fixing older than max_rate_age_days Blocks. This also catches a run started before month end
IMPLAUSIBLE_RATE: a move above 50% since the last close Blocks: a quotation unit error, not a market move
A move above max_move_pct Needs approved_by

What is revalued

Monetary items only: receivables, payables, foreign currency bank balances and an intercompany loan. Prepayments are non-monetary and stay at their historical rate. Each open item is revalued from its booked rate to the closing rate. The journal is dated period end, by balance sheet account and currency, against 7810 (unrealized gain) or 7820 (unrealized loss). The reversal is dated day one of the next month (the reverse-and-revalue method), so each month starts from the booked rates again.

The exposure report shows the net monetary position per currency and the effect of a 10% move. That is the number treasury needs to decide whether to hedge.

How it works

  1. period_control and period_gate refuse a closed period or one whose previous period is open, and load the last closing rates.
  2. fetch_rates (io.kestra.plugin.core.flow.If) downloads the ECB CSV with ecb_rates (io.kestra.plugin.core.http.Download) when rates_source is ECB.
  3. revalue (io.kestra.plugin.jdbc.duckdb.Queries) builds the rates, runs the rate controls, selects the open items at period end, revalues them and writes the journals and the exposure.
  4. log_revaluation prints the run. evidence stores rates.csv, open-items.csv, revaluation-by-account.csv, exposure.csv, journal.csv, reversal.csv and blockers.csv.
  5. decide (io.kestra.plugin.core.flow.Switch):
    • READY: lock_period stores the closing rates and the net result.
    • Every other verdict fails with the reason.

Tested end to end

On Kestra 2.0.3 OSS, with live ECB data.

Run Inputs Result
1 2026-08, ECB EUR 1.1596, GBP 1.353914, JPY 0.006261, INR 0.010507, CHF 1.236775 USD, all fixed 2026-08-31. 98 monetary items revalued, 9 prepayments left alone. Net unrealized gain 162,139.94, mostly the EUR 2,000,000 intercompany loan. Closed
2 2026-09, max_move_pct: 1.5 CHF -3.13%, EUR -2.08%, GBP -1.87%, JPY +1.74%: stops for approval
3 2026-09 ISSUES Blocked: JPY moved +10,073.9% (quoted per 100 yen), INR not published
4 2026-09 with approved_by Net unrealized 75,033.76 on 90 items. Closed, reversal on 2026-10-01
5 2026-10, run on October 6 Blocked: every rate is the 2026-10-05 fixing, 26 days before period end
6 2026-08 again, after a reset, EMBEDDED 162,139.94, the same as run 1 with live data
7 already closed, or a month skipped Refused

I recomputed run 1 independently in Python from open-items.csv with Decimal, and it gave the same 162,139.94.

Problems found while testing

  • A currency with an older fixing disappeared. The triangulation joined each currency's last fixing with the functional currency's last fixing. When INR stopped publishing, its last date no longer matched the dollar's, and INR silently dropped out instead of being reported as stale. The join now uses the dollar fixing of the same day.
  • hash() % n - 30 overflowed. DuckDB's hash returns an unsigned integer, so subtracting from a small remainder failed. The remainder is cast to INT first.

Inputs

Input Default Purpose
period 2026-08 YYYY-MM, validated
functional_currency USD Any ECB currency, or EUR
rates_source ECB ECB or EMBEDDED
max_rate_age_days 4 Staleness limit
max_move_pct 5.0 Move that needs approval
scenario CLEAN Demo only
approved_by empty The reviewer

Use it with your data

  • Replace items with your open items from AR, AP, bank and intercompany, each with its booked rate, and accounts with your chart of accounts.
  • The demo's booked rates assume USD as the functional currency. With another functional currency, use your ledger's booked rates.
  • Add currencies to the ECB URL (D.USD+GBP+...). The ECB publishes about 30 currencies. For others, add a second source.

Things to know

  • An intercompany loan that is not expected to be settled can qualify as part of a net investment. Its revaluation then goes to other comprehensive income on consolidation, not profit or loss. Map its account accordingly.
  • Realized gains and losses come from settlement and are booked by AR and AP, not here.
  • Reopening a period is deliberate: delete the KV key fx_revaluation.

Links

This blueprint was created by zkasuran.

See How

New to Kestra?

Use blueprints to kickstart your first workflows.