Registration & discovery
Registration & discovery
How your spec and builder are wired together at runtime — plus the optional surface that makes a custom bar type a first-class citizen: seeding the live handoff, JSON persistence, and availability probing.
On this page
The factory: spec → builder
The runtime only knows it needs a builder for a spec; the mapping lives behind
IBarBuilderFactory. It is two methods:
IBarBuilderFactory.csnamespace TradeStrike.Pipeline.Bars;
public interface IBarBuilderFactory
{
IBarBuilder Create(BarSpecification spec);
bool Supports(BarSpecification spec);
}
In a deployed plugin you do not register bar types by hand — dropping the DLL is enough, because the
host scans it and files your spec subclass and builder into its discovery buckets. When you need explicit
control (most often in a unit test, or when composing a factory yourself), register the mapping from a
spec type to a builder factory delegate on the host's DefaultBarBuilderFactory, then ask the
factory whether it supports a spec and to create the builder.
Registrationusing TradeStrike.Pipeline.Bars;
var factory = new DefaultBarBuilderFactory(instruments); // instruments: IInstrumentMetadata
// A tick-count bar needs no tick size, so the delegate just passes the spec.
factory.Register<MomentumBarSpec>(spec => new NTickBarBuilder(spec));
// A price-distance bar gets its tick size resolved HERE, at registration,
// and passed into the builder's constructor (the built-in range bar does exactly this):
factory.Register<RangeBarSpec>(spec =>
new NRangeBarBuilder(spec, instruments.GetTickSize(spec.InstrumentId)));
if (factory.Supports(spec))
{
IBarBuilder builder = factory.Create(spec);
}
Alternatively, supply your own IBarBuilderFactory implementation entirely — the runtime
depends only on the interface.
Tick size & instrument metadata
Any price-distance bar type (range, Renko, Kagi, Point&Figure, …) needs the instrument's tick
size to turn a setting like "4 ticks" into a price distance. That is the job of
IInstrumentMetadata, which the factory is constructed with. Resolution happens once, at
registration time, inside the delegate — the factory calls
GetTickSize(instrumentId) and passes the resulting double into the builder's
constructor. Your builder receives a ready tick size; it never holds an IInstrumentMetadata
reference or looks anything up at run time.
IInstrumentMetadata.cs (excerpt)namespace TradeStrike.Pipeline.Bars;
public interface IInstrumentMetadata
{
double GetTickSize(string instrumentId); // throws if unregistered — fail loud
bool IsRegistered(string instrumentId);
// Optional facets (default null / Contract) — sizing, currency, classification:
double? TryGetPointValue(string instrumentId) => null;
QuantityUnit GetQuantityUnit(string instrumentId) => QuantityUnit.Contract;
double? TryGetLotSize(string instrumentId) => null;
double? TryGetQuantityStep(string instrumentId) => null;
double? TryGetMinQuantity(string instrumentId) => null;
string? TryGetQuoteCurrency(string instrumentId) => null;
string? TryGetAdjustmentVersion(string instrumentId) => null;
InstrumentCategory? TryGetCategory(string instrumentId) => null;
}
GetTickSize throws when the instrument is not registered — deliberately, so a range bar
is never built on a fabricated tick size. The SDK ships InMemoryInstrumentMetadata (a
thread-safe registry with a fluent Register(...)) for tests and host wiring. The optional
facets describe sizing and classification — QuantityUnit (Contract / Share / Lot / Unit
/ Coin) and InstrumentCategory (Forex / Equity / Crypto / Index / Commodity / Metal / Bond)
— and feed order-entry surfaces rather than bar building. The
GetQuantityModel(instrumentId) extension resolves unit + step + min into one
QuantityModel for those surfaces.
Seeding the live handoff
When a chart switches from historical backfill to the live feed, chain-from-close bar types must continue the series or they leave a phantom gap. A builder opts into this by implementing one of two seam interfaces; the handoff checks for them and applies the result uniformly, so any builder — built-in or plugin — participates without bespoke handoff code.
ISeedableBarBuilder.csnamespace TradeStrike.Pipeline.Bars;
public readonly record struct BarSeedResult(bool Seeded, DateTime Cutoff, bool LastSeriesBarIsInProgress)
{
public static readonly BarSeedResult NotSeeded = new(false, default, false);
}
// Single-bar seed: continue from the last historical bar.
public interface ISeedableBarBuilder
{
BarSeedResult SeedFromLastBar(in Bar lastBar, DateTime nowUtc);
}
// Multi-bar seed: builders whose rule looks back N bars (e.g. Line-Break).
// Preferred over ISeedableBarBuilder when both are present.
public interface IHistorySeedableBarBuilder
{
BarSeedResult SeedFromHistory(IBarSeries series, DateTime nowUtc);
}
-
Time bars re-open the last (possibly still-forming) period bar so live ticks extend it. They
return a
BarSeedResultwithLastSeriesBarIsInProgress = true. -
Range / Renko / Kagi / Median-Renko chain — the next bar opens at the previous close, so
seeding opens a fresh in-progress bar off the last closed bar (
LastSeriesBarIsInProgress = false). TheCutofftells the runtime which live ticks the seed already accounts for. - Volume / tick bars do NOT chain — the next bar opens at the next trade's own price — so they are not seedable; the unseeded behaviour is already correct.
-
Line-Break needs the last
LineCountcloses, so it implementsIHistorySeedableBarBuilderand reconstructs its window from the historicalIBarSeries(barsAgo-indexed,[0]= newest).
Derived builders use the separate ISeedableDerivedBarBuilder seam covered in
A derived bar.
BarSeedResult fields. Seeded — whether a bar was
seeded. Cutoff — live ticks at or before this instant are already in the seeded bar and
must be dropped. LastSeriesBarIsInProgress — true when the seed re-opened the last series
bar (time-bar partial period), false when it opened a new bar and the last series bar stays final
(range-bar chain-from-close).Persistence: BarSpecificationCodec
Chart templates and workspaces persist the exact bar spec across restarts. BarSpecificationCodec
is the bidirectional spec ↔ JSON codec — the template and workspace layers never hand-roll spec
JSON. The wire format is a single object with a string "type" discriminator plus the type's
own fields; "instrumentId" is always present, and continuous-futures + trading-hours intent
are written only when set (so a literal series round-trips byte-identically to the pre-feature format).
BarSpecificationCodec.csusing TradeStrike.Pipeline.Bars;
string json = BarSpecificationCodec.Encode(new TimeBarSpec("MNQ", TimeSpan.FromMinutes(5)));
// {"type":"time","instrumentId":"MNQ","periodTicks":3000000000}
BarSpecification spec = BarSpecificationCodec.Decode(json);
The codec handles every built-in discriminator (time, tick, range,
volume, renko, medianrenko, kagi,
pointandfigure, linebreak, ha). Decoding is case-insensitive and
forgiving (missing optional keys fall back to safe defaults); an unknown discriminator throws
NotSupportedException with the offending string, so a workspace written by a newer app
version surfaces a clear error rather than silently restoring nothing.
BarSpecificationCodec is closed over the
built-in subtypes — Encode throws NotSupportedException for an unknown spec
type. If your plugin spec must survive restarts through this codec, the host's codec needs a switch arm for
it; otherwise persist your spec through your own plugin's storage.Availability probing
Which bar types a chart may offer for an instrument depends on the data provider. The
venue-agnostic helper BarTypeAvailability answers "which bar types can THIS venue produce?"
without any UI or venue knowledge — the caller passes a supports probe (in practice
IDataProvider.SupportsBarSpec) and the instrument id.
BarTypeAvailability.csnamespace TradeStrike.Pipeline.Bars;
public static class BarTypeAvailability
{
// One representative spec per built-in bar type, in chart-picker order.
public static IReadOnlyList<BarSpecification> RepresentativeSpecs(string instrumentId);
// The representative specs the provider accepts, in the same order.
public static IReadOnlyList<BarSpecification> SupportedSpecs(
Func<BarSpecification, bool> supports, string instrumentId);
}
A provider accepts a spec when it can fulfil it either natively or by building it client-side from its historical ticks — both collapse into one boolean, so the UI only needs "offerable or not". The consumer (New Chart dialog, Data Series dialog, chart toolbar, a future web client) maps each returned spec's runtime type onto its own picker and hides the rest. The representative specs use canonical, strictly-positive parameters because the probe asks a type-level question — only the spec's runtime type (and parameter positivity, for venues that validate it) affects the verdict.
ITimeBarPeriodSupport (in TradeStrike.Pipeline.Runtime):
SupportsNatively(period) and LargestNativeDivisor(period). The generic
re-aggregating backfill provider uses it to fetch a divisor period and roll it up. Tick / volume / range /
Renko bypass this entirely.