The BarSpecification
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.
On this page
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 fromInstrumentIdand 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 returnsthisunchanged — safe, but a chart on your spec would keep the user-typed symbol. Override it with a recordwith-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.
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.
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.