Operating company LBO
Outputs below are real: the notebook runs against the
opcopack's benchmark model, which CFDL validates against an independent reference. To run it yourself, see the Python SDK guide.
A five-year services buyout: revenue/opex growth, DSO/DPO/DIO working capital, %-of-revenue capex, a term loan (IO then amortizing with a balloon), cash taxes, and an exit on trailing-twelve EBITDA.
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_sdkCompile
Compile the model directory to IR.
model_dir = ROOT / "benchmarks/opco/lbo_buyout"
model = cfdl_sdk.compile(model_dir, packs_dir=PACKS)
print("streams:", len(model.ir["streams"]))streams: 11
Run
Run with the benchmark's configuration and apply the opco pack's domain metrics.
results = model.run(
config=str(model_dir / "run.json"),
pack="opco",
)
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: (60, 29)
account.asset.target.balance account.asset.target.balance.term_debt \
period
2026-01 21000000.0 21000000.0
2026-02 21000000.0 21000000.0
2026-03 21000000.0 21000000.0
2026-04 21000000.0 21000000.0
2026-05 21000000.0 21000000.0
asset.target.opco_capex_growth asset.target.opco_opex_growth \
period
2026-01 1.0 1.000000
2026-02 1.0 1.003274
2026-03 1.0 1.006558
2026-04 1.0 1.009853
2026-05 1.0 1.013159
asset.target.opco_revenue_growth domain.opco.cfads domain.opco.debt_service_coverage \
period
2026-01 1.000000 -745379.794521 -5.010957
2026-02 1.004868 302754.171794 2.035322
2026-03 1.009759 304616.658715 2.047843
2026-04 1.014674 306490.722025 2.060442
2026-05 1.019613 308376.426292 2.073119
domain.opco.debt_service_periodic domain.opco.ebitda_periodic \
period
2026-01 148750.0 350000.000000
2026-02 148750.0 352739.619707
2026-03 148750.0 355495.966170
2026-04 148750.0 358269.131912
2026-05 148750.0 361059.209939
domain.opco.lfcf_periodic ... stream.opco.capex.line stream.opco.debt.interest \
period ...
2026-01 2.010587e+07 ... -30000.000000 -148750.0
2026-02 1.540042e+05 ... -30146.026517 -148750.0
2026-03 1.558667e+05 ... -30292.763825 -148750.0
2026-04 1.577407e+05 ... -30440.215385 -148750.0
2026-05 1.596264e+05 ... -30588.384673 -148750.0
stream.opco.debt.principal stream.opco.debt.proceeds stream.opco.exit.selling_costs \
period
2026-01 0.0 21000000.0 0.0
2026-02 0.0 0.0 0.0
2026-03 0.0 0.0 0.0
2026-04 0.0 0.0 0.0
2026-05 0.0 0.0 0.0
stream.opco.exit.value stream.opco.opex.recurring stream.opco.revenue.recurring \
period
2026-01 0.0 -650000.000000 1.000000e+06
2026-02 0.0 -652127.930858 1.004868e+06
2026-03 0.0 -654262.828009 1.009759e+06
2026-04 0.0 -656404.714257 1.014674e+06
2026-05 0.0 -658553.612483 1.019613e+06
stream.opco.taxes.cash stream.opco.working_capital.adjustment
period
2026-01 -13325.000000 -1.052055e+06
2026-02 -14037.301124 -5.802120e+03
2026-03 -14753.951204 -5.832592e+03
2026-04 -15474.974297 -5.863220e+03
2026-05 -16200.394584 -5.894004e+03
[5 rows x 29 columns]
# Requires the [viz] extra (pip install cfdl-sdk[viz]).
results.plot.cumulative()<Axes: >

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 free_cash_flow Revenue to unlevered free cash flow bridge pack
1 ebitda EBITDA columns pack
2 coverage Debt service coverage lines pack
3 cash_by_activity Cash flow by activity columns pack
4 returns Returns bridge pack
results.plot.chart() # the pack's default chart<Axes: title={'left': 'Revenue to unlevered free cash flow'}>

# How the return is made, one layer of cash at a time.
results.plot.chart("returns")<Axes: title={'left': 'Returns'}>

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.opco.capex 2.084574e+06 USD domain:opco
1 domain.opco.debt_service 2.828325e+07 USD domain:opco
2 domain.opco.ebitda 2.646942e+07 USD domain:opco
3 domain.opco.ebitda_margin 3.809330e-01 domain:opco
4 domain.opco.fcf 2.173644e+07 USD domain:opco
5 domain.opco.fcf_to_debt_service 7.685270e-01 domain:opco
6 domain.opco.revenue 6.948579e+07 USD domain:opco
7 domain.opco.taxes 2.648405e+06 USD domain:opco
8 domain.opco.working_capital 2.000000e-06 USD domain:opco
9 entity.asset.target.total 3.335678e+07 USD core
10 metric.moic 3.471947e+00 core
11 model.irr 3.076380e-01 core
12 model.npv 1.388314e+07 USD core
13 model.payback_periods 5.900000e+01 core
14 model.payback_years 5.000000e+00 core
15 model.total 3.335678e+07 USD core
16 model.wal_years 4.791671e+00 core
17 run.annual_discount_rate 1.200000e-01 core
18 run.periods_per_year 1.200000e+01 core
19 stream.opco.acquisition.price.total -3.360000e+07 USD core
20 stream.opco.capex.line.total -2.084574e+06 USD core
21 stream.opco.debt.interest.total -7.283247e+06 USD core
22 stream.opco.debt.principal.total -2.100000e+07 USD core
23 stream.opco.debt.proceeds.total 2.100000e+07 USD core
24 stream.opco.exit.selling_costs.total -7.995469e+05 USD core
25 stream.opco.exit.value.total 5.330313e+07 USD core
26 stream.opco.opex.recurring.total -4.301637e+07 USD core
27 stream.opco.revenue.recurring.total 6.948579e+07 USD core
28 stream.opco.taxes.cash.total -2.648405e+06 USD core
29 stream.opco.working_capital.adjustme... -0.000000e+00 USD core
What-if
Report the free-cash-flow-to-debt-service coverage and MOIC.
m = results.metrics()
print("FCF / debt service:", round(m["domain.opco.fcf_to_debt_service"], 3))
print("MOIC:", round(m["metric.moic"], 3))FCF / debt service: 0.769
MOIC: 3.472
Handing the run over
A package is the model, its run configuration and its results, with a manifest of their checksums and hashes. The recipient verifies it without running anything, and reproduces it by running it.
import json, shutil, tempfile
from pathlib import Path
deal = Path(tempfile.mkdtemp()) / "lbo"
shutil.copytree(model_dir, deal)
(deal / "results.json").write_text(json.dumps(results.to_dict()), encoding="utf-8")
manifest = cfdl_sdk.package(deal)
print("files:", [f["path"] for f in manifest["files"]])
print("ledger hash:", manifest["ledger_hashes"]["base"][:12], "== results:", results.ledger_hash[:12])
print(cfdl_sdk.verify(deal)["note"])
print(cfdl_sdk.reproduce(deal)["note"])files: ['model.cfdl', 'results.json', 'run.json']
ledger hash: 1c72d4b42b6f == results: 1c72d4b42b6f
verify: all 9 checks passed
reproduce: all 2 checks passed