The BarSpecification

Bar types

The BarSpecification

The immutable, value-comparable identity record that names your bar type and carries its parameters — the half of a custom bar type the engine uses to deduplicate work across charts.

The base record

A BarSpecification is an abstract record, which is deliberate: records compare by value, so two requests for the same instrument and the same settings hash and equate identically, and the engine can hand both charts one shared builder and one output series. The base record carries the instrument id, optional continuous-futures wiring, optional trading-hours (session) binding, and the hooks the host needs for labels and symbol translation. You inherit from it and add your own settings.

BarSpecification.csnamespace SabrTrader.Pipeline.Bars;

public abstract record BarSpecification(string InstrumentId)
{
    // Continuous-futures intent (null for a literal / dated contract).
    public string? ContinuousRoot { get; init; }
    public BarAdjustmentMode ContinuousAdjustment { get; init; } = BarAdjustmentMode.BackAdjusted;

    // Trading-hours (session) binding. Null = no session filtering.
    public string? TradingHoursTemplateName { get; init; }
    public bool ResetOnNewTradingDay { get; init; } = true;

    // Specific source exchange (e.g. a delayed feed). Null = venue resolves the default.
    public string? Exchange { get; init; }

    // Human-readable label for logs / UI. Each concrete spec overrides it.
    public abstract string DisplayName { get; }

    // Symbol-translation hook. Default returns this unchanged.
    public virtual BarSpecification WithInstrumentId(string newInstrumentId) => this;

    // Convenience clones (not overridable) the host's resolution chokepoints use.
    public BarSpecification WithContinuous(string? continuousRoot, BarAdjustmentMode adjustment);
    public BarSpecification WithTradingHours(string? templateName, bool resetOnNewTradingDay = true);
    public BarSpecification WithExchange(string? exchange);
}

Members you override

A concrete spec is a sealed record that adds its settings as positional record parameters and overrides two members the host calls into.

  • DisplayName — the abstract label shown in chart UI and logs. Build it from InstrumentId and your settings (e.g. "MNQ Range 4t").
  • WithInstrumentId(string) — the symbol-translation hook. When the chart layer re-targets a series from a user-typed symbol ("ES") to the venue-native one ("/ESM26:XCME"), the host calls this to clone the spec onto the new instrument. The base default returns this unchanged — safe, but a chart on your spec would keep the user-typed symbol. Override it with a record with-expression so your settings copy across, and, for a composite spec, propagate the rewrite into any nested spec.

The base also exposes WithContinuous(...), WithTradingHours(...), and WithExchange(...). You do not override these — they are non-virtual clone helpers the host's continuous-futures factory, session-resolution chokepoint, and chart factory use to stamp intent onto any spec via the record with. Because ContinuousRoot, ContinuousAdjustment, TradingHoursTemplateName, ResetOnNewTradingDay, and Exchange are part of the record's value, two otherwise-identical specs with different sessions (one RTH, one ETH) — or different source exchanges (real-time vs delayed) — correctly do not share a builder.

Continuous-futures adjustment. BarAdjustmentMode (in SabrTrader.Pipeline.Bars) has three values — None (raw stitched bars), BackAdjusted (additive, the TA-friendly default), and RatioAdjusted (multiplicative, for returns analysis). It is honoured by the data layer that stitches contracts; your builder never sees it.

The built-in subtypes

All ship in BarSpecification.cs. Study them as templates — your custom type will look like one of these.

Record Parameters
TimeBarSpec (string InstrumentId, TimeSpan Period)
TickBarSpec (string InstrumentId, int Ticks)
VolumeBarSpec (string InstrumentId, long Volume, OversizedTradePolicy OversizedTrades = KeepAtomic)
RangeBarSpec (string InstrumentId, int RangeTicks)
RenkoBarSpec (string InstrumentId, int BrickTicks, bool WithWicks = false)
MedianRenkoBarSpec (string InstrumentId, int BrickTicks, int TrendThresholdTicks)
KagiBarSpec (string InstrumentId, int ReversalTicks)
PointAndFigureBarSpec (string InstrumentId, int BoxTicks, int Reversal)
LineBreakBarSpec (string InstrumentId, int LineCount)
HeikinAshiBarSpec (string InstrumentId, BarSpecification Underlying)

