"""
PyMBS is a Python library for use in modeling Mortgage-Backed Securities.
Copyright (C) 2019 Brian Farrell
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Contact: brian.farrell@me.com
"""
from collections import OrderedDict
import decimal
import pandas as pd
from pymbs.log import get_logger
logger = get_logger(__name__)
dec = decimal.Decimal
d0 = dec('0')
d1200 = dec('1200')
cleanup = dec('1E-2')
# TODO: Get the initial rate from the model and get the subsequent rates
# from a FRED lookup
LIBOR = 1.31
[docs]class Tranche(object):
"""The Tranche object represents a tranche in the deal.
It is used to store all pertinent information about the tranche,
including any cash flows calculated for it.
The current design of this object provides an experimental 'child tranche'
option, which allows for the creation of pseudo tranches, to enable a
true tree-like representation of the cash flow structure and the payment
waterfall.
Generally speaking, when reversing a deal, the cash flow structure is
flattened so that only the tranches disclosed in the Prospectus Supplement
are included in the model.
The tree representation with its attending pseudo tranches offeres a truer
rendition of the model, but may not be necessary and requires a greater
understanding of structured cash flow models. This functionality *may*
be deprecated and removed in future releases.
"""
def __init__(self, *args, **kwargs):
self._assumed_price = dec('1.00')
self._cash_flows = {}
self._child_tranches = []
self._coupon = kwargs.get('coupon')
self._cusip = kwargs.get('cusip')
self._dated_date = kwargs.get('dated_date')
self._delay = kwargs.get('delay')
self._final_payment_date = kwargs.get('final_payment_date')
self._floater_cap = kwargs.get('floater_cap')
self._floater_floor = kwargs.get('floater_floor')
self._floater_formula = kwargs.get('floater_formula')
self._group_id = kwargs.get('group_id')
self._id = kwargs.get('class_id')
self._int_type = kwargs.get('int_type')
self._macr = kwargs.get('macr')
self._next_payment_date = kwargs.get('next_payment_date')
self._notional_with = kwargs.get('notional_with')
self._original_upb = kwargs.get('upb')
self._periodic_cf = OrderedDict()
self._periodic_prepay_scenario = ''
self._price_at_issuance = dec('1.00')
self._prin_type = kwargs.get('prin_type')
self._pro_rated_ratio = dec('0')
self._retail = kwargs.get('retail')
self._schedule = kwargs.get('schedule')
self._strips = []
self._upb = kwargs.get('upb')
@property
def assumed_price(self):
return self._assumed_price
@assumed_price.setter
def assumed_price(self, value):
self._assumed_price = value
@property
def cash_flows(self):
return self._cash_flows
@property
def child_tranches(self):
return self._child_tranches
@property
def coupon(self):
return self._coupon
@coupon.setter
def coupon(self, value):
self._coupon = value
@property
def cusip(self):
return self._cusip
@cusip.setter
def cusip(self, value):
self._cusip = value
@property
def dated_date(self):
return self._dated_date
@dated_date.setter
def dated_date(self, value):
self._dated_date = value
@property
def delay(self):
return self._delay
@delay.setter
def delay(self, value):
self._delay = value
@property
def final_payment_date(self):
return self._final_payment_date
@final_payment_date.setter
def final_payment_date(self, value):
self._final_payment_date = value
@property
def floater_cap(self):
return self._floater_cap
@floater_cap.setter
def floater_cap(self, value):
self._floater_cap = value
@property
def floater_floor(self):
return self._floater_floor
@floater_floor.setter
def floater_floor(self, value):
self._floater_floor = value
@property
def floater_formula(self):
return self._floater_formula
@floater_formula.setter
def floater_formula(self, value):
self._floater_formula = value
@property
def group_id(self):
return self._group_id
@group_id.setter
def group_id(self, value):
self._floater_floor = value
@property
def id(self):
return self._id
@id.setter
def id(self, value):
self._floater_floor = value
@property
def int_type(self):
return self._int_type
@int_type.setter
def int_type(self, value):
self._int_type = value
@property
def macr(self):
return self._macr
@macr.setter
def macr(self, value):
self._macr = value
@property
def next_payment_date(self):
return self._next_payment_date
@next_payment_date.setter
def next_payment_date(self, value):
self._next_payment_date = value
@property
def notional_with(self):
return self._notional_with
@notional_with.setter
def notional_with(self, value):
self._notional_with.clear()
self._notional_with.extend(value)
@property
def original_upb(self):
return self._original_upb
@original_upb.setter
def original_upb(self, value):
self._original_upb = value
@property
def periodic_cf(self):
return self._periodic_cf
@periodic_cf.setter
def periodic_cf(self, value):
self._periodic_cf = value
@property
def periodic_prepay_scenario(self):
return self._periodic_prepay_scenario
@periodic_prepay_scenario.setter
def periodic_prepay_scenario(self, value):
self._periodic_prepay_scenario = value
@property
def price_at_issuance(self):
return self._price_at_issuance
@price_at_issuance.setter
def price_at_issuance(self, value):
self._price_at_issuance = dec(value)
@property
def prin_type(self):
return self._prin_type
@prin_type.setter
def prin_type(self, value):
self._prin_type = value
@property
def pro_rated_ratio(self):
return self._pro_rated_ratio
@pro_rated_ratio.setter
def pro_rated_ratio(self, value):
self._pro_rated_ratio = dec(value)
@property
def retail(self):
return self._retail
@retail.setter
def retail(self, value):
self._retail = value
@property
def schedule(self):
return self._schedule
@schedule.setter
def schedule(self, value):
self._schedule = value
@property
def strips(self):
"""This is a list of Notional tranches, whose interest was stripped
from this tranche. It is the corollary to the 'notional_with'
attribute.
TODO: Need to actually populate this list during the loading of the
model!
"""
return self._strips
@strips.setter
def strips(self, value):
self._strips.append(value)
@property
def upb(self):
return self._upb
@upb.setter
def upb(self, value):
self._upb = value
[docs] def new_child_tranche(
self, group_id, id, upb, coupon, floater_formula, floater_cap,
floater_floor, prin_type, int_type, notional_with, delay, retail, macr,
final_payment_date, cusip, schedule,
dated_date=None, next_payment_date=None
):
tranche = Tranche(
group_id, id, upb, coupon, floater_formula, floater_cap,
floater_floor, prin_type, int_type, notional_with, delay, retail,
macr, final_payment_date, cusip, schedule,
dated_date=None, next_payment_date=None
)
self.child_tranches.append(tranche)
return tranche
[docs] def end_periodic_cf(self):
self.cash_flows[self.periodic_prepay_scenario].append(
self.periodic_cf.copy()
)
[docs] def new_periodic_cf(self, prepay_scenario, period, payment_date):
if prepay_scenario != self.periodic_prepay_scenario:
self.periodic_prepay_scenario = prepay_scenario
self.cash_flows[self.periodic_prepay_scenario] = []
self.upb = self.original_upb
self.periodic_cf.clear()
self._initialize_periodic_flow(period, payment_date)
def _initialize_periodic_flow(self, period, payment_date):
self.periodic_cf = OrderedDict().fromkeys(
[
'period',
'payment_date',
'beginning_balance',
'interest',
'accrual',
'principal',
'ending_balance'
]
)
self.periodic_cf['period'] = period
self.periodic_cf['payment_date'] = payment_date
self.periodic_cf['beginning_balance'] = self.upb
self.periodic_cf['interest'] = dec('0')
self.periodic_cf['accrual'] = dec('0')
self.periodic_cf['principal'] = dec('0')
self.periodic_cf['ending_balance'] = self.upb
[docs] def pay_accrue(self):
self.periodic_cf['accrual'] += self.periodic_cf['interest']
self.periodic_cf['ending_balance'] += self.periodic_cf['accrual']
self.upb += self.periodic_cf['accrual']
self.periodic_cf['interest'] = dec('0')
[docs] def pay_interest(self, collat_net_interest):
balance = self.upb
if self.floater_formula:
coupon = min(
max(dec(eval(self.floater_formula)), self.floater_floor),
self.floater_cap
)
else:
coupon = self.coupon
interest_payment = balance * (coupon / d1200)
self.periodic_cf['interest'] = interest_payment
collat_net_interest -= interest_payment
return collat_net_interest
[docs] def pay_principal(self, principal_payment):
self.upb -= principal_payment
if self.upb <= cleanup:
self.upb = d0
self.periodic_cf['principal'] += principal_payment
self.periodic_cf['ending_balance'] -= principal_payment
[docs] def reduce_notional_balance(self):
pass
[docs] def tabulate_cf(self):
for prepay_scenario in self.cash_flows:
cash_flow_table = pd.DataFrame.from_dict(
self.cash_flows[prepay_scenario]
)
self.cash_flows[prepay_scenario] = cash_flow_table
[docs]class IndexRate(object):
"""Create and Index rate object for each Benchmark Index used in the deal.
Traditionally, the most popular index for Mortgage-Backed Securites has
been 1-Month LIBOR (The London Inter-bank Offered Rate).
However, in light of recent revelations reagrding LIBOR-fixing
by market makers, it is being phased-out, in favor of SOFR -
the Secured Overnight Financing Rate.
Other Index Rate Benchmarks may be used as well.
"""
def __init__(self, name, benchmark, fred_ticker, initial_rate):
self._name = name
self._benchmark = benchmark
self._fred_ticker = fred_ticker
self._initial_rate = initial_rate
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
@property
def benchmark(self):
return self._benchmark
@benchmark.setter
def benchmark(self, value):
self._benchmark = value
@property
def fred_ticker(self):
return self._fred_ticker
@fred_ticker.setter
def fred_ticker(self, value):
self._fred_ticker = value
@property
def initial_rate(self):
return self._initial_rate
@initial_rate.setter
def initial_rate(self, value):
self._initial_rate = value