1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
|
#!/usr/bin/env runhaskell
-- | Calculations for FIRE (financial independence, retire early)
module Main where
import Data.Decimal (Decimal(..), DecimalRaw(..), roundTo, divide)
import Data.Either (fromRight)
import qualified Data.List as List
import Data.Text (pack)
import Data.Time.Calendar (Day, toGregorian)
import Data.Time.Clock (UTCTime(utctDay), getCurrentTime)
import Hledger
data Config = Config
{ age :: Decimal
}
main = do
j <- getJournal
today <- getCurrentTime >>= return . utctDay
let (thisyear, _, _) = toGregorian today
let cfg = Config { age = (fromInteger thisyear) - 1992 }
say [ "savings rate:", show $ savingsRate j today ]
say [ "target fund:", show $ targetFund j today ]
let n = whenFreedom j today
say [ "when free:", show $ n, "months"
, "(I'll be", show $ roundTo 1 $ (n/12) + (age cfg), "years old)"
]
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 $ allSavings / (- allIncome)
-- gotta flip the sign because income is negative
where
allSavings = getTotal j d query
query = List.intercalate " " $ savingsAccounts ++ ["cur:USD", "-p 'from 2019-11-01'"]
allIncome = getTotal j d "^in"
-- | 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 lastquarter 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"
|