#!/usr/bin/env python # encoding: utf-8 """ demonstration.py Created on 2010-07-07 Copyright (c) 2010 __Richard Careaga__ 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 the Richard Careaga nor the names of 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 HOLDER 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 RICHARD CAREAGA 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. """ # Obtain various standard helper functions and classes from __future__ import division # needs to be first line import sys import os import urllib2 from collections import defaultdict from datetime import date from datetime import datetime from dateutil.relativedelta import * try: from lxml import etree except: from xml.etree import ElementTree as etree from StringIO import StringIO help_message = ''' demonstration: calculate a decrement table for Sequoia 2010-H1 at a constant prepayment rate assumption modified so that each loan that prepays does so in full, rather than a curtailment. Usage: python ./demonstration.py cpr where cpr is a decimal fraction between 0.01 and 1.00, inclusive For discussion of the code visit http://www.pylaw.org/demonstration.html ''' '''Constants, from Sequoia Mortgage Trust 2010-H1 (http://goo.gl/I9Wi)''' dealname = 'Sequoia 2010-H1' bond = 'Class A-1' replinefile = 'http://www.pylaw.org/dectable.csv' margin = 2.25 # identical for each loan index = 0.9410 # assumed constant per 'modelling assumptions' expfee = 0.2585 # servicing and trustee fees reset = margin + index - expfee # interest rate calcuation on adjustment # dates pbal = 237838333.0 # initial aggregate principal balance of the loans obal = 222378000.0 # initial aggregate principal balance of the Class A-1 srpct = obal/pbal # initial Senior Principal Percentage cod = date(2010,5,1)# cut-off date close_month = cod - relativedelta(months=1) anniversary_month = (cod - relativedelta(months=1)).strftime('%B') '''stepdown dates''' stepdown = dict( stepone = [date(2017,5,1), 1.0], steptwo = [date(2018,5,1), 0.7], stepthree = [date(2019,5,1), 0.6], stepfour = [date(2020,5,1), 0.4], stepfive = [date(2021,5,1), 0.2] ) tttdate = date(2013,5,1) # two times test date num_replines = 16 num_loans = 255 speeds = [0, 0.1, 0.2, 0.3, 0.4, 0.5] url='http://www.revisedregab.com/xmlsample.xhtml' #XML file of loans # def generateItems(seq): for item in seq: yield item def md(lexicon,key, contents): """Generic append key, contents to lexicon""" lexicon.setdefault(key,[]).append(contents) class Solver(object): '''takes a function, named arg value (opt.) and returns a Solver object http://code.activestate.com/recipes/303396/''' def __init__(self,f,**args): self._f=f self._args={} # see important note on order of operations in __setattr__ below. for arg in f.func_code.co_varnames[0:f.func_code.co_argcount]: self._args[arg]=None self._setargs(**args) def __repr__(self): argstring=','.join(['%s=%s' % (arg,str(value)) for (arg,value) in self._args.items()]) if argstring: return 'Solver(%s,%s)' % (self._f.func_code.co_name, argstring) else: return 'Solver(%s)' % self._f.func_code.co_name def __getattr__(self,name): '''used to extract function argument values''' self._args[name] return self._solve_for(name) def __setattr__(self,name,value): '''sets function argument values''' # Note - once self._args is created, no new attributes can # be added to self.__dict__. This is a good thing as it throws # an exception if you try to assign to an arg which is inappropriate # for the function in the solver. if self.__dict__.has_key('_args'): if name in self._args: self._args[name]=value else: raise KeyError, name else: object.__setattr__(self,name,value) def _setargs(self,**args): '''sets values of function arguments''' for arg in args: self._args[arg] # raise exception if arg not in _args setattr(self,arg,args[arg]) def _solve_for(self,arg): '''Newton's method solver''' TOL=0.0000001 # tolerance ITERLIMIT=1000 # iteration limit CLOSE_RUNS=10 # after getting close, do more passes args=self._args if self._args[arg]: x0=self._args[arg] else: x0=1 if x0==0: x1=1 else: x1=x0*1.1 def f(x): '''function to solve''' args[arg]=x return self._f(**args) fx0=f(x0) n=0 while 1: # Newton's method loop here fx1 = f(x1) if fx1==0 or x1==x0: # managed to nail it exactly break if abs(fx1-fx0)ITERLIMIT: print "Failed to converge; exceeded iteration limit" break slope=(fx1-fx0)/(x1-x0) if slope==0: if close_flag: # we're close but have zero slope, finish break else: print 'Zero slope and not close enough to solution' break x2=x0-fx0/slope # New 'x1' fx0 = fx1 x0=x1 x1=x2 n+=1 self._args[arg]=x1 return x1 def tvm(pv,fv,pmt,n,i): '''equation for time value of money''' i=i/100 tmp=(1+i)**n return pv*tmp+pmt/i*(tmp-1)-fv ## end of http://code.activestate.com/recipes/303396/ }}} class Payoff(): '''prepares a decrement table given constant prepayment speed''' def __init__(self, L, C): self.L = L self.C = C self.bbal = float(L[0]) #beginning balance self.rbal = self.bbal #remaining balance self.i = float(L[1]) #interest rate in form 4.5 self.rtm = int(L[2]) #remaining months to maturity self.mtr = int(L[3])+1 #months to roll date new i in effect self.mta = int(L[4]) #months remaining of interest only self.cod = C[0] #cut-off date self.tttdate = C[1] #twotimes test date self.srpct = C[2] #initial senior percentage self.osrpct = C[2] #original senior percentage self.reset = C[3] #interest rate at reset self.stepdown = C[4] #stepdown dates self.pbal = C[5] #original aggregate principal balance self.obal = C[6] #original aggregate class balance self.obsupct = 1 - C[2] #original subordinate percentage s = Solver(tvm,pv=self.bbal, fv=0, i = self.i/12, n = self.rtm) self.pmt = s.pmt #monthly payment self.teaser = self.mtr #counter for initial fixed rate period self.io = self.mta #counter for remaining interest only self.n = self.rtm+1 #to take into account range() self.current = self.cod + relativedelta(months=+1) self.smm = 0.0 #single monthly mortality def __nonzero__(self): return True def __bool__(self): return False def payone(self): def is_twice(): #Twotimes test if self.subprct >= 2*self.osubpct: return 1 else: return 0 def is_shrinking(): if self.srpct > self.osrpct: return 1 else: return 0 def payoff(): import random #import standard randomization module space = int(1//self.smm) #calculate sample space outcomes = [1] #create list with one positive outcome for n in range(space-1): #for the remainder of the sample space outcomes.append(0) #populate with negative outcome payoff = random.choice(outcomes)#randomly choose an outcome return payoff #report result to calling function def senior_prepay_percentage(): if self.current < self.tttdate and is_twice: self.srpppct = self.srpct + 0.5*(1-self.srpct) elif self.current >= self.tttdate and is_twice: self.srpppct = self.srpct elif self.current < self.stepdown['stepone'][0]: if is_shrinking(): self.srpppct = 1.0 elif is_twice(): self.srpppct = self.stepdown['stepone'][1] else: self.srpppct = self.srpppct elif self.current < self.stepdown['steptwo'][0]: if is_shrinking(): self.srpppct = 1.0 elif is_twice(): self.srpppct = self.stepdown['steptwo'][1] else: self.srpppct = self.srpppct elif self.current < self.stepdown['stepthree'][0]: if is_shrinking(): self.srpppct = 1.0 elif is_twice(): self.srpppct = self.stepdown['stepthree'][1] else: self.srpppct = self.srpppct elif self.current < self.stepdown['stepfour'][0]: if is_shrinking(): self.srpppct = 1.0 elif is_twice(): self.srpppct = self.stepdown['stepfour'][1] else: self.srpppct = self.srpppct elif self.current < self.stepdown['stepfive'][0]: if is_shrinking(): self.srpppct = 1.0 elif is_twice(): self.srpppct = self.stepdown['stepfive'][1] else: self.srpppct = self.srpppct elif self.current >= self.stepdown['stepfive'][0]: self.srpppct = self.srpct else: self.srpppct = self.srpct next_month = self.current + relativedelta(months=+1) self.current = next_month senior_prepay_percentage() #calculate senior prepayment #percentage self.teaser -= 1 #reduce remaining teaser period self.io -= 1 #reduce remaining interest only period self.bbal = self.rbal #beginning balance to last period's ending ipay = self.rbal*self.i/1200 #interest payment portion if payoff(): self.smm = 1.0 if self.mta > 0: #if during interest only period self.paydown = 0 #no scheduled principal self.prepay = self.smm*(self.bbal-self.paydown) else: self.paydown = -self.pmt-ipay # reverse negative paid out conv self.prepay = self.smm*(self.bbal-self.paydown) if self.rtm > 0: #decrement remaining term to maturity self.rtm -= 1 if self.mtr == 0: #begin 12-month reset period 11 .. 0 self.mtr = 11 elif self.mtr > 0: #decrement months to reset self.mtr -= 1 if self.mta > 0: #decrement months to end of i/o period self.mta -= 1 if self.bbal == 0: #see if final payment has been made self.paydown = 0 self.prepay = 0 elif self.bbal >= self.paydown + self.prepay: #not last payment? self.rbal -= self.paydown + self.prepay elif self.bbal < self.paydown: # scheduled payment enough to final out self.paydown = self.bbal self.prepay = 0 self.rbal = 0 elif self.bbal < self.prepay: # prepayment enough to final out self.paydown = self.bbal if self.bbal > 0: # if any still left, allocate to prepay self.prepay = self.bbal self.rbal = 0 else: self.rbal = 0 if self.teaser == 1: #last month of fixed rate period self.i = self.reset #change interest rate for following month s = Solver(tvm,pv=self.rbal, fv=0, i = self.i/12, \ n = self.rtm+1) #calculate new amortizing payment self.pmt = s.pmt #set new payment if self.io == 1: #last month of i/o period s = Solver(tvm,pv=self.rbal, fv=0, i = self.i/12, \ n = self.rtm) #calculate amortizing payment self.pmt = s.pmt #set new payment yield self.srpct*self.paydown + self.srpppct*self.prepay #create an empty dictionary for each loan record websters = [] for i in range(num_loans): websters.append(defaultdict(list)) content = urllib2.urlopen(url).read() root = etree.fromstring(content) records = list(root) lexicon = generateItems(websters) for record in records: lex = lexicon.next() for field in record: md(lex, field.attrib['name'], field.text) tape = [] for loan in websters: record = [] record.append(float(loan['obal'][0])) record.append(float(loan['cintpct'][0])) tmat = loan['maturity'][0] mat = datetime.strptime(tmat, '%Y-%m-%d').date() to_mat = relativedelta(mat,cod) mtm = to_mat.months + to_mat.years*12 record.append(mtm) fpd = datetime.strptime(loan['fpd'][0], '%Y-%m-%d').date() to_roll = relativedelta(fpd + relativedelta(months=60), cod) mtr = to_roll.months + to_roll.years*12 record.append(mtr) intonlyterm = int(loan['intonlyterm'][0]) to_amort = relativedelta(fpd + relativedelta(months=intonlyterm), cod) mta = to_amort.months + to_amort.years*12 record.append(mta) tape.append(record) def run_loan_payoff(cpr): '''cpr = 0.1 Constant Prepayment Rate in decimal fraction''' C = [cod, tttdate, srpct, reset, stepdown, pbal, obal] cbal = obal anniversary = cod.year+1 E = {} for record in tape: md(E,'tape', Payoff(record,C)) twelfth = 1.0/12.0 smm = 1.0 - (1.0-cpr)**twelfth # single monthly mortality column = [] # empty list to collect principal payments for year in range(2011,2041): annual = [] # temporary list for month in range(12): for entry in E['tape']: payment = [] # temporary list entry.srpct = srpct # set object senior percentage entry.subpct = 1 - srpct entry.smm = smm # set smm for object try: # while still data payment.append(entry.payone().next()) except StopIteration: pass annual.append(sum(payment)) # aggregate for month cbal -= sum(payment) # knock down senior sprct = cbal/obal # recalculate senior percentage column.append(annual) # collect months column[:] = [sum(item) for item in column] # aggregate for year cbal=obal ''' output decrement table for given CPR speed ''' print "%s %s at CPR of %d%%" % (dealname, bond, cpr*100) for year in column: cbal -= year percentout = round(cbal/obal*100,2) if percentout >= 1: print("%s %d:\t\t%0.0f") % (anniversary_month, anniversary,\ percentout) elif percentout <= 0: print("%s %d:\t\t0") % (anniversary_month, anniversary) else: percentout < 1 print("%s %d:\t\t*") % (anniversary_month, anniversary) anniversary += 1 def main(cpr_arg): print help_message cpr = float(cpr_arg) # command line argument is a string run_loan_payoff(cpr) # call the function to produce the table if __name__ == "__main__": sys.exit(main(sys.argv[1]))