hledger-interest 1.6.7 → 1.7.0
raw patch · 8 files changed
+354/−120 lines, 8 filesdep −Cabalsetup-changed
Dependencies removed: Cabal
Files
- CHANGELOG.md +62/−0
- Hledger/Interest.hs +50/−8
- Hledger/Interest/DayCountConvention.hs +13/−5
- Hledger/Interest/Rate.hs +121/−59
- Main.hs +12/−4
- README.md +54/−3
- Setup.hs +0/−2
- hledger-interest.cabal +42/−39
+ CHANGELOG.md view
@@ -0,0 +1,62 @@+# Revision history for hledger-interest++## 1.7.0 (2026-11-11)++### Changes that affect computed interest++Several bugs have been fixed that made hledger-interest compute wrong+amounts. Re-running it on existing journals may produce different+results in the following cases:++- `--bgb288` applied 5.37% to every date after 2011-06-30, because a+ stray entry in the table of base rates shadowed all later rates. The+ table has been fixed, checked against the rates published by the+ Deutsche Bundesbank, and extended up to 2026-12-31. Dates after the+ end of the table now produce a clear error message instead of a+ crash.+- `--annual-schedule` used the rate in effect at the start of a period+ until the next transaction or year end, ignoring any rate changes in+ between, and a new rate took effect only on the day after its start+ date. Rates now take effect exactly on their start date+ ([#20](https://github.com/peti/hledger-interest/issues/20),+ [#23](https://github.com/peti/hledger-interest/pull/23)). Dates+ before the first entry of the schedule earn no interest instead of+ crashing.+- A transaction on the first day of a rate period, e.g. on January 1st+ with `--annual`, earned one day of interest too many+ ([#21](https://github.com/peti/hledger-interest/issues/21)).+- `--30E-360isda` counted the days of a period that starts on the 31st+ of a month wrongly, e.g. 30 instead of 15 days from 2023-01-31 to+ 2023-02-15 ([#28](https://github.com/peti/hledger-interest/issues/28)).+- Interest on balances without decimal places is now shown with at+ least two decimal places instead of being rounded to whole units.++### New features++- The new `--post-on-transactions` flag posts interest only when the+ balance changes (and on the final day with `--today`), rather than at+ the end of every year and rate period. Interest is still computed+ separately for every year and rate, but no longer compounds at the end+ of each year+ ([#16](https://github.com/peti/hledger-interest/issues/16),+ [#26](https://github.com/peti/hledger-interest/issues/26)).+- Without `--file`, hledger-interest now reads `$LEDGER_FILE` or+ `~/.hledger.journal` like hledger does, instead of silently reading+ nothing ([#24](https://github.com/peti/hledger-interest/issues/24)).+ Errors while reading the journal, such as a missing file, are+ reported without the "Uncaught exception" noise.++### Other changes++- Descriptions of interest on multi-currency balances no longer contain+ line breaks ([#11](https://github.com/peti/hledger-interest/issues/11),+ [#13](https://github.com/peti/hledger-interest/pull/13)).+- The README explains how interest is computed with each day counting+ convention, and that `--act` corresponds to Actual/Actual ISDA+ ([#25](https://github.com/peti/hledger-interest/issues/25)).+- Support hledger-lib up to version 1.52.x and GHC 9.6 to 9.14.+- Drop the obsolete dependency on Cabal and the `Setup.hs` file.++## 1.6.7 and earlier++See the [git history](https://github.com/peti/hledger-interest/commits/master).
Hledger/Interest.hs view
@@ -33,17 +33,24 @@ , targetAccount :: AccountName , dayCountConvention :: DayCountConvention , interestRate :: Rate+ , postOnTransactions :: Bool+ -- ^ Post accrued interest only on transaction dates rather than at the+ -- end of every rate period (e.g. at every year end with 'perAnno'). } data InterestState = InterestState { balancedUntil :: Day , balance :: MixedAmount+ , accruedDays :: Integer -- ^ interest accrued but not posted yet+ , accruedInterest :: MixedAmount } nullInterestState :: InterestState nullInterestState = InterestState { balancedUntil = nulldate , balance = nullmixedamt+ , accruedDays = 0+ , accruedInterest = nullmixedamt } processTransaction :: Transaction -> Computer ()@@ -56,8 +63,12 @@ bal <- gets balance modify (\st -> st { balance = bal + pamount p }) +-- | Compute and post all interest up to and including the given day. computeInterest :: Day -> Computer ()-computeInterest day = do+computeInterest day = accrueInterest day >> postInterest day Nothing++accrueInterest :: Day -> Computer ()+accrueInterest day = do from <- gets balancedUntil bal <- gets balance rate <- asks interestRate@@ -67,30 +78,51 @@ modify (\st -> st { balancedUntil = newFrom }) when (to >= from && not (mixedAmountIsZero bal)) $ do diff <- asks dayCountConvention- t <- mkTrans to ((from `diff` to) + 1) ratePerAnno+ yearDays <- daysInYear to+ let days = (from `diff` to) + 1+ interest = mixed [ a { aquantity = (aquantity a * ratePerAnno) / fromInteger yearDays * fromInteger days } | a <- amounts bal ]+ modify (\st -> st { accruedDays = accruedDays st + days+ , accruedInterest = accruedInterest st + interest+ })+ onTransactionsOnly <- asks postOnTransactions+ unless onTransactionsOnly (postInterest to (Just ratePerAnno))+ when (newFrom <= day) (accrueInterest day)++-- | Post all accrued interest as one transaction on the given day. The+-- description mentions the rate only if one is given, i.e. if the interest+-- was accrued over a single rate period.+postInterest :: Day -> Maybe Decimal -> Computer ()+postInterest day ratePerAnno = do+ st <- get+ when (accruedDays st > 0) $ do+ put st { accruedDays = 0, accruedInterest = nullmixedamt }+ t <- mkTrans day (accruedDays st) (accruedInterest st) ratePerAnno tell [t] processTransaction t- when (newFrom < day) (computeInterest day) daysInYear :: Day -> Computer Integer daysInYear now = asks dayCountConvention >>= \diff -> return (day1 `diff` day2) where day1 = fromGregorian (fst (toOrdinalDate now)) 1 1 day2 = fromGregorian (succ (fst (toOrdinalDate now))) 1 1 -mkTrans :: Day -> Integer -> Decimal -> Computer Transaction-mkTrans day days ratePerAnno = do+-- | Create the transaction for the given interest, which was accrued over+-- the given number of days.+mkTrans :: Day -> Integer -> MixedAmount -> Maybe Decimal -> Computer Transaction+mkTrans day days interest ratePerAnno = do bal <- gets balance srcAcc <- asks sourceAccount targetAcc <- asks targetAccount- perDayScalar <- daysInYear day let t = nulltransaction { tdate = day- , tdescription = T.pack $ showPercent ratePerAnno ++ " interest for " ++ showMixedAmount bal ++ " over " ++ show days ++ " days"+ , tdescription = T.pack $ maybe "" (\r -> showPercent r ++ " ") ratePerAnno ++ "interest for " ++ showMixedAmountOneLine bal ++ " over " ++ show days ++ " days" , tpostings = [pTarget,pSource] }+ -- balance might have been an exact amount, in which case its precision might+ -- be set to 0 decimal places. We ensure that interest is displayed with at least+ -- two decimal points pTarget = nullposting { paccount = targetAcc- , pamount = mixed [ a { aquantity = (aquantity a * ratePerAnno) / fromInteger perDayScalar * fromInteger days } | a <- amounts bal ]+ , pamount = mixed [ setMinPrecision 2 a | a <- amounts interest ] , ptype = RegularPosting , ptransaction = Just t }@@ -101,6 +133,16 @@ , ptransaction = Just t } return t+ where+ -- Copy of amountSetPrecisionMin (with amountDisplayPrecision inlined) from+ -- hledger-lib >= 1.32, which older hledger-lib versions (still allowed by our+ -- lower bound) do not provide+ setMinPrecision minp a = amountSetPrecision (Precision $ max minp displayPrecision) a+ where+ displayPrecision =+ case asprecision $ astyle a of+ Precision n -> n+ NaturalPrecision -> decimalPlaces (normalizeDecimal (aquantity a)) showPercent :: Decimal -> String showPercent r = shows (r * 100) "%"
Hledger/Interest/DayCountConvention.hs view
@@ -33,13 +33,21 @@ (y1,m1,d1) = toGregorian date1 (y2,m2,d2) = toGregorian date2 --- This variant additionally normalizes end-of-months to 30, i.e. there--- are 30 days between 2003-02-28 and 2003-03-31.-+-- | This variant additionally normalizes end-of-months to 30, i.e. there+-- are 30 days between 2003-02-28 and 2003-03-31. (Strict 30E/360 ISDA+-- exempts a February month-end that is the maturity date; we have no+-- notion of maturity, so that exception is not applied.)+--+-- >>> diff30E_360isda (fromGregorian 2003 2 28) (fromGregorian 2003 3 31)+-- 30+-- >>> diff30E_360isda (fromGregorian 2023 1 31) (fromGregorian 2023 2 15)+-- 15+-- >>> diff30E_360isda (fromGregorian 2023 1 15) (fromGregorian 2023 2 28)+-- 45 diff30E_360isda :: DayCountConvention diff30E_360isda date1 date2 = assert (date1 <= date2) $ mkDiff30_360 (y1, m1, d1') (y2, m2, d2') where (y1,m1,d1) = toGregorian date1 (y2,m2,d2) = toGregorian date2- d1' = if d1 > 30 || d1 == gregorianMonthLength y1 m1 then 30 else d1- d2' = if d1 > 30 || d2 == gregorianMonthLength y2 m2 then 30 else d2+ d1' = if d1 == gregorianMonthLength y1 m1 then 30 else d1+ d2' = if d2 == gregorianMonthLength y2 m2 then 30 else d2
Hledger/Interest/Rate.hs view
@@ -3,7 +3,8 @@ import Data.Time.Calendar import Data.Time.Calendar.OrdinalDate import Data.Decimal-import Data.List (sortOn)+import Data.List (sortOn, find)+import Data.Maybe (fromMaybe) type Rate = Day -> (Day,Decimal) @@ -14,9 +15,28 @@ perAnno rate date = (yearEnd date, rate) perAnnoSchedule :: [(Day,Decimal)] -> Rate-perAnnoSchedule schedule date = (yearEnd date, effectiveRate)+perAnnoSchedule schedule date = (effectiveUntil, effectiveRate) where- (_, effectiveRate) = last $ takeWhile (\(fromDate, _) -> fromDate<date) sortedSchedule+ effectiveUntil =+ -- make sure that end date we report does not cross the year boundary+ min (yearEnd date) endOfPeriod+ (endOfPeriod, effectiveRate) =+ case find (\(fromDate, toDate, _) -> fromDate<=date && date<=toDate) fromToSchedule of+ -- date precedes the schedule: no interest until the first rate takes effect+ Nothing -> (beforeSchedule, 0)+ Just (_,toDate,rate) -> (toDate, rate)+ beforeSchedule =+ case sortedSchedule of+ (firstDate, _):_ -> pred firstDate+ [] -> yearEnd date+ fromToSchedule =+ -- Convert the list of (startDate,rate) to (startDate,endDate,rate) intervals, where+ -- the end date of the interval is the day before the start of the next interval,+ -- and the last interval goes to infinity+ zipWith+ (\(from, rate) (to, _) -> (from, pred to, rate))+ sortedSchedule+ (drop 1 sortedSchedule ++ [(day 999999 1 1,0)]) sortedSchedule = sortOn fst schedule day :: Integer -> Int -> Int -> Day@@ -25,69 +45,113 @@ yearEnd :: Day -> Day yearEnd date = day (fst (toOrdinalDate date)) 12 31 +-- | Interest according to German BGB §288: the base rate published by the+-- Deutsche Bundesbank (BGB §247) plus 5 percentage points.+--+-- >>> bgb288 (day 2011 08 15)+-- (2011-12-31,0.0537)+-- >>> bgb288 (day 2018 07 01)+-- (2018-12-31,0.0412)+-- >>> bgb288 (day 2023 06 01)+-- (2023-06-30,0.0662)+-- >>> bgb288 (day 2026 07 01)+-- (2026-12-31,0.0652) bgb288 :: Rate bgb288 = basiszins (5/100) basiszins :: Decimal -> Rate-basiszins r date = (to, r + p)+basiszins r date =+ case tableRate basiszinsPeriods date of+ Just (to, p) -> (to, r + p)+ Nothing -> error ("hledger-interest: the BGB base rate after " ++ show lastEnd +++ " is not known yet; please update basiszinsTable in Hledger/Interest/Rate.hs") where- (_,to,p) = head (dropWhile (\(_,to',_) -> to' < date) basiszinsTable)+ lastEnd = case reverse basiszinsPeriods of+ (_,to,_) : _ -> to+ [] -> error "basiszinsTable is empty" -basiszinsTable :: [(Day, Day, Decimal)]+-- | Find the period of a (from, to, rate) table that has not ended yet at+-- the given date, and return its end date and rate.+tableRate :: [(Day, Day, Decimal)] -> Day -> Maybe (Day, Decimal)+tableRate table date =+ case dropWhile (\(_,to,_) -> to < date) table of+ (_,to,p) : _ -> Just (to, p)+ [] -> Nothing++-- | Like 'tableRate', but fail with an error message if the table does not+-- cover the given date.+tableRate' :: String -> [(Day, Day, Decimal)] -> Rate+tableRate' name table date =+ fromMaybe (error ("hledger-interest: no " ++ name ++ " interest rate is known for " ++ show date))+ (tableRate table date)++-- | Turn the start dates of 'basiszinsTable' into (from, to, rate) periods.+-- Each period ends the day before the next one starts; the last one is+-- valid for six months.+basiszinsPeriods :: [(Day, Day, Decimal)]+basiszinsPeriods = zipWith (\(from, p) to -> (from, to, p)) basiszinsTable ends+ where+ starts = map fst basiszinsTable+ ends = map pred (drop 1 starts ++ map (addGregorianMonthsClip 6) (take 1 (reverse starts)))++-- | Basiszinssatz according to BGB §247, keyed by the date it takes effect.+-- Source: https://www.bundesbank.de/de/bundesbank/organisation/agb-und-regelungen/basiszinssatz-607820+basiszinsTable :: [(Day, Decimal)] basiszinsTable =- [ (day 2002 01 01, day 2002 06 30, 257 / 10000)- , (day 2002 07 01, day 2002 12 31, 247 / 10000)- , (day 2003 01 01, day 2003 06 30, 197 / 10000)- , (day 2003 07 01, day 2003 12 31, 122 / 10000)- , (day 2004 01 01, day 2004 06 30, 114 / 10000)- , (day 2004 07 01, day 2004 12 31, 113 / 10000)- , (day 2005 01 01, day 2005 06 30, 121 / 10000)- , (day 2005 07 01, day 2005 12 31, 117 / 10000)- , (day 2006 01 01, day 2006 06 30, 137 / 10000)- , (day 2006 07 01, day 2006 12 31, 195 / 10000)- , (day 2007 01 01, day 2007 06 30, 270 / 10000)- , (day 2007 07 01, day 2007 12 31, 319 / 10000)- , (day 2008 01 01, day 2008 06 30, 332 / 10000)- , (day 2008 07 01, day 2008 12 31, 319 / 10000)- , (day 2009 01 01, day 2009 06 30, 162 / 10000)- , (day 2009 07 01, day 2009 12 31, 12 / 10000)- , (day 2010 01 01, day 2010 06 30, 12 / 10000)- , (day 2010 07 01, day 2010 12 31, 12 / 10000)- , (day 2011 01 01, day 2011 06 30, 12 / 10000)- , (day 2011 07 01, day 2999 12 31, 37 / 10000)- , (day 2011 01 01, day 2011 06 30, 12 / 10000)- , (day 2011 07 01, day 2011 12 31, 37 / 10000)- , (day 2012 01 01, day 2012 06 30, 12 / 10000)- , (day 2012 07 01, day 2012 12 31, 12 / 10000)- , (day 2013 01 01, day 2013 06 30, (-13) / 10000)- , (day 2013 07 01, day 2013 12 31, (-38) / 10000)- , (day 2014 01 01, day 2014 06 30, (-63) / 10000)- , (day 2014 07 01, day 2014 12 31, (-73) / 10000)- , (day 2015 01 01, day 2015 06 30, (-83) / 10000)- , (day 2015 07 01, day 2015 12 31, (-83) / 10000)- , (day 2016 01 01, day 2016 06 30, (-83) / 10000)- , (day 2016 07 01, day 2016 12 31, (-88) / 10000)- , (day 2017 01 01, day 2017 06 30, (-88) / 10000)- , (day 2017 07 01, day 2017 12 31, (-88) / 10000)- , (day 2018 01 01, day 2018 06 30, (-88) / 10000)- , (day 2019 01 01, day 2019 06 30, (-88) / 10000)- , (day 2019 07 01, day 2019 12 31, (-88) / 10000)- , (day 2020 01 01, day 2020 06 30, (-88) / 10000)- , (day 2020 07 01, day 2020 12 31, (-88) / 10000)- , (day 2021 01 01, day 2021 06 30, (-88) / 10000)- , (day 2021 07 01, day 2021 12 31, (-88) / 10000)- , (day 2022 01 01, day 2022 06 30, (-88) / 10000)- , (day 2022 07 01, day 2022 12 31, (-88) / 10000)- , (day 2023 01 01, day 2023 06 30, 162 / 10000)- , (day 2023 07 01, day 2023 12 31, 312 / 10000)- , (day 2024 01 01, day 2024 06 30, 362 / 10000)- , (day 2024 07 01, day 2024 12 31, 337 / 10000)+ [ (day 2002 01 01, 257 / 10000)+ , (day 2002 07 01, 247 / 10000)+ , (day 2003 01 01, 197 / 10000)+ , (day 2003 07 01, 122 / 10000)+ , (day 2004 01 01, 114 / 10000)+ , (day 2004 07 01, 113 / 10000)+ , (day 2005 01 01, 121 / 10000)+ , (day 2005 07 01, 117 / 10000)+ , (day 2006 01 01, 137 / 10000)+ , (day 2006 07 01, 195 / 10000)+ , (day 2007 01 01, 270 / 10000)+ , (day 2007 07 01, 319 / 10000)+ , (day 2008 01 01, 332 / 10000)+ , (day 2008 07 01, 319 / 10000)+ , (day 2009 01 01, 162 / 10000)+ , (day 2009 07 01, 12 / 10000)+ , (day 2010 01 01, 12 / 10000)+ , (day 2010 07 01, 12 / 10000)+ , (day 2011 01 01, 12 / 10000)+ , (day 2011 07 01, 37 / 10000)+ , (day 2012 01 01, 12 / 10000)+ , (day 2012 07 01, 12 / 10000)+ , (day 2013 01 01, (-13) / 10000)+ , (day 2013 07 01, (-38) / 10000)+ , (day 2014 01 01, (-63) / 10000)+ , (day 2014 07 01, (-73) / 10000)+ , (day 2015 01 01, (-83) / 10000)+ , (day 2015 07 01, (-83) / 10000)+ , (day 2016 01 01, (-83) / 10000)+ , (day 2016 07 01, (-88) / 10000)+ , (day 2017 01 01, (-88) / 10000)+ , (day 2017 07 01, (-88) / 10000)+ , (day 2018 01 01, (-88) / 10000)+ , (day 2018 07 01, (-88) / 10000)+ , (day 2019 01 01, (-88) / 10000)+ , (day 2019 07 01, (-88) / 10000)+ , (day 2020 01 01, (-88) / 10000)+ , (day 2020 07 01, (-88) / 10000)+ , (day 2021 01 01, (-88) / 10000)+ , (day 2021 07 01, (-88) / 10000)+ , (day 2022 01 01, (-88) / 10000)+ , (day 2022 07 01, (-88) / 10000)+ , (day 2023 01 01, 162 / 10000)+ , (day 2023 07 01, 312 / 10000)+ , (day 2024 01 01, 362 / 10000)+ , (day 2024 07 01, 337 / 10000)+ , (day 2025 01 01, 227 / 10000)+ , (day 2025 07 01, 127 / 10000)+ , (day 2026 01 01, 127 / 10000)+ , (day 2026 07 01, 152 / 10000) ] ingDiba :: Rate-ingDiba date = (to, p)- where- (_,to,p) = head (dropWhile (\(_,to',_) -> to' < date) ingDibaTable)+ingDiba = tableRate' "ING-DiBa" ingDibaTable ingDibaTable :: [(Day, Day, Decimal)] ingDibaTable =@@ -98,9 +162,7 @@ ] db24 :: Rate-db24 date = (to, p)- where- (_,to,p) = head (dropWhile (\(_,to',_) -> to' < date) db24Table)+db24 = tableRate' "Deutsche Bank 24" db24Table db24Table :: [(Day, Day, Decimal)] db24Table =
Main.hs view
@@ -5,7 +5,7 @@ import Hledger.Read import Hledger.Utils ( toRegex' ) -import Control.Exception ( bracket )+import Control.Exception ( ErrorCall(..), bracket, handle ) import Control.Monad import Data.List ( sortOn ) import Data.Maybe@@ -28,6 +28,7 @@ , optDCC :: Maybe DayCountConvention , optRate :: Maybe Rate , optBalanceToday :: Bool+ , optPostOnTxns :: Bool , optIgnoreAssertions :: Bool } @@ -42,6 +43,7 @@ , optDCC = Nothing , optRate = Nothing , optBalanceToday = False+ , optPostOnTxns = False , optIgnoreAssertions = False } @@ -52,11 +54,12 @@ , Option ['v'] ["verbose"] (NoArg (\o -> o { optVerbose = True })) "echo input ledger to stdout (default)" , Option ['q'] ["quiet"] (NoArg (\o -> o { optVerbose = False })) "don't echo input ledger to stdout" , Option [] ["today"] (NoArg (\o -> o { optBalanceToday = True })) "compute interest up until today"- , Option ['f'] ["file"] (ReqArg (\f o -> o { optInput = f : optInput o }) "FILE") "input ledger file (pass '-' for stdin)"+ , Option [] ["post-on-transactions"] (NoArg (\o -> o { optPostOnTxns = True })) "post interest only when the balance changes\n(and with --today), not at the end of\nevery year or rate period"+ , Option ['f'] ["file"] (ReqArg (\f o -> o { optInput = f : optInput o }) "FILE") "input ledger file (pass '-' for stdin)\ndefault: $LEDGER_FILE or ~/.hledger.journal" , Option ['s'] ["source"] (ReqArg (\a o -> o { optSourceAcc = a }) "ACCOUNT") "interest source account" , Option ['t'] ["target"] (ReqArg (\a o -> o { optTargetAcc = a }) "ACCOUNT") "interest target account" , Option ['I'] ["ignore-assertions"] (NoArg (\o -> o { optIgnoreAssertions = True })) "ignore any failing balance assertions"- , Option [] ["act"] (NoArg (\o -> o { optDCC = Just diffAct })) "use 'act' day counting convention"+ , Option [] ["act"] (NoArg (\o -> o { optDCC = Just diffAct })) "use 'act/act' day counting convention\n(Actual/Actual ISDA; see README)" , Option [] ["30-360"] (NoArg (\o -> o { optDCC = Just diff30_360 })) "use '30/360' day counting convention" , Option [] ["30E-360"] (NoArg (\o -> o { optDCC = Just diff30E_360 })) "use '30E/360' day counting convention" , Option [] ["30E-360isda"] (NoArg (\o -> o { optDCC = Just diff30E_360isda })) "use '30E/360isda' day counting convention"@@ -92,7 +95,11 @@ when (isNothing (optDCC opts)) (commandLineError "no day counting convention specified\n") when (isNothing (optRate opts)) (commandLineError "no interest rate specified\n") let ledgerInputOptions = definputopts { balancingopts_ = (balancingopts_ definputopts) { ignore_assertions_ = optIgnoreAssertions opts } }- jnl' <- runExceptT (readJournalFiles ledgerInputOptions (reverse (optInput opts))) >>= either fail return+ inputFiles <- case optInput opts of+ [] -> pure <$> defaultJournalPath+ fs -> return (reverse fs)+ jnl' <- handle (\(ErrorCall msg) -> hPutStrLn stderr msg >> exitFailure) $+ runExceptT (readJournalFiles ledgerInputOptions inputFiles) >>= either fail return interestAcc <- case args of [] -> commandLineError "required argument ACCOUNT is missing\n" [acc] -> return $ T.pack acc@@ -105,6 +112,7 @@ , targetAccount = T.pack (optTargetAcc opts) , dayCountConvention = fromJust (optDCC opts) , interestRate = fromJust (optRate opts)+ , postOnTransactions = optPostOnTxns opts } thisDay <- getCurrentDay let finalize
README.md view
@@ -1,8 +1,13 @@ hledger-interest ================ +[](http://hackage.haskell.org/package/hledger-interest)+[](http://stackage.org/lts/package/hledger-interest)+[](http://stackage.org/nightly/package/hledger-interest)+[](https://github.com/peti/hledger-interest/actions/workflows/haskell-ci.yml)+ hledger-interest is a small command-line utility based on [Simon-Michael's hlegder library](http://hledger.org/). Its purpose is to+Michael's hledger library](http://hledger.org/). Its purpose is to compute interest for a given ledger account. Using command line flags, the program can be configured to use various [day counting conventions](http://en.wikipedia.org/wiki/Day_count_convention), such as@@ -14,16 +19,21 @@ the source code. An overview over the available run-time options can be displayed by-running "`hleder-interest --help`":+running "`hledger-interest --help`": Usage: hledger-interest [OPTION...] ACCOUNT -v --verbose echo input ledger to stdout (default) -q --quiet don't echo input ledger to stdout --today compute interest up until today+ --post-on-transactions post interest only when the balance changes+ (and with --today), not at the end of+ every year or rate period -f FILE --file=FILE input ledger file (pass '-' for stdin)+ default: $LEDGER_FILE or ~/.hledger.journal -s ACCOUNT --source=ACCOUNT interest source account -t ACCOUNT --target=ACCOUNT interest target account- --act use 'act' day counting convention+ --act use 'act/act' day counting convention+ (Actual/Actual ISDA; see README) --30-360 use '30/360' day counting convention --30E-360 use '30E/360' day counting convention --30E-360isda use '30E/360isda' day counting convention@@ -42,6 +52,47 @@ transfers the interest that accrued so far. Interest will be debited from the account designed by the `--source` flag and credited to the account designed by the `--target` flag.++## Day counting conventions++For every period, hledger-interest computes++ interest = balance * rate * days / days-in-year++where `days` is the length of the period, both ends included, and+`days-in-year` is the length of the calendar year in which the period+ends. Both are counted with the selected convention:++- `--act` counts actual calendar days, so a year has 365 or 366 days.+ `--annual`, `--annual-schedule`, and `--bgb288` never let a period+ cross a year boundary, which makes this the [Actual/Actual+ ISDA](https://en.wikipedia.org/wiki/Day_count_convention#Actual/Actual_ISDA)+ method. It is also the reason why these rates post interest on+ December 31st of every year (see below). With `--constant`, a period can span+ several years and is divided by the length of the year it ends in, so+ the result is only an approximation of Actual/Actual ISDA in that case.+- `--30-360`, `--30E-360`, and `--30E-360isda` count every month as 30+ days and every year as 360 days. They differ in how they treat the+ 31st and the end of February; see the [Wikipedia+ article](https://en.wikipedia.org/wiki/Day_count_convention#30/360_methods)+ for details. `--30E-360isda` does not apply the February exception+ for the maturity date, because hledger-interest has no notion of+ maturity.++By default, hledger-interest posts the interest at the end of every+period: on every transaction date, but also on December 31st and on+every date at which the rate changes. If the interest is credited to the+interest-bearing account itself, it compounds from then on. Many loans+and savings accounts compound less often, though, e.g. only when a+payment is made. With `--post-on-transactions`, interest is still+computed separately for every year and rate, but posted only on+transaction dates (and on the final day with `--today`). Since such a+posting may cover several rates, its description does not mention a+rate, e.g. for a rate change from 5% to 7% on 2021-12-20:++ 2022-01-10 interest for -9841.10 USD over 31 days+ Liabilities:Loan -53.65 USD+ Expenses:Interest 53.65 USD ## Examples
− Setup.hs
@@ -1,2 +0,0 @@-import Distribution.Simple-main = defaultMain
hledger-interest.cabal view
@@ -1,5 +1,5 @@ Name: hledger-interest-Version: 1.6.7+Version: 1.7.0 Synopsis: computes interest for a given account License: BSD3 License-file: LICENSE@@ -10,14 +10,14 @@ Build-type: Simple Cabal-version: >= 1.10 Extra-source-files: README.md+ CHANGELOG.md Stability: stable-tested-with: GHC == 8.4.4, GHC == 8.6.5, GHC == 8.8.4, GHC == 8.10.7,- GHC == 9.0.2, GHC == 9.2.8, GHC == 9.4.8, GHC == 9.6.3,- GHC == 9.8.4, GHC == 9.10.1+tested-with: GHC == 9.6.7, GHC == 9.8.4, GHC == 9.10.3,+ GHC == 9.12.2, GHC == 9.14.1 Description: hledger-interest is a small command-line utility based on Simon- Michael's hleder library. Its purpose is to compute interest for a+ Michael's hledger library. Its purpose is to compute interest for a given ledger account. Using command line flags, the program can be configured to use various schemes for day-counting, such as act\/act, 30\/360, 30E\/360, and 30\/360isda. Furthermore, it supports a (small)@@ -50,45 +50,41 @@ ledger would look like this: . > $ hledger-interest --file=test.ledger --source=Expenses:Interest --target=Liabilities:Bank --30-360 --annual=0.05 Liabilities:Bank- > 2008/09/26 Loan- > Assets:Bank EUR 10000.00- > Liabilities:Bank- >- > 2008/11/27 Payment- > Assets:Bank EUR -3771.12- > Liabilities:Bank+ > 2008-09-26 Loan+ > Assets:Bank EUR 10000.00+ > Liabilities:Bank EUR -10000.00 >- > 2008/11/27 5.00% interest for EUR -10000.00 over 61 days- > Liabilities:Bank EUR -84.72- > Expenses:Interest+ > 2008-11-27 5% interest for EUR -10000.00 over 61 days+ > Liabilities:Bank EUR -84.72+ > Expenses:Interest EUR 84.72 >- > 2008/12/31 5.00% interest for EUR -6313.60 over 34 days- > Liabilities:Bank EUR -29.81- > Expenses:Interest+ > 2008-11-27 Payment+ > Assets:Bank EUR -3771.12+ > Liabilities:Bank EUR 3771.12 >- > 2009/05/03 Payment- > Assets:Bank EUR -1200.00- > Liabilities:Bank+ > 2008-12-31 5% interest for EUR -6313.60 over 34 days+ > Liabilities:Bank EUR -29.81+ > Expenses:Interest EUR 29.81 >- > 2009/05/03 5.00% interest for EUR -6343.42 over 123 days- > Liabilities:Bank EUR -108.37- > Expenses:Interest+ > 2009-05-03 5% interest for EUR -6343.42 over 123 days+ > Liabilities:Bank EUR -108.37+ > Expenses:Interest EUR 108.37 >- > 2009/12/31 5.00% interest for EUR -5251.78 over 238 days- > Liabilities:Bank EUR -173.60- > Expenses:Interest+ > 2009-05-03 Payment+ > Assets:Bank EUR -1200.00+ > Liabilities:Bank EUR 1200.00 >- > 2010/12/10 Payment- > Assets:Bank EUR -3700.00- > Liabilities:Bank+ > 2009-12-31 5% interest for EUR -5251.78 over 238 days+ > Liabilities:Bank EUR -173.60+ > Expenses:Interest EUR 173.60 >- > 2010/12/10 5.00% interest for EUR -5425.38 over 340 days- > Liabilities:Bank EUR -256.20- > Expenses:Interest+ > 2010-12-10 5% interest for EUR -5425.38 over 340 days+ > Liabilities:Bank EUR -256.20+ > Expenses:Interest EUR 256.20 >- > 2010/12/31 5.00% interest for EUR -1981.58 over 21 days- > Liabilities:Bank EUR -5.78- > Expenses:Interest+ > 2010-12-10 Payment+ > Assets:Bank EUR -3700.00+ > Liabilities:Bank EUR 3700.00 . Running the utility with @--help@ gives a brief overview over the available options:@@ -99,19 +95,26 @@ > -v --verbose echo input ledger to stdout (default) > -q --quiet don't echo input ledger to stdout > --today compute interest up until today+ > --post-on-transactions post interest only when the balance changes+ > (and with --today), not at the end of+ > every year or rate period > -f FILE --file=FILE input ledger file (pass '-' for stdin)+ > default: $LEDGER_FILE or ~/.hledger.journal > -s ACCOUNT --source=ACCOUNT interest source account > -t ACCOUNT --target=ACCOUNT interest target account > -I --ignore-assertions ignore any failing balance assertions- > --act use 'act' day counting convention+ > --act use 'act/act' day counting convention+ > (Actual/Actual ISDA; see README) > --30-360 use '30/360' day counting convention > --30E-360 use '30E/360' day counting convention > --30E-360isda use '30E/360isda' day counting convention > --constant=RATE constant interest rate+ > --annual=RATE annual interest rate > --annual-schedule=SCHEDULE schedule of annual interest rates. > syntax: '[(Date1,Rate1),(Date2,Rate2),...]'- > --annual=RATE annual interest rate > --bgb288 compute interest according to German BGB288+ > --db24 HACK: Deutsche Bank 24+ > --ing-diba HACK: compute interest according for Ing-Diba Tagesgeld account Source-Repository head Type: git@@ -120,7 +123,7 @@ Executable hledger-interest default-language: Haskell2010 Main-is: Main.hs- Build-depends: base >= 3 && < 5, hledger-lib >= 1.26 && < 1.42, time, mtl, Cabal, Decimal, text+ Build-depends: base >= 3 && < 5, hledger-lib >= 1.26 && < 1.53, time, mtl, Decimal, text other-modules: Hledger.Interest Hledger.Interest.DayCountConvention Hledger.Interest.Rate