# ---
# jupyter:
#   jupytext:
#     formats: ipynb,py:percent
#     text_representation:
#       extension: .py
#       format_name: percent
#       format_version: '1.3'
#       jupytext_version: 1.19.1
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# %% [markdown]
# # Spread Toolkit: G, I, Z, ASW, OAS
#
# Relative-value work rarely stops at one spread measure. This notebook compares government, swap, discounted-cashflow, asset-swap, and option-adjusted spread views on a plain fixed-rate bond and a callable variant.

# %%
from __future__ import annotations

from decimal import Decimal

import matplotlib

matplotlib.use("Agg")

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

from fuggers_py.analytics.spreads import (
    BenchmarkSpec,
    GovernmentCurve,
    GSpreadCalculator,
    ISpreadCalculator,
    OASCalculator,
    ParParAssetSwap,
    ZSpreadCalculator,
)
from fuggers_py.analytics.yas import YASCalculator
from fuggers_py.bonds import CallType, CallableBondBuilder, FixedBondBuilder, Tenor, YieldCalculationRules
from fuggers_py.bonds.options import HullWhiteModel
from fuggers_py.core import Currency, Date, Frequency, Price
from fuggers_py.curves import DiscountCurveBuilder
from fuggers_py.data import AnalyticsCurves

from examples._shared.datasets import (
    build_cds_demo_bundle,
    build_futures_demo_bundle,
    build_global_rv_demo_bundle,
    build_repo_demo_bundle,
)
from examples._shared.plotting import apply_research_style
from examples._shared.tables import display_table

apply_research_style()

# %% [markdown]
# ## Benchmark framework
#
# We use a Treasury benchmark strip for g-spread and a flat swap-style discount curve for i-spread, z-spread, and asset-swap calculations.

# %%
settlement = Date.from_ymd(2020, 6, 15)
plain_bond = (
    FixedBondBuilder.new()
    .with_issue_date(Date.from_ymd(2015, 6, 15))
    .with_maturity_date(Date.from_ymd(2025, 6, 15))
    .with_coupon_rate(Decimal('0.075'))
    .with_frequency(Frequency.SEMI_ANNUAL)
    .with_currency(Currency.USD)
    .with_rules(YieldCalculationRules.us_corporate())
    .build()
)
plain_price = Price.new(Decimal('110.503'), plain_bond.currency())
discount_curve = (
    DiscountCurveBuilder(reference_date=settlement)
    .add_zero_rate(1.0, Decimal('0.0300'))
    .add_zero_rate(10.0, Decimal('0.0300'))
    .build()
)
government_curve = GovernmentCurve.us_treasury(settlement)
for tenor, rate in [('2Y', '0.0200'), ('5Y', '0.0300'), ('10Y', '0.0400')]:
    government_curve.add_benchmark(Tenor.parse(tenor), Decimal(rate))

curve_table = pd.DataFrame(
    {
        'tenor_years': [2.0, 5.0, 7.0, 10.0],
        'government_yield': [float(government_curve.interpolated_yield(x)) for x in [2.0, 5.0, 7.0, 10.0]],
        'discount_zero': [float(discount_curve.zero_rate_at_tenor(x)) for x in [2.0, 5.0, 7.0, 10.0]],
    }
)

display_table(curve_table, percent_columns=['government_yield', 'discount_zero'], float_columns=['tenor_years'])

# %% [markdown]
# ## Plain-bond spread decomposition
#
# The YAS calculator gives us a solved yield. We then map that yield into g-spread and i-spread, solve z-spread from discounted cashflows, and compute a simple par-par asset-swap spread from the same dirty price.

# %%
yas = YASCalculator(curve=discount_curve, government_curve=government_curve, benchmark=BenchmarkSpec.nearest())
analysis = yas.calculate(plain_bond, plain_price, settlement)
solved_yield = analysis.ytm / Decimal('100')
plain_spreads = pd.DataFrame(
    [
        {'measure': 'G-spread', 'spread_bps': GSpreadCalculator(government_curve).spread_bps(plain_bond, solved_yield, benchmark=BenchmarkSpec.nearest())},
        {'measure': 'I-spread', 'spread_bps': ISpreadCalculator(discount_curve).spread_bps(plain_bond, solved_yield, settlement)},
        {'measure': 'Z-spread', 'spread_bps': ZSpreadCalculator(discount_curve).spread_bps(plain_bond, plain_price, settlement)},
        {'measure': 'ASW', 'spread_bps': Decimal('10000') * ParParAssetSwap(discount_curve).calculate(plain_bond, analysis.invoice.dirty_price, settlement)},
    ]
)

display_table(plain_spreads, bps_columns=['spread_bps'])

# %% [markdown]
# ## Full asset-swap decomposition
#
# The low-level ASW kernels give us a spread. The full rates-layer product adds a structured funding decomposition on top: specific repo to GC, GC to unsecured overnight, and unsecured overnight to the term floating reference.

# %%
from fuggers_py.bonds.types import ASWType
from fuggers_py.rates import AssetSwap, AssetSwapPricer, FloatingLegSpec, PayReceive, ScheduleDefinition

# %%
repo_demo = build_repo_demo_bundle(as_of=settlement)
asset_swap_curves = repo_demo.analytics_curves
full_asset_swap = AssetSwap(
    bond=plain_bond,
    settlement_date=settlement,
    floating_leg=FloatingLegSpec(
        pay_receive=PayReceive.RECEIVE,
        notional=Decimal('100'),
        index_name='TERM',
        index_tenor='6M',
        currency=Currency.USD,
        schedule=ScheduleDefinition(frequency=Frequency.QUARTERLY),
    ),
    market_dirty_price=analysis.invoice.dirty_price,
    quoted_spread=Decimal('0.0225'),
    asset_swap_type=ASWType.PAR_PAR,
)
full_asset_swap_result = AssetSwapPricer().price(full_asset_swap, asset_swap_curves)

