# Credit Instruments

Bonds, credit derivatives, and structured products for pricing, risk, and spread analytics.

All types are available via flat import:

```python notest
from vade import FixedRateBond, Bill, FloatRateNote, SubPeriodFRN
from vade import ZeroCouponBond, StepUpBond, AmortizingBond, PIKBond
from vade import CappedFloatRateNote, AssetSwap, CallableBond, CDS
```

Or via product-path import:

```python notest
from vade.instruments.credit import FixedRateBond, Bill, FloatRateNote, SubPeriodFRN
from vade.instruments.credit import ZeroCouponBond, StepUpBond, AmortizingBond, PIKBond
from vade.instruments.credit import CappedFloatRateNote, AssetSwap, CallableBond, CDS
```

See [Conventions](../../conventions.md) for all accepted string parameter values.

**Contents:**
[FixedRateBond](#fixedratebond) | [Bill](#bill) | [FloatRateNote](#floatratenote) | [SubPeriodFRN](#subperiodfrn) | [ZeroCouponBond](#zerocouponbond) | [StepUpBond](#stepupbond) | [AmortizingBond](#amortizingbond) | [PIKBond](#pikbond) | [CappedFloatRateNote](#cappedfloatratenote) | [AssetSwap](#assetswap) | [CallableBond](#callablebond) | [CDS](#cds)

---

## FixedRateBond

Fixed Rate Bond with full analytics suite. _Python wrapper composing FixedLeg with bond analytics._

See also: [Bonds Guide](../guides/credit/bonds.md) for bond pricing, yield analysis, and risk metrics.

### Constructor

```python notest
FixedRateBond(
    *,
    spec=None,
    effective=None,
    termination=None,
    coupon=0.0,
    frequency="s",
    settlement_days=1,
    ex_div_days=0,
    calendar=None,
    currency="USD",
    convention="actactisda",
    accrued_convention=None,
    face_value=100.0,
    modifier="none",
    disc_curve_id=None,
)
```

### Parameters

| Name                 | Type                       | Default        | Description                                                     |
| -------------------- | -------------------------- | -------------- | --------------------------------------------------------------- |
| `spec`               | `str` or `None`            | `None`         | Instrument spec name                                            |
| `effective`          | `datetime.date` or `None`  | `None`         | Issue or settlement date                                        |
| `termination`        | `date`, `str`, or `None`   | `None`         | Maturity date or tenor string                                   |
| `coupon`             | `float`                    | `0.0`          | Coupon rate (percentage, e.g., `5.0` for 5%)                    |
| `frequency`          | `str`                      | `"s"`          | Coupon payment frequency                                        |
| `settlement_days`    | `int`                      | `1`            | Settlement lag in business days                                 |
| `ex_div_days`        | `int`                      | `0`            | Ex-dividend days before coupon payment                          |
| `calendar`           | `str` or `None`            | `None`         | Named calendar                                                  |
| `currency`           | `str`                      | `"USD"`        | Currency code                                                   |
| `convention`         | `str`                      | `"actactisda"` | Day count convention for coupon accrual                         |
| `accrued_convention` | `str` or `None`            | `None`         | Separate day count for accrued interest (defaults to `convention`) |
| `face_value`         | `float`                    | `100.0`        | Face value of the bond                                          |
| `modifier`           | `str`                      | `"none"`       | Business day adjustment rule                                    |
| `disc_curve_id`      | `str` or `None`            | `None`         | Discount curve identifier for [Solver](solver.md#solver) lookup |

See [Conventions](../conventions.md) for accepted values for `frequency`, `convention`, and `modifier`.

### Methods

#### Standard Methods

| Method                                    | Returns     | Description                |
| ----------------------------------------- | ----------- | -------------------------- |
| `.rate(curves, *, solver=None)`           | `float`     | Par rate against curves    |
| `.npv(curves, *, solver=None)`            | `float`     | Net present value          |
| `.cashflows(curves, *, solver=None)`      | `DataFrame` | Period-level cashflow table |
| `.spread(curves, *, solver=None)`         | `float`     | Par spread                 |

#### Price and Yield

| Method                                                             | Returns | Description                             |
| ------------------------------------------------------------------ | ------- | --------------------------------------- |
| `.price(curves, *, solver=None, settlement=None)`                  | `float` | Clean price                             |
| `.dirty_price(curves, *, solver=None, settlement=None)`            | `float` | Dirty price (clean + accrued)           |
| `.accrued_interest(settlement)`                                    | `float` | Accrued interest at settlement date     |
| `.ytm(clean_price, settlement, convention="periodic")`             | `float` | Yield to maturity from clean price      |
| `.price_from_ytm(ytm, settlement, convention="periodic")`          | `float` | Clean price from yield to maturity      |

YTM `convention` accepts `"periodic"`, `"annual"`, `"semi_annual"`, or `"continuous"`.

#### Risk Analytics

| Method                                                                                  | Returns | Description                     |
| --------------------------------------------------------------------------------------- | ------- | ------------------------------- |
| `.duration(curves, *, solver=None, metric="modified", settlement=None)`                 | `float` | Modified or Macaulay duration   |
| `.convexity(curves, *, solver=None, settlement=None)`                                   | `float` | Price convexity                 |
| `.dv01(curves, *, solver=None)`                                                         | `float` | Dollar value of 1bp             |
| `.z_spread(curves, *, solver=None, price=None, settlement=None)`                        | `float` | Z-spread over curve             |
| `.asw_spread(curves, *, solver=None, price=None, method="par_par", settlement=None)`    | `float` | Asset swap spread               |
| `.cs01(curves, *, solver=None)`                                                         | `float` | Credit spread sensitivity (1bp) |
| `.price_yield_sensitivity(curves, *, solver=None)`                                      | `float` | Price-yield sensitivity         |
| `.analytic_delta(curves, *, solver=None)`                                               | `float` | Analytic delta                  |

Duration `metric` accepts `"modified"` or `"macaulay"`. ASW `method` accepts `"par_par"` or `"proceeds"`.

### Example

```python
import datetime
from vade import FixedRateBond, DiscountCurve

nodes = {
    datetime.date(2024, 1, 1): 1.0,
    datetime.date(2025, 1, 1): 0.97,
    datetime.date(2026, 1, 1): 0.94,
    datetime.date(2027, 1, 1): 0.91,
}
curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f")

bond = FixedRateBond(
    effective=datetime.date(2024, 1, 1),
    termination="2Y",
    coupon=5.0,
    frequency="s",
    convention="actactisda",
)
bond.npv(curve)  # net present value
bond.price(curve, settlement=datetime.date(2024, 1, 2))  # clean price
bond.dirty_price(curve, settlement=datetime.date(2024, 1, 2))  # dirty price
bond.accrued_interest(datetime.date(2024, 3, 1))  # accrued at settlement
bond.ytm(99.5, datetime.date(2024, 1, 2))  # yield to maturity
bond.duration(curve, metric="modified", settlement=datetime.date(2024, 1, 2))  # modified duration
```

______________________________________________________________________

## Bill

Zero-coupon bill (Treasury bill). _Python wrapper composing zero-coupon cashflow with bill analytics._

### Constructor

```python notest
Bill(
    *,
    spec=None,
    effective=None,
    termination=None,
    settlement_days=1,
    calendar=None,
    currency="USD",
    convention="act360",
    face_value=100.0,
    disc_curve_id=None,
)
```

### Parameters

| Name              | Type                       | Default    | Description                                                     |
| ----------------- | -------------------------- | ---------- | --------------------------------------------------------------- |
| `spec`            | `str` or `None`            | `None`     | Instrument spec name                                            |
| `effective`       | `datetime.date` or `None`  | `None`     | Issue date                                                      |
| `termination`     | `date`, `str`, or `None`   | `None`     | Maturity date or tenor string (e.g., `"6M"`)                    |
| `settlement_days` | `int`                      | `1`        | Settlement lag in business days                                 |
| `calendar`        | `str` or `None`            | `None`     | Named calendar                                                  |
| `currency`        | `str`                      | `"USD"`    | Currency code                                                   |
| `convention`      | `str`                      | `"act360"` | Day count convention                                            |
| `face_value`      | `float`                    | `100.0`    | Face value                                                      |
| `disc_curve_id`   | `str` or `None`            | `None`     | Discount curve identifier for [Solver](solver.md#solver) lookup |

See [Conventions](../conventions.md) for accepted values for `convention`.

### Methods

| Method                                              | Returns     | Description                     |
| --------------------------------------------------- | ----------- | ------------------------------- |
| `.rate(curves, *, solver=None)`                     | `float`     | Par rate against curves         |
| `.npv(curves, *, solver=None)`                      | `float`     | Net present value               |
| `.cashflows(curves, *, solver=None)`                | `DataFrame` | Period-level cashflow table     |
| `.price(curves, *, solver=None)`                    | `float`     | Bill price                      |
| `.dirty_price(curves, *, solver=None)`              | `float`     | Dirty price                     |
| `.ytm(clean_price, settlement)`                     | `float`     | Yield to maturity from price    |

### Example

```python
import datetime
from vade import Bill, DiscountCurve

nodes = {datetime.date(2024, 1, 1): 1.0, datetime.date(2024, 7, 1): 0.985, datetime.date(2025, 1, 1): 0.97}
curve = DiscountCurve(nodes, interpolation="log_linear", convention="act360")

bill = Bill(
    effective=datetime.date(2024, 1, 1),
    termination="6M",
    convention="act360",
)
bill.rate(curve)  # implied discount rate
bill.price(curve)  # bill price
bill.ytm(99.0, datetime.date(2024, 1, 2))  # yield from price
```

### Advanced Example

```python
import datetime, math
from vade import Bill, DiscountCurve, LineCurve

# 6-month US Treasury Bill
bill = Bill(
    effective=datetime.date(2024, 6, 15),
    termination="6m",
    convention="act360",
    currency="usd",
)

# Flat 4% discount curve
base = datetime.date(2024, 6, 15)
nodes = {base: 1.0}
for y in range(1, 6):
    nodes[datetime.date(2024 + y, 6, 15)] = math.exp(-0.04 * y)
curve = DiscountCurve(nodes, interpolation="log_linear", convention="act360")

# Pricing
price = bill.price(curve)
assert 97.0 < float(price) < 100.0

dirty = bill.dirty_price(curve)
assert 97.0 < float(dirty) < 100.0

# NPV
npv = bill.npv(curve)
assert isinstance(float(npv), float)

# Cashflows -- single redemption row
cf = bill.cashflows(curve)
assert len(cf) >= 1

# Yield to maturity (from price and settlement date)
settlement = datetime.date(2024, 6, 16)
ytm = bill.ytm(float(price), settlement)
assert 0.02 < float(ytm) < 0.06

# Rate (discount rate)
rate = bill.rate(curve)
assert isinstance(float(rate), float)

# I-spread against a benchmark swap curve
benchmark = LineCurve({base: 0.035, datetime.date(2030, 6, 15): 0.04})
i_spr = bill.i_spread(settlement, benchmark)
assert isinstance(float(i_spr), float)
```

______________________________________________________________________

## FloatRateNote

Floating Rate Note (FRN). _Python wrapper composing FloatLeg with FRN analytics._

### Constructor

```python notest
FloatRateNote(
    *,
    spec=None,
    effective=None,
    termination=None,
    spread=0.0,
    frequency="q",
    settlement_days=2,
    ex_div_days=0,
    calendar=None,
    currency="USD",
    convention="act360",
    accrued_convention=None,
    face_value=100.0,
    modifier="mf",
    disc_curve_id=None,
)
```

### Parameters

| Name                 | Type                       | Default    | Description                                                        |
| -------------------- | -------------------------- | ---------- | ------------------------------------------------------------------ |
| `spec`               | `str` or `None`            | `None`     | Instrument spec name                                               |
| `effective`          | `datetime.date` or `None`  | `None`     | Issue date                                                         |
| `termination`        | `date`, `str`, or `None`   | `None`     | Maturity date or tenor string                                      |
| `spread`             | `float`                    | `0.0`      | Spread over floating rate (basis points)                           |
| `frequency`          | `str`                      | `"q"`      | Coupon payment frequency                                           |
| `settlement_days`    | `int`                      | `2`        | Settlement lag in business days                                    |
| `ex_div_days`        | `int`                      | `0`        | Ex-dividend days before coupon payment                             |
| `calendar`           | `str` or `None`            | `None`     | Named calendar                                                     |
| `currency`           | `str`                      | `"USD"`    | Currency code                                                      |
| `convention`         | `str`                      | `"act360"` | Day count convention                                               |
| `accrued_convention` | `str` or `None`            | `None`     | Separate day count for accrued interest (defaults to `convention`) |
| `face_value`         | `float`                    | `100.0`    | Face value                                                         |
| `modifier`           | `str`                      | `"mf"`     | Business day adjustment rule                                       |
| `disc_curve_id`      | `str` or `None`            | `None`     | Discount curve identifier for [Solver](solver.md#solver) lookup    |

See [Conventions](../conventions.md) for accepted values for `frequency`, `convention`, and `modifier`.

### Methods

| Method                                    | Returns     | Description                |
| ----------------------------------------- | ----------- | -------------------------- |
| `.rate(curves, *, solver=None)`           | `float`     | Par rate against curves    |
| `.npv(curves, *, solver=None)`            | `float`     | Net present value          |
| `.cashflows(curves, *, solver=None)`      | `DataFrame` | Period-level cashflow table |
| `.price(curves, *, solver=None)`          | `float`     | Clean price                |
| `.dirty_price(curves, *, solver=None)`    | `float`     | Dirty price                |
| `.analytic_delta(curves, *, solver=None)` | `float`     | Analytic delta             |

### Example

```python
import datetime
from vade import FloatRateNote, DiscountCurve

nodes = {datetime.date(2024, 1, 1): 1.0, datetime.date(2025, 1, 1): 0.97, datetime.date(2026, 1, 1): 0.94}
curve = DiscountCurve(nodes, interpolation="log_linear", convention="act360")

frn = FloatRateNote(
    effective=datetime.date(2024, 1, 1),
    termination="1Y",
    frequency="q",
    spread=50.0,
    convention="act360",
)
frn.rate(curve)  # par spread
frn.npv(curve)  # net present value
frn.price(curve)  # clean price
```

______________________________________________________________________

## SubPeriodFRN

A floating rate note where the coupon payment frequency differs from the fixing frequency. Each coupon period compounds multiple sub-period fixings: coupon = Product(1 + r\_i \* dcf\_i) - 1 + spread. _Python wrapper composing FloatLeg with sub-period compounding._

For example, a quarterly-pay / monthly-fix SubPeriodFRN has 4 coupon periods per year, but the floating rate within each coupon is compounded from ~3 monthly fixings. This is common in leveraged loan and CLO markets.

### Constructor

```python notest
SubPeriodFRN(
    *,
    effective=None,
    termination=None,
    spread=0.0,
    frequency="q",
    fixing_frequency="m",
    settlement_days=2,
    ex_div_days=0,
    calendar=None,
    currency="USD",
    convention="act360",
    accrued_convention=None,
    face_value=100.0,
    modifier="mf",
    disc_curve_id=None,
    index=None,
)
```

### Parameters

| Name                 | Type                       | Default    | Description                                                              |
| -------------------- | -------------------------- | ---------- | ------------------------------------------------------------------------ |
| `effective`          | `datetime.date` or `None`  | `None`     | Issue date                                                               |
| `termination`        | `date`, `str`, or `None`   | `None`     | Maturity date or tenor string                                            |
| `spread`             | `float`                    | `0.0`      | Spread over floating rate (decimal, e.g., `0.001` = 10bp)               |
| `frequency`          | `str`                      | `"q"`      | Coupon payment frequency                                                 |
| `fixing_frequency`   | `str`                      | `"m"`      | Sub-period fixing frequency (must be higher than `frequency`)            |
| `settlement_days`    | `int`                      | `2`        | Settlement lag in business days                                          |
| `ex_div_days`        | `int`                      | `0`        | Ex-dividend days before coupon payment                                   |
| `calendar`           | `str` or `None`            | `None`     | Named calendar                                                           |
| `currency`           | `str`                      | `"USD"`    | Currency code                                                            |
| `convention`         | `str`                      | `"act360"` | Day count convention                                                     |
| `accrued_convention` | `str` or `None`            | `None`     | Separate day count for accrued interest (defaults to `convention`)       |
| `face_value`         | `float`                    | `100.0`    | Face value                                                               |
| `modifier`           | `str`                      | `"mf"`     | Business day adjustment rule                                             |
| `disc_curve_id`      | `str` or `None`            | `None`     | Discount curve identifier for [Solver](solver.md#solver) lookup          |
| `index`              | `str` or `None`            | `None`     | Floating rate index name for fixing lookups                              |

See [Conventions](../conventions.md) for accepted values for `frequency`, `convention`, and `modifier`.

### Methods

#### Standard Methods

| Method                                    | Returns     | Description                |
| ----------------------------------------- | ----------- | -------------------------- |
| `.rate(curves, *, solver=None)`           | `float`     | Par rate against curves    |
| `.npv(curves, *, solver=None)`            | `float`     | Net present value          |
| `.cashflows(curves, *, solver=None)`      | `DataFrame` | Period-level cashflow table |
| `.spread(curves, *, solver=None)`         | `float`     | Par spread                 |

#### Price and Yield

| Method                                                             | Returns | Description                             |
| ------------------------------------------------------------------ | ------- | --------------------------------------- |
| `.price(curves, *, solver=None, settlement=None)`                  | `float` | Clean price                             |
| `.dirty_price(curves, *, solver=None, settlement=None)`            | `float` | Dirty price (clean + accrued)           |
| `.accrued_interest(settlement)`                                    | `float` | Accrued interest at settlement date     |
| `.ytm(clean_price, settlement, convention="periodic")`             | `float` | Yield to maturity from clean price      |
| `.price_from_ytm(ytm, settlement, convention="periodic")`          | `float` | Clean price from yield to maturity      |

#### Risk Analytics

| Method                                                                                  | Returns | Description                     |
| --------------------------------------------------------------------------------------- | ------- | ------------------------------- |
| `.duration(curves, *, solver=None, metric="modified", settlement=None)`                 | `float` | Modified or Macaulay duration   |
| `.convexity(curves, *, solver=None, settlement=None)`                                   | `float` | Price convexity                 |
| `.dv01(curves, *, solver=None)`                                                         | `float` | Dollar value of 1bp             |
| `.z_spread(curves, *, solver=None, price=None, settlement=None)`                        | `float` | Z-spread over curve             |
| `.asw_spread(curves, *, solver=None, price=None, method="par_par", settlement=None)`    | `float` | Asset swap spread               |
| `.analytic_delta(curves, *, solver=None)`                                               | `float` | Analytic delta                  |
| `.i_spread(settlement, swap_curve)`                                                     | `float` | Interpolated spread vs swap curve |

#### SubPeriodFRN-Specific

| Method                  | Returns                                        | Description                                   |
| ----------------------- | ---------------------------------------------- | --------------------------------------------- |
| `.sub_period_schedule()` | `list[tuple[date, date, date, list[tuple]]]`  | Nested schedule showing sub-period structure   |

Each element of the outer list is a coupon period tuple: `(accrual_start, accrual_end, payment_date, sub_periods)`. Each sub-period is a tuple: `(start, end, dcf, fixing)`.

### Example

```python
import datetime
from vade import SubPeriodFRN, DiscountCurve

# Build a discount curve
nodes = {
    datetime.date(2024, 1, 15): 1.0,
    datetime.date(2025, 1, 15): 0.965,
    datetime.date(2026, 1, 15): 0.93,
    datetime.date(2030, 1, 15): 0.85,
}
curve = DiscountCurve(nodes, interpolation="log_linear")

# 2Y quarterly-pay / monthly-fix SubPeriodFRN with 10bp spread
frn = SubPeriodFRN(
    effective=datetime.date(2024, 1, 15),
    termination=datetime.date(2026, 1, 15),
    spread=0.001,
    frequency="q",
    fixing_frequency="m",
    convention="act360",
)

# Price using [discount_curve, forecast_curve] list
dp = frn.dirty_price(curves=[curve, curve])
assert isinstance(dp, float)
assert 90 < dp < 110  # near par

npv = frn.npv(curves=[curve, curve])
assert isinstance(npv, float)
```

### Advanced Example

```python
import datetime
from vade import SubPeriodFRN, DiscountCurve

# Build curve with several nodes for accurate forward rates
nodes = {
    datetime.date(2024, 1, 15): 1.0,
    datetime.date(2024, 7, 15): 0.982,
    datetime.date(2025, 1, 15): 0.965,
    datetime.date(2025, 7, 15): 0.948,
    datetime.date(2026, 1, 15): 0.93,
}
curve = DiscountCurve(nodes, interpolation="log_linear")

# 1Y SubPeriodFRN: quarterly coupon, monthly sub-period compounding
frn = SubPeriodFRN(
    effective=datetime.date(2024, 1, 15),
    termination=datetime.date(2025, 1, 15),
    spread=0.0,
    frequency="q",
    fixing_frequency="m",
    convention="act360",
)

# Inspect sub-period structure: 4 quarterly periods, each with ~3 monthly fixings
schedule = frn.sub_period_schedule()
assert len(schedule) == 4  # 4 quarterly coupon periods

for accrual_start, accrual_end, payment_date, sub_periods in schedule:
    assert accrual_start < accrual_end  # valid period
    assert len(sub_periods) >= 2  # at least 2 sub-period fixings per quarter
    for sub_start, sub_end, dcf, fixing in sub_periods:
        assert sub_start < sub_end
        assert 0 < dcf < 1  # day count fraction for a monthly period

# Pricing and analytics
dp = frn.dirty_price(curves=[curve, curve])
assert 90 < dp < 110  # near par for zero-spread FRN

npv = frn.npv(curves=[curve, curve])
assert isinstance(npv, float)
assert abs(npv - dp) < 1e-6  # NPV equals dirty price for FRN

ad = frn.analytic_delta(curves=[curve, curve])
assert isinstance(ad, float)
assert ad < 0  # negative delta (price falls as rates rise)
```

______________________________________________________________________

## ZeroCouponBond

Zero-coupon bond with OID (Original Issue Discount) accrued interest. _Python wrapper composing zero-coupon cashflow with compound discounting._

Accrued interest uses the OID compound method: at any settlement date, accrued equals the amortized cost (issue price compounded at yield) minus the issue price, prorated within each semi-annual period.

### Constructor

```python notest
ZeroCouponBond(
    *,
    effective=None,
    termination=None,
    issue_price=100.0,
    frequency="s",
    settlement_days=1,
    calendar=None,
    currency="USD",
    convention="actactisda",
    face_value=100.0,
    disc_curve_id=None,
)
```

### Parameters

| Name              | Type                       | Default        | Description                                                     |
| ----------------- | -------------------------- | -------------- | --------------------------------------------------------------- |
| `effective`       | `datetime.date` or `None`  | `None`         | Issue date                                                      |
| `termination`     | `date`, `str`, or `None`   | `None`         | Maturity date or tenor string                                   |
| `issue_price`     | `float`                    | `100.0`        | Original issue price (OID basis for accrued calculation)        |
| `frequency`       | `str`                      | `"s"`          | Accrual frequency for OID calculation                           |
| `settlement_days` | `int`                      | `1`            | Settlement lag in business days                                 |
| `calendar`        | `str` or `None`            | `None`         | Named calendar                                                  |
| `currency`        | `str`                      | `"USD"`        | Currency code                                                   |
| `convention`      | `str`                      | `"actactisda"` | Day count convention                                            |
| `face_value`      | `float`                    | `100.0`        | Face value at maturity                                          |
| `disc_curve_id`   | `str` or `None`            | `None`         | Discount curve identifier for [Solver](solver.md#solver) lookup |

See [Conventions](../conventions.md) for accepted values for `frequency` and `convention`.

### Methods

#### Standard Methods

| Method                                    | Returns     | Description                |
| ----------------------------------------- | ----------- | -------------------------- |
| `.rate(curves, *, solver=None)`           | `float`     | Par rate against curves    |
| `.npv(curves, *, solver=None)`            | `float`     | Net present value          |
| `.cashflows(curves, *, solver=None)`      | `DataFrame` | Period-level cashflow table |

#### Price and Yield

| Method                                                             | Returns | Description                             |
| ------------------------------------------------------------------ | ------- | --------------------------------------- |
| `.price(curves, *, solver=None, settlement=None)`                  | `float` | Clean price                             |
| `.dirty_price(curves, *, solver=None, settlement=None)`            | `float` | Dirty price (clean + OID accrued)       |
| `.accrued_interest(settlement)`                                    | `float` | OID accrued interest at settlement date |
| `.ytm(clean_price, settlement, convention="periodic")`             | `float` | Yield to maturity from clean price      |
| `.price_from_ytm(ytm, settlement, convention="periodic")`          | `float` | Clean price from yield to maturity      |

#### Risk Analytics

| Method                                                                                  | Returns | Description                   |
| --------------------------------------------------------------------------------------- | ------- | ----------------------------- |
| `.duration(curves, *, solver=None, metric="modified", settlement=None)`                 | `float` | Modified or Macaulay duration |
| `.convexity(curves, *, solver=None, settlement=None)`                                   | `float` | Price convexity               |
| `.z_spread(curves, *, solver=None, price=None, settlement=None)`                        | `float` | Z-spread over curve           |

### Example

```python
import math
import datetime
from vade import ZeroCouponBond, DiscountCurve

# Flat 4% continuously compounded curve
base = datetime.date(2024, 1, 1)
nodes = {base: 1.0}
for y in range(1, 11):
    nodes[datetime.date(2024 + y, 1, 1)] = math.exp(-0.04 * y)
curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f")

zcb = ZeroCouponBond(
    effective=datetime.date(2024, 1, 1),
    termination=datetime.date(2029, 1, 1),
    issue_price=80.0,
    face_value=100.0,
    frequency="s",
    convention="actactisda",
)
settlement = datetime.date(2024, 1, 2)

price = zcb.price(curve, settlement=settlement)
assert 50.0 < price < 80.0  # deep discount zero-coupon bond

accrued = zcb.accrued_interest(settlement)
assert accrued >= 0.0  # OID accrued is non-negative

ytm = zcb.ytm(float(price), settlement)
assert 0.03 < ytm < 0.06  # yield in reasonable range

dur = zcb.duration(curve, metric="modified", settlement=settlement)
assert dur > 4.0  # near maturity duration for 5Y zero
```

### Advanced Example

```python
import datetime, math
from vade import ZeroCouponBond, DiscountCurve

# 5-year zero coupon bond
zcb = ZeroCouponBond(
    effective=datetime.date(2024, 1, 15),
    termination="5y",
    face_value=100.0,
    frequency="s",
    convention="act365f",
    currency="usd",
)

# Flat 4% discount curve
base = datetime.date(2024, 1, 15)
nodes = {base: 1.0}
for y in range(1, 11):
    nodes[datetime.date(2024 + y, 1, 15)] = math.exp(-0.04 * y)
curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f")

settlement = datetime.date(2024, 1, 16)

# Pricing and yield
price = zcb.price(curve, settlement=settlement)
assert 75.0 < float(price) < 95.0

ytm = zcb.ytm(float(price), settlement)
assert 0.02 < float(ytm) < 0.06

# Price-from-YTM roundtrip
price_rt = zcb.price_from_ytm(ytm, settlement)
assert abs(float(price_rt) - float(price)) < 0.01

# OID accrued interest grows over time (requires issue_price < face_value)
zcb_oid = ZeroCouponBond(
    effective=datetime.date(2024, 1, 15),
    termination="5y",
    issue_price=80.0,
    face_value=100.0,
    frequency="s",
    convention="act365f",
    currency="usd",
)
ai_early = zcb_oid.accrued_interest(datetime.date(2025, 1, 15))
ai_mid = zcb_oid.accrued_interest(datetime.date(2027, 1, 15))
assert float(ai_mid) > float(ai_early)  # accrued grows over time

# Cashflows -- single redemption
cf = zcb.cashflows(curve)
assert len(cf) >= 1

# Risk analytics
dur = zcb.duration(curve, settlement=settlement)
assert 3.0 < float(dur) < 6.0

conv = zcb.convexity(curve, settlement=settlement)
assert float(conv) > 0

z_spr = zcb.z_spread(curve, price=float(price), settlement=settlement)
assert abs(float(z_spr)) < 0.01  # near zero when priced off same curve
```

______________________________________________________________________

## StepUpBond

Fixed-rate bond with coupon rates that change at specified dates. _Python wrapper composing FixedLeg with dated rate resolution._

The `coupon_rates` parameter replaces the single `coupon` used by FixedRateBond, allowing step-up (or step-down) coupon schedules where the rate changes at predefined dates.

### Constructor

```python notest
StepUpBond(
    *,
    effective=None,
    termination=None,
    coupon_rates=None,
    frequency="s",
    settlement_days=1,
    ex_div_days=0,
    calendar=None,
    currency="USD",
    convention="actactisda",
    accrued_convention=None,
    face_value=100.0,
    modifier="none",
    disc_curve_id=None,
)
```

### Parameters

| Name                 | Type                               | Default        | Description                                                        |
| -------------------- | ---------------------------------- | -------------- | ------------------------------------------------------------------ |
| `effective`          | `datetime.date` or `None`          | `None`         | Issue date                                                         |
| `termination`        | `date`, `str`, or `None`           | `None`         | Maturity date or tenor string                                      |
| `coupon_rates`       | `list[tuple[date, float]]` or `None` | `None`       | Dated coupon rate schedule as `[(date, rate), ...]`                 |
| `frequency`          | `str`                              | `"s"`          | Coupon payment frequency                                           |
| `settlement_days`    | `int`                              | `1`            | Settlement lag in business days                                    |
| `ex_div_days`        | `int`                              | `0`            | Ex-dividend days before coupon payment                             |
| `calendar`           | `str` or `None`                    | `None`         | Named calendar                                                     |
| `currency`           | `str`                              | `"USD"`        | Currency code                                                      |
| `convention`         | `str`                              | `"actactisda"` | Day count convention for coupon accrual                            |
| `accrued_convention` | `str` or `None`                    | `None`         | Separate day count for accrued interest (defaults to `convention`) |
| `face_value`         | `float`                            | `100.0`        | Face value of the bond                                             |
| `modifier`           | `str`                              | `"none"`       | Business day adjustment rule                                       |
| `disc_curve_id`      | `str` or `None`                    | `None`         | Discount curve identifier for [Solver](solver.md#solver) lookup    |

See [Conventions](../conventions.md) for accepted values for `frequency`, `convention`, and `modifier`.

### Methods

#### Standard Methods

| Method                                    | Returns     | Description                |
| ----------------------------------------- | ----------- | -------------------------- |
| `.rate(curves, *, solver=None)`           | `float`     | Par rate against curves    |
| `.npv(curves, *, solver=None)`            | `float`     | Net present value          |
| `.cashflows(curves, *, solver=None)`      | `DataFrame` | Period-level cashflow table |
| `.spread(curves, *, solver=None)`         | `float`     | Par spread                 |

#### Price and Yield

| Method                                                             | Returns | Description                             |
| ------------------------------------------------------------------ | ------- | --------------------------------------- |
| `.price(curves, *, solver=None, settlement=None)`                  | `float` | Clean price                             |
| `.dirty_price(curves, *, solver=None, settlement=None)`            | `float` | Dirty price (clean + accrued)           |
| `.accrued_interest(settlement)`                                    | `float` | Accrued interest at settlement date     |
| `.ytm(clean_price, settlement, convention="periodic")`             | `float` | Yield to maturity from clean price      |
| `.price_from_ytm(ytm, settlement, convention="periodic")`          | `float` | Clean price from yield to maturity      |

#### Risk Analytics

| Method                                                                                  | Returns | Description                     |
| --------------------------------------------------------------------------------------- | ------- | ------------------------------- |
| `.duration(curves, *, solver=None, metric="modified", settlement=None)`                 | `float` | Modified or Macaulay duration   |
| `.convexity(curves, *, solver=None, settlement=None)`                                   | `float` | Price convexity                 |
| `.dv01(curves, *, solver=None)`                                                         | `float` | Dollar value of 1bp             |
| `.z_spread(curves, *, solver=None, price=None, settlement=None)`                        | `float` | Z-spread over curve             |
| `.asw_spread(curves, *, solver=None, price=None, method="par_par", settlement=None)`    | `float` | Asset swap spread               |
| `.analytic_delta(curves, *, solver=None)`                                               | `float` | Analytic delta                  |

### Example

```python
import math
import datetime
from vade import StepUpBond, DiscountCurve

# Flat 4% continuously compounded curve
base = datetime.date(2024, 1, 1)
nodes = {base: 1.0}
for y in range(1, 11):
    nodes[datetime.date(2024 + y, 1, 1)] = math.exp(-0.04 * y)
curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f")

bond = StepUpBond(
    effective=datetime.date(2024, 1, 1),
    termination=datetime.date(2029, 1, 1),
    coupon_rates=[
        (datetime.date(2024, 1, 1), 0.03),   # 3% for first 3 years
        (datetime.date(2027, 1, 1), 0.05),   # steps up to 5%
    ],
    frequency="s",
    face_value=100.0,
    convention="actactisda",
)
settlement = datetime.date(2024, 1, 2)

price = bond.price(curve, settlement=settlement)
assert 90.0 < price < 110.0  # near par

ytm = bond.ytm(float(price), settlement)
assert 0.03 < ytm < 0.06  # yield between step-up rates

dur = bond.duration(curve, metric="modified", settlement=settlement)
assert 3.0 < dur < 5.5  # reasonable duration for 5Y bond
```

### Advanced Example

```python
import datetime, math
from vade import StepUpBond, DiscountCurve

# 7-year step-up bond with 3 coupon steps
sub = StepUpBond(
    effective=datetime.date(2024, 3, 15),
    termination="7y",
    coupon_rates=[
        (datetime.date(2024, 3, 15), 0.03),    # 3.0% initial
        (datetime.date(2026, 3, 15), 0.035),   # steps to 3.5% at year 2
        (datetime.date(2028, 3, 15), 0.04),    # steps to 4.0% at year 4
        (datetime.date(2030, 3, 15), 0.045),   # steps to 4.5% at year 6
    ],
    frequency="s",
    convention="act365f",
    currency="usd",
)

# Flat 4% discount curve
base = datetime.date(2024, 3, 15)
nodes = {base: 1.0}
for y in range(1, 11):
    nodes[datetime.date(2024 + y, 3, 15)] = math.exp(-0.04 * y)
curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f")

# Pricing
price = sub.price(curve)
assert 90.0 < float(price) < 110.0

dirty = sub.dirty_price(curve)
assert 90.0 < float(dirty) < 115.0

# Yield to maturity
settlement = datetime.date(2024, 3, 16)
ytm = sub.ytm(float(price), settlement)
assert 0.02 < float(ytm) < 0.06

# Cashflows -- shows varying coupon amounts across steps
cf = sub.cashflows(curve)
assert len(cf) > 10  # semi-annual over 7 years

# Risk analytics
dur = sub.duration(curve)
assert 3.0 < float(dur) < 8.0

conv = sub.convexity(curve)
assert float(conv) > 0

dv01 = sub.dv01(curve)
assert float(dv01) > 0

z_spr = sub.z_spread(curve, price=float(price))
assert abs(float(z_spr)) < 0.01  # near zero when priced off same curve
```

______________________________________________________________________

## AmortizingBond

Fixed-rate bond with declining notional via scheduled principal repayments. _Python wrapper composing FixedLeg with amortization schedule._

Principal repayment amounts are specified as absolute values. Coupon payments are computed on the remaining (reduced) notional after each principal repayment.

### Constructor

```python notest
AmortizingBond(
    *,
    effective=None,
    termination=None,
    coupon=0.0,
    amortization_schedule=None,
    frequency="s",
    settlement_days=1,
    ex_div_days=0,
    calendar=None,
    currency="USD",
    convention="actactisda",
    accrued_convention=None,
    face_value=100.0,
    modifier="none",
    disc_curve_id=None,
)
```

### Parameters

| Name                    | Type                                  | Default        | Description                                                        |
| ----------------------- | ------------------------------------- | -------------- | ------------------------------------------------------------------ |
| `effective`             | `datetime.date` or `None`             | `None`         | Issue date                                                         |
| `termination`           | `date`, `str`, or `None`              | `None`         | Maturity date or tenor string                                      |
| `coupon`                | `float`                               | `0.0`          | Coupon rate (percentage, e.g., `0.05` for 5%)                      |
| `amortization_schedule` | `list[tuple[date, float]]` or `None`  | `None`         | Principal repayment schedule as `[(date, amount), ...]`            |
| `frequency`             | `str`                                 | `"s"`          | Coupon payment frequency                                           |
| `settlement_days`       | `int`                                 | `1`            | Settlement lag in business days                                    |
| `ex_div_days`           | `int`                                 | `0`            | Ex-dividend days before coupon payment                             |
| `calendar`              | `str` or `None`                       | `None`         | Named calendar                                                     |
| `currency`              | `str`                                 | `"USD"`        | Currency code                                                      |
| `convention`            | `str`                                 | `"actactisda"` | Day count convention for coupon accrual                            |
| `accrued_convention`    | `str` or `None`                       | `None`         | Separate day count for accrued interest (defaults to `convention`) |
| `face_value`            | `float`                               | `100.0`        | Initial face value of the bond                                     |
| `modifier`              | `str`                                 | `"none"`       | Business day adjustment rule                                       |
| `disc_curve_id`         | `str` or `None`                       | `None`         | Discount curve identifier for [Solver](solver.md#solver) lookup    |

See [Conventions](../conventions.md) for accepted values for `frequency`, `convention`, and `modifier`.

### Methods

#### Standard Methods

| Method                                    | Returns     | Description                |
| ----------------------------------------- | ----------- | -------------------------- |
| `.rate(curves, *, solver=None)`           | `float`     | Par rate against curves    |
| `.npv(curves, *, solver=None)`            | `float`     | Net present value          |
| `.cashflows(curves, *, solver=None)`      | `DataFrame` | Period-level cashflow table |
| `.spread(curves, *, solver=None)`         | `float`     | Par spread                 |

#### Price and Yield

| Method                                                             | Returns | Description                             |
| ------------------------------------------------------------------ | ------- | --------------------------------------- |
| `.price(curves, *, solver=None, settlement=None)`                  | `float` | Clean price                             |
| `.dirty_price(curves, *, solver=None, settlement=None)`            | `float` | Dirty price (clean + accrued)           |
| `.accrued_interest(settlement)`                                    | `float` | Accrued interest at settlement date     |
| `.ytm(clean_price, settlement, convention="periodic")`             | `float` | Yield to maturity from clean price      |
| `.price_from_ytm(ytm, settlement, convention="periodic")`          | `float` | Clean price from yield to maturity      |

#### Risk Analytics

| Method                                                                                  | Returns | Description                     |
| --------------------------------------------------------------------------------------- | ------- | ------------------------------- |
| `.duration(curves, *, solver=None, metric="modified", settlement=None)`                 | `float` | Modified or Macaulay duration   |
| `.convexity(curves, *, solver=None, settlement=None)`                                   | `float` | Price convexity                 |
| `.dv01(curves, *, solver=None)`                                                         | `float` | Dollar value of 1bp             |
| `.z_spread(curves, *, solver=None, price=None, settlement=None)`                        | `float` | Z-spread over curve             |
| `.asw_spread(curves, *, solver=None, price=None, method="par_par", settlement=None)`    | `float` | Asset swap spread               |
| `.analytic_delta(curves, *, solver=None)`                                               | `float` | Analytic delta                  |

### Example

```python
import math
import datetime
from vade import AmortizingBond, DiscountCurve

# Flat 4% continuously compounded curve
base = datetime.date(2024, 1, 1)
nodes = {base: 1.0}
for y in range(1, 11):
    nodes[datetime.date(2024 + y, 1, 1)] = math.exp(-0.04 * y)
curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f")

bond = AmortizingBond(
    effective=datetime.date(2024, 1, 1),
    termination=datetime.date(2029, 1, 1),
    coupon=0.05,
    amortization_schedule=[
        (datetime.date(2026, 1, 1), 25.0),   # repay 25 at year 2
        (datetime.date(2028, 1, 1), 25.0),   # repay 25 at year 4
    ],
    frequency="a",
    face_value=100.0,
    convention="actactisda",
)
settlement = datetime.date(2024, 1, 2)

price = bond.price(curve, settlement=settlement)
assert 95.0 < price < 115.0  # above par when coupon > yield

ytm = bond.ytm(float(price), settlement)
assert 0.03 < ytm < 0.06  # reasonable yield range

# Accrued interest reflects current notional
ai_early = bond.accrued_interest(datetime.date(2024, 7, 1))
ai_late = bond.accrued_interest(datetime.date(2026, 7, 1))
assert ai_early > ai_late  # lower notional after amortization
```

### Advanced Example

```python
import datetime, math
from vade import AmortizingBond, DiscountCurve

# 5-year amortizing bond: 25% principal at Y2 and Y4
ab = AmortizingBond(
    effective=datetime.date(2024, 3, 15),
    termination="5y",
    coupon=0.05,
    frequency="s",
    convention="act365f",
    currency="usd",
    amortization_schedule=[
        (datetime.date(2026, 3, 15), 25.0),
        (datetime.date(2028, 3, 15), 25.0),
    ],
)

# Flat 4% discount curve
base = datetime.date(2024, 3, 15)
nodes = {base: 1.0}
for y in range(1, 11):
    nodes[datetime.date(2024 + y, 3, 15)] = math.exp(-0.04 * y)
curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f")

# Pricing and yield
settlement = datetime.date(2024, 3, 16)
price = ab.price(curve)
assert 100.0 < float(price) < 110.0  # above par, coupon > discount rate

ytm = ab.ytm(float(price), settlement)
assert 0.03 < float(ytm) < 0.06

# Cashflows -- inspect types: "Amortizing" and "Principal" rows present
cf = ab.cashflows(curve)
assert len(cf) > 5
cf_types = cf["Type"].to_list()
assert "Amortizing" in cf_types
assert "Principal" in cf_types  # amortization creates principal repayment rows

# Accrued interest declines as notional amortizes
ai_before = ab.accrued_interest(datetime.date(2025, 6, 15))  # before any amort
ai_after = ab.accrued_interest(datetime.date(2026, 6, 15))   # after first 25% amort
assert float(ai_before) > float(ai_after)  # lower notional -> lower accrued

# Risk analytics -- amortization reduces effective duration
dur = ab.duration(curve)
assert 2.0 < float(dur) < 5.0  # shorter than 5Y bullet

conv = ab.convexity(curve)
assert float(conv) > 0

dv01 = ab.dv01(curve)
assert float(dv01) > 0

z_spr = ab.z_spread(curve, price=float(price))
assert abs(float(z_spr)) < 0.01  # near zero when priced off same curve
```

______________________________________________________________________

## PIKBond

Payment-in-kind bond where accrued interest compounds into a growing notional. _Python wrapper composing FixedLeg with PIK notional accretion._

With `pik_fraction=1.0` (full PIK), no cash coupons are paid; all interest accrues to principal. With `pik_fraction=0.5`, half the coupon is paid in cash and half compounds into the notional. The `notional_schedule()` method returns the growing notional at each period boundary.

### Constructor

```python notest
PIKBond(
    *,
    effective=None,
    termination=None,
    coupon=0.0,
    pik_fraction=1.0,
    frequency="s",
    settlement_days=1,
    ex_div_days=0,
    calendar=None,
    currency="USD",
    convention="actactisda",
    accrued_convention=None,
    face_value=100.0,
    modifier="none",
    disc_curve_id=None,
)
```

### Parameters

| Name                 | Type                       | Default        | Description                                                        |
| -------------------- | -------------------------- | -------------- | ------------------------------------------------------------------ |
| `effective`          | `datetime.date` or `None`  | `None`         | Issue date                                                         |
| `termination`        | `date`, `str`, or `None`   | `None`         | Maturity date or tenor string                                      |
| `coupon`             | `float`                    | `0.0`          | Coupon rate (percentage, e.g., `0.06` for 6%)                      |
| `pik_fraction`       | `float`                    | `1.0`          | Fraction of coupon paid in kind (`1.0` = full PIK, `0.0` = all cash) |
| `frequency`          | `str`                      | `"s"`          | Coupon payment frequency                                           |
| `settlement_days`    | `int`                      | `1`            | Settlement lag in business days                                    |
| `ex_div_days`        | `int`                      | `0`            | Ex-dividend days before coupon payment                             |
| `calendar`           | `str` or `None`            | `None`         | Named calendar                                                     |
| `currency`           | `str`                      | `"USD"`        | Currency code                                                      |
| `convention`         | `str`                      | `"actactisda"` | Day count convention for coupon accrual                            |
| `accrued_convention` | `str` or `None`            | `None`         | Separate day count for accrued interest (defaults to `convention`) |
| `face_value`         | `float`                    | `100.0`        | Initial face value of the bond                                     |
| `modifier`           | `str`                      | `"none"`       | Business day adjustment rule                                       |
| `disc_curve_id`      | `str` or `None`            | `None`         | Discount curve identifier for [Solver](solver.md#solver) lookup    |

See [Conventions](../conventions.md) for accepted values for `frequency`, `convention`, and `modifier`.

### Methods

#### Standard Methods

| Method                                    | Returns     | Description                |
| ----------------------------------------- | ----------- | -------------------------- |
| `.rate(curves, *, solver=None)`           | `float`     | Par rate against curves    |
| `.npv(curves, *, solver=None)`            | `float`     | Net present value          |
| `.cashflows(curves, *, solver=None)`      | `DataFrame` | Period-level cashflow table |
| `.spread(curves, *, solver=None)`         | `float`     | Par spread                 |

#### Price and Yield

| Method                                                             | Returns | Description                             |
| ------------------------------------------------------------------ | ------- | --------------------------------------- |
| `.price(curves, *, solver=None, settlement=None)`                  | `float` | Clean price                             |
| `.dirty_price(curves, *, solver=None, settlement=None)`            | `float` | Dirty price (clean + accrued)           |
| `.accrued_interest(settlement)`                                    | `float` | Accrued interest at settlement date     |
| `.ytm(clean_price, settlement, convention="periodic")`             | `float` | Yield to maturity from clean price      |
| `.price_from_ytm(ytm, settlement, convention="periodic")`          | `float` | Clean price from yield to maturity      |

#### Risk Analytics

| Method                                                                                  | Returns | Description                     |
| --------------------------------------------------------------------------------------- | ------- | ------------------------------- |
| `.duration(curves, *, solver=None, metric="modified", settlement=None)`                 | `float` | Modified or Macaulay duration   |
| `.convexity(curves, *, solver=None, settlement=None)`                                   | `float` | Price convexity                 |
| `.dv01(curves, *, solver=None)`                                                         | `float` | Dollar value of 1bp             |
| `.z_spread(curves, *, solver=None, price=None, settlement=None)`                        | `float` | Z-spread over curve             |
| `.asw_spread(curves, *, solver=None, price=None, method="par_par", settlement=None)`    | `float` | Asset swap spread               |
| `.analytic_delta(curves, *, solver=None)`                                               | `float` | Analytic delta                  |

#### PIK-Specific

| Method              | Returns                    | Description                              |
| ------------------- | -------------------------- | ---------------------------------------- |
| `.notional_schedule()` | `list[tuple[date, float]]` | Growing notional at each period boundary |

### Example

```python
import math
import datetime
from vade import PIKBond, DiscountCurve

# Flat 4% continuously compounded curve
base = datetime.date(2024, 1, 1)
nodes = {base: 1.0}
for y in range(1, 11):
    nodes[datetime.date(2024 + y, 1, 1)] = math.exp(-0.04 * y)
curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f")

bond = PIKBond(
    effective=datetime.date(2024, 1, 1),
    termination=datetime.date(2029, 1, 1),
    coupon=0.06,
    pik_fraction=1.0,   # full PIK: all interest compounds
    frequency="a",
    face_value=100.0,
    convention="actactisda",
)

# Full PIK means no cash coupon accrues
assert bond.accrued_interest(datetime.date(2024, 7, 1)) == 0.0

# Notional grows each period
schedule = bond.notional_schedule()
assert schedule[0][1] == 100.0    # initial face value
assert schedule[1][1] > 100.0     # notional grows after first period

price = bond.price(curve, settlement=datetime.date(2024, 1, 2))
assert 100.0 < price < 120.0  # above par for 6% PIK vs 4% discount
```

### Advanced Example

```python
import datetime, math
from vade import PIKBond, DiscountCurve

# Full PIK bond: 6% coupon, all interest capitalised
pik_full = PIKBond(
    effective=datetime.date(2024, 1, 15),
    termination="5y",
    coupon=0.06,
    pik_fraction=1.0,
    frequency="a",
    convention="act365f",
    currency="usd",
)

# Partial PIK bond: same terms, 50% cash / 50% PIK
pik_partial = PIKBond(
    effective=datetime.date(2024, 1, 15),
    termination="5y",
    coupon=0.06,
    pik_fraction=0.5,
    frequency="a",
    convention="act365f",
    currency="usd",
)

# Flat 4% discount curve
base = datetime.date(2024, 1, 15)
nodes = {base: 1.0}
for y in range(1, 11):
    nodes[datetime.date(2024 + y, 1, 15)] = math.exp(-0.04 * y)
curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f")

# notional_schedule() shows compounding growth for full PIK
ns = pik_full.notional_schedule()
assert len(ns) > 1
# First entry is par, subsequent entries grow as interest is capitalised
assert ns[0][1] == 100.0
assert ns[-1][1] > 100.0  # notional has grown

# Full PIK has zero accrued (all capitalised)
ai_full = pik_full.accrued_interest(datetime.date(2024, 7, 15))
assert float(ai_full) == 0.0

# Partial PIK has positive accrued (cash portion)
ai_partial = pik_partial.accrued_interest(datetime.date(2024, 7, 15))
assert float(ai_partial) > 0.0

# Full PIK price is higher (more future cashflows from compounding)
price_full = pik_full.price(curve)
price_partial = pik_partial.price(curve)
assert float(price_full) > float(price_partial)

# Cashflows
cf = pik_full.cashflows(curve)
assert len(cf) > 0

# Risk analytics
dur = pik_full.duration(curve)
assert 3.0 < float(dur) < 6.0

conv = pik_full.convexity(curve)
assert float(conv) > 0
```

______________________________________________________________________

## CappedFloatRateNote

Floating rate note with embedded cap and/or floor on the coupon rate. _Python wrapper composing FloatLeg with Black-76 or Bachelier cap/floor pricing._

Requires `vol` (volatility) -- there is no intrinsic-only fallback. At least one of `cap_rate` or `floor_rate` must be specified. When both are set, the instrument is a collared FRN. Optionally supports sub-period compounding via `fixing_frequency`.

### Constructor

```python notest
CappedFloatRateNote(
    *,
    effective=None,
    termination=None,
    spread=0.0,
    frequency="q",
    fixing_frequency=None,
    cap_rate=None,
    floor_rate=None,
    vol=None,
    vol_model="black76",
    settlement_days=2,
    ex_div_days=0,
    calendar=None,
    currency="USD",
    convention="act360",
    accrued_convention=None,
    face_value=100.0,
    modifier="mf",
    disc_curve_id=None,
)
```

### Parameters

| Name                 | Type                       | Default    | Description                                                              |
| -------------------- | -------------------------- | ---------- | ------------------------------------------------------------------------ |
| `effective`          | `datetime.date` or `None`  | `None`     | Issue date                                                               |
| `termination`        | `date`, `str`, or `None`   | `None`     | Maturity date or tenor string                                            |
| `spread`             | `float`                    | `0.0`      | Spread over floating rate (decimal)                                      |
| `frequency`          | `str`                      | `"q"`      | Coupon payment frequency                                                 |
| `fixing_frequency`   | `str` or `None`            | `None`     | Sub-period fixing frequency (e.g., `"m"` for monthly within quarterly)   |
| `cap_rate`           | `float` or `None`          | `None`     | Cap strike rate (decimal). At least one of cap_rate/floor_rate required. |
| `floor_rate`         | `float` or `None`          | `None`     | Floor strike rate (decimal)                                              |
| `vol`                | `float` or `None`          | `None`     | Volatility for option pricing. **Required** -- no intrinsic fallback.    |
| `vol_model`          | `str`                      | `"black76"` | Pricing model: `"black76"` or `"bachelier"`                             |
| `settlement_days`    | `int`                      | `2`        | Settlement lag in business days                                          |
| `ex_div_days`        | `int`                      | `0`        | Ex-dividend days before coupon payment                                   |
| `calendar`           | `str` or `None`            | `None`     | Named calendar                                                           |
| `currency`           | `str`                      | `"USD"`    | Currency code                                                            |
| `convention`         | `str`                      | `"act360"` | Day count convention                                                     |
| `accrued_convention` | `str` or `None`            | `None`     | Separate day count for accrued interest (defaults to `convention`)       |
| `face_value`         | `float`                    | `100.0`    | Face value                                                               |
| `modifier`           | `str`                      | `"mf"`     | Business day adjustment rule                                             |
| `disc_curve_id`      | `str` or `None`            | `None`     | Discount curve identifier for [Solver](solver.md#solver) lookup          |

See [Conventions](../conventions.md) for accepted values for `frequency`, `convention`, and `modifier`.

### Methods

#### Standard Methods

| Method                                    | Returns     | Description                |
| ----------------------------------------- | ----------- | -------------------------- |
| `.rate(curves, *, solver=None)`           | `float`     | Par rate against curves    |
| `.npv(curves, *, solver=None)`            | `float`     | Net present value          |
| `.cashflows(curves, *, solver=None)`      | `DataFrame` | Period-level cashflow table |
| `.spread(curves, *, solver=None)`         | `float`     | Par spread                 |

#### Price and Yield

| Method                                                             | Returns | Description                             |
| ------------------------------------------------------------------ | ------- | --------------------------------------- |
| `.price(curves, *, solver=None, settlement=None)`                  | `float` | Clean price                             |
| `.dirty_price(curves, *, solver=None, settlement=None)`            | `float` | Dirty price (clean + accrued)           |
| `.accrued_interest(settlement)`                                    | `float` | Accrued interest at settlement date     |
| `.ytm(clean_price, settlement, convention="periodic")`             | `float` | Yield to maturity from clean price      |
| `.price_from_ytm(ytm, settlement, convention="periodic")`          | `float` | Clean price from yield to maturity      |

#### Risk Analytics

| Method                                                                                  | Returns | Description                     |
| --------------------------------------------------------------------------------------- | ------- | ------------------------------- |
| `.duration(curves, *, solver=None, metric="modified", settlement=None)`                 | `float` | Modified or Macaulay duration   |
| `.convexity(curves, *, solver=None, settlement=None)`                                   | `float` | Price convexity                 |
| `.dv01(curves, *, solver=None)`                                                         | `float` | Dollar value of 1bp             |
| `.z_spread(curves, *, solver=None, price=None, settlement=None)`                        | `float` | Z-spread over curve             |
| `.asw_spread(curves, *, solver=None, price=None, method="par_par", settlement=None)`    | `float` | Asset swap spread               |
| `.analytic_delta(curves, *, solver=None)`                                               | `float` | Analytic delta                  |

#### CappedFRN-Specific

| Method                  | Returns     | Description                                    |
| ----------------------- | ----------- | ---------------------------------------------- |
| `.sub_period_schedule()` | `DataFrame` | Sub-period schedule when `fixing_frequency` set |

### Example

```python
import math
import datetime
from vade import CappedFloatRateNote, FloatRateNote, DiscountCurve

# Steep discount curve (rates ~5-6%)
base = datetime.date(2024, 1, 1)
nodes = {base: 1.0}
for y in range(1, 11):
    nodes[datetime.date(2024 + y, 1, 1)] = math.exp(-0.055 * y)
curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f")

# Uncapped FRN for comparison
frn = FloatRateNote(
    effective=datetime.date(2024, 1, 1),
    termination=datetime.date(2026, 1, 1),
    spread=0.0,
    frequency="q",
    convention="act360",
)
frn_price = frn.dirty_price(curves=[curve, curve])

# Capped FRN with 2% cap (well below market rates)
capped = CappedFloatRateNote(
    effective=datetime.date(2024, 1, 1),
    termination=datetime.date(2026, 1, 1),
    spread=0.0,
    frequency="q",
    cap_rate=0.02,
    vol=0.20,
    convention="act360",
)
capped_price = capped.dirty_price(curves=[curve, curve])

assert isinstance(capped_price, float)
assert capped_price < frn_price  # cap reduces value to investor
```

### Advanced Example

```python
import datetime, math
from vade import CappedFloatRateNote, DiscountCurve

# Base discount/forecast curve (flat 4%)
base = datetime.date(2024, 6, 15)
nodes = {base: 1.0}
for y in range(1, 6):
    nodes[datetime.date(2024 + y, 6, 15)] = math.exp(-0.04 * y)
curve = DiscountCurve(nodes, interpolation="log_linear", convention="act360")

# Capped FRN: floating rate capped at 5%
capped = CappedFloatRateNote(
    effective=datetime.date(2024, 6, 15),
    termination="3y",
    frequency="q",
    convention="act360",
    currency="usd",
    cap_rate=0.05,
    vol=0.50,
)

# Floored FRN: floating rate floored at 2%
floored = CappedFloatRateNote(
    effective=datetime.date(2024, 6, 15),
    termination="3y",
    frequency="q",
    convention="act360",
    currency="usd",
    floor_rate=0.02,
    vol=0.50,
)

# Collared FRN: both cap and floor
collared = CappedFloatRateNote(
    effective=datetime.date(2024, 6, 15),
    termination="3y",
    frequency="q",
    convention="act360",
    currency="usd",
    cap_rate=0.05,
    floor_rate=0.02,
    vol=0.50,
)

# Pricing -- pass curves as list [discount, forecast] (required format)
price_capped = capped.dirty_price(curves=[curve, curve])
price_floored = floored.dirty_price(curves=[curve, curve])
price_collared = collared.dirty_price(curves=[curve, curve])

assert isinstance(float(price_capped), float)
assert isinstance(float(price_floored), float)
assert isinstance(float(price_collared), float)

# Floor adds value (guarantees minimum coupon)
# Cap reduces value (limits upside)
# So: capped < collared < floored
assert float(price_capped) < float(price_floored)

# Bachelier model comparison (vol_model="bachelier")
capped_bach = CappedFloatRateNote(
    effective=datetime.date(2024, 6, 15),
    termination="3y",
    frequency="q",
    convention="act360",
    currency="usd",
    cap_rate=0.05,
    vol=0.01,  # Bachelier vol in absolute terms
    vol_model="bachelier",
)
price_bach = capped_bach.dirty_price(curves=[curve, curve])
assert isinstance(float(price_bach), float)
# Bachelier and Black-76 produce different prices
assert float(price_bach) != float(price_capped)

# Cashflows
cf = capped.cashflows(curves=[curve, curve])
assert len(cf) > 0

# Risk analytics -- near-zero for FRN is expected (resets to par)
dur = capped.duration(curves=[curve, curve])
assert isinstance(float(dur), float)
```

______________________________________________________________________

## AssetSwap

Asset swap decomposing a bond into a floating-rate equivalent. _Python wrapper composing bond + floating leg._

Supports `"par"` (par-par) and `"non_par"` asset swap types. The par asset swap computes the spread over floating that makes the package NPV zero at par. The non-par variant requires a `dirty_price` input.

See also: [Spread Analytics Guide](../guides/credit/spread-analytics.md) for asset swap workflows and par vs non-par comparison.

### Constructor

```python notest
AssetSwap(bond, *, asw_type="par", dirty_price=None, spread=None, disc_curve_id=None)
```

### Parameters

| Name            | Type                               | Default  | Description                                                     |
| --------------- | ---------------------------------- | -------- | --------------------------------------------------------------- |
| `bond`          | bond instrument                    | *required* | Underlying bond (FixedRateBond, FloatRateNote, StepUpBond, etc.). Positional. |
| `asw_type`      | `str`                              | `"par"`  | Asset swap type: `"par"` or `"non_par"`                         |
| `dirty_price`   | `float` or `None`                  | `None`   | Dirty price of the bond (required for `"non_par"`)              |
| `spread`        | `float` or `None`                  | `None`   | Fixed spread override                                           |
| `disc_curve_id` | `str` or `None`                    | `None`   | Discount curve identifier for [Solver](solver.md#solver) lookup |

### Methods

| Method                                    | Returns     | Description                       |
| ----------------------------------------- | ----------- | --------------------------------- |
| `.rate(curves, *, solver=None)`           | `float`     | Asset swap spread                 |
| `.npv(curves, *, solver=None)`            | `float`     | Net present value of the package  |
| `.cashflows(curves, *, solver=None)`      | `DataFrame` | Period-level cashflow table       |

### Example

```python
import math
import datetime
from vade import AssetSwap, FixedRateBond, DiscountCurve

# Flat 3% continuously compounded curve
base = datetime.date(2024, 1, 1)
nodes = {base: 1.0}
for y in range(1, 11):
    nodes[datetime.date(2024 + y, 1, 1)] = math.exp(-0.03 * y)
curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f", id="disc")

bond = FixedRateBond(
    effective=datetime.date(2024, 1, 1),
    termination="5Y",
    coupon=0.04,
    frequency="s",
    face_value=100.0,
)

# Par asset swap
asw = AssetSwap(bond=bond, asw_type="par")
spread = asw.rate(curves=curve)
assert isinstance(spread, float)
assert -0.5 < spread < 0.5  # spread in reasonable range

# NPV is zero at fair spread (no fixed spread override)
npv = asw.npv(curves=curve)
assert abs(npv) < 1e-8

# Non-par asset swap with explicit dirty price
asw_np = AssetSwap(bond=bond, asw_type="non_par", dirty_price=102.5)
spread_np = asw_np.rate(curves=curve)
assert isinstance(spread_np, float)
```

______________________________________________________________________

## CallableBond

Bond with embedded call and/or put options, priced via Hull-White trinomial tree. _Python wrapper composing bond + option schedule._

Tree-based methods require Hull-White model parameters: `a` (mean reversion speed) and `sigma` (short-rate volatility). Standard bond methods (rate, npv, cashflows) delegate to the underlying bond.

See also: [Callable Bonds Guide](../guides/credit/callable-bonds.md) for OAS analysis and effective duration workflows.

### Constructor

```python notest
CallableBond(bond, *, call_schedule=None, put_schedule=None, disc_curve_id=None)
```

### Parameters

| Name            | Type                                | Default  | Description                                                     |
| --------------- | ----------------------------------- | -------- | --------------------------------------------------------------- |
| `bond`          | bond instrument                     | *required* | Underlying bond (typically FixedRateBond). Positional.         |
| `call_schedule` | `list[tuple[date, float]]` or `None` | `None`  | Call dates and prices as `[(date, strike_price), ...]`          |
| `put_schedule`  | `list[tuple[date, float]]` or `None` | `None`  | Put dates and prices as `[(date, strike_price), ...]`           |
| `disc_curve_id` | `str` or `None`                     | `None`   | Discount curve identifier for [Solver](solver.md#solver) lookup |

### Methods

#### Standard Methods (delegated to underlying bond)

| Method                                    | Returns     | Description                |
| ----------------------------------------- | ----------- | -------------------------- |
| `.rate(curves, *, solver=None)`           | `float`     | Par rate against curves    |
| `.npv(curves, *, solver=None)`            | `float`     | Net present value          |
| `.cashflows(curves, *, solver=None)`      | `DataFrame` | Period-level cashflow table |

#### Tree-Based Methods (Hull-White)

| Method                                                                                          | Returns | Description                                      |
| ----------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------ |
| `.tree_price(curves, a, sigma, *, spread=0.0)`                                                  | `float` | Option-adjusted price via trinomial tree         |
| `.oas(curves, market_price, a, sigma)`                                                          | `float` | Option-adjusted spread from market price         |
| `.effective_duration(curves, a, sigma, *, oas=0.0, bump=0.0001)`                                | `float` | Effective duration (curve bump)                  |
| `.effective_convexity(curves, a, sigma, *, oas=0.0, bump=0.0001)`                               | `float` | Effective convexity (curve bump)                 |
| `.vega(curves, a, sigma, *, oas=0.0, bump=0.0001)`                                              | `float` | Vega (price sensitivity to volatility)           |

Tree-based methods take `a` (mean reversion speed, e.g., `0.1`) and `sigma` (short-rate vol, e.g., `0.01`).

### Example

```python
import math
import datetime
from vade import CallableBond, FixedRateBond, DiscountCurve

# Flat 3% continuously compounded curve
base = datetime.date(2024, 1, 1)
nodes = {base: 1.0}
for y in range(1, 11):
    nodes[datetime.date(2024 + y, 1, 1)] = math.exp(-0.03 * y)
curve = DiscountCurve(nodes, interpolation="log_linear", convention="act365f", id="disc")

bond = FixedRateBond(
    effective=datetime.date(2024, 1, 1),
    termination="5Y",
    coupon=0.05,
    frequency="a",
    face_value=100.0,
)

cb = CallableBond(
    bond=bond,
    call_schedule=[
        (datetime.date(2026, 1, 1), 100.0),
        (datetime.date(2027, 1, 1), 100.0),
        (datetime.date(2028, 1, 1), 100.0),
    ],
)

# Tree price with Hull-White parameters
tree_px = cb.tree_price(curves=curve, a=0.1, sigma=0.01)
assert 80.0 < tree_px < 120.0  # reasonable price range

# OAS: use tree_price as market price => OAS should be ~0
oas = cb.oas(curves=curve, market_price=tree_px, a=0.1, sigma=0.01)
assert abs(oas) < 1e-4  # OAS ~0 when market = model price

# Effective duration and convexity
eff_dur = cb.effective_duration(curves=curve, a=0.1, sigma=0.01)
assert eff_dur > 0  # positive duration

eff_cvx = cb.effective_convexity(curves=curve, a=0.1, sigma=0.01)
assert isinstance(eff_cvx, float)
```

______________________________________________________________________

## CDS

Credit Default Swap with hazard rate curve pricing. _Rust-backed._

See also: [Credit Curves & CDS Guide](../guides/credit/credit-curves-cds.md) for CDS-based credit curve calibration.

**Alias:** `CDS = CreditDefaultSwap`

### Constructor

```python notest
CDS(
    *,
    spec=None,
    effective=None,
    termination=None,
    frequency="q",
    notional=10_000_000.0,
    recovery_rate=0.4,
    fixed_rate=100.0,
    convention="act360",
    currency="USD",
    premium_accrued=True,
    modifier="mf",
    stub="shortfront",
)
```

### Parameters

| Name              | Type                       | Default          | Description                                           |
| ----------------- | -------------------------- | ---------------- | ----------------------------------------------------- |
| `spec`            | `str` or `None`            | `None`           | Instrument spec name                                  |
| `effective`       | `datetime.date` or `None`  | `None`           | Protection start date                                 |
| `termination`     | `date`, `str`, or `None`   | `None`           | Protection end date or tenor string (e.g., `"5Y"`)    |
| `frequency`       | `str`                      | `"q"`            | Premium payment frequency                             |
| `notional`        | `float`                    | `10_000_000.0`   | Notional amount                                       |
| `recovery_rate`   | `float`                    | `0.4`            | Assumed recovery rate (decimal, e.g., `0.4` for 40%)  |
| `fixed_rate`      | `float`                    | `100.0`          | Fixed premium rate (basis points)                     |
| `convention`      | `str`                      | `"act360"`       | Day count convention                                  |
| `currency`        | `str`                      | `"USD"`          | Currency code                                         |
| `premium_accrued` | `bool`                     | `True`           | Whether premium accrues to default date               |
| `modifier`        | `str`                      | `"mf"`           | Business day adjustment rule                          |
| `stub`            | `str`                      | `"shortfront"`   | Stub period preference                                |

See [Conventions](../conventions.md) for accepted values for `frequency`, `convention`, `modifier`, and `stub`.

### Methods

CDS methods take two separate curve arguments: a discount curve and a [CreditImpliedCurve](curves.md#creditimpliedcurve) for survival probabilities.

| Method                                                  | Returns     | Description                        |
| ------------------------------------------------------- | ----------- | ---------------------------------- |
| `.rate(disc_curve, credit_curve)`                       | `float`     | Par spread (same as `.spread`)     |
| `.npv(disc_curve, credit_curve)`                        | `float`     | Net present value                  |
| `.spread(disc_curve, credit_curve)`                     | `float`     | Par spread                         |
| `.cashflows(disc_curve, credit_curve)`                  | `DataFrame` | Period-level cashflow table        |
| `.accrued(today=None)`                                  | `float`     | Accrued premium                    |
| `.analytic_rec_risk(disc_curve, credit_curve)`          | `float`     | Recovery risk                      |
| `.jtd(today=None)`                                      | `float`     | Jump-to-default exposure           |
| `.ead()`                                                | `float`     | Exposure at default                |
| `.upfront(disc_curve, credit_curve)`                    | `float`     | Upfront payment amount             |
| `.to_upfront_spread(disc_curve, credit_curve)`          | `float`     | Convert running to upfront spread  |

### Example

```python
import datetime
from vade import CDS, DiscountCurve, CreditImpliedCurve

disc_nodes = {
    datetime.date(2024, 1, 1): 1.0,
    datetime.date(2025, 1, 1): 0.97,
    datetime.date(2026, 1, 1): 0.94,
    datetime.date(2029, 1, 1): 0.85,
}
disc_curve = DiscountCurve(disc_nodes, interpolation="log_linear", convention="act365f")

credit_nodes = {
    datetime.date(2024, 1, 1): 1.0,
    datetime.date(2025, 1, 1): 0.99,
    datetime.date(2026, 1, 1): 0.98,
    datetime.date(2029, 1, 1): 0.95,
}
credit_curve = CreditImpliedCurve(credit_nodes, convention="act365f", recovery_rate=0.4)

cds = CDS(
    effective=datetime.date(2024, 1, 1),
    termination="5Y",
    fixed_rate=100.0,
    frequency="q",
    convention="act360",
    recovery_rate=0.4,
)
cds.npv(disc_curve, credit_curve)  # net present value
cds.spread(disc_curve, credit_curve)  # par spread in basis points
cds.ead()  # exposure at default
```

______________________________________________________________________

---

## See Also

- [Credit Guides](../../guides/credit/) -- Bond analytics, CDS pricing, fitted curves, and spread analysis guides
- [Curves API](../rates/curves.md) -- CreditImpliedCurve, FittedBondCurve, SpreadCurve

