# ---
# 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]
# # Dates, Day Counts, and Calendars
#
# **Research question.** How much do settlement calendars and accrual conventions move the numbers for the same fixed-rate bond?
#
# This notebook uses one deterministic USD bond and a small set of holiday-sensitive dates to show three things:
#
# 1. business-day calendars change coupon adjustment and T+2 settlement timing,
# 2. day-count conventions change the accrued fraction through the coupon period,
# 3. the accrual difference flows directly into the cash amount paid at settlement.

# %%
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.core import (
    Act360,
    Act365Fixed,
    ActActIcma,
    BusinessDayConvention,
    Date,
    Frequency,
    SIFMACalendar,
    Target2Calendar,
    Thirty360US,
    UKCalendar,
)

from examples._shared.datasets import (
    build_foundation_bond,
    foundation_calendar_scenarios,
    foundation_coupon_period_context,
)
from examples._shared.formatting import as_percent
from examples._shared.plotting import apply_research_style, percent_axis
from examples._shared.tables import display_table

apply_research_style()


# %% [markdown]
# ## Market Assumptions
#
# The setup is intentionally small. We use:
#
# - one semi-annual fixed-rate USD bond,
# - three market calendars: US SIFMA, UK, and TARGET2,
# - four accrual conventions: ACT/360, ACT/365F, ACT/ACT ICMA, and 30/360 US.
#
# The point is not to create a full desk pricing environment. The point is to isolate how conventions alter settlement mechanics and accrued interest.

# %%
bond = build_foundation_bond()
calendar_scenarios = foundation_calendar_scenarios()
coupon_period = foundation_coupon_period_context()

bond_summary = pd.DataFrame(
    [
        {
            "attribute": "issue_date",
            "value": bond.issue_date(),
        },
        {
            "attribute": "maturity_date",
            "value": bond.maturity_date(),
        },
        {
            "attribute": "coupon_rate",
            "value": f"{as_percent(bond.coupon_rate()):.3f}%",
        },
        {
            "attribute": "frequency",
            "value": str(Frequency.SEMI_ANNUAL),
        },
        {
            "attribute": "notional",
            "value": f"{bond.notional():.2f}",
        },
        {
            "attribute": "library_accrual_rule",
            "value": bond.rules().accrual_day_count_obj().name(),
        },
    ]
)

display_table(bond_summary, date_columns=["value"])


# %% [markdown]
# ## Calendar Comparison
#
# We test dates around:
#
# - US Independence Day,
# - TARGET Labour Day,
# - the UK Early May bank holiday,
# - a month-end Saturday.
#
# For each scenario we compare whether the date is a business day, where the coupon date lands under following and modified-following adjustment, and how many calendar days it takes to reach T+2 settlement.

# %%
calendars = {
    "US SIFMA": SIFMACalendar.global_(),
    "UK": UKCalendar.global_(),
    "TARGET2": Target2Calendar.global_(),
}


def calendar_comparison_table() -> pd.DataFrame:
    records: list[dict[str, object]] = []
    for row in calendar_scenarios.itertuples(index=False):
        for calendar_name, calendar in calendars.items():
            trade_date = row.trade_date
            coupon_date = row.candidate_coupon_date
            settlement_date = calendar.settlement_date(trade_date, 2)
            records.append(
                {
                    "scenario": row.scenario,
                    "calendar": calendar_name,
                    "trade_date": trade_date,
                    "candidate_coupon_date": coupon_date,
                    "trade_is_business_day": calendar.is_business_day(trade_date),
                    "coupon_is_business_day": calendar.is_business_day(coupon_date),
                    "following_coupon_date": calendar.adjust(coupon_date, BusinessDayConvention.FOLLOWING),
                    "modified_following_coupon_date": calendar.adjust(
                        coupon_date,
                        BusinessDayConvention.MODIFIED_FOLLOWING,
                    ),
                    "t_plus_2_settlement": settlement_date,
                    "settlement_lag_days": trade_date.days_between(settlement_date),
                }
            )
    return pd.DataFrame(records)


calendar_table = calendar_comparison_table()
calendar_display = display_table(
    calendar_table[
        [
            "scenario",
            "calendar",
            "trade_date",
            "candidate_coupon_date",
            "following_coupon_date",
            "modified_following_coupon_date",
            "t_plus_2_settlement",
            "settlement_lag_days",
        ]
    ],
    date_columns=[
        "trade_date",
        "candidate_coupon_date",
        "following_coupon_date",
        "modified_following_coupon_date",
        "t_plus_2_settlement",
    ],
)
calendar_display


