Source code for jactus.observers.scenario

"""Scenario management for ACTUS contract simulation.

A ``Scenario`` bundles together all the risk factor observers (both market
and behavioral) needed for a simulation run into a single, named,
reusable configuration.

This provides a higher-level abstraction over individual observers,
enabling:
- Named, reusable simulation configurations
- Consistent bundling of market data with behavioral models
- Easy scenario comparison (base case vs. stress scenarios)

The scenario automatically composes its market and behavioral observers
using a ``CompositeRiskFactorObserver`` so that a single unified observer
can be passed to the simulation engine.

References:
    ACTUS Risk Service v2.0 - Scenario API
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import TYPE_CHECKING

from jactus.observers.behavioral import BehaviorRiskFactorObserver, CalloutEvent
from jactus.observers.risk_factor import CompositeRiskFactorObserver, RiskFactorObserver

if TYPE_CHECKING:
    from jactus.core import ContractAttributes


[docs] @dataclass class Scenario: """A named simulation scenario bundling market and behavioral observers. A scenario acts as a simulation environment, declaring which market data sources and behavioral models are available. It provides a unified risk factor observer that the simulation engine can use directly. Attributes: scenario_id: Unique identifier for this scenario. description: Human-readable description. market_observers: Dictionary mapping identifiers to market risk factor observers. These handle pure market data lookups (time series, curves, constants). behavior_observers: Dictionary mapping identifiers to behavioral risk factor observers. These are state-aware and can inject callout events into the simulation timeline. Example: >>> from jactus.observers import ( ... ConstantRiskFactorObserver, ... TimeSeriesRiskFactorObserver, ... ) >>> from jactus.observers.prepayment import PrepaymentSurfaceObserver >>> >>> scenario = Scenario( ... scenario_id="base-case", ... description="Base case with 5Y UST falling and moderate prepayment", ... market_observers={ ... "rates": TimeSeriesRiskFactorObserver({ ... "UST-5Y": [ ... (ActusDateTime(2024, 1, 1), 0.045), ... (ActusDateTime(2025, 1, 1), 0.035), ... ], ... }), ... }, ... behavior_observers={ ... "prepayment": prepayment_observer, ... }, ... ) >>> # Get unified observer for simulation >>> observer = scenario.get_observer() >>> contract.simulate(risk_factor_observer=observer) """ scenario_id: str description: str = "" market_observers: dict[str, RiskFactorObserver] = field(default_factory=dict) behavior_observers: dict[str, BehaviorRiskFactorObserver] = field(default_factory=dict)
[docs] def get_observer(self) -> RiskFactorObserver: """Get a unified risk factor observer that combines all market observers. Returns a ``CompositeRiskFactorObserver`` that chains all market observers in order, providing a single observer for the simulation engine. If only one market observer is configured, it is returned directly. If no market observers are configured, raises ValueError. Returns: Unified RiskFactorObserver. Raises: ValueError: If no market observers are configured. """ observers = list(self.market_observers.values()) if not observers: raise ValueError(f"Scenario '{self.scenario_id}' has no market observers configured") if len(observers) == 1: return observers[0] return CompositeRiskFactorObserver(observers, name=f"Scenario({self.scenario_id})")
[docs] def get_callout_events( self, attributes: ContractAttributes, ) -> list[CalloutEvent]: """Collect callout events from all behavioral observers. Calls ``contract_start()`` on each behavioral observer and aggregates the returned callout events. Args: attributes: Contract attributes. Returns: Combined list of callout events from all behavioral models, sorted by time. """ all_events: list[CalloutEvent] = [] for observer in self.behavior_observers.values(): events = observer.contract_start(attributes) all_events.extend(events) return sorted(all_events, key=lambda e: e.time)
[docs] def add_market_observer(self, identifier: str, observer: RiskFactorObserver) -> None: """Add or replace a market risk factor observer. Args: identifier: Observer identifier. observer: Market risk factor observer. """ self.market_observers[identifier] = observer
[docs] def add_behavior_observer(self, identifier: str, observer: BehaviorRiskFactorObserver) -> None: """Add or replace a behavioral risk factor observer. Args: identifier: Observer identifier. observer: Behavioral risk factor observer. """ self.behavior_observers[identifier] = observer
[docs] def list_risk_factors(self) -> dict[str, str]: """List all configured risk factor sources. Returns: Dictionary mapping identifiers to their observer type names. """ result: dict[str, str] = {} for name, obs in self.market_observers.items(): result[name] = type(obs).__name__ for name, obs in self.behavior_observers.items(): result[name] = type(obs).__name__ return result