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:
- Build an instrument containing contractual economics.
- Wrap it in a trade containing position metadata.
- Prepare a pricing context containing market data as of one date.
- Select a pricer and the outputs to calculate.
- 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:
| Scalar | Use |
|---|---|
f64 | Value-only calculations and simulation paths where the surrounding API supports plain values |
Fwd<T> | Forward-mode automatic differentiation |
Dual<T> | Reverse-mode automatic differentiation |
DualFwd | The 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.
| Layer | Main types | Responsibility |
|---|---|---|
| Raw data | QuoteStore, FixingStore, FxStore | Quotes, historical fixings, and spot FX observations |
| Configuration | Curve, volatility, credit, and simulation configurations | Instructions for constructing market objects |
| Constructed data | ConstructedElementStore | Discount, dividend, and credit curves; volatility surfaces and cubes; simulations |
| Orchestration | PricingContext | Owns 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:
MarketDataRequestis produced by the pricer and describes required market inputs.Requestis 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:
| Type | Contents |
|---|---|
EvaluationResults | Optional price, fair rate, sensitivities, and cashflows exposed through public getters |
SensitivityMap | Parallel market-pillar labels and NPV derivatives |
CashflowsTable | Column-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:
| Category | Typical cause |
|---|---|
ValueNotSetErr | A required builder input is absent |
NotFoundErr | A required curve, fixing, quote, model, or pricer is unavailable |
InvalidValueErr | Inputs are present but inconsistent or outside the accepted domain |
InterpolationErr / NodeError | Curve or surface construction/evaluation failed |
SolverErr | Calibration or root finding failed |
TapeError / DualFwdError | Automatic-differentiation state is invalid |
| Parsing and serialization errors | External 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:
| Type | Role |
|---|---|
Date | Calendar date and date arithmetic |
Period / TimeUnit | Relative terms such as three months or five years |
DayCounter | Converts date intervals to year fractions |
Frequency | Coupon, compounding, or schedule frequency |
Calendar | Holiday and business-day rules |
BusinessDayConvention | Adjustment rule when a date is not a business day |
DateGenerationRule | Forward, backward, IMM, CDS, and related schedule rules |
MakeSchedule | Builds 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
Instrumentand normally provide aMake*builder and trade wrapper. - New pricing method: implement
Pricerand declare market dependencies inmarket_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
Evaluatorunder the trade’sTypeId.
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.