Skip to main content
CFDL

Credit loan pool (level-pay)

Outputs below are real: the notebook runs against the credit pack's benchmark model, which CFDL validates against an independent reference. To run it yourself, see the Python SDK guide.

A homogeneous level-pay loan pool with CPR prepayments, CDR defaults, loss severity, a recovery lag, a servicing strip and prepayment penalties — priced at a discount to par.

This notebook uses one of the benchmark models, which CFDL validates against an independent reference to the penny.

# On Colab, install the SDK and fetch the models this notebook reads.
# Inside a checkout both are already present and this cell does nothing.
import subprocess, sys
from pathlib import Path

REPO = "https://github.com/cfdl-dev/cfdl"


def repo_root() -> Path:
    """The checkout holding benchmarks/ and packs/, cloning it if need be.

    Searching a bounded set of ancestors means a plain `python` run outside a
    checkout fails with an explanation rather than walking to the filesystem
    root. On a hosted runtime there is no checkout to find, so fetch one.
    """
    here = Path.cwd().resolve()
    for candidate in (here, *here.parents):
        if (candidate / "Cargo.toml").exists() and (candidate / "packs").is_dir():
            return candidate

    if "google.colab" not in sys.modules:
        raise RuntimeError(
            f"No CFDL checkout found above {here}. This notebook reads a model "
            f"from benchmarks/ and pack definitions from packs/, so run it "
            f"inside a clone of {REPO}."
        )

    subprocess.run([sys.executable, "-m", "pip", "install", "-q", "cfdl-sdk[viz]"], check=True)

    # Packs and benchmark models track the engine, so take the checkout at the
    # tag matching the wheel pip just resolved. `main` runs ahead of the last
    # release and its packs may use metric ops the released engine rejects.
    import importlib
    from importlib.metadata import PackageNotFoundError, version

    importlib.invalidate_caches()
    try:
        tag = f"v{version('cfdl-sdk')}"
    except PackageNotFoundError:
        tag = None

    clone = ["git", "clone", "--depth", "1", "-q", REPO]
    target = Path("/content/cfdl")
    if not target.exists():
        pinned = tag is not None and not subprocess.run(
            clone + ["--branch", tag, str(target)]
        ).returncode
        if not pinned:
            # A dev or pre-release wheel has no matching tag; main is the best
            # available, and the notebook may fail if the two have diverged.
            print(f"warning: no {tag} tag for this SDK build; falling back to main.")
            subprocess.run(clone + [str(target)], check=True)
    return target


ROOT = repo_root()
PACKS = ROOT / "packs"

import cfdl_sdk

Compile

Compile the model directory to IR.

model_dir = ROOT / "benchmarks/credit/level_pay_pool"
model = cfdl_sdk.compile(model_dir, packs_dir=PACKS)
print("streams:", len(model.ir["streams"]))
streams: 10

Run

Run with the benchmark's configuration and apply the credit pack's domain metrics.

results = model.run(
    config=str(model_dir / "run.json"),
    pack="credit",
)
print("status:", results.status, "| warnings:", len(results.warnings))
status: ok | warnings: 0

Cash flows

The engine returns per-period signed cash flows; cashflows() gives a wide DataFrame indexed by period.

cf = results.cashflows()
print('shape:', cf.shape)
cf.head()
shape: (126, 28)
         account.asset.buyer.balance  account.asset.buyer.balance.auto_a  \
period                                                                     
2026-01                 2.463766e+07                        2.463766e+07   
2026-02                 2.427892e+07                        2.427892e+07   
2026-03                 2.392374e+07                        2.392374e+07   
2026-04                 2.357208e+07                        2.357208e+07   
2026-05                 2.322393e+07                        2.322393e+07   

         asset.buyer.credit_loan_balance_lag_auto_a  asset.buyer.credit_loan_payment_auto_a  \
period                                                                                        
2026-01                               25000000.0                              283869.943050   
2026-02                               25000000.0                              281426.809237   
2026-03                               25000000.0                              279004.702317   
2026-04                               25000000.0                              276603.441321   
2026-05                               25000000.0                              274222.846838   

         asset.buyer.credit_loan_payment_lag_auto_a  domain.credit.balance_outstanding  \
period                                                                                   
2026-01                             283869.94305                          2.463766e+07   
2026-02                             283869.94305                          2.427892e+07   
2026-03                             283869.94305                          2.392374e+07   
2026-04                             283869.94305                          2.357208e+07   
2026-05                             283869.94305                          2.322393e+07   

         domain.credit.gross_collections  domain.credit.net_collections  \
period                                                                    
2026-01                    457194.867943                  446795.723595   
2026-02                    452225.142236                  441976.718401   
2026-03                    447301.537920                  437202.339282   
2026-04                    442423.647210                  432472.191964   
2026-05                    437591.065867                  427785.885597   

         domain.credit.net_interest_collections  domain.credit.original_balance  ...  \
period                                                                           ...   
2026-01                           124789.732181                      25000000.0  ...   
2026-02                           122981.086013                      25000000.0  ...   
2026-03                           121190.383650                      25000000.0  ...   
2026-04                           119417.462962                      25000000.0  ...   
2026-05                           117662.163243                      25000000.0  ...   

         stream.credit.loan.defaults.auto_a  stream.credit.loan.dilution.auto_a  \
period                                                                            
2026-01                       -42053.563818                                 0.0   
2026-02                       -41444.058407                                 0.0   
2026-03                       -40840.599975                                 0.0   
2026-04                       -40243.133886                                 0.0   
2026-05                       -39651.605981                                 0.0   

         stream.credit.loan.interest.auto_a  stream.credit.loan.penalty.auto_a  \
