ntro.valuations turns a property’s facts and a set of assumptions into a
forecast cashflow and the comparable valuation lanes the Analysis UI renders.
It has three parts: a pure engine, a typed domain model, and a versioned
store.
The whole namespace obeys one law:
value = f(facts, assumptions, engine_version)
Outputs are never an input. You store the facts (assets) and the assumptions,
record which engine produced a snapshot, and re-derive (or read a cache) on
demand. Four data kinds flow through:
| Kind | What | Where it lives |
|---|---|---|
| Facts | The asset(s): units, income streams, facilities | ledgers.assets |
| Assumptions | Growth, occupancy, opex, tax, debt terms | analysis.valuations.assumption_set |
| Derived | The forecast cashflow (a cache) | analysis.valuations.forecast_snapshot |
| Actuals | Realised book value / cashflow from the GL | analysis.valuations (kind="actuals") |
Engine
run_model is deterministic, pure, and entity-agnostic — same inputs, same
bytes out. It is the single source of truth; the ui-tenant sensitivity controls
call this one engine rather than reimplementing it.
ENGINE_VERSION (re-0.1.0) stamps every snapshot. Bump it when the calc
changes; cached forecasts produced by an older version are treated as stale.
Output shape
MonthRow — one row per model month:
| Field | Meaning |
|---|---|
month_index, year, month | Position in the model |
effective_occupancy | Occupancy after the ramp |
total_income | Gross rental + parking + EV + ancillary |
total_direct_costs, gross_profit | After direct costs |
total_opex, ebitda | NOI (= ebitda) |
loan_opening, interest, arrangement_fee, loan_closing | Financing |
npbt, corporation_tax, npat | Levered, pre / post tax |
capex | Cash capex outflow (lumpy, negative) |
free_cash_flow | FCFE — after tax, after capex |
property_nbv | Carrying value (net book value) at month-end |
FYRow — fiscal-year rollup: gross_rental_income, ebitda, npat,
gross_yield, net_yield, property_value (NBV at the FY-end month).
Domain model
ntro.valuations.models is the typed input surface. All money is Decimal.
| Symbol | Purpose |
|---|---|
Asset | A modelled property / unit — income_streams, optional facility, asset_type, provenance |
IncomeStream | One revenue line — kind (rent / parking / ev / …), monthly_amount, grows_with_rent, subject_to_occupancy, optional metered |
MeteredIncome | Usage-priced income — capacity, hours_per_day, utilisation, rate_per_unit |
Facility | Debt — loan, interest_rate_pa, term_years (0 = interest-only); exposes derived LTV / DSCR |
PeriodicOpex | A cost that applies only in certain calendar months (e.g. insurance) |
RealEstateAssumptions | The full assumption set — horizon, occupancy ramp, growth, opex, tax, capex cycle, mortgage |
CashflowModel | A DCF set: assumptions + assets + a cached forecast. headline() → comparable value / yield / ERV |
PointValuation | A point set (Bank lane): observed value / equivalent_yield / estimated_rental_value / valuation_date. headline() matches CashflowModel.headline() so lanes line up |
Store
ntro.valuations.store persists versioned valuation sets. Two set kinds,
discriminated by engine_version:
| Kind | Save / load | engine_version |
|---|---|---|
| DCF (Forecast / Projected) | save_model / load_model | the engine’s (e.g. re-0.1.0) |
| Point (Bank) | save_point_valuation / load_point_valuation | "" (the discriminator) |
save_* for an (entity, label) writes model_version = max(existing) + 1
— old versions are retained so you can diff Forecast-v2 vs v3, or Bank vs
Indicative.
Caching: inputs_hash / forecast_is_stale
A saved DCF set caches its forecast snapshot keyed by inputs_hash — a sha256
of (assumptions, assets, engine_version) via compute_inputs_hash. Before
trusting a cached forecast, check forecast_is_stale(row, assumptions, assets):
True when the inputs or the engine version moved, and you should re-run_model.
Storage layout
Two schemas, deliberately split:ledgers.assets— the facts (oneAssetRowper asset). Lives in theledgersschema alongside the GL because it is reconciliation-grade truth.analysis.valuations— the forward / analytical model (assumption set, engine version, cached forecast, asset references). Theanalysisschema is the forward lane, never an accounting ledger. This is the model’s own store — not a subledger you code against directly; go throughntro.valuations.store.
CashflowModel references the AssetRow ids it was built from, so re-loading
reconstructs the exact facts the snapshot was derived from.
Worked example
The engine is authoritative. The Analysis page’s sensitivity controls send
adjusted assumptions to this same
run_model — there is no JavaScript
reimplementation to drift out of sync.Related
assets
AssetRow — the per-title facts the engine models.Subledgers overview
Row base + the ledgers / analysis schema split.Data capability
Data-plane access used by the store helpers.
Typing
Period and the shared field types.