# ---
# 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]
# # Market Data, Reference Data, And Pricing Specs
#
# The data layer separates live market observations from slower-moving reference terms and from the pricing instructions that tell the analytics stack how to use both. This notebook makes those boundaries explicit with a small synthetic bond universe.

# %%
from __future__ import annotations

from decimal import Decimal

import matplotlib

matplotlib.use("Agg")

import matplotlib.pyplot as plt
import pandas as pd

from fuggers_py.credit import bootstrap_credit_curve
from fuggers_py.core import Currency, Date, Frequency
from fuggers_py.data import (
    AnalyticsCurves,
    BondReferenceData,
    BondType,
    CallScheduleEntry,
    CurveId,
    CurveInputs,
    CurvePoint,
    FloatingRateTerms,
    InMemoryFixingSource,
    InMemoryQuoteSource,
    IndexFixing,
    InstrumentId,
    IssuerType,
    PricingSpec,
    QuoteSide,
    RawQuote,
)
from fuggers_py.engine import CurveBuilder
from fuggers_py.funding import (
    RepoCurve,
    haircut_financing_cost,
    implied_repo_rate,
    specialness_spread,
)
from fuggers_py.rates import (
    basis_metrics,
    cheapest_to_deliver,
    conversion_factor,
    fair_futures_price,
    invoice_breakdown,
    oabpv,
)

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

apply_research_style()

# %%
fixed_id = InstrumentId('SCENARIO-A-FIXED')
curve_id = CurveId('usd.discount')
as_of = Date.from_ymd(2026, 1, 15)
quote = RawQuote(
    instrument_id=fixed_id,
    value=Decimal('101.25'),
    side=QuoteSide.MID,
    as_of=as_of,
    currency=Currency.USD,
)
curve_inputs = CurveInputs.from_points(
    curve_id,
    as_of,
    [
        CurvePoint(Decimal('0.25'), Decimal('0.0450')),
        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')),
    ],
)
fixing_source = InMemoryFixingSource(
    [
        IndexFixing('SOFR', Date.from_ymd(2026, 1, 13), Decimal('0.0410')),
        IndexFixing('SOFR', Date.from_ymd(2026, 1, 14), Decimal('0.0411')),
        IndexFixing('SOFR', Date.from_ymd(2026, 1, 15), Decimal('0.0412')),
    ]
)
quote_source = InMemoryQuoteSource([quote])

fixed_reference = BondReferenceData(
    instrument_id=fixed_id,
    bond_type=BondType.FIXED_RATE,
    issuer_type=IssuerType.CORPORATE,
    issue_date=Date.from_ymd(2024, 1, 15),
    maturity_date=Date.from_ymd(2031, 1, 15),
    currency=Currency.USD,
    coupon_rate=Decimal('0.0450'),
    frequency=Frequency.SEMI_ANNUAL,
    sector='CORPORATE',
    rating='A',
)
callable_reference = BondReferenceData(
    instrument_id=InstrumentId('SCENARIO-B-CALLABLE'),
    bond_type=BondType.CALLABLE,
    issuer_type=IssuerType.CORPORATE,
    issue_date=Date.from_ymd(2023, 1, 15),
    maturity_date=Date.from_ymd(2033, 1, 15),
    currency=Currency.USD,
    coupon_rate=Decimal('0.0500'),
    frequency=Frequency.SEMI_ANNUAL,
    call_schedule=(
        CallScheduleEntry(Date.from_ymd(2028, 1, 15), Decimal('100.00')),
        CallScheduleEntry(Date.from_ymd(2029, 1, 15), Decimal('100.00')),
    ),
)
frn_reference = BondReferenceData(
    instrument_id=InstrumentId('SCENARIO-C-FRN'),
    bond_type=BondType.FLOATING_RATE,
    issuer_type=IssuerType.CORPORATE,
    issue_date=Date.from_ymd(2025, 1, 15),
    maturity_date=Date.from_ymd(2028, 1, 15),
    currency=Currency.USD,
    floating_rate_terms=FloatingRateTerms(
        index_name='SOFR',
        spread=Decimal('0.0125'),
        reset_frequency=Frequency.QUARTERLY,
        current_reference_rate=Decimal('0.0410'),
    ),
)
repo_demo = build_repo_demo_bundle(as_of=as_of)
futures_demo = build_futures_demo_bundle(as_of=as_of)
cds_demo = build_cds_demo_bundle(reference_date=as_of)

