"""Deterministic synthetic datasets for research-style notebooks."""

from __future__ import annotations

from dataclasses import dataclass, replace
from decimal import Decimal
from pathlib import Path

import numpy as np
import pandas as pd

from fuggers_py.bonds import (
    BondIndex,
    CallType,
    CallableBondBuilder,
    FixedBond,
    FixedBondBuilder,
    FloatingRateNoteBuilder,
    IndexConventions,
    IndexFixingStore,
    OvernightCompounding,
    RateIndex as BondRateIndex,
    Tenor,
    YieldCalculationRules,
)
from fuggers_py.bonds.types import ASWType
from fuggers_py.credit import CdsQuote, CreditDefaultSwap
from fuggers_py.core import Currency, Date, Frequency
from fuggers_py.curves import (
    DiscountCurveBuilder,
    ExponentialSplineCurveModel,
    FittedBondCurveFitter,
    FittedBondFitConfig,
    FittedBondFitResult,
    FittedBondObservation,
    MultiCurveEnvironmentBuilder,
    RateIndex as CurveRateIndex,
    RegressionExposure,
    dirty_price_from_curve,
)
from fuggers_py.data import (
    AnalyticsCurves,
    BondFutureContractReference,
    BondFutureQuote,
    BondReferenceData,
    BondType,
    CdsReferenceData,
    CurvePoint,
    DeliverableBondReference,
    HaircutQuote,
    InstrumentId,
    IssuerType,
    PricingSpec,
    RepoQuote,
    RepoReferenceData,
    SwapQuote,
    SwapReferenceData,
    VolPoint,
    VolQuoteType,
    VolSurfaceId,
    VolSurfaceType,
    VolatilitySurface,
    YearMonth,
)
from fuggers_py.funding import RepoTrade
from fuggers_py.io import create_file_market_data, create_file_reference_data
from fuggers_py.portfolio import (
    Classification,
    CreditRating,
    Holding,
    Portfolio,
    PortfolioBuilder,
    RatingInfo,
    Sector,
    SectorInfo,
)
from fuggers_py.rates import (
    AssetSwap,
    BasisSwap,
    CrossCurrencyBasisSwap,
    DeliverableBasket,
    DeliverableBond,
    FixedFloatSwap,
    FixedLegSpec,
    FloatingLegSpec,
    Fra,
    GovernmentBondFuture,
    OneFactorDeliveryOptionModel,
    PayReceive,
    ScheduleDefinition,
    YieldGridCTDSwitchModel,
)


def build_foundation_bond() -> FixedBond:
    """Return a deterministic fixed-rate bond used in the foundations notebook."""

    return (
        FixedBondBuilder.new()
        .with_issue_date(Date.from_ymd(2025, 1, 31))
        .with_maturity_date(Date.from_ymd(2030, 1, 31))
        .with_coupon_rate(Decimal("0.04625"))
        .with_frequency(Frequency.SEMI_ANNUAL)
        .with_currency(Currency.USD)
        .with_notional(Decimal("100"))
        .with_rules(YieldCalculationRules.us_treasury())
        .build()
    )


def foundation_calendar_scenarios() -> pd.DataFrame:
    """Representative trade and coupon dates around weekends and holidays."""

    return pd.DataFrame(
        [
            {
                "scenario": "US holiday cluster",
                "trade_date": Date.from_ymd(2025, 7, 3),
                "candidate_coupon_date": Date.from_ymd(2025, 7, 4),
            },
            {
                "scenario": "TARGET Labour Day",
                "trade_date": Date.from_ymd(2025, 4, 30),
                "candidate_coupon_date": Date.from_ymd(2025, 5, 1),
            },
            {
                "scenario": "UK bank holiday",
                "trade_date": Date.from_ymd(2025, 5, 2),
                "candidate_coupon_date": Date.from_ymd(2025, 5, 5),
            },
            {
                "scenario": "Month-end weekend",
                "trade_date": Date.from_ymd(2025, 5, 29),
                "candidate_coupon_date": Date.from_ymd(2025, 5, 31),
            },
        ]
    )


def foundation_coupon_period_context() -> dict[str, object]:
    """Return the coupon-period dates used for the accrual comparison."""

    accrual_start = Date.from_ymd(2025, 1, 31)
    accrual_end = Date.from_ymd(2025, 7, 31)
    settlement_dates = [
        Date.from_ymd(2025, 2, 28),
        Date.from_ymd(2025, 3, 31),
        Date.from_ymd(2025, 4, 30),
        Date.from_ymd(2025, 5, 30),
        Date.from_ymd(2025, 6, 30),
        Date.from_ymd(2025, 7, 15),
    ]
    return {
        "accrual_start": accrual_start,
        "accrual_end": accrual_end,
        "selected_settlement": Date.from_ymd(2025, 4, 30),
        "settlement_dates": settlement_dates,
    }


def examples_root() -> Path:
    """Return the examples root irrespective of the current working directory."""

    return Path(__file__).resolve().parents[1]


def fixtures_root() -> Path:
    """Return the deterministic fixture directory used by the examples."""

    return examples_root() / "_fixtures"


def load_fixture_market_data():
    """Return file-backed market-data providers built from curated fixtures."""

    fixtures = fixtures_root()
    return create_file_market_data(
        quotes_csv=fixtures / "quotes.csv",
        curve_inputs_json=fixtures / "curve_inputs.json",
        fixings_csv=fixtures / "fixings.csv",
    )


