Constructing and selecting a market¶
From observations to curves¶
Curve construction translates observed prices and rates into discount factors and forwards. A quote store holds the observations and reference date; a curve configuration selects the calibration instruments and numerical conventions. The constructed curve can then be queried or used for valuation.
This connection also determines risk reporting. A bootstrapped valuation can report sensitivities to its OIS quotes, while a direct zero-rate curve reports sensitivities to the rates supplied at its pillars.
The example below bootstraps a USD SOFR curve from a deposit and OIS quotes. The identifiers encode instrument and market conventions understood by the native quote parser. They are not arbitrary maturity labels.
import quantsupport as qs
observations = [
("FixedRateDeposit_USD_SOFR_1D", 0.04500),
("OIS_USD_SOFR_1Y", 0.04837),
("OIS_USD_SOFR_2Y", 0.04528),
("OIS_USD_SOFR_5Y", 0.04077),
]
bootstrap_quotes = qs.QuoteStore.from_dict({
"reference_date": "2026-02-24",
"quotes": [{"identifier": key, "mid": value}
for key, value in observations],
})
bootstrap_spec = qs.CurveConfiguration(
market_index="SOFR", quotes=[key for key, _ in observations],
day_counter="Actual360", interpolator="LogLinear",
enable_extrapolation=False,
)
bootstrap_context = qs.PricingContext(
bootstrap_quotes, [bootstrap_spec],
csa_policy=qs.SingleCurveCSADiscountPolicy("USD", "SOFR"),
)
with bootstrap_context:
curve = bootstrap_context.curve("SOFR")
print(curve.nodes())
print(curve.pillars())
forward = curve.forward_rate(
"2027-02-24", "2028-02-24",
qs.Compounding.Simple, qs.Frequency.Annual,
)
print(forward)
nodes() reports interpolation dates and discount factors; pillars()
reports labelled market inputs where available. Together they show how the
curve is represented and which inputs determine its sensitivities.
Construction may also need other projection or collateral curves. Supply
those dependencies under their configured market indices.
For a simple forward under the curve’s time basis,
Changing the day counter, compounding convention, or coupon schedule changes the interpretation of a rate even when the displayed number is unchanged. Log-linear discount-factor interpolation preserves a different curve shape from linear zero-rate interpolation. Extrapolation is a modeling choice and must be recorded explicitly.
The bootstrap uses native OIS calibration instruments with compounded
overnight coupons. Python’s standalone Swap uses regular single-fixing
coupons; a standalone OvernightIndexSwap wrapper is currently unavailable.
Direct curve quotes¶
Discount factors and zero rates can also be supplied directly in QuoteStore.
Reference their identifiers in CurveConfiguration.quotes with the default
construction rule:
direct_quotes = qs.QuoteStore.from_dict({
"reference_date": "2026-02-24",
"quotes": [
{"identifier": "DiscountFactor_USD_SOFR_1Y", "mid": 0.96},
{"identifier": "DiscountFactor_USD_SOFR_2028-02-24", "mid": 0.91},
],
})
direct_spec = qs.CurveConfiguration(
market_index="SOFR", quotes=direct_quotes.identifiers(),
day_counter="Actual365", interpolator="LogLinear",
)
with qs.PricingContext(direct_quotes, [direct_spec]) as context:
print(context.curve("SOFR").discount_factor("2027-02-24"))
Use DiscountFactor_CCY_Index_Pillar for dimensionless factors, or
ZeroRate_CCY_Index_Pillar[_Compounding[_Frequency]] for annual decimal
zero rates. Pillar is a tenor such as 1Y or an ISO date. Zero rates
default to Continuous compounding and Annual frequency; for example,
ZeroRate_USD_SOFR_1Y_Compounded_Semiannual quotes a semiannually compounded
annual rate. The enclosing configuration supplies the day counter,
interpolator, and extrapolation setting. Interpolation applies to factors for
discount-factor quotes and to rates for zero-rate quotes.
Direct pillars are sorted by date. Each curve uses one direct quote family, currency, and rate convention, with identifiers matching its market index. Do not mix direct observations with calibration instruments in one curve. Dates must be distinct and on or after the reference date. Discount factors must be finite and positive; the reference-date factor is fixed at one and is inserted automatically. A supplied reference-date factor must equal one, and a discount curve needs at least one future pillar.
Direct quotes use the selected Mid/Bid/Ask level and retain their original
identifiers for scenarios and sensitivities. They can be components of
composite curves or dependencies of calibrated curves. All observed values stay
in QuoteStore; configuration contains identifiers and construction conventions.
Composite curves and standalone term structures¶
The separate ZeroRateTermStructure and TenorBasedZeroRateTermStructure
classes are reusable numeric term structures. Their defaults are Actual365,
Continuous, Annual, Linear, and disabled extrapolation. A configuration’s
defaults are different: Actual360, LogLinear, and enabled extrapolation.
Choose conventions rather than transferring a number between these APIs and
assuming identical behavior. A tenor curve can be serialized as the equivalent
dated zero-rate curve.
A composite curve multiplies two component discount factors:
Continuous zero rates add when their time bases agree. This is a
discount-factor composition, not an arithmetic average of quoted yields.
Configure it with
CurveConfiguration.composite(market_index=..., base_index=..., spread_index=...) and supply
both component configurations. Dependencies are resolved by construction;
cycles and missing parents are errors.
For a loan funding experiment, a useful specification is a benchmark curve, an
quoted spread curve identified by MarketIndex.other("FUNDINGSPREAD"),
and their composite. Choose the composite through the fixed-income discount
policy rather than adding a discount index to the loan contract.
Quotes, fixings, FX, and policies¶
Quote levels are selected with quote_level=Level.Mid, Bid, or Ask.
The default is Mid. Every quote consumed by construction or calibration must
contain the selected side; missing Bid/Ask values do not fall back to Mid.
Quote units depend on the instrument: a decimal rate, a futures price, and a
volatility quote are not transformed into a common percentage convention.
Historical fixings are separate observations. FixingStore.add(index, date,
value) records a published fixing, while a forward curve projects an
unobserved future reset. A contract that needs a past fixing should fail when
that observation is missing; a zero fixing is not an acceptable substitute.
Read the product’s fixing calendar and lag independently of its payment
schedule.
Supply verified historical observations. If the source has gaps, handle them
through an explicitly validated preprocessing policy. The native
fill_missing_fixings routine uses reversed interpolation coordinates and
is unavailable through Python.
FxStore stores rates in quote-currency units per base-currency unit.
For example {"base": "EUR", "quote": "USD", "rate": 1.1} means
1 EUR = 1.1 USD. Direct, inverse, and triangulated spot lookup are available.
Native Rust pricers use this store to convert monetary PVs and sensitivities to
context.base_currency, including FX conversion risk. Python’s
PricingContext.evaluate returns those native results.
Cashflow reports retain their original payment currencies. Missing conversion
paths raise an error when the native valuation requires conversion.
SingleCurveCSADiscountPolicy defines collateral currency and remuneration
index for curve construction and derivative valuation. A different cashflow
currency may require its collateral-adjusted curve. FixedIncomeDiscountPolicy
maps currencies to bond/loan discount curves and controls whether an instrument
index takes precedence. CsaTerms instead describes one netting agreement’s
credit, funding, and collateral terms. These three choices answer different
financial questions.
When comparing Python and Excel valuations, use matching pricers, discount policies and reporting currencies. The bindings expose different policy controls, so a common base currency alone is insufficient to establish those assumptions.
Volatility and calibration inputs¶
A volatility configuration identifies quote inputs, smile coordinates, and
construction policies. A constructed VolatilitySurface or
VolatilityCube is a read-only context view, not a replacement quote store.
Rate strikes are annual decimal rates; equity and FX strikes are absolute
prices under their own currency conventions.
The configured volatility type matters: a Black lognormal volatility is not a
normal volatility or an absolute short-rate sigma. Inspect volatility_type
and smile_type before interpreting a surface lookup. For a cube, expiry
and underlying tenor are separate coordinates.
Surface and cube construction supports sparse slices and singleton axes.
The default extrapolation policy is Reject, so each query must lie within
the selected slices. An ATM calibration strike can fall outside a smile even
when its expiry is covered. Choose an in-domain basket or an explicit Flat
policy consistent with the intended boundary behavior.
PricerConfiguration selects an instrument pricing implementation and
index-specific model assignments. SimulationConfiguration describes a
generated simulation. Calibrated Hull–White/LGM parameters consume the selected
quote side and configured volatility source. A calibrated sigma schedule is an
output of that fit; fixed sigma is an input in absolute short-rate units,
not an option-market volatility percentage.
Native serialization¶
Configuration from_dict returns one configuration; from_dict_list
returns a list and accepts a native object, array, or single-key
array wrapper where documented. Product from_dict instead calls the
product constructor using keyword terms. Do not confuse these two schemas.
Read files with Python’s json.load and pass the decoded data to the appropriate
dictionary constructor:
import json
with open("quotes.json", encoding="utf-8") as stream:
quotes = qs.QuoteStore.from_dict(json.load(stream))
with open("curve_specs.json", encoding="utf-8") as stream:
curves = qs.CurveConfiguration.from_dict_list(json.load(stream))
Excel’s JSON functions take JSON text in worksheet cells. Parameterized native indices use tagged
objects such as {"Equity": "SPX"}, {"Credit": "ACME"}, or
{"FxPair": ["EUR", "USD"]}; typed Python factories avoid reliance on
display-label parsing.
Historical repricing without look-ahead¶
A historical snapshot captures one coherent market: quotes and construction, volatility, simulation, and pricer inputs. It is selected by observation timestamp, while its quote reference date remains the valuation date. The following independent example makes that distinction explicit.
import quantsupport as qs
def recorded_market(day, rate):
observations = qs.QuoteStore.from_dict({
"reference_date": f"2026-02-{day:02d}", "quotes": [
{"identifier": f"ZeroRate_USD_SOFR_{tenor}", "mid": rate}
for tenor in ["0D", "10Y"]
],
})
specification = qs.CurveConfiguration(
market_index="SOFR", day_counter="Actual360",
interpolator="Linear",
quotes=observations.identifiers(),
)
return qs.PricingContext(observations, [specification])
archive = qs.MarketSnapshotStore()
for day, rate in [(23, 0.03), (24, 0.031)]:
snapshot = qs.MarketSnapshot.from_context(
f"2026-02-{day:02d}T16:00:00Z", recorded_market(day, rate),
)
archive.insert(snapshot)
selector = qs.SnapshotSelector(
"2026-02-25T08:00:00+00:00",
rule=qs.SnapshotSelectionRule.Previous, level=qs.Level.Mid,
)
selected = archive.select(selector)
with archive.pricing_context(selector) as historical:
assert str(historical.evaluation_date) == "2026-02-24"
assert historical.snapshot_timestamp == selected.timestamp
Previous is the default and never chooses a later observation. Exact
requires equality; Closest may choose a future market and resolves ties
to the earlier timestamp. Duplicate UTC instants are errors even if the input
strings use different offsets. Inspect the matched timestamp rather than
reporting the requested timestamp as though it were observed data.
Snapshots rebuild from construction inputs rather than serializing runtime
curves, caches, or AD state. Zero rates and discount factors use the selected
quote side, just like calibration instruments; FX spots are side-independent. Fixings are supplied separately to archive.pricing_context(selector, fixings=...); there is
no automatic publication-vintage filtering. For a historical backtest, supply
the fixing set that was known at each observation instant.
Continue with Valuation, quotation, and model choice to choose product-specific requests and models, or study the curve notebook for a larger calibration investigation.