Skip to main content

cdfi-stress-tester

PyPI version Python 3.9+ License: MIT

Professional CDFI portfolio stress testing engine for Python.

Monte Carlo simulation with correlated shocks to NOI, interest rates, and property values. Compute VaR, CVaR, and capital adequacy across 2008-style recession, COVID, rate-spike, and regional CRE crash scenarios — all from pure numpy, no simulation libraries required.


Why cdfi-stress-tester?

CDFI Fund regulations and investor due diligence increasingly require stress-tested capital adequacy projections. Building Monte Carlo models in Excel is brittle and hard to audit. cdfi-stress-tester provides a reproducible, testable Python engine tuned to community development finance portfolio characteristics.

Installation

pip install cdfi-stress-tester

Requires: numpy (Python 3.9+). No other runtime dependency.

Quickstart

The snippet below and the output under it are generated together by scripts/render_readme_block.py, which executes the snippet and pastes back what it actually printed. CI re-renders and byte-diffs this region, so the numbers on this page cannot drift away from what the code does.

These figures are reproducible, not stable by contract: they come from numpy.random.Generator, which carries no bit-stream compatibility guarantee across numpy releases. They were verified identical on numpy 1.26.4 and 2.2.6. If the byte-diff goes red after a numpy upgrade with no source change, the stream moved — see Known limitations item 5.

from cdfistress import (
    generate_sample_portfolio,
    from_standard,
    MonteCarloEngine,
    generate_stress_report,
    scenario_comparison_table,
    capital_adequacy_report,
)

# A reproducible 50-loan CDFI portfolio
loans = generate_sample_portfolio(n=50, seed=42)

# Monte Carlo engine with $5MM of loss-absorbing capital
engine = MonteCarloEngine(loans=loans, available_capital=5_000_000)

# Run a 2008-style recession
result = engine.run_simulation(from_standard("2008_recession"), n_iterations=1000, seed=42)
print(generate_stress_report(result))

# Capital adequacy against the loss distribution that run produced
print()
for key, value in capital_adequacy_report(5_000_000, engine.loss_distribution).items():
    print(f"  {key:<18} {value:>18,.4f}")

# Inspect a scenario's parameters
print()
print(from_standard("rate_spike").summary())

# Compare several scenarios side by side
scenarios = [from_standard(k) for k in ["mild_downturn", "rate_spike", "2008_recession"]]
results = [engine.run_simulation(s, n_iterations=500, seed=0) for s in scenarios]
print()
print(scenario_comparison_table(results))

Output (verbatim -- this block is generated by executing the snippet above):

============================================================
CDFI PORTFOLIO STRESS TEST REPORT
Scenario: 2008-Style Recession  [SEVERE]
============================================================

SCENARIO PARAMETERS
  NOI shock            : -35.0%
  Rate shock           : +0 bps
  Property value shock : -40.0%
  Default mult         : 4.0x baseline

LOSS METRICS
  Expected Loss        : $      8,843,297
  VaR (95%)            : $     15,033,139
  VaR (99%)            : $     17,396,438

CAPITAL ADEQUACY
  CAR vs Expected Loss : 0.57x
  Simulation breaches  : 858 / 1000 (85.8%)

STATUS: INSUFFICIENT
============================================================

  available_capital      5,000,000.0000
  expected_loss          8,843,296.9130
  var_95                15,033,139.3438
  var_99                17,396,437.8562
  cvar_99               20,104,925.2590
  car_vs_el                      0.5654
  car_vs_var99                   0.2874
  car_vs_cvar99                  0.2487
  breaches                     858.0000
  breach_rate                    0.8580

Scenario: Rate Spike (+300 bps) [moderate]
  NOI shock        : -5.0%
  Rate shock       : +300 bps
  Property shock   : -20.0%
  Default mult     : 1.8x

Scenario                       Severity               EL          VaR95          VaR99     CAR   Breaches
---------------------------------------------------------------------------------------------------------
Mild Cyclical Downturn         mild       $    2,476,091 $    6,029,387 $    7,761,442   2.02x      52/500
Rate Spike (+300 bps)          moderate   $    3,122,681 $    7,171,131 $    8,733,905   1.60x      94/500
2008-Style Recession           severe     $    8,769,697 $   15,572,653 $   19,507,034   0.57x     429/500

Key Features