# %%
asset_swap_summary = pd.DataFrame(
    [
        {
            'par_spread': full_asset_swap_result.par_spread,
            'quoted_spread': full_asset_swap_result.breakdown.quoted_spread,
            'funding_component': full_asset_swap_result.funding_component,
            'credit_component': full_asset_swap_result.credit_component,
            'present_value': full_asset_swap_result.present_value,
        }
    ]
)
reference_rate_breakdown = pd.DataFrame(
    [
        {
            'component': 'Repo vs GC',
            'value': full_asset_swap_result.breakdown.reference_rates.repo_vs_gc,
        },
        {
            'component': 'GC vs unsecured overnight',
            'value': full_asset_swap_result.breakdown.reference_rates.gc_vs_unsecured_overnight,
        },
        {
            'component': 'Unsecured overnight vs term',
            'value': full_asset_swap_result.breakdown.reference_rates.unsecured_overnight_vs_term,
        },
        {
            'component': 'Total funding basis',
            'value': full_asset_swap_result.breakdown.reference_rates.total_funding_basis,
        },
    ]
)

display_table(
    asset_swap_summary,
    percent_columns=['par_spread', 'quoted_spread', 'funding_component', 'credit_component'],
    float_columns=['present_value'],
)

display_table(
    reference_rate_breakdown,
    percent_columns=['value'],
)

# %% [markdown]
# ## Callable bond and OAS
#
# Once a call schedule is present, spread measures that ignore embedded optionality can mislead. The option-adjusted spread separates the option cost from the bond's underlying credit/rates compensation.

# %%
callable_base = (
    FixedBondBuilder.new()
    .with_issue_date(settlement)
    .with_maturity_date(settlement.add_years(5))
    .with_coupon_rate(Decimal('0.0500'))
    .with_frequency(Frequency.SEMI_ANNUAL)
    .with_currency(Currency.USD)
    .build()
)
callable_bond = (
    CallableBondBuilder.new()
    .with_base_bond(callable_base)
    .add_call(call_date=settlement.add_years(2), call_price=Decimal('101.00'), call_type=CallType.EUROPEAN)
    .add_call(call_date=settlement.add_years(3), call_price=Decimal('100.50'), call_type=CallType.EUROPEAN)
    .build()
)
model = HullWhiteModel(mean_reversion=Decimal('0.03'), volatility=Decimal('0.01'), term_structure=discount_curve)
oas_calculator = OASCalculator(model=model)
market_price = Decimal('101.47670133056229')
oas = oas_calculator.calculate(callable_bond, market_price, settlement)
callable_summary = pd.DataFrame(
    [
        {'metric': 'OAS', 'value': oas},
        {'metric': 'Option value', 'value': oas_calculator.option_value(callable_bond, oas, settlement)},
        {'metric': 'Effective duration', 'value': oas_calculator.effective_duration(callable_bond, oas, settlement)},
        {'metric': 'Effective convexity', 'value': oas_calculator.effective_convexity(callable_bond, oas, settlement)},
    ]
)

display_table(callable_summary, percent_columns=['value'])

# %% [markdown]
# The plain bond shows how the spread labels line up against different benchmark conventions. The callable bond reminds us that spread compression can be partly an option story rather than pure carry or credit.

# %%
oas_grid = np.linspace(float(oas - Decimal('0.0100')), float(oas + Decimal('0.0100')), 9)
oas_sensitivity = pd.DataFrame(
    {
        'oas': oas_grid,
        'price': [oas_calculator.price_with_oas(callable_bond, value, settlement) for value in oas_grid],
    }
)

figure, axes = plt.subplots(1, 2, figsize=(12, 4.8))
axes[0].bar(plain_spreads['measure'], plain_spreads['spread_bps'])
axes[0].set_title('Spread Decomposition For The Plain Bond')
axes[0].set_xlabel('Measure')
axes[0].set_ylabel('Spread (bps)')

axes[1].plot(10000 * oas_sensitivity['oas'], oas_sensitivity['price'], marker='o', linewidth=2)
axes[1].axvline(10000 * float(oas), color='#8c2f39', linestyle='--', linewidth=1.5)
axes[1].set_title('Callable Price Versus OAS')
axes[1].set_xlabel('OAS (bps)')
axes[1].set_ylabel('Price')
plt.tight_layout()
figure

# %% [markdown]
# ## Choosing the measure in practice
#
# - Use g-spread when the investor really benchmarks against sovereign curves.
# - Use i-spread when swaps are the live funding or hedging reference.
# - Use z-spread when you want a discounted-cashflow spread that is internally consistent with the chosen curve.
# - Use asset-swap spread when the trade is genuinely being compared to a floating funding package.
# - Use OAS when optionality changes expected cashflow timing and plain spread measures overstate carry.

# %% [markdown]
# ## Local RV from a fitted bond curve
#
# Once we move from one bond to a local cross section, the question changes from "what is the spread?" to "which bond screens rich or cheap relative to the fitted curve?" We fit a small Treasury-like cross section, rank residuals, and then turn the ranking into switch, butterfly, constant-maturity, and new-issue analytics.

