Understanding Test Suites & Thresholds¶
A test suite is a collection of compliance tests that run together to evaluate a CLO portfolio against its indenture requirements. CalcBridge provides pre-built test suites for common CLO structures and allows full customization for deal-specific requirements.
What is a Test Suite?¶
A test suite groups related compliance tests into a single executable unit. Each suite contains:
- Test definitions - The specific compliance tests to run
- Threshold configurations - Pass/fail boundaries for each test
- Warning levels - Early warning thresholds (typically 90% of limit)
- Execution order - Dependencies between tests
- Reporting template - How results are formatted
flowchart TB
subgraph Suite["Test Suite: Standard CLO"]
direction TB
A[Concentration Tests]
B[Rating Tests]
C[Coverage Tests]
D[Quality Tests]
E[Diversity Tests]
end
A --> F[Results]
B --> F
C --> F
D --> F
E --> F
style Suite fill:#EFF6FF,stroke:#3B82F6
style F fill:#DCFCE7,stroke:#22C55E Test Categories¶
CalcBridge organizes compliance tests into five categories, each addressing different aspects of portfolio risk:
1. Concentration Tests¶
Concentration tests ensure the portfolio is not overly exposed to any single obligor, industry, or geographic region.
| Test Name | Description | Typical Limit |
|---|---|---|
| Single Obligor | Maximum exposure to any one borrower | 2-10% of par |
| Top 5 Obligors | Combined exposure to largest 5 borrowers | 20-40% of par |
| Top 10 Obligors | Combined exposure to largest 10 borrowers | 40-60% of par |
| Single Industry | Maximum exposure to any one industry | 10-15% of par |
| Single Country | Maximum exposure to any one country | 15-25% of par |
| Second Lien | Maximum second lien loan exposure | 5-15% of par |
How Single Obligor Limits Work
The single obligor limit is calculated as:
Obligor Concentration = (Obligor Par Value / Total Portfolio Par) * 100
If any obligor exceeds the limit, the test fails. CalcBridge evaluates every obligor in the portfolio and reports the highest concentration.
2. Rating Tests¶
Rating tests monitor the credit quality distribution of the portfolio.
| Test Name | Description | Typical Limit |
|---|---|---|
| CCC/Caa Bucket | Loans rated CCC+ or below | 5-7.5% of par |
| WARF | Weighted Average Rating Factor | 2500-3000 |
| Minimum Rating | Percentage meeting minimum rating | 90-95% of par |
| Split Ratings | Loans with divergent agency ratings | 10-15% of par |
Rating Agency Mappings:
CalcBridge supports all major rating agencies with automatic cross-mapping:
| Moody's | S&P | Fitch | Numeric Score |
|---|---|---|---|
| Aaa | AAA | AAA | 1 |
| Aa1 | AA+ | AA+ | 2 |
| Aa2 | AA | AA | 3 |
| ... | ... | ... | ... |
| B3 | B- | B- | 16 |
| Caa1 | CCC+ | CCC+ | 17 |
| Caa2 | CCC | CCC | 18 |
| Caa3 | CCC- | CCC- | 19 |
CCC Threshold
Loans rated Caa1/CCC+ (numeric score 17) or worse are considered "CCC bucket" loans. The composite rating uses the worst rating from available agencies.
Split Rating Handling¶
A split rating occurs when two or more rating agencies assign different credit ratings to the same loan. For example, a loan might be rated B1 by Moody's but BB+ by S&P -- a meaningful divergence that affects which compliance bucket the loan falls into.
Indentures specify how to resolve split ratings. CalcBridge supports all common methods via the split_rating_method parameter in the test suite configuration.
Resolution Methods¶
| Method | Description | Example (Moody's B1, S&P BB+) | Used In |
|---|---|---|---|
| Lower of Two | Uses the worse (lower quality) rating | B1 (lower quality) | Most US CLOs |
| Higher of Two | Uses the better (higher quality) rating | BB+ (higher quality) | Some European CLOs |
| Worst of All | Uses the worst rating across all available agencies | Worst across all available | Conservative indentures |
| Specified Agency | Uses only one agency's rating regardless of others | As specified by indenture | Agency-specific deals |
Why Split Ratings Matter
Split ratings are not cosmetic. In the example above, B1 (Moody's) has a rating factor of 2,220 while BB+ (S&P equivalent Ba1) has a factor of 940. Using the wrong resolution method can swing a WARF calculation by hundreds of points and flip a test from pass to fail. See Rating Agency Methodologies for full factor tables.
Configuration¶
Set the split rating resolution method in the test suite threshold configuration:
{
"rating": {
"split_rating_method": "lower_of_two",
"ccc_bucket_limit_pct": 7.5,
"maximum_warf": 2850,
"rating_agencies": ["moodys", "sp"],
"warf_rating_source": "moodys"
}
}
Implementation¶
import numpy as np
import pandas as pd
# Numeric scale: lower number = better credit quality
NUMERIC_SCALE = {
"Aaa": 1, "AAA": 1,
"Aa1": 2, "AA+": 2, "Aa2": 3, "AA": 3, "Aa3": 4, "AA-": 4,
"A1": 5, "A+": 5, "A2": 6, "A": 6, "A3": 7, "A-": 7,
"Baa1": 8, "BBB+": 8, "Baa2": 9, "BBB": 9, "Baa3": 10, "BBB-": 10,
"Ba1": 11, "BB+": 11, "Ba2": 12, "BB": 12, "Ba3": 13, "BB-": 13,
"B1": 14, "B+": 14, "B2": 15, "B": 15, "B3": 16, "B-": 16,
"Caa1": 17, "CCC+": 17, "Caa2": 18, "CCC": 18, "Caa3": 19, "CCC-": 19,
"Ca": 20, "CC": 20, "C": 21, "D": 21,
}
def resolve_split_rating(
df: pd.DataFrame,
method: str = "lower_of_two",
specified_agency: str | None = None,
) -> pd.Series:
"""
Resolve split ratings to a single composite rating per loan.
Args:
df: DataFrame with 'rating_moodys' and 'rating_sp' columns.
method: One of 'lower_of_two', 'higher_of_two',
'worst_of_all', or 'specified_agency'.
specified_agency: Required when method is 'specified_agency'.
One of 'moodys' or 'sp'.
Returns:
Series of resolved ratings (using Moody's notation as the
canonical output scale).
"""
moodys_numeric = df["rating_moodys"].map(NUMERIC_SCALE)
sp_numeric = df["rating_sp"].map(NUMERIC_SCALE)
# Build reverse lookup: numeric -> Moody's rating
numeric_to_moodys = {v: k for k, v in NUMERIC_SCALE.items() if len(k) <= 4}
if method == "lower_of_two":
# Higher numeric = worse credit quality = lower rating
composite_numeric = np.maximum(
moodys_numeric.fillna(sp_numeric),
sp_numeric.fillna(moodys_numeric),
)
elif method == "higher_of_two":
composite_numeric = np.minimum(
moodys_numeric.fillna(sp_numeric),
sp_numeric.fillna(moodys_numeric),
)
elif method == "worst_of_all":
# Same as lower_of_two for two agencies; extensible to Fitch
composite_numeric = np.maximum(
moodys_numeric.fillna(sp_numeric),
sp_numeric.fillna(moodys_numeric),
)
elif method == "specified_agency":
if specified_agency == "moodys":
return df["rating_moodys"]
elif specified_agency == "sp":
return df["rating_sp"]
else:
raise ValueError(f"Unknown agency: {specified_agency}")
else:
raise ValueError(f"Unknown split rating method: {method}")
return composite_numeric.map(numeric_to_moodys)
Impact on Other Tests¶
The resolved composite rating feeds into multiple downstream tests:
flowchart LR
A[Moody's<br/>Rating] --> C[Split Rating<br/>Resolution]
B[S&P<br/>Rating] --> C
C --> D[Composite<br/>Rating]
D --> E[WARF<br/>Calculation]
D --> F[CCC Bucket<br/>Test]
D --> G[Minimum Rating<br/>Test]
D --> H[Diversity Score<br/>Calculation]
style C fill:#FEF3C7,stroke:#F59E0B
style D fill:#EFF6FF,stroke:#3B82F6 Split Rating Resolution Must Happen Before All Rating Tests
The composite rating must be computed once at the start of the compliance test run and reused across all rating-dependent tests. Do not resolve split ratings independently in each test -- this wastes compute and risks inconsistency if the resolution logic is implemented differently in different test functions.
3. Coverage Tests¶
Coverage tests verify that the portfolio provides adequate protection for debt holders.
| Test Name | Description | Typical Minimum |
|---|---|---|
| Senior OC Ratio | Senior overcollateralization | 115-125% |
| Mezzanine OC Ratio | Mezzanine overcollateralization | 105-115% |
| Junior OC Ratio | Junior overcollateralization | 100-110% |
| Senior IC Ratio | Senior interest coverage | 120-150% |
| Mezzanine IC Ratio | Mezzanine interest coverage | 110-130% |
OC Ratio Calculation:
OC Ratio = (Collateral Principal Balance / Tranche Principal Balance) * 100
Example:
- Collateral Balance: $500,000,000
- Senior Tranche: $400,000,000
- Senior OC Ratio: ($500M / $400M) * 100 = 125%
4. Quality Tests¶
Quality tests assess the overall characteristics of the loan portfolio.
| Test Name | Description | Typical Range |
|---|---|---|
| WAS | Weighted Average Spread | Minimum 3.0-4.0% |
| WAL | Weighted Average Life | Maximum 4.0-6.0 years |
| WARF | Weighted Average Rating Factor | Maximum 2500-3000 |
| Minimum Coupon | Weighted average coupon | Minimum 4.0-5.0% |
WAS Calculation:
# Weighted Average Spread
WAS = Sum(Loan Par * Loan Spread) / Sum(Loan Par)
# Example
Portfolio:
- Loan A: $10M par, 3.50% spread
- Loan B: $15M par, 4.00% spread
- Loan C: $25M par, 3.75% spread
WAS = ($10M * 3.50% + $15M * 4.00% + $25M * 3.75%) / $50M
WAS = ($350K + $600K + $937.5K) / $50M
WAS = 3.775%
5. Diversity Tests¶
Diversity tests measure how well the portfolio is diversified across obligors and industries.
| Test Name | Description | Typical Minimum |
|---|---|---|
| Moody's Diversity Score | Effective number of independent obligors | 40-60 |
| Industry Diversity | Number of distinct industries | 15-20 |
| Obligor Count | Minimum number of obligors | 100-150 |
Diversity Score Calculation:
The Moody's diversity score approximates the effective number of independent obligors using the Herfindahl-Hirschman Index (HHI):
# Simplified diversity score calculation
HHI = Sum(Obligor Weight ^ 2)
Diversity Score = 1 / HHI
# Example
Portfolio with 4 obligors:
- Obligor A: 40% weight -> 0.16
- Obligor B: 30% weight -> 0.09
- Obligor C: 20% weight -> 0.04
- Obligor D: 10% weight -> 0.01
HHI = 0.16 + 0.09 + 0.04 + 0.01 = 0.30
Diversity Score = 1 / 0.30 = 3.33
Full Moody's Calculation
The actual Moody's diversity score calculation includes industry correlation adjustments. CalcBridge implements the complete methodology when industry data is available.
Threshold Configuration¶
Setting Threshold Values¶
Thresholds are configured per test suite and can be customized for each deal:
{
"suite_name": "Standard CLO v2",
"description": "Standard compliance tests for US BSL CLOs",
"thresholds": {
"concentration": {
"single_obligor_limit_pct": 10.0,
"top_5_obligor_limit_pct": 40.0,
"single_industry_limit_pct": 15.0,
"second_lien_limit_pct": 10.0
},
"rating": {
"ccc_bucket_limit_pct": 7.5,
"maximum_warf": 2850
},
"coverage": {
"minimum_senior_oc_ratio": 120.0,
"minimum_mezzanine_oc_ratio": 108.0,
"minimum_senior_ic_ratio": 150.0
},
"quality": {
"minimum_was": 3.25,
"maximum_wal": 5.5
},
"diversity": {
"minimum_diversity_score": 45.0,
"minimum_obligor_count": 120
}
},
"warning_threshold_pct": 90.0
}
Warning Levels¶
CalcBridge uses a three-tier status system:
| Status | Description | Default Trigger |
|---|---|---|
| Pass | Test is within acceptable limits | < 90% of threshold |
| Warning | Test is approaching threshold | 90-100% of threshold |
| Fail | Test has breached threshold | > 100% of threshold |
Customizing Warning Levels
The default warning threshold is 90% of the limit. For critical tests, you may want to set this lower (e.g., 80%) to get earlier warnings.
Test Direction¶
Some tests have minimum thresholds (higher is better), while others have maximum thresholds (lower is better):
| Test Type | Direction | Pass Condition |
|---|---|---|
| Concentration | Maximum | Current < Threshold |
| CCC Bucket | Maximum | Current < Threshold |
| OC Ratio | Minimum | Current > Threshold |
| IC Ratio | Minimum | Current > Threshold |
| WAS | Minimum | Current > Threshold |
| WAL | Maximum | Current < Threshold |
| Diversity | Minimum | Current > Threshold |
Pre-Built Test Suites¶
CalcBridge includes several pre-built test suites:
Standard CLO Suite¶
The most commonly used suite for US Broadly Syndicated Loan (BSL) CLOs:
Suite: Standard CLO
Tests: 24
Categories:
- Concentration (8 tests)
- Rating (4 tests)
- Coverage (6 tests)
- Quality (3 tests)
- Diversity (3 tests)
Middle Market CLO Suite¶
Adjusted thresholds for middle market loan portfolios:
Suite: Middle Market CLO
Tests: 20
Key Differences:
- Higher single obligor limits (15% vs 10%)
- Lower diversity requirements (30 vs 45)
- Higher CCC bucket tolerance (10% vs 7.5%)
European CLO Suite¶
Compliance tests for European CLO structures:
Suite: European CLO
Tests: 22
Key Differences:
- Currency-adjusted calculations
- EU regulatory requirements
- Retention rule compliance
Creating Custom Test Suites¶
Via API¶
curl -X POST https://api.calcbridge.io/api/v1/compliance/suites \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Custom Deal Suite",
"description": "Custom thresholds for Deal ABC 2024-1",
"base_suite": "standard_clo",
"threshold_overrides": {
"concentration": {
"single_obligor_limit_pct": 8.0
},
"rating": {
"ccc_bucket_limit_pct": 5.0
}
},
"enabled_tests": [
"single_obligor",
"top_5_obligor",
"ccc_bucket",
"senior_oc_ratio",
"was",
"wal",
"diversity_score"
]
}'
Via UI¶
- Navigate to Settings > Compliance > Test Suites
- Click Create New Suite
- Select a base suite to start from
- Adjust thresholds as needed
- Enable/disable individual tests
- Save and assign to workbooks
Best Practices¶
Threshold Management Best Practices
-
Start from indenture documents - Always verify thresholds against the actual deal documents
-
Version control suites - Create new suite versions rather than modifying existing ones
-
Document changes - Record why thresholds were adjusted
-
Test conservatively - Set warning levels earlier rather than later
-
Review quarterly - Audit suite configurations during quarterly reporting
Example: Typical CLO Compliance Tests¶
Here is a complete example of a typical CLO test suite configuration:
# ComplianceCalculator default thresholds
STANDARD_CLO_THRESHOLDS = {
# Concentration Limits
"single_obligor_limit_pct": Decimal("10.0"),
"top_5_obligor_limit_pct": Decimal("40.0"),
"single_industry_limit_pct": Decimal("15.0"),
# Rating Limits
"ccc_bucket_limit_pct": Decimal("7.5"),
# Coverage Minimums
"minimum_oc_ratio": Decimal("120.0"),
"minimum_ic_ratio": Decimal("150.0"),
# Quality Metrics
"minimum_was": Decimal("3.0"),
"maximum_wal": Decimal("5.0"),
# Diversity Minimums
"minimum_diversity_score": Decimal("40.0"),
}
Next: Running Compliance Tests