The banker sends a CIM

Your banker emails over a CIM and a forecast. I want to know how much growth can disappoint before debt stops coming down, and what the equity return looks like without a higher exit multiple.

I’d give the agents the first pass through the documents and the financial definitions, then keep the cash-flow and debt calculations in code. I want to change growth, margin or financing and follow the effect through the annual schedule. The workbook should keep each case’s assumptions beside its results.

DOCUMENTS TO WORKBOOK
  1. DOCUMENTSCIM and financial schedulesPreserve files, periods, definitions and source locations.
  2. AGENT ANALYSISExtract and reconcileReturn financial findings, assumptions and unresolved gaps.
  3. MODEL INPUTSFinancials and assumptionsSeparate reported, calculated and assumed values.
  4. UNDERWRITING CASESBase, upside and downsideRecalculate cash flow, debt and equity returns for each case.
  5. WORKBOOKSheets, cells and formulasCompile the selected assumptions into editable schedules.
  6. READBACKFormulas and valuesCompare outputs and test case selectors.
Agents produce structured financial analysis. The model calculates the cases, and the compiler produces the workbook.

Keep the CIM (confidential information memorandum), supporting files and their versions in a document manifest. Record the available text extracts and tables, and retain the filename, page or tab, period, currency and scale with each figure. You’ll want that reference when a number looks wrong three steps later.

Suppose a revenue schedule puts actual results and a forecast in adjacent columns. Preserve that distinction when extracting the table. A trailing-twelve-month figure also needs its own period label. For adjusted EBITDA, keep the adjustment bridge and definition alongside the amount; don’t leave the explanation behind in the PDF.

Give the agents a defined job

To get that first pass done, split it into five jobs. In the diagram, Jared handles diligence, Monica financial analysis, Richard thesis development, Dinesh synthesis and Gilfoyle evidence review. The harness is the code that supplies their instructions, tools and output schemas and runs the tasks in order.

Jared looks for contradictions, missing schedules and questions to send back to the banker. Monica extracts the operating history and forecast, defines the metrics and calculates supported ratios. Both read the evidence package. They run sequentially, but Monica goes back to the financial materials rather than using Jared’s report as her financial source.

AGENT TASKS

EVIDENCE PACKAGE

  1. DILIGENCE

    Questions and contradictions

    JaredDiligence analyst
    INPUT
    CIM, financial schedules and supporting documents
    OUTPUT
    Questions, contradictions and evidence gaps

    Identify missing schedules, conflicting definitions and follow-up questions.

    FINANCIAL ANALYSIS

    Periods and metrics

    MonicaFinancial analyst
    INPUT
    Financial tables, forecasts and metric definitions
    OUTPUT
    Historical financials, forecasts and sourced calculations

    Extract operating history and forecasts with definitions and source references.

  2. THESIS

    Operating assumptions

    RichardThesis writer
    INPUT
    Diligence findings and financial analysis
    OUTPUT
    Operating proposals and the assumptions they require

    Describe what would need to change and which inputs those changes affect.

  3. SYNTHESIS

    Combined evaluation

    DineshInvestment evaluator
    INPUT
    Diligence, financial and thesis outputs
    OUTPUT
    Combined structured evaluation

    Assemble the findings without promoting an assumption to a reported fact.

  4. EVIDENCE REVIEW

    Draft against documents

    GilfoyleEvidence reviewer
    INPUT
    Draft evaluation and original supporting materials
    OUTPUT
    Reviewed evaluation with unsupported claims corrected

    Remove or qualify claims the supplied evidence does not support.

Robot sketches adapted from Robohash set1 by Zikri Kader (CC BY 3.0 / 4.0).

Diligence and financial analysis read the same evidence package. Thesis receives both outputs; synthesis and evidence review follow.

For a software business, ask Monica to reconcile revenue, annual recurring revenue, bookings and billings. A services invoice in the sales table shouldn’t quietly join the recurring-revenue series. Each metric needs a definition, a period and a source, with missing and conflicting values recorded separately.