# %%
from fuggers_py.analytics.rv import (
    BondSignal,
    MaturitySignal,
    NewIssueRequest,
    bond_signal_workflow,
    construct_bond_switch,
    construct_butterfly,
    estimate_new_issue_fair_value,
    generate_constant_maturity_benchmark,
    maturity_signal_workflow,
    rank_rich_cheap,
)
from fuggers_py.curves import RegressionExposure

# %%
global_rv_demo = build_global_rv_demo_bundle(reference_date=Date.from_ymd(2026, 1, 15))
rv_fit = global_rv_demo.fit_result
rv_ranking = rank_rich_cheap(rv_fit, threshold_bps=Decimal("1.0"))

ranking_table = pd.DataFrame(
    [
        {
            "rank": signal.rank,
            "instrument_id": signal.instrument_id.as_str(),
            "classification": signal.classification,
            "bp_residual": signal.bp_residual,
            "z_score": signal.z_score,
        }
        for signal in rv_ranking
    ]
)

display_table(ranking_table, bps_columns=["bp_residual"], float_columns=["z_score"])

# %% [markdown]
# The positive sign convention is explicit here: positive `bp_residual` means cheap, negative means rich. We can therefore buy the cheap bond, fund it against the rich bond, and inspect a duration-neutral butterfly around the belly.

# %%
switch_trade = construct_bond_switch(rv_fit)
butterfly_trade = construct_butterfly(
    rv_fit,
    short_wing_instrument_id="UST4Y",
    body_instrument_id="UST6Y",
    long_wing_instrument_id="UST10Y",
)

switch_table = pd.DataFrame(
    [
        {
            "cheap_instrument_id": switch_trade.cheap_instrument_id.as_str(),
            "rich_instrument_id": switch_trade.rich_instrument_id.as_str(),
            "buy_notional": switch_trade.buy_notional,
            "sell_notional": switch_trade.sell_notional,
            "duration_hedge_ratio": switch_trade.duration_hedge_ratio,
            "expected_bp_convergence": switch_trade.expected_bp_convergence,
            "expected_price_convergence": switch_trade.expected_price_convergence,
        }
    ]
)
butterfly_table = pd.DataFrame(
    [
        {
            "short_wing": butterfly_trade.short_wing_instrument_id.as_str(),
            "body": butterfly_trade.body_instrument_id.as_str(),
            "long_wing": butterfly_trade.long_wing_instrument_id.as_str(),
            "short_weight": butterfly_trade.short_wing_weight,
            "body_weight": butterfly_trade.body_weight,
            "long_weight": butterfly_trade.long_wing_weight,
            "signal_bps": butterfly_trade.signal_bps,
        }
    ]
)

display_table(
    switch_table,
    bps_columns=["expected_bp_convergence"],
    float_columns=["buy_notional", "sell_notional", "duration_hedge_ratio", "expected_price_convergence"],
)

display_table(
    butterfly_table,
    bps_columns=["signal_bps"],
    float_columns=["short_weight", "body_weight", "long_weight"],
)

# %% [markdown]
# The same fitted surface can generate a constant-maturity benchmark and a hypothetical new-issue fair value. That keeps the relative-value toolkit internally consistent with the fitted cross section rather than mixing in a separate benchmark curve.

# %%
constant_maturity = generate_constant_maturity_benchmark(rv_fit, Decimal("7.0"))
new_issue = estimate_new_issue_fair_value(
    rv_fit,
    NewIssueRequest(
        issue_date=rv_fit.reference_date,
        maturity_date=rv_fit.reference_date.add_years(7),
        coupon_rate=Decimal("0.0340"),
        marketed_clean_price=Decimal("99.65"),
        regression_exposures=(RegressionExposure("liquidity", Decimal("0.30")),),
    ),
)

constant_maturity_table = pd.DataFrame(
    [
        {
            "target_maturity_years": constant_maturity.target_maturity_years,
            "coupon_rate": constant_maturity.coupon_rate,
            "fair_value_clean_price": constant_maturity.fair_value_clean_price,
            "fair_value_yield": constant_maturity.fair_value_yield,
        }
    ]
)
new_issue_table = pd.DataFrame(
    [
        {
            "fair_value_clean_price": new_issue.fair_value_clean_price,
            "fair_value_yield": new_issue.fair_value_yield,
            "concession_price": new_issue.concession_price,
            "concession_bps": new_issue.concession_bps,
        }
    ]
)
benchmark_components = pd.DataFrame(
    [
        {
            "instrument_id": component.instrument_id.as_str(),
            "weight": component.weight,
            "maturity_years": component.maturity_years,
            "fair_value_yield": component.fair_value_yield,
        }
        for component in constant_maturity.benchmark.components
    ]
)

display_table(
    constant_maturity_table,
    percent_columns=["coupon_rate", "fair_value_yield"],
    float_columns=["target_maturity_years", "fair_value_clean_price"],
)

display_table(
    new_issue_table,
    percent_columns=["fair_value_yield"],
    bps_columns=["concession_bps"],
    float_columns=["fair_value_clean_price", "concession_price"],
)

display_table(
    benchmark_components,
    percent_columns=["weight", "fair_value_yield"],
    float_columns=["maturity_years"],
)

# %%
rv_points = pd.DataFrame(
    [
        {
            "instrument_id": point.instrument_id.as_str(),
            "maturity_years": float(point.maturity_years),
            "bp_residual": float(point.bp_residual),
        }
        for point in rv_fit.bonds
    ]
)

