Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

QuantSupport

QuantSupport (QS) is a quantitative finance library written in Rust, with Python and Excel bindings. It supports instrument valuation across several asset classes, market sensitivities using automatic differentiation, interest-rate and credit curve construction, volatility markets, Monte Carlo simulation, and valuation adjustments such as CVA and FVA.

The library separates observable market data, such as quotes and fixings, from objects constructed from those observations, such as bootstrapped curves and implied-volatility surfaces. Instruments describe contractual payments and observation rules. Trades combine those instruments with position terms, including direction, notional and trade date. A pricer then values the trade using the market data and numerical method appropriate to the contract.

Using the library

Rust provides direct access to the financial types and the interfaces used to extend pricing, market-data and simulation components. The Rust guide introduces these interfaces, and the first valuation constructs and prices a receive-fixed swap.

The Python package exposes native instruments and typed market configurations, with results available as scalar properties and pandas tables. Its context manager controls the lifetime of constructed markets and differentiation state. Working in Python provides a complete valuation and introduces scenarios, historical markets and scripting.

The Excel add-in provides worksheet functions for market construction, valuation and risk. Formulas return handles to native objects, allowing a workbook to express the dependencies between inputs and results. Working in Excel explains this workflow and its recalculation rules. The add-in requires 64-bit Windows desktop Excel.

Installation

QuantSupport can be used as a Rust crate, a Python package or an Excel add-in. Choose the interface that fits your application. The Python package includes a compiled Rust extension; the Excel add-in is distributed as a Windows XLL.

Rust applications

QuantSupport uses the Rust 2021 edition. A stable toolchain installed through rustup supplies the compiler and Cargo. Add the published crate to an application with:

cargo add quantsupport

A local application can instead use a path dependency during development:

[dependencies]
quantsupport = { path = "../quantsupport" }

The optional plot feature enables plotting support; it is not necessary for core pricing. Applications loading JSON add their own serde_json dependency. The library owns serializable financial types, while the application owns file locations and I/O.

For the source checkout:

git clone https://github.com/jmelo11/quantsupport
cd quantsupport
cargo build -p quantsupport
cargo run -p valuation-swaps

The workspace includes the library, Rust examples, benchmarks, and both binding packages. Run cargo test -p quantsupport to check the native library and its doctests. Contributor checks also include Clippy and documentation builds.

Python and Excel

Install the published Python package with:

python -m pip install quantsupport

For a manual download, the Python packages page provides folder ZIPs and wheels by operating system, architecture and Python version. Folder ZIPs contain only QuantSupport; install dependencies separately with python -m pip install pandas.

The package supports Python 3.9 and later and includes pandas reporting. Optional typing tools are available with quantsupport[typing]. To use local source changes, build the extension using the binding development instructions.

Excel requires 64-bit Windows desktop Excel. Installing the add-in provides the released XLL and platform-specific steps. Excel for macOS and the browser cannot load it.

The first valuation establishes a numerical reference in Rust. The Python and Excel chapters explain how their execution and reporting models differ while using the same native financial objects.

A first valuation

Consider a five-year USD interest-rate swap with a notional of USD 10 million. It receives a fixed coupon of 3%, using simple compounding and ACT/360 accrual, and pays a floating coupon every six months. We use MakeSwap to define the contract, then value it against a flat curve.

The complete example is examples/valuation/swaps/src/main.rs. Run it from the repository root:

cargo run -p valuation-swaps

From contract to market

The program below builds the instrument and wraps it in a SwapTrade to record the position. It then supplies a flat, continuously compounded 3% curve for both projection and discounting. This keeps the market setup small; the curve chapters explain how to replace it with curves calibrated to market quotes.

The floating leg uses one SOFR-indexed observation per coupon period. Daily compounded overnight coupons are available through MakeOvernightIndexSwap, introduced in Swaps.

extern crate quantsupport;
use std::{cell::RefCell, rc::Rc};
use quantsupport::prelude::*;

fn main() -> Result<()> {
    let rd = Date::new(2024, 1, 15);
    let notional = 10_000_000.0;
    let instrument = MakeSwap::<DualFwd>::default()
        .with_identifier("USD_IRS_5Y".into())
        .with_start_date(rd)
        .with_maturity_date(Date::new(2029, 1, 15))
        .with_notional(notional)
        .with_fixed_rate(0.03)
        .with_rate_definition(RateDefinition::new(
            DayCounter::Actual360, Compounding::Simple, Frequency::Semiannual,
        ))
        .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()?;
    let trade = SwapTrade::new(instrument, rd, notional, Side::LongReceive);
    let curve = FlatForwardTermStructure::new(
        rd, DualFwd::from(0.03),
        RateDefinition::new(
            DayCounter::Actual360, Compounding::Continuous, Frequency::Annual,
        ),
    ).with_pillar_label("SOFR_flat".into());
    let mut elements = ConstructedElementStore::default();
    elements.discount_curves_mut().insert(
        MarketIndex::SOFR,
        DiscountCurveElement::new(MarketIndex::SOFR, Rc::new(RefCell::new(curve))),
    );
    let context = PricingContext::new()
        .with_quote_store(QuoteStore::new(rd))
        .with_constructed_elements(elements);
    let pricer = DiscountedCashflowPricer::<Swap<DualFwd>, SwapTrade<DualFwd>>::new();
    let result = pricer.evaluate(
        &trade, &[Request::Value, Request::Cashflows, Request::Sensitivities],
        &context,
    )?;
    println!("NPV: {:?}", result.price());
    if let Some(risk) = result.sensitivities() {
        for (label, derivative) in risk.instrument_keys().iter().zip(risk.exposure()) {
            println!("{label}: {derivative}");
        }
    }
    Ok(())
}

No fixing is required here because the first observation occurs on the evaluation date and can be forecast when no same-day fixing is supplied. Seasoned coupons require the relevant historical observations.

Interpreting the result

The native example produces an NPV of approximately −10,723.59 USD, with flat-rate derivative −47,405,484.05 USD per unit rate, or approximately −4,740.55 USD for a +1 bp move to first order.

To understand the value, consider the floating coupon. A continuously compounded 3% curve implies a simple forward over an accrual interval \(\tau\) of

\[ L=\frac{e^{0.03\tau}-1}{\tau}, \]

which is slightly above the fixed coupon of 3%. The receive-fixed position therefore has a small negative value. The fixed coupon and the curve rate use different compounding conventions even though both were entered as 0.03.

The sensitivity is labelled SOFR_flat, the name assigned to the flat rate. It measures the PV change per unit change in that decimal rate. Multiplying by 0.0001 gives the first-order change for a one-basis-point increase. With calibrated curves, the report uses the original quote pillars instead.

The cashflow table contains 24 rows, including principal exchanges generated on both legs. It reports signed contractual amounts: receipts are positive and payments are negative, including initial advances. The table includes all supplied cashflows and has no discounted-PV column, so reconciling it to NPV requires payment selection and discounting. Swaps describes these reporting conventions in more detail.

Working in Rust

A Rust valuation starts with a maker. Define dates or tenors, call build() or build_using(&market), and wrap the resulting instrument in a trade. A pricer evaluates that trade against a MarketDataProvider and returns the requested results, such as value, cashflows and sensitivities. Import quantsupport::prelude::* to bring the common workflow types into scope.

The construction guide covers date anchors, floating loan amortization and reuse across contexts. Fully specified makers can also construct instruments directly with build().

The instrument, market provider and pricer have separate interfaces. This allows an application to reuse a contract with different markets or numerical methods. For example, tests can supply a flat curve directly, while a portfolio application can construct its curves from quoted instruments.

Scalars and ownership

Many mathematical types are generic over T: Scalar. Plain f64 supports numerical work without derivative recording. Fwd<T> carries a value and a forward derivative seed, and nesting it raises the derivative order. Dual<T> records reverse-mode operations. Standard deterministic pricing and constructed-market wrappers generally use DualFwd = Dual<Fwd2>, with Fwd2 = Fwd<Fwd<f64>>.

Choose a scalar supported by the full valuation workflow. The generic discounted-cashflow pricer, for example, requires trades implementing LegsProvider<DualFwd>, even though many underlying mathematical types also support f64 and other scalars.

Constructed curves commonly use Rc<RefCell<dyn ADCurveElement>>. They share market objects within a workflow and permit controlled mutation such as pillar registration. Their underlying AD values refer to thread-local tape state. Parallel scripting and exposure therefore reconstruct worker-local markets; they do not move a live tape graph freely between threads.

Requests, dependencies and results

There are two separate requests. The caller’s Request selects outputs such as value, cashflows or sensitivities. The pricer’s MarketDataRequest declares inputs such as discount curves, fixings, FX or a volatility market. A provider resolves the latter before the pricer runs its valuation.

EvaluationResults contains optional fields for the measures produced. Request support and cashflow reporting conventions depend on the selected pricer. Check its reference when choosing requests or reconciling reported amounts to PV; an unsupported request leaves its result field unpopulated.

Concrete pricers provide compile-time dispatch. For a heterogeneous portfolio, Evaluator associates an erased pricer with a trade’s TypeId and performs runtime dispatch. Registration is an application decision; PricerConfiguration does not turn every direct Rust pricer into an automatically selected service. For example, configured swaptions use ClosedFormHullWhiteSwaptionPricer::from_configuration.

Choosing an extension boundary

An instrument owns contractual terms. Its capabilities, such as access to legs, determine which pricers can consume it. A DiscountPolicy selects the market index used to discount an object; it should not rewrite the contract. An InterestRatesTermStructure<T> answers discount and forward queries without requiring the consumer to know how the curve was constructed.

Choose the interface that corresponds to the component you need to extend: Pricer for a valuation method, MarketDataProvider for a market-data source, and the term-structure interface for a curve representation. A MarketModel provides pathwise observations for scripts and exposure claims.

Fallible public operations use Result<T> with QSError. Context attached to an error identifies the failing construction, event or path while preserving the original category through root_cause(). Propagate errors with ? and interpret them at an application boundary. Numerical solvers also expose convergence status; successful return alone is not a universal proof that an iteration tolerance was met.

The first valuation provides a complete Rust program. Use the Rust reference for signatures, parameter descriptions and implementation-specific behavior.

Working in Python

The Python package provides access to QuantSupport’s native instruments, market construction, valuation and risk calculations. Build a contract and trade, configure its market, then evaluate the position inside a pricing context. Results are available as scalar properties and pandas tables.

Classes can be imported from domain packages or from the package root:

import quantsupport as qs
from quantsupport.pricing import PricingContext, Trade

assert Trade is qs.Trade

A complete market and valuation

A small store of zero-rate quotes makes the example independent of external data files. Its input rates are continuously compounded annual decimals. Configuration remains serializable; the live curve is constructed when pricing requests it.

import quantsupport as qs

quotes = qs.QuoteStore.from_dict({
    "reference_date": "2026-01-02",
    "quotes": [
        {"identifier": f"ZeroRate_USD_SOFR_{tenor}", "mid": 0.03}
        for tenor in ["1Y", "5Y", "10Y"]
    ],
})
curve = qs.CurveConfiguration(
    market_index=qs.MarketIndex.SOFR,
    quotes=quotes.identifiers(),
    enable_extrapolation=True,
)
swap = qs.Swap(
    "USD_IRS_5Y", "2026-01-02", "2031-01-02",
    1_000_000.0, 0.035, "USD", "SOFR",
)
position = qs.Trade(swap, "2026-01-02", qs.Side.LongReceive)
context = qs.PricingContext(quotes=quotes, curves=[curve])
with context:
    result = context.evaluate(
        position, [qs.Request.Value, qs.Request.Cashflows, qs.Request.Sensitivities]
    )
    print(result.price)
    print(result.cashflows)
    print(result.sensitivities)

An instrument contains the contract; Trade supplies the position date, direction and supported notional overrides. Bond trades additionally require a BondPricingAnchor. Native dispatch chooses the compatible pricer, using typed context-level pricer configurations for model-dependent choices. Python-defined pricers can also be registered explicitly.

Context lifetime and mutability

Entering with context: validates configuration and activates the pricing scope. It does not eagerly build the entire market. Requests construct and cache the dependencies they consume. Explicit context.initialize() performs eager construction when that is the intended workflow.

Exiting the outer scope releases the context’s constructed market and its AD state, including on exceptions. Keep returned results for reporting, not live market views whose validity depends on that scope. Inputs have explicit setters: assigning a replacement collection invalidates dependent state. Collection properties return copies, so edit-and-reassign rather than assume an in-place change has reached the native context.

Contract and result properties are generally read-only. Operations such as evaluation, curve queries and serialization are methods. Date-like inputs accept the forms documented for their signatures, commonly qs.Date, datetime.date and ISO date strings. Enum strings are conveniences at specific conversion boundaries, not permission to invent new market identities.

Scenarios and historical markets

Temporary shocks belong in an independent scenario scope:

scenario = qs.Scenario(shocks=[
    qs.Shock(target="SOFR", value=0.0001, shock_type=qs.ScenarioType.Absolute),
])
with context, scenario:
    stressed = context.evaluate(position, [qs.Request.Value])

The base inputs remain unchanged. Scenarios compose in entry order and rebuild only requested affected dependencies. To reprice against an observed historical market, select a complete snapshot from a market archive:

archive = qs.MarketSnapshotStore()
archive.insert(qs.MarketSnapshot.from_context(
    qs.DateTime(2026, 1, 2, 16, 30), context,
))
selector = qs.SnapshotSelector(
    qs.DateTime(2026, 1, 2, 17),
    rule=qs.SnapshotSelectionRule.Previous,
    level=qs.Level.Mid,
)
with archive.pricing_context(selector) as historical:
    print(historical.snapshot_timestamp, historical.evaluation_date)
    print(historical.evaluate(position, [qs.Request.Value]).price)

The observation instant and market business date are distinct. Selection does not roll curves to the requested date. Archives retain construction inputs, not runtime AD graphs, fixing publication vintages, custom Python pricers or Python-only policy registrations. Supply those separately as appropriate.

Scripts and portfolio workflows

context.evaluate_script evaluates a ScriptedProduct or corresponding dictionary through native rate models and returns averaged script variables. Call it inside an entered context or after explicit initialization, with a positive path count. Required discount and forward indices need configured rate models; absent models do not imply zero-volatility dynamics. Its result_variable identifies the target for derivative/control processing; it does not filter the returned dictionary. Models are resolved from pricer configuration, then simulation configuration, with script model overrides. Hull–White settings map to the equivalent LGM rate representation.

The discount index needs a configured model and constructed curves; a script outside the base currency needs an explicit discount index. This service does not expose arbitrary FX/equity factors, custom correlations, raw paths, or scripted cashflow/sensitivity tables. Native-trade exposure/XVA is available through the separate context XVA workflow. Use the Rust interfaces when the workflow needs those additional facilities.

Native failures use QuantSupportError; invalid Python inputs can also raise TypeError or ValueError. The executable notebooks develop complete applications. The optional Python guide develops binding-specific workflows; the Python reference provides declaration-level details.

Download and install

Download the Excel add-in and IntelliSense ZIP for 64-bit Windows Excel

The ZIP includes quantsupport.xll, its matching XML metadata, ExcelDna.IntelliSense64.xll, installation instructions and license notices. The standalone XLL remains available in the same download directory.

Release history provides versioned downloads. Excel for macOS and Excel in the browser cannot load an XLL.

  1. Extract all files from the ZIP into a stable local folder.
  2. In Excel open File > Options > Add-ins.
  3. Select Excel Add-ins, click Go, then Browse, and load both XLLs. Keep quantsupport.intellisense.xml beside quantsupport.xll, then restart Excel.
  4. Enter =QS.VERSION() to check that it loaded.
  5. Open the swap example workbook and press Ctrl+Alt+F9 to rebuild its object handles.

The workbook includes pricing, curve tables, cashflows, and sensitivities.

Discover functions

=QS.FUNCTIONS() lists worksheet names. =QS.FUNCTIONS.DETAILS() spills the catalog. =QS.HELP("QS.INSTRUMENT.SWAP") displays parameter guidance in cells. The same help appears in Excel’s Function Arguments dialog and the optional worksheet reference.

For parameter hints while typing, load the ZIP’s included ExcelDna.IntelliSense64.xll alongside QuantSupport and keep the matching quantsupport.intellisense.xml beside quantsupport.xll. Restart Excel with both add-ins enabled, then type =QS.EVALUATE followed by an opening parenthesis to see its signature and active-argument help. The companion requires .NET Framework 4.6.2 or later.

Without the companion, type the function name and press Ctrl+A for the Function Arguments dialog, or Ctrl+Shift+A to insert its argument names.

Function names and aliases are public. Registry objects, raw pointers, and Rust helper functions are internal. Scalar date functions can run on calculation threads; registry and pricing functions run on Excel’s main thread.

Build from source

On Windows, install the 64-bit MSVC Rust toolchain and Visual Studio Build Tools, then run from the repository root:

rustup target add x86_64-pc-windows-msvc
powershell -ExecutionPolicy Bypass -File tools/excel/build.ps1

The build packages bindings/excel/dist/quantsupport-excel-windows-x64.zip and the individual XLL, XML and companion files. It downloads a pinned Excel-DNA IntelliSense release and verifies its SHA-256. Keep the files together if moving the add-in.

Working in Excel

The Excel add-in lets a workbook construct markets, define trades and request valuations from the native library. Input cells hold quotes and contractual terms. Worksheet functions combine them into market and trade objects, which pricing functions consume to calculate values and sensitivities.

Excel’s formula dependencies determine recalculation order. Object handles connect the formulas to native objects owned by the add-in.

Handles and dependencies

A constructor returns a revisioned handle such as QSObject:QuoteStore:usd_quotes:3. The add-in, not the cell, owns the native object. Handles are process-local and must be reconstructed from formulas after Excel restarts. Use workbook-unique names and a single ordered mutation chain:

A1 =QS.QUOTES.CREATE("usd_quotes","2026-01-02")
A2 =QS.QUOTES.SET(A1,"OIS_USD_SOFR_1Y",B2,,)
A3 =QS.QUOTES.SET(A2,"OIS_USD_SOFR_2Y",B3,,)
A4 =QS.CURVE.CONFIGS.CREATE("usd_curves")
A5 =QS.CURVE.CONFIGS.ADD(A4,"SOFR","Actual360","LogLinear",TRUE,E2:E10)
A6 =QS.CONTEXT.CREATE("usd_ctx",A3,A5,,,,,,,,,,"USD","SOFR")
A7 =QS.RESULT.VALUE(D6)
A8 =QS.CURVE.GET(A6,"SOFR")
A9 =QS.CURVE.TABLE(A8)

