packages feed

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 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 ================ +[![hackage release](https://img.shields.io/hackage/v/hledger-interest.svg?label=hackage)](http://hackage.haskell.org/package/hledger-interest)+[![stackage LTS package](http://stackage.org/package/hledger-interest/badge/lts)](http://stackage.org/lts/package/hledger-interest)+[![stackage Nightly package](http://stackage.org/package/hledger-interest/badge/nightly)](http://stackage.org/nightly/package/hledger-interest)+[![CI Status](https://github.com/peti/hledger-interest/actions/workflows/haskell-ci.yml/badge.svg)](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