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:

  1. A factory that declares two credential fields and validates them in Create.
  2. A provider that implements IDataProvider, the base IBackfillProvider, and the range-aware extension on one class.
  3. Unsupported sub-interfaces returning null, and a SupportsBarSpec that accepts only time bars.
  4. 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.