# ---
# 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]
# # End-To-End Research Pipeline
#
# This is the flagship deterministic workflow: load fixtures, build the engine, construct curves, price a mixed basket, and roll the outputs into ETF and portfolio views from the same run.

# %%
from __future__ import annotations

from dataclasses import asdict
from decimal import Decimal

import matplotlib

matplotlib.use("Agg")

import matplotlib.pyplot as plt
import pandas as pd

from fuggers_py.core import Date
from fuggers_py.data import EtfHolding, InstrumentId, QuoteSide
from fuggers_py.engine import EtfPricer, PortfolioAnalyzer, PortfolioPosition, PricingEngineBuilder, PricingInput
from fuggers_py.io import create_empty_output

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

apply_research_style()

# %% [markdown]
# ## Load raw inputs
#
# The notebook uses the curated fixture set, so every run starts from the same quotes, fixings, and bond reference records.

# %%
market_data = load_fixture_market_data()
reference_data = load_fixture_reference_data()
settlement = Date.from_ymd(2026, 3, 13)
engine = (
    PricingEngineBuilder()
    .with_market_data_provider(market_data)
    .with_reference_data_provider(reference_data)
    .with_output_publisher(create_empty_output())
    .with_settlement_date(settlement)
    .build()
)
curve_bundle = engine.curve_builder.bundle(discount_curve='usd.discount')
curve_points = pd.DataFrame(
    {
        'tenor_years': [float(point.tenor) for point in market_data.get_curve_inputs('usd.discount').points],
        'zero_rate': [float(point.value) for point in market_data.get_curve_inputs('usd.discount').points],
    }
)

# %% [markdown]
# ## Price the mixed basket
#
# The fixture universe includes a fixed-rate corporate and a callable bond. We take market quotes from the file-backed provider and route both instruments through the engine's batch workflow.

# %%
fixed_id = InstrumentId('US1234567890')
callable_id = InstrumentId('US0987654321')
fixed_reference = reference_data.get_bond_reference(fixed_id)
callable_reference = reference_data.get_bond_reference(callable_id)
fixed_quote = market_data.get_quote(fixed_id, QuoteSide.MID)
callable_quote = market_data.get_quote(callable_id, QuoteSide.BID)
pricing_inputs = [
    PricingInput(
        instrument=fixed_reference.to_instrument(),
        settlement_date=settlement,
        market_price=fixed_quote.value,
        curves=curve_bundle,
        instrument_id=fixed_id,
    ),
    PricingInput(
        instrument=callable_reference.to_instrument(),
        settlement_date=settlement,
        market_price=callable_quote.value,
        curves=curve_bundle,
        instrument_id=callable_id,
    ),
]
batch = engine.pricing_router.price_batch(pricing_inputs)
quote_outputs = {InstrumentId.parse(key): value for key, value in batch.outputs.items()}
pricing_table = pd.DataFrame(
    [
        {
            'instrument_id': instrument_id.as_str(),
            'pricing_path': output.pricing_path,
            'clean_price': output.clean_price,
            'yield_to_maturity': output.yield_to_maturity,
            'z_spread': output.z_spread,
            'oas': output.oas,
            'duration': output.modified_duration or output.effective_duration,
            'dv01': output.dv01,
        }
        for instrument_id, output in quote_outputs.items()
    ]
)

# %% [markdown]
# ## Aggregate into ETF and portfolio views
#
# The same quote outputs feed both an ETF-style basket summary and a traditional portfolio analytics pass.

# %%
holdings = [EtfHolding(fixed_id, quantity=Decimal('100')), EtfHolding(callable_id, quantity=Decimal('50'))]
positions = [PortfolioPosition(fixed_id, Decimal('100')), PortfolioPosition(callable_id, Decimal('50'))]
reference_map = {fixed_reference.instrument_id: fixed_reference, callable_reference.instrument_id: callable_reference}
etf_output = EtfPricer().price('fixture-etf', holdings, quote_outputs, shares_outstanding=Decimal('1000'))
portfolio_output = PortfolioAnalyzer().analyze('fixture-portfolio', positions, quote_outputs, reference_data=reference_map)
summary_table = pd.DataFrame(
    [
        {'stage': 'Inputs loaded', 'result': 'quotes + fixings + bond reference data'},
        {'stage': 'Curves built', 'result': 'usd.discount from fixture curve inputs'},
        {'stage': 'Basket priced', 'result': f'{len(quote_outputs)} instruments, {len(batch.errors)} failures'},
        {'stage': 'ETF view', 'result': f'NAV {etf_output.nav}'},
        {'stage': 'Portfolio view', 'result': f'Market value {portfolio_output.total_market_value}'},
    ]
)
portfolio_breakdown = pd.DataFrame(
    {'bucket': list(portfolio_output.sector_breakdown.keys()), 'market_value': list(portfolio_output.sector_breakdown.values())}
)

# %%
display_table(summary_table)
display_table(pricing_table, percent_columns=['yield_to_maturity', 'z_spread', 'oas'], float_columns=['clean_price', 'duration', 'dv01'])

# %% [markdown]
# The pipeline summary is deliberately compact: it should let a researcher audit the full run quickly before diving into the instrument-level analytics.

# %%
figure, axes = plt.subplots(1, 2, figsize=(12, 4.8))
axes[0].plot(curve_points['tenor_years'], 100 * curve_points['zero_rate'], marker='o', linewidth=2)
axes[0].set_title('Market Curve Feeding The Engine')
axes[0].set_xlabel('Tenor (years)')
axes[0].set_ylabel('Zero rate')
percent_axis(axes[0])

axes[1].bar(portfolio_breakdown['bucket'], portfolio_breakdown['market_value'])
axes[1].set_title('Portfolio Dashboard: Sector Market Value')
axes[1].set_xlabel('Sector')
axes[1].set_ylabel('Market value')
plt.tight_layout()
figure

# %%
figure, axes = plt.subplots(1, 2, figsize=(12, 4.8))
axes[0].bar(pricing_table['instrument_id'], pricing_table['clean_price'])
axes[0].set_title('Priced Basket')
axes[0].set_xlabel('Instrument')
axes[0].set_ylabel('Clean price')
axes[0].tick_params(axis='x', rotation=20)

axes[1].bar(['ETF NAV', 'Portfolio MV', 'Portfolio DV01'], [float(etf_output.nav), float(portfolio_output.total_market_value), float(portfolio_output.aggregate_dv01)])
axes[1].set_title('End-State Dashboard')
axes[1].set_xlabel('Metric')
axes[1].set_ylabel('Value')
plt.tight_layout()
figure

# %% [markdown]
# ## What changes when inputs move?
#
# A quote move changes the priced basket immediately; a curve move changes both the basket and the downstream ETF/portfolio analytics; a reference-data change can alter instrument construction itself. The point of the pipeline is to keep that whole chain explicit and reproducible.