figure, axes = plt.subplots(1, 2, figsize=(12, 4.8))
axes[0].bar(rv_points["instrument_id"], rv_points["bp_residual"], color="#355070")
axes[0].axhline(0.0, color="black", linewidth=1)
axes[0].set_title("Local RV Residuals")
axes[0].set_xlabel("Instrument")
axes[0].set_ylabel("Residual (bps)")

axes[1].plot(rv_points["maturity_years"], rv_points["bp_residual"], marker="o", linewidth=2, color="#8c2f39")
axes[1].axhline(0.0, color="black", linewidth=1)
axes[1].set_title("Residual Term Structure")
axes[1].set_xlabel("Maturity (years)")
axes[1].set_ylabel("Residual (bps)")
plt.tight_layout()
figure

# %% [markdown]
# ## Deterministic RV Workflow Hooks
#
# External factor or richness models often give us scores, not trades. The workflow hooks keep that interface explicit: we feed in maturity or bond signals, map them onto the fitted-bond universe, and then turn the chosen bonds into a neutralized trade expression.

# %%
maturity_workflow = maturity_signal_workflow(
    rv_fit,
    signals=(
        MaturitySignal(name="belly_factor", target_maturity_years=Decimal("6.1"), score=Decimal("1.4")),
        MaturitySignal(name="front_end_factor", target_maturity_years=Decimal("4.0"), score=Decimal("-1.1")),
    ),
    benchmark_only=True,
    minimum_liquidity_score=Decimal("1.0"),
)
bond_workflow = bond_signal_workflow(
    rv_fit,
    signals=(
        BondSignal(name="cheap_external_signal", instrument_id="UST8Y", score=Decimal("2.0")),
        BondSignal(name="rich_external_signal", instrument_id="UST4Y", score=Decimal("-1.8")),
    ),
)

# %%
workflow_choices = pd.DataFrame(
    [
        {
            "workflow": "maturity_signal",
            "long_bond": maturity_workflow.long_choice.instrument_id.as_str(),
            "short_bond": maturity_workflow.short_choice.instrument_id.as_str(),
            "long_target_years": maturity_workflow.long_choice.target_maturity_years,
            "short_target_years": maturity_workflow.short_choice.target_maturity_years,
            "hedge_ratio": maturity_workflow.trade.hedge_ratio,
            "expected_bp_convergence": maturity_workflow.trade.expected_bp_convergence,
        },
        {
            "workflow": "bond_signal",
            "long_bond": bond_workflow.long_choice.instrument_id.as_str(),
            "short_bond": bond_workflow.short_choice.instrument_id.as_str(),
            "long_target_years": bond_workflow.long_choice.maturity_years,
            "short_target_years": bond_workflow.short_choice.maturity_years,
            "hedge_ratio": bond_workflow.trade.hedge_ratio,
            "expected_bp_convergence": bond_workflow.trade.expected_bp_convergence,
        },
    ]
)
workflow_trade_breakdown = pd.DataFrame(
    [
        {
            "workflow": "maturity_signal",
            "neutrality_target": maturity_workflow.trade.neutrality_target.value,
            "long_notional": maturity_workflow.trade.long_leg.notional,
            "short_notional": maturity_workflow.trade.short_leg.notional,
            "net_dv01": maturity_workflow.trade.net_dv01,
            "expected_price_convergence": maturity_workflow.trade.expected_price_convergence,
        },
        {
            "workflow": "bond_signal",
            "neutrality_target": bond_workflow.trade.neutrality_target.value,
            "long_notional": bond_workflow.trade.long_leg.notional,
            "short_notional": bond_workflow.trade.short_leg.notional,
            "net_dv01": bond_workflow.trade.net_dv01,
            "expected_price_convergence": bond_workflow.trade.expected_price_convergence,
        },
    ]
)

display_table(
    workflow_choices,
    bps_columns=["expected_bp_convergence"],
    float_columns=["long_target_years", "short_target_years", "hedge_ratio"],
)

display_table(
    workflow_trade_breakdown,
    float_columns=["long_notional", "short_notional", "net_dv01", "expected_price_convergence"],
)

# %% [markdown]
# ## CDS stack: bootstrap, pricing, and basis
#
# The credit-domain layer now adds a first-class CDS product, a survival-curve bootstrap, and a small set of local relative-value helpers. We start from quoted CDS spreads, bootstrap a survival curve, and then price an off-the-run contract against that calibrated curve.

# %%
from fuggers_py.credit import (
    CdsPricer,
    CdsQuote,
    CreditDefaultSwap,
    adjusted_cds_breakdown,
    bond_cds_basis_breakdown,
    bootstrap_credit_curve,
    proxy_risk_free_breakdown,
)

# %%
cds_demo = build_cds_demo_bundle(reference_date=Date.from_ymd(2026, 1, 15))
cds_reference_date = cds_demo.reference_date
cds_discount_curve = cds_demo.discount_curve
cds_quotes = list(cds_demo.quote_strip)
cds_bootstrap = bootstrap_credit_curve(
    cds_quotes,
    valuation_date=cds_reference_date,
    discount_curve=cds_discount_curve,
    recovery_rate=cds_demo.primary_quote.recovery_rate,
)
cds_curve = cds_bootstrap.credit_curve

# %%
cds_bootstrap_table = pd.DataFrame(
    [
        {
            "instrument_id": point.instrument_id.as_str(),
            "maturity_tenor": point.maturity_tenor,
            "market_par_spread": point.market_par_spread,
            "fitted_par_spread": point.fitted_par_spread,
            "survival_probability": point.survival_probability,
            "hazard_rate": point.hazard_rate,
        }
        for point in cds_bootstrap.points
    ]
)

