FinanceModels.FX API Reference
FinanceModels.FX.BasisSwapLegFinanceModels.FX.ConvertedFinanceModels.FX.ForwardFinanceModels.FX.ForwardsFinanceModels.FX.PairFinanceModels.FX.OutrightFinanceModels.FX.ParBasisSwapFinanceModels.FX.implied_zcb_quotes
Exported API
FinanceModels.FX — Module
The FX module provides foreign exchange models, contracts, and quote conventions:
FX.Pair— a currency pair, e.g.FX.Pair(:EUR, :USD)FX.Forwards— a covered-interest-parity model of outright FX forward rates, built from a spot rate and two discount curvesFX.Forward— an outright FX forward contractFX.Converted— a wrapper that converts a contract's projected cashflows into the quote currency at CIP forward rates, enabling cross-currency swaps and hedged multi-currency valuationFX.Outright— market forward quotes as zero-priceQuotesFX.ParBasisSwap— par cross-currency basis-swap spread quotes for calibrating the long-dated basis (withFX.BasisSwapLegas the underlying instrument)FX.implied_zcb_quotes— transform FX forward quotes into the implied base-currency zero-coupon quotes used for curve construction
The design philosophy mirrors the rest of FinanceModels: market conventions that are a common source of silent errors (pair direction, points scale, which curve discounts what) are encoded explicitly in types and keyword arguments, and the curves inside an FX model are ordinary yield models, so splines, parametric curves, curve arithmetic (e.g. foreign_ois + basis), and fit all apply unchanged.
Present values follow one rule: present_value is denominated in the currency of the contract's own cashflows — the quote currency for an FX.Forward (it settles in quote-currency units), the base currency for an FX.BasisSwapLeg (a strip of base-currency cashflows). Convert before combining values across denominations: a base-currency present value times spot is the quote-currency present value.
Example
using FinanceModels
eurusd = FX.Pair(:EUR, :USD)
usd = Yield.Constant(Continuous(0.05))
# market outright forwards (for points quotes, convert explicitly: spot .+ points ./ scale)
quotes = FX.Outright.(eurusd, [1.1055, 1.1113, 1.1225, 1.1459], [0.25, 0.5, 1.0, 2.0])
# closed-form implied EUR discount factors, bootstrapped through a spline
m = fit(FX.Forwards(eurusd, 1.10, usd, Spline.Cubic()), quotes, Fit.Bootstrap())
forward(m, 1.0) # ≈ 1.1225, and every quote reprices to zero PVUnexported API
FinanceModels.FX.BasisSwapLeg — Type
FX.BasisSwapLeg(pair, cashflows)The foreign-currency leg of a constant-notional cross-currency basis swap, materialized into deterministic cashflows: the periodic coupons — projection-curve forward plus the quoted basis spread, times the accrual fraction — and the final unit principal. Amounts are denominated in the base (foreign) currency of pair. Produced by FX.ParBasisSwap; see that docstring for the quoting assumptions.
Because the amounts are fixed once the projection curve is known, the leg is priceable by any single discount curve, which is what lets par basis-swap quotes flow through the standard curve-fitting machinery. Under an FX.Forwards model for the same pair, present_value(m, leg) is the leg's value in base-currency units, priced on m.foreign (the basis-adjusted/CSA discount curve); a mismatched pair throws an ArgumentError naming both pairs.
FinanceModels.FX.Converted — Type
FX.Converted(contract, pair, key)Wrap a contract whose cashflows are denominated in the base (foreign) currency of pair, converting each projected cashflow into the quote (domestic) currency at the arbitrage-free forward exchange rate for that cashflow's time: an amount paid at time t is multiplied by forward(fx, t), where fx is the FX.Forwards model looked up from the projection's model store under key — the same key/value pattern used by Bond.Floating for its reference rate.
pair declares the conversion the wrapper expects: if the model found under key is for a different pair — including the inverted one — projection throws an ArgumentError naming both pairs, rather than silently converting at a crossed or reciprocal rate. What the wrapper cannot verify is the wrapped contract's own denomination, since bare cashflows carry no currency; pair is your declaration that its cashflows are base-currency amounts.
Converting at CIP forwards and then discounting on the domestic curve is identical to discounting the unconverted cashflows on the FX model's foreign curve and converting the result at spot:
pv(domestic, Converted(c)) == spot * pv(foreign, c)so collateralized (hedged) cross-currency valuation is consistent whichever way it is sliced, and — when the FX model was fit to market forwards — automatically reflects the cross-currency basis.
Because the wrapper works on any projectable contract (including transducer-modified ones and Bond.Floating, whose reference model is resolved from the same store), cross-currency swaps are ordinary Composites — see the "Foreign Exchange" documentation page for a worked fixed-for-fixed and floating-floating example.
maturity(::Converted) delegates to the wrapped contract, so it is undefined (MethodError) when the wrapped contract is a transducer-modified Eduction: a transducer may alter cashflow times, so no general delegation through it is sound.
Examples
eurusd = FX.Pair(:EUR, :USD)
usd = Yield.Constant(Continuous(0.05))
eur = Yield.Constant(Continuous(0.03))
fx = FX.Forwards(eurusd, 1.08, usd, eur)
bond = Bond.Fixed(0.04, Periodic(1), 2.0) # a EUR-denominated bond
p = Projection(FX.Converted(bond, eurusd, "EURUSD"), Dict("EURUSD" => fx), CashflowProjection())
collect(p) # cashflows now in USD: each amount is multiplied by forward(fx, t)
pv(usd, p) # == 1.08 * pv(eur, bond)FinanceModels.FX.Forward — Type
FX.Forward(pair, strike, time)An outright FX forward contract: at time, receive one unit of the base currency of pair and pay strike units of the quote currency. Its value is expressed in the quote currency.
Under an FX.Forwards model m for the same pair:
present_value(m, c, cur_time = 0.0) = (forward(m, c.time) - c.strike) * discount(m.domestic, cur_time, c.time)A forward struck at the market outright has zero present value, which is how market quotes are represented — see FX.Outright. cur_time moves only the discounting date — the model is not rolled: forward(m, c.time) is still today's forward, the package's usual static-curve valuation convention. A forward that settled before cur_time is worth zero, consistent with how cashflows before cur_time are treated everywhere else in the package.
Pricing a contract whose pair differs from the model's pair throws an ArgumentError naming both pairs, rather than producing a silently inverted or crossed price.
Examples
eurusd = FX.Pair(:EUR, :USD)
m = FX.Forwards(eurusd, 1.10, Yield.Constant(Continuous(0.05)), Yield.Constant(Continuous(0.03)))
atm = FX.Forward(eurusd, forward(m, 1.0), 1.0)
present_value(m, atm) # 0.0
off = FX.Forward(eurusd, 1.10, 1.0) # struck below the forward
present_value(m, off) # (forward(m, 1.0) - 1.10) * discount at 1.0 > 0FinanceModels.FX.Forwards — Type
FX.Forwards(pair, spot, domestic, foreign)A covered-interest-parity (CIP) model of outright FX forward rates for the given FX.Pair:
forward(m, t) = spot * discount(foreign, t) / discount(domestic, t)where forward(m, t) is the arbitrage-free forward exchange rate (quote currency per one unit of base currency) for delivery at time t, and forward(m, 0) == spot. The model is also callable: m(t) == forward(m, t).
Arguments
pair: theFX.Pairthis model prices, e.g.FX.Pair(:EUR, :USD).spot: the current exchange rate (quote currency per unit of base currency); must be strictly positive and finite, or construction throws anArgumentError.domestic: the quote-currency discount curve (any yield model).foreign: the base-currency discount curve (any yield model). See the note on cross-currency basis below.
inv(m) returns the model for the flipped pair: spot is inverted and the curves swap roles, so forward(inv(m), t) == 1 / forward(m, t). Inverting is calibration-exact — a model fit to market quotes reprices the inverted quotes at 1 / F identically, with no refit.
There is deliberately no two-time forward(m, from, to) (a MethodError): under CIP the fair forward for delivery at to is forward(m, to) no matter when the exchange is contracted, so the yield-curve forward-forward form has no FX analog.
Cross-currency basis
foreign is defined by what reprices the FX market, not necessarily the base currency's own OIS or government curve. Since the collapse of covered interest parity against textbook curves (post-2008), market forwards embed a cross-currency basis. Two equivalent ways to handle it:
- Absorbed:
fitthe model to market forward quotes (seeFX.implied_zcb_quotesand thefitmethods forFX.Forwards). The fittedforeigncurve is the basis-adjusted ("CSA" / collateralized) discount curve, and the basis is embedded — hedge pricing with the fitted model is basis-consistent by construction. - Explicit: build
foreigncompositionally from a base-currency OIS curve and a fitted basis spread curve using ordinary curve arithmetic:foreign_ois + basis(zero-rate addition viaCompositeYield). The basis curve is then a first-class, inspectable object:basis = implied_foreign - foreign_ois.
Examples
eurusd = FX.Pair(:EUR, :USD)
usd = Yield.Constant(Continuous(0.05))
eur = Yield.Constant(Continuous(0.03))
m = FX.Forwards(eurusd, 1.10, usd, eur)
forward(m, 0.0) # 1.10 (spot)
forward(m, 1.0) # 1.10 * exp(0.05 - 0.03) ≈ 1.12222
m(1.0) # same as forward(m, 1.0)
# explicit −20bp EUR/USD cross-currency basis on the EUR leg:
m_basis = FX.Forwards(eurusd, 1.10, usd, eur + Yield.Constant(Continuous(-0.002)))FinanceModels.FX.Pair — Type
FX.Pair(base, quote)A currency pair. FX.Pair(:EUR, :USD) denotes the price of one unit of the base currency (:EUR) expressed in units of the quote — also called domestic or terms — currency (:USD), matching the market's "EURUSD" naming.
Every FX contract and model carries its pair, so direction errors — the classic FX bug — surface as immediate, descriptive ArgumentErrors rather than silently inverted prices: an FX.Forwards model only prices contracts denominated in an equal pair.
inv flips the pair: inv(FX.Pair(:EUR, :USD)) == FX.Pair(:USD, :EUR).
Currencies are usually Symbols, but any values work — strings (including the InlineStrings codes CSV readers produce) or ISO 4217 numeric codes (FX.Pair(978, 840)). Pairs compare by content: string-typed currencies are equal whenever their characters match, regardless of storage type — FX.Pair(String3("EUR"), String3("USD")) == FX.Pair("EUR", "USD") — because AbstractString equality and hashing are content-based. A Symbol pair and a string pair are not equal, however; pick one convention for a given system, normalizing at the data boundary (e.g. Symbol.(codes)) if sources disagree.
A degenerate pair such as FX.Pair(:USD, :USD) is deliberately permitted: with unit spot and identical curves it is the identity pair (forward ≡ 1), which lets generic multi-currency code route domestic contracts through the same FX.Converted machinery as foreign ones.
Examples
julia> FX.Pair(:EUR, :USD)
FX.Pair(:EUR, :USD)
julia> inv(FX.Pair(:EUR, :USD))
FX.Pair(:USD, :EUR)FinanceModels.FX.Outright — Method
FX.Outright(pair, forward_rate, time)A Quote for an outright FX forward: entering a forward at the market rate costs nothing, so this returns a zero-price Quote on an FX.Forward struck at forward_rate. This is the analog of par-bond quoting for FX and is the input to the fit methods for FX.Forwards.
Use broadcasting to create a set of quotes: FX.Outright.(pair, rates, times).
The interbank market often quotes forwards as points over spot in pair-specific pip units (1/10_000 for most pairs, 1/100 for JPY-quoted pairs). There is deliberately no points constructor — a guessed scale would be the classic silent JPY error, and a required one would only rename the arithmetic — so convert explicitly at the data boundary, where the pip factor stays visible: FX.Outright.(pair, spot .+ points ./ 10_000, times).
Examples
julia> q = FX.Outright(FX.Pair(:EUR, :USD), 1.1225, 1.0);
julia> q.price, q.instrument.strike, q.instrument.time
(0.0, 1.1225, 1.0)FinanceModels.FX.ParBasisSwap — Method
FX.ParBasisSwap(pair, spread, maturity; reference, frequency=Periodic(4))A Quote for a constant-notional cross-currency basis swap struck at par: receive the foreign (base-currency) leg paying reference-curve forwards plus the quoted basis spread (a decimal, e.g. -0.0015 for −15bp) on a unit foreign notional, pay the domestic (quote-currency) leg flat, with notionals exchanged at inception and maturity.
Quoting assumption (standard for collateralized, OIS-discounted swaps): the domestic leg pays the forwards of the same curve used for domestic discounting, so it is worth par and drops out of the calibration. What remains is the base-currency par condition
Σᵢ (fᵢ + spread) * δᵢ * DF_f(tᵢ) + DF_f(T) = 1where fᵢ is the (known) simple annualized reference forward over each coupon's accrual window, δᵢ the window's length (1/frequency except for a short first stub), and DF_f the basis-adjusted (CSA) discount curve being calibrated. The returned quote is therefore Quote(1.0, FX.BasisSwapLeg(pair, cashflows)): a deterministic base-currency cashflow strip that must price to par on the curve being fit. Each coupon is fix-in-advance at the reference forward over its accrual period, exactly matching how Bond.Floating projects, so the strip equals the projected floating leg.
A maturity that is not a whole number of periods keeps the schedule anchored at maturity (regular 1/frequency periods counted backward — the shape Bond.coupon_times produces) and makes the first period a short stub. The stub coupon accrues its actual length at the forward over its true window [0, t₁] (never a negative time), compound-accrued so that a zero-spread leg on its own curve telescopes to exactly par — the same stub convention as ParYield's stub par bonds and Bond.Floating's stub coupons. Schedules irregular anywhere else (long stubs, back stubs, custom rolls) are out of scope.
Because the strip is deterministic, these quotes flow through the same fit methods as FX.Outright quotes, and the two can be mixed in a single calibration — forward points for the liquid short end, basis swaps for the long end:
eurusd = FX.Pair(:EUR, :USD)
sofr = Yield.Constant(0.05) # domestic (USD) discount curve
estr = Yield.Constant(0.03) # foreign (EUR) *projection* curve, already fitted
quotes = [
FX.Outright.(eurusd, [1.1055, 1.1113], [0.25, 0.5]);
FX.ParBasisSwap.(eurusd, [-0.0012, -0.0018], [2.0, 5.0]; reference = estr)
]
m = fit(FX.Forwards(eurusd, 1.10, sofr, Spline.Linear()), quotes, Fit.Bootstrap())Prefer a local interpolation (such as Spline.Linear, as above) when the quote set includes coupon-bearing instruments: their cashflows fall between knots, and a global spline reshapes already-solved segments as later knots are added, silently drifting solved quotes off par.
This is the constant-notional representation. Interbank basis swaps are quoted on the mark-to-market (resetting-notional) structure, in which the flat domestic leg's notional resets to spot each period while the spread leg's notional stays constant. Under the same quoting assumption as above and deterministic curves, the two par spreads are exactly equal: valued at CIP forwards, the resetting leg is a telescoping strip of one-period loans at its own curve's forwards — each worth par — so it drops out period by period just as the constant-notional leg drops out in one step, leaving the identical par condition (asserted to machine precision in the test suite). Quoted MTM spreads therefore calibrate this representation without approximation. The residual real-world difference is the FX–rates convexity of the resetting notional — identically zero under deterministic curves — which is one reason the MTM contract is deliberately not modeled yet; see the "Foreign Exchange" documentation page.
FinanceModels.FX.implied_zcb_quotes — Method
FX.implied_zcb_quotes(quotes, spot, domestic)
FX.implied_zcb_quotes(m::FX.Forwards, quotes)Transform FX forward quotes (as produced by FX.Outright) into zero-coupon-bond Quotes for the implied base-currency discount curve, given the spot rate and the domestic (quote-currency) discount curve.
Because a forward's price is (F(t) - K) * DF_d(t), the implied base-currency discount factor at each quote's maturity is closed-form:
DF_f(t) = (K * DF_d(t) + price) / spot(for the usual zero-price outright quotes this is F(t) * DF_d(t) / spot). The returned quotes can be fed to any curve-fitting method — this is how the fit methods for FX.Forwards construct the foreign curve, and the resulting curve is the basis-adjusted (CSA) discount curve described in FX.Forwards.
All quotes must share a single currency pair (and match the model's pair in the second form); mixed pairs throw an ArgumentError, since they would otherwise blend into a meaningless curve. A quote implying a non-positive discount factor (a corrupt price, points scale, or spot) also throws an ArgumentError naming the offending maturity, rather than letting NaNs surface inside a downstream curve fit.
Examples
eurusd = FX.Pair(:EUR, :USD)
usd = Yield.Constant(Continuous(0.05))
quotes = FX.Outright.(eurusd, [1.1055, 1.1113, 1.1225], [0.25, 0.5, 1.0])
implied = FX.implied_zcb_quotes(quotes, 1.10, usd) # Vector of ZCB-price Quotes
eur = fit(Spline.Cubic(), implied, Fit.Bootstrap()) # the implied EUR discount curvePlease open an issue if you encounter any issues or confusion with the package.