hax-0.0.1: src/HAX/Assets.hs
{-# LANGUAGE TypeSynonymInstances
, NoMonomorphismRestriction
, FlexibleInstances
#-}
-- | This module provides a collection of accounting actions for
-- different asset/account types implemented via the 'Asset' class.
--
-- Use these actions to build more complex accounting actions for
-- 'Entity's from "Accounting".
--
-- They are implemented using the more basic combinators from
-- "Bookkeeping".
--
-- Many assets make use the 'InterestRate's defined below.
module HAX.Assets where
import HAX.Bookkeeping
import Control.Monad.Reader
import HAX.Common
-- * The Asset class
-- | An asset is anything that can be handled within an accounting
-- action.
class Asset a where
handle :: a -> AccountingRW s ()
-- ^ derives the action corresponding to the asset's characteristics
-- * Transactional Account with asymmetric interest rates.
-- | This assets calculates interest according to its average positive
-- and negative account balances over the last time period and debits
-- it againt the 'InterestRate's 'iSource' accounts.
--
-- Other names: Revolving credit, line of credit, Kontokorrent
data TransactionalAccount = TransactionalAccount {
taCredit :: InterestRate
-- ^ credit interest for negative balances
-- for one period (not neccessarily p.a.)
, taDebit :: InterestRate
-- ^ debit interest for positive balances
-- for one period (not neccessarily p.a.)
, taAcc :: AccountName -- ^ asset's account name
, taPeriod :: ASpan -- ^ period in months
}
instance Asset TransactionalAccount where
handle (TransactionalAccount icredit idebit acc period) = do
let avg m = sum m / fromIntegral (sMonths period)
tx a b c = interestTx a (b ++ " für "++ show acc) acc (Just $ return $ avg c)
date <- curDate
onceEvery period (month 12) $ do
(debit,credit) <- partition (>0) <$> balancesSince (shift (1-period) date) acc
when ((sum $ debit ++ fmap negate credit) /= 0) $
logLedger $ printf "%s: positive balances: %v, negative balances: %v"
acc (PList debit) (PList credit)
tx icredit "Sollzins" credit
tx idebit "Habenzins" debit
return ()
-- * Loan with fixed (annuity) or variable payments
-- | Select a payment schedule
data PaymentType = Linear Decimal
-- ^ Decreasing payments with fixed repay and
-- decreasing interest portions.
--
-- The number specifies the repay as a fraction of
-- the principal amount.
| Annuity Decimal
-- ^ Fixed payments with decreasing interest and
-- increasing repay portions.
--
-- The number specifies the initial repay as a
-- fraction of the principal amount.
deriving Show
data Loan = Loan { lPrincipal :: Amount -- ^ Principal amount payed to lPaymentAccount
, lPType :: PaymentType
, lPaymentAcc :: AccountName -- ^ account for the payments
, lLoanAcc :: AccountName -- ^ account for the open balance
, lInterest :: InterestRate
-- ^ interest rate for one period (not neccessarily p.a.)
, lStart :: ADate -- ^ payout date
, lPeriod :: ASpan -- ^ period of the interest payments
}
instance Asset Loan where
handle (Loan principal pType paymentAcc loanAcc ir start period) = do
date <- curDate
let name = show loanAcc
dur = duration pType ir
onceAt start $ fromTo principal ("Principal for "++name) loanAcc paymentAcc
>> lift (do printf "Loan %v runs for %v periods\n" name $ show $ roundTo 3 $ conv dur
-- printf "%s == %s" (show pType) $ show $ annuity dur (iRate ir) - (iRate ir)
)
onlyAfter start $ onceEvery period start $ do
curInterest <- interestTx ir ("Interest on "++name) loanAcc Nothing
let payment (Linear x) = x*principal + curInterest
payment (Annuity x)= x*principal + interest ir principal
fromToLimit (negate $ roundTo 3 $ payment pType) ("Rate for "++name) loanAcc paymentAcc
-- * Liquitidy Simulation
-- | This 'Asset' produces a list of twelve payments, one for each
-- month, that are shifted in such a way, that they sum up to zero
data Liquidity = Liquidity { lFrom :: AccountName
, lTo :: AccountName
, lPayments :: [Amount]
}
instance Asset Liquidity where
handle (Liquidity from to pays) = do
m <-reader (getMonth.eDate)
fromTo (pays !! pred m) "Liquidity Simulation" from to
where avg = sum (take 12 pays) / 12
-- ** Helpers
-- | Calculate the number of periods time until the loan is completely repaid.
duration :: PaymentType -> InterestRate -> Decimal
duration ptype ir = conv $ duration' ptype $ iRate ir
where duration' (Linear x) _ = conv $ 1/x
duration' (Annuity x) 0 = conv $ 1/x
duration' (Annuity x) rate = negate $ on logBase conv (1 + rate) (x/(rate + x))
-- | Calculate the annuity (as a fraction of the principal) for a
-- given number of periods interest rate.
annuity :: Decimal -- ^ number of periods
-> Decimal -- ^ interest rate
-> Amount
annuity duration' rate' = conv $ ((1 + rate) ** duration) * rate/(((1 + rate) ** duration) - 1)
where rate = conv rate' :: Double
duration = conv duration' :: Double
-- * Fixed Payments
-- | Recurring Fixed Payment
data FixedPayment = FixedPayment {
fPayment :: Amount
, fStart :: ADate
, fOffset :: ADate
, fPeriod :: ASpan
, fSource :: AccountName
, fSink :: AccountName
, fComment :: Comment
}
instance Asset FixedPayment where
handle (FixedPayment am start offset period source sink comment) =
onlyAfter start $ onceEvery period offset $ fromTo am comment source sink
-- * Interest Rates
-- | Interest rates together with the nominal account, the could be
-- identified with the source (for incoming interest payments) or sink
-- (for outgoing, i.e. negative interest payments).
--
-- For example, if the interest is an expense, 'iSource' could be
-- \"Expenses\". If the interest is capital yield, 'iSource' could be
-- \"Income\".
data InterestRate = IR {
iRate :: Amount
, iSource :: AccountName
-- ^ this can be something like \"Earnings\" if the interest can
-- be considered earnings or another entity, if
}
-- | Calculate the interest for a given amount
interest :: InterestRate -> Amount -> Amount
interest ir am = roundTo 2 $ am * (iRate ir)
-- | Calculate the interest for a current balance of an account
currentInterest :: (Monoid w, Ledger l) => InterestRate-> AccountName -> AmountA s l w
currentInterest ir acc = interest ir <$> currentBalance acc
-- | Calculate and transfer the interest from the 'iSource' account to
-- some other account.
interestTx :: InterestRate -> String -- ^ comment
-> AccountName -- ^ Sink account
-> Maybe (AmountRW s)
-- ^ optional: Use this amount instead of the balance of
-- the sink account
-> AmountRW s
interestTx ir comment acc maybeAm = do
am <- maybe (currentInterest ir acc) (interest ir <$>) maybeAm
when (am /= 0) $
fromTo am comment (iSource ir) acc
return am