# ---
# 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]
# # Tutorial: fuggers_py.bonds examples
#
# This notebook demonstrates the **bonds** layer:
#
# - Build a fixed coupon bond
# - Generate schedules and cashflows
# - Compute accrued interest
# - Compute price from yield and yield from price
# - Compute basic risk measures (duration/convexity/DV01)
#

# %% [markdown]
# ## Outline
#
# 1. Setup
# 2. Build a fixed-rate bond
# 3. Schedule + cashflows
# 4. Accrued interest
# 5. Price from yield / yield from price
# 6. Risk metrics
# 7. YieldCalculationRules presets (details)
# 8. UK gilt ex-dividend accrued interest
# 9. Identifier validation
#

# %%
# Setup cell
from __future__ import annotations

import sys
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"))


# %% [markdown]
# ## Imports
#

# %%
from fuggers_py.bonds.instruments import FixedBondBuilder  # noqa: E402
from fuggers_py.bonds.pricing import BondPricer  # noqa: E402
from fuggers_py.bonds.risk import RiskMetrics  # noqa: E402
from fuggers_py.bonds.types import YieldCalculationRules  # noqa: E402
from fuggers_py.core import Compounding, Currency, Date, Frequency, Price, Yield  # noqa: E402


# %% [markdown]
# ## Build a fixed coupon bond
#

# %%
issue = Date.from_ymd(2024, 1, 1)
maturity = Date.from_ymd(2026, 1, 1)

bond = (
    FixedBondBuilder.new()
    .with_issue_date(issue)
    .with_maturity_date(maturity)
    .with_coupon_rate(Decimal("0.05"))  # 5% annual coupon
    .with_frequency(Frequency.SEMI_ANNUAL)
    .with_currency(Currency.USD)
    .with_rules(YieldCalculationRules.us_treasury())
    .build()
)

bond.issue_date(), bond.maturity_date(), bond.frequency()


# %% [markdown]
# ## Schedule + cashflows
#
# Cashflows are expressed in **percentage-of-par** terms (face value defaults to 100).

# %%
schedule = bond.schedule()
schedule.unadjusted_dates, schedule.dates

# %%
cashflows = bond.cash_flows()
[(str(cf.date), cf.flow_type, cf.amount) for cf in cashflows]


# %% [markdown]
# ## Accrued interest
#
# Accrued interest is returned as a **percentage-of-par** amount.

# %%
settlement = Date.from_ymd(2024, 4, 1)
accrued = bond.accrued_interest(settlement)
accrued

# %%
# For this US Treasury-style bond, accrued at 2024-04-01 is half the semiannual coupon: 1.25% of par.
assert accrued == Decimal("1.25")


# %% [markdown]
# ## Price from yield / yield from price
#
# We use a yield-based pricer (no curve required).

# %%
pricer = BondPricer()
ytm = Yield.new(Decimal("0.05"), Compounding.SEMI_ANNUAL)

price_result = pricer.price_from_yield(bond, ytm, settlement)
price_result

# %%
yield_result = pricer.yield_from_price(bond, price_result.clean, settlement)
yield_result.ytm, yield_result.engine

# %%
print("clean price:", price_result.clean.as_percentage())
print("dirty price:", price_result.dirty.as_percentage())
print("accrued:", price_result.accrued)
print("ytm:", yield_result.ytm.value(), yield_result.ytm.compounding())


# %% [markdown]
# ## Risk metrics (duration / convexity / DV01)

# %%
metrics = RiskMetrics.from_bond(bond, ytm, settlement)
metrics

# %%
print("modified duration:", metrics.modified_duration)
print("macaulay duration:", metrics.macaulay_duration)
print("convexity:", metrics.convexity)
print("dv01:", metrics.dv01)


# %% [markdown]
# ## Conventions presets
#
# `YieldCalculationRules` includes simple presets for common markets.

# %%
treasury = YieldCalculationRules.us_treasury()
corp = YieldCalculationRules.us_corporate()
gilt = YieldCalculationRules.uk_gilt()

treasury.description, corp.description, gilt.description

