Documentation

v1.0.0  ·  Fixed Income  ·  Java 21+  ·  Apache 2.0

Fixed Income · Bonds

Bonds

Bond pricing, yield inversion, duration, and curve construction — all built around the immutable Bond record. Every operation is expressed as an interface so implementations can be swapped without changing calling code.

Bond record

Immutable representation of a fixed-coupon bond. Being a Java record, equality is field-based — two Bond instances with identical parameters are equal and can be used as map keys.

Fields
FieldTypeDescription
faceValuedoublePrincipal amount (e.g. 1000). Must be > 0.
annualRatedoubleAnnual coupon rate, decimal (e.g. 0.06 for 6%). Must be ≥ 0.
maturityYearsdoubleTime to maturity in years. Must be > 0 and coherent with couponFrequency.
couponFrequencyFrequencyPayment frequency: ANNUALLY, SEMI_ANNUALLY, QUARTERLY, MONTHLY, DAILY.
Methods
double getCouponPayment() Returns faceValue × annualRate / periodsPerYear.
Map<Double, Double> getCashflows() Ordered map of (time in years → cash flow). The final period includes the face value redemption.
Bond.java
// 6% annual coupon rate, semi-annual payments, 5-year maturity
Bond bond = new Bond(1000.0, 0.06, 5.0, Frequency.SEMI_ANNUALLY);

bond.getCouponPayment(); // → 30.0  (1000 × 0.06 / 2)
bond.getCashflows();    // → {0.5=30.0, 1.0=30.0, ..., 5.0=1030.0}
Pricing
YieldBondPricer class implements BondPricer

Prices a bond by discounting all cash flows at a single Yield to Maturity. The compounding convention for the yield is supplied explicitly and may differ from the bond's coupon frequency.

Bond price
$$P = \sum_{t} CF(t) \cdot DF(y,\, t)$$

$CF(t)$ — cash flow at time $t$.   $DF(y, t)$ — discount factor from the compounding strategy at yield $y$.

Example
// 6% annual coupon rate, semi-annual payments, 5-year maturity
Bond bond = new Bond(1000.0, 0.06, 5.0, Frequency.SEMI_ANNUALLY);

CompoundingStrategy cs = new DiscreteCompoundingStrategy(2); // semi-annual
BondPricer pricer = new YieldBondPricer(0.07, cs);
double price = pricer.price(bond); // → 958.42
ZeroCouponBondRateBondPricer class implements BondPricer

Prices a bond by discounting each cash flow at its own zero (spot) rate, interpolated from a provided curve.

Zero-curve price
$$P = \sum_{t} CF(t) \cdot DF\!\left(r(t),\, t\right)$$

$r(t)$ — zero rate at tenor $t$, obtained by interpolating the supplied curve. Each cash flow is discounted at its own tenor rate.

Example
// 6% annual coupon rate, semi-annual payments, 5-year maturity
Bond bond = new Bond(1000.0, 0.06, 5.0, Frequency.SEMI_ANNUALLY);

Map<Double, Double> curve = Map.of(
    0.5, 0.050,  1.0, 0.055,
    2.0, 0.060,  5.0, 0.065);

BondPricer pricer = new ZeroCouponBondRateBondPricer(
    curve,
    new LinearInterpolationStrategy(),
    new ContinuousCompoundingStrategy());

double price = pricer.price(bond); // → 976.57
Yield
RootFindingBondYieldCalculator class implements BondYieldCalculator

Inverts the pricing formula numerically to find the YTM that makes the theoretical price equal to the observed market price. The solver algorithm is injected as a strategy — use Bisection, Newton-Raphson, or Secant interchangeably.

Root-finding problem
$$\text{Find } y \text{ such that } P(y) - P_{\text{market}} = 0$$ $$P(y) = \sum_{t} CF(t) \cdot DF(y,\, t)$$

Cash flows are captured once before the solver loop to avoid recomputation across iterations.

Example
// 6% annual coupon rate, semi-annual payments, 5-year maturity
Bond bond = new Bond(1000.0, 0.06, 5.0, Frequency.SEMI_ANNUALLY);

BondYieldCalculator calc = new RootFindingBondYieldCalculator(
    new DiscreteCompoundingStrategy(2),
    new NewtonRaphsonSolver());

double ytm = calc.yieldToMaturity(bond, 958.42);
// → 0.0700  (7.00%)

