Payment-in-kind bond
This page lists what you need to price a payment-in-kind (PIK) bond: the constructor, the capitalisation schedule, the accreted face and redemption, how it values off a curve, and an example you can run. A PIK pays its coupon in more bond instead of cash, so the face grows and each later coupon accrues on a larger amount.
Quick example
The quotes are stated in the file; the Rust tab borrows only the Sheet type from the
desk sheets, so load them once and it runs as shown.
This books a five-year EUR PIK on ten million, capitalising 8% a year on ACT/365 Fixed. It prints the capitalisation schedule, reads the face on dates between and on capitalisations, values the bond off an €STR curve stated in the file on 3 July 2028, prices it at a 10% yield, and shows that a rate that isn't positive is refused.
use chrono::NaiveDate;
use helixio_docs_sheets::Sheet;
use helixrs::{
AnyInstrument, CollateralSpec, Currency, DayCountFraction, Frequency, MarketContext,
PaymentInKindBond, PikError, QuoteSheet, YieldBasis, YieldCurve,
};
fn date(year: i32, month: u32, day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
}
const RATE: f64 = 0.08;
const FACE: f64 = 10_000_000.0;
/// €STR OIS par closes on 3 July 2028, the day the bond is valued.
const EUR_ESTR_JULY_2028: Sheet = &[
("EUR-ESTR-OIS", "1Y", 0.025),
("EUR-ESTR-OIS", "2Y", 0.026),
("EUR-ESTR-OIS", "3Y", 0.027),
("EUR-ESTR-OIS", "5Y", 0.029),
];
fn main() -> Result<(), Box<dyn std::error::Error>> {
let issue = date(2026, 1, 2);
let maturity = date(2031, 1, 2);
let eur = Currency::new("EUR").expect("a registered currency");
let pik = PaymentInKindBond::new(
eur,
issue,
maturity,
RATE,
Frequency::Annual,
DayCountFraction::Act365Fixed,
FACE,
)?;
// Each coupon date adds the period's interest to the face instead of paying it.
let table = pik.capitalisations();
println!("date year frac. face before capitalised face after");
for row in &table {
println!(
"{} {:.6} {:.2} {:.2} {:.2}",
row.date,
row.year_fraction,
row.face_before,
row.amount(),
row.face_after
);
}
// The accreted face on any date: the face at issue, then larger after each
// capitalisation, and flat in between.
let probes = [issue, date(2026, 7, 1), date(2027, 1, 2), date(2029, 6, 30), maturity];
let faces: Vec<f64> = probes.iter().map(|at| pik.current_face(*at)).collect();
for (at, value) in probes.iter().zip(&faces) {
println!("face on {at} {value:.2}");
}
let factor = faces[faces.len() - 1] / FACE;
println!("factor at maturity {factor:.10}");
println!("(1 + r)^5 {:.10}", (1.0_f64 + RATE).powf(5.0));
println!("1 + 5r {:.10}", 1.0 + 5.0 * RATE);
// A PIK pays nothing until maturity, when it redeems the accreted face. Its
// value is that one flow at the curve's discount factor. At a yield, the
// dirty price per 100 of par face is the redemption discounted.
let settlement = date(2028, 7, 3);
let sheet = QuoteSheet::from_rows(EUR_ESTR_JULY_2028.iter().copied())?;
let market =
MarketContext::as_of(settlement).with_recipe_quotes(&CollateralSpec::Currency(eur), &sheet)?;
let redemption = pik.redemption_per_100();
let lifted = market.lift::<f64>();
let view = market.view(&lifted)?;
let value = view.npv(&AnyInstrument::PaymentInKindBond(Box::new(pik.clone())))?;
let annual = YieldBasis::compounded_at(1.0);
let price_par = pik.dirty_price_on_par_face(0.10, settlement, annual);
println!("redemption per 100 {redemption:.6}");
println!("value off the curve {value:.2}");
println!("dirty per 100 at 10% {price_par:.6}");
// A zero rate capitalises nothing and a negative one would shrink the face.
let mut refusals = 0;
for rate in [0.0, -0.01] {
let built = PaymentInKindBond::new(
eur,
issue,
maturity,
rate,
Frequency::Annual,
DayCountFraction::Act365Fixed,
FACE,
);
if matches!(built, Err(PikError::NonPositiveRate)) {
refusals += 1;
}
}
println!("refused {refusals} of 2 non-positive rates");
Ok(())
}import datetime as dt
import helixio as hlx
ISSUE = dt.date(2026, 1, 2)
MATURITY = dt.date(2031, 1, 2)
RATE = 0.08
FACE = 10_000_000.0
PIK = hlx.products.payment_in_kind_bond(
"EUR", ISSUE, MATURITY, rate=RATE, frequency=hlx.dates.Frequency.Annual,
day_count=hlx.dates.DayCountFraction.Act365Fixed, face=FACE,
)
# €STR OIS par closes on 3 July 2028, the day the bond is valued.
EUR_ESTR_JULY_2028 = {
"EUR-ESTR-OIS": {"1Y": 0.025, "2Y": 0.026, "3Y": 0.027, "5Y": 0.029},
}
# Each coupon date adds the period's interest to the face instead of paying it.
table = hlx.bonds.capitalisations(PIK).to_dict()
print("date year frac. face before capitalised face after")
for k, paid in enumerate(table["date"]):
print(
f"{paid} {table['year_fraction'][k]:.6f} {table['face_before'][k]:.2f}"
f" {table['amount'][k]:.2f} {table['face_after'][k]:.2f}"
)
# The accreted face on any date: the face at issue, then larger after each
# capitalisation, and flat in between.
probes = [ISSUE, dt.date(2026, 7, 1), dt.date(2027, 1, 2), dt.date(2029, 6, 30), MATURITY]
faces = [hlx.bonds.accreted_face(PIK, at) for at in probes]
for at, value in zip(probes, faces):
print(f"face on {at} {value:.2f}")
factor = faces[-1] / FACE
print(f"factor at maturity {factor:.10f}")
print(f"(1 + r)^5 {(1.0 + RATE) ** 5:.10f}")
print(f"1 + 5r {1.0 + 5.0 * RATE:.10f}")
# A PIK pays nothing until maturity, when it redeems the accreted face. Its
# value is that one flow at the curve's discount factor. At a yield, the dirty
# price per 100 of par face is the redemption discounted.
settlement = dt.date(2028, 7, 3)
market = hlx.MarketContext(settlement).with_recipe_quotes("EUR", EUR_ESTR_JULY_2028)
redemption = hlx.bonds.accreted_redemption(PIK)
value = PIK.npv(market)
tau = (MATURITY - settlement).days / 365.0
price_par = redemption * (1.0 + 0.10) ** -tau
print(f"redemption per 100 {redemption:.6f}")
print(f"value off the curve {value:.2f}")
print(f"dirty per 100 at 10% {price_par:.6f}")
# A zero rate capitalises nothing and a negative one would shrink the face.
refusals = 0
for rate in [0.0, -0.01]:
try:
hlx.products.payment_in_kind_bond("EUR", ISSUE, MATURITY, rate=rate, frequency="annual", day_count="ACT/365.FIXED", face=FACE)
except ValueError:
refusals += 1
print(f"refused {refusals} of 2 non-positive rates")date year frac. face before capitalised face after
2027-01-02 1.000000 10000000.00 800000.00 10800000.00
2028-01-02 1.000000 10800000.00 864000.00 11664000.00
2029-01-02 1.002740 11664000.00 935676.49 12599676.49
2030-01-02 1.000000 12599676.49 1007974.12 13607650.61
2031-01-02 1.000000 13607650.61 1088612.05 14696262.66
face on 2026-01-02 10000000.00
face on 2026-07-01 10000000.00
face on 2027-01-02 10800000.00
face on 2029-06-30 12599676.49
face on 2031-01-02 14696262.66
factor at maturity 1.4696262662
(1 + r)^5 1.4693280768
1 + 5r 1.4000000000
redemption per 100 146.962627
value off the curve 13749365.95
dirty per 100 at 10% 115.789316
refused 2 of 2 non-positive rates
The face is flat between capitalisations and grows at each one. The 2028 period runs 366 days, so the factor at maturity is a little above , and well above simple interest. The bond has one cash flow, the accreted face at maturity, so its value and its price at a yield are that flow discounted.
How it's priced
Each capitalisation multiplies the face by one plus the rate times that period's year fraction. Per 100 of par face the only flow is the redemption at maturity :
Who issues PIK, and why the two quote bases differ, is covered in how bonds work.
API
| Argument | Type | Default | Meaning |
|---|---|---|---|
currency |
str |
required | the bond's currency |
issue, maturity |
date |
required | the accrual start and the redemption date |
rate |
float |
required | the PIK rate; it must be positive |
frequency |
dates.Frequency or str |
required | how often interest capitalises |
day_count |
dates.DayCountFraction or str |
required | how each period's year fraction is counted |
face |
float |
required | the original principal |
products.payment_in_kind_bond(currency, issue, maturity, *, rate, frequency, day_count, face)
builds it as an Instrument; in Rust, PaymentInKindBond::new(ccy, issue, maturity, rate, frequency, day_count, face)?.
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| , | each capitalisation and its year fraction | pik.capitalisations() |
bonds.capitalisations(pik), columns date, year_fraction, face_before, amount, face_after |
| the accreted face on a date | pik.current_face(T) |
bonds.accreted_face(pik, T) |
|
| redemption per 100 of par face | pik.redemption_per_100() |
bonds.accreted_redemption(pik) |
|
| value against a market | MarketView::npv on AnyInstrument::PaymentInKindBond |
Instrument.npv |
|
| , | price per 100 par at a yield, and back | pik.dirty_price_on_par_face, pik.yield_from_par_face |
the closed forms above, from bonds.accreted_redemption |
Defaults and limits
- A rate that isn't positive is refused:
ValueErrorin Python,PikError::NonPositiveRatein Rust. A zero rate capitalises nothing and a negative one would shrink the face. - Market inputs: the discount curve of the bond's currency. The accretion itself needs no market data.
- Quote basis: prices are per 100 of par face. Divide by the accretion factor at settlement to restate one per 100 of accreted face.
- Python yields: the price-to-yield calls are Rust only; in Python use the closed forms, since a PIK has one flow.
See also
- Source:
crates/helixrs/src/instruments/bonds.rs - Related pages:
- How bonds work, for why PIK exists and its two quote bases.
- Fixed-rate bond, for a bond that pays its coupons in cash.
- Bond conventions, for the day counts a period can accrue on.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.