# ---
# 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 bond examples

# %%
from __future__ import annotations

import sys
from dataclasses import replace
from decimal import Decimal
from pathlib import Path

root = Path.cwd()
if (root / "src").exists():
    sys.path.insert(0, str(root / "src"))
elif (root.parent / "src").exists():
    sys.path.insert(0, str(root.parent / "src"))

# %%
from fuggers_py.bonds.instruments import CallableBondBuilder, FixedBond  # noqa: E402
from fuggers_py.bonds.instruments.callable import CallSchedule, CallEntry, CallType  # noqa: E402
from fuggers_py.bonds.options import BondOption, ExerciseStyle, HullWhiteModel, OptionType  # noqa: E402
from fuggers_py.bonds.types import PutType, YieldCalculationRules  # noqa: E402
from fuggers_py.core import Date, Frequency  # noqa: E402
from fuggers_py.curves import DiscountCurveBuilder  # noqa: E402

# %% [markdown]
# ## Build the base fixed bond

# %%
rules = replace(YieldCalculationRules.us_corporate(), frequency=Frequency.ANNUAL)
issue = Date.from_ymd(2024, 2, 20)
base_bond = FixedBond.new(
    issue_date=issue,
    maturity_date=issue.add_years(5),
    coupon_rate=Decimal("0.05"),
    frequency=Frequency.ANNUAL,
    rules=rules,
)
base_bond.cash_flows()

# %% [markdown]
# ## Create European and American call schedules

# %%
european_schedule = CallSchedule.new(
    [
        CallEntry(call_date=Date.from_ymd(2026, 2, 20), call_price=Decimal("101.50"), call_type=CallType.EUROPEAN),
        CallEntry(call_date=Date.from_ymd(2027, 2, 22), call_price=Decimal("100.75"), call_type=CallType.EUROPEAN),
    ]
)

american_schedule = CallSchedule.new(
    [
        CallEntry(
            call_date=Date.from_ymd(2026, 2, 20),
            call_end_date=Date.from_ymd(2027, 2, 22),
            call_price=Decimal("101.00"),
            call_type=CallType.AMERICAN,
        )
    ],
    protection_end_date=Date.from_ymd(2025, 8, 20),
)

european_schedule, american_schedule

# %% [markdown]
# ## Construct a callable bond and calculate yield-to-call metrics

# %%
callable_bond = CallableBondBuilder.new().with_base_bond(base_bond).with_call_schedule(european_schedule).build()
settlement = Date.from_ymd(2024, 8, 20)
clean_price = Decimal("101.25")

print("Yield to maturity:", callable_bond.yield_to_maturity(clean_price, settlement))
print("Yield to first call:", callable_bond.yield_to_first_call(clean_price, settlement))
print("Yield to worst:", callable_bond.yield_to_worst(clean_price, settlement))

# %% [markdown]
# ## Inspect call prices on a few dates

# %%
for check_date in [Date.from_ymd(2025, 8, 20), Date.from_ymd(2026, 2, 20), Date.from_ymd(2027, 2, 22)]:
    print(check_date, callable_bond.call_price_on(check_date))

print("American protection active on 2025-02-20:", american_schedule.call_price_on(Date.from_ymd(2025, 2, 20), maturity_date=base_bond.maturity_date()))
print("American call becomes active on 2026-02-20:", american_schedule.call_price_on(Date.from_ymd(2026, 2, 20), maturity_date=base_bond.maturity_date()))

# %% [markdown]
# ## Embedded put support and yield-to-worst

# %%
putable_bond = (
    CallableBondBuilder.new()
    .with_base_bond(base_bond)
    .with_call_schedule(european_schedule)
    .add_put(put_date=Date.from_ymd(2026, 2, 20), put_price=Decimal("101.50"), put_type=PutType.EUROPEAN)
    .build()
)

print("Yield to maturity:", putable_bond.yield_to_maturity(clean_price, settlement))
print("Yield to first call:", putable_bond.yield_to_first_call(clean_price, settlement))
print("Yield to first put:", putable_bond.yield_to_first_put(clean_price, settlement))
print("Yield to worst:", putable_bond.yield_to_worst(clean_price, settlement))

# %% [markdown]
# ## Bond option pricing with Hull-White + binomial tree

# %%
term_structure = (
    DiscountCurveBuilder(reference_date=issue)
    .add_zero_rate(1.0, Decimal("0.03"))
    .add_zero_rate(10.0, Decimal("0.04"))
    .build()
)
model = HullWhiteModel(mean_reversion=Decimal("0.03"), volatility=Decimal("0.01"), term_structure=term_structure)
bond_option = BondOption(
    expiry=Date.from_ymd(2026, 2, 20),
    strike=Decimal("100"),
    bond=base_bond,
    model=model,
    option_type=OptionType.CALL,
    exercise_style=ExerciseStyle.EUROPEAN,
    valuation_date=issue,
)
print("European call option price:", bond_option.price())
