Blogs / Math and Code on This Blog: A Worked Annuity Example

  • actuarial
  • python
  • time value of money

Math and Code on This Blog: A Worked Annuity Example

The blog now renders LaTeX math and runs Python live in your browser. To prove it works, here's a small worked example: pricing a level annuity.

Ananta A Joy··4 min read

This site recently moved to a new stack, and the blog can now render proper mathematics and syntax-highlighted code — and the Python below is live: hit Run and it executes right in your browser (via Pyodide, Python compiled to WebAssembly — nothing is sent to any server). Rather than a lorem-ipsum test page, here’s a tiny worked example from the first thing every actuarial student learns: the time value of money.

The setup

Money today is worth more than money tomorrow. If the effective annual interest rate is ii, then one unit payable in a year is worth v=11+iv = \frac{1}{1+i} today — the discount factor. A payment of 1 due in nn years is worth vnv^n now.

An annuity-immediate pays 1 at the end of each year for nn years. Its present value is just the sum of the discounted payments, which collapses into a closed form:

an=v+v2++vn=1vnia_{\overline{n}|} = v + v^2 + \cdots + v^n = \frac{1 - v^n}{i}

If payments happen at the start of each year instead (an annuity-due), every payment arrives one year earlier, so the whole thing is worth one year’s interest more:

a¨n=(1+i)an=1vnd,d=i1+i\ddot{a}_{\overline{n}|} = (1+i)\, a_{\overline{n}|} = \frac{1 - v^n}{d}, \qquad d = \frac{i}{1+i}

The same thing in Python — run it

Ten lines, no libraries. The first click downloads the Python runtime (a few seconds, cached afterwards); after that, runs are instant.

def annuity_pv(i: float, n: int, due: bool = False) -> float:
  """Present value of a level annuity of 1 per year for n years."""
  v = 1 / (1 + i)
  pv = (1 - v**n) / i
  return pv * (1 + i) if due else pv

# A 10-year annuity of 1 per year at 5%
print(f"immediate: {annuity_pv(0.05, 10):.6f}")
print(f"due:       {annuity_pv(0.05, 10, due=True):.6f}")

The brute-force check — summing the discounted payments directly — agrees with the closed form to the last decimal place, which is exactly the point of the closed form:

i, n = 0.05, 10
v = 1 / (1 + i)

closed_form = (1 - v**n) / i
brute_force = sum(v**t for t in range(1, n + 1))

print(f"closed form: {closed_form:.12f}")
print(f"brute force: {brute_force:.12f}")
print("match!" if abs(closed_form - brute_force) < 1e-12 else "mismatch")

Since the interpreter runs on your machine, nothing here can touch this site — and once the runtime is cached, every later runnable post loads instantly too.

And in R — also live

The same calculation, vectorised the R way. This one runs on webR — R itself compiled to WebAssembly, so it executes in your browser exactly like the Python above. The first run on this page loads a slightly larger runtime (R plus base packages), then every later run — Python or R — is instant.

annuity_pv <- function(i, n, due = FALSE) {
v <- 1 / (1 + i)
pv <- (1 - v^n) / i
if (due) pv * (1 + i) else pv
}

annuity_pv(0.05, 10)              # 7.721735
sum((1 / 1.05)^(1:10))            # same answer, brute force

Why this matters beyond the demo

Every reserve, every premium, and every IFRS 17 fulfilment cash flow is, at its heart, this operation repeated at scale: project cash flows, discount them, sum them. The interesting actuarial questions are about which cash flows to project (mortality, lapse, expenses) and which discount curve to use — but the machinery underneath is the humble CFtvt\sum \text{CF}_t \, v^t.

That’s also why this post exists: upcoming posts on IFRS 17 — starting with how the contractual service margin (CSM) rolls forward — will lean on exactly this combination of formulas and runnable code. Now the blog can carry both.