This is a follow up to part 1 and part 2 of my overview of using Haskell to build a rate of return calculation system at Steadyhand. As before, please note that I'm a beginner Haskell programmer, so don't look to these articles as inspiration for the 'right way to do things'.
The last two articles explained how we can get the monthly RoR for an account based on the transaction history of the account. Today I'll discuss how the information is stored in the system for both monthly and annualized perspectives.
The 2nd article describes how instead of directly storing monthly rate of returns, instead we store cumulative unit values for each performance object:
ACCOUNT DATE CUV ---------- ---------- ---------- 1 20070122 100 1 20070131 99.9748 1 20070228 100.123174 1 20070331 102.868235
I have two functions using the Takusen database library to retrieve either a single month's CUV or all CUVs for a performance object,
getCUVs, respectively. Both functions return
[(ISODateInt, CUV)] where CUV is type Double.
If we know the CUV last month and the RoR for this month, the ending CUV for this month is:
-- |Calculate CUV given beginning CUV and RoR calcCUV :: CUV -> RoR -> CUV calcCUV beginCUV ror = beginCUV * (1 + ror)
To calculate the CUV for a month, using our
totalReturn function from last time, we do:
-- |Primary function to calculate CUV for account/portfolio for single month calcMonthCUV :: PerfObject -> ISODateInt -> ISODateInt -> IO [((Int, Int), CUV)] calcMonthCUV perfObj prevMonthEnd monthEnd = do startCUV <- getMonthCUV prevMonthEnd perfObj if startCUV ==  then calcCUVs (accounts perfObj) else do ror <- totalReturn (accounts perfObj) prevMonthEnd monthEnd let cuv = calcCUV (snd $ head startCUV) ror let cuvs = [((prevMonthEnd,monthEnd), cuv)] return cuvs
calcCUVs function is used if we don't have any existing CUV data stored in the database. I won't go into it here, but it calculates the CUVs for each month up until the monthEnd.
Once we have our list of CUVs, we use a storeCUV function to update the database:
-- |Store CUVs for account in database saveCUVs :: PerfObject -> [((ISODateInt, ISODateInt), CUV)] -> IO () saveCUVs perfObject cuvs = do r' <- mapM (\((_,date),cuv) -> storeCUV date perfObject cuv) cuvs return ()
processObjCUVs function brings the retrieval/calculation together with storing the new values:
-- |Generate and store CUVs for an account/portfolio for a month or from inception when call with dates=0 processObjCUVs :: PerfObject -> ISODateInt -> ISODateInt -> IO () processObjCUVs perfObject prevMonthEnd monthEnd = if prevMonthEnd == 0 && monthEnd == 0 then calcCUVs (accounts perfObject) >>= saveCUVs perfObject else calcMonthCUV perfObject prevMonthEnd monthEnd >>= saveCUVs perfObject
Finally, a separate function is used in conjunction with the command line to retrieve all performance objects and
processObjCUVs for each performance object for a given month. This is used once a month to update the CUV data in the system.
Along with CUV information, the system stores summary information for reporting. At the moment, the approach is very crude, we have a SUMMARY table with a row for each performance object/month with fields for 3mo, 1yr, 2yr, 3yr, 4yr, 5yr, and since inception.
calcRoRSummary function takes a list of CUVS for an account, and based on the size of the account calculates the annualized rates of return for the account. At the moment it also relies far too much on hardcoding, but the current approach at least makes the concepts clear:
-- |Calculate performance summary values for the periods: 3mo, 1yr, 2yr, 3yr, 4yr, 5yr, inception -- ("DATE","OBJECT_ID","OBJECT_TYPE","1_MO","3_MO","1_YR","2_YR","3_YR","4_YR","5_YR","INCEPT") calcRoRSummary :: [(ISODateInt,RoR)] -> [Maybe RoR] calcRoRSummary  = [Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing] calcRoRSummary cuvs | months > 60 = [one_mo,three_mo,one_yr,two_yr,three_yr,four_yr,five_yr,incept] | months > 48 = [one_mo,three_mo,one_yr,two_yr,three_yr,four_yr,Nothing,incept] | months > 36 = [one_mo,three_mo,one_yr,two_yr,three_yr,Nothing,Nothing,incept] | months > 24 = [one_mo,three_mo,one_yr,two_yr,Nothing,Nothing,Nothing,incept] | months > 12 = [one_mo,three_mo,one_yr,Nothing,Nothing,Nothing,Nothing,incept] | months > 3 = [one_mo,three_mo,Nothing,Nothing,Nothing,Nothing,Nothing,incept] | months > 1 = [one_mo,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,incept] | otherwise = [Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing,Nothing] where months = if (isMonthEnd $ fst (last cuvs)) then length cuvs else length cuvs - 1 one_mo = simple_rate (cuvs !! 0) (cuvs !! 1) three_mo = simple_rate (cuvs !! 0) (cuvs !! 3) one_yr = simple_rate (cuvs !! 0) (cuvs !! 12) two_yr = annual_rate (1/2) (cuvs !! 0) (cuvs !! 24) three_yr = annual_rate (1.0/3.0) (cuvs !! 0) (cuvs !! 36) four_yr = annual_rate (1.0/4.0) (cuvs !! 0) (cuvs !! 48) five_yr = annual_rate (1.0/5.0) (cuvs !! 0) (cuvs !! 60) incept = if months > 12 then annual_rate (365/fromIntegral (dayCount (fst $ cuvs !! 0) (fst $ last cuvs))) (cuvs !! 0) (last cuvs) else simple_rate (cuvs !! 0) (last cuvs) simple_rate x y = Just (rate x y * 100.00) annual_rate n x y = Just ((((rate x y + 1) ** n) - 1.0) * 100.00) rate x y = snd x/snd y - 1.0
calcRoRSummary function is called by
processObjSummary which retrieves the required CUVs, and then stores the new results back in the database. As above,
calcRoRSummary is called via command line function which runs the function over each performance object:
-- |Calculate and store performance summaries for an account up to a specific month-end date processObjSummary :: ISODateInt -> PerfObject -> IO () processObjSummary date perfObject = do cuvs <- getCUVs perfObject date let returns = calcRoRSummary cuvs results <- storeSummary date perfObject returns if results > 0 then putStrLn (show perfObject ++ " - OK") else putStrLn (show perfObject ++ " - Error") return ()
That's it for this series of postings. There is plenty of future work around this project, but hopefully I've shown that it's possible to build useful systems in a commercial environment using Haskell.