Registration & discovery

Bar types

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.

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 BarSeedResult with LastSeriesBarIsInProgress = 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). The Cutoff tells 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 LineCount closes, so it implements IHistorySeedableBarBuilder and reconstructs its window from the historical IBarSeries (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.

Custom specs and persistence. 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.

Time bars and re-aggregation. For TIME bars specifically, a venue describes which periods it serves natively via 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.