Richard takes the diligence and financial outputs and describes what would need to change in the business. Dinesh assembles the findings into one evaluation. Gilfoyle checks the draft against the materials and removes or qualifies unsupported claims. The reviewed output keeps the same structured shape so the model can read it.

AGENT ORCHESTRATION
evidence = load_documents(manifest) diligence = run_task("diligence", evidence)financials = run_task("financial_analysis", evidence) thesis = run_task("thesis", {    "evidence": evidence,    "diligence": diligence,    "financials": financials,}) draft = run_task("synthesis", {    "diligence": diligence,    "financials": financials,    "thesis": thesis,}) reviewed = run_task("evidence_review", {    "evidence": evidence,    "draft": draft,})
Pseudocode for the task order and handoffs. Both analysis tasks read the evidence package; thesis receives their structured outputs.

Give the agents tools that fit those jobs. A document reader retrieves narrative evidence. Table tools list registered tables, describe columns and run read-only queries. The runtime controls the available files and tables; text inside a CIM or cell comment cannot grant another permission.

Also decide what happens when a task fails. Record the failure and the missing fields, then carry those gaps forward. A typed response can reject a string where a number belongs. It won’t tell you whether somebody put bookings in the ARR column.

Decide what goes into the model

Monica’s output now has to become something the model can use: entry period, revenue, EBITDA, historical periods and a projection basis, all with source notes. Put financing and operating assumptions in a separate record. I want to be able to point to a value and say where it came from.

If the materials give you revenue and an EBITDA margin, calculate EBITDA and retain the expression and both inputs. If the margin is missing too, you need an assumption or another answer from the banker. An analyst-entered margin must stay distinguishable from a reported one.

Missing evidence and model assumptions are different fields. A default can make an example runnable, but it cannot establish a business fact. Record which rows were reported, calculated or assumed. One source-status label for the whole package loses that distinction when the origins are mixed.

FINANCIAL OBSERVATION
@dataclass(frozen=True)class SourceRef:    file: str    location: str    period: str @dataclass(frozen=True)class Observation:    amount: Decimal | None    units: str    period_type: Literal["actual", "forecast"]    origin: Literal["reported", "calculated", "assumed"]    sources: tuple[SourceRef, ...]
An example data contract keeps the amount, reporting period, origin and source references together.

The assumptions record covers valuation, debt, interest, fees, cash requirements, taxes, capex, working capital and hold period. Validate units and ranges before calculation. A percentage entered as 25 instead of 0.25 should fail a decimal-rate contract. An EBITDA-based purchase multiple needs EBITDA for the stated entry period.

For the worked numbers, use Pied Piper: entry revenue of $100m, EBITDA of $25m and an entry multiple of 10×. Debt and sponsor equity fund the acquisition. The entry table separates enterprise value, fees and cash funded into the business, so you can reproduce the equity cheque.

PIED PIPER ENTRY AND EXIT
USD MILLIONS
Entry uses of $260m are financed by debt of $125m and sponsor equity of $135m. Base-case exit equity is $314.85m, after subtracting debt and adding cash.
MODEL LINEUSD MILLIONS
ENTRY ENTERPRISE VALUE250.00
FEES5.00
FUNDED CASH5.00
TOTAL USES260.00
ENTRY DEBT125.00
SPONSOR EQUITY135.00
EXIT ENTERPRISE VALUE367.33
EXIT DEBT57.48
EXIT CASH5.00
EXIT EQUITY314.85
The worked example funds enterprise value, fees and minimum cash with debt and sponsor equity. Exit proceeds include all closing cash.SOURCEPIED PIPER MODEL
WORKED MODEL INPUTS
inputs.json
{  "company": "Pied Piper",  "currency": "USD",  "units": "millions",  "years": 5,  "entry_revenue": 100,  "ebitda_margin": 0.25,  "revenue_growth": 0.08,  "entry_multiple": 10,  "exit_multiple": 10,  "entry_debt": 125,  "entry_fees": 5,  "minimum_cash": 5,  "interest_rate": 0.08,  "tax_rate": 0.25,  "depreciation_pct": 0.02,  "capex_pct": 0.03,  "nwc_pct": 0.1,  "forecast_ebitda_margin": 0.25}
Inputs for the Pied Piper calculation. Entry EBITDA and forecast EBITDA margin are separate assumptions.

