Bar aggregation and resolutions¶
ONIX serves seven chart resolutions — 1m, 5m, 15m, 30m, 1h, 4h, 1d — from exactly one
source of truth for what each one means: spine.resolutions, reused by both the gateway's
GET /v2/resolutions route and the derivation engine, so no module carries its own copy of the
table.
Durable versus derived¶
Only 1m is durable: it is the catalog's own canonical bar type
({instrument_id}-1-MINUTE-LAST-EXTERNAL), stored verbatim, never a composite of itself. Every
other resolution is derived on read from that 1-minute series using Nautilus's own composite
bar-type mechanism — BarType.new_composite, the same construction a live composite subscription
would use — never a hand-rolled resampling routine. GET /v2/resolutions publishes, per key, the
bar-type suffix, whether the source is "catalog" (only 1m) or "derived" (everything else),
and the interval width in nanoseconds computed by Nautilus's own
BarSpecification.get_interval_ns() — not a hand-maintained {"MINUTE": 60_000_000_000, ...}
table.
| Resolution | Source | Nautilus aggregation |
|---|---|---|
1m |
catalog (durable) | none — this is the stored series |
5m, 15m, 30m |
derived | TimeBarAggregator over the 1m constituents |
1h, 4h |
derived | TimeBarAggregator over the 1m constituents |
1d |
derived | TimeBarAggregator over the 1m constituents |
Close semantics¶
A derived bar's close (ts_event) lands on Nautilus's own UTC calendar grid, because
get_time_bar_start inside the real TimeBarAggregator computes it that way — proven directly by
the backend's own boundary-alignment tests for every interval this contract names, not asserted
from documentation. ONIX code only builds the request for that behavior (the composite bar-type
string); it never computes a calendar boundary itself. calendar_aligned: true on every
GET /v2/resolutions entry is therefore an assertion backed by test coverage, not an aspiration.
A closed-bar ts_event is the interval close, for every resolution including the durable 1m
series — a bar stamped exactly at 00:00:00 opened at 23:59 and belongs to the previous UTC day.
Consumers converting a close timestamp to a chart-library interval start subtract the interval's
own step_ns.
Incomplete windows are withheld, not approximated¶
Deriving a bucket requires every 1-minute constituent inside its window. If the durable 1-minute
series has an unresolved gap — a range the catalog knows is incomplete rather than genuinely
empty — the derivation layer withholds any derived bucket whose constituent window intersects that
gap, rather than serving a bucket computed from a partial set of inputs. This is enforced by a
dedicated gap ledger consulted before every derivation, not left to the aggregator to notice on its
own; see ingestion.md for how a gap is recorded in the first place.
Live versus history suffix¶
Under a live/history split configuration (see ingestion.md for when this
applies), the published 1m resolution entry carries two suffixes: bar_type_suffix (the
history/durable-catalog series, unchanged) and live_bar_type_suffix (the series a consumer
should subscribe to for the live stream). They are equal unless the node is running a live/history
split; a consumer that only ever reads bar_type_suffix still gets the correct history series in
either configuration.
Second-tier bars¶
A separate, non-durable second-resolution tier (…-1-SECOND-LAST-INTERNAL) exists for a
bounded prefix of the subscribed universe (configurably, the first N instruments by sorted id
— 25 by default). It is aggregated inside Nautilus from the live trade stream, not a second venue
subscription.
A quiet second produces no bar, not a carry-forward flat one. The node runs with
time_bars_build_with_no_updates=False set node-wide (the deployed policy, INTERNAL_1M_DATA_ENGINE_OVERRIDES
in spine.internal_1m_collector) — the opposite of the pinned wheel's own default, which is True
and would synthesize a flat open=high=low=close=last close bar for an interval with zero trades.
This is proven, not asserted from a docstring: test_internal_1m_replay_gates.py::
test_b4_option_a_also_makes_the_one_second_tier_sparse runs five traded minutes (300 candidate
seconds) through a real BacktestEngine with the shipped policy installed and asserts the closed
1-second bar count is bounded by the number of ticks that actually arrived (0 < len(seconds) <=
len(_ALL_TICKS) + 1) with no duplicate timestamps — a quiet second is absent from the output
entirely, live and persisted alike, exactly the same "no trade, no bar" rule the durable INTERNAL
1-minute series follows (see ingestion.md). This tier's cost therefore scales
with the number of subscribed instruments and how often each one actually trades, not with
wall-clock time — bounding the instrument prefix still matters because Nautilus installs and
maintains one aggregator instance per subscribed series regardless of trade frequency, but the
emitted bar volume itself is sparse, not one-per-second-per-instrument.
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:python/src/onix_engine/spine/resolutions.pybackend@efa38e04:python/src/onix_engine/spine/internal_1m_collector.pybackend@efa38e04:python/tests/test_internal_1m_replay_gates.pybackend@efa38e04:docs/contracts/market-spine-v2.md
Status tokens are defined on the documentation and status model page. Every pin on this site is listed under versions and source pins.