financialkit.fixed_income#

Bond cash flows, prices and yield to maturity (scipy.optimize.brentq), accrued interest and STRIPS; Macaulay, modified and Fisher-Weil duration, convexity and key rate durations; Lutz forward rates; zero curves bootstrapped from bond prices or par rates, Carleton-Cooper discount factors and McCulloch’s spline discount function (numpy.linalg.lstsq); Nelson-Siegel and Svensson fits (scipy.optimize.least_squares) and the principal components of curve moves (numpy.linalg.eigh); Redington immunization and the Fong-Vasicek \(M^2\) bound; and interest-rate and currency swaps.

financialkit.fixed_income: bonds, duration, yield curves, immunization and swaps.

Bond cash flows, prices and yield to maturity (scipy.optimize.brentq()), accrued interest and STRIPS; Macaulay, modified and Fisher-Weil duration, convexity and Ho’s key rate durations; Lutz forward rates, bootstrapping a zero curve from bond prices or par rates (hand-written, solving each node with brentq()), Carleton-Cooper discount factors and McCulloch’s spline discount function (numpy.linalg.lstsq()), Nelson-Siegel and Svensson fits (scipy.optimize.least_squares()) and the principal components of curve moves (numpy.linalg.eigh()); Redington immunization and the Fong-Vasicek \(M^2\) bound; and interest-rate and currency swaps valued as pairs of bonds.

Every curve is a YieldCurve of continuously compounded zero rates, and every function that takes a curve also accepts a flat rate. Times are year fractions; converting dates under a day-count convention belongs to financialkit.time_value.

class financialkit.fixed_income.BondRisk(price, macaulay_duration, modified_duration, convexity, yield_=0.0, compounding=None)[source]#

Bases: object

Price and first/second-order yield sensitivities of a cash-flow stream.

Parameters:
compounding: object = None#

Compounding frequency of the yield (None = continuous).

Type:

int or None

convexity: float#

\(\frac{1}{P}\frac{d^2P}{dy^2}\).

Type:

float

property dollar_duration: float#

\(-dP/dy = D_{mod} P\).

Type:

float

property dv01: float#

Price change for a one-basis-point fall in yield.

Type:

float

macaulay_duration: float#

Present-value-weighted average time to payment.

Type:

float

modified_duration: float#

\(-\frac{1}{P}\frac{dP}{dy}\); equals Macaulay under continuous compounding.

Type:

float

price: float#

Present value at the given yield.

Type:

float

price_change(dy, order=2)[source]#

Duration (order=1) or duration-plus-convexity (order=2) estimate of the price change for a yield move dy.

Parameters:

order (int)

yield_: float = 0.0#

The yield the measures were computed at.

Type:

float

class financialkit.fixed_income.CashFlows(times, amounts)[source]#

Bases: object

A stream of fixed amounts paid at times (years from today).

Parameters:
amounts: ndarray#

Payment amounts.

Type:

ndarray, shape (n,)

present_value(curve)[source]#

Sum of the amounts discounted on curve.

Return type:

float

Parameters:

curve (YieldCurve | float)

scaled(factor)[source]#

The same dates with every amount multiplied by factor.

Return type:

CashFlows

Parameters:

factor (float)

times: ndarray#

Payment times, ascending.

Type:

ndarray, shape (n,)

class financialkit.fixed_income.CurveFitResult(curve, params, residuals, model='')[source]#

Bases: object

A fitted term structure and its goodness of fit.

Parameters:
curve: YieldCurve#

The fitted curve.

Type:

YieldCurve

model: str = ''#

"nelson_siegel", "svensson" or "mcculloch_spline".

Type:

str

params: ndarray#

Fitted parameters, in the model’s documented order.

Type:

ndarray

residuals: ndarray#

Fitted minus observed values (yields or prices).

Type:

ndarray

property rmse: float#

Root-mean-square residual.

Type:

float

class financialkit.fixed_income.CurvePCAResult(tenors, eigenvalues, loadings, scores)[source]#

Bases: object

Principal components of yield-curve changes (Litterman-Scheinkman).

Parameters:
eigenvalues: ndarray#

Component variances, descending.

Type:

ndarray, shape (n,)

property explained_variance_ratio: ndarray#

Share of total variance carried by each component.

Type:

ndarray

loadings: ndarray#

Unit eigenvectors as columns, loadings[:, k] pairs with eigenvalues[k]. Signs are fixed so the first component loads positively on average and the second rises with maturity.

Type:

ndarray, shape (n, n)

scores: ndarray#

The centred changes projected on the components.

Type:

ndarray, shape (n_obs, n)

tenors: ndarray#

Maturities of the yield columns.

Type:

ndarray, shape (n,)

class financialkit.fixed_income.FlatCurve(rate)[source]#

Bases: YieldCurve

The same continuously compounded rate at every maturity.

Parameters:

rate (float)

zero_rate(t)[source]#

Continuously compounded zero rate for maturity t (years); broadcasts.

class financialkit.fixed_income.ImmunizationResult(holdings, assets, liabilities, curve, pv_assets=0.0, pv_liabilities=0.0, duration_assets=0.0, duration_liabilities=0.0, convexity_assets=0.0, convexity_liabilities=0.0, extra=<factory>)[source]#

Bases: object