// Any RootSolver works — swap without changing the rest of the code
BondYieldCalculator calc2 = new RootFindingBondYieldCalculator(
    new DiscreteCompoundingStrategy(2),
    new BisectionSolver());
Duration & Risk
YieldBondDurationCalculator class implements BondDurationCalculator

Computes Macaulay duration, modified duration, and DV01 from a yield and compounding convention. All three measures share the same weighted cash flow schedule, ensuring internal consistency.

Macaulay Duration
$$D_{\text{mac}} = \frac{1}{P} \sum_{t}\, t \cdot CF(t) \cdot DF(y,\, t)$$

Time-weighted average of discounted cash flows, in years.

Modified Duration
$$D_{\text{mod}} = \frac{D_{\text{mac}}}{1 + y/m} \quad \text{(discrete,}\;m\text{ periods/year)}$$ $$D_{\text{mod}} = D_{\text{mac}} \quad \text{(continuous compounding)}$$

Percentage price change per unit parallel shift in yield. The conversion is delegated to the compounding strategy, so the formula is automatically correct for both conventions.

DV01
$$\text{DV01} = D_{\text{mod}} \times P \times 0.0001$$

Absolute price change for a 1 basis point (0.01%) increase in yield. Same currency unit as the bond price.

Example
// 6% annual coupon rate, semi-annual payments, 5-year maturity
Bond bond = new Bond(1000.0, 0.06, 5.0, Frequency.SEMI_ANNUALLY);

CompoundingStrategy cs = new DiscreteCompoundingStrategy(2);
BondDurationCalculator calc = new YieldBondDurationCalculator(0.07, cs);

double price = 958.42;
double dmac  = calc.macaulayDuration(bond, price); // → 4.38 years
double dmod  = calc.modifiedDuration(bond, price); // → 4.23
double dv01  = calc.dv01(bond, price);             // → 0.0405
Curve Construction
SpotRateCurveBootstrappingStrategy class implements BootstrappingStrategy

Bootstraps a zero-coupon (spot rate) curve from coupon bonds sorted in ascending maturity order. At each step, all intermediate cash flows are discounted at already-derived spot rates, and the unknown rate at the bond's maturity is solved analytically.

Bootstrapping step at maturity $T$
$$DF(T) = \frac{P_T - \displaystyle\sum_{t < T} CF(t) \cdot DF\!\left(r_t,\, t\right)}{CF(T)}$$ $$r_T = \text{rateFromDiscountFactor}\!\left(DF(T),\; T\right)$$

Applied sequentially for each bond in ascending maturity order. The initial zero curve provides seed rates for the shortest tenors. The result is an unmodifiable NavigableMap.

Example — par bonds
BootstrappingStrategy boot = new SpotRateCurveBootstrappingStrategy(
    Map.of(0.5, 0.04),              // seed: 6-month spot rate
    new ContinuousCompoundingStrategy());

NavigableMap<Double, Double> curve = boot.bootstrapFromParBonds(
    List.of(
        new Bond(1000, 0.04,  1.0, Frequency.SEMI_ANNUALLY),
        new Bond(1000, 0.05,  2.0, Frequency.SEMI_ANNUALLY),
        new Bond(1000, 0.055, 3.0, Frequency.SEMI_ANNUALLY)),
    new LinearInterpolationStrategy());
// → {0.5=0.04, 1.0=0.0396, 2.0=0.0497, 3.0=0.0548}
Example — market prices
// Seed curve: known short-end spot rates
TreeMap<Double, Double> zeroCurve = new TreeMap<>(
    Map.of(0.5, 0.1238, 1.0, 0.1165));

Bond bond1 = new Bond(100, 0.08, 1.5, Frequency.SEMI_ANNUALLY);
Bond bond2 = new Bond(100, 0.10, 2.0, Frequency.SEMI_ANNUALLY);

Map<Bond, Double> marketPrices = Map.of(bond1, 94.84, bond2, 97.12);

BootstrappingStrategy boot = new SpotRateCurveBootstrappingStrategy(
    zeroCurve, new ContinuousCompoundingStrategy());

Map<Double, Double> curve = boot.bootstrapFromMarketPrices(
    List.of(bond1, bond2), marketPrices, new LinearInterpolationStrategy());
// → {0.5=0.1238, 1.0=0.1165, 1.5=0.1150, 2.0=0.1130}

Fixed Income · Rates

Rates

