Building a Rate of Return System in Haskell – Part 3, Annualized Returns

Posted on July 28, 2011

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.

Updating CUVs

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, getMonthCUV and 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

The 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 ()

The 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.

Summary Data

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.

The 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

The 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 ()

Conclusion

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.