Asset holdings that immunize a liability stream against parallel shifts (Redington).

Parameters:
assets: list#

Each asset’s cash flows per unit.

Type:

list of CashFlows

convexity_assets: float = 0.0#

Convexity of the asset portfolio.

Type:

float

convexity_liabilities: float = 0.0#

Convexity of the liabilities.

Type:

float

curve: YieldCurve#

The curve the match was made on.

Type:

YieldCurve

duration_assets: float = 0.0#

Fisher-Weil duration of the asset portfolio.

Type:

float

duration_liabilities: float = 0.0#

Fisher-Weil duration of the liabilities.

Type:

float

extra: dict#

Free-form diagnostics.

Type:

dict

holdings: ndarray#

Units of each asset held.

Type:

ndarray, shape (n_assets,)

liabilities: CashFlows#

The liability stream.

Type:

CashFlows

property portfolio: CashFlows#

The combined asset portfolio.

Type:

CashFlows

pv_assets: float = 0.0#

Present value of the asset portfolio.

Type:

float

pv_liabilities: float = 0.0#

Present value of the liabilities.

Type:

float

property redington_satisfied: bool#

Asset convexity exceeds liability convexity, so the surplus has a local minimum at zero shift.

Type:

bool

surplus(shifts)[source]#

Asset minus liability value after parallel shifts shifts of the curve.

Return type:

ndarray

class financialkit.fixed_income.KeyRateDurationResult(key_tenors, durations, parallel_duration, price)[source]#

Bases: object

Ho’s key rate durations of a cash-flow stream.

Parameters:
durations: ndarray#

Duration with respect to each key rate.

Type:

ndarray, shape (k,)

key_tenors: ndarray#

Key maturities.

Type:

ndarray, shape (k,)

parallel_duration: float#

Effective duration under a parallel shift; equals durations.sum() to first order.

Type:

float

price: float#

Present value on the unshocked curve.

Type:

float

class financialkit.fixed_income.NelsonSiegelCurve(beta0, beta1, beta2, tau)[source]#

Bases: YieldCurve

Nelson and Siegel’s (1987) parsimonious yield curve.

\[r(t) = \beta_0 + \beta_1 \frac{1 - e^{-t/\tau}}{t/\tau} + \beta_2 \left(\frac{1 - e^{-t/\tau}}{t/\tau} - e^{-t/\tau}\right)\]

\(\beta_0\) is the long rate, \(\beta_0 + \beta_1\) the short rate, \(\beta_2\) a hump whose position \(\tau\) sets.

Examples

>>> curve = NelsonSiegelCurve(0.05, -0.02, 0.01, 2.0)
>>> round(float(curve.zero_rate(0.0)), 6), round(float(curve.zero_rate(1e6)), 6)
(0.03, 0.05)
Parameters:
instantaneous_forward(t, h=0.0)[source]#

\(f(t) = \beta_0 + \beta_1 e^{-t/\tau} + \beta_2 (t/\tau) e^{-t/\tau}\), in closed form.

Parameters:

h (float)

property params: ndarray#

[beta0, beta1, beta2, tau].

Type:

ndarray

zero_rate(t)[source]#

Continuously compounded zero rate for maturity t (years); broadcasts.

class financialkit.fixed_income.ShiftedCurve(base, delta)[source]#

Bases: YieldCurve

A base curve plus a spread on its zero rates: a parallel shift (float) or a shaped shock (callable).

Parameters:
zero_rate(t)[source]#

Continuously compounded zero rate for maturity t (years); broadcasts.

class financialkit.fixed_income.SplineDiscountCurve(coefficients, knots, degree=3)[source]#

Bases: YieldCurve

McCulloch’s spline discount function \(d(t) = 1 + \sum_j a_j g_j(t)\).

The basis is the truncated power basis of the given degree, \(t, t^2, \dots, t^p\) and \((t - \kappa)_+^p\) at each interior knot \(\kappa\), so \(d(0) = 1\) holds by construction.

Parameters:
  • coefficients (array_like) – The \(a_j\), polynomial terms first.

  • knots (array_like) – Interior knots (years).

  • degree (int) – 2 (McCulloch, 1971) or 3 (McCulloch, 1975).

static basis(t, knots, degree)[source]#

Truncated power basis at t, shape t.shape + (degree + len(knots),).

Return type:

ndarray

Parameters:

degree (int)

discount(t)[source]#

Discount factor \(P(0, t) = e^{-r(t) t}\); broadcasts.

zero_rate(t)[source]#

Continuously compounded zero rate for maturity t (years); broadcasts.

class financialkit.fixed_income.SvenssonCurve(beta0, beta1, beta2, beta3, tau1, tau2)[source]#

Bases: YieldCurve

Svensson’s (1994) extension of Nelson-Siegel with a second hump.

\[r(t) = \text{NS}(t; \beta_0, \beta_1, \beta_2, \tau_1) + \beta_3 \left(\frac{1 - e^{-t/\tau_2}}{t/\tau_2} - e^{-t/\tau_2}\right)\]
Parameters:
instantaneous_forward(t, h=0.0)[source]#

Instantaneous forward rate \(f(t) = \partial_t [r(t) t]\), by central differences.

Parameters:

h (float)

property params: ndarray#

[beta0, beta1, beta2, beta3, tau1, tau2].