def load_fixture_reference_data():
    """Return file-backed reference-data providers built from curated fixtures."""

    fixtures = fixtures_root()
    return create_file_reference_data(bonds_csv=fixtures / "bonds.csv")


def research_reference_date() -> Date:
    """Reference date shared by portfolio and engine notebooks."""

    return Date.from_ymd(2024, 1, 2)


def build_usd_discount_curve(reference_date: Date | None = None):
    """Return a small upward-sloping USD curve for deterministic examples."""

    reference_date = reference_date or research_reference_date()
    return (
        DiscountCurveBuilder(reference_date=reference_date)
        .add_zero_rate(0.5, Decimal("0.0300"))
        .add_zero_rate(2.0, Decimal("0.0330"))
        .add_zero_rate(5.0, Decimal("0.0380"))
        .add_zero_rate(10.0, Decimal("0.0410"))
        .build()
    )


def annual_corporate_rules():
    """Annual-pay corporate yield rules used by portfolio examples."""

    return replace(YieldCalculationRules.us_corporate(), frequency=Frequency.ANNUAL)


def make_portfolio_holding(
    label: str,
    *,
    years: int,
    coupon: str,
    price: str,
    sector: Sector,
    rating: CreditRating,
    quantity: str = "100",
    liquidity_score: str = "0.85",
    reference_date: Date | None = None,
) -> Holding:
    """Create a deterministic holding with sector, rating, and liquidity tags."""

    reference_date = reference_date or research_reference_date()
    bond = (
        FixedBondBuilder.new()
        .with_issue_date(reference_date)
        .with_maturity_date(reference_date.add_years(years))
        .with_coupon_rate(Decimal(coupon))
        .with_frequency(Frequency.ANNUAL)
        .with_currency(Currency.USD)
        .with_rules(annual_corporate_rules())
        .build()
    )
    return Holding(
        id=label,
        instrument=bond,
        quantity=Decimal(quantity),
        clean_price=Decimal(price),
        label=label,
        classification=Classification(sector=sector, rating=rating, currency=Currency.USD),
        rating_info=RatingInfo(rating=rating),
        sector_info=SectorInfo(sector=sector),
        liquidity_score=Decimal(liquidity_score),
    )


def build_research_portfolio(reference_date: Date | None = None) -> Portfolio:
    """Return a small diversified USD credit portfolio."""

    reference_date = reference_date or research_reference_date()
    return (
        PortfolioBuilder()
        .with_currency(Currency.USD)
        .add_holding(
            make_portfolio_holding(
                "gov_short",
                years=2,
                coupon="0.0325",
                price="99.5",
                sector=Sector.GOVERNMENT,
                rating=CreditRating.AA,
                liquidity_score="0.95",
                reference_date=reference_date,
            )
        )
        .add_holding(
            make_portfolio_holding(
                "corp_bbb",
                years=5,
                coupon="0.0500",
                price="101.0",
                sector=Sector.CORPORATE,
                rating=CreditRating.BBB,
                liquidity_score="0.80",
                reference_date=reference_date,
            )
        )
        .add_holding(
            make_portfolio_holding(
                "corp_bb",
                years=8,
                coupon="0.0625",
                price="98.0",
                sector=Sector.INDUSTRIALS,
                rating=CreditRating.BB,
                liquidity_score="0.72",
                reference_date=reference_date,
            )
        )
        .build()
    )


def build_benchmark_portfolio(reference_date: Date | None = None) -> Portfolio:
    """Return a simpler benchmark portfolio with shorter duration and tighter spreads."""

    reference_date = reference_date or research_reference_date()
    return (
        PortfolioBuilder()
        .with_currency(Currency.USD)
        .add_holding(
            make_portfolio_holding(
                "bench_gov",
                years=3,
                coupon="0.0300",
                price="100.5",
                sector=Sector.GOVERNMENT,
                rating=CreditRating.A,
                liquidity_score="0.97",
                reference_date=reference_date,
            )
        )
        .add_holding(
            make_portfolio_holding(
                "bench_corp",
                years=6,
                coupon="0.0450",
                price="100.0",
                sector=Sector.CORPORATE,
                rating=CreditRating.BBB,
                liquidity_score="0.82",
                reference_date=reference_date,
            )
        )
        .build()
    )


@dataclass(frozen=True, slots=True)
class RepoDemoBundle:
    as_of: Date
    collateral_reference: BondReferenceData
    repo_quote: RepoQuote
    haircut_quote: HaircutQuote
    swap_quote: SwapQuote
    repo_reference: RepoReferenceData
    swap_reference: SwapReferenceData
    repo_trade: RepoTrade
    repo_batch: tuple[tuple[str, RepoTrade], ...]
    repo_curve: object
    collateral_curve: object
    overnight_curve: object
    fx_forward_curve: object
    term_curve: object
    analytics_curves: AnalyticsCurves
    vol_surface: VolatilitySurface


@dataclass(frozen=True, slots=True)
class FuturesDemoBundle:
    as_of: Date
    future_quote: BondFutureQuote
    contract_reference: BondFutureContractReference
    deliverable_references: tuple[DeliverableBondReference, ...]
    contract: GovernmentBondFuture
    basket: DeliverableBasket
    deterministic_delivery_option_model: YieldGridCTDSwitchModel
    stochastic_delivery_option_model: OneFactorDeliveryOptionModel


