Source code for qis.portfolio.stress.instruments
"""Public instrument terms and the read-only payoff extension context."""
from dataclasses import dataclass
from enum import Enum
from typing import Protocol, runtime_checkable
import numpy as np
import pandas as pd
[docs]
class InstrumentType(Enum):
"""Supported primitive payoffs.
Attributes:
DELTA_1: Funded exposure with an exponential response.
CALL: Signed intrinsic call payoff.
PUT: Signed intrinsic put payoff.
FUTURE: Change in futures quote times signed units and multiplier.
"""
DELTA_1 = "delta_1"
CALL = "call"
PUT = "put"
FUTURE = "future"
[docs]
class ResponseBasis(Enum):
"""Currency basis of the fitted response.
Attributes:
REFERENCE: Response includes conversion into the portfolio reference currency.
LOCAL: Response describes only the quote in its own currency.
"""
REFERENCE = "reference"
LOCAL = "local"
[docs]
class KinkPolicy(Enum):
"""One-sided quote derivative used consistently across a holding's legs.
The policy binds only when the quote at which the derivative is taken equals a leg's strike
exactly, by floating-point equality with no tolerance. A quote any distance from the strike,
however small, uses the ordinary slope: one for a call above its strike and zero below, and
the call slope minus one for a put. Current risk takes the derivative at the baseline quote;
scenario-local bands take it at each scenario quote.
Attributes:
LEFT: Derivative approached from lower quotes.
RIGHT: Derivative approached from higher quotes.
MIDPOINT: Average of left and right derivatives.
"""
LEFT = "left"
RIGHT = "right"
MIDPOINT = "midpoint"
[docs]
@dataclass(frozen=True)
class Underlying:
"""An actual quote with a possibly shared fitted response.
Attributes:
quote_id: Unique actual quote identifier, independent of the response proxy.
spot0: Positive baseline quote in quote-currency units.
currency: Quote currency.
response_id: RiskModel response row, or None for explicitly deterministic cash.
response_basis: Whether the fitted response includes reference-currency FX.
"""
quote_id: str
spot0: float
currency: str
response_id: str | None
response_basis: ResponseBasis
def __post_init__(self):
"""Reject ambiguous or unsupported quote terms."""
if not self.quote_id or not self.currency:
raise ValueError("quote_id and currency must be nonempty")
if not np.isfinite(self.spot0) or self.spot0 <= 0:
raise ValueError("spot0 must be positive for multiplicative quote shocks")
if self.response_id is not None and not self.response_id:
raise ValueError("response_id must be nonempty or explicitly None")
if not isinstance(self.response_basis, ResponseBasis):
raise ValueError("response_basis must be a ResponseBasis")
[docs]
@dataclass(frozen=True)
class InstrumentLeg:
"""A signed primitive valued in its underlying's quote currency.
Attributes:
instrument_type: Primitive enum.
underlying_id: Actual quote ID in the portfolio registry.
quantity: Signed number of units; negative means short.
multiplier: Positive contract multiplier.
strike: Nonnegative option strike; absent for funded assets and futures.
"""
instrument_type: InstrumentType
underlying_id: str
quantity: float
multiplier: float = 1.0
strike: float | None = None
def __post_init__(self):
"""Validate contract terms before any scenario is evaluated."""
if not isinstance(self.instrument_type, InstrumentType):
raise ValueError("instrument_type must be an InstrumentType")
if not self.underlying_id or not np.isfinite(self.quantity):
raise ValueError("underlying_id and finite signed quantity are required")
if not np.isfinite(self.multiplier) or self.multiplier <= 0:
raise ValueError("multiplier must be positive and finite")
option = self.instrument_type in (InstrumentType.CALL, InstrumentType.PUT)
if option:
if self.strike is None or not np.isfinite(self.strike) or self.strike < 0:
raise ValueError("options require a nonnegative finite strike")
elif self.strike is not None:
raise ValueError("strike is only valid for calls and puts")
[docs]
def get_payoff(self, quotes: pd.Series, spot0: float) -> pd.Series:
"""Evaluate signed model value locally, before currency conversion.
Args:
quotes: Scenario quotes indexed by scenario ID.
spot0: Baseline quote, used as the futures reference level.
Returns:
Local-currency model values with the supplied index.
"""
if not np.isfinite(quotes.to_numpy(dtype=float)).all() or (quotes <= 0).any():
raise ValueError("scenario quotes must be positive and finite")
if self.instrument_type is InstrumentType.CALL:
value = (quotes - self.strike).clip(lower=0)
elif self.instrument_type is InstrumentType.PUT:
value = (self.strike - quotes).clip(lower=0)
elif self.instrument_type is InstrumentType.FUTURE:
value = quotes - spot0
else:
value = quotes
return self.quantity * self.multiplier * value
[docs]
def get_quote_delta(self, spot0: float, kink_policy: KinkPolicy) -> float:
"""Return current local derivative with respect to the actual quote.
For an option the call slope is one above the strike and zero below; the put slope is
the call slope minus one. ``kink_policy`` sets the call slope only when ``spot0 ==
strike`` exactly, with no tolerance.
Args:
spot0: Quote at which the derivative is taken: the baseline quote for current risk,
or a scenario quote for scenario-local bands.
kink_policy: Common one-sided derivative policy for this holding.
Returns:
Signed units including multiplier and intrinsic exercise state.
"""
if not isinstance(kink_policy, KinkPolicy):
raise ValueError("kink_policy must be a KinkPolicy")
slope = 1.0
if self.instrument_type in (InstrumentType.CALL, InstrumentType.PUT):
call_slope = float(spot0 > self.strike)
if spot0 == self.strike:
call_slope = {
KinkPolicy.LEFT: 0.0,
KinkPolicy.RIGHT: 1.0,
KinkPolicy.MIDPOINT: 0.5,
}[kink_policy]
slope = call_slope if self.instrument_type is InstrumentType.CALL else call_slope - 1.0
return float(self.quantity * self.multiplier * slope)
[docs]
class PayoffContext:
"""Read-only labelled market view for public composite payoff implementations.
DataFrame/Series properties return defensive copies. Quotes and FX include only
the supplied scenario rows; baseline values are supplied separately.
Attributes:
quote_currencies: Quote-currency code for each actual quote.
response_log_shocks: Scenario-by-shared-response log returns.
quotes: Scenario-by-actual-quote local prices.
baseline_quotes: Actual quote baselines.
fx_rates: Scenario FX, in reference currency per unit of local currency.
baseline_fx_rates: Baseline reference/local currency conversions.
factor_log_shocks: Fully resolved scenario-by-factor log shocks.
quote_response_jacobian: Derivative of each local log quote by shared response.
fx_response_jacobian: Derivative of each log FX conversion by shared response.
reference_currency: Portfolio value currency.
"""
[docs]
def __init__(
self,
quotes: pd.DataFrame,
baseline_quotes: pd.Series,
fx_rates: pd.DataFrame,
baseline_fx_rates: pd.Series,
factor_log_shocks: pd.DataFrame,
quote_response_jacobian: pd.DataFrame,
fx_response_jacobian: pd.DataFrame,
reference_currency: str,
quote_currencies: pd.Series,
response_log_shocks: pd.DataFrame,
):
"""Copy the market view so custom payoffs cannot alter shared evaluations."""
self._quote_currencies = quote_currencies.copy(deep=True)
self._response_log_shocks = response_log_shocks.copy(deep=True)
self._quotes = quotes.copy(deep=True)
self._baseline_quotes = baseline_quotes.copy(deep=True)
self._fx_rates = fx_rates.copy(deep=True)
self._baseline_fx_rates = baseline_fx_rates.copy(deep=True)
self._factor_log_shocks = factor_log_shocks.copy(deep=True)
self._quote_response_jacobian = quote_response_jacobian.copy(deep=True)
self._fx_response_jacobian = fx_response_jacobian.copy(deep=True)
self._reference_currency = reference_currency
@property
def quote_currencies(self) -> pd.Series:
"""Return each actual quote's currency."""
return self._quote_currencies.copy(deep=True)
@property
def response_log_shocks(self) -> pd.DataFrame:
"""Return shared fitted response log returns."""
return self._response_log_shocks.copy(deep=True)
@property
def quotes(self) -> pd.DataFrame:
"""Return local scenario quotes."""
return self._quotes.copy(deep=True)
@property
def baseline_quotes(self) -> pd.Series:
"""Return current local quotes."""
return self._baseline_quotes.copy(deep=True)
@property
def fx_rates(self) -> pd.DataFrame:
"""Return scenario reference-per-local FX rates."""
return self._fx_rates.copy(deep=True)
@property
def baseline_fx_rates(self) -> pd.Series:
"""Return baseline reference-per-local FX rates."""
return self._baseline_fx_rates.copy(deep=True)
@property
def factor_log_shocks(self) -> pd.DataFrame:
"""Return resolved factor log shocks."""
return self._factor_log_shocks.copy(deep=True)
@property
def quote_response_jacobian(self) -> pd.DataFrame:
"""Return local log-quote sensitivities to shared log responses."""
return self._quote_response_jacobian.copy(deep=True)
@property
def fx_response_jacobian(self) -> pd.DataFrame:
"""Return log-FX sensitivities to shared log responses."""
return self._fx_response_jacobian.copy(deep=True)
@property
def reference_currency(self) -> str:
"""Return the portfolio reference currency."""
return self._reference_currency
[docs]
@runtime_checkable
class HoldingPayoff(Protocol):
"""Public extension for a composite valued in reference currency.
Attributes:
implementation_id: Stable implementation/version identifier for the audit.
coverage: Plain-language payoff approximation and omitted contract states.
boundary_policy: Declared current derivative convention at discontinuities.
For conditional bands at nonzero shocks, also implement the optional
scenario_response_jacobian(context) method. It reads the single scenario row
from context.quotes and context.fx_rates, while baseline properties retain
the original valuation references. Return shared-response dollar derivatives
with the same boundary policy. Baseline-only composites remain valid for
valuation/current risk; requesting stressed risk without the method fails
explicitly. Disable StressTestConfig.ordinary_asset_bands for those consumers.
"""
implementation_id: str
coverage: str
boundary_policy: str
[docs]
def evaluate(self, context: PayoffContext) -> pd.Series:
"""Return scenario model payoff values in reference currency.
Args:
context: Public quotes, FX and mappings; no private engine imports.
Returns:
Finite Series with exactly the scenario index.
"""
...
[docs]
def response_jacobian(self, context: PayoffContext) -> pd.Series:
"""Return current dollar sensitivities indexed by shared response ID.
Args:
context: Zero-shock market view, with the shared response mappings.
Returns:
Finite labelled derivative, including zero for every unused response.
If unavailable, raise NotImplementedError; full risk analysis fails
explicitly while direct payoff valuation remains usable.
"""
...