display_table(
    cds_bootstrap_table,
    percent_columns=["market_par_spread", "fitted_par_spread", "hazard_rate"],
    float_columns=["maturity_tenor", "survival_probability"],
)

# %%
cds_grid = pd.DataFrame(
    {
        "tenor_years": [1.0, 2.0, 3.0, 5.0, 7.0],
        "survival_probability": [
            float(cds_curve.survival_probability(cds_reference_date.add_days(int(365 * tenor))))
            for tenor in [1.0, 2.0, 3.0, 5.0, 7.0]
        ],
        "credit_spread": [
            float(cds_curve.credit_spread(cds_reference_date.add_days(int(365 * tenor))))
            for tenor in [1.0, 2.0, 3.0, 5.0, 7.0]
        ],
    }
)

figure, axes = plt.subplots(1, 2, figsize=(12, 4.6))
axes[0].plot(cds_grid["tenor_years"], cds_grid["survival_probability"], marker="o", linewidth=2, color="#355070")
axes[0].set_title("Bootstrapped Survival Curve")
axes[0].set_xlabel("Tenor (years)")
axes[0].set_ylabel("Survival probability")

axes[1].plot(10000 * cds_grid["credit_spread"], cds_grid["tenor_years"], marker="o", linewidth=2, color="#8c2f39")
axes[1].set_title("CDS-Implied Credit Spread")
axes[1].set_xlabel("Spread (bps)")
axes[1].set_ylabel("Tenor (years)")
plt.tight_layout()
figure

# %%
off_the_run_cds = cds_demo.off_the_run_cds
cds_pricer = CdsPricer()
cds_result = cds_pricer.price(
    off_the_run_cds,
    AnalyticsCurves(discount_curve=cds_discount_curve, credit_curve=cds_curve),
)
cds_pricing_table = pd.DataFrame(
    [
        {
            "par_spread": cds_result.par_spread,
            "running_spread": off_the_run_cds.running_spread,
            "fair_upfront": cds_result.upfront,
            "present_value": cds_result.present_value,
            "risky_pv01": cds_result.risky_pv01,
            "cs01": cds_result.cs01,
        }
    ]
)

display_table(
    cds_pricing_table,
    percent_columns=["par_spread", "running_spread", "fair_upfront"],
    float_columns=["present_value", "risky_pv01", "cs01"],
)

# %% [markdown]
# The sign conventions stay explicit:
#
# - `fair_upfront` is the fraction of notional paid by the protection buyer.
# - `present_value` and `cs01` are from the contract side, so a protection buyer has positive `cs01`.
# - The bootstrap itself stays generic by returning a standard `CreditCurve` wrapper over survival probabilities.

# %%
adjusted_cds = adjusted_cds_breakdown(
    quoted_spread=cds_result.par_spread,
    delivery_option_adjustment=cds_demo.delivery_option_adjustment,
    fx_adjustment=cds_demo.fx_adjustment,
)
basis_breakdown = bond_cds_basis_breakdown(
    bond_spread=cds_demo.bond_spread,
    cds_spread=cds_result.par_spread,
    delivery_option_adjustment=cds_demo.delivery_option_adjustment,
    fx_adjustment=cds_demo.fx_adjustment,
)
proxy_breakdown = proxy_risk_free_breakdown(
    bond_yield=cds_demo.bond_yield,
    cds_spread=cds_result.par_spread,
    delivery_option_adjustment=cds_demo.delivery_option_adjustment,
    fx_adjustment=cds_demo.fx_adjustment,
    liquidity_adjustment=cds_demo.liquidity_adjustment,
    funding_adjustment=cds_demo.funding_adjustment,
)
credit_basis_table = pd.DataFrame(
    [
        {
            "metric": "Adjusted CDS spread",
            "value": adjusted_cds.adjusted_spread,
        },
        {
            "metric": "Bond/CDS basis",
            "value": basis_breakdown.basis,
        },
        {
            "metric": "Proxy risk-free rate",
            "value": proxy_breakdown.proxy_risk_free_rate,
        },
    ]
)

display_table(credit_basis_table, percent_columns=["value"])

# %% [markdown]
# ## Cross-currency basis and global RV
#
# Global RV work is still deterministic here. We start from a EUR fixed-rate bond, transform it into USD SOFR floating cashflows with an asset swap, a same-currency basis swap, and a EUR/USD CCBS, and then compare the resulting USD expressions with both a fitted-curve fixed-cashflow yardstick and a USD SOFR floating yardstick.

# %%
from fuggers_py.analytics.rv import (
    bond_to_common_currency_fixed,
    bond_to_common_currency_floating,
    decompose_floating_view_links,
    global_fixed_cashflow_rv,
    global_usd_sofr_rv,
    usd_sofr_adjusted_rv_measure,
)

# %%
global_reference_date = global_rv_demo.reference_date
global_curves = global_rv_demo.curves
euro_asset_swap = global_rv_demo.euro_asset_swap
eur_basis_swap = global_rv_demo.local_basis_swap
eur_usd_ccbs = global_rv_demo.cross_currency_basis_swap

