diff options
author | Ben Sima <ben@bsima.me> | 2019-12-29 13:37:35 -0800 |
---|---|---|
committer | Ben Sima <ben@bsima.me> | 2019-12-29 13:37:35 -0800 |
commit | 7f9288091cf08d51be400d0e0583971484c1cc62 (patch) | |
tree | 445fe2c129cb264e7172b2fbad2ac6aa757dc5c3 | |
parent | 3ca10220c0816576331a1356b92f4f2081110c4e (diff) |
add hledger-fire
-rwxr-xr-x | hledger-fire.hs | 76 |
1 files changed, 76 insertions, 0 deletions
diff --git a/hledger-fire.hs b/hledger-fire.hs new file mode 100755 index 0000000..6c7c7c9 --- /dev/null +++ b/hledger-fire.hs @@ -0,0 +1,76 @@ +#!/usr/bin/env runhaskell +-- | Calculations for FIRE (financial independence, retire early) +module Main where +import Hledger +import Data.Decimal (DecimalRaw, roundTo) +import Data.Either (fromRight) +import Data.Time.Calendar (Day) +import Data.Time.Clock (UTCTime(utctDay), getCurrentTime) +import Data.Text (pack) + +main = do + j <- getJournal + today <- getCurrentTime >>= return . utctDay + say ["savings rate:", show $ savingsRate j today] + say ["target fund:", show $ targetFund j today] + say ["when free:", show $ whenFreedom j today] + +say = putStrLn . unwords + +getJournal :: IO Journal +getJournal = do + jp <- defaultJournalPath + let opts = definputopts { auto_ = True } + ej <- readJournalFile opts jp + return $ fromRight undefined ej + +-- | Helper for getting the total out of a balance report. +getTotal :: Journal -> Day -> String -> Quantity +getTotal j d q = head $ map aquantity $ total + where + opts = defreportopts { balancetype_ = CumulativeChange } + (query, _) = parseQuery d $ pack q + (_, (Mixed total)) = balanceReport opts query j + +-- | These are the accounts that I consider a part of my savings and not my +-- cash-spending accounts. +savingsAccounts :: [String] +savingsAccounts = + [ "as:me:save", "as:me:vest" ] + +-- | Savings rate is a FIRE staple. Basically take your savings and divide it by +-- your income on a monthly basis. +-- +savingsRate :: Journal -> Day -> Quantity +savingsRate j d = roundTo 2 $ monthlySavings / monthlyIncome + where + monthlySavings = sum $ map (getTotal j d) $ map appendMonthly savingsAccounts + appendMonthly s = s ++ " --monthly" + monthlyIncome = getTotal j d "^in --monthly" + +-- | The target fund is simply 25x your annual expenditure. +-- +-- This is going to be incomplete until I have a full year of +-- expenses.. currently, I just use my most recent quarter times 4 as a proxy +-- for the yearly expenses. +-- +-- Assumptions: 4% withdrawal rate, 3-5% return on investments. +-- +targetFund :: Journal -> Day -> Quantity +targetFund j d = 25 * yearlyExpenses + where + yearlyExpenses = 4 * quarterlyExpenses + quarterlyExpenses = sum $ map aquantity $ total + (query, _) = parseQuery d $ pack "^ex -p thisquarter --forecast cur:USD" + (_, (Mixed total)) = balanceReport opts query j + opts = defreportopts + +-- | How long until I can live off of my savings and investment returns? +-- +-- Return integer is number of months until I'm free. +-- +whenFreedom :: Journal -> Day -> Quantity +whenFreedom j d = roundTo 1 $ targetFund j d / monthlySavings + where + monthlySavings = sum $ map (getTotal j d) $ map appendMonthly savingsAccounts + appendMonthly s = s ++ " --monthly" |