CLO Waterfall Model — absbox Implementation Plan
0%
Python · absbox · Hastructure · Polars · Quarto · great_tables

CLO Waterfall Model
Implementation Plan

A step-by-step plan for building a full cashflow waterfall projection of a $600m US broadly syndicated CLO using the absbox Python library, the Hastructure Haskell engine via Docker, and Polars DataFrames for results processing — delivered as a Quarto document that renders to a styled HTML report with great_tables output tables.
$600m
Deal Size
250
Loans in Pool
7
Tranches
1
Scenario
8
Coding Steps

Overview

We are building a Quarto document that models the full lifecycle cashflow waterfall of a realistic US broadly syndicated CLO — from loan pool collections through to tranche-by-tranche distributions and equity residual. The model runs a single base case scenario and produces Polars DataFrames for each tranche's cashflows, the OC/IC test timeline, and standard analytics (WAL, IRR, discount margin). All assumptions are cleanly separated so that stress scenarios can be added later by simply providing a second assumption dict to api.runDeal().

The computation engine is Hastructure, a high-performance structured finance cashflow engine written in Haskell. It runs as a local Docker container and exposes a REST API. The absbox Python library is the thin client that serialises deal definitions to JSON, POSTs them to Hastructure, and deserialises the cashflow results back into Python. All result DataFrames are converted from pandas to Polars immediately for downstream processing.

Loan Tape
Polars DataFrame
~250 US BSL loans
→
absbox
Python library
deal definition API
→
Hastructure
Haskell engine
Docker :8081
→
Results
Polars DataFrames
cashflows + metrics
absbox
pip install absbox==0.52.3
Python library for structured finance cashflow modelling. Wraps the Hastructure engine with a clean deal definition API. Version must match the Hastructure image minor version.
Hastructure
docker pull yellowbean/hastructure
Open-source Haskell cashflow engine. Runs locally on port 8081 and exposes a REST/JSON API. Handles all deal mechanics — waterfall, OC tests, CDR/CPR application.
Polars
pl.from_pandas(df)
absbox returns pandas DataFrames. We convert all results to Polars immediately for fast, expressive downstream analytics — joins, rolling windows, lazy evaluation.
runDeal()
api.runDeal(deal, assump)
The main absbox method. Takes a fully-specified Deal object and an assumption set, returns a dict of pandas DataFrames — one per named bond, pool, and account.
runPool()
api.runPool(pool, assump)
Validates the loan pool in isolation before deal assembly. Used in Step 3 to confirm total collections match the expected gross interest and principal schedule.
Quarto
quarto render clo_model.qmd
Literate programming document format. Interleaves prose and Python code cells that render to a self-contained HTML report. A custom CSS file is linked in the YAML front matter to apply project styling.
great_tables
pip install great-tables
Python table rendering library. Takes a Polars or pandas DataFrame and produces polished HTML tables with headers, formatters, colour scales, and footnotes. Used for all cashflow result displays.
Prerequisites

Docker Desktop must be running with the Hastructure container started: docker run -p 8081:8081 yellowbean/hastructure:a0.52.4. Python packages: absbox==0.52.3, quarto, polars, great-tables, pandas (absbox internal dependency), numpy.

Document Convention — Acronym Signposting

The Quarto document uses a large number of structured finance acronyms (CDR, CPR, WAL, IRR, OC, IC, PIK, SOFR, WAS, WARF, BSL, LGD and others). Every acronym should be wrapped in an HTML <abbr> tag with a title attribute carrying the full form. This gives a native browser tooltip on hover. A small CSS rule added to the document's custom stylesheet renders a dotted underline so readers know the tooltip is available:

CSS — Acronym Hover Style (add to custom stylesheet)
abbr[title] {
  text-decoration: underline dotted;
  text-underline-offset: 3px;
  cursor: help;
}

Usage in prose cells — wrap every acronym on first use and wherever clarity helps:

Quarto Markdown — Inline abbr Example
The pool's <abbr title="Weighted Average Spread">WAS</abbr> is 411bps over
<abbr title="Secured Overnight Financing Rate, 3-month term">SOFR3M</abbr>.
The annual <abbr title="Constant Default Rate">CDR</abbr> assumption is 2%.

Deal Architecture — Blackstone BSL CLO 2024-1

