2 The Financial Cash Flow Model: Python on Wall Street

Keywords: fintec, data wrangling, Python

2.1 Case Description

After the financial crisis beginning in 2008, the Securities and Exchange Commission issued a proposed rulemaking in 2010 that asked whether it should require

The asset-level information … according to proposed standards and in a tagged data format using eXtensible Markup Language (XML)… [and the] filing of a computer program of the contractual cash flow provisions expressed as downloadable source code in Python

in offerings of residential mortgage backed securities and other asset types. In the trade, the asset-level information is called the tape.

In my comment letter, I supported both requirements and provided a demonstration of how they would work based on an actual transaction.

2.2 XML Conversion

The asset data for this transaction was filed in HTML format in particularly ugly form in multiple tables.

    </font></td>
              </tr>
              <tr bgcolor="white">
                <td align="right" valign="top" width="3%">
                  <div style="DISPLAY: block; MARGIN-LEFT: 0pt; TEXT-INDENT: 0pt; MARGIN-RIGHT: 0pt" align="right"><font style="DISPLAY: inline; FONT-SIZE: 8pt; COLOR: #000000; FONT-FAMILY: times new roman"><font style="DISPLAY: inline; COLOR: #000000">6</font></font></div>
                </td>
                <td align="right" valign="top" width="7%">
                  <div style="DISPLAY: block; MARGIN-LEFT: 0pt; TEXT-INDENT: 0pt; MARGIN-RIGHT: 0pt" align="right"><font style="DISPLAY: inline; FONT-SIZE: 8pt; COLOR: #000000; FONT-FAMILY: times new roman"><font style="DISPLAY: inline; COLOR: #000000">1000115</font></font></div>
                </td>
                <td align="right" valign="top" width="7%">
                  <div style="DISPLAY: block; MARGIN-LEFT: 0pt; TEXT-INDENT: 0pt; MARGIN-RIGHT: 0pt" align="right"><font style="DISPLAY: inline; FONT-SIZE: 8pt; COLOR: #000000; FONT-FAMILY: times new roman"><font style="DISPLAY: inline; COLOR: #000000">0.0025</font></font></div>
                </td>
                <td valign="top" width="5%"><font style="DISPLAY: inline; FONT-SIZE: 8pt; FONT-FAMILY: times new roman">&#160;
    </font></td>
                <td valign="top" width="5%"><font style="DISPLAY: inline; FONT-SIZE: 8pt; FONT-FAMILY: times new roman">&#160;
    </font></td>
                <td align="right" valign="top" width="5%">
                  <div style="DISPLAY: block; MARGIN-LEFT: 0pt; TEXT-INDENT: 0pt; MARGIN-RIGHT: 0pt" align="right"><font style="DISPLAY: inline; FONT-SIZE: 8pt; COLOR: #000000; FONT-FAMILY: times new roman"><font style="DISPLAY: inline; COLOR: #000000">1000115</font></font></div>
                </td>
                <td valign="top" width="5%"><font style="DISPLAY: inline; FONT-SIZE: 8pt; FONT-FAMILY: times new roman">&#160;
    </font></td>
                <td align="right" valign="top" width="5%">
                  <div style="DISPLAY: block; MARGIN-LEFT: 0pt; TEXT-INDENT: 0pt; MARGIN-RIGHT: 0pt" align="right"><font style="DISPLAY: inline; FONT-SIZE: 8pt; COLOR: #000000; FONT-FAMILY: times new roman"><font style="DISPLAY: inline; COLOR: #000000">5264358737</font></font></div>
                </td>
                <td align="right" valign="top" width="5%">
                  <div style="DISPLAY: block; MARGIN-LEFT: 0pt; TEXT-INDENT: 0pt; MARGIN-RIGHT: 0pt" align="right"><font style="DISPLAY: inline; FONT-SIZE: 8pt; COLOR: #000000; FONT-FAMILY: times new roman"><font style="DISPLAY: inline; COLOR: #000000">2</font></font></div>
                </td>
                <td align="right" valign="top" width="5%">
                  <div style="DISPLAY: block; MARGIN-LEFT: 0pt; TEXT-INDENT: 0pt; MARGIN-RIGHT: 0pt" align="right"><font style="DISPLAY: inline; FONT-SIZE: 8pt; COLOR: #000000; FONT-FAMILY: times new roman"><font style="DISPLAY: inline; COLOR: #000000">1</font></font></div>

