A bond's price, live¶
The present value of a coupon bond at a coupon date, its coupons' annuity and its face discounted at the yield a period: P = c (1 − v) / y + F v, with y the yield a period, n the periods, c the coupon a period and v = (1 + y)⁻ⁿ; where the yield is nothing, P = c n + F (lusine.finance.bond_price). A ratio, in no currency: the face is a number, 100 for a price per 100. Below, a ten-year 5% bond paying twice a year is priced on your GPU as you read at yields from 0% to 15%, a quarter point a step; and at each of the sixty-one yields it is held to its cash flows, each coupon and the face discounted and summed in float64.
This page runs the program on your GPU through WebGPU, so it needs a browser with WebGPU (a recent Chrome, Edge, Safari or Firefox). Without it the program does not run and nothing is drawn below.
What it shows¶
- The strip above the controls is green while every check holds, and turns red for good once one fails.
- The curve: the chart draws the price per 100 against the yield in percent, a point a step: 150 at no yield, 100 at the coupon, falling and bending upward. Below it, the yield and the price.
- The checks: Fabozzi's bond, a five-year 6% bond at 8% priced at 91.889 per 100 within 0.001 (Fabozzi, Bond Markets, Analysis and Strategies, ch. 2); the cash flows summed, the price at each of the sixty-one yields within its bound of the cash flows discounted and summed in float64, relative: binary32's epsilon (2⁻²³) times 8, and the rounding of 1 + y, half an epsilon, carried n periods into the discount and through the annuity's (1 − v) / y; falling and convex, the price falling at every step of the yield and each fall smaller than the one before; par, no yield, no coupon, 5% bonds of 1, 5 and 30 years at a yield of 5% at par within 8 epsilons, a ten-year 5% bond at no yield its coupons and face undiscounted, 150, within 4, and a seven-year bond with no coupon at 4% its face discounted fourteen periods, 100 / 1.02¹⁴, within 8 and 7; linear in the face, a price per 200 of face twice the price per 100, bit for bit; the long bond moves more, a rise from 3% to 4% costing a ten-year 3% bond more than a two-year one. Each is held at every step.
Every number here is computed by the program on the GPU; the page only reads the values back and draws them. The curve's bond and the textbook's bonds are the program's settings, which the host may write; this page offers no control to change them yet.
The program¶
The yields, the bonds, the sweep and the checks, the panels the page draws, and the one procedure, main, run each step: the yield a quarter point on, the price there and at the sixty-one yields, the textbook's bonds, and the checks.
// The bond price's live page: a ten-year 5% bond priced at yields from 0% to 15% beside its cash flows discounted and
// summed in float64; Fabozzi's bond; a bond at par, at no yield and with no coupon; and the face and the tenor, with
// the page's checks computed here on the GPU and shown by the panels the program declares.
namespace bond_price;
lusine.version version = { { number = 1 } };
// The reader's settings: the curve's bond, its face, coupon, tenor and coupons a year.
host struct {
float face = 100.0; float<year^-1> coupon = 0.05 year^-1; float<year> years = 10.0 year;
float<year^-1> freq = 2.0 year^-1;
} settings;
// Sixty-one yields from 0% to 15% a quarter point apart, each with the bond's price per 100, its cash flows discounted
// and summed in float64, and its bound, relative, in binary32 epsilons (2^-23) times EPS: 8, and the rounding of 1 + y,
// half an epsilon, carried n periods into the discount and through the annuity's (1 - v) / y; the kernel's price, and
// whether it is within the bound.
struct point { integer number; float<year^-1> yld; float reference; float bound; float price; bool holds; };
host table<point> points(capacity = 61) = {
{ number = 0, yld = 0.0 year^-1, reference = 150.0,
bound = 2.1457672119140625e-06 },
{ number = 1, yld = 0.0025 year^-1, reference = 146.88223651348557,
bound = 1.7977200559137244e-05 },
{ number = 2, yld = 0.005 year^-1, reference = 143.84009863855425,
bound = 1.002969252969427e-05 },
{ number = 3, yld = 0.0075 year^-1, reference = 140.8716073211935,
bound = 7.380375700309787e-06 },
{ number = 4, yld = 0.01 year^-1, reference = 137.97483829333993,
bound = 6.055607120079491e-06 },
{ number = 5, yld = 0.0125 year^-1, reference = 135.1479204815538,
bound = 5.260658226435223e-06 },
{ number = 6, yld = 0.015 year^-1, reference = 132.38903446397484,
bound = 4.730619521784012e-06 },
{ number = 7, yld = 0.0175 year^-1, reference = 129.69641097402683,
bound = 4.351958384447282e-06 },
{ number = 8, yld = 0.02 year^-1, reference = 127.06832944940564,
bound = 4.067908521387249e-06 },
{ number = 9, yld = 0.0225 year^-1, reference = 124.50311662491629,
bound = 3.846933119107885e-06 },
{ number = 10, yld = 0.025 year^-1, reference = 121.9991451677821,
bound = 3.670110104008014e-06 },
{ number = 11, yld = 0.0275 year^-1, reference = 119.55483235409017,
bound = 3.5253981698076093e-06 },
{ number = 12, yld = 0.03 year^-1, reference = 117.1686387850821,
bound = 3.4047697912543794e-06 },
{ number = 13, yld = 0.0325 year^-1, reference = 114.83906714204022,
bound = 3.3026674622671835e-06 },
{ number = 14, yld = 0.035 year^-1, reference = 112.56466097856256,
bound = 3.2151215462976808e-06 },
{ number = 15, yld = 0.0375 year^-1, reference = 110.34400354905249,
bound = 3.1392209865414947e-06 },
{ number = 16, yld = 0.04 year^-1, reference = 108.17571667229853,
bound = 3.072782499814594e-06 },
{ number = 17, yld = 0.0425 year^-1, reference = 106.05845962904465,
bound = 3.014136525526686e-06 },
{ number = 18, yld = 0.045 year^-1, reference = 103.99092809249404,
bound = 2.9619845249915393e-06 },
{ number = 19, yld = 0.0475 year^-1, reference = 101.97185309072105,
bound = 2.9153013441078625e-06 },
{ number = 20, yld = 0.05 year^-1, reference = 100.00000000000011,
bound = 2.8732668672309277e-06 },
{ number = 21, yld = 0.0525 year^-1, reference = 98.07416756809134,
bound = 2.8352171985045795e-06 },
{ number = 22, yld = 0.055 year^-1, reference = 96.19318696655597,
bound = 2.8006091573706718e-06 },
{ number = 23, yld = 0.0575 year^-1, reference = 94.35592087119859,
bound = 2.7689940361148874e-06 },
{ number = 24, yld = 0.06 year^-1, reference = 92.56126256977221,
bound = 2.7399979180215717e-06 },
{ number = 25, yld = 0.0625 year^-1, reference = 90.80813509609982,
bound = 2.713306719166981e-06 },
{ number = 26, yld = 0.065 year^-1, reference = 89.09549038979938,
bound = 2.688654682102069e-06 },
{ number = 27, yld = 0.0675 year^-1, reference = 87.42230848082309,
bound = 2.6658154264904175e-06 },
{ number = 28, yld = 0.07 year^-1, reference = 85.7875966980478,
bound = 2.6445949174624734e-06 },
{ number = 29, yld = 0.0725 year^-1, reference = 84.19038890117699,
bound = 2.624825888789e-06 },
{ number = 30, yld = 0.075 year^-1, reference = 82.62974473523889,
bound = 2.6063633814158705e-06 },
{ number = 31, yld = 0.0775 year^-1, reference = 81.10474890698997,
bound = 2.589081145504395e-06 },
{ number = 32, yld = 0.08 year^-1, reference = 79.61451048254841,
bound = 2.572868717085287e-06 },
{ number = 33, yld = 0.0825 year^-1, reference = 78.15816220561459,
bound = 2.5576290262264012e-06 },
{ number = 34, yld = 0.085 year^-1, reference = 76.73485983564518,
bound = 2.5432764272848974e-06 },
{ number = 35, yld = 0.08750000000000001 year^-1, reference = 75.34378150537444,
bound = 2.5297350668269316e-06 },
{ number = 36, yld = 0.09 year^-1, reference = 73.98412709709268,
bound = 2.516937523557286e-06 },
{ number = 37, yld = 0.0925 year^-1, reference = 72.65511763711123,
bound = 2.5048236687975895e-06 },
{ number = 38, yld = 0.095 year^-1, reference = 71.35599470786127,
bound = 2.4933397068857506e-06 },
{ number = 39, yld = 0.0975 year^-1, reference = 70.08601987709251,
bound = 2.4824373632030527e-06 },
{ number = 40, yld = 0.1 year^-1, reference = 68.84447414364999,
bound = 2.4720731939940767e-06 },
{ number = 41, yld = 0.10250000000000001 year^-1, reference = 67.63065739933171,
bound = 2.462207997185558e-06 },
{ number = 42, yld = 0.105 year^-1, reference = 66.44388790633732,
bound = 2.45280630737104e-06 },
{ number = 43, yld = 0.1075 year^-1, reference = 65.28350178983861,
bound = 2.44383596125993e-06 },
{ number = 44, yld = 0.11 year^-1, reference = 64.14885254521515,
bound = 2.4352677223807263e-06 },
{ number = 45, yld = 0.1125 year^-1, reference = 63.039310559513446,
bound = 2.4270749558211365e-06 },
{ number = 46, yld = 0.115 year^-1, reference = 61.95426264670144,
bound = 2.4192333453907933e-06 },
{ number = 47, yld = 0.11750000000000001 year^-1, reference = 60.893111596304976,
bound = 2.411720646888345e-06 },
{ number = 48, yld = 0.12 year^-1, reference = 59.855275735021564,
bound = 2.404516472207735e-06 },
{ number = 49, yld = 0.1225 year^-1, reference = 58.84018850092624,
bound = 2.3976020998781002e-06 },
{ number = 50, yld = 0.125 year^-1, reference = 57.84729802988987,
bound = 2.3909603083366372e-06 },
{ number = 51, yld = 0.1275 year^-1, reference = 56.87606675384551,
bound = 2.3845752288142468e-06 },
{ number = 52, yld = 0.13 year^-1, reference = 55.92597101054892,
bound = 2.378432215193842e-06 },
{ number = 53, yld = 0.1325 year^-1, reference = 54.99650066449042,
bound = 2.3725177285996782e-06 },
{ number = 54, yld = 0.135 year^-1, reference = 54.08715873862556,
bound = 2.3668192348081898e-06 },
{ number = 55, yld = 0.1375 year^-1, reference = 53.19746105660286,
bound = 2.361325112848548e-06 },
{ number = 56, yld = 0.14 year^-1, reference = 52.32693589517723,
bound = 2.3560245733942948e-06 },
{ number = 57, yld = 0.14250000000000002 year^-1, reference = 51.475123646504585,
bound = 2.3509075857436734e-06 },
{ number = 58, yld = 0.145 year^-1, reference = 50.64157649002761,
bound = 2.345964812352173e-06 },
{ number = 59, yld = 0.1475 year^-1, reference = 49.82585807366655,
bound = 2.3411875500213034e-06 },
{ number = 60, yld = 0.15 year^-1, reference = 49.027543204040455,
bound = 2.3365676769671095e-06 },
};
// The textbook's bonds: Fabozzi's five-year 6% bond at 8%; 5% bonds of 1, 5 and 30 years at their coupon; a ten-year 5%
// bond at no yield; a seven-year bond with no coupon at 4%; and, for the face and the tenor, 3% bonds of 10 and 2 years
// at 3% and 4%.
host struct {
float<year^-1> fabozzi_coupon = 0.06 year^-1; float<year> five = 5.0 year; float<year^-1> eight = 0.08 year^-1;
float<year^-1> five_percent = 0.05 year^-1; float<year> one = 1.0 year; float<year> thirty = 30.0 year;
float<year> ten = 10.0 year; float<year^-1> nothing = 0.0 year^-1; float<year> seven = 7.0 year;
float<year^-1> four = 0.04 year^-1; float<year^-1> three = 0.03 year^-1; float<year> two = 2.0 year;
} bonds;
// The sweep: the step, from 0 to 60 and round again; the yield in percent; the price there, and at the two steps
// before.
struct { integer step = -1; float<year^-1> yld; float yield_percent; float price; float before; float earlier; } sweep;
// The checks: Fabozzi's bond 91.889 per 100 within 0.001; every yield's price within its bound of the cash flows
// summed; the price falling as the yield rises and bending upward; a bond at its coupon at par within 8 epsilons, at no
// yield its coupons and face undiscounted, 150, within 4, and with no coupon its face discounted 14 periods within 8
// and 7; a price per 200 of face twice the price per 100, bit for bit; and a point of yield costing a ten-year bond
// more than a two-year one.
struct {
bool fabozzi = true; bool cash_flows = true; bool convex = true; bool limits = true; bool face = true;
bool tenor = true;
} checks;
// A strip of lamps the page draws, lit green while every check holds and red once one fails.
struct lamp { vector<float, 4> colour; };
lattice<lamp, 2> lamps(spacing = 1 m, boundary = wall);
vector<float, 4> lamp_colour(bool holds)
{
return holds ? vector<float, 4>(0.15, 0.6, 0.3, 1.0) : vector<float, 4>(0.75, 0.15, 0.15, 1.0);
}
float eps() { return 1.0 / 8388608.0; }
bool near(float price, float reference, float bound) { return abs(price / reference - 1.0) <= bound; }
integer failing(table<bool> holds) { return count(holds) where !holds; }
bool limits(float face, float<year^-1> coupon, float<year> one, float<year> five, float<year> thirty, float<year> ten,
float<year^-1> nothing, float<year> seven, float<year^-1> four, float<year^-1> freq)
{
let par1 = lusine.finance.bond_price(face, coupon, years = one, yld = coupon, freq);
let par5 = lusine.finance.bond_price(face, coupon, years = five, yld = coupon, freq);
let par30 = lusine.finance.bond_price(face, coupon, years = thirty, yld = coupon, freq);
let free = lusine.finance.bond_price(face, coupon, years = ten, yld = nothing, freq);
let zero = lusine.finance.bond_price(face, coupon = nothing, years = seven, yld = four, freq);
return abs(par1 - 100.0) <= 8.0 * eps() * 100.0 && abs(par5 - 100.0) <= 8.0 * eps() * 100.0
&& abs(par30 - 100.0) <= 8.0 * eps() * 100.0 && abs(free - 150.0) <= 4.0 * eps() * 150.0
&& abs <= eps * (8.0 + 14.0 / 2.0);
}
bool tenor(float face, float<year^-1> three, float<year^-1> four, float<year> two, float<year> ten,
float<year^-1> freq)
{
let short = lusine.finance.bond_price(face, coupon = three, years = two, yld = four, freq)
/ lusine.finance.bond_price(face, coupon = three, years = two, yld = three, freq) - 1.0;
let long = lusine.finance.bond_price(face, coupon = three, years = ten, yld = four, freq)
/ lusine.finance.bond_price(face, coupon = three, years = ten, yld = three, freq) - 1.0;
return long < short && short < 0.0;
}
output table<lusine.widget> panels = {
lusine.series { { panel = "the curve", chart = "price per 100", quantity = sweep.price, steps = 61,
against = sweep.yield_percent, label = "bond_price" } },
lusine.value { { panel = "the curve", quantity = sweep.yield_percent, label = "yield, %" } },
lusine.value { { panel = "the curve", quantity = sweep.price, label = "price per 100" } },
lusine.value { { panel = "the checks", quantity = checks.fabozzi, label = "Fabozzi's bond" } },
lusine.value { { panel = "the checks", quantity = checks.cash_flows, label = "the cash flows summed" } },
lusine.value { { panel = "the checks", quantity = checks.convex, label = "falling and convex" } },
lusine.value { { panel = "the checks", quantity = checks.limits, label = "par, no yield, no coupon" } },
lusine.value { { panel = "the checks", quantity = checks.face, label = "linear in the face" } },
lusine.value { { panel = "the checks", quantity = checks.tenor, label = "the long bond moves more" } },
};
void main()
{
sweep.step = sweep.step >= 60 ? 0 : sweep.step + 1;
sweep.earlier = sweep.before;
sweep.before = sweep.price;
sweep.yld = float(sweep.step) * 0.0025 year^-1;
sweep.yield_percent = 100.0 * float(sweep.step) * 0.0025;
sweep.price = lusine.finance.bond_price(settings.face, settings.coupon, settings.years, sweep.yld, settings.freq);
points.price = lusine.finance.bond_price(settings.face, settings.coupon, settings.years, points.yld, settings.freq);
points.holds = near(points.price, points.reference, points.bound);
checks.fabozzi = checks.fabozzi
&& abs(lusine.finance.bond_price(settings.face, coupon = bonds.fabozzi_coupon, years = bonds.five,
yld = bonds.eight, settings.freq) - 91.889) < 0.001;
checks.cash_flows = checks.cash_flows && failing(holds = points.holds) == 0;
checks.convex = checks.convex && (sweep.step < 1 || sweep.price < sweep.before)
&& (sweep.step < 2 || sweep.price - sweep.before > sweep.before - sweep.earlier);
checks.limits = checks.limits && limits(settings.face, coupon = bonds.five_percent, bonds.one, bonds.five,
bonds.thirty, bonds.ten, bonds.nothing, bonds.seven, bonds.four,
settings.freq);
checks.face = checks.face
&& lusine.finance.bond_price(face = 2.0 * settings.face, coupon = bonds.three, years = bonds.ten,
yld = bonds.four, settings.freq)
== 2.0 * lusine.finance.bond_price(settings.face, coupon = bonds.three, years = bonds.ten,
yld = bonds.four, settings.freq);
checks.tenor = checks.tenor && tenor(settings.face, bonds.three, bonds.four, bonds.two, bonds.ten, settings.freq);
lamps.colour = lamp_colour(holds = checks.fabozzi && checks.cash_flows && checks.convex && checks.limits
&& checks.face && checks.tenor);
}
The kernel's other proof (the same curve read back on the host) is on its proof page.