@loic001/fx
v0.2.0
Published
Money in many currencies, one number: convert at the rate of the day it happened, or at one constant rate so growth is growth and not the currency moving.
Maintainers
Readme
fx
Money in many currencies, one number — converted at the rate of the day it happened, or at one constant rate so growth is growth and not the currency moving.
Laws: PRINCIPLES.md.
import { bothViews, converter, toUsdCents } from '@loic001/fx';
// "how many units per 1 USD", one table per day
const dated = new Map([
['2026-09-01', { AUD: 1.5, EUR: 0.9, JPY: 150 }],
['2026-09-10', { AUD: 1.4, EUR: 0.86, JPY: 148 }],
]);
toUsdCents(1000, 'JPY', { JPY: 150 }); // 667 — not 7 (law 2)
toUsdCents(1000, 'VND', { USD: 1 }); // throws FxRateMissingError (law 1)
const c = converter(dated, { kind: 'dated' });
c.usdCents(100_000, 'AUD', '2026-09-01'); // 66 667
c.usdCents(100_000, 'AUD', '2026-09-10'); // 71 429 — the same A$1,000The two questions
"How much money is it?" and "did we grow?" are different, and one rate cannot answer both.
bothViews(rows, dated);
// { datedCents, constantCents, constantOn: '2026-09-10', fxEffectCents }fxEffectCents is what the currency did on its own — the part to subtract
before claiming growth. If AUD moves 8% and you report dated rates only, 1.3%
of "growth" was nobody selling anything.
A month is a flow, not a closing balance
Pricing thirty days of trading at whatever the rate did on the 30th is a balance-sheet convention applied to a flow. For revenue, volume, commission — anything accumulated over a period — the average of the period's daily rates is the honest one.
import { monthlyRates, toUsdCents } from '@loic001/fx';
const june = monthlyRates(dated, '2026-06'); // average of June's daily tables
toUsdCents(row.minorUnits, row.currency, june);A currency that only starts quoting mid-month is averaged over the days it actually existed, not dragged toward zero by the ones it did not.
What it refuses to do
- Guess a missing rate. A silent
1.0turns 1,000,000 VND (≈ $38) into $1,000,000 and nobody ever notices, because a total still looks like a total. - Treat every minor unit as a hundredth. ¥1,000 is
1000, not100000. - Price a payment with a rate that did not exist yet. Lookups walk backwards only; a missing day carries the most recent prior one and reports how stale it is, so the caller decides what is too old.
- Fetch anything. No network, no clock, no storage. You load rates from wherever you keep them; this package does arithmetic and says no.
