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

Rust API

QuantSupport separates product definition, market construction, pricing, and results. Most applications follow the same path regardless of asset class:

quotes and fixings -> PricingContext -> Pricer -> EvaluationResults
                           ^              ^
                           |              |
                   market requests   instrument + trade

The easiest entry point is quantsupport::prelude::*, which re-exports the types used by normal pricing workflows. Lower-level modules remain useful when implementing a new instrument, pricer, curve, or simulation model.

This chapter is a map of those responsibilities. It is not an exhaustive API reference; the generated Rust documentation remains the source for every method and trait bound.

The pricing pipeline

A typical valuation has five steps:

  1. Build an instrument containing contractual economics.
  2. Wrap it in a trade containing position metadata.
  3. Prepare a pricing context containing market data as of one date.
  4. Select a pricer and the outputs to calculate.
  5. Read those outputs from evaluation results.
use quantsupport::prelude::*;

let instrument = MakeSwap::<DualFwd>::default().build()?; // contract fields
let trade = SwapTrade::new(instrument, trade_date, notional, side);
let context = PricingContext::new(); // quotes, fixings, curves, volatility, and configuration

let pricer = DiscountedCashflowPricer::<Swap<DualFwd>, SwapTrade<DualFwd>>::new();
let results = pricer.evaluate(
    &trade,
    &[Request::Value, Request::Sensitivities],
    &context,
)?;

The first swap chapter develops a complete example of this flow.

Numerical scalar types

Curves, instruments, and models commonly use a generic T: Scalar. The scalar supplies arithmetic and mathematical operations while allowing the same financial logic to run with different numeric representations.

pub trait Scalar: Copy + PartialOrd {
    fn scalar(value: f64) -> Self;
    fn value(&self) -> f64;
    fn zero() -> Self;
    fn one() -> Self;
    // arithmetic and elementary functions
}

The principal choices are:

ScalarUse
f64Value-only calculations and simulation paths where the surrounding API supports plain values
Fwd<T>Forward-mode automatic differentiation
Dual<T>Reverse-mode automatic differentiation
DualFwdThe library’s standard nested AD scalar for pricing and market sensitivities

Scalar types must agree across connected objects. For example, a Swap<DualFwd> is valued against curves that produce DualFwd. The current constructed-market and standard pricing-context infrastructure is AD-oriented, so DualFwd is the normal choice for direct pricing. Some simulations and standalone numerical components use f64.

Use .value() when a scalar calculation reaches a reporting boundary. Do not convert intermediate values to f64, because doing so discards derivative information.

See Automatic Differentiation for tape and sensitivity behavior.

Instruments and trades

An instrument describes contractual economics. The base trait intentionally guarantees only an identifier:

pub trait Instrument: Send + Sync {
    fn identifier(&self) -> String;
}

Product-specific traits expose additional capabilities. Examples include leg access, currency, discounting index, strike, or maturity. Pricers use these narrower capability traits rather than placing every possible property on Instrument.

Instruments are normally created with Make* builders:

let swap = MakeSwap::<DualFwd>::default()
    .with_identifier("USD_IRS_5Y".to_string())
    .with_start_date(start_date)
    .with_maturity_date(maturity_date)
    .with_notional(10_000_000.0)
    .with_fixed_rate(0.03)
    .with_currency(Currency::USD)
    .with_market_index(MarketIndex::SOFR)
    .with_rate_definition(rate_definition)
    .build()?;

Builders collect required and optional fields, apply defaults, construct schedules and legs, and return QSError for invalid or missing inputs. This is preferable to calling long positional constructors in application code.

A trade adds position-level information:

pub trait Trade<I: Instrument>: Send + Sync {
    fn instrument(&self) -> &I;
    fn trade_date(&self) -> Date;
    fn side(&self) -> Side;
}

pub enum Side {
    PayShort,    // sign = -1
    LongReceive, // sign = +1
}

The exact meaning of the side follows the product. For a vanilla swap, LongReceive receives the fixed leg and pays the floating leg. Pricers generally accept the trade type, not the bare instrument.

Market data and the pricing context

Market data is split between raw observations and constructed valuation objects.

