Tutorial 4: Portfolio Simulation

Run Monte Carlo simulations for portfolio analytics.


Step 1: Configure Portfolio

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

# Generate portfolio
generator = LoanTapeGenerator(
    n_loans=10000,
    n_months=60,
    asset_class_weights={
        'corporate': 0.40,
        'consumer': 0.25,
        'realestate': 0.25,
        'receivables': 0.10
    },
    random_seed=42
)

loans_df, _ = generator.generate_static_features()

print(f"Portfolio size: ${loans_df['original_balance'].sum():,.0f}")
print(f"Number of loans: {len(loans_df)}")
print(f"Average loan: ${loans_df['original_balance'].mean():,.0f}")

Step 2: Configure Waterfall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# CLO-style waterfall structure
waterfall_config = WaterfallConfig(
    tranches=[
        {'name': 'AAA', 'size': 0.60, 'coupon': 0.045, 'priority': 1},
        {'name': 'AA', 'size': 0.10, 'coupon': 0.055, 'priority': 2},
        {'name': 'A', 'size': 0.08, 'coupon': 0.065, 'priority': 3},
        {'name': 'BBB', 'size': 0.07, 'coupon': 0.080, 'priority': 4},
        {'name': 'BB', 'size': 0.05, 'coupon': 0.100, 'priority': 5},
        {'name': 'Equity', 'size': 0.10, 'coupon': None, 'priority': 6},
    ],
    oc_trigger=1.20,
    ic_trigger=1.05,
    reinvestment_period=24,
    management_fee=0.005
)

aggregator = PortfolioAggregator(waterfall_config)

Step 3: Run Monte Carlo Simulation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import torch
from privatecredit.models import MacroVAE, TransitionTransformer, LoanTrajectoryModel

# Load trained models
checkpoint = torch.load('models/trained_models.pt')
macro_vae = MacroVAE(checkpoint['configs']['macro_vae'])
macro_vae.load_state_dict(checkpoint['macro_vae'])
# ... load other models

# Run simulation
n_simulations = 10000

results = aggregator.monte_carlo_simulate(
    loans_df=loans_df,
    macro_vae=macro_vae,
    transition_model=transition_model,
    trajectory_model=trajectory_model,
    n_simulations=n_simulations,
    scenario_mix={'baseline': 0.6, 'adverse': 0.3, 'severe': 0.1}
)

print(f"Completed {n_simulations} simulations")

Step 4: Analyze Loss Distribution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import matplotlib.pyplot as plt
import numpy as np

# Portfolio loss distribution
losses = results.portfolio_losses

fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# Histogram
axes[0].hist(losses * 100, bins=50, density=True, alpha=0.7, color='steelblue')
axes[0].axvline(results.var_99 * 100, color='red', linestyle='--', label=f'VaR 99%: {results.var_99:.1%}')
axes[0].axvline(results.cvar_99 * 100, color='orange', linestyle='--', label=f'CVaR 99%: {results.cvar_99:.1%}')
axes[0].set_xlabel('Portfolio Loss (%)')
axes[0].set_ylabel('Density')
axes[0].set_title('Loss Distribution')
axes[0].legend()

# CDF
sorted_losses = np.sort(losses)
cdf = np.arange(1, len(sorted_losses) + 1) / len(sorted_losses)
axes[1].plot(sorted_losses * 100, cdf * 100)
axes[1].axhline(99, color='red', linestyle='--', alpha=0.5)
axes[1].axvline(results.var_99 * 100, color='red', linestyle='--', alpha=0.5)
axes[1].set_xlabel('Portfolio Loss (%)')
axes[1].set_ylabel('Cumulative Probability (%)')
axes[1].set_title('Loss CDF')

plt.tight_layout()
plt.savefig('loss_distribution.pdf')

Step 5: Calculate Risk Metrics