Keep the plan, then underwrite it

Keep management’s forecast as a reference, then write down what you’re willing to underwrite. I’d keep those in different records so the assumptions have an owner. Build base, upside and downside cases, each stating how growth, margins, financing or exit assumptions differ.

Each case needs a complete assumption set. The upside might combine higher growth with better margins and a different exit multiple. The downside tests slower growth, weaker margins and a lower exit multiple. I’d show those changes beside the return, including the case with no help from a higher exit multiple.

Take Richard’s operating proposals back to the inputs. If the thesis depends on pricing, specify whether it changes revenue growth, gross margin or both, and when. A cost program needs a margin path. A paragraph saying pricing will improve is not yet a forecast assumption.

Keep the reference forecast unchanged when editing a case. Store overrides separately and retain them with the outputs. In the Pied Piper comparison, forecast growth, forecast EBITDA margin and exit multiple change. Entry EBITDA, purchase enterprise value of $250m and sponsor equity of $135m stay fixed.

CASE OVERRIDES
cases = {    "base": {},    "upside": {        "revenue_growth": 0.10,        "forecast_ebitda_margin": 0.27,        "exit_multiple": 10.5,    },    "downside": {        "revenue_growth": 0.05,        "forecast_ebitda_margin": 0.22,        "exit_multiple": 9.0,    },} for name, overrides in cases.items():    case_inputs = {**base_inputs, **overrides}    results[name] = calculate(case_inputs)
Case overrides change forecast assumptions and exit valuation. The entry transaction stays fixed.

Run those three input sets through the same calculation. The case selector below lets you compare the assumptions and resulting returns. The labels describe scenarios; they don’t attach a probability to what happens.

UNDERWRITING CASES

Pied Piper underwriting cases

Select a case to view its assumptions and calculated returns.

Base, upside and downside case comparison for Pied Piper. All three retain the same entry transaction and recalculate the five-year cash and debt schedules.

SELECT CASE

BASE

CASE ASSUMPTIONS

REVENUE GROWTH
8%
FORECAST EBITDA MARGIN
25%
ENTRY EV / EBITDA
10×
EXIT EV / EBITDA
10×

CALCULATED RETURNS

EXIT EQUITY
$314.85m
EQUITY MOIC
2.33×
ANNUALIZED EQUITY RETURN
18.5%

ANNUAL CASH AND DEBT

USD MILLIONS
ANNUAL CASH AND DEBT
MODEL LINEYEAR 1YEAR 2YEAR 3YEAR 4YEAR 5
REVENUE108.00116.64125.97136.05146.93
EBITDA27.0029.1631.4934.0136.73
CASH FLOW BEFORE REPAYMENT9.2511.1513.2615.6218.24
DEBT REPAYMENT9.2511.1513.2615.6218.24
CLOSING DEBT115.75104.6191.3475.7257.48
CLOSING CASH5.005.005.005.005.00
Pied Piper underwriting cases
CASEREVENUE GROWTHFORECAST EBITDA MARGINENTRY EV / EBITDAEXIT EV / EBITDAEXIT EQUITYEQUITY MOICANNUALIZED EQUITY RETURN
BASE8%25%10×10×$314.85m2.33×18.5%
UPSIDE10%27%10×10.5×$420.4m3.11×25.5%
DOWNSIDE5%22%10×9×$178.39m1.32×5.7%
Entry revenue, entry EBITDA and purchase financing stay fixed. Cases change forecast growth, forecast EBITDA margin and the exit multiple.SOURCEPIED PIPER MODEL

