Jupitus.Sdk
0.7.0-alpha.26
dotnet add package Jupitus.Sdk --version 0.7.0-alpha.26
NuGet\Install-Package Jupitus.Sdk -Version 0.7.0-alpha.26
<PackageReference Include="Jupitus.Sdk" Version="0.7.0-alpha.26" />
<PackageVersion Include="Jupitus.Sdk" Version="0.7.0-alpha.26" />
<PackageReference Include="Jupitus.Sdk" />
paket add Jupitus.Sdk --version 0.7.0-alpha.26
#r "nuget: Jupitus.Sdk, 0.7.0-alpha.26"
#:package Jupitus.Sdk@0.7.0-alpha.26
#addin nuget:?package=Jupitus.Sdk&version=0.7.0-alpha.26&prerelease
#tool nuget:?package=Jupitus.Sdk&version=0.7.0-alpha.26&prerelease
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.
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 serveResolveAsyncfor symbols the user already knows.ListInstrumentsAsyncandSearchInstrumentsAsyncshould throwNotSupportedException.
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
nullfromResolveAsyncto 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
GetCapabilitiesreturnsnullfor an instrument, do not return it fromList,SearchorResolve. 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.ProviderIdmust be yourIdverbatim, 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;
OperationCanceledExceptionis expected and is never logged as an error. - Do not guess between near-matches in
ResolveAsync. Returning the wrong instrument confidently is worse than returningnull. assetClassHintis a hint, not a filter. If the only match sits in another class, returning it is correct.- A non-positive
limitmeans "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
decimalfor all prices and volumes — neverdoublein domain code. Convert at the wire boundary only (e.g. when reading from a REST/WebSocket response that delivers floating-point numbers). CancellationTokenmust be passed through all I/O calls. The terminal cancels subscriptions when the user changes instrument or closes the panel.SubscribeBarsAsyncbar stitching:fromis the open timestamp of the last historical bar. Start your stream from there; the kernel deduplicates by timestamp so overlap is safe.- Throw
NotSupportedExceptionfromGetHistoricalBarsAsync/SubscribeBarsAsyncif called with an unsupportedTimeFrameorPriceType. 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 implementResolveAsync, 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)" />
Links
- Website: https://jupitus.io
- GitHub: https://github.com/jupitus-io/jupitus
- Issues: https://github.com/jupitus-io/jupitus/issues
- License: Apache 2.0
| Product | Versions 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. |
-
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 |