@dataclass(frozen=True, slots=True)
class CdsDemoBundle:
    reference_date: Date
    primary_quote: CdsQuote
    primary_reference: CdsReferenceData
    quote_strip: tuple[CdsQuote, ...]
    discount_curve: object
    off_the_run_cds: CreditDefaultSwap
    delivery_option_adjustment: Decimal
    fx_adjustment: Decimal
    bond_spread: Decimal
    bond_yield: Decimal
    liquidity_adjustment: Decimal
    funding_adjustment: Decimal


@dataclass(frozen=True, slots=True)
class GlobalRvDemoBundle:
    reference_date: Date
    fit_result: FittedBondFitResult
    curves: AnalyticsCurves
    euro_asset_swap: AssetSwap
    local_basis_swap: BasisSwap
    cross_currency_basis_swap: CrossCurrencyBasisSwap
    yardstick_spread: Decimal
    adjusted_cds_spread: Decimal


@dataclass(frozen=True, slots=True)
class RatesEngineDemoBundle:
    settlement: Date
    discount_points: tuple[CurvePoint, ...]
    government_points: tuple[CurvePoint, ...]
    benchmark_points: tuple[CurvePoint, ...]
    forward_points: tuple[CurvePoint, ...]
    term_forward_points: tuple[CurvePoint, ...]
    fixed_bond: FixedBond
    callable_bond: object
    frn: object
    fixed_bond_market_price: Decimal
    callable_bond_market_price: Decimal
    frn_market_price: Decimal
    fixed_pricing_spec: object
    callable_pricing_spec: object
    frn_pricing_spec: object
    swap: FixedFloatSwap
    fra: Fra
    basis_swap: BasisSwap
    repo_bundle: RepoDemoBundle


def _demo_vol_surface(as_of: Date) -> VolatilitySurface:
    return VolatilitySurface(
        surface_id=VolSurfaceId("demo.swaption"),
        surface_type=VolSurfaceType.SWAPTION,
        as_of=as_of,
        points=(
            VolPoint(
                expiry=YearMonth(as_of.year() + 1, 6),
                tenor=YearMonth(as_of.year() + 5, 6),
                strike=Decimal("0.0400"),
                volatility=Decimal("0.20"),
                quote_type=VolQuoteType.LOGNORMAL,
            ),
        ),
    )


