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. Every request arrives as one HistoryRequest: the instrument, the exchange the user picked (or null for your default routing), the UTC window, and a progress sink (or null).

The raw historical sourcespublic sealed record HistoryRequest(
    string InstrumentId,
    string? Exchange,                          // a picked feed (delayed data), or null
    DateTime FromUtc,
    DateTime ToUtcExclusive,
    IProgress<BackfillProgress>? Progress = null);

// Native time bars for a UTC range. The workhorse: nearly every venue has this.
public interface IHistoricalTimeBarSource
{
    Task<HistoryServed<Bar>> LoadTimeBars(
        HistoryRequest request, TimeSpan period, CancellationToken ct = default);
}

// Raw ticks for a UTC range. Feeds the tick cache, from which Range / Renko /
// volume / Footprint specs are built. Every source states ProvidesHistoricalTicks.
public interface IHistoricalTickSource
{
    Task<HistoryServed<Tick>> LoadTicks(HistoryRequest request, CancellationToken ct = default);

    bool ProvidesHistoricalTicks { get; }
}

// 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<HistoryServed<OrderFlowBar>> LoadProfiledMinuteBars(
        HistoryRequest request, CancellationToken ct = default);
}

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

A venue with no historical trades API hands the composer NoHistoricalTicks.Instance. It reports ProvidesHistoricalTicks = false, so the chain never asks it for ticks and tick-derived specs build forward from the live stream instead.

Saying what you served

The platform caches history on disk a day at a time, and a day it has cached is never asked for again. So every answer says exactly how much of the window the venue served, and the cache keeps only the whole days inside that span. Anything outside it stays missing and is asked for on the next load.

What happened What you return
The venue served the whole window, including a window it confirms is empty (a weekend, a holiday) HistoryServed<T>.Complete(items, request)
The venue stopped partway: a page budget ran out, a stream went quiet, a dataset ends mid-window HistoryServed<T>.Through(items, request, servedToUtcExclusive), where the served end is how far you actually received data
The venue's history does not reach back to the start of the window (it keeps only the last N candles, or a maximum age) HistoryServed<T>.Span(items, request, servedFromUtc, servedToUtcExclusive)
The venue answered but holds nothing it can vouch for in this window: it lies wholly before the venue's history or after its dataset end, or an empty reply cannot tell a quiet window from one the venue does not hold HistoryServed<T>.None(request). Nothing is stored, and the chain goes on to the other windows
The venue could not be asked: not connected, logged out, its session or data plan lapsed (a 401 or 403), the transport failed, it answered 5xx or 429, or the request timed out throw BackfillSourceUnavailableException. The chain stops asking your venue for the rest of this load
A genuine fault: an unknown symbol, a malformed reply, a period the venue cannot produce throw an ordinary exception
Never answer an empty list for a window you did not serve. An empty complete answer tells the cache the venue confirmed there is no data there, and the cache keeps that answer for good. A venue that could not be reached, or stopped early, must say so, or the chart shows a hole that no reload fills.

Items are oldest first. A page that overshoots the window is fine; the chain clips it.

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 and more for bar building
    IBackfillProvider DirectBackfill,      // the venue's own uncached backfill
    ITimeBarPeriodSupport? PeriodSupport,  // native period truth; null means 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 says so with Span. A window that lies entirely before that floor answers None. 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. Report each page to request.Progress when it is not null (native bars get the sink as the progress parameter); the host turns those reports into the venue-labelled activity row. A source that cannot report progress simply never calls it.

The offline condition

When a historical request arrives while the venue is not connected, or the venue cannot be reached, throw BackfillSourceUnavailableException from the source. The chain serves what it already has on disk and leaves the rest missing, so the next load after the venue comes back fills it. Any other exception fails the load instead.

MyHistoricalSource.cspublic async Task<HistoryServed<Bar>> LoadTimeBars(
    HistoryRequest request, TimeSpan period, CancellationToken ct = default)
{
    if (!_client.IsConnected)
        throw new BackfillSourceUnavailableException("My Venue is not connected.");

    var bars = await _client.LoadBarsAsync(
        request.InstrumentId, period, request.FromUtc, request.ToUtcExclusive, ct);
    return HistoryServed<Bar>.Complete(bars, request);
}

Exchange-routed history

A venue that can serve the same instrument from more than one exchange or feed reads the exchange from request.Exchange. Null means your default routing, so a venue with a single feed ignores it.

