Tutorial 5: Creating Custom Macro Scenarios
Learn how to create user-defined macro scenarios, interpolate between standard scenarios, and replay historical data.
Prerequisites
1
2
3
4
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from privatecredit.data import MacroScenarioGenerator
1. Understanding Scenario Structure
Each macro scenario contains 9 variables over 60 months:
| Variable | Description | Baseline Range |
|---|---|---|
gdp_growth_yoy |
Year-over-year GDP growth | 1-4% |
unemployment_rate |
Unemployment rate | 3-6% |
inflation_yoy |
Year-over-year inflation | 1-3% |
fed_funds_rate |
Federal funds rate | 2-5% |
treasury_10y |
10-year Treasury yield | 2-4% |
credit_spread_ig |
Investment grade spread | 80-150 bps |
credit_spread_hy |
High yield spread | 300-500 bps |
property_index |
Commercial property index | 95-110 |
equity_return |
Equity market return | -5% to +20% |
2. Using the Standard Generator
Generate Built-in Scenarios
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Initialize generator
generator = MacroScenarioGenerator(
n_months=60,
start_date='2024-01-01',
seed=42
)
# Generate standard scenarios
baseline = generator.generate_scenario('baseline')
adverse = generator.generate_scenario('adverse')
severe = generator.generate_scenario('severely_adverse')
stagflation = generator.generate_scenario('stagflation')
print(f"Scenario shape: {baseline.shape}")
print(f"Variables: {list(baseline.columns)}")
View Scenario Summary
1
2
3
4
5
6
7
8
9
10
11
def scenario_summary(scenario, name):
"""Print summary statistics for a scenario."""
print(f"\n{name.upper()} Scenario:")
print(f" GDP Growth: {scenario['gdp_growth_yoy'].mean():.2%}")
print(f" Unemployment: {scenario['unemployment_rate'].mean():.1%}")
print(f" Inflation: {scenario['inflation_yoy'].mean():.2%}")
print(f" HY Spread: {scenario['credit_spread_hy'].mean():.0f} bps")
for name, scen in [('Baseline', baseline), ('Adverse', adverse),
('Severe', severe), ('Stagflation', stagflation)]:
scenario_summary(scen, name)
3. Creating Custom Scenarios
Method 1: Parameter Override
1
2
3
4
5
6
7
8
9
10
# Create custom scenario with specific parameters
custom_params = {
'gdp_growth_mean': 0.01, # 1% GDP growth
'unemployment_peak': 0.08, # Peak at 8%
'inflation_mean': 0.035, # 3.5% inflation
'hy_spread_peak': 800, # Peak spread 800 bps
'duration_stress': 18 # Stress lasts 18 months
}
custom_scenario = generator.generate_custom_scenario(**custom_params)
Method 2: Direct DataFrame Construction
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
def create_manual_scenario(n_months=60):
"""Create a scenario manually with specific paths."""
months = pd.date_range('2024-01-01', periods=n_months, freq='M')
# Define time-varying paths
t = np.linspace(0, 1, n_months)
scenario = pd.DataFrame({
'date': months,
# V-shaped recovery
'gdp_growth_yoy': 0.025 - 0.05 * np.sin(np.pi * t) * np.exp(-2*t),
# Rising then falling unemployment
'unemployment_rate': 0.04 + 0.04 * np.sin(np.pi * t * 0.8),
# Persistent inflation
'inflation_yoy': 0.03 + 0.02 * (1 - np.exp(-3*t)),
# Rate hike cycle
'fed_funds_rate': 0.025 + 0.025 * np.minimum(t * 2, 1),
# 10Y follows short rates with lag
'treasury_10y': 0.03 + 0.015 * np.minimum(t * 1.5, 1),
# Credit spreads widen then normalize
'credit_spread_ig': 100 + 100 * np.sin(np.pi * t) * np.exp(-2*t),
'credit_spread_hy': 400 + 400 * np.sin(np.pi * t) * np.exp(-1.5*t),
# Property decline
'property_index': 100 - 10 * np.sin(np.pi * t * 0.6),
# Equity volatility
'equity_return': 0.05 - 0.15 * np.sin(np.pi * t * 0.5) * np.exp(-1.5*t)
})
return scenario.set_index('date')
custom = create_manual_scenario()
Method 3: Scenario Templates
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
class ScenarioTemplate:
"""Reusable scenario templates."""
@staticmethod
def rate_shock(n_months=60, shock_size=0.02):
"""Interest rate shock scenario."""
t = np.linspace(0, 1, n_months)
base = MacroScenarioGenerator(n_months=n_months).generate_scenario('baseline')
# Apply rate shock
base['fed_funds_rate'] += shock_size * (1 - np.exp(-5*t))
base['treasury_10y'] += shock_size * 0.8 * (1 - np.exp(-3*t))
# Ripple effects
base['gdp_growth_yoy'] -= shock_size * 0.5 * np.exp(-t)
base['property_index'] -= shock_size * 500 * (1 - np.exp(-2*t))
return base
@staticmethod
def credit_crunch(n_months=60, spread_peak=1000):
"""Credit market stress scenario."""
t = np.linspace(0, 1, n_months)
base = MacroScenarioGenerator(n_months=n_months).generate_scenario('baseline')
# Credit spread spike
spread_path = spread_peak * np.sin(np.pi * t * 0.6) * np.exp(-t)
base['credit_spread_hy'] += spread_path
base['credit_spread_ig'] += spread_path * 0.3
# Economic impact
base['gdp_growth_yoy'] -= 0.02 * np.sin(np.pi * t * 0.6)
return base
# Use templates
rate_shock = ScenarioTemplate.rate_shock(shock_size=0.03)
credit_crunch = ScenarioTemplate.credit_crunch(spread_peak=800)
4. Interpolating Between Scenarios
Linear Interpolation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def interpolate_scenarios(scenario1, scenario2, weight):
"""
Linearly interpolate between two scenarios.
Args:
scenario1: First scenario DataFrame
scenario2: Second scenario DataFrame
weight: Weight for scenario2 (0 = scenario1, 1 = scenario2)
"""
return scenario1 * (1 - weight) + scenario2 * weight
# Create intermediate scenario (30% adverse, 70% baseline)
mild_stress = interpolate_scenarios(baseline, adverse, weight=0.3)
# Verify
print(f"Baseline GDP: {baseline['gdp_growth_yoy'].mean():.2%}")
print(f"Adverse GDP: {adverse['gdp_growth_yoy'].mean():.2%}")
print(f"Mild Stress GDP: {mild_stress['gdp_growth_yoy'].mean():.2%}")
Scenario Blending Over Time
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
def time_varying_blend(scenario1, scenario2, transition_start, transition_end):
"""
Blend scenarios with time-varying weights.
Starts in scenario1, transitions to scenario2.
"""
n_months = len(scenario1)
weights = np.zeros(n_months)
# Create smooth transition weights
for i in range(n_months):
if i < transition_start:
weights[i] = 0
elif i >= transition_end:
weights[i] = 1
else:
# Smooth cosine transition
progress = (i - transition_start) / (transition_end - transition_start)
weights[i] = 0.5 * (1 - np.cos(np.pi * progress))
# Apply weights
blended = scenario1.copy()
for col in blended.columns:
blended[col] = scenario1[col] * (1 - weights) + scenario2[col] * weights
return blended, weights
# Start baseline, transition to adverse over months 12-24
blended, weights = time_varying_blend(baseline, adverse,
transition_start=12,
transition_end=24)
5. Historical Scenario Replay
Load Historical Data
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
def load_historical_data(start_date, end_date):
"""
Load historical macro data from FRED (simulated here).
In production, use fredapi or similar.
"""
# Simulated historical data
dates = pd.date_range(start_date, end_date, freq='M')
n = len(dates)
np.random.seed(123)
# 2008-2009 style recession
historical = pd.DataFrame({
'date': dates,
'gdp_growth_yoy': np.concatenate([
np.linspace(0.02, -0.04, n//3),
np.linspace(-0.04, 0.02, n//3),
np.linspace(0.02, 0.025, n - 2*(n//3))
]) + np.random.normal(0, 0.003, n),
'unemployment_rate': np.concatenate([
np.linspace(0.05, 0.10, n//2),
np.linspace(0.10, 0.06, n - n//2)
]) + np.random.normal(0, 0.002, n),
# ... other variables
}).set_index('date')
return historical
# Load and extend historical scenario
historical = load_historical_data('2007-01-01', '2011-12-31')
Extend Historical to 60 Months
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
def extend_scenario(historical_data, target_months=60):
"""
Extend historical scenario to target length using mean reversion.
"""
current_months = len(historical_data)
if current_months >= target_months:
return historical_data.iloc[:target_months]
# Mean reversion parameters
long_run_means = {
'gdp_growth_yoy': 0.025,
'unemployment_rate': 0.045,
'inflation_yoy': 0.02,
# ...
}
reversion_speed = 0.1
# Extend each variable
extended = historical_data.copy()
last_values = historical_data.iloc[-1]
for month in range(current_months, target_months):
new_row = {}
for col in extended.columns:
if col in long_run_means:
prev = extended.iloc[-1][col]
mean = long_run_means[col]
# AR(1) with mean reversion
new_row[col] = prev + reversion_speed * (mean - prev) + np.random.normal(0, 0.002)
else:
new_row[col] = extended.iloc[-1][col]
new_date = extended.index[-1] + pd.DateOffset(months=1)
extended.loc[new_date] = new_row
return extended
6. Scenario Validation
Check Correlations
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def validate_correlations(scenario):
"""
Validate that macro variables have reasonable correlations.
"""
expected = {
('gdp_growth_yoy', 'unemployment_rate'): (-0.8, -0.4),
('gdp_growth_yoy', 'credit_spread_hy'): (-0.7, -0.2),
('inflation_yoy', 'fed_funds_rate'): (0.3, 0.8),
}
print("Correlation Validation:")
for (var1, var2), (low, high) in expected.items():
if var1 in scenario.columns and var2 in scenario.columns:
corr = scenario[var1].corr(scenario[var2])
status = "PASS" if low <= corr <= high else "WARN"
print(f" {var1} vs {var2}: {corr:.3f} [{status}]")
validate_correlations(custom)
Check Economic Plausibility
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def check_plausibility(scenario):
"""
Check if scenario values are economically plausible.
"""
checks = [
('unemployment_rate', 0.02, 0.15, 'Unemployment'),
('inflation_yoy', -0.02, 0.10, 'Inflation'),
('credit_spread_hy', 200, 2000, 'HY Spread'),
]
print("\nPlausibility Checks:")
for col, min_val, max_val, name in checks:
if col in scenario.columns:
actual_min = scenario[col].min()
actual_max = scenario[col].max()
if actual_min < min_val or actual_max > max_val:
print(f" {name}: [{actual_min:.3f}, {actual_max:.3f}] - WARNING")
else:
print(f" {name}: [{actual_min:.3f}, {actual_max:.3f}] - OK")
check_plausibility(custom)
7. Visualization
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def plot_scenario_comparison(scenarios, names, variables=None):
"""
Plot multiple scenarios side by side.
"""
if variables is None:
variables = ['gdp_growth_yoy', 'unemployment_rate',
'inflation_yoy', 'credit_spread_hy']
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
for ax, var in zip(axes.flat, variables):
for scenario, name in zip(scenarios, names):
ax.plot(scenario[var], label=name)
ax.set_title(var.replace('_', ' ').title())
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
plot_scenario_comparison(
[baseline, custom, adverse],
['Baseline', 'Custom', 'Adverse']
)
Summary
| Method | Use Case | Complexity |
|---|---|---|
| Parameter Override | Quick variations | Low |
| Manual DataFrame | Full control | Medium |
| Templates | Reusable patterns | Medium |
| Interpolation | Scenario gradations | Low |
| Historical Replay | Backtesting | High |
Next: Calibration Guide