def build_repo_demo_bundle(as_of: Date | None = None) -> RepoDemoBundle:
    """Return deterministic repo, haircut, and swap data plus funding curves."""

    as_of = as_of or Date.from_ymd(2026, 1, 15)
    collateral_reference = BondReferenceData(
        instrument_id=InstrumentId("US91282CFX4"),
        bond_type=BondType.FIXED_RATE,
        issuer_type=IssuerType.SOVEREIGN,
        issue_date=Date.from_ymd(2021, 1, 15),
        maturity_date=Date.from_ymd(2031, 1, 15),
        currency=Currency.USD,
        coupon_rate=Decimal("0.0450"),
        frequency=Frequency.SEMI_ANNUAL,
        amount_outstanding=Decimal("1250000000"),
        benchmark_flag=True,
        liquidity_score=Decimal("1.10"),
    )
    repo_quote = RepoQuote(
        instrument_id=InstrumentId("SCENARIO-D-REPO"),
        rate=Decimal("0.0410"),
        haircut=Decimal("0.0200"),
        term="1M",
        collateral_type="UST",
        as_of=as_of,
        currency=Currency.USD,
    )
    haircut_quote = HaircutQuote(
        instrument_id=InstrumentId("SCENARIO-G-HAIRCUT"),
        haircut=Decimal("0.0150"),
        collateral_type="UST",
        as_of=as_of,
        currency=Currency.USD,
    )
    swap_quote = SwapQuote(
        instrument_id=InstrumentId("SCENARIO-E-SWAP"),
        rate=Decimal("0.0380"),
        tenor="5Y",
        floating_index="SOFR",
        fixed_frequency="SEMI_ANNUAL",
        as_of=as_of,
        currency=Currency.USD,
    )
    repo_reference = RepoReferenceData(
        instrument_id=repo_quote.instrument_id,
        currency=Currency.USD,
        collateral_currency=Currency.USD,
        term="1M",
        collateral_type="UST",
        haircut=repo_quote.haircut,
        settlement_lag_days=1,
    )
    swap_reference = SwapReferenceData(
        instrument_id=swap_quote.instrument_id,
        tenor="5Y",
        floating_index="SOFR",
        fixed_frequency=Frequency.SEMI_ANNUAL,
        floating_frequency=Frequency.QUARTERLY,
        day_count="ACT/360",
        calendar="NYC",
    )
    repo_trade = RepoTrade(
        start_date=as_of,
        end_date=as_of.add_days(30),
        rate=repo_quote.rate,
        haircut=repo_quote.haircut,
        collateral_price=Decimal("101.50"),
        notional=Decimal("1000000"),
        currency=Currency.USD,
        collateral_instrument_id=collateral_reference.instrument_id,
    )
    repo_batch = (
        ("front_month_repo", repo_trade),
        (
            "cash_specified_repo",
            RepoTrade(
                start_date=as_of,
                end_date=as_of.add_days(14),
                rate=Decimal("0.0405"),
                haircut=Decimal("0.0150"),
                collateral_price=Decimal("100.75"),
                cash_amount=Decimal("750000"),
                currency=Currency.USD,
                collateral_instrument_id=collateral_reference.instrument_id,
            ),
        ),
    )
    repo_curve = (
        DiscountCurveBuilder(reference_date=as_of)
        .add_zero_rate(0.25, Decimal("0.0390"))
        .add_zero_rate(1.0, Decimal("0.0395"))
        .add_zero_rate(5.0, Decimal("0.0400"))
        .build()
    )
    collateral_curve = (
        DiscountCurveBuilder(reference_date=as_of)
        .add_zero_rate(0.25, Decimal("0.0220"))
        .add_zero_rate(1.0, Decimal("0.0225"))
        .add_zero_rate(5.0, Decimal("0.0230"))
        .build()
    )
    overnight_curve = (
        DiscountCurveBuilder(reference_date=as_of)
        .add_zero_rate(0.25, Decimal("0.0250"))
        .add_zero_rate(1.0, Decimal("0.0260"))
        .add_zero_rate(5.0, Decimal("0.0290"))
        .build()
    )
    fx_forward_curve = (
        DiscountCurveBuilder(reference_date=as_of)
        .add_zero_rate(0.25, Decimal("0.0110"))
        .add_zero_rate(1.0, Decimal("0.0125"))
        .add_zero_rate(5.0, Decimal("0.0140"))
        .build()
    )
    term_curve = (
        DiscountCurveBuilder(reference_date=as_of)
        .add_zero_rate(0.25, Decimal("0.0290"))
        .add_zero_rate(1.0, Decimal("0.0305"))
        .add_zero_rate(5.0, Decimal("0.0320"))
        .build()
    )
    vol_surface = _demo_vol_surface(as_of)
    analytics_curves = AnalyticsCurves(
        discount_curve=overnight_curve,
        forward_curve=overnight_curve,
        repo_curve=repo_curve,
        collateral_curve=collateral_curve,
        fx_forward_curve=fx_forward_curve,
        multicurve_environment={
            "discount_curve": overnight_curve,
            "repo_curve": repo_curve,
            "projection_curves": {"USD-TERM-6M": term_curve},
        },
        projection_curves={"USD-TERM-6M": term_curve},
        vol_surface=vol_surface,
    )
    return RepoDemoBundle(
        as_of=as_of,
        collateral_reference=collateral_reference,
        repo_quote=repo_quote,
        haircut_quote=haircut_quote,
        swap_quote=swap_quote,
        repo_reference=repo_reference,
        swap_reference=swap_reference,
        repo_trade=repo_trade,
        repo_batch=repo_batch,
        repo_curve=repo_curve,
        collateral_curve=collateral_curve,
        overnight_curve=overnight_curve,
        fx_forward_curve=fx_forward_curve,
        term_curve=term_curve,
        analytics_curves=analytics_curves,
        vol_surface=vol_surface,
    )


def build_futures_demo_bundle(as_of: Date | None = None) -> FuturesDemoBundle:
    """Return deterministic government-bond-futures records and basket objects."""

    as_of = as_of or Date.from_ymd(2026, 1, 15)
    future_quote = BondFutureQuote(
        instrument_id=InstrumentId("SCENARIO-F-FUTURE"),
        price=Decimal("112.50"),
        delivery_month="2026-06",
        conversion_factor=Decimal("0.8125"),
        cheapest_to_deliver=InstrumentId("long-duration"),
        as_of=as_of,
        currency=Currency.USD,
    )
    contract_reference = BondFutureContractReference(
        instrument_id=future_quote.instrument_id,
        currency=Currency.USD,
        delivery_month="2026-06",
        first_delivery_date=Date.from_ymd(2026, 6, 1),
        last_delivery_date=Date.from_ymd(2026, 6, 30),
        contract_size=Decimal("100000"),
        tick_size=Decimal("0.015625"),
        exchange="CBOT",
        standard_coupon_rate=Decimal("0.06"),
        coupon_frequency=Frequency.SEMI_ANNUAL,
        underlying_basket=(InstrumentId("long-duration"), InstrumentId("short-duration")),
    )
    deliverable_references = (
        DeliverableBondReference(
            instrument_id=InstrumentId("long-duration"),
            issue_date=Date.from_ymd(2020, 3, 1),
            maturity_date=Date.from_ymd(2040, 3, 1),
            coupon_rate=Decimal("0.0200"),
        ),
        DeliverableBondReference(
            instrument_id=InstrumentId("short-duration"),
            issue_date=Date.from_ymd(2021, 3, 1),
            maturity_date=Date.from_ymd(2031, 3, 1),
            coupon_rate=Decimal("0.0600"),
        ),
    )
    contract = GovernmentBondFuture.from_reference(contract_reference)
    target_equivalent_price = Decimal("105")
    deliverables = []
    for reference in deliverable_references:
        template = DeliverableBond.from_reference(reference, clean_price=Decimal("100"))
        clean_price = template.price_from_yield(Decimal("0.04"), as_of)
        deliverables.append(
            replace(
                template,
                clean_price=clean_price,
                published_conversion_factor=clean_price / target_equivalent_price,
            )
        )
    basket = DeliverableBasket(as_of=as_of, deliverables=tuple(deliverables))
    return FuturesDemoBundle(
        as_of=as_of,
        future_quote=future_quote,
        contract_reference=contract_reference,
        deliverable_references=deliverable_references,
        contract=contract,
        basket=basket,
        deterministic_delivery_option_model=YieldGridCTDSwitchModel((Decimal("-100"), Decimal("0"), Decimal("100"))),
        stochastic_delivery_option_model=OneFactorDeliveryOptionModel(
            yield_volatility_bps=Decimal("100"),
            scenario_multipliers=(Decimal("-1"), Decimal("1")),
            scenario_probabilities=(Decimal("0.5"), Decimal("0.5")),
        ),
    )


