A receive-fixed swap: cashflows, value and risk¶
Consider a five-year USD swap that receives a fixed coupon of 4% on a USD 1 million notional. We value it at origination using the illustrative 24 February 2026 SOFR market, then reconstruct its PV from the reported cashflows and examine its quote sensitivities.
The fixed leg pays semiannually and the floating leg quarterly, with
unadjusted dates and Actual/360 accrual. A single SOFR curve supplies
projection and USD collateral discounting. The Swap defines the contract,
and Trade records our receive-fixed position.
Setup · Par-rate calculation.
Computational study. Canonical notebook · Companion files and setup. Download this notebook.
This page renders saved outputs; the documentation build does not execute it. Run the canonical notebook from its own directory with the checkout-built binding to reproduce the computation. Each study records its market inputs and numerical assumptions.
import pandas as pd
import quantsupport as qs
from market_data import load_market
market = load_market()
ref = market.quotes.reference_date
print("Market date:", ref)
market.quotes.to_dataframe().head(8)
Market date: 2026-02-24
| identifier | mid | bid | ask | |
|---|---|---|---|---|
| 0 | CapletFloorlet_USD_SOFR_3M_10Y_Absolute_0.035_... | 0.20 | None | None |
| 1 | CapletFloorlet_USD_SOFR_3M_10Y_Absolute_0.045_... | 0.18 | None | None |
| 2 | CapletFloorlet_USD_SOFR_3M_10Y_Absolute_0.055_... | 0.20 | None | None |
| 3 | CapletFloorlet_USD_SOFR_3M_12Y_Absolute_0.035_... | 0.19 | None | None |
| 4 | CapletFloorlet_USD_SOFR_3M_12Y_Absolute_0.045_... | 0.17 | None | None |
| 5 | CapletFloorlet_USD_SOFR_3M_12Y_Absolute_0.055_... | 0.19 | None | None |
| 6 | CapletFloorlet_USD_SOFR_3M_1Y_Absolute_0.035_S... | 0.33 | None | None |
| 7 | CapletFloorlet_USD_SOFR_3M_1Y_Absolute_0.045_S... | 0.29 | None | None |
Dates and rate conventions¶
The contract needs dates, an accrual basis and payment frequencies. Adding a
Period to a Date performs calendar arithmetic. Apply a Calendar separately
when a date requires business-day adjustment. A DayCounter then measures
the year fraction used for interest accrual.
We use typed enums for these conventions. Rates are decimal fractions:
0.04 means 4%, and 0.0001 means one basis point.
six_months = qs.Period(6, qs.TimeUnit.Months)
calendar = qs.Calendar("UnitedStates(Sofr)")
end = ref + six_months
print("Unadjusted end:", end)
print("Adjusted end:", calendar.adjust(end, qs.BusinessDayConvention.ModifiedFollowing))
print("Actual/360 accrual:", qs.DayCounter.Actual360.year_fraction(ref, end))
print("Receive / pay signs:", qs.Side.LongReceive.sign(), qs.Side.PayShort.sign())
Unadjusted end: 2026-08-24
Adjusted end: 2026-08-24
Actual/360 accrual: 0.5027777777777778
Receive / pay signs: 1.0 -1.0
The curve and its market index¶
MarketIndex.SOFR identifies the curve used by the product and its market
configuration. The deposit and OIS quotes are calibration pillars, and the
solved discount factors form the curve’s interpolation nodes.
The USD CSA policy selects SOFR for discounting in this example. The same curve projects floating coupons. A multi-curve setup can assign separate curves to those roles.
print(market.csa_policy)
market.curves[0].to_dict()
SingleCurveCSADiscountPolicy(collateral_currency='USD', collateral_index='SOFR')
{'day_counter': 'Actual360',
'enable_extrapolation': True,
'interpolator': 'LogLinear',
'market_index': 'SOFR',
'quotes': ['FixedRateDeposit_USD_SOFR_1D',
'OIS_USD_SOFR_1Y',
'OIS_USD_SOFR_2Y',
'OIS_USD_SOFR_3Y',
'OIS_USD_SOFR_5Y',
'OIS_USD_SOFR_7Y',
'OIS_USD_SOFR_10Y',
'OIS_USD_SOFR_30Y']}
Defining the position¶
We construct the five-year contract, then create a Trade with
LongReceive to receive fixed and pay floating. Its identifier will link the
valuation and risk tables back to this position.
swap = qs.Swap(
identifier="USD-RECEIVE-FIXED-5Y",
start_date=ref,
maturity_date=ref + "5Y",
notional=1_000_000,
fixed_rate=0.04,
currency=qs.Currency.USD,
market_index=qs.MarketIndex.SOFR,
day_counter=qs.DayCounter.Actual360,
fixed_leg_frequency=qs.Frequency.Semiannual,
floating_leg_frequency=qs.Frequency.Quarterly,
)
trade = qs.Trade(swap, ref, qs.Side.LongReceive)
trade
Trade(instrument=Swap(compounding='Simple', currency='USD', day_counter='Actual360', fixed_leg_frequency='Semiannual', fixed_rate=0.04, floating_leg_frequency='Quarterly', identifier='USD-RECEIVE-FIXED-5Y', market_index='SOFR', maturity_date='2031-02-24', notional=1000000.0, rate_frequency='Semiannual', spread=None, start_date='2026-02-24'), trade_date='2026-02-24', side=LongReceive)
Valuation in a pricing context¶
The context holds the market inputs and constructs the curves requested by
the swap pricer. Inside with context:, we request value, par rate, cashflows
and sensitivities. Curve queries also belong inside this scope because they
use its constructed market and differentiation state.
We retain scalar results and copied pandas tables for analysis after the scope closes.
context = qs.PricingContext(
quotes=market.quotes, curves=market.curves, csa_policy=market.csa_policy
)
with context:
sofr = context.curve(qs.MarketIndex.SOFR)
five_year_df = sofr.discount_factor(ref + "5Y")
nodes = sofr.nodes()
result = context.evaluate(
trade,
[
qs.Request.Value,
qs.Request.FairRate(0),
qs.Request.Cashflows,
qs.Request.Sensitivities,
],
)
cashflows = result.cashflows
risk = result.sensitivities
pd.Series(
{
"position_pv_usd": result.price,
"par_fixed_rate": result.fair_rate,
"five_year_discount_factor": five_year_df,
}
)
position_pv_usd -1664.991324
par_fixed_rate 0.040368
five_year_discount_factor 0.817367
dtype: float64
Interpreting the valuation¶
price is the signed USD position PV. The fair_rate is the fixed coupon
that balances this vanilla swap under the selected market and conventions.
Comparing it with our agreed 4% coupon explains the position’s value.
The cashflow table contains undiscounted contractual amounts. Floating coupons use projected rates for future observations, so the table describes payments under today’s curve assumptions. We apply direction and discounting explicitly in the reconciliation below.
cashflows[["payment_date", "leg_index", "type", "amount", "currency"]].head(10)
| payment_date | leg_index | type | amount | currency | |
|---|---|---|---|---|---|
| 0 | 2026-02-24 | 0 | Disbursement | -1000000.000000 | USD |
| 1 | 2031-02-24 | 0 | Redemption | 1000000.000000 | USD |
| 2 | 2026-08-24 | 0 | FixedRateCoupon | 20111.111111 | USD |
| 3 | 2027-02-24 | 0 | FixedRateCoupon | 20444.444444 | USD |
| 4 | 2027-08-24 | 0 | FixedRateCoupon | 20111.111111 | USD |
| 5 | 2028-02-24 | 0 | FixedRateCoupon | 20444.444444 | USD |
| 6 | 2028-08-24 | 0 | FixedRateCoupon | 20222.222222 | USD |
| 7 | 2029-02-26 | 0 | FixedRateCoupon | 20666.666667 | USD |
| 8 | 2029-08-24 | 0 | FixedRateCoupon | 19888.888889 | USD |
| 9 | 2030-02-25 | 0 | FixedRateCoupon | 20555.555556 | USD |
The sensitivity table identifies the original calibration quotes. Each
value is the PV derivative per unit change in its input quote. All inputs
here are decimal interest rates, so multiplying by 1e-4 estimates the signed
USD change for a one-basis-point increase in each quote.
quote_risk = risk.assign(pnl_for_plus_1bp_usd=risk["value"] * 1e-4)
quote_risk.sort_values("pnl_for_plus_1bp_usd")
| pillar | value | pnl_for_plus_1bp_usd | |
|---|---|---|---|
| 6 | OIS_USD_SOFR_5Y | -4.418095e+06 | -441.809500 |
| 5 | OIS_USD_SOFR_3Y | -6.761138e+03 | -0.676114 |
| 2 | OIS_USD_SOFR_1Y | -3.050580e+03 | -0.305058 |
| 0 | FixedRateDeposit_USD_SOFR_1D | -2.772806e+03 | -0.277281 |
| 1 | OIS_USD_SOFR_10Y | 0.000000e+00 | 0.000000 |
| 4 | OIS_USD_SOFR_30Y | 0.000000e+00 | 0.000000 |
| 7 | OIS_USD_SOFR_7Y | 0.000000e+00 | 0.000000 |
| 3 | OIS_USD_SOFR_2Y | 1.333279e+03 | 0.133328 |
Reconciling PV from cashflows¶
For leg signs \(s_\ell\), the swap value is \(V=\sum_{\ell,i}s_\ell C_{\ell i}P(0,T_{\ell i})\). The native table already contains signed amounts: leg 0 is fixed and leg 1 is floating. Receipts are positive, payments are negative, and initial advances have the opposite sign to repayments. We discount each reported amount directly. Matching principal exchanges cancel at origination and maturity.
reconciliation = cashflows.copy()
reconciliation["signed_amount_usd"] = reconciliation["amount"]
with context:
curve = context.curve(qs.MarketIndex.SOFR)
reconciliation["discount_factor"] = reconciliation["payment_date"].map(
curve.discount_factor
)
opposite = context.evaluate(
qs.Trade(swap, ref, qs.Side.PayShort), [qs.Request.Value]
)
reconciliation["pv_usd"] = (
reconciliation["signed_amount_usd"] * reconciliation["discount_factor"]
)
print(f"Receive-fixed PV: USD {result.price:,.2f}; par coupon: {result.fair_rate:.6%}.")
print(
f"Cashflow reconstruction residual: USD {reconciliation['pv_usd'].sum() - result.price:.3e}."
)
print(
"The 4% contract is above par."
if result.fair_rate < 0.04
else "The 4% contract is below par."
)
reconciliation.groupby("leg_index")[["signed_amount_usd", "pv_usd"]].sum()
Receive-fixed PV: USD -1,664.99; par coupon: 4.036802%.
Cashflow reconstruction residual: USD -1.637e-11.
The 4% contract is below par.
| signed_amount_usd | pv_usd | |
|---|---|---|
| leg_index | ||
| 0 | 202888.888889 | -1.664991e+03 |
| 1 | -202698.050026 | -1.164153e-10 |
The reconstruction shows how each leg contributes to the position value. Reversing the trade side should reverse that value, which provides a second check on direction. To change the agreed coupon while holding the market fixed, continue with par-rate mathematics. For market moves, see quote shocks and portfolio risk.