Compounding strategies, compound interest, and rate conversion utilities. All compounding strategies share a common CompoundingStrategy interface and are interchangeable across the library.

Compounding
ContinuousCompoundingStrategy class implements CompoundingStrategy

Implements continuous compounding. Used throughout the library wherever a compounding convention is required. Under continuous compounding, modified duration equals Macaulay duration (no adjustment needed).

Discount & accumulation factors
$$DF(r, t) = e^{-rt} \qquad AF(r, t) = e^{rt}$$
Present & future value
$$PV = FV \cdot e^{-rt} \qquad FV = PV \cdot e^{rt}$$
Rate from discount factor
$$r = -\frac{\ln(DF)}{t}$$
Forward rate
$$F(t_1, t_2) = \frac{r_2\, t_2 - r_1\, t_1}{t_2 - t_1}$$
Macaulay → Modified duration
$$D_{\text{mod}} = D_{\text{mac}}$$
Example
CompoundingStrategy cs = new ContinuousCompoundingStrategy();

cs.discountFactor(0.05, 2.0);          // → e^(-0.10) = 0.9048
cs.accumulationFactor(0.05, 2.0);      // → e^( 0.10) = 1.1052
cs.futureValue(1000, 0.05, 2.0);       // → 1105.17
cs.rateFromDiscountFactor(0.9048, 2.0); // → 0.0500
cs.forwardRate(0.04, 1.0, 0.05, 2.0);  // → 0.06
DiscreteCompoundingStrategy class implements CompoundingStrategy

Implements discrete compounding with a configurable number of periods per year $m$. Common conventions: semi-annual ($m=2$) for bonds, annual ($m=1$) for general finance, monthly ($m=12$) for consumer products.

Discount & accumulation factors
$$DF(r, t) = \left(1 + \frac{r}{m}\right)^{-mt} \qquad AF(r, t) = \left(1 + \frac{r}{m}\right)^{mt}$$
Rate from discount factor
$$r = m \cdot \left(DF^{-1/(mt)} - 1\right)$$
Forward rate
$$\left(1 + \frac{F}{m}\right)^{m(t_2 - t_1)} = \frac{AF(r_2, t_2)}{AF(r_1, t_1)}$$
Macaulay → Modified duration
$$D_{\text{mod}} = \frac{D_{\text{mac}}}{1 + y/m}$$
Example
CompoundingStrategy semi = new DiscreteCompoundingStrategy(2);

semi.discountFactor(0.06, 5.0);           // → (1.03)^(-10) = 0.7441
semi.futureValue(1000, 0.06, 5.0);         // → 1343.92
semi.rateFromDiscountFactor(0.7441, 5.0);  // → 0.0600
semi.forwardRate(0.04, 1.0, 0.05, 2.0);   // → 0.06
Interest
CompoundInterestCalculator class

Calculates the period-by-period growth of an investment under compound interest. Supports an initial principal, regular periodic contributions (ordinary annuity), and independent compounding and contribution frequencies.

Balance per compounding period
$$B_n = B_{n-1} \cdot \left(1 + \frac{r}{m}\right) + C_n$$

$m$ — compounding periods per year.   $r$ — annual rate.   $C_n$ — contribution aggregated for period $n$ (added after interest, ordinary annuity convention). When contribution and compounding frequencies differ, $C_n$ is scaled proportionally.

CompoundInterestResult fields
FieldTypeDescription
depositsList<Double>Contribution added each period (index 0 = initial investment).
interestsList<Double>Interest earned each compounding period.
totalDepositList<Double>Cumulative principal deposited through each period.
accuredInterestList<Double>Cumulative interest earned through each period.
balanceList<Double>Total account balance at the end of each period.
Example
CompoundInterestCalculator calc = new CompoundInterestCalculator();

CompoundInterestResult res = calc.calculate(
    10_000,             // initial investment
    200,                // $200 monthly contribution
    Frequency.MONTHLY,  // contribution frequency
    5.0,                // 5-year horizon
    0.06,               // 6% annual rate
    Frequency.MONTHLY); // monthly compounding

double finalBalance = res.balance().getLast(); // → ~27,442
int    periods      = res.balance().size();     // → 61 (period 0 + 60 monthly periods)
Rate Converter
RateConverter static utility

Static utility for converting interest rates between compounding conventions. All conversions are derived from the principle of equated accumulation factors — two rates are equivalent if they produce the same future value over the same period.