def build_cds_demo_bundle(reference_date: Date | None = None) -> CdsDemoBundle:
    """Return deterministic CDS quotes, reference data, and pricing inputs."""

    reference_date = reference_date or Date.from_ymd(2026, 1, 15)
    primary_quote = CdsQuote(
        instrument_id=InstrumentId("SCENARIO-H-CDS"),
        par_spread=Decimal("0.0125"),
        recovery_rate=Decimal("0.40"),
        tenor="5Y",
        reference_entity="ACME Corp",
        as_of=reference_date,
        currency=Currency.USD,
    )
    primary_reference = CdsReferenceData(
        instrument_id=primary_quote.instrument_id,
        reference_entity="ACME Corp",
        tenor="5Y",
        seniority="SNRFOR",
        restructuring_clause="XR",
        coupon=Decimal("0.05"),
        recovery_rate=Decimal("0.40"),
    )
    quote_strip = (
        CdsQuote(instrument_id="ACME-CDS-1Y", par_spread=Decimal("0.0080"), tenor="1Y", recovery_rate=Decimal("0.40"), as_of=reference_date, currency=Currency.USD),
        CdsQuote(instrument_id="ACME-CDS-3Y", par_spread=Decimal("0.0110"), tenor="3Y", recovery_rate=Decimal("0.40"), as_of=reference_date, currency=Currency.USD),
        CdsQuote(instrument_id="ACME-CDS-5Y", par_spread=Decimal("0.0135"), tenor="5Y", recovery_rate=Decimal("0.40"), as_of=reference_date, currency=Currency.USD),
        CdsQuote(instrument_id="ACME-CDS-7Y", par_spread=Decimal("0.0150"), tenor="7Y", recovery_rate=Decimal("0.40"), as_of=reference_date, currency=Currency.USD),
    )
    discount_curve = (
        DiscountCurveBuilder(reference_date=reference_date)
        .add_zero_rate(1.0, Decimal("0.0300"))
        .add_zero_rate(10.0, Decimal("0.0300"))
        .build()
    )
    off_the_run_cds = CreditDefaultSwap(
        effective_date=reference_date,
        maturity_date=reference_date.add_years(5),
        running_spread=Decimal("0.0120"),
        notional=Decimal("10000000"),
        recovery_rate=Decimal("0.40"),
        instrument_id="ACME-CDS-5Y-OFFRUN",
        reference_entity="ACME",
    )
    return CdsDemoBundle(
        reference_date=reference_date,
        primary_quote=primary_quote,
        primary_reference=primary_reference,
        quote_strip=quote_strip,
        discount_curve=discount_curve,
        off_the_run_cds=off_the_run_cds,
        delivery_option_adjustment=Decimal("0.0010"),
        fx_adjustment=Decimal("0.0005"),
        bond_spread=Decimal("0.0165"),
        bond_yield=Decimal("0.0520"),
        liquidity_adjustment=Decimal("0.0007"),
        funding_adjustment=Decimal("0.0004"),
    )