period                                                                           
2026-01                       135188.876529                        1720.816184   
2026-02                       133229.509847                        1695.759733   
2026-03                       131289.582287                        1670.952092   
2026-04                       129368.918209                        1646.391013   
2026-05                       127467.343513                        1622.074267   

         stream.credit.loan.prepay.auto_a  stream.credit.loan.recoveries.auto_a  \
period                                                                            
2026-01                     172081.618419                                   0.0   
2026-02                     169575.973277                                   0.0   
2026-03                     167095.209193                                   0.0   
2026-04                     164639.101296                                   0.0   
2026-05                     162207.426682                                   0.0   

         stream.credit.loan.sched_principal.auto_a  stream.credit.loan.servicing.auto_a  \
period                                                                                    
2026-01                            148203.556810                          -10399.144348   
2026-02                            147723.899379                          -10248.423834   
2026-03                            147245.794348                          -10099.198637   
2026-04                            146769.236692                           -9951.455247   
2026-05                            146294.221405                           -9805.180270   

         stream.credit.loan.zero_balance.auto_a  stream.credit.purchase.price.auto_a  
period                                                                                
2026-01                                     0.0                          -24750000.0  
2026-02                                     0.0                                  0.0  
2026-03                                     0.0                                  0.0  
2026-04                                     0.0                                  0.0  
2026-05                                     0.0                                  0.0  

[5 rows x 28 columns]
# Requires the [viz] extra (pip install cfdl-sdk[viz]).
results.plot.cumulative()
<Axes: >

Chart produced by the preceding cell

The pack's charts

The run publishes the charts its pack declares, each resolved by the engine: the series it sums, how its periods fold into a year, its unit. results.plot.chart draws one as the playground does, in the site's colors.

results.charts()[["id", "label", "kind", "origin"]]
                  id                                 label     kind origin
0        collections                           Collections  columns   pack
1       pool_balance                          Pool balance    lines   pack
2        pool_factor                           Pool factor    lines   pack
3  capital_structure                     Capital structure    stack   pack
4            returns                               Returns   bridge   pack
5    available_funds  Gross collections to available funds   bridge   pack
results.plot.chart()  # the pack's default chart
<Axes: title={'left': 'Collections'}>

Chart produced by the preceding cell

# The pool's balance outstanding: a level, so a year shows its last month.
results.plot.chart("pool_balance", grain="annual")
<Axes: title={'left': 'Pool balance'}>

Chart produced by the preceding cell

Metrics

Core metrics (NPV/IRR/MOIC/...) plus the pack's domain metrics, with their source labeled.

results.metrics_frame()
                                     metric         value currency         source
0                 domain.credit.collections  3.072748e+07      USD  domain:credit
1        domain.credit.collections_multiple  1.241514e+00           domain:credit
2                    domain.credit.interest  6.499895e+06      USD  domain:credit
3                   domain.credit.penalties  8.220768e+04      USD  domain:credit
4                   domain.credit.principal  2.297806e+07      USD  domain:credit
5                    domain.credit.purchase  2.475000e+07      USD  domain:credit
6                  domain.credit.recoveries  1.167320e+06      USD  domain:credit
7                   domain.credit.servicing  4.999919e+05      USD  domain:credit
8                   domain.credit.wal_years  4.056967e+00           domain:credit
9                  entity.asset.buyer.total  5.477491e+06      USD           core
10                              metric.moic  1.225381e+00                    core
11                                model.irr  5.630900e-02                    core
12                                model.npv -2.959752e+05      USD           core
13                    model.payback_periods  8.000000e+01                    core
14                      model.payback_years  6.750000e+00                    core
15                              model.total  5.477491e+06      USD           core
16                          model.wal_years  3.843940e+00                    core
17                 run.annual_discount_rate  6.000000e-02                    core
18                     run.periods_per_year  1.200000e+01                    core
19  stream.credit.loan.defaults.auto_a.t... -2.021940e+06      USD           core
20  stream.credit.loan.dilution.auto_a.t...  0.000000e+00      USD           core
21  stream.credit.loan.interest.auto_a.t...  6.499895e+06      USD           core
22  stream.credit.loan.penalty.auto_a.total  8.220768e+04      USD           core
23   stream.credit.loan.prepay.auto_a.total  8.220768e+06      USD           core
24  stream.credit.loan.recoveries.auto_a...  1.167320e+06      USD           core
25  stream.credit.loan.sched_principal.a...  1.475729e+07      USD           core
26  stream.credit.loan.servicing.auto_a.... -4.999919e+05      USD           core
27  stream.credit.loan.zero_balance.auto...  0.000000e+00      USD           core
28  stream.credit.purchase.price.auto_a.... -2.475000e+07      USD           core

What-if

Show the collections multiple and the principal-weighted WAL.

m = results.metrics()
print("collections multiple:", round(m["domain.credit.collections_multiple"], 4))
print("WAL (years):", round(m["domain.credit.wal_years"], 3))
collections multiple: 1.2415
WAL (years): 4.057

The record behind the numbers

Every period of the run is recorded with its reason. The loan's lifecycle holds in current for the whole run, so the stream that pays only once the pool reaches a zero balance never runs: it is out_of_state, and the trace names the state.

results.trace(kind="lifecycle")
        kind                        actor  from   to reason    state  left  edge tested entry
0  lifecycle  contract.credit.loan.auto_a     0  125   held  current  None  None     []  None
results.trace(kind="stream").query("reason != 'ran'")
      kind                            actor  from   to          reason    state  left  edge  \
8   stream  credit.loan.zero_balance.auto_a     0  125    out_of_state  current  None  None   
10  stream     credit.purchase.price.auto_a     1  125  after_schedule      NaN  None  None   

   tested entry  
8      []  None  
10     []  None