Skip to content

About

Values private companies from their financial statements using industry market multiples. The model reads the documents; the arithmetic is deterministic.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Valuation Engine

Market-multiple valuation of private companies from their published financial statements.

Upload an income statement and a balance sheet, name the industry, and get back a valuation across three methods — price/earnings, EV/EBITDA (with an EBIT fallback) and price/sales — each showing the industry benchmark it used and the reliability checks it passed or failed, reduced to a single concluded range.

Every figure is computed deterministically. The language model transcribes the statements into structured line items; it performs no arithmetic. The same inputs always produce the same valuation, and every number in a report can be traced to a formula in src/valuation/domain/.


Quick start

python -m venv .venv && . .venv/Scripts/activate     # Windows
# python -m venv .venv && source .venv/bin/activate  # Linux / macOS

pip install -e ".[dev]"
cp .env.example .env          # then set GOOGLE_API_KEY
python -m valuation

The API is on http://127.0.0.1:8000, with interactive docs at /docs.

Nothing above needs an API key if you set LLM__PROVIDER=mock — the whole pipeline runs offline, and the POST /v1/valuations/compute endpoint never touches a model at all.

Try it

# Resolve a business description to a benchmark industry (no model call)
curl -s localhost:8000/v1/industries/classify \
  -H 'content-type: application/json' \
  -d '{"query": "B2B SaaS platform for logistics"}'

# Value a company from figures you already hold (no model call, no upload)
curl -s localhost:8000/v1/valuations/compute \
  -H 'content-type: application/json' \
  -d '{
        "industry": "enterprise software",
        "company_name": "Acme Ltd",
        "currency": "USD",
        "units_label": "in 000",
        "scale_multiplier": 1000,
        "periods": [
          {"fiscal_year": 2023, "revenue": 240000, "net_income": 30000,
           "profit_before_tax": 50000, "interest_expense": 6000, "tax_expense": 20000,
           "depreciation_amortisation": 12000, "total_assets": 260000,
           "total_equity": 95000, "non_current_debt": 40000,
           "cash_and_equivalents": 9000},
          {"fiscal_year": 2022, "revenue": 200000, "net_income": 24000,
           "profit_before_tax": 40000, "interest_expense": 5000, "tax_expense": 16000,
           "depreciation_amortisation": 10000, "total_assets": 235000,
           "total_equity": 78000, "non_current_debt": 45000,
           "cash_and_equivalents": 6000}
        ]
      }'

# Value a company from its statements
curl -s -X POST 'localhost:8000/v1/valuations?wait=true' \
  -F 'files=@income_statement.pdf' \
  -F 'files=@balance_sheet.pdf' \
  -F 'industry=cement manufacturing'

API

Method Path Purpose
GET /health, /ready Liveness and readiness probes
GET /v1/industries The benchmark catalogue, with an optional ?search= filter
GET /v1/industries/{key} One industry's multiples, growth rates and thresholds
POST /v1/industries/classify Resolve free text to an industry, with scored alternatives
POST /v1/valuations Submit statements; add ?wait=true to block for the report
GET /v1/valuations/{run_id} Run status, stage, token usage and the report once ready
GET /v1/valuations/{run_id}/report Just the report
POST /v1/valuations/compute Value from supplied figures — deterministic, no model, no upload

Errors carry a stable code, a message and optional details. Every response echoes an X-Request-ID header; quote it when reporting a problem.

Configuration

Environment variables, or a .env file. Nested settings use a double underscore. See .env.example for the full set.

Variable Default Purpose
LLM__PROVIDER google google, anthropic, openai or mock
LLM__MODEL per provider Overrides the provider's default model
GOOGLE_API_KEY / ANTHROPIC_API_KEY / OPENAI_API_KEY — Key for the selected provider
API__API_KEYS (empty) Comma-separated keys for X-API-Key. Empty means unauthenticated
API__CORS_ALLOW_ORIGINS (empty) Comma-separated allowed origins
DOCUMENTS__MAX_FILE_BYTES 26214400 Per-file upload limit
RUNS__TTL_SECONDS 86400 How long a finished run is retained
ENVIRONMENT local local, staging or production

The default provider model is gemini-2.5-flash; Anthropic and OpenAI default to claude-sonnet-5 and gpt-4.1. Switching provider is a one-line config change — no code imports a vendor SDK.

How a valuation is produced

 upload ──▶ parse ──▶ extract ──▶ normalise ──▶ classify ──▶ compute ──▶ summarise
            PDF/XLSX   LLM +       units,        lexical,     multiples,   weighted
            → text     schema      signs         model        constraints  range
                       ▲                         arbitration
                       └── the only model call in the numeric path,
                           and it returns line items, never figures it derived
  1. Parse — PDF via pypdf, workbooks via openpyxl, both off the event loop.
  2. Extract — one schema-enforced model call returns the statement line items.
  3. Normalise — apply the statement's scale (Rs. in '000 → ×1000) and coerce expense signs.
  4. Classify — resolve the industry lexically; consult the model only when the lexical score is weak, and only to pick from the real taxonomy.
  5. Compute — derive growth, volatility and leverage; apply each method's multiple; evaluate its constraints.
  6. Summarise — reduce the applicable methods to a low/central/high range, weighted by how cleanly each passed its checks.

Read ARCHITECTURE.md for the reasoning behind the layering, and MIGRATION.md for what changed from the six-service predecessor and the defects that motivated it.

Valuation methods

Method Value Driver Hard constraints Soft constraints
Price / Earnings Equity Latest net income Latest earnings > 0; every period profitable Earnings volatility ≤ industry limit; income growth vs industry ≥ threshold
EV / EBITDA (EBIT fallback) Enterprise → equity Latest EBITDA or EBIT Operating earnings > 0 Debt/assets and debt/equity ≤ industry limits; net debt known
Price / Sales Equity Latest revenue Latest revenue > 0 Revenue growth vs industry ≥ threshold

A hard failure withholds the method — the arithmetic would produce a number without meaning. A soft failure reports the value, flags the breach, and halves its weight in the concluded range. A constraint that could not be tested is recorded as untested, never as passed.

Enterprise values are bridged to equity by subtracting net debt. Where net debt is unknown the enterprise figure is reported on its own but excluded from the range, because it measures a different quantity from the two equity methods.

Benchmark data

data/industry_benchmarks.json holds 94 industries: growth CAGRs, four multiples and five risk thresholds each. It is versioned, and every report records the dataset_version it was computed against.

The file is generated by scripts/build_benchmarks.py, which merges the four partial copies the legacy services each carried and asserts that every overlapping column agrees before writing. To refresh the multiples, edit the JSON and bump dataset_version.

Testing

pytest                              # full suite, no network, no API key
pytest --cov=valuation --cov-report=term-missing
ruff check . && ruff format --check .
mypy

The domain layer is pure, so its tests are ordinary function calls. The pipeline and API tests run against MockClient, which serves scripted responses — the whole product is exercisable offline.

Deployment

docker compose -f docker/docker-compose.yml up --build

The image runs as a non-root user and speaks plain HTTP; terminate TLS at your ingress. Scale with replicas rather than in-process workers: the default run store is per-process, and python -m valuation refuses to start with WEB_CONCURRENCY > 1 rather than serve intermittent 404s on run polling.

Licence

Proprietary.

About

Values private companies from their financial statements using industry market multiples. The model reads the documents; the arithmetic is deterministic.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages