Builder interfaces
Builder interfaces
The stateful workers that turn input into closed bars. Pick ITickBarBuilder to
aggregate raw ticks, or IDerivedBarBuilder to transform another series.
On this page
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.
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.
-
CurrentBarmust throw when nothing is forming. Reading it whileHasCurrentBarisfalsethrows (the built-ins throwInvalidOperationException) — callers always checkHasCurrentBarfirst. 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. -
ClosingTickBelongsToNextBardrives volume attribution.false(the default) credits the closing tick's volume to the bar that just closed (volume / tick bars).truemeans 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
-
ResetsOnSessionBoundary—truefor 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) returnfalse— 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 whenResetsOnSessionBoundaryis true. The default no-op suits builders that opt out; threshold builders implementForceCloseCurrentBar, and time/second builders also overrideForceCloseAtSessionEndto 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. -
ExposesFormingBar—truewhenCurrentBaris a live tail bar the runtime opens, updates per tick, and finalises. Classical wickless Renko returnsfalse: bricks are quantised,CurrentBaris the last completed brick, and a new brick appends rather than rewriting the tail.