Jupitus.Sdk 0.7.0-alpha.26

This is a prerelease version of Jupitus.Sdk.
dotnet add package Jupitus.Sdk --version 0.7.0-alpha.26
                    
NuGet\Install-Package Jupitus.Sdk -Version 0.7.0-alpha.26
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Jupitus.Sdk" Version="0.7.0-alpha.26" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Jupitus.Sdk" Version="0.7.0-alpha.26" />
                    
Directory.Packages.props
<PackageReference Include="Jupitus.Sdk" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Jupitus.Sdk --version 0.7.0-alpha.26
                    
#r "nuget: Jupitus.Sdk, 0.7.0-alpha.26"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Jupitus.Sdk@0.7.0-alpha.26
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Jupitus.Sdk&version=0.7.0-alpha.26&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Jupitus.Sdk&version=0.7.0-alpha.26&prerelease
                    
Install as a Cake Tool

Jupitus.Sdk

The C# extension SDK for the Jupitus analytical terminal.

Implement IMarketDataProvider (or any other contribution-point interface) to connect your own data source, broker, or indicator library to Jupitus. Your extension is discovered at runtime from ~/.jupitus/extensions/ — no changes to the Jupitus source are required.

NuGet License


Installation

dotnet add package Jupitus.Sdk

Quick start — market data provider

Create a class library, add the package, implement the interface, and ship the compiled assembly alongside a jupitus.manifest.json.

1. Implement IMarketDataProvider

using Jupitus.Sdk.Abstractions;
using Jupitus.Sdk.Models;

public sealed class MyProvider : IMarketDataProvider
{
    public string Id          => "myorg.myprovider";
    public string DisplayName => "My Provider";

    public IReadOnlySet<AssetClass> SupportedAssetClasses =>
        new HashSet<AssetClass> { AssetClass.Equity };

    public MarketDataCapabilities? GetCapabilities(Instrument instrument)
    {
        // Return null for instruments you don't support.
        if (!string.Equals(instrument.Exchange, "MYEXCHANGE", StringComparison.OrdinalIgnoreCase))
            return null;

        return new MarketDataCapabilities(
            SupportsStreaming:       true,
            SupportsHistorical:     true,
            DataDelay:              TimeSpan.Zero,
            SupportedTimeFrames:    new HashSet<TimeFrame> { TimeFrame.OneMinute, TimeFrame.OneHour },
            SupportedPriceTypes:    new HashSet<PriceType> { PriceType.Last },
            EarliestHistoricalData: null,
            PriceSource:            PriceSource.Exchange);
    }

    public async Task<IReadOnlyList<Bar>> GetHistoricalBarsAsync(
        Instrument instrument, TimeFrame timeFrame, PriceType priceType,
        DateTimeOffset from, DateTimeOffset to, CancellationToken ct)
    {
        // Fetch OHLCV bars from your source.
        // Bar.Timestamp is the bar OPEN time (UTC).
        // Use decimal throughout — no double in domain code.
        return Array.Empty<Bar>();
    }

    public async IAsyncEnumerable<Bar> SubscribeBarsAsync(
        Instrument instrument, TimeFrame timeFrame, PriceType priceType,
        DateTimeOffset from, [EnumeratorCancellation] CancellationToken ct)
    {
        // Stream live bars until ct is cancelled.
        // Same Timestamp as the previous bar = update live candle in-place.
        // New Timestamp = previous bar closed, new bar started.
        while (!ct.IsCancellationRequested)
        {
            yield return await FetchNextBarAsync(instrument, ct);
        }
    }

    public async IAsyncEnumerable<Tick> SubscribeTicksAsync(
        Instrument instrument, [EnumeratorCancellation] CancellationToken ct)
    {
        // Stream live trade prints until ct is cancelled.
        while (!ct.IsCancellationRequested)
        {
            yield return await FetchNextTickAsync(instrument, ct);
        }
    }
}

2. Implement IExtension

using Jupitus.Sdk.Abstractions;

public sealed class MyExtension : IExtension
{
    public Task ActivateAsync(IExtensionContext context, CancellationToken ct)
    {
        var provider = new MyProvider();
        context.MarketData.Register(provider);

        // If your provider holds resources (HttpClient, WebSocket, etc.),
        // register it as disposable so the terminal cleans up on shutdown.
        context.RegisterDisposable(provider);

        return Task.CompletedTask;
    }

