{-# LANGUAGE FlexibleInstances #-} {-# OPTIONS_GHC -fno-warn-missing-methods -fno-warn-orphans #-} import Prelude import Data.List import Text.Printf import Control.Monad -- Main data Verbosity = Verbose | Quiet deriving Eq main = runScenarios sampleMortgage overpaySchemes Verbose where sampleMortgage = Mortgage (1000 Pounds) (annualRate 5) (10 Years) overpaySchemes = [ ("2 pm, 200 initial", lumpSumOf (200 Pounds) .+ monthlyPaymentsOf (2 Pounds)) , ("2 pm only", monthlyPaymentsOf (2 Pounds)) , ("200 initial", lumpSumOf (200 Pounds)) , ("400 initial", lumpSumOf (400 Pounds)) , ("200 after 2y", lumpSumOf (200 Pounds) `after` 2 Years) , ("400 after 2y", lumpSumOf (400 Pounds) `after` 2 Years) ] runScenarios myMortgage overpayScenarios verbosity = do let baseline = analyze $ expandOverpayment myMortgage noPayments printf "We are looking at %s, which has required monthly payment of %s\n" (show myMortgage) (show $ requiredRepayment myMortgage) putStrLn $ "Baseline:\n\t" ++ show baseline forM_ overpayScenarios $ \(name, overpayScenario) -> do let expansion = expandOverpayment myMortgage overpayScenario analysis = analyze expansion putStrLn $ "Overpayment scenario \"" ++ name ++ "\":\n\t" ++ show analysis ++ "\n\tCompared to baseline: " ++ compareAnalysis baseline analysis when (verbosity == Verbose) $ mapM_ print expansion -- Mortgage data Mortgage = Mortgage { loan :: Currency, loanRate :: Interest, duration :: Period } instance Show Mortgage where show m = printf "loan of %s at %s over %s" (show $ loan m) (show $ loanRate m) (show $ duration m) -- Monthly expansion details data Cycle = Cycle { month :: Integer, balanceAtStart :: Currency, interestAccrued :: Currency, paymentMade :: Currency, balanceEnd :: Currency } instance Show Cycle where show c = printf "For month %d, balance: %s -> %s\t(interest: %s, payment: %s)" (month c) (show $balanceAtStart c) (show $ balanceEnd c) (show $ interestAccrued c) (show $ paymentMade c) expand :: Mortgage -> PaymentSchedule -> [Cycle] expand mortgage (PS payments) = iterate step initial where initial = Cycle 0 (loan mortgage) zero zero (loan mortgage) step before = let thisMonth = month before + 1 balStart = balanceEnd before interest = balStart .* (toMonthly . loanRate $ mortgage) thisPayment = payments thisMonth `min` (balStart .+ interest) bChange = interest .- thisPayment balEnd = balStart .+ bChange in Cycle thisMonth balStart interest thisPayment balEnd -- The finance stuff compound :: Currency -> Interest -> Integer -> Currency compound principal rate months = principal .* ((1+toMonthly rate) ^ months) requiredRepayment :: Mortgage -> Currency requiredRepayment (Mortgage initial rate duration) = let mrate = toMonthly rate in (initial .* mrate) ./ (1 - exp (-1 * fromIntegral (toMonths duration) * log (1.0 + mrate))) expandOverpayment mortgage overpayments = takeWhile debtRemaining $ drop 1 $ expand mortgage payments where payments = overpayments .+ monthlyPaymentsOf (requiredRepayment mortgage) debtRemaining c = toPence (balanceAtStart c) > 0 -- Analysis of total interest and payments data Analysis = Analysis { totalInterest :: Currency, totalPayments :: Currency, totalDuration :: Period } instance Show Analysis where show a = "Total interest: " ++ show (totalInterest a) ++ " Total payments: " ++ show (totalPayments a) ++ " Duration=" ++ show (totalDuration a) analyze cycles = Analysis (sumOf interestAccrued) (sumOf paymentMade) (PMonths $ fromIntegral $ length cycles) where sumOf proj = foldl' (.+) zero $ map proj cycles compareAnalysis (Analysis ti tp td) (Analysis ti' tp' td') = printf "interest=%s, payments=%s, duration=%s" (show $ ti' .- ti) (show $ tp' .- tp) (show (PMonths ((toMonths td') - (toMonths td)))) -- Common operations: addition and scaling class Quantity q where zero :: q (.+) :: q -> q -> q (.-) :: q -> q -> q (.*) :: RealFrac k => q -> k -> q (./) :: RealFrac k => q -> k -> q -- Interest rates data Interest = I Double instance Show Interest where show (I i) = show i ++ "%" annualRate = I toMonthly (I i) = i/1200 -- Currency; stored as integer number of pence -- Multiplication/division round result down to nearest whole number of pence data Currency = C Integer deriving (Ord,Eq) instance Show Currency where show (C c) = printf "£%d.%02d" pounds (abs pence) where (pounds,pence) = quotRem c 100 pounds = C . (* 100) toPence (C p) = p instance Quantity Currency where zero = C 0 (C a) .+ (C b) = C $ a + b (C a) .* k = C $ floor $ k * fromIntegral a c ./ k = c .* (1/k) (C a) .- (C b) = C $ a - b -- Durations data Period = PMonths Integer toMonths (PMonths n) = n instance Show Period where show (PMonths n) = show years ++ "y" ++ (if months == 0 then "" else " " ++ show (abs months) ++ "m") where (years,months) = quotRem n 12 -- Payment schedules - function from month number to amount data PaymentSchedule = PS (Integer -> Currency) amount `every` (PMonths period) = PS $ \n -> if n `rem` period == 0 then amount else zero instance Quantity PaymentSchedule where zero = PS $ \n -> C 0 (PS p) .+ (PS p') = PS $ \n -> p n .+ p' n (PS p) .- (PS p') = PS $ \n -> p n .- p' n (PS p) .* k = PS $ \n -> p n .* k (PS p) ./ k = PS $ \n -> (p n) .* (1/k) noPayments = zero monthlyPaymentsOf amount = amount `every` (1 Months) lumpSumOf p = PS $ \n -> if n == 1 then p else zero (PS ps) `after` (PMonths delay) = PS $ \n -> ps (n-delay) -- Hacks data MONEY = Pounds | Pence instance Eq (MONEY -> Currency) instance Show (MONEY -> Currency) instance Num (MONEY -> Currency) where fromInteger i Pounds = C (i * 100) fromInteger i Pence = C i data DURATION = Months | Month | Years | Year instance Eq (DURATION -> Period) instance Show (DURATION -> Period) instance Num (DURATION -> Period) where fromInteger i Months = PMonths i fromInteger i Month = PMonths i fromInteger i Years = PMonths $ i * 12 fromInteger i Year = PMonths $ i * 12