Discrete → Continuous
$$R_c = m \cdot \ln\!\left(1 + \frac{R_d}{m}\right)$$
Continuous → Discrete
$$R_d = m \cdot \left(e^{R_c/m} - 1\right)$$
Discrete → Discrete (frequency change)
$$\left(1 + \frac{r_1}{m_1}\right)^{m_1} = \left(1 + \frac{r_2}{m_2}\right)^{m_2}$$ $$r_2 = m_2 \cdot \left[\left(1 + \frac{r_1}{m_1}\right)^{m_1/m_2} - 1\right]$$
Example
// Semi-annual 5% → equivalent continuous rate
double rc = RateConverter.discreteToContinuous(0.05, Frequency.SEMI_ANNUALLY);
// → 0.04938  (4.938%)

// Continuous → quarterly discrete
double rq = RateConverter.continuousToDiscrete(rc, Frequency.QUARTERLY);
// → 0.04969  (4.969%)

// Semi-annual 5% → monthly discrete
double rm = RateConverter.convertDiscreteRates(
    0.05, Frequency.SEMI_ANNUALLY, Frequency.MONTHLY);
// → 0.04949  (4.949%)

Math

Numerical Solvers

Three root-finding strategies implement the RootSolver interface. All are injected into RootFindingBondYieldCalculator and are interchangeable without changing calling code.

Solver Convergence Requires Best for
BisectionSolver Linear — halves interval each step Sign change in [a, b] Guaranteed convergence, robust fallback
NewtonRaphsonSolver Quadratic near root Single initial guess Fast convergence when guess is close
SecantSolver Superlinear (~1.618×) Two distinct initial guesses No bracket needed, faster than bisection
BisectionSolver class implements RootSolver

Guaranteed convergence by halving a bracketing interval at each step. Requires a sign change in the interval: f(a) × f(b) < 0. Slower than Newton-Raphson but will always converge if a root exists in the bracket.

Algorithm
$$\text{mid} = \frac{a + b}{2}, \qquad \text{update } a \text{ or } b = \text{mid based on sign of } f(\text{mid})$$ $$\text{stop when } |f(\text{mid})| < \varepsilon \;\text{ or }\; \tfrac{b-a}{2} < \varepsilon$$
NewtonRaphsonSolver class implements RootSolver

Quadratic convergence near the root. The derivative is approximated numerically using a symmetric finite difference with step $h = 10^{-6}$. Fastest of the three for well-conditioned problems; may diverge far from the root.

Iteration
$$x_{n+1} = x_n - \frac{f(x_n)}{f'(x_n)}, \qquad f'(x) \approx \frac{f(x+h) - f(x-h)}{2h}, \quad h = 10^{-6}$$
SecantSolver class implements RootSolver

Superlinear convergence (~1.618×). Approximates the derivative using the slope of the chord through the two most recent iterates. No bracketing interval required — needs two distinct initial guesses.

Iteration
$$y_{n+1} = y_n - f(y_n) \cdot \frac{y_n - y_{n-1}}{f(y_n) - f(y_{n-1})}$$

Math

Interpolation

The InterpolationStrategy interface is used by ZeroCouponBondRateBondPricer and SpotRateCurveBootstrappingStrategy to look up rates at arbitrary maturities between known curve points.

LinearInterpolationStrategy class implements InterpolationStrategy

Linearly interpolates between the two nearest known data points. Falls back to flat extrapolation (nearest edge value) when the requested point lies outside the data range.

Linear interpolation
$$y = y_1 + \frac{(x - x_1)(y_2 - y_1)}{x_2 - x_1}, \qquad x_1 \le x \le x_2$$

Outside range: $y = y_{\min}$ if $x < x_{\min}$  (left flat extrapolation),  $y = y_{\max}$ if $x > x_{\max}$  (right flat extrapolation).

Example
NavigableMap<Double, Double> curve = new TreeMap<>(Map.of(
    1.0, 0.04,
    2.0, 0.05,
    5.0, 0.06));

InterpolationStrategy interp = new LinearInterpolationStrategy();

interp.interpolate(curve, 1.5); // → 0.045  (midpoint between 1yr and 2yr)
interp.interpolate(curve, 3.5); // → 0.055  (midpoint between 2yr and 5yr)
interp.interpolate(curve, 8.0); // → 0.060  (flat extrapolation past 5yr)
interp.interpolate(curve, 0.5); // → 0.040  (flat extrapolation before 1yr)