A backfill-only provider
Market data connections
A backfill-only provider SIMPLE
A complete, end-to-end connection plugin that serves historical bars from a REST API and nothing else — the smallest thing that is genuinely useful.
What we are building
The smallest useful connection serves historical bars from a REST API, with no live feed. It advertises
only Backfill, so the host wires it for charts and backtests and never expects live ticks.
This example puts the whole pattern together:
- A factory that declares two credential fields and validates them in
Create. - A provider that implements
IDataProvider, the baseIBackfillProvider, and the range-aware extension on one class. - Unsupported sub-interfaces returning null, and a
SupportsBarSpecthat accepts only time bars. - A connection lifecycle that reduces to flipping a status flag — there is no persistent socket to manage.
MyHistoryPlugin.csusing System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using TradeStrike.Pipeline.Bars;
using TradeStrike.Pipeline.Providers;
using TradeStrike.Pipeline.Runtime;
public sealed class MyHistoryFactory : IDataProviderFactory
{
public string Key => "myhistory";
public string DisplayName => "My History API";
public IReadOnlyList<ProviderCredentialField> RequiredCredentials => new[]
{
new ProviderCredentialField("ApiKey", "API key", ProviderCredentialKind.Secret),
new ProviderCredentialField("BaseUrl", "Base URL", ProviderCredentialKind.Url,
Placeholder: "https://api.example.com", Required: false),
};
public IDataProvider Create(IReadOnlyDictionary<string, string> creds)
{
if (!creds.TryGetValue("ApiKey", out var key) || string.IsNullOrWhiteSpace(key))
throw new ArgumentException("ApiKey is required.", nameof(creds));
creds.TryGetValue("BaseUrl", out var url);
return new MyHistoryProvider(key, url ?? "https://api.example.com");
}
}
// One class implements the provider AND the backfill interfaces it advertises.
public sealed class MyHistoryProvider : IDataProvider, IRangeAwareBackfillProvider
{
private readonly string _key, _url;
public MyHistoryProvider(string key, string url) { _key = key; _url = url; }
public string Key => "myhistory"; // matches the factory
public string DisplayName => "My History API";
public ProviderCapabilities Capabilities => ProviderCapabilities.Backfill;
// Backfill is non-null because the flag is set; the rest stay null.
public IBackfillProvider? Backfill => this;
public ITickSource? LiveTicks => null;
public IInstrumentMetadata? Instruments => null;
// Depth / Mbo / OpenInterest / MarketSummary / Fundamentals default to null.
public bool SupportsBarSpec(BarSpecification spec) => spec is TimeBarSpec; // time bars only
public ProviderConnectionStatus Status { get; private set; } = ProviderConnectionStatus.Disconnected;
public event Action<ProviderConnectionStatus>? ConnectionStatusChanged;
public Task ConnectAsync(CancellationToken cancellationToken = default)
{
Status = ProviderConnectionStatus.Connected;
ConnectionStatusChanged?.Invoke(Status);
return Task.CompletedTask;
}
public Task DisconnectAsync()
{
Status = ProviderConnectionStatus.Disconnected;
ConnectionStatusChanged?.Invoke(Status); // no-throw: always safe
return Task.CompletedTask;
}
// IBackfillProvider: the whole series, oldest-first.
public async Task<IReadOnlyList<Bar>> Load(BarSpecification spec, CancellationToken ct = default)
{
var rows = await FetchCandlesAsync(spec.InstrumentId, period: null, from: null, to: null, ct);
return ToBars(rows);
}
// IRangeAwareBackfillProvider: only the requested window — cheaper for the venue.
public async Task<IReadOnlyList<Bar>> Load(
BarSpecification spec, DateTime fromUtc, DateTime toUtcExclusive, CancellationToken ct = default)
{
if (fromUtc.Kind != DateTimeKind.Utc || toUtcExclusive.Kind != DateTimeKind.Utc)
throw new ArgumentException("Range bounds must be UTC.");
if (fromUtc >= toUtcExclusive)
throw new ArgumentException("fromUtc must be strictly before toUtcExclusive.", nameof(fromUtc));
var period = (spec as TimeBarSpec)?.Period;
var rows = await FetchCandlesAsync(spec.InstrumentId, period, fromUtc, toUtcExclusive, ct);
return ToBars(rows);
}
private static List<Bar> ToBars(IReadOnlyList<Candle> rows) // rows already chronological
{
var bars = new List<Bar>(rows.Count);
foreach (var r in rows)
bars.Add(new Bar(r.StartUtc, r.EndUtc, r.O, r.H, r.L, r.C, r.Vol, r.Ticks));
return bars;
}
public void Dispose() { }
}
Grow it incrementally. Add live ticks by setting
LiveTicks and the
LiveTicks flag; add depth by setting Depth / Mbo and the
OrderbookDepth flag; add tick sizing by implementing IInstrumentMetadata and the
InstrumentMetadata flag. Nothing here changes — each capability is independent.