Backfill (history)
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.
On this page
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);
}
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);
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),
});
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;
}