Type:

ndarray

zero_rate(t)[source]#

Continuously compounded zero rate for maturity t (years); broadcasts.

class financialkit.fixed_income.SwapValuation(value, receive_leg, pay_leg, kind='')[source]#

Bases: object

Value of a swap as the difference of two bonds: the leg received less the leg paid.

Parameters:
kind: str = ''#

"pay_fixed", "receive_fixed", "receive_foreign" or "pay_foreign".

Type:

str

pay_leg: float#

Present value of the leg paid.

Type:

float

receive_leg: float#

Present value of the leg received.

Type:

float

value: float#

receive_leg - pay_leg, in the valuation currency.

Type:

float

class financialkit.fixed_income.YieldCurve[source]#

Bases: ABC

A term structure of continuously compounded zero rates.

Subclasses implement zero_rate(); discount factors, forward rates and par yields are derived here, so every curve answers them the same way.

discount(t)[source]#

Discount factor \(P(0, t) = e^{-r(t) t}\); broadcasts.

forward_rate(t1, t2)[source]#

Continuously compounded forward rate between t1 and t2 (Lutz, 1940).

\[f(t_1, t_2) = \frac{r(t_2) t_2 - r(t_1) t_1}{t_2 - t_1}\]
instantaneous_forward(t, h=1e-05)[source]#

Instantaneous forward rate \(f(t) = \partial_t [r(t) t]\), by central differences.

Parameters:

h (float)

par_yield(maturity, freq=2)[source]#

Coupon rate that prices a bond (or the fixed leg of a swap) at par.

\[c = m \, \frac{1 - P(0, T)}{\sum_i P(0, t_i)}\]

with coupons at T, T - 1/m, ... down to the first date after 0.

Parameters:

freq (int)

shifted(delta)[source]#

This curve with delta added to every zero rate (a float, or a callable of t).

Return type:

ShiftedCurve

abstractmethod zero_rate(t)[source]#

Continuously compounded zero rate for maturity t (years); broadcasts.

class financialkit.fixed_income.ZeroCurve(tenors, rates)[source]#

Bases: YieldCurve

Zero rates at node maturities, linearly interpolated and flat beyond the ends.

Linear interpolation in the zero rate is the convention of Hull’s bootstrapping examples (Options, Futures, and Other Derivatives, 9th ed., Sec. 4.5), and is what bootstrap_zero_curve() solves under.

Parameters:
  • tenors (array_like, shape (n,)) – Strictly increasing positive maturities in years.

  • rates (array_like, shape (n,)) – Continuously compounded zero rates at tenors.

Examples

>>> curve = ZeroCurve([1.0, 2.0], [0.03, 0.04])
>>> round(float(curve.zero_rate(1.5)), 6)
0.035
>>> round(float(curve.forward_rate(1.0, 2.0)), 6)
0.05
zero_rate(t)[source]#

Continuously compounded zero rate for maturity t (years); broadcasts.

financialkit.fixed_income.accrued_interest(face, coupon_rate, maturity, freq=2)[source]#

Coupon interest earned since the last coupon date, owed by the buyer to the seller.

The accrual fraction is the elapsed share of the current coupon period measured in years; how calendar dates become those years is a day-count question for financialkit.time_value; for accrual between calendar dates use financialkit.time_value.accrued_interest().

Return type:

float

Parameters:

Examples

>>> accrued_interest(100.0, 0.08, 1.375, freq=2)  # a quarter of the way into a 4.0 coupon
1.0
financialkit.fixed_income.as_curve(curve)[source]#

Return curve itself, or a FlatCurve if it is a number.

Return type:

YieldCurve

Parameters:

curve (YieldCurve | float)

financialkit.fixed_income.bond_cash_flows(face, coupon_rate, maturity, freq=2)[source]#

Cash flows of a fixed-coupon bullet bond.

Parameters:
  • face (float) – Principal repaid at maturity.

  • coupon_rate (float) – Annual coupon rate (decimal); each coupon is face * coupon_rate / freq. Zero gives a zero-coupon bond with a single payment.

  • maturity (float) – Years to maturity.

  • freq (int) – Coupons per year.

Return type:

CashFlows

Returns:

CashFlows

Examples

>>> cf = bond_cash_flows(100.0, 0.06, 1.5)
>>> cf.times.tolist(), cf.amounts.tolist()
([0.5, 1.0, 1.5], [3.0, 3.0, 103.0])
financialkit.fixed_income.bond_price(face, coupon_rate, maturity, y, freq=2, compounding=None)[source]#

Dirty price of a fixed-coupon bond at yield y; broadcasts over y.

Hull’s convention (continuous compounding, compounding=None) is the default; pass compounding=freq for the street convention of a yield compounded at the coupon frequency.

Examples

Hull (9th ed., Sec. 4.4): a 2-year 6% semiannual bond yielding 6.76% continuously compounded is worth 98.39.

>>> round(bond_price(100.0, 0.06, 2.0, 0.0676), 2)
98.39
>>> round(bond_price(100.0, 0.05, 10.0, 0.05, compounding=2), 10)  # at par
100.0
Parameters:
financialkit.fixed_income.bond_risk(cash_flows, y, compounding=None)[source]#

Price, Macaulay and modified duration, and convexity at a single yield.

