Nathan Grigg

Calculating the rate of return of a 401(k)

After many years of school, I now have a Real Job. Which means I need to save for retirement. I don’t do anything fancy, just index funds in a 401(k). Nevertheless, I am curious about how my money is growing.

The trouble with caring even a little about the stock market is that all the news and charts focus on a day at a time. Up five percent, down a percent, down another two percent. I don’t care about that. I could average the price changes over longer periods of time, but that is not helpful because I’m making periodic contributions, so some dollars have been in the account longer than others.

What I really want to know is, if I put all my money into a savings account with a constant interest rate, what would that rate need to be to have the same final balance as my retirement account?

Now it’s math. A single chunk of money P with interest rate r becomes the well-known Pert after t years. So if I invest a bunch of amounts Pi, each for a different ti years at interest rate r, I get ∑ Pierti. I need to set this equal to the actual balance B of my account and solve for r.

At this point, I could use solve the equation using something from scipy.optimize. But since I’m doing this for fun, I may as well write something myself. The nice thing about my interest function is that it increases if I increase r and decreases if I decrease r. (This is called monotonic and is a property of the exponential function, but is also intuitively obvious.) So I can just pick values for r and plug them in, and I’ll immediately know if I need to go higher or lower. This is a textbook scenario for a binary search algorithm.

The following Python function will find when our monotonic function is zero.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from __future__ import division  # For Python 2.

def FindRoot(f, lower, upper, tolerance):
    """Find the root of a monotonically increasing function."""
    r = (lower + upper) / 2
    while abs(upper - lower) > tolerance:
        r = (lower + upper) / 2
        if f(r) > 0:
            upper = r
        else:
            lower = r
    return (lower + upper) / 2

This will look for a root between lower and upper, stopping when it gets within tolerance. At each stage of the loop, the difference between lower and upper is cut in half, which is why it is called binary search, and which means it will find the answer quickly.

Now suppose that I have a Python list transactions of pairs (amount, time), where amount is the transaction amount and time is how long ago in years (or fractions of years, in my case) the transaction happened. Also, I have the current balance stored in balance. The difference between our hypothetical savings account and our actual account is computed as follows:

import math

diff = lambda r: sum(p * math.exp(r * t) for p, t in transactions) - balance

Now I can use FindRoot to find when this is zero.

rate = FindRoot(diff, -5, 5, 1e-4)

This will go through the loop about 16 times. (log2((upper−lower)/tolerance))

The U.S. government mandates that interest rates be given as annual percentage yield (APY), which is the amount of interest you would earn on one dollar in one year, taking compounding into consideration. Since I have assumed interest is compounded continuously, I should convert to APY for easier comparison. In one year, one dollar compounded continuously becomes er. Subtracting the original dollar, I get the APY:

apy = math.exp(rate) - 1