    public Task DeactivateAsync(CancellationToken ct) => Task.CompletedTask;
}

3. Add jupitus.manifest.json

Place this alongside your compiled assembly in the extension directory:

{
  "$schema": "https://jupitus.io/schemas/manifest/v1.json",
  "jupitusManifest": "1.0",
  "id": "myorg.myprovider",
  "version": "1.0.0",
  "displayName": "My Provider",
  "description": "Connects Jupitus to My Exchange.",
  "contributes": {
    "marketData": [
      { "entryPoint": "MyNamespace.MyExtension" }
    ]
  }
}

4. Install

Copy the extension folder to ~/.jupitus/extensions/myorg.myprovider/. Jupitus discovers and loads it on next start.


Core interfaces

IMarketDataProvider

The primary contribution point. Delivers OHLCV bars and live ticks for one or more instruments.

Member Purpose
Id Stable identifier (e.g. "myorg.provider"). Never changes.
DisplayName Human-readable name shown in the UI.
SupportedAssetClasses Coarse filter — terminal skips provider for asset classes not in this set.
GetCapabilities(instrument) Per-instrument precision — return null if not supported.
GetHistoricalBarsAsync(...) Fetch historical OHLCV bars for a range.
SubscribeBarsAsync(...) Stream live bars as IAsyncEnumerable<Bar>.
SubscribeTicksAsync(...) Stream live ticks as IAsyncEnumerable<Tick>.
GetTickerStatsAsync(...) Optional: 24hr price statistics for watchlist change%. Default returns empty.

IInstrumentSource

How your provider's instruments become findable. Without it, a user has to already know the exact symbol — they cannot type "Apple" and get AAPL. Implement it and your catalogue joins the terminal's cross-provider search and its typed Resolve.

There is no separate registry. Implement IInstrumentSource on the same class as your IMarketDataProvider and register once, exactly as you already do:

public sealed class AcmeProvider : IMarketDataProvider, IInstrumentSource
{
    public string Id          => "acme.provider";
    public string DisplayName => "Acme";

    public InstrumentDiscoveryMode DiscoveryMode => InstrumentDiscoveryMode.Enumerable;

    public Task<IReadOnlyList<InstrumentDescriptor>> ListInstrumentsAsync(CancellationToken ct)
        => FetchCatalogueAsync(ct);   // let failures out — see the rules below

    public Task<IReadOnlyList<InstrumentDescriptor>> SearchInstrumentsAsync(
        string query, AssetClass? assetClass, int limit, CancellationToken ct) => /* ... */;

    public Task<InstrumentDescriptor?> ResolveAsync(
        string symbolOrName, AssetClass? assetClassHint, CancellationToken ct) => /* ... */;

    // ... IMarketDataProvider members
}

// One registration covers both roles.
context.MarketData.Register(new AcmeProvider());
Member Purpose
Id Same value as your IMarketDataProvider.Id. Appears in every descriptor you return.
DiscoveryMode Enumerable | Searchable | None. A promise about listing and searching only.
ListInstrumentsAsync(ct) Hand over the whole universe once; the host indexes it locally. Enumerable only.
SearchInstrumentsAsync(...) Answer a query directly. For universes too large or opaque to hand over.
ResolveAsync(...) Symbol-or-name → typed instrument, or null for "not mine". Required in every mode.