Printed out, this is about 12 pages, depending on your printer. If you assigned a conscientious junior lawyer to perform a count, he or she would report back that there are approximately 2,843 lines, 34,524 words and 195,652 characters visible. (Junior lawyers, by and large have a limited understanding of the word approximate.) If, however, you asked your IT person the same question, you would learn that there are exactly 137,811 lines, 56,881 words and 2,689,760 characters. Why did the lawyer only pick up a little more than 7% of the bytes?

They are both right from their perspectives of what the eye can see and what the computer has to process. The difference is that vast proportions of the file containing the data is devoted to making it appear as if it were printed. That’s fine if what you plan to do is read. If you want to perform data crunching, however, say to run your own model on the tape, you have to get rid of a lot of crud before you can proceed.

Here’s the typical payload of one of the HTML blocks:

9
1000115
0.0025
1000115
1332854261
2
1
0
9

We would prefer, of course, a comma delimited file

2,1000115,0.0025,NULL,1000115,,NULL,6875009669,2,1,0,9,,NULL,NULL,NULL,1,4,0.00,NULL,NULL,,242000,NULL,2009-05-26,623700,0.04500,240,,360,2009-07-01,NULL,120,NULL,,552900.71,552900.71,0.04500,2073.38,2010-04-01,,NULL,39,45,0.02250,NULL,0.00125,,60,0.05000,0.02250,12,0.02000,0.02000,0.09500,0.0225,0,NULL,,60,12,NULL,NULL,,0,NULL,NULL,0,NULL,NULL,0,,0,NULL,7.70,4.70,5.00,NULL,,NULL,NULL,NULL,722,778,NULL,,NULL,NULL,NULL,,NULL,770000000000,NULL,12500,0.00,9422.3,0.00,12500,21922.3,,1,5,NULL,3,NULL,4,,NULL,184669.53,NULL,0.23310,4,NULL,WILMETTE,IL,60091,1,1,,NULL,1500000,NULL,NULL,,NULL,NULL,NULL,0.57710,0.41580,0.00,0,0,NULL,,NULL,0.23310,NULL,NULL,,NULL,NULL,NULL,NULL,Full,Doc,less,than,12,months,,1,125.54,8.00,11.00,225939,2039-06-01, 3,1000115,0.0025,NULL,1000115

XML is potentially a large improvement over HTML. It does one thing very well, which is to separate content from decoration. All of the decisions about font, size, color,alignment, etc., can be isolated to a separate file, called a stylesheet. Here is an XML file of the tape with a minimalist style rendering.

