Accrual: simple interest¶
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.
A ratio, in no currency: a game multiplies its own cash by it, rounds to its quantum by its amount's declared rule, and counts what it minted. Source: Hull, Options, Futures and Other Derivatives, 4.1 and 6.1 (day counts). Checked against a reference in numpy, which has its own tests.
- Source
- Hull, Options, Futures and Other Derivatives, 4.1 and 6.1 (day counts).
- Proof
- reference, limit, invariant.
accrue.trait
namespace lusine.finance;
float accrue(float<year^-1> rate, float<year> time)
{
return 1.0 + rate * time;
}
The factor a balance grows by over a time at an annual rate, g = 1 + r t. A ratio, in no currency: a game multiplies its own cash by it, rounds to its quantum by its amount's declared rule, and counts what it minted.
import os, sys
root = os.getcwd()
while not os.path.isfile(os.path.join(root, 'docsite.json')):
root = os.path.dirname(root)
sys.path.insert(0, os.path.join(root, 'tools'))
import numpy as np
import matplotlib.pyplot as plt
from lusine import trait_run
EPS = float(np.finfo(np.float32).eps)
YEAR = 31557600.0 # lusine's year row, the Julian year in seconds: the host stores and reads in the base units
The kernel¶
The law as the library states it, and as this page runs it.
print(trait_run.source('finance/accrue.trait'))
// lusine.finance.accrue: 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. A ratio, in no currency: a game
// multiplies its own cash by it, rounds to its quantum by its amount's declared
// rule, and counts what it minted.
// Source: Hull, Options, Futures and Other Derivatives, 4.1 and 6.1 (day counts).
// Checked against a reference in numpy, which has its own tests.
namespace lusine.finance;
float accrue(float<year^-1> rate, float<year> time)
{
return 1.0 + rate * time;
}
The program¶
Each section runs the kernel over a lattice of items, one a case: the host sets every parameter, and main() binds the kernel over the items. The host speaks the base units, so a time in years is stored as t * YEAR seconds and an annual rate as r / YEAR, and a result in years is read back the same way; inside, the law runs on those, in binary32.
print(trait_run.law_program('finance/accrue.trait', 'accrue'))
namespace probe;
lusine.version version = { { number = 1 } };
struct item {
host float<year^-1> rate;
host float<year> time;
float result_0;
};
lattice<item, 1> items(spacing = 1 m, boundary = wall);
void main() { items.result_0 = lusine.finance.accrue(items.rate, items.time); }
Reference: simple interest and the day counts¶
A day at 3.65% on a 365-day year grows a balance by a ten-thousandth; for the same 300 days at 5%, the euro's ACT/360 pays 365/360 of sterling's ACT/365. Each factor is within binary32's rounding of its value; the interest, g - 1, loses the digits of the 1 it is read from, so the ratio holds to eps / (g - 1) of each.
g = trait_run.law('finance/accrue.trait', 'accrue', rate=np.array([0.0365, 0.05, 0.05]) / YEAR,
time=np.array([1 / 365, 300 / 360, 300 / 365]) * YEAR)
assert abs(g[0] - 1.0001) < 2 * EPS, g[0]
eur, gbp = g[1] - 1, g[2] - 1
assert abs(eur / gbp / (365 / 360) - 1) < 2 * EPS / eur + 2 * EPS / gbp, eur / gbp
print(f'a day: {g[0]:.7f}; 300 days ACT/360 {eur:.6f}, ACT/365 {gbp:.6f}')
a day: 1.0001000; 300 days ACT/360 0.041667, ACT/365 0.041096
Limit: no rate, no interest¶
At no rate the factor is one exactly, whatever the time; no time accrues nothing; a negative rate draws down.
g = trait_run.law('finance/accrue.trait', 'accrue', rate=np.array([0.0, 0.0, 0.05, -0.005]) / YEAR,
time=np.array([0.7, 30.0, 0.0, 1.0]) * YEAR)
assert g[0] == 1.0 and g[1] == 1.0 and g[2] == 1.0 and g[3] < 1.0, g
print('no rate, no time:', g[:3], ' a negative rate:', g[3])
no rate, no time: [1. 1. 1.] a negative rate: 0.9950000047683716
Invariant: simple, not compound¶
The interest is linear in the time, r t to binary32's rounding of the factor; compounding yearly falls below it inside the first year and curves away above it after.
t = np.linspace(0.0, 10.0, 101)
g = trait_run.law('finance/accrue.trait', 'accrue', rate=0.05 / YEAR, time=t * YEAR)
assert np.all(np.abs(g - (1 + 0.05 * t)) <= 2 * EPS * (1 + 0.05 * t))
assert np.all(np.diff(g) > 0)
assert np.all((1.05 ** t - g)[1:10] < 0) and np.all((1.05 ** t - g)[11:] > 0)
fig, ax = plt.subplots(figsize=(7, 3.2))
ax.plot(t, g, label='simple, the kernel')
ax.plot(t, 1.05 ** t, '--', label='compounded yearly')
ax.set_xlabel('years')
ax.set_ylabel('growth')
ax.legend()
plt.show()