Change a lever and follow the cash

Say you want to test the growth assumption. Follow it through revenue, EBITDA, tax, capex and working capital before looking at the return. Do the same for a financing change: debt affects interest, repayment and the balance left at exit.

In the base example, Year 1 revenue is $108m and EBITDA is $27m. Opening debt is $125m, so opening-balance interest is $10m. Depreciation affects tax; it is not deducted again from cash. After tax, capex and the working-capital increase, $9.25m remains before debt repayment.

YEAR 1 CASH FLOW

Year 1 cash-flow calculation

Subtract interest, cash tax, capex and the working-capital increase from EBITDA.

USD MILLIONS

CASH FLOW BEFORE REPAYMENT9.25
CASH FLOW BEFORE REPAYMENT
9.25
RUNNING TOTAL
9.25
DATA TABLE
Year 1 cash-flow calculation
STEPCHANGERUNNING TOTAL
EBITDA27.0027.00
INTEREST−10.0017.00
CASH TAX−3.7113.29
CAPEX−3.2410.05
WORKING CAPITAL−0.809.25
CASH FLOW BEFORE REPAYMENT9.259.25
Interest is 8% of opening debt. Tax is 25% of positive EBITDA less depreciation and interest.SOURCEPIED PIPER MODEL

The sweep uses cash above the $5m minimum balance and cannot repay more than the debt outstanding. Year 1 ends with $115.75m of debt. If the calculation cannot maintain the cash floor, it raises a cash-shortfall error. Once debt is repaid, further cash accumulates.

Now change the debt amount. The initial equity cheque changes, but so do interest, tax, cash available for repayment and later debt balances. Reusing the original debt schedule would leave out those effects. I’d inspect the annual schedule before spending much time on the new IRR.

REVENUE AND DEBT

Revenue and closing debt

Revenue grows from $100m at acquisition to $146.93m in Year 5. Cash flow repays debt from $125m to $57.48m.

USD MILLIONS

YEAR 5

REVENUE
146.93
CLOSING DEBT
57.48
DATA TABLE
Revenue and closing debt
PERIODENTRYYEAR 1YEAR 2YEAR 3YEAR 4YEAR 5
REVENUE100.00108.00116.64125.97136.05146.93
CLOSING DEBT125.00115.75104.6191.3475.7257.48
Entry and five annual periods. Both series use the same monetary axis.SOURCEPIED PIPER MODEL
BASE-CASE CASH AND DEBT
USD MILLIONS
Base-case revenue, EBITDA, cash deductions and debt balances for acquisition through Year 5. Debt falls from $125m to $57.48m.
MODEL LINEENTRYYEAR 1YEAR 2YEAR 3YEAR 4YEAR 5
REVENUE100.00108.00116.64125.97136.05146.93
EBITDA25.0027.0029.1631.4934.0136.73
DEPRECIATION—2.162.332.522.722.94
OPENING DEBT—125.00115.75104.6191.3475.72
INTEREST—10.009.268.377.316.06
CASH TAX—3.714.395.156.006.93
CAPEX—3.243.503.784.084.41
CHANGE IN WORKING CAPITAL—0.800.860.931.011.09
CASH FLOW BEFORE REPAYMENT—9.2511.1513.2615.6218.24
OPENING CASH—5.005.005.005.005.00
CASH BEFORE REPAYMENT—14.2516.1518.2620.6223.24
DEBT REPAYMENT—9.2511.1513.2615.6218.24
CLOSING DEBT125.00115.75104.6191.3475.7257.48
CLOSING CASH5.005.005.005.005.005.00
CASH SHORTFALL—0.000.000.000.000.00
Year 0 is acquisition. The five annual operating periods use the base assumptions; values are rounded for display.SOURCEPIED PIPER MODEL