Feature Detail
Monte Carlo engine MonteCarloEngine with seed-controlled reproducibility
Correlated shocks Multivariate normal draws for NOI, rate, and property value (see Known limitations before relying on this)
Standard scenarios 2008 recession, COVID, rate spike (+300 bps), regional CRE crash, mild downturn
Custom scenarios create_recession_scenario(), create_rate_shock_scenario()
VaR / CVaR 95th and 99th percentile VaR; Expected Shortfall (CVaR)
Capital adequacy CAR vs EL, VaR, and CVaR; breach count and breach rate
Tier 1 under stress Post-stress Tier 1 ratio given risk-weighted assets
Sample portfolio Reproducible 50-loan CDFI portfolio with realistic parameters
LGD modeling Per-loan LGD with collateral shortfall and 30% foreclosure-cost floor
Reports generate_stress_report() and scenario_comparison_table()

Scenarios

STANDARD_SCENARIOS = {
    "2008_recession":    {"noi_shock": -0.35, "rate_shock": 0.00, "property_value_shock": -0.40, "default_rate_multiplier": 4.0},
    "covid_shock":       {"noi_shock": -0.25, "rate_shock": -0.01, "property_value_shock": -0.15, "default_rate_multiplier": 2.5},
    "rate_spike":        {"noi_shock": -0.05, "rate_shock": +0.03, "property_value_shock": -0.20, "default_rate_multiplier": 1.8},
    "regional_cre_crash":{"noi_shock": -0.20, "rate_shock": +0.01, "property_value_shock": -0.50, "default_rate_multiplier": 3.0},
    "mild_downturn":     {"noi_shock": -0.10, "rate_shock": +0.005,"property_value_shock": -0.08, "default_rate_multiplier": 1.4},
}

Use Cases

  • CDFI Fund reporting — Demonstrate capital adequacy under regulatory stress scenarios.
  • Rating agency submissions — Produce reproducible loss distributions for credit analysis.
  • Board risk committees — Run scenario comparison tables for governance reporting.
  • Investors / lenders — Stress-test CDFI portfolios during due diligence.
  • Policy researchers — Analyze CDFI portfolio resilience under macro stress. (Shocks are portfolio-wide; the engine does not target a single sector or geography — see Known limitations.)

API Reference

# Data
generate_sample_portfolio(n=50, seed=42)   # 50-loan CDFI portfolio
Loan(loan_id, borrower_name, outstanding_balance, noi, debt_service,
     property_value, interest_rate, sector, state, ltv)
  .dscr                    # noi / debt_service
  .is_stressed             # dscr < 1.0

StressScenario(name, noi_shock, rate_shock, property_value_shock,
               default_rate_multiplier, severity)
  .summary()

StressResult(scenario, expected_loss, var_95, var_99,
             capital_adequacy_ratio, num_breaches, total_simulations)
  .summary()

STANDARD_SCENARIOS       # dict of the five prebuilt scenarios
CORRELATION_DEFAULTS     # default pairwise risk-factor correlations
SECTOR_DEFAULT_RATES     # house baseline annual default rate per sector

# Scenarios
from_standard("2008_recession")
create_recession_scenario(noi_shock, rate_shock, property_value_shock,
                          default_rate_multiplier, name, severity)
create_rate_shock_scenario(rate_shock, property_value_shock)
apply_shock_to_loan(loan, scenario)        # returns stressed Loan

# Simulation
MonteCarloEngine(loans, available_capital, correlation_matrix)
  .run_simulation(scenario, n_iterations=1000, seed=None)  # → StressResult
  .apply_correlated_shocks(scenario, n_iterations, seed)   # → ndarray (n, 3)
  .default_probabilities(scenario)                          # → {loan_id: pd}
  .loss_distribution                                        # → ndarray, last run's losses

# VaR / Capital
value_at_risk(losses, confidence=0.95)
conditional_var(losses, confidence=0.95)
expected_loss(losses)
tail_loss(losses, pct=0.01)
capital_adequacy(available_capital, expected_loss_amount)
tier1_under_stress(tier1_capital, losses, risk_weighted_assets, confidence=0.99)
buffer_breach_count(losses, capital_buffer)
capital_adequacy_report(available_capital, losses)

# Correlations
build_correlation_matrix(noi_rate, noi_property, rate_property)
default_correlations()
is_positive_semidefinite(matrix)
sector_correlation_boost(sector)

# Reports
generate_stress_report(result)         # → str
scenario_comparison_table(results)     # → str

Known limitations

