47 lines
1.3 KiB
Python
47 lines
1.3 KiB
Python
|
|
"""Glide-path functions — stock-allocation as a function of years
|
||
|
|
since retirement.
|
||
|
|
|
||
|
|
Pfau & Kitces (2014) showed that *rising* equity glide paths
|
||
|
|
(starting low and rising) reduce sequence-of-returns risk in the
|
||
|
|
critical first decade of retirement. We default to that, with a
|
||
|
|
classic static 60/40 also available.
|
||
|
|
|
||
|
|
Each glide returns a fraction in [0, 1] for stock allocation — the
|
||
|
|
remainder is bonds.
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from collections.abc import Callable
|
||
|
|
|
||
|
|
GlideFn = Callable[[int], float]
|
||
|
|
|
||
|
|
|
||
|
|
def rising_equity(start: float = 0.30, end: float = 0.70, ramp_years: int = 15) -> GlideFn:
|
||
|
|
"""Linear interpolation from `start` to `end` over `ramp_years`,
|
||
|
|
then constant at `end`."""
|
||
|
|
span = end - start
|
||
|
|
|
||
|
|
def fn(year: int) -> float:
|
||
|
|
if year >= ramp_years:
|
||
|
|
return end
|
||
|
|
return start + span * (year / ramp_years)
|
||
|
|
|
||
|
|
return fn
|
||
|
|
|
||
|
|
|
||
|
|
def static(allocation: float) -> GlideFn:
|
||
|
|
"""Constant allocation, e.g. 60/40 = static(0.60)."""
|
||
|
|
return lambda _year: allocation
|
||
|
|
|
||
|
|
|
||
|
|
GLIDE_PATHS: dict[str, GlideFn] = {
|
||
|
|
"rising": rising_equity(),
|
||
|
|
"static_60_40": static(0.60),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def get(name: str) -> GlideFn:
|
||
|
|
if name not in GLIDE_PATHS:
|
||
|
|
raise KeyError(f"Unknown glide path: {name!r}. Known: {sorted(GLIDE_PATHS)}")
|
||
|
|
return GLIDE_PATHS[name]
|