# ---
# 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]
# # Curve Builder And Batch Pricing Router
#
# This notebook takes market-style inputs into the engine's curve builder, assembles a small curve bundle, and routes fixed-rate, callable, and floating-rate instruments through one batch pricing workflow.

# %%
from __future__ import annotations

from dataclasses import asdict, replace
from decimal import Decimal

import matplotlib

matplotlib.use("Agg")

import matplotlib.pyplot as plt
import pandas as pd

from fuggers_py.bonds.types import Tenor
from fuggers_py.bonds import (
    BondIndex,
    CallType,
    CallableBondBuilder,
    FixedBond,
    FloatingRateNoteBuilder,
    IndexConventions,
    IndexFixingStore,
    OvernightCompounding,
    RateIndex,
    YieldCalculationRules,
)
from fuggers_py.core import Currency, Date
from fuggers_py.curves import MultiCurveEnvironmentBuilder, RateIndex as CurveRateIndex
from fuggers_py.data import AnalyticsCurves, CurveId, InstrumentId
from fuggers_py.engine import CurveBuilder, FundingPricingRouter, PricingInput, PricingRouter, RatesPricingRouter

from examples._shared.datasets import build_rates_engine_demo_bundle
from examples._shared.plotting import apply_research_style, percent_axis
from examples._shared.tables import display_table

apply_research_style()

# %%
engine_demo = build_rates_engine_demo_bundle(settlement=Date.from_ymd(2026, 1, 15))
settlement = engine_demo.settlement
curve_builder = CurveBuilder()
discount_points = list(engine_demo.discount_points)
government_points = list(engine_demo.government_points)
benchmark_points = list(engine_demo.benchmark_points)
forward_points = list(engine_demo.forward_points)
term_forward_points = list(engine_demo.term_forward_points)
discount_curve = curve_builder.add_zero_curve(CurveId('usd.discount'), discount_points, settlement)
government_curve = curve_builder.add_zero_curve(CurveId('usd.government'), government_points, settlement)
benchmark_curve = curve_builder.add_zero_curve(CurveId('usd.benchmark'), benchmark_points, settlement)
forward_curve = curve_builder.add_forward_curve(CurveId('usd.forward'), forward_points, settlement)
term_forward_curve = curve_builder.add_forward_curve(CurveId('usd.term6m'), term_forward_points, settlement)

curve_summary = pd.DataFrame(
    {
        'tenor_years': [float(point.tenor) for point in discount_points],
        'discount_zero': [float(point.value) for point in discount_points],
        'government_zero': [float(point.value) for point in government_points],
        'benchmark_zero': [float(point.value) for point in benchmark_points],
    }
)

fixed_bond = engine_demo.fixed_bond
callable_bond = engine_demo.callable_bond
frn = engine_demo.frn

router = PricingRouter()
curve_bundle = AnalyticsCurves(
    discount_curve=discount_curve,
    government_curve=government_curve,
    benchmark_curve=benchmark_curve,
    forward_curve=forward_curve,
)
pricing_inputs = [
    PricingInput(
        instrument=fixed_bond,
        settlement_date=settlement,
        market_price=engine_demo.fixed_bond_market_price,
        instrument_id=InstrumentId('SCENARIO-A-FIXED'),
        pricing_spec=engine_demo.fixed_pricing_spec,
        curves=curve_bundle,
    ),
    PricingInput(
        instrument=callable_bond,
        settlement_date=settlement,
        market_price=engine_demo.callable_bond_market_price,
        instrument_id=InstrumentId('SCENARIO-B-CALLABLE'),
        pricing_spec=engine_demo.callable_pricing_spec,
        curves=curve_bundle,
    ),
    PricingInput(
        instrument=frn,
        settlement_date=settlement,
        market_price=engine_demo.frn_market_price,
        instrument_id=InstrumentId('SCENARIO-C-FRN'),
        pricing_spec=engine_demo.frn_pricing_spec,
        curves=AnalyticsCurves(discount_curve=discount_curve, forward_curve=forward_curve),
    ),
]
batch = router.price_batch(pricing_inputs)
result_rows = []
for instrument_id, output in batch.outputs.items():
    row = asdict(output)
    row['instrument_id'] = instrument_id
    result_rows.append(row)
results = pd.DataFrame(result_rows)
results['duration'] = results['modified_duration'].fillna(results['effective_duration'])

