Skip to content

Latest commit

 

History

283 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

databentodotnet

CI Latest Release License .NET 10.0

A .NET client for Databento market data — real-time streaming, historical data, and reference data, with a zero-copy DBN codec at its core.

Status: code complete, in beta at 0.10.0. The DBN codec, live streaming, the historical client, reference data and the hosting extensions are all merged to master and published to NuGet at 0.10.0 — five packages, 2,043 tests, zero warnings, a public API locked by an analyzer, and full method-level parity with databento-rs. It is 0.x rather than 1.0.0 deliberately: 1.0.0 undertakes not to break a 3,805-member public surface, and that undertaking is cheap to make and expensive to withdraw. The beta is where that surface gets contested, and it works — designing the fifth package against the other four is what turned up the one gap 0.10.0 fills (#86). If something is awkward to use, an issue now is far cheaper than a major version later. Tracked as #74 and #102, with 1.0.0 as #68.

The NuGet badges below read the live feed, so they are the authority on what is actually published.

  • Documentation — the site: guides, API reference, and release notes
  • ROADMAP.md — milestones, architecture, and design decisions
  • PORTING.md — Rust→.NET mapping guide for the port

Decoding a DBN stream

using DatabentoDotNet.Dbn;

using var decoder = new DbnDecoder(File.OpenRead("data.dbn.zst"));   // zstd is detected, not declared
Metadata? metadata = decoder.Metadata;

while (decoder.TryNextRecord(out RecordRef record))
{
    if (record.TryGet(out TradeMsg trade))
        Console.WriteLine($"{DbnTime.ToInstant(trade.IndexTs)} {trade.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. RecordRef.IndexTs picks the right field per record type.

Records are reinterpreted in place over the read buffer — no allocation per record. That is why RecordRef is a ref struct and TryNextRecord is synchronous: neither can cross an await, which is the boundary that keeps the zero-copy path sound. A record is valid only until the next call on the decoder.

Prices are long at a fixed 1e-9 scale and timestamps are ulong nanoseconds, both deliberately: decimal would cost throughput on the hot path, and a record field's type is its wire layout, so nothing wider than the 8 bytes on the wire can go there.

Above the codec, dates and times are NodaTimeInstant and LocalDate, never the BCL's DateTime family, whose 100 ns tick cannot represent a nanosecond timestamp at all. DbnTime is the single conversion between the two, and it reports DBN's undefined-timestamp sentinel as absent rather than as a time one nanosecond before the epoch.

Pricing a request before you send it

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

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

// get_cost prices the exact request you're about to send, not an approximation of it. A
// timeseries.get_range call renders this same value with GetRangeParams.ToQuery().
var request = new MetadataQueryParams
{
    Dataset = "XNAS.ITCH",
    Symbols = Symbols.From(["AAPL", "MSFT"]),
    Schema = Schema.Trades,
    DateTimeRange = DateTimeRange.Between(
        Instant.FromUtc(2023, 7, 1, 0, 0, 0), Instant.FromUtc(2023, 8, 1, 0, 0, 0)),
};

decimal cost = await client.Metadata.GetCostAsync(request);
if (cost > 5.00m)
{
    Console.WriteLine($"${cost} for that range — narrowing it before pulling any data.");
    return;
}

get_cost answers, in dollars, what pulling this exact range would cost — before any data moves. That is deliberately not a second parameter set assembled by hand: GetRangeParams.ToQuery() renders the MetadataQueryParams for the very request you are about to send, so what was priced and what is sent cannot drift apart. SubmitJobParams carries the same conversion. The cost comes back as decimal, not double — the API's own f64 is a Rust standard-library limitation rather than a choice, and a per-gigabyte unit price gets multiplied by a record count before a caller ever sees a figure.

Running a live session inside a host

The same session as a BackgroundService: registered once, configured from appsettings.json, and started, reconnected and shut down by the host rather than by your code.

dotnet add package DatabentoDotNet.Extensions.Hosting
dotnet add package Microsoft.Extensions.Hosting   # only in a plain console app — the Worker and Web SDKs carry it
using DatabentoDotNet.Dbn;
using DatabentoDotNet.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddDatabento();                                    // the key: Databento:ApiKey, else DATABENTO_API_KEY
builder.Services.AddDatabentoLive().AddRecordHandler<TradePrinter>();

using var host = builder.Build();
await host.RunAsync();   // connect, authenticate, subscribe, start — billing begins inside here

// One singleton per session rather than an instance per record: the package exists because this
// path does not allocate, and a handler resolved per record would be the first thing that did.
internal sealed class TradePrinter : ILiveRecordHandler
{
    public void OnRecord(scoped RecordRef record)
    {
        // Copy out what you need. The RecordRef points into the session's read buffer and is
        // valid for this call only — which is why OnRecord is not async and cannot be.
        if (record.TryGet(out TradeMsg trade))
        {
            Console.WriteLine($"{record.Header.InstrumentId} {trade.Price} x {trade.Size}");
        }
    }

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

No key in that file. The precedence chain is the session's own ApiKey, then Databento:ApiKey, then DATABENTO_API_KEY — so a key can stay in the environment while everything else stays in configuration. A missing dataset, a schema that is not a schema, a subscription with no symbols or a duration that is not ISO-8601 fails at startup, naming its configuration path — startup validation converts these strings to the library's types, so what it catches is everything that conversion can see. A dataset you are simply not entitled to is not one of those; the gateway says so, at start_session.

Reconnection is bounded and off the caller's hands: LiveClient itself never reconnects, because a client that silently re-subscribes can hand you a gap it did not mention. The hosted session does reconnect, with capped exponential backoff and a MaxAttempts ceiling, and it re-subscribes from the configuration it was given. AddHealthCheck() on the same builder reports the session's state to an ASP.NET Core /health endpoint, and four instruments publish on the DatabentoDotNet.Extensions.Hosting meter.

Hosting and Dependency Injection is the full guide — two sessions in one host, the transport seam, and what startup validation does and does not cover.

Samples

Five runnable console programs live under samples/ — a live stream, a historical range, a batch download, symbol resolution applied to decoded records, and the same live stream again as a hosted BackgroundService configured from appsettings.json. Each takes its key from DATABENTO_API_KEY, each runs with no arguments beyond what HostedLive's configuration file supplies, and each says what it costs before it spends anything.

export DATABENTO_API_KEY=db-...
dotnet run --project samples/DatabentoDotNet.Samples.HistoricalRange

See samples/README.md for what each one shows and what it costs to run.

Documentation

jerbersoft.github.io/databentodotnet is the documentation — the guides, the API reference and the release notes, on one searchable site. Start at Getting Started.

The two pages worth reading before writing anything real: Zero-Copy and Allocation (a RecordRef is valid until the next decoder call, and breaking that reads stale bytes rather than throwing) and Timestamps and Prices (nanoseconds, NodaTime, and the three sentinels).

The API reference is generated from the XML documentation comments, so it cannot drift from the code it describes. The same comments ship inside the NuGet package, which means every member's docs — worked <example> blocks included — also reach IntelliSense in your editor, at the call site, without opening a browser.

That site replaced the wiki in #82. The guides live in docs/ now, which means a change in behaviour and the change to the page describing it land in the same pull request and are reviewed together — and the site is built with --warningsAsErrors, so a cross-reference that stops resolving fails the build instead of becoming a dead link nobody reports.

For contributing: CLAUDE.md · PORTING.md · ROADMAP.md

Why this exists

Databento maintains official clients for Python, C++, and Rust — but not .NET. This fills that gap, with the wire format ported from the normative databento/dbn Rust implementation and struct layouts pinned against the static_asserts in databento-cpp.

Packages

DatabentoDotNet.* is used consistently for package IDs, assemblies, and namespaces. This is a third-party client, so it stays out of Databento.* — that is the vendor's namespace, and an unreserved NuGet prefix they could claim at any time.

Package / namespace NuGet Contents
DatabentoDotNet.Dbn NuGet DBN codec: records, metadata, symbol maps
DatabentoDotNet.Live NuGet Real-time TCP gateway client
DatabentoDotNet.Historical NuGet Historical HTTPS/REST client
DatabentoDotNet.Reference NuGet Security master, corporate actions
DatabentoDotNet.Extensions.Hosting NuGet IServiceCollection registration, IConfiguration binding, and a hosted live session

All five badges read the live feed, so they are the authority on what is actually published — not this page. The fifth package is newer than the others and carries no SemVer promise yet; see ROADMAP.md §8.

Install

All five packages are published to nuget.org:

dotnet add package DatabentoDotNet.Dbn
dotnet add package DatabentoDotNet.Live
dotnet add package DatabentoDotNet.Historical
dotnet add package DatabentoDotNet.Reference
dotnet add package DatabentoDotNet.Extensions.Hosting   # ASP.NET Core / generic host
using DatabentoDotNet.Dbn;

Status

Status
CI CI
Native AOT Native AOT
Publish Publish NuGet
License Apache 2.0
Latest GitHub Release

Target frameworks

net10.0, with three public dependencies across the four client packages, each a deliberate cost rather than an accident: NodaTime for all date and time handling, ZstdSharp.Port for DBN's Zstandard transport compression, and Microsoft.Extensions.Logging.Abstractions for the two HTTP clients' optional LoggerFactory. DatabentoDotNet.Extensions.Hosting adds the Microsoft.Extensions.* packages it exists to integrate with — options and configuration binding, hosting abstractions, IHttpClientFactory and health checks — which is what a hosting package is, not an accident either. ZstdSharp.Port is pure managed — no P/Invoke, no native asset, no per-RID build — so the packages stay trim- and AOT-friendly, which is verified by publishing and running a Native AOT binary rather than by the analyzers alone.

A net11.0 target existed briefly, to pick up System.IO.Compression.ZstandardStream from the BCL and ship dependency-free. It was removed in #16 while .NET 11 is still preview: the preview SDK is not installed on dev machines, so that code path was compiled nowhere, and CI inferred the target from the installed SDK — meaning a failed SDK resolution silently dropped it and the build still passed. An unverifiable branch is worse than one dependency.

Every zstd call routes through a single internal seam, so restoring the target at GA is a one-file change.

Building

dotnet build
dotnet test

Requires the .NET 10 SDK or newer. CI builds and tests on Linux, macOS, and Windows, and a separate workflow publishes and runs a Native AOT binary on every push.

About

Databento Client for .NET

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages