Ozakboy.TradeKit.Abstractions 0.6.0

dotnet add package Ozakboy.TradeKit.Abstractions --version 0.6.0
                    
NuGet\Install-Package Ozakboy.TradeKit.Abstractions -Version 0.6.0
                    
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="Ozakboy.TradeKit.Abstractions" Version="0.6.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Ozakboy.TradeKit.Abstractions" Version="0.6.0" />
                    
Directory.Packages.props
<PackageReference Include="Ozakboy.TradeKit.Abstractions" />
                    
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 Ozakboy.TradeKit.Abstractions --version 0.6.0
                    
#r "nuget: Ozakboy.TradeKit.Abstractions, 0.6.0"
                    
#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 Ozakboy.TradeKit.Abstractions@0.6.0
                    
#: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=Ozakboy.TradeKit.Abstractions&version=0.6.0
                    
Install as a Cake Addin
#tool nuget:?package=Ozakboy.TradeKit.Abstractions&version=0.6.0
                    
Install as a Cake Tool

Ozakboy.TradeKit.Abstractions

Exchange-neutral abstractions for derivatives trading on .NET.

繁體中文說明

This package defines what a futures exchange looks like — orders, positions, accounts, candles, the book, fills, and the trading rules that govern them — so that strategy and backtest code depends on the shape of an exchange rather than on one particular exchange's client library.

It contains type definitions and pure functions only. There is no HTTP, no WebSocket, no signing, and no exchange-specific code anywhere in it.

Why it exists

Backtesting. A backtest engine has to inject a fake exchange, which is impossible if the strategy talks directly to a concrete exchange client. Every interface here is designed against one hard acceptance criterion:

A purely in-memory matching engine must be able to implement it.

Nothing in the API presumes a network connection, a WebSocket, or an API key. The test suite includes a small in-memory exchange that implements every interface, as a standing check on that claim.

Install

dotnet add package Ozakboy.TradeKit.Abstractions

Targets net10.0. Depends only on Ozakboy.Core.Abstractions — no third-party packages.

Design rules

Rule Reason
Every fallible operation returns Result<T> Rejections, rate limits, and missing orders are routine, not exceptional. Exceptions are reserved for defects.
All prices, quantities, and amounts are decimal Binary floating point cannot represent 0.1, and the error turns into real differences in order size.
All timestamps are DateTimeOffset with UTC semantics Mixing time zones skews reconciliation and backtest timelines, and the skew moves with daylight saving.
Every model is immutable (record / readonly record struct / init-only) Snapshots that can be mutated after the fact cannot be reasoned about or replayed.
Quantities are always aligned down to the step size Rounding up makes the real position larger than the size risk management calculated.

Quick start

Normalising an order before sending it:

var symbol = new SymbolInfo
{
    Name = "BTCUSDT",
    BaseAsset = "BTC",
    QuoteAsset = "USDT",
    TickSize = 0.1m,
    StepSize = 0.001m,
    MinQuantity = 0.001m,
    MinNotional = 100m,
};

var request = new OrderRequest
{
    Symbol = "BTCUSDT",
    Side = OrderSide.Buy,
    OrderType = OrderType.Limit,
    Price = 50_000.16m,   // not on a tick
    Quantity = 0.0025m,   // not on a step
    ClientOrderId = "pt-0001",
};

var normalised = request.NormalizeFor(symbol);

if (normalised.TryGetValue(out var ready))
{
    // Price 50000.2, quantity 0.002, notional checked against MinNotional.
    var placed = await exchange.PlaceOrderAsync(ready, cancellationToken);
}
else
{
    // e.g. trade.notional_below_min — this order cannot be traded at all.
    logger.LogWarning("{Error}", normalised.Error);
}

An order that is too small comes back as an explicit failure rather than a value the exchange will reject with an opaque "invalid parameter".

What is in the box

EnumsOrderSide, PositionSide, OrderType, TimeInForce, OrderStatus, KlineInterval, MarginMode, TriggerPriceType, PriceRounding, ConditionalOrderType, ConditionalOrderStatus.