There is, however, a rub. To allow the ability to decorate the content, the designers of XML require, in effect, a new header for every row of data. So, the first of the 255 rows in the XML version looks like:
<record>
    <field name="id">1</field>
    <field name="servicer">1000115</field>
    <field name="sfpct">0.002500</field>
    <field name="sfamt">0.00</field>
    <field name="adv">0</field>
    <field name="orig">1000115</field>
    <field name="lg">NULL</field>
    <field name="lnum">2147483647</field>
    <field name="amtype">2</field>
    <field name="lienpos">1</field>
    <field name="heloc">0</field>
    <field name="purpose">9</field>
    <field name="cashoutamt">0.00</field>
    <field name="points">0.000</field>
    <field name="chcl">0</field>
    <field name="relo">0</field>
    <field name="broker">0</field>
    <field name="channel">1</field>
    <field name="escrecord">0</field>
    <field name="balsenior">0.00</field>
    <field name="ltypesr">0</field>
    <field name="hybridper">0</field>
    <field name="negamlmtsr">0.0000</field>
    <field name="jrbal">0.00</field>
    <field name="odatesenior">0000-00-00</field>
    <field name="odate">2009-06-23</field>
    <field name="obal">446000.00</field>
    <field name="oint">0.0475</field>
    <field name="oterm">240</field>
    <field name="ottm">360</field>
    <field name="fpd">2009-08-01</field>
    <field name="inttype">0</field>
    <field name="intonlyterm">120</field>
    <field name="bdownper">0</field>
    <field name="helocper">0</field>
    <field name="cbal">446000.00</field>
    <field name="sbal">446000.00</field>
    <field name="cintpct">0.0475</field>
    <field name="cintamt">1765.42</field>
    <field name="ptd">2010-03-01</field>
    <field name="cstatus">0</field>
    <field name="indextype">39</field>
    <field name="lookdays">45</field>
    <field name="gmargin">0.0225</field>
    <field name="rounded">0</field>
    <field name="roundfac">0.0012</field>
    <field name="ofixper">60</field>
    <field name="ocapup">0.0500</field>
    <field name="ocapdn">0.0250</field>
    <field name="resetper">12</field>
    <field name="capup">0.0200</field>
    <field name="capdn">0.0200</field>
    <field name="ceiling">0.0975</field>
    <field name="floor">0.0225</field>
    <field name="negammax">0.0000</field>
    <field name="orecast">0</field>
    <field name="recast">0</field>
    <field name="ofixedpay">60</field>
    <field name="spayreset">12</field>
    <field name="opercap">0.0000</field>
    <field name="percap">0.0000</field>
    <field name="opayreset">0</field>
    <field name="payreset">0</field>
    <field name="optionarm">0</field>
    <field name="optionrecast">0</field>
    <field name="ominpay">0.00</field>
    <field name="minpay">0.00</field>
    <field name="prepaycalc">0</field>
    <field name="prepaytype">0</field>
    <field name="prepayterm">0</field>
    <field name="prepayhard">0</field>
    <field name="pid">0</field>
    <field name="propnum">0</field>
    <field name="borrecorders">0</field>
    <field name="selfemp">0</field>
    <field name="comonpay">0.00</field>
    <field name="pempl">36.00</field>
    <field name="sempl">0.00</field>
    <field name="yearshome">8.00</field>
    <field name="ficomodel">0</field>
    <field name="ficodate">0000-00-00</field>
    <field name="pequifax">0</field>
    <field name="pexperian">0</field>
    <field name="ptransu">0</field>
    <field name="sequifax">0</field>
    <field name="sexperian">0</field>
    <field name="stranstransu">0</field>
    <field name="pofico">802</field>
    <field name="prfico">806</field>
    <field name="srfico">0</field>
    <field name="cficometh">0</field>
    <field name="pvant">0</field>
    <field name="svant">0</field>
    <field name="cvantmeth">0</field>
    <field name="vantdate">0000-00-00</field>
    <field name="longtrade">NULL</field>
    <field name="maxtrade">0.00</field>
    <field name="numtrade">0</field>
    <field name="tradeuse">0.00</field>
    <field name="payhist">770000000000</field>
    <field name="monbk">0</field>
    <field name="monfc">0</field>
    <field name="pwage">8750.00</field>
    <field name="swage">0.00</field>
    <field name="pothinc">24883.57</field>
    <field name="sothinc">0.00</field>
    <field name="allwage">8750.00</field>
    <field name="alltot">33633.57</field>
    <field name="t_4506">1</field>
    <field name="pincver">5</field>
    <field name="sincver">0</field>
    <field name="pempver">3</field>
    <field name="sempver">0</field>
    <field name="pastver">4</field>
    <field name="sastver">0</field>
    <field name="liquid">250000.00</field>
    <field name="mondebt">0.00</field>
    <field name="odti">0.11</field>
    <field name="fullindex">4</field>
    <field name="ownfundsdown">0.00</field>
    <field name="city">CLARKSTON</field>
    <field name="state">MI</field>
    <field name="zip">48348</field>
    <field name="ptype">1</field>
    <field name="occ">1</field>
    <field name="price">0.00</field>
    <field name="oappr">575000.00</field>
    <field name="ovaltype">0</field>
    <field name="ovaldate">0000-00-00</field>
    <field name="oavm">0</field>
    <field name="oavmscore">0.0000</field>
    <field name="rpval">0.00</field>
    <field name="rpvaltype">0</field>
    <field name="rpvaldate">0000-00-00</field>
    <field name="ravm">0</field>
    <field name="ravmscore">0.0000</field>
    <field name="ocltv">0.78</field>
    <field name="oltv">0.78</field>
    <field name="opledge">0.00</field>
    <field name="micomp">0</field>
    <field name="mipct">0.00</field>
    <field name="poolcomp">0</field>
    <field name="stoploss">0.0000</field>
    <field name="micert">NULL</field>
    <field name="rdtifront">0.11</field>
    <field name="rdtibback">0.00</field>
    <field name="modpaydate">0000-00-00</field>
    <field name="totcap">0.00</field>
    <field name="totdef">0.00</field>
    <field name="premodint">0.00</field>
    <field name="premodpi">0.00</field>
    <field name="premodoicap">0.00</field>
    <field name="premodsubicap">0.00</field>
    <field name="premodnxtdate">0000-00-00</field>
    <field name="premodioterm">0</field>
    <field name="fbal">0.00</field>
    <field name="fint">0.00</field>
    <field name="doccode">Citiquik process</field>
    <field name="rwtinc">less than 12 months</field>
    <field name="rwtast">1</field>
    <field name="cashatclose">1048.73</field>
    <field name="pyrind">36.00</field>
    <field name="syrind">0.00</field>
    <field name="jrdrawn">0.00</field>
    <field name="maturity">2039-07-01</field>
  </record>