To use this fragment, populate E2:E10 with matching quote identifiers and D2 with a position from QS.TRADE.CREATE on a QS.INSTRUMENT.SWAP contract. Store an explicit policy and native pricer in D4:D5, then evaluate:

D4 =QS.DISCOUNT.POLICY.CSA("usd_policy","USD","SOFR")
D5 =QS.PRICER.CREATE("usd_pricer","DiscountedCashflowPricer",D4)
D6 =QS.EVALUATE("usd_results",D5,D2,A6,"Value")

The instrument defines contractual terms, the trade defines position terms, and the pricer selects the discount policy. Funding is an ordinary configured curve selected by that policy. Table result readers accept transpose and headers flags; QS.REQUEST.FAIR.RATE(index) requests one zero-based leg or loan rate segment and produces a scalar fair_rate result. The bundled workbook supplies the complete inputs.

Unchanged recalculation reuses cached objects. A changed quote advances its handle and invalidates dependent contexts and values; reads reject stale revisions. Branching mutations from one old revision can make another branch stale. Arrange the worksheet so each update consumes the preceding handle and the final context depends on the completed input chain.

When rows are deleted or identifiers renamed, clear and repopulate a store using QS.OBJECT.RESET. A version cell provides an explicit rebuild signal:

A1 =QS.QUOTES.CREATE("usd_quotes",$B$1)
A2 =QS.OBJECT.RESET(A1,$Z$1)
A3 =QS.QUOTES.SET(A2,"OIS_USD_SOFR_1Y",B3,,)

Financial interpretation

Dates are ISO strings. Rates and spreads are decimals, not percentage-point inputs. A curve’s day count and compounding convention do not replace the contract’s coupon conventions. An index such as SOFR identifies market objects; it does not automatically turn a regular swap into a daily-compounded OIS or select every required foreign-currency curve.

The output table’s semantics matter as much as its layout. Generic DCF cashflows show signed amounts, positive for receipts and negative for payments, and do not supply a PV column. Quote sensitivities are derivatives in each input’s units: multiply a decimal-rate derivative by 0.0001 to estimate a +1 bp change. Compare currencies only after an explicit, supported conversion.

Bond trades carry the valuation anchor. Supply CleanPrice, DirtyPrice or ZSpread and its value to QS.TRADE.CREATE on a QS.INSTRUMENT.FIXED.BOND contract. Evaluate with an explicitly configured BondPricer, then read QS.RESULT.VALUE, QS.RESULT.MODIFIED.DURATION and QS.RESULT.Z.SPREAD. A quote anchors the solved spread; it is not a replacement for signed position PV.

Scope and reproducibility

Worksheet functions cover market construction, contracts, pricing, analytics, curve inspection and native historical snapshots. Registry operations and pricing execute on Excel’s main thread; registered scalar functions can be thread-safe. Volatile registrations still reuse unchanged cached inputs. There is no worksheet script constructor; the direct payoff-program interfaces described later belong to Rust and Python.

For historical analysis, serialize coherent construction inputs rather than save handle strings. Historical valuation explains timestamp selection, quote levels and separately supplied fixings. QS.FUNCTIONS, QS.FUNCTIONS.DETAILS and QS.HELP expose worksheet help. The optional Excel guide develops binding-specific workflows, while the worksheet reference provides signatures without changing the dependency model described here.

Historical pricing

Historical pricing evaluates a trade against a sequence of recorded markets. The Excel workflow uses the same native snapshot archive and selection rules as Python. Each snapshot records the observations and construction inputs for one market instant.

Load the XLL before opening the example workbook, then press Ctrl+Alt+F9 to reconstruct its objects. Regenerate both Excel examples with python tools/excel/examples/create_examples.py after installing bindings/excel/examples/requirements.txt. The historical workbook uses explicit zero rates; the same snapshot workflow accepts bootstrapped markets, volatility, FX, simulation, and model configurations.

Capture each market

Build a separate context for each market reference date with the usual QS.QUOTES.*, curve configurations, and QS.CONTEXT.CREATE. Give every constructor a workbook-unique name. Store the context handles in B12 and C12, and capture their observations:

B13 =QS.SNAPSHOT.FROM.CONTEXT("history_snapshot_20260115","2026-01-15T16:00:00Z",B12)
C13 =QS.SNAPSHOT.FROM.CONTEXT("history_snapshot_20260116","2026-01-16T16:00:00Z",C12)
B15 =QS.SNAPSHOTS.CREATE("history_archive",B13:C13)

The archive is reconstructed from the full range. Changes to market inputs advance snapshot and archive handles; deleting a row removes that observation. Blank range cells are skipped. Duplicate instants, including equivalent timezone offsets, are errors. A missing or blank scalar input creates an empty archive. Unchanged recalculation reuses current handles without reconstructing markets.

Each snapshot contains one complete set of native construction inputs. To use curves already supplied as zero rates in a sheet, place ISO date text or tenor text in A5:A7 and annual decimal rates in B5:B7:

B10 =QS.CURVE.CONFIGS.CREATE("history_curves_20260115")
B11 =QS.CURVE.CONFIGS.ADD.ZERO(B10,"SOFR","Actual365","Linear",FALSE,A5:A7,B5:B7,"Continuous","Annual")

Pass B11 as the curve configurations when creating the context. The pillar and rate ranges must have equal counts; rates must be finite numbers. Pillars must resolve in increasing date order. Composite construction is also available:

=QS.CURVE.CONFIGS.ADD.COMPOSITE(configurations,"TermSOFR6m","SOFR","TermSOFR3m")

The two component curves must be configured in the same market. Direct QS.CURVE.FLAT / constructed-element objects are runtime values and cannot be captured. An explicit zero curve with one 0D pillar and extrapolation enabled provides serializable flat-curve construction inputs.

Select and price

Keep the trade handle in B17. Create one selector/context pair for each requested timestamp and reuse the same trade:

B20 =QS.SNAPSHOT.SELECTOR("history_selector_20260115","2026-01-15T16:00:00Z","Exact","Mid")
B21 =QS.CONTEXT.FROM.SNAPSHOT("history_selected_20260115",$B$15,B20,)
B22 =QS.CONTEXT.SNAPSHOT.TIMESTAMP(B21)
B23 =QS.CONTEXT.EVALUATION.DATE(B21)
B31 =QS.DISCOUNT.POLICY.CSA("history_policy","USD","SOFR")
B32 =QS.PRICER.CREATE("history_pricer","DiscountedCashflowPricer",B31)
B33 =QS.EVALUATE("history_results",B32,$B$17,B21,"Value")
B24 =QS.RESULT.VALUE(B33)

The optional fourth context argument is a FixingStore handle. Fixings are supplied separately, without publication-vintage filtering. Use the correct historical fixing dataset when publication times or revisions affect a result.

Selector ruleBehavior
ExactRequires that exact UTC instant.
Previous (default)Latest snapshot at or before the request.
ClosestNearest snapshot; ties prefer the earlier instant; may select future data.

The default quote side is Mid; Bid and Ask rebuild quoted construction and calibration inputs at that side. A consumed missing side raises an error. FX spots are side-independent; zero-rate quotes use the selected level.

Selection preserves the matched snapshot’s market reference date as the valuation date. Requesting Monday with Previous and matching Friday prices at Friday’s market date; it does not roll curves to Monday. The observation instant and market business date are distinct, including across UTC midnight. Inspect both context properties when displaying historical results.

QS.SNAPSHOTS.SELECT(name,store,selector) returns the matched snapshot handle without constructing a context. Inspect it with QS.SNAPSHOT.TIMESTAMP and QS.SNAPSHOT.REFERENCE.DATE. QS.SNAPSHOTS.TIMESTAMPS(store) lists available instants, while QS.CONTEXT.QUOTE.LEVEL(context) reports the construction side. QS.DATETIME.PARSE normalizes offset-aware timestamp text to UTC; QS.DATETIME.DATE returns its UTC calendar day. Neither accepts naive timestamps or Excel serial datetimes.

Python and Rust interoperability

QS.SNAPSHOTS.FROM.JSON(name,json_text) imports the same native snapshot-array schema saved by Python’s MarketSnapshotStore.to_json. QS.SNAPSHOTS.TO.JSON(store) returns JSON text, excluding fixing histories, constructed curves, and AD state. These functions exchange JSON as worksheet text. Export fails above Excel’s 32767 UTF-16-unit cell limit.

The archive captures base market configurations. Materialized scenario contexts with supplied runtime elements are rejected; capture the original base context. The workbook chapter relates these handles to ordinary valuation and recalculation. The worksheet reference provides exact argument lists.

Cashflows, contracts and positions

QuantSupport represents many financial contracts as cashflows grouped into legs. An instrument collects contractual terms, including payment amounts, dates and observation rules. A trade adds the position held in that instrument. Pricers use these objects for valuation, and supported trades can also be converted into claims for exposure simulation.

Amounts and signs

A fixed coupon combines notional \(N\), a rate \(r\), and a rate definition. Its general amount is \(N(C(r,\tau)-1)\), where \(C\) is the compound factor. Only simple compounding reduces this to \(Nr\tau\). A floating coupon combines an observed or projected index rate with spread and accrual conventions. Optional coupon variants retain a cap, floor or more general payoff expression. Redemptions and disbursements represent principal movements separately.

A leg shares currency, side, projection identity and optional discount identity. Its side is LongReceive or PayShort, with signs \(+1\) and \(-1\). For a regular swap, leg zero is fixed and leg one is floating; the builder assigns opposite directions. Product-specific pricers and claim converters apply position direction according to their own contract/trade conventions. Do not multiply signs twice because both an instrument and a trade expose direction-related fields.

Cashflow reports show signed amounts: receipts are positive and payments are negative. The generic report applies leg direction and reverses disbursements, so an initial advance has the opposite sign to principal repayment. The loan pricer reports projected signed receipts after prepayment and credit adjustments.

Schedules are economic inputs

MakeSchedule and product builders convert start/maturity dates, payment frequency, calendar, business-day convention and generation rule into dated obligations. Front and back stubs, end-of-month behavior and termination-date adjustment can change amounts as well as payment dates.

A coupon has an accrual interval, an observation date or schedule, and a payment date. These dates serve different purposes. For example, a loan can pay monthly while resetting against EURIBOR annually. An OIS coupon compounds daily overnight observations with the appropriate accrual weights. Its observation rules determine which fixings enter the coupon, while payment lag determines when the resulting amount is paid.

DayCounter converts dates into accrual fractions. Curve interpolation can use a different day counter from the coupon; the equivalent projected rate must still be expressed on the coupon’s basis. Calendar adjustment also differs from day-count measurement. Neither can be inferred from a displayed rate alone.

Calendars

Generic builders often default to a null calendar and unadjusted dates; market-specific builders may apply presets. SOFR OIS uses its index calendar, while SONIA and TONAR use London and Japanese settlement holidays. Canonical serialized forms include UnitedKingdom(Settlement) and Japan.

Joint calendars encode which markets must be open. JoinHolidays requires all components to be business days; JoinBusinessDays requires at least one. Canonical nested definitions round-trip through JSON. Local holiday overrides participate in calculations, but serialization stores the calendar definition, not an independently complete record of mutable overrides.

Holiday rules are historical models too. London recurring rules follow the regime since 1971; Japan’s public-holiday rules begin in July 1948. Future Japanese equinox dates remain estimates until announced, and exceptional closures require updates or explicit local overrides.

Builders and positions

Make* makers retain contractual inputs. Their MakeInstrument::build method uses supplied parameters; build_using supplies a reference date and market observations when needed. Both methods return concrete instruments for trades and pricers. Missing context dependencies raise UnresolvedProductErr. See Building instruments explicitly.

Makers validate required terms and construct the instrument. Check their defaults against the contract’s market conventions. MakeSwap produces regular single-fixing coupons; MakeOvernightIndexSwap starts from index-specific overnight conventions. Bond and loan builders have distinct principal, schedule and optionality responsibilities.

A trade adds date, side and position sizing as supported by the product. Bond positions additionally carry a clean/dirty price or spread anchor. Discounting remains a pricing policy rather than a general contractual promise embedded in every instrument.

For simulation, supported trades implement IntoContingentClaims. Each claim represents a scheduled payment with evaluation instructions and stable trade and leg identifiers. Conversion checks whether the exposure implementation supports the contract’s features. The legacy claim converter, for example, rejects nonstandard overnight conventions.

Building instruments explicitly

Instrument makers implement MakeInstrument. This interface fragment shows its two construction methods and date-resolution hook:

pub trait MakeInstrument: Sized {
    type Instrument;
    fn is_resolved(&self) -> bool;
    fn resolve_dates(&self, reference_date: Date) -> Result<Self>;
    fn build(&self) -> Result<Self::Instrument>;
    fn build_using(&self, context: &dyn MarketDataProvider)
        -> Result<Self::Instrument>;
}

Both methods return the same concrete product type and leave the maker unchanged. Import MakeInstrument to call these trait methods; it is included in quantsupport::prelude::*. A trade contains a built instrument and an explicit execution date. Pricers and portfolio evaluators value those contracts without rebuilding their terms.

is_resolved() checks whether date inputs and required market observations are already resolved. It does not validate the contract. build() uses this check and, when it returns false, reports the missing dependency and directs the caller to supply it explicitly or call build_using(&context).

The trait’s default build_using() calls resolve_dates() with the context’s evaluation date, then calls build() on the returned maker. Each maker defines its date anchors and conventions. resolve_dates() leaves the original maker unchanged and does not fetch market observations. The loan maker overrides build_using() to obtain its initial amortization rate when needed.

Build from supplied terms

build() constructs directly from the maker’s concrete terms. Date tenors, including a maturity tenor paired with an explicit start date, are resolved by build_using(). Unresolved date inputs or market observations return QSError::UnresolvedProductErr. Missing mandatory terms and invalid values keep their normal error categories.

extern crate quantsupport;
use quantsupport::prelude::*;

fn main() -> Result<()> {
let deposit = MakeFixedRateDeposit::<f64>::default()
    .with_identifier("deposit".into())
    .with_start_date(Date::new(2026, 2, 24))
    .with_maturity_date(Date::new(2027, 2, 24))
    .with_currency(Currency::EUR)
    .with_notional(100_000.0)
    .with_rate(0.03)
    .with_rate_definition(RateDefinition::new(DayCounter::Actual360, Compounding::Simple, Frequency::Annual))
    .build()?;
assert_eq!(deposit.maturity_date(), Date::new(2027, 2, 24));
Ok(())
}

Build with a context

build_using() supplies an evaluation date, fixing policy, and market observations explicitly. The provider can be a PricingContext, MarketView, or another MarketDataProvider.

This fragment uses an existing market and pricer to build and value a swap:

let maker = MakeSwap::<DualFwd>::default()
    .with_identifier("spot-5y".into())
    .with_start_tenor(Period::from_str("0D")?)
    .with_tenor(Period::from_str("5Y")?)
    .with_notional(1_000_000.0)
    .with_fixed_rate(0.03)
    .with_currency(Currency::EUR)
    .with_market_index(MarketIndex::EURIBOR6m);

assert!(matches!(maker.build(), Err(QSError::UnresolvedProductErr(_))));
let instrument = maker.build_using(&market)?;
let trade = SwapTrade::new(
    instrument, market.evaluation_date(), 1_000_000.0, Side::LongReceive,
);
let result = pricer.evaluate(&trade, &[Request::Value], &market)?;

The maker can build another product using another context. Revaluing the existing trade preserves its contractual dates and principal. Market changes affect its projected receipts, discounting, and risk.

Date anchors and conventions

TermsTenor methodAnchor
Scheduled startwith_start_tenorContext evaluation date
Scheduled maturitywith_tenor / with_maturity_tenorConstructed start
First couponwith_first_coupon_tenorConstructed start
Penultimate couponwith_next_to_last_coupon_tenorMaturity; usually a negative offset
Equity/FX option or equity future expirywith_expiry_tenorContext evaluation date
Forward deliverywith_delivery_tenorContext evaluation date
NDF fixingas_ndf_tenorContext evaluation date
Swaption expirywith_expiry_tenorContext evaluation date
Swaption underlying startwith_start_tenorExpiry
Swaption maturitywith_swap_tenorUnderlying start
Rate future accrual endwith_end_tenorAccrual start
Loan CPR observationswith_prepayment_tenorsLoan start
Script eventswith_event_tenorsExplicit script reference date or context date

Date and tenor setters replace the alternative input. Supplied absolute dates are preserved. Relative inputs use the maker’s calendar and business-day conventions. With a calendar, day tenors count business days; longer tenors use calendar arithmetic followed by adjustment. Negative offsets are supported.

Floating loan amortization

Floating-first French and balloon loans need an initial all-in rate to establish contractual principal. Supply with_initial_amortization_rate, or call build_using() to obtain the first index observation and apply the spread and coupon payoff. Fixed-first loans can use their fixed coupon; bullet loans do not need an amortization forecast.

With fixing anchoring enabled, observations strictly before the evaluation date require stored fixings. Today and future observations project from curves. Disabling anchoring projects all observations, provided the curve supports their interval. Missing fixings or curves fail during construction. The built loan retains its initial amortization rate and principal schedule across valuations.

Python

Python exposes named makers with the same methods. A maker is separate from its product and cannot be placed into Trade.

maker = qs.MakeSwap(
    identifier="spot-5y", start_tenor="0D", tenor="5Y",
    notional=1_000_000, fixed_rate=0.03, currency="USD", market_index="SOFR",
)
# maker.build() raises qs.UnresolvedProductErr: no reference date was supplied.
with context:
    instrument = maker.build_using(context)
    trade = qs.Trade(instrument, context.evaluation_date)
    result = context.evaluate(trade)

MakeLoan follows the same approach for its initial amortization observation. MakeTrade can construct an explicit execution date from trade_tenor using build_using(context). Existing product constructors remain convenient for fully specified contracts. No instrument or trade is resolved during pricing.

Excel

QS.INSTRUMENT.* and QS.TRADE.CREATE take a construction context immediately after the object name. They publish handles only after construction succeeds. For example:

=QS.INSTRUMENT.FX.FORWARD("fx",B2,"","EUR","USD",1.10,"1Y")
=QS.TRADE.CREATE("position",B2,D2,"","Receive",1000000)

Here B2 contains a context handle. Relative dates and initial loan amortization are calculated during construction. Background construction may show #N/A while waiting for committed market inputs; it never publishes an unresolved product. Context revisions invalidate dependent constructor calculations.

Observations and historical markets

Market construction starts from quotes, historical fixings and spot FX rates. QuantSupport stores these observations separately because they are used at different stages of valuation. QuoteStore supplies the prices or rates used for calibration at a reference date. FixingStore supplies realized index observations, and FxStore supplies spot exchange rates. A pricing context combines these stores with the instructions needed to construct curves and other market objects.

Quotes and identities

A Quote combines QuoteDetails with QuoteLevels. Details describe the quoted instrument, while levels hold optional bid, mid and ask values. Construction chooses one Level; a missing consumed level is an error, not a request to substitute another side.

#![allow(unused)]
fn main() {
extern crate quantsupport;
use quantsupport::prelude::{Quote, QuoteDetails, QuoteLevels, QSError};

let details: QuoteDetails = "FixedRateDeposit_USD_SOFR_6M".parse()?;
let quote = Quote::new(details, QuoteLevels::with_mid(0.04));
assert_eq!(quote.levels().mid(), Some(0.04));
Ok::<(), QSError>(())
}

Quote identifiers specify the calibration contract. Swap_USD_TermSOFR3m_5Y_Semiannual_Quarterly identifies product, currency, projection index, tenor and payment frequencies. An option identifier also specifies expiry and strike conventions; for a swaption, expiry and underlying swap tenor are separate coordinates.

Rate indices own their swap defaults through RateIndexDetails::swap_conventions(tenor): fixed coupon day count and compounding, both payment frequencies, floating accrual day count, settlement lag and business-day adjustment. Swap quotes and swaption underlyings consume these defaults. Direct MakeSwap builders use the same metadata. There is no shared semiannual/quarterly fallback.

For example, Euribor uses annual 30/360 fixed coupons, floating payments at the index tenor with ACT/360 accrual, TARGET and two settlement days. TIBOR uses semiannual ACT/365 fixed coupons; NIBOR uses annual 30/360 fixed coupons. SONIA uses annual ACT/365 coupons and same-day settlement, while SOFR uses annual ACT/360 coupons and two settlement days. AUD BBSW fixed payments are quarterly below four years and semiannual from four years.

All quote parameters are positional. A regular swap accepts Swap_CCY_Index_Tenor[_FixedFreq[_FloatFreq[_SettlementDays[_FixedDC[_FloatDC]]]]]. Empty fields retain the index defaults. For example, Swap_EUR_EURIBOR3m_1Y___1_Actual365 changes settlement to one business day and fixed accrual to ACT/365. Appending _Actual360 explicitly sets the floating day count too. Overrides apply independently, including when supplied through Python quote records or Excel quote identifiers.

Day counts are also positional for deposits, futures, bonds, zero rates, CDS, caps, caplets, OIS, basis and cross-currency swaps, and swaptions. Bond coupon and yield day counts are separate; two-leg contracts allow separate accrual conventions. See the QuoteDetails identifier schemas for each field order.

Swaption_USD_SOFR_1Y_5Y_ATM_Black quotes a one-year-expiry option on a five-year swap. CapletFloorlet_USD_SOFR_3M_1Y_Absolute_0.045_Straddle_Black quotes an option with a three-month index tenor, one-year expiry and 4.5% absolute strike. Black volatility is relative lognormal volatility; Normal volatility is in absolute rate units per square-root year.

MarketIndex connects these contracts to constructed objects. A standard rate index such as SOFR is distinct from Collateral(CLP, USD), which discounts CLP payments under USD collateral, or Credit("NAME"), which identifies credit survival. Their serialized forms must agree across configurations, models and contracts.

Curves may also consume direct observations through CurveConfiguration.quotes: DiscountFactor_USD_SOFR_1Y supplies a discount factor, and ZeroRate_USD_SOFR_2027-02-24 supplies an annual decimal zero rate. Pillars accept tenors or ISO dates. Zero rates default to Continuous compounding and Annual frequency; append _Compounded_Semiannual, for example, to change the rate convention. The configuration supplies day count, interpolation and extrapolation. Use one direct quote family with consistent currency and rate conventions per curve, with the quoted index matching the configured index. These inputs retain their quote identifiers, selected sides and scenario risk. Discount-factor curves automatically include the fixed reference-date factor of one and require at least one future pillar.

Fixings

Fixings resolve observations already known at valuation. Missing required past observations are errors. Same-day observations can be used when present and forecast otherwise in the relevant coupon workflows. The fixing date, not merely the payment date or trade date, determines whether history is required.

Daily overnight accrual needs business-day observations with weekend and holiday accrual weights. Term coupons may require one reset for several payments. Supply the required historical observations explicitly. Handle gaps through a validated data-preparation policy. The current FixingStore::fill_missing_fixings uses reversed interpolation coordinates and applies no business-day filter; its reference documents that restriction.

FX observations

FxStore interprets a stored rate from base to quote as quote-currency units per base unit: storing USD→CLP at 935 means one USD buys 935 CLP. Lookup uses a direct rate, a reciprocal rate, or breadth-first triangulation through connected currencies. A disconnected pair is an error.

Pricers request spot rates from this graph and determine which amounts to convert and which currency to report. Ordinary FX requests use the stored spot market even when the request carries a date. To select a historical FX market, load the corresponding snapshot as described below.

Coherent historical markets

MarketSnapshot records one observation instant together with a complete set of native construction inputs. MarketSnapshotStore holds such observations. Snapshots preserve quote levels, FX and configuration—including direct zero rates—but not constructed curves or an AD tape. Contexts supplied with runtime market objects cannot be captured as if those objects were serializable construction instructions.

Timestamp selection and valuation date answer different questions. Previous selects the latest instant at or before the request; Exact requires the instant itself; Closest chooses the nearest observation, preferring the earlier on ties, and can select future data. Timestamp values normalize offset-aware input to UTC. Duplicate equivalent instants are rejected.

The selected snapshot retains its own market reference date. Selecting Friday’s market for a Monday request does not roll Friday’s curves to Monday. Inputs from several snapshots are not blended. A selected bid or ask market rebuilds all consumed instrument, zero-rate and discount-factor quotes at that level; spot FX is side-independent.

Fixing histories are supplied separately and do not carry publication-vintage filtering. Historical valuation therefore requires the application to supply the appropriate historical observations, including any revision policy. The Python and Excel workflows use the same native archive and selection rules.

Constructing and querying a market

A valuation needs a consistent set of observations and constructed market objects. PricingContext brings these together: it holds quote, fixing and FX stores, curve and model configurations, and the objects built from them. Its evaluation date is the quote store’s reference date.

A pricer requests the market data needed for a trade, and the context constructs and caches the corresponding dependencies. Applications can also call initialize() to build the configured market in advance.

The following fragment illustrates configuration-driven setup; quotes and curves are previously loaded typed inputs:

let mut context = PricingContext::new()
    .with_quote_store(quotes)
    .with_curve_configurations(curves)
    .with_fixing_store(fixings)
    .with_fx_store(fx)
    .with_base_currency(Currency::USD)
    .with_base_index(MarketIndex::SOFR)
    .with_quote_level(Level::Mid);
context.initialize()?;

validate() checks compatibility and duplicate configuration without calibrating curves or generating paths. initialize() clears request caches, starts from supplied runtime elements, then constructs configured discount curves, credit curves, volatility surfaces, cubes and simulations in that order. Later components can therefore consume earlier dependencies. Reinitialization rebuilds from the original inputs.

Native pricers and simulation services manage their own differentiation recording scopes. In Python, entering a context also establishes the binding’s scope and enables construction on request; explicit initialize() performs the eager build.

Dependency-driven construction

A MarketDataRequest asks for a focused subset of the market: projection and discount curves, a volatility surface/cube, simulations, historical fixings, spot FX, or typed pricing/calibration configuration. A request through the provider constructs and caches required objects and dependencies.

For example, a swap may need only two rate curves and a fixing series even if the context also describes unrelated equities and simulations. A calibrated swaption additionally needs source option identities and its volatility market. The context reports missing configuration for the requested resource.

ConstructedElementStore groups constructed objects by MarketIndex. Directly supplied curves are useful for controlled tests such as the first valuation. Configuration-driven markets are more suitable for serialization, snapshots and coherent scenario reconstruction.

Discounting is a policy

An index identifies a curve; a DiscountPolicy determines which curve applies to an object. SingleCurveCSADiscountPolicy selects its collateral index for payments in the collateral currency and Collateral(payment_ccy, collateral_ccy) for other currencies. FixedIncomeDiscountPolicy can prefer an instrument index or choose a configured per-currency risk-free index.

The bootstrap policy uses these rules while solving calibration instruments. Valuation pricers must also receive the appropriate policy. The context’s base currency/index guide base collateral construction, but they are not a promise that every direct Rust pricer will automatically install a policy or convert its output to that currency.

PricerConfiguration associates an instrument family with a typed pricing implementation and model assignments. Binding services use these assignments for supported families. Direct Rust callers construct the selected concrete pricer.

Independent market views

A Scenario borrows the base context and presents transformed quotes, including direct zero rates, through an independent request cache. It does not mutate the base store or require reconstructing every configured simulation. Unchanged calibrated values can be reused with detached AD wrappers; affected dependencies are rebuilt when consumed.

Construction inputs can be retained between valuations, while each market view has its own cache and each valuation uses temporary differentiation state. These lifetimes determine when a curve can be reused and when it must be rebuilt. The risk chapter explains how market inputs remain connected to the reported derivatives.

with_zero_rate_risk() and reproject_risk() create RiskContext views for reporting sensitivities in another curve basis. They retain the pricing curves and change their AD connections to the reported pillars. Zero-rate views use rates implied from constructed discount factors; instrument views use the implied quotes of a supplied calibration basket. Both are accepted by existing Rust pricers through MarketDataProvider. See the risk basis examples.

Discount factors and rate conventions

Interest-rate curves provide the discount factors used to value future payments and the forward rates used to project floating coupons. QuantSupport exposes these queries through InterestRatesTermStructure<T>, so a pricer can use a calibrated curve, a flat curve or a composite funding curve through the same interface.

A curve’s rate conventions determine how discount factors translate into quoted rates. Understanding these conventions is useful both when constructing a market and when interpreting a forward-rate query.

Write \(P(0,t)\) for the discount factor from reference date zero to date \(t\). For an interval with year fraction \(\tau\),

\[ P(t_1,t_2)=\frac{P(0,t_2)}{P(0,t_1)},\qquad L(t_1,t_2)=\frac{P(0,t_1)/P(0,t_2)-1}{\tau}. \]

The second expression is a simple annualized forward rate. The equivalent continuously compounded forward is \(-\log(P(t_1,t_2))/\tau\). Both describe the same accumulation; their numbers need not agree. A curve’s interpolation clock and a coupon’s accrual basis must also be distinguished when translating between them.

Rate definitions

RateDefinition combines day counter, compounding and frequency. InterestRate uses that definition to translate a rate into compound and discount factors. For year fraction \(\tau\), principal conventions are

\[ C_{\rm simple}=1+r\tau,\qquad C_{\rm periodic}=(1+r/m)^{m\tau},\qquad C_{\rm continuous}=e^{r\tau}. \]

Mixed simple/periodic conventions additionally switch behavior according to their interval rule. Frequency matters for periodic compounding even when payment dates follow another schedule. The first-swap example illustrates why identical displayed percentages under different conventions are not equal financial prices.

Day counters include ACT/360, ACT/365, 30/360 variants, Actual/Actual and Business/252. The day counter measures the accrual fraction between dates, while the calendar determines any business-day adjustment to those dates.

ActualActual splits at calendar-year boundaries and includes every complete intervening year. Thirty360US adjusts February month-ends after its 31st-day adjustments, differing from common US/NASD rules. Validate these conventions against the required dates; their API references describe the implemented rules. The examples here use ACT/360 or ACT/365.

Curve representations

DiscountTermStructure<T> stores dated discount factors. Its first factor is one at the reference date, and date/value vectors must agree. Pricing values are interpolated on year fractions. With log-linear interpolation,

\[ \log P(t)=(1-w)\log P(t_i)+w\log P(t_{i+1}), \quad w=\frac{t-t_i}{t_{i+1}-t_i}, \]

so the instantaneous forward is constant between adjacent nodes. Linear and cubic-spline choices describe different interpolation models. Extrapolation is a separate permission: log-linear continuation extends the terminal forward, while linear/cubic choices use their implemented linear extrapolation. Disabled extrapolation makes out-of-domain queries errors.

FlatForwardTermStructure<T> represents the term structure with one rate and rate definition. An explicit ZeroRateTermStructure interpolates zero rates, and tenor-based zero curves resolve pillar dates from the reference date. Choose the representation and interpolator together, since they determine the implied shape between pillars.

A spread representation can express a continuously compounded adjustment:

\[ s(t)=-\frac{\log(P_{\rm target}(t)/P_{\rm base}(t))}{t}, \qquad P_s(t)=e^{-s(t)t}. \]

CompositeTermStructure multiplies component factors, \(P=P_bP_s\). Matching reference dates and day counters keep the component clocks consistent. This is useful for separating benchmark and funding inputs, and is supported through serializable curve configurations as well as direct construction.

Pricing nodes and risk pillars

A calibrated curve has discount-factor nodes used for interpolation and market quote pillars used for sensitivity reporting. Calibration connects the two: changing a deposit, swap or bond quote changes the solved discount factors and hence the value of a trade. On some directly constructed curves, the pricing nodes themselves are the independent risk inputs.

Pillars<DualFwd> exposes labels, values and tape-registration operations. Calibration supplies a Jacobian to connect pricing nodes to independent market inputs. A derivative labeled OIS_USD_SOFR_5Y is therefore a derivative with respect to that quoted contract, not necessarily a five-year zero rate or a discount-factor node. The next chapter derives that connection.

Calibration and dependent curves

Curve calibration chooses discount factors so that the prices or rates of selected instruments reproduce market observations. A quote-based CurveConfiguration specifies the instruments, interpolation rule and market index of the resulting curve.

Some calibration instruments need other curves. For example, a term-rate projection curve may use an overnight curve for discounting. MultiCurveBootstrapper resolves these dependencies, then solves a nonlinear system for all pillars of each curve.

Residuals and the nonlinear solve

Let \(x=(P_1,\ldots,P_n)\) be unknown factors at the calibration pillars, \(q\) the associated market quotes, and \(z\) already solved parent-curve inputs. Calibration imposes

\[ F(x,q,z)=0. \]

Each residual follows the quoted contract. Deposits compare implied and quoted rates; par swaps and basis swaps contribute NPV conditions; futures compare implied and quoted rates with supported convexity adjustment. FX forwards use their outright/points strategy. Bond anchors compare model price with either a quoted price or a yield-converted target, normally in units per 100. These bond strategies do not constitute OAS calibration.

The bootstrapper uses a Newton solve with finite-difference residual Jacobians. Trial bumps and accepted steps preserve positive discount factors. The current setup starts each factor at 0.99, with tolerance 1e-12 and 200 iterations. Inspect convergence status and repricing residuals: iteration exhaustion can return a result without SolverErr.

Dependencies and collateral

Calibration instruments may project one index and discount on another. SOFR discounting can therefore be a parent of a Term SOFR projection curve calibrated from basis swaps. Foreign-currency payments under USD collateral may require Collateral(CLP, USD) and an FX spot store. A topological sort orders the configured dependencies; absent parents and cycles are errors.

The discount policy makes these roles explicit. For USD collateral, USD payments use the selected USD collateral index and CLP payments use their USD-collateralized CLP curve. A local ICP curve can separately project CLP coupons. Only the instruments actually chosen as collateral-curve pillars determine whether ICP is a dependency of that calibration.

The bootstrapper determines solve order from dependencies, so configurations can be supplied in any order. The same list can include quoted zero-rate curves and composites. A composite depends on both component curves and can itself become a parent of calibrated curves. Duplicate identities, self-references and cycles are rejected.

Overnight-index calibration instruments

An OIS exchanges fixed coupons against compounded overnight interest. OIS_CCY_Index_Tenor identifies this contract and builds an OvernightIndexSwap for calibration. The preset uses coupon-only legs with annual schedules, index calendars and day counts, Modified Following accrual adjustment and Following payment adjustment. SOFR uses two business days to settlement and two business days of payment lag; SONIA uses zero settlement days and CORRA one. Other index presets use two settlement days and zero payment lag.

Optional positional fields in an OIS identifier override those contract conventions. Empty intermediate fields retain defaults: for example, OIS_USD_SOFR_1Y___0_3 retains frequencies and sets settlement/payment lag to zero/three. Calendars use canonical names such as UnitedStates(Sofr), not an assumed bare-country alias.

Standard future compounded coupons use discount-ratio telescoping. Seasoned coupons compound historical business-day observations with their accrual weights, then project the remainder. Same-day observations are used when available and forecast otherwise. Payment lag affects discounting, not accrual. Lookback, lockout, observation shift and daily spread compounding alter observation semantics and must match the actual contract. Deterministic valuation supports nonstandard overnight settings that the legacy XVA claim converter rejects.

Swap_CCY_Index_Tenor[_FixedFreq[_FloatFreq[_SettlementDays[_FixedDC[_FloatDC]]]]] uses single-fixing floating coupons, including when the index is overnight. Its defaults come from the index’s swap_conventions(tenor) metadata, which is separate from the rate’s deposit/forward quoting definition. The tenor runs from settlement on the index calendar. Swaption underlyings and direct swap builders use the same defaults.

Euribor defaults to annual Thirty360 (bond basis) fixed coupons, floating coupons at the index tenor with ACT/360 accrual, TARGET and two settlement days. TIBOR defaults to semiannual ACT/365 fixed coupons, while NIBOR uses annual 30/360. SOFR uses annual ACT/360 on both legs; SONIA uses annual ACT/365 and zero settlement days. There is no universal fixed-leg default.

Optional fields override each convention independently. For example, Swap_EUR_EURIBOR3m_1Y_Annual_Quarterly_1_Thirty360_Actual360 supplies both frequencies, one settlement day and both accrual conventions. Swap_EUR_EURIBOR3m_1Y___1_Actual365 retains the index frequencies and floating day count while changing settlement and fixed accrual. Empty fields retain defaults. Rust builders and serialized quote details can supply the same terms through their individual setters and fields.

Settlement lag delays the start of accrual; regular swap payments occur at accrual end. Use OIS quotes for compounded daily observations and their separate payment-lag conventions. To reproduce curves made with the former semiannual/quarterly defaults, supply those frequencies and the former day counts explicitly.