quotes_table = pd.DataFrame([{'instrument_id': quote.instrument_id.as_str(), 'clean_price': quote.value, 'side': quote.side.value}])
curve_table = pd.DataFrame({'tenor_years': [float(point.tenor) for point in curve_inputs.points], 'zero_rate': [float(point.value) for point in curve_inputs.points]})
fixings_table = pd.DataFrame(
    [
        {'index': 'SOFR', 'fixing_date': fixing.fixing_date, 'value': fixing.value}
        for fixing in fixing_source.fixings.values()
    ]
)
reference_table = pd.DataFrame(
    [
        {'instrument_id': record.instrument_id.as_str(), 'bond_type': record.bond_type.value, 'maturity_date': record.maturity_date, 'currency': record.currency.value}
        for record in [fixed_reference, callable_reference, frn_reference]
    ]
)
cross_domain_market_table = pd.DataFrame(
    [
        {'record_type': 'RepoQuote', 'layer': 'market', 'id': repo_demo.repo_quote.instrument_id.as_str(), 'primary_field': 'rate', 'primary_value': repo_demo.repo_quote.rate},
        {'record_type': 'HaircutQuote', 'layer': 'market', 'id': repo_demo.haircut_quote.instrument_id.as_str(), 'primary_field': 'haircut', 'primary_value': repo_demo.haircut_quote.haircut},
        {'record_type': 'SwapQuote', 'layer': 'market', 'id': repo_demo.swap_quote.instrument_id.as_str(), 'primary_field': 'rate', 'primary_value': repo_demo.swap_quote.rate},
        {'record_type': 'BondFutureQuote', 'layer': 'market', 'id': futures_demo.future_quote.instrument_id.as_str(), 'primary_field': 'price', 'primary_value': futures_demo.future_quote.price},
        {'record_type': 'CdsQuote', 'layer': 'market', 'id': cds_demo.primary_quote.instrument_id.as_str(), 'primary_field': 'par_spread', 'primary_value': cds_demo.primary_quote.par_spread},
        {'record_type': 'BondFutureContractReference', 'layer': 'reference', 'id': futures_demo.contract_reference.instrument_id.as_str(), 'primary_field': 'delivery_month', 'primary_value': futures_demo.contract_reference.delivery_month},
        {'record_type': 'DeliverableBondReference', 'layer': 'reference', 'id': futures_demo.deliverable_references[0].instrument_id.as_str(), 'primary_field': 'maturity_date', 'primary_value': futures_demo.deliverable_references[0].maturity_date},
        {'record_type': 'DeliverableBondReference', 'layer': 'reference', 'id': futures_demo.deliverable_references[1].instrument_id.as_str(), 'primary_field': 'maturity_date', 'primary_value': futures_demo.deliverable_references[1].maturity_date},
        {'record_type': 'RepoReferenceData', 'layer': 'reference', 'id': repo_demo.repo_reference.instrument_id.as_str(), 'primary_field': 'term', 'primary_value': repo_demo.repo_reference.term},
        {'record_type': 'SwapReferenceData', 'layer': 'reference', 'id': repo_demo.swap_reference.instrument_id.as_str(), 'primary_field': 'tenor', 'primary_value': repo_demo.swap_reference.tenor},
        {'record_type': 'CdsReferenceData', 'layer': 'reference', 'id': cds_demo.primary_reference.instrument_id.as_str(), 'primary_field': 'reference_entity', 'primary_value': cds_demo.primary_reference.reference_entity},
    ]
)

