Early accessSome features may be unavailable
Back to Blog
stress testrecessionmacroscenario

Stress Testing with Causal DAGs: A 2008 Recession Replay

Walk through VynFi's recession_2008_replay scenario pack step by step: the causal DAG structure, calibrated macro parameters, intervention sequence, and how to compare baseline vs. stressed portfolios in Python.

VynFi Team · EngineeringApril 17, 20269 min read

Regulatory stress testing (CCAR, DFAST, EBA) requires banks to project losses under hypothetical adverse scenarios. Building these scenarios internally is expensive: risk teams spend weeks calibrating macro shocks, mapping transmission channels, and validating that the resulting portfolio impact is internally consistent. VynFi 3.0's <code>recession_2008_replay</code> scenario pack encapsulates the causal structure of the 2007-2009 financial crisis into a reusable, parameterizable simulation that generates stressed portfolio data in minutes.

**DataSynth 3.1 update:** Pair `recession_2008_replay` with the new `financial_services` sector preset (`scenarios.causal_model.preset: "financial_services"`) for correspondent-banking, regulatory-pressure, KYC, AML-screening, and NPL-ratio transmission in the same DAG. Custom node/edge topologies are now fully supported (were silently ignored in 3.0). Generate scheme-level fraud concurrently via `fraud.documentFraudRate` so the baseline-vs-stressed diff captures both macro shocks and control failure cascades.

The Causal DAG

The recession pack defines a 14-node causal DAG that models the transmission from macro shocks to portfolio losses. The top-level exogenous nodes are <code>housing_price_index</code>, <code>fed_funds_rate</code>, and <code>equity_market_return</code>. These propagate through intermediate nodes — <code>mortgage_default_rate</code>, <code>corporate_spread</code>, <code>unemployment</code>, <code>consumer_confidence</code> — down to portfolio-level outcomes: <code>loan_loss_provision</code>, <code>trading_pnl</code>, <code>capital_adequacy_ratio</code>, and <code>liquidity_coverage_ratio</code>.

Each edge in the DAG has a structural equation calibrated against historical data from the 2007-2009 period. The housing price decline of 27% peak-to-trough, the fed funds rate cut from 5.25% to near zero, and the S&P 500 drawdown of 57% are encoded as the default intervention parameters. You can adjust any of these to explore alternative histories.

Running the Scenario Pack

Python
import vynfi
client = vynfi.VynFi()
# Inspect the scenario pack
pack = client.simulation.get_scenario_pack("recession_2008_replay")
print(f"DAG nodes: {len(pack.dag.nodes)}")
print(f"Named interventions: {[i.name for i in pack.interventions]}")
# ['peak_to_trough', 'early_phase', 'recovery_phase', 'severe_variant']
# Run with default parameters (full peak-to-trough scenario)
job = client.jobs.create(
mode="simulate",
scenario_pack="recession_2008_replay",
baseline={
"sector": "banking",
"rows": 100_000,
"periods": 12,
"companies": 20,
},
interventions=["peak_to_trough", "severe_variant"],
paired=True,
)
result = client.jobs.wait(job.id)
archive = client.jobs.download_archive(result.id)

Comparing Baseline and Stressed Portfolios

Python
import pandas as pd
baseline = pd.read_parquet(archive.file("baseline.parquet"))
stressed = pd.read_parquet(archive.file("peak_to_trough.parquet"))
severe = pd.read_parquet(archive.file("severe_variant.parquet"))
# Portfolio-level impact summary
for name, df in [("Baseline", baseline), ("Peak-to-Trough", stressed), ("Severe", severe)]:
print(f"\n--- {name} ---")
print(f" Mean default rate: {df['default_rate'].mean():.4f}")
print(f" Mean loss provision: ${df['loan_loss_provision'].mean():,.0f}")
print(f" Capital adequacy ratio: {df['capital_adequacy_ratio'].mean():.4f}")
print(f" Liquidity coverage: {df['liquidity_coverage_ratio'].mean():.4f}")
# Period-over-period trajectory
trajectory = stressed.groupby("period").agg({
"default_rate": "mean",
"loan_loss_provision": "sum",
"capital_adequacy_ratio": "mean",
}).reset_index()
print("\nStressed trajectory by period:")
print(trajectory.to_string(index=False))

Customizing the Scenario

The pack's default parameters replicate the historical crisis, but you can override any intervention value. This is useful for exploring alternative scenarios: what if the housing decline was 15% instead of 27%? What if the Fed had cut rates six months earlier? What if equity markets had recovered faster?

Python
# Custom intervention: milder housing decline, faster rate cuts
job_custom = client.jobs.create(
mode="simulate",
scenario_pack="recession_2008_replay",
baseline={"sector": "banking", "rows": 50_000, "periods": 12},
interventions=[
{
"name": "milder_scenario",
"do": {
"housing_price_index": -0.15, # -15% instead of -27%
"fed_funds_rate": 0.005, # cut to 0.5% instead of 0.25%
"equity_market_return": -0.30, # -30% instead of -57%
},
},
],
paired=True,
)

The paired output lets you compute the marginal impact of each parameter change. By running the full scenario and the milder variant against the same baseline with identical noise terms, you can attribute exactly how much of the loss reduction comes from the smaller housing decline versus the faster rate cuts. This kind of decomposition analysis is invaluable for regulatory submissions where examiners ask why your stress loss estimate differs from the supervisory scenario.

Ready to try VynFi?

Start generating synthetic financial data with 10,000 free credits. No credit card required.