Parameters:
  • cash_flows (CashFlows)

  • y (float) – Yield (decimal).

  • compounding (int | None) – Compounding frequency of y; None for continuous.

Return type:

BondRisk

Returns:

BondRisk

Examples

Hull (9th ed., Table 4.6 and Sec. 4.9): a 3-year 10% semiannual bond at a 12% continuously compounded yield.

>>> from financialkit.fixed_income.systems.bonds import bond_cash_flows
>>> risk = bond_risk(bond_cash_flows(100.0, 0.10, 3.0), 0.12)
>>> round(risk.price, 3), round(risk.macaulay_duration, 3), round(risk.convexity, 2)
(94.213, 2.653, 7.57)
financialkit.fixed_income.bootstrap_par_curve(maturities, par_rates, freq=2)[source]#

Zero curve from par yields (or swap rates): every bond prices at par.

Return type:

ZeroCurve

Parameters:

freq (int)

Examples

A flat 5% semiannual par curve is a flat zero curve at \(2 \ln 1.025\):

>>> curve = bootstrap_par_curve([1, 2, 5, 10], [0.05] * 4)
>>> bool(abs(curve.rates - 2 * np.log(1.025)).max() < 1e-9)
True
financialkit.fixed_income.bootstrap_zero_curve(maturities, coupon_rates, prices, freq=2, face=100.0)[source]#

Strip a zero curve from coupon bond prices, shortest maturity first.

Each bond adds one node at its maturity. Its earlier cash flows are discounted on the curve built so far, linearly interpolated in the zero rate; when a coupon falls between the last known node and the new one, its rate depends on the unknown node too, so the node is solved for with scipy.optimize.brentq() rather than by back-substitution.

Parameters:
  • maturities (array_like, shape (n,)) – Distinct bond maturities (years).

  • coupon_rates (array_like, shape (n,)) – Annual coupon rates (0 for zero-coupon bonds).

  • prices (array_like, shape (n,)) – Dirty prices per face.

  • freq (int) – Coupon frequency.

  • face (float)

Return type:

ZeroCurve

Returns:

ZeroCurve – Nodes at the bond maturities.

Examples

Hull (9th ed., Tables 4.3-4.4):

>>> curve = bootstrap_zero_curve([0.25, 0.5, 1.0, 1.5, 2.0], [0, 0, 0, 0.08, 0.12], [97.5, 94.9, 90.0, 96.0, 101.6])
>>> (100 * curve.rates).round(3).tolist()
[10.127, 10.469, 10.536, 10.681, 10.808]
financialkit.fixed_income.carleton_cooper_discount_factors(times, cash_flow_matrix, prices)[source]#

Discount factors by regressing bond prices on their cash flows.

Bond \(i\) pays \(C_{ik}\) at the common dates \(t_k\), so \(P_i = \sum_k C_{ik} d_k\). With as many bonds as dates this is a triangular system (bootstrapping); with more bonds it is solved in the least-squares sense, which averages out pricing noise.

Parameters:
  • times (array_like, shape (m,)) – Payment dates shared by the bonds.

  • cash_flow_matrix (array_like, shape (n, m)) – Row i holds bond i’s payment on each date (0 where none).

  • prices (array_like, shape (n,))

Return type:

CurveFitResult

Returns:

CurveFitResult – params are the discount factors; curve the implied zero curve.

Examples

>>> res = carleton_cooper_discount_factors([1.0, 2.0], [[105.0, 0.0], [6.0, 106.0]], [100.0, 99.0])
>>> res.params.round(6).tolist()
[0.952381, 0.880054]
financialkit.fixed_income.clean_price(face, coupon_rate, maturity, y, freq=2, compounding=None)[source]#

Quoted (clean) price: the dirty price less accrued interest; broadcasts over y.

The dirty price jumps down by a coupon on each payment date and climbs between them; the clean price removes that sawtooth.

Parameters:
financialkit.fixed_income.convexity(cash_flows, y, compounding=None)[source]#

Convexity \(\frac{1}{P}\frac{d^2P}{dy^2}\) at yield y.

Return type:

float

Parameters:
financialkit.fixed_income.coupon_times(maturity, freq)[source]#

Coupon dates maturity - k / freq > 0, ascending; the first period may be short.

Return type:

ndarray

Parameters:

Examples

>>> coupon_times(1.25, 2).tolist()
[0.25, 0.75, 1.25]
financialkit.fixed_income.currency_swap_value(domestic_curve, foreign_curve, spot, domestic_notional, foreign_notional, domestic_rate, foreign_rate, maturity, freq=1, receive_foreign=True)[source]#

Value, in domestic currency, of a fixed-for-fixed currency swap with exchange of principal.

Each leg is a bond in its own currency discounted on its own curve; the foreign bond is converted at the spot rate.

Parameters:
  • domestic_curve (YieldCurve | float)

  • foreign_curve (YieldCurve | float)

  • spot (float) – Domestic currency per unit of foreign currency.

  • domestic_notional (float)

  • foreign_notional (float)

  • domestic_rate (float) – Annual fixed coupon rates on each leg.

  • foreign_rate (float) – Annual fixed coupon rates on each leg.

  • maturity (float)

  • freq (int)

  • receive_foreign (bool) – Value to the party receiving the foreign leg (True) or paying it.

