Metadata-Version: 2.4
Name: quantsupport
Version: 0.1.9
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Office/Business :: Financial
Requires-Dist: pandas
Requires-Dist: pandas-stubs ; extra == 'typing'
Requires-Dist: typing-extensions>=4.15 ; extra == 'typing'
Provides-Extra: typing
Summary: Derivative pricing and risk analytics (Rust core with Python bindings).
License: MIT
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# quantsupport (Python bindings)

PyO3 bindings for the [quantsupport](https://github.com/jmelo11/quantsupport) Rust
derivative pricing and risk analytics library.

## Install (development)

```bash
pip install maturin
maturin develop -m bindings/python/Cargo.toml --release
```

## Guided examples

Start with the [examples guide](examples/README.md) for setup and a suggested
reading order. The notebooks explain the library to readers who know the
financial products, with typed Python contracts and local data-loading helpers.
They are cataloged under the same scopes as the Rust examples: `bootstrap`,
`valuation`, `volatilitysurface`, `hullwhite`, `sensitivity`, `scripting`, and `cva`.

| Notebook | Workflow |
| --- | --- |
| [Curve bootstrapping](examples/bootstrap/curves.ipynb) | Quote inputs, construction conventions, nodes, pillars, and forwards |
| [First swap](examples/valuation/tour.ipynb) | Market inputs, dates, context lifecycle, requests, and result units |
| [Product valuations](examples/valuation/product_valuations.ipynb) | Swaps, straight bonds, caplets and caps; par rates, yields, duration and spreads |
| [Curves and volatility](examples/volatilitysurface/curves_and_volatility.ipynb) | Construction, inspection, and their impact on swap and option values |
| [Simulations](examples/hullwhite/simulations.ipynb) | Calibration, path retrieval, distributions, seeds and sampling error |
| [Portfolio risk](examples/sensitivity/portfolio_risk.ipynb) | Position aggregation, DV01/vega, cashflow reports, and full-revaluation checks |
| [Stepped-coupon bond](examples/valuation/stepped_coupon_bond.ipynb) | Callable terms, tree/Fourier engines, OAS and sensitivities |
| [Scripted cashflows](examples/scripting/scripted_cashflows.ipynb) | Dated payoffs and native/scripted representation checks |
| [XVA](examples/cva/xva.ipynb) | Netting sets, client terms, exposure profiles and adjustment risk |

Execute all notebooks with `python bindings/python/examples/run_notebooks.py`
after installing the example dependencies and building the binding. The runner
uses a fresh kernel per notebook and leaves the source notebooks unchanged.

## Usage

Market data and configurations are typed objects created from dicts
(`from_dict`) or JSON files (`from_json`), deserialized directly by the Rust
library's serde implementations. Create `PricingContext` and enter `with ctx:`
to bootstrap curves, volatility surfaces/cubes and simulations automatically
and start the AD tape. Exiting releases constructed market data and rewinds
the tape, including when the block raises an exception. Call `initialize()`
explicitly only when using the context without a `with` block.

```python
import quantsupport as qs

quotes = qs.QuoteStore.from_json("quotes.json")
curves = qs.CurveConfiguration.from_json("curve_specs.json")   # list
fx = qs.FxStore.from_dict([{"base": "CLP", "quote": "USD", "rate": 1 / 900}])
csa_policy = qs.SingleCurveCSADiscountPolicy(collateral_currency=qs.Currency.USD, collateral_index=qs.MarketIndex.SOFR)

ref = quotes.reference_date            # qs.Date
swap = qs.Swap(
    identifier="USD_IRS_5Y",
    start_date=ref,
    maturity_date=ref + "5Y",
    notional=10_000_000.0,
    fixed_rate=0.0378,
    currency=qs.Currency.USD,
    market_index=qs.MarketIndex.SOFR,
)
trade = qs.Trade(swap, ref, qs.Side.LongReceive)

ctx = qs.PricingContext(quotes=quotes, curves=curves, fx=fx, csa_policy=csa_policy)

with ctx:
    # explore the bootstrapped market data
    sofr = ctx.curve(qs.MarketIndex.SOFR)
    print(sofr.nodes())                 # DataFrame: date / discount_factor
    print(sofr.discount_factor(ref + "5Y"))
    print(sofr.forward_rate(ref + "1Y", ref + "2Y",
                            qs.Compounding.Simple, qs.Frequency.Annual))

    res = ctx.evaluate(trade, [qs.Request.Value, qs.Request.Cashflows, qs.Request.Sensitivities])
    print(res.price)            # NPV (float)
    print(res.sensitivities)    # pandas DataFrame: pillar / value
    print(res.cashflows)        # pandas DataFrame
```

Every enum argument also accepts its string name (`currency="USD"`,
`side="pay"`, `requests=["Value"]`, ...).

The [currency-conversion walkthrough](examples/valuation/currency_conversion.ipynb)
values a CLP swap in the USD base currency under a USD CSA, explains FX pair
direction, and reconciles the native value from converted, discounted cashflows.

### Instruments And Trades

Built-in products such as `Swap`, `FixedRateBond`, and `FxOption` are
`Instrument` subclasses containing contract terms, not positions. Both
`ctx.evaluate()` and `NettingSet` require explicit `Trade` objects:

```python
long = qs.Trade(swap, ref, qs.Side.LongReceive)
short = qs.Trade(swap, ref, qs.Side.PayShort, notional=500_000)
assert long.instrument is short.instrument
```

The execution date is required and is never inferred from maturity or expiry.
`side` defaults to `LongReceive`. An optional `notional` overrides the
instrument's contractual amount for that position without mutating it.
FX forwards, FX/equity options, CDS and caplets have no contractual notional
field: their position notional belongs exclusively to `Trade` (default `1.0`).
Cross-currency positions use `domestic_notional` and `foreign_notional`
overrides; futures positions use `num_contracts` (default `1.0`). Position
amounts must be finite and non-negative; direction belongs to `side`.
Bond positions additionally own `pricing_anchor`, `settlement_date`, and
`spread_day_counter`. Product constructors and product dictionaries no longer
accept these position fields, `side`, `trade_date`, or `num_contracts`.

Select the quote level during setup with
`qs.PricingContext(quotes=quotes, curves=curves, quote_level=qs.Level.Bid)`.
The setting accepts `qs.Level.Mid`, `qs.Level.Bid`, `qs.Level.Ask`, or their
case-insensitive names, such as `quote_level="ask"`. It defaults to mid and
applies to market construction and model calibration. `ctx.quote_level`
reports the selection. Every consumed quote must provide the requested level.

### European swaptions, equity forwards, and calibration reports

`EquityForward(identifier, market_index, delivery_date, strike, currency)` is a
native instrument. Its position size defaults to one in `Trade`. Pricing uses
the evaluation-date spot fixing, the index's forward curve, an optional dividend
curve (zero dividends when absent), and the context's CSA discount policy.
`Value`, `FairRate`, and `Sensitivities` use the same Rust pricer as Excel.

```python
forward = qs.EquityForward("SPX-FWD", "EQ:SPX", "2027-02-24", 5100.0, "USD")
position = qs.Trade(forward, "2026-02-24", notional=100.0)
```

`EuropeanSwaption(identifier, expiry_date, maturity_date, strike, notional,
currency, market_index, swaption_type="Payer")` builds the underlying swap at
expiry. `SwaptionType.Payer` and `.Receiver` are also accepted. Optional rate
conventions, payment frequencies, and a calendar configure the underlying
schedule. Product notional is the default position size; `Trade.notional`
overrides it. Both products support `from_dict` and `from_json`.

Swaptions require this explicit `PricerConfiguration` in
`PricingContext(pricer_configurations=[configuration], ...)`:

```python
configuration = qs.PricerConfiguration.from_dict({
    "instrument_type": "EuropeanSwaption",
    "pricer": "HullWhiteClosedForm",
    "models": [{"market_index": "SOFR", "model": {"HullWhite": {
        "alpha": 0.1, "parameter_source": {"Calibrated": {
            "source": {"Surface": {"market_index": "SOFR"}},
            "calibration_basket": {"strike": "Atm"}
        }}
    }}}],
})
```

This engine supports `Value` and curve and volatility `Sensitivities`. It uses
the same Hull–White calibration and piecewise volatility schedule as callable
bonds and simulations. Supply the source through `volatility_surfaces` or use
`"source": {"Cube": {"market_index": "SOFR"}}` with `volatility_cubes` and a
basket selecting one tenor per expiry. The source's curve and calibration quotes
are requested automatically. `{"Fixed": {"sigma": 0.01}}` remains available
when calibration is unnecessary.

After building a calibrated Hull–White simulation,
`ctx.simulation("SOFR").calibration_diagnostics()` returns a DataFrame with
`identifier`, `expiry`, `t`, `big_t`, `market_vol`, `market_price`, `model_price`,
`calibrated_sigma`, `forward_rate`, `effective_strike`, and `price_error` (model
minus market price). Fixed-parameter and other simulation models return `None`.

Calendar strings include `UnitedKingdom(Settlement)`, `Japan`, and
`JointCalendar(JoinHolidays,UnitedKingdom(Settlement),Japan)`. `JoinHolidays`
requires every market to be open; `JoinBusinessDays` requires at least one.
SONIA and TONAR use the local bank calendars in Rust index conventions.

Wheels include the public API stubs and `py.typed` marker. Install
`quantsupport[typing]` to include the pandas stubs and typing helpers. The bindings workflow
builds and tests CPython 3.9–3.14 wheels on Linux, Windows, and both Mac
architectures. Pushes to `main` publish the wheels and a source archive to PyPI
through the [release workflow](../../README.md#releases), using the same version
as the Rust crate. Wheels are also available as Actions artifacts and GitHub
release assets. To build one locally, run `maturin build --release` inside
`bindings/python`; no Rust compiler is needed when installing the resulting wheel.

### In-memory product and market schemas

All built-in products support `Product.from_dict(spec)` and
`Product.from_json(path)`. Product dict keys are exactly the keyword constructor
arguments, with the same defaults and validation. Dates accept ISO strings;
enum arguments accept their names. No `qs.Date` or enum objects are required.
The classmethods also preserve subclasses.

```python
swap = qs.Swap.from_dict({
    "identifier": "USD-IRS", "start_date": "2026-02-24",
    "maturity_date": "2031-02-24", "notional": 1_000_000,
    "fixed_rate": 0.04, "currency": "USD", "market_index": "SOFR",
    "side": "LongReceive", "fixed_leg_frequency": "Semiannual",
})

quotes = qs.QuoteStore.from_dict(quote_data)
curves = qs.CurveConfiguration.from_dict_list(curve_data)
volatility_surfaces = qs.VolatilitySurfaceConfiguration.from_dict_list(volatility_data)
csa_policy = qs.SingleCurveCSADiscountPolicy.from_dict({"collateral_currency": "USD", "collateral_index": "SOFR"})
fx = qs.FxStore.from_dict([{"base": "EUR", "quote": "USD", "rate": 1.1}])
```

`from_dict_list` is available on `CurveConfiguration`,
`VolatilitySurfaceConfiguration`, `VolatilityCubeConfiguration`,
`PricerConfiguration`, and `SimulationConfiguration`. Like `from_json`, it
accepts a list, one configuration dict, or a single-key object wrapping a list,
such as `{"curve_specs": [...]}`. `from_dict` on those classes still returns
one configuration. `QuoteStore`, `FixingStore`, `FxStore`, `Scenario`, and
`SingleCurveCSADiscountPolicy` keep their existing `from_dict` methods.

Market configs use the native Rust serde schemas. For product market-index
fields, rate indices are strings (`"SOFR"`); other variants use native tagged
dicts such as `{"Equity": "AAPL"}`, `{"Credit": "CORPORATE-A"}`,
`{"FxPair": ["EUR", "USD"]}`, and `{"Collateral": ["EUR", "USD"]}`.
Strikes accept a float, `"ATM"`, or a native tagged dict such as
`{"Absolute": 100.0}`.

### Bond pricing, Z-spread, and OAS

Straight fixed-rate bonds use `BondPricer`. Supply the required `pricing_anchor`
when constructing `qs.Trade(bond, ref, pricing_anchor=anchor)`. Use `trade.with_pricing_anchor(anchor)`
to create a copy with another quote; `PricingContext.evaluate(trade, requests)`
reads the quote from that trade.

`FixedRateBond.discount_index` is optional. Omit it to use the context's
fixed-income discount policy, which maps the base currency to the base index.
An explicit index takes precedence when `prefer_instrument_index=True`.
Pass `fixed_income_policy=qs.FixedIncomeDiscountPolicy({"USD": "SOFR"},
prefer_instrument_index=False)` to select the policy curve explicitly.
An unmapped currency produces an error rather than silently selecting a curve.

```python
anchor = qs.BondPricingAnchor.clean_price(99.25)
result = ctx.evaluate(
    qs.Trade(bond, ref, pricing_anchor=anchor),
    [qs.Request.Value, qs.Request.ModifiedDuration,
     qs.Request.ZSpread, qs.Request.Sensitivities],
)
print(result.z_spread)
print(result.modified_duration)
print(result.sensitivities)  # curve rows plus <identifier>.ZSpread
```

Callable debt uses the separate `SteppedCouponBond`. Coupon definitions and
outstanding-notional percentages are positional; the final entry in each list
repeats for all later generated payment periods. Calls always use explicit
dates and strikes:

```python
callable_bond = qs.SteppedCouponBond(
    identifier="USD-STEPPED-CALL-2031", start_date=ref, maturity_date=ref + "6Y",
    notional=1_000_000, currency=qs.Currency.USD,
    coupons=[
        qs.Coupon.fixed(0.045),
        qs.Coupon.floating(qs.MarketIndex.SOFR, spread=0.006,
                           floor=0.0, cap=0.06),
    ],
    notional_percentages=[1.0, 0.90, 0.75],
    call_schedule=[
        qs.CallScheduleEntry(qs.Date(2028, 1, 1), 101.0),
        qs.CallScheduleEntry(qs.Date(2029, 1, 1), 100.0),
    ],
)

# The JSON entry is keyed by instrument_type="SteppedCouponBond" and selects
# TreeBased or BackwardFourierEvolution plus a typed HullWhite model.
pricers = qs.PricerConfiguration.from_json("pricers.json")

ctx = qs.PricingContext(quotes=quotes, curves=curves,
                       pricer_configurations=pricers,
                       csa_policy=csa_policy)

with ctx:
    result = ctx.evaluate(
        qs.Trade(callable_bond, ref, pricing_anchor=qs.BondPricingAnchor.dirty_price(101.10)),
        [qs.Request.Value, qs.Request.OAS, qs.Request.Sensitivities],
    )
    print(result.oas)
    print(result.sensitivities)  # curve rows plus <identifier>.OAS
```

The constructor generates coupon dates from `payment_frequency`. Both bond
wrappers accept `calendar`, `business_day_convention`, `date_generation_rule`,
`end_of_month`, and `first_coupon_date`. Stepped bonds additionally accept
`next_to_last_coupon_date` and `termination_date_convention` (defaulting to the
business-day convention). Calendar names or `qs.Calendar` objects and string or
enum conventions are accepted. These fields survive dictionary round trips.

`side`, `pricing_anchor`, `settlement_date`, and `spread_day_counter` belong to
the trade. The Rust instrument builder always builds the contractual cashflows
in the receiving direction; valuation applies trade side once. Defaults for
settlement and spread day count remain the context date and curve convention.

`csa_policy` configures collateral remuneration for curve construction and
native derivative pricing. A foreign-currency leg resolves the corresponding
collateral-adjusted curve. `CsaTerms` remains the netting/XVA input, including
credit, recovery and funding terms; it is distinct from the context's CSA policy.

`SteppedCouponBond` has no contractual `discount_index` argument or dictionary
field. Discounting is selected by the context's fixed-income policy. Its nested
coupon and call entries may be plain native dicts or the existing wrappers:

```python
bond_spec = {
    "identifier": "STEP", "start_date": "2026-02-24",
    "maturity_date": "2031-02-24", "notional": 1_000_000,
    "currency": "USD", "units": 100.0,
    "payment_frequency": "Semiannual", "coupon_day_counter": "Actual365",
    "coupons": [
        {"Fixed": {"rate": 0.045}},
        {"Floating": {"index": "SOFR", "spread": 0.006,
                      "floor": 0.0, "cap": 0.06}},
    ],
    "notional_percentages": [1.0, 0.90, 0.75],
    "call_schedule": [
        {"date": "2028-02-24", "strike": 101.0},
        {"start_date": "2029-02-24", "end_date": "2029-08-24", "strike": 100.0},
    ],
}
bond = qs.SteppedCouponBond.from_dict(bond_spec)
restored = qs.SteppedCouponBond.from_dict(bond.to_dict())
```

`Coupon` wraps existing native `CashflowType<f64>` coupon variants; there is
no additional coupon-definition enum. `Coupon.from_dict` and `Coupon.to_dict`
retain compact `Fixed`/`Floating` constructor shapes. Input also accepts
`FixedRateCoupon`, `FloatingRateCoupon`, and `OptionEmbeddedCoupon` tags.
Python enums remain supported in direct constructors and dictionaries,
including nested configuration dicts. For example, use `qs.Currency.USD`,
`qs.MarketIndex.SOFR`, `qs.Frequency.Semiannual`, and `qs.Request.Value` in
Python-native code; strings remain suitable for data loaded from JSON files.
Calls serialize with `start_date`; `date` remains
an input alias. Omitting `end_date` creates a discrete call. Both window
boundaries are inclusive, and reversed windows or invalid strikes are rejected.
`CallScheduleEntry(date, strike)` remains supported, along with
`CallScheduleEntry.window(start_date, end_date, strike)`, keyword
`start_date`/`end_date` construction, `from_dict`, `to_dict`, `date`,
`start_date`, `end_date`, `strike`, and `contains(date)`.

A call strike excludes contractual cashflows on the same date. Same-day
coupon and amortization are paid, while the issuer call caps the remaining
bond value. There is no exercise-type enum because the product supports issuer
calls only. Yield to maturity and modified duration are intentionally not
defined for `SteppedCouponBond`; use price, OAS, and sensitivities.

With no call entries, use an explicit zero Z-spread anchor on its trade.

### In-process payoff scripting

`ScriptedProduct.from_dict` accepts an inline specification with `events` (a
list of `{"date": "YYYY-MM-DD", "code": "..."}`) and `currency`. Optional
fields are `name` (default `"script"`, with `identifier` as an alias),
`discount_index`, `result_variable`, and `models`. `from_json(path)` and
`to_dict()` use the same schema. Native event field names `event_date`/`script`
are also accepted as aliases for `date`/`code`.

```python
script_spec = {
    "name": "SCRIPTED-NOTE",
    "currency": "USD",
    "result_variable": "note",
    "events": [{
        "date": "2026-02-24",
        "code": 'note = 0; note pays 100 on "2028-02-24";',
    }],
    "models": [{
        "market_index": "SOFR",
        "model": {"Lgm": {
            "lambda": 0.05,
            "parameter_source": {"Fixed": {"sigma": 0.0}},
        }},
    }],
}
product = qs.ScriptedProduct.from_dict(script_spec)
ctx = qs.PricingContext(quotes=quotes, curves=curves, csa_policy=csa_policy)

with ctx:
    values = ctx.evaluate_script(product, n_paths=1000, seed=42)
    inline_values = ctx.evaluate_script(script_spec, n_paths=1000, seed=42)
    print(values["note"])
```

`evaluate_script` requires an initialized context and returns a plain
`dict[str, float]` of path-averaged numeric script variables. `n_paths` must be
positive; using the same market, path count, and seed reproduces results.
`result_variable`, when supplied, selects the native engine's sensitivity
target and must name a numeric variable; the returned dict still includes all
numeric variables. This API returns values, not sensitivity or cashflow tables.
It invokes the native `ScriptEngine` and `LgmMarketModel` directly, without
subprocesses or Cargo.

Omitting `discount_index` uses the context base index for the base currency;
another script currency requires an explicit index. Models resolve per index
from context pricer configurations, then simulation configurations, then
`script_spec["models"]`, with later assignments taking precedence. Models
must be configured for the discount and any requested forward curves. There
is no implicit zero-volatility fallback. This API currently assembles rate
models only, not FX/equity factors or custom correlations.

Both native `Lgm` (`lambda`) and `HullWhite` (`alpha`) schemas are accepted;
Hull-White uses its equivalent LGM representation. `Fixed` sigma is an absolute
short-rate volatility, not a Black quote. For calibration, supply the context's
volatility surfaces/cubes and replace the parameter source with, for example:

```python
parameter_source = {"Calibrated": {
    "source": {"Surface": {"market_index": "SOFR"}},
    "calibration_basket": {"expiries": ["1Y", "2Y"], "strike": "Atm"},
}}
```

Native calibration resolves option prices from the context's market and fits
the sigma schedule. Missing models, curves, calibration data, invalid event
order/dates, and script parse/runtime failures raise Python exceptions.

See [examples/valuation/stepped_coupon_bond.ipynb](examples/valuation/stepped_coupon_bond.ipynb)
for tree/Fourier pricing and [examples/scripting/scripted_cashflows.ipynb](examples/scripting/scripted_cashflows.ipynb)
for the native-versus-scripted comparison.

### Extending the library in Python

The Python module keeps the same separation as the Rust API: an instrument
contains product terms, a trade represents a position, a pricer contains the
valuation method, a market-data provider supplies the market state, and an
evaluator dispatches by trade type. These are extension points, not only names
for built-in Rust implementations.

The base classes are optional conveniences. Registration uses structural
typing, so ordinary Python classes and dataclasses work too.

```python
class ZeroCoupon(qs.Instrument):
    def __init__(self, identifier, maturity, amount, discount_index):
        super().__init__(identifier)
        self.maturity = maturity
        self.amount = amount
        self.discount_index = discount_index


class ZeroCouponTrade(qs.Trade):
    pass


class ZeroCouponPricer(qs.Pricer):
    def evaluate(self, trade, requests, context):
        instrument = trade.instrument
        discount = context.curve(instrument.discount_index)
        value = instrument.amount * discount.discount_factor(instrument.maturity)
        return qs.EvaluationResults(
            context.evaluation_date,
            instrument.identifier,
            price=value if qs.Request.Value in requests else None,
        )


note = ZeroCoupon("NOTE-1Y", ref + "1Y", 1_000_000, qs.MarketIndex.SOFR)
trade = ZeroCouponTrade(note, ref)

# Context registration participates in the normal ctx.evaluate workflow.
ctx.register_pricer(ZeroCouponTrade, ZeroCouponPricer())
with ctx:
    result = ctx.evaluate(trade, [qs.Request.Value])
    print(result.price)
```

Python registrations are resolved by exact trade type first and then by compatible
base type. They take precedence over built-in dispatch, so a trade holding a
native instrument can use a custom pricer without modifying the Rust binding. Use
`ctx.unregister_pricer(TradeType)` to restore built-in dispatch.
Native instruments and `Trade` are subclassable. An unregistered `Trade`
subclass holding a native instrument keeps native pricing. Register the trade
subclass, not the instrument class, to select a Python implementation.

For a heterogeneous collection or a custom market-data provider, use the
standalone evaluator:

```python
evaluator = qs.Evaluator()
evaluator.register(ZeroCouponTrade, ZeroCouponPricer())
result = evaluator.evaluate(trade, provider, [qs.Request.Value])
```

`provider` may be an initialized `PricingContext`, a subclass of
`MarketDataProvider`, or any Python object offering the methods required by the
registered pricer. `PricingContext` exposes `quotes`, `fixings`, `fx`,
`base_currency`, `base_index`, curves, volatility objects, and simulations.
Raw stores support `QuoteStore.value`, `FixingStore.fixing`, and `FxStore.rate`.

### XVA

The engine configuration covers only the simulation setup (models, paths,
seed, frequency). Collateral treatment and credit/funding conditions are per
client: each `NettingSet` carries its own `CsaTerms`.

```python
config = qs.XvaConfig.from_json("xva_config.json")

csa_a = qs.CsaTerms(collateral_index="SOFR", collateral_currency="USD",
                    credit_spread=0.010, recovery=0.40, funding_spread=0.005)
funding_curve = qs.TenorBasedZeroRateTermStructure(
    ref, ["0D", "1Y", "3Y"], [0.004, 0.005, 0.006],
    enable_extrapolation=True,
)
csa_b = qs.CsaTerms(collateral_index="SOFR", collateral_currency="USD",
                    credit_spread=0.015, recovery=0.40,
                    funding_spread_curve=funding_curve,
                    variation_margin=True, counterparty_threshold=1000.0,
                    own_threshold=1000.0, minimum_transfer_amount=500.0)

ctx = qs.PricingContext(quotes=quotes, curves=curves, fx=fx, csa_policy=csa_policy)

with ctx:
    result = ctx.run_xva(config, netting_sets=[
        qs.NettingSet("clientA", [qs.Trade(swap, ref)], csa_a),
        qs.NettingSet("clientB", [qs.Trade(xccy, ref)], csa_b),
    ])
    print(result.xva_values)      # DataFrame: netting_set / measure / value
    print(result.sensitivities)   # DataFrame: parameter / value
    for p in result.exposures:    # per-netting-set EPE/ENE/EE profiles
        print(p.netting_set)
        print(p.to_dataframe())   # DataFrame: date / epe / ene / ee
```

See [examples/cva/xva.ipynb](examples/cva/xva.ipynb) for a complete netting comparison
and [the examples guide](examples/README.md) for the full learning sequence.

`ZeroRateTermStructure(ref, dates, rates)` and
`TenorBasedZeroRateTermStructure(ref, tenors, rates)` are reusable curve objects.
Both expose `zero_rate`, `discount_factor`, and `forward_rate`, plus their
time-based variants and a `to_dict()` export of the equivalent dated curve.
The dated class also supports `from_dict()` and `from_json()`.
Keyword options select `day_counter`, `compounding`, `frequency`, `interpolator`,
and `enable_extrapolation`. Defaults are Actual365, Continuous, Annual, Linear,
and disabled extrapolation.

`CsaTerms.funding_spread_curve` accepts either class (or a dated curve dictionary)
and returns a `ZeroRateTermStructure`. Its rates are annualized spread quotes;
FVA uses the curve's reference date, day counter, interpolation, and extrapolation
settings. `funding_index` can reuse a bootstrapped funding curve, with the
explicit spread curve as an optional additive overlay. There is no separate
`FundingSpreadCurve` type or list-of-pairs constructor. Serialized spread curves
now use the zero-curve schema (`reference_date`, `dates`, `rates`, `rate_definition`,
`interpolator`, `enable_extrapolation`), replacing the old `dates`/`spreads` schema.

## Focused validation

After rebuilding the wheel, run `python -m unittest discover -s
bindings/python/tests`. The in-process Rust harness also tests the freshly
compiled PyO3 module without relying on an installed wheel:

```text
cargo check -p quantsupport-py
cargo test -p quantsupport-py python_dict_and_script_contracts -- --nocapture
```

On Windows, the selected interpreter's directory and runtime DLL directories
must be on `PATH` for Rust test linking/loading. For Conda, this typically
includes the environment root and `Library/bin`. Set `PYO3_PYTHON` to the
selected interpreter when Cargo cannot discover it. Do not enable
`extension-module` for Rust tests; wheel builds enable it separately.

## API surface (v1)

Enums (typed classes; every argument also accepts the string name, and each
enum has a case-insensitive `parse`):

- `Currency` — with ISO metadata (`code`, `name`, `symbol`, `precision`, `numeric_code`)
- `MarketIndex` — rate-index constants (`MarketIndex.SOFR`, ...) plus factories
  `equity(name)`, `fx_pair(base, quote)`, `collateral(base, quote)`, `other(name)`
- `Side`, `Compounding`, `Frequency`, `DayCounter`, `TimeUnit`,
  `BusinessDayConvention`, `Request`, `VolatilityType`, `SmileType`

Time:

- `Date(y, m, d)` / `Date.parse(s)` — arithmetic with days, `Period` or period
  strings (`date + "6M"`), `weekday()`, `end_of_month()`, `to_datetime()`
- `Period(n, TimeUnit)` / `Period.parse("1Y6M")` / `Period.from_frequency(f)`
- `Calendar(name)` — `is_business_day`, `adjust`, `advance`,
  `business_days_between`, `holiday_list`, `business_day_list`
- `DayCounter.year_fraction(start, end)` / `day_count(start, end)`

Market data & configuration (each with `from_dict` / `from_json`, plus
`to_dict` on configurations):

- `QuoteStore` — quotes with a reference date (`reference_date`,
  `identifiers()`, `value()`, `levels()`, `to_dataframe()`)
- `FixingStore` — historical index fixings (`add()`, `fixing()`,
  `to_dataframe()`)
- `FxStore` — FX spot rates (`add()`, `rate()`)
- `CurveConfiguration`, `VolatilitySurfaceConfiguration`,
  `VolatilityCubeConfiguration`, `PricerConfiguration`,
  `SimulationConfiguration`
- `SingleCurveCSADiscountPolicy(collateral_currency, collateral_index)` — collateral policy for curve construction and derivative valuation
- `FixedIncomeDiscountPolicy(risk_free_indices, prefer_instrument_index=True)` — per-currency bond discount curves

Pricing:

- `PricingContext(quotes, curves, fixings=None, fx=None, volatility_surfaces=None, volatility_cubes=None, pricer_configurations=None, simulations=None, csa_policy=None, fixed_income_policy=None, scenarios=None)` — context manager
- inputs: `ctx.reference_date` / `ctx.evaluation_date`, `ctx.quotes`,
  `ctx.fixings`, `ctx.fx`, `ctx.base_currency`, `ctx.base_index`,
  `ctx.curve_configurations`,
  `ctx.volatility_surface_configurations`, `ctx.volatility_cube_configurations`,
  `ctx.simulation_configurations`
- constructed outputs (inside the `with` block, or after an explicit
  `initialize()` when using the context without one):
  - `ctx.curves()` / `ctx.curve(index)` → `DiscountCurve`
    (`nodes()`, `pillars()`, `discount_factor(date)`, `forward_rate(...)`)
  - `ctx.volatility_surfaces()` / `ctx.volatility_surface(index)` → `VolatilitySurface`
  - `ctx.volatility_cubes()` / `ctx.volatility_cube(index)` → `VolatilityCube`
  - `ctx.simulations()` / `ctx.simulation(index)` → `Simulation`
    (`dates()`, `n_paths`, `dt`, `paths()` and `calibration_diagnostics()` DataFrames)
- `Swap`, `CrossCurrencySwap`, `FixedRateBond`, `SteppedCouponBond` — instrument
    specifications with typed getters; callable stepped bonds use `Coupon` and
  explicit `CallScheduleEntry(date, strike)` terms
- `ctx.evaluate(trade, requests)` → `EvaluationResults` (`price`, `yield_to_maturity`, `modified_duration`, `z_spread`, `oas`, `fair_rate`, `sensitivities` DataFrame, `cashflows` DataFrame)

- extensibility: `Instrument`, `Trade`, `Pricer`, `MarketDataProvider`,
  `Evaluator`; `ctx.register_pricer(type, pricer)`,
  `ctx.unregister_pricer(type)`, `ctx.pricer_for(trade)`
- Python pricer output: construct `EvaluationResults(evaluation_date,
  identifier, price=..., sensitivities=..., cashflows=...)`

XVA:

- `XvaConfig` — simulation/model setup only
- `CsaTerms` — per-client collateral treatment + credit/funding parameters
- `NettingSet(name, trades, csa)` — one client's trades under its CSA
- `ctx.run_xva(config, netting_sets)` → `XvaResult` (`xva_values` DataFrame, `sensitivities` DataFrame, `exposures`)
- `QuantSupportError` — exception raised on library errors

All JSON schemas match the Rust examples in `examples/*/data/`. New products
and pricing methods can be implemented and registered entirely in Python;
adding a native binding in `bindings/python/src/trades.rs` remains available
when Rust-level performance or integration is required.

### Bond API migration

Bond `Trade` constructors require `pricing_anchor=qs.BondPricingAnchor.z_spread(0.0)`
for an explicitly unspread straight bond, or `qs.BondPricingAnchor.OAS(0.0)`
for an issuer-call bond. Pass a price anchor to imply the appropriate spread.
Remove position metadata from product constructors and wrap each product in
`qs.Trade(instrument, trade_date, side, pricing_anchor=...)` before valuation.
The `bond_anchor` evaluation argument has been removed. Replace the former
`discounting`/`DiscountingConfig` context input with `csa_policy` and
`SingleCurveCSADiscountPolicy(collateral_currency=..., collateral_index=...)`.