def _build_rv_fit_result(reference_date: Date) -> FittedBondFitResult:
    curve_model = ExponentialSplineCurveModel((Decimal("0.40"), Decimal("1.20")))
    true_curve = curve_model.build_curve(
        reference_date,
        np.asarray([0.032, -0.008, 0.004], dtype=float),
        max_t=10.0,
    )
    universe = (
        ("UST2Y", 2, Decimal("0.0200"), Decimal("-1.2"), Decimal("400000000"), Decimal("0.80"), False, Decimal("0.00")),
        ("UST3Y", 3, Decimal("0.0225"), Decimal("-0.8"), Decimal("500000000"), Decimal("0.90"), False, Decimal("0.00")),
        ("UST4Y", 4, Decimal("0.0250"), Decimal("-0.4"), Decimal("700000000"), Decimal("1.00"), True, Decimal("0.90")),
        ("UST5Y", 5, Decimal("0.0275"), Decimal("0.0"), Decimal("900000000"), Decimal("1.10"), True, Decimal("0.00")),
        ("UST6Y", 6, Decimal("0.0300"), Decimal("0.4"), Decimal("1100000000"), Decimal("1.15"), True, Decimal("0.00")),
        ("UST8Y", 8, Decimal("0.0350"), Decimal("0.8"), Decimal("800000000"), Decimal("1.00"), True, Decimal("-1.40")),
        ("UST10Y", 10, Decimal("0.0400"), Decimal("1.2"), Decimal("600000000"), Decimal("0.85"), False, Decimal("-0.20")),
    )
    observations: list[FittedBondObservation] = []
    for instrument_id, maturity_years, coupon_rate, liquidity_exposure, amount_outstanding, liquidity_score, benchmark_flag, residual_shock in universe:
        bond = (
            FixedBondBuilder.new()
            .with_issue_date(Date.from_ymd(2021, 1, 15))
            .with_maturity_date(reference_date.add_years(maturity_years))
            .with_coupon_rate(coupon_rate)
            .with_frequency(Frequency.SEMI_ANNUAL)
            .with_currency(Currency.USD)
            .with_rules(YieldCalculationRules.us_treasury())
            .build()
        )
        reference_data = BondReferenceData(
            instrument_id=instrument_id,
            bond_type=BondType.FIXED_RATE,
            issuer_type=IssuerType.SOVEREIGN,
            issue_date=Date.from_ymd(2021, 1, 15),
            maturity_date=reference_date.add_years(maturity_years),
            currency=Currency.USD,
            coupon_rate=coupon_rate,
            frequency=Frequency.SEMI_ANNUAL,
            amount_outstanding=amount_outstanding,
            benchmark_flag=benchmark_flag,
            futures_deliverable_flags=("USTFUT",) if maturity_years in {6, 8} else (),
            liquidity_score=liquidity_score,
        )
        observed_dirty = dirty_price_from_curve(bond, true_curve, reference_date) + Decimal("0.25") * liquidity_exposure + residual_shock
        observations.append(
            FittedBondObservation(
                instrument_id=InstrumentId(instrument_id),
                bond=bond,
                clean_price=observed_dirty - bond.accrued_interest(reference_date),
                settlement_date=reference_date,
                regression_exposures=(RegressionExposure("liquidity", liquidity_exposure),),
                reference_data=reference_data,
            )
        )
    return FittedBondCurveFitter(curve_model=curve_model, config=FittedBondFitConfig()).fit(observations)


def build_global_rv_demo_bundle(reference_date: Date | None = None) -> GlobalRvDemoBundle:
    """Return deterministic local/global RV setup objects and curve bundles."""

    reference_date = reference_date or Date.from_ymd(2026, 1, 15)
    fit_result = _build_rv_fit_result(reference_date)
    eur_discount_curve = (
        DiscountCurveBuilder(reference_date=reference_date)
        .add_zero_rate(1.0, Decimal("0.0200"))
        .add_zero_rate(10.0, Decimal("0.0200"))
        .build()
    )
    usd_discount_curve = (
        DiscountCurveBuilder(reference_date=reference_date)
        .add_zero_rate(1.0, Decimal("0.0300"))
        .add_zero_rate(10.0, Decimal("0.0300"))
        .build()
    )
    eur_term_6m_curve = (
        DiscountCurveBuilder(reference_date=reference_date)
        .add_zero_rate(1.0, Decimal("0.0240"))
        .add_zero_rate(10.0, Decimal("0.0240"))
        .build()
    )
    eur_term_3m_curve = (
        DiscountCurveBuilder(reference_date=reference_date)
        .add_zero_rate(1.0, Decimal("0.0225"))
        .add_zero_rate(10.0, Decimal("0.0225"))
        .build()
    )
    usd_sofr_curve = (
        DiscountCurveBuilder(reference_date=reference_date)
        .add_zero_rate(1.0, Decimal("0.0340"))
        .add_zero_rate(10.0, Decimal("0.0340"))
        .build()
    )
    eur_6m_index = CurveRateIndex.new("EURIBOR", Tenor.parse("6M"), Currency.EUR)
    eur_3m_index = CurveRateIndex.new("EURIBOR", Tenor.parse("3M"), Currency.EUR)
    usd_sofr_index = CurveRateIndex.new("SOFR", Tenor.parse("3M"), Currency.USD)
    multicurve = (
        MultiCurveEnvironmentBuilder()
        .add_discount_curve(Currency.EUR, eur_discount_curve)
        .add_discount_curve(Currency.USD, usd_discount_curve)
        .add_projection_curve(eur_6m_index, eur_term_6m_curve)
        .add_projection_curve(eur_3m_index, eur_term_3m_curve)
        .add_projection_curve(usd_sofr_index, usd_sofr_curve)
        .build()
    )
    curves = AnalyticsCurves(
        discount_curve=eur_discount_curve,
        forward_curve=eur_term_6m_curve,
        multicurve_environment=multicurve,
        projection_curves={
            str(eur_6m_index): eur_term_6m_curve,
            str(eur_3m_index): eur_term_3m_curve,
            str(usd_sofr_index): usd_sofr_curve,
        },
    )
    euro_bond = (
        FixedBondBuilder.new()
        .with_issue_date(Date.from_ymd(2021, 1, 15))
        .with_maturity_date(reference_date.add_years(5))
        .with_coupon_rate(Decimal("0.0325"))
        .with_frequency(Frequency.SEMI_ANNUAL)
        .with_currency(Currency.EUR)
        .with_rules(YieldCalculationRules.us_corporate())
        .build()
    )
    euro_asset_swap = AssetSwap(
        bond=euro_bond,
        settlement_date=reference_date,
        floating_leg=FloatingLegSpec(
            pay_receive=PayReceive.RECEIVE,
            notional=Decimal("100"),
            index_name="EURIBOR",
            index_tenor="6M",
            currency=Currency.EUR,
            schedule=ScheduleDefinition(frequency=Frequency.SEMI_ANNUAL),
        ),
        market_dirty_price=Decimal("100.00"),
    )
    local_basis_swap = BasisSwap(
        effective_date=reference_date,
        maturity_date=reference_date.add_years(5),
        pay_leg=FloatingLegSpec(
            pay_receive=PayReceive.PAY,
            notional=Decimal("100"),
            index_name="EURIBOR",
            index_tenor="6M",
            currency=Currency.EUR,
            schedule=ScheduleDefinition(frequency=Frequency.SEMI_ANNUAL),
        ),
        receive_leg=FloatingLegSpec(
            pay_receive=PayReceive.RECEIVE,
            notional=Decimal("100"),
            index_name="EURIBOR",
            index_tenor="3M",
            currency=Currency.EUR,
            schedule=ScheduleDefinition(frequency=Frequency.QUARTERLY),
        ),
        quoted_leg=PayReceive.RECEIVE,
    )
    cross_currency_basis_swap = CrossCurrencyBasisSwap(
        effective_date=reference_date,
        maturity_date=reference_date.add_years(5),
        pay_leg=FloatingLegSpec(
            pay_receive=PayReceive.PAY,
            notional=Decimal("100"),
            index_name="EURIBOR",
            index_tenor="3M",
            currency=Currency.EUR,
            schedule=ScheduleDefinition(frequency=Frequency.QUARTERLY),
        ),
        receive_leg=FloatingLegSpec(
            pay_receive=PayReceive.RECEIVE,
            notional=Decimal("110"),
            index_name="SOFR",
            index_tenor="3M",
            currency=Currency.USD,
            schedule=ScheduleDefinition(frequency=Frequency.QUARTERLY),
        ),
        spot_fx_rate=Decimal("1.10"),
        quoted_leg=PayReceive.RECEIVE,
    )
    return GlobalRvDemoBundle(
        reference_date=reference_date,
        fit_result=fit_result,
        curves=curves,
        euro_asset_swap=euro_asset_swap,
        local_basis_swap=local_basis_swap,
        cross_currency_basis_swap=cross_currency_basis_swap,
        yardstick_spread=Decimal("0.0150"),
        adjusted_cds_spread=Decimal("0.0125"),
    )