# %%
global_floating_view = bond_to_common_currency_floating(
    euro_asset_swap,
    global_curves,
    local_basis_swap=eur_basis_swap,
    cross_currency_basis_swap=eur_usd_ccbs,
)
global_fixed_view = bond_to_common_currency_fixed(
    euro_asset_swap,
    global_curves,
    local_basis_swap=eur_basis_swap,
    cross_currency_basis_swap=eur_usd_ccbs,
)
global_link_breakdown = decompose_floating_view_links(
    global_floating_view,
    adjusted_cds_spread=global_rv_demo.adjusted_cds_spread,
)
usd_sofr_measure = usd_sofr_adjusted_rv_measure(
    global_floating_view,
    yardstick_spread=global_rv_demo.yardstick_spread,
    adjusted_cds_spread=global_rv_demo.adjusted_cds_spread,
)
fixed_cashflow_global_rv = global_fixed_cashflow_rv(
    euro_asset_swap,
    global_curves,
    rv_fit,
    local_basis_swap=eur_basis_swap,
    cross_currency_basis_swap=eur_usd_ccbs,
)
usd_sofr_global_rv = global_usd_sofr_rv(
    euro_asset_swap,
    global_curves,
    yardstick_spread=global_rv_demo.yardstick_spread,
    adjusted_cds_spread=global_rv_demo.adjusted_cds_spread,
    local_basis_swap=eur_basis_swap,
    cross_currency_basis_swap=eur_usd_ccbs,
)

# %%
global_cashflow_table = pd.DataFrame(
    [
        {"metric": "Asset-swap spread", "value": global_floating_view.asset_swap_spread},
        {"metric": "Same-currency basis", "value": global_floating_view.same_currency_basis},
        {"metric": "Cross-currency basis", "value": global_floating_view.cross_currency_basis},
        {"metric": "USD SOFR floating spread", "value": global_floating_view.common_currency_floating_spread},
        {"metric": "USD fixed-equivalent rate", "value": global_fixed_view.common_currency_fixed_rate},
        {"metric": "Fitted USD fixed yardstick", "value": fixed_cashflow_global_rv.fitted_curve_yardstick},
    ]
)
global_rv_table = pd.DataFrame(
    [
        {"metric": "Funding basis total", "value": global_link_breakdown.funding_basis_total},
        {"metric": "Adjusted CDS spread", "value": global_link_breakdown.adjusted_cds_spread},
        {"metric": "USD SOFR yardstick", "value": usd_sofr_measure.yardstick_spread},
        {"metric": "USD SOFR RV residual", "value": usd_sofr_measure.residual_to_yardstick},
    ]
)

display_table(global_cashflow_table, percent_columns=["value"])
display_table(global_rv_table, percent_columns=["value"])

# %% [markdown]
# The link decomposition stays explicit:
#
# - `asset_swap_spread` is the starting floating spread on the bond's home-currency term index.
# - `same_currency_basis` moves that spread onto the home-currency overnight or front-end reference.
# - `cross_currency_basis` then moves the bond into the target currency.
# - `usd_sofr_adjusted_rv_measure` keeps the final USD SOFR spread, the chosen yardstick, and the optional adjusted-CDS comparison separate so the RV call is auditable.

# %% [markdown]
# ## Balance-sheet overlays
#
# The base pricing logic stays clean: asset-swap, reference-rate, and global RV measures are computed first, and then haircut, capital, and shadow-cost overlays are added explicitly afterwards. That makes the institution-specific assumptions transparent and reusable across workflows.

# %%
from fuggers_py.analytics.spreads import (
    BalanceSheetSpreadOverlay,
    CapitalSpreadAdjustment,
    HaircutSpreadAdjustment,
    ShadowCostSpreadAdjustment,
)

# %%
balance_sheet_overlay = BalanceSheetSpreadOverlay(
    adjustments=(
        HaircutSpreadAdjustment(
            collateral_value=Decimal("100"),
            haircut=Decimal("0.02"),
            repo_rate=Decimal("0.0310"),
            haircut_funding_rate=Decimal("0.0450"),
            year_fraction=Decimal("0.50"),
        ),
        CapitalSpreadAdjustment(
            exposure=Decimal("100"),
            risk_weight=Decimal("0.35"),
            capital_ratio=Decimal("0.10"),
            hurdle_rate=Decimal("0.12"),
            pass_through=Decimal("0.50"),
        ),
        ShadowCostSpreadAdjustment(
            shadow_cost_rate=Decimal("0.0015"),
            utilization=Decimal("0.80"),
            pass_through=Decimal("0.75"),
        ),
    )
)
adjusted_asw_overlay = balance_sheet_overlay.apply_to_funding_spread(
    base_funding_spread=full_asset_swap_result.funding_component,
    credit_spread=full_asset_swap_result.credit_component,
)
adjusted_global_overlay = balance_sheet_overlay.apply(base_spread=usd_sofr_measure.usd_sofr_spread)

# %%
overlay_components_table = pd.DataFrame(
    [
        {
            "component": component.name,
            "spread": component.spread_adjustment,
        }
        for component in adjusted_asw_overlay.overlay_summary.components
    ]
)
overlay_summary_table = pd.DataFrame(
    [
        {
            "measure": "ASW funding component",
            "base": adjusted_asw_overlay.base_funding_spread,
            "adjusted": adjusted_asw_overlay.adjusted_funding_spread,
        },
        {
            "measure": "ASW all-in spread",
            "base": full_asset_swap_result.par_spread,
            "adjusted": adjusted_asw_overlay.adjusted_all_in_spread,
        },
        {
            "measure": "USD SOFR spread",
            "base": usd_sofr_measure.usd_sofr_spread,
            "adjusted": adjusted_global_overlay.adjusted_spread,
        },
        {
            "measure": "USD SOFR residual",
            "base": usd_sofr_measure.residual_to_yardstick,
            "adjusted": adjusted_global_overlay.adjusted_spread - usd_sofr_measure.yardstick_spread,
        },
    ]
)