ModelsSymbolInfo (identity plus trading rules and the normalisation methods), OrderRequest, Order, OrderIdentifier, Position, Balance, AccountSnapshot (both with maintenance and initial margin, null when the exchange does not provide them), Kline, KlineQuery, Trade, MarkPriceUpdate, BookTicker (the best bid and ask with their sizes), NormalizedOrderSize, AccountUpdate (a delta, not a snapshot, built from PositionChange and BalanceChange, which carry only what the event delivers), MarginCall (with MarginCallPosition), ResyncRequired, and the conditional order set: ConditionalOrderRequest, ConditionalOrder, ConditionalOrderUpdate, ConditionalOrderIdentifier.

InterfacesIExchangeInfoProvider (symbols, server time), IExchangeClient (account, positions, orders, conditional orders), IMarketDataFeed (historical klines, live klines, mark prices, and the book), IUserDataFeed (orders, conditional orders, fills, account changes, margin calls, and the signal that local state must be reconciled in full).

Error codesTradeErrorCodes holds the neutral codes (trade.order_not_found, trade.notional_below_min, trade.rate_limited, …) and TradeErrors builds the corresponding Error values. Mapping an exchange's own codes onto this set is the implementation package's job, so that no strategy ever contains if (code == -2011).

A maker order is priced from the book, not from the last price

BookTicker carries the best bid and ask with the size resting at each, and IMarketDataFeed exposes it twice: GetBookTickerAsync(symbol) for one snapshot and SubscribeBookTickersAsync(symbols) for the live stream.

var book = await feed.GetBookTickerAsync("BTCUSDT");

if (book.TryGetValue(out var top))
{
    // Post-Only buy: at the bid or below, never across the spread.
    var entryPrice = top.BidPrice - (2 * symbol.TickSize);
}

A strategy that must not take liquidity — Post-Only, or Binance's GTX — has to quote from the bid and the ask. Pricing from the last traded price or from MarkPriceUpdate.MarkPrice produces an order that would match on arrival, and the exchange rejects it outright. Nothing looks broken: the strategy runs, signals fire, and no position is ever opened.

Two things the type insists on:

  • ReceivedAt is required. A book starts going stale the moment it leaves the exchange, and a stale one is indistinguishable from a fresh one without a timestamp. Defaulting it would put every book in the year 1, so a freshness check would silently reject all of them.
  • ExchangeTime is null when the exchange sends none, never a copy of ReceivedAt. The gap between the two is the transit latency, and filling one from the other makes that measurement permanently zero.

MidPrice is for valuation, not for quoting: a buy at the midpoint sits above the best bid, which for a maker order is a rejection waiting to happen. A negative Spread means a crossed book — bad data rather than a tradable market — and such a book should be discarded rather than priced from.

Conditional orders travel their own path

Stops, take-profits, and trailing stops go through PlaceConditionalOrderAsync, not PlaceOrderAsync — exchanges have moved them onto a separate service with their own numbering, cancellation endpoint, and state machine, and the plain order endpoint rejects those types outright.

Three consequences that are easy to miss:

  • GetOpenOrdersAsync does not see them. Reconciling with it alone concludes "no resting orders" while the stops sit safely on the other path — or are genuinely missing, which looks identical.
  • SubscribeOrderUpdatesAsync does not carry them. A stop triggering is visible only on SubscribeConditionalOrderUpdatesAsync; what reaches the order stream is the fill of the order the trigger produced, linked back only through ConditionalOrder.TriggeredOrderId.
  • CancelAllOrdersAsync does not cancel them. An emergency exit calls CancelAllConditionalOrdersAsync too, because a stop left behind after the position closes opens a new one in the opposite direction.

Streams are IAsyncEnumerable<Result<T>>

Subscriptions return IAsyncEnumerable<Result<T>> rather than events:

  • a backtest implementation is just a yield return over stored candles;
  • await foreach is sequential, so a strategy is never re-entered with the next candle while it is still handling the previous one;
  • cancellation and cleanup ride on the CancellationToken instead of an easily forgotten -=.