To reproduce a supplied market curve, match the calibration contracts, interpolation and valuation assumptions as well as the quoted rates.

In particular, a quote’s extraction date need not be its effective date, and discount_factor(T) is anchored at the curve reference date. Compare a factor measured from a later start S using discount_factor(T) / discount_factor(S). The curve’s day counter measures interpolation time; it does not override the swap quote’s fixed or floating coupon day counts. See the executable Euribor 3M reconciliation for an annual-versus-semiannual comparison and an independent cashflow check.

Differentiating calibration

At a converged solution, implicit differentiation gives

\[ \frac{\partial x}{\partial q} =-J^{-1}\frac{\partial F}{\partial q},\qquad \frac{\partial x}{\partial z} =-J^{-1}\frac{\partial F}{\partial z}, \quad J=\frac{\partial F}{\partial x}. \]

The implemented own-quote derivative is diagonal by calibration-instrument construction. Parent dependencies are finite-differenced. These matrices reconnect solved factors to tape inputs through a local first-order relation:

\[ x_i^{\rm AD}=x_i^\ast +\sum_j S_{ij}(q_j^{\rm AD}-q_j^\ast) +\sum_k C_{ik}(z_k^{\rm AD}-z_k^\ast). \]

The relation preserves the calibrated value and its local market derivatives, including chained parent-curve risk. It does not replay the Newton iterations or establish exact second-order differentiation of the calibration map. Finite-difference Jacobians and solver tolerances remain numerical approximations.

Credit calibration

CreditCurveBootstrapper strips piecewise-constant hazards from CDS par spreads. A CreditCurveElement uses a term-structure representation whose curve value is survival probability \(Q(t)\), not a risk-free discount factor. Configuration supplies credit identity, payment currency, recovery, premium frequency and the discount index for the premium/protection legs.

For each maturity the new hazard interval is solved with earlier intervals fixed. The implemented bisection search uses bounds \(10^{-12}\) to 20 and 200 iterations. A finite-difference IFT step connects survival to CDS quotes. Empty quote sets and duplicate maturities are rejected. Discount and credit curves therefore share construction and risk concepts, but their numerical values have different economic meanings.

Volatility markets and model calibration

Option markets quote implied volatility across expiries, strikes and, for swaptions, underlying swap tenors. QuantSupport constructs surfaces and cubes from these observations. Pricers can query them directly or use them as targets when calibrating a stochastic model.

Calibration converts the selected volatility quotes into option prices, then fits model parameters to those prices. The resulting parameters describe the model’s dynamics and use units appropriate to that model.

Coordinates and interpolation

VolatilitySurfaceConfiguration selects expiry/smile quotes. VolatilityCubeConfiguration adds underlying tenor, as needed for swaptions. The volatility type is Black or Normal; the smile coordinate can be strike, delta or log-moneyness. The generic Strike::resolve maps Absolute, Atm and Relative to \(K\), \(F\) and \(F+\text{spread}\). Consumers do not all use that helper: the Black and Hull–White caplet/cap pricers interpret Relative(p) multiplicatively as \(F(1+p)\). Prefer an absolute strike when specifying an unambiguous contractual rate.

Configured identifiers supply the actual coordinates. Equivalent tenors such as 1Y and 12M cannot define duplicate nodes. Builders reject empty sets, nonpositive expiry/tenor, nonfinite keys and negative or nonfinite volatilities. The selected bid/mid/ask level supplies the quoted values.

Interpolated surfaces first interpolate within each smile, then across expiry. Cubes interpolate smile, tenor and finally expiry. Rectangular grids reproduce bilinear/trilinear interpolation, but sparse grids can have different strikes per slice and different tenor sets per expiry. Exact nodes reproduce quoted values without needing a neighboring slice.

ExtrapolationPolicy::Reject is the default. A query must lie within each selected slice; a singleton coordinate must match exactly. Flat clamps to boundaries and treats singleton axes as constant. Both reject nonfinite queries. Interpolation is in the implemented volatility coordinates, not an automatic guarantee of arbitrage-free total variance.

An FX market can store one pair orientation. OrientedFxVolSurface resolves a reciprocal request and maps inverse strike with \(K\mapsto1/K\). Smile-type metadata elsewhere does not guarantee universal conversion between strike, delta and moneyness. In particular, the current equity option pricer passes a resolved strike directly; configure a compatible surface rather than assume a delta inversion is being solved.

Source instruments and risk pillars

Constructed volatility objects retain calibration-instrument identities. These encode option expiry, tenor and strike and allow model calibration to select a meaningful basket. Risk pillars identify differentiable inputs whose labels appear in sensitivity results. An interpolated quote grid commonly uses the same identifiers for both purposes, but the roles are conceptually different.

CalibrationBasket selects expiries, tenors and a strike rule from the configured source. Omitted axes include all available values. Every explicitly requested value must exist and participate after the other filters are applied. A caplet surface’s tenor is the index observation tenor; a cube’s tenor is the underlying swap tenor. A three-month caplet source cannot satisfy a six-month basket by silently reinterpreting those contracts.

Parameter sources

ParameterSource::Fixed supplies a complete model parameter set. ParameterSource::Calibrated names a surface or cube and a semantic basket. Hull–White and LGM use Gaussian sigma parameters; lognormal Brownian motion uses a volatility parameter. These are distinct units and parameterizations, even when they are ultimately fitted to the same option premium.

Gaussian calibration converts each selected caplet or swaption’s Black or Normal volatility into an option price. It fits a constant sigma to each instrument independently, then assembles the fitted values into a piecewise schedule. Repricing under that combined schedule can differ from the targets because the individual fits do not jointly solve its interval volatilities.

Hull–White quality reports retain instrument identity, target volatility/price, fitted price, sigma, forward and strike. Their fitted prices describe the individual constant-sigma solves, not repricing under the assembled schedule.

For lognormal Brownian motion, term-volatility stripping instead enforces

\[ \int_0^{T_i}\sigma(u)^2\,du=\sigma_{\rm term}(T_i)^2T_i. \]

Negative forward variance is rejected. The stripped interval volatilities reproduce the accumulated variance implied by the selected term quotes.

The Gaussian calibration Jacobian uses diagonal forward differences with absolute volatility bump \(h=10^{-6}\):

\[ \frac{\partial\sigma_i}{\partial\nu_j} \approx\delta_{ij} \frac{\sigma_i(\nu_i+h)-\sigma_i(\nu_i)}{h}. \]

This numerical response holds curve inputs and calibration strikes fixed. It is not a full joint schedule-calibration Jacobian. Direct pricing may still have curve risk. Cube calibration selects at most one underlying tenor per expiry and must respect the source domain. Materialized numerical simulations introduce further derivative limits, explained in Stochastic models.

Swaps and discounted cashflows

An interest-rate swap is valued by projecting its floating coupons and discounting the payments on both legs. Historical fixings determine coupons whose observations are already known; forward curves supply the remaining rates. The discount policy selects the curve appropriate to each payment.

For the payments included in the valuation, we can write

\[ V=\sum_{\ell}s_\ell\sum_i C_{\ell i}\,P_{d,\ell}(0,T_{\ell i})\,X_{\ell i}. \]

Here \(s_\ell\) is leg direction and \(X_{\ell i}\) is the applicable conversion factor when the pricing policy calls for conversion. The pricer’s payment-selection and currency-conversion conventions determine which terms enter the sum. The generic implementation’s conventions are described below.

Regular fixed–floating swaps

MakeSwap builds fixed leg zero and floating leg one; SwapTrade supplies position metadata. A receive-fixed contract uses LongReceive on its fixed side. For simple fixed coupons, \(C_i=NK\tau_i\); other compounding conventions use the coupon’s compound factor instead.

A regular floating coupon has a single observation. The pricer uses required historical fixings for past observations and otherwise projects from the forward curve. The spread is then added under the coupon’s conventions. Missing required history is an error. Choosing SOFR as the index does not change this into overnight daily accrual.

Projection and discounting are separate. A Term SOFR coupon can project from TermSOFR3m while a USD CSA discounts it on SOFR. Without an explicit discount policy, the generic pricer attempts to infer curves from leg identities; ambiguous choices can fail. Production multi-curve setups should make collateral assumptions explicit.

Payment selection and reporting

The generic DiscountedCashflowPricer iterates the cashflows supplied by the instrument; it does not apply a universal payment_date > evaluation_date filter. Do not use it as an implicit settlement/seasoning filter for an arbitrary existing leg. Specialized bond and loan pricers apply their own payment-selection conventions.

The default HandleCashflows report shows signed amounts, including principal and initial disbursements where present. It applies leg direction and reverses disbursements: receipts are positive and payments are negative. Use its payment dates, leg indices, currencies and observations to inspect the contract. Reconciling the table to PV also requires payment selection, discount factors and any currency conversion.

In the first valuation, the builder produces principal exchanges on both sides, giving 24 rows in the cashflow table.

Fair fixed rate

For a vanilla fixed-versus-floating coupon swap under a specified market, the familiar expression is

\[ K^\ast=\frac{PV_{\rm float}}{A},\qquad A=\sum_iN_i\tau_iP_d(0,T_i). \]

Request::FairRate(index) selects a zero-based leg. Target 0 solves the fixed coupon and target 1 the floating spread on a vanilla swap. The scalar result makes total trade PV zero, retaining all other legs’ quotes, principal exchanges, projection indices, discount policy and leg signs. Selected fixed or floating coupons are replaced by a common coupon/spread; mixed coupon types in one selected leg, daily spread compounding and legs without rate exposure are rejected. Request one target per evaluation. The same selection applies to linear basis and cross-currency legs under a compatible native discount policy.

Overnight index swaps

MakeOvernightIndexSwap constructs OvernightIndexSwap with coupon-only legs and index-specific overnight conventions:

#![allow(unused)]
fn main() {
extern crate quantsupport;
use quantsupport::prelude::*;

let rd = Date::new(2026, 1, 2);
let ois = MakeOvernightIndexSwap::<DualFwd>::new(
    MarketIndex::SOFR, rd, Period::new(5, TimeUnit::Years), 0.04,
)
.with_identifier("USD_SOFR_OIS_5Y".into())
.with_notional(10_000_000.0)
.with_side(Side::LongReceive)
.build()?;
let _trade = OvernightIndexSwapTrade::new(
    ois, rd, 10_000_000.0, Side::LongReceive,
);
Ok::<(), QSError>(())
}

For standard future compounding the overnight accumulation telescopes to a discount-factor ratio. Seasoned periods combine weighted historical daily observations with projected remainder. Lookback, lockout, observation shift, daily spread treatment and payment lag are separate inputs; the calibration chapter explains their market presets and the legacy XVA conversion limit.

Basis swaps extend the same logic to two floating indices. Their value and quote risk depend on both projection curves and the policy-selected discount curve. They are also calibration instruments, so a basis curve’s derivative can carry parent OIS risk through the IFT.

Deposits and floating-rate notes share leg-based mechanics. Straight bonds require settlement, quote anchors and yield/spread analytics and therefore have a specialized bond pricer. Listed rate futures use RateFuturesPricer: implied rate is the decimal \((100-Q)/100\), and contract size is value per futures price point, not per basis point. A one-basis-point rate change changes a 100-100r quote by 0.01 price points. These unit distinctions are essential before comparing their risk with swap sensitivities.

Cross-currency contracts

A cross-currency contract exchanges payments in two currencies. Valuation requires curves for projecting each leg, discount curves consistent with the collateral agreement, and an FX rate for converting between currencies. Cross-currency market quotes help calibrate the collateral-adjusted curves.

For a USD-collateralized USD/CLP swap, for example, SOFR can provide USD projection and discounting, ICP can project CLP coupons, and a separate USD-collateralized CLP curve discounts those payments.

Swaps and collateral curves

Fixed–floating and floating–floating cross-currency builders create two currency legs with initial/final principal exchanges. Notionals can be chosen to be spot-equivalent at inception: for USD→CLP spot \(S\) quoted in CLP per USD, CLP principal is USD principal times \(S\). State this orientation explicitly when supplying spot or interpreting results.

A USD CSA policy selects SOFR for USD payments and Collateral(CLP, USD) for CLP payments. The latter is not the local CLP ICP projection curve. Calibration can use FX forward quotes and cross-currency swaps, with an FX store supplying consistent inception notionals.

For a CLP payment converted to USD under that policy, the implemented discount-and-convert contribution has the form

\[ PV_{\rm USD}=C_{\rm CLP}\,S_{\rm CLP\to USD} \,P_{\rm CLP|USD}(0,T). \]

The pricer’s actual currency rule must be checked for each product; this is not a universal context-level reporting conversion. Generic DCF payment filtering and zero-based FairRate(index) leg selection described in Swaps apply.

Risk follows the construction graph. USD projection/discounting reaches SOFR quotes; CLP coupon projection reaches ICP; collateral discounting reaches cross-currency and FX-forward calibration inputs. Spot risk also requires recorded spot inputs. The existence of a spot store alone does not guarantee that every particular sensitivity report includes all spot dependencies.

FX forwards: the implemented convention

MakeFxForward creates a future currency exchange, with optional non-deliverable settlement configuration. FxForwardPricer requires an explicit discount policy and supports value, fair rate and sensitivities. Its current implementation computes

\[ F_{\rm impl}=S\frac{P_{\rm quote}(0,T)}{P_{\rm base}(0,T)},\qquad V=N\,s\,(F_{\rm impl}-K)P_{\rm quote}(0,T) \]

when a contractual forward price is present.

For spot expressed as quote currency per base currency, this implementation uses the reciprocal of the usual covered-interest-parity discount ratio. The FX-option pricer uses the usual base/quote ratio. This discrepancy affects comparisons between the two pricers and must be accounted for when reproducing forward values or testing option parity.

An outright forward rate and forward points are different calibration strategies. AnchorOutrightPrice uses the outright quote; AnchorForwardPoints uses the configured points interpretation. Their market identities and native implementation must match the feed’s units. Replicating a vendor forward requires validating orientation, quote units, collateral curves and settlement behavior together.

Bonds and issuer optionality

Bond valuation combines contractual coupon and principal payments with a market quotation. For a straight fixed-rate bond, QuantSupport solves the spread implied by a price anchor and provides yield and duration analytics. For a stepped-coupon bond with issuer calls, valuation also requires a short-rate model and an exercise calculation.

InstrumentPricerSupported analytics
FixedRateBondBondPricervalue, cashflows, yield to maturity, modified duration, Z-spread, sensitivities, accrued amount
SteppedCouponBondTreeBasedBondPricer or BackwardFourierEvolutionBondPricervalue, cashflows, OAS, sensitivities, accrued amount

Stepped-coupon bonds use price or spread anchors. Their supported analytics are value, cashflows, OAS, sensitivities and accrued amount; yield-to-maturity and modified-duration requests leave those result fields absent.

Contractual accrued amount

We use Request::AccruedAmount(start, end) to request signed coupon interest between two dates. The result is an undiscounted currency amount in EvaluationResults::accrued_amount(), using the context base currency when configured. Principal is excluded, and the requested dates are clipped to each coupon period. Adjacent windows reconcile to the complete coupon.

The request uses numeric coupon accrual without solving a spread, normalizing a clean or dirty quote, or recording a differentiation tape. A callable bond reports contractual interest assuming its scheduled coupons continue; this measure does not include future exercise-probability weighting. Accrual alone requires no tree or Fourier valuation model.

Pricing anchors

Every bond trade stores exactly one required BondPricingAnchor. Pricers are reusable and take no quote in their constructors:

  • CleanPrice(value) is a price per bond units excluding accrued interest.
  • DirtyPrice(value) is a price per bond units including accrued interest.
  • ZSpread(value) is a continuously compounded spread for a straight bond.
  • OAS(value) is an option-adjusted spread for a bond with issuer calls.

A clean or dirty price anchor implies Z-spread for a straight bond and OAS for a bond with issuer calls. The pricer rejects OAS on a straight bond and Z-spread on a callable bond.

The solved or supplied spread becomes an independent AD variable. The common sensitivity table therefore reports curve risk together with either dPV/dZSpread or dPV/dOAS.

Straight fixed-rate bonds

Only coupons and principal strictly after settlement enter valuation. A clean quote is converted to dirty value by adding accrued interest. Z-spread z solves

\[ B_{\rm dirty}(S;z)=\sum_{T_i>S}CF_i \frac{P(0,T_i)}{P(0,S)} \exp\!\left[-z\,\tau(S,T_i)\right]. \]

Here \(S\) is settlement and \(\tau\) uses the spread day counter. Quoted prices are normalized to the bond’s units; the expression above is the settlement value before that normalization.

The solver expands a bracket and bisects inside it. Yield to maturity is solved independently under the fixed bond’s day-count, frequency, and compounding convention. Modified duration is -(dP/dy)/P, using the analytic derivative of each yield discount factor.

Request::Value remains signed position PV at the evaluation date. A quoted price anchors the spread; it does not redefine PV.

Stepped-coupon contract

The builder delegates date generation to MakeSchedule. In addition to payment_frequency, it accepts a calendar, business-day convention, termination-date convention, date-generation rule, end-of-month flag, first-coupon date and next-to-last-coupon date. The final date defaults to the same adjustment convention as intermediate dates. Python exposes the same schedule controls.

Side belongs to SteppedCouponBondTrade or FixedRateBondTrade, together with the pricing anchor and optional settlement/spread day-count terms. Instrument builders do not accept side. Cashflows describe the contract; the pricer applies the trade direction to PV and sensitivities, and claim decomposition applies that direction to XVA claims.

SteppedCouponBond is built from three independent schedules:

  1. coupons is positional. Entry zero applies to the first generated coupon period, entry one to the second, and so on. If the payment schedule is longer, the last coupon definition repeats through maturity. A list longer than the generated schedule is rejected rather than silently truncated.
  2. notional_percentages is also positional and uses the same carry-forward rule. Each value is the fraction of original principal outstanding during that period. The first value must be 1.0; values must remain in (0, 1] and cannot increase. Extra entries beyond the generated schedule are rejected.
  3. call_schedule is not positional. Each entry supplies start_date, an optional end_date, and strike. Without an end date it is a discrete call. Otherwise it is an inclusive exercise window. Windows are sorted, must follow the bond start and end before final payment, and cannot overlap, including at a shared endpoint. The legacy serialized date field remains accepted as an alias for start_date.

A coupon schedule contains the existing CashflowType<T> variants:

