Builder interfaces

Bar types

Builder interfaces

The stateful workers that turn input into closed bars. Pick ITickBarBuilder to aggregate raw ticks, or IDerivedBarBuilder to transform another series.

The shared surface, plus one drive method

Where the spec is the cheap identity, the builder is the machine that does the work. All builders share a small common surface — the spec they belong to, whether a bar is currently forming, and the in-progress bar itself — and then each kind adds exactly one drive method that the engine calls as new input arrives. You implement whichever drive method matches your data source.

IBarBuilder.csnamespace TradeStrike.Pipeline.Bars;

public interface IBarBuilder
{
    BarSpecification Spec { get; }
    bool HasCurrentBar { get; }   // false until the first bar starts forming
    Bar CurrentBar { get; }       // throws while HasCurrentBar is false
}

// Driven by raw ticks. Returns how many bars this single tick CLOSED.
public interface ITickBarBuilder : IBarBuilder
{
    int OnTick(in Tick tick, Span<Bar> closedBars);   // usually 0 or 1; Range/Renko can close N

    bool ClosingTickBelongsToNextBar => false;        // does the closing tick open the next bar?
}

// Driven by another series' bars (e.g. Heikin-Ashi over time bars).
public interface IDerivedBarBuilder : IBarBuilder
{
    int OnSourceBarClosed(in Bar sourceBar, Span<Bar> closedBars);  // commit + advance state
    void PreviewSourceBar(in Bar developingSourceBar);             // refresh live tail, no advance
}

The pivotal detail in both drive methods is the closedBars span and its return value. Rather than allocating a list, the caller hands you a pre-sized Span<Bar> to write into and trusts the integer you return to say how many entries are valid. This keeps the hot path allocation-free, but it puts the contract on you: write exactly the bars you closed, in chronological order, and return that count.

The closedBars span. Write each newly-completed bar into the caller's span and return the count. Returning 0 means "the current bar is still forming". A single tick can legitimately close several bars (a range/Renko tick that jumps multiple thresholds) — write them all and return N. Callers stackalloc a span large enough for the maximum expected closures (16 is plenty for any realistic scenario).

The Bar you produce

Both drive methods produce Bar values, and the constructor argument order is easy to get wrong — there is no plain Time field; time is split into an open stamp and a close stamp, and tick count is a first-class field after volume. Bar is a 64-byte readonly record struct passed by value through the pipeline. Here is the exact shape:

Bar.csnamespace TradeStrike.Pipeline.Bars;

public readonly record struct Bar(
    DateTime StartUtc,    // bar open time
    DateTime EndUtc,      // moment the bar closed (closing tick's timestamp for non-time bars)
    double Open,
    double High,
    double Low,
    double Close,
    long Volume,
    long TickCount);      // underlying ticks that formed this bar — load-bearing for orderflow

The Tick you consume

An ITickBarBuilder reads Tick values from TradeStrike.Pipeline.Ticks. Bar builders consume only ticks with the TickFlags.Trade bit set — bid/ask quote updates and the PreviousClose reference price are ignored at this layer.

Tick.csnamespace TradeStrike.Pipeline.Ticks;

public readonly record struct Tick(
    long SequenceNumber,
    DateTime ExchangeTimestampUtc,
    string InstrumentId,
    double Price,
    long Size,
    TickFlags Flags);

[Flags]
public enum TickFlags : byte
{
    None = 0, Bid = 1, Ask = 2, Trade = 4,
    AtAsk = 8, AtBid = 16, PreviousClose = 32, Synthetic = 128,
}

Gate every OnTick on the trade bit before folding the price — if ((tick.Flags & TickFlags.Trade) == 0) return 0; — and use tick.ExchangeTimestampUtc for your bar's StartUtc / EndUtc.

Contract rules to honour

The builder contract has a few sharp edges the runtime relies on. Get these right and your bar type behaves like a built-in; get them wrong and you will see exceptions or mis-credited volume.

  • CurrentBar must throw when nothing is forming. Reading it while HasCurrentBar is false throws (the built-ins throw InvalidOperationException) — callers always check HasCurrentBar first. Do not return a zero-filled bar.
  • Do not overflow closedBars. Writing past the span's length throws. If your rule could close more bars than fit, write what fits and return that count.
  • ClosingTickBelongsToNextBar drives volume attribution. false (the default) credits the closing tick's volume to the bar that just closed (volume / tick bars). true means the threshold-crossing tick opens the next bar instead (time / range / Renko), so its print must not be double-counted — this is also what the orderflow aggregator reads to decide which bar a print belongs to.

Optional: session-boundary & forming-bar hooks

ITickBarBuilder exposes several members with default implementations, so a simple builder can ignore them entirely. Override them only if your bar type interacts with trading sessions or has unusual forming-bar semantics.

ITickBarBuilder.cs (defaults)// Force-close the developing bar at a session boundary ("Break at EOD")?
bool ResetsOnSessionBoundary => true;             // true for time/tick/volume/range
int ForceCloseCurrentBar(Span<Bar> closedBars) => 0;
int ForceCloseAtSessionEnd(DateTime sessionEndUtc, Span<Bar> closedBars)
    => ForceCloseCurrentBar(closedBars);          // time builders clamp END to the close
void BeginSession(DateTime sessionBeginUtc) { }   // time-grid builders anchor to session open

// Is CurrentBar a continuously-forming tail bar, or just the last completed bar?
bool ExposesFormingBar => true;                   // false for classical (wickless) Renko
  • ResetsOnSessionBoundarytrue for threshold builders (time / tick / volume / range): the forming bar force-closes at the boundary so it does not bleed across the overnight gap. Continuous price-pattern builders (Renko, Median-Renko, Kagi, Point&Figure, Line-Break) return false — bricks and lines carry across the gap; out-of-session ticks simply do not contribute.
  • ForceCloseCurrentBar / ForceCloseAtSessionEnd — the session runtime calls these on the boundary tick when ResetsOnSessionBoundary is true. The default no-op suits builders that opt out; threshold builders implement ForceCloseCurrentBar, and time/second builders also override ForceCloseAtSessionEnd to clamp the bar's end to the exact session close.
  • BeginSession — lets a time-grid builder anchor its bars to the session open (an RTH 60-min chart runs 09:30–10:30, not 09:00–10:00). A no-op for non-time builders.
  • ExposesFormingBartrue when CurrentBar is a live tail bar the runtime opens, updates per tick, and finalises. Classical wickless Renko returns false: bricks are quantised, CurrentBar is the last completed brick, and a new brick appends rather than rewriting the tail.