and every following row, except for the few bytes devoted to data, looks the same, bulking the tape up almost to the size of the HTML version.

2.3 From XML to Plain Text

The good news is that a few lines of Python is sufficient to make the HTML-XML conversion. The next step is to set up a template:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:template match="/">
<HTML>
<BODY>
    <xsl:apply-templates/>
</BODY>
</HTML>
</xsl:template>

<xsl:template match="/*">
<TABLE BORDER="0">
<TR>
        <xsl:for-each select="*[position() = 1]/*">
          <TD>
              <xsl:value-of select="local-name()"/>
          </TD>
        </xsl:for-each>
</TR>
      <xsl:apply-templates/>
</TABLE>
</xsl:template>

<xsl:template match="/*/*">
<TR>
    <xsl:apply-templates/>
</TR>
</xsl:template>

<xsl:template match="/*/*/*">
<TD>
    <xsl:value-of select="."/>
</TD>
</xsl:template>

</xsl:stylesheet>

to apply to the conversion, using

import amara # package for parsing xml
doc = amara.parse("xmlsample.xhtml") #URL
sequoia = doc.xml_children[1] # skip 0th item, just a header
records = sequoia.xml_children
exemplar = records[1] # skip newline
fields = exemplar.xml_children
elements = fields[1] #skip newline
rec_id = elements.xml_children
loan_id = int(rec_id[0].xml_value.encode('us-ascii'))
loan_id
1

To recap progress to date, we can pull XML data directly from a web page, parse it into a list of loan level records, and identify and exclude by copy the blank and constant fields. We ended up with a list that has 255 sublists, one for each loan. What can we do with the list?

Since we want to preserve loan identity (we may care which FICO goes with which zipcode), we can’t just use one dictionary to hold everything. Instead, we will give each record its own dictionary, d1, d2, … d255.