def build_rates_engine_demo_bundle(settlement: Date | None = None) -> RatesEngineDemoBundle:
    """Return deterministic point sets and instruments for engine notebooks."""

    settlement = settlement or Date.from_ymd(2026, 1, 15)
    discount_points = (
        CurvePoint(Decimal("0.25"), Decimal("0.0450")),
        CurvePoint(Decimal("0.50"), Decimal("0.0445")),
        CurvePoint(Decimal("1.00"), Decimal("0.0435")),
        CurvePoint(Decimal("2.00"), Decimal("0.0415")),
        CurvePoint(Decimal("5.00"), Decimal("0.0395")),
        CurvePoint(Decimal("10.00"), Decimal("0.0400")),
    )
    government_points = (
        CurvePoint(Decimal("0.25"), Decimal("0.0420")),
        CurvePoint(Decimal("0.50"), Decimal("0.0410")),
        CurvePoint(Decimal("1.00"), Decimal("0.0400")),
        CurvePoint(Decimal("2.00"), Decimal("0.0385")),
        CurvePoint(Decimal("5.00"), Decimal("0.0370")),
        CurvePoint(Decimal("10.00"), Decimal("0.0380")),
    )
    benchmark_points = (
        CurvePoint(Decimal("0.25"), Decimal("0.0435")),
        CurvePoint(Decimal("0.50"), Decimal("0.0428")),
        CurvePoint(Decimal("1.00"), Decimal("0.0418")),
        CurvePoint(Decimal("2.00"), Decimal("0.0402")),
        CurvePoint(Decimal("5.00"), Decimal("0.0388")),
        CurvePoint(Decimal("10.00"), Decimal("0.0392")),
    )
    forward_points = (
        CurvePoint(Decimal("0.25"), Decimal("0.0430")),
        CurvePoint(Decimal("0.50"), Decimal("0.0420")),
        CurvePoint(Decimal("1.00"), Decimal("0.0405")),
        CurvePoint(Decimal("2.00"), Decimal("0.0390")),
        CurvePoint(Decimal("3.00"), Decimal("0.0385")),
    )
    term_forward_points = (
        CurvePoint(Decimal("0.25"), Decimal("0.0440")),
        CurvePoint(Decimal("0.50"), Decimal("0.0430")),
        CurvePoint(Decimal("1.00"), Decimal("0.0415")),
        CurvePoint(Decimal("2.00"), Decimal("0.0400")),
        CurvePoint(Decimal("3.00"), Decimal("0.0395")),
    )
    fixed_rules = replace(YieldCalculationRules.us_corporate(), frequency=Frequency.SEMI_ANNUAL)
    fixed_bond = FixedBond.new(
        issue_date=Date.from_ymd(2024, 1, 15),
        maturity_date=Date.from_ymd(2031, 1, 15),
        coupon_rate=Decimal("0.0450"),
        frequency=Frequency.SEMI_ANNUAL,
        currency=Currency.USD,
        notional=Decimal("100"),
        rules=fixed_rules,
    )
    callable_base = FixedBond.new(
        issue_date=Date.from_ymd(2023, 1, 15),
        maturity_date=Date.from_ymd(2033, 1, 15),
        coupon_rate=Decimal("0.0500"),
        frequency=Frequency.SEMI_ANNUAL,
        currency=Currency.USD,
        notional=Decimal("100"),
        rules=fixed_rules,
    )
    callable_bond = (
        CallableBondBuilder.new()
        .with_base_bond(callable_base)
        .add_call(call_date=Date.from_ymd(2028, 1, 15), call_price=Decimal("100.00"), call_type=CallType.EUROPEAN)
        .add_call(call_date=Date.from_ymd(2029, 1, 15), call_price=Decimal("100.00"), call_type=CallType.EUROPEAN)
        .build()
    )
    fixing_store = IndexFixingStore()
    for fixing_date in (
        Date.from_ymd(2025, 12, 29),
        Date.from_ymd(2025, 12, 30),
        Date.from_ymd(2025, 12, 31),
        Date.from_ymd(2026, 1, 2),
        Date.from_ymd(2026, 1, 5),
        Date.from_ymd(2026, 1, 6),
        Date.from_ymd(2026, 1, 7),
        Date.from_ymd(2026, 1, 8),
        Date.from_ymd(2026, 1, 9),
        Date.from_ymd(2026, 1, 12),
        Date.from_ymd(2026, 1, 13),
        Date.from_ymd(2026, 1, 14),
    ):
        fixing_store.add_fixing("SOFR", fixing_date, Decimal("0.0410"))
    index = BondIndex(
        name="SOFR",
        rate_index=BondRateIndex.SOFR,
        currency=Currency.USD,
        fixing_store=fixing_store,
        conventions=IndexConventions(overnight_compounding=OvernightCompounding.COMPOUNDED),
    )
    frn = (
        FloatingRateNoteBuilder.new()
        .with_issue_date(Date.from_ymd(2025, 1, 15))
        .with_maturity_date(Date.from_ymd(2028, 1, 15))
        .with_index(BondRateIndex.SOFR)
        .with_index_definition(index)
        .with_quoted_spread(Decimal("0.0125"))
        .with_frequency(Frequency.QUARTERLY)
        .with_currency(Currency.USD)
        .with_notional(Decimal("100"))
        .with_rules(replace(YieldCalculationRules.us_treasury(), frequency=Frequency.QUARTERLY))
        .with_current_reference_rate(Decimal("0.0410"))
        .build()
    )
    swap = FixedFloatSwap(
        effective_date=settlement,
        maturity_date=settlement.add_years(5),
        instrument_id=InstrumentId("SCENARIO-D-SWAP"),
        fixed_leg=FixedLegSpec(
            pay_receive=PayReceive.RECEIVE,
            notional=Decimal("1000000"),
            fixed_rate=Decimal("0.0410"),
            currency=Currency.USD,
            schedule=ScheduleDefinition(frequency=Frequency.SEMI_ANNUAL),
        ),
        floating_leg=FloatingLegSpec(
            pay_receive=PayReceive.PAY,
            notional=Decimal("1000000"),
            index_name="SOFR",
            index_tenor="3M",
            currency=Currency.USD,
            schedule=ScheduleDefinition(frequency=Frequency.QUARTERLY),
        ),
    )
    fra = Fra(
        start_date=settlement.add_months(6),
        end_date=settlement.add_months(9),
        notional=Decimal("1000000"),
        fixed_rate=Decimal("0.0400"),
        pay_receive=PayReceive.RECEIVE,
        currency=Currency.USD,
        index_name="SOFR",
        index_tenor="3M",
        instrument_id=InstrumentId("SCENARIO-E-FRA"),
    )
    basis_swap = BasisSwap(
        effective_date=settlement,
        maturity_date=settlement.add_years(5),
        instrument_id=InstrumentId("SCENARIO-F-BASIS"),
        pay_leg=FloatingLegSpec(
            pay_receive=PayReceive.PAY,
            notional=Decimal("1000000"),
            index_name="SOFR",
            index_tenor="3M",
            currency=Currency.USD,
            schedule=ScheduleDefinition(frequency=Frequency.QUARTERLY),
        ),
        receive_leg=FloatingLegSpec(
            pay_receive=PayReceive.RECEIVE,
            notional=Decimal("1000000"),
            index_name="TERM",
            index_tenor="6M",
            currency=Currency.USD,
            schedule=ScheduleDefinition(frequency=Frequency.SEMI_ANNUAL),
        ),
    )
    return RatesEngineDemoBundle(
        settlement=settlement,
        discount_points=discount_points,
        government_points=government_points,
        benchmark_points=benchmark_points,
        forward_points=forward_points,
        term_forward_points=term_forward_points,
        fixed_bond=fixed_bond,
        callable_bond=callable_bond,
        frn=frn,
        fixed_bond_market_price=Decimal("101.25"),
        callable_bond_market_price=Decimal("102.50"),
        frn_market_price=Decimal("100.15"),
        fixed_pricing_spec=PricingSpec(compute_spreads=True, compute_key_rates=True, include_asset_swap=True),
        callable_pricing_spec=PricingSpec(callable_mean_reversion=Decimal("0.03"), callable_volatility=Decimal("0.01")),
        frn_pricing_spec=PricingSpec(market_price_is_dirty=True),
        swap=swap,
        fra=fra,
        basis_swap=basis_swap,
        repo_bundle=build_repo_demo_bundle(as_of=settlement),
    )
