In my last post I walked through the math of building a Kelly-optimal investment portfolio. Plug in some expected returns, volatilities, and correlations, run an optimizer, and boom you’ve got your portfolio weights. Easy, right?
Well, sort of. After writing that I got to thinking about some things I glossed over that actually matter a lot. First, there was a cool paper published recently that argues your job should be treated like a bond in your portfolio, which meaningfully changes the optimal allocation. Second, how do I know what risk-aversion factor (aka Kelly fraction) is right for me? Third, those “expected return” numbers I plugged in are doing a lot of heavy lifting. Where should they actually come from? And lastly, the correlation matrix I used assumes the relationships between assets are stable. Spoiler alert: they are not. Let’s dig in. (Same caveat as before: I’m not your investment advisor! Talk to a professional)
Your Job is a Bond
There was an article in the Wall Street Journal last week discussing a recent paper by Yale professor James Choi and friends that formalizes something that’s actually pretty intuitive once you hear it: your future paychecks are a financial asset. You don’t usually think of them that way, but from the perspective of portfolio theory, the present value of all the income you’ll earn between now and retirement is a big chunk of your total wealth. And since that income stream is relatively stable and predictable (exactly of like a bond) it should be treated as an implicit bond holding in your portfolio.
This idea goes back to Robert Merton in the 1970s, but it was new to me, and the new paper makes it more usable. Computing the return on your future labor income is tricky. You can’t just discount future cash flows at the risk-free rate. The paper’s key contribution is providing regression-based approximations for the right discount rates as a function of things you actually know about yourself: your age, risk aversion, the risk-free rate, equity premium, how risky your income is, and your expected Social Security replacement rate.
What does all this mean qualitatively? Young people with lots of human capital relative to financial capital should have very aggressive portfolios, often 100% stocks. The paper finds that a 45-year-old with a high risk aversion should still hold 100% stocks until her financial wealth exceeds about 1.6x her discounted lifetime income. This is way more aggressive than the popular “100 minus your age” rule, which would say 55% stocks at 45.
One caveat: if your income is highly correlated with the stock market (say you work in finance or tech and your compensation is heavily equity-based), your human capital is less bond-like and more stock-like. In that case, the right thing to do is adjust the correlations, making your financial portfolio more conservative. For the average person though, the correlation between labor income and stock returns is quite low.
Wait, How Do I Know My Risk Aversion?
All the math we’ve talked about requires a number for your coefficient of relative risk aversion (CRRA or ). This is probably the most important input to the whole calculation, and also the one most people have no idea how to estimate. So here’s a quick-and-dirty way to figure it out.
The thought experiment is simple: imagine you’re offered a bet where you have a 50/50 chance of either increasing your total lifetime wealth by some percentage, or decreasing it by the same percentage. What’s the biggest downside percentage you’d accept? Your answer maps to a risk aversion coefficient. (This should sound a lot like Shannon’s Demon from my Kelly post)
Here’s a rough mapping, adapted from the literature on CRRA estimation:
| “I’d accept a 50/50 chance of losing X% of my wealth for an equal chance of gaining X%” | Approximate Risk Aversion |
|---|---|
| 40% or more (very aggressive. you’d tolerate losing nearly half) | 2 |
| 30% | 3-4 |
| 20% | 5-6 |
| 15% | 7-8 |
| 10% or less (very conservative. even small losses make you queasy) | 9-10 |
A few notes on this. First, be honest with yourself. Most people think they’re more risk-tolerant than they actually are when markets are going up. The real test is how you’d feel watching your portfolio drop 30% in a month, which actually happened in March 2020. If your answer is “I’d panic and drink heavily,” you’re probably a CRRA of 8-10 regardless of what you say in a calm moment. I used 2-4 in the previous post, but having worked in trading for many years, I’m probably more desensitized to losses in positive expected-value situations than most. The Choi paper uses values between 4 and 10, noting that at the optimal equity allocation is already almost always 100%, so for many practical purposes you don’t need to go below 4. It’s also worth noting, your risk aversion isn’t necessarily fixed. Research shows it can shift with age, wealth changes, and macroeconomic conditions, so don’t feel like you need to keep this constant your whole life.
For the code below, I’ll default to , but definitely tailor it to your situation.
Where Do Expected Returns Come From?
In the last post I used historical average returns as the expected return for each asset class. This is the most common approach, but it has some real problems. As the saying goes, past performance is not indicative of future results.
There are roughly three families of approaches people use to estimate expected returns, and they give pretty different answers:
1. Historical averages. Just look at what the asset returned over the last N years and assume the future will look similar. The advantage is simplicity. The problem is that the answer changes a lot depending on your lookback window, and there’s a strong argument that current conditions (like today’s high stock valuations) should matter. US stocks returned about 10% annualized over the last century, but firms like Vanguard currently forecast only 3.9–5.9% over the next decade because current valuations are stretched.
2. Fundamental decomposition (the Bogle method). Jack Bogle, the Vanguard founder, proposed breaking stock returns into three components: starting dividend yield + earnings growth + change in the P/E multiple. The first two are somewhat estimable from current data and long-run averages. The third, how much investors will be willing to pay for a dollar of earnings in the future, is where all the uncertainty lives. This is more art than science, but at least it forces you to be explicit about your assumptions. Most institutional investors (J.P. Morgan, Schwab, etc.) use some variant of this approach for their published 10-year forecasts, which currently land around 5–7% for US equities.
3. Valuation-based models (CAPE and friends). Robert Shiller’s CAPE ratio (cyclically adjusted price-to-earnings) has historically been a decent predictor of 10-year stock returns. When CAPE is high, future returns tend to be low, and vice versa. A recent paper by Murphy et al. found that the “earnings yield gap”, i.e., the difference between the stock market’s earnings yield (1/CAPE) and the real yield on TIPS, has been the best performing CAPE-based predictor. This has intuitive appeal: it compares what stocks are earning relative to what you can get risk-free.
For bonds, the good news is that forecasting is much easier. The starting yield of a bond is actually a very good predictor of its return over the life of the bond. So if 10-year Treasuries yield 4.1%, your expected 10-year return from holding them is… roughly 4.1%. This is why you see much less disagreement among forecasters about bond returns than stock returns.
The uncomfortable truth is that which return estimate you use can dramatically change your optimal portfolio. If you believe stocks will return 10% (historical average), Kelly says to bet big on equities. If you believe Vanguard’s 4–5% forecast, the optimal portfolio looks a lot more balanced. My suggestion: don’t just pick one number. Run the optimizer with a few different scenarios and see how sensitive the output is. If small changes in expected returns flip your portfolio upside down, that’s a sign you should be more diversified (or use a smaller Kelly fraction) than the point estimate suggests.
Correlations Are Lies!
My friend Pratik brought this up in the comments of the last post and it’s a great point. The entire premise of portfolio diversification is that when one asset zigs, another zags. We measure this with correlations, and in my last post I used a single correlation matrix estimated from historical data. The problem? Correlations are not stable. They change over time, and they have a nasty habit of spiking exactly when you need diversification the most.
The most painful example is the stock-bond relationship. From the late 1990s through 2020, US stocks and Treasuries were negatively correlated: when stocks fell, bonds rose, providing a beautiful natural hedge. This was the longest negative stock-bond correlation regime ever observed in any country, and an entire generation of investors grew up thinking this was normal. Then 2022 happened: the Fed hiked rates aggressively, and both stocks AND bonds fell simultaneously, producing double-digit losses in both asset classes.
Why did this happen? Research suggests the stock-bond correlation is largely a function of inflation. BNP Paribas found that when core CPI is below roughly 2.5%, bonds tend to diversify equity risk (negative correlation). When inflation runs above 2.5%, the correlation flips positive because rising rates hurt both stocks and bonds. So the correlation matrix you estimate from the 2000–2020 era (a period of unusually low and stable inflation) may tell you nothing useful about the next decade if inflation stays elevated.
This pattern extends beyond stocks and bonds. A Two Sigma analysis found that during the 2008 financial crisis, pairwise equity correlations spiked from about 40% to nearly 70% and stayed elevated for over five years. Commodities, currencies, and sovereign bonds all saw similar correlation spikes. When everyone panics, everything moves together.
So what can you actually do about this? A few thoughts:
Use longer lookback windows. Assuming you have more than a decade of life left, there’s a good chance you can ride out temporary correlation spikes. Including data from multiple market environments using a longer time window to compute the correlations gives a more robust estimate. Just don’t go too far back; the world of the 1970s is not today’s world.
Stress test with crisis correlations. Run the optimizer not just with your base-case correlation matrix, but also with a “crisis” matrix where correlations are all closer to 1. If your portfolio blows up under the stress scenario, modulate the allocations until you get a portfolio that behaves tolerably in all scenarios.
Think about why correlations exist. Instead of treating them as statistical black boxes, ask what’s driving them. If the stock-bond correlation is driven by inflation, and you think inflation will stay elevated, maybe reduce your reliance on bonds as a diversifier and look at assets like gold or commodities that have tended to hold up better in inflationary environments.
Wrapping Up
I’ve updated the code from the last post to incorporate the human capital adjustment from the Choi et al. paper. You give it your age, income, expected retirement age, etc. and it computes your human capital, then runs the Kelly optimizer treating that an implicit bond allocation. As before, you can paste this into an online Python interpreter to run it.
If I were updating my portfolio today (which I am) here’s what I’d change from the previous post:
- I’d compute my human capital using the Choi discount rates and add it to my effective asset allocation. For most people still in their working years, this pushes you toward holding more stocks in your financial portfolio than you might think.
- I’d use a range of expected return estimates (not just historical averages) and check how sensitive my portfolio is to those assumptions. At a minimum, I’d cross-reference with the fundamental decomposition approach and the institutional forecasts from Vanguard, Schwab, or J.P. Morgan.
- I’d stress test my correlation assumptions, especially the stock-bond correlation, and consider what happens to my portfolio if we stay in a higher-inflation world where that historical negative correlation doesn’t hold. (This would likely lead to adding commodities as a new asset class to the portfolio)
None of this changes the fundamental framework from the last post. Kelly-optimal allocation with constraints is still the right structure. But the inputs matter enormously, and being thoughtful about them is the difference between a portfolio that looks good on paper and one that actually works in the real world. Happy investing!
import numpy as npfrom scipy.optimize import minimizedef compute_human_capital( age: int, annual_income: float, retirement_age: int = 66, max_age: int = 100, retirement_replacement_rate: float = 0.40, risk_aversion: float = 3.0, log_equity_premium: float = 0.02, log_risk_free_rate: float = 0.02, sigma_perm: float = 0.130, # college grad default sigma_temp: float = 0.242, # college grad default) -> dict: """ Compute human capital using discount rate approximations from Choi, Liu, and Liu (2025) "Practical Finance". """ sigma_r = 0.185 # stock return volatility (log) # Merton optimal equity share (for reference only) alpha_star = (log_equity_premium + 0.5 * sigma_r**2) / ( risk_aversion * sigma_r**2 ) alpha_star = max(0.0, min(1.0, alpha_star)) human_capital = 0.0 cumulative_gross_discount = 1.0 for future_age in range(age + 1, max_age + 1): discount_age = future_age - 1 if future_age <= retirement_age: # Working life discount rate (Table 1, column 3) r_h = ( 0.087 * (risk_aversion / 10) - 0.267 * log_equity_premium + 1.132 * log_risk_free_rate + 4.332 * sigma_perm**2 + 0.028 * sigma_temp**2 + 0.010 * retirement_replacement_rate - 0.149 * (discount_age / 100) + 0.142 * (discount_age / 100) ** 2 - 0.020 ) expected_income = annual_income else: # Retirement discount rate (Table 2, column 3) r_h = ( 0.0003 * (risk_aversion / 10) - 0.217 * log_equity_premium + 0.893 * log_risk_free_rate + 0.476 * (discount_age / 100) - 0.295 * (discount_age / 100) ** 2 - 0.166 ) expected_income = annual_income * retirement_replacement_rate cumulative_gross_discount *= 1 + r_h human_capital += expected_income / cumulative_gross_discount return {"human_capital": human_capital, "alpha_star": alpha_star}def build_cov_from_vol_corr(vol, corr): vol = np.asarray(vol, float).reshape(-1) C = 0.5 * (np.asarray(corr, float) + np.asarray(corr, float).T) D = np.diag(vol) Sigma = D @ C @ D return 0.5 * (Sigma + Sigma.T)def kelly_with_human_capital( assets, mu, vol, corr, *, treasury_name="Treasuries", w_max=0.50, age=35, annual_income=100_000, financial_wealth=500_000, retirement_age=66, retirement_replacement_rate=0.40, risk_aversion=3.0, log_equity_premium=0.02, log_risk_free_rate=0.02, sigma_perm=0.130, sigma_temp=0.242, hc_vol=0.05, hc_corr_equities=0.0, hc_corr_ig_bonds=0.3, hc_corr_treasuries=0.3, hc_corr_gold=0.0, effective_years=5.0, uncertainty_aversion=1.0,): """ Kelly-optimal portfolio with human capital as a FIXED asset. We maximize CRRA expected utility over total wealth: max mu_e' w - (gamma / 2) * w' Sigma w This is equivalent to fractional Kelly with f = 1/gamma. Full Kelly (log utility, max growth) is gamma = 1. Human capital enters as an additional asset the investor is forced to hold. The optimizer allocates financial wealth across tradeable assets, accounting for the risk already in the fixed HC position. """ mu = np.asarray(mu, float).reshape(-1) vol = np.asarray(vol, float).reshape(-1) n = len(assets) # Step 1: Compute human capital hc = compute_human_capital( age=age, annual_income=annual_income, retirement_age=retirement_age, retirement_replacement_rate=retirement_replacement_rate, risk_aversion=risk_aversion, log_equity_premium=log_equity_premium, log_risk_free_rate=log_risk_free_rate, sigma_perm=sigma_perm, sigma_temp=sigma_temp, ) human_capital = hc["human_capital"] total_wealth = financial_wealth + human_capital hc_frac = human_capital / total_wealth fin_frac = financial_wealth / total_wealth # Step 2: Build full (n+1) covariance matrix including HC treasury_idx = assets.index(treasury_name) hc_mu = mu[treasury_idx] # HC "return" ~ risk-free rate hc_corrs = np.array([hc_corr_equities, hc_corr_ig_bonds, hc_corr_treasuries, hc_corr_gold]) full_corr = np.eye(n + 1) full_corr[:n, :n] = np.asarray(corr, float) full_corr[:n, n] = hc_corrs full_corr[n, :n] = hc_corrs full_corr = 0.5 * (full_corr + full_corr.T) full_vol = np.append(vol, hc_vol) full_mu = np.append(mu, hc_mu) full_Sigma = build_cov_from_vol_corr(full_vol, full_corr) # Step 3: Optimize over total-wealth weights rf = float(mu[treasury_idx]) mu_e = full_mu - rf # Mean uncertainty penalty (Garlappi, Uppal, Wang 2007) se = full_vol / np.sqrt(effective_years) Sigma_mu = np.diag(se**2) Sigma_mu[n, :] = 0.0 # no estimation uncertainty for HC Sigma_mu[:, n] = 0.0 # gamma is the Kelly scaling. gamma = float(risk_aversion) Q = gamma * (full_Sigma + uncertainty_aversion * Sigma_mu) Q = 0.5 * (Q + Q.T) # Optimize tradeable weights; HC weight is fixed at hc_frac def obj(w_trade): w_full = np.append(w_trade, hc_frac) return -(mu_e @ w_full - 0.5 * (w_full @ Q @ w_full)) def grad_obj(w_trade): w_full = np.append(w_trade, hc_frac) return (-mu_e + Q @ w_full)[:n] max_tw = w_max * fin_frac cons = [{"type": "eq", "fun": lambda w: np.sum(w) - fin_frac, "jac": lambda w: np.ones(n)}] bounds = [(0.0, max_tw) for _ in range(n)] w0 = np.full(n, fin_frac / n) w0 = np.clip(w0, 0.0, max_tw) w0 *= fin_frac / w0.sum() res = minimize(obj, w0, method="SLSQP", jac=grad_obj, bounds=bounds, constraints=cons, options={"ftol": 1e-12, "maxiter": 50_000}) w_total = np.clip(res.x, 0.0, max_tw) w_total *= fin_frac / w_total.sum() w_financial = w_total / fin_frac return { "assets": assets, "w_financial": w_financial, "w_total": w_total, "hc_frac_total": hc_frac, "human_capital": human_capital, "financial_wealth": financial_wealth, "total_wealth": total_wealth, "hc_ratio": human_capital / financial_wealth, "alpha_star_merton": hc["alpha_star"], "risk_aversion": risk_aversion, "age": age, }if __name__ == "__main__": # --- EDIT THESE TO MATCH YOUR SITUATION --- AGE = 35 ANNUAL_INCOME = 100_000 FINANCIAL_WEALTH = 500_000 RETIREMENT_AGE = 66 SS_REPLACEMENT_RATE = 0.40 # ~40% high earners, ~80% low RISK_AVERSION = 3 # see table in blog post LOG_RISK_FREE_RATE = 0.02 LOG_EQUITY_PREMIUM = 0.02 # conservative; historical ~6.5% # HC risk profile: low vol, ~zero equity correlation. # If you work in finance/tech with lots of equity comp, # increase hc_corr_equities toward 0.2-0.3. HC_VOL = 0.05 HC_CORR_EQUITIES = 0.0 HC_CORR_IG_BONDS = 0.3 HC_CORR_TREASURIES = 0.3 HC_CORR_GOLD = 0.0 # Asset class assumptions (same as Part 1) assets = ["Equities", "IG Bonds", "Treasuries", "Gold"] mu = np.array([0.097, 0.05, 0.041, 0.068]) vol = np.array([0.18, 0.06, 0.056, 0.18]) corr = np.array([ [ 1.0, 0.2, -0.1, 0.0], [ 0.2, 1.0, 0.85, 0.1], [-0.1, 0.85, 1.0, 0.2], [ 0.0, 0.1, 0.2, 1.0], ]) out = kelly_with_human_capital( assets=assets, mu=mu, vol=vol, corr=corr, treasury_name="Treasuries", w_max=0.50, age=AGE, annual_income=ANNUAL_INCOME, financial_wealth=FINANCIAL_WEALTH, retirement_age=RETIREMENT_AGE, retirement_replacement_rate=SS_REPLACEMENT_RATE, risk_aversion=RISK_AVERSION, log_equity_premium=LOG_EQUITY_PREMIUM, log_risk_free_rate=LOG_RISK_FREE_RATE, hc_vol=HC_VOL, hc_corr_equities=HC_CORR_EQUITIES, hc_corr_ig_bonds=HC_CORR_IG_BONDS, hc_corr_treasuries=HC_CORR_TREASURIES, hc_corr_gold=HC_CORR_GOLD, ) print("=" * 55) print("PORTFOLIO ALLOCATION WITH HUMAN CAPITAL") print("=" * 55) print(f" Age: {out['age']}") print(f" Risk aversion (gamma):{out['risk_aversion']}") print(f" Kelly fraction (1/g): {1/out['risk_aversion']:.2f}") print(f" Annual income: ${ANNUAL_INCOME:,.0f}") print(f" Financial wealth: ${out['financial_wealth']:,.0f}") print(f" Human capital (H): ${out['human_capital']:,.0f}") print(f" Total wealth (W+H): ${out['total_wealth']:,.0f}") print(f" H/W ratio: {out['hc_ratio']:.2f}") print(f" Merton alpha* (no HC):{100*out['alpha_star_merton']:.1f}%") print() print("Your total wealth allocation:") for a, wt in zip(out["assets"], out["w_total"]): print(f" {a:14s}: {100*wt:6.2f}%") print(f" {'Human Capital':14s}: " f"{100*out['hc_frac_total']:6.2f}% (fixed)") print() print("Your portfolio allocation:") for a, wf in zip(out["assets"], out["w_financial"]): dollar = wf * out["financial_wealth"] print(f" {a:14s}: {100*wf:6.2f}% (${dollar:,.0f})")
Leave a comment