Your First Swap
This chapter introduces the QuantSupport pricing workflow through a five-year USD SOFR swap. The values are specific to the example, but the workflow applies to other products:
- Define the instrument’s contractual economics.
- Wrap the instrument in a trade.
- Assemble the market state for an evaluation date.
- Select a compatible pricer and request specific calculations.
- Read the requested values from
EvaluationResults.
The complete program is in examples/valuation/src/main.rs. Run it from the workspace root with cargo run -p valuation.
Choosing a scalar type
Most numerical types in QuantSupport are generic over T: Scalar. The scalar determines whether a calculation carries only values or also automatic derivatives:
f64is appropriate for value-only calculations where the relevant market data and pricer support it.DualFwdcarries automatic-differentiation information used by the current pricing and sensitivity infrastructure.
The instrument, curves, and pricer must use compatible scalar types. This example requests curve sensitivities, so it uses DualFwd throughout.
1. Define the instrument
An instrument describes a financial product contractual economics: schedules, rates, indices, currencies, and payoff direction. QuantSupport constructs instruments with Make* builders (builder pattern). A builder collects inputs, applies documented defaults, and validates required fields in build(). For a vanilla fixed-versus-floating swap, MakeSwap<T> creates a fixed leg and a floating leg, with the given parameters.
In this example
The contract receives a 3% fixed rate and pays six-month SOFR on USD 10 million from 15 January 2024 to 15 January 2029:
use std::{cell::RefCell, rc::Rc};
use quantsupport::prelude::*;
let start_date = Date::new(2024, 1, 15);
let maturity_date = Date::new(2029, 1, 15);
let notional = 10_000_000.0;
let rate_definition = RateDefinition::new(
DayCounter::Actual360,
Compounding::Simple,
Frequency::Semiannual,
);
let swap = MakeSwap::<DualFwd>::default()
.with_identifier("USD_IRS_5Y".to_string())
.with_start_date(start_date)
.with_maturity_date(maturity_date)
.with_fixed_rate(0.030)
.with_notional(notional)
.with_rate_definition(rate_definition)
.with_currency(Currency::USD)
.with_market_index(MarketIndex::SOFR)
.with_side(Side::LongReceive)
.with_fixed_leg_frequency(Frequency::Semiannual)
.with_floating_leg_frequency(Frequency::Semiannual)
.build()?;
build() returns QSError when a required field is absent or invalid. For MakeSwap, the required fields are the identifier (a string to identify this particular swap), dates, notional, fixed rate, rate definition, currency, and floating-rate index. The principal optional settings are:
| Builder method | Default |
|---|---|
with_spread(f64) | 0.0 on the floating leg |
with_side(Side) | Side::LongReceive |
with_fixed_leg_frequency(Frequency) | Frequency::Semiannual |
with_floating_leg_frequency(Frequency) | Frequency::Quarterly |
with_calendar(Calendar) | Calendar::NullCalendar (no holiday adjustment) |
with_business_day_convention(BusinessDayConvention) | Unadjusted |
with_date_generation_rule(DateGenerationRule) | Backward for bullet legs |
with_end_of_month(bool) | false |
Internally, legs are stored in a vector, where leg 0 is fixed and has the swap’s side and leg 1 is floating, references MarketIndex::SOFR, and has the opposite side. Both are bullet legs, so their notionals do not amortize. In this example we choose to override the floating-leg frequency from its quarterly default to semiannual.
2. Add the trade layer
An instrument defines what pays; a trade adds position-level metadata such as trade date, notional, and side. This separation lets pricing and portfolio workflows operate on positions without putting lifecycle metadata into every product definition. Pricers generally accept trades rather than bare instruments.
In this example
let trade = SwapTrade::new(swap, start_date, notional, Side::LongReceive);
LongReceive means receive the fixed leg and pay the floating leg; PayShort reverses those signs.
3. Assemble the market
Pricing needs a market state (a set of market variables) as of an evaluation date. QuantSupport separates that state into three layers:
- Raw stores contain observations such as quotes, historical fixings, and FX rates.
ConstructedElementStorecontains derived objects such as discount and credit curves, volatility objects, and simulations.PricingContextowns those stores and implementsMarketDataProvider, the interface through which pricers request only the data they need.
In a configuration-driven workflow, the user should populate quotes and configurations and call PricingContext::initialize(), as this will intialize all elements required for pricing, such as discount curves and volatility surfaces. All risk factors or elements are keyed by a MarketIndex, so for example a SOFR leg can resolve against the SOFR curve already registered in the context. If a product references a MarketIndex not available in the context, an error is returned.
In this example
In this example, we create a flat SOFR curve. FlatForwardTermStructure represents a constant rate interpreted using its RateDefinition; here the input is 3% with continuous compounding. As we want to obtain sensitivities to this curve, the pillar label is required for sensitivity reporting.
let evaluation_date = Date::new(2024, 1, 15);
let discount_curve = FlatForwardTermStructure::new(
evaluation_date,
DualFwd::from(0.03),
RateDefinition::new(
DayCounter::Actual360,
Compounding::Continuous,
Frequency::Annual,
),
)
.with_pillar_label("SOFR_flat".to_string());
let mut constructed_elements = ConstructedElementStore::default();
constructed_elements.discount_curves_mut().insert(
MarketIndex::SOFR,
DiscountCurveElement::new(
MarketIndex::SOFR,
Rc::new(RefCell::new(discount_curve)),
),
);
let context = PricingContext::new()
.with_quote_store(QuoteStore::new(evaluation_date))
.with_fixing_store(FixingStore::default())
.with_base_currency(Currency::USD)
.with_constructed_elements(constructed_elements);
The curve is wrapped in Rc<RefCell<_>>, allowing constructed elements to be shared and updated by calibration workflows. The fixing store is empty because the swap starts on the evaluation date. The example does not call initialize() because its required curve has already been constructed and inserted.
4. Select a pricer and requests
A pricer connects a trade to market data in order to get different Requests. Its market_data_request() declares the required curves, fixings, FX rates, and volatility objects it needs to evaluate the product, and the market data provider resolves that declaration. An user can separately choose outputs with different Request, avoiding calculations that are not needed.
| Request | Meaning |
|---|---|
Request::Value | Present value or NPV |
Request::Cashflows | Coupon and payment details |
Request::Sensitivities | Derivatives with respect to labelled market pillars |
Request::FairRate | Rate that makes the instrument NPV equal to zero |
Request support is pricer-specific, as not all pricer and product share the same variables.
In this example
let pricer = DiscountedCashflowPricer::<Swap<DualFwd>, SwapTrade<DualFwd>>::new();
let requests = vec![Request::Value, Request::Cashflows, Request::Sensitivities];
let results = pricer.evaluate(&trade, &requests, &context)?;
The generic parameters of DiscountedCashflowPricer<I, T> identify its instrument and trade types. It supports value, fair rate, cashflows, and sensitivities for leg-based products; YieldToMaturity and ModifiedDuration are not populated by this pricer. Value, cashflows, and sensitivities share one prepared valuation state during this evaluate() call.
5. Interpret the results
EvaluationResults is an envelope of optional outputs. A getter returns Some(...) when the corresponding result was produced and None otherwise. Callers should read the fields associated with the requests they submitted rather than assume every field is present.
In this example
if let Some(price) = results.price() {
println!("Swap NPV = {price:.2}");
}
if let Some(sensitivities) = results.sensitivities() {
for (key, exposure) in sensitivities
.instrument_keys()
.iter()
.zip(sensitivities.exposure())
{
println!(" {key}: {exposure:.4}");
}
}
if let Some(cashflows) = results.cashflows() {
let dates = cashflows.payment_dates();
let types = cashflows.cashflow_types();
let amounts = cashflows.amounts();
let currencies = cashflows.currencies();
for i in 0..dates.len() {
println!(
"{:<12} {:<22} {:>14.2} {:>6}",
dates[i], types[i], amounts[i], currencies[i]
);
}
}
SensitivityMap contains parallel instrument_keys() and exposure() vectors. Each exposure is the derivative of NPV with respect to the labelled market pillar. This flat-curve example has one pillar, SOFR_flat, so it reports one value for (\partial\mathrm{NPV}/\partial r). A bootstrapped curve instead reports sensitivities against its quote labels, such as OIS_USD_SOFR_5Y.
CashflowsTable is column-oriented. In addition to the columns printed above, it exposes fixing(), accrual_periods(), leg_indices(), and optional caplet/floorlet strikes. Leg index 0 identifies fixed-leg rows and index 1 identifies floating-leg rows.
What to read next
- Rust API summarizes the traits behind the objects used above.
- Pricing Context explains configuration-driven market construction and
initialize(). - Interest Rate Swaps covers fair rates, spreads, fixings, and basis swaps.