DatabentoDotNet

A .NET client for Databento market data — real-time streaming, historical queries, and a zero-copy DBN codec.

Get started API reference GitHub

Note

This is a third-party client. It is not published or endorsed by Databento, who ship official clients for Python, C++ and Rust. 0.10.0 is a beta: the code is complete and tested, and what is not yet settled is whether the public surface is the right shape. The hosting extensions are newest — 0.10.0 is their first release.

Install

Five packages, and you take only the ones you use. DatabentoDotNet.Dbn is the only one with no sibling dependency; every other one brings it in.

Package Latest Contents
DatabentoDotNet.Dbn NuGet The DBN codec — record structs, metadata, decoder, symbol maps
DatabentoDotNet.Live NuGet Real-time and intraday-replay streaming over the raw TCP gateway
DatabentoDotNet.Historical NuGet Historical HTTPS API — timeseries, batch, symbology, metadata
DatabentoDotNet.Reference NuGet Security master, corporate actions, adjustment factors
DatabentoDotNet.Extensions.Hosting NuGet ASP.NET Core and generic-host registration, and a hosted live session

The badges read the live feed, so they are the authority on what is actually published — not this page, and not the version named in the note above.

dotnet add package DatabentoDotNet.Dbn          # the DBN codec
dotnet add package DatabentoDotNet.Live         # real-time streaming
dotnet add package DatabentoDotNet.Historical   # historical HTTPS API
dotnet add package DatabentoDotNet.Reference    # security master, corporate actions
dotnet add package DatabentoDotNet.Extensions.Hosting   # ASP.NET Core / generic host

Requires .NET 10 or newer. Every date and time on the public surface is NodaTime — an Instant, never a DateTime, because a DateTime tick is 100 nanoseconds and a DBN timestamp is one.

Stream live trades

Four calls, because the protocol has four steps — and StartAsync is where billing begins. Nothing before it moves market data.

using DatabentoDotNet;
using DatabentoDotNet.Dbn;
using DatabentoDotNet.Live;

await using var client = new LiveClient
{
    ApiKey = new ApiKey(Environment.GetEnvironmentVariable("DATABENTO_API_KEY")!),
    Dataset = "EQUS.MINI",
};

await client.ConnectAsync();
await client.AuthenticateAsync();
await client.SubscribeAsync(new Subscription
{
    Schema = Schema.Trades,
    Symbols = Symbols.From(["AAPL", "MSFT"]),
});

await client.StartAsync();

while (true)
{
    // Drain what the last read produced before asking the socket for more: a refill may move the
    // buffer, and that is what ends a RecordRef's life.
    while (client.TryNextRecord(out RecordRef record))
    {
        if (record.TryGet(out TradeMsg trade))
        {
            Console.WriteLine($"{DbnTime.ToInstant(trade.IndexTs)} {trade.Price} x {trade.Size}");
        }
    }

    if (await client.FillBufferAsync() == 0)
    {
        break;   // the gateway closed the stream
    }
}

await client.CloseAsync();

Run that session inside a host

The loop above, lifted into the .NET generic host. The four protocol calls become one registration, the session's shape moves to appsettings.json, and a dropped connection is retried with bounded backoff instead of ending the program.

using DatabentoDotNet.Dbn;
using DatabentoDotNet.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddDatabento();
builder.Services.AddDatabentoLive("equities")
    .AddRecordHandler<TradePrinter>()
    .AddHealthCheck();       // opt-in — nothing registers one for you

using var host = builder.Build();
await host.RunAsync();       // connecting through starting happens in here, and billing with it

internal sealed class TradePrinter : ILiveRecordHandler
{
    // Not async, and it cannot be: a ref struct crosses neither an await nor a yield return. The
    // record points into the session's read buffer and is valid for this call only, so copy out
    // what you need and let OnFlushAsync do anything that has to await.
    public void OnRecord(scoped RecordRef record)
    {
        if (record.TryGet(out TradeMsg trade))
        {
            Console.WriteLine($"{DbnTime.ToInstant(trade.IndexTs)} {trade.Price} x {trade.Size}");
        }
    }

    public ValueTask OnFlushAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask;
}
{
  "Databento": {
    "Live": {
      "equities": {
        "Dataset": "EQUS.MINI",
        "Subscriptions": [
          { "Schema": "trades", "StypeIn": "raw_symbol", "Symbols": ["AAPL", "MSFT"] }
        ],
        "Reconnect": { "Enabled": true, "InitialDelay": "PT1S", "MaxDelay": "PT30S", "MaxAttempts": 10 }
      }
    }
  }
}