sofr_3m = CurveRateIndex.new('SOFR', Tenor.parse('3M'), Currency.USD)
term_6m = CurveRateIndex.new('TERM', Tenor.parse('6M'), Currency.USD)
rates_multicurve = (
    MultiCurveEnvironmentBuilder()
    .add_discount_curve(Currency.USD, discount_curve)
    .add_projection_curve(sofr_3m, forward_curve)
    .add_projection_curve(term_6m, term_forward_curve)
    .build()
)
rates_curves = AnalyticsCurves(
    discount_curve=discount_curve,
    forward_curve=forward_curve,
    multicurve_environment=rates_multicurve,
    projection_curves={
        str(sofr_3m): forward_curve,
        str(term_6m): term_forward_curve,
    },
)
rates_router = RatesPricingRouter()
swap = engine_demo.swap
fra = engine_demo.fra
basis_swap = engine_demo.basis_swap
rates_outputs = [
    rates_router.price_swap(swap, curves=rates_curves),
    rates_router.price_fra(fra, curves=rates_curves),
    rates_router.price_basis_swap(basis_swap, curves=rates_curves),
]
rates_results = pd.DataFrame(asdict(output) for output in rates_outputs)
rates_results['instrument_id'] = [
    str(output.instrument_id) if output.instrument_id is not None else None
    for output in rates_outputs
]
funding_router = FundingPricingRouter()
repo_labels = [label for label, _ in engine_demo.repo_bundle.repo_batch]
repo_trades = [trade for _, trade in engine_demo.repo_bundle.repo_batch]
repo_outputs = funding_router.price_batch(repo_trades, repo_curve=engine_demo.repo_bundle.repo_curve)
repo_results = pd.DataFrame(
    {
        'trade_id': repo_labels,
        'cash_lent': [output.cash_lent for output in repo_outputs],
        'interest_amount': [output.interest_amount for output in repo_outputs],
        'repurchase_amount': [output.repurchase_amount for output in repo_outputs],
        'haircut_amount': [output.haircut_amount for output in repo_outputs],
        'curve_zero_rate': [output.curve_zero_rate for output in repo_outputs],
        'forward_rate': [output.forward_rate for output in repo_outputs],
        'funding_spread': [output.funding_spread for output in repo_outputs],
    }
)

# %%
display_table(curve_summary, percent_columns=['discount_zero', 'government_zero', 'benchmark_zero'], float_columns=['tenor_years'])
display_table(
    results[['instrument_id', 'pricing_path', 'clean_price', 'yield_to_maturity', 'z_spread', 'oas', 'discount_margin', 'duration', 'dv01']],
    percent_columns=['yield_to_maturity', 'z_spread', 'oas', 'discount_margin'],
    float_columns=['clean_price', 'duration', 'dv01'],
)
display_table(
    rates_results[['instrument_id', 'pricing_path', 'par_rate', 'forward_rate', 'basis_spread', 'present_value', 'pv01']],
    percent_columns=['par_rate', 'forward_rate', 'basis_spread'],
    float_columns=['present_value', 'pv01'],
)

# %% [markdown]
# ## Separate funding router
#
# Repo analytics now sit on their own engine path. The shared curve-builder output can feed a small batch of repo trades without widening the bond router, which keeps the domain split explicit: bonds on the bond router, tradable rates on the rates router, and secured funding on the funding router.

# %%
display_table(
    repo_results,
    percent_columns=['curve_zero_rate', 'forward_rate', 'funding_spread'],
    float_columns=['cash_lent', 'interest_amount', 'repurchase_amount', 'haircut_amount'],
)

# %% [markdown]
# The batch router is conceptually simple: curve bundle in, instrument metadata in, normalized quote outputs out. The value is that fixed, callable, and floating-rate instruments can share the same operational path while still landing on instrument-specific analytics.

# %% [markdown]
# ## Separate rates router
#
# Bond routing stays unchanged, but tradable rates products can now use a dedicated engine path with the same curve-builder output. That keeps the bond router narrow while still giving the desk a simple research surface for swaps, FRAs, and basis swaps.

# %%
figure, axes = plt.subplots(1, 2, figsize=(12, 4.8))
axes[0].plot(curve_summary['tenor_years'], 100 * curve_summary['discount_zero'], marker='o', linewidth=2, label='Discount')
axes[0].plot(curve_summary['tenor_years'], 100 * curve_summary['government_zero'], marker='o', linewidth=2, label='Government')
axes[0].plot(curve_summary['tenor_years'], 100 * curve_summary['benchmark_zero'], marker='o', linewidth=2, label='Benchmark')
axes[0].set_title('Engine-Built Curves')
axes[0].set_xlabel('Tenor (years)')
axes[0].set_ylabel('Zero rate')
percent_axis(axes[0])
axes[0].legend(title='')

axes[1].scatter(results['duration'], results['yield_to_maturity'].fillna(0), s=100)
for _, row in results.iterrows():
    axes[1].annotate(row['pricing_path'], (row['duration'], row['yield_to_maturity'] or 0), fontsize=9)
axes[1].set_title('Priced Universe: Yield Versus Duration')
axes[1].set_xlabel('Duration')
axes[1].set_ylabel('Yield to maturity')
percent_axis(axes[1])
plt.tight_layout()
figure

# %%
figure, axis = plt.subplots(figsize=(10, 4.2))
axis.bar(results['pricing_path'], results['clean_price'])
axis.set_title('Clean Prices By Routed Instrument Type')
axis.set_xlabel('Pricing path')
axis.set_ylabel('Clean price')
plt.tight_layout()
figure

# %% [markdown]
# ## Operational summary
#
# The router produced a single normalized result table while still preserving instrument-specific fields such as `oas` for the callable bond and `discount_margin` for the FRN. That is the main reason to centralize pricing through the engine rather than through one-off notebook code.