Next, we lazily generate the statements needed to create them by a little statement to print out the short commands, then cutting and pasting back to actually run them. If we had more than a couple of hundred records, we’d need to find a more elegant way of doing this, but this is a down-and-dirty way that’s easy to follow.

from collections import defaultdict

for record in range(256):
    print ("%s = defaultdict(list)") % ('d'+str(record))
d1 = defaultdict(list)
d2 = defaultdict(list)
#...
d255 = defaultdict(list)

This gives up 255 blank dictionary objects which we will assemble in a list:

websters = [d1, d2, ..., d255]

Then it is a simple matter to pair up empty dictionaries with the revised list of records:

z = zip(websters,LR)
for entries in z:
    for pairs in entries[1]:
        entries[0][pairs[0]].append(pairs[1])

and we now have a set of populated dictionaries with which we can do useful work.

2.4 Proof of Concept, Summary FICO Statistics

fico = flatten([entry['prfico'] for entry in websters])
mean(fico)
771.6313725490196
min(fico)
701
max(fico)
815
median(fico)
777.0
std(fico)
23.592234433539552
#Poor man's distribution graph of the unweighted scores
print stemplot(data0)
70 | 1 1 7 8 9 9
71 | 6 7 7 8 9
72 | 1 4 6 8
73 | 0 2 2 2 4 4 4 5 6 7 8 9 9
74 | 0 0 1 1 2 2 2 3 4 4 4 6 6 6 7 9
75 | 0 1 2 3 3 3 3 3 4 5 5 5 5 6 6 6 7 7 7 8 8 8 9
76 | 1 1 1 1 2 2 2 3 3 3 3 3 3 4 4 5 5 6 6 7 7 8 8 8 8 8 9 9 9
77 | 0 0 0 0 0 1 1 1 1 1 1 1 1 1 2 2 2 2 3 3 4 4 4 5 5 5 5 5 6 6 6 7 7 7 7 7 7 7 8 8 8 8 8 8 9 9 9 9
78 | 0 0 0 0 0 0 0 1 1 1 1 1 1 2 2 2 2 2 3 3 3 4 4 4 4 5 5 5 5 5 5 5 6 6 6 6 7 7 7 7 8 8 8 8 8 8 8 8 9 9 9 9 9
79 | 0 0 0 0 1 1 1 1 1 1 1 3 3 3 3 3 4 4 5 5 5 5 5 5 6 6 6 7 7 7 8 8 9 9 9
80 | 0 1 1 1 1 2 2 2 2 3 3 3 4 5 6 6 7 8 9 9
81 | 0 3 5

This is a stem and leaf plot, which is quite useful. For the top line, read 701, 701, 707, 708, 799, 799, for example. It’s useful as a quick check on barbell distributions of credit scores.

2.5 The Cash Flow Model

The purpose of the next program is to replicate the results of pages S-84 and S-85 in the actual transaction. The trade name for these tables is decrement table and they show for each class of security when it will be retired, given certain assumptions.

"""
demonstration.py
Created on 2010-07-07
Python 2.6
"""
# Obtain various standard helper functions and classes
from __future__ import division        # needs to be first line
import sys
import os
import plac
import urllib2
from collections import defaultdict
from datetime import date
from datetime import datetime
from dateutil.relativedelta import *
from lxml import 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
'''

'''Constants, from Sequoia Mortgage Trust 2010-H1 (http://goo.gl/I9Wi)'''
dealname = 'Sequoia 2010-H1'
bond = 'Class A-1'
replinefile = '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='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)<TOL:    # very close
                close_flag=True
                if CLOSE_RUNS==0:       # been close several times
                    break
                else:
                    CLOSE_RUNS-=1       # try some more
            else:
                close_flag=False
            if n>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