# %% [markdown]
# The differences are small in line count but meaningful in settlement operations. The US holiday cluster pushes T+2 further than the UK or TARGET2, while month-end weekend dates show why modified-following is distinct from following.

# %%
settlement_lag = calendar_table.pivot(index="scenario", columns="calendar", values="settlement_lag_days")

figure, axis = plt.subplots(figsize=(11, 4.8))
settlement_lag.plot(kind="bar", ax=axis, width=0.8)
axis.set_title("T+2 Settlement Lag By Calendar Scenario")
axis.set_xlabel("")
axis.set_ylabel("Calendar days from trade date to settlement")
axis.legend(title="")
plt.xticks(rotation=20, ha="right")
plt.tight_layout()
figure


# %% [markdown]
# ## Accrual-Fraction Comparison
#
# We now freeze one coupon period and walk through settlement dates inside that period. The same coupon cashflow is translated with four day-count conventions so the accrual path can be compared directly.

# %%
accrual_start = coupon_period["accrual_start"]
accrual_end = coupon_period["accrual_end"]
selected_settlement = coupon_period["selected_settlement"]
settlement_dates = coupon_period["settlement_dates"]

day_counts = {
    "ACT/360": Act360(),
    "ACT/365F": Act365Fixed(),
    "ACT/ACT ICMA": ActActIcma.semi_annual(),
    "30/360 US": Thirty360US(),
}


def accrual_fraction(day_count: object, settlement: Date) -> Decimal:
    if isinstance(day_count, ActActIcma):
        return day_count.year_fraction_with_period(accrual_start, settlement, accrual_start, accrual_end)
    return day_count.year_fraction(accrual_start, settlement)


def coupon_amount_for_fraction(fraction: Decimal) -> Decimal:
    return bond.notional() * bond.coupon_rate() * fraction


accrual_records: list[dict[str, object]] = []
for settlement in settlement_dates:
    for convention_name, day_count in day_counts.items():
        fraction = accrual_fraction(day_count, settlement)
        accrual_records.append(
            {
                "settlement_date": settlement,
                "convention": convention_name,
                "accrual_fraction": fraction,
                "accrued_interest": coupon_amount_for_fraction(fraction),
            }
        )

accrual_path = pd.DataFrame(accrual_records)
accrual_snapshot = accrual_path.loc[accrual_path["settlement_date"] == selected_settlement].reset_index(drop=True)
accrual_snapshot["library_treasury_accrued"] = bond.accrued_interest(selected_settlement)

display_table(
    accrual_snapshot[
        [
            "settlement_date",
            "convention",
            "accrual_fraction",
            "accrued_interest",
            "library_treasury_accrued",
        ]
    ],
    date_columns=["settlement_date"],
    percent_columns=["accrual_fraction"],
    float_columns=["accrued_interest", "library_treasury_accrued"],
)


# %% [markdown]
# On the April settlement point, the library bond's own accrued amount lines up with its treasury rule, while the alternative conventions move the invoice amount by several cents per 100 notional. That is small per bond and material across size.

# %%
accrual_plot = accrual_path.copy()
accrual_plot["settlement_label"] = accrual_plot["settlement_date"].map(str)
accrual_plot["accrual_fraction_pct"] = accrual_plot["accrual_fraction"].map(as_percent)

figure, axis = plt.subplots(figsize=(11, 4.8))
for convention_name, group in accrual_plot.groupby("convention"):
    axis.plot(
        group["settlement_label"],
        group["accrual_fraction_pct"],
        marker="o",
        linewidth=2,
        label=convention_name,
    )

axis.set_title("Accrual Fraction Through The Coupon Period")
axis.set_xlabel("Settlement date")
axis.set_ylabel("Accrual fraction")
percent_axis(axis, digits=2)
axis.legend(title="")
plt.xticks(rotation=20, ha="right")
plt.tight_layout()
figure


# %% [markdown]
# ## Practitioner Takeaways
#
# - T+2 is not a fixed number of calendar days. Holiday calendars change operational settlement lag materially around local closures.
# - Following and modified-following are not interchangeable at month-end; month changes matter for coupon schedules and settlement workflows.
# - Accrual day counts move the clean-to-dirty bridge even when the bond, coupon, and settlement date are unchanged.
# - ACT/ACT ICMA is the most period-aware of the conventions shown here, which is why it is common in sovereign-style settings.
# - Research notebooks should make these convention choices explicit before moving on to yield, spread, or portfolio analytics.