Return type:

SwapValuation

Returns:

SwapValuation

Examples

Hull (9th ed., Example 7.3): receive 5% on 1,200 million yen and pay 8% on $10 million annually for 3 years, with flat rates of 4% (yen) and 9% (dollar) and 110 yen per dollar.

>>> v = currency_swap_value(0.09, 0.04, 1 / 110, 10.0, 1200.0, 0.08, 0.05, 3.0)
>>> round(v.value, 3)
1.543
financialkit.fixed_income.effective_duration(cash_flows, curve, bump=0.0001)[source]#

Duration by repricing under parallel curve shifts of +/- bump (central difference).

Works for any curve and agrees with fisher_weil_duration() to \(O(\text{bump}^2)\).

Return type:

float

Parameters:
financialkit.fixed_income.fisher_weil_convexity(cash_flows, curve)[source]#

Convexity under parallel shifts of a zero curve, \(\frac{1}{P}\sum_i t_i^2 c_i P(0, t_i)\).

Return type:

float

Parameters:
financialkit.fixed_income.fisher_weil_duration(cash_flows, curve)[source]#

Fisher-Weil duration: Macaulay’s weights computed on a zero curve.

\[D_{FW} = \frac{1}{P} \sum_i t_i \, c_i \, P(0, t_i), \qquad P = \sum_i c_i \, P(0, t_i)\]

It is exactly \(-\frac{1}{P}\frac{dP}{ds}\) for a parallel shift \(s\) of the continuously compounded zero curve.

Examples

>>> from financialkit.fixed_income.core.base import ZeroCurve
>>> from financialkit.fixed_income.systems.bonds import bond_cash_flows
>>> cf = bond_cash_flows(100.0, 0.05, 3.0, freq=1)
>>> round(fisher_weil_duration(cf, ZeroCurve([1, 2, 3], [0.02, 0.03, 0.04])), 4)
2.8588
Return type:

float

Parameters:
financialkit.fixed_income.fit_mcculloch_spline(bonds, prices, knots=None, degree=3)[source]#

Fit McCulloch’s spline discount function to bond prices by linear least squares.

Because the discount function is linear in its coefficients, so is every bond price: \(P_i - \sum_k c_{ik} = \sum_j a_j \sum_k c_{ik} g_j(t_{ik})\).

Parameters:
  • bonds (list)

  • prices (array_like, shape (n,)) – Dirty prices.

  • knots (array_like, optional) – Interior knots. By default about \(\sqrt{n} - 1\) of them at quantiles of the maturities, McCulloch’s rule of thumb.

  • degree (int) – Spline degree (2 or 3).

Return type:

CurveFitResult

Returns:

CurveFitResult – curve is a SplineDiscountCurve; residuals are in price units.

Examples

>>> from financialkit.fixed_income.core.base import FlatCurve
>>> bonds = [bond_cash_flows(100.0, 0.05, T) for T in np.arange(1, 11)]
>>> prices = [b.present_value(FlatCurve(0.04)) for b in bonds]
>>> res = fit_mcculloch_spline(bonds, prices)
>>> bool(abs(res.curve.zero_rate(5.0) - 0.04) < 1e-4)
True
financialkit.fixed_income.fit_nelson_siegel(tenors, yields, tau_grid=None)[source]#

Fit a NelsonSiegelCurve to zero yields.

For fixed \(\tau\) the betas are a linear least-squares problem, so the best \(\tau\) on a grid gives the starting point and scipy.optimize.least_squares() then refines all four parameters.

Parameters:
  • tenors (array_like, shape (n,)) – Maturities and continuously compounded zero yields.

  • yields (array_like, shape (n,)) – Maturities and continuously compounded zero yields.

  • tau_grid (array_like, optional) – Candidate decay parameters; default 60 points from 0.1 to 15 years.

Return type:

CurveFitResult

Returns:

CurveFitResult

Examples

>>> t = np.array([0.25, 0.5, 1, 2, 3, 5, 7, 10, 20, 30])
>>> res = fit_nelson_siegel(t, NelsonSiegelCurve(0.05, -0.02, 0.015, 1.8).zero_rate(t))
>>> res.params.round(6).tolist()
[0.05, -0.02, 0.015, 1.8]
financialkit.fixed_income.fit_svensson(tenors, yields, tau_grid=None)[source]#

Fit a SvenssonCurve to zero yields.

The same two-stage scheme as fit_nelson_siegel(), with a grid over pairs \(\tau_1 < \tau_2\) (which also keeps the two hump terms identifiable).

Return type:

CurveFitResult

Examples

>>> t = np.array([0.25, 0.5, 1, 2, 3, 4, 5, 7, 10, 15, 20, 30])
>>> true = SvenssonCurve(0.045, -0.015, -0.02, 0.03, 1.0, 6.0)
>>> bool(fit_svensson(t, true.zero_rate(t)).rmse < 1e-7)
True
financialkit.fixed_income.fong_vasicek_bound(m2, K)[source]#

Fong-Vasicek lower bound \(-\tfrac12 K M^2\) on the relative change in horizon value.

Holds for a portfolio whose Fisher-Weil duration equals the horizon when the shock to instantaneous forward rates has slope at most K.

Return type:

float

Parameters:
financialkit.fixed_income.forward_rates(tenors, zero_rates)[source]#