d1 = defaultdict(list)
d2 = defaultdict(list)
d3 = defaultdict(list)
d4 = defaultdict(list)
d5 = defaultdict(list)
d6 = defaultdict(list)
d7 = defaultdict(list)
d8 = defaultdict(list)
d9 = defaultdict(list)
d10 = defaultdict(list)
d11 = defaultdict(list)
d12 = defaultdict(list)
d13 = defaultdict(list)
d14 = defaultdict(list)
d15 = defaultdict(list)
d16 = defaultdict(list)
d17 = defaultdict(list)
d18 = defaultdict(list)
d19 = defaultdict(list)
d20 = defaultdict(list)
d21 = defaultdict(list)
d22 = defaultdict(list)
d23 = defaultdict(list)
d24 = defaultdict(list)
d25 = defaultdict(list)
d26 = defaultdict(list)
d27 = defaultdict(list)
d28 = defaultdict(list)
d29 = defaultdict(list)
d30 = defaultdict(list)
d31 = defaultdict(list)
d32 = defaultdict(list)
d33 = defaultdict(list)
d34 = defaultdict(list)
d35 = defaultdict(list)
d36 = defaultdict(list)
d37 = defaultdict(list)
d38 = defaultdict(list)
d39 = defaultdict(list)
d40 = defaultdict(list)
d41 = defaultdict(list)
d42 = defaultdict(list)
d43 = defaultdict(list)
d44 = defaultdict(list)
d45 = defaultdict(list)
d46 = defaultdict(list)
d47 = defaultdict(list)
d48 = defaultdict(list)
d49 = defaultdict(list)
d50 = defaultdict(list)
d51 = defaultdict(list)
d52 = defaultdict(list)
d53 = defaultdict(list)
d54 = defaultdict(list)
d55 = defaultdict(list)
d56 = defaultdict(list)
d57 = defaultdict(list)
d58 = defaultdict(list)
d59 = defaultdict(list)
d60 = defaultdict(list)
d61 = defaultdict(list)
d62 = defaultdict(list)
d63 = defaultdict(list)
d64 = defaultdict(list)
d65 = defaultdict(list)
d66 = defaultdict(list)
d67 = defaultdict(list)
d68 = defaultdict(list)
d69 = defaultdict(list)
d70 = defaultdict(list)
d71 = defaultdict(list)
d72 = defaultdict(list)
d73 = defaultdict(list)
d74 = defaultdict(list)
d75 = defaultdict(list)
d76 = defaultdict(list)
d77 = defaultdict(list)
d78 = defaultdict(list)
d79 = defaultdict(list)
d80 = defaultdict(list)
d81 = defaultdict(list)
d82 = defaultdict(list)
d83 = defaultdict(list)
d84 = defaultdict(list)
d85 = defaultdict(list)
d86 = defaultdict(list)
d87 = defaultdict(list)
d88 = defaultdict(list)
d89 = defaultdict(list)
d90 = defaultdict(list)
d91 = defaultdict(list)
d92 = defaultdict(list)
d93 = defaultdict(list)
d94 = defaultdict(list)
d95 = defaultdict(list)
d96 = defaultdict(list)
d97 = defaultdict(list)
d98 = defaultdict(list)
d99 = defaultdict(list)
d100 = defaultdict(list)
d101 = defaultdict(list)
d102 = defaultdict(list)
d103 = defaultdict(list)
d104 = defaultdict(list)
d105 = defaultdict(list)
d106 = defaultdict(list)
d107 = defaultdict(list)
d108 = defaultdict(list)
d109 = defaultdict(list)
d110 = defaultdict(list)
d111 = defaultdict(list)
d112 = defaultdict(list)
d113 = defaultdict(list)
d114 = defaultdict(list)
d115 = defaultdict(list)
d116 = defaultdict(list)
d117 = defaultdict(list)
d118 = defaultdict(list)
d119 = defaultdict(list)
d120 = defaultdict(list)
d121 = defaultdict(list)
d122 = defaultdict(list)
d123 = defaultdict(list)
d124 = defaultdict(list)
d125 = defaultdict(list)
d126 = defaultdict(list)
d127 = defaultdict(list)
d128 = defaultdict(list)
d129 = defaultdict(list)
d130 = defaultdict(list)
d131 = defaultdict(list)
d132 = defaultdict(list)
d133 = defaultdict(list)
d134 = defaultdict(list)
d135 = defaultdict(list)
d136 = defaultdict(list)
d137 = defaultdict(list)
d138 = defaultdict(list)
d139 = defaultdict(list)
d140 = defaultdict(list)
d141 = defaultdict(list)
d142 = defaultdict(list)
d143 = defaultdict(list)
d144 = defaultdict(list)
d145 = defaultdict(list)
d146 = defaultdict(list)
d147 = defaultdict(list)
d148 = defaultdict(list)
d149 = defaultdict(list)
d150 = defaultdict(list)
d151 = defaultdict(list)
d152 = defaultdict(list)
d153 = defaultdict(list)
d154 = defaultdict(list)
d155 = defaultdict(list)
d156 = defaultdict(list)
d157 = defaultdict(list)
d158 = defaultdict(list)
d159 = defaultdict(list)
d160 = defaultdict(list)
d161 = defaultdict(list)
d162 = defaultdict(list)
d163 = defaultdict(list)
d164 = defaultdict(list)
d165 = defaultdict(list)
d166 = defaultdict(list)
d167 = defaultdict(list)
d168 = defaultdict(list)
d169 = defaultdict(list)
d170 = defaultdict(list)
d171 = defaultdict(list)
d172 = defaultdict(list)
d173 = defaultdict(list)
d174 = defaultdict(list)
d175 = defaultdict(list)
d176 = defaultdict(list)
d177 = defaultdict(list)
d178 = defaultdict(list)
d179 = defaultdict(list)
d180 = defaultdict(list)
d181 = defaultdict(list)
d182 = defaultdict(list)
d183 = defaultdict(list)
d184 = defaultdict(list)
d185 = defaultdict(list)
d186 = defaultdict(list)
d187 = defaultdict(list)
d188 = defaultdict(list)
d189 = defaultdict(list)
d190 = defaultdict(list)
d191 = defaultdict(list)
d192 = defaultdict(list)
d193 = defaultdict(list)
d194 = defaultdict(list)
d195 = defaultdict(list)
d196 = defaultdict(list)
d197 = defaultdict(list)
d198 = defaultdict(list)
d199 = defaultdict(list)
d200 = defaultdict(list)
d201 = defaultdict(list)
d202 = defaultdict(list)
d203 = defaultdict(list)
d204 = defaultdict(list)
d205 = defaultdict(list)
d206 = defaultdict(list)
d207 = defaultdict(list)
d208 = defaultdict(list)
d209 = defaultdict(list)
d210 = defaultdict(list)
d211 = defaultdict(list)
d212 = defaultdict(list)
d213 = defaultdict(list)
d214 = defaultdict(list)
d215 = defaultdict(list)
d216 = defaultdict(list)
d217 = defaultdict(list)
d218 = defaultdict(list)
d219 = defaultdict(list)
d220 = defaultdict(list)
d221 = defaultdict(list)
d222 = defaultdict(list)
d223 = defaultdict(list)
d224 = defaultdict(list)
d225 = defaultdict(list)
d226 = defaultdict(list)
d227 = defaultdict(list)
d228 = defaultdict(list)
d229 = defaultdict(list)
d230 = defaultdict(list)
d231 = defaultdict(list)
d232 = defaultdict(list)
d233 = defaultdict(list)
d234 = defaultdict(list)
d235 = defaultdict(list)
d236 = defaultdict(list)
d237 = defaultdict(list)
d238 = defaultdict(list)
d239 = defaultdict(list)
d240 = defaultdict(list)
d241 = defaultdict(list)
d242 = defaultdict(list)
d243 = defaultdict(list)
d244 = defaultdict(list)
d245 = defaultdict(list)
d246 = defaultdict(list)
d247 = defaultdict(list)
d248 = defaultdict(list)
d249 = defaultdict(list)
d250 = defaultdict(list)
d251 = defaultdict(list)
d252 = defaultdict(list)
d253 = defaultdict(list)
d254 = defaultdict(list)
d255 = defaultdict(list)
websters = [d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11, d12, d13, d14, d15, d16, d17, d18, d19, d20, d21, d22, d23, d24, d25, d26, d27, d28, d29, d30, d31, d32, d33, d34, d35, d36, d37, d38, d39, d40, d41, d42, d43, d44, d45, d46, d47, d48, d49, d50, d51, d52, d53, d54, d55, d56, d57, d58, d59, d60, d61, d62, d63, d64, d65, d66, d67, d68, d69, d70, d71, d72, d73, d74, d75, d76, d77, d78, d79, d80, d81, d82, d83, d84, d85, d86, d87, d88, d89, d90, d91, d92, d93, d94, d95, d96, d97, d98, d99, d100, d101, d102, d103, d104, d105, d106, d107, d108, d109, d110, d111, d112, d113, d114, d115, d116, d117, d118, d119, d120, d121, d122, d123, d124, d125, d126, d127, d128, d129, d130, d131, d132, d133, d134, d135, d136, d137, d138, d139, d140, d141, d142, d143, d144, d145, d146, d147, d148, d149, d150, d151, d152, d153, d154, d155, d156, d157, d158, d159, d160, d161, d162, d163, d164, d165, d166, d167, d168, d169, d170, d171, d172, d173, d174, d175, d176, d177, d178, d179, d180, d181, d182, d183, d184, d185, d186, d187, d188, d189, d190, d191, d192, d193, d194, d195, d196, d197, d198, d199, d200, d201, d202, d203, d204, d205, d206, d207, d208, d209, d210, d211, d212, d213, d214, d215, d216, d217, d218, d219, d220, d221, d222, d223, d224, d225, d226, d227, d228, d229, d230, d231, d232, d233, d234, d235, d236, d237, d238, d239, d240, d241, d242, d243, d244, d245, d246, d247, d248, d249, d250, d251, d252, d253, d254, d255]

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__":
        plac.call(main)

