Bar-close ordering on tied timestamps¶
When several BarAggregator instances close on the same wall-clock instant — for example, a 4-hour bar, a 1-hour bar, and a 5-minute bar all sharing a midnight UTC boundary — the order in which they fire onBar matters. A strategy that updates a coarse-context indicator on the 4-hour close and reads it on the 5-minute close depends on the coarse one firing first.
This page is about ordering between aggregators. For the ordering inside one close — whether the trade that crossed the threshold belongs to the bar it closed or to the one that opens next — see the per-type rules in Bar types: Tick, Volume, Range and BpsRange fold the crossing trade in and detect the close on the following trade, Renko applies the crossing trade to the brick it completes and then closes that brick at its boundary, and Time drops a trade whose bucket has already been published.
Rule¶
lrvx dispatches bars in aggregator registration order. The first aggregator added to a MultiTimeframeAggregator (or subscribed to a BarBus) emits its tied-timestamp bar first; the next emits next; and so on.
For multi-TF strategies, this gives a simple authoring contract:
Register coarsest-first.
using namespace std::chrono_literals;
lrvx::MultiTimeframeAggregator<3> agg(&bus);
agg.addTimeInterval(4h); // coarsest first → emits first on tied closes
agg.addTimeInterval(1h);
agg.addTimeInterval(5min); // finest last → emits last on tied closes
The registration calls are typed by bar kind: addTimeInterval(std::chrono::seconds), addTickInterval(uint32_t), addVolumeInterval(double). Each returns the slot index it took. Note that addTimeInterval takes seconds, whereas BarEvent::barTypeParam reports the interval in nanoseconds — do not pass an _NS constant to the registration call.
A strategy that does
void onSymbolBar(SymbolContext& c, const BarEvent& ev) override {
// barTypeParam is nanoseconds for Time bars; timeframe::H4.param is the
// matching constant.
if (ev.barTypeParam == lrvx::timeframe::H4.param) cacheTrend(ev.bar);
else if (ev.barTypeParam == lrvx::timeframe::M5.param) maybeEnter(c, ev, lastH4Trend());
}
reads the up-to-date H4 context inside the M5 handler because the H4 callback fired first when both bars closed on the same wall-clock instant.
Why not coarsest-first as an automatic rule¶
A previous draft considered automatic descending-duration sort inside MultiTimeframeAggregator::onTrade. Two reasons not to do it implicitly:
- The duration analog for non-Time bars (Tick, Volume, Range, Renko, Bps) does not have a single obvious sort key. A 200-volume threshold and a 5-minute interval are not directly comparable; a lexicographic policy would need to be invented and documented anyway.
- Authors who set up the aggregator know the coarsest-first ordering they want; making it explicit in the registration call is one line of code and keeps the dispatch path branch-free.
If a user really wants coarsest-first regardless of registration order, sort the timeframe list before registering:
std::sort(intervals.begin(), intervals.end(), std::greater<>{});
for (auto seconds : intervals) agg.addTimeInterval(seconds);
Replay determinism¶
Because the rule is registration order — not wall-clock arrival, not duration — replay produces the same dispatch sequence as the original run. The replay-equivalence gate exercises a multi-TF tied-close fixture; a regression that changes the iteration order in MultiTimeframeAggregator::onTrade (or in any BarBus fan-out) would surface there.
See also¶
- Multi-TF context helpers. Read the most recent closed bar for any (symbol, timeframe) pair from a strategy.
- Bar aggregation. Pre-aggregate bars for fast backtesting.