Elements are Result<T> because reconnects and unparsable messages are everyday events on a live stream, and the consumer should decide whether to log and continue rather than have an exception tear out of the loop.

  • Ozakboy.Core.AbstractionsResult, Money, Precision, RetryPolicy.
  • Ozakboy.TradeKit.Binance — the Binance USDⓈ-M implementation of these interfaces.

License

MIT. See LICENSE.

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.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Ozakboy.TradeKit.Abstractions:

Package Downloads
Ozakboy.TradeKit.Binance

Ozakboy.TradeKit.Abstractions 的幣安 USDⓈ-M 永續合約實作,建構在 Ozakboy.Http 的簽章/限流/重試管線之上,不使用任何第三方社群套件。本版提供交易規則(exchangeInfo 的 PRICE_FILTER / LOT_SIZE / MIN_NOTIONAL / MARKET_LOT_SIZE)對映與每日快照快取、伺服器時間、帳戶與持倉查詢,以及下單、撤單、查單與槓桿/保證金模式設定(下單一律不可重試,並以 clientOrderId 作為冪等識別碼),再加上幣安錯誤碼到交易所中立錯誤碼的完整對映(時間戳偏移、簽章無效、IP 白名單、保證金不足、限流各自可辨識)。REST 與 WebSocket 端點以「環境」成組提供,杜絕主網與 Testnet 混接;交易規則快取以環境為鍵,避免 Testnet 的步進值被誤用到主網。行情方面提供歷史 K 線與盤口(買一賣一)查詢,以及 WebSocket 的 K 線、標記價與盤口訂閱(連線管理、重連與重連後重放訂閱取自 Ozakboy.WebSockets);K 線的「是否已收盤」逐筆正確對映,REST 回應沒有這個旗標的部分則以收盤時間推得,策略不會拿到未收盤的 K 線當成收盤資料。條件單(停損、停利、移動停損)走幣安 2025-12-09 起啟用的 Algo Order 端點,與一般委託分成兩條路徑:送單同樣絕不重試,clientAlgoId 失敗時也帶得回來,狀態變化由使用者資料串流的 ALGO_UPDATE 事件送出,而一般的掛單查詢、委託串流與撤銷全部掛單都看不到條件單,緊急出場必須兩邊都撤。使用者資料串流提供委託、條件單、成交、帳戶增量、保證金追繳與對帳訊號六種訂閱:單一連線內部分流,listenKey 自動建立/續期/重建/刪除,以 LIST_SUBSCRIPTIONS 心跳做閒置偵測;跟不上的訂閱者以失敗結束而不靜默丟棄事件,串流憑證不進任何錯誤與日誌,並在取得當下登記成遮罩器的已知祕密,連本套件管不到的路徑流出的那一份也會被換成遮罩字串。憑證的續期成功、續期失敗與失效都有日誌與計數(日誌一律不含憑證),次數與時刻另以 ListenKeyStatus 快照公開給健康度呈現,續期失敗會以短退避重試而不是空等下一個排程。A Binance USDⓈ-M perpetual futures implementation of Ozakboy.TradeKit.Abstractions, built on the Ozakboy.Http signing, rate-limiting, and retry pipeline with no third-party dependencies. This release covers exchange-info trading rules with a per-environment daily cache, server time, account and position queries, order placement, cancellation and lookup, leverage and margin mode, and a full Binance-to-neutral error code mapping. Order placement is never retried and carries a clientOrderId as its idempotency key. Market data covers historical klines and book ticker snapshots plus WebSocket kline, mark price, and book ticker subscriptions, with the candle closed flag mapped from the stream and derived from the close time on REST, never assumed. Conditional orders (stop, take-profit, trailing) use the Algo Order endpoints Binance switched to on 2025-12-09 and form a path of their own: placement is never retried, the clientAlgoId comes back even on failure, state changes arrive on the user data stream as ALGO_UPDATE, and the ordinary open-orders query, order stream, and cancel-all never see them, so an emergency exit has to clear both sides. The user data stream covers order updates, conditional order updates, fills, account deltas, margin calls, and resync signals over one fanned-out connection, with a fully managed listenKey and heartbeat-based idle detection; a subscriber that falls behind ends with a failure instead of silently losing events, and the stream credential never reaches an error or a log and is registered as a known secret on the client masker the moment it is obtained, so even a copy leaving by a route this package does not control comes out masked. Credential renewals, their failures, and an expiry are logged and counted without the credential ever appearing in a line, the counts and instants are exposed as a ListenKeyStatus snapshot for health display, and a failed renewal is retried after a short backoff rather than waiting out the next scheduled attempt.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.6.0 106 9/16/2026
0.5.0 97 9/15/2026
0.4.0 123 9/14/2026
0.3.0 148 9/11/2026
0.2.0 95 9/11/2026
0.1.1 100 9/11/2026
0.1.0 94 9/11/2026