Lutz’s forward rates for each period between consecutive tenors.

\[f_{k} = \frac{r_k t_k - r_{k-1} t_{k-1}}{t_k - t_{k-1}}, \qquad t_0 = 0\]

The first period’s forward is the first zero rate. Under the expectations hypothesis these are the market’s forecasts of future short rates, and the zero rate is their time average.

Parameters:
  • tenors (array_like, shape (n,)) – Strictly increasing positive maturities.

  • zero_rates (array_like, shape (n,)) – Continuously compounded zero rates.

Return type:

ndarray

Returns:

ndarray, shape (n,)

Examples

Hull (9th ed., Table 4.5):

>>> forward_rates([1, 2, 3, 4, 5], [0.03, 0.04, 0.046, 0.05, 0.053]).round(4).tolist()
[0.03, 0.05, 0.058, 0.062, 0.065]
financialkit.fixed_income.horizon_value(cash_flows, curve, horizon)[source]#

Value of cash_flows carried forward to horizon: present value divided by \(P(0, H)\).

Return type:

float

Parameters:
financialkit.fixed_income.interest_rate_swap_value(curve, notional, fixed_rate, maturity, freq=2, pay_fixed=True, floating_rate=None)[source]#

Value of a fixed-for-floating interest-rate swap.

The fixed leg is a bond with coupon fixed_rate. The floating leg is worth par immediately after a reset, so today it is worth the notional plus the already-fixed next coupon, discounted from the next payment date. Payments on both legs fall on maturity - k/freq.

Parameters:
  • curve (YieldCurve | float) – Discount curve.

  • notional (float)

  • fixed_rate (float) – Annual fixed rate.

  • maturity (float) – Remaining life (years).

  • freq (int) – Payments per year on both legs.

  • pay_fixed (bool) – Value to the fixed-rate payer (True) or receiver.

  • floating_rate (float | None) – Annual rate already fixed for the next floating payment. May be omitted only on a reset date (maturity a whole number of periods), where the floating leg is worth exactly the notional.

Return type:

SwapValuation

Returns:

SwapValuation

Examples

Hull (9th ed., Example 7.1): receive 8% and pay LIBOR on $100 million, 1.25 years left, LIBOR zero rates 10%, 10.5% and 11% at 3, 9 and 15 months, and 10.2% fixed for the next payment.

>>> from financialkit.fixed_income.core.base import ZeroCurve
>>> curve = ZeroCurve([0.25, 0.75, 1.25], [0.10, 0.105, 0.11])
>>> v = interest_rate_swap_value(curve, 100.0, 0.08, 1.25, pay_fixed=False, floating_rate=0.102)
>>> round(v.receive_leg, 3), round(v.pay_leg, 3), round(v.value, 3)
(98.238, 102.505, -4.267)
financialkit.fixed_income.key_rate_durations(cash_flows, curve, key_tenors, bump=0.0001)[source]#

Ho’s key rate durations: sensitivity to each key maturity’s zero rate.

Each key rate is shifted by +/- bump with the triangular shape of key_rate_weights(), and the bond is repriced on the shocked curve.

Parameters:
  • cash_flows (CashFlows)

  • curve (YieldCurve | float)

  • key_tenors (array_like) – Key maturities (years), strictly increasing.

  • bump (float) – Size of each shock in rate units.

Return type:

KeyRateDurationResult

Returns:

KeyRateDurationResult

Examples

>>> from financialkit.fixed_income.systems.bonds import bond_cash_flows
>>> res = key_rate_durations(bond_cash_flows(100.0, 0.05, 10.0), 0.04, [2.0, 5.0, 10.0, 30.0])
>>> bool(abs(res.durations.sum() - res.parallel_duration) < 1e-6)
True
financialkit.fixed_income.key_rate_weights(t, key_tenors)[source]#

Ho’s triangular key-rate shock shapes evaluated at maturities t.

Shock k is 1 at key_tenors[k], falls linearly to 0 at the neighbouring keys and is flat beyond the first and last keys. The shapes sum to 1 at every maturity, so the key rate durations add up to the parallel duration.

Parameters:
  • t (array_like, shape (n,))

  • key_tenors (array_like, shape (k,)) – Strictly increasing key maturities.

Return type:

ndarray

Returns:

ndarray, shape (k, n)

Examples

>>> key_rate_weights([1.0, 3.5, 20.0], [2.0, 5.0, 10.0]).round(3).tolist()
[[1.0, 0.5, 0.0], [0.0, 0.5, 0.0], [0.0, 0.0, 1.0]]
financialkit.fixed_income.linear_forward_shock(level, slope, horizon)[source]#

Zero-rate shift produced by the forward-rate shock \(\Delta f(s) = a + b (s - H)\).

Integrating gives \(\Delta r(t) = a + b (t/2 - H)\); pass the result to shifted().

Return type:

Callable[[ndarray], ndarray]

Parameters:

Examples

>>> from financialkit.fixed_income.core.base import CashFlows, FlatCurve
>>> cf = CashFlows([2.0, 8.0], [50.0, 50.0])
>>> H = 5.0  # equal weights on 2 and 8 at a zero rate: duration exactly 5
>>> base, shocked = FlatCurve(0.0), FlatCurve(0.0).shifted(linear_forward_shock(0.0, 0.002, H))
>>> change = horizon_value(cf, shocked, H) / horizon_value(cf, base, H) - 1
>>> bool(change >= fong_vasicek_bound(m_squared(cf, H, base), 0.002))
True
financialkit.fixed_income.m_squared(cash_flows, horizon, curve)[source]#