At exit, value the business using final-year EBITDA and the exit multiple. This example includes all closing cash in equity proceeds, no exit fees and full sponsor ownership. The equity bridge is $367.33m−$57.48m+$5m=$314.85m\$367.33\mathrm{m}-\$57.48\mathrm{m}+\$5\mathrm{m}=\$314.85\mathrm{m}. Divide the proceeds by initial sponsor equity to get $314.85m$135m≈2.33×\frac{\$314.85\mathrm{m}}{\$135\mathrm{m}}\approx 2.33\times.

YEAR 5 EXIT

Exit equity calculation

$367.33m enterprise value − $57.48m debt + $5m cash = $314.85m equity proceeds.

USD MILLIONS

EQUITY PROCEEDS314.85
EQUITY PROCEEDS
314.85
RUNNING TOTAL
314.85
DATA TABLE
Exit equity calculation
STEPCHANGERUNNING TOTAL
ENTERPRISE VALUE367.33367.33
DEBT−57.48309.85
CASH+5.00314.85
EQUITY PROCEEDS314.85314.85
Returns!B13 includes all closing cash and no exit fees. Values are rounded for display.SOURCEPIED PIPER MODEL

The annualized return is 18.5% over five years with no interim distributions. Distributions on other dates need the corresponding cash-flow calculation; Google distinguishes IRR for periodic cash flows from XIRR for irregular dates.

Run the cases, then the sensitivities

Now take the base case and lower growth while keeping the exit multiple fixed. Inspect how cash available for repayment and the debt balance change. Then vary both inputs across a grid, with annualized equity return in each cell. Name the axes and output so there’s no guessing what the colors mean.

In this public example, every grid point reruns the annual operating and debt schedules. That matters when growth changes working-capital investment or debt changes interest. Keeping the original ending-debt figure would hide part of the effect you’re trying to test.

SENSITIVITY

Revenue growth and exit multiple

Each cell reruns the five-year model with the selected growth rate and exit multiple.

ANNUALIZED EQUITY RETURN

Five-by-five annualized equity-return table. Revenue growth runs from 4% to 12% across five values; exit multiples run from 8× to 12×. The center uses base-case growth and exit assumptions.

EXIT EV / EBITDA

REVENUE GROWTH

SELECTED ASSUMPTIONS

REVENUE GROWTH
8%
EXIT EV / EBITDA
10×
ANNUALIZED EQUITY RETURN
18.5%BASE
SENSITIVITY DATA
Revenue growth and exit multiple
EXIT EV / EBITDA
REVENUE GROWTH8×9×10×11×12×
4%6.1%9.4%12.4%15.1%17.6%
6%9.3%12.5%15.5%18.1%20.6%
8%12.3%15.6%18.5%BASE21.1%23.5%
10%15.3%18.5%21.3%24%26.4%
12%18.1%21.3%24.2%26.8%29.2%
The remaining base-case inputs stay fixed. Each grid point recalculates operating cash flow and debt repayment before calculating the return.SOURCEPIED PIPER MODEL

Start with the center cell and compare it with the base case. Then move along a row or column. An entry-multiple versus exit-multiple table examines purchase price and sale valuation; a growth versus exit-multiple table combines an operating assumption with valuation.

EQUITY MOIC
2.33×
Exit equity proceeds divided by initial sponsor equity.
ANNUALIZED EQUITY RETURN
18.46%
Five-year return with no interim distributions.
EXIT EQUITY
314.85
Includes all closing cash; no exit fees.

The base example produces 2.33× and 18.5%. When another cell looks attractive, open its schedule and inspect the implied revenue, margin, debt balance and cash requirement. Read those rows alongside the return.

Save the input set and calculation result for every run. When you return to the workbook, a chart or return should identify its case. You also need to distinguish a changed assumption from new evidence in an updated forecast.

Put the model in Google Sheets

Once you’ve worked through those cases, put the model into an editable workbook. The compiler assigns inputs and calculations to sheets, cells and formulas. Keep the operating model, debt schedule, cash flow and returns linked to the selected case. A case selector needs to change those references as well as the displayed name.

The downloadable example uses three sheets: Inputs, CashFlow and Returns. Its manifest records 92 formula cells. In CashFlow!C13, =MIN(C5,MAX(C12-Inputs!$B$9,0)) caps repayment at opening debt and cash above the minimum balance. Keep the address and expression together.

BASE-CASE WORKBOOK
USD MILLIONS
  • ASSUMPTION
  • FORMULA
  • RESULT
CELLC6FORMULA=C5*Inputs!$B$10
MODEL LINE
ENTRY
YEAR 1
YEAR 2
YEAR 3
YEAR 4
YEAR 5
2
REVENUE
100.00
108.00
116.64
125.97
136.05
146.93
3
EBITDA
25.00
27.00
29.16
31.49
34.01
36.73
4
DEPRECIATION
—
2.16
2.33
2.52
2.72
2.94
5
OPENING DEBT
—
125.00
115.75
104.61
91.34
75.72
6
INTEREST
—
10.00
9.26
8.37
7.31
6.06
7
CASH TAX
—
3.71
4.39
5.15
6.00
6.93
8
CAPEX
—
3.24
3.50
3.78
4.08
4.41
9
CHANGE IN WORKING CAPITAL
—
0.80
0.86
0.93
1.01
1.09
10
CASH FLOW BEFORE REPAYMENT
—
9.25
11.15
13.26
15.62
18.24
11
OPENING CASH
—
5.00
5.00
5.00
5.00
5.00
12
CASH BEFORE REPAYMENT
—
14.25
16.15
18.26
20.62
23.24
13
DEBT REPAYMENT
—
9.25
11.15
13.26
15.62
18.24
14
CLOSING DEBT
125.00
115.75
104.61
91.34
75.72
57.48
15
CLOSING CASH
5.00
5.00
5.00
5.00
5.00
5.00
16
CASH SHORTFALL
—
0.00
0.00
0.00
0.00
0.00

SELECTED CELL The expression is a compiled workbook formula. Its displayed result comes from the local evaluator.

Select a formula cell to see its address and expression. The values were evaluated locally.SOURCELOCAL FORMULA EVALUATION
WORKBOOK FORMULAS
artifact.json
{  "CashFlow!C6": "=C5*Inputs!$B$10",  "CashFlow!C7": "=MAX(C3-C4-C6,0)*Inputs!$B$11",  "CashFlow!C10": "=C3-C6-C7-C8-C9",  "CashFlow!C13": "=MIN(C5,MAX(C12-Inputs!$B$9,0))",  "Returns!B13": "=B10-B11+B12",  "Returns!B14": "=B13/B7",  "Returns!B15": "=B14^(1/5)-1"}
The manifest records each formula at its workbook address.

Before writing, compare formula coordinates with the manifest and reject unexpected or missing formulas. Write literal contents with RAW, then write only the compiler’s formula cells with USER_ENTERED. Google’s input-option reference explains the difference: RAW leaves strings unparsed; USER_ENTERED interprets them as spreadsheet input.

GOOGLE SHEETS WRITES
export.py
for batch in workbook['value_batches']:    service.spreadsheets().values().batchUpdate(        spreadsheetId=spreadsheet_id, body=batch,    ).execute()
Write literal values with RAW, then the compiler’s formula cells with USER_ENTERED.

The two batch methods do different work. spreadsheets.values.batchUpdate writes cell ranges. spreadsheets.batchUpdate applies structural requests such as formatting, validation and charts. Keep the formatting requests separate from the financial calculation.

A live export needs an authenticated account, a destination and a sharing policy. The example’s helper accepts an authenticated service and leaves sign-in and permissions to the caller. Check Sheets scopes and Drive access rules for that account and destination.

Read it back, then rerun it

