# ---
# 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]
# # `fuggers_py.core` examples
#
# This notebook demonstrates the **core** layer API:
#
# 1. `Date` creation and helpers
# 2. `Currency`, `Frequency`, `Compounding`
# 3. `Price`, `Yield`, `Spread` conversions
# 4. `CashFlow` and `CashFlowSchedule`
# 5. Day-count conventions
# 6. Built-in calendars
# 7. Business-day adjustment rules
# 8. `DynamicCalendar` round-trip from JSON / data
# 9. `JointCalendar`
# 10. A tiny toy `YieldCurve` + PV example
#

# %%
from pathlib import Path
import sys

# Add the repo `src/` directory so `import fuggers_py` works when running this
# notebook from `examples/`.
cwd = Path.cwd().resolve()
candidates = [cwd] + list(cwd.parents)
repo_root = None
for p in candidates:
    if (p / "src" / "fuggers_py").exists():
        repo_root = p
        break

assert repo_root is not None, "Could not locate repo root containing src/fuggers_py"
sys.path.insert(0, str(repo_root / "src"))

from decimal import Decimal

from fuggers_py.core import (
    Act360,
    Act365Fixed,
    ActActIcma,
    BusinessDayConvention,
    CalendarData,
    CashFlow,
    CashFlowSchedule,
    Compounding,
    Currency,
    Date,
    DynamicCalendar,
    Frequency,
    JointCalendar,
    Price,
    SIFMACalendar,
    Spread,
    SpreadType,
    Target2Calendar,
    Thirty360US,
    UKCalendar,
    WeekendCalendar,
    Yield,
)


# %% [markdown]
# ## 1) `Date`
#

# %%
d = Date.from_ymd(2024, 2, 29)
print("date:", d)
print("weekday (Mon=0):", d.weekday())
print("day_of_year:", d.day_of_year())

assert str(d) == "2024-02-29"
assert d.is_leap_year()
assert d.is_end_of_month()

jan31 = Date.parse("2024-01-31")
assert jan31.add_months(1) == Date.parse("2024-02-29")  # clamps
assert Date.parse("2024-02-29").add_years(1) == Date.parse("2025-02-28")


# %% [markdown]
# ## 2) `Currency`, `Frequency`, `Compounding`
#

# %%
usd = Currency.USD
print(usd.code(), usd.symbol(), usd.numeric_code(), "settle T+", usd.standard_settlement_days())

freq = Frequency.SEMI_ANNUAL
print("frequency:", freq, "periods/year:", freq.periods_per_year(), "months/period:", freq.months_per_period())

comp = Compounding.CONTINUOUS
print("compounding:", comp, "is_continuous:", comp.is_continuous())


# %% [markdown]
# ## 3) `Price`, `Yield`, `Spread` conversions
#

# %%
p = Price.new("98.5", Currency.USD)
print("Price:", p.as_percentage(), "(pct)", p.as_decimal(), "(decimal)")
assert p.as_decimal() == Decimal("0.985")

y = Yield.from_bps(250, Compounding.SEMI_ANNUAL)
print("Yield:", y.value(), "(decimal)", y.as_percentage(), "(pct)", y.as_bps(), "(bps)")
y_cont = y.convert_to(Compounding.CONTINUOUS)
print("Yield (continuous):", y_cont.value())

s = Spread.new(125, SpreadType.Z_SPREAD)
print("Spread:", s.as_bps(), "bps", "=", s.as_decimal(), "decimal")
assert s.as_percentage() == Decimal("1.25")


# %% [markdown]
# ## 4) `CashFlow` and `CashFlowSchedule`
#

# %%
accrual_start = Date.parse("2025-01-01")
payment = Date.parse("2025-07-01")

coupon = CashFlow.coupon_with_accrual(payment, "1.23", accrual_start, payment)
principal = CashFlow.principal(payment, "100")

schedule = CashFlowSchedule.new()
schedule.push(coupon)
schedule.push(principal)
schedule.sort_by_date()

print("schedule total:", schedule.total())
assert schedule.total() == Decimal("101.23")


# %% [markdown]
# ## 5) Day-count conventions
#

# %%
start = Date.parse("2024-01-01")
end = Date.parse("2024-07-01")

act360 = Act360()
act365f = Act365Fixed()
thirty360 = Thirty360US()

print("ACT/360:", act360.year_fraction(start, end))
print("ACT/365F:", act365f.year_fraction(start, end))
print("30/360 US:", thirty360.year_fraction(start, end))

icma = ActActIcma.semi_annual()
print("ACT/ACT ICMA (semi-annual):", icma.year_fraction_with_period(start, end, start, end))


# %% [markdown]
# ## 6) Built-in calendars
#
# Note: the calendar singletons use `global_()` because `global` is a reserved keyword in Python,
# so the singleton constructors are named `global_()` instead.
#

# %%
wcal = WeekendCalendar()
sifma = SIFMACalendar.global_()
t2 = Target2Calendar.global_()
uk = UKCalendar.global_()

holiday = Date.parse("2024-07-04")
print("WeekendCalendar business?", wcal.is_business_day(holiday))
print("SIFMA business?", sifma.is_business_day(holiday))
print("TARGET2 business?", t2.is_business_day(holiday))
print("UK business?", uk.is_business_day(holiday))

assert sifma.is_business_day(holiday) is False
assert t2.is_business_day(holiday) is True


# %% [markdown]
# ## 7) Business-day adjustment examples
#

# %%
sat = Date.parse("2025-05-31")
print("Following:", wcal.adjust(sat, BusinessDayConvention.FOLLOWING))
print("Modified following:", wcal.adjust(sat, BusinessDayConvention.MODIFIED_FOLLOWING))
print("End-of-month:", wcal.adjust(sat, BusinessDayConvention.END_OF_MONTH))

assert wcal.adjust(sat, BusinessDayConvention.END_OF_MONTH) == Date.parse("2025-05-30")


# %% [markdown]
# ## 8) `DynamicCalendar` from JSON / data
#

# %%
data = CalendarData.new("MyCal").with_holiday(Date.parse("2024-12-24"))
dyn = DynamicCalendar.from_calendar_data(data)
assert dyn.is_business_day(Date.parse("2024-12-24")) is False

payload = dyn.to_json()
print(payload)

dyn2 = DynamicCalendar.from_json(payload)
assert dyn2.holiday_count() == 1


# %% [markdown]
# ## 9) `JointCalendar`
#

# %%
joint = JointCalendar.new([sifma, t2])
assert joint.is_business_day(Date.parse("2024-07-04")) is False  # closed in US
assert joint.is_business_day(Date.parse("2024-05-01")) is False  # closed in TARGET2
print("joint calendar ok")


# %% [markdown]
# ## 10) Toy `YieldCurve` + present value
#

# %%
from fuggers_py.core.traits import YieldCurve


class FlatCurve(YieldCurve):
    def __init__(self, ref: Date, rate_cont: Decimal):
        self._ref = ref
        self._rate = rate_cont

    def reference_date(self) -> Date:
        return self._ref

    def discount_factor(self, date: Date) -> Decimal:
        days = self._ref.days_between(date)
        t = Decimal(days) / Decimal(365)
        return (-self._rate * t).exp()

    def zero_rate(self, date: Date) -> Yield:
        return Yield.new(self._rate, Compounding.CONTINUOUS)

    def max_date(self) -> Date:
        return self._ref.add_years(100)


curve = FlatCurve(Date.parse("2025-01-01"), Decimal("0.05"))
cf = CashFlow.principal(Date.parse("2026-01-01"), Decimal("100"))
pv = cf.present_value(curve)
print("PV:", pv)
assert pv < Decimal("100")
