Backfill (history)

Market data connections

Backfill (history)

Backfill is how your provider hands the platform a block of historical bars. The base contract is one method; richer extension interfaces let the host ask for exactly the window it needs, with progress and — for order-flow / open-interest charts — the per-bar microstructure attached.

The base contract

Backfill is the most commonly implemented capability, because charts and backtests both need history before they can show anything. The contract starts deliberately small: a single Load that returns the complete series for a bar specification, ordered oldest-first. If that is all you implement, the host simply asks for everything and trims as needed.

IBackfillProvider.csusing System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using TradeStrike.Pipeline.Bars;

namespace TradeStrike.Pipeline.Runtime;

public interface IBackfillProvider
{
    // Complete historical set for spec, chronological (oldest first). May return
    // an empty list (cold instrument). Honour ct promptly. Implementations own caching —
    // the runtime calls Load once per spec per session.
    Task<IReadOnlyList<Bar>> Load(BarSpecification spec, CancellationToken ct = default);
}
Order matters. Return bars in chronological order, oldest first. The runtime appends in order to its series storage; a reversed list corrupts the indexer semantics that assume this[0] is the newest bar.

Window & progress extensions

Most real venues can do better than "give me everything" — a REST API usually accepts a lookback or an explicit date range, and fetching only what is needed is far cheaper. You express that by also implementing one or more of the extension interfaces below, all in TradeStrike.Pipeline.Runtime. The host detects them and prefers the most specific one available, falling back to the base Load when none is present.

Interface Adds
IBackfillProvider Load(spec, ct) — the whole series.
ILookbackAwareBackfillProvider Load(spec, TimeSpan lookback, ct) — "last 30 days".
IRangeAwareBackfillProvider Load(spec, fromUtc, toUtcExclusive, ct) — an explicit absolute window.
IProgressReportingBackfillProvider LoadWithProgress(spec, progress, ct) — loading-overlay updates.
IProgressReportingRangeBackfillProvider LoadWithProgress(spec, fromUtc, toUtcExclusive, progress, ct) — range + progress, for accurate percent-complete.

These compose cleanly: one class can implement the base interface plus the lookback and range variants, and the host chooses per request. The semantic difference between lookback and range is that ranges are not relative to "now" — the chart can request an analysis window that ended weeks ago. A range overload must validate that both timestamps are DateTimeKind.Utc and that fromUtc is strictly before toUtcExclusive, throwing before any I/O.

IBackfillProvider.cs (extensions)namespace TradeStrike.Pipeline.Runtime;

public interface ILookbackAwareBackfillProvider : IBackfillProvider
{
    Task<IReadOnlyList<Bar>> Load(BarSpecification spec, TimeSpan lookback, CancellationToken ct = default);
}

public interface IRangeAwareBackfillProvider : IBackfillProvider
{
    // Bars whose timestamps fall in [fromUtc, toUtcExclusive). Both MUST be Utc;
    // fromUtc strictly before toUtcExclusive (throws ArgumentException before any I/O).
    Task<IReadOnlyList<Bar>> Load(
        BarSpecification spec, DateTime fromUtc, DateTime toUtcExclusive, CancellationToken ct = default);
}
Honour cancellation throughout. A user who scrolls away or closes a chart should not keep an expensive history fetch alive. Long paginated loops must call ct.ThrowIfCancellationRequested() between chunks.

Reporting progress

For multi-second fetches, IProgressReportingBackfillProvider drives the chart's loading overlay so the UI does not look frozen. You report through an IProgress<BackfillProgress>; every field is a best-effort estimate except StageMessage. The contract guarantees only that at least one report fires on success, with a final PercentComplete = 1.0. A null progress sink means "no reporting requested" — treat it as such without throwing.

BackfillProgress.csusing System;

namespace TradeStrike.Pipeline.Runtime;

public sealed record BackfillProgress(
    int BarsReceived,                 // grows monotonically
    DateTime? LatestBarTimeUtc,       // null until the first bar arrives
    double PercentComplete,           // 0.0 when un-estimable, jumps to 1.0 on completion
    string StageMessage);             // the only fully-reliable field

Order-flow-aware backfill

Footprint / cumulative-delta / volume-profile charts need per-bar bid/ask volume at price, which native OHLCV bars do not carry. A provider that can deliver bars plus per-bar order flow in one round-trip implements IOrderFlowAwareBackfillProvider. The host routes to it when at least one attached indicator is marked RequiresOrderFlow; otherwise it falls back to the base Load and the chart degrades gracefully. The returned Bars and OrderFlow lists are equal-length and index-aligned.

IOrderFlowAwareBackfillProvider.csusing System.Threading;
using System.Threading.Tasks;
using TradeStrike.Pipeline.Bars;

namespace TradeStrike.Pipeline.Runtime;

public interface IOrderFlowAwareBackfillProvider : IBackfillProvider
{
    // Bars + per-bar orderflow, equal length, aligned indices, chronological.
    Task<BackfillResultWithOrderFlow> LoadWithOrderFlow(
        BarSpecification spec, OrderFlowBackfillRequest request, CancellationToken ct = default);
}

OrderFlowBackfillRequest is an options bag — one method instead of a lookback/range/progress overload explosion. Exactly one of Lookback or the (FromUtc, ToUtcExclusive) pair is meaningful; both null means "your default window". Resolution selects fidelity: Auto (the default) lets the provider derive a concrete tier from the primary spec's period, with Tick exact-but-heaviest and Second/Minute far lighter at the cost of an estimated aggressor. Call request.Validate() to enforce the field invariants once.

