Skip to content

Accrual, live

Simple interest as a growth factor, g = 1 + r t: what a balance is multiplied by over a time at an annual rate, linear in the time, not compounded; a negative rate draws down (lusine.finance.accrue). A ratio, in no currency: a game multiplies its own cash by it. Below, a balance is grown at 5% a year for ten years on your GPU as you read, a tenth of a year a step, by the library's factor over the whole time, beside the same balance compounded at each year's end: the library's factor over a year for each year so far, and its factor over the part of the year since.

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 balance: the chart draws the interest on a balance of 100 EUR, simple and compounded yearly, against the time in years, a point a step: one through the first year, the compounded interest curving away above the straight line after it, to 62.89 EUR against 50 EUR at ten years. Below it, the time and the two growths.
  • The checks: a day at 3.65%, a day on a 365-day year growing a balance by a ten-thousandth, to 1.0001 within two binary32 epsilons (2⁻²³); ACT/360 and ACT/365, 300 days at 5% paying 365/360 as much counted on a 360-day year as on a 365-day one, within the rounding the interest inherits from the factor (two epsilons over each interest); linear in the time, the interest over twice the time twice the interest, within two epsilons of the factor; simple, not compound, the compounded balance the simple one bit for bit through the first year and above it after; no rate, no time, no rate growing nothing over 0.7 and 30 years, and no time nothing at 5%, each exactly 1, and a negative rate drawing down. 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 rate and the years swept are the program's settings, which the host may write; this page offers no control to change them yet.

The program

The sweep and the checks, the panels the page draws, and the one procedure, main, run each step: the time a tenth of a year on, the two balances, and the checks.

// The accrual's live page: a balance grown at 5% a year by simple interest for ten years, beside the same balance
// compounded at each year's end, the library's growth factor at each, held to the day counts' textbook cases, to
// interest linear in the time and to its limits, with the page's checks computed here on the GPU and shown by the
// panels the program declares.

namespace accrue;

lusine.version version = { { number = 1 } };

// The reader's settings: the rate and the years the sweep runs.
host struct { float<year^-1> rate = 0.05 year^-1; integer years = 10; } settings;

// The textbook's cases: a day of a 365-day year at 3.65%; 300 days at 5% on a 360-day year and on a 365-day one; and
// for the limits, no rate over 0.7 and 30 years, no time at 5%, and a rate of -0.5% over a year.
host struct {
  float<year^-1> day_rate = 0.0365 year^-1; float<year> day = 0.0027397260273972603 year;
  float<year^-1> rate = 0.05 year^-1; float<year> act360 = 0.8333333333333334 year;
  float<year> act365 = 0.821917808219178 year; float<year> short = 0.7 year; float<year> long = 30.0 year;
  float<year^-1> negative = -0.005 year^-1; float<year^-1> no_rate = 0.0 year^-1; float<year> no_time = 0.0 year;
  float<year> one = 1.0 year;
} cases;

// The sweep: the time in tenths of a year, from 0 to the years and round again, and in years; the growth by simple
// interest, the library's; the growth compounded at each year's end, the years so far each the library's factor over a
// year, and simple within the year; the interest over the time and over twice it; and, for the chart, the interest on a
// balance of 100 EUR each way.
struct {
  integer tenths = -1; float<year> time; float simple; float yearly = 1.0; float compounded; float interest;
  float doubled; float<EUR> simple_interest; float<EUR> compounded_interest;
} sweep;

// The checks: a day at 3.65% grows a balance by a ten-thousandth, and 300 days at 5% pay 365/360 as much counted on a
// 360-day year as on a 365-day year, each within binary32's rounding; the interest over twice the time twice the
// interest, within the factors' rounding; compounding no different in the first year and above simple after it; no rate
// or no time growing nothing, bit for bit, and a negative rate drawing down.
struct { bool day = true; bool counts = true; bool linear = true; bool compound = true; bool limits = 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);
}

// binary32's epsilon, 2^-23.
float eps() { return 1.0 / 8388608.0; }

