# Modeling

The modeling routes return derived analytics — margins, returns, growth rates,
TTM aggregates — computed against a fixed point-in-time snapshot rather than
against today's data. That anchoring is the point: a ratio is only reproducible
if the statement versions behind it are pinned.

Every route here keys off `company_id`, the UUID, not the CIK. Get one from
[`/v1/identity/resolve`](https://docs.arche.fi/identity).

## Snapshot first

Every modeling read requires `as_of`, and that date must name a snapshot that
already exists. Without one the route returns `404`:

```json
{
  "error": {
    "code": "NOT_FOUND",
    "http_status": 404,
    "message": "As-of snapshot not found for modeling ratios query.",
    "details": {},
    "trace_id": "…"
  }
}
```

> **Warning:** This is the single most common surprise on these routes. A `404`
> here does not mean the company is uncovered or the date is invalid — it
> means nobody has built a snapshot for that date yet. Build one, then read.

The modeling reads and snapshot builds are both available from Growth, so a
Growth key can build the snapshot it then reads.

## Build a snapshot

`POST /v1/modeling/universe/pit-snapshot` computes the snapshot and returns the
metric rows it produced. It takes up to 50 CIKs at a time:

```bash
curl -X POST "https://api.arche.fi/v1/modeling/universe/pit-snapshot" \
  -H "X-Api-Key: $ARCHE_API_KEY" \
  -H "X-Request-ID: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "ciks": ["0000320193", "0000789019"],
    "as_of_date": "2025-06-30",
    "statement_types": ["INCOME_STATEMENT"],
    "metrics_bundle": "tier1"
  }'
```

All four fields are required. `metrics_bundle` is `all` or `tier1`.

```json
{
  "page": 1,
  "page_size": 50,
  "total": 12,
  "items": [
    {
      "cik": "0000320193",
      "statement_type": "INCOME_STATEMENT",
      "metric_name": "BASIC_EPS",
      "metric_value": "1.650000",
      "statement_date": "2025-03-29",
      "fiscal_year": 2025,
      "fiscal_period": "Q2",
      "_meta": {
        "snapshot_id": "2128682cf89d4e3a364918c482ca249c2d888b066de903f559bb67cf0b0e48cb",
        "as_of_snapshot_id": "48e9af60-bab1-4fb5-9027-704bdfc9e0e6",
        "snapshot_hash": "26f836b84110370870b2a9239d058669f816c3fefc09431e329569508ea48156",
        "statement_version_id": "003c8265-6a73-4a41-bf80-3b78e7d8ddca",
        "strategy_id": "accepted_at_fallback_filed_at_eod_utc_source_preferred_own_period_acceptance_ordered_v4"
      }
    }
  ]
}
```

A large build can answer `202` instead of `200`, meaning the snapshot is still
being computed. Retry the read rather than the build.

Every row carries the `statement_version_id` it came from and the
`strategy_id` that selected it, so a figure in a model traces back to a specific
filed statement. Record `as_of_snapshot_id` alongside your model run — it is
what makes the run reproducible.

## Model periods

The normalized per-period rows the other routes are computed from. This is the
route to read when you want the inputs rather than the derived figures:

```bash
curl -X GET "https://api.arche.fi/v1/modeling/companies/a2cb31b5-dc0f-4a17-a665-e41f59cb8e0a/periods?as_of=2025-06-30&page_size=50" \
  -H "X-Api-Key: $ARCHE_API_KEY" \
  -H "X-Request-ID: $(uuidgen)" \
  -H "Accept: application/json"
```

Each row names its statement identity, its `version_sequence`, the
`statement_version_id` behind it, and `normalization_provenance_id` — so you can
carry a model input back to [its provenance](https://docs.arche.fi/provenance).

`/v1/modeling/companies/{company_id}/multi-year` returns the same rows arranged
for multi-year comparison, with the valuation inputs computed from each one.
A period row holds one statement, so an income-statement row has no balance
sheet inputs; each null input names why in `unavailable_metrics`.

## Ratios

Margins, returns and capital-structure ratios for each period in the snapshot:

```bash
curl -X GET "https://api.arche.fi/v1/modeling/companies/a2cb31b5-dc0f-4a17-a665-e41f59cb8e0a/ratios?as_of=2025-06-30&page_size=50" \
  -H "X-Api-Key: $ARCHE_API_KEY" \
  -H "X-Request-ID: $(uuidgen)" \
  -H "Accept: application/json"
```

Representative response:

```json
{
  "page": 1,
  "page_size": 50,
  "total": 67,
  "items": [
    {
      "model_ratio_id": "8baeabfd-6ef4-4e07-952a-73e0f8b64dbe",
      "company_id": "a2cb31b5-dc0f-4a17-a665-e41f59cb8e0a",
      "cik": "0000320193",
      "statement_type": "INCOME_STATEMENT",
      "accounting_standard": "US_GAAP",
      "statement_date": "2025-03-29",
      "fiscal_year": 2025,
      "fiscal_period": "Q2",
      "currency": "USD",
      "version_sequence": 6,
      "as_of_snapshot_id": "48e9af60-bab1-4fb5-9027-704bdfc9e0e6",
      "as_of_timestamp": "2025-06-30T23:59:59Z",
      "snapshot_hash": "26f836b84110370870b2a9239d058669f816c3fefc09431e329569508ea48156",
      "metrics": {
        "GROSS_MARGIN": "0.470506",
        "OPERATING_MARGIN": "0.310291",
        "NET_MARGIN": "0.259860",
        "ROE": "0.370980",
        "ROA": "0.074811",
        "ROIC": "0.191197",
        "DEBT_TO_EQUITY": "1.380382",
        "CURRENT_RATIO": "0.820870",
        "QUICK_RATIO": "0.516245",
        "BOOK_VALUE_PER_SHARE": "4.436465",
        "DSO": "24.667205",
        "DPO": "96.477462",
        "INVENTORY_TURNOVER": "8.054235",
        "CASH_CONVERSION_CYCLE": "-60.636012"
      },
      "unavailable_metrics": [
        {
          "metric": "INTEREST_COVERAGE",
          "reason": "MISSING_INPUT",
          "missing_inputs": ["INTEREST_EXPENSE"]
        }
      ]
    }
  ]
}
```

Ratios live in `metrics` as decimal strings, not floats — `"0.470506"` is a
47.05% gross margin. Keep them as strings or decimals; parsing to binary
floating point is how a reported figure stops reconciling.

> **Note:** Ratio names such as `GROSS_MARGIN` and `ROE` exist
> **only here**. They are derived, so they are not canonical
> statement metrics and `/v1/coverage/metrics/GROSS_MARGIN` reports
> zeros for them. See [Metric catalog](https://docs.arche.fi/metrics) for which names
> live where.

## Growth

Year-over-year, quarter-over-quarter and CAGR figures, with `cagr_years` naming
the window:

```bash
curl -X GET "https://api.arche.fi/v1/modeling/companies/a2cb31b5-dc0f-4a17-a665-e41f59cb8e0a/growth?as_of=2025-06-30&page_size=50" \
  -H "X-Api-Key: $ARCHE_API_KEY" \
  -H "X-Request-ID: $(uuidgen)" \
  -H "Accept: application/json"
```

Rows carry `REVENUE_GROWTH_YOY`, `REVENUE_GROWTH_QOQ`, `NET_INCOME_GROWTH_YOY`
and similar in `metrics`, in the same decimal-string form.

## Trailing twelve months

TTM aggregates, with the window stated on every row so a figure is never
ambiguous about what it sums:

```bash
curl -X GET "https://api.arche.fi/v1/modeling/companies/a2cb31b5-dc0f-4a17-a665-e41f59cb8e0a/ttm?as_of=2025-06-30&page_size=50" \
  -H "X-Api-Key: $ARCHE_API_KEY" \
  -H "X-Request-ID: $(uuidgen)" \
  -H "Accept: application/json"
```

Each row carries `ttm_window_start` and `ttm_window_end` beside the summed
`metrics`. Check the window before comparing two companies: a TTM figure that
ends on a different date is a different measurement.

A line is totalled only when every quarter in the window reports it. A line
some quarter omits is listed in `unavailable_metrics` as
`INCOMPLETE_TRAILING_YEAR` rather than summed over fewer than four quarters.

## When a metric can't be computed

Ratio, growth and multi-year rows never leave a metric silently absent. Each
metric the route computes that a row does not carry is listed in
`unavailable_metrics`, with the reason and, when an unreported input is the
cause, the canonical lines that were missing. TTM rows list the lines their
window reports but cannot total. In the ratios example above,
Apple's income statement for the quarter has no interest expense line, so
interest coverage has no divisor:

```json
"unavailable_metrics": [
  {
    "metric": "INTEREST_COVERAGE",
    "reason": "MISSING_INPUT",
    "missing_inputs": ["INTEREST_EXPENSE"]
  }
]
```

`reason` is one of a fixed set, so code can branch on it:

| `reason`                   | Meaning                                                                                                             |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `NOT_REPORTED`             | The filing does not report the line for the period.                                                                 |
| `NOT_ON_STATEMENT`         | The value comes from a different statement than the row. Read that statement's row for the period.                  |
| `MISSING_INPUT`            | An input the formula needs is not reported for the period. `missing_inputs` names it.                               |
| `DENOMINATOR_ZERO`         | Every input is present, but the divisor is zero.                                                                    |
| `NON_POSITIVE_VALUE`       | The formula is defined only for positive values, such as a CAGR base or a share count, and one is zero or negative. |
| `NO_COMPARABLE_PERIOD`     | A growth rate has no earlier period of the same length to compare against.                                          |
| `NOT_APPLICABLE_TO_PERIOD` | The metric is not defined for this kind of period, such as quarter-over-quarter growth on an annual row.            |
| `INCOMPLETE_TRAILING_YEAR` | Not every quarter of the trailing twelve months reports the line.                                                   |
| `PENDING_REBUILD`          | The inputs now support a value that the stored row predates. The next model build fills it.                         |

On ratios and growth, every metric in the route's set is in `metrics` or in
`unavailable_metrics`. On multi-year, every `null` in `valuation_inputs` has an
entry. Model periods carry the statement as reported, so a line absent from
their `metrics` was not reported.

## Coverage varies by company

A snapshot covering a company does not guarantee every derived table has rows
for it. In the same snapshot above, Apple returns 67 ratio rows and Microsoft
returns none, while both return model periods.

An empty `items` with `total: 0` here means the derived rows were not computed
for that company, not that the company is uncovered. Read
`/v1/modeling/companies/{company_id}/periods` to confirm the inputs exist.
Within a row that does exist, a missing ratio is never zero: its reason is in
`unavailable_metrics`.

## Next steps

- [Company identity](https://docs.arche.fi/identity): Resolve tickers to full issuer records, search by name, and read mapping validity.
- [Time series](https://docs.arche.fi/time-series): Pull a metric across a whole reporting history, point-in-time or as-reported.
- [Metric catalog](https://docs.arche.fi/metrics): Look up canonical and derived metric names, formulas, bundles and segments.