0.6.0 — The top of the book arrives. BookTicker carries Symbol, BidPrice, BidQuantity, AskPrice and AskQuantity — all required, all decimal — plus a nullable ExchangeTime, a required ReceivedAt, and the derived Spread and MidPrice. A pure-maker strategy (Post-Only, GTX) must quote from the bid and the ask; substituting the last traded price or the mark price prices an order that would match immediately, the exchange rejects it, and the strategy never enters a position while everything looks healthy. Breaking: IMarketDataFeed gains GetBookTickerAsync(symbol) and SubscribeBookTickersAsync(symbols), so existing implementations — test doubles and backtest feeds included — must supply them to compile. ReceivedAt is required because a defaulted DateTimeOffset of 0001-01-01 makes every book two thousand years old, and a freshness check then rejects all of them in silence. ExchangeTime is null when the exchange provides none and is never filled from ReceivedAt, which would make the measured transit latency permanently zero. The five price and size members have no missing case: an implementation fails the read rather than substituting zero, which would read as a zero ask or an empty bid. MidPrice is a valuation reference, not a tradable price. 0.5.0 — The account model gains the maintenance margin. Balance adds MaintenanceMargin and InitialMargin, and AccountSnapshot adds TotalMaintenanceMargin and TotalInitialMargin, all decimal?. A margin ratio (margin balance ÷ maintenance margin) needs the maintenance margin; substituting the much larger initial margin understates the ratio and blocks orders too early. null means the exchange did not provide the figure and 0 means it reported zero — a missing value is never filled with zero, because zero reads as no liquidation risk. The account totals are in the unit the exchange aggregates the account in, which in a multi-asset margin mode need not match any single asset. Not breaking: the new members are init properties defaulting to null, so existing implementations compile unchanged and report unknown. 0.4.0 — Conditional orders (stops, take-profits, trailing stops) get their own surface: ConditionalOrderRequest, ConditionalOrder, ConditionalOrderUpdate, ConditionalOrderType, ConditionalOrderStatus, ConditionalOrderIdentifier, and five neutral error codes. Breaking: IExchangeClient gains PlaceConditionalOrderAsync, CancelConditionalOrderAsync, GetConditionalOrderAsync, GetOpenConditionalOrdersAsync and CancelAllConditionalOrdersAsync, IUserDataFeed gains SubscribeConditionalOrderUpdatesAsync, and PlaceOrderAsync now takes non-conditional orders only — exchanges have moved conditional orders to a separate service and the plain order endpoint rejects those types. Conditional order state changes do NOT appear on SubscribeOrderUpdatesAsync. IExchangeClient also gains GetUserTradesAsync(symbol, since, fromId, limit) for reconciliation: the fill stream drops, nothing else backfills what was missed while it was down, and the engine sweeps from the timestamp of its last known fill — such a sweep always overlaps, so callers de-duplicate on Trade.TradeId. Full notes: https://github.com/ozakboy/Ozakboy.TradeKit.Abstractions/blob/main/CHANGELOG.md