We model a representative US broadly syndicated CLO based on standard 2024 market conventions. The fictional deal — "Blackstone BSL CLO 2024-1" — is sized at $600m with seven tranches: six rated debt classes (A through F) and an unrated equity tranche. All rated tranches are SOFR3M-based floaters resetting quarterly.

The capital structure is calibrated to typical 2024 US CLO arbitrage parameters: Class A at 62% of the stack (consistent with Moody's AAA sizing methodology), a debt-to-equity ratio of 19:1, and an equity tranche sized at 5% — the minimum risk retention required under US CLO 2.0 / Dodd-Frank Credit Risk Retention rules (79 FR 77601, December 2014).

Class A
AAA
$372m
62%
SOFR3M+150bps
Class B
AA
$66m
11%
SOFR3M+220bps
Class C
A
$54m
9%
SOFR3M+300bps
Class D
BBB
$36m
6%
SOFR3M+425bps
Class E
BB
$30m
5%
SOFR3M+650bps
Class F
B
$12m
2%
SOFR3M+875bps
Equity
NR
$30m
5%
Residual
ClassRatingBalance% Stack Spread (bps)All-In @ SOFR 5.25%Quarterly Interest
AAAA$372.0m62.0%1506.75%$6.278m
BAA$66.0m11.0%2207.45%$1.229m
CA$54.0m9.0%3008.25%$1.114m
DBBB$36.0m6.0%4259.50%$0.855m
EBB$30.0m5.0%65011.75%$0.881m
FB$12.0m2.0%87514.00%$0.420m
EquityNR$30.0m5.0%—Residual—
Total$600.0m100%$10.777m debt service
Portfolio Characteristics
  • par — $600m face value
  • loans — ~250 US BSL obligors
  • WAS — ~410bps over SOFR3M
  • WARF — ~2800 (B/B− average)
  • diversity score — ≥ 60 (Moody's)
  • WAL — ~5.7 years
Deal Structure
  • reinvestment period — 2 years
  • legal maturity — 12 years
  • payment frequency — quarterly
  • SOFR reset — 3-month (SOFR3M)
  • senior mgmt fee — 0.15% p.a.
  • sub mgmt fee — 0.35% p.a.
US BSL CLO 2.0 Convention

Under Dodd-Frank Credit Risk Retention rules, the CLO manager must hold at least 5% of each tranche or a "vertical slice" — hence equity sized at exactly 5%. US BSL CLOs use SOFR3M (3-month term SOFR), not overnight SOFR, because the 3-month tenor matches the quarterly payment cycle and avoids daily compounding complexity.

Loan Pool Construction

The loan pool is the engine of the CLO — all cash flowing through the waterfall originates here. We will construct a statistically representative synthetic tape of ~250 US broadly syndicated leveraged loans, calibrated to publicly available 2024 Leveraged Commentary & Data (LCD) market statistics. No Bloomberg or CUSIP licence is required.

Loans are diversified across 12 GICS industries, with technology, healthcare, and industrials making up the largest concentrations — consistent with the current US leveraged loan index composition. Each loan is represented by 7 fields that absbox needs to project its cashflows.

Balance
originBalance / currentBalance
Face value at origination and today. Drawn from a log-normal distribution centred at $2.4m — giving a well-diversified pool that sums to $600m par.
Rate / Spread
SOFR3M + spread (decimal)
Floating rate expressed as a spread over SOFR3M. Sampled by industry bucket: tech/software 375–425bps, healthcare 350–400bps, industrials/energy 400–475bps.
Remaining Term
remainTerm (months)
Months until maturity. US BSL loans are typically 6–7yr original tenor with bullet repayment. We draw from 60–84 months remaining — reflecting a seasoned portfolio.
Loan Type
bullet / interest-only
US broadly syndicated term loans are interest-only with a bullet principal repayment at maturity. absbox models this as a zero-amortisation loan with a balloon payment.
Industry Tag
tag = ["Tech", "US"]
Each loan carries industry and country tags. absbox can apply tag-based CDR assumptions — for example, a higher default rate on Energy loans — enabling sector stress tests.
Recovery Rate
pool-level in assumptions
Recovery is applied at pool level in the scenario, not per-loan. 2024 US BSL recovery rates average 65% (Moody's LGD database), with energy and retail materially lower.
Loan Tape — Illustrative Sample (Polars DataFrame)
id
str
orig_bal
f64
curr_bal
f64
spread
f64
remain_mth
i32
industry
str
L0013,200,000.03,200,000.00.040078Technology
L0021,800,000.01,800,000.00.037566Healthcare
L0032,600,000.02,600,000.00.045072Industrials
L0042,100,000.02,100,000.00.042560Consumer Disc.
………………
L2502,400,000.02,400,000.00.041075Software
250 rows × 6 columns  |  Pool par: $600,000,000  |  WAS: 411bps  |  Avg WAL: 5.7yr
Polars → absbox Conversion

absbox expects loans as Python dicts, not DataFrames. We write a helper df_to_pool(df: pl.DataFrame) → list[dict] that maps each row to the absbox loan schema. The pool object also receives a cutoffDate of "2024-03-31" — the date from which cashflows are projected.

absbox API Primer

absbox has three main building blocks: the Pool (the loan assets), the Bond (an individual CLO tranche), and the Deal (the full CLO structure combining pool, bonds, accounts, fees, and waterfall). The API object connects to the Docker engine and exposes runPool() and runDeal().

Core absbox Object Hierarchy
API(EnginePath.LOCAL) → connection to Docker :8081
Pool(assets=[loan_dicts], cutoff="2024-03-31") → loan pool
Bond(balance=372e6, rate=Floater("SOFR3M", 0.015)) → AAA tranche
api.runDeal(deal, assump) → dict[str, pd.DataFrame]
Step 1 — Connect to Hastructure Engine
from absbox import API, EnginePath
import polars as pl

# EnginePath.LOCAL resolves to "http://localhost:8081"
# Hastructure must be running: docker run -p 8081:8081 yellowbean/hastructure
api = API(EnginePath.LOCAL)

# Sanity-check — prints the engine version
print(api.version())
Step 4 — Defining a Bond (Single Tranche)
from absbox.local.component import Bond, Floater

# Class A: $372m, SOFR3M + 150bps, quarterly payments
class_a = Bond(
    balance       = 372_000_000,
    rate          = Floater("SOFR3M", 0.0150),
    originDate    = "2024-03-31",
    startDate     = "2024-03-31",
    rateResetDates = ["2024-06-30", "2024-09-30", "..."],
    paymentDates   = ["2024-06-30", "2024-09-30", "..."]
)
Step 6 — Run Deal & Convert All Results to Polars
result = api.runDeal(deal, base_assumptions)

# result is a dict keyed by bond/account name → pandas DataFrames
# Convert everything to Polars in one pass
pool_cf = pl.from_pandas(result["pool"])

bonds = {
    k: pl.from_pandas(v)
    for k, v in result.items()
    if k.startswith("Class") or k == "Equity"
}

# bonds["ClassA"], bonds["ClassB"], ..., bonds["Equity"]
Version Pinning

The absbox Python package minor version must align with the Hastructure Docker image. We use absbox==0.52.3 with the image tag yellowbean/hastructure:a0.52.4. Mismatched versions produce JSON parse errors that are hard to debug — always pin both together.

Waterfall Logic — Priority of Payments

The CLO waterfall defines the strict order in which quarterly cash collected from the loan pool is distributed. Cash flows top-down — senior creditors are paid first. OC and IC tests are evaluated at designated steps; a failure diverts remaining cash upward to sequentially pay down senior principal until the ratio recovers.

In absbox, the waterfall is encoded as a Python list of action tuples. Each action is a two-element list: an action type string (e.g. "payFee", "payInt", "runOCTest") and the name of the target account or bond. absbox handles the conditional OC/IC diversion logic automatically.

Step 1
Senior Expenses
Trustee, admin, rating agencies — ~$0.240m/qtr fixed
Step 2
Senior Management Fee
0.15% p.a. × $600m ÷ 4 = $0.225m/qtr
Step 3
Class A Interest
$372m × 6.75% ÷ 4 = $6.278m/qtr — largest single outflow
OC Test
A
OC Test A — FAIL → divert to A principal paydown
Par ÷ A notes outstanding ≥ floor (typically ~148%)
Step 4
Class B, C, D Interest (sequential)
$1.229m + $1.114m + $0.855m = $3.198m/qtr combined
OC Test
D
OC Test D — FAIL → sequential paydown A → B → C → D
Par ÷ (A-1+B+C+D notes outstanding) ≥ floor (typically ~109%)
Step 5
Class E & F Interest (sequential)
$0.881m + $0.420m = $1.301m/qtr combined
Step 6
Subordinated Management Fee
0.35% p.a. × $600m ÷ 4 = $0.525m/qtr — deferred if OC fails
Step 7
Reinvestment (during 2-year reinvestment period)
Principal proceeds recycled into new loans to maintain par at $600m
Step 8
Equity Residual Distribution
All remaining cash → equity. Est. ~$2.5m/qtr (~33% annualised) in base case
Q1 Base Case — Gross Interest & Equity Residual Estimate
Gross Interest = $600m × (5.25% + 4.10%) ÷ 4 = $14.025m

− Debt service = $10.777m    − Fees = $0.990m
= Equity residual ≈ $2.258m/qtr → ~$9.0m p.a. → ~30% on $30m equity

Interest Deferral — PIK vs Non-PIK by Tranche

When OC tests fail and the waterfall diverts cash upward, not every tranche is affected in the same way. The critical distinction is whether unpaid interest is non-deferrable — meaning it must be paid in full in cash every quarter — or PIK-eligible (PIK = Payment In Kind) — meaning unpaid interest capitalises onto the outstanding note balance and accrues at the stated coupon until the waterfall recovers.

Non-Deferrable — Classes A, B, C, D
  • Interest must be paid in cash every quarter, no exceptions
  • OC test failure triggers principal paydown, not interest deferral — cash is diverted from junior tranches to retire senior principal until the ratio recovers
  • Senior coupons continue uninterrupted throughout
  • A missed cash payment on any of these classes is a hard default event
PIK-Eligible — Classes E and F
  • Unpaid interest capitalises onto the outstanding note balance each quarter
  • Accrues at the full stated spread (SOFR3M + spread) on the grown balance — it compounds
  • No cash default event — deferral is a structural feature, not a breach
  • Deferred interest must be fully cleared before equity receives any residual once OC tests cure
  • Compounding means equity IRR deteriorates faster than the face shortfall alone implies
absbox — Configuring PIK at the Bond Level

In absbox, deferral behaviour is set on each Bond object individually. Non-deferrable tranches (A–D) use a standard sequential-pay configuration — if the waterfall runs short, absbox records a hard loss rather than accruing the shortfall. PIK-eligible tranches (E and F) require a deferrable flag so that the Hastructure engine tracks unpaid interest separately, adds it to the note balance each period it goes unpaid, and clears it ahead of equity distributions once tests recover. If this flag is omitted, absbox silently defaults to non-deferrable behaviour for all tranches — understating junior tranche balances in any quarter where the waterfall falls short.

Equity — No Accrual Mechanism

Equity receives whatever cash remains after all debt tranches and fees are settled. If nothing remains, equity simply receives zero that quarter — there is no accrual, no PIK, and no catch-up mechanism. Missed equity distributions are gone permanently. This is why equity IRR is so sensitive to the number of quarters where the waterfall is fully consumed by OC-triggered paydowns.

Base Case Assumptions

All modelling assumptions are defined in a single dictionary before being passed to api.runDeal(). Keeping them in one place makes the model easy to audit and extend — adding a stress scenario later is as simple as creating a second dict that overrides the specific values you want to change.

The base case reflects typical 2024 US leveraged loan market conditions: a 2% annual CDR (broadly in line with the trailing 12-month US leveraged loan default rate), 15% CPR (driven by refinancing and repricing activity), 65% recovery rate (consistent with Moody's LGD database for US senior secured loans), and SOFR3M held flat at 5.25%.

Python — Base Case Assumptions (clearly separated for extensibility)
# ── Pool cashflow assumptions ─────────────────────────────────────────────
# All scenario inputs live here. To add a stress case, create a new dict
# overriding only the keys that differ:
#   mild_assump = {**BASE_ASSUMPTIONS, "default": ("CDR", 0.05), ...}
# ─────────────────────────────────────────────────────────────────────────

BASE_ASSUMPTIONS = {
    # Annual Constant Default Rate — 2% reflects 2024 US BSL market conditions
    "default"     : ("CDR", 0.02),

    # Annual Constant Prepayment Rate — 15% reflects active refi/reprice market
    "prepay"      : ("CPR", 0.15),

    # Recovery rate on defaulted par — 65% is the Moody's US senior secured avg
    "recovery"    : 0.65,

    # Months from default event to cash recovery (typical US workout timeline)
    "recoveryLag" : 12,

    # Forward curve: hold SOFR3M flat at 5.25% for the full projection horizon
    "rates"       : {"SOFR3M": 0.0525},
}
Base Case Parameters
  • CDR — 2.0% annual default rate
  • CPR — 15% annual prepayment rate
  • Recovery — 65% of defaulted par
  • Recovery lag — 12 months
  • SOFR3M — 5.25% (flat)
  • Equity IRR (est.) — ~16–18%
Extending to Stress Scenarios
  • Create a new dict overriding specific keys only
  • Mild stress: CDR 5%, CPR 10%, Recovery 55%
  • Severe stress: CDR 10%, CPR 5%, Recovery 40%
  • Pass each dict to api.runDeal() separately
  • Compare results with Polars joins on the date column
Recovery Lag Mechanics

recoveryLag = 12 means defaulted par is removed from the pool immediately (reducing OC ratios) but recovered cash only arrives 12 months later — the typical workout timeline for US leveraged loans. This creates a short window where OC tests are most at risk: pool par has shrunk from defaults but recoveries have not yet arrived to replenish it.

Outputs & Analytics

api.runDeal() returns a dict of pandas DataFrames keyed by the names given to bonds and accounts in the Deal definition. We convert all of them to Polars immediately. The key tables are described below — these are the DataFrames we will work with throughout Steps 6–8.

Pool Cashflow Table — pool_cf

ColumnTypeDescription
DateDateQuarterly payment date
BalanceFloat64Outstanding pool par at period start — shrinks as defaults accumulate outside the reinvestment period
InterestFloat64Gross interest collected from performing loans this quarter
PrincipalFloat64Scheduled principal (bullet repayments at loan maturity)
PrepaymentFloat64Unscheduled prepayments driven by the CPR assumption
DefaultFloat64Par removed from pool this quarter due to defaults (CDR × balance)
RecoveryFloat64Cash recovered from prior defaults (arrives after recoveryLag months)

Bond Cashflow Table — bonds["ClassA"] etc.

ColumnTypeDescription
DateDatePayment date
BalanceFloat64Note balance at period start — $0 during reinvestment period for all tranches except when OC test forces early paydown
InterestFloat64Coupon payment received — SOFR3M × spread × balance ÷ 4
PrincipalFloat64Principal received — zero during reinvestment; sequential after
LossFloat64Credit loss allocated to this tranche (junior tranches absorb first)
Illustrative Output — Equity Cashflows (Base Case, first 8 quarters)
date         interest   principal   equity_dist
2024-06-30   0.0        0.0         2_258_000
2024-09-30   0.0        0.0         2_258_000
2024-12-31   0.0        0.0         2_258_000
2025-03-31   0.0        0.0         2_258_000
2025-06-30   0.0        0.0         2_190_000   ← minor CDR erosion begins
2025-09-30   0.0        0.0         2_127_000
2026-03-31   0.0        0.0         1_940_000
2026-06-30   0.0        0.0         1_880_000   ← recoveries arriving, par stabilising

Analytics We Compute in Step 7

Per-Tranche (via Polars)
  • WAL — Weighted Average Life (years)
  • IRR — Internal Rate of Return
  • DM — Discount Margin vs SOFR3M
  • total_interest — total coupon received
  • loss_rate — % of face value lost
Deal-Level (via Polars joins)
  • OC_ratio_A — per quarter timeline
  • OC_ratio_D — per quarter timeline
  • first_breach_date — if any OC test fails
  • equity_irr — base case equity return
  • excess_spread — quarterly timeline
Why Polars Here

Computing OC ratios requires joining pool par values against cumulative note balances across 40+ quarters for all 7 tranches and 3 scenarios — roughly 840 cells of state. Polars' lazy execution plan handles this as a single vectorised expression without materialising intermediate DataFrames. WAL calculation (time-weighted average of principal cashflows) is also a natural Polars with_columns expression over the bond cashflow table.

Implementation Roadmap — 8 Coding Steps

The document is organised into 8 Quarto code sections, each building on the previous one. Every section is self-contained: a short prose block explains the absbox concept being introduced, followed by a code cell that implements it and a great_tables display of the key outputs. You can inspect what absbox is consuming and producing at each stage before moving on.

1
Environment Setup & Docker Connection
~15 lines

Import dependencies, instantiate the API object pointing to localhost:8081, and verify the connection is live. Print the Hastructure engine version to confirm it matches the absbox package version.

Key imports
absbox · polars
great_tables · numpy
Output
api object
engine version string
2
Build the Loan Tape (Polars DataFrame)
~60 lines

Generate a 250-loan US BSL tape using numpy random draws calibrated to 2024 LCD market statistics. Assign loans to 12 GICS industries with industry-specific spread ranges. Compute and display pool summary: par, WAS, WARF, WAL, loan count by sector.

Inputs
numpy seed (42)
market calibration dict
Output
loan_tape: pl.DataFrame
pool_summary: pl.DataFrame
3
Convert Tape → absbox Pool & Validate
~35 lines

Write df_to_pool(df) to map the Polars DataFrame to a list of absbox loan dicts. Run api.runPool() with Base assumptions to validate pool cashflows before adding deal structure. Inspect total interest collections match the expected $14m/qtr.

Inputs
loan_tape DataFrame
base_assump dict
Output
pool object
pool_cf: pl.DataFrame
4
Define the 7 Bond Objects (Capital Structure)
~70 lines

Instantiate Bond objects for Classes A, B, C, D, E, F, and Equity. Each bond receives its balance, a Floater("SOFR3M", spread) rate, and the deal's quarterly payment and rate reset schedule. Classes A–D are configured as non-deferrable — interest must pay in cash each quarter. Classes E and F are configured as PIK-eligible so that any interest the waterfall cannot reach capitalises onto the note balance rather than recording a hard shortfall. Equity is defined as a residual claim with no accrual. Print a summary table confirming total debt ($570m) + equity ($30m) = pool par ($600m).

Inputs
capital structure params
quarterly date schedule
Output
bonds: dict[str, Bond]
capital_structure_df
5
Define Waterfall, Fees & Assemble Deal
~90 lines

Encode the 8-step priority of payments as an absbox waterfall list. Define fee accounts (senior expenses, senior management fee, subordinated management fee). Set OC test floors per tranche. Assemble the complete Deal object combining pool, bonds, accounts, triggers, and waterfall. Print a human-readable waterfall summary.

Inputs
bonds · pool · fee params
OC/IC floor thresholds
Output
deal: Deal object
waterfall_summary: str
6
Run Base Case via api.runDeal()
~35 lines

Pass the deal and BASE_ASSUMPTIONS dict to api.runDeal(). Convert all results to Polars immediately. Organise by tranche name: bonds["ClassA"], bonds["ClassB"], etc. Show a Q1 Class A cashflow preview and confirm the equity residual matches the $2.26m estimate. The assumption dict is already structured so a second scenario can be added with one extra runDeal() call.

Inputs
deal + BASE_ASSUMPTIONS dict
Output
bonds: dict[str, pl.DataFrame]
pool_cf: pl.DataFrame
7
Compute Analytics — WAL, IRR, OC/IC Timeline
~80 lines

Using Polars expressions: compute WAL per tranche (time-weighted principal cashflows), IRR per tranche, and OC ratios by joining pool and bond balance columns. Flag the first quarter where each OC test breaches (if at all in the base case). Display the equity IRR and the full OC ratio timeline across all 48 quarters.

Inputs
bonds dict · pool_cf
OC floor constants
Output
metrics_df: great_tables GT
oc_timeline: great_tables GT
8
Final Output — Consolidated Waterfall & Summary
~55 lines

Produce the final deliverables as great_tables: (1) a full quarterly waterfall distribution table — all tranches, all 48 periods, with currency formatting and alternating row shading; (2) a per-tranche analytics summary with WAL, IRR, and loss rate, colour-coded by seniority; (3) an OC ratio timeline confirming no test breaches in the base case. All tables render inline in the Quarto HTML output.

Inputs
bonds dict + analytics
Output
waterfall GT table
tranche_summary GT table
What You Will Have at the End

After all 8 steps: a validated $600m US BSL CLO running in Hastructure; quarterly cashflows for all 7 tranches over a 12-year horizon as Polars DataFrames; an OC/IC test timeline confirming test passage in the base case; equity IRR of ~16–18%; and WAL / discount margin per tranche. All outputs are in Polars and ready for further analysis, export, or extension with stress scenarios.

Learning Structure

Each section introduces exactly one new absbox concept. Prose before the code cell explains the concept and the key parameters involved. Output is rendered inline as great_tables HTML tables — no separate visualisation step needed. To re-run under different assumptions, update BASE_ASSUMPTIONS and re-render with quarto render clo_model.qmd.