Decorators pass the request on whole. If you wrap a source, hand the inner source the same request (or a copy made with request with { ... }). Building a new request without the exchange silently drops the picked feed, 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 chain uses it.

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 ILiveHistoryProvider Load history up to the one instant the chart hands over to live ticks, and return the FORMING bar's reach with it as a LiveHandoffPoint (the instant, and how many trades at it the bar already holds), so the history→live handoff lands on the right edge: nothing under-counted, nothing counted twice.
your period support LadderTimeBarPeriodSupport A class, 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. A venue that serves any whole-second period and publishes no ladder passes AnyWholeSecondPeriodSupport.Instance instead.
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.

Live history

A chart that goes live after its backfill asks the provider through ILiveHistoryProvider (namespace SabrTrader.Pipeline.Runtime). It passes a LiveHistoryRequest whose AsOfUtc is the one instant it read before the load and hands over to live ticks at. The answer, a LiveHistory, holds the bars and, when the last bar is still forming, its FormingReach: a LiveHandoffPoint saying which live ticks that bar already holds. Because the load and the handoff use the same instant, they always agree about which bar is open.

The chain the composer returns implements this for you, building the forming bar from its caches. You only touch it when you wrap a backfill provider yourself. The rule for a wrapper: it serves live history exactly when the provider it wraps does, and reports that through ProvidesLiveHistory. A wrapper that transforms bars transforms LiveHistory.Bars (and the footprint) the same way its Load does, and keeps FormingReach only while the forming bar is still the last bar.

TimedBackfill.csusing SabrTrader.Pipeline.Bars;
using SabrTrader.Pipeline.Runtime;

// Pass-through wrapper that logs how long each load takes.
public sealed class TimedBackfill : IBackfillProvider, ILiveHistoryProvider
{
    private readonly IBackfillProvider _inner;
    private readonly Action<string> _log;

    public TimedBackfill(IBackfillProvider inner, Action<string> log)
    {
        _inner = inner;
        _log = log;
    }

    public async Task<IReadOnlyList<Bar>> Load(BarSpecification spec, CancellationToken ct = default)
    {
        var started = DateTime.UtcNow;
        var bars = await _inner.Load(spec, ct);
        _log($"{spec.DisplayName}: {bars.Count} bars in {DateTime.UtcNow - started}");
        return bars;
    }

    public bool ProvidesLiveHistory => _inner is ILiveHistoryProvider { ProvidesLiveHistory: true };

    public async Task<LiveHistory> LoadLive(
        BarSpecification spec, LiveHistoryRequest request, CancellationToken ct = default)
    {
        if (_inner is not ILiveHistoryProvider { ProvidesLiveHistory: true } live)
            throw new InvalidOperationException("The wrapped provider serves no live history.");

        var started = DateTime.UtcNow;
        var history = await live.LoadLive(spec, request, ct);
        _log($"{spec.DisplayName} as of {request.AsOfUtc:O}: {history.Bars.Count} bars, " +
             $"forming reach {history.FormingReach?.At:O}, in {DateTime.UtcNow - started}");
        return history;   // bars untouched, so the reach stays valid
    }
}

When you serve the request yourself, let the request do the window arithmetic: request.Validate() rejects a request that cannot describe a window, request.ResolveWindow(defaultLookback) gives the concrete (FromUtc, ToUtcExclusive) with your default depth filled in, TryGetExplicitWindow tells an absolute window from a "last N days up to now" one, and ToOrderFlowRequest() turns a request that asked for order flow into the matching OrderFlowBackfillRequest for an IOrderFlowAwareBackfillProvider. The member details are in the providers reference.

Continuous futures

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

A product has one roll. A venue whose server-stitched series also says which dated contract it is on decides that roll: the platform reads the series while a chart is live on that contract, and the product's live month follows the venue. A venue that cannot say which contract its series is on should not offer the series here; the platform then stitches the venue's dated contracts on its own roll dates, and live data follows the same dates.

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

    // The dated contract that series is on right now, in the venue's own spelling ("ESZ6"), asked of
    // the venue at call time. Null when the venue names none; throw when it cannot be asked. The
    // platform bounds the wait with ct, so stop at its cancellation.
    Task<string?> ContinuousSeriesContractAsync(string root, CancellationToken ct)
        => Task.FromResult<string?>(null);

    // True once proven: the series also serves tick-level history (tick, volume, range and Renko
    // bars, and order flow). Default false; a series that serves time bars only leaves it false.
    bool ContinuousSeriesServesTickData => false;

    // Cached per-contract tick source for continuous tick replay (backtests).
    IRawTickRangeProvider? ContinuousTickContracts => null;
}