bool day(float<year^-1> rate, float<year> time)
{
  return abs(lusine.finance.accrue(rate, time) - 1.0001) < 2.0 * eps();
}

bool counts(float<year^-1> rate, float<year> act360, float<year> act365)
{
  let eur = lusine.finance.accrue(rate, time = act360) - 1.0;
  let gbp = lusine.finance.accrue(rate, time = act365) - 1.0;
  return abs < 2.0 * eps / eur + 2.0 * eps / gbp;
}

bool limits(float<year^-1> rate, float<year^-1> negative, float<year^-1> no_rate, float<year> short, float<year> long,
            float<year> no_time, float<year> one)
{
  return lusine.finance.accrue(rate = no_rate, time = short) == 1.0
      && lusine.finance.accrue(rate = no_rate, time = long) == 1.0
      && lusine.finance.accrue(rate, time = no_time) == 1.0
      && lusine.finance.accrue(rate = negative, time = one) < 1.0;
}

output table<lusine.widget> panels = {
  lusine.series { { panel = "the balance", chart = "interest on 100 EUR", quantity = sweep.simple_interest,
                    steps = 101, against = sweep.time, label = "simple" } },
  lusine.series { { panel = "the balance", chart = "interest on 100 EUR", quantity = sweep.compounded_interest,
                    steps = 101, against = sweep.time, label = "compounded yearly" } },
  lusine.value { { panel = "the balance", quantity = sweep.time, label = "time" } },
  lusine.value { { panel = "the balance", quantity = sweep.simple, label = "simple" } },
  lusine.value { { panel = "the balance", quantity = sweep.compounded, label = "compounded yearly" } },
  lusine.value { { panel = "the checks", quantity = checks.day, label = "a day at 3.65%" } },
  lusine.value { { panel = "the checks", quantity = checks.counts, label = "ACT/360 and ACT/365" } },
  lusine.value { { panel = "the checks", quantity = checks.linear, label = "linear in the time" } },
  lusine.value { { panel = "the checks", quantity = checks.compound, label = "simple, not compound" } },
  lusine.value { { panel = "the checks", quantity = checks.limits, label = "no rate, no time" } },
};

void main()
{
  sweep.tenths = sweep.tenths >= 10 * settings.years ? 0 : sweep.tenths + 1;
  sweep.time = float(sweep.tenths) * 0.1 year;
  sweep.simple = lusine.finance.accrue(settings.rate, sweep.time);
  // A year's end: the balance so far grows by the library's factor over the year.
  sweep.yearly = sweep.tenths == 0 ? 1.0
                 : (sweep.tenths % 10 == 0 ? sweep.yearly * lusine.finance.accrue(settings.rate, time = 1.0 year)
                                           : sweep.yearly);
  sweep.compounded = sweep.yearly * lusine.finance.accrue(settings.rate, time = float(sweep.tenths % 10) * 0.1 year);
  sweep.simple_interest = (sweep.simple - 1.0) * 100.0 EUR;
  sweep.compounded_interest = (sweep.compounded - 1.0) * 100.0 EUR;
  sweep.interest = sweep.simple - 1.0;
  sweep.doubled = lusine.finance.accrue(settings.rate, time = 2.0 * sweep.time) - 1.0;
  checks.day = checks.day && day(rate = cases.day_rate, time = cases.day);
  checks.counts = checks.counts && counts(cases.rate, cases.act360, cases.act365);
  checks.linear = checks.linear && abs(sweep.doubled - 2.0 * sweep.interest) <= 2.0 * eps() * (1.0 + sweep.doubled);
  checks.compound = checks.compound
                    && (sweep.tenths <= 10 ? sweep.compounded == sweep.simple : sweep.compounded > sweep.simple);
  checks.limits = checks.limits && limits(cases.rate, cases.negative, cases.no_rate, cases.short, cases.long,
                                                  cases.no_time, cases.one);
  lamps.colour = lamp_colour(holds = checks.day && checks.counts && checks.linear && checks.compound && checks.limits);
}

The kernel's other proof (a hundred and one times against a reference in float64) is on its proof page.