display_table(overlay_components_table, percent_columns=["spread"])
display_table(overlay_summary_table, percent_columns=["base", "adjusted"])

# %% [markdown]
# ## Rates options and stochastic delivery-option overlays
#
# The same deterministic rates stack now supports simple options products. Swaptions and caps/floors reuse `AnalyticsCurves.vol_surface`, while futures options can still be tied back to the government-bond-futures fair-value machinery and an externally parameterized delivery-option model.

# %%
from fuggers_py.analytics.options import option_rv_signal
from fuggers_py.data import VolPoint, VolQuoteType, VolSurfaceId, VolSurfaceType, VolatilitySurface, YearMonth
from fuggers_py.rates import (
    BachelierPricer,
    Black76Pricer,
    CapFloor,
    CapFloorType,
    FixedFloatSwap,
    FixedLegSpec,
    FuturesOption,
    Swaption,
    basis_metrics,
    cheapest_to_deliver,
    fair_futures_price,
    invoice_breakdown,
    oabpv,
)

# %%
options_reference_date = Date.from_ymd(2026, 1, 15)
options_discount_curve = (
    DiscountCurveBuilder(reference_date=options_reference_date)
    .add_zero_rate(1.0, Decimal("0.0300"))
    .add_zero_rate(10.0, Decimal("0.0325"))
    .build()
)
options_projection_curve = (
    DiscountCurveBuilder(reference_date=options_reference_date)
    .add_zero_rate(1.0, Decimal("0.0350"))
    .add_zero_rate(10.0, Decimal("0.0365"))
    .build()
)
swaption_surface = VolatilitySurface(
    surface_id=VolSurfaceId("usd.swaption"),
    surface_type=VolSurfaceType.SWAPTION,
    as_of=options_reference_date,
    points=(
        VolPoint(
            expiry=YearMonth(2026, 7),
            tenor=YearMonth(2031, 7),
            strike=Decimal("0.0350"),
            volatility=Decimal("0.20"),
            quote_type=VolQuoteType.LOGNORMAL,
        ),
    ),
)
cap_floor_surface = VolatilitySurface(
    surface_id=VolSurfaceId("usd.capfloor"),
    surface_type=VolSurfaceType.CAP_FLOOR,
    as_of=options_reference_date,
    points=(
        VolPoint(
            expiry=YearMonth(2026, 4),
            tenor=YearMonth(2026, 7),
            strike=Decimal("0.0300"),
            volatility=Decimal("0.0100"),
            quote_type=VolQuoteType.NORMAL,
        ),
        VolPoint(
            expiry=YearMonth(2026, 7),
            tenor=YearMonth(2026, 10),
            strike=Decimal("0.0300"),
            volatility=Decimal("0.0100"),
            quote_type=VolQuoteType.NORMAL,
        ),
    ),
)

# %%
swaption_curves = AnalyticsCurves(
    discount_curve=options_discount_curve,
    forward_curve=options_projection_curve,
    vol_surface=swaption_surface,
    projection_curves={"USD-SOFR-3M": options_projection_curve},
)
cap_floor_curves = AnalyticsCurves(
    discount_curve=options_discount_curve,
    forward_curve=options_projection_curve,
    vol_surface=cap_floor_surface,
    projection_curves={"USD-SOFR-3M": options_projection_curve},
)
underlying_swap = FixedFloatSwap(
    effective_date=Date.from_ymd(2026, 7, 15),
    maturity_date=Date.from_ymd(2031, 7, 15),
    fixed_leg=FixedLegSpec(
        pay_receive=PayReceive.PAY,
        notional=Decimal("1000000"),
        fixed_rate=Decimal("0.0350"),
        currency=Currency.USD,
    ),
    floating_leg=FloatingLegSpec(
        pay_receive=PayReceive.RECEIVE,
        notional=Decimal("1000000"),
        index_name="SOFR",
        index_tenor="3M",
        currency=Currency.USD,
    ),
)
swaption = Swaption(
    expiry_date=Date.from_ymd(2026, 7, 15),
    underlying_swap=underlying_swap,
    strike=Decimal("0.0350"),
    exercise_into=PayReceive.PAY,
)
cap = CapFloor(
    effective_date=options_reference_date,
    maturity_date=Date.from_ymd(2026, 10, 15),
    floating_leg=FloatingLegSpec(
        pay_receive=PayReceive.RECEIVE,
        notional=Decimal("1000000"),
        index_name="SOFR",
        index_tenor="3M",
        currency=Currency.USD,
    ),
    strike=Decimal("0.0300"),
    cap_floor_type=CapFloorType.CAP,
)
swaption_result = Black76Pricer().swaption(swaption, swaption_curves, valuation_date=options_reference_date)
cap_result = BachelierPricer().cap_floor(cap, cap_floor_curves, valuation_date=options_reference_date)
swaption_rv = option_rv_signal(
    implied_volatility=swaption_result.volatility,
    realized_volatility=Decimal("0.1750"),
    greeks=swaption_result,
)

# %%
futures_demo = build_futures_demo_bundle(as_of=Date.from_ymd(2025, 12, 31))
delivery_contract = futures_demo.contract
delivery_basket = futures_demo.basket
delivery_option_model = futures_demo.stochastic_delivery_option_model
futures_option = FuturesOption(
    expiry_date=Date.from_ymd(2026, 2, 15),
    underlying_future=delivery_contract,
    strike=Decimal("110"),
)
futures_option_result = Black76Pricer().futures_option(
    futures_option,
    curves=AnalyticsCurves(discount_curve=options_discount_curve),
    basket=delivery_basket,
    delivery_option_model=delivery_option_model,
    volatility=Decimal("0.12"),
    valuation_date=options_reference_date,
)

