FX revaluation: marking foreign positions to market
When the rate moves, every foreign-currency balance on your books moves with it.
By Solomon Ajayi · Free to read, no signup
Your fintech holds USD reserves at a sponsor bank ($1,000) for cross-border payouts. On your NGN books, that USD was booked at the rate the day it landed: 1500 NGN/USD = ₦1,500,000. A month passes. NGN devalues; rate is now 1600. The SAME $1,000 is now worth ₦1,600,000 on your books, BUT your ledger still says ₦1,500,000. The gap (₦100,000) is an unrealised FX gain that has to be posted, or your balance sheet lies. This is FX revaluation: at period close, every foreign-currency asset and liability is re-marked at the spot rate; the gain or loss flows to a P&L account.
A foreign-currency balance has two numbers: how many units you hold, and what those units are worth in your reporting currency. Your ledger reports in NGN, so when you buy $1,000 it gets booked at the day's rate as ₦1,500,000, and that NGN figure is what the books remember. The dollar count does not change while it sits there. The NGN value does, every time the exchange rate moves, and the ledger does not notice on its own.
Revaluation is the period-close step that reconciles the two. You take the native balance, $1,000, multiply by today's spot rate, and compare against the book value already on the ledger. At 1600 the position is worth ₦1,600,000 but the books say ₦1,500,000, so you post the ₦100,000 gap: debit the USD asset up, credit FX Gain. Nothing was bought or sold; you only re-marked the book value so the balance sheet matches reality. When the rate falls back, the same entry runs in reverse through FX Loss.
This gain is unrealised, which matters. You have not converted any dollars, so no cash changed hands; you are simply telling the truth about what the position is worth on the reporting date. Run it as an automated month-end job, make it idempotent on the period and account so a re-run cannot double-post, and keep the native-currency balance queryable, because without the dollar count you cannot recompute the value at the next rate.
Worked example, step by step
Open the USD position at the original rate
Buy $1,000 USD at the sponsor bank. Today's rate is 1500 NGN/USD. You pay ₦1,500,000 in NGN, receive $1,000 in your USD asset account.
| Account | Debit | Credit |
|---|---|---|
| USD at Sponsor Bank (1300) | $1,500,000.00 | |
| NGN Treasury (1310) | ₦1,500,000.00 |
USD at Sponsor Bank UP $1,000 (₦1,500,000 at today's rate). NGN Treasury DOWN ₦1,500,000. This is the BOOK COST: the cost in your reporting currency at the moment of purchase. The ledger remembers the NGN amount, not the rate.
Month-end: NGN weakened to 1600 NGN/USD
A month later. Spot rate is now 1600 NGN/USD. Your $1,000 is worth ₦1,600,000 today, but your ledger still shows ₦1,500,000 (the original book cost). The gap (₦100,000) is the unrealised FX gain, your NGN-denominated balance sheet would understate the asset by ₦100,000 if you closed the books right now.
| Account | Debit | Credit |
|---|---|---|
| USD at Sponsor Bank (1300) | $100,000.00 | |
| FX Gain (4500) | ₦100,000.00 |
Post the revaluation: debit USD at Sponsor Bank ₦100,000 (its NGN value goes UP), credit FX Gain ₦100,000 (you recognise the unrealised gain in P&L). Nothing actually moved, no USD bought or sold, but the BOOK VALUE updates so the balance sheet reflects today's rate.
Month-end revaluation: compute and post the delta per FX-bearing account.
-- For each foreign-currency asset/liability, compare its current book
-- NGN value to its native-currency balance * today's spot rate. Post the
-- delta to FX Gain or FX Loss.
WITH positions AS (
SELECT
la.id AS account_id,
la.code,
la.currency,
SUM(jl.debit) - SUM(jl.credit) AS current_book_ngn -- assets: D-C
FROM ledger_account la
JOIN entry_line jl ON jl.account_id = la.id
WHERE la.type = 'asset' AND la.currency <> 'NGN'
GROUP BY la.id, la.code, la.currency
),
native_balances AS (
SELECT
p.account_id,
p.currency,
p.current_book_ngn,
-- Track native-unit balances by either an "fx_units" column on
-- entry_line, or by storing rates in metadata and back-deriving:
SUM(jl.fx_native_amount) AS native_balance
FROM positions p
JOIN entry_line jl ON jl.account_id = p.account_id
GROUP BY p.account_id, p.currency, p.current_book_ngn
)
SELECT
nb.account_id,
nb.currency,
nb.native_balance,
nb.current_book_ngn,
nb.native_balance * fx.spot_ngn AS today_value_ngn,
nb.native_balance * fx.spot_ngn - nb.current_book_ngn AS reval_delta_ngn
FROM native_balances nb
JOIN fx_rate fx ON fx.from_ccy = nb.currency AND fx.to_ccy = 'NGN'
WHERE fx.as_of_date = $1;Atomic month-end job: revalue every foreign position in one transaction.
def revalue_fx_positions(conn, as_of_date) -> None:
with conn.transaction():
rows = conn.execute(
"""
SELECT account_code, native_balance, current_book_ngn, today_value_ngn
FROM v_fx_reval_candidates
WHERE as_of_date = %s
""",
(as_of_date,),
).fetchall()
for row in rows:
delta = row["today_value_ngn"] - row["current_book_ngn"]
if delta == 0:
continue
entry_id = conn.execute(
"""
INSERT INTO journal_entry (description, idempotency_key, posted_at)
VALUES (%s, %s, %s)
RETURNING id
""",
(
f"FX revaluation: {row['account_code']} {delta:+}",
f"reval_{as_of_date}_{row['account_code']}",
as_of_date,
),
).fetchone()[0]
if delta > 0:
# gain: debit the asset, credit FX Gain (income)
conn.execute(
"""
INSERT INTO journal_line (entry_id, account_code, debit, credit) VALUES
(%s, %s, %s, 0),
(%s, '4500', 0, %s)
""",
(entry_id, row["account_code"], delta, entry_id, delta),
)
else:
# loss: debit FX Loss (expense), credit the asset
conn.execute(
"""
INSERT INTO journal_line (entry_id, account_code, debit, credit) VALUES
(%s, '5500', %s, 0),
(%s, %s, 0, %s)
""",
(entry_id, -delta, entry_id, row["account_code"], -delta),
)Next month: NGN recovers to 1550 NGN/USD
The next month, NGN strengthens partially. Spot is now 1550. Your $1,000 is worth ₦1,550,000 today. The previous reval put it at ₦1,600,000 (rate 1600). So today's book value should drop by ₦50,000.
| Account | Debit | Credit |
|---|---|---|
| FX Loss (5500) | ₦50,000.00 | |
| USD at Sponsor Bank (1300) | $50,000.00 |
Post the reverse direction: debit FX Loss ₦50,000 (you recognise the partial give-back in P&L), credit USD at Sponsor Bank ₦50,000 (its NGN value goes DOWN). Same pattern, opposite signs. After this entry, USD asset shows ₦1,550,000, exactly its market value at today's rate.
Takeaway
FX revaluation runs at every period close. Every foreign-currency asset and liability is re-marked at the spot rate; the delta vs book value flows to FX Gain (income) or FX Loss (expense). The position itself didn't change, no currency was bought or sold, but the NGN value on your books has to track market reality, or your balance sheet lies. Run this as an automated job (the SQL pattern is straightforward), make it idempotent on (period_end, account_code), and store the original native-currency balance somewhere queryable. Without it, your CFO can't tell investors what the company is worth in their reporting currency.
The code behind it
Reference solution
CREATE TABLE fx_rate (
from_ccy text NOT NULL,
to_ccy text NOT NULL,
as_of_date date NOT NULL,
spot_ngn numeric(18,6) NOT NULL,
posted_at timestamptz NOT NULL DEFAULT now(),
-- One rate per currency pair per closing date. The PRIMARY KEY also
-- serves as the index the reval JOIN hits on every month-end run.
PRIMARY KEY (from_ccy, to_ccy, as_of_date)
);Practice this on a real ledger
Reading is half of it. Open this lesson in the lab to post the entries yourself against a real Postgres-backed double-entry ledger, with the validation on. Free, your sandbox is yours.