A derived bar
A derived bar
Some bar types do not consume ticks at all — they transform another bar series. Heikin-Ashi is the canonical example, and it shows the two-method derived pattern.
ADVANCED A derived bar (transform another series)
A derived builder implements IDerivedBarBuilder instead of ITickBarBuilder. It
sits downstream of another series and reshapes its bars. Because the source has both committed (closed)
bars and a live forming bar, the interface splits into two methods:
-
OnSourceBarClosed(in Bar, Span<Bar>)— the upstream series just closed a bar. Compute the transformed bar, advance your running state, write it intoclosedBars, and return the count (always 1 for Heikin-Ashi; the span shape leaves room for future 0/1/N transforms). -
PreviewSourceBar(in Bar)— the upstream forming bar evolved on a tick. RefreshCurrentBarfrom it so the live tail propagates, but do not commit any previous-bar state — the developing source bar may change or be replaced on the next tick. If the preview touched your running fields, the next real close would compute from a polluted baseline.
The Heikin-Ashi shape below applies one formula in one place; only OnSourceBarClosed writes
_prevHaOpen / _prevHaClose.
HeikinAshiBarBuilder.cs (shape)using System;
using TradeStrike.Pipeline.Bars;
public sealed class HeikinAshiBarBuilder : IDerivedBarBuilder
{
private double _prevHaOpen, _prevHaClose;
private bool _seeded, _has;
private Bar _current;
public BarSpecification Spec { get; }
public bool HasCurrentBar => _has;
public Bar CurrentBar => _has ? _current : throw new InvalidOperationException("No bar in progress.");
public HeikinAshiBarBuilder(HeikinAshiBarSpec spec) => Spec = spec;
public int OnSourceBarClosed(in Bar s, Span<Bar> closedBars)
{
Bar ha = ComputeHa(s);
_prevHaOpen = ha.Open; _prevHaClose = ha.Close; _seeded = true; // advance recurrence
closedBars[0] = ha; _has = true; _current = ha;
return 1; // one HA bar per source bar
}
public void PreviewSourceBar(in Bar developing)
{
_current = ComputeHa(developing); // refresh live tail; do NOT touch _prev*
_has = true;
}
private Bar ComputeHa(in Bar s)
{
double close = (s.Open + s.High + s.Low + s.Close) / 4;
double open = _seeded ? (_prevHaOpen + _prevHaClose) / 2 : (s.Open + s.Close) / 2;
double high = Math.Max(s.High, Math.Max(open, close));
double low = Math.Min(s.Low, Math.Min(open, close));
return new Bar(s.StartUtc, s.EndUtc, open, high, low, close, s.Volume, s.TickCount);
}
}
HeikinAshiBarSpec.Underlying is an open
BarSpecification rather than a fixed time bar.Continuing the recurrence across the live handoff
A derived transform whose output depends on its own previous bar — Heikin-Ashi's
prevHaOpen / prevHaClose — needs that recurrence state to survive the
history→live boundary, or the first live bar falls back to its first-bar formula and visibly jumps.
Opt in by also implementing ISeedableDerivedBarBuilder:
TradeStrike.Pipeline.Barspublic interface ISeedableDerivedBarBuilder
{
void SeedFromLastDerivedBar(in Bar lastDerivedBar);
}
At the handoff the series holds the derived bars, so the last one carries exactly the recurrence
state the transform needs. The runtime hands it to SeedFromLastDerivedBar; your
implementation primes _prevHaOpen / _prevHaClose (and sets
_seeded) so the next derived bar continues correctly. The underlying tick builder starts
fresh — the transform is lossy, so it cannot be inverted from a derived bar, and that first
begin-of-period gap is identical to any non-derived chart. More on the broader seeding seam in
Registration & discovery.