# %%
options_summary = pd.DataFrame(
    [
        {
            "instrument": "payer swaption",
            "present_value": swaption_result.present_value,
            "vega": swaption_result.greeks.vega,
            "implied_minus_realized": swaption_rv.volatility_gap,
        },
        {
            "instrument": "cap strip",
            "present_value": cap_result.present_value,
            "vega": cap_result.greeks.vega,
            "implied_minus_realized": np.nan,
        },
        {
            "instrument": "futures option",
            "present_value": futures_option_result.present_value,
            "vega": futures_option_result.greeks.vega,
            "implied_minus_realized": np.nan,
        },
    ]
)
delivery_option_scenarios = pd.DataFrame(
    [
        {
            "scenario": scenario.scenario_label,
            "yield_shift_bps": scenario.yield_shift_bps,
            "ctd": scenario.cheapest_to_deliver.as_str(),
            "probability": scenario.probability,
            "switching_benefit": scenario.switching_benefit,
        }
        for scenario in delivery_option_model.delivery_option_adjustment(delivery_contract, delivery_basket).scenarios
    ]
)

display_table(options_summary, float_columns=["present_value", "vega"], percent_columns=["implied_minus_realized"])
display_table(delivery_option_scenarios, bps_columns=["yield_shift_bps"], percent_columns=["probability"], float_columns=["switching_benefit"])

# %% [markdown]
# ## Desk Summary Tear Sheet
#
# The notebook stays modular above, but a desk note usually needs one compact view that lines up the plain spread stack, the full ASW decomposition, CDS adjustments, global RV translation, balance-sheet overlays, and the options / futures add-ons.

# %%
futures_ctd = cheapest_to_deliver(delivery_contract, delivery_basket, futures_demo.future_quote.price)
futures_ctd_bond = delivery_basket.get_deliverable(futures_ctd.cheapest_to_deliver)
futures_invoice = invoice_breakdown(
    delivery_contract.contract_size,
    futures_demo.future_quote.price,
    futures_ctd.conversion_factor,
    futures_ctd_bond.accrued_interest(delivery_contract.resolved_delivery_date()),
)
futures_fair = fair_futures_price(
    delivery_contract,
    delivery_basket,
    delivery_option_model=delivery_option_model,
)
futures_basis = basis_metrics(
    futures_demo.future_quote.price,
    futures_ctd.conversion_factor,
    futures_ctd_bond.clean_price,
    delivery_option_value=futures_fair.delivery_option_adjustment,
)
desk_summary = pd.DataFrame(
    [
        {
            "desk_view": "Plain spreads",
            "headline_metric": "Z-spread",
            "value": plain_spreads.loc[plain_spreads["measure"] == "Z-spread", "spread_bps"].iloc[0] / Decimal("10000"),
            "secondary_metric": "G-spread",
            "secondary_value": plain_spreads.loc[plain_spreads["measure"] == "G-spread", "spread_bps"].iloc[0] / Decimal("10000"),
            "note": "Cash bond against government and discount benchmarks.",
        },
        {
            "desk_view": "Full ASW",
            "headline_metric": "Par spread",
            "value": full_asset_swap_result.par_spread,
            "secondary_metric": "Funding component",
            "secondary_value": full_asset_swap_result.funding_component,
            "note": "Repo, GC, and unsecured term pieces stay separated.",
        },
        {
            "desk_view": "Adjusted CDS",
            "headline_metric": "Adjusted CDS",
            "value": adjusted_cds.adjusted_spread,
            "secondary_metric": "Bond/CDS basis",
            "secondary_value": basis_breakdown.basis,
            "note": "Delivery-option and FX adjustments stay explicit.",
        },
        {
            "desk_view": "Global RV",
            "headline_metric": "USD SOFR spread",
            "value": usd_sofr_measure.usd_sofr_spread,
            "secondary_metric": "Residual to yardstick",
            "secondary_value": usd_sofr_measure.residual_to_yardstick,
            "note": "EUR bond translated into common-currency floating space.",
        },
        {
            "desk_view": "Balance-sheet overlay",
            "headline_metric": "Adjusted ASW",
            "value": adjusted_asw_overlay.adjusted_all_in_spread,
            "secondary_metric": "Overlay add-on",
            "secondary_value": adjusted_asw_overlay.overlay_summary.total_adjustment,
            "note": "Haircut, capital, and shadow-cost add-ons are modular.",
        },
        {
            "desk_view": "Options RV",
            "headline_metric": "Swaption vol gap",
            "value": swaption_rv.volatility_gap,
            "secondary_metric": "Vega notional",
            "secondary_value": swaption_rv.vega_notional,
            "note": "Implied-versus-realized vol plus scaling.",
        },
        {
            "desk_view": "Futures delivery",
            "headline_metric": "Net basis",
            "value": futures_basis.net_basis,
            "secondary_metric": "OABPV",
            "secondary_value": oabpv(delivery_contract, delivery_basket, delivery_option_model=delivery_option_model),
            "note": f"CTD {futures_ctd.cheapest_to_deliver.as_str()}, invoice {futures_invoice.invoice_amount:.2f}.",
        },
    ]
)

display_table(desk_summary, percent_columns=["value"], float_columns=["secondary_value"])
