Skip to content

Bar aggregation and resolutions

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

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.py
  • backend@efa38e04:python/src/onix_engine/spine/internal_1m_collector.py
  • backend@efa38e04:python/tests/test_internal_1m_replay_gates.py
  • backend@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.