Don’t stop at the new spreadsheet URL. Read the formulas and calculated values back. Google’s render-option reference defines FORMULA and UNFORMATTED_VALUE; its cell reference distinguishes entered formulas, effective values and calculation errors.

Check required sheets and ranges, formula errors, financing identities and selected outputs. If the workbook has a case selector, switch it, read the dependent ranges and compare them with that case’s expected results. Then restore the original selection. You want the numbers to move with the case.

  • Entry debt + sponsor equity = enterprise value + fees + funded cash.
  • Opening debt − repayment = closing debt.
  • Opening cash + cash flow before repayment − repayment = closing cash.

The worked example evaluates all 92 formulas locally and compares 85 numeric cells with the Python calculation. Its 102 checks include those comparisons and cash, debt and financing identities. The preview shows those local results; Google Sheets has not calculated them.

BASE-CASE VERIFICATION
artifact.json
{  "checks": 102,  "formula_cells": 92,  "compared_cells": 85,  "tolerance": "0.000001",  "google_sheets_executed": false}
The base-case local run evaluates 92 formulas, compares 85 numeric cells and reports 102 total checks.SOURCELOCAL RUN OUTPUT

One test deliberately changes the closing-debt formula to add repayment and changes the manifest to match. The formula text agrees with the manifest, but Year 1 debt becomes $134.25m instead of $115.75m. The numeric comparison fails. Compare formula text and calculated values separately.

Then your banker sends version two of the forecast. Keep the old file, record the new source version and review which inputs changed before rerunning the cases. A model can balance while using the wrong period or an assumed margin. The source references let you go back to the document and make that decision.

To run the calculation yourself, use python3 run.py from the downloaded example’s root, then python3 -m unittest discover -s . -v for the tests. Change inputs.json to change the assumptions. The download contains the financial calculation, workbook compiler and local verifier; the document-analysis sequence above is pseudocode.

WORKED MODEL
  1. INSTRUCTIONSSKILL.mdCommands and modeling conventions.
  2. INPUTSinputs.jsonEntry, forecast and financing assumptions.
  3. CALCULATIONmodel.pyAnnual cash, debt and equity proceeds.
  4. WORKBOOKworkbook.pyInputs and formulas assigned to cells.
  5. VERIFICATIONverify.pyFormulas evaluated; outputs compared.

EXPORT IS A SEPARATE CALL