These are measured against the 0.2.0 code, not inferred. Read them before putting output into a regulatory, board, or investor submission.

  1. Scenario shocks are portfolio-wide — there is no sector or geographic targeting. A scenario's noi_shock, rate_shock, property_value_shock and default_rate_multiplier are scalars with no per-segment override, and run_simulation applies them to every loan. 0.1.0 shipped create_sector_specific_scenario(sector, ...), but sector only built a display string: two different sectors produced byte-identical loss distributions. The function was removed in 0.2.0 rather than dressed up with invented per-sector calibrations. Do not name a scenario in a way that implies it is confined to one segment.

    What is missing is calibration, not mechanism. The engine already resolves each loan's sector into a per-loan baseline default rate via SECTOR_DEFAULT_RATES, and that per-loan array is what the simulation scales — so a single run already carries sector-differentiated default probabilities. Segment-targeted stress would scale that existing per-loan array; it does not require an engine rewrite. What does not exist is a primary source for how much harder a retail book should be shocked than a multifamily one, so the per-sector factors would be invented. If you want this capability, scope a calibration source, not an engine.

  2. Only the NOI draw affects losses. Each Monte Carlo path draws a correlated triple (NOI, rate, property value), but only the NOI draw is consumed — it scales each loan's default probability. The rate draw is discarded. The property-value shock enters LGD only at its scenario mean, computed once outside the path loop, so it does not vary by path.

  3. Consequently correlation_matrix does not change the loss distribution. Averaged over 12 seeds at 4,000 iterations each on the sample portfolio under 2008_recession, mean expected loss was $8,807,537 with the default correlations, $8,804,655 with the identity matrix, and $8,814,075 with all pairwise correlations at +0.99 — a spread well inside the ~$54,000 across-seed standard deviation. Passing a different matrix reshuffles the NOI sample; it does not change the distribution being sampled.

  4. The calibrations are house assumptions. SECTOR_DEFAULT_RATES, the factor volatilities (NOI 15%, rate 0.5%, property value 20%), and the 30% LGD floor are the author's working assumptions, not published or regulator-endorsed calibrations. Substitute your own before relying on absolute loss levels.

  5. The committed figures are reproducible, not guaranteed stable across numpy releases. Every number on this page is generated by executing the code, and CI byte-diffs it. But the draws come from numpy.random.Generator, whose documentation states "No Compatibility Guarantee … the bit stream may change", and they route through LAPACK gesdd, whose singular-vector signs are not a standardised convention across builds. The figures here were verified identical on numpy 1.26.4 and 2.2.6; the declared floor is numpy>=1.22. If a golden gate goes red after a numpy or BLAS/LAPACK upgrade with no source change, the stream moved. Re-rendering alone is not the fix: re-render and update every document that hand-copies these figures — this section and CHANGELOG.md — in the same commit. tests/test_documented_figures.py gates those hand-copied duplicates so the two cannot silently disagree, which is precisely the 0.1.0 defect.

Items 2 and 3 describe existing engine behaviour. 0.2.0 documents them; it does not change the simulation math.

Changelog

See CHANGELOG.md. 0.2.0 corrects a 0.1.0 README that reported loss figures the engine never produced and reversed its own capital-adequacy verdict; the simulation math was not changed.

License

MIT © Jay Patel

Metadata

Release files for cdfi-stress-tester 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cdfi-stress-tester 0.2.0
File Size Uploaded
cdfi_stress_tester-0.2.0.tar.gz 76.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for cdfi-stress-tester 0.2.0
File Interpreter ABI Platform
cdfi_stress_tester-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 98.4 kB

Release files / cdfi_stress_tester-0.2.0.tar.gz

Download URL cdfi_stress_tester-0.2.0.tar.gz
Size 76.6 kB
Tags Source
SHA-256 checksum
How to use checksums
a007ff4f2c9408b20d650e4a29449dae11cfd5c80bc77b268499fc217a989a62
BLAKE2b-256 checksum
How to use checksums
ea486a81f5c1a2c3439b4dd18d1935a149d2fbb17cd0727bba8d5ead31e22d47
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release files / cdfi_stress_tester-0.2.0-py3-none-any.whl

Download URL cdfi_stress_tester-0.2.0-py3-none-any.whl
Size 21.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
80fbef6279f77e5467b7428b44ad668f0c416c514cbccfe0fc4705bc02a8addc
BLAKE2b-256 checksum
How to use checksums
6f48b364fb41390b37438982770e5e3a09eb23270cc7841dbe97bbac09ef680f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page