Ozakboy.Core.Abstractions
0.3.0
dotnet add package Ozakboy.Core.Abstractions --version 0.3.0
NuGet\Install-Package Ozakboy.Core.Abstractions -Version 0.3.0
<PackageReference Include="Ozakboy.Core.Abstractions" Version="0.3.0" />
<PackageVersion Include="Ozakboy.Core.Abstractions" Version="0.3.0" />
<PackageReference Include="Ozakboy.Core.Abstractions" />
paket add Ozakboy.Core.Abstractions --version 0.3.0
#r "nuget: Ozakboy.Core.Abstractions, 0.3.0"
#:package Ozakboy.Core.Abstractions@0.3.0
#addin nuget:?package=Ozakboy.Core.Abstractions&version=0.3.0
#tool nuget:?package=Ozakboy.Core.Abstractions&version=0.3.0
Ozakboy.Core.Abstractions
Zero-dependency .NET building blocks for systems that need exact decimal arithmetic and explicit error handling.
English | 繁體中文
dotnet add package Ozakboy.Core.Abstractions
Requires .NET 10. Nothing outside the BCL.
What is in it
Four types, each solving a problem that actually bit us.
| Type | Problem it solves |
|---|---|
Result / Result<T> |
Expected failures should not travel as exceptions |
Money |
Amounts in different currencies get added, and the type system says nothing |
Precision |
Decimal step alignment, and string serialisation for APIs |
RetryPolicy |
Describing a retry strategy and computing backoff intervals |
Written for an automated trading system, but nothing about trading leaked in. Any project can use it.
Result: getting expected failures out of exceptions
Network timeouts, rate limiting, arguments that fail validation — these happen every day. They are not exceptional. Modelling them as exceptions forces callers to write control flow inside try/catch, and turns "forgot to handle it" into a runtime surprise.
Result<Order> Submit(OrderRequest request)
{
if (request.Quantity <= 0)
{
return Error.Validation("order.quantity_invalid", "Quantity must be positive");
}
return new Order(request); // implicit conversion; just return the value
}
On the reading side:
var result = Submit(request);
if (result.TryGetValue(out var order))
{
Console.WriteLine($"Submitted {order.Id}");
}
else
{
// The compiler knows Error is not null here
Console.WriteLine(result.Error.Message);
}
IsSuccess and IsFailure both carry MemberNotNullWhen, so after a check the compiler knows whether Error is null. No null-forgiving operator needed.
Chains short-circuit on failure, and the intermediate delegates never run:
var report = LoadAccount(id)
.Ensure(a => a.IsActive, Error.Conflict("account.inactive", "Account is disabled"))
.Map(a => a.Balance)
.Then(BuildReport);
default(Result<T>) is a failure carrying Error.Uninitialized. That is deliberate: an unassigned result mistaken for success hides for a long time.
Genuine defects should still throw. Result is not a replacement for exceptions.
Async chains
The combinators above take synchronous delegates, so a chain breaks the moment one step is asynchronous. The *Async extensions accept both a Task<Result<T>> receiver and asynchronous continuations, so the chain survives:
var name = await LoadAccountAsync(id)
.EnsureAsync(a => a.IsActive, Error.Conflict("account.inactive", "Account is disabled"))
.ThenAsync(FetchProfileAsync) // Func<Account, Task<Result<Profile>>>
.MapAsync(p => p.DisplayName)
.MatchAsync(n => n, e => e.Code);
Failures short-circuit here too: no delegate after the first failure is invoked. Every await inside uses ConfigureAwait(false).
Crossing an exception boundary
Some signatures leave no room for a Result — DelegatingHandler.SendAsync must return Task<HttpResponseMessage>, BackgroundService.ExecuteAsync must return Task. ResultException is the one carrier for those:
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
{
var result = await _pipeline.SendAsync(request, ct);
return result.TryGetValue(out var response)
? response
: throw result.Error.ToException();
}
catch (ResultException ex)
{
logger.LogWarning("{Code}: {Message}", ex.Error.Code, ex.Error.Message);
if (ex.Error.IsTransient) { /* ... */ }
}
It derives from InvalidOperationException, which is also what ThrowIfFailure() and GetValueOrThrow() throw — so existing catch (InvalidOperationException) handlers keep working, and only the code that wants the Error back needs to change.
Money: making the currency part of the type
A trading system carries quote-currency balances (USDT) and base-currency quantities (BTC) side by side. Both are decimal, and nothing stops you adding them.
var balance = new Money(1000m, "USDT");
var position = new Money(0.05m, "BTC");
var wrong = balance + position; // CurrencyMismatchException
Same-currency arithmetic works as you would expect:
var fee = balance * 0.0004m;
var net = balance - fee;
Console.WriteLine(net); // 999.6 USDT
default(Money) is a currency-less zero that adds to anything, which makes it a convenient accumulator seed:
var total = fills.Aggregate(default(Money), (sum, f) => sum + f.Amount);
One asymmetry is intentional: Equals returns false across currencies without throwing, while CompareTo throws. Equality must be safe for any input; ordering must not silently mix currencies.
Precision: step alignment and string serialisation
Exchanges define a minimum increment for price and quantity on every instrument, and reject anything that is not aligned to it.
Precision.FloorToStep(0.123456m, 0.001m); // 0.123
Precision.CeilingToStep(0.123456m, 0.001m); // 0.124
Precision.IsAlignedToStep(0.123m, 0.001m); // true
Always use FloorToStep for quantities. Rounding up makes the real position larger than the size risk management calculated, quietly increasing exposure.
Serialisation is the other frequent trap. APIs take prices as strings, and decimal keeps its trailing zeros:
(1.2300m).ToString(); // "1.2300"
Precision.ToPlainString(1.2300m); // "1.23"
Precision.ToPlainString(0.00000001m); // "0.00000001", never 1E-08
Exponent notation or redundant zeros usually come back as an opaque parameter error that gives no hint of the real cause.
TryParsePlain and ParsePlain are the inverse, with a guaranteed round trip — and always InvariantCulture, which is the part that is easy to leave out and only breaks under a different locale:
Precision.TryParsePlain("0.00000001", out var qty); // true
Precision.TryParsePlain("1E-08", out _); // false, by design
Precision.ParsePlain("nope"); // Result<decimal> failure, ErrorCategory.Validation
RetryPolicy: describes the strategy, does not run the loop
var policy = new RetryPolicy
{
MaxAttempts = 5,
BaseDelay = TimeSpan.FromMilliseconds(200),
MaxDelay = TimeSpan.FromSeconds(30),
Strategy = BackoffStrategy.Exponential,
JitterRatio = 0.2,
};
for (var attempt = 1; attempt <= policy.MaxAttempts; attempt++)
{
var result = await CallAsync();
if (result.IsSuccess || !policy.ShouldRetry(attempt, result.Error))
{
return result;
}
await Task.Delay(policy.GetDelay(attempt));
}
It never retries anything and never touches I/O, so one policy applies across transports and the interval arithmetic stays testable without time — GetDelay(attempt, jitterSample) takes a fixed jitter sample and gives a fully deterministic result.
Only transient categories (Timeout, Network, RateLimited, Unavailable) are considered worth retrying by ShouldRetry. ErrorCategory.Exhausted is deliberately not one of them: it means the operation used to work but the retry or reconnect budget is spent, so the caller needs a fresh object rather than another attempt.
When transience is not the right test — one 429 carries a Retry-After and is worth retrying, another means the IP is banned — set RetryPredicate. It replaces the IsTransient check outright rather than adding to it, so there is only ever one place to look when asking why something was or was not retried:
var policy = RetryPolicy.Default with
{
RetryPredicate = e => e.TryGetInt64("retryAfterMs", out var ms) && ms < 5000,
};
MaxAttempts still always applies. RetryPredicate is excluded from equality, because two lambdas with identical source are never equal and would make two identical policies compare as different.
Do not retry non-idempotent operations. A timeout does not mean the request failed to arrive, and resending blindly duplicates the side effect. Use RetryPolicy.NoRetry there, and confirm the outcome through an idempotency key plus a follow-up query.
License
MIT
| 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 (6)
Showing the top 5 NuGet packages that depend on Ozakboy.Core.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. |
|
|
Ozakboy.TradeKit.Abstractions
交易所中立的合約交易抽象層:委託、持倉、帳戶、K 線、盤口(買一賣一)、成交等不可變模型,交易規則(tickSize/stepSize/minNotional)校正,以及 IExchangeClient / IMarketDataFeed / IUserDataFeed 介面。所有會失敗的操作一律回傳 Result<T>,價量金額一律 decimal,時間一律 UTC 語意的 DateTimeOffset;不含任何交易所專屬實作,可由純記憶體的回測撮合器完整實作。Exchange-neutral abstractions for derivatives trading: immutable order/position/account/kline/book-ticker/trade models, exchange trading-rule normalisation, and client and market-data interfaces. Every fallible operation returns Result<T>; no exchange-specific code, so an in-memory backtest engine can implement it end to end. |
|
|
Ozakboy.Http
建構在官方 HttpClientFactory 與 DelegatingHandler 之上的 HTTP 管線,取代 RestSharp / Flurl / Polly 這類第三方函式庫:順序固定的簽章、加權多桶限流、冪等感知重試與脫敏日誌四段處理器。簽章以有序參數容器杜絕字典列舉順序造成的隨機失敗,限流建於官方 System.Threading.RateLimiting 之上並以 TimeProvider 驅動,重試僅對安全方法或明確標記為冪等的請求生效並尊重 Retry-After。所有預期失敗以 Result<T> 表達而非例外。除 Microsoft 官方套件與 Ozakboy.* 自研套件外,零第三方相依。A signing, rate-limiting, retry, and log-masking DelegatingHandler pipeline built on the official HttpClientFactory, replacing third-party libraries such as RestSharp, Flurl, and Polly. Ordered query containers keep signatures deterministic, rate limiting is driven by TimeProvider for testability, retries apply only to safe or explicitly idempotent requests, and expected failures are returned as Result<T> rather than thrown. |
|
|
Ozakboy.WebSockets
建立在 BCL ClientWebSocket 之上的長命 WebSocket 客戶端:自動重連(退避+抖動)、重連後自動重放訂閱、閒置逾時存活偵測(ClientWebSocket 的 ping/pong 在協定層自動處理,應用層看不到,只有「多久沒收到訊息」能發現靜默的假連線)、System.Threading.Channels 有界佇列背壓與可觀察的丟棄、以 Result<T> 表達預期失敗。不含任何交易所專屬邏輯,也不做反序列化。A long-lived WebSocket client on top of the BCL ClientWebSocket: automatic reconnect with backoff and jitter, subscription replay after reconnect, idle-timeout liveness detection, bounded-channel backpressure with observable drops, and Result-based failures. |
|
|
Ozakboy.Ai.Abstractions
與供應商無關的語言模型抽象層:IAiCompletionClient 以「純文字進、型別化物件出」為唯一契約,請求、完成結果與 token / 費用計量皆為不可變模型。結構化輸出由 System.Text.Json 的 JsonSchemaExporter 從目標型別產生 JSON Schema 附進提示,回來的文字以 UnmappedMemberHandling.Disallow 嚴格反序列化,再交由呼叫端的語意驗證委派把關數值範圍——JSON Schema 在結構化輸出下不強制 minimum / maximum,那一段本來就得自己做。所有預期失敗一律回傳 Result<T>,費用為 decimal。除 .NET BCL 與 Ozakboy.Core.Abstractions 外零相依。Vendor-neutral abstractions for language models: IAiCompletionClient contracts plain text in and a typed object out, with immutable request, completion and token/cost models. Structured output derives a JSON Schema from the target type via System.Text.Json's JsonSchemaExporter, appends it to the prompt, deserialises the answer strictly with UnmappedMemberHandling.Disallow, and leaves value ranges to a caller-supplied semantic validator, because JSON Schema does not enforce minimum / maximum in a structured-output setting. Every expected failure returns Result<T>; cost is decimal. |
GitHub repositories
This package is not used by any popular GitHub repositories.