{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ParallelListComp  #-}
{-# LANGUAGE TemplateHaskell   #-}
{-# LANGUAGE TupleSections     #-}
{-# LANGUAGE RecordWildCards   #-}
{-# LANGUAGE ViewPatterns #-}
{-|

The @roi@ command prints internal rate of return and time-weighted rate of return for and investment.

-}

module Hledger.Cli.Commands.Roi (
  roimode
  , roi
) where

import Control.Monad
import System.Exit
import Data.Time.Calendar
import Text.Printf
import Data.Bifunctor (second)
import Data.Either (fromLeft, fromRight, isLeft)
import Data.Function (on)
import Data.List
import Numeric.RootFinding
import Data.Decimal
import qualified Data.Text as T
import qualified Data.Text.Lazy.IO as TL
import System.Console.CmdArgs.Explicit as CmdArgs

import Text.Tabular.AsciiWide as Tab

import Hledger
import Hledger.Cli.CliOptions


roimode :: Mode RawOpts
roimode = [Char]
-> [Flag RawOpts]
-> [([Char], [Flag RawOpts])]
-> [Flag RawOpts]
-> ([Arg RawOpts], Maybe (Arg RawOpts))
-> Mode RawOpts
hledgerCommandMode
  $(embedFileRelative "Hledger/Cli/Commands/Roi.txt")
  [[[Char]] -> (RawOpts -> RawOpts) -> [Char] -> Flag RawOpts
forall a. [[Char]] -> (a -> a) -> [Char] -> Flag a
flagNone [[Char]
"cashflow"] ([Char] -> RawOpts -> RawOpts
setboolopt [Char]
"cashflow") [Char]
"show all amounts that were used to compute returns"
  ,[[Char]] -> Update RawOpts -> [Char] -> [Char] -> Flag RawOpts
forall a. [[Char]] -> Update a -> [Char] -> [Char] -> Flag a
flagReq [[Char]
"investment"] (\[Char]
s RawOpts
opts -> RawOpts -> Either [Char] RawOpts
forall a b. b -> Either a b
Right (RawOpts -> Either [Char] RawOpts)
-> RawOpts -> Either [Char] RawOpts
forall a b. (a -> b) -> a -> b
$ [Char] -> [Char] -> RawOpts -> RawOpts
setopt [Char]
"investment" [Char]
s RawOpts
opts) [Char]
"QUERY"
    [Char]
"query to select your investment transactions"
  ,[[Char]] -> Update RawOpts -> [Char] -> [Char] -> Flag RawOpts
forall a. [[Char]] -> Update a -> [Char] -> [Char] -> Flag a
flagReq [[Char]
"profit-loss",[Char]
"pnl"] (\[Char]
s RawOpts
opts -> RawOpts -> Either [Char] RawOpts
forall a b. b -> Either a b
Right (RawOpts -> Either [Char] RawOpts)
-> RawOpts -> Either [Char] RawOpts
forall a b. (a -> b) -> a -> b
$ [Char] -> [Char] -> RawOpts -> RawOpts
setopt [Char]
"pnl" [Char]
s RawOpts
opts) [Char]
"QUERY"
    [Char]
"query to select profit-and-loss or appreciation/valuation transactions"
  ]
  [([Char], [Flag RawOpts])
generalflagsgroup1]
  [Flag RawOpts]
hiddenflags
  ([], Arg RawOpts -> Maybe (Arg RawOpts)
forall a. a -> Maybe a
Just (Arg RawOpts -> Maybe (Arg RawOpts))
-> Arg RawOpts -> Maybe (Arg RawOpts)
forall a b. (a -> b) -> a -> b
$ [Char] -> Arg RawOpts
argsFlag [Char]
"[QUERY]")

-- One reporting span,
data OneSpan = OneSpan
  Day -- start date, inclusive
  Day   -- end date, exclusive
  MixedAmount -- value of investment at the beginning of day on spanBegin_
  MixedAmount -- value of investment at the end of day on spanEnd_
  [(Day,MixedAmount)] -- all deposits and withdrawals (but not changes of value) in the DateSpan [spanBegin_,spanEnd_)
  [(Day,MixedAmount)] -- all PnL changes of the value of investment in the DateSpan [spanBegin_,spanEnd_)
 deriving (Int -> OneSpan -> [Char] -> [Char]
[OneSpan] -> [Char] -> [Char]
OneSpan -> [Char]
(Int -> OneSpan -> [Char] -> [Char])
-> (OneSpan -> [Char])
-> ([OneSpan] -> [Char] -> [Char])
-> Show OneSpan
forall a.
(Int -> a -> [Char] -> [Char])
-> (a -> [Char]) -> ([a] -> [Char] -> [Char]) -> Show a
$cshowsPrec :: Int -> OneSpan -> [Char] -> [Char]
showsPrec :: Int -> OneSpan -> [Char] -> [Char]
$cshow :: OneSpan -> [Char]
show :: OneSpan -> [Char]
$cshowList :: [OneSpan] -> [Char] -> [Char]
showList :: [OneSpan] -> [Char] -> [Char]
Show)


roi ::  CliOpts -> Journal -> IO ()
roi :: CliOpts -> Journal -> IO ()
roi CliOpts{rawopts_ :: CliOpts -> RawOpts
rawopts_=RawOpts
rawopts, reportspec_ :: CliOpts -> ReportSpec
reportspec_=rspec :: ReportSpec
rspec@ReportSpec{_rsReportOpts :: ReportSpec -> ReportOpts
_rsReportOpts=ReportOpts{Bool
Int
[Text]
[Status]
Maybe Int
Maybe Text
Maybe NormalSign
Maybe ValuationType
Maybe ConversionOp
Interval
Period
StringFormat
Layout
AccountListMode
BalanceAccumulation
BalanceCalculation
period_ :: Period
interval_ :: Interval
statuses_ :: [Status]
conversionop_ :: Maybe ConversionOp
value_ :: Maybe ValuationType
infer_prices_ :: Bool
depth_ :: Maybe Int
date2_ :: Bool
empty_ :: Bool
no_elide_ :: Bool
real_ :: Bool
format_ :: StringFormat
pretty_ :: Bool
querystring_ :: [Text]
average_ :: Bool
related_ :: Bool
txn_dates_ :: Bool
balancecalc_ :: BalanceCalculation
balanceaccum_ :: BalanceAccumulation
budgetpat_ :: Maybe Text
accountlistmode_ :: AccountListMode
drop_ :: Int
declared_ :: Bool
row_total_ :: Bool
no_total_ :: Bool
summary_only_ :: Bool
show_costs_ :: Bool
sort_amount_ :: Bool
percent_ :: Bool
invert_ :: Bool
normalbalance_ :: Maybe NormalSign
color_ :: Bool
transpose_ :: Bool
layout_ :: Layout
period_ :: ReportOpts -> Period
interval_ :: ReportOpts -> Interval
statuses_ :: ReportOpts -> [Status]
conversionop_ :: ReportOpts -> Maybe ConversionOp
value_ :: ReportOpts -> Maybe ValuationType
infer_prices_ :: ReportOpts -> Bool
depth_ :: ReportOpts -> Maybe Int
date2_ :: ReportOpts -> Bool
empty_ :: ReportOpts -> Bool
no_elide_ :: ReportOpts -> Bool
real_ :: ReportOpts -> Bool
format_ :: ReportOpts -> StringFormat
pretty_ :: ReportOpts -> Bool
querystring_ :: ReportOpts -> [Text]
average_ :: ReportOpts -> Bool
related_ :: ReportOpts -> Bool
txn_dates_ :: ReportOpts -> Bool
balancecalc_ :: ReportOpts -> BalanceCalculation
balanceaccum_ :: ReportOpts -> BalanceAccumulation
budgetpat_ :: ReportOpts -> Maybe Text
accountlistmode_ :: ReportOpts -> AccountListMode
drop_ :: ReportOpts -> Int
declared_ :: ReportOpts -> Bool
row_total_ :: ReportOpts -> Bool
no_total_ :: ReportOpts -> Bool
summary_only_ :: ReportOpts -> Bool
show_costs_ :: ReportOpts -> Bool
sort_amount_ :: ReportOpts -> Bool
percent_ :: ReportOpts -> Bool
invert_ :: ReportOpts -> Bool
normalbalance_ :: ReportOpts -> Maybe NormalSign
color_ :: ReportOpts -> Bool
transpose_ :: ReportOpts -> Bool
layout_ :: ReportOpts -> Layout
..}}} Journal
j = do
  -- We may be converting posting amounts to value, per hledger_options.m4.md "Effect of --value on reports".
  let
    -- lbl = lbl_ "roi"
    today :: Day
today = ReportSpec -> Day
_rsDay ReportSpec
rspec
    priceOracle :: PriceOracle
priceOracle = Bool -> Journal -> PriceOracle
journalPriceOracle Bool
infer_prices_ Journal
j
    styles :: Map Text AmountStyle
styles = Rounding -> Journal -> Map Text AmountStyle
journalCommodityStylesWith Rounding
HardRounding Journal
j
    mixedAmountValue :: Day -> Day -> MixedAmount -> MixedAmount
mixedAmountValue Day
periodlast Day
date =
        -- These calculations can generate very precise decimals. To avoid showing too many digits:
        -- If we have no style for the valuation commodity, generate one that will limit the precision ?
        -- But it's not easy to find out the valuation commodity (or commodities) here if it's implicit,
        -- as that information is buried in the price graph.
        -- Instead, do what we don't like to do: hard code a max precision, overriding commodity styles.
        Word8 -> MixedAmount -> MixedAmount
mixedAmountSetPrecisionMax Word8
defaultMaxPrecision
      (MixedAmount -> MixedAmount)
-> (MixedAmount -> MixedAmount) -> MixedAmount -> MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (MixedAmount -> MixedAmount)
-> (ValuationType -> MixedAmount -> MixedAmount)
-> Maybe ValuationType
-> MixedAmount
-> MixedAmount
forall b a. b -> (a -> b) -> Maybe a -> b
maybe MixedAmount -> MixedAmount
forall a. a -> a
id (PriceOracle
-> Map Text AmountStyle
-> Day
-> Day
-> Day
-> ValuationType
-> MixedAmount
-> MixedAmount
mixedAmountApplyValuation PriceOracle
priceOracle Map Text AmountStyle
styles Day
periodlast Day
today Day
date) Maybe ValuationType
value_
      (MixedAmount -> MixedAmount)
-> (MixedAmount -> MixedAmount) -> MixedAmount -> MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (MixedAmount -> MixedAmount)
-> (ConversionOp -> MixedAmount -> MixedAmount)
-> Maybe ConversionOp
-> MixedAmount
-> MixedAmount
forall b a. b -> (a -> b) -> Maybe a -> b
maybe MixedAmount -> MixedAmount
forall a. a -> a
id (Map Text AmountStyle -> ConversionOp -> MixedAmount -> MixedAmount
mixedAmountToCost Map Text AmountStyle
styles) Maybe ConversionOp
conversionop_

  let
    ropts :: ReportOpts
ropts = ReportSpec -> ReportOpts
_rsReportOpts ReportSpec
rspec
    wd :: WhichDate
wd = ReportOpts -> WhichDate
whichDate ReportOpts
ropts
    showCashFlow :: Bool
showCashFlow = [Char] -> RawOpts -> Bool
boolopt [Char]
"cashflow" RawOpts
rawopts
    prettyTables :: Bool
prettyTables = Bool
pretty_
    makeQuery :: [Char] -> m Query
makeQuery [Char]
flag = do
        Query
q <- ([Char] -> m Query)
-> ((Query, [QueryOpt]) -> m Query)
-> Either [Char] (Query, [QueryOpt])
-> m Query
forall a c b. (a -> c) -> (b -> c) -> Either a b -> c
either [Char] -> m Query
forall a. [Char] -> a
usageError (Query -> m Query
forall a. a -> m a
forall (m :: * -> *) a. Monad m => a -> m a
return (Query -> m Query)
-> ((Query, [QueryOpt]) -> Query) -> (Query, [QueryOpt]) -> m Query
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Query, [QueryOpt]) -> Query
forall a b. (a, b) -> a
fst) (Either [Char] (Query, [QueryOpt]) -> m Query)
-> ([Char] -> Either [Char] (Query, [QueryOpt]))
-> [Char]
-> m Query
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Day -> Text -> Either [Char] (Query, [QueryOpt])
parseQuery Day
today (Text -> Either [Char] (Query, [QueryOpt]))
-> ([Char] -> Text) -> [Char] -> Either [Char] (Query, [QueryOpt])
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Char] -> Text
T.pack ([Char] -> m Query) -> [Char] -> m Query
forall a b. (a -> b) -> a -> b
$ [Char] -> RawOpts -> [Char]
stringopt [Char]
flag RawOpts
rawopts
        Query -> m Query
forall a. a -> m a
forall (m :: * -> *) a. Monad m => a -> m a
return (Query -> m Query) -> (Query -> Query) -> Query -> m Query
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Query -> Query
simplifyQuery (Query -> m Query) -> Query -> m Query
forall a b. (a -> b) -> a -> b
$ [Query] -> Query
And [ReportOpts -> Query
queryFromFlags ReportOpts
ropts{period_=PeriodAll}, Query
q]

  Query
investmentsQuery <- [Char] -> IO Query
forall {m :: * -> *}. Monad m => [Char] -> m Query
makeQuery [Char]
"investment"
  Query
pnlQuery         <- [Char] -> IO Query
forall {m :: * -> *}. Monad m => [Char] -> m Query
makeQuery [Char]
"pnl"

  let
    filteredj :: Journal
filteredj = Query -> Journal -> Journal
filterJournalTransactions Query
investmentsQuery Journal
j
    trans :: [Transaction]
trans = [Char] -> [Transaction] -> [Transaction]
forall a. Show a => [Char] -> a -> a
dbg3 [Char]
"investments" ([Transaction] -> [Transaction]) -> [Transaction] -> [Transaction]
forall a b. (a -> b) -> a -> b
$ Journal -> [Transaction]
jtxns Journal
filteredj

  Bool -> IO () -> IO ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
when ([Transaction] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [Transaction]
trans) (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ do
    [Char] -> IO ()
putStrLn [Char]
"No relevant transactions found. Check your investments query"
    IO ()
forall a. IO a
exitFailure

  let (DateSpan
fullPeriod, [DateSpan]
spans) = Journal -> ReportSpec -> (DateSpan, [DateSpan])
reportSpan Journal
filteredj ReportSpec
rspec

  let priceDirectiveDates :: [Day]
priceDirectiveDates = [Char] -> [Day] -> [Day]
forall a. Show a => [Char] -> a -> a
dbg3 [Char]
"priceDirectiveDates" ([Day] -> [Day]) -> [Day] -> [Day]
forall a b. (a -> b) -> a -> b
$ (PriceDirective -> Day) -> [PriceDirective] -> [Day]
forall a b. (a -> b) -> [a] -> [b]
map PriceDirective -> Day
pddate ([PriceDirective] -> [Day]) -> [PriceDirective] -> [Day]
forall a b. (a -> b) -> a -> b
$ Journal -> [PriceDirective]
jpricedirectives Journal
j

  let processSpan :: DateSpan -> IO [Text]
processSpan (DateSpan Maybe EFDay
Nothing Maybe EFDay
_) = [Char] -> IO [Text]
forall a. HasCallStack => [Char] -> a
error [Char]
"Undefined start of the period - will be unable to compute the rates of return"
      processSpan (DateSpan Maybe EFDay
_ Maybe EFDay
Nothing) = [Char] -> IO [Text]
forall a. HasCallStack => [Char] -> a
error [Char]
"Undefined end of the period - will be unable to compute the rates of return"
      processSpan spn :: DateSpan
spn@(DateSpan (Just EFDay
begin) (Just EFDay
end)) = do
        -- Spans are [begin,end), and end is 1 day after the actual end date we are interested in
        let
          b :: Day
b = EFDay -> Day
fromEFDay EFDay
begin
          e :: Day
e = EFDay -> Day
fromEFDay EFDay
end
          cashFlowApplyCostValue :: [(Day, MixedAmount)] -> [(Day, MixedAmount)]
cashFlowApplyCostValue = ((Day, MixedAmount) -> (Day, MixedAmount))
-> [(Day, MixedAmount)] -> [(Day, MixedAmount)]
forall a b. (a -> b) -> [a] -> [b]
map (\(Day
d,MixedAmount
amt) -> (Day
d,Day -> Day -> MixedAmount -> MixedAmount
mixedAmountValue Day
e Day
d MixedAmount
amt))

          valueBefore :: MixedAmount
valueBefore =
            Day -> Day -> MixedAmount -> MixedAmount
mixedAmountValue Day
e Day
b (MixedAmount -> MixedAmount) -> MixedAmount -> MixedAmount
forall a b. (a -> b) -> a -> b
$
            [Transaction] -> Query -> MixedAmount
total [Transaction]
trans ([Query] -> Query
And [ Query
investmentsQuery
                             , DateSpan -> Query
Date (Maybe EFDay -> Maybe EFDay -> DateSpan
DateSpan Maybe EFDay
forall a. Maybe a
Nothing (EFDay -> Maybe EFDay
forall a. a -> Maybe a
Just EFDay
begin))])

          valueAfter :: MixedAmount
valueAfter  =
            Day -> Day -> MixedAmount -> MixedAmount
mixedAmountValue Day
e Day
e (MixedAmount -> MixedAmount) -> MixedAmount -> MixedAmount
forall a b. (a -> b) -> a -> b
$
            [Transaction] -> Query -> MixedAmount
total [Transaction]
trans ([Query] -> Query
And [Query
investmentsQuery
                             , DateSpan -> Query
Date (Maybe EFDay -> Maybe EFDay -> DateSpan
DateSpan Maybe EFDay
forall a. Maybe a
Nothing (EFDay -> Maybe EFDay
forall a. a -> Maybe a
Just EFDay
end))])

          priceDates :: [Day]
priceDates = [Char] -> [Day] -> [Day]
forall a. Show a => [Char] -> a -> a
dbg3 [Char]
"priceDates" ([Day] -> [Day]) -> [Day] -> [Day]
forall a b. (a -> b) -> a -> b
$ [Day] -> [Day]
forall a. Eq a => [a] -> [a]
nub ([Day] -> [Day]) -> [Day] -> [Day]
forall a b. (a -> b) -> a -> b
$ (Day -> Bool) -> [Day] -> [Day]
forall a. (a -> Bool) -> [a] -> [a]
filter (DateSpan -> Day -> Bool
spanContainsDate DateSpan
spn) [Day]
priceDirectiveDates
          cashFlow :: [(Day, MixedAmount)]
cashFlow =
            (((Day -> (Day, MixedAmount)) -> [Day] -> [(Day, MixedAmount)]
forall a b. (a -> b) -> [a] -> [b]
map (,MixedAmount
nullmixedamt) [Day]
priceDates)[(Day, MixedAmount)]
-> [(Day, MixedAmount)] -> [(Day, MixedAmount)]
forall a. [a] -> [a] -> [a]
++) ([(Day, MixedAmount)] -> [(Day, MixedAmount)])
-> [(Day, MixedAmount)] -> [(Day, MixedAmount)]
forall a b. (a -> b) -> a -> b
$
            [(Day, MixedAmount)] -> [(Day, MixedAmount)]
cashFlowApplyCostValue ([(Day, MixedAmount)] -> [(Day, MixedAmount)])
-> [(Day, MixedAmount)] -> [(Day, MixedAmount)]
forall a b. (a -> b) -> a -> b
$
            WhichDate -> [Transaction] -> Query -> [(Day, MixedAmount)]
calculateCashFlow WhichDate
wd [Transaction]
trans ([Query] -> Query
And [ Query -> Query
Not Query
investmentsQuery
                                            , Query -> Query
Not Query
pnlQuery
                                            , DateSpan -> Query
Date DateSpan
spn ] )

          pnl :: [(Day, MixedAmount)]
pnl =
            [(Day, MixedAmount)] -> [(Day, MixedAmount)]
cashFlowApplyCostValue ([(Day, MixedAmount)] -> [(Day, MixedAmount)])
-> [(Day, MixedAmount)] -> [(Day, MixedAmount)]
forall a b. (a -> b) -> a -> b
$
            WhichDate -> [Transaction] -> Query -> [(Day, MixedAmount)]
calculateCashFlow WhichDate
wd [Transaction]
trans ([Query] -> Query
And [ Query -> Query
Not Query
investmentsQuery
                                            , Query
pnlQuery
                                            , DateSpan -> Query
Date DateSpan
spn ] )

          thisSpan :: OneSpan
thisSpan = [Char] -> OneSpan -> OneSpan
forall a. Show a => [Char] -> a -> a
dbg3 [Char]
"processing span" (OneSpan -> OneSpan) -> OneSpan -> OneSpan
forall a b. (a -> b) -> a -> b
$
                     Day
-> Day
-> MixedAmount
-> MixedAmount
-> [(Day, MixedAmount)]
-> [(Day, MixedAmount)]
-> OneSpan
OneSpan Day
b Day
e MixedAmount
valueBefore MixedAmount
valueAfter [(Day, MixedAmount)]
cashFlow [(Day, MixedAmount)]
pnl

        Double
irr <- Map Text AmountStyle -> Bool -> Bool -> OneSpan -> IO Double
internalRateOfReturn Map Text AmountStyle
styles Bool
showCashFlow Bool
prettyTables OneSpan
thisSpan
        (Double
periodTwr, Double
annualizedTwr) <- Map Text AmountStyle
-> Bool
-> Bool
-> Query
-> [Transaction]
-> (Day -> Day -> MixedAmount -> MixedAmount)
-> OneSpan
-> IO (Double, Double)
timeWeightedReturn Map Text AmountStyle
styles Bool
showCashFlow Bool
prettyTables Query
investmentsQuery [Transaction]
trans Day -> Day -> MixedAmount -> MixedAmount
mixedAmountValue OneSpan
thisSpan
        let cashFlowAmt :: MixedAmount
cashFlowAmt = MixedAmount -> MixedAmount
maNegate (MixedAmount -> MixedAmount)
-> ([MixedAmount] -> MixedAmount) -> [MixedAmount] -> MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [MixedAmount] -> MixedAmount
forall (t :: * -> *). Foldable t => t MixedAmount -> MixedAmount
maSum ([MixedAmount] -> MixedAmount) -> [MixedAmount] -> MixedAmount
forall a b. (a -> b) -> a -> b
$ ((Day, MixedAmount) -> MixedAmount)
-> [(Day, MixedAmount)] -> [MixedAmount]
forall a b. (a -> b) -> [a] -> [b]
map (Day, MixedAmount) -> MixedAmount
forall a b. (a, b) -> b
snd [(Day, MixedAmount)]
cashFlow
        let smallIsZero :: a -> a
smallIsZero a
x = if a -> a
forall a. Num a => a -> a
abs a
x a -> a -> Bool
forall a. Ord a => a -> a -> Bool
< a
0.01 then a
0.0 else a
x
        [Text] -> IO [Text]
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return [ Day -> Text
showDate Day
b
               , Day -> Text
showDate (Integer -> Day -> Day
addDays (-Integer
1) Day
e)
               , [Char] -> Text
T.pack ([Char] -> Text) -> [Char] -> Text
forall a b. (a -> b) -> a -> b
$ MixedAmount -> [Char]
showMixedAmount (MixedAmount -> [Char]) -> MixedAmount -> [Char]
forall a b. (a -> b) -> a -> b
$ Map Text AmountStyle -> MixedAmount -> MixedAmount
forall a. HasAmounts a => Map Text AmountStyle -> a -> a
styleAmounts Map Text AmountStyle
styles (MixedAmount -> MixedAmount) -> MixedAmount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ MixedAmount
valueBefore
               , [Char] -> Text
T.pack ([Char] -> Text) -> [Char] -> Text
forall a b. (a -> b) -> a -> b
$ MixedAmount -> [Char]
showMixedAmount (MixedAmount -> [Char]) -> MixedAmount -> [Char]
forall a b. (a -> b) -> a -> b
$ Map Text AmountStyle -> MixedAmount -> MixedAmount
forall a. HasAmounts a => Map Text AmountStyle -> a -> a
styleAmounts Map Text AmountStyle
styles (MixedAmount -> MixedAmount) -> MixedAmount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ MixedAmount
cashFlowAmt
               -- , T.pack $ showMixedAmount $
               --   -- dbg0With (lbl "cashflow after styling".showMixedAmountOneLine) $
               --   mapMixedAmount (amountSetFullPrecisionOr (Just defaultMaxPrecision)) $
               --   styleAmounts (styles
               --                 -- & dbg0With (lbl "styles".show))
               --   cashFlowAmt
               --   -- & dbg0With (lbl "cashflow before styling".showMixedAmountOneLine)
               , [Char] -> Text
T.pack ([Char] -> Text) -> [Char] -> Text
forall a b. (a -> b) -> a -> b
$ MixedAmount -> [Char]
showMixedAmount (MixedAmount -> [Char]) -> MixedAmount -> [Char]
forall a b. (a -> b) -> a -> b
$ Map Text AmountStyle -> MixedAmount -> MixedAmount
forall a. HasAmounts a => Map Text AmountStyle -> a -> a
styleAmounts Map Text AmountStyle
styles (MixedAmount -> MixedAmount) -> MixedAmount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ MixedAmount
valueAfter
               , [Char] -> Text
T.pack ([Char] -> Text) -> [Char] -> Text
forall a b. (a -> b) -> a -> b
$ MixedAmount -> [Char]
showMixedAmount (MixedAmount -> [Char]) -> MixedAmount -> [Char]
forall a b. (a -> b) -> a -> b
$ Map Text AmountStyle -> MixedAmount -> MixedAmount
forall a. HasAmounts a => Map Text AmountStyle -> a -> a
styleAmounts Map Text AmountStyle
styles (MixedAmount -> MixedAmount) -> MixedAmount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ (MixedAmount
valueAfter MixedAmount -> MixedAmount -> MixedAmount
`maMinus` (MixedAmount
valueBefore MixedAmount -> MixedAmount -> MixedAmount
`maPlus` MixedAmount
cashFlowAmt))
               , [Char] -> Text
T.pack ([Char] -> Text) -> [Char] -> Text
forall a b. (a -> b) -> a -> b
$ [Char] -> Double -> [Char]
forall r. PrintfType r => [Char] -> r
printf [Char]
"%0.2f%%" (Double -> [Char]) -> Double -> [Char]
forall a b. (a -> b) -> a -> b
$ Double -> Double
forall {a}. (Ord a, Fractional a) => a -> a
smallIsZero Double
irr
               , [Char] -> Text
T.pack ([Char] -> Text) -> [Char] -> Text
forall a b. (a -> b) -> a -> b
$ [Char] -> Double -> [Char]
forall r. PrintfType r => [Char] -> r
printf [Char]
"%0.2f%%" (Double -> [Char]) -> Double -> [Char]
forall a b. (a -> b) -> a -> b
$ Double -> Double
forall {a}. (Ord a, Fractional a) => a -> a
smallIsZero Double
periodTwr
               , [Char] -> Text
T.pack ([Char] -> Text) -> [Char] -> Text
forall a b. (a -> b) -> a -> b
$ [Char] -> Double -> [Char]
forall r. PrintfType r => [Char] -> r
printf [Char]
"%0.2f%%" (Double -> [Char]) -> Double -> [Char]
forall a b. (a -> b) -> a -> b
$ Double -> Double
forall {a}. (Ord a, Fractional a) => a -> a
smallIsZero Double
annualizedTwr ]

  [[Text]]
periodRows <- [DateSpan] -> (DateSpan -> IO [Text]) -> IO [[Text]]
forall (t :: * -> *) (m :: * -> *) a b.
(Traversable t, Monad m) =>
t a -> (a -> m b) -> m (t b)
forM [DateSpan]
spans DateSpan -> IO [Text]
processSpan
  [Text]
totalRow <- DateSpan -> IO [Text]
processSpan DateSpan
fullPeriod

  let rowTitles :: Header Text
rowTitles = Properties -> [Header Text] -> Header Text
forall h. Properties -> [Header h] -> Header h
Tab.Group Properties
Tab.NoLine ((Integer -> Header Text) -> [Integer] -> [Header Text]
forall a b. (a -> b) -> [a] -> [b]
map (Text -> Header Text
forall h. h -> Header h
Header (Text -> Header Text)
-> (Integer -> Text) -> Integer -> Header Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Char] -> Text
T.pack ([Char] -> Text) -> (Integer -> [Char]) -> Integer -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Integer -> [Char]
forall a. Show a => a -> [Char]
show) (Int -> [Integer] -> [Integer]
forall a. Int -> [a] -> [a]
take ([[Text]] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [[Text]]
periodRows) [Integer
1..]))

  let isSingleSpan :: Bool
isSingleSpan = [DateSpan] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [DateSpan]
spans Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
== Int
1

  let table :: Table Text Text Text
table = Header Text -> Header Text -> [[Text]] -> Table Text Text Text
forall rh ch a. Header rh -> Header ch -> [[a]] -> Table rh ch a
Table
              (if Bool
isSingleSpan
                then Header Text
rowTitles
                else Properties -> [Header Text] -> Header Text
forall h. Properties -> [Header h] -> Header h
Tab.Group Properties
Tab.SingleLine  [ Header Text
rowTitles, Properties -> [Header Text] -> Header Text
forall h. Properties -> [Header h] -> Header h
Tab.Group Properties
Tab.NoLine [ Text -> Header Text
forall h. h -> Header h
Header Text
"Total" ]]
              )
              (Properties -> [Header Text] -> Header Text
forall h. Properties -> [Header h] -> Header h
Tab.Group Properties
Tab.DoubleLine
               [ Properties -> [Header Text] -> Header Text
forall h. Properties -> [Header h] -> Header h
Tab.Group Properties
Tab.SingleLine [Text -> Header Text
forall h. h -> Header h
Header Text
"Begin", Text -> Header Text
forall h. h -> Header h
Header Text
"End"]
               , Properties -> [Header Text] -> Header Text
forall h. Properties -> [Header h] -> Header h
Tab.Group Properties
Tab.SingleLine [Text -> Header Text
forall h. h -> Header h
Header Text
"Value (begin)", Text -> Header Text
forall h. h -> Header h
Header Text
"Cashflow", Text -> Header Text
forall h. h -> Header h
Header Text
"Value (end)", Text -> Header Text
forall h. h -> Header h
Header Text
"PnL"]
               , Properties -> [Header Text] -> Header Text
forall h. Properties -> [Header h] -> Header h
Tab.Group Properties
Tab.SingleLine [Text -> Header Text
forall h. h -> Header h
Header Text
"IRR"]
               , Properties -> [Header Text] -> Header Text
forall h. Properties -> [Header h] -> Header h
Tab.Group Properties
Tab.SingleLine [Text -> Header Text
forall h. h -> Header h
Header Text
"TWR/period", Text -> Header Text
forall h. h -> Header h
Header Text
"TWR/year"]])
              (if Bool
isSingleSpan then [[Text]]
periodRows else [[Text]]
periodRows [[Text]] -> [[Text]] -> [[Text]]
forall a. [a] -> [a] -> [a]
++ [[Text]
totalRow])

  Text -> IO ()
TL.putStrLn (Text -> IO ()) -> Text -> IO ()
forall a b. (a -> b) -> a -> b
$ Bool
-> (Text -> Text)
-> (Text -> Text)
-> (Text -> Text)
-> Table Text Text Text
-> Text
forall a rh ch.
Show a =>
Bool
-> (rh -> Text)
-> (ch -> Text)
-> (a -> Text)
-> Table rh ch a
-> Text
Tab.render Bool
prettyTables Text -> Text
forall a. a -> a
id Text -> Text
forall a. a -> a
id Text -> Text
forall a. a -> a
id Table Text Text Text
table

timeWeightedReturn :: Map Text AmountStyle
-> Bool
-> Bool
-> Query
-> [Transaction]
-> (Day -> Day -> MixedAmount -> MixedAmount)
-> OneSpan
-> IO (Double, Double)
timeWeightedReturn Map Text AmountStyle
styles Bool
showCashFlow Bool
prettyTables Query
investmentsQuery [Transaction]
trans Day -> Day -> MixedAmount -> MixedAmount
mixedAmountValue (OneSpan Day
begin Day
end MixedAmount
valueBeforeAmt MixedAmount
valueAfter [(Day, MixedAmount)]
cashFlow [(Day, MixedAmount)]
pnl) = do
  let valueBefore :: Decimal
valueBefore = MixedAmount -> Decimal
unMix MixedAmount
valueBeforeAmt
  let initialUnitPrice :: Decimal
initialUnitPrice = Decimal
100 :: Decimal
  let initialUnits :: Decimal
initialUnits = Decimal
valueBefore Decimal -> Decimal -> Decimal
forall a. Fractional a => a -> a -> a
/ Decimal
initialUnitPrice
  let changes :: [(Day, Either MixedAmount MixedAmount)]
changes =
        -- If cash flow and PnL changes happen on the same day, this
        -- will sort PnL changes to come before cash flows (on any
        -- given day), so that we will have better unit price computed
        -- first for processing cash flow. This is why pnl changes are Left
        -- and cashflows are Right.
        -- However, if the very first date in the changes list has both
        -- PnL and CashFlow, we would not be able to apply pnl change to 0 unit,
        -- which would lead to an error. We make sure that we have at least one
        -- cashflow entry at the front, and we know that there would be at most
        -- one for the given date, by construction. Empty CashFlows added
        -- because of a begin date before the first transaction are not seen as
        -- a valid cashflow entry at the front.
        [(Day, Either MixedAmount MixedAmount)]
-> [(Day, Either MixedAmount MixedAmount)]
forall {a} {a}.
[(a, Either a MixedAmount)] -> [(a, Either a MixedAmount)]
zeroUnitsNeedsCashflowAtTheFront
        ([(Day, Either MixedAmount MixedAmount)]
 -> [(Day, Either MixedAmount MixedAmount)])
-> [(Day, Either MixedAmount MixedAmount)]
-> [(Day, Either MixedAmount MixedAmount)]
forall a b. (a -> b) -> a -> b
$ [(Day, Either MixedAmount MixedAmount)]
-> [(Day, Either MixedAmount MixedAmount)]
forall a. Ord a => [a] -> [a]
sort
        ([(Day, Either MixedAmount MixedAmount)]
 -> [(Day, Either MixedAmount MixedAmount)])
-> [(Day, Either MixedAmount MixedAmount)]
-> [(Day, Either MixedAmount MixedAmount)]
forall a b. (a -> b) -> a -> b
$ [(Day, Either MixedAmount MixedAmount)]
forall {a}. [(Day, Either a MixedAmount)]
datedCashflows [(Day, Either MixedAmount MixedAmount)]
-> [(Day, Either MixedAmount MixedAmount)]
-> [(Day, Either MixedAmount MixedAmount)]
forall a. [a] -> [a] -> [a]
++ [(Day, Either MixedAmount MixedAmount)]
forall {b}. [(Day, Either MixedAmount b)]
datedPnls
        where
          zeroUnitsNeedsCashflowAtTheFront :: [(a, Either a MixedAmount)] -> [(a, Either a MixedAmount)]
zeroUnitsNeedsCashflowAtTheFront [(a, Either a MixedAmount)]
changes1 =
            if Decimal
initialUnits Decimal -> Decimal -> Bool
forall a. Ord a => a -> a -> Bool
> Decimal
0 then [(a, Either a MixedAmount)]
changes1
            else
              let ([(a, Either a MixedAmount)]
leadingEmptyCashFlows, [(a, Either a MixedAmount)]
rest) = ((a, Either a MixedAmount) -> Bool)
-> [(a, Either a MixedAmount)]
-> ([(a, Either a MixedAmount)], [(a, Either a MixedAmount)])
forall a. (a -> Bool) -> [a] -> ([a], [a])
span (a, Either a MixedAmount) -> Bool
forall {a} {a}. (a, Either a MixedAmount) -> Bool
isEmptyCashflow [(a, Either a MixedAmount)]
changes1
                  ([(a, Either a MixedAmount)]
leadingPnls, [(a, Either a MixedAmount)]
rest') = ((a, Either a MixedAmount) -> Bool)
-> [(a, Either a MixedAmount)]
-> ([(a, Either a MixedAmount)], [(a, Either a MixedAmount)])
forall a. (a -> Bool) -> [a] -> ([a], [a])
span (Either a MixedAmount -> Bool
forall a b. Either a b -> Bool
isLeft (Either a MixedAmount -> Bool)
-> ((a, Either a MixedAmount) -> Either a MixedAmount)
-> (a, Either a MixedAmount)
-> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (a, Either a MixedAmount) -> Either a MixedAmount
forall a b. (a, b) -> b
snd) [(a, Either a MixedAmount)]
rest
                  ([(a, Either a MixedAmount)]
firstCashflow, [(a, Either a MixedAmount)]
rest'') = Int
-> [(a, Either a MixedAmount)]
-> ([(a, Either a MixedAmount)], [(a, Either a MixedAmount)])
forall a. Int -> [a] -> ([a], [a])
splitAt Int
1 [(a, Either a MixedAmount)]
rest'
              in [(a, Either a MixedAmount)]
leadingEmptyCashFlows [(a, Either a MixedAmount)]
-> [(a, Either a MixedAmount)] -> [(a, Either a MixedAmount)]
forall a. [a] -> [a] -> [a]
++ [(a, Either a MixedAmount)]
firstCashflow [(a, Either a MixedAmount)]
-> [(a, Either a MixedAmount)] -> [(a, Either a MixedAmount)]
forall a. [a] -> [a] -> [a]
++ [(a, Either a MixedAmount)]
leadingPnls [(a, Either a MixedAmount)]
-> [(a, Either a MixedAmount)] -> [(a, Either a MixedAmount)]
forall a. [a] -> [a] -> [a]
++ [(a, Either a MixedAmount)]
rest''

          isEmptyCashflow :: (a, Either a MixedAmount) -> Bool
isEmptyCashflow (a
_date, Either a MixedAmount
amt) = case Either a MixedAmount
amt of
            Right MixedAmount
amt' -> MixedAmount -> Bool
mixedAmountIsZero MixedAmount
amt'
            Left a
_     -> Bool
False

          datedPnls :: [(Day, Either MixedAmount b)]
datedPnls = ((Day, MixedAmount) -> (Day, Either MixedAmount b))
-> [(Day, MixedAmount)] -> [(Day, Either MixedAmount b)]
forall a b. (a -> b) -> [a] -> [b]
map ((MixedAmount -> Either MixedAmount b)
-> (Day, MixedAmount) -> (Day, Either MixedAmount b)
forall b c a. (b -> c) -> (a, b) -> (a, c)
forall (p :: * -> * -> *) b c a.
Bifunctor p =>
(b -> c) -> p a b -> p a c
second MixedAmount -> Either MixedAmount b
forall a b. a -> Either a b
Left) ([(Day, MixedAmount)] -> [(Day, Either MixedAmount b)])
-> [(Day, MixedAmount)] -> [(Day, Either MixedAmount b)]
forall a b. (a -> b) -> a -> b
$ [(Day, MixedAmount)] -> [(Day, MixedAmount)]
forall {b}. Ord b => [(b, MixedAmount)] -> [(b, MixedAmount)]
aggregateByDate [(Day, MixedAmount)]
pnl

          datedCashflows :: [(Day, Either a MixedAmount)]
datedCashflows = ((Day, MixedAmount) -> (Day, Either a MixedAmount))
-> [(Day, MixedAmount)] -> [(Day, Either a MixedAmount)]
forall a b. (a -> b) -> [a] -> [b]
map ((MixedAmount -> Either a MixedAmount)
-> (Day, MixedAmount) -> (Day, Either a MixedAmount)
forall b c a. (b -> c) -> (a, b) -> (a, c)
forall (p :: * -> * -> *) b c a.
Bifunctor p =>
(b -> c) -> p a b -> p a c
second MixedAmount -> Either a MixedAmount
forall a b. b -> Either a b
Right) ([(Day, MixedAmount)] -> [(Day, Either a MixedAmount)])
-> [(Day, MixedAmount)] -> [(Day, Either a MixedAmount)]
forall a b. (a -> b) -> a -> b
$ [(Day, MixedAmount)] -> [(Day, MixedAmount)]
forall {b}. Ord b => [(b, MixedAmount)] -> [(b, MixedAmount)]
aggregateByDate [(Day, MixedAmount)]
cashFlow

          aggregateByDate :: [(b, MixedAmount)] -> [(b, MixedAmount)]
aggregateByDate [(b, MixedAmount)]
datedAmounts =
            -- Aggregate all entries for a single day, assuming that intraday interest is negligible
            [(b, MixedAmount)] -> [(b, MixedAmount)]
forall a. Ord a => [a] -> [a]
sort
            ([(b, MixedAmount)] -> [(b, MixedAmount)])
-> [(b, MixedAmount)] -> [(b, MixedAmount)]
forall a b. (a -> b) -> a -> b
$ ([(b, MixedAmount)] -> (b, MixedAmount))
-> [[(b, MixedAmount)]] -> [(b, MixedAmount)]
forall a b. (a -> b) -> [a] -> [b]
map (\[(b, MixedAmount)]
date_cash -> let ([b]
dates, [MixedAmount]
cash) = [(b, MixedAmount)] -> ([b], [MixedAmount])
forall a b. [(a, b)] -> ([a], [b])
unzip [(b, MixedAmount)]
date_cash in ([b] -> b
forall a. HasCallStack => [a] -> a
head [b]
dates, [MixedAmount] -> MixedAmount
forall (t :: * -> *). Foldable t => t MixedAmount -> MixedAmount
maSum [MixedAmount]
cash))
            ([[(b, MixedAmount)]] -> [(b, MixedAmount)])
-> [[(b, MixedAmount)]] -> [(b, MixedAmount)]
forall a b. (a -> b) -> a -> b
$ ((b, MixedAmount) -> (b, MixedAmount) -> Bool)
-> [(b, MixedAmount)] -> [[(b, MixedAmount)]]
forall a. (a -> a -> Bool) -> [a] -> [[a]]
groupBy (b -> b -> Bool
forall a. Eq a => a -> a -> Bool
(==) (b -> b -> Bool)
-> ((b, MixedAmount) -> b)
-> (b, MixedAmount)
-> (b, MixedAmount)
-> Bool
forall b c a. (b -> b -> c) -> (a -> b) -> a -> a -> c
`on` (b, MixedAmount) -> b
forall a b. (a, b) -> a
fst)
            ([(b, MixedAmount)] -> [[(b, MixedAmount)]])
-> [(b, MixedAmount)] -> [[(b, MixedAmount)]]
forall a b. (a -> b) -> a -> b
$ ((b, MixedAmount) -> b) -> [(b, MixedAmount)] -> [(b, MixedAmount)]
forall b a. Ord b => (a -> b) -> [a] -> [a]
sortOn (b, MixedAmount) -> b
forall a b. (a, b) -> a
fst
            ([(b, MixedAmount)] -> [(b, MixedAmount)])
-> [(b, MixedAmount)] -> [(b, MixedAmount)]
forall a b. (a -> b) -> a -> b
$ ((b, MixedAmount) -> (b, MixedAmount))
-> [(b, MixedAmount)] -> [(b, MixedAmount)]
forall a b. (a -> b) -> [a] -> [b]
map ((MixedAmount -> MixedAmount)
-> (b, MixedAmount) -> (b, MixedAmount)
forall b c a. (b -> c) -> (a, b) -> (a, c)
forall (p :: * -> * -> *) b c a.
Bifunctor p =>
(b -> c) -> p a b -> p a c
second MixedAmount -> MixedAmount
maNegate)
            ([(b, MixedAmount)] -> [(b, MixedAmount)])
-> [(b, MixedAmount)] -> [(b, MixedAmount)]
forall a b. (a -> b) -> a -> b
$ [(b, MixedAmount)]
datedAmounts

  let units :: [(Decimal, Decimal, Decimal, Decimal)]
units =
        [(Decimal, Decimal, Decimal, Decimal)]
-> [(Decimal, Decimal, Decimal, Decimal)]
forall a. HasCallStack => [a] -> [a]
tail ([(Decimal, Decimal, Decimal, Decimal)]
 -> [(Decimal, Decimal, Decimal, Decimal)])
-> [(Decimal, Decimal, Decimal, Decimal)]
-> [(Decimal, Decimal, Decimal, Decimal)]
forall a b. (a -> b) -> a -> b
$
        ((Decimal, Decimal, Decimal, Decimal)
 -> (Day, Either MixedAmount MixedAmount)
 -> (Decimal, Decimal, Decimal, Decimal))
-> (Decimal, Decimal, Decimal, Decimal)
-> [(Day, Either MixedAmount MixedAmount)]
-> [(Decimal, Decimal, Decimal, Decimal)]
forall b a. (b -> a -> b) -> b -> [a] -> [b]
scanl
          (\(Decimal
_, Decimal
_, Decimal
unitPrice, Decimal
unitBalance) (Day
date, Either MixedAmount MixedAmount
amt) ->
             let valueOnDate :: Decimal
valueOnDate = MixedAmount -> Decimal
unMix (MixedAmount -> Decimal) -> MixedAmount -> Decimal
forall a b. (a -> b) -> a -> b
$ Day -> Day -> MixedAmount -> MixedAmount
mixedAmountValue Day
end Day
date (MixedAmount -> MixedAmount) -> MixedAmount -> MixedAmount
forall a b. (a -> b) -> a -> b
$ [Transaction] -> Query -> MixedAmount
total [Transaction]
trans ([Query] -> Query
And [Query
investmentsQuery, DateSpan -> Query
Date (Maybe EFDay -> Maybe EFDay -> DateSpan
DateSpan Maybe EFDay
forall a. Maybe a
Nothing (EFDay -> Maybe EFDay
forall a. a -> Maybe a
Just (EFDay -> Maybe EFDay) -> EFDay -> Maybe EFDay
forall a b. (a -> b) -> a -> b
$ Day -> EFDay
Exact Day
date))])
             in
             case Either MixedAmount MixedAmount
amt of
               Right MixedAmount
amt' ->
                 -- we are buying or selling
                 let unitsBoughtOrSold :: Decimal
unitsBoughtOrSold = MixedAmount -> Decimal
unMix MixedAmount
amt' Decimal -> Decimal -> Decimal
forall a. Fractional a => a -> a -> a
/ Decimal
unitPrice
                 in (Decimal
valueOnDate, Decimal
unitsBoughtOrSold, Decimal
unitPrice, Decimal
unitBalance Decimal -> Decimal -> Decimal
forall a. Num a => a -> a -> a
+ Decimal
unitsBoughtOrSold)
               Left MixedAmount
pnl' ->
                 -- PnL change
                 let valueAfterDate :: Decimal
valueAfterDate = Decimal
valueOnDate Decimal -> Decimal -> Decimal
forall a. Num a => a -> a -> a
+ MixedAmount -> Decimal
unMix MixedAmount
pnl'
                     unitPrice' :: Decimal
unitPrice' = Decimal
valueAfterDateDecimal -> Decimal -> Decimal
forall a. Fractional a => a -> a -> a
/Decimal
unitBalance
                 in (Decimal
valueOnDate, Decimal
0, Decimal
unitPrice', Decimal
unitBalance))
          (Decimal
0, Decimal
0, Decimal
initialUnitPrice, Decimal
initialUnits)
          ([(Day, Either MixedAmount MixedAmount)]
 -> [(Decimal, Decimal, Decimal, Decimal)])
-> [(Day, Either MixedAmount MixedAmount)]
-> [(Decimal, Decimal, Decimal, Decimal)]
forall a b. (a -> b) -> a -> b
$ [Char]
-> [(Day, Either MixedAmount MixedAmount)]
-> [(Day, Either MixedAmount MixedAmount)]
forall a. Show a => [Char] -> a -> a
dbg3 [Char]
"changes" [(Day, Either MixedAmount MixedAmount)]
changes

  let finalUnitBalance :: Decimal
finalUnitBalance = if [(Decimal, Decimal, Decimal, Decimal)] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [(Decimal, Decimal, Decimal, Decimal)]
units then Decimal
initialUnits else let (Decimal
_,Decimal
_,Decimal
_,Decimal
u) = [(Decimal, Decimal, Decimal, Decimal)]
-> (Decimal, Decimal, Decimal, Decimal)
forall a. HasCallStack => [a] -> a
last [(Decimal, Decimal, Decimal, Decimal)]
units in Decimal
u
      finalUnitPrice :: Decimal
finalUnitPrice = if Decimal
finalUnitBalance Decimal -> Decimal -> Bool
forall a. Eq a => a -> a -> Bool
== Decimal
0 then
                         if [(Decimal, Decimal, Decimal, Decimal)] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [(Decimal, Decimal, Decimal, Decimal)]
units then Decimal
initialUnitPrice
                         else let (Decimal
_,Decimal
_,Decimal
lastUnitPrice,Decimal
_) = [(Decimal, Decimal, Decimal, Decimal)]
-> (Decimal, Decimal, Decimal, Decimal)
forall a. HasCallStack => [a] -> a
last [(Decimal, Decimal, Decimal, Decimal)]
units in Decimal
lastUnitPrice
                       else (MixedAmount -> Decimal
unMix MixedAmount
valueAfter) Decimal -> Decimal -> Decimal
forall a. Fractional a => a -> a -> a
/ Decimal
finalUnitBalance
      -- Technically, totalTWR should be (100*(finalUnitPrice - initialUnitPrice) / initialUnitPrice), but initalUnitPrice is 100, so 100/100 == 1
      totalTWR :: Decimal
totalTWR = Word8 -> Decimal -> Decimal
forall i. Integral i => Word8 -> DecimalRaw i -> DecimalRaw i
roundTo Word8
2 (Decimal -> Decimal) -> Decimal -> Decimal
forall a b. (a -> b) -> a -> b
$ (Decimal
finalUnitPrice Decimal -> Decimal -> Decimal
forall a. Num a => a -> a -> a
- Decimal
initialUnitPrice)
      (Integer
startYear, Int
_, Int
_) = Day -> (Integer, Int, Int)
toGregorian Day
begin
      years :: Double
years = Integer -> Double
forall a b. (Integral a, Num b) => a -> b
fromIntegral (Day -> Day -> Integer
diffDays Day
end Day
begin) Double -> Double -> Double
forall a. Fractional a => a -> a -> a
/ (if Integer -> Bool
isLeapYear Integer
startYear then Double
366 else Double
365) :: Double
      annualizedTWR :: Double
annualizedTWR = Double
100Double -> Double -> Double
forall a. Num a => a -> a -> a
*((Double
1Double -> Double -> Double
forall a. Num a => a -> a -> a
+(Decimal -> Double
forall a b. (Real a, Fractional b) => a -> b
realToFrac Decimal
totalTWRDouble -> Double -> Double
forall a. Fractional a => a -> a -> a
/Double
100))Double -> Double -> Double
forall a. Floating a => a -> a -> a
**(Double
1Double -> Double -> Double
forall a. Fractional a => a -> a -> a
/Double
years)Double -> Double -> Double
forall a. Num a => a -> a -> a
-Double
1) :: Double

  Bool -> IO () -> IO ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
when Bool
showCashFlow (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ do
    [Char] -> Text -> Text -> IO ()
forall r. PrintfType r => [Char] -> r
printf [Char]
"\nTWR cash flow for %s - %s\n" (Day -> Text
showDate Day
begin) (Day -> Text
showDate (Integer -> Day -> Day
addDays (-Integer
1) Day
end))
    let ([Day]
dates', [Either MixedAmount MixedAmount]
amts) = [(Day, Either MixedAmount MixedAmount)]
-> ([Day], [Either MixedAmount MixedAmount])
forall a b. [(a, b)] -> ([a], [b])
unzip [(Day, Either MixedAmount MixedAmount)]
changes
        cashflows' :: [MixedAmount]
cashflows' = (Either MixedAmount MixedAmount -> MixedAmount)
-> [Either MixedAmount MixedAmount] -> [MixedAmount]
forall a b. (a -> b) -> [a] -> [b]
map (MixedAmount -> Either MixedAmount MixedAmount -> MixedAmount
forall b a. b -> Either a b -> b
fromRight MixedAmount
nullmixedamt) [Either MixedAmount MixedAmount]
amts
        pnls :: [MixedAmount]
pnls = (Either MixedAmount MixedAmount -> MixedAmount)
-> [Either MixedAmount MixedAmount] -> [MixedAmount]
forall a b. (a -> b) -> [a] -> [b]
map (MixedAmount -> Either MixedAmount MixedAmount -> MixedAmount
forall a b. a -> Either a b -> a
fromLeft MixedAmount
nullmixedamt) [Either MixedAmount MixedAmount]
amts
        ([Decimal]
valuesOnDate,[Decimal]
unitsBoughtOrSold', [Decimal]
unitPrices', [Decimal]
unitBalances') = [(Decimal, Decimal, Decimal, Decimal)]
-> ([Decimal], [Decimal], [Decimal], [Decimal])
forall a b c d. [(a, b, c, d)] -> ([a], [b], [c], [d])
unzip4 [(Decimal, Decimal, Decimal, Decimal)]
units
        add :: a -> [a] -> [a]
add a
x [a]
lst = if Decimal
valueBeforeDecimal -> Decimal -> Bool
forall a. Eq a => a -> a -> Bool
/=Decimal
0 then a
xa -> [a] -> [a]
forall a. a -> [a] -> [a]
:[a]
lst else [a]
lst
        dates :: [Day]
dates = Day -> [Day] -> [Day]
forall a. a -> [a] -> [a]
add Day
begin [Day]
dates'
        cashflows :: [MixedAmount]
cashflows = MixedAmount -> [MixedAmount] -> [MixedAmount]
forall a. a -> [a] -> [a]
add MixedAmount
valueBeforeAmt [MixedAmount]
cashflows'
        unitsBoughtOrSold :: [Decimal]
unitsBoughtOrSold = Decimal -> [Decimal] -> [Decimal]
forall a. a -> [a] -> [a]
add Decimal
initialUnits [Decimal]
unitsBoughtOrSold'
        unitPrices :: [Decimal]
unitPrices = Decimal -> [Decimal] -> [Decimal]
forall a. a -> [a] -> [a]
add Decimal
initialUnitPrice [Decimal]
unitPrices'
        unitBalances :: [Decimal]
unitBalances = Decimal -> [Decimal] -> [Decimal]
forall a. a -> [a] -> [a]
add Decimal
initialUnits [Decimal]
unitBalances'

    Text -> IO ()
TL.putStr (Text -> IO ()) -> Text -> IO ()
forall a b. (a -> b) -> a -> b
$ Bool
-> (Text -> Text)
-> (Text -> Text)
-> ([Char] -> Text)
-> Table Text Text [Char]
-> Text
forall a rh ch.
Show a =>
Bool
-> (rh -> Text)
-> (ch -> Text)
-> (a -> Text)
-> Table rh ch a
-> Text
Tab.render Bool
prettyTables Text -> Text
forall a. a -> a
id Text -> Text
forall a. a -> a
id [Char] -> Text
T.pack
      (Header Text -> Header Text -> [[[Char]]] -> Table Text Text [Char]
forall rh ch a. Header rh -> Header ch -> [[a]] -> Table rh ch a
Table
       (Properties -> [Header Text] -> Header Text
forall h. Properties -> [Header h] -> Header h
Tab.Group Properties
NoLine ((Day -> Header Text) -> [Day] -> [Header Text]
forall a b. (a -> b) -> [a] -> [b]
map (Text -> Header Text
forall h. h -> Header h
Header (Text -> Header Text) -> (Day -> Text) -> Day -> Header Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Day -> Text
showDate) [Day]
dates))
       (Properties -> [Header Text] -> Header Text
forall h. Properties -> [Header h] -> Header h
Tab.Group Properties
DoubleLine [ Properties -> [Header Text] -> Header Text
forall h. Properties -> [Header h] -> Header h
Tab.Group Properties
Tab.SingleLine [Text -> Header Text
forall h. h -> Header h
Tab.Header Text
"Portfolio value", Text -> Header Text
forall h. h -> Header h
Tab.Header Text
"Unit balance"]
                         , Properties -> [Header Text] -> Header Text
forall h. Properties -> [Header h] -> Header h
Tab.Group Properties
Tab.SingleLine [Text -> Header Text
forall h. h -> Header h
Tab.Header Text
"Pnl", Text -> Header Text
forall h. h -> Header h
Tab.Header Text
"Cashflow", Text -> Header Text
forall h. h -> Header h
Tab.Header Text
"Unit price", Text -> Header Text
forall h. h -> Header h
Tab.Header Text
"Units"]
                         , Properties -> [Header Text] -> Header Text
forall h. Properties -> [Header h] -> Header h
Tab.Group Properties
Tab.SingleLine [Text -> Header Text
forall h. h -> Header h
Tab.Header Text
"New Unit Balance"]])
       [ [[Char]
val, [Char]
oldBalance, [Char]
pnl', [Char]
cashflow, [Char]
prc, [Char]
udelta, [Char]
balance]
       | [Char]
val <- (Decimal -> [Char]) -> [Decimal] -> [[Char]]
forall a b. (a -> b) -> [a] -> [b]
map Decimal -> [Char]
showDecimal [Decimal]
valuesOnDate
       | [Char]
oldBalance <- (Decimal -> [Char]) -> [Decimal] -> [[Char]]
forall a b. (a -> b) -> [a] -> [b]
map Decimal -> [Char]
showDecimal (Decimal
0Decimal -> [Decimal] -> [Decimal]
forall a. a -> [a] -> [a]
:[Decimal]
unitBalances)
       | [Char]
balance <- (Decimal -> [Char]) -> [Decimal] -> [[Char]]
forall a b. (a -> b) -> [a] -> [b]
map Decimal -> [Char]
showDecimal [Decimal]
unitBalances
       | [Char]
pnl' <- (MixedAmount -> [Char]) -> [MixedAmount] -> [[Char]]
forall a b. (a -> b) -> [a] -> [b]
map (MixedAmount -> [Char]
showMixedAmount (MixedAmount -> [Char])
-> (MixedAmount -> MixedAmount) -> MixedAmount -> [Char]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Map Text AmountStyle -> MixedAmount -> MixedAmount
forall a. HasAmounts a => Map Text AmountStyle -> a -> a
styleAmounts Map Text AmountStyle
styles) [MixedAmount]
pnls
       | [Char]
cashflow <- (MixedAmount -> [Char]) -> [MixedAmount] -> [[Char]]
forall a b. (a -> b) -> [a] -> [b]
map (MixedAmount -> [Char]
showMixedAmount (MixedAmount -> [Char])
-> (MixedAmount -> MixedAmount) -> MixedAmount -> [Char]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Map Text AmountStyle -> MixedAmount -> MixedAmount
forall a. HasAmounts a => Map Text AmountStyle -> a -> a
styleAmounts Map Text AmountStyle
styles) [MixedAmount]
cashflows
       | [Char]
prc <- (Decimal -> [Char]) -> [Decimal] -> [[Char]]
forall a b. (a -> b) -> [a] -> [b]
map Decimal -> [Char]
showDecimal [Decimal]
unitPrices
       | [Char]
udelta <- (Decimal -> [Char]) -> [Decimal] -> [[Char]]
forall a b. (a -> b) -> [a] -> [b]
map Decimal -> [Char]
showDecimal [Decimal]
unitsBoughtOrSold ])

    [Char]
-> [Char]
-> [Char]
-> [Char]
-> [Char]
-> Double
-> Double
-> IO ()
forall r. PrintfType r => [Char] -> r
printf [Char]
"Final unit price: %s/%s units = %s\nTotal TWR: %s%%.\nPeriod: %.2f years.\nAnnualized TWR: %.2f%%\n\n"
      (MixedAmount -> [Char]
showMixedAmount (MixedAmount -> [Char]) -> MixedAmount -> [Char]
forall a b. (a -> b) -> a -> b
$ Map Text AmountStyle -> MixedAmount -> MixedAmount
forall a. HasAmounts a => Map Text AmountStyle -> a -> a
styleAmounts Map Text AmountStyle
styles MixedAmount
valueAfter) (Decimal -> [Char]
showDecimal Decimal
finalUnitBalance) (Decimal -> [Char]
showDecimal Decimal
finalUnitPrice) (Decimal -> [Char]
showDecimal Decimal
totalTWR) Double
years Double
annualizedTWR

  (Double, Double) -> IO (Double, Double)
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ((Decimal -> Double
forall a b. (Real a, Fractional b) => a -> b
realToFrac Decimal
totalTWR) :: Double, Double
annualizedTWR)

internalRateOfReturn :: Map Text AmountStyle -> Bool -> Bool -> OneSpan -> IO Double
internalRateOfReturn Map Text AmountStyle
styles Bool
showCashFlow Bool
prettyTables (OneSpan Day
begin Day
end MixedAmount
valueBefore MixedAmount
valueAfter [(Day, MixedAmount)]
cashFlow [(Day, MixedAmount)]
_pnl) = do
  let prefix :: (Day, MixedAmount)
prefix = (Day
begin, MixedAmount -> MixedAmount
maNegate MixedAmount
valueBefore)

      postfix :: (Day, MixedAmount)
postfix = (Day
end, MixedAmount
valueAfter)

      totalCF :: [(Day, MixedAmount)]
totalCF = ((Day, MixedAmount) -> Bool)
-> [(Day, MixedAmount)] -> [(Day, MixedAmount)]
forall a. (a -> Bool) -> [a] -> [a]
filter (MixedAmount -> Bool
maIsNonZero (MixedAmount -> Bool)
-> ((Day, MixedAmount) -> MixedAmount)
-> (Day, MixedAmount)
-> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Day, MixedAmount) -> MixedAmount
forall a b. (a, b) -> b
snd) ([(Day, MixedAmount)] -> [(Day, MixedAmount)])
-> [(Day, MixedAmount)] -> [(Day, MixedAmount)]
forall a b. (a -> b) -> a -> b
$ (Day, MixedAmount)
prefix (Day, MixedAmount) -> [(Day, MixedAmount)] -> [(Day, MixedAmount)]
forall a. a -> [a] -> [a]
: (((Day, MixedAmount) -> Day)
-> [(Day, MixedAmount)] -> [(Day, MixedAmount)]
forall b a. Ord b => (a -> b) -> [a] -> [a]
sortOn (Day, MixedAmount) -> Day
forall a b. (a, b) -> a
fst [(Day, MixedAmount)]
cashFlow) [(Day, MixedAmount)]
-> [(Day, MixedAmount)] -> [(Day, MixedAmount)]
forall a. [a] -> [a] -> [a]
++ [(Day, MixedAmount)
postfix]

  Bool -> IO () -> IO ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
when Bool
showCashFlow (IO () -> IO ()) -> IO () -> IO ()
forall a b. (a -> b) -> a -> b
$ do
    [Char] -> Text -> Text -> IO ()
forall r. PrintfType r => [Char] -> r
printf [Char]
"\nIRR cash flow for %s - %s\n" (Day -> Text
showDate Day
begin) (Day -> Text
showDate (Integer -> Day -> Day
addDays (-Integer
1) Day
end))
    let ([Day]
dates, [MixedAmount]
amts) = [(Day, MixedAmount)] -> ([Day], [MixedAmount])
forall a b. [(a, b)] -> ([a], [b])
unzip [(Day, MixedAmount)]
totalCF
    Text -> IO ()
TL.putStrLn (Text -> IO ()) -> Text -> IO ()
forall a b. (a -> b) -> a -> b
$ Bool
-> (Text -> Text)
-> (Text -> Text)
-> (Text -> Text)
-> Table Text Text Text
-> Text
forall a rh ch.
Show a =>
Bool
-> (rh -> Text)
-> (ch -> Text)
-> (a -> Text)
-> Table rh ch a
-> Text
Tab.render Bool
prettyTables Text -> Text
forall a. a -> a
id Text -> Text
forall a. a -> a
id Text -> Text
forall a. a -> a
id
      (Header Text -> Header Text -> [[Text]] -> Table Text Text Text
forall rh ch a. Header rh -> Header ch -> [[a]] -> Table rh ch a
Table
       (Properties -> [Header Text] -> Header Text
forall h. Properties -> [Header h] -> Header h
Tab.Group Properties
Tab.NoLine ((Day -> Header Text) -> [Day] -> [Header Text]
forall a b. (a -> b) -> [a] -> [b]
map (Text -> Header Text
forall h. h -> Header h
Header (Text -> Header Text) -> (Day -> Text) -> Day -> Header Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Day -> Text
showDate) [Day]
dates))
       (Properties -> [Header Text] -> Header Text
forall h. Properties -> [Header h] -> Header h
Tab.Group Properties
Tab.SingleLine [Text -> Header Text
forall h. h -> Header h
Header Text
"Amount"])
       ((MixedAmount -> [Text]) -> [MixedAmount] -> [[Text]]
forall a b. (a -> b) -> [a] -> [b]
map ((Text -> [Text] -> [Text]
forall a. a -> [a] -> [a]
:[]) (Text -> [Text]) -> (MixedAmount -> Text) -> MixedAmount -> [Text]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Char] -> Text
T.pack ([Char] -> Text) -> (MixedAmount -> [Char]) -> MixedAmount -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. MixedAmount -> [Char]
showMixedAmount (MixedAmount -> [Char])
-> (MixedAmount -> MixedAmount) -> MixedAmount -> [Char]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Map Text AmountStyle -> MixedAmount -> MixedAmount
forall a. HasAmounts a => Map Text AmountStyle -> a -> a
styleAmounts Map Text AmountStyle
styles) [MixedAmount]
amts))

  -- 0% is always a solution, so require at least something here
  case [(Day, MixedAmount)]
totalCF of
    [] -> Double -> IO Double
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return Double
0
    [(Day, MixedAmount)]
_ -> case RiddersParam
-> (Double, Double) -> (Double -> Double) -> Root Double
ridders (Int -> Tolerance -> RiddersParam
RiddersParam Int
100 (Double -> Tolerance
AbsTol Double
0.00001))
                      (Double
0.000000000001,Double
10000)
                      (Day -> [(Day, MixedAmount)] -> Double -> Double
interestSum Day
end [(Day, MixedAmount)]
totalCF) of
        Root Double
rate    -> Double -> IO Double
forall a. a -> IO a
forall (m :: * -> *) a. Monad m => a -> m a
return ((Double
rateDouble -> Double -> Double
forall a. Num a => a -> a -> a
-Double
1)Double -> Double -> Double
forall a. Num a => a -> a -> a
*Double
100)
        Root Double
NotBracketed -> [Char] -> IO Double
forall a. [Char] -> a
error' ([Char] -> IO Double) -> [Char] -> IO Double
forall a b. (a -> b) -> a -> b
$ [Char]
"Error (NotBracketed): No solution for Internal Rate of Return (IRR).\n"
                        [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++       [Char]
"  Possible causes: IRR is huge (>1000000%), balance of investment becomes negative at some point in time."
        Root Double
SearchFailed -> [Char] -> IO Double
forall a. [Char] -> a
error' ([Char] -> IO Double) -> [Char] -> IO Double
forall a b. (a -> b) -> a -> b
$ [Char]
"Error (SearchFailed): Failed to find solution for Internal Rate of Return (IRR).\n"
                        [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++       [Char]
"  Either search does not converge to a solution, or converges too slowly."

type CashFlow = [(Day, MixedAmount)]

interestSum :: Day -> CashFlow -> Double -> Double
interestSum :: Day -> [(Day, MixedAmount)] -> Double -> Double
interestSum Day
referenceDay [(Day, MixedAmount)]
cf Double
rate = [Double] -> Double
forall a. Num a => [a] -> a
forall (t :: * -> *) a. (Foldable t, Num a) => t a -> a
sum ([Double] -> Double) -> [Double] -> Double
forall a b. (a -> b) -> a -> b
$ ((Day, MixedAmount) -> Double) -> [(Day, MixedAmount)] -> [Double]
forall a b. (a -> b) -> [a] -> [b]
map (Day, MixedAmount) -> Double
go [(Day, MixedAmount)]
cf
  where go :: (Day, MixedAmount) -> Double
go (Day
t,MixedAmount
m) = Decimal -> Double
forall a b. (Real a, Fractional b) => a -> b
realToFrac (MixedAmount -> Decimal
unMix MixedAmount
m) Double -> Double -> Double
forall a. Num a => a -> a -> a
* Double
rate Double -> Double -> Double
forall a. Floating a => a -> a -> a
** (Integer -> Double
forall a b. (Integral a, Num b) => a -> b
fromIntegral (Day
referenceDay Day -> Day -> Integer
`diffDays` Day
t) Double -> Double -> Double
forall a. Fractional a => a -> a -> a
/ Double
365)


calculateCashFlow :: WhichDate -> [Transaction] -> Query -> CashFlow
calculateCashFlow :: WhichDate -> [Transaction] -> Query -> [(Day, MixedAmount)]
calculateCashFlow WhichDate
wd [Transaction]
trans Query
query =
  [ (WhichDate -> Posting -> Day
postingDateOrDate2 WhichDate
wd Posting
p, Posting -> MixedAmount
pamount Posting
p) | Posting
p <- (Posting -> Bool) -> [Posting] -> [Posting]
forall a. (a -> Bool) -> [a] -> [a]
filter (Query -> Posting -> Bool
matchesPosting Query
query) ((Transaction -> [Posting]) -> [Transaction] -> [Posting]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap Transaction -> [Posting]
realPostings [Transaction]
trans), MixedAmount -> Bool
maIsNonZero (Posting -> MixedAmount
pamount Posting
p) ]

total :: [Transaction] -> Query -> MixedAmount
total :: [Transaction] -> Query -> MixedAmount
total [Transaction]
trans Query
query = [Posting] -> MixedAmount
sumPostings ([Posting] -> MixedAmount)
-> ([Posting] -> [Posting]) -> [Posting] -> MixedAmount
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Posting -> Bool) -> [Posting] -> [Posting]
forall a. (a -> Bool) -> [a] -> [a]
filter (Query -> Posting -> Bool
matchesPosting Query
query) ([Posting] -> MixedAmount) -> [Posting] -> MixedAmount
forall a b. (a -> b) -> a -> b
$ (Transaction -> [Posting]) -> [Transaction] -> [Posting]
forall (t :: * -> *) a b. Foldable t => (a -> [b]) -> t a -> [b]
concatMap Transaction -> [Posting]
realPostings [Transaction]
trans

unMix :: MixedAmount -> Quantity
unMix :: MixedAmount -> Decimal
unMix MixedAmount
a =
  case (MixedAmount -> Maybe Amount
unifyMixedAmount (MixedAmount -> Maybe Amount) -> MixedAmount -> Maybe Amount
forall a b. (a -> b) -> a -> b
$ MixedAmount -> MixedAmount
mixedAmountCost MixedAmount
a) of
    Just Amount
a' -> Amount -> Decimal
aquantity Amount
a'
    Maybe Amount
Nothing -> [Char] -> Decimal
forall a. [Char] -> a
error' ([Char] -> Decimal) -> [Char] -> Decimal
forall a b. (a -> b) -> a -> b
$ [Char]
"Amounts could not be converted to a single cost basis: " [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++ [[Char]] -> [Char]
forall a. Show a => a -> [Char]
show ((Amount -> [Char]) -> [Amount] -> [[Char]]
forall a b. (a -> b) -> [a] -> [b]
map Amount -> [Char]
showAmount ([Amount] -> [[Char]]) -> [Amount] -> [[Char]]
forall a b. (a -> b) -> a -> b
$ MixedAmount -> [Amount]
amounts MixedAmount
a) [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++
               [Char]
"\nConsider using --value to force all costs to be in a single commodity." [Char] -> [Char] -> [Char]
forall a. [a] -> [a] -> [a]
++
               [Char]
"\nFor example, \"--cost --value=end,<commodity> --infer-market-prices\", where commodity is the one that was used to pay for the investment."

-- Show Decimal rounded to two decimal places, unless it has less places already. This ensures that "2" won't be shown as "2.00"
showDecimal :: Decimal -> String
showDecimal :: Decimal -> [Char]
showDecimal Decimal
d = if Decimal
d Decimal -> Decimal -> Bool
forall a. Eq a => a -> a -> Bool
== Decimal
rounded then Decimal -> [Char]
forall a. Show a => a -> [Char]
show Decimal
d else Decimal -> [Char]
forall a. Show a => a -> [Char]
show Decimal
rounded
  where
    rounded :: Decimal
rounded = Word8 -> Decimal -> Decimal
forall i. Integral i => Word8 -> DecimalRaw i -> DecimalRaw i
roundTo Word8
2 Decimal
d