FixedRateCoupon(FixedRateCoupon<T>)
FloatingRateCoupon(FloatingRateCoupon<T>)
OptionEmbeddedCoupon(OptionEmbeddedCoupon<T>)

For a floating fixing L, the all-in coupon rate is

\[ R=\min\!\left(\max(L+\text{spread},\text{floor}),\text{cap}\right), \]

with a missing floor interpreted as negative infinity and a missing cap as positive infinity. Fixed coupons use the bond’s coupon day counter. Floating coupons use the referenced index convention.

An outstanding-notional reduction creates a principal redemption at the end of that coupon period. The remaining principal is redeemed at maturity.

Call entries give the issuer the right to redeem; holder puts are unsupported. Strikes are quoted in bond units and exclude same-day coupons. During backward valuation the engine first discounts continuation, then applies the issuer obstacle min(V, K), then adds contractual cashflows mapped to that date. Consequently, same-day coupon and scheduled amortization are paid before the remaining bond is called.

The following Rust fragment illustrates the contract and position terms. Its context and market configuration are supplied separately.

let bond = MakeSteppedCouponBond::<DualFwd>::default()
    .with_identifier("USD-STEP-CALL-2031".into())
    .with_start_date(Date::new(2025, 1, 1))
    .with_maturity_date(Date::new(2031, 1, 1))
    .with_notional(1_000_000.0)
    .with_units(100.0)
    .with_currency(Currency::USD)
    .with_payment_frequency(Frequency::Semiannual)
    .with_coupons(vec![
        CashflowType::fixed(0.045),
        CashflowType::floating(
            MarketIndex::SOFR,
            0.006,
            Some(0.0),
            Some(0.06),
        ),
    ])
    .with_notional_percentages(vec![1.0, 0.90, 0.75])
    .with_call_schedule(vec![
        CallScheduleEntry::window(
          Date::new(2028, 1, 1), Date::new(2028, 6, 30), 101.0,
        ),
        CallScheduleEntry::new(Date::new(2029, 1, 1), 100.0),
    ])
    .build()?;

let trade = SteppedCouponBondTrade::new(
    bond, Date::new(2025, 1, 1), 1_000_000.0, Side::LongReceive,
    BondPricingAnchor::OAS(0.0075),
);
let mut pricer = TreeBasedBondPricer::new();
  pricer.set_discount_policy(Box::new(
    FixedIncomeDiscountPolicy::new(false)
      .with_risk_free_index(Currency::USD, MarketIndex::SOFR),
  ));

The stepped bond and its builder have no discount index. Discounting is a pricing choice resolved by the pricer’s DiscountPolicy. Straight-bond pricing also requires a policy; an embedded instrument index does not bypass it. A policy can explicitly choose whether to prefer legacy instrument indices.

CashflowType::fixed and CashflowType::floating create coupon templates. The builder rebinds existing cashflows to the generated dates and outstanding notional, preserving rates, spreads, payoff operations and fixed-rate conventions unless a day counter is overridden.

Floating-coupon projection convention

Historical in-advance coupons use the fixing stored on their accrual-start date. Future coupons use the current forward curve for their reference index, then apply spread, floor, and cap. The resulting amount remains an AD expression, so its forward-curve sensitivity appears in the ordinary risk table.

The callable engines use the Hull–White short-rate state to value the issuer option. Future floating coupons are projected from valuation-date forwards and remain deterministic within that exercise calculation. Their forward-curve sensitivity is retained, but the engines omit stochastic future coupon fixings.

Context-level pricer and model selection

Choose the callable engine through a PricerConfiguration for InstrumentType::SteppedCouponBond. The context uses the same configuration mechanism for other product families. Each model assignment associates a typed ModelConfiguration with a MarketIndex.

A tree configuration for the stepped-coupon bond is:

{
  "instrument_type": "SteppedCouponBond",
  "pricer": {
    "TreeBased": {
      "time_grid": {
        "steps_per_year": 96,
        "max_steps": 4096,
        "day_counter": "Actual365"
      }
    }
  },
  "models": [{
    "market_index": "SOFR",
    "model": {
      "HullWhite": {
        "alpha": 0.05,
        "parameter_source": { "Fixed": { "sigma": 0.01 } }
      }
    }
  }]
}

Backward Fourier evolution uses the same model assignment:

{
  "instrument_type": "SteppedCouponBond",
  "pricer": {
    "BackwardFourierEvolution": {
      "time_grid": {
        "steps_per_year": 96,
        "max_steps": 4096,
        "day_counter": "Actual365"
      },
      "fourier_grid": {
        "points": 512,
        "standard_deviations": 10.0
      }
    }
  },
  "models": [{
    "market_index": "SOFR",
    "model": {
      "HullWhite": {
        "alpha": 0.05,
        "parameter_source": { "Fixed": { "sigma": 0.01 } }
      }
    }
  }]
}

The configured model is resolved using the policy-selected discount index. Automatic Python dispatch reads the same configuration and instantiates the selected engine.

Hull-White dynamics and curve fit

Both callable engines use the one-factor Hull-White state

\[ dx_t=-\alpha x_t\,dt+\sigma\,dW_t,\qquad r_t=x_t+\phi(t). \]

For one interval dt, the exact transition is

\[ E[x_{t+\Delta}\mid x_t=x]=e^{-\alpha\Delta}x,\qquad \operatorname{Var}[x_{t+\Delta}\mid x_t] =\sigma^2\frac{1-e^{-2\alpha\Delta}}{2\alpha}, \]

with limit sigma^2 dt when alpha is zero. The deterministic shift phi(t) fits the initial discount curve. OAS is added to the short rate after that fit, so changing OAS does not recalibrate drift.

Contractual payments and discrete call dates are mapped to their nearest time grid points. Call windows permit exercise at every numerical time inside the inclusive interval in both engines, not only at endpoints. A window that already started remains exercisable at settlement until its end date. Strikes use principal remaining after amortization mapped to the exercise step. Increasing steps_per_year improves time resolution; max_steps caps total work.

Recombining tree

TreeBasedBondPricer places nodes at spacing sqrt(3 Var[dx]). At each node it selects the nearest middle successor to the conditional mean and assigns three probabilities that match the first two conditional moments. State prices propagate forward, and the deterministic shift at each layer is chosen so their discounted sum matches the input curve discount factor. The bond value is then obtained by backward induction with the call obstacle.

Backward Fourier evolution

BackwardFourierEvolutionBondPricer uses a uniform state grid. If \(V_{i+1}(x)\) is the next-step value and \(\eta\) the Gaussian innovation, first form the convolution

\[ g_i(y)=\mathcal F^{-1}\!\left( \mathcal F(V_{i+1})(k) e^{-\tfrac12\operatorname{Var}(\eta)k^2}\right)(y). \]

The conditional expectation is then \(E[V_{i+1}(e^{-\alpha\Delta}x+\eta)]=g_i(e^{-\alpha\Delta}x)\). Separating convolution from evaluation at the mean avoids confusing the Fourier multiplier with the mean-reversion coordinate transformation.

The implementation performs an FFT, multiplies by the Gaussian characteristic function, and performs an inverse FFT. It interpolates at the mean-reverted state, discounts by exp(-(x + phi_i + OAS)dt), and applies the same call obstacle as the tree.

fourier_grid.points must be a power of two. More points reduce interpolation and spectral error. standard_deviations controls the truncated state-domain half-width; increasing it reduces periodic wrap-around at the cost of a wider grid.

The Fourier curve fit uses Gaussian integrated-state variance. With I_n = dt sum_(i<n) x_i, Gaussianity gives

\[ E[e^{-I_n}]=e^{\tfrac12\operatorname{Var}(I_n)},\qquad \sum_{i<n}\phi_i\Delta=-\log P(0,t_n) +\tfrac12\operatorname{Var}(I_n). \]

This supplies an independent numerical construction of the same initial-curve fit used by the tree.

OAS inversion and risk

For a clean or dirty price anchor, the selected callable engine is evaluated inside a bracketed scalar root solve until model dirty value equals quoted dirty value. The converged OAS is then placed on the AD tape for final valuation. Request::Sensitivities contains all discount/forward-curve pillar adjoints and one additional row:

<identifier>.OAS    dPV/dOAS

This is the partial derivative with OAS treated as an independent market factor. Curve and volatility sensitivities hold that spread fixed.

Validation and examples

Tests cover invalid schedules, carry-forward behavior, amortization, clean/dirty spread round trips, AD-versus-central-difference spread risk, extreme OAS, call boundaries, tree refinement, a Hull-White closed-form zero-coupon bond-option benchmark, and agreement between the tree and Fourier engines for fixed and mixed capped/floored coupons.

Runnable examples are:

  • cargo run -p valuation-bonds prices the same mixed-coupon instrument with both engines.
  • cargo run --manifest-path examples/scripting/Cargo.toml --bin stepped-coupon-bond compares the native tree with an equivalent scripted deterministic payoff.
  • bindings/python/examples/valuation/bonds/stepped_coupon_bond.ipynb constructs and prices the product from Python with typed terms and auxiliary model settings.
  • bindings/python/examples/scripting/scripted_cashflows.ipynb explains the scripted underlying and its deterministic native comparison.

Calibrated Hull–White parameters retain the entire piecewise sigma schedule. Transition variance integration splits at every sigma boundary, using the first sigma from time zero and extrapolating the last beyond its final pillar. Calibration uses the curve’s reference date and day counter while preserving the configured step density and maximum steps. Callable valuation currently requires settlement to equal the context evaluation date.

Volatility sensitivities chain the calibration Jacobian through sampled surface or cube quotes to their original risk pillars. That schedule assembles independent constant-sigma fits, not a joint repricing calibration. The diagonal forward-difference Jacobian includes volatility inputs only: sigma’s response to curve quotes and changes in calibration strikes is not included. Cube calibration must select at most one underlying tenor per expiry and stay within the source domain.

Loans, prepayment and credit

A loan promises interest and principal payments according to a contractual repayment schedule. Its value also depends on expected prepayment and default. QuantSupport keeps the contractual schedule fixed while the pricer projects these expected receipts under the chosen market assumptions.

Loan<T> stores the contract as a Leg<T> of coupons, redemptions and the initial advance. Coupon notionals and principal redemptions are contractual f64 amounts. Supply a leg directly or use MakeLoan<T> to generate it from repayment and schedule terms.

The builder defaults to monthly payments, French amortization, Actual/365 accrual, a null calendar, unadjusted payments, zero fixing lag, no prepayment or credit, and 40% recovery. Set the calendar, fixing lag, and payment convention to the contract’s actual terms.

Coupons use the same positional list as SteppedCouponBond: Rust accepts with_coupons(Vec<CashflowType<T>>) and Python accepts the corresponding FixedRateCoupon, FloatingRateCoupon, and OptionEmbeddedCoupon objects in coupons=[...]. The final supplied coupon repeats through maturity. For monthly payments, [qs.FixedRateCoupon(rate=qs.InterestRate(rate=0.04))] * 12 followed by [qs.FloatingRateCoupon(index=qs.MarketIndex.EURIBOR12m, spread=0.01)] defines twelve fixed periods followed by floating periods. Lists can also step fixed coupons, spreads or floating indices. Empty lists, non-coupon entries and lists longer than the generated schedule are rejected. OptionEmbeddedCoupon accepts a native PayoffOps expression, such as (2 * qs.PayoffOps.Index + 0.005).max(0.02).min(0.09). The expression evaluates the observed index; the coupon’s separate spread is added afterward. Serialization preserves the complete expression tree.

Contractual principal

The builder computes principal before considering any prepayment function. For initial principal \(B\) and annual amortization rate \(r\), let \(\tau_k\) be the contractual day-count fraction and \(B_f\) the final balloon. With \(B_{k-1}\) the opening scheduled balance,

\[ i_k=r\tau_k,\qquad D_k=\prod_{j=1}^{k}(1+i_j)^{-1},\qquad A=\frac{B-B_fD_n}{\sum_{k=1}^{n}D_k}, \] \[ I_k=B_{k-1}i_k,\qquad R_k=A-I_k. \]

Here \(A\) is the constant scheduled instalment, \(I_k\) interest and \(R_k\) scheduled principal.

These annuity factors use contractual interest accrual; they are independent of market discount curves. Unequal intervals and final stubs use their own fractions. If every contractual fraction is exactly \(1/12\), a 100,000 principal, 6% coupon and 360 payments produce an instalment of 599.550525. This numerical example assumes equal accrual fractions. Actual schedule fractions depend on the payment dates and day counter. At zero interest and with no balloon, the instalment is principal divided by the number of periods.

Rust makers built with MakeInstrument::build_using obtain the initial all-in rate from the first coupon’s fixing or forecast, including spread and payoff rules. An explicit with_initial_amortization_rate overrides that observation. Direct build() construction requires an explicit initial all-in amortization_rate for floating-first French/balloon loans. Python makers use build_using(context) when that rate is omitted. Excel constructs the loan using its explicit construction context. The resolved rate fixes the principal schedule. Subsequent market moves or reset observations change floating interest without changing contractual coupon notionals or redemptions. Otherwise the first fixed coupon supplies the initial amortization rate for every principal period; later coupon steps do not automatically recast principal. Bullet loans repay principal at maturity.

Accrual day counters belong to coupons. A floating coupon uses its explicit day counter, falling back to its index convention when omitted. Fixed coupons use their InterestRate day counter. The loan uses those same conventions for principal amortization and preserves them when generating each cashflow. MakeLoan::with_day_counter and Python Loan(day_counter=...) have been removed; move that setting into the coupon. With an initial amortization rate matching the first all-in floating fixing, French payments are level through the first reset, including months with different accrual lengths. Later resets can change payments because contractual principal remains fixed.

The following Rust fragment builds a floating mortgage; its market and trade are supplied by the surrounding application.

use quantsupport::prelude::*;
use quantsupport::time::calendars::target::TARGET;

let loan = MakeLoan::<DualFwd>::default()
    .with_identifier("EUR-MORTGAGE".into())
    .with_start_date(Date::new(2026, 2, 24))
    .with_maturity_date(Date::new(2056, 2, 24))
    .with_notional(250_000.0)
    .with_currency(Currency::EUR)
    .with_coupons(vec![CashflowType::floating(MarketIndex::EURIBOR12m, 0.01, None, None)])
    .with_initial_amortization_rate(0.05)
    .with_payment_frequency(Frequency::Monthly)
    .with_calendar(Calendar::TARGET(TARGET::new()))
    .with_fixing_lag_business_days(2)
    .build()?;

let contractual_cashflows = loan.contractual_leg().cashflows();
let mut pricer = LoanPricer::new();
pricer.set_discount_policy(Box::new(
    FixedIncomeDiscountPolicy::new(false)
        .with_risk_free_index(Currency::EUR, MarketIndex::ESTR),
));

Coupon observation requests

Payment frequency, reset frequency, and index tenor are independent. RateIndexDetails::frequency() defines the index observation frequency, separately from its quote compounding frequency. The builder reads this metadata rather than matching particular index names. EURIBOR12m, for example, supplies annual resets and a 12-month observation tenor for monthly-payment loans. Explicit builder frequencies and tenors override the metadata. Daily indices default to point observations at the loan’s payment boundaries; specify the loan’s actual reset conventions explicitly when these differ.

Each floating coupon carries a ForwardRateRequest for its applicable reset. Monthly coupons can share the same annual request. The request contains the fixing date and full index observation start/end dates. This separates the rate observation from the coupon’s interest accrual interval. Coupon changes occur at payment accrual boundaries. Each contiguous run of a floating index starts a new reset grid; changing only its spread or bounds preserves the existing grid.

The pricer resolves required historical resets from exact stored fixings. Today’s fixing is used when present and forecast otherwise. Future observations use the index curve and its quote convention. It installs fixings on a cloned leg, leaving the instrument unchanged.

Prepayment functions

A Loan<T> carries a PrepaymentModel<T> function independently of its contractual cashflows. PrepaymentModelConfiguration::Cpr, CprCurve, and Refinancing implement that function for any supported scalar. Custom models passed to MakeLoan<T>::with_custom_prepayment_model use the same T as the coupon templates. For a MakeLoan<DualFwd> already containing the contract terms, a custom function can be attached as in this Rust fragment:

let loan = builder.with_custom_prepayment_model(
    |state: &PrepaymentModelState<DualFwd>| -> Result<DualFwd> {
        // Illustrative annual CPR converted to this interval's probability.
        Ok(DualFwd::scalar(1.0 - 0.88_f64.powf(state.interval_year_fraction())))
    },
).build()?;

Changing a loan’s scalar type requires an explicit target-scalar prepayment model. Use loan.into_scalar::<DualFwd>(Arc::new(model)), and the same method on LoanTrade<T> to preserve its date and side. For portable configurations, pass the same configuration again. For custom models, supply an equivalent implementation for the target scalar; converting an arbitrary model automatically could discard its AD dependencies. Implicit From/Into conversions between loan scalar types are therefore unavailable.

The pricer evaluates the function after scheduled principal. A regular monthly interval converts annual CPR to 1 - (1 - CPR)^(1/12); partial or irregular intervals use actual days divided by 365. Prepayment is a principal receipt. The projected remaining share of the contractual balance reduces future expected interest and scheduled principal proportionally. No contractual leg is modified. For seasoned loans the projection starts from contractual outstanding principal at the evaluation date; prior actual prepayments are not inferred.

Refinancing-incentive models receive an explicitly configured index benchmark. Its observation tenor comes from the index’s metadata. This benchmark excludes replacement-loan lending spreads, fees, and transaction costs. The state exposes it as refinancing_benchmark_rate(), alongside contractual_coupon_rate() and outstanding_principal(). Models return an interval probability through conditional_prepayment_probability(). Native prepayment functions can retain AD dependencies on market rates without changing the cashflows’ f64 notionals.

Valuation and cashflows

LoanPricer implements the standard request handlers. Request::Cashflows uses HandleCashflows over projected legs containing existing cashflow variants: leg 0 contains scheduled coupons/redemptions, leg 1 prepayment redemptions, and leg 2 recovery amounts. The table reports expected signed receipts strictly after the evaluation date. Their discounted sum reproduces Value. The initial advance remains part of the contractual leg but is excluded from receipt PV.

Bounded floating templates retain OptionEmbeddedCoupon. Their floor/cap payoff is applied to the resolved index observation with the loan’s accrual basis. This is a deterministic cashflow projection; it does not add option time value.

