Skip to content

Screener engine

VerifiedOwner Backend / platformLast verified backend@efa38e04e2db37a444e9f17100323c2ad4b83647 nautilus@2.0.0rc3

ONIX's live screener is a standalone Rust process (onix-screener, crates/onix-screener-live) that computes a registry of typed features over incoming bars and answers bounded, filterable screens against them. It communicates over a newline-delimited canonical-JSON socket protocol — one ingest connection per relay identity, one query connection per client — and is fronted for ONIX's Python side by screener.bridge/screener.client, and for the gateway/MCP surface by GET /v2/screener/* and the corresponding MCP tools.

Feature registry

Every screenable value is declared once in a feature registry entry (onix.feature_spec): a stable feature_id and semver feature_version, where it computes (implementation), what it consumes (input_kind), its canonical parameters (exact decimal/integer strings — never floats, because these bytes are hashed into a config_hash and a float has no single spelling), its warmup requirement, and its declared outputs. GET /v2/screener/features (and the spine_screener_features MCP tool) enumerate the full registry — enumeration only, no values.

Resolutions and rows

onix.feature_row is the per-instrument, per-resolution snapshot the registry actually produced. At this pin, the row-producing projection (onix_engine.screener.projection) emits frames for all seven resolutions in the frozen chart-resolution contract — 1m (native, folded from the spine's own 1-minute fanout) and 5m/15m/30m/1h/4h/1d (folded from derived closed bars the Rust runtime emits back over the same ingest socket as DERIVED_BAR egress frames, via onix_engine.screener.bridge). projection.RESOLUTIONS_PRODUCED is built directly from spine.resolutions.RESOLUTIONS rather than a second, hand-typed list. This closes a defect that used to leave RESOLUTIONS_PRODUCED == ("1m",), pinned shut by test_screener_bridge.py's own executable assertion: assert RESOLUTIONS_PRODUCED == ("1m", "5m", "15m", "30m", "1h", "4h", "1d"). A derived timeframe's values still report ABSENT until its own series has accumulated enough closed bars to warm up its indicators (e.g. ema_200 needs 200 closed bars of that timeframe) — that is ordinary warmup behavior, identical to how 1m itself starts ABSENT for a freshly-subscribed instrument, not the structural defect this section used to describe.

The absent-value honesty rule

This is the screener's central design guarantee, and it is enforced at the schema level, not left to convention. A feature value on the wire is one of a closed set of self-describing typed variants (DECIMAL, an integer, a UnixNanos string, or ABSENT). The schema states the rule directly: a feature that is not yet warm, or whose input is unavailable, reports ABSENT with a reason — it never reports a zero, a null, or a silently-omitted key. A missing key means "not in the registry"; ABSENT means "registered, but currently untrustworthy" — and the schema is explicit that conflating the two is exactly how a screen ends up silently matching on garbage. The same distinction holds for row deltas: a key can leave a row entirely (removed — the feature left the registry) or become ABSENT while staying on screen (still registered, currently untrustworthy) — a consumer that conflated them would either display a stale value forever or blank a column that never should have been touched.

Confluence: honest arithmetic over honest inputs, and a fixed defect

GET /v2/screener/instrument/{id} additionally returns a cross-timeframe confluence vote, derived client-side from the rows the screener already returned (no second query). The rule: per timeframe, trend is +1/-1/0 from price versus a long moving average, momentum is +1/-1/0 from a bounded RSI-style oscillator, and a timeframe only votes when its inputs are trustworthy — the row must exist, be warm, and the value must not be ABSENT.

A stale statement in the Python confluence module's own docstring says every derived timeframe is permanently ABSENT because "the row-producing projection only emits 1m." That was true of an earlier state of this codebase and is contradicted, at this pin, by projection.py (see "Resolutions and rows" above) and by test_screener_bridge.py's own executable test, whose comment says so directly: "Pins the CONFLUENCE DEFECT closed. RESOLUTIONS_PRODUCED == ("1m",) WAS the defect." A stale docstring is prose, not behavior — this documentation follows the test, which would fail if the claim it pins were false, not the comment, which would not. At this pin every derived timeframe can vote once its own series is warm; a freshly-subscribed instrument's higher timeframes still report ABSENT (and therefore vote neutral) only until enough closed bars of that timeframe have accumulated to initialize their indicators — the same warmup behavior 1m itself exhibits for a brand-new subscription, not a structural inability to ever vote. This documentation does not have a live measurement of a fully-warmed multi-timeframe confluence score at this pin (the only measurement on record predates the fix and shows the old, defective behavior), so the end-to-end quality of a fully warm vote is unverified; the mechanism that makes it possible — all seven resolutions being projected and eligible to vote — is verified by the cited test.

Health

onix.screener_health reports process state, input lag, processed/dropped counters, checkpoint age, and connection/egress accounting — every field a real, verifiable counter, not a synthesized conclusion. A screener that is not running reports process_state: STOPPED with a real last_error, produced by the gateway itself when it cannot reach the process at all — this is how "the screener is not running" is distinguished from "it is running and nothing currently matches," which is the failure mode a health surface exists to prevent. Even a synthesized STOPPED document reports its connection counters honestly as all-zero (a true fact about a process that never ran) rather than omitting the block entirely, and an egress-accounting block that has no seam to describe is reported as an explicit null, not three zeroes that would falsely claim a seam ran and lost nothing.

Evidence and source pins for this page

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

Verified on against the following immutable sources:

  • backend@efa38e04:schemas/feature_row.schema.json
  • backend@efa38e04:schemas/feature_spec.schema.json
  • backend@efa38e04:python/src/onix_engine/screener/projection.py
  • backend@efa38e04:python/src/onix_engine/screener/bridge.py
  • backend@efa38e04:python/tests/test_screener_bridge.py
  • backend@efa38e04:schemas/screener_health.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.