DatabentoDotNet.Extensions.Hosting references only the abstractions half of Microsoft.Extensions.Hosting, so a plain console app needs that package too; the Worker and Web SDKs already carry it.

The key is not in that file and should not be: the chain is the session's own ApiKey, then Databento:ApiKey, then the DATABENTO_API_KEY environment variable. Durations are ISO-8601 strings because the BCL's TimeSpan is banned repo-wide, and an options DTO therefore cannot carry one. A session that names a schema that is not a schema fails at startup rather than on first read.

Hosting and Dependency Injection is the full guide.

Download a historical range

Ask the price before taking the data. metadata.get_cost is free, and ToQuery() renders it for the very request you are about to send — so what was priced and what is sent cannot drift apart.

using DatabentoDotNet;
using DatabentoDotNet.Dbn;
using DatabentoDotNet.Historical;
using NodaTime;

await using var client = new HistoricalClient
{
    ApiKey = new ApiKey(Environment.GetEnvironmentVariable("DATABENTO_API_KEY")!),
};

var request = new GetRangeParams
{
    Dataset = "GLBX.MDP3",
    Symbols = Symbols.From("ESH4"),
    Schema = Schema.Trades,
    DateTimeRange = DateRange.OnDay(new LocalDate(2024, 1, 2)).ToDateTimeRange(),
    Limit = 10,
};

decimal cost = await client.Metadata.GetCostAsync(request.ToQuery());
if (cost > 0.01m)
{
    Console.WriteLine($"${cost} is more than this program will spend.");
    return;
}

await using var reader = await client.Timeseries.GetRangeAsync(request);
await foreach (OwnedRecord record in reader.ReadRecordsAsync())
{
    if (record.TryGet(out TradeMsg trade))
    {
        Console.WriteLine($"{DbnTime.ToInstant(trade.IndexTs)} {trade.Price} x {trade.Size}");
    }
}

Decode a local file

No key, no network. .dbn, .dbn.zst and .dbn.frag, with Zstandard framing detected rather than declared.

using DatabentoDotNet.Dbn;

using var decoder = new DbnDecoder(File.OpenRead("data.dbn.zst"));

Metadata metadata = decoder.Metadata!;
Console.WriteLine($"DBN v{metadata.Version} {metadata.Dataset}");

while (decoder.TryNextRecord(out RecordRef record))
{
    if (record.TryGet(out TradeMsg trade))
    {
        // Prices are fixed-point integers at a 1e-9 scale. Dividing through decimal keeps the
        // exact value; dividing through double would not.
        decimal price = (decimal)trade.Price / DbnConstants.FixedPriceScale;
        Console.WriteLine($"{DbnTime.ToInstant(trade.IndexTs)} {price} x {trade.Size}");
    }
}

IndexTs, not Header.TsEvent. Most schemas — trades included — index on ts_recv, and the two can fall on opposite sides of UTC midnight, so keying a symbol lookup on ts_event silently returns the previous day's symbol with nothing looking broken.

Where to go next

If you want to Go to
Install it and read your first record Getting Started
Stream live market data Live Streaming
Run a live session inside ASP.NET Core or a worker Hosting and Dependency Injection
Query historical data Historical Data
Know what a RecordRef may outlive Zero-Copy and Allocation
Know why nothing here takes a DateTime Timestamps and Prices
Look up a type, member, or overload — each with an example API reference
Work out why something is failing Troubleshooting · FAQ
Run something end to end The five samples
See what changed between versions Release notes
Contribute CLAUDE.md

This site is the documentation. The guides, the API reference and the release notes all live here, and each fact lives in exactly one of them. That rule is not new — it is the one the wiki used to state, and #82 kept it by moving the wiki's pages here and retiring the wiki rather than by running two surfaces that would disagree within a release.

Repository conventions are the one thing deliberately not here. They bind a contributor at the commit they are working on, which is an argument for living in the tree beside it, so they stay in CLAUDE.md.

Why the reference is complete

GenerateDocumentationFile and TreatWarningsAsErrors are both on for all five projects, so a public member without a documentation comment has never compiled in this repository. There is no undocumented corner to find, and a broken <see cref> is a build error rather than a bare word on a page.

The same gate covers the examples. Every one on this site is a <code> block inside an XML documentation comment, which means it ships inside the NuGet package and reaches IntelliSense at the call site — the site is a second rendering of it, not its home.