With a borrower credit_index, receipts are weighted by survival conditional on the evaluation date. Default between payments pays recovery on opening principal at the period-end payment date. This is a discrete recovery timing approximation. Credit and prepayment affect pricing state, not the contract.

import quantsupport as qs

loan = qs.Loan(
    identifier="EUR-MORTGAGE", start_date=qs.Date(year=2026, month=2, day=24),
    maturity_date=qs.Date(year=2056, month=2, day=24), notional=250_000.0,
    coupons=[qs.FloatingRateCoupon(index=qs.MarketIndex.EURIBOR12m, spread=0.01)],
    currency=qs.Currency.EUR,
    repayment=qs.LoanRepaymentStructure.french(), amortization_rate=0.05,
    payment_frequency=qs.Frequency.Monthly,
    calendar=qs.Calendar.TARGET, fixing_lag=2,
    prepayment=qs.PrepaymentModelConfiguration.constant_annual_cpr(cpr=0.12),
)
trade = qs.Trade(instrument=loan, trade_date=reference_date, side=qs.Side.LongReceive)
discount_policy = qs.FixedIncomeDiscountPolicy(discount_curve_by_currency={qs.Currency.EUR: qs.MarketIndex.ESTR})
with qs.PricingContext(quotes=quotes, curves=curves, fixings=fixings, csa_policy=csa_policy,
                      fixed_income_policy=discount_policy) as context:
    result = context.evaluate(trade=trade, requests=[qs.Request.Value, qs.Request.Cashflows, qs.Request.Sensitivities])
    value = result.price
    expected_receipts = result.cashflows
    risk = result.sensitivities

The Python constructor delegates to the Rust builder. to_dict and from_dict serialize construction parameters; use Python’s json module for files. Typed definitions and their plain serialized dictionaries are both accepted. loan.coupon_fixing_requests() lists unique coupon observations for loading fixings; valuation belongs to the pricer.

context.evaluate(qs.Trade(loan, reference_date), requests) dispatches directly to the core Rust LoanPricer. Use PricerConfiguration.loan(...) with Python arguments and a typed MarketIndex to select fair-rate and refinancing settings:

context.pricer_configurations = [qs.PricerConfiguration.loan(
    refinancing_benchmark_index=qs.MarketIndex.EURIBOR12m,
)]

Omitting this configuration uses the native defaults: no refinancing benchmark and preserved amortization during fair-rate trials. The context’s fixed_income_policy selects discounting. Loans carry their payment currency, while the policy selects the discount curve. The same contract can therefore be valued against ESTR, EURIBOR, or a funding curve by changing the policy. Native Rust valuation requires a configured policy. In Python, the default context policy maps its base currency to its base curve when no explicit fixed-income policy is supplied. The same native settings apply to individual loan trades, portfolio members and scenarios. No Python pricer registration or trade subclass is needed.

Sensitivities are derivatives with respect to labelled market pillars. Multiply a rate-quote derivative by 0.0001 for the first-order PV change from a one-basis-point increase. Native refinancing and survival weights participate in AD; contractual principal schedules stay fixed during market sensitivity calculations.

Fair rate and schedule choice

FairRate bootstraps one fixed coupon or floating spread per contiguous contractual segment at or before origination. A change in fixed rate, spread, coupon type, or floating index starts a segment; index fixing resets do not. First shorten the loan to the first segment’s end and repay all remaining capital there. Solve that segment’s rate so receipt PV equals the original advance PV, lock the result, extend through the next segment, and repeat. Every solved prefix therefore lends at par with its outstanding principal repaid at that prefix’s end. Bounded coupons remain unsupported.

Request::FairRate(index) in Rust and qs.Request.FairRate(index) in Python select one zero-based segment and return its scalar in fair_rate. Only the earlier segments needed for that target are solved. Use separate evaluations for several targets; invalid indices are errors. There is no second fair-rate attribute or implicit array. In Excel, QS.REQUEST.FAIR.RATE(index) supplies the request to QS.EVALUATE, then QS.RESULT.FAIR.RATE reads its scalar.

with context:
    second_rate = context.evaluate(
        trade=trade, requests=[qs.Request.FairRate(1)]
    ).fair_rate

The pricer’s rebuild_amortization_for_fair_rate parameter controls how solver trials treat principal. The default false preserves the already created amortization schedule. With true, each trial regenerates the builder’s French, bullet, or balloon schedule using the first trial coupon’s all-in rate. Once the first segment is solved, its rate stays fixed during later trials. A custom leg needs explicit repayment metadata to use this mode. Neither mode changes the original instrument.

original_configurations = context.pricer_configurations
try:
    for rebuild in [False, True]:
        context.pricer_configurations = [qs.PricerConfiguration.loan(
            rebuild_amortization_for_fair_rate=rebuild,
        )]
        with context:
            fair_rate = context.evaluate(trade, [qs.Request.FairRate(0)]).fair_rate
            print(rebuild, fair_rate)
finally:
    context.pricer_configurations = original_configurations

In Rust, use LoanPricer::new().with_fair_rate_amortization_rebuild(true) for rebuilding. The solver searches within [-5%, 50%], reducing the upper bound when a trial rate would produce invalid amortization. It reports unbracketed solutions as errors. To reprice a preserving-mode solution through the builder, retain the original initial amortization rate while changing the coupon/spread. To reprice a rebuilding-mode solution, use the candidate all-in rate for amortization.

Runnable examples remain in their separate product directories:

  • Rust: cargo run -p valuation-loans.
  • Python: python bindings/python/examples/run_notebooks.py valuation/loans.

Both use the illustrative EUR and borrower-credit inputs in examples/valuation/loans/data/loan_*.json. The mortgage notebook develops amortization, annual EURIBOR resets, composite cost-of-funds discounting, market scenarios, prepayment, credit, and quote sensitivities over thirty years. It reuses one context by assigning context.curves and context.fixed_income_policy. Temporary benchmark and funding-spread shocks use with context, scenario: with qs.Scenario(shocks=[qs.Shock(target=..., value=..., shock_type=...)]). Pricing constructs requested market objects in an independent view. The base context retains its inputs and curves, including when the scope raises an exception.

In Rust, Scenario::new(vec![Shock::new(target, value, ScenarioType::Absolute)]) creates the same combination. scenario.apply(&context)? returns a borrowed MarketDataProvider accepted by pricers and Evaluator; the context stays unchanged. Funding-spread sensitivities use the original zero-rate quote identifiers ZeroRate_EUR_FUNDINGSPREAD_<tenor>. The notebook isolates those derivatives by funding tenor, reports EUR changes for +1 bp, and compares the parallel funding exposure with fully repriced funding stress.

Interest-rate options

An interest-rate option gives its holder a payment or exercise right tied to future rates. Caps and floors protect individual coupon periods, while a swaption gives the right to enter a swap. Their valuation uses the underlying schedules and curves together with an option-pricing model.

QuantSupport supports optionlet valuation from Black or Normal volatility markets and Gaussian short-rate models for caplets, caps and swaptions. The choice determines the required volatility inputs and the interpretation of model sensitivities.

Optionlets and Black/Normal markets

A caplet fixes at \(T\), accrues over \([T,S]\), and pays \(N\tau(L-K)^+\). A floorlet replaces the payoff by \(N\tau(K-L)^+\). MakeCapFloor builds a strip of these periods. Under Black-76,

\[ C=N\tau P_d(0,S) \left[F\Phi(d_1)-K\Phi(d_2)\right],\qquad d_1=\frac{\log(F/K)+\tfrac12\sigma^2T}{\sigma\sqrt T}, \quad d_2=d_1-\sigma\sqrt T. \]

Projection determines \(F\), the policy-selected curve determines \(P_d\), and the index surface supplies volatility at the fixing/strike coordinates. Strike can be absolute, ATM or relative to the forward. The Black and Hull–White caplet/cap pricers interpret Relative(p) as \(F(1+p)\), unlike the additive \(F+p\) returned by the generic Strike::resolve helper. An absolute strike avoids this consumer-specific ambiguity. VolatilityType::Normal selects Bachelier rather than forcing a lognormal formula on absolute-rate volatility:

\[ C=N\tau P_d(0,S) \left[(F-K)\Phi(d)+\sigma_N\sqrt T\,\phi(d)\right], \quad d=\frac{F-K}{\sigma_N\sqrt T}. \]

A cap/floor sums the corresponding optionlets. Market sensitivities can therefore reach projection and discount quotes and the neighboring surface pillars. The units of Black vega and Normal vega are not interchangeable.

Gaussian bond-option representation

A one-curve caplet can be expressed as a put on the accrual-end bond:

\[ C=N(1+\tau K)\, \operatorname{PutBond}\left(T,S,\frac{1}{1+\tau K}\right). \]

Hull–White supplies an affine bond price and its option volatility. For constant sigma and nonzero mean reversion \(\alpha\),

\[ B(t,S)=\frac{1-e^{-\alpha(S-t)}}{\alpha},\qquad \sigma_P=\sigma B(T,S) \sqrt{\frac{1-e^{-2\alpha T}}{2\alpha}}. \]

The fixed-parameter Hull–White caplet and cap pricers take alpha and sigma directly. Their risk reports treat these supplied parameters as fixed, while configured calibration is responsible for connecting model parameters to market volatility quotes.

Swaptions and Jamshidian decomposition

MakeSwaption creates a EuropeanSwaption with distinct expiry and underlying swap maturity, payer/receiver direction, strike, rate convention and schedule. A payer has the right to pay fixed. The specialized closed-form pricer uses Jamshidian’s bond-option decomposition.

At expiry \(T_0\), choose the critical rate \(r^\ast\) satisfying

\[ \sum_i c_iP(T_0,T_i\mid r^\ast)=1, \qquad X_i=P(T_0,T_i\mid r^\ast). \]

Here \(c_i\) includes the fixed coupons and final principal in the decomposition. A payer becomes a weighted sum of bond puts with strikes \(X_i\); a receiver uses calls. The converged critical-rate dependency is reconnected by implicit differentiation rather than differentiation of branch choices in the scalar root solver.

ClosedFormHullWhiteSwaptionPricer::from_configuration accepts a typed EuropeanSwaption/HullWhiteClosedForm assignment and a Hull–White model for the selected index. Fixed parameters give a constant sigma. A calibrated source selects caplet-surface or swaption-cube contracts and resolves a piecewise sigma schedule. A custom provider must supply source quote details as well as constructed volatility objects and curves.

The schedule assembles independent constant-sigma fits; it is not a joint piecewise-volatility repricing calibration. Its diagonal forward-difference Jacobian includes sigma’s response to volatility inputs, but not its response to curve quotes or moving calibration strikes. Direct curve risk through valuation still exists. Cube calibration permits at most one underlying tenor per expiry and must remain in-domain. Sensitivity reports therefore include partial calibration risk with these inputs held fixed.

The model chapter distinguishes these closed forms from actual numerical path generation and its zero-parameter limits.

Equity and FX options

Valuing a European equity or currency option requires spot, carrying costs, a volatility market and a discount curve. These inputs determine the forward, the distribution of the payoff and its present value. QuantSupport provides closed-form pricers for both asset classes and a separate Monte Carlo route for equity options.

FX options

For an option on base currency priced in quote currency, let spot \(S\) mean quote units per base unit. The Garman–Kohlhagen representation used by FxEuropeanOptionPricer is

\[ F=S\frac{P_{\rm base}(0,T)}{P_{\rm quote}(0,T)},\qquad C=P_{\rm quote}(0,T) \left[F\Phi(d_1)-K\Phi(d_2)\right]. \]

The trade contributes notional and side. The call value is expressed in the quote currency. Both policy-resolved discount curves, pair spot and a compatible FX surface are required; an explicit policy is mandatory.

For USD/CLP under USD collateral this can mean SOFR for USD and Collateral(CLP, USD) for CLP. A reciprocal surface is oriented through inverse strike mapping. Curves, active volatility pillars and recorded spot inputs participate in risk. The standalone FX-forward implementation uses a different discount ratio; the book does not assume parity between those implementations.

Equity carry and European valuation

For an equity with spot \(S\), a dividend discount curve \(P_q\) and rate curve \(P_r\), the standard forward is \(F=SP_q/P_r\). A zero-dividend assumption uses \(P_q=1\). The Black expression is

\[ C=P_r(0,T)\left[F\Phi(d_1)-K\Phi(d_2)\right],\qquad d_1=\frac{\log(F/K)+\tfrac12\sigma^2T}{\sigma\sqrt T}, \quad d_2=d_1-\sigma\sqrt T. \]

The instrument supplies expiry, strike, index, currency and call/put type. The closed-form pricer reads the associated surface; its current query uses the resolved strike directly, so arbitrary smile-type metadata does not provide an automatic delta-to-strike conversion.

EquityForwardPricer separately distinguishes forwarding and policy-selected payment discounting. Its spot is the equity fixing on the context evaluation date, not necessarily the trade date. An absent dividend curve means zero dividends; a missing required spot is an error. For the implemented forward,

\[ F=S\frac{P_{\rm dividend}(0,T)}{P_{\rm forward}(0,T)},\qquad V=N\,s\,(F-K)P_{\rm payment}(0,T). \]

This distinguishes carrying costs from the curve used to discount the payment.

Materialized Monte Carlo valuation

BlackMCEuropeanOptionPricer reads a configured path set from the market, selects the stored date closest to expiry and averages intrinsic payoffs:

\[ \widehat V=N P_d(0,T)\frac1n\sum_p \max(S^p_{\text{nearest grid date}}-K,0) \]

for the call form. Relative/ATM strike resolution uses the sample terminal mean as a forward proxy. Grid choice therefore matters independently of sampling error.

The stored paths are generated numerically in f64, with their generation inputs held fixed during pricing. The current sensitivity handler therefore reports discount-curve pillars and omits path-generation delta and vega. Model-backed scripting and exposure use a separate workflow that retains differentiated path construction for supported factors.

Differentiation and market sensitivities

Automatic differentiation computes a valuation’s local response to its inputs. QuantSupport registers those inputs under market pillar labels, so a risk report can identify the deposit, swap, volatility quote or other quantity whose change affects the trade.

For a calibrated curve, the derivative includes the connection between market quotes and solved curve nodes. The label and units of each input determine how to translate the derivative into a monetary change.

For a decimal rate quote \(q\), the reported row is \(\partial V/\partial q\). Its first-order +1 bp change is \(10^{-4}\partial V/\partial q\). A volatility quote, FX spot or spread anchor has different units. Apply the scaling appropriate to each input when reporting DV01, vega or other market-move estimates.

Forward and reverse representations

Fwd<T> stores a value and a first-order seed of the inner scalar type. Nesting supplies higher orders: Fwd1 = Fwd<f64> and Fwd2 = Fwd<Fwd<f64>>. Dual<T> records reverse operations; DualFwd = Dual<Fwd2> is the standard pricing scalar.

#![allow(unused)]
fn main() {
extern crate quantsupport;
use quantsupport::prelude::*;

let x = Fwd2::var(1.5);
let y = x * x;
assert!((y.value() - 2.25).abs() < 1e-12);
assert!((y.first_derivative() - 3.0).abs() < 1e-12);
assert!((y.second_derivative() - 2.0).abs() < 1e-12);
}

Reverse mode starts from a selected output and accumulates adjoints toward registered independent inputs. In the example below, x is a tracked variable and the exponential’s derivative is propagated back to it.

#![allow(unused)]
fn main() {
extern crate quantsupport;
use quantsupport::ad::expr::FloatExt;
use quantsupport::prelude::*;

Tape::start_recording_fwd();
let x = DualFwd::new(0.04);
let y: DualFwd = (x * DualFwd::scalar(2.0)).exp().into();
y.backward()?;
assert!((x.adjoint()?.value() - 2.0 * 0.08_f64.exp()).abs() < 1e-12);
Tape::stop_recording_fwd();
Ok::<(), QSError>(())
}

The tape is thread-local. Marks separate persistent market dependencies from temporary trade/path nodes. Pricers register required pillars, calculate values and propagate adjoints before reporting labelled derivatives. Script and XVA services rewind path-local nodes and use worker-local markets. These valuation services manage recording separately from context initialization.

Calibration dependencies

For calibrated rate curves, the nonlinear solve is numerical f64 work. Finite-difference residual Jacobians and the implicit-function theorem reconnect the converged discount factors to market quotes. The resulting connection approximates the local first-order calibration response; higher-order calibration effects require separate validation.

The resulting ladder can include parent curves. A Term SOFR swap discounted on SOFR may expose basis quotes and OIS quotes because both define the market used by valuation. Model calibration has its own omissions: current Gaussian sigma Jacobians estimate independent-fit volatility response by diagonal forward differences, not joint schedule-calibration risk or curve/strike-induced recalibration. Materialized simulation paths omit generation dependencies entirely.

SensitivityMap stores parallel labels and values. Aggregation sums repeated labels reached through several paths. Reporting is pricer-specific: do not assume every implementation removes zero rows or includes every conceivable model parameter. Bond Z-spread/OAS anchors, for example, appear as explicitly independent spread factors rather than being re-solved under every curve move.

Changing the curve risk basis

PricingContext provides two transformations that return a RiskContext implementing MarketDataProvider. Pass the resulting view to the same pricer and request Request::Sensitivities as usual. Both transformations preserve the original curve values and interpolation, including prices between nodes.

The complete runnable example is risk-reprojection.rs. Run cargo run -p sensitivity --bin risk-reprojection from the repository root to compare deposit, OIS and post-bootstrap zero-rate sensitivities for the same swap. The example verifies that all three NPVs are identical.

This fragment reports sensitivities to zero rates implied from bootstrapped discount factors:

let convention = RateDefinition::new(
    DayCounter::Actual365,
    Compounding::Continuous,
    Frequency::Annual,
);
let zero_context = context.with_zero_rate_risk(&[
    ZeroRateRisk::new(MarketIndex::SOFR, convention),
    ZeroRateRisk::new(MarketIndex::TermSOFR3m, convention),
])?;
let result = pricer.evaluate(
    &trade,
    &[Request::Value, Request::Sensitivities],
    &zero_context,
)?;

Each grid defaults to that curve’s future nodes. Add .with_dates(vec![date_1, date_2, ...]) to select another grid; curves without nodes, such as composites, require explicit dates. Labels include the curve, date and rate convention. Exposures are derivatives per unit of annual decimal zero rate; multiply by 1e-4 for a first-order +1 bp change. Selected curves’ parent quote dependencies enter the transformation jointly. Other curve quotes retain their original coordinates.

This fragment reports curve risk against another set of calibration instruments:

let instruments: Vec<QuoteDetails> = [
    "OIS_USD_SOFR_1Y",
    "OIS_USD_SOFR_2Y",
    "Swap_USD_TermSOFR3m_1Y",
    "Swap_USD_TermSOFR3m_2Y",
].iter().map(|label| label.parse()).collect::<Result<_>>()?;
let projected_context = context.reproject_risk(&instruments)?;
let result = pricer.evaluate(&trade, &[Request::Sensitivities], &projected_context)?;

The target contracts need not have observations in the quote store. Their implied quotes are solved against the original curves. The view exposes target_labels() and target_values() for inspection. The target basket must span all native discount/forward curve pillars in the context; the example assumes two pillars on each of its two curves. Instrument reprojection requires configuration-driven curves. Supplied runtime curves support zero-rate risk.

For target quotes x and original curve quotes q, reprojection computes J = dx/dq and reports dV/dx = dV/dq * J⁺. Zero-rate Jacobians use AD; instrument Jacobians use central differences of reconstructed source curves. Insufficient or singular bases return an error. With more target factors than source factors, the pseudoinverse chooses minimum Euclidean-norm exposures in the supplied quote units. These transformations are local and first order; construct a fresh view when market inputs change. FX spots, volatility and other independent factors are held fixed in the curve basis calculation and keep their native reported risk. This does not add model recalibration or simulation-path dependencies omitted by the original pricer.

Validation and numerical accuracy

AD differentiates the active implemented program. Branches use their selected local behavior; hard digitals have zero pathwise derivative almost everywhere. Scripting can smooth comparisons, introducing a deliberate approximation. Numerical integration, finite-difference calibration and time/state grids retain their errors.

Reverse work often avoids a separate pricing run for every market input, but there is no universal fixed runtime multiplier. Memory, calibration graph, trade count and path count matter. Validation should compare selected labeled derivatives against same-input, same-seed full revaluation, with a bump size small enough to test local behavior and large enough to avoid numerical cancellation.

Scenarios and full revaluation

Scenarios measure the effect of a specified market move by changing inputs, rebuilding the affected market objects and repricing the trade. Comparing the revalued change with the sensitivity estimate shows how well a local linear approximation describes that move:

\[ \Delta V=V(q+\Delta q)-V(q),\qquad \Delta V_{\rm linear}=\sum_j \frac{\partial V}{\partial q_j}\Delta q_j. \]

The difference can reflect curvature, a change in exercise or payoff behavior, and recalibration effects omitted from partial model sensitivities.

Ordered input transformations

A Scenario contains ordered Shock definitions. An absolute shock adds its value; a relative shock multiplies by one plus its value. The target matches an exact identifier or requires every underscore-separated target segment to occur in the input identifier. Segment order and multiplicity are ignored. The exact-string branch does not prevent that same target from matching other identifiers through the segment rule.

A fully qualified identifier is therefore not a universal exact-only selector. Repeated tenor/expiry tokens can make targets overlap, so constructing one shock per full quote identifier can move some quotes more than once. For a parallel family stress, prefer one family selector and verify the transformed values of every affected quote. An isolated finite-difference bump requires checking the actual matched set rather than assuming the target is unique.

This fragment assumes an existing context, trade and compatible pricer:

let scenario = Scenario::new(vec![
    Shock::new("OIS_USD_SOFR", 0.0001, ScenarioType::Absolute),
    Shock::new("CapletFloorlet_USD_SOFR", 0.10, ScenarioType::Relative),
]);
let stressed_market = scenario.apply(&context)?;
let stressed = pricer.evaluate(&trade, &[Request::Value], &stressed_market)?;

The first change adds one basis point to matching OIS rate quotes; the second scales matching optionlet volatilities by 1.10. Overlapping transformations compose in supplied order. All available quote levels are transformed, then the context’s selected bid/mid/ask level is consumed.

Direct zero-rate quotes can also be targeted. A funding spread curve may expose ZeroRate_EUR_FUNDINGSPREAD_<tenor> identifiers; targeting FUNDINGSPREAD changes all matching spread quotes. Historical fixings and spot FX are not implied targets of this quote mechanism. Empty targets, nonfinite shocks and unmatched selectors are errors.

Independent views and construction

scenario.apply(&context) returns a borrowed MarketDataProvider with an independent cache. The base observations and constructed objects remain unchanged. Requests rebuild affected dependencies; unrelated simulations are not generated. Unchanged calibrated data can be reused with detached AD wrappers. An affected simulation uses its configured seed when rebuilt.

In Python, the corresponding scope is with context, scenario:. The innermost active context supplies the base market, nested scenarios compose in entry order, and scope exit releases temporary state even on exceptions. Define scenarios separately from the context constructor.

Local validation and stress interpretation

For one input, a central comparison uses

\[ \frac{V(q+h)-V(q-h)}{2h}. \]

Use identical seeds, grids, policies, payment selection and quote levels. Check that only the intended target moved. Vary \(h\) to distinguish curvature from solver/sampling noise; one finite difference is not a universal truth about a derivative.

For a broad stress, report the full revalued change and the assumptions it reconstructs. A large parallel funding shock can change the whole composite discount curve, whereas a derivative row is local to one rate pillar. Credit, prepayment and smoothed script behavior can also make stress results depart substantially from a linear ladder.

Stochastic models and simulation

Simulation provides the future rates, spot prices and discount factors needed for stochastic payoffs and exposure calculations. A model specifies their dynamics, while the date grid, path count and random seed control the numerical experiment.

QuantSupport supports stored path sets generated from a SimulationConfiguration and model-backed observations supplied by a MarketModel to scripting and exposure services. Stored paths are useful for inspection and numerical payoff valuation. The model-backed services also retain the path-construction dependencies used for automatic differentiation.

Materialized numerical paths

SimulationBuilder constructs a date grid strictly after the market reference date through the configured horizon. Model inputs include index, path count, seed, frequency and day counter. It converts required curves to f64 and generates Hull–White, LGM or lognormal Brownian paths using standard-normal draws from seeded StdRng. This route does not inherit the Sobol/antithetic construction of LgmMarketModel.

GeneratedMonteCarloSimulation stores the resulting values in DualFwd containers with generation inputs held fixed. It exposes no simulation pillars, so subsequent differentiation covers only the operations performed on the stored values. The MC equity pricer can differentiate its subsequent discounting without providing path-generation delta or vega.

For lognormal Brownian motion, the interval step has the exponential form

\[ S_{t+\Delta}=S_t\exp\left[ (r-q-\tfrac12\sigma^2)\Delta+\sigma\sqrt{\Delta}Z\right]. \]

Here \(\sigma\) is sampled at the interval endpoint. The step does not integrate time-dependent variance across sigma boundaries; aligning or refining the grid matters when the volatility schedule changes. Calibrated term-volatility stripping matches accumulated variance at its selected expiries, but does not change that path-stepping rule. Spot comes from the reference-date fixing. The builder’s rate is obtained from the terminal discount factor; a general term structure does not thereby become a fully time-dependent equity carry model.

Hull–White representation

The one-factor Gaussian short-rate model is conventionally written

\[ dr_t=(\theta(t)-a r_t)\,dt+\sigma(t)\,dW_t. \]

Its affine bond representation uses \(P(t,T\mid r)=A(t,T)e^{-B(t,T)r}\), with \(B(t,T)=(1-e^{-a(T-t)})/a\). Model calibration converts caplet or swaption prices into a piecewise-constant sigma schedule and retains instrument-level quality records. Selected-source domain and calibration-Jacobian limitations are described in Volatility markets.

The low-level materialized Hull–White path generator is Euler-discretized:

\[ x_{i+1}=x_i-a x_i\Delta_i+ \sigma(t_{i+1})\sqrt{\Delta_i}Z_i. \]

The generator updates a variance estimate and adds a curve-derived shift to the state. Its Euler discretization differs from the exact OU transitions used in callable-bond valuation. Refine the grid to assess discretization bias and increase path count to assess sampling precision.

Several low-level Hull–White formulas divide by mean reversion and do not implement a zero-alpha limit. Do not infer zero-alpha support from the formal continuous mathematical limit. LGM, described below, explicitly handles zero lambda.

LGM rates and numeraire

LgmRateModel expresses a rate factor through loading \(H\), volatility loading \(\alpha\), and accumulated variance \(\zeta\):

\[ H(t)=\frac{1-e^{-\lambda t}}{\lambda},\quad H(t)=t\ \text{when }\lambda=0,\qquad \alpha(t)=\sigma(t)e^{\lambda t},\quad \zeta(t)=\int_0^t\alpha(u)^2\,du. \]

The zero-lambda limit applies to direct LGM dynamics. Its calibrated construction delegates to the low-level Hull–White calibrator, whose zero-alpha singularities remain; zero-lambda calibration is not implied.

For its Gaussian state \(z\), the implemented discount factor and reference numeraire are

\[ P(t,T;z)=\frac{P(0,T)}{P(0,t)} \exp\left[-(H(T)-H(t))z -\tfrac12(H(T)^2-H(t)^2)\zeta(t)\right], \] \[ N(t,z)=\frac{\exp(H(t)z+\tfrac12H(t)^2\zeta(t))}{P(0,t)}. \]

The reference numeraire defines the measure under which the domestic factor is driftless. Foreign rate factors receive measure-change drift terms involving domestic-rate and FX correlations.

LgmMarketModel combines rate factors with supported FX/equity components. FX component spot is domestic currency per foreign unit. Curve-driver mappings let another curve reuse a factor without claiming that the two initial curves are identical. The state vector orders rates, FX log spots and equity log spots; correlation input must match that ordering.

Path construction uses Owen-scrambled Sobol draws, antithetic pairing and Cholesky correlation. Consecutive paths share opposite-sign normal draws. Even counts preserve complete pairs; odd counts, including one path, are also supported. Reproducing a run requires matching the seed, grid, model and indices. Path count controls sampling, while volatility and model inputs determine whether the dynamics are stochastic.

Model-backed observations

MarketModel<T> configures evaluation dates and typed simulation requests, then returns fallible path scenarios. Responses contain requested discounts, forwards, FX, spots, path-dependent observations and numeraires. Its generate_path and resolve_request return Result, so failures retain their market/model categories instead of becoming silently absent paths.

Scripting and XVA operate on these model responses in DualFwd. They can propagate through registered model/curve inputs while rewinding temporary path nodes. The resulting sensitivities remain subject to fixed correlations, partial calibration Jacobians and payoff smoothing. Stored numerical simulations retain the more limited risk dependencies described above.

Dated payoff programs

Payoff scripts describe observations, state changes and payments in a dated sequence of events. They are useful for contracts whose coupon or redemption rules can be expressed directly as a small program. The native engine parses the events, determines their market-data requirements and evaluates them against simulated market states.

Variables persist along each path, allowing a later payment to depend on earlier observations. Payment statements apply discounting and currency conversion through the configured market model.

CodedEvent pairs a date with source text. Conversion into an EventStream parses each program into an expression tree. Events must be nonempty, ordered and not precede the reference date; equal-date events execute in vector order. Every variable read must follow an assignment in the current or an earlier event.

State, observations and payments

The following event describes one receive-fixed/pay-floating coupon:

swap = 0;
fixed_rate = 0.035;
accrual = cvg("2027-03-09", "2027-06-09", "Actual360");
floating = RateIndex("SOFR", "2027-03-09", "2027-06-09");
swap pays 10000000 * (fixed_rate - floating) * accrual on "2027-06-09";

Attach this event to the fixing/start date, 9 March. The event observes the rate at that date and declares the payment due on 9 June.

Statements terminate with semicolons; blocks use braces. Assignment, compound assignment, arithmetic, power, comparisons, boolean logic, arrays and bounded for loops provide ordinary state manipulation. Identifiers are case-sensitive. Script strings follow the native payoff-language grammar, including its supported comment and expression syntax.

RateIndex requests a forward observation over the specified interval. Spot("AAPL") requests an equity spot; a currency pair such as Spot("USD","CLP") requests CLP per USD. Df requests a discount factor, and cvg delegates year-fraction measurement to a named day counter. The model must support the requested observations; parsing a name does not create a missing factor.

A payment has the form

accumulator pays amount on "2027-06-09" in "USD";

Date and currency clauses can be omitted, in which case they use the event date and local currency. The amount is contractual before discounting. Evaluation converts/discounts it and deflates by the appropriate numeraire before adding to the accumulator. Every payment node receives a stable identity for cashflow reporting and claim conversion.

Conditional payoffs and smoothing

A hard digital payoff \(\mathbf1_{\{X>K\}}\) has zero pathwise derivative away from its boundary. Merely recording it on a tape does not produce a useful Monte Carlo digital sensitivity. Scripting deliberately smooths market-dependent conditionals with a finite-width transition.

For a comparison reduced to \(x=\text{lhs}-\text{rhs}\), its truth degree is

\[ d_\varepsilon(x)= \begin{cases} 0,&x<-\varepsilon/2,\ (x+\varepsilon/2)/\varepsilon,&|x|\leq\varepsilon/2,\ 1,&x>\varepsilon/2. \end{cases} \]

The evaluated state blends both branches as \(d_\varepsilon V_{\rm then}+(1-d_\varepsilon)V_{\rm else}\). Equality uses a butterfly profile; boolean operations combine truth degrees through products/complements. Nested conditionals need separate branch-state snapshots.

The engine’s automatic width is

\[ \varepsilon=\max\left( 0.02\max(|\text{lhs}|,|\text{rhs}|),10^{-8}\right). \]

A 4% rate barrier therefore receives roughly 8 bp width, whereas an equity barrier at 150 receives width 3. These are economic approximations as well as numerical choices. Price and derivative belong to the smoothed program, not necessarily the exact discontinuous term sheet.

For an explicitly chosen width, fif(x,a,b,eps) blends between two numerical expressions:

coupon = fif(
    RateIndex("SOFR", "2027-03-09", "2027-06-09") - 0.04,
    0.05, 0.0, 0.001
);

Here the digital-style coupon changes from zero to 5% across a 10 bp rate interval. Piecewise min/max payoffs remain differentiable only along their selected active branches.

Products and validation

ScriptedProduct compiles an event stream and discovers payment nodes, including those in branches and loops. It requires at least one payment and rejects payments before the reference date. The same engine is shared by claims, avoiding repeated parsing for each coupon.

Parsing and evaluation use the common QSError categories: ParsingErr for grammar, InvalidValueErr for structural rules, ValueNotSetErr for uninitialized state, NotFoundErr for unavailable responses/results and EvaluationErr for invalid expression operations. Context preserves the event/path that failed, and root_cause() exposes the underlying category. Validate the event stream before starting a long simulation so structural and grammar errors can be resolved first.

Script valuation and claim integration

ScriptEngine prepares a dated payoff program and evaluates it over market paths. Preparation parses the program once and identifies the observations needed from the model. Each path then supplies those observations, updates script variables and accumulates payments.

For a selected result variable, the engine also propagates derivatives to registered market and model inputs. Temporary differentiation state is released after each path to bound memory use.

Preparing the program

Construction receives an EventStream, reference date, local currency and local discount index. Variable indexing assigns storage slots and payment identities. Market calls become per-event SimulationDataRequest values and flattened model requests with observation dates. Conditional transformation, branch analysis and domain processing prepare the evaluator and remove statically impossible branches.

The program’s horizon includes requested observations and payments, not only source-event dates. This is important for a fixing event that declares a much later settlement. The model must be configured for the complete requested timeline.

The following fragment assumes an existing event stream and differentiable market model:

let engine = ScriptEngine::new(
    events, reference_date, Currency::USD, MarketIndex::SOFR,
)?;
let values = engine.evaluate(&mut model, Some("swap"))?;
let value = values.get("swap");

The selected variable identifies the result whose weighted adjoints are propagated; returned averaged variables are not limited to that one name. Without conditionals the engine uses exact expression evaluation. Conditional programs use the smoothed evaluator described in the language chapter.

Pathwise AD and reporting

Curve pillars and model inputs must be registered on the current tape before evaluation. For each path the engine rewinds to its mark, generates requested responses, evaluates the program and accumulates its contribution. It propagates the selected weighted result back to pre-mark dependencies before discarding path-local nodes. After the path loop, adjoints reach the registered leaves.

This bounds temporary tape memory to one path rather than all paths at once. Parallel evaluation constructs a model and tape per worker through ScriptModelSetup, returning partial values, labeled sensitivities and cashflows. The total path count determines normalization. Reproducible path indices do not promise bitwise identical floating-point reductions across arbitrary thread layouts.

evaluate_with_cashflows additionally collects executed payments. Each ExpectedCashflow distinguishes mean undiscounted amount in payment currency from mean discounted, numeraire-deflated present value in local currency. These are raw sampled payments.

Martingale controls and reconciliation

For a selected result, at least 16 paths and eligible discount/forward requests, the engine estimates martingale control coefficients on a separate pilot sample of up to 64 paths. Known initial-curve expectations anchor discounted bond/forward controls. The main estimator is schematically

\[ \widehat V_{\rm controlled} =\frac1n\sum_p\left[Y_p- \widehat\beta^\mathsf T(C_p-E[C])\right]. \]

The fitted coefficients are held fixed during differentiation. The adjusted estimator reduces variance; assess its remaining error through sampling comparisons.

Only the selected result variable receives this adjustment. Expected cashflow present values remain unadjusted sampled payments. Consequently their sum need not equal the reported controlled result at finite path count. Equality also requires that the selected variable actually represents the total payment accumulator rather than another state quantity. Use the estimator’s sampling error when assessing a stochastic reconciliation; a deterministic comparison has a different tolerance.

From payments to exposure claims

ScriptedProduct::contingent_claims creates one claim per payment identity. Each uses unit notional and LongReceive, because scale and direction already belong to the script expression. Claims share one compiled engine.

At an exposure date, a scripted claim replays the required program against its response block and returns that payment’s value. The live-claim schedule controls which payments remain. Required discount, rate, FX and spot requests must all be supported by the market model; the claim bridge does not create missing dynamics.

Scripted and native claims can share a netting set. The script must specify any exercise rule it uses; this claim bridge does not solve general optimal stopping for scripted callable products.

The Rust examples compare native and scripted swaps under controlled matching setups. Python’s evaluate_script is a narrower averaged-variable service, described in Working in Python; it does not expose every cashflow, path or claim-bridge facility available in Rust.

Claims, exposure and the XVA engine

Exposure measures the value of a trade or portfolio at future dates. To calculate it, QuantSupport converts supported positions into scheduled ContingentClaim payments and simulates the market observations needed to value them. At each exposure date, the remaining payments are valued under the simulated market state.

