Metadata-Version: 2.4
Name: cdfi-stress-tester
Version: 0.2.0
Summary: CDFI portfolio stress testing engine — Monte Carlo simulation with correlated NOI, rate, and property-value shocks
Home-page: https://github.com/Jaypatel1511/cdfi-stress-tester
Author: Jay Patel
Author-email: Jay Patel <thejaypatel1511@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/Jaypatel1511/cdfi-stress-tester
Project-URL: Repository, https://github.com/Jaypatel1511/cdfi-stress-tester
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business :: Financial
Classifier: Topic :: Scientific/Engineering :: Mathematics
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.22
Dynamic: author
Dynamic: home-page
Dynamic: license-file
Dynamic: requires-python

# cdfi-stress-tester

[![PyPI version](https://badge.fury.io/py/cdfi-stress-tester.svg)](https://badge.fury.io/py/cdfi-stress-tester)
[![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/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

```bash
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](#known-limitations) item 5.

<!-- BEGIN GENERATED: quickstart (scripts/render_readme_block.py) -->
<!-- Do not edit by hand.  Regenerate: python scripts/render_readme_block.py -->

```python
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):

```text
============================================================
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
```

<!-- END GENERATED: quickstart -->

## 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](#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

```python
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](#known-limitations).)

## API Reference

```python
# 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](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