curve_builder = CurveBuilder()
discount_curve = curve_builder.add_zero_curve(curve_id, list(curve_inputs.points), curve_inputs.reference_date)
forward_curve = curve_builder.add_forward_curve(
    CurveId('usd.forward'),
    [
        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')),
    ],
    as_of,
)
cds_bootstrap = bootstrap_credit_curve(
    cds_demo.quote_strip,
    valuation_date=cds_demo.reference_date,
    discount_curve=cds_demo.discount_curve,
    recovery_rate=cds_demo.primary_quote.recovery_rate,
)
analytics_curves = AnalyticsCurves(
    discount_curve=discount_curve,
    forward_curve=forward_curve,
    repo_curve=repo_demo.repo_curve,
    collateral_curve=repo_demo.collateral_curve,
    fx_forward_curve=repo_demo.fx_forward_curve,
    multicurve_environment=repo_demo.analytics_curves.multicurve_environment,
    projection_curves={'SOFR': forward_curve, **repo_demo.analytics_curves.projection_curves},
    credit_curve=cds_bootstrap.credit_curve,
    vol_surface=repo_demo.vol_surface,
)
analytics_curve_roles_table = pd.DataFrame(
    [
        {'role': 'repo_curve', 'available': analytics_curves.repo_curve is not None, 'handle_type': type(analytics_curves.repo_curve).__name__, 'source': 'funding bundle'},
        {'role': 'collateral_curve', 'available': analytics_curves.collateral_curve is not None, 'handle_type': type(analytics_curves.collateral_curve).__name__, 'source': 'funding bundle'},
        {'role': 'fx_forward_curve', 'available': analytics_curves.fx_forward_curve is not None, 'handle_type': type(analytics_curves.fx_forward_curve).__name__, 'source': 'funding bundle'},
        {'role': 'projection_curves', 'available': bool(analytics_curves.projection_curves), 'handle_type': 'dict', 'source': ', '.join(sorted((analytics_curves.projection_curves or {}).keys()))},
        {'role': 'credit_curve', 'available': analytics_curves.credit_curve is not None, 'handle_type': type(analytics_curves.credit_curve).__name__, 'source': 'bootstrapped from CDS quotes'},
        {'role': 'vol_surface', 'available': analytics_curves.vol_surface is not None, 'handle_type': type(analytics_curves.vol_surface).__name__, 'source': repo_demo.vol_surface.surface_id.value},
    ]
)
repo_curve = RepoCurve(repo_demo.repo_curve)
collateral_curve = RepoCurve(repo_demo.collateral_curve)
repo_handoff_table = pd.DataFrame(
    [
        {
            'bundle_item': label,
            'rate': trade.rate,
            'haircut': trade.haircut,
            'term_days': trade.start_date.days_between(trade.end_date),
            'collateral_id': trade.collateral_instrument_id.as_str() if trade.collateral_instrument_id is not None else None,
            'notional': trade.notional,
            'cash_amount': trade.cash_amount,
        }
        for label, trade in repo_demo.repo_batch
    ]
)
repo_analytics_table = pd.DataFrame(
    [
        {'metric': 'year_fraction', 'value': repo_demo.repo_trade.year_fraction()},
        {'metric': 'cash_lent', 'value': repo_demo.repo_trade.cash_lent()},
        {'metric': 'interest_amount', 'value': repo_demo.repo_trade.interest_amount()},
        {'metric': 'repurchase_amount', 'value': repo_demo.repo_trade.repurchase_amount()},
        {'metric': 'curve_zero_rate', 'value': repo_curve.zero_rate(repo_demo.repo_trade.end_date)},
        {'metric': 'curve_forward_rate', 'value': repo_curve.forward_rate(repo_demo.repo_trade.start_date, repo_demo.repo_trade.end_date)},
        {
            'metric': 'implied_repo_rate',
            'value': implied_repo_rate(
                spot_price=Decimal('100'),
                futures_price=Decimal('125'),
                conversion_factor=Decimal('0.8'),
                start_date=repo_demo.repo_trade.start_date,
                end_date=repo_demo.repo_trade.end_date,
                coupon_income=Decimal('0.25'),
            ),
        },
        {
            'metric': 'specialness_spread',
            'value': specialness_spread(
                general_collateral_rate=collateral_curve.zero_rate(repo_demo.repo_trade.end_date),
                specific_collateral_rate=repo_demo.repo_quote.rate,
            ),
        },
        {
            'metric': 'haircut_financing_cost',
            'value': haircut_financing_cost(
                collateral_value=repo_demo.repo_trade.collateral_market_value(),
                haircut=repo_demo.repo_trade.haircut,
                funding_rate=Decimal('0.0600'),
                year_fraction=repo_demo.repo_trade.year_fraction(),
            ),
        },
    ]
)
ctd_result = cheapest_to_deliver(futures_demo.contract, futures_demo.basket, futures_demo.future_quote.price)
ctd_bond = futures_demo.basket.get_deliverable(ctd_result.cheapest_to_deliver)
ctd_cf = conversion_factor(futures_demo.contract, ctd_bond)
fair_future = fair_futures_price(
    futures_demo.contract,
    futures_demo.basket,
    delivery_option_model=futures_demo.deterministic_delivery_option_model,
)
invoice = invoice_breakdown(
    futures_demo.contract.contract_size,
    futures_demo.future_quote.price,
    ctd_result.conversion_factor,
    ctd_bond.accrued_interest(futures_demo.contract.resolved_delivery_date()),
)
futures_reference_table = pd.DataFrame(
    [
        {
            'record_type': 'future_contract',
            'id': futures_demo.contract_reference.instrument_id.as_str(),
            'detail': f'{futures_demo.contract_reference.first_delivery_date} to {futures_demo.contract_reference.last_delivery_date}',
        },
        *[
            {
                'record_type': 'deliverable_bond',
                'id': reference.instrument_id.as_str(),
                'detail': reference.maturity_date,
            }
            for reference in futures_demo.deliverable_references
        ],
    ]
)
futures_candidates_table = pd.DataFrame(
    [
        {
            'instrument_id': candidate.instrument_id.as_str(),
            'clean_price': candidate.clean_price,
            'conversion_factor': candidate.conversion_factor,
            'equivalent_futures_price': candidate.futures_equivalent_price,
            'gross_basis': candidate.gross_basis,
        }
        for candidate in ctd_result.candidates
    ]
)
futures_summary_table = pd.DataFrame(
    [
        {'metric': 'ctd', 'value': ctd_result.cheapest_to_deliver.as_str()},
        {'metric': 'delivery_payoff', 'value': ctd_result.delivery_payoff},
        {'metric': 'gross_basis', 'value': ctd_result.gross_basis},
        {'metric': 'published_conversion_factor', 'value': ctd_cf.conversion_factor},
        {
            'metric': 'net_basis',
            'value': basis_metrics(
                futures_demo.future_quote.price,
                ctd_result.conversion_factor,
                ctd_bond.clean_price,
                delivery_option_value=fair_future.delivery_option_adjustment,
            ).net_basis,
        },
        {'metric': 'invoice_amount', 'value': invoice.invoice_amount},
        {'metric': 'delivery_option_adjustment', 'value': fair_future.delivery_option_adjustment},
        {'metric': 'fair_futures_price', 'value': fair_future.fair_futures_price},
        {'metric': 'oabpv', 'value': oabpv(futures_demo.contract, futures_demo.basket, delivery_option_model=futures_demo.deterministic_delivery_option_model)},
    ]
)
cds_handoff_table = pd.DataFrame(
    [
        {
            'instrument_id': quote.instrument_id.as_str(),
            'tenor': quote.tenor,
            'market_par_spread': quote.par_spread,
            'fitted_par_spread': point.fitted_par_spread,
            'survival_probability': point.survival_probability,
        }
        for quote, point in zip(cds_demo.quote_strip, cds_bootstrap.points, strict=False)
    ]
)
pricing_spec_table = pd.DataFrame(
    [
        {'field': 'quote_side', 'value': PricingSpec().quote_side.value, 'why_it_matters': 'Selects bid / mid / ask semantics.'},
        {'field': 'compute_spreads', 'value': PricingSpec().compute_spreads, 'why_it_matters': 'Turns on spread analytics for relative-value workflows.'},
        {'field': 'compute_key_rates', 'value': PricingSpec(compute_key_rates=True).compute_key_rates, 'why_it_matters': 'Requests tenor-level risk decomposition.'},
        {'field': 'market_price_is_dirty', 'value': PricingSpec(market_price_is_dirty=True).market_price_is_dirty, 'why_it_matters': 'Controls whether accrual is added or already embedded.'},
    ]
)

