# ---
# 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.curves` examples
#
# This notebook demonstrates the **curves** layer:
#
# - Build a curve from dated zero rates
# - Query discount factors and zero rates
# - Compute forward rates

# %% [markdown]
# ## Setup

# %%
from __future__ import annotations

import math
import sys
from decimal import Decimal
from pathlib import Path

import numpy as np

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.types import Tenor  # noqa: E402
from fuggers_py.core import Currency, Date  # noqa: E402
from fuggers_py.curves import (  # noqa: E402
    CreditCurve,
    CurveBuilder,
    CurveFamily,
    CurveTransform,
    CurrencyPair,
    DelegatedCurve,
    DerivedCurve,
    DiscountCurveBuilder,
    ForwardCurve,
    InterpolationMethod,
    RateCurve,
    SegmentBuilder,
    ValueType,
    ZeroCurveBuilder,
    key_rate_profile,
)

np.set_printoptions(precision=10, suppress=True)


# %% [markdown]
# ## Build a curve from zero rates (date-based)
#
# We'll build a simple curve from a few dated **continuous** zero rates.

# %%
ref = Date.from_ymd(2024, 1, 1)

zc: RateCurve = (
    ZeroCurveBuilder(reference_date=ref)
    .with_interpolation(InterpolationMethod.LINEAR)
    .add_rate(ref.add_days(365), Decimal("0.05"))
    .add_rate(ref.add_days(365 * 2), Decimal("0.05"))
    .build()
)

zc.reference_date(), zc.max_date()

# %% [markdown]
# ## Query discount factors and zero rates

# %%
one_year = ref.add_days(365)
two_year = ref.add_days(365 * 2)

df_1y = zc.discount_factor(one_year)
df_2y = zc.discount_factor(two_year)
z_1y = zc.zero_rate(one_year)
z_2y = zc.zero_rate(two_year)

print("DF(1y):", df_1y, "expected:", Decimal(str(math.exp(-0.05))))
print("DF(2y):", df_2y, "expected:", Decimal(str(math.exp(-0.10))))
print("z(1y):", z_1y, "(decimal rate)")
print("z(2y):", z_2y, "(decimal rate)")

# %% [markdown]
# ## Compute a forward rate (date-based)
#
# `core.traits.YieldCurve.forward_rate` returns an annualized **simple** forward
# rate, using ACT/365F.

# %%
fwd_simple = zc.forward_rate(one_year, two_year)
print("simple fwd(1y,2y):", fwd_simple)

# %% [markdown]
# ## Forward curve helper (tenor-based)
#
# `ForwardCurve` wraps a discount curve and a forward tenor. Here we create a
# 1Y forward curve and evaluate the forward rate at `t=1.0` (i.e., 1Y→2Y).

# %%
fwd_curve = ForwardCurve.new(zc, forward_tenor=1.0)
fwd_cont_1y_2y = fwd_curve.forward_rate_at(1.0)
print("forward tenor:", fwd_curve.forward_tenor())
print("continuous fwd(1.0,2.0):", fwd_cont_1y_2y)

# %%
# Sanity check: for a constant continuous zero rate of 5%, the instantaneous forward is 5%.
assert abs(fwd_cont_1y_2y - 0.05) < 1e-12

# %% [markdown]
# ## Compare interpolation methods (discount factors)

# %%
r = 0.05
df_pillars = {
    1.0: math.exp(-r * 1.0),
    2.0: math.exp(-r * 2.0),
}

dc_linear = (
    DiscountCurveBuilder(reference_date=ref)
    .with_interpolation(InterpolationMethod.LINEAR)
    .add_pillar(1.0, df_pillars[1.0])
    .add_pillar(2.0, df_pillars[2.0])
    .build()
)
dc_log_linear = (
    DiscountCurveBuilder(reference_date=ref)
    .with_interpolation(InterpolationMethod.LOG_LINEAR)
    .add_pillar(1.0, df_pillars[1.0])
    .add_pillar(2.0, df_pillars[2.0])
    .build()
)

t = 1.5
print("DF linear(t=1.5):", dc_linear.curve.value_at(t))
print("DF log-linear(t=1.5):", dc_log_linear.curve.value_at(t))
print("DF exact (5% cont):", math.exp(-r * t))

# %% [markdown]
# ## Out-of-range behavior (`value_at` vs `try_value_at`)
#
# - `value_at(t)` extrapolates (flat by default)
# - `try_value_at(t)` enforces tenor bounds

# %%
t = 0.5
print("zc.curve.tenor_bounds():", zc.curve.tenor_bounds())
print("in_range(t=0.5):", zc.curve.in_range(t))
print("value_at(t=0.5):", zc.curve.value_at(t))
print("try_value_at would raise:", "yes" if not zc.curve.in_range(t) else "no")

# %% [markdown]
# ## Segment, delegate, and derive curves

# %%
segmented = (
    CurveBuilder.discount(ref)
    .add_segment(
        SegmentBuilder(0.0, 5.0).with_pillars(
            [0.0, 2.0, 5.0],
            [1.0, 0.97, 0.90],
            value_type=ValueType.discount_factor(),
            interpolation_method=InterpolationMethod.LOG_LINEAR,
        )
    )
    .add_segment(
        SegmentBuilder(5.0, 30.0).with_pillars(
            [5.0, 10.0, 30.0],
            [0.90, 0.80, 0.40],
            value_type=ValueType.discount_factor(),
            interpolation_method=InterpolationMethod.LOG_LINEAR,
        )
    )
    .build()
)
derived = CurveTransform.parallel_shift(Decimal("0.0025"))
derived_curve = CurveBuilder.zero(ref).add_zero_rate(1.0, Decimal("0.03")).add_zero_rate(30.0, Decimal("0.03")).build()
shifted_curve = DerivedCurve.from_curve(derived_curve, derived)

primary_curve = DiscountCurveBuilder(reference_date=ref).add_pillar(0.0, 1.0).add_pillar(2.0, 0.95).build()
fallback_curve = DiscountCurveBuilder(reference_date=ref).add_pillar(0.0, 1.0).add_pillar(10.0, 0.75).build()
delegated = DelegatedCurve(primary=primary_curve.curve, fallback=fallback_curve.curve)

print("Segmented DF(7Y):", segmented.discount_factor(ref.add_days(365 * 7)))
print("Delegated raw DF(5Y):", delegated.value_at(5.0))
print("Shifted zero rate (10Y):", shifted_curve.zero_rate(ref.add_days(365 * 10)))
print("Currency pair:", CurrencyPair(base=Currency.USD, quote=Currency.EUR))
print("Key-rate profile (5Y):", key_rate_profile(Tenor.parse("5Y")))

# %% [markdown]
# ## Credit-curve wrapper

# %%
credit_curve = (
    CurveBuilder.credit(ref, family=CurveFamily.HAZARD_RATE)
    .add_pillar(1.0, Decimal("0.02"))
    .add_pillar(5.0, Decimal("0.02"))
    .build()
)
print("Survival probability at 5Y:", credit_curve.survival_probability(ref.add_days(365 * 5)))
