Backfill (history)

Venue plugins

Backfill (history)

You write the thin venue-specific historical sources; the host composes the platform's caching, gap-filling and re-aggregation chain around them.

Who builds what

Historical loading has two halves, and the seam draws a hard line between them. You write the thin venue-specific parts: "give me time bars for this instrument between these two instants", "give me raw ticks". The host composes the platform's backfill chain around them — on-disk caching, gap filling, bar building from ticks, re-aggregation of non-native periods, order-flow ladder construction.

This is what lets a venue plugin be contracts-only: it hands the host its raw sources and receives a finished, chart-facing IBackfillProvider. The caching policy is platform knowledge and stays in one place, so every venue gets the same gap-filling and re-aggregation behaviour for free.

graph LR
  V["Venue raw sources
time bars · ticks · ladders"] -->|VenueBackfillParts| C["IVenueBackfillComposer
(host)"] C -->|caching · gap fill · re-aggregation| B["IBackfillProvider"] B --> CH["Chart / backtest / strategy session"]

The raw sources you write

All in namespace SabrTrader.Pipeline.Storage. Implement the ones your venue actually serves; hand a source that returns empty for a history flavour the venue does not have.

The raw historical sources// Native time bars for a UTC range. The workhorse — nearly every venue has this.
public interface IHistoricalTimeBarSource
{
    Task<IReadOnlyList<Bar>> LoadTimeBars(
        string instrumentId, TimeSpan period,
        DateTime fromUtc, DateTime toUtcExclusive, CancellationToken ct = default);
}

// Raw ticks for a UTC range. Feeds the tick cache, from which Range / Renko /
// volume / Footprint specs are built. ProvidesHistoricalTicks = false for a
// venue with no tape history.
public interface IHistoricalTickSource
{
    Task<IReadOnlyList<Tick>> LoadTicks(
        string instrumentId, DateTime fromUtc, DateTime toUtcExclusive,
        CancellationToken ct = default);

    bool ProvidesHistoricalTicks => true;
}

// EXACT venue-built footprint ladders — only for venues whose trades carry the
// aggressor side. Null instead means footprints derive from cached ticks.
public interface IHistoricalOrderFlowBarSource
{
    bool ProvidesProfiledMinuteBars { get; }
    Task<IReadOnlyList<OrderFlowBar>> LoadProfiledMinuteBars(
        string instrumentId, DateTime fromUtc, DateTime toUtcExclusive,
        CancellationToken ct = default);
}

// Venue-built NON-time bars (tick / volume / range served server-side).
public interface INativeBarSource
{
    bool SupportsNativeBars(BarSpecification spec);
    Task<IReadOnlyList<Bar>> LoadNativeBars(
        BarSpecification spec, DateTime fromUtc, DateTime toUtcExclusive,
        CancellationToken ct = default);
}

Composing the chain

In ConnectAsync, hand your parts to context.BackfillComposer and publish the result as the data port's Backfill.

IVenueBackfillComposer.cspublic interface IVenueBackfillComposer
{
    ComposedVenueBackfill Compose(VenueBackfillParts parts);
}

public sealed record VenueBackfillParts(
    string ProviderKey,                    // stable key — also the cache namespace
    string VenueDisplayName,               // for backfill-activity labels
    IHistoricalTimeBarSource TimeBars,
    IHistoricalTickSource Ticks,           // empty source when the venue has no tick history
    IInstrumentMetadata Instruments,       // tick size etc., for bar building
    IBackfillProvider DirectBackfill,      // the venue's own uncached backfill
    ITimeBarPeriodSupport? PeriodSupport,  // native period truth; null = any period
    TimeSpan DefaultLookback,
    Action<string>? Log = null,
    IHistoricalOrderFlowBarSource? OrderFlowBars = null,
    INativeBarSource? NativeBars = null);

public sealed record ComposedVenueBackfill(
    IBackfillProvider Backfill,
    bool SupportsTickDerived);             // true when the cached tick path is wired
MyDataProvider.csvar composed = (context.BackfillComposer ?? DirectVenueBackfillComposer.Instance)
    .Compose(new VenueBackfillParts(
        ProviderKey:      "myvenue",
        VenueDisplayName: "My Venue",
        TimeBars:         _historicalBars,
        Ticks:            _historicalTicks,
        Instruments:      _instruments,
        DirectBackfill:   _directBackfill,
        PeriodSupport:    MyVenuePeriods.Instance,
        DefaultLookback:  TimeSpan.FromDays(30),
        Log:              context.Notices.Log));

_backfill            = composed.Backfill;          // publish as IDataProvider.Backfill
_supportsTickDerived = composed.SupportsTickDerived;

SupportsTickDerived tells you whether tick-derived specs (Range, Renko, volume, Footprint) can build for this session — gate SupportsBarSpec on it rather than guessing, so a chart never asks for a spec the chain cannot serve.

The direct composer

context.BackfillComposer is nullable, because a bare tooling host or a test rig can run with no backfill plane. DirectVenueBackfillComposer.Instance is the composer for that case: it returns your uncached direct backfill verbatim, so native time bars serve exactly as they do in the app and SupportsTickDerived reports false. Defaulting to it is what lets the same venue code run unit tests without a host.

Native periods & re-aggregation

ITimeBarPeriodSupport is how the venue states its period truth, so the host can re-aggregate the rest instead of asking for periods the venue would silently approximate.

ITimeBarPeriodSupport.cspublic interface ITimeBarPeriodSupport
{
    bool SupportsNatively(TimeSpan period);

    // The largest natively-served period that divides `period` evenly — the host
    // loads that and re-aggregates up. Null = nothing divides it.
    TimeSpan? LargestNativeDivisor(TimeSpan period);
}
Clamp availability at the source. A venue that only holds 30 days of ticks, or no intraday history before a certain date, clamps the requested window inside its own source and returns what it can. Venue knowledge belongs in the venue; the chain above must never have to guess.

Progress & activity

A long paged load should report progress so the chart's backfill indicator moves. Implement the progress-reporting variant of your source and the composed chain forwards it; the host's context.Stores.BackfillActivity tracker surfaces the venue-labelled activity row.

IProgressReportingTimeBarSource.cspublic interface IProgressReportingTimeBarSource : IHistoricalTimeBarSource
{
    Task<IReadOnlyList<Bar>> LoadTimeBarsWithProgress(
        string instrumentId, TimeSpan period,
        DateTime fromUtc, DateTime toUtcExclusive,
        IProgress<BackfillProgress> progress, CancellationToken ct = default);
}

The offline condition

When a historical request arrives while the venue is not connected, throw the venue-neutral VenueNotConnectedException (namespace SabrTrader.Pipeline.Venues) from the source. The chain classifies it as a backfill-source-unavailable condition rather than a data error, so the chart retries cleanly instead of caching an empty result as truth.

MyHistoricalSource.cspublic Task<IReadOnlyList<Bar>> LoadTimeBars(
    string instrumentId, TimeSpan period, DateTime fromUtc, DateTime toUtcExclusive,
    CancellationToken ct = default)
{
    if (!_client.IsConnected)
        throw new VenueNotConnectedException("My Venue is not connected.");

    return _client.LoadBarsAsync(instrumentId, period, fromUtc, toUtcExclusive, ct);
}

Exchange-routed history

A venue that can serve the same instrument from more than one exchange or feed takes the exchange on the request. Every historical source carries a default-implemented overload with a string? exchange parameter that delegates to the plain one, so a venue with a single feed implements nothing extra and a routing venue overrides it.

Routed overloadsTask<IReadOnlyList<Bar>> LoadTimeBars(
    string instrumentId, string? exchange, TimeSpan period,
    DateTime fromUtc, DateTime toUtcExclusive, CancellationToken ct = default)
    => LoadTimeBars(instrumentId, period, fromUtc, toUtcExclusive, ct);
Decorators must forward it. If you wrap a source, override the routed overload and forward the exchange. A decorator that inherits the default silently drops the picked exchange, and the chart mixes feeds — backfill from one, live from another.

Opt-in source extensions

Four optional interfaces let a venue tell the chain something it cannot otherwise know. Each is detected on the source you already supply — implement it and the behaviour changes, ignore it and nothing does.

Implement on Interface What it buys you
your tick source ISeamPriorityHistoricalTickSource Serve the history→live seam's uncached fetch on a dedicated replay lane, so the handoff is not starved behind a bulk day backfill.
your time-bar source IOpenInterestAwareTimeBarSource Report per-bar open interest alongside native bars. The OI-aware load bypasses the on-disk bar cache (fixed-width OHLCV cannot hold OI), exactly as the order-flow path does.
your backfill provider IFormingBarFreshnessSource Report how far the FORMING bar you appended is actually complete, so the session's history→live cutoff lands on the right edge instead of under-counting it.
your period support LadderTimeBarPeriodSupport Not an interface — a shipped ITimeBarPeriodSupport for the common venue shape: a fixed ladder of native grains. Give it your REAL native set and it answers both questions correctly.
Using the ladder// OANDA-style granularities: everything on the ladder is native, a period an entry
// divides exactly is re-aggregated from the largest such entry, the rest is refused.
private static readonly ITimeBarPeriodSupport Periods = new LadderTimeBarPeriodSupport(new[]
{
    TimeSpan.FromSeconds(5),  TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30),
    TimeSpan.FromMinutes(1),  TimeSpan.FromMinutes(5),  TimeSpan.FromMinutes(15),
    TimeSpan.FromHours(1),    TimeSpan.FromHours(4),    TimeSpan.FromDays(1),
});
The ladder must be the venue's REAL native set. It is the truth the period picker gates on and the re-aggregator plans against — an aspirational entry produces a period the venue silently cannot serve.

Continuous futures

One optional capability covers everything continuous-contract related, probed on the data port with AsCapability<IContinuousFuturesSource>(). All three members default to null, so a venue implements only what it actually offers.

IContinuousFuturesSource.cspublic interface IContinuousFuturesSource
{
    // A server-stitched continuous symbol for a root ("ES" -> "ES.c.0"), if the venue has one.
    string? ContinuousBackfillSymbol(string root) => null;

    // Bare per-contract bar source used to stitch a continuous series platform-side.
    IHistoricalTimeBarSource? ContinuousBarContracts => null;

    // Cached per-contract tick source for the same job on the tick path.
    IRawTickRangeProvider? ContinuousTickContracts => null;
}