Risk, netting, and extensibility¶
Sensitivity labels define the experiment¶
A sensitivity measures how a valuation changes locally with one market input. Under a bootstrapped market, that input can be a deposit, swap or volatility quote. For a direct zero curve, it is the original zero-rate quote. A bond spread can also be registered as an independent input.
The pillar label identifies the input, and its units determine how to scale the derivative into a monetary change. Check those units and the valuation currency before aggregating a portfolio risk report.
For a small input move,
Decimal-rate derivatives become approximate +1 bp currency changes by multiplication by \(10^{-4}\). A lognormal volatility quoted as a decimal typically moves by 0.01 for one absolute volatility percentage point; a futures price uses price points instead. Neither conversion should be applied to every row automatically. Curve sensitivities also include numerical calibration differentiation. Compare selected derivatives with full revaluation to assess that local approximation.
The following check reuses context and trade from
A position, a market, and a valuation. It shifts both SOFR zero-rate quotes and
compares their summed derivative with central full revaluation.
import math
h = 0.00001
up = qs.Scenario(shocks=[qs.Shock(
target="SOFR", value=h, shock_type=qs.ScenarioType.Absolute,
)])
down = qs.Scenario(shocks=[qs.Shock(
target="SOFR", value=-h, shock_type=qs.ScenarioType.Absolute,
)])
with context:
base_result = context.evaluate(trade, ["Value", "Sensitivities"])
parallel_derivative = base_result.sensitivities["value"].sum()
with up:
up_value = context.evaluate(trade).price
with down:
down_value = context.evaluate(trade).price
restored_value = context.evaluate(trade).price
finite_difference = (up_value - down_value) / (2 * h)
assert math.isclose(parallel_derivative, finite_difference, rel_tol=2e-5)
assert math.isclose(restored_value, base_result.price, abs_tol=1e-8)
print("Approximate +1bp change:", parallel_derivative * 0.0001)
Enter a context before its Scenario scope. The innermost active context
supplies the scenario’s base market. Requested shocked objects and their
dependencies are constructed in an independent cache. Nested scenarios compose
in entry order; exit restores the parent market, including after exceptions.
The same inactive scenario can be reused with another context. Do not retain a
shocked AD-backed market view after its scope ends.
An Absolute shock adds a value in the target’s units; a Relative shock
multiplies by 1 + value. Targets use case-insensitive set membership of
underscore-separated tokens after an initial identifier-equality check.
Token order and multiplicity are ignored: a full identifier is
not an exact-only selector.
Repeated tenor/expiry tokens can make one full caplet ID select another expiry.
Overlapping shocks then compose in order without deduplication, potentially
moving one quote twice.
Use one unambiguous family selector when one family-wide move is intended;
for example CapletFloorlet_USD_SOFR rather than one shock per full caplet
ID. For a quoted funding spread curve, FUNDINGSPREAD selects its
zero-rate quotes; appending a pillar narrows the token selector but does not enable an
exact-only mode. Inspect every transformed quote, including
unexpected matches, and verify each intended input changes once before
interpreting a stress as a parallel market move. An aggregate NPV difference
alone cannot reveal a duplicated or unintended input move.
Portfolio aggregation¶
Start with How a portfolio works for trade membership, position sizes, request rules and automatic currency conversion. It reconciles aggregate PV, sensitivities and payments against individual trades and checks rate risk by full revaluation.
The portfolio aggregation notebook
uses context.evaluate(qs.Portfolio("PORTFOLIO", trades), requests)
and returns standard EvaluationResults. Its six-position deposit/swap book
matches the Excel portfolio workbook. It reconciles PV, every sensitivity and
payment-date totals against individual evaluations, then optionally exports
five tables as CSV.
Portfolio stores only the ordered trade membership. Context dispatch selects
Rust’s PortfolioPricer, which implements the standard Pricer interface
for Value, Sensitivities and Cashflows and delegates each member’s valuation
to its own pricer.
Native Rust pricers convert monetary results to the context base currency using its FX store, including FX sensitivities, before portfolio aggregation. Cashflow reports keep their original currencies.
result.coupons preserves coupon types, accrual dates, contractual and
effective fixing indices, signed notionals and compatible weighted rates or
spreads. The two deposits combine to 400,000 at 5.5%, while SOFR and TermSOFR3m
coupons and opposing directions remain separate. result.cashflow_summary
reports payment-date interest, capital, other and total amounts, without
allocating interest to accrual periods. Numerical aggregation follows each
trade’s differentiation, avoiding a combined portfolio AD graph.
Value each explicit Trade under the same market assumptions, then aggregate
compatible monetary values and risk labels. Two opposite positions on the
same contract cancel only after side and size are applied; raw unsigned
cashflow amounts need not cancel. A sum of native-currency cashflows is not a
reporting-currency portfolio PV.
EvaluationResults.sensitivities aggregates duplicate labels and sorts
them; raw_sensitivities retains the original pairs. For a portfolio, carry
trade identifiers alongside individual tables before grouping by pillar.
Keep model/input units in the report, and compare the aggregated first-order
prediction with a common full-revaluation scenario. Large shocks, options,
calibration changes, and exercise boundaries make linear predictions less
reliable.
Exposure profiles and netting¶
NettingSet groups trade positions under one client’s CsaTerms.
Supported Python contingent-claim conversion currently covers Swap,
CrossCurrencySwap, BasisSwap, FixFloatCrossCurrencySwap,
FixedRateBond, FloatingRateNote, and FixedRateDeposit. It does not
accept every product that ordinary context evaluation can price; loans,
callable bonds, options, and custom Python pricers are not automatically XVA
claim implementations.
For path value \(V_p(t_i)\), the stored profiles are pointwise means:
ENE is signed and nonpositive. These profiles are neither PFE quantiles nor time averages. Their values are in the engine’s base currency as of each future simulation date, before variation margin; they are not already discounted adjustment PVs.
The example below groups the swap into a netting set and runs an XVA calculation with variation margin. Its eight paths keep the example quick; increase the path count and compare seeds for a numerical study.
xva_configuration = qs.XvaConfig.from_dict({
"model_configs": [{
"market_index": "SOFR", "lambda": 0.05,
"parameter_source": {"Fixed": {"sigma": 0.005}},
}],
"n_paths": 8, "seed": 42, "frequency": "Quarterly",
})
terms = qs.CsaTerms(
collateral_index="SOFR", collateral_currency="USD",
credit_spread=0.01, recovery=0.40, funding_spread=0.005,
variation_margin=True, counterparty_threshold=1000,
own_threshold=1000, minimum_transfer_amount=500,
)
agreement = qs.NettingSet("CLIENT-A", [trade], terms)
with context:
adjustments = context.run_xva(xva_configuration, [agreement])
print(adjustments.xva_values)
profile = adjustments.exposures[0].to_dataframe()
assert (profile["ee"] - profile["epe"] - profile["ene"]).abs().max() < 1e-8
High-level run_xva constructs CVA and FVA aggregators. Native DVA support is
a lower-level aggregator capability, not an additional high-level Python
result promised by this call. With flat credit spread \(s_c\) and recovery
\(R\), the flat hazard approximation is \(\lambda=s_c/(1-R)\), not
\(\lambda=s_c\). A configured credit curve provides a different, explicitly
bootstrapped survival input.
FVA integrates signed path NPV, not EPE alone. Its right-endpoint time increments use ACT/365; optional system discounting and variation-margin treatment apply in the aggregator. Schematically,
where \(\widetilde V\) denotes the aggregator’s margin-treated value.
Variation margin affects adjustment aggregation, not the stored pre-margin
exposure profiles. An explicit funding-spread zero curve can overlay a funding
index where configured; it is not a separate FundingSpreadCurve class.
Adjustment risk needs an additional coordinate check. With credit_index,
the engine snapshots numeric survival nodes and their labels, then the CVA
factory creates independent survival-probability leaves. A CVA.<label>
row therefore measures \(\partial\mathrm{XVA}/\partial S(T_k)\), not a
derivative with respect to the original CDS spread, even when the label looks
like a CDS quote identifier. No credit-bootstrap Jacobian is replayed along
this aggregator path.
Likewise, curve-driven FVA creates independent derived funding-spread leaves,
prefixed FVA.; it does not replay the original funding-curve calibration.
These are spread-pillar coordinates, not original funding-quote risk. Flat
credit_spread and funding_spread parameters instead retain their own
decimal-rate units. Do not scale a survival-probability derivative as though it
were a CDS-spread DV01. Ordinary deterministic CDS pricing is a different
differentiation graph and must be interpreted separately.
System/base discount factors supplied to the adjustment aggregators are numerical snapshots held fixed in their weights; this contribution is not differentiated system-curve risk. Calibrated sigma also omits curve/strike recalibration terms. Differentiable model-backed paths do not restore those omitted dependencies. Interpret the returned coordinates as partial risks and check proposed hedges by full market revaluation, rather than treating a quote-like label as a complete recalibrated market-quote derivative.
Thresholds, minimum transfer amount, recovery, credit/funding indices, and the model/path/date grid are economic and numerical assumptions. Netting-set names must be unique in one run. Fixed seeds allow reproducible comparisons. Vary path counts and seeds to assess whether a client-level change exceeds sampling variation. The XVA study develops the comparison.
Extending valuation in Python¶
The binding exposes extension interfaces rather than requiring new native
code for every product. Instrument describes terms, Trade a position,
Pricer the valuation method, MarketDataProvider the required state, and
Evaluator dispatch. Structural registration also permits ordinary Python
classes and dataclasses. A custom implementation owns its treatment of side,
position size, requests, units, and cashflow filtering.
This example implements a deterministic USD receipt using the same market as the first valuation.
class Receipt(qs.Instrument):
def __init__(self, identifier, maturity, amount):
super().__init__(identifier)
self.maturity = maturity
self.amount = amount
class ReceiptTrade(qs.Trade):
pass
class ReceiptPricer(qs.Pricer):
def evaluate(self, trade, requests, context):
instrument = trade.instrument
size = trade.notional if trade.notional is not None else 1.0
value = (
instrument.amount * size * trade.side.sign()
* context.curve("SOFR").discount_factor(instrument.maturity)
)
return qs.EvaluationResults(
context.evaluation_date, instrument.identifier,
price=value if qs.Request.Value in requests else None,
)
receipt = Receipt("RECEIPT", reference + "1Y", 100)
receipt_trade = ReceiptTrade(receipt, reference)
context.register_pricer(ReceiptTrade, ReceiptPricer())
with context:
custom_result = context.evaluate(receipt_trade, ["Value"])
print(custom_result.price)
context.unregister_pricer(ReceiptTrade)
Registration selects the exact trade type first and then compatible bases;
Python registrations override built-in dispatch. Register the trade type, not
the contractual instrument class. An unregistered trade subclass containing a
native instrument still uses native pricing. A standalone Evaluator can
instead use a custom provider; it is not necessary to subclass every supplied
base interface.
The custom pricer returns Value. Additional measures require their own implementation and validation: arbitrary Python arithmetic is outside native AD, and native XVA requires a supported claim conversion.