The local runner validates inputs, calculates the model, compiles formulas and verifies the workbook.SOURCEEXAMPLE FILES
MODEL AND WORKBOOK EXAMPLE
lbo-example/model.py
import jsonfrom decimal import Decimal, localcontextfrom pathlib import Path D = DecimalZERO = D(0)INPUT_KEYS = {    'company', 'currency', 'units', 'years', 'entry_revenue', 'ebitda_margin',    'revenue_growth', 'entry_multiple', 'exit_multiple', 'entry_debt',    'entry_fees', 'minimum_cash', 'interest_rate', 'tax_rate',    'depreciation_pct', 'capex_pct', 'nwc_pct', 'forecast_ebitda_margin',}  def load_inputs(path: Path) -> dict:    data = json.loads(path.read_text(), parse_float=D, parse_int=D)    return validate_inputs(data)  def validate_inputs(data: dict) -> dict:    if set(data) != INPUT_KEYS:        raise ValueError(f'input_keys: missing={INPUT_KEYS - set(data)} extra={set(data) - INPUT_KEYS}')    result = dict(data)    for key in INPUT_KEYS - {'company', 'currency', 'units'}:        value = D(str(data[key]))        if not value.is_finite():            raise ValueError(f'non_finite: {key}')        result[key] = value    if result['currency'] != 'USD' or result['units'] != 'millions':        raise ValueError('units')    if not isinstance(result['company'], str) or not result['company'].strip():        raise ValueError('company')    if result['years'] != 5:        raise ValueError('years')    result['years'] = int(result['years'])    for key in ('entry_revenue', 'entry_multiple', 'exit_multiple'):        if result[key] <= 0:            raise ValueError(f'non_positive: {key}')    for key in ('entry_debt', 'entry_fees', 'minimum_cash'):        if result[key] < 0:            raise ValueError(f'negative: {key}')    for key in ('ebitda_margin', 'forecast_ebitda_margin', 'interest_rate', 'tax_rate', 'depreciation_pct', 'capex_pct', 'nwc_pct'):        if not ZERO <= result[key] <= 1:            raise ValueError(f'rate: {key}')    if result['revenue_growth'] <= -1:        raise ValueError('revenue_growth')    return result  def calculate(data: dict) -> dict:    a = validate_inputs(data)    with localcontext() as context:        context.prec = 36        entry_ebitda = a['entry_revenue'] * a['ebitda_margin']        enterprise_value = entry_ebitda * a['entry_multiple']        uses = enterprise_value + a['entry_fees'] + a['minimum_cash']        equity = uses - a['entry_debt']        if equity <= 0:            raise ValueError('sponsor_equity')        previous_revenue = a['entry_revenue']        debt = a['entry_debt']        cash = a['minimum_cash']        rows = []        for year in range(1, a['years'] + 1):            revenue = previous_revenue * (1 + a['revenue_growth'])            ebitda = revenue * a['forecast_ebitda_margin']            depreciation = revenue * a['depreciation_pct']            interest = debt * a['interest_rate']            taxes = max(ebitda - depreciation - interest, ZERO) * a['tax_rate']            capex = revenue * a['capex_pct']            delta_nwc = (revenue - previous_revenue) * a['nwc_pct']            free_cash_flow = ebitda - interest - taxes - capex - delta_nwc            cash_before_sweep = cash + free_cash_flow            shortfall = max(a['minimum_cash'] - cash_before_sweep, ZERO)            if shortfall > 0:                raise ValueError(f'cash_shortfall: year={year} amount={shortfall}')            repayment = min(debt, max(cash_before_sweep - a['minimum_cash'], ZERO))            closing_debt = debt - repayment            closing_cash = cash_before_sweep - repayment            rows.append({                'year': year, 'revenue': revenue, 'ebitda': ebitda,                'depreciation': depreciation, 'opening_debt': debt,                'interest': interest, 'taxes': taxes, 'capex': capex,                'delta_nwc': delta_nwc, 'free_cash_flow': free_cash_flow,                'opening_cash': cash, 'cash_before_sweep': cash_before_sweep,                'repayment': repayment, 'closing_debt': closing_debt,                'closing_cash': closing_cash, 'shortfall': shortfall,            })            previous_revenue, debt, cash = revenue, closing_debt, closing_cash        exit_ev = rows[-1]['ebitda'] * a['exit_multiple']        exit_equity = exit_ev - debt + cash        moic = exit_equity / equity        irr = moic ** (D(1) / D(a['years'])) - 1        return {            'entry': {'ebitda': entry_ebitda, 'enterprise_value': enterprise_value,                      'fees': a['entry_fees'], 'cash': a['minimum_cash'],                      'uses': uses, 'debt': a['entry_debt'], 'equity': equity},            'years': rows,            'exit': {'enterprise_value': exit_ev, 'debt': debt, 'cash': cash,                     'equity': exit_equity, 'moic': moic, 'irr': irr},        }  def json_number(value):    if isinstance(value, D):        return float(value)    raise TypeError(type(value).__name__)
The download contains the financial calculation, workbook compiler, local verifier and export helper. Agent orchestration is shown separately as pseudocode.SOURCEEXAMPLE FILES
DOWNLOAD EXAMPLE
  1. 1The worked model includes all closing cash in equity proceeds. It assumes no interim dividends, no exit fees and full sponsor ownership.
  2. 2Keep reported values, calculated amounts and analyst assumptions distinct. A passing calculation does not establish the source of an input.