Choosing a discovery mode:

  • Enumerable — you can return your full symbol list in one call (an exchange symbol table, a broker's tradable set). The host indexes it, so type-ahead costs no network round-trip. Prefer this whenever it is possible.
  • Searchable — the universe is too large or opaque to hand over, but you can answer queries. The host proxies to you on a debounce, so your results arrive after indexed ones.
  • None — you offer no discovery surface. A legitimate choice, not a failure: you stay out of type-ahead and still serve ResolveAsync for symbols the user already knows. ListInstrumentsAsync and SearchInstrumentsAsync should throw NotSupportedException.

Rules — read these before you write the implementation:

  • An empty result means "I looked and there is nothing". Nothing else. If the lookup could not be performed — network down, credential rejected, rate limit exhausted, plan does not include the endpoint — throw. Do not return an empty list, and do not return null from ResolveAsync to mean "could not ask". The host catches, logs with attribution, and turns it into a typed error; one source throwing never ends the fan-out over the others. A failure collapsed into an empty result is unrecoverable — the terminal can then only render "no match", which is a confident, wrong claim about the instrument universe made by a provider that never managed to look.
  • Do not offer what your bar path will not serve. If GetCapabilities returns null for an instrument, do not return it from List, Search or Resolve. A user who can find and select an instrument expects a chart for it; surfacing one you cannot price produces a dead end that reads as a broken terminal.
  • Put your full id in every descriptor. InstrumentDescriptor.ProviderId must be your Id verbatim, including its namespace prefix — "acme.provider", never "provider". The registry matches exactly, with no prefix-stripping and no aliasing, so a short name resolves to nothing at all and the resulting failure looks exactly like "this instrument has no data".
  • Cancellation is not a fault. Honour the token; OperationCanceledException is expected and is never logged as an error.
  • Do not guess between near-matches in ResolveAsync. Returning the wrong instrument confidently is worse than returning null.
  • assetClassHint is a hint, not a filter. If the only match sits in another class, returning it is correct.
  • A non-positive limit means "use your own sensible default", never "return nothing".

Asset class: InstrumentDescriptor carries AssetClass — the nine-value vocabulary (forex, equity, crypto, futures, options, commodity, index, ETF, bond) — rather than the older six-value InstrumentType, which cannot express ETF, commodity or bond. The search UI groups by asset class, so declare yours accurately. Where the two vocabularies must meet, AssetClassMapping is the one helper that does it; its rule is that a value the older enum cannot express matches nothing, never something adjacent.

IExtension

The extension lifecycle contract. ActivateAsync is called once at startup; DeactivateAsync is called on shutdown (or when the extension is unloaded).

public interface IExtension
{
    Task ActivateAsync(IExtensionContext context, CancellationToken cancellationToken);
    Task DeactivateAsync(CancellationToken cancellationToken);
}

IExtensionContext

Passed to ActivateAsync. Use it to register providers and disposables.

public interface IExtensionContext
{
    IMarketDataProviderRegistry MarketData  { get; }
    void RegisterDisposable(IDisposable disposable);
}

Key types

// OHLCV bar — Timestamp is bar OPEN time (UTC).
sealed record Bar(
    DateTimeOffset Timestamp,
    decimal Open, decimal High, decimal Low, decimal Close, decimal Volume,
    decimal? Spread    = null,  // bid-ask spread at close; null if unavailable
    long?    TickCount = null   // trade count; null if unavailable
);

// Single trade print.
sealed record Tick(
    DateTimeOffset Timestamp,
    decimal Price,
    decimal Size,
    TickSide Side       // Bid | Ask | Last
);

// Instrument routing.
sealed record Instrument(
    string Symbol,      // e.g. "BTC/USDT", "AAPL"
    string Exchange,    // e.g. "BINANCE", "NYSE"
    string Description,
    InstrumentType Type,
    decimal TickSize,
    decimal PointValue,
    string Currency,
    DateOnly? Expiry = null
);

// Instrument DISCOVERY — what IInstrumentSource returns. Distinct from Instrument
// above, which is the pricing shape the bar/tick path speaks. A descriptor answers
// "what is this thing, who serves it, what is it called?"; an Instrument answers
// "what do I pass to a market-data call?".
sealed record InstrumentDescriptor(
    string     Symbol,      // exactly as ProviderId expects it back — round-trips verbatim
    string     Name,        // what a user would search for, e.g. "Apple Inc."
    string     Exchange,    // e.g. "BINANCE", "XNAS"; empty if there is no venue
    AssetClass AssetClass,  // the nine-value vocabulary — NOT InstrumentType
    string     ProviderId,  // your full runtime id: "acme.provider", never "provider"
    decimal    TickSize     // 0 = you do not publish one; NOT "no tick size"
);

// How your instruments are discovered. See IInstrumentSource above.
// None is the zero value deliberately: an unset field must claim the least.
enum InstrumentDiscoveryMode { None, Enumerable, Searchable }

// Returned by GetCapabilities — drives UI (disables unsupported timeframes, etc.).
sealed record MarketDataCapabilities(
    bool                    SupportsStreaming,
    bool                    SupportsHistorical,
    TimeSpan                DataDelay,              // Zero = real-time
    IReadOnlySet<TimeFrame> SupportedTimeFrames,
    IReadOnlySet<PriceType> SupportedPriceTypes,
    DateTimeOffset?         EarliestHistoricalData, // null = unknown
    PriceSource             PriceSource             // Exchange | DealerQuote | CFD | Synthetic
);

Supported timeframes

Tick, OneSecond, FiveSeconds, TenSeconds, ThirtySeconds,
OneMinute, TwoMinutes, ThreeMinutes, FiveMinutes, TenMinutes,
FifteenMinutes, ThirtyMinutes, OneHour, TwoHours, FourHours,
SixHours, TwelveHours, OneDay, OneWeek, OneMonth

Important rules

  • Use decimal for all prices and volumes — never double in domain code. Convert at the wire boundary only (e.g. when reading from a REST/WebSocket response that delivers floating-point numbers).
  • CancellationToken must be passed through all I/O calls. The terminal cancels subscriptions when the user changes instrument or closes the panel.
  • SubscribeBarsAsync bar stitching: from is the open timestamp of the last historical bar. Start your stream from there; the kernel deduplicates by timestamp so overlap is safe.
  • Throw NotSupportedException from GetHistoricalBarsAsync / SubscribeBarsAsync if called with an unsupported TimeFrame or PriceType. The terminal validates capabilities before dispatching, so this should only occur during development.

Reference implementations

The Jupitus repository contains built-in provider implementations you can study. Each implements IMarketDataProvider and IInstrumentSource on one class, so between them they show both discovery modes in working code:

Provider Location Mode What it shows
Simulation extensions/provider-simulation/ Enumerable Seeded random-walk OHLCV data over a fixed in-process catalogue. Simplest possible implementation.
Binance extensions/provider-binance/ Enumerable REST + WebSocket, pagination, tick streaming, 24hr stats, and a whole symbol table enumerated from one exchangeInfo call. Full production pattern.
Massive extensions/provider-polygon/ Searchable An opaque universe behind a rate-limited search endpoint — the non-enumerable path, and how a 403 / 429 / 401 reaches the user as a failure rather than as "no such symbol".

Choosing your DiscoveryMode

Declare what your source can do today, not what it will do once something ships. The host reads the mode before it calls anything, so an overstated one does not produce a clear error — it produces a NotSupportedException attributed to your provider for a question it should never have been asked.

  • Can you hand over the whole list in one call? Enumerable.
  • Only answer queries? Searchable.
  • Neither? None — a legitimate mode, not a degraded one. You still implement ResolveAsync, so a user who already knows a symbol can still reach it.

If you are tempted to declare Enumerable because the enumeration is nearly ready, wait. Changing the mode and wiring the call belong in the same commit.


Versioning

This SDK follows Semantic Versioning. Until v1.0.0, minor versions may contain breaking interface changes as the contribution point model is finalised. Lock to a minor version range in pre-1.0:

<PackageReference Include="Jupitus.Sdk" Version="[0.1,0.2)" />

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.7.0-alpha.26 66 9/1/2026
0.7.0-alpha.25 70 8/29/2026
0.7.0-alpha.24 65 8/25/2026
0.7.0-alpha.23 66 8/24/2026
0.7.0-alpha.22 67 8/19/2026
0.7.0-alpha.21 88 8/17/2026
0.7.0-alpha.20 67 8/16/2026
0.7.0-alpha.19 78 8/2/2026
0.7.0-alpha.18 71 8/2/2026
0.7.0-alpha.17 60 7/25/2026
0.7.0-alpha.16 67 7/25/2026
0.7.0-alpha.15 71 7/24/2026
0.7.0-alpha.14 67 7/23/2026
0.7.0-alpha.13 60 7/23/2026
0.7.0-alpha.12 58 7/20/2026
0.7.0-alpha.11 60 7/20/2026
0.7.0-alpha.10 54 7/18/2026
0.7.0-alpha.9 62 7/17/2026
0.7.0-alpha.8 73 6/28/2026
0.7.0-alpha.7 84 6/23/2026
Loading failed