1
2
3
4
5
6
7
8
9
# Key risk metrics
print("=== Portfolio Risk Metrics ===")
print(f"Expected Loss:     {results.expected_loss:.2%}")
print(f"Loss Volatility:   {results.loss_std:.2%}")
print(f"VaR 95%:           {results.var_95:.2%}")
print(f"VaR 99%:           {results.var_99:.2%}")
print(f"VaR 99.9%:         {results.var_999:.2%}")
print(f"CVaR 99%:          {results.cvar_99:.2%}")
print(f"Max Observed Loss: {results.max_loss:.2%}")

Step 6: Tranche-Level Analysis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Tranche returns
print("\n=== Tranche Returns ===")
print(f"{'Tranche':<10} {'IRR':<10} {'Yield':<10} {'Loss Rate':<12} {'WAL':<8}")
print("-" * 50)

for tranche in results.tranche_results:
    print(f"{tranche.name:<10} "
          f"{tranche.irr:.2%}    "
          f"{tranche.yield_:.2%}    "
          f"{tranche.loss_rate:.2%}      "
          f"{tranche.wal:.1f}")

# Tranche loss probability
print("\n=== Tranche Loss Probability ===")
for tranche in results.tranche_results:
    print(f"{tranche.name}: P(Loss > 0) = {tranche.prob_any_loss:.2%}, "
          f"P(Total Loss) = {tranche.prob_total_loss:.4%}")

Step 7: Cashflow Projections

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Monthly cashflow projections
cashflows = results.aggregate_cashflows()

fig, axes = plt.subplots(2, 2, figsize=(12, 8))

# Interest collections
axes[0, 0].plot(cashflows['collected_interest'].mean(axis=0) / 1e6)
axes[0, 0].fill_between(
    range(60),
    cashflows['collected_interest'].quantile(0.05, axis=0) / 1e6,
    cashflows['collected_interest'].quantile(0.95, axis=0) / 1e6,
    alpha=0.3
)
axes[0, 0].set_title('Interest Collections ($ millions)')
axes[0, 0].set_xlabel('Month')

# Principal collections
axes[0, 1].plot(cashflows['collected_principal'].mean(axis=0) / 1e6)
axes[0, 1].fill_between(
    range(60),
    cashflows['collected_principal'].quantile(0.05, axis=0) / 1e6,
    cashflows['collected_principal'].quantile(0.95, axis=0) / 1e6,
    alpha=0.3
)
axes[0, 1].set_title('Principal Collections ($ millions)')
axes[0, 1].set_xlabel('Month')

# Defaults
axes[1, 0].plot(cashflows['defaults'].mean(axis=0) / 1e6)
axes[1, 0].set_title('Defaults ($ millions)')
axes[1, 0].set_xlabel('Month')

# Cumulative losses
axes[1, 1].plot(cashflows['losses'].cumsum(axis=1).mean(axis=0) / 1e6)
axes[1, 1].set_title('Cumulative Losses ($ millions)')
axes[1, 1].set_xlabel('Month')

plt.tight_layout()
plt.savefig('cashflow_projections.pdf')

Step 8: Tranche Waterfall Visualization

1
2
3
4
5
6
7
8
9
10
11
12
13
# Visualize waterfall mechanics
from privatecredit.evaluation import plot_waterfall

fig = plot_waterfall(
    results,
    month=36,  # Show waterfall at month 36
    scenario='mean'
)
plt.savefig('waterfall_month36.pdf')

# Animate waterfall over time
from privatecredit.evaluation import animate_waterfall
animate_waterfall(results, output='waterfall_animation.mp4')

Step 9: Export Results

1
2
3
4
5
6
7
8
9
# Export to Excel
results.to_excel('simulation_results.xlsx', include_paths=False)

# Export detailed paths (large file)
results.to_parquet('simulation_paths.parquet')

# Summary statistics
summary = results.summary()
print(summary.to_markdown())

Summary Statistics

Metric Value
Expected Loss 2.3%
VaR 99% 5.8%
CVaR 99% 7.2%
AAA Tranche IRR 4.5%
Equity Tranche IRR 12.8%
Equity Loss Probability 15.3%

Next Steps