Structured Credit Products (CDOs and Securitizations) › Cash Flow Waterfall
Quantitative FinanceAll chapters Structured Credit Products (CDOs and Securitizations) › Cash Flow WaterfallOne section · Sections in this chapter4 / 12 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
The waterfall structure determines how cash flows from the underlying assets are distributed among tranches. This mechanism is what creates the different risk profiles, so senior tranches receive their promised payments before junior tranches see any return.
Priority of Payments
In a typical cash CDO, cash flows follow a strict priority known as the waterfall. The term "waterfall" evokes the image of water flowing down a series of pools: the top pool must fill completely before any water spills to the next level, and so on down the structure. Similarly, each category of payment must be satisfied in full before the next category receives anything:
- Trustee and administrative fees
- Senior tranche interest
- Mezzanine tranche interest
- Senior tranche principal (if coverage tests fail)
- Mezzanine tranche principal (if coverage tests fail)
- Equity tranche (residual cash flows)
This priority structure explains why senior tranches can achieve AAA ratings even when the underlying loans are far from risk-free. The senior tranche receives payment first, and it only suffers losses after all subordinate tranches have been completely wiped out. In essence, the equity and mezzanine tranches serve as a buffer, absorbing losses that would otherwise reach senior investors.
Let's implement a simplified waterfall model to see how this works in practice:
class CDOWaterfall:
"""Simple CDO cash flow waterfall model."""
def __init__(self, tranches: dict, pool_size: float):
"""
Initialize CDO structure.
Parameters:
-----------
tranches : dict
Dictionary with tranche names as keys, containing:
- 'size': notional amount
- 'coupon': annual coupon rate
- 'attachment': attachment point (fraction)
- 'detachment': detachment point (fraction)
pool_size : float
Total notional of underlying asset pool
"""
self.tranches = tranches
self.pool_size = pool_size
self.tranche_balances = {
name: t["size"] for name, t in tranches.items()
}
def calculate_losses(self, pool_loss_rate: float) -> dict:
"""Calculate losses allocated to each tranche given pool loss rate."""
total_loss = pool_loss_rate * self.pool_size
tranche_losses = {}
for name, tranche in self.tranches.items():
attach = tranche["attachment"] * self.pool_size
detach = tranche["detachment"] * self.pool_size
if total_loss <= attach:
loss = 0
elif total_loss >= detach:
loss = detach - attach
else:
loss = total_loss - attach
tranche_losses[name] = loss
return tranche_losses
def distribute_cash_flow(
self, available_cash: float, pool_loss_rate: float
) -> dict:
"""Distribute available cash flow according to waterfall."""
# First, allocate losses
losses = self.calculate_losses(pool_loss_rate)
# Update tranche balances
current_balances = {}
for name, tranche in self.tranches.items():
current_balances[name] = max(0, tranche["size"] - losses[name])
distributions = {}
remaining_cash = available_cash
# Pay interest in order of seniority (senior first)
seniority_order = ["Senior", "Mezzanine", "Equity"]
for name in seniority_order:
if name in self.tranches:
tranche = self.tranches[name]
interest_due = current_balances[name] * tranche["coupon"]
interest_paid = min(remaining_cash, interest_due)
distributions[name] = {
"interest": interest_paid,
"principal": 0,
}
remaining_cash -= interest_paid
# Remaining cash goes to equity as excess spread
if "Equity" in distributions:
distributions["Equity"]["excess_spread"] = remaining_cash
return distributions, lossesNow let's create a CDO and see how it distributes cash flows under different loss scenarios:
# Define CDO structure
pool_notional = 100_000_000 # $100 million pool
tranches = {
"Senior": {
"size": 90_000_000, # 90% of pool
"coupon": 0.02, # 2% coupon (LIBOR + spread)
"attachment": 0.10, # Absorbs losses after 10%
"detachment": 1.00, # Up to 100%
},
"Mezzanine": {
"size": 7_000_000, # 7% of pool
"coupon": 0.07, # 7% coupon
"attachment": 0.03, # Absorbs losses after 3%
"detachment": 0.10, # Up to 10%
},
"Equity": {
"size": 3_000_000, # 3% of pool
"coupon": 0.0, # No fixed coupon, receives residual
"attachment": 0.00, # First loss
"detachment": 0.03, # Up to 3%
},
}
cdo = CDOWaterfall(tranches, pool_notional)

import pandas as pd
# Analyze loss allocation under different scenarios
loss_scenarios = [0.01, 0.03, 0.05, 0.08, 0.12, 0.20]
loss_analysis = []
for loss_rate in loss_scenarios:
losses = cdo.calculate_losses(loss_rate)
for name, tranche in tranches.items():
tranche_loss_pct = (
losses[name] / tranche["size"] * 100 if tranche["size"] > 0 else 0
)
loss_analysis.append(
{
"Pool Loss (%)": loss_rate * 100,
"Tranche": name,
"Tranche Loss ($M)": losses[name] / 1e6,
"Tranche Loss (%)": tranche_loss_pct,
}
)
loss_df = pd.DataFrame(loss_analysis)
pivot_df = loss_df.pivot(
index="Pool Loss (%)", columns="Tranche", values="Tranche Loss (%)"
)
pivot_df = pivot_df[["Equity", "Mezzanine", "Senior"]]Tranche Loss Rates (%) for Different Pool Loss Scenarios: ------------------------------------------------------------ Tranche Equity Mezzanine Senior Pool Loss (%) 1.0 33.3 0.0 0.0 3.0 100.0 0.0 0.0 5.0 100.0 28.6 0.0 8.0 100.0 71.4 0.0 12.0 100.0 100.0 2.2 20.0 100.0 100.0 11.1
This analysis reveals the leverage embedded in lower tranches. At 5% pool losses, the equity tranche is wiped out (100% loss), the mezzanine has lost 28.6% of its value, while the senior tranche remains untouched. The equity tranche, representing only 3% of the capital structure, absorbs the entire impact of moderate losses. This leverage cuts both ways: in good times, the equity tranche captures all excess returns, but in bad times, it bears concentrated losses that far exceed the pool's overall loss rate.
pool_losses = np.linspace(0, 0.25, 100)
tranche_loss_curves = {name: [] for name in tranches.keys()}
for pl in pool_losses:
losses = cdo.calculate_losses(pl)
for name, tranche in tranches.items():
loss_pct = (
losses[name] / tranche["size"] * 100 if tranche["size"] > 0 else 0
)
tranche_loss_curves[name].append(loss_pct)