The output of a run of this program looks like this:

Sequoia 2010-H1 Class A-1 at CPR of 10%
April 2011:     78
April 2012:     60
April 2013:     47
April 2014:     35
April 2015:     26
April 2016:     20
April 2017:     16
April 2018:     11
April 2019:     8
April 2020:     5
April 2021:     3
April 2022:     *
April 2023:     0
April 2024:     0
April 2025:     0
April 2026:     0
April 2027:     0
April 2028:     0
April 2029:     0
April 2030:     0
April 2031:     0
April 2032:     0
April 2033:     0
April 2034:     0
April 2035:     0
April 2036:     0
April 2037:     0
April 2038:     0
April 2039:     0
April 2040:     0
>>> 

The results are likely to be different each time. This is because whether any given loan pays off in any given month is based on a probability for a given prepayment speed. Thus, at a CPR of 0.1, a loan has an approximately 1 in 113 chance of paying of in any particular month. The determination, each month, of whether a payoff occurs is based on a random selection from the possibilities of payoff (1 chance) or no payoff (112 chances). The function assumes no defaults, similarly to the traditional decrement table. However, a default function based on some combination of loan characteristics could be used to arrive at a probability of default in any given month similarly.

I tested the results of the model against the issuer’s table, and I found good agreement with results taken to the second decimal place.

This is the most complex Python program that I’ve written. Again, I don’t consider it production code, but did find it a useful prototype.

No one else submitted an example program, and the agency ended up dropping the proposal. That’s a shame, because in the round trip from the jargon of the trader’s desk to the lawyer’s chambers and then to the proprietary model, some meaning is lost in translation. On an industry conference call during the comment period, one proprietary modeler admitted that he didn’t know how to interpret the legal term notwithstanding. That is just as understandable as a lawyer being innocent of a NAND gate, but potentially more harmful.