# %%
display_table(quotes_table, float_columns=['clean_price'])
display_table(curve_table, percent_columns=['zero_rate'], float_columns=['tenor_years'])
display_table(fixings_table, date_columns=['fixing_date'], percent_columns=['value'])
display_table(reference_table, date_columns=['maturity_date'])
display_table(cross_domain_market_table)
display_table(analytics_curve_roles_table)
display_table(repo_handoff_table, percent_columns=['rate', 'haircut'], float_columns=['term_days', 'notional', 'cash_amount'])
display_table(repo_analytics_table)
display_table(futures_reference_table)
display_table(futures_candidates_table)
display_table(futures_summary_table)
display_table(cds_handoff_table, percent_columns=['market_par_spread', 'fitted_par_spread'], float_columns=['survival_probability'])
display_table(pricing_spec_table)

# %% [markdown]
# The distinction is operationally useful: market data changes intraday, reference data changes only when the instrument definition changes, and pricing specs reflect the analytic choices of the desk rather than the market itself.

# %% [markdown]
# ## Government bond futures records can now stay in the same data workflow
#
# The widened futures contract reference and deliverable-bond reference records let the data layer hold the basket definition, while the `rates` layer consumes those records for conversion factors, CTD, basis, invoice, and option-adjusted futures risk.

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

reference_counts = reference_table['bond_type'].value_counts().reset_index()
reference_counts.columns = ['bond_type', 'count']
axes[1].bar(reference_counts['bond_type'], reference_counts['count'])
axes[1].set_title('Reference Universe Coverage')
axes[1].set_xlabel('Bond type')
axes[1].set_ylabel('Count')
plt.tight_layout()
figure

# %%
figure, axis = plt.subplots(figsize=(10, 4.2))
axis.bar([str(value) for value in fixings_table['fixing_date']], fixings_table['value'])
axis.set_title('Fixing History Used By The FRN Workflow')
axis.set_xlabel('Fixing date')
axis.set_ylabel('Fixing')
percent_axis(axis)
plt.tight_layout()
figure

# %% [markdown]
# ## Hand-off to the engine layer
#
# Once the quote, fixing, reference, and pricing-spec objects are cleanly separated, the engine can consume them without needing notebook-specific glue code.