# %%
# Example: change conventions (e.g. 30/360 US for corporates)
bond_corp = (
    FixedBondBuilder.new()
    .with_issue_date(issue)
    .with_maturity_date(maturity)
    .with_coupon_rate(Decimal("0.05"))
    .with_frequency(corp.frequency)
    .with_currency(Currency.USD)
    .with_rules(corp)
    .build()
)

corp_price = pricer.price_from_yield(bond_corp, ytm, settlement)
corp_price.clean.as_percentage()


# %% [markdown]
# ## YieldCalculationRules presets (details)

# %%
def summarize_rules(name: str, rules: YieldCalculationRules) -> dict[str, object]:
    return {
        "name": name,
        "convention": rules.convention,
        "compounding": (rules.compounding.kind, rules.compounding.frequency),
        "frequency": rules.frequency,
        "calendar": rules.calendar.as_str(),
        "settlement_days": rules.settlement_rules.days,
        "accrual_day_count": rules.accrual_day_count,
        "yield_day_count": rules.yield_day_count,
        "discount_day_count": rules.discount_day_count,
        "accrued_convention": rules.accrued_convention,
        "ex_dividend_days": None if rules.ex_dividend_rules is None else rules.ex_dividend_rules.ex_dividend_days,
    }


[
    summarize_rules("us_treasury", treasury),
    summarize_rules("us_corporate", corp),
    summarize_rules("uk_gilt", gilt),
]


# %% [markdown]
# ## UK gilt ex-dividend accrued interest
#
# UK gilts go ex-dividend a fixed number of **business days** before a coupon date.
# In the ex-div window, accrued interest is typically reduced by the upcoming coupon
# amount (often resulting in negative accrued).

# %%
from dataclasses import replace  # noqa: E402

from fuggers_py.bonds.types import AccruedConvention  # noqa: E402

gilt_rules_exdiv = YieldCalculationRules.uk_gilt()
gilt_rules_standard = replace(
    gilt_rules_exdiv,
    accrued_convention=AccruedConvention.STANDARD,
    ex_dividend_rules=None,
)

gilt_bond_exdiv = (
    FixedBondBuilder.new()
    .with_issue_date(Date.from_ymd(2024, 1, 1))
    .with_maturity_date(Date.from_ymd(2025, 1, 1))
    .with_coupon_rate(Decimal("0.05"))
    .with_frequency(gilt_rules_exdiv.frequency)
    .with_currency(Currency.GBP)
    .with_rules(gilt_rules_exdiv)
    .build()
)

gilt_bond_standard = (
    FixedBondBuilder.new()
    .with_issue_date(Date.from_ymd(2024, 1, 1))
    .with_maturity_date(Date.from_ymd(2025, 1, 1))
    .with_coupon_rate(Decimal("0.05"))
    .with_frequency(gilt_rules_standard.frequency)
    .with_currency(Currency.GBP)
    .with_rules(gilt_rules_standard)
    .build()
)

# %%
coupon_date = gilt_bond_exdiv.next_coupon_date(Date.from_ymd(2024, 1, 2))
coupon_date

# %%
# Choose a settlement date within 7 UK business days of the coupon date.
settle_exdiv = Date.from_ymd(2024, 6, 25)

accrued_standard = gilt_bond_standard.accrued_interest(settle_exdiv)
accrued_exdiv = gilt_bond_exdiv.accrued_interest(settle_exdiv)

print("coupon date:", coupon_date)
print("settlement:", settle_exdiv)
print("standard accrued:", accrued_standard)
print("ex-div accrued:", accrued_exdiv)
print("ex-div accrued sign:", "negative" if accrued_exdiv < 0 else "non-negative")


# %% [markdown]
# ## Identifier validation
#
# The bond layer includes strict identifier validation for common market IDs.

# %%
import pytest  # noqa: E402

from fuggers_py.bonds.errors import InvalidIdentifier  # noqa: E402
from fuggers_py.bonds.types import Cusip, Figi, Isin, Sedol  # noqa: E402

Cusip.new("037833100"), Isin.new("US0378331005"), Figi.new("BBG000BLNNH6"), Sedol.new("2588173")

# %%
with pytest.raises(InvalidIdentifier):
    _ = Cusip.new("037833101")  # wrong check digit
