Portfolio Aggregator

Differentiable waterfall simulation for portfolio and tranche-level analytics.


Architecture

1
2
3
4
5
6
7
8
9
10
11
12
13
Loan Trajectories (from Loan Trajectory Model)
    |
    v
Cashflow Aggregation
    |
    v
Waterfall Engine
    |
    v
Tranche Cashflows & Returns
    |
    v
Loss Distribution & Risk Metrics

Waterfall Structure

Typical CLO/SPV waterfall:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Collections (Interest + Principal)
    |
    +---> Senior Management Fees
    |
    +---> Senior Tranche Interest
    |
    +---> Senior Tranche Principal (if OC test fails)
    |
    +---> Mezzanine Tranche Interest
    |
    +---> Mezzanine Tranche Principal
    |
    +---> Junior Tranche Interest
    |
    +---> Subordinated Fees
    |
    +---> Residual to Equity

Tranche Configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from privatecredit.models import PortfolioAggregator, WaterfallConfig

tranches = [
    {'name': 'Senior', 'size': 0.70, 'coupon': 0.05, 'priority': 1},
    {'name': 'Mezz', 'size': 0.15, 'coupon': 0.08, 'priority': 2},
    {'name': 'Junior', 'size': 0.10, 'coupon': 0.12, 'priority': 3},
    {'name': 'Equity', 'size': 0.05, 'coupon': None, 'priority': 4},
]

config = WaterfallConfig(
    tranches=tranches,
    oc_trigger=1.20,      # OC test threshold
    ic_trigger=1.05,      # IC test threshold
    reinvestment_period=24  # Months
)

aggregator = PortfolioAggregator(config)

Portfolio Aggregation

From individual loan trajectories to portfolio cashflows:

1
2
3
4
5
6
7
8
9
10
# Aggregate loan trajectories
portfolio_cf = aggregator.aggregate_cashflows(
    trajectories=loan_trajectories,
    loan_features=loans_df
)

print(portfolio_cf.columns)
# ['month', 'scheduled_interest', 'scheduled_principal',
#  'collected_interest', 'collected_principal',
#  'defaults', 'recoveries', 'losses']

Monte Carlo Simulation

Run full simulation for loss distribution:

1
2
3
4
5
6
7
8
9
10
11
12
13
results = aggregator.simulate(
    loan_trajectories=trajectories,  # (n_simulations, n_loans, n_months)
    n_simulations=10000
)

# Portfolio-level metrics
print(f"Expected Loss: {results.expected_loss:.2%}")
print(f"VaR 99%: {results.var_99:.2%}")
print(f"CVaR 99%: {results.cvar_99:.2%}")

# Tranche-level returns
for tranche in results.tranche_returns:
    print(f"{tranche.name}: IRR = {tranche.irr:.2%}")

Coverage Tests

The waterfall implements standard CLO tests:

Test Formula Trigger
OC Test Par Value / Tranche Balance < 1.20
IC Test Interest Collections / Interest Due < 1.05

When tests fail, cashflows are redirected to senior tranches.


Loss Distribution

The aggregator estimates the full loss distribution:

1
2
3
4
5
6
7
8
9
import matplotlib.pyplot as plt

losses = results.portfolio_losses
plt.hist(losses, bins=50, density=True)
plt.axvline(results.var_99, color='r', label='VaR 99%')
plt.axvline(results.cvar_99, color='orange', label='CVaR 99%')
plt.xlabel('Portfolio Loss Rate')
plt.ylabel('Density')
plt.legend()

Differentiability

The entire waterfall is differentiable via soft approximations:

1
2
Hard: if x > threshold then action
Soft: sigmoid((x - threshold) / temperature) * action

This enables end-to-end training with portfolio-level objectives:

1
2
3
4
5
6
# Joint training with tranche return targets
loss = model.compute_loss(
    predicted_returns=tranche_returns,
    target_returns=historical_returns
)
loss.backward()

Key Metrics

Metric Description
Expected Loss Mean of loss distribution
VaR Value at Risk at confidence level
CVaR Conditional VaR (Expected Shortfall)
Tranche IRR Internal rate of return per tranche
Attachment/Detachment Loss levels where tranche is affected
WAL Weighted average life

Back to Architecture