The resulting path values can be inspected as exposure profiles or combined under netting and collateral agreements to calculate valuation adjustments.

Claim-level valuation

A claim retains payment/fixing/accrual dates, currencies, notional, side and stable trade/leg identifiers. Its strategy describes deterministic amounts, rate coupons, optional rates, spot/path-dependent payoffs, exercise-contingent payments or a scripted payment.

For a native claim, the implemented contribution is

\[ v_{j,p,k} =a_{j,p,k}\,P_{j,p}(t_k,T_j)\, X_{j,p,k}\,s_jN_j. \]

The strategy determines the raw amount \(a\); discount, currency conversion, sign and notional supply the remaining factors. A scripted claim obtains its payment value from the compiled script response block instead. Claim construction must use the strategy’s amount/notional convention consistently to avoid double scaling.

Before simulation, FixingPreprocessor installs realized or partially realized observations. Compression can merge compatible deterministic claims to reduce repeated evaluation. The claim converter checks supported contract features, including overnight observation rules, before these steps.

Cubes and pointwise profiles

NpvCube stores npvs[path][date] for a trade. For values \(V_{p,k}\),

\[ EE_k=\frac1n\sum_pV_{p,k},\qquad EPE_k=\frac1n\sum_p\max(V_{p,k},0),\qquad ENE_k=\frac1n\sum_p\min(V_{p,k},0). \]

ENE is signed and nonpositive. These are pointwise date profiles; the methods do not calculate a time-average EPE or a PFE quantile. XvaAggregator reduces each path into an adjustment contribution, with implementations for CVA, DVA and FVA. Contributions are normalized by the total path count and summed across paths.

Returned cubes and these profiles remain raw, pre-margin values. Adjustment aggregators can consume netted, post-margin exposure instead. A sum of trade-level EPE is generally not the EPE of the netted sum:

\[ \max\left(\sum_jV_j,0\right) \neq\sum_j\max(V_j,0). \]

Calculate netted profiles from the summed path values before taking their positive and negative parts.

High-level orchestration

XvaEngine receives a market provider and XvaEngineConfig. Rate factors identify mean reversion/parameter sources or reuse driver factors; FX configuration adds foreign currencies, volatilities and supported correlations. The configured frequency determines the simulation/exposure grid.

At a high level, run preprocesses claims, determines required curves and factors, builds the date grid, prepares system discount factors and per-netting set factories, then evaluates shared model paths. Foreign currencies and policy-selected discount identities need corresponding supported models. The engine reports an error if a required factor is missing.

The following fragment assumes prepared context, configuration and netting_sets:

let engine = XvaEngine::new(&context, configuration)?;
let result = engine.run(&mut netting_sets)?;
for cube in &result.cubes {
    println!("{}: {:?}", cube.trade_id, cube.epe());
}
for adjustment in result.xva_values.iter().flatten() {
    println!("{} {} {}", adjustment.netting_set, adjustment.measure, adjustment.value);
}

One run returns trade-level cubes, optional adjustments by netting set and optional aggregated labeled sensitivities. High-level construction supplies CVA and FVA factories. DVA exists through lower-level aggregators with explicit own-credit inputs, not an automatic third output of run.

Numerical and modeling boundaries

Shared paths make netting internally coherent, but finite grids and samples remain approximations. LGM path construction, calibration targets and correlations determine the future-state distribution. Optional payoffs may require numerical conditioning or smoothing. Comparisons with native pricers need matching payoff, model and payment-selection assumptions, together with tolerances for numerical and sampling error.

The engine’s system discount sequence is stored numerically and held fixed during AD. Credit/funding factories also introduce particular independent leaves rather than replay every original calibration. The adjustment chapter explains the resulting derivative meaning; collateral explains which exposures those adjustments actually consume.

Netting and collateral

Trades covered by one netting agreement are valued together so that positive and negative amounts can offset. NettingSet stores their claims and the applicable discount policy. Attach CsaTerms to supply the collateral, credit and funding inputs used by the high-level XVA engine.

The agreement’s collateral currency determines discounting. Variation-margin settings additionally determine the simulated collateral balance and residual exposure used in adjustment calculations.

Currency and discounting

The CSA currency/index imply a SingleCurveCSADiscountPolicy. Payments in that currency use the collateral index; foreign payments use Collateral(payment_ccy, collateral_ccy). Each selected identity needs a constructed curve and supported rate model. FX models connect the relevant currencies on paths.

NettingSet::new accepts an explicit policy for lower-level exposure work. with_csa_terms attaches the additional economic inputs used by high-level adjustment factories. Claims from native instruments and scripts can coexist, provided their direction, scale and model requests are consistent.

Variation margin

variation_margin defaults to false. When enabled, the engine maintains one signed collateral balance per netting set and path. Thresholds and minimum transfer amount are finite, nonnegative amounts in collateral currency, independent of the reporting currency.

For raw exposure \(V\), counterparty threshold \(H_c\) and own threshold \(H_o\), target collateral is

\[ C^\ast=\max(V-H_c,0)-\max(-V-H_o,0). \]

The transfer rule replaces the current balance \(C\) when \(|C^\ast-C|\geq\mathrm{MTA}\), including equality. Otherwise the previous balance remains. Between calls, retained collateral accrues using the simulated CSA remuneration-curve interval discount factor; foreign collateral is converted at simulated FX.

Adjustment aggregators receive residual exposure \(V-C\) after each call. Returned NPV cubes and their EPE/ENE/EE methods still show raw pre-margin exposure. That reporting distinction is essential when reconciling profiles to a collateralized adjustment.

Calls occur on every simulation date, including the initial date, and settle immediately. The implementation omits separate call frequency, settlement lag, margin period of risk and initial margin. Refine the simulation grid to assess the effect of the implemented call timing.

Credit and funding inputs

With credit_index, CVA uses a constructed survival curve. Otherwise credit_spread is interpreted as a flat credit spread and converted to hazard using loss given default:

\[ \lambda=\frac{\text{credit_spread}}{1-\text{recovery}}. \]

The field is not itself a hazard-rate input. Recovery also determines the loss multiplier in CVA.

Funding precedence is separate. A configured funding_index derives forward spreads relative to the system curve, with an optional explicit spread overlay. Without that index, funding_spread_curve supplies dated annualized spreads; otherwise the flat funding_spread is used.

Explicit spread curves reuse ZeroRateTermStructure or the supported tenor-based zero representation. They respect their reference date, day counter, interpolation and extrapolation conventions. The native AD representation can retain explicit zero-rate pillar risk, but derived funding-index factory leaves have a different scope, explained next.

Multiple netting sets share the run’s market paths, while each retains its own agreement and adjustment inputs. Netting changes positive and negative parts nonlinearly. FVA’s signed integration means that a smaller positive-exposure profile alone does not establish a smaller funding adjustment.

Credit, funding and XVA risk

Credit and funding adjustments combine future exposure with counterparty, own-credit or funding assumptions. QuantSupport first generates market paths and values the portfolio, then applies a measure-specific aggregator to each path. Netting and variation margin determine the exposure supplied to that aggregator.

The formulas below describe the implemented integration and the inputs held fixed when its sensitivities are calculated.

Let \(U_{p,k}\) be exposure after the netting/collateral rules applied for the adjustment, \(D_k\) the numerical system discount factor, and \(\Delta t_k\) the ACT/365 year fraction between consecutive grid dates. The implemented integration uses right-endpoint exposure.

CVA and DVA

For counterparty survival \(Q_k\) and recovery \(R\),

\[ \mathrm{CVA}=(1-R)\frac1n \sum_p\sum_{k=1}^{m}D_k\max(U_{p,k},0)(Q_{k-1}-Q_k). \]

Flat spread \(s\) is converted to hazard \(\lambda=s/(1-R)\), then \(Q(t)=e^{-\lambda t}\). With a credit curve, survival comes from log-linear survival interpolation and the implemented terminal hazard continuation. A credit spread is not interchangeable with hazard, and recovery can affect both the flat hazard and the loss multiplier.

DVA uses own-credit survival/recovery and \(\max(-U,0)\). It therefore turns a negative exposure into a positive own-default benefit measure. DvaAggregator and its factory are lower-level facilities. XvaEngine::run automatically constructs CVA and FVA, not a fully specified bilateral CVA/DVA model.

The aggregators produce named measures; they do not prescribe a universal accounting sign for adding/subtracting those measures from a clean price. State that convention at the application boundary.

Signed funding integration

The implemented FVA is

\[ \mathrm{FVA}=\frac1n\sum_p\sum_{k=1}^{m} D_k\,U_{p,k}\,s_f(t_k)\,\Delta t_k. \]

This symmetric funding convention uses signed exposure, so positive and negative contributions can cancel. It uses one spread for both directions; separate borrowing and lending spreads are outside this model. As a result, a reduction in EPE alone does not determine the change in FVA.

Funding inputs can be flat, dated explicit spreads, or spreads derived from a configured funding curve relative to the system curve. System discount factors are optional at lower levels; high-level construction supplies them. Without them, aggregation is not automatically present-valued by another hidden discounting stage.

What the returned sensitivities mean

The engine returns one aggregated list of labeled derivatives across the run. Rate-path leaves can retain original curve calibration/IFT dependencies. Fixed model lambda and configured correlations are not automatically independent risk factors. Calibrated Gaussian sigma includes the supplied volatility Jacobian but omits curve/strike-induced recalibration.

The numerical system discount sequence \(D_k\) is held fixed during AD. A quote scenario that recalculates it can therefore disagree with the reported partial derivative for a legitimate reason.

Credit-curve CVA has another important boundary. High-level construction snapshots numerical survival pillars and their labels; CreditCurveCvaFactory creates independent survival leaves. A label such as CVA.<quote-like label> therefore describes a derivative with respect to a survival pillar, not a replayed original CDS-spread calibration derivative. Recovery is separately labeled. Quote-like text does not change a leaf’s numerical units.

Similarly, funding-index construction snapshots derived forward spreads. FundingCurveFvaFactory creates independent spread leaves, generally labeled with FVA. prefixes, rather than differentiating their derivation from all funding/system curve quotes. Explicit funding-spread curves can contribute their own dated zero-rate leaves. Distinguish these factory risks from direct deterministic credit/funding-curve sensitivities.

Value and gradient share simulated paths and sampling noise. Validate selected dependencies with same-seed runs and carefully specified bumps, but compare like with like: a full quote shock reconstructs more than a partial survival/spread/model derivative may include. Reporting the leaf meaning, units and held-fixed assumptions is as important as reporting the number.

Serializable configuration

Configuration records the observations and construction rules needed to rebuild a market. Rust uses Serde for the native types, Python accepts typed definitions and supported dictionaries, and Excel constructors create the corresponding native configurations. Applications load and save these inputs using their own file conventions.

Observations and curve membership

The quote-record form combines one reference date with identifier/level records:

{
  "reference_date": "2026-02-24",
  "quotes": [
    {"identifier": "FixedRateDeposit_USD_SOFR_1D", "mid": 0.04},
    {"identifier": "OIS_USD_SOFR_1Y", "mid": 0.042, "bid": 0.0419, "ask": 0.0421}
  ]
}

Rust deserializes this as QuoteStoreRecords and converts with QuoteStore::try_from. The selected Level is consumed consistently by construction/calibration. Fixing JSON separately maps index names to dated rate arrays. No file-level quote reference date substitutes for an observation’s actual fixing date.

Examples often wrap a vector of curve configurations in {"curve_specs":[...]}. That wrapper belongs to their loader; the native configuration is one entry:

{
  "market_index": "SOFR",
  "day_counter": "Actual360",
  "interpolator": "LogLinear",
  "enable_extrapolation": true,
  "quotes": ["FixedRateDeposit_USD_SOFR_1D", "OIS_USD_SOFR_1Y"]
}

Missing optional fields use native defaults. The quote identifiers determine whether a curve calibrates instrument prices or interpolates zero rates or discount factors directly. All numeric observations belong in the quote store:

[
  {
    "market_index": {"Other": "FUNDINGSPREAD"},
    "day_counter": "Actual360",
    "interpolator": "Linear",
    "quotes": ["ZeroRate_EUR_FUNDINGSPREAD_0D", "ZeroRate_EUR_FUNDINGSPREAD_1Y", "ZeroRate_EUR_FUNDINGSPREAD_5Y"]
  },
  {
    "market_index": {"Other": "COSTOFFUNDS"},
    "base_index": "EURIBOR12m",
    "spread_index": {"Other": "FUNDINGSPREAD"}
  }
]

The quote store supplies the funding spread values, such as 0.005, 0.0075 and 0.01, under those identifiers. Pillars can use tenors or ISO dates. The benchmark curve must also be configured. Composite components must share their reference date and day counter; dependency resolution is independent of list order. A composite cannot also supply quote identifiers.

Credit configurations additionally need currency, discount index, recovery and CDS quote membership. They construct survival rather than discount factors. Volatility configurations select market identity, Black/Normal convention, smile coordinates, source quotes and extrapolation policy. Reject is the default; use explicit Flat only when that continuation is intended.

Typed models and calibration sources

Models use externally tagged variants rather than arbitrary strings. Fixed Hull–White parameters, for example, are:

{
  "HullWhite": {
    "alpha": 0.1,
    "parameter_source": {"Fixed": {"sigma": 0.01}}
  }
}

LGM uses lambda and Gaussian sigma. Brownian motion uses lognormal volatility and optional dividend rate. A calibrated parameter source replaces Fixed with a source and semantic basket:

{
  "Calibrated": {
    "source": {"Surface": {"market_index": "SOFR"}},
    "calibration_basket": {
      "expiries": ["1Y", "2Y"],
      "tenors": ["3M"],
      "strike": "Atm"
    }
  }
}

The source volatility configuration owns quote membership. The basket selects contracts already available there; absent requested coordinates are errors, not instructions to fabricate an interpolated calibration contract.

PricerConfiguration selects an instrument family, implementation and model assignments. Swaptions can select HullWhiteClosedForm; callable bonds select TreeBased or BackwardFourierEvolution with numerical grids. Direct Rust callers still construct the concrete pricer. A simulation configuration separately supplies index, model, paths, seed, horizon, frequency and time basis.

XVA, CSA and payoff inputs

XvaEngineConfig contains rate-model definitions/driver mappings, FX definitions and path/grid controls. CsaTerms contains legal-group collateral, margin, credit and funding assumptions. Specify these agreement inputs separately from the rate model. See Netting and collateral for field precedence and timing limits.

A scripted-product input is an ordered array of date/source pairs:

[
  {
    "event_date": "2027-03-09",
    "script": "deal = 0; deal pays 1000 on \"2027-06-09\" in \"USD\";"
  }
]

Deserialize it as Vec<CodedEvent> and parse an EventStream; source strings do not bypass language or date validation.

Across these types, dates use YYYY-MM-DD, periods use forms such as 3M and 5Y, and enum spelling follows canonical native variants. Standard MarketIndex variants serialize as strings; structured forms include {"Collateral":["CLP","USD"]}, {"Credit":"NAME"} and {"Equity":"AAPL"}. A supported Python string conversion is not necessarily an alias accepted by a serialized Rust enum.

Historical snapshot archives preserve complete native construction inputs using this discipline. They exclude runtime AD state, fixing histories and binding-only registrations. Preserve those external assumptions separately when recording a reproducible valuation.

Executable reading route

The example collection supplies complete market inputs and valuations for the workflows developed in this book. Run the Rust packages from the repository root so their data paths resolve. Python notebooks have companion loaders and data files in their own directories.

From one valuation to a constructed market

Begin with cargo run -p valuation-swaps. It isolates one flat-curve regular swap and gives a numerical reference for compounding, sign and cashflow-table interpretation. cargo run -p bootstrap then replaces the hand-built market with calibrated SOFR, Term SOFR, ICP, collateralized CLP and bond-anchored curves. cargo run -p sensitivity follows those dependencies into quote-level trade risk.

The product examples develop valuation and quotation choices for each family. valuation-caps connects optionlet quotes to volatility risk. valuation-bonds compares tree and backward-Fourier callable valuation. valuation-loans separates contractual principal from market observations, prepayment, credit and fair-rate schedule choices.

From market targets to future states

cargo run -p volatilitysurface inspects the implied-volatility grid. cargo run -p hullwhite calibrates model volatility, reports instrument-level quality and generates numerical paths. cargo run -p pfe develops claim-based exposure, while cargo run -p cva adds netting-set credit/funding inputs and the high-level XVA engine.

The scripting package has explicit binaries:

cargo run -p scripting-examples --bin valuation
cargo run -p scripting-examples --bin xva
cargo run -p scripting-examples --bin stepped-coupon-bond

These compare native and scripted representations under specified models and payoff rules. Their assertions test those setups; stochastic comparisons also require a sampling tolerance and matching exercise assumptions.

Python and Excel

The Python notebook collection follows the same scopes under bindings/python/examples: bootstrap; valuation/swaps, bonds, caps and loans; volatility surfaces; Hull–White; sensitivities; scripting; and XVA. Each can run in a fresh kernel. The repository’s existing runner executes a selected scope or the whole collection:

python bindings/python/examples/run_notebooks.py valuation/swaps
python bindings/python/examples/run_notebooks.py

The optional rendered studies present the same canonical notebooks and saved outputs without requiring a local kernel.

The notebook directory contains local loaders and illustrative market/model files. Read the problem-setting narrative and units alongside the code; do not copy helper defaults into an unrelated market without reviewing them.

Excel supplies swap and historical-pricing workbooks. Load the released XLL before opening them and reconstruct native handles with full recalculation. The workbook chapter explains revisioned dependencies; the historical chapter explains snapshots. Their formulas, not saved handle strings, are the reproducible workflow.

Checking examples and prose

cargo test -p quantsupport runs the native tests and doctests. The book’s self-contained Rust fences can be built and checked with:

RUSTUP_TOOLCHAIN=stable python tools/book/check_rust.py
python -m unittest discover -s tools/tests -p 'test_book.py' -v

The runner builds the library and selects the artifacts reported by Cargo, then gives mdBook a temporary library search directory. Cached builds with different features cannot introduce duplicate crate candidates. The build and rustdoc harness use the same compiler toolchain, selected explicitly above.

Runnable book snippets include the hidden crate declaration required by mdBook’s rustdoc harness. Fences marked rust,ignore are explicitly described fragments requiring surrounding inputs, not a substitute for testing complete examples. Structural tests check chapter coverage, local links and this snippet distinction.