Structured Credit Products (CDOs and Securitizations) › Cash Flow Waterfall

Michael BrenndoerferNovember 19, 20257 min read
Quantitative FinanceAll chapters
Part 4 of 15Swaps and Credit Derivatives
Chapter 23Structured Credit Products (CDOs and Securitizations) › Cash Flow Waterfall
Reading section 4 of 12 · Cash Flow Waterfall · 7 minSection 4 of 12 · 7 min

Book contents

Section 4 of 12 · 7 min read

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:

  1. Trustee and administrative fees
  2. Senior tranche interest
  3. Mezzanine tranche interest
  4. Senior tranche principal (if coverage tests fail)
  5. Mezzanine tranche principal (if coverage tests fail)
  6. 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:

In[7]:
Code
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, losses

Now let's create a CDO and see how it distributes cash flows under different loss scenarios:

In[8]:
Code
# 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)
Out[9]:
Visualization
Bar chart showing normal-condition distributions of $1.80 million to senior, $0.49 million to mezzanine, and $3.71 million to equity.
Interest waterfall under normal conditions (1% pool loss). Senior and mezzanine interest are paid before the remaining $3.71 million of excess spread flows to equity.
Bar chart showing stressed-condition distributions of $1.80 million to senior, $0.14 million to mezzanine, and $2.56 million to equity, with notes that mezzanine principal is impaired and equity principal is wiped out.
Interest waterfall under stressed conditions (8% pool loss). Senior receives $1.80 million, mezzanine receives $0.14 million after principal impairment, and $2.56 million of residual cash is distributed even though the equity principal balance is wiped out in this simplified model.
In[10]:
Code
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"]]
Out[11]:
Console
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.

In[12]:
Code
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)
Out[13]:
Visualization
Line chart showing loss rates for three CDO tranches versus pool loss rate
Tranche loss sensitivity to pool losses. The equity tranche exhibits extreme leverage, reaching 100% loss when the pool loses only 3%, while the senior tranche remains protected until pool losses exceed 10%.