LayerMain typesResponsibility
Raw dataQuoteStore, FixingStore, FxStoreQuotes, historical fixings, and spot FX observations
ConfigurationCurve, volatility, credit, and simulation configurationsInstructions for constructing market objects
Constructed dataConstructedElementStoreDiscount, dividend, and credit curves; volatility surfaces and cubes; simulations
OrchestrationPricingContextOwns the market state and serves pricer requests

MarketIndex is the key connecting products to market objects. A SOFR floating leg requests data under MarketIndex::SOFR; the context must contain or construct the corresponding curve.

There are two common ways to prepare a context.

Configuration-driven construction

Provide quotes and configurations, then initialize once:

let mut context = PricingContext::new()
    .with_quote_store(quotes)
    .with_fixing_store(fixings)
    .with_curve_configurations(curve_configurations)
    .with_base_currency(Currency::USD)
    .with_base_index(MarketIndex::SOFR);

context.initialize()?;

initialize() applies scenarios and builds configured curves, credit curves, volatility objects, and simulations in dependency order.

Direct construction

Small applications and tests can create market objects themselves and insert them into a ConstructedElementStore. In that case, initialize() is unnecessary because the objects already exist. The first swap uses this route.

Request and response boundary

Pricers do not traverse PricingContext directly. They declare a MarketDataRequest, and a MarketDataProvider returns the requested subset as MarketData:

pub trait MarketDataProvider {
    fn handle_request(&self, request: &MarketDataRequest) -> Result<MarketData>;
    fn evaluation_date(&self) -> Date;
}

This boundary keeps pricing logic independent of how the market was assembled. It also makes focused tests possible with a small provider that returns hand-built data.

Pricers and calculation requests

A pricer binds one trade type to one pricing methodology:

pub trait Pricer: Send + Sync {
    type Item;
    type Policy: ?Sized + Send + Sync;

    fn evaluate(
        &self,
        trade: &Self::Item,
        requests: &[Request],
        context: &impl MarketDataProvider,
    ) -> Result<EvaluationResults>;

    fn market_data_request(&self, trade: &Self::Item) -> Option<MarketDataRequest>;
    fn set_discount_policy(&mut self, policy: Box<Self::Policy>);
    fn discount_policy(&self) -> Option<&Self::Policy>;
}

There are two different request concepts:

  • MarketDataRequest is produced by the pricer and describes required market inputs.
  • Request is supplied by the caller and describes desired outputs.
pub enum Request {
    Value,
    YieldToMaturity,
    ModifiedDuration,
    Sensitivities,
    Cashflows,
    FairRate,
}

Support is pricer-specific. Request only outputs listed for that pricer in the pricing overview. Depending on the implementation, an unsupported request may be ignored rather than producing a populated result.

When several outputs share valuation work, pricers can calculate the common state once. For example, DiscountedCashflowPricer prepares value state once for value, cashflow, and sensitivity requests submitted in the same call.

Results

EvaluationResults is a non-generic reporting envelope. Its fields are optional because the caller chooses which calculations to request:

let results = pricer.evaluate(
    &trade,
    &[Request::Value, Request::Cashflows],
    &context,
)?;

if let Some(npv) = results.price() {
    println!("NPV: {npv:.2}");
}

if let Some(cashflows) = results.cashflows() {
    for (date, amount) in cashflows
        .payment_dates()
        .iter()
        .zip(cashflows.amounts())
    {
        println!("{date}: {amount:.2}");
    }
}

The main result types are:

TypeContents
EvaluationResultsOptional price, fair rate, sensitivities, and cashflows exposed through public getters
SensitivityMapParallel market-pillar labels and NPV derivatives
CashflowsTableColumn-oriented payment dates, types, amounts, fixings, accrual periods, currencies, leg indices, and optional strikes

Always check the relevant Option; creating an EvaluationResults value does not imply that every calculation was performed.

Discount policies

Discounting is a policy decision rather than an intrinsic property of every payoff. A DiscountPolicy maps a discountable object to the curve index that should discount it:

pub trait DiscountPolicy: Send + Sync {
    fn accept(&self, target: &dyn Discountable) -> Result<MarketIndex>;
    fn discount_indices(&self) -> Vec<MarketIndex>;
}

