Interest Rate Swap Valuation and Applications › Determining the Par Swap Rate

Michael BrenndoerferUpdated January 5, 20263 min read
Quantitative FinanceAll chapters
Part 4 of 15Swaps and Credit Derivatives
Chapter 21Interest Rate Swap Valuation and Applications › Determining the Par Swap Rate
Reading section 8 of 17 · Determining the Par Swap Rate · 3 minSection 8 of 17 · 3 min

Book contents

Section 8 of 17 · 3 min read

Interest Rate Swap Valuation and Applications › Determining the Par Swap RateOne section · Sections in this chapter8 / 17 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

To calculate a par quote for a new swap, find the fixed coupon that makes its initial model value zero under the specified payment, floating-index and discounting conventions. A client can also request an off-market coupon, in which case a nonzero initial value may be offset by an upfront payment. The calculation below uses discount factors to obtain the par coupon in the chapter's single-curve model.

For a par swap at inception, fixed and floating coupon PVs are equal. Equivalently, after adding equal hypothetical principal repayments, the corresponding fixed- and floating-rate bonds both have value N under the matching single-curve reset assumptions.

In[15]:
Code
def calculate_par_swap_rate(discount_factors, maturities, payment_freq=1.0):
    """Par rate for a complete regular coupon grid in a single curve.

    payment_freq is the common accrual interval in years. Every coupon date
    must appear, not only quoted curve pillars. Stubs/day-count variation
    and separate projection curves require a more general implementation.
    """
    discounts = np.asarray(discount_factors, dtype=float)
    times = np.asarray(maturities, dtype=float)
    delta = float(payment_freq)
    if (
        discounts.ndim != 1
        or times.ndim != 1
        or not len(times)
        or len(discounts) != len(times)
        or not np.isfinite(delta)
        or delta <= 0
        or not np.all(np.isfinite(discounts))
        or np.any(discounts <= 0)
        or not np.all(np.isfinite(times))
        or not np.allclose(
            times, delta * np.arange(1, len(times) + 1), rtol=0, atol=1e-10
        )
    ):
        raise ValueError("Provide positive factors at every regular coupon date.")
    annuity = delta * np.sum(discounts)
    return (1 - discounts[-1]) / annuity


# Calculate par rates for different tenors
tenors = [1, 2, 3, 5, 7, 10]
calculated_par_rates = []

for i, tenor in enumerate(tenors):
    # Reprice every annual coupon date, not just quoted maturity nodes.
    payment_times = np.arange(1, int(tenor) + 1, dtype=float)
    payment_zeros = np.interp(payment_times, market_maturities, boot_zero_rates)
    payment_discounts = np.exp(-payment_zeros * payment_times)
    par_rate = calculate_par_swap_rate(payment_discounts, payment_times)
    calculated_par_rates.append(par_rate)
Out[16]:
Console
Par Swap Rate Calculation
==================================================
Tenor       Market Rate         Calculated
--------------------------------------------------
    1 yr        4.7000%          4.7000%
    2 yr        5.0000%          5.0000%
    3 yr        5.2000%          5.2000%
    5 yr        5.5000%          5.5000%
    7 yr        5.7000%          5.7000%
   10 yr        5.8000%          5.8000%

Full annual coupon repricing should recover each input quote to numerical tolerance.

The calculated rates reprice the hypothetical input quotes using the full annual coupon schedule and the same interpolation policy as calibration. Remaining numerical differences reflect solver and floating-point tolerance. Different contractual schedules require their own payment dates and accrual fractions; changing frequency is a separate calculation, not an explanation for roundoff.