# ---
# 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]
# # Callable, Puttable, and Sinking-Fund Structures
#
# We compare three cashflow structures built off the same base economics: a bullet bond, an embedded-option bond, and a sinking-fund amortizer.

# %%
from __future__ import annotations

from dataclasses import replace
from decimal import Decimal

import matplotlib

matplotlib.use("Agg")

import matplotlib.pyplot as plt
import pandas as pd

from fuggers_py.bonds import (
    CallableBondBuilder,
    FixedBond,
    SinkingFundBond,
    SinkingFundEntry,
    SinkingFundSchedule,
    YieldCalculationRules,
)
from fuggers_py.core import Date, Frequency

from examples._shared.plotting import apply_research_style
from examples._shared.tables import display_table

apply_research_style()

# %%
issue = Date.from_ymd(2026, 1, 15)
rules = replace(YieldCalculationRules.us_corporate(), frequency=Frequency.ANNUAL)
base_bond = FixedBond.new(
    issue_date=issue,
    maturity_date=issue.add_years(5),
    coupon_rate=Decimal('0.0500'),
    frequency=Frequency.ANNUAL,
    rules=rules,
)
callable_puttable = (
    CallableBondBuilder.new()
    .with_base_bond(base_bond)
    .add_call(call_date=issue.add_years(3), call_price=Decimal('101.00'))
    .add_put(put_date=issue.add_years(2), put_price=Decimal('102.00'))
    .build()
)
sinking_schedule = SinkingFundSchedule.new(
    [
        SinkingFundEntry(issue.add_years(1), Decimal('0.80')),
        SinkingFundEntry(issue.add_years(2), Decimal('0.60')),
        SinkingFundEntry(issue.add_years(3), Decimal('0.35')),
        SinkingFundEntry(issue.add_years(4), Decimal('0.15')),
        SinkingFundEntry(issue.add_years(5), Decimal('0.00')),
    ]
)
sinking_bond = SinkingFundBond.new(
    issue_date=issue,
    maturity_date=issue.add_years(5),
    coupon_rate=Decimal('0.0500'),
    frequency=Frequency.ANNUAL,
    sinking_schedule=sinking_schedule,
    rules=rules,
)
settlement = issue.add_months(6)

# %%
term_sheet = pd.DataFrame(
    [
        {'structure': 'Bullet', 'maturity': base_bond.maturity_date(), 'coupon_rate': base_bond.coupon_rate(), 'average_life': None},
        {'structure': 'Callable/puttable', 'maturity': callable_puttable.maturity_date(), 'coupon_rate': callable_puttable.base_bond.coupon_rate(), 'average_life': None},
        {'structure': 'Sinking fund', 'maturity': sinking_bond.maturity_date(), 'coupon_rate': sinking_bond.coupon_rate(), 'average_life': sinking_bond.average_life()},
    ]
)
term_sheet_display = term_sheet.copy()
term_sheet_display['average_life'] = term_sheet_display['average_life'].where(term_sheet_display['average_life'].notna(), 'n/a')
display_table(term_sheet_display, date_columns=['maturity'], percent_columns=['coupon_rate'])

# %%
bullet_flows = pd.DataFrame([{'date': cf.date, 'amount': cf.amount, 'structure': 'Bullet'} for cf in base_bond.cash_flows()])
callable_flows = pd.DataFrame([{'date': cf.date, 'amount': cf.amount, 'structure': 'Callable/puttable'} for cf in callable_puttable.cash_flows()])
sinking_flows = pd.DataFrame([{'date': cf.date, 'amount': cf.amount, 'structure': 'Sinking fund'} for cf in sinking_bond.cash_flows()])
comparison = pd.concat([bullet_flows, callable_flows, sinking_flows], ignore_index=True)
comparison_table = comparison.pivot_table(index='date', columns='structure', values='amount', aggfunc='sum').fillna(0)
display_table(comparison_table.reset_index(), date_columns=['date'])

summary = pd.DataFrame(
    [
        {'metric': 'yield_to_worst_callable', 'value': callable_puttable.yield_to_worst(Decimal('101.25'), settlement)},
        {'metric': 'yield_to_average_life_sinking', 'value': sinking_bond.yield_to_average_life(Decimal('99.50'), settlement)},
        {'metric': 'accrued_bullet', 'value': base_bond.accrued_interest(settlement)},
        {'metric': 'accrued_sinking', 'value': sinking_bond.accrued_interest(settlement)},
    ]
)
summary

# %% [markdown]
# The optioned structure changes workout risk, while the sinking-fund structure changes principal timing. That means spread and duration interpretation should be tied to expected cashflow timing, not only to final maturity.

# %%
figure, axes = plt.subplots(1, 2, figsize=(12, 4.6))
for structure, group in comparison.groupby('structure'):
    axes[0].plot(group['date'].astype(str), group['amount'], marker='o', linewidth=2, label=structure)
axes[0].set_title('Cashflow Timing Comparison')
axes[0].set_xlabel('Payment date')
axes[0].set_ylabel('Amount per 100 par')
axes[0].legend(title='')
for tick in axes[0].get_xticklabels():
    tick.set_rotation(25)
    tick.set_ha('right')

amortization = pd.DataFrame(sinking_bond.amortization_schedule(), columns=['date', 'principal_paydown'])
amortization['remaining_factor'] = [float(sinking_bond.factor_on(date)) for date in amortization['date']]
axes[1].bar(amortization['date'].astype(str), amortization['principal_paydown'], label='Principal paydown')
axes[1].plot(amortization['date'].astype(str), 100 * amortization['remaining_factor'], color='#b7410e', marker='o', linewidth=2, label='Remaining factor (%)')
axes[1].set_title('Sinking-Fund Notional Profile')
axes[1].set_xlabel('Payment date')
axes[1].set_ylabel('Level')
axes[1].legend(title='')
for tick in axes[1].get_xticklabels():
    tick.set_rotation(25)
    tick.set_ha('right')
plt.tight_layout()
figure
