packages feed

hledger-irr (empty) → 0.1

raw patch · 4 files changed

+342/−0 lines, 4 filesdep +Cabaldep +basedep +hledger-libsetup-changed

Dependencies added: Cabal, base, hledger-lib, statistics, time

Files

+ LICENSE view
@@ -0,0 +1,29 @@+Copyright (c) 2011 Peter Simons <simons@cryp.to>+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Peter Simons nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Main.hs view
@@ -0,0 +1,198 @@+module Main ( main ) where++import Hledger++import Control.Exception ( bracket )+import Control.Monad+import Distribution.Text ( display )+import System.Console.GetOpt+import System.Environment+import System.Exit+import System.IO+import Data.Time.Calendar+import Text.Printf+import Data.List+import Data.Ord+import Statistics.Math.RootFinding+++import Paths_hledger_irr ( version )++data Options = Options+  { optShowVersion  :: Bool+  , optShowHelp     :: Bool+  , optCashFlow     :: Bool+  , optInput        :: FilePath+  , optInvAcc       :: String+  , optInterestAcc  :: String+  , optBegin        :: Maybe String+  , optEnd          :: Maybe String+  , optInterval     :: Maybe Interval+  }++defaultOptions :: Options+defaultOptions = Options+  { optShowVersion  = False+  , optShowHelp     = False+  , optCashFlow     = False+  , optInput        = "-"+  , optInvAcc       = ""+  , optInterestAcc  = ""+  , optBegin        = Nothing+  , optEnd          = Nothing+  , optInterval     = Nothing+  }++options :: [OptDescr (Options -> Options)]+options =+ [ Option "h" ["help"]+              (NoArg (\o -> o { optShowHelp = True }))+              "print this message and exit"+ , Option "V" ["version"]+              (NoArg (\o -> o { optShowVersion = True }))+              "show version number and exit"+ , Option "c" ["cashflow"]+              (NoArg (\o -> o { optCashFlow = True }))+              "also show all revant transactions"+ , Option "f" ["file"]+              (ReqArg (\f o -> o { optInput = f }) "FILE")+              "input ledger file (pass '-' for stdin)"+ , Option "i" ["investment-account"]+              (ReqArg (\a o -> o { optInvAcc = a }) "ACCOUNT")+              "investment account"+ , Option "t" ["interest-account"]+              (ReqArg (\a o -> o { optInterestAcc = a }) "ACCOUNT")+              "interest/gain/fees/losses account"+ , Option "b" ["begin"]+              (ReqArg (\d o -> o { optBegin = Just d }) "DATE")+              "calculate interest from this date"+ , Option "e" ["end"]+              (ReqArg (\d o -> o { optEnd = Just d }) "DATE")+              "calculate interest until this date"+ , Option "D" ["daily"]+              (NoArg (\o -> o { optInterval = Just (Days 1) }))+              "calculate intereste for each day"+ , Option "W" ["weekly"]+              (NoArg (\o -> o { optInterval = Just (Weeks 1) }))+              "calculate intereste for each week"+ , Option "M" ["monthly"]+              (NoArg (\o -> o { optInterval = Just (Months 1) }))+              "calculate intereste for each month"+ , Option "Y" ["yearly"]+              (NoArg (\o -> o { optInterval = Just (Years 1) }))+              "calculate intereste for each year"+ ]++usageMessage :: String+usageMessage = usageInfo header options+  where header = "Usage: hledger-irr [OPTION...]"++commandLineError :: String -> IO a+commandLineError err = do hPutStrLn stderr (err ++ usageMessage)+                          exitFailure++parseOpts :: [String] -> IO (Options, [String])+parseOpts argv =+   case getOpt Permute options argv of+      (o,n,[]  ) -> return (foldl (flip id) defaultOptions o, n)+      (_,_,errs) -> commandLineError (concat errs)++main :: IO ()+main = bracket (return ()) (\() -> hFlush stdout >> hFlush stderr) $ \() -> do+  (opts, args) <- getArgs >>= parseOpts+  when (optShowVersion opts) (putStrLn (display version) >> exitSuccess)+  when (optShowHelp opts) (putStr usageMessage >> exitSuccess)+  when (null (optInvAcc opts)) (commandLineError "required --investment-account option is missing\n")+  when (null (optInterestAcc opts)) (commandLineError "required --interest-account option is missing\n")+  when (length args > 0) (commandLineError "no command line arguments allowed")+  jnl' <- readJournalFile Nothing Nothing (optInput opts) >>= either fail return++  let ts = jtxns $ filterJournalTransactions (Acct (optInvAcc opts)) jnl'+  when (null ts) $ do+    putStrLn "No relevant transactions found. Did you mis-spell your accounts?"+    exitFailure++  thisDay <- getCurrentDay+  let firstDay = minimum $ map transactionEffectiveDate ts+  let lastDay = maximum $ map transactionEffectiveDate ts+  let existingSpan = DateSpan (Just firstDay) (Just lastDay)++  let begin = maybe firstDay (fixSmartDateStr' thisDay) (optBegin opts)+  let end =   maybe thisDay  (fixSmartDateStr' thisDay) (optEnd opts)+  let wholeSpan = DateSpan (Just begin) (Just end)++  let spans = case optInterval opts of+        Nothing -> [wholeSpan]+        Just interval ->+            splitSpan interval $+            spanIntersect existingSpan wholeSpan++  forM_ spans $ \(DateSpan (Just ibegin) (Just iend)) -> do+      let preQuery = And [ Acct (optInvAcc opts),+                           EDate (openClosedSpan Nothing (Just ibegin))]+          pre_amount = negate $ unMix $ accountAmount preQuery ts+      let prefix = (ibegin, pre_amount)++      let eQuery = And [Acct (optInvAcc opts), EDate (openClosedSpan Nothing (Just iend))]+      let final = unMix $ accountAmount eQuery ts+      let postfix = (iend, final)++      let cfQuery = And [ Not (Or [Acct (optInvAcc opts), Acct (optInterestAcc opts)]), +                          EDate (openClosedSpan (Just ibegin) (Just iend)) ] +      let cf = calculateCashFlow cfQuery ts++      let totalCF = sortBy (comparing fst) $ filter ((/=0) . aquantity . snd) $ prefix : cf ++ [postfix]++      when (optCashFlow opts) $ do+          mapM_ (putStrLn . showCashFlowEntry) totalCF++      -- 0% is always a solution, so require at least something here+      putStr $ printf "%s - %s: " (showDate ibegin) (showDate iend)+      case ridders 0.00001 (0.000001,1000) (aquantity . interestSum iend totalCF) of+        Root rate -> putStrLn (printf "%0.2f%%" ((rate-1)*100))+        _ -> putStrLn "Error: Failed to find solution."++openClosedSpan :: Maybe Day -> Maybe Day -> DateSpan+openClosedSpan md1 md2 = DateSpan (fmap (addDays 1) md1) (fmap (addDays 1) md2)+++-- Bad hack – what to do?+unMix :: MixedAmount -> Amount+unMix = sum . amounts+++showCashFlowEntry :: (Day, Amount) -> String+showCashFlowEntry (d, a) = showDate d ++ ": " ++ showAmount a++type CashFlow = [(Day, Amount)]+++-- | Divide an amount's quantity by a constant.+multiplyAmount :: Amount -> Double -> Amount+multiplyAmount a@Amount{aquantity=q} d = a{aquantity=q*d}+++interestSum :: Day -> CashFlow -> Double -> Amount+interestSum referenceDay cf rate = sum $ map go cf+    where go (t,m) = multiplyAmount m (rate ** (fromIntegral (referenceDay `diffDays` t) / 365))+++calculateCashFlow :: Query -> [Transaction] -> CashFlow+calculateCashFlow query = map go+    where+    go t = (transactionEffectiveDate t, amt)+        where+        amt = sum $+              map (unMix . pamount) $ filter (matchesPosting query) $+              realPostings t++accountAmount :: Query -> [Transaction] -> MixedAmount+accountAmount query = sumPostings . filter (matchesPosting query) . concatMap realPostings ++-- | Convert a smart date string to a day using+-- the provided reference date, or raise an error.+fixSmartDateStr' :: Day -> String -> Day+fixSmartDateStr' d s = either+                       (\e->error' $ printf "could not parse date %s %s" (show s) (show e))+                       id+                       $ fixSmartDateStrEither' d s
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ hledger-irr.cabal view
@@ -0,0 +1,113 @@+Name:                   hledger-irr+Version:                0.1+Synopsis:               computes the internal rate of return of an investment+License:                BSD3+License-file:           LICENSE+Author:                 Joachim Breitner <mail@joachim-breitner.de>+Maintainer:             Joachim Breitner <mail@joachim-breitner.de>+Category:               Finance+Build-type:             Simple+Cabal-version:          >= 1.6+Tested-with:            GHC >= 7.4.1 && <= 7.4.1+Description:+ hledger-irr is a small command-line utility based on Simon+ Michael's hleder library. Its purpose is to compute the internal rate of+ return, also known as the effective interest rate, of a given investment.+ After specifying what account holds the investment, and what account stores+ the gains (or losses, or fees, or cost), it calculates the hypothetical+ annualy rate of fixed rate investment that would have provided the exact same+ cash flow.+ .+ As an example, consider the following irregular investment recorded in a file+ called @speculation.ledger@. The account “Speculation” holds the investment which+ could be, for example, a stock. Regularly, we make sure that the value of the+ account matches the value of the stock, by moving money from or to the account+ “Rate Gain”. It does not really matter when we adjust the price, as long as it+ is correct at the end of our reporting period.+ .+ > 2011-01-01 Some wild speculation – I wonder if it pays off+ >   Speculation   €100.00+ >   Cash+ > + > 2011-02-01 More speculation (and adjustment of value)+ >   Cash         -€10.00+ >   Rate Gain     -€1.00 + >   Speculation+ > + > 2011-03-01 Lets pull out some money (and adjustment of value)+ >   Cash          €30.00+ >   Rate Gain     -€3.00 + >   Speculation+ > + > 2011-04-01 More speculation (and it lost some money!)+ >   Cash         -€50.00+ >   Rate Gain     € 5.00+ >   Speculation+ > + > 2011-05-01 Getting some money out (and adjustment of value)+ >   Speculation  -€44.00+ >   Rate Gain    -€ 3.00+ >   Cash+ > + > 2011-06-01 Emptying the account (after adjusting the value)+ >   Speculation   -€85.00+ >   Cash           €90.00+ >   Rate Gain     -€ 5.00+ .+ We can now calculate the rate of return for the whole time or just for parts+ of it (and be freaked out by the volatility of the investment):+ .+ > $ hledger-irr -f speculation.ledger -t "Rate Gain" -i Speculation -c+ > 2011/01/01: €-100.00+ > 2011/02/01: €-10.00+ > 2011/03/01: €30.00+ > 2011/04/01: €-50.00+ > 2011/05/01: €47.00+ > 2011/06/01: €90.00+ > 2011/01/01 - 2012/12/07: 17.72%+ > $ hledger-irr -f speculation.ledger -t "Rate Gain" -i Speculation -e 2011-03-01+ > 2011/01/01 - 2011/03/01: 26.11%+ > $ hledger-irr -f speculation.ledger -t "Rate Gain" -i Speculation -b 2011-03-01+ > 2011/03/01 - 2012/12/07: 12.28%+ > $ hledger-irr -f speculation.ledger -t "Rate Gain" -i Speculation --monthly+ > 2011/01/01 - 2011/02/01: 12.43%+ > 2011/02/01 - 2011/03/01: 41.57%+ > 2011/03/01 - 2011/04/01: -51.45%+ > 2011/04/01 - 2011/05/01: 32.27%+ > 2011/05/01 - 2011/06/01: 96.01%+ .+ Running the utility with @--help@ gives a brief overview over the+ available options:+ .+ > $ hledger-irr --help+ > Usage: hledger-irr [OPTION...]+ >   -h          --help                        print this message and exit+ >   -V          --version                     show version number and exit+ >   -c          --cashflow                    also show all revant transactions+ >   -f FILE     --file=FILE                   input ledger file (pass '-' for stdin)+ >   -i ACCOUNT  --investment-account=ACCOUNT  investment account+ >   -t ACCOUNT  --interest-account=ACCOUNT    interest/gain/fees/losses account+ >   -b DATE     --begin=DATE                  calculate interest from this date+ >   -e DATE     --end=DATE                    calculate interest until this date+ >   -D          --daily                       calculate intereste for each day+ >   -W          --weekly                      calculate intereste for each week+ >   -M          --monthly                     calculate intereste for each month+ >   -Y          --yearly                      calculate intereste for each year+ .+ Known bugs and issues:+ .+ * Currenlty, hledger-irr does not cope well with multiple commodities (e.g.+   Euro and Dollar, or shares).+ .+ * Also, interest or fees that do not pass through the account selected by+   @--investment-account@ are not taken into consideration.++Executable hledger-irr+  Main-is:              Main.hs+  Build-depends:        base >= 3 && < 5, hledger-lib >= 0.19.3, time, Cabal, statistics >= 0.10+  Ghc-Options:          -Wall++source-repository head+    type:     darcs+    location: http://darcs.nomeata.de/hledger-irr+