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.
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
import vynficlient = vynfi.VynFi()# Inspect the scenario packpack = 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
import pandas as pdbaseline = 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 summaryfor 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 trajectorytrajectory = 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?
# Custom intervention: milder housing decline, faster rate cutsjob_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.