Important implementations include:

  • SingleCurveCSADiscountPolicy, for collateralized discounting under one remuneration index and currency.
  • FixedIncomeDiscountPolicy, which can prefer an instrument’s own index or use a configured risk-free index by currency.

Without an explicit policy, DiscountedCashflowPricer uses its default curve-resolution rules. NettingSet also owns a discount policy so exposure and XVA preprocessing can resolve discount curves consistently.

Curves, volatility, and models

The common rate-curve abstraction is InterestRatesTermStructure<T>. It provides discount factors, forward rates, dates, nodes, and day-count information. Principal implementations are:

  • FlatForwardTermStructure<T> for a constant rate.
  • DiscountTermStructure<T> for an interpolated discount-factor curve.

Volatility surfaces resolve an expiry and strike coordinate; volatility cubes add tenor. Model and simulation APIs consume constructed curves and volatility objects rather than raw quote strings.

Use the dedicated chapters for domain behavior:

Static and dynamic dispatch

Direct use of a concrete pricer gives compile-time type checking and is the simplest option:

let pricer = DiscountedCashflowPricer::<Swap<DualFwd>, SwapTrade<DualFwd>>::new();
let results = pricer.evaluate(&trade, &[Request::Value], &context)?;

Applications pricing heterogeneous portfolios can register pricers in Evaluator. It stores ErasedPricer implementations by the trade’s TypeId and performs the downcast at runtime:

use std::{any::{Any, TypeId}, collections::HashMap};
use quantsupport::core::{evaluator::Evaluator, pricer::ErasedPricer};

let mut pricers: HashMap<TypeId, Box<dyn ErasedPricer>> = HashMap::new();
pricers.insert(
    TypeId::of::<SwapTrade<DualFwd>>(),
    Box::new(DiscountedCashflowPricer::<Swap<DualFwd>, SwapTrade<DualFwd>>::new()),
);

let evaluator = Evaluator::new(pricers);
let results = evaluator.evaluate(
    &trade as &dyn Any,
    &[Request::Value],
    &context,
)?;

Use direct dispatch for isolated pricing and generic library code. Use Evaluator when the trade type is known only at runtime.

Errors

Public fallible APIs return the crate alias:

pub type Result<T> = std::result::Result<T, QSError>;

Common error categories are:

CategoryTypical cause
ValueNotSetErrA required builder input is absent
NotFoundErrA required curve, fixing, quote, model, or pricer is unavailable
InvalidValueErrInputs are present but inconsistent or outside the accepted domain
InterpolationErr / NodeErrorCurve or surface construction/evaluation failed
SolverErrCalibration or root finding failed
TapeError / DualFwdErrorAutomatic-differentiation state is invalid
Parsing and serialization errorsExternal identifiers or data could not be decoded

Use ? to propagate errors and add application context at system boundaries. Avoid treating a missing optional result as an error unless that result was required by the workflow.

Time conventions

Time types are shared across instruments, curves, and models:

TypeRole
DateCalendar date and date arithmetic
Period / TimeUnitRelative terms such as three months or five years
DayCounterConverts date intervals to year fractions
FrequencyCoupon, compounding, or schedule frequency
CalendarHoliday and business-day rules
BusinessDayConventionAdjustment rule when a date is not a business day
DateGenerationRuleForward, backward, IMM, CDS, and related schedule rules
MakeScheduleBuilds explicit date schedules

These conventions are part of valuation inputs. A coupon’s day count, its payment frequency, and a curve’s compounding convention are separate choices and should not be assumed to match.

Extending the library

Add functionality at the narrowest suitable boundary:

  • New contract: implement Instrument and normally provide a Make* builder and trade wrapper.
  • New pricing method: implement Pricer and declare market dependencies in market_data_request().
  • New curve: implement InterestRatesTermStructure<T> and the pillar traits required by its use case.
  • New discounting convention: implement DiscountPolicy.
  • New portfolio dispatch entry: register the pricer with Evaluator under the trade’s TypeId.

Keep economic definitions in instruments, market lookup in providers, numerical valuation in pricers, and presentation outside EvaluationResults. Maintaining those boundaries is what lets the same products participate in direct pricing, calibration, simulation, and XVA workflows.