packages feed

fire-number-calc-0.1.0: src/FireNumber/Calc.hs

-- | Fire number math for early retirement planning.
--
-- A fire number is annual spending divided by a withdrawal rate, and the
-- honest version of the plan also counts the gap from your current
-- balance, the years of saving it takes to close, and what happens to
-- that timeline when the spending budget grows before you retire.
module FireNumber.Calc
  ( fireNumber
  , gapToTarget
  , yearsToTarget
  , budgetCost
  ) where

-- | The balance a withdrawal rate has to support a given annual spending.
-- A zero or negative rate guards to zero.
fireNumber :: Double -> Double -> Double
fireNumber annualSpending withdrawalRate
  | withdrawalRate <= 0 = 0
  | otherwise           = annualSpending / withdrawalRate

-- | Dollars still missing between today's balance and the fire number.
-- Never negative.
gapToTarget :: Double -> Double -> Double
gapToTarget currentBalance target = max 0 (target - currentBalance)

-- | Whole years until the portfolio (balance plus annual saving,
-- compounding at 'returnRate') covers the fire number. The target itself
-- grows at 'spendingGrowth' each year, so a growing budget pushes the
-- crossing point out. Returns 0 when you are already there and positive
-- infinity when the budget outruns the savings forever.
yearsToTarget :: Double -> Double -> Double -> Double -> Double -> Double
yearsToTarget currentBalance annualSaving target returnRate spendingGrowth
  | currentBalance >= target = 0
  | otherwise                = go (1 :: Int)
  where
    go n
      | n > 600   = 1 / 0
      | bal >= tgt = fromIntegral n
      | otherwise = go (n + 1)
      where
        grow = (1 + returnRate) ** fromIntegral n
        bal  = currentBalance * grow
             + if returnRate > 0
                 then annualSaving * (grow - 1) / returnRate
                 else annualSaving * fromIntegral n
        tgt  = target * (1 + spendingGrowth) ** fromIntegral n

-- | Both timelines: years to the target with a flat budget and with one
-- that grows each year until retirement. The difference is the price of
-- the growing budget in working years.
budgetCost :: Double -> Double -> Double -> Double -> Double -> (Double, Double)
budgetCost currentBalance annualSaving target returnRate spendingGrowth =
  ( yearsToTarget currentBalance annualSaving target returnRate 0
  , yearsToTarget currentBalance annualSaving target returnRate spendingGrowth
  )