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.