A derived bar

Bar types

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 into closedBars, 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. Refresh CurrentBar from 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);
    }
}
Derived-over-anything. The framework wraps a derived builder over any underlying tick builder automatically, so Heikin-Ashi-over-range "just works" through composition — you only implement the transform. That is why 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.