Adventures in modern web programming

At this point, I’ve fallen so far behind of where JS developers are that I don’t think I’ll ever be able to figure out what’s going on. However, Vercel is a portfolio company of GV’s, so I decided to give it a valiant effort.

Thus, I started at vercel.com. I went through their deploy flow for a Gatsby template, linked my GitHub account, and ended up with a static webpage. This created a new Gatsby repository on my GitHub account. Unfortunately, I also have no idea how to use Gatsby. However, I’ve also been meaning to learn Gatsby, so let’s dive in.

I cloned the repository and opened up in Visual Studio Code. Unfortunately, I’m not super familiar with VS Code, either, so then I had to look up how to add the damn folder to my workspace. (The weird thing about working at Google is that I have the best tools in the world at my disposal… just not the ones anyone else in the world uses.)

One quick StackOverflow search later, I’m suspiciously inspecting index.js in VS Code. This seems to be the business end of the app, but unfortunately I’m not familiar with React nor Helmet, both of which seem to be doing some lifting here.

Usually I’ve found the best way to learn a new thing is to mess around with it, so let’s start by changing the front end. I change the h1, commit, and push.

I head to the Vercel equivalent of the github page (e.g., my repo is github.com/kchodorow/gatsby, so my Vercel dashboard for it is https://vercel.com/kchodorow/gatsby. Nice. After a second, it updates and shows my new commit as the deployed version. Very nice. It also has been emailing me about its actions each step, which is a bit much for a personal project but would be nice in general.

Okay, time to get serious. How do I actually connect Vercel to a backend? Googling around for this, it looks like I’m going to be writing serverless functions. Guess what else I’m not familiar with? However, this looks interesting. Basically I can put node.js functions in files like api/foo.ts and this becomes a server request my app can make (/api/foo). I rename date.ts to hello.ts and push it out.

Vercel displays “Build failed.” Clicking on it, It gives me the build logs:

I take a look at index.js and realize that there’s some code that calls the backend function and loads it into a variable, which I completely neglected to change. Well, that’s good, just having {hello} work would be a bit too magic for my blood (and how would nested directories in /api be specified?). I update index.js and this time, cleverly, run yarn run build before pushing.

Sigh. Fine. I install yarn. Then I run yarn. It immediately fails because I needed to run npm install first. So I install dependencies, then I run yarn. Success! A push later, a successful build, and:

Verdict: Vercel is very cool. And I feel a little less behind the curve.

See the actual code behind this paragon of frontend programming at https://github.com/kchodorow/gatsby.

Hassle-free LaTeX with Overleaf

There is something delightful about LaTeX. However, the last time I bothered with it was in college, since I don’t have much call for PDFs in day-to-day life. I recently came across Overleaf, which is an online LaTeX editor. The nice part is that it live-renders your work and you can right-click->Save as an PNG. Thus, you can suddenly embed gorgeously formatted math anywhere. For example, here’s one of my favorite proofs, that the square root of two is not a rational number:

Proof by contradiction.

Source code:

\documentclass[varwidth=true, border=10pt]{standalone}
\usepackage[utf8]{inputenc}
\usepackage{amsmath}

\begin{document}

Suppose $\sqrt{2}$ was rational. Then we could write:

\[ \sqrt{2} = \frac{a}{b} \]

...where $a/b$ is in lowest terms. Squaring both sides yields:

\[ 2 = \frac{a^{2}}{b^{2}} \]

Now multiply both sides by $b^{2}$:

\[ 2b^{2} = a^{2} \]

$a^{2}$ must be even, since $b^{2}$ is multiplied by 2. For $a^{2}$ to be even, $a$ must be even, so we can say that $a = 2c$ for some $c$. 

Thus, we can write this equation as:

\[ 2b^{2} = (2c)^{2} \]

or:

\[ 2b^{2} = 4c^{2} \]

Now we can divide both sides by 2... but we end up with $b^{2} = 2c^{2}$, which is shaped the same as $2b^{2} = a^{2}$ above!

We can continue expanding this equation out forever, so there are no whole numbers that $a$ and $b$ can resolve to.

Thus, $\sqrt{2}$ is irrational.

\end{document}

Gorgeous.

Risking it all

Sorry to keep posting financial stuff, but whatever, it’s my blog.

It’s interesting how the amount of investment risk that a human can put up with is very relevant to how much they have invested, and it isn’t linear. Let’s take the case of three investors, all of whom currently can invest $1k/month and need $1M in assets to live comfortably off of assets alone. While more is more, suppose these people aren’t particularly driven to keep accumulating wealth beyond their needs ($1M).

They start with:

  • Investor A: $1,000 in investments
  • Investor B: $1,000,000 in investments
  • Investor C: $1,000,000,000 in investments

To simplify things, we’ll say they keep 100% of their assets in stocks. Now, let’s say the market plunges by 90%: $100 invested in the market is now worth $10. What happens to each investor?

  • Investor A now has $100 in investments
  • Investor B now has $100,000 in investments
  • Investor C now has $100,000,000 in investments

I would argue that investors A & C are in a similar boat here, ironically. Investor A started out .1% of the way towards their goal and next month, they will be back to that. Not much has changed for them: the market set them back by one month.

Conversely, it doesn’t really matter what happens to Investor C’s portfolio. They’re doing fine regardless: greater than 100% of the money they need is still greater than 100%, even if it’s less than before.

Thus, Investor B is the only one in the danger zone. They were exactly at their investment goal, and now they’re only 1/10th of the way there! Theoretically, they’re now 7 years (900 months) away from $1M!

I was reading about “bond tents” as a way to defend against stock market crashes at retirement: you don’t want a market crash right when you retire, because then you’ll sell your stocks and have no way to replenish them to take advantage of the market recovery. (This is called sequence of returns risk, which ERN does a great job explaining.) Thus, it’s a good idea to increase your bond allocation going into your retirement so you don’t have to sell any stocks if there is a crash. Bond tents might be a good mechanism for investors like Investor B, too: if you’re near your goal you have more to lose than any other time.

Part 4: compounding returns

As Einstein (maybe) said, compounding interest is the eighth wonder of the world. In the previous posts in this series, we used a very linear benchmark: 4% off of the amount contributed forever. However, this is a weird way to benchmark results. Imagine you and friend (call him Baelish) are both investing and comparing results. You start off by investor $1k in August, 2020 and hope to have $1040 in one year. One year later, you have succeeded and have $1040. You tell your friend Peter Baelish about your investment and he puts $1040 in the market and sets a goal of making 4%: he’ll try to have $1081.60 by next year. However, if you stick with the model used in the previous post, your goal for the year will only be $1080 by 2022, leaving you a whole $1.60 poorer than your friend. (Arguably. We are talking about the benchmark, not the actual amount of money you’re earning. However, this whole series could probably be titled “It’s easy to mislead yourself that you’re doing better than you are,” so it’s fitting the theme)

So instead of a fixed amount, we want to continuous compound our benchmark. Because each value in the benchmark is determined based on the previous one, this is not Pandas-friendly. We’ll have to drop into “normal Python” to compute a more accurate benchmark.

Let’s stick with SPY, making once-a-month $1k contributions with the goal of a 4% annual return. The only difference is that we now want a 4% compounding annual return.

There are 253 trading days in a year and the formula for continuously compounding returns is:

Pt = P0 * ert

That is, the amount you have at time t (Pt) is the amount you have at the beginning of the period (P0), multiplied by the constant e raised to the rate (4%) times the amount of time. For example, if we contribute $1k and wait 1 year, we’ll want to have:

P1 = $1000 * e4% * 1 year = $1000 * e.04 * 1 = $1040.81

We get an extra $.81 cents off of the continuous compounding.

However, we want to be able to graph this benchmark by the day, so our t isn’t 1, it’s 1 year / 253 days = .00395. Plugging this in, each day we should have e.00016 more than the previous day. Using the code from previous posts, this gives us:

import math
benchmark = []
idx = 0
for day in df.index:
  contribution = 0
  if day in my_portfolio.index:
    contribution = my_portfolio.loc[day].total_cost_basis
  # The first time through, benchmark is [] (False), so it just adds
  # todays_benchmark. Each subsequent iteration it uses the previous row.
  if benchmark:
    today = benchmark[-1] * math.exp(.004 * (1/253)) + contribution
  else:
    today = contribution
  benchmark.append(today)
df.assign(benchmark=benchmark)[['total', 'benchmark']].plot()

This is, honestly, extremely similar to the original chart. After all, at the end of 1 year we only expect them to have diverged by ~80 cents per $1000! However, over time this number grows, which is clearer if we subtract our cost basis from the benchmark:

(df.assign(cost_basis=my_portfolio.total_cost_basis.cumsum())
    .fillna(method='ffill')
    .assign(performance=lambda x: x.benchmark - x.cost_basis)
    .performance
    .plot())
If you squint, you can still see the three distinct contributions. Pretty sure the wiggles are caused by weekends.

The nice thing about investing is that your money works “while you sleep” (depending on timezone). Tightening up the benchmark line by using a compounding benchmark lets us hold our money accountable when reviewing performance.

A multi-stock portfolio: comparing returns

The last posts have discussed portfolio performance with a very boring portfolio: one stock! Things get more interesting when there’s more than one stock to compare.

Let’s say we have a two stock portfolio now: SPY (as before) and DOCU (Docusign). We’ll combine the two tickers into one dataframe:

spy = pandas_datareader.data.DataReader('SPY', 'yahoo', start='2020-05-01', end='2020-07-31')
docu = pandas_datareader.data.DataReader('DOCU', 'yahoo', start='2020-05-01', end='2020-07-31')
df = (
    pd.concat([spy.assign(ticker='SPY'), docu.assign(ticker='DOCU')])
    .reset_index()
    .set_index(['ticker', 'Date'])
    .sort_index())

Now that things are getting more complicated, I’m going to switch to graphing with Altair, which I’ve found to be a more sophisticated (and prettier) alternative to matplotlib (which is what df.plot gives you).

import altair as alt
alt.Chart(df.reset_index()).mark_line().encode(
    x='Date',
    y='Close',
    color='ticker',
)
Similar, but a bit more sophisticated-looking.

Both stocks are doing well and it looks like DOCU is doing a bit better. So what would happen if we had invested $1000 in each of these stocks each month? We’ll create a random portfolio in a similar way to the first post, but notice that now we have to group by ticker symbol when we want ticker-specific info.

import numpy as np
# Get a random price on the first of the month _for each ticker_.
first_of_the_month = df[df.index.get_level_values(1).day == 1]
my_portfolio = (
    first_of_the_month
    .apply(lambda x: np.random.randint(x.Low, x.High), axis=1)
    .rename('share_cost')
    .to_frame()
    .assign(
         # Add a cost basis column to show $1k/month.
         cost_basis=1000,
         shares_purchased=lambda x: 1000/x.share_cost,
    )
    # Here's where the multiple tickers becomes interesting. We only want to
    # sum over rows for the same stock, not all rows. 
    .assign(
        cost_basis=lambda x: x.groupby('ticker').cost_basis.cumsum(),
        total_shares=lambda x: x.groupby('ticker').shares_purchased.cumsum(),
    )
)
my_portfolio

Add my portfolio back into the main dataframe:

df = (
    df
    .assign(
        shares_owned=my_portfolio.total_shares,
        cost_basis=my_portfolio.cost_basis,
    )
    .fillna(method='ffill')
    .assign(total_value=lambda x: x.shares_owned * x.Close)
)
alt.Chart(df.reset_index()).mark_line().encode(
    x='Date',
    y='total_value',
    color='ticker',
)

This makes it nicely clear that, while SPY grew our investment by a couple hundred over this period, DOCU trounced it, nearly returning $2k on the same investment.

…or did it? After all, these gains aren’t realized. You don’t actually have $4917.68 in your wallet, you have shares that are worth that on the market. And to get that into your wallet, you need to pay taxes. So, a somewhat less exuberant way of looking at this is to include taxes in our calculations.

Everyone has a different tax situation, but since most people reading this are probably developers, let’s assume you’re in a pretty high tax bracket (40% income tax, 20% long-term cap gains). For the first year we hold this stock, profits will be taxed at 40% if we sell. After that, we could decrease the taxes to 20%, but we only have 3 months here, so let’s say we’re still in short-term gains territory.

df = df.assign(
    # Capital gains
    gain=lambda x: x.total_value - x.cost_basis,
    taxes=lambda x: x.gain * .4,
    realizable_value=lambda x: x.total_value - x.taxes,
)

If the gain is less than zero it’s a loss and could be used to cancel out gains, but I’m not handling that here (technically I don’t have to because there aren’t any loses, but I hear that sometimes the stock market goes down).

If we subtract taxes and our original investment and pretend we sold on the last day on the chart (July 31), we get:

(df.realizable_value - df.cost_basis).groupby('ticker').last()
ticker
DOCU 1150.608974
SPY 166.252482
dtype: float64

DOCU still does amazing (although less so), SPY’s returns are starting to look fairly ho-hum. Although less dramatic, I like this way of looking at returns since it’s more reflective of what you actually end up with. And a good incentive to let stocks marinate for a year.

Show me the money: tracking returns

Last post went over building a very simple portfolio tracker to show a portfolio’s performance over time. However, it would be easy to trick myself: “My portfolio value is going up over time, I’m doing great!” But I’m also adding money to my portfolio over time, so that money shouldn’t “count” in terms of performance. I really want to be able to see the difference between having stashed the money in my mattress vs. put it into the market. We’ll figure out how to graph that in this post.

Last post, we ended with this chart:

Cash invested and portfolio value

Let’s say that we don’t care about cash invested, only profits and losses. To see that, we can subtract out the cost basis and see what the raw performance looks like:

df = df.assign(total_profit=df.total_value - df.cash_pos)
_ = df.total_profit.plot()

This gives a pretty nice breakdown of how I’m doing vs. keeping money in a mattress. However, how am I doing vs. my goals? Say my goal is to return at least 4%/year, but I can’t just draw a line from 0% January 1 to 4% December 31 because the money wasn’t invested in a lump sum. Money that’s been sitting there for a year should have yielded 4%, but money that was put in 1 month ago should yield 1/12 of that.

I think the easiest way to model this is to figure out how much our cost basis (cash_pos) should return per day, and then take a cumulative sum as the days pass to get the total expected return for any given day. (This isn’t perfectly accurate, but good enough for my purposes.)

benchmark_percent = .04  # 4%
trading_days_per_year = 253  # Market isn't open 365 days/year
daily_return = benchmark_percent / trading_days_per_year
df = df.assign(benchmark=np.cumsum(df.cash_pos * daily_return))
_ = df[['total_profit', 'benchmark']].plot()
Note that the benchmark seems to “curve” up as we add more money (although if you plot it on its own, you’ll see it’s linear segments of increasing slope as more money is invested).

We’re doing, uh, pretty good vs. the benchmark!

This is a nice way to handle benchmarking because it’ll work even if the portfolio isn’t quite such a “toy” example: if you’re investing irregular amounts at irregular intervals, this will still show you (roughly) the correct expected return over time.

Building a (very simple) portfolio tracker with pandas

All of these graphs were created in Colab.

I’ve actually never found a commercial product that does everything I want, so I figured I’d build one up in a series of blog posts. We’ll see how many I get through! πŸ§΅πŸ‘‡

First, we’ll get SPY’s stock history with pandas_datareader.

import pandas_datareader
df = pandas_datareader.data.DataReader(
    'SPY', 'yahoo', start='2020-05-01')

This returns a dataframe that looks like this (as of this writing):

Basically Axe Capital over here.

If we plot the High and Low columns (_ = df[['High', 'Low']].plot()), we get:

Now let’s throw in an actual portfolio. Let’s say I bought stock on the first of each month at some random point between the high and low:

import numpy as np
# Get the prices at the first day of each month.
first_of_the_month = df[df.index.day == 1]
my_portfolio = (
    first_of_the_month
    # Choose a random number between the day's high and low.
    .apply(lambda x: np.random.randint(x.Low, x.High), axis=1)
    .rename('cost_basis'))
my_portfolio
Date
2020-05-01 282
2020-06-01 304
2020-07-01 310
Name: cost_basis, dtype: int64

Let’s say that we have $1000 to invest each month (and our broker supports buying partial shares). So we can invest the following amounts each month:

my_portfolio = my_portfolio.assign(
    # Get the number of shares purchased this month.
    shares_purchased=1000/my_portfolio.cost_basis,
    # Get all shares I own so far.
    total_shares=lambda x: np.cumsum(x.shares_purchased),
)
my_portfolio

Finally, we want to add that back into the ticker info and plot it (let’s go with “Close” as the value for each day):

df = (
    df
    # Combine my_portfolio with the stock ticker.
    .assign(shares_owned=my_portfolio.total_shares)
    # Fill in the # of shares owned per day.
    .fillna(method='ffill')
    # Portfolio value = number of shares * price
    .assign(total_shares=lambda x: x.shares_owned * x.Close)
)
_ = df.total_value.plot()

Looks a bit less impressive than the S&P 500 graph. Let’s compare it to keeping our money in cash:

my_portfolio = my_portfolio.assign(cash_pos=range(1000, 4000, 1000))
df = df.assign(cash_pos=my_portfolio.cash_pos).fillna(method='ffill')
_ = df[['total_value', 'cash_pos']].plot()
Move over, Renaissance Capital.

We can see that our portfolio is doing better than an all-cash position almost immediately. However, there are a couple of things I’d like to add:

  • A real portfolio might have more than one stock. I want overall performance, plus broken out in different “sub-portfolios.”
  • At least for me, it’s not good enough to beat cash, I want to beat a benchmark (e.g., return at least 4%/year).

Let me know any other features you wish your broker supported in the comments and I may (or may not πŸ˜…) implement them.

On the other hand, this is a quote I’ve been thinking about a lot as I strategize about my portfolio:

We are all at a wonderful ball where the champagne sparkles in every glass and soft laughter falls upon the summer air. We know, by the rules, that at some moment, the Black Horseman will come shattering through the great terrace doors, wreaking vengeance and scattering the survivors. Those who leave early are saved, but the ball is so splendid no one wants to leave while there is still time, so that everyone keeps asking β€œWhat time is it? What time is it?” But none of the clocks have any hands.

-Adam Smith (not that one)

The stock market is going down

Not the stocks, mind you, but the market itself. There are less than 4,000 companies listed, and new companies have less and less appetite for going public. Conversely, there are over 8,000 private-equity-backed companies in the US, and growing.

There are a couple of problems with the stock market mouldering. One is that this is where most of America keeps their retirement. Common wisdom is that if you invest in a nice index fund, then your money should grow apace to the US economy. This works until the economy grows without the market.

Suppose the public market continues to dwindle. New companies stay private, so the companies on the market get older and older. Old companies have a tendency to grow more slowly and, eventually, go out-of-business. Without new blood, I’d guess the market would ossify around a few large surviving stocks.

Without the possibility of explosive growth in the public markets, any money that could go into private equity would (and is). Unfortunately, at the moment, private equity is only open to accredited investors (i.e., the already well-off) so most of America won’t be able to participate at all.

It’s important that everyone in the US:

  1. Has the opportunity to benefit from economic growth and
  2. Able to participate in a way where they won’t get fleeced.

As I understand it, the SEC doesn’t allow the middle nor lower class to invest in private companies because they don’t want people who are barely making ends meet to be hoodwinked out of all of their money.

Why is investing private equity riskier than public companies? Small companies tend to be riskier, but there are plenty of private companies that are larger than public ones. If I understand correctly, most of it is related public companies being better-regulated than private ones.

My knee-jerk reaction is, then: how can we get more companies to go public? And maybe that’s a good goal. However, why are we in the pocket of big-market here?

The goal is to allow more people to benefit from private companies, not bail out banks/NYSE/NASDAQ. So one possible solution is making the private market more liquid while adding protections for investors.

Making the private markets more liquid is easy: just let anyone buy and sell shares without being accredited and without needing the company’s approval.

However, given that something like 50% of VC firms don’t even return the capital they invest and (almost) everyone there is doing this professionally and on the up-and-up, making sure people aren’t fleeced seems more difficult. My strawman proposal is to tighten up regulations on private companies based on top-of-line revenue. Making $1M/year? Great, you need some sort of S-1-like prospectus for investors. Making $10M/year? Great, you can’t just spout off about “funding secured” on Twitter. Making $50M/year? Great, you basically have to follow all of the rules public companies do. I dunno, it’s a first draft. But I think it’s important the SEC gets on making private equity more accessible before the middle and lower classes are left in the dust.

Losing money sucks – the mathematics of loss aversion

There’s a lot of research on loss aversion: how bad people feel when they lose $50 vs. how good they feel good about gaining $50. This research is kind of taking an absolute value of emotion, positive or negative. We can represent this with emoji equations:

  • πŸ˜€ == 😫 (large-magnitude happiness or sadness)
  • πŸ™‚ == πŸ™ (small-magnitude happiness or sadness)
  • 😫 > πŸ™‚ > 😐 (ordering magnitude of emotion)

What research has shown is that, when someone loses $X and feels 😫 (large-magnitude), if they gain $X they feel πŸ™‚ (small-magnitude). Gaining money doesn’t make them as happy as losing money makes them sad!

My hypothesis is that people have an intuitive feeling about the math behind these experiments and the results make a lot of sense if you look at percentages instead of values.

For example, suppose you invest $100 and your investment goes up to $150. Holding that money in your hand, $100 is a distant memory, when you had 33% less than you currently have. ($150 – (33%*$150) = $100)

Now let’s say your investment doesn’t do well and you’re standing there with $50, feeling sad. Now you have 50% of what you started with.

My hypothesis is that loss aversion is really an intuition about the difference between 33% and 50%. My guess is that the emoji magnitude would be (roughly) equal if you gained and lost the same percentage. E.g., these would have equal magnitudes of satisfaction:

LoserWinner
33% plan$67 πŸ™$150 πŸ™‚
50% plan$50 😫$200 πŸ˜€

If we extend this out to an infinite number of plans, we can see it breaks down at the ends. I don’t think losing 80% would feel the same magnitude as making 4x, but it also feels hard to imagine. Losing 80% of a meaningful amount of money would feel terrible, but would it feel as terrible as 4x-ing my money feels good? 10x-ing? 10x feels more significant than 4x, but 4x-5x? I’m not sure I’d be materially cockier.

Anyway, I assume the ratios are a bit different for different people. But it always struck me with the loss aversion studies that most people understand that going from $1 to $2 doubles your money, but going from $2 to $3 does less.

The surprisingly complex math behind startup equity and taxes

Taxes for employees at startups are weird and can vastly change the amount you make.Β  To illustrate why, let’s take a simple example.

Suppose we have a group of early employees at a startup, we’ll call them the Unicorn Inc. Mafia.Β  They’re all fresh out of college and managed to get through it without any debt, so they each have a net worth of $0. They all join the same day and are given the same equity package: $10k in stock options with a strike prices of $1 (so, 10,000 common stock). We’ll take four members of the mafia, each with a different strategy.

Name Description Net worth
Alice Exercise very early, before price changes $0
Bob Exercise somewhat early $0
Carol Wait until liquid to exercise, wait until long-term capital gains apply $0
David Wait until liquid to exercise, sell immediately $0

From here on out, keep in mind that this could be the end of the story. The company could always fold, leaving everyone with zero or, if they’ve exercised, a negative net worth.

However, suppose the company is doing well and lets the employees know that they’re going out to raise a $40M round.Β  Alice exercises her options before the round happens.Β  This means that she has to pay for them, so she’s in the hole $10k.Β  Now if anything goes wrong, she’s out $10,000.

Once the company raises the round, the stock is worth $5/share.Β  Unfortunately, Bob’s significant other got a job across the country, so he has to find a new job. He feels like the company is going places, though, so he wants to collect his equity before he goes. He exercises his options. Because he is buying his stock for $1 and it is now worth $5, the IRS says that he just “made” $4. So he has to pay normal income taxes on that $40,000. To keep things simple, let’s say everyone’s tax rate is 25%. So now he’s paid $10k for the stock and $10k for taxes:

Name Description Exercise Income taxes Net worth
Alice Exercise very early, before price changes ($10,000) 0 ($10,000)
Bob Exercise somewhat early ($10,000) 25%*$40,000 -> ($10,000) ($20,000)
Carol Wait until liquid to exercise, wait until long-term capital gains apply $0 $0 $0
David Wait until liquid to exercise, sell immediately $0 $0 $0

So Bob’s out $20k if the company goes under (ouch!).

However, luckily for Alice & Bob, over the next several years, the company continues to grow and raise money. Finally, the company goes public for $100/share. Wow! Once the lockup period expires, everyone eventually sells (somehow it’s still exactly at the IPO price) and makes $1M. Our final shakeout looks like:

Name Description Exercise Income taxes Short-term capital gains Long-term capital gains Sell price Net worth
Alice Exercise very early, before price changes ($10,000) 0 0 0 $1,000,000 $990,000
Bob Exercise somewhat early ($10,000) 25%*$40,000 -> ($10,000) 0 20%*$990,000 -> ($198,000) $1,000,000 $782,000
Carol Wait until liquid to exercise, wait until long-term capital gains apply ($10,000) 25%*$990,000 -> ($247,500) 0 20%*$990,000 -> ($198,000) $1,000,000 $544,500
David Wait until liquid to exercise, sell immediately ($10,000) 25%*$990,000 -> ($247,500) 25%*$990,000 -> ($247,500) 0 $1,000,000 $495,000

There are, uh, a couple of different outcomes. Alice obviously has an accountant in the family: she avoided paying any taxes at all! How is this possible? First, she exercised his options before the price changed, so she didn’t have to pay any taxes on exercise. Then she held them long enough to qualify for long-term capital gains. However, she didn’t even have to pay those! It turns out that, if you own stock in a startup before it has $50M in assets, long-term capital gains up to $10M are tax-free (Google “QSBS” for details). However, Alice is also taking on more risk for longer than anyone else: most startups don’t have outcomes like this and she’d have just been out $10,000 if they had gone out of business.

Obviously there are a ton of simplifying assumptions (stock prices never change! Everyone has the same tax rate, which happens to be one that make numbers easy!). However, I wish someone had told me about all this ~10 years ago, so putting this out there in the hopes that it’ll help someone else.