HeikinAshiBarSpec is the one composite: its Underlying is another BarSpecification (usually a TimeBarSpec, but the framework does not restrict it). Its WithInstrumentId override rewrites both itself and the nested underlying so the composed spec stays self-consistent.

Defining your own spec

For a plugin bar type (the usual path), use a parameterless record constructor so the host catalog and BarSpecificationCodec can construct the type, put settings on init properties marked with [BarParameter], override DisplayName / WithInstrumentId, and optionally set [BarTypeDisplayName] for the picker label.

SampleFixedTickBarSpec.csusing SabrTrader.Pipeline.Bars;

[BarTypeDisplayName("Sample Fixed Tick")]
public sealed record SampleFixedTickBarSpec() : BarSpecification("SAMPLE")
{
    [BarParameter(DisplayName = "Ticks per bar",
                  Description = "Bar closes after this many trade ticks.",
                  MinValue = 1)]
    public int Ticks { get; init; } = 50;

    public override string DisplayName => $"{InstrumentId} Sample {Ticks}t";

    public override BarSpecification WithInstrumentId(string newInstrumentId)
        => this with { InstrumentId = newInstrumentId };
}

Built-in specs still use positional record parameters (TickBarSpec(string InstrumentId, int Ticks)). That shape is fine inside the host assembly; plugin specs must stay constructible with no caller-supplied arguments so discovery and workspace restore work.

Keep specs pure data. A spec is comparable, hashable, and used as a dictionary key. Hold only the parameters that define the series identity — no live state, no services, no tick size. Tick size is resolved later, when the builder is created (see Registration).

Plugin parameters: [BarParameter]

Declaring [BarParameter] on a public get+set or init property is the editor and persistence contract — the same pattern as Market Analyzer column parameters. The New Chart / Data Series dialogs reflect those properties into editors; BarSpecificationCodec writes them into the plugin JSON params map on encode and applies them on decode.

BarParameterAttribute.cs[AttributeUsage(AttributeTargets.Property)]
public sealed class BarParameterAttribute : Attribute
{
    public string DisplayName { get; init; } = "";
    public string Description { get; init; } = "";
    public double MinValue { get; init; } = double.NaN;   // inclusive; NaN = unbounded
    public double MaxValue { get; init; } = double.NaN;
}
  • Supported types: bool, int, long, double, decimal, string, TimeSpan, DateTime, enums.
  • Unsupported types fail at install — the catalog reflects parameters when the plugin loads, so a bad property surfaces immediately rather than when a chart opens.
  • [BarTypeDisplayName("…")] is optional; without it the picker uses the type's short name.

A setting worth knowing: OversizedTradePolicy

One built-in setting deserves a callout because its default is load-bearing for orderflow. The VolumeBarSpec takes an OversizedTradePolicy alongside its volume threshold, which decides what happens when a single trade is larger than the volume remaining in the forming bar.

  • KeepAtomic (default) — treat the trade as one indivisible event: the bar closes with its full traded volume even if that overshoots the threshold. This is required for orderflow correctness, because a single exchange print carries one bid/ask snapshot and one aggressor side; splitting it across bars would duplicate that state and corrupt volume-at-price and delta.
  • SplitAcrossBars — divide the oversized trade across consecutive bars to hit the threshold exactly. Each synthetic bar is a single price point, with the tick-count credit on the first bar only. Acceptable for pure volume-only backtests, but wrong for footprint work.

If you author a custom volume-like bar type, mirror this choice and default to KeepAtomic unless you have a specific reason not to — a footprint indicator built on a SplitAcrossBars series will show corrupted delta.