# Candidate states

> What NEW, UNCHANGED, CHANGED, OWNED, ORDERED, BLOCKED and STALE mean on screener candidates, and how resumable screener runs work.

Source: https://investi.live/docs/concepts/candidate-states

The 52-week-low screener surfaces names near their lows and tells you, for each one, whether your last analysis still holds. That answer is the candidate state. States exist so you spend time only on names where something actually changed. See [Discover](/docs/app/discover) for the screen itself.

## How a state is assigned

For every candidate, Investi runs a quick, price-only evaluation under your current policy. Price, ATR, the 52-week low and market cap come from the latest scan. Fundamentals, filing flags and earnings dates are carried over from your last full analysis of that name, and are unknown if there is none. The result is compared with the stored full analysis:

| State | Assigned when |
| --- | --- |
| `BLOCKED` | The name matches one of your hard rules (sector, industry, ticker, minimum market cap or minimum price). The reason names the rule. |
| `STALE` | The scan data for the name is more than 36 hours old, or a full analysis exists and the quick evaluation comes back INSUFFICIENT. |
| `NEW` | You have never run a full analysis of this name. |
| `CHANGED` | The policy version tag differs from the one your last analysis used, or the input fingerprint differs, or your last full analysis is more than 7 days old. |
| `UNCHANGED` | A prior analysis exists, under the same policy, with the same input fingerprint, and it is no more than 7 days old. |
| `OWNED` | You hold a position: your fills net to a nonzero quantity, or a position snapshot shows shares. |
| `ORDERED` | You have a working order for the name. |

A review brought in from your own tracker ([verdict history import](/docs/getting-started/import-history#verdict-history)) counts as a prior analysis but has no engine inputs to compare, so it is judged by verdict alone and never expires by age: `UNCHANGED` while today's verdict matches your review (or while only a price check exists), `CHANGED` once it differs, and never `NEW` or `STALE`.

NEW, CHANGED and UNCHANGED are mutually exclusive; exactly one of them always applies. The others are added on top, so a name can be OWNED and CHANGED at once.

A never-analyzed name lacks fundamentals by design, so its quick evaluation often comes back INSUFFICIENT. That is reported as NEW, not STALE. STALE for that reason applies only once a full analysis exists.

### The reason line

Each candidate carries one reason sentence, taken from the first state that produced one, in the order above:

- "Blocked by rule: No Health Care sector"
- "Data is 41h old"
- "Never analyzed under the current record"
- "Policy changed (quality-velocity@v2#... → quality-velocity@v3#...)"
- "Verdict YELLOW → GREEN" or "Material inputs changed"
- "Prior GREEN analysis still valid"

### The primary badge

When a name has several states, one leads, in this priority:

1. BLOCKED
2. STALE
3. CHANGED
4. NEW
5. ORDERED
6. OWNED
7. UNCHANGED

### What CHANGED really measures

CHANGED compares fingerprints, not prices. The fingerprint leaves out the last price and rounds or buckets noisy inputs, so a quote ticking up and down does not flip a name to CHANGED. A change in rule outcomes does, as does a new policy version. See [The input fingerprint](/docs/concepts/verdicts-and-policy#the-input-fingerprint).

A name needs a full analysis when it is NEW or CHANGED.

## Screener runs

A run scans a universe of listed US equities, records price metrics for each name, then queues full analyses where they are needed. It is a durable job stored in Postgres, processed in small steps so each step fits inside one serverless request.

### Starting a run

`POST /api/v1/screener/runs` (or the run button on Discover) builds the universe from Nasdaq's listings:

- market cap at or above your screener minimum (a Settings value),
- last price of at least $5,
- country United States (or unspecified) and a plain ticker of one to five letters with an optional class suffix such as `.B`,
- ordered with names you own first, then names closest to their lows in earlier scans, then by market cap,
- capped at your screener maximum number of candidates (a Settings value).

The run is identified by a screen hash of the filters, the date and your policy version. Starting a run while one with the same hash is still running returns the existing run instead of creating a duplicate.

### Steps

`POST /api/v1/screener/runs/{id}/step` processes the next 24 names. For each, it fetches two years of daily bars and stores the last price, 52-week low and high with dates, distance above the low, 14-day ATR, recent new lows, 30-session dollar volume, 50-day direction, 5 and 21 session returns, and a 30-day sparkline. Names with fewer than 30 bars, or that cannot be resolved to a security, are counted as skipped. Errors are counted as failures, and up to 10 error messages per step are kept.

The cursor advances atomically before the work starts. Two step calls running at once take different chunks, and a step that dies part way never makes the next one repeat earlier names. That is what makes a run resumable: close the tab, come back, and keep stepping the same run id. When the cursor passes the end of the universe, the run's status moves from `running` to `scanned`.

### Analysis queue

`POST /api/v1/screener/analyze?run={id}` runs full analyses for up to 4 names per call. It picks candidates that need analysis (NEW or CHANGED), are not BLOCKED, and sit within one and a half times their own ATR-scaled entry gate. Names whose low ATR puts them on the dormant list are skipped. Each full analysis stores a new evaluation revision if its fingerprint changed.

The run records how many names were analyzed, how many were reused (their prior analysis still holds), how many sit inside their gate, and which newly analyzed names came back GREEN. When the queue is empty, the run is marked `complete`.

### Reading results

- `GET /api/v1/screener/candidates` returns candidates with their states, verdicts and price metrics, ordered by distance above the low. It includes names up to 25% above their low by default; pass `maxPctAboveLow` to change that.
- `GET /api/v1/screener/runs` returns your latest run, and `GET /api/v1/screener/runs/{id}` a specific run with its stats.

All screener endpoints need the `research:read` scope. See the [REST API](/docs/integrations/rest-api).

> **Note:** Scan rows are a shared cache of public price metrics, so a scan by any user refreshes them. States, verdicts, ownership and orders are always computed from your own records and policy.