Fong and Vasicek’s dispersion \(M^2 = \sum_i w_i (t_i - H)^2\), with present-value weights \(w_i\).

Return type:

float

Parameters:

Examples

>>> m_squared(CashFlows([5.0], [1.0]), 5.0, 0.04)  # a zero maturing at the horizon
0.0
financialkit.fixed_income.macaulay_duration(cash_flows, y, compounding=None)[source]#

Macaulay duration \(D = \sum_i t_i \, c_i \, d(t_i) / P\) at yield y.

Return type:

float

Parameters:

Examples

>>> from financialkit.fixed_income.systems.bonds import bond_cash_flows
>>> macaulay_duration(bond_cash_flows(100.0, 0.0, 7.0), 0.05)  # a zero's duration is its maturity
7.0
financialkit.fixed_income.modified_duration(cash_flows, y, compounding=None)[source]#

Modified duration \(-\frac{1}{P}\frac{dP}{dy}\): Hicks’s elasticity in yield units.

Equals \(D_{mac} / (1 + y/m)\) for a yield compounded m times a year, and \(D_{mac}\) itself under continuous compounding.

Return type:

float

Parameters:

Examples

>>> from financialkit.fixed_income.systems.bonds import bond_cash_flows
>>> cf = bond_cash_flows(100.0, 0.06, 5.0)
>>> d_mac = macaulay_duration(cf, 0.06, compounding=2)
>>> bool(abs(modified_duration(cf, 0.06, compounding=2) - d_mac / 1.03) < 1e-12)
True
financialkit.fixed_income.plot_cash_flows(cash_flows, curve=None, ax=None)[source]#

Bar chart of a cash-flow stream, overlaid with present values if a curve is given.

Returns:

matplotlib.axes.Axes

Parameters:
financialkit.fixed_income.plot_key_rate_durations(result, ax=None, label=None)[source]#

Bar chart of key rate durations against key maturity.

Returns:

matplotlib.axes.Axes

Parameters:
financialkit.fixed_income.plot_pca_loadings(result, n_components=3, ax=None)[source]#

Loadings of the leading principal components against maturity.

Returns:

matplotlib.axes.Axes

Parameters:
financialkit.fixed_income.plot_price_yield(cash_flows, y0, compounding=None, dy=0.04, ax=None)[source]#

Price-yield curve with its duration tangent and duration-plus-convexity parabola at y0.

Returns:

matplotlib.axes.Axes

Parameters:
financialkit.fixed_income.plot_surplus(result, shifts=None, ax=None)[source]#

Surplus (assets minus liabilities) against a parallel shift of the curve, in basis points.

Returns:

matplotlib.axes.Axes

Parameters:

result (ImmunizationResult)

financialkit.fixed_income.plot_yield_curve(curve, t_max=30.0, ax=None, label='', show_forward=True)[source]#

Zero curve, and optionally its instantaneous forward curve, in percent.

Parameters:
  • curve (YieldCurve)

  • t_max (float) – Longest maturity plotted.

  • ax (matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.

  • label (str) – Legend label prefix.

  • show_forward (bool) – Also draw the instantaneous forward curve (dashed).

Returns:

matplotlib.axes.Axes

financialkit.fixed_income.price_from_yield(cash_flows, y, compounding=None)[source]#

Present value of cash_flows discounted at a single yield y; broadcasts over y.

Parameters:
  • cash_flows (CashFlows)

  • y (float or array_like) – Yield (decimal).

  • compounding (int | None) – Compounding frequency of y; None for continuous.

Returns:

float or ndarray

financialkit.fixed_income.redington_immunize(liabilities, assets, curve)[source]#

Hold two assets in the amounts that match the liabilities’ present value and duration.

Solves the 2x2 linear system \(\sum_j h_j PV_j = PV_L\) and \(\sum_j h_j PV_j D_j = PV_L D_L\) (Fisher-Weil durations on curve). Check redington_satisfied for Redington’s third condition, asset convexity above liability convexity; it holds when the assets’ maturities straddle the liabilities’.

Parameters:
Return type:

ImmunizationResult

Returns:

ImmunizationResult

Examples

>>> from financialkit.fixed_income.systems.bonds import bond_cash_flows
>>> L = CashFlows([10.0], [1000.0])
>>> res = redington_immunize(L, [bond_cash_flows(1.0, 0.0, 5.0), bond_cash_flows(1.0, 0.0, 20.0)], 0.05)
>>> res.holdings.round(2).tolist(), res.redington_satisfied
([519.2, 549.57], True)
financialkit.fixed_income.simulate_yield_changes(tenors, n_obs, factor_vols=(0.006, 0.003, 0.0015), noise_vol=0.0004, tau=2.0, rng=None)[source]#

Daily zero-yield changes driven by level, slope and curvature shocks.

Each day’s change is \(\sum_k \sigma_k z_k \ell_k(t) + \varepsilon(t)\) with Nelson-Siegel loadings \(\ell_k\) (flat, decaying, humped) and independent maturity-specific noise, which is the factor structure Litterman and Scheinkman found in Treasury returns.

Parameters:
  • tenors (array_like, shape (n,)) – Maturities (years).

  • n_obs (int) – Number of days.

  • factor_vols (tuple of float) – Daily standard deviations of the level, slope and curvature shocks.

  • noise_vol (float) – Daily standard deviation of the idiosyncratic noise.

  • tau (float) – Nelson-Siegel decay parameter of the loadings.

  • rng (Generator | int | None)

Return type:

ndarray

Returns:

ndarray, shape (n_obs, n)

financialkit.fixed_income.strips_curve(times, strip_prices, face=100.0)[source]#

Zero curve implied by STRIPS prices: \(r(t) = -\ln(p/F)/t\).

Return type:

ZeroCurve

Parameters:

face (float)

Examples

>>> curve = strips_curve([0.5, 1.0], [97.5, 95.0])
>>> round(float(curve.zero_rate(1.0)), 6)
0.051293
financialkit.fixed_income.strips_decomposition(cash_flows)[source]#

Split a coupon bond into the zero-coupon bonds (STRIPS) it is a portfolio of.

Return type:

list

Returns:

list of CashFlows – One single-payment stream per payment date, in date order.

Parameters:

cash_flows (CashFlows)

Examples

>>> [s.amounts.tolist() for s in strips_decomposition(bond_cash_flows(100.0, 0.04, 1.0))]
[[2.0], [102.0]]
financialkit.fixed_income.strips_value(cash_flows, strip_prices, face=100.0)[source]#

Cost of replicating cash_flows with STRIPS quoted at strip_prices per face of each payment date.

Comparing this with the bond’s market price is the stripping / reconstitution trade: if the strips are worth more, buy the bond and strip it; if less, buy the strips and reconstitute.

Parameters:
  • cash_flows (CashFlows)

  • strip_prices (array_like or YieldCurve) – Prices per face of zero-coupon bonds maturing on each payment date, or a curve to price them on.

  • face (float)

Return type:

float

Returns:

float

financialkit.fixed_income.swap_annuity(curve, maturity, freq=2)[source]#

Present value of 1 per year paid freq times a year to maturity: the fixed leg’s PV01 times 10,000.

Return type:

float

Parameters:
financialkit.fixed_income.swap_rate(curve, maturity, freq=2)[source]#

Par swap rate: the fixed rate that makes a new swap worth zero.

\[K = \frac{1 - P(0, T)}{\frac{1}{m}\sum_i P(0, t_i)}\]

It equals the par yield of a bond with the same schedule.

Examples

>>> from financialkit.fixed_income.core.base import FlatCurve
>>> round(swap_rate(FlatCurve(np.log(1.03)), 5.0, freq=1), 10)
0.03
Parameters:
financialkit.fixed_income.yield_curve_pca(changes, tenors=None)[source]#

Principal components of yield-curve changes: Litterman and Scheinkman’s level, slope and curvature.

Parameters:
  • changes (array_like, shape (n_obs, n_tenors)) – Yield changes (one row per date, one column per maturity).

  • tenors (ndarray | None) – Maturities of the columns (for plotting); default 0, 1, ....

Return type:

CurvePCAResult

Returns:

CurvePCAResult

Examples

>>> from financialkit.fixed_income.utils.synthetic import simulate_yield_changes
>>> tenors = np.array([0.5, 1, 2, 3, 5, 7, 10, 20, 30])
>>> pca = yield_curve_pca(simulate_yield_changes(tenors, 2000, rng=0), tenors)
>>> bool(pca.explained_variance_ratio[:3].sum() > 0.95)
True
financialkit.fixed_income.yield_discount_derivatives(y, t, compounding=None)[source]#

First and second derivatives of discount_factor() with respect to the yield y.

Continuous: \(-t e^{-yt}\) and \(t^2 e^{-yt}\). Periodic: \(-t (1+y/m)^{-mt-1}\) and \(t (t + 1/m) (1+y/m)^{-mt-2}\).

Returns:

d1, d2 (ndarray)

Parameters:

compounding (int | None)

financialkit.fixed_income.yield_to_maturity(price, cash_flows, compounding=None, tol=1e-10)[source]#

The single discount rate that equates cash_flows’ present value to price.

The price is strictly decreasing in the yield for positive cash flows, so the root is unique; it is bracketed by expanding an interval and found with scipy.optimize.brentq().

Parameters:
  • price (float) – Dirty price.

  • cash_flows (CashFlows) – Positive cash flows.

  • compounding (int | None) – Compounding frequency of the returned yield; None for continuous.

  • tol (float) – Absolute tolerance on the yield.

Return type:

float

Returns:

float

Examples

>>> cf = bond_cash_flows(100.0, 0.06, 2.0)
>>> round(yield_to_maturity(98.39, cf), 4)  # Hull, 9th ed., Sec. 4.4
0.0676
financialkit.fixed_income.zero_coupon_bond_price(T, y, face=100.0, compounding=None)[source]#

Price of a zero-coupon bond paying face at T, at yield y; broadcasts.

Examples

>>> round(float(zero_coupon_bond_price(1.0, 0.10536)), 2)  # Hull, Table 4.3
90.0
Parameters: