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 +29/−0
- Main.hs +198/−0
- Setup.hs +2/−0
- hledger-irr.cabal +113/−0
+ 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+