OrderFlowBackfillRequest.csusing System;
using TradeStrike.Pipeline.OrderFlow;

namespace TradeStrike.Pipeline.Runtime;

public sealed record OrderFlowBackfillRequest(
    DateTime? FromUtc = null,
    DateTime? ToUtcExclusive = null,
    TimeSpan? Lookback = null,
    IProgress<BackfillProgress>? Progress = null,
    OrderFlowResolution Resolution = OrderFlowResolution.Auto)   // Auto | Tick | Second | Minute
{
    public bool HasRange => FromUtc.HasValue && ToUtcExclusive.HasValue;
    public bool HasLookback => Lookback.HasValue;
    public void Validate();   // throws on bad Utc kind / ordering / negative lookback
}
Drive the aggregator, not a second cache. Order-flow-aware loads must aggregate bars and order flow together in one pass from the cached tick stream — never maintain a divergent, pre-aggregated order-flow cache on disk (the "derived-cache trap"). Ticks are stored once; bars + order flow are built together. The result may differ marginally from the venue's server-side aggregation (rounding, late-print inclusion); for order-flow charts that is the right trade-off.

Open-interest-aware backfill

Open interest is a venue-reported scalar (outstanding contracts), not derivable from trades, so there is no tick aggregation — you simply map whatever per-bar OI the feed exposes and emit double.NaN for bars the venue does not report it for. A provider opts in with IOpenInterestAwareBackfillProvider; the host uses it when an attached indicator is marked RequiresOpenInterest, and otherwise the OI channel stays all-NaN.

IOpenInterestAwareBackfillProvider.csnamespace TradeStrike.Pipeline.Runtime;

public interface IOpenInterestAwareBackfillProvider : IBackfillProvider
{
    // result.Bars and result.OpenInterest are equal length, aligned (NaN = no OI for that bar).
    Task<BackfillResultWithOpenInterest> LoadWithOpenInterest(
        BarSpecification spec, OpenInterestBackfillRequest request, CancellationToken ct = default);
}

Both result types — BackfillResultWithOrderFlow and BackfillResultWithOpenInterest — validate the equal-length alignment invariant in their constructor, so downstream consumers index either parallel list by the same i without extra checks. Each exposes Bars, the parallel data list, Count, FirstBarStartUtc, LastBarEndUtc, and a shared Empty instance.

The source & store seam

The built-in caching backfill provider does not call your venue directly; it sits on a pair of source and store seams in TradeStrike.Pipeline.Storage. A venue adapter implements the source (the un-cached upstream fetch); the host's caching layer owns the store (coverage-tracked, gap-filling) and calls the source only for sub-ranges it does not already hold. This is why one download of a day's ticks serves every chart and bar type forever, and why re-opening a chart that already cached the last 30 days makes zero venue calls.

You implement (source) For
IHistoricalTimeBarSource LoadTimeBars(instrumentId, period, fromUtc, toUtcExclusive, ct) — native time bars.
IHistoricalTickSource LoadTicks(instrumentId, fromUtc, toUtcExclusive, ct) — raw ticks. ProvidesHistoricalTicks declares whether tick-derived bars can be back-filled.
INativeBarSource SupportsNativeBars(spec) + LoadNativeBars(...) — range / volume / tick bars the venue aggregates server-side.
IOpenInterestAwareTimeBarSource time bars + per-bar OI in one call.

The *WithProgress twins (IProgressReportingTimeBarSource, INativeBarProgressSource) add a progress sink that the caching provider prefers when one is supplied. The cache layer's coverage records use the half-open DateRange value type, where adjacent [a, b) and [b, c) merge cleanly into [a, c) — a range that was fetched and produced zero bars (weekend, holiday) stays covered, not re-fetched forever.

IHistoricalTimeBarSource.csusing System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using TradeStrike.Pipeline.Bars;

namespace TradeStrike.Pipeline.Storage;

public interface IHistoricalTimeBarSource
{
    // Bars whose StartUtc falls in [fromUtc, toUtcExclusive), chronological. An empty
    // result is legal (the cache still marks the range covered). Honour ct promptly.
    Task<IReadOnlyList<Bar>> LoadTimeBars(
        string instrumentId, TimeSpan period,
        DateTime fromUtc, DateTime toUtcExclusive, CancellationToken ct = default);
}
Signal "offline" precisely. When your upstream is unreachable because the connection has not been established or has dropped, throw BackfillSourceUnavailableException — and only that — for the not-connected condition. The caching provider treats it specially: it serves whatever the on-disk cache already holds and returns, then re-runs backfill when the connection comes up. Any other exception propagates unchanged, so real bugs are never masked behind a silent cache-only fallback.

Integrity observers

Two optional observer seams let the host audit feed integrity synchronously with the fetch, before bad data ever reaches the cache — the answer to NinjaTrader 8's "silent missing tick" pain. Both run on the backfill task continuation, so an implementation must be cheap and must not throw. A Null…Observer.Instance no-op default is provided for each.

Observer Reports
ISequenceGapObserver a SequenceGapEvent when consecutive ticks for an instrument show a sequence delta > 1 (the venue dropped a tick the adapter never recovered).
IVolumeConsistencyObserver a VolumeMismatchEvent when a bar's TotalBidVolume + TotalAskVolume does not equal its reported Volume — a builder bug or upstream hiccup.