> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ntropii.com/llms.txt
> Use this file to discover all available pages before exploring further.

# ntro.valuations

> ntro.valuations — a deterministic real-estate cashflow engine, a typed domain model, and versioned storage for valuation sets.

**`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`](/sdk/subledgers/types/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.

```python theme={null}
from ntro.valuations import run_model, ENGINE_VERSION

result = run_model(assumptions, assets)   # -> CashflowResult
result.engine_version    # "re-0.1.0"
result.months            # list[MonthRow] — the monthly ladder
result.fiscal_years      # list[FYRow]   — FY rollups
```

`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)       |

```python theme={null}
from ntro.valuations import (
    save_model, load_model,
    save_point_valuation, load_point_valuation,
    list_valuation_sets,
)

# Latest version of every lane for an entity — what the Analysis page reads.
sets = await list_valuation_sets(entity_id=eid, tenant_slug="acme")
# -> [{label, basis, version, engine_version, period, headline}, …]
```

Each `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** (one [`AssetRow`](/sdk/subledgers/types/assets)
  per asset). Lives in the `ledgers` schema alongside the GL because it is
  reconciliation-grade truth.
* **`analysis.valuations`** — the **forward / analytical** model (assumption set,
  engine version, cached forecast, asset references). The `analysis` schema is
  the forward lane, never an accounting ledger. This is the model's own store —
  not a subledger you code against directly; go through `ntro.valuations.store`.

A `CashflowModel` references the `AssetRow` ids it was built from, so re-loading
reconstructs the exact facts the snapshot was derived from.

## Worked example

```python theme={null}
from decimal import Decimal
from uuid import UUID
from ntro.valuations import (
    Asset, IncomeStream, Facility, RealEstateAssumptions,
    run_model, save_model, load_model,
)

assumptions = RealEstateAssumptions(
    model_years=15, start_year=2026, start_month=1, fiscal_year_end_month=9,
    occupancy_ramp=[Decimal("0.5"), Decimal("0.8"), Decimal("1")],
    rental_growth_pa=Decimal("0.03"), opex_inflation_pa=Decimal("0.02"),
    building_appreciation_pa=Decimal("0.02"), building_value_cost=Decimal("1200000"),
    number_of_assets=10, vat_rate=Decimal("0.2"), corp_tax_rate=Decimal("0.19"),
    average_tenancy_months=Decimal("12"), agent_fee_rate=Decimal("0.1"),
    letup_fee=Decimal("500"), dilapidation=Decimal("0"),
    opex_monthly={"management": Decimal("400")},
    sinking_fund_rate_pa=Decimal("0"), capex_refurb_year=10,
    capex_cost_per_asset=Decimal("8000"),
    mortgage_amount=Decimal("1200000"), arrangement_fee_rate=Decimal("0.01"),
    interest_rate_pa=Decimal("0.06"),
)

assets = [
    Asset(
        name="Flat 1",
        income_streams=[IncomeStream(kind="rent", monthly_amount=Decimal("1500"))],
        facility=Facility(loan=Decimal("120000"), interest_rate_pa=Decimal("0.06")),
    ),
    # … 9 more
]

# Derive
result = run_model(assumptions, assets)
print(result.fiscal_years[0].property_value, result.months[0].free_cash_flow)

# Persist a new version (DCF set) with the forecast cached
version = await save_model(
    entity_id=UUID("…"), task_id=UUID("…"), period="2026-01",
    assumptions=assumptions, assets=assets,
    label="Projected", basis="Forecast",
    forecast=result.model_dump(mode="json"), tenant_slug="acme",
)

# Read it back and re-run (cache-or-recompute is the caller's choice)
model = await load_model(entity_id=UUID("…"), label="Projected", tenant_slug="acme")
fresh = run_model(model.assumptions, model.assets)
```

<Note>
  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.
</Note>

## Related

<CardGroup cols={2}>
  <Card title="assets" icon="building" href="/sdk/subledgers/types/assets">
    `AssetRow` — the per-title facts the engine models.
  </Card>

  <Card title="Subledgers overview" icon="table" href="/sdk/subledgers">
    `Row` base + the `ledgers` / `analysis` schema split.
  </Card>

  <Card title="Data capability" icon="database" href="/sdk/capabilities/data">
    Data-plane access used by the store helpers.
  </Card>

  <Card title="Typing" icon="types" href="/sdk/typing">
    `Period` and the shared field types.
  </Card>
</CardGroup>
