haslo (empty) → 0.1
raw patch · 15 files changed
+1700/−0 lines, 15 filesdep +QuickCheckdep +basedep +mtlsetup-changed
Dependencies added: QuickCheck, base, mtl, old-time, time, wtk
Files
- Haslo.hs +40/−0
- Haslo/BasicType.hs +43/−0
- Haslo/CalcCalendar.hs +91/−0
- Haslo/CalcConfigurationType.hs +199/−0
- Haslo/CalcConstructors.hs +301/−0
- Haslo/Calculator.hs +268/−0
- Haslo/ErrorHandling.hs +128/−0
- Haslo/InstalmentPlan.hs +101/−0
- Haslo/InstalmentPlanProps.hs +219/−0
- Haslo/Parameters.hs +135/−0
- HasloQC.hs +72/−0
- Sample.hs +32/−0
- Setup.hs +2/−0
- haslo.cabal +38/−0
- license +31/−0
+ Haslo.hs view
@@ -0,0 +1,40 @@+--------------------------------------------------------- +-- +-- Module : Haslo +-- Copyright : Bartosz Wójcik (2012) +-- License : BSD3 +-- +-- Maintainer : bartek@sudety.it +-- Stability : Unstable +-- Portability : portable +-- +-- | Haslo aplication interface. The only module from haslo library you need to import. +--------------------------------------------------------- + +module Haslo+ (module Haslo.Parameters + ,module Haslo.ErrorHandling + ,module Haslo.InstalmentPlan + ,module Haslo.InstalmentPlanProps + ,module Haslo.CalcConfigurationType + ,module Haslo.CalcConstructors + ,module Haslo.Calculator+ ,getVersionHaslo+ ) +where + +import Haslo.Parameters +import Haslo.ErrorHandling +import Haslo.InstalmentPlan +import Haslo.InstalmentPlanProps +import Haslo.CalcConfigurationType +import Haslo.CalcConstructors +import Haslo.Calculator++-- | Human friendly formatet output of list wrapped in the Either.+-- Usefull for quick validation of loan constructors.+niceShow :: (Show a, Show a1) => Either a1 [a] -> IO ()+niceShow (Right xs) = mapM_ (putStrLn . show) xs+niceShow (Left err) = putStrLn $ show err++getVersionHaslo = "0.1"
+ Haslo/BasicType.hs view
@@ -0,0 +1,43 @@+--------------------------------------------------------- +-- +-- Module : BasicType +-- Copyright : Bartosz Wójcik (2010) +-- License : BSD3 +-- +-- Maintainer : bartek@sudety.it +-- Stability : Unstable +-- Portability : portable +-- +-- Basic types of Haskell Loan. +--------------------------------------------------------- +{-# LANGUAGE TypeSynonymInstances #-} +module Haslo.BasicType +where + +import Text.PrettyShow + + +-- | Each amount has its own fix precision. Usually two digits after decimal point, sometimes +-- even till full hundrest or thousends. Therefore all amounts are stored as Ints. This prevents +-- situation where rounded amounts don't sum up to given value. Alternatilvely @Int@ would be +-- suitable. +type Amount = Integer++-- | Interest calculated. Interest paid is of @Amount@ data type. +type Interest = Double++-- | Interest rate. +type Rate = Double++-- | Duration of the loan. +type Duration = Int + + +-- | Operations on @Amount@s have to be rounded to full amount. +data RoundingType = Rounded + | Truncated + deriving (Eq, Ord, Show, Enum) + +instance PrettyShow RoundingType where + showWithLen n = showWithLen n . show +
+ Haslo/CalcCalendar.hs view
@@ -0,0 +1,91 @@+--------------------------------------------------------- +-- +-- Module : CalcCalendar +-- Copyright : Bartosz Wójcik (2010) +-- License : BSD3 +-- +-- Maintainer : bartek@sudety.it +-- Stability : Unstable +-- Portability : portable +-- +-- Bank calendar of Haskell Loan. +--------------------------------------------------------- +-- | Provides data types and basic functions operating on dates +-- in specific calendars. +module Haslo.CalcCalendar + (module Data.Time + ,module Data.Time.Calendar.OrdinalDate + ,CalendarType (..) + ,diffLoanCalendar + ,addMonth + ,addMonths + ,addYears + ,setDay + ,setOrdinalDay + ,Freq (..) + ,freqPerYear + ,addPeriods + ) +where + +import Data.Time +--import Data.Time.Calendar.Julian +import Data.Time.Calendar.OrdinalDate +import Text.PrettyShow + +-- | Bank calendar is usually different than real one. +data CalendarType = Y360 -- ^ year has 360 days, each month 30 days + | Y360Specific -- ^ year has 360 days, each month 30 days (specific version, + -- where diff 15.02 31.01 = 14 [not 15 as expected!]) + | RealCalendar -- ^ each month has real number of days + deriving (Eq, Show, Enum) + +-- | Number of days between two dates in different calendars +diffLoanCalendar :: Day -> Day -> CalendarType -> Int +diffLoanCalendar d1 d2 Y360 = fromIntegral (360 * (yd1 - yd2)) + 30 * (md1 - md2) + min 30 dd1 - min 30 dd2 + where (yd1,md1,dd1) = toGregorian d1 + (yd2,md2,dd2) = toGregorian d2 +diffLoanCalendar d1 d2 Y360Specific = fromIntegral (360 * (yd1 - yd2)) + 30 * (md1 - md2) + dd1 - dd2 + where (yd1,md1,dd1) = toGregorian d1 + (yd2,md2,dd2) = toGregorian d2 +diffLoanCalendar d1 d2 RealCalendar = fromIntegral $ diffDays d1 d2 +--diffLoanCalendar _ _ cal = error $ "Calendar " ++ show cal ++ " is not yet defined" + +addMonth date | m == 12 = fromGregorian (y+1) 1 d + | otherwise = fromGregorian y (m+1) d + where (y,m,d) = toGregorian date + +addMonths :: Integer -> Day -> Day +addMonths n date = fromGregorian (y + ((n+m-1) `div` 12)) (fromIntegral (n+m-1) `mod` 12 + 1) d + where (y,m',d) = toGregorian date + m = fromIntegral m' + +addYears :: Integer -> Day -> Day +addYears n date = fromGregorian (y+n) m d + where (y,m,d) = toGregorian date + +setDay n date = fromGregorian y m n + where (y,m,d) = toGregorian date + +setOrdinalDay n date = fromOrdinalDate y n + where (y,d) = toOrdinalDate date + +-- | Frequency. +data Freq = Daily + | Monthly + | Yearly + deriving (Eq, Ord, Show, Enum) + +instance PrettyShow Freq where + showWithLen n = showWithLen n . show + +freqPerYear Daily = 365 +freqPerYear Monthly = 12 +freqPerYear Yearly = 1 + +-- | Increases a date by given number of given units. +addPeriods :: Freq -> Integer -> Day -> Day+addPeriods Daily = addDays +addPeriods Monthly = addMonths +addPeriods Yearly = addYears +
+ Haslo/CalcConfigurationType.hs view
@@ -0,0 +1,199 @@+--------------------------------------------------------- +-- +-- Module : CalcConfigurationType +-- Copyright : Bartosz Wójcik (2010) +-- License : BSD3 +-- +-- Maintainer : bartek@sudety.it +-- Stability : Unstable +-- Portability : portable +-- +-- | This module implements data types of loan consturctors. +--------------------------------------------------------- +module Haslo.CalcConfigurationType + (module Haslo.CalcCalendar + ,InstalmentLoanData (..) + ,ClassicLoan (..) + ,Balloons (..) + ,UnfdBalloons (..) + ,Classical (..) + ,Balloon (..) + ,BalloonPlus (..) + ,UnfdBalloon (..) + ,UnfdBalloonPlus (..) + ,ReversBalloon (..) + ,Bullet (..) + ,myRound + ) + +where + +import Haslo.BasicType +import Haslo.CalcCalendar +import Haslo.ErrorHandling +import Haslo.Parameters +import Haslo.InstalmentPlan +import Text.PrettyShow + + +-- ============================== +-- Types' definitions +-- ============================== + +-- | Data type for retriving loan data of particular products directly from consturctors. +data InstalmentLoanData = InstalmentLoanData {principal :: Amount + ,duration :: Duration + ,deferrment :: Duration + ,rate :: Rate + } + deriving (Eq, Ord, Show) + +-- | Classical Loan provides a constructor function which +-- creates @Instalment Plan@ of this loan. +class ClassicLoan a where + newLoanI :: a -- ^ type of loan + -> IPPMonad InstalmentPlan + extract :: a -> InstalmentLoanData + +-- | Class of loans with specific last instalment. +class (ClassicLoan a) => Balloons a where + balloon :: a -> Amount + +-- | Class of loans with extended duration. +class (Balloons a) => UnfdBalloons a where + eXtendedDuration :: a -> Duration -- ^ Max duration of extended period. + +-- | Eeach installment is equal:+-- principal, duration in freq units, 1st instalment postponement in duration units, +-- yearly effective interest rate +data Classical = Classical !Amount -- principal + !Duration -- duration in freq units + !Duration -- 1st instalment postponement in duration units + !Rate -- yearly effective interest rate + deriving (Eq, Ord) + +-- | Last installment is given, rest are equal +-- Datatype elements: principal, duration in freq units, 1st instalment postponement in duration units, +-- yearly effective interest rate,balloon amount. +data Balloon = Balloon !Amount -- principal + !Duration -- duration in freq units + !Duration -- 1st instalment postponement in duration units + !Rate -- yearly effective interest rate + !Amount -- balloon amount + deriving (Eq, Ord) + +-- | Balloon + given amount is put on top of last installment. +-- | Last installment is given, rest are equal +-- Datatype elements: principal, duration in freq units, 1st instalment postponement in duration units, +-- yearly effective interest rate,balloon amount. +data BalloonPlus = BalloonPlus !Amount -- principal + !Duration -- duration in freq units + !Duration -- 1st instalment postponement in duration units + !Rate -- yearly effective interest rate + !Amount -- balloon amount + deriving (Eq, Ord) + +-- | 2 periods product with given duration of 1st one and +-- residual balloon amount (RBA). +-- RBA serves to calculate first period installment amount like in balloon. +-- 2nd period amount is equal to 1st one, its duration is calculated +-- based on it. Last installment may differ.+-- Datatype elements: principal, duration in freq units, 1st instalment postponement in duration units, +-- yearly effective interest rate, residual balloon amount (RBA), maximal duration of unfolded RBA.+data UnfdBalloon = UnfdBalloon !Amount -- principal + !Duration -- duration till balloon (inclusive) in freq units + !Duration -- 1st instalment postponement in duration units + !Rate -- yearly effective interest rate + !Amount -- residual balloon amount (RBA). + !Duration -- maximal duration of unfolded RBA + deriving (Eq, Ord) + +-- | Similar to UnfdBalloon. There is 1 difference: RBA maturess together with +-- last installment of 1st period (like BalloonPlus), +-- whilst in UnfdBalloon - one month later. +-- Datatype elements: principal, duration in freq units, 1st instalment postponement in duration units, +-- yearly effective interest rate, residual balloon amount (RBA), maximal duration of unfolded RBA. +data UnfdBalloonPlus = UnfdBalloonPlus !Amount -- principal + !Duration -- duration till balloon (inclusive) in freq units + !Duration -- 1st instalment postponement in duration units + !Rate -- yearly effective interest rate + !Amount -- residual balloon amount (RBA). + !Duration -- maximal duration of unfolded RBA + deriving (Eq, Ord) + +-- | Each installment is equal to given amount but the last one. +-- Datatype elements: principal, duration in freq units, 1st instalment postponement in duration units, +-- yearly effective interest rate, regular instalment amount. +data ReversBalloon = ReversBalloon !Amount -- principal + !Duration -- duration till balloon (inclusive) in freq units + !Duration -- 1st instalment postponement in duration units + !Rate -- yearly effective interest rate + !Amount -- regular instalment amount. + deriving (Eq, Ord) + +-- | Balloon, where all installments equal 0 except last one which contains full +-- capital and first one which contains interest. +-- Datatype elements: principal, duration in freq units, 1st instalment postponement in duration units, +-- yearly effective interest rate. +data Bullet = Bullet !Amount -- principal + !Duration -- duration till balloon (inclusive) in freq units + !Duration -- 1st instalment postponement in duration units + !Rate -- yearly effective interest rate + deriving (Eq, Ord) + + +instance Show Classical where + show (Classical p n d r) = showWithLen 20 "Classical" ++ + showAmtWithLen 11 p ++ + showWithLen 5 n ++ + showWithLen 3 d ++ + " " ++ show r +instance Show Balloon where + show (Balloon p n d r b) = showWithLen 20 "Balloon" ++ + showAmtWithLen 11 p ++ + showWithLen 5 n ++ + showWithLen 3 d ++ + " " ++ show r ++ " " ++ + showAmtWithLen 11 b +instance Show BalloonPlus where + show (BalloonPlus p n d r b) = showWithLen 20 "BalloonPlus" ++ + showAmtWithLen 11 p ++ + showWithLen 5 n ++ + showWithLen 3 d ++ + " " ++ show r ++ " " ++ + showAmtWithLen 11 b +instance Show UnfdBalloon where + show (UnfdBalloon p n d r b x) = showWithLen 20 "UnfoldedBalloon" ++ + showAmtWithLen 11 p ++ + showWithLen 5 n ++ + showWithLen 3 d ++ + " " ++ show r ++ " " ++ + showAmtWithLen 11 b ++ + showWithLen 5 x +instance Show UnfdBalloonPlus where + show (UnfdBalloonPlus p n d r b x) = showWithLen 20 "UnfoldedBalloonPlus" ++ + showAmtWithLen 11 p ++ + showWithLen 5 n ++ + showWithLen 3 d ++ + " " ++ show r ++ " " ++ + showAmtWithLen 11 b ++ + showWithLen 5 x +instance Show ReversBalloon where + show (ReversBalloon p n d r b) = showWithLen 20 "ReversBalloon" ++ + showAmtWithLen 11 p ++ + showWithLen 5 n ++ + showWithLen 3 d ++ + " " ++ show r ++ " " ++ + showAmtWithLen 11 b +instance Show Bullet where + show (Bullet p n d r) = showWithLen 20 "Bullet" ++ + showAmtWithLen 11 p ++ + showWithLen 5 n ++ + showWithLen 3 d ++ + " " ++ show r + +-- | Rounding function out of RoundingType. +myRound :: (RealFrac a) => RoundingType -> (a -> Amount) +myRound Rounded = round +myRound Truncated = truncate +
+ Haslo/CalcConstructors.hs view
@@ -0,0 +1,301 @@+ --------------------------------------------------------- +-- +-- Module : CalcConstructors +-- Copyright : Bartosz Wójcik (2010) +-- License : BSD3 +-- +-- Maintainer : bartek@sudety.it +-- Stability : Unstable +-- Portability : portable +-- +-- | Convenience functions to construct different kinds of abstract loans or 'InstalmentPlan'. +-- ------------------------------------------------------- +module Haslo.CalcConstructors + (newLoanR + ,newLoanRIL + ,newInstalmentPlanLine+ ,recalculateEffectiveRate + ) +where + +import Haslo.BasicType +import Haslo.CalcCalendar +import Haslo.CalcConfigurationType +import Haslo.InstalmentPlan +import Haslo.Calculator +import Haslo.ErrorHandling +import Haslo.Parameters +import Data.Maybe (fromJust) +import Control.Monad.Reader + +moduleName = "CalcConstructors" + +-- ------ Loan constructors --------------- +-- | Instalment details have some assumptions: +-- late interest accrue interest +-- installment is always in full installment duration +-- there are no gaps between instalments, though instalment amount can be = 0 +newInstalment :: Amount -- ^ capital before installment (excluded late interest) + -> Rate -- ^ rate in frequency unit + -> Amount -- ^ instalment amount; when < 0 will be forced to interest + late interest + -> Interest -- ^ late interest to be paid by the instalment + -> Instalment -- ^ full details for intalment +newInstalment c r i lateInterest + | i < 0 = I interestPaid' + 0 + interest + interestPaid' + | otherwise = I i + capitalPaid + interest + interestPaid + where interest = (fromIntegral c + lateInterest) * r + interestPaid' = round $ interest + lateInterest + interestPaid = min i $ round $ interest + lateInterest + capitalPaid = i - interestPaid + +-- | Instalment details based on next instalment details +-- Constrain: rate >= 0 +-- Late interest cannot be recognized. +prevInstalment :: Amount -- ^ capital after instalment + -> Rate -- ^ rate in frequency unit + -> Amount -- ^ instalment amount + -> Instalment +prevInstalment c r i = I i + capitalPaid + interest + interestPaid + where interest = fromIntegral (c + i) * r / (1 + r) + interestPaid = round interest + capitalPaid = i - interestPaid -- constrain for r >=0 ! + + +-- | Creates one line of instalment plan +newInstalmentPlanLine :: Amount -- ^ capital before excluded late interest + -> Interest -- ^ total late interest before instalment + -> Rate -- ^ nominal rate in frequency units + -> Amount -- ^ instalment amount + -> InstalmentPlanLine -- ^ instalment plan one row +newInstalmentPlanLine c iL r i = IPL inst + (c - iRepayment inst) + (iL + iInterest inst -- late interest can be negative + - fromIntegral (iIntPaid inst)) + r + where inst = newInstalment c r i iL + +-- | Creates one line of instalment plan +prevInstalmentPlanLine :: Amount -- ^ capital after instalment excluded late interest + -> Rate -- ^ rate in frequency units + -> Amount -- ^ instalment amount + -> InstalmentPlanLine -- ^ instalment plan one row +prevInstalmentPlanLine c r i = IPL inst + c + 0 + r + where inst = prevInstalment c r i + + + +-- | Calculates instalment plan for input where interest rate is not known.+-- Specialization of 'newLoanRIL'. +newLoanR :: Amount -- ^ principal + -> [Amount] -- ^ list of instalment amounts + -> ValidMonad InstalmentPlan +newLoanR = newLoanRIL 0 + + +-- | Calculates instalment loan details for input where interest rate is not known. +-- Checks whether InstalmentPlan amortizes properly. If not throws apropriate error. +-- Amotizes means: no deferred interest remains. Remaining capital is 0. +-- Additionaly allows to move part of interest to the begining of the loan. +-- This feature is usefull for early regulation case, first interest is paid before principal. +newLoanRIL :: Interest -- ^ late interest - amount of interest to be taken before principal. + -> Amount -- ^ capital + -> [Amount] -- ^ list of instalment amounts + -> ValidMonad InstalmentPlan +newLoanRIL iL c is = rateIrr is (c + round iL) >>= check . newLoanR' c is iL + where newLoanR' _ [] _ _ = [] + newLoanR' c (i:is') iL' r = newIPL + : newLoanR' (iplPrincipal newIPL) is' (iplIntLate newIPL) r + where newIPL = newInstalmentPlanLine c iL' r i + check ip | lastCap /= 0 && r > 0 = throwError $ NotAmortized lastCap ip + | lastCap > fromIntegral (length is) && + r == 0 = throwError $ NotAmortized lastCap ip + | abs lastDefInt > 1e-2 = throwError $ NotPaidDefferedInterest lastDefInt ip + | otherwise = return ip + where IPL _ lastCap lastDefInt r = last ip++-- | Gives recalculated single effective interest rate of given loan.+recalculateEffectiveRate :: Freq -> InstalmentPlan -> ValidMonad Rate+recalculateEffectiveRate fr ip = liftM (cN2E fr) $ rateIrr (instList ip) (initPrincipal ip)++-- ============================================================================= + +-- | Classical Loan: all instalments are equal. +instance ClassicLoan Classical where + newLoanI (Classical c n d rE) = + ask >>= \param -> + let r = cE2N (freq param) rE + i = myRound (rounding param) $ rawCalcInstCl (fromIntegral c) n r d + in lift $ newLoanR c $ replicate d 0 ++ replicate n i + + extract (Classical p n d r) = InstalmentLoanData p n d r + +-- | Balloon: last instalment is given, the other are equal. +instance ClassicLoan Balloon where + newLoanI (Balloon c n d rE b) = + ask >>= \param -> + let r = cE2N (freq param) rE + i = myRound (rounding param) $ rawCalcInstBal (fromIntegral c) n r d (fromIntegral b) + in lift $ newLoanR c $ replicate d 0 ++ replicate (n-1) i ++ [b] + + extract (Balloon p n d r _) = InstalmentLoanData p n d r + +-- | BalloonPlus: last instalment is equal to normal instalment + given amount. +-- The other instalments are equal. +instance ClassicLoan BalloonPlus where + newLoanI (BalloonPlus c n d rE b) = + ask >>= \param -> + let r = cE2N (freq param) rE + i = myRound (rounding param) $ rawCalcInstBalPlus (fromIntegral c) n r d + (fromIntegral b) + in lift $ newLoanR c $ replicate d 0 ++ replicate (n-1) i ++ [b + i] + + extract (BalloonPlus p n d r _) = InstalmentLoanData p n d r + +-- | UnfdBalloon: Loan type based on Balloon type. It differs from Balloon that is unfolds balloon instalment so +-- that all instalments are equal, except the last one which is less or equal. +instance ClassicLoan UnfdBalloon where + newLoanI (UnfdBalloon c n d rE b x) = + ask >>= \param -> + let r = cE2N (freq param) rE + i = myRound (rounding param) $ rawCalcInstBal (fromIntegral c) n r d (fromIntegral b) + durOfBal :: Integer + durOfBal = calcDurCl (fromIntegral $ calcCapBeforeBal b r) (fromIntegral i) r 0 truncate + durOfBal' :: Int + durOfBal' = fromIntegral durOfBal + lastInst = round $ rawCalcInstCl (fromIntegral $ capBeforeLast) 1 r 0 + capBeforeLast = calcCapAfterN c i (n - 1 + durOfBal') d r + unfoldedBalloon | durOfBal > fromIntegral x = replicate x newI + | otherwise = replicate durOfBal' i ++ [lastInst] + where newI = myRound (rounding param) $ + rawCalcInstCl (fromIntegral $ calcCapBeforeBal b r) x r 0 + in lift $ newLoanR c $ replicate d 0 ++ replicate (n-1) i ++ unfoldedBalloon + + extract (UnfdBalloon p n d r _ _) = InstalmentLoanData p n d r + +-- | Loan type based on Balloon Plus type. It differs from Balloon Plus that is unfolds balloon instalment so +-- that all instalments are equal, except the last one which is less or equal. +instance ClassicLoan UnfdBalloonPlus where + newLoanI (UnfdBalloonPlus c n d rE b x) = + ask >>= \param -> + let r = cE2N (freq param) rE + i = myRound (rounding param) $ + rawCalcInstBalPlus (fromIntegral c) n r d (fromIntegral b) + durOfBal :: Integer + durOfBal = calcDurCl (fromIntegral $ calcCapBeforeBal b r) + (fromIntegral i) r 0 truncate + durOfBal' :: Int + durOfBal' = fromIntegral durOfBal + lastInst = round $ rawCalcInstCl (fromIntegral $ capBeforeLast) 1 r 0 + capBeforeLast = calcCapAfterN c i (n + durOfBal') d r + unfoldedBalloon | durOfBal > fromIntegral x = replicate x newI + | otherwise = replicate durOfBal' i ++ [lastInst] + where newI = myRound (rounding param) $ + rawCalcInstCl (fromIntegral $ calcCapBeforeBal b r) x r 0 + in lift $ newLoanR c $ replicate d 0 ++ replicate n i ++ unfoldedBalloon + + extract (UnfdBalloonPlus p n d r _ _) = InstalmentLoanData p n d r + +-- | Loan type Balloon like. It differs from Balloon that its regural instalment +-- is given and balloon amount is to be calculated. +instance ClassicLoan ReversBalloon where + newLoanI (ReversBalloon c n d rE i) = + ask >>= \param -> + let r = cE2N (freq param) rE + b = myRound (rounding param) $ + rawCalcBalBal (fromIntegral c) n r d (fromIntegral i) + in lift $ newLoanR c (replicate d 0 ++ replicate (n-1) i ++ [b]) + + extract (ReversBalloon p n d r _) = InstalmentLoanData p n d r + +-- | Loan type Balloon like. Its all instalment equal zero except the last one which equals principal +-- and the first one which contains all interest. +instance ClassicLoan Bullet where + newLoanI (Bullet c n d rE) = + ask >>= \param -> + let r = cE2N (freq param) rE + fstInst = myRound (rounding param) $ + rawCalcMaxFstInst (fromIntegral c) n r d + in lift $ newLoanRIL (fromIntegral fstInst) c $ + replicate d 0 ++ [fstInst] ++ replicate (n-2) 0 ++ [c] + + extract (Bullet p n d r) = InstalmentLoanData p n d r + +-- =================== Additional instances ==================== + +instance Balloons Balloon where + balloon (Balloon _ _ _ _ b) = b + +instance Balloons BalloonPlus where + balloon (BalloonPlus _ _ _ _ b) = b + +instance Balloons UnfdBalloon where + balloon (UnfdBalloon _ _ _ _ b _) = b + +instance Balloons UnfdBalloonPlus where + balloon (UnfdBalloonPlus _ _ _ _ b _) = b + +instance Balloons Bullet where + balloon (Bullet p _ _ _ ) = p + +instance UnfdBalloons UnfdBalloon where + eXtendedDuration (UnfdBalloon _ _ _ _ _ x) = x + +instance UnfdBalloons UnfdBalloonPlus where + eXtendedDuration (UnfdBalloonPlus _ _ _ _ _ x) = x + +-- ======================================================================= +-- =========== Functions not yet or ever used ============================ +-- ======================================================================= + +-- | Adjusts 1st instalment of 'InstalmentPlan' by addind giving amout to +-- instalment amount and to paid late interest (the latter due to instalment balance rule). +-- There is no amount control. +adj1stInst :: Amount -- ^ Amount of late interest 1st instalment is to be adjusted by + -> InstalmentPlan + -> InstalmentPlan +adj1stInst iL ip = adjIPL iL (head ip) : tail ip + +adjIPL iL ipl = ipl { iplInst = newI } + where i = iplInst ipl + newI = i {iAmt = iAmt i + iL + ,iIntPaid = iIntPaid i + iL + } + +callLateInstalment ipl = ipl { iplInst = newI } + where newI = i {iAmt = iAmt i - iIntPaid i + ,iIntPaid = 0 + } + i = iplInst ipl + +-- | Calculates initial part of the loan - means doesn't have to finish with capital 0 at the end. +initLoan :: Amount -- ^ capital. + -> [Amount] -- ^ list of instalment amounts + -> Rate -- ^ interest rate in frequency units + -> Interest -- ^ late interest on the begining. Ignored if capital == 0 + -> InstalmentPlan +initLoan _ [] _ _ = [] +initLoan c (i:is) r iL = newIPL : initLoan (iplPrincipal newIPL) is r (iplIntLate newIPL) + where newIPL = newInstalmentPlanLine c iL r i + +-- | Calculates backwards last part of the loan. +tailLoan :: [Amount] -- ^ list of instalment amounts + -> Rate -- ^ interest rate in frequency units + -> InstalmentPlan +tailLoan is r = reverse $ tailLoan' 0 (reverse is) r + where tailLoan' _ [] _ = [] + tailLoan' c (i:is) r = newIPL : tailLoan' (iRepayment $ iplInst $ newIPL) is r + where newIPL = prevInstalmentPlanLine c r i + +
+ Haslo/Calculator.hs view
@@ -0,0 +1,268 @@+--------------------------------------------------------- +-- +-- Module : Calculator +-- Copyright : Bartosz Wójcik (2010) +-- License : BSD3 +-- +-- Maintainer : bartek@sudety.it +-- Stability : Unstable +-- Portability : portable +-- +-- | Basic loan arithmetics. +--------------------------------------------------------- +module Haslo.Calculator (cE2N + ,cN2E + ,rawCalcInstCl + ,rawCalcInstBal + ,rawCalcInstBalPlus + ,rawCalcBalBal + ,rawCalcMaxFstInst + ,rateIrr + ,calcDurCl + ,calcCapBeforeBal + ,calcCapAfterN + ,calcCapCl + ) +where + +import Haslo.BasicType +import Haslo.CalcCalendar (Freq,freqPerYear) +import Haslo.ErrorHandling + +moduleName = "Calculator" +-- ============================ +-- Set of calculation functions +-- ============================ +-- | Converse yealry Effective rate to Nominal one. +-- Nominal is in the frequency mode +cE2N :: Freq -> Rate -> Rate +cE2N fr r = (1+r)**(1/n) - 1 + where n = freqPerYear fr+ +-- | Converse Nominal rate to yearly Effective one. +-- Nominal is in the frequency mode +cN2E :: Freq -> Rate -> Rate +cN2E fr r = (1+r)^n - 1 + where n = freqPerYear fr + +-- Raw installment and capital calculations for some type of loans (raw - not adjusted to Amount). +-- c-capital amount +-- i-installment amount +-- n-number of instalments +-- r-interest rate in same frequency as installment +-- d - 1st installment delay in frequency units+-- | Cacluates principal for given instalment amount, duration, interest rate and 1st instalment deferrment. +rawCalcCapCl :: Double -- ^ Instalment amount deAmounted+ -> Duration -- ^ Loan duration+ -> Rate -- ^ Interest rate+ -> Duration -- ^ 1st instalment deferrment+ -> Double+rawCalcCapCl i n 0 _ = i * (fromIntegral n) +rawCalcCapCl i n r d = i * (1-p^n) / (p^n * (1-p)) / p^d + where p = (1+r)**(1/12) ++-- | Calculates raw instalment amount -- not yet rounded or truncated.+rawCalcInstCl :: Double -- ^ Principal deAmounted+ -> Duration -- ^ Loan duration+ -> Rate -- ^ Interest rate+ -> Duration -- ^ 1st instalment deferrment+ -> Double+rawCalcInstCl _ 0 _ _ = 0 +rawCalcInstCl c n 0 _ = c / (fromIntegral n) +rawCalcInstCl c n r d = c * p^d * p^n * r / (p^n - 1) + where p = r+1 + +-- b - balloon installment amount +rawCalcCapBal b i n 0 d = i * fromIntegral (n-1) + b +rawCalcCapBal b i n r d = b/p^n + rawCalcCapCl i (n-1) r d + where p = r+1 + +-- | Cacluates instalment amount for given principal, duration, interest rate, 1st instalment deferrment and +-- balloon amount.For @Balloon@ loan constructor. +rawCalcInstBal :: Double -- ^ Principal deAmounted+ -> Duration -- ^ Loan duration+ -> Rate -- ^ Interest rate+ -> Duration -- ^ 1st instalment deferrment+ -> Double -- ^ Balloon amount deAmounted+ -> Double+rawCalcInstBal _ 0 _ _ _ = 0 +rawCalcInstBal c n 0 _ b = (c - b) / (fromIntegral n - 1) +rawCalcInstBal c n r d b = (c * p^d - b/p^n) * p^m * r / (p^m - 1) + where p = r+1 + m = n-1 +--rawCalcInstBal c n r d b = (c * p^d - b*q^n)*(q-1) / (q^n - q) +-- where p = r+1 +-- q = 1/p + +-- | Cacluates ballon amount for given principal, duration, interest rate, 1st instalment deferrment and +-- instalment amount. +rawCalcBalBal :: Double -- ^ Principal deAmounted+ -> Duration -- ^ Loan duration+ -> Rate -- ^ Interest rate+ -> Duration -- ^ 1st instalment deferrment+ -> Double -- ^ Instalment amount deAmounted+ -> Double+rawCalcBalBal c n 0 _ i = c - i * (fromIntegral n - 1) +rawCalcBalBal c n r d i = (c * p^d - i * (q^n - q)/(q - 1)) * p^n + where p = r+1 + q = 1/p + +-- | Balloon Plus -- balloon amount + usual amount = last instalment amount +rawCalcCapBalPlus b i n 0 d = i * fromIntegral n + b +rawCalcCapBalPlus b i n r d = b/p^n + rawCalcCapCl i n r d + where p = (1+r)**(1/12) + +-- | Cacluates instalment amount for given principal, duration, interest rate, 1st instalment deferrment and +-- balloon amount. For @BalloonPlus@ loan constructor. +rawCalcInstBalPlus :: Double -- ^ Principal deAmounted+ -> Duration -- ^ Loan duration+ -> Rate -- ^ Interest rate+ -> Duration -- ^ 1st instalment deferrment+ -> Double -- ^ Balloon amount deAmounted+ -> Double+rawCalcInstBalPlus _ 0 _ _ _ = 0 +rawCalcInstBalPlus c n 0 _ b = (c - b) / fromIntegral n +rawCalcInstBalPlus c n r d b = (c*p^(n+d) - b)*r / (p^n - 1) + where p = r+1 + +rawCalcBalBalPlus c n 0 _ i = c - i * fromIntegral n +rawCalcBalBalPlus c n r d i = c*p^d - i * (1 - p^n)/(1 - p) + where p = (1+r)**(1/12) + +-- | Calculated maximum amount of first instalment: for @Bullet@ loan constructor. +rawCalcMaxFstInst :: Double -- ^ Principal deAmounted+ -> Duration -- ^ Instalment duration+ -> Rate -- ^ Nominal interest rate+ -> Duration -- ^ 1st instalment deferrment+ -> Double+rawCalcMaxFstInst _ _ 0 _ = 0 +rawCalcMaxFstInst c n r d = c * p^d * (p^n - 1)/(p^(n-1)) + where p = 1+r + +-- | Instalment amount under when k-th instalment is greater than calculated one +-- by x. +--calcInstWithKth x k c n r d = rawCalcInstCl c r n d - x * (r+1)^(n-k-1) * r / ((r+1)^n - 1) +--calcInstBalWithKth x k c n r d b = rawCalcInstBal c n r d b - x * (r+1)^(n-k-1) * r / ((r+1)^(n-1) - 1) + +-- Nominal interest rate calculation. Raphson-Newton algorithm. +-- i1-1st installment amount +-- d1-number of days between financing and 1st installment date +rateCl1stI :: (RealFloat a, Integral b) => a -> a -> a -> a -> a -> a -> a -> b -> a +rateCl1stI i i1 c n d d1 r count | abs (f / ff) < 0.000000001 = r - f / ff + | count > 30 = -1 -- error case + | otherwise = rateCl1stI i i1 c n d d1 (r - f / ff) (count + 1) + where f = i*((r+1)**(n-1)- 1)/(r+1)**n / r + i1/(r+1) - c*(r+1)**(d + d1/30 - 1) + ff = i*((n-1)*(r+1)**(n-1)*r - ((r+1)**(n-1) - 1) * (n*r+r+1)) + / (r+1)**(n+1) / r^2 + - i1/(r+1)^2 + - c*(d + d1/30 - 1) + +-- is-installment list +-- f-sum is(i)/(r+1)^i - c*... +-- ff-sum -i*is(i)/(r+1)^(i+1) - c*... +-- Important! 'r' is here first, as hoc extrapolation of nominal interest rate. Suggested value [0.001 - 0.01]. +-- If value is out of this range, result may not be calculated in foreseen 30 steps +rateIrr1stI :: [Amount] + -> Amount + -> Int + -> ValidMonad Rate +rateIrr1stI is c d1 = rateIrr1stI' (map fromIntegral is) (fromIntegral c) (length is) 0 (fromIntegral d1) 0.1 0 +rateIrr1stI' is c n d d1 r count | abs (f / ff) < 0.000000001 = return $ r - f / ff + | count > 30 = throwError $ OtherError $ + "rateIrr1stI c:" ++ + show c ++ + " d1:" ++ show d1 ++ + " is:" ++ show is + | otherwise = rateIrr1stI' is c n d d1 (r - f / ff) (count + 1) + where f = sum(map (uncurry (/)) (zip is [(1+r)^i|i <- [1..n]])) + - c*(r+1)**(d + d1 / 30 - 1) + ff = sum(map (uncurry (/)) (zip (map (uncurry (*)) (zip is [fromIntegral(-i)|i <- [1..n]])) [(1+r)^(i+1)|i <- [1..n]])) + - c*(d + d1/30 - 1) + +-- | General rate calculation. +-- Delay of 1st installment is integrated into instalment list +rateIrr :: [Amount] -- ^ list of instalment amounts + -> Amount -- ^ initial principal + -> ValidMonad Rate +rateIrr is c = case rateIrr' (map fromIntegral is) (fromIntegral c) (length is) 0.01 0 of + Right x -> Right x + Left _ -> rateIrr' (map fromIntegral is) (fromIntegral c) (length is) eps 0 + where eps = fromIntegral (sum is - c) / fromIntegral c / fromIntegral (length is) / 1000 + +rateIrr' is c n r count | epsilon < 1e-13 && result < 0 && eta > (-2) = return 0 + | epsilon < 1e-13 && result < 0 = throwError $ OtherError $ + "negative rate r=" ++ show result ++ + " is:" ++ show is + | epsilon < 1e-13 = return result + | count > 30 = throwError $ OtherError $ + "rateIrr c:" ++ + show c ++ + " is:" ++ show is + | otherwise = rateIrr' is c n (r - f / ff) (count + 1) + where f = sum(map (uncurry (/)) (zip is [(1+r)^i|i <- [1..n]])) - c + ff = sum $ map (uncurry (/)) + (zip (map (uncurry (*)) + (zip is [fromIntegral(-i)|i <- [1..n]])) + [(1+r)^(i+1)|i <- [1..n]]) + epsilon = abs (f / ff) + result = r - f / ff + eta = result * c + +rateCl :: Amount -- ^ instalment amount + -> Amount -- ^ principal + -> Duration -- ^ duraiton in freq units + -> Duration -- ^ 1st instalment postponement in freq units + -> ValidMonad Rate +rateCl i c n d = rateCl' (fromIntegral i) (fromIntegral c) (fromIntegral n) (fromIntegral d) 0.01 0 +rateCl' i c n d r count | abs (f / ff) < 1e-13 = return $ r - f / ff + | count > 30 = throwError $ OtherError $ + "rateCl c:" ++ + show i ++ + "c:" ++ show c ++ + "n:" ++ show n ++ + "d:" ++ show d + | otherwise = rateCl' i c n d (r - f / ff) (count + 1) + where f = i*((r+1)**n - 1) / (r+1)**n / r - c * (r+1)**d + ff = i*(n*(r+1)**n * r - ((r+1)**n - 1) * (n*r+r+1)) / (r+1)**(n+1) / r^2 - c * d + + +-- | Calculates duration of regural loan in the units of interest rate. +calcDurCl :: Double -- ^ Principal deAmounted+ -> Double -- ^ Instalment amount deAmounted+ -> Rate -- ^ Nominal interest rate+ -> Duration -- ^ Instalment duration+ -> (Double -> Amount) -- ^ Rounding function (truncate or round) - taken from parameters+ -> Amount+calcDurCl _ 0 _ _ _ = 0 +calcDurCl c i 0 _ roundingFun = roundingFun $ c / i +calcDurCl c i r d roundingFun = roundingFun $ log (i / (i + c * p^d * (1-p))) / log p + where p = 1+r + +-- | Same calcualtions as for @rawCalcCapCl@ above but rounded to @Amount@.+calcCapCl :: Double -- ^ Deintegraled instalment amount.+ -> Duration -- ^ Duration+ -> Rate -- ^ Nominal interest rate+ -> Duration -- ^ First instalment deferrment + -> Amount +calcCapCl i n r = round . rawCalcCapCl i n r +calcCapBal b i n r = round . rawCalcCapBal b i n r +calcCapBalPlus b i n r = round . rawCalcCapBalPlus b i n r + +-- | Calculates principal before last installment +calcCapBeforeBal :: Amount -- ^ Balloon amount+ -> Rate -- ^ Nominal interest rate+ -> Amount +calcCapBeforeBal b r = round $ fromIntegral b / (1+r) + +-- | calculates principal after Nth installment +calcCapAfterN :: Amount -- ^ Principal+ -> Amount -- ^ Instalment amount+ -> Duration -- ^ Loan duration+ -> Duration -- ^ First instalment deferrment+ -> Rate -- ^ Nominal interest rate+ -> Amount +calcCapAfterN c i n _ 0 = c - i * fromIntegral n +calcCapAfterN c i n d r = truncate $ fromIntegral c * (1+r)^(n+d) - fromIntegral i * (rXn - 1) / r + where rXn = (1+r)^n + +
+ Haslo/ErrorHandling.hs view
@@ -0,0 +1,128 @@+--------------------------------------------------------- +-- +-- Module : ErrorHandling +-- Copyright : Bartosz Wójcik (2010) +-- License : BSD3 +-- +-- Maintainer : bartek@sudety.it +-- Stability : Unstable +-- Portability : portable +-- +-- Error handling data structures, functions, etc. +--------------------------------------------------------- +-- | Provides data types and basic functions allowing better and direct error handling. +module Haslo.ErrorHandling (module Control.Monad.Error + ,module Haslo.BasicType + ,ValidationError (..) + ,ValidMonad + ) +where + +import Control.Monad.Error.Class +import Control.Monad.Error +import Haslo.BasicType +import Haslo.InstalmentPlan +import Text.PrettyShow +import Data.Time (Day + ,fromGregorian) + +-- | Error handlig data type. +data ValidationError = + -- | Instalment discrepancy:+ -- Instalment amount, Repayment, Interest paid + InstalmentDiscrepancy !Amount !Amount !Amount + + -- | Incorect interest:+ -- Principal before, Late interest before, Interest calculated (incorectly),+ -- Interest rate + | IPLInterest !Amount !Interest !Interest !Rate + + -- | Simple follow up interest mismatch (a1 + a2 - a3 /= a4):+ -- Error description, a1, a2, a3, a4 + | FollowUpInterestError String !Interest !Interest !Amount !Interest + + -- | Simple follow up mismatch (a1 - a /= a2):+ -- Error description, a1, a, a2 + | FollowUpError String !Amount !Amount !Amount + + -- | Simpler follow up mismatch (a /= b):+ -- Error description, a, b + | SimplerFollowUpError String !Amount !Amount + + -- | Deffered interest cannot be paid. + | NotPaidDefferedInterest !Interest InstalmentPlan + + -- | Capital doesn't amortize. This is due to rounding error if happens. + | NotAmortized !Amount InstalmentPlan + + | OtherError String + + -- Any error can occur independently before and after financing. + -- Errors after financing usually are linked to date. +-- | FinancingError Day ValidationError + +-- | We make ValidationError an instance of the Error class +-- to be able to throw it as an exception. +instance Error ValidationError where + noMsg = OtherError "(!)" + strMsg s = OtherError s + +instance Show ValidationError where + show (InstalmentDiscrepancy a p iP) = "Instalment discrepancy:\ + \instalment amount /= repayment + interest paid (" + ++ (showAmtWithLen 8 a) ++ "/=" ++ + (showAmtWithLen 8 p) ++ "+" ++ + (showAmtWithLen 8 iP) ++ ")" + show (IPLInterest p iL i r) = "IPL interest discrepancy: \ + \ recalculated interest=" ++ show iR ++ + ", given interest=" ++ show i ++ + " (principal:" ++ showAmtWithLen 8 p ++ + " (late interest:" ++ showWithLenDec 11 6 iL ++ + " (interest rate:" ++ show r ++ ")" + where iR = (fromIntegral p + iL) * r + show (FollowUpInterestError msg a1 a2 a3 a4) = msg ++ + show (a1 / 100) ++ " - " ++ + show (a2 / 100) ++ " + " ++ + showAmtWithLen 8 a3 ++ " /= " ++ + show (a4 / 100) + show (FollowUpError msg a1 a a2) = msg ++ + showAmtWithLen 8 a1 ++ " - " ++ + showAmtWithLen 8 a ++ " /= " ++ + showAmtWithLen 8 a2 + show (SimplerFollowUpError msg a b) = msg ++ + showAmtWithLen 8 a ++ " /= " ++ + showAmtWithLen 8 b + show (NotPaidDefferedInterest iL ip) = "Deferred interest not paid off. Remains:" ++ + show (iL / 100) ++ + " " ++ show ip + show (NotAmortized c ip) = "Principal doesn't amortize fully. Remains:" ++ showAmtWithLen 8 c ++ + " " ++ show ip + show (OtherError msg) = msg +-- show (FinancingError d err) = show d ++ " " ++ show err + +maybeShow Nothing = "" +maybeShow (Just a) = show a ++ " " + +-- | Monad wrapping error message or correct value. Broadly used. +type ValidMonad = Either ValidationError ++ +--errMsg moduleName functionName msg = throwError $ OtherError $ +-- moduleName ++ " " ++ +-- functionName ++ " " ++ +-- msg + +{-errDate :: Day -> ValidMonad a -> ValidMonad a +errDate d (Left (FollowUpError w x y z _)) = Left $ FollowUpError w x y z (Just d) +errDate d (Left (InstalmentDiscrepancy w x y z _)) = Left $ InstalmentDiscrepancy w x y z (Just d) +errDate d (Left (IPLInterest w x y z _)) = Left $ IPLInterest w x y z (Just d) +errDate _ x = x-} + +--data FinancingError = SFinancingError Day ValidationError + +-- | Allows adding date to any error of @ValidationError@ type. +--liftDate :: MonadError ValidationError m => Day +-- -> ValidationError +-- -> m a +--liftDate date err = throwError $ FinancingError date err +
+ Haslo/InstalmentPlan.hs view
@@ -0,0 +1,101 @@+--------------------------------------------------------- +-- +-- Module : InstalmentPlan +-- Copyright : Bartosz Wójcik (2010) +-- License : BSD3 +-- +-- Maintainer : bartek@sudety.it +-- Stability : Unstable +-- Portability : portable +-- +-- | This module implements 'InstalmentPlan' data type. +--------------------------------------------------------- +module Haslo.InstalmentPlan +where +import Haslo.BasicType +import Text.PrettyShow + + +-- | Single instalment details. +-- Single instalment is always balanced - see @instalmentCheck@. +data Instalment = I { iAmt :: Amount -- ^ installment amount + , iRepayment :: Amount -- ^ repayment + , iInterest :: Interest -- ^ interest calculated over the full period + , iIntPaid :: Amount -- ^ interest paid with installment + } + deriving (Eq,Ord) + +instance Show Instalment where + show (I i c int iP) = showAmtWithLen 10 i ++ "=" + ++ showAmtWithLen 10 c ++ "+" + ++ showAmtWithLen 8 iP ++ "; " + ++ show (int / 100) + + + + +-- | Single element of 'InstalmentPlan'. +-- One element of instlment plan contains following ideas: +-- - details of instalment (see above) +-- - principal after instalment has matured +-- - late not yet paid interest (which are base for interest accrual) +-- - interest rate of current instalment +data InstalmentPlanLine = IPL { iplInst :: Instalment -- ^ Instalment itself + , iplPrincipal :: Amount -- ^ Principal after + , iplIntLate :: Interest -- ^ Sum of not paid late interest after current instalment + , iplRate :: Rate -- ^ Nominal interest rate + } + deriving (Eq,Ord) + +instance Show InstalmentPlanLine where + show (IPL i c iL r) = "(" ++ show i ++ ") " + ++ showAmtWithLen 11 c ++ " " + ++ show (iL / 100) ++ " " + ++ show r + + + +-- | Plan of instalments reflects whole life of loan. Following rules are implemented: +-- +-- * All instalments are regular duration. Duration is not stored within instalment plan. +-- +-- * Each instalment can have its own interest rate which is stored within each row separately. +-- +-- * Any payments postponements like first intalment postponement are implemented as +-- usual instalment with instalment amount equal 0. +type InstalmentPlan = [InstalmentPlanLine] + + +-- | Unwraps list of instalment amounts from InstalmentPlan +instList :: InstalmentPlan -> [Amount] +instList = map (iAmt . iplInst) + +-- | Initial principal of a loan +initPrincipal :: InstalmentPlan -> Amount +initPrincipal ip = iplPrincipal ipl + (iRepayment . iplInst) ipl + where ipl = head ip ++-- | Folded instalment plan is another view of the loan.+-- It folds all instalments of same amount and interest rate into one entry of given duration.+type FoldedInstalmentPlan = [FoldedInstalmentPlanLine]++-- | One entry of @FoldedInstalmentPlan@.+data FoldedInstalmentPlanLine = FIPL { fiplAmt :: !Amount -- ^ Instalment amount.+ , fiplDur :: !Duration -- ^ Number of such instalments in this series.+ , fiplRate :: !Rate -- ^ Interest rate of series.+ }+ deriving (Eq, Ord, Show)+ + +-- | Folds @InstalmentPlan@ to @FoldedInstalmentPlan@+foldIP :: InstalmentPlan -> FoldedInstalmentPlan+foldIP [] = []+foldIP (ipl : ip) = foldIP' ip [iplToFipl ipl]+ where iplToFipl (IPL (I a _ _ _) _ _ r) = FIPL a 1 r+ incDuration (FIPL a n r) = FIPL a (n+1) r+ foldIP' [] result = reverse result+ foldIP' (ipl : ip) (x:xs) | a == fiplAmt x && r == fiplRate x = foldIP' ip (incDuration x : xs)+ | otherwise = foldIP' ip (iplToFipl ipl : x : xs)+ where a = (iAmt $ iplInst ipl)+ r = (iplRate ipl)+
+ Haslo/InstalmentPlanProps.hs view
@@ -0,0 +1,219 @@+--------------------------------------------------------- +-- +-- Module : InstalmentPlanProps +-- Copyright : Bartosz Wójcik (2010) +-- License : BSD3 +-- +-- Maintainer : bartek@sudety.it +-- Stability : Unstable +-- Portability : portable +-- +-- | This module implements properities of 'InstalmentPlan'. +--------------------------------------------------------- +module Haslo.InstalmentPlanProps ( instalmentPlanCheck + ,instalmentPlanLineCheck + ,instalmentPlanCheckM + ,instalmentPlanLineCheckM + ,initIPL + ,rateCheckTruncated + ,rateCheckTruncatedM + ,rateCheckRounded + ,rateCheckRoundedM + ) +where + +import Haslo.BasicType +import Haslo.CalcCalendar +import Haslo.CalcConstructors +import Haslo.ErrorHandling +import Haslo.Parameters +import Haslo.InstalmentPlan +--import CalcConfigurationType + +-- | Condition each installment has to fulfil +instalmentCheck :: Instalment -> Bool +instalmentCheck i = iAmt i == iRepayment i + iIntPaid i + +-- | Control function with diagnostic for humans. +instalmentCheckM :: Instalment -> ValidMonad () +instalmentCheckM i | iAmt i == iRepayment i + iIntPaid i = return () + | otherwise = throwError $ InstalmentDiscrepancy + (iAmt i) + (iRepayment i) + (iIntPaid i) + +-- | For control purposes artificial initial 'InstalmentPlanLine' has to be constructed+initIPL :: Amount -> Interest -> InstalmentPlanLine +initIPL cap iL = (IPL (I 0 0 0 0) cap iL 0) ++-- | Condition, each InstalmentPlanLine fulfills before financing. +instalmentPlanLineCheck :: Amount + -> Interest + -> InstalmentPlanLine + -> Bool +instalmentPlanLineCheck capBefore lateIntBefore ipl = + abs ((fromIntegral capBefore + lateIntBefore) * iplRate ipl - (iInterest . iplInst) ipl) < 1e-4 && + abs (lateIntBefore - (fromIntegral . iIntPaid . iplInst) ipl + + (iInterest . iplInst) ipl + - iplIntLate ipl) < 1e-2 && + capBefore - (iRepayment . iplInst) ipl == iplPrincipal ipl && + (instalmentCheck . iplInst) ipl + +-- | Control function with diagnostic for humans. +instalmentPlanLineCheckM :: InstalmentPlanLine + -> InstalmentPlanLine + -> ValidMonad InstalmentPlanLine +instalmentPlanLineCheckM ipl1 ipl2 = + (instalmentCheckM . iplInst) ipl2 >> checkInterestM >> checkLateInterestM >> checkCapitalAfterM + where checkInterestM | abs (intReCalcul - intStored) < 1e-2 = return ipl2 + | otherwise = throwError $ IPLInterest capBefore + lateIntBefore + intStored + (iplRate ipl2) + checkLateInterestM | checkLateInterest = return ipl2 + | otherwise = throwError $ FollowUpInterestError errorLateInt + lateIntBefore + (iInterest $ iplInst ipl2) + (iIntPaid $ iplInst ipl2) + (iplIntLate ipl2) + checkCapitalAfterM | checkCapitalAfter = return ipl2 + | otherwise = throwError $ FollowUpError errorPrincipal + capBefore + (iRepayment $ iplInst ipl2) + (iplPrincipal ipl2) + intReCalcul = ((fromIntegral capBefore + lateIntBefore) * iplRate ipl2) + intStored = (iInterest . iplInst) ipl2 + checkLateInterest = abs (lateIntBefore - (fromIntegral . iIntPaid . iplInst) ipl2 + + (iInterest . iplInst) ipl2 + - iplIntLate ipl2) < 1e-2 + checkCapitalAfter = capBefore - (iRepayment . iplInst) ipl2 == iplPrincipal ipl2 + capBefore = iplPrincipal ipl1 + lateIntBefore = iplIntLate ipl1 + errorLateInt = "IPL discrepancy: late interest before + interest calculated -\ + \ interest paid /= late interest after" + errorPrincipal = "IPL discrepancy: principal before - principal matured /= principal after" + +-- | Checks if all lines of installment plan fulfill their +-- validation rules. +instalmentPlanCheck :: Amount -- ^ Initial principal + -> Interest -- ^ Initial late interest (usually 0) + -> InstalmentPlan -- ^ Instalment Plan to be checked + -> Bool +instalmentPlanCheck cap iL ip = (\(x,cf,iLf,i,r) -> x && -- All rows of instlment plan are OK + (cf == 0 || -- Amortizes properly to 0 + cf < fromIntegral (length ip) && r == 0) && + abs iLf < 1e-2) $ -- There is no late interest left + foldl check (True, cap, iL, I 0 0 0 0, 0) ip + where check :: (Bool,Amount,Interest,Instalment,Rate) + -> InstalmentPlanLine + -> (Bool,Amount,Interest,Instalment,Rate) + check (bool,cap,iL,_,_) ipl = (bool && instalmentPlanLineCheck cap iL ipl + ,iplPrincipal ipl + ,iplIntLate ipl + ,iplInst ipl + ,iplRate ipl) + +-- | Control function with diagnostic for humans. +instalmentPlanCheckM :: Amount -- ^ Initial principal + -> Interest -- ^ Initial late interest (usually 0) + -> InstalmentPlan -- ^ Instalment Plan to be checked + -> ValidMonad () +instalmentPlanCheckM cap iL ip = foldM instalmentPlanLineCheckM (IPL (I 0 0 0 0) cap iL 0) ip + >>= lastCheck + where lastCheck (IPL i c iL r) | abs iL > 1e-2 = throwError $ NotPaidDefferedInterest iL ip + | r > 0 && c /= 0 = throwError $ NotAmortized c ip + | r == 0 && c > fromIntegral (length ip) = throwError $ NotAmortized c ip + | otherwise = return () + +-- | Checks whether instalment plan is complete, i.e. if there is no +-- remaining principal. +isInstalmentPlanComplete :: InstalmentPlan -> Bool +isInstalmentPlanComplete ip = (iplPrincipal. last) ip <= 0 + +-- All loans with fixed interest rate have to fulfil following: +-- If Truncated: interest rate is <= that given one. +-- Add 1 to each instalment amount, recalculate interest rate and check that +-- it's greater than given one. +-- If Rounded: if interest rate > given one: subtract 1 from each instalment amount, recalculate +-- interest rate and check that it's < given one. +-- if interest rate < given one: add 1 to each instalment amount, recalculate +-- interest rate and check that it's > given one.++-- | Checks recalculated interest rate against original one.+-- Works only for parameters where @Truncated@ is selected. +rateCheckTruncated :: Amount -- ^ Initial principal + -> Interest -- ^ Initial late interest (usually 0) + -> Rate -- ^ Initial interest rate in nominal format. + -> InstalmentPlan -- ^ Instalment Plan to be checked + -> ValidMonad Bool +rateCheckTruncated p iL r ip = liftM (iplRate . head) (newLoanRILPlus p iL 1 ip) >>= \r'' -> + (return $ r' - r <= e1 && r'' > r) + where r' = iplRate $ head ip + +e1 = 1e-9 + +-- | Checks recalculated interest rate against original one.+-- Works only for parameters where @Rounded@ is selected. +rateCheckRounded :: Amount -- ^ Initial principal + -> Interest -- ^ Initial late interest (usually 0) + -> Rate -- ^ Initial interest rate in nominal format. + -> InstalmentPlan -- ^ Instalment Plan to be checked + -> ValidMonad Bool +rateCheckRounded p iL r ip | r == 0 = return True + | r' > r = liftM (iplRate . head) (newLoanRILPlus p iL (-1) ip) >>= \r'' -> + (return $ r'' < r) + | r' < r = liftM (iplRate . head) (newLoanRILPlus p iL 1 ip) >>= \r'' -> + (return $ r'' > r) + | otherwise = return True + where r' = iplRate $ head ip ++-- | Like @rateCheckTruncated@ with diagnostics for humans +rateCheckTruncatedM :: Amount -- ^ Initial principal + -> Interest -- ^ Initial late interest (usually 0) + -> Rate -- ^ Initial interest rate in nominal format. + -> InstalmentPlan -- ^ Instalment Plan to be checked + -> ValidMonad InstalmentPlan +rateCheckTruncatedM p iL r ip = liftM (iplRate . head) (newLoanRILPlus p iL 1 ip) >>= \r'' -> + case r' - r <= e1 && r'' > r of + True -> return ip + False -> throwError $ OtherError $ "Interest rate not fits." ++ + " Original: " ++ show r ++ + " Recalculated: " ++ show r' ++ + " Of next higher loan: " ++ show r'' + where r' = iplRate $ head ip + +-- | Like @rateCheckRounded@ with diagnostics for humans +rateCheckRoundedM :: Amount -- ^ Initial principal + -> Interest -- ^ Initial late interest (usually 0) + -> Rate -- ^ Initial interest rate in nominal format. + -> InstalmentPlan -- ^ Instalment Plan to be checked + -> ValidMonad InstalmentPlan +rateCheckRoundedM p iL r ip | r == 0 = return ip + | r' > r = liftM (iplRate . head) (newLoanRILPlus p iL (-1) ip) >>= \r'' -> + case r'' < r of + True -> return ip + False -> throwError $ OtherError $ "Interest rate not fits." ++ + " Original: " ++ show r ++ + " Recalculated: " ++ show r' ++ + " Of next higher loan: " ++ show r'' + | r' < r = liftM (iplRate . head) (newLoanRILPlus p iL 1 ip) >>= \r'' -> + case r'' > r of + True -> return ip + False -> throwError $ OtherError $ "Interest rate not fits." ++ + " Original: " ++ show r ++ + " Recalculated: " ++ show r' ++ + " Of next higher loan: " ++ show r'' + | otherwise = return ip + where r' = iplRate $ head ip + + +-- | Adds given amount to each istalment and recalculates the InstalmentPlan. +newLoanRILPlus :: Amount + -> Interest + -> Amount + -> InstalmentPlan + -> ValidMonad InstalmentPlan +newLoanRILPlus p iL delta = newLoanRIL iL p . map (+| delta) . instList + where a +| b | a + b < 0 = 0 + | otherwise = a + b +
+ Haslo/Parameters.hs view
@@ -0,0 +1,135 @@+--------------------------------------------------------- +-- +-- Module : Parameters +-- Copyright : Bartosz Wójcik (2010) +-- License : BSD3 +-- +-- Maintainer : bartek@sudety.it +-- Stability : Unstable +-- Portability : portable +-- +-- Parameters hidden in the reader monad +--------------------------------------------------------- + +module Haslo.Parameters (IPPMonad+ ,runWithIPP+ ,InstalmentPlanParam (..)+ ,paramDT+ ,paramMT+ ,paramYT+ ,paramDR+ ,paramMR+ ,paramYR+ ) +where + +import Control.Monad.Reader +import Haslo.ErrorHandling +import Haslo.CalcCalendar + +type ParamMonad = ReaderT Param IPPMonad + +runWithParam param loan = runReaderT loan param + +data Param = Param + {ipp :: InstalmentPlanParam + ,fin :: FinancingParameters + } + deriving Show ++-- | Parameters for loan construction. They have been separated, because they +-- usually do not change often, if at all. +data InstalmentPlanParam = IPP + {freq :: Freq + ,rounding :: RoundingType + } + deriving Show+ +-- | Reader monad for IPP -- to run loan with parameters.+type IPPMonad = ReaderT InstalmentPlanParam ValidMonad ++-- | Convenience function. +runWithIPP :: InstalmentPlanParam -> IPPMonad a -> ValidMonad a+runWithIPP param loan = runReaderT loan param + +absParamMonthlyTrunc = IPP Monthly Truncated +absParamDailyTrunc = IPP Daily Truncated +absParamYearlyTrunc = IPP Yearly Truncated + +-- | Rule of 1st instalment date as function of last financing tranche's date. +data FstInstRule = FstInstDays Int -- ^ Not less than given number of days after. + | FstInstFullFreqAfter -- ^ at least one full 'instFreq' period after, less than two periods. + deriving (Eq,Show) + +instance Enum FstInstRule where + toEnum 0 = FstInstFullFreqAfter + toEnum n = FstInstDays n + + fromEnum FstInstFullFreqAfter = 0 + fromEnum (FstInstDays n) = n + +-- | Rule of calculation and maturity of interest for first period of the loan +-- from 1st tranche date till one instalment period before first instalment. +data FstIntRule = + + -- | Interest is calculated daily. + -- Mature on each instalment day. + FstIntDaily + + -- | Interest is calculated from tranche date till next instalment day, if next instalment + -- day is before first instalment date. + -- Interest matures on each instalment day. + | FstIntTillNextDue + + -- | Interest is calculated from tranche date till first instalment date. + -- Interest matures on first instalment date. + | FstIntTill1stInst + + -- | Interest is calculated from tranche date till first instalment date + -- which amount > 0. All instalments = 0 before have to be removed. Date of + -- first >0 instalment doesn't change though. + -- Interest matures on date of first >0 instalment. + | FstIntTill1stInstGT0 + + -- | Proprietary solution. + -- Interest is calculated and matures like for @FstIntTill1stInstGT0@, the difference + -- is that common nominal interest rate is re-caluclated using real number of days of + -- between tranche date and first instalment date. In this solution only one tranche is + -- alowed. + | FstIntCtlm + deriving (Eq,Show,Enum) + +-- | Rule defining what to do with late interest coming out of grace period. +data LateIntOfGracePeriodRule = + + -- | Late interest will be due on first instalment - will increase its amount. + LateIntFstInst + + -- | Late interest will be due on first significant instalment - will increase its amount. + | LateIntFstSignInst + + -- | Late interest will be due on last instalment - will increase its amount. + | LateIntLastInst + deriving (Eq,Show) + +-- | Collection of parameters customizing financing. +data FinancingParameters = FP { + instFreq :: Freq -- ^ Frequency of instalment must be >= than @intFreq@ + , intFreq :: Freq -- ^ Frequency of interest + , fin1stInstRule :: FstInstRule + , fin1stIntRule :: FstIntRule + , finCalendar :: CalendarType -- ^ calendar type + , lateIntRule :: LateIntOfGracePeriodRule + } + deriving (Eq,Show) + +paramDT = IPP Daily Truncated+-- | Monthly Truncated+paramMT :: InstalmentPlanParam +paramMT = IPP Monthly Truncated +paramYT = IPP Yearly Truncated +paramDR = IPP Daily Truncated +paramMR = IPP Monthly Rounded +-- | Yearly Rounded+paramYR :: InstalmentPlanParam +paramYR = IPP Yearly Rounded
+ HasloQC.hs view
@@ -0,0 +1,72 @@+--------------------------------------------------------- +-- +-- Module : HasloQC +-- Copyright : Bartosz Wójcik (2012) +-- License : BSD3 +-- +-- Maintainer : bartek@sudety.it +-- Stability : Unstable +-- Portability : portable +-- +-- Part of haslo. Quick Check of all necessary functions. +--------------------------------------------------------- + +module Main +where + +import Haslo +import Haslo.HasloQCTestL +import Data.IORef +import Data.List +import Data.Time +import Control.Monad.Reader +import Test.QuickCheck + + +main = do + putStrLn "Test suite run. It may take up to several minutes time." + myRun "Checking interest rate" heavyArgs propRate + myRun "Checking rate conversion" myArgs prop_cEN + myRun "Checking abstract amortization table" veryHeavyArgs propInstPlan +-- myRun "Checking AMOR T" myArgs propInstPlanM + + +myRun txt args prop = do + putStr $ txt ++ " - " ++ show (maxSize args) ++ " random tests" + putStrLn " ... " + quickCheckWith args prop + +veryHeavyArgs = Args + { replay = Nothing + , maxSuccess = 100000 + , maxDiscard = 7000 + , maxSize = 100000 + , chatty = True +-- noShrinking flag? + } + +heavyArgs = Args + { replay = Nothing + , maxSuccess = 10000 + , maxDiscard = 7000 + , maxSize = 10000 + , chatty = True +-- noShrinking flag? + } + +myArgs = Args + { replay = Nothing + , maxSuccess = 300 + , maxDiscard = 2500 + , maxSize = 300 + , chatty = True +-- noShrinking flag? + } + +patientArgs = Args + { replay = Nothing + , maxSuccess = 300 + , maxDiscard = 10000 + , maxSize = 300 + , chatty = True +}
+ Sample.hs view
@@ -0,0 +1,32 @@+--------------------------------------------------------- +-- +-- Module : Sample +-- Copyright : Bartosz Wójcik (2011) +-- License : BSD3 +-- +-- Maintainer : bartek@sudety.it +-- Stability : Unstable +-- Portability : portable +-- +-- Part of haslo. Quick Check sample test cases. +--------------------------------------------------------- + +module Main +where + +import Haslo +import Haslo.HasloQCTestL +import Data.IORef +import Data.List +import Data.Time +import Control.Monad.Reader +import Test.QuickCheck + +main = putStrLn "Product \ + \Princip. \ + \Dur \ + \De \ + \Rate \ + \(Balloon) \ + \Parameters" >> + sample (arbitrary :: Gen TestL)
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple +main = defaultMain
+ haslo.cabal view
@@ -0,0 +1,38 @@+Name: haslo +Version: 0.1 +License: BSD3+License-file: license +Author: Bartosz Wojcik +Maintainer: Bartosz Wojcik <bartoszmwojcik@gmail.com> +Copyright: Copyright (c) 2011 Bartosz Wojcik +Category: Financial +Synopsis: Loan calculator engine. +Stability: experimental +Build-type: Simple +Description: Loan calculator engine implementing rules mentioned in paper "Haskell Loan Library".+ See more here: https://github.com/bartoszw/haslo/blob/master/haslo.pdf.+ Acronym from Haskell Loan. + +Cabal-Version: >=1.2.3 + +library + Build-Depends: base >= 3 && < 5, old-time, wtk, mtl + Exposed-Modules: Haslo + Other-Modules: Haslo.BasicType, + Haslo.CalcCalendar, + Haslo.Parameters, + Haslo.ErrorHandling, + Haslo.InstalmentPlan, + Haslo.InstalmentPlanProps, + Haslo.CalcConfigurationType, + Haslo.CalcConstructors, + Haslo.Calculator + +Executable hasloQC + Build-Depends: old-time, QuickCheck, mtl, time, wtk + Main-Is: HasloQC.hs + +Executable hasloSample + Build-Depends: old-time, QuickCheck, mtl, time, wtk + Main-Is: Sample.hs +
+ license view
@@ -0,0 +1,31 @@+Copyright (c) 2012, Bartosz Wójcik++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 Isaac Jones 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.