Skip to content

Screener Product Surface

VerifiedOwner Frontend / productLast verified frontend@99f49dae6368414896bf2858cb848f5a7c7fcf98

The screener is a center-zone dashboard widget (src/screener/**) that renders live, multi-symbol, multi-feature rows against a registry the backend publishes. This page describes the frontend's product surface only — what it renders, how a screen is defined and evaluated in the browser, and how it refreshes. The screener engine — the Rust process that computes feature values across the instrument universe — is a backend concern documented separately at ../backend/screener.md; this page does not restate its internals.

What the frontend reads

The screener client (src/screener/gateway-v2.ts) is built against the spine.v2 screener surface: GET /v2/screener/features (the feature registry), GET /v2/screener/snapshot and /rows (current rows), GET /v2/screener/health, GET /v2/screener/observations (pattern overlays), POST /v2/screener/screen (server-side screen evaluation), and GET /v2/stream/screener (the live delta stream) — all through the same fetchV2/postV2Command/subscribeToV2Stream transport the rest of the app uses. The module's own comment records that this exact route set was "VERIFIED LIVE" by a read-only probe against a spine gateway at an earlier point in the project, including concrete shape facts (e.g. /v2/screener/health is both the spine.v2 envelope and a complete onix.screener_health document in one flat object) — a historical, point-in-time observation, not a claim about any currently running deployment (see "Status" below).

Every document this module reads is decoded through the same ajv-compiled, vendored-schema validation the rest of the app uses (onix.feature_row, onix.feature_row_delta, onix.feature_spec, onix.pattern_observation, onix.screener_health, onix.screener_snapshot) — see client-state-and-cache.md. A CORRECT operation arriving on a DTO whose own contract promises only UPSERT/RETIRE is treated as a genuine contract violation and surfaced, not silently widened.

Features are multi-output; columns follow outputs, not features

The backend's feature registry (onix.feature_spec) is multi-output: one feature (e.g. volume_spike) can publish several named outputs (e.g. a relative_volume value), each with its own declared value type, decimal scale, and refresh cadence. src/screener/columns.ts builds one table column per output, not per feature — a three-output feature contributes three columns, each keyed exactly the way a row's values map is keyed ("<feature_id>.<output_name>"). Column headers render the backend's declared identifier verbatim; the module's own comment is explicit that "the registry declares no display label or unit for an output," so nothing here invents an English name. The one basis for visually highlighting a cell is a threshold the feature itself declares in its own registered parameters — never a client-side guess at what counts as notable.

A small set of "stable" columns — raw symbol, venue, last close, freshness — are facts about row identity, not a view preference: they are never hidden, reordered, or persisted. Which feature outputs are visible and in what order is a persisted view preference (see below).

What "client-side" versus "backend-computed" means here

Every feature value shown in the table — the number, its warmup state, its freshness — is computed by the backend and rendered as published; the frontend does not calculate, interpolate, or resample a feature value. What does run client-side:

  • Column layout and visibility — which outputs are shown, in what order — is local view state (src/screener/persistence.ts), not a backend fact.
  • Screen definitions (a saved set of filter conditions, a sort, and a rank) are validated against the currently-loaded feature registry entirely in the browser before they are ever submitted (src/screener/screen.ts's validateScreenDefinition), so a screen naming an output the registry no longer has degrades safely rather than reaching the backend as a malformed request. The same pure evaluation logic (evaluateScreen) can also be run directly over already-fetched rows — used by the workspace UI and by the automated Chart-MCP bridge's applyScreen — for immediate, no-round-trip feedback while a saved screen is being edited.
  • Canonical, ranked results for an actually-submitted screen come from the backend via POST /v2/screener/screen, which the module's documentation notes "owns the grammar of what it accepts" — the frontend relays the caller's exact document rather than re-interpreting it.

Comparison arithmetic in the client-side evaluator is careful about the same precision rules the rest of the app follows: decimal cell comparisons stay on exact string comparison (compareDecimal), and the one place a BigInt conversion happens on an already-lossy integer cell is called out explicitly in the code as not recovering precision already lost at JSON.parse — only avoiding losing more.

Refresh model

Rows refresh two ways, deliberately overlapping: a polled snapshot (/v2/screener/snapshot or /rows) and a live delta stream (/v2/stream/screener), both carrying the same onix.feature_row-shaped documents. src/screener/resolution-clock.tsx governs which resolution (1m through 1d, and eventually a sub-minute live/session base per the registry's own published resolutions vocabulary) the screener is currently scoped to — the same venue-neutral resolution vocabulary the chart's /v2/resolutions contract speaks, converted for any TradingView-facing surface by the same shared conversion function the datafeed uses, never by a second local table.

A row that is not yet "warm" (has not observed the feature's declared warmupBars of closed input) says so explicitly rather than rendering a zero or a blank that could be mistaken for a real value — this is the ABSENT-with-reason cell convention the wire contract itself defines (onix.feature_row's FeatureValue).

Linking to the chart

Selecting a screener row does not draw on the chart directly. It publishes through a narrow, one-way overlay slot (src/screener/overlay-slot.ts) that a separate adapter (src/screener/overlay-adapter.tsx) consumes to render pattern-observation overlays on the currently mounted Advanced Charts widget — see dashboard-and-workspaces.md for the shared-selection contexts a row click also updates (instrument, bar type, resolution).

Status

The route set, decode discipline, column model, client-side screen validation/evaluation, and overlay publishing described above are verified against source and unit tests at this pin (screen.test.ts, columns.test.ts, gateway-v2.test.ts, overlay-adapter.test.tsx). The project's own contract-request log records earlier gaps in this surface (docs/contract-requests/012-screener-v2-surface.md, 033-screener-stream-capability-on-the-operator-relay.md); at this pin the code and its live-probe comments indicate the core read/screen/stream routes are implemented, but the exact current status of every capability listed in those requests was not independently re-verified for this page — see evidence/frontend/open-questions.md. For what the backend screener engine itself computes and how, see ../backend/screener.md.

Evidence and source pins for this page

Verified. Current behaviour, confirmed in source at the pinned commit.

Verified on against the following immutable sources:

  • frontend@99f49dae:src/screener/contract.ts
  • frontend@99f49dae:src/screener/gateway-v2.ts
  • frontend@99f49dae:src/screener/columns.ts
  • frontend@99f49dae:src/screener/screen.ts
  • frontend@99f49dae:contracts/vendor/onix-engine/schemas/screener_row.schema.json

Status tokens are defined on the documentation and status model page. Every pin on this site is listed under versions and source pins.