1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-10-16 16:10:53 +00:00

Compare commits

..

33 Commits

Author SHA1 Message Date
Jkorf b9f93cb3fe wip 2026-09-18 14:29:45 +02:00
Jkorf 15f7ba32e1 wip 2026-09-18 13:54:48 +02:00
Jkorf a34684cd31 Added WithRateLimitAdmissionAsync for REST and Socket clients 2026-09-17 11:33:47 +02:00
Jkorf e5da147ba8 Fixed parameters to form data url encoding in .Net Framework when parameters contain special characters 2026-09-16 14:56:16 +02:00
Jkorf 404d62ee5c Added RateLimitAdmission client option delegate 2026-09-16 14:07:38 +02:00
Jkorf 45a42fb028 wip 2026-09-15 10:04:46 +02:00
Jkorf c59bb0f625 wip 2026-09-11 13:25:50 +02:00
Jkorf 8419af278d wip 2026-09-11 13:22:48 +02:00
Jkorf 911e09be61 wip 2026-09-10 15:03:29 +02:00
Jkorf 876c2d27f7 wip 2026-09-10 13:26:33 +02:00
Jkorf dc4a6af695 wip 2026-09-10 12:53:11 +02:00
Jkorf c56e31abab wip 2026-09-10 12:02:57 +02:00
Jkorf 08972d3d63 wip 2026-09-08 15:15:08 +02:00
Jkorf 357809fd0c wip 2026-09-08 15:08:34 +02:00
Jkorf 7629455dec wip 2026-09-07 15:23:08 +02:00
JKorf 9df960361a wip 2026-09-06 20:27:37 +02:00
JKorf f215b729ed wip 2026-09-05 20:28:11 +02:00
Jkorf 431ef93611 wip 2026-09-04 16:21:13 +02:00
Jkorf a545efbb45 wip 2026-09-04 13:33:50 +02:00
Jkorf 67bf7d08cb wip 2026-09-04 12:40:54 +02:00
JKorf 2704693600 wip 2026-09-02 17:49:53 +02:00
Jkorf 1643487e73 wip 2026-09-02 09:07:49 +02:00
JKorf 1a0101a45f wip 2026-09-01 21:22:30 +02:00
Jkorf 20977e28b7 wip 2026-09-01 16:56:15 +02:00
Jkorf d0f5a8d6cc wip 2026-09-01 15:57:19 +02:00
Jkorf 2fff7a307f Merge branch 'master' into feature/shared-apis-v2 2026-09-01 09:58:01 +02:00
Jkorf ca2bf414a7 wip 2026-09-01 08:58:03 +02:00
Jkorf 5f101e3b88 wip 2026-08-31 11:23:53 +02:00
Jkorf 32e74e9bc4 wip 2026-08-27 15:57:28 +02:00
Jkorf d93d6b7a2e wip 2026-08-26 14:26:24 +02:00
Jkorf d0ce2563de wip 2026-08-25 16:09:15 +02:00
Jkorf 407509e9cd wip 2026-08-25 09:12:39 +02:00
Jkorf 3dd68dcad4 wip 2026-08-24 16:13:22 +02:00
25 changed files with 380 additions and 1066 deletions
+58 -63
View File
@@ -1,92 +1,87 @@
--- ---
description: Conventions for cross-exchange C# code using CryptoExchange.Net Shared API V2 capabilities. description: Conventions for cross-exchange code using CryptoExchange.Net SharedApis abstractions. Apply when generating C# code that interacts with multiple cryptocurrency exchanges through a unified interface.
globs: globs:
- "**/*.cs" - "**/*.cs"
- "**/*.csproj" - "**/*.csproj"
alwaysApply: false alwaysApply: false
--- ---
# CryptoExchange.Net conventions # CryptoExchange.Net Conventions
CryptoExchange.Net is the base dependency for exchange-specific libraries. Install Binance.Net, Bybit.Net, JK.OKX.Net, or another exchange library; use `CryptoClients.Net` when the application needs the bundle. This codebase uses **CryptoExchange.Net** abstractions for multi-exchange access. Each exchange has its own library (Binance.Net, Bybit.Net, OKX.Net, ...). Use `CryptoExchange.Net.SharedApis` for code that should work across exchanges.
## Use Shared API V2 ## Multi-exchange pattern
For new cross-exchange code, use the fine-grained interfaces in `CryptoExchange.Net.SharedApis` and each API surface's `.SharedApi` property:
```csharp ```csharp
using Binance.Net.Clients;
using OKX.Net.Clients;
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
IGetTickerRest binance = new BinanceRestClient().SpotApi.SharedApi; ISpotTickerRestClient binance = new BinanceRestClient().SpotApi.SharedClient;
IGetTickerRest okx = new OKXRestClient().UnifiedApi.SharedApi; ISpotTickerRestClient okx = new OKXRestClient().UnifiedApi.SharedClient;
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT"); var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var result = await binance.GetTickerAsync(new GetTickerRequest(symbol));
var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol));
// ticker.Data.LastPrice — same model regardless of exchange
``` ```
Use one capability per operation, such as `IGetTickerRest`, `IGetOrderBookRest`, `IPlaceSpotOrderRest`, or `ISubscribeTradesSocket`. Do not use a broad V1 interface for new code. `.SharedClient` remains only for incremental V1 migration. ## Symbol normalization
## Capability discovery `SharedSymbol(TradingMode.Spot, "BTC", "USDT")` is portable. Each library translates to its native format internally. Don't pass raw strings like `"BTCUSDT"` to shared methods.
When the exchange and API surface are known, assign the typed `.SharedApi` to the required capability. When selection is dynamic, use an exchange-wide `I[Exchange]SharedApiClient` and a strongly typed reference: ## Symbol metadata and catalogs
In 12.2.0, `SharedSpotSymbol` and `SharedFuturesSymbol` include `DisplayName` and base/quote asset classification through `SharedAssetType` (`Crypto`, `Fiat`, `TradFi`) and `SharedAssetSubType` (`StableCoin`, `Equity`, `Commodity`). Pass the matching base/quote filters to `GetSymbolsRequest` when discovery should return only a class of markets.
After calling `GetSpotSymbolsAsync`, `ISpotSymbolRestClient.SpotSymbolCatalog` maps asset and symbol names to shared metadata. `IFuturesSymbolRestClient.FuturesSymbolCatalog` works the same way after `GetFuturesSymbolsAsync`. Treat either property as unavailable before its corresponding request has populated the cache.
When implementing an exchange library, use `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` only as best-effort classifiers and supply exchange-specific additions where needed.
## Shared market-data quantities
In 12.4.0, use `SharedOrderQuantity`-valued `Volumes` on shared spot/futures tickers and klines, and `Quantities` on shared trades. The scalar `Volume`, `QuoteVolume`, and `Quantity` members are obsolete.
## WebSocket order commands
In 12.5.0, `ISpotOrderManagementSocketClient` and `IFuturesOrderManagementSocketClient` optionally provide place/cancel order commands over WebSocket. Their methods return `QueryResult<SharedId>`, not `WebSocketResult<UpdateSubscription>`. Check whether the exchange implements the interface.
## Result pattern
REST methods return `HttpResult<T>` and websocket subscription methods return `WebSocketResult<UpdateSubscription>`. Always check `.Success`. `.Exchange` property identifies which exchange responded — useful for logging.
## Available shared interfaces
- REST tickers/symbols/orderbook/klines/trades, orders (spot/futures, regular/trigger/TP-SL), balances, positions, fees, deposits/withdrawals, transfers
- WebSocket tickers, book tickers, order book, trades, klines, user data, and optional spot/futures order management
Each exchange documents which it implements. Not every exchange supports every operation.
## Multi-exchange aggregation
Run requests across exchanges concurrently via `Task.WhenAll` — the library is async-safe and concurrent requests are the norm.
```csharp ```csharp
var match = sharedClient.GetCapability( var clients = new ISpotTickerRestClient[] { binance, okx, bybit };
SharedCapabilities.Orders.Futures.PlaceOrder.Rest, var tasks = clients.Select(c => c.GetSpotTickerAsync(new GetTickerRequest(symbol)));
TradingMode.PerpetualLinear);
if (match is not null)
await match.Capability.PlaceFuturesOrderAsync(request);
```
`GetCapability` returns one preferred match or `null`; `GetCapabilities` returns all matches. Specify `TradingMode` when multiple API surfaces may implement the same capability. A `SharedCapabilities` reference is a lookup key, not proof of exchange support.
## Symbols and parameters
Use `SharedSymbol` instead of native strings such as `BTCUSDT`. The exchange library performs formatting.
Capability presence does not imply support for every request property. For dynamic code, inspect `match.Options.RequestParameterRules`, `ExchangeParameterRules`, and `SupportedTradingModes`. Supply exchange-specific values through the request's `ExchangeParameters`.
## Transports and results
- Transport-agnostic capabilities return `IExchangeCallResult<T>`.
- `...Rest` capabilities return `HttpResult<T>`.
- socket command capabilities return `QueryResult<T>`.
- `ISubscribe...Socket` capabilities return `WebSocketResult<UpdateSubscription>`.
Use `.Rest` or `.Socket` on capability references when the transport matters. Otherwise exchange-wide lookup honors `PreferredTransport`, normally REST. Always check `.Success` before `.Data` and include `.Exchange` in multi-exchange logs.
## Concurrent aggregation
```csharp
var clients = new IGetTickerRest[] { binance, okx, bybit };
var tasks = clients.Select(x =>
x.GetTickerAsync(new GetTickerRequest(symbol)));
var results = await Task.WhenAll(tasks); var results = await Task.WhenAll(tasks);
``` ```
Reuse clients through dependency injection and run independent exchange calls concurrently.
## V2 semantics
- `GetTickerAsync` and `GetAllTickersAsync` return `SharedTicker` for spot and futures.
- WebSocket order updates use `SharedSpotOrderUpdate` and `SharedFuturesOrderUpdate`.
- `ICloseFullPosition` closes the entire position; use an order capability for partial closes.
- Each exchange supports a subset of capabilities and parameter combinations.
## Hard rules ## Hard rules
- Never install CryptoExchange.Net alone and expect exchange endpoints. - ❌ Never install `CryptoExchange.Net` alone and expect to call exchanges — it's a base library; you need exchange-specific packages
- Never pass exchange-native models or symbols through shared services. - ❌ Never mix exchange-specific models in cross-exchange code (use `SharedSymbol`, `SharedSpotTicker`, etc.)
- Never assume a capability or request field is supported; resolve and inspect it. - ❌ Never use `.Result` / `.Wait()` — async-only
- Never use `.Result` or `.Wait()`. - ❌ Never iterate sequentially when concurrency is fine — `Task.WhenAll` is your friend
- Never instantiate exchange clients per request. - ❌ Never instantiate clients per-request — reuse via DI
- Prefer `Task.WhenAll` for independent cross-exchange calls. - ✅ Always use `.SharedClient` for cross-exchange code
- Prefer `.SharedApi` and fine-grained V2 interfaces for new code. - ✅ Always check `.Success` before reading `.Data`
- ✅ Always log with `.Exchange` so multi-exchange logs are decipherable
- ✅ Always handle "not supported on this exchange" errors gracefully
## Reference ## Reference
- `AGENTS.md` for fuller examples - `AGENTS.md` in repo root has fuller examples
- `docs/ai-api-map.md` for the V2 interface map - `llms.txt` for AI context
- `docs/SHARED_API_V2_MIGRATION.md` for V1 migration details - `Examples/ai-friendly/` for compilable examples
- `llms-full.txt` for expanded AI context - For single-exchange code, see that exchange's library (e.g., Binance.Net `AGENTS.md`)
+36 -40
View File
@@ -1,70 +1,66 @@
# Copilot Instructions for CryptoExchange.Net # Copilot Instructions for CryptoExchange.Net
CryptoExchange.Net is the base library behind exchange-specific C#/.NET clients such as Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, and Coinbase.Net. This repository is **CryptoExchange.Net** — the base library powering 28+ cryptocurrency exchange wrappers in C#/.NET (Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, Coinbase.Net, etc.).
## Package choice When generating code in this ecosystem, follow these conventions:
Do not install CryptoExchange.Net alone to call an exchange. Install the exchange-specific package, or use `CryptoClients.Net` for the full bundle. For single-exchange code, prefer that library's native client. For portable multi-exchange code, use `CryptoExchange.Net.SharedApis`. ## You don't install CryptoExchange.Net directly
## Generate Shared API V2 code Install the exchange-specific library you need (`Binance.Net`, `JK.OKX.Net`, `Bybit.Net`, ...) or `CryptoClients.Net` for the bundle. CryptoExchange.Net is pulled in as a dependency.
Use fine-grained V2 capabilities through `.SharedApi`: ## Multi-exchange code uses SharedApis
For code that must work against multiple exchanges, use `CryptoExchange.Net.SharedApis` interfaces accessed via `.SharedClient` properties on each exchange's API surface:
```csharp ```csharp
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
IGetTickerRest binance = new BinanceRestClient().SpotApi.SharedApi; ISpotTickerRestClient binance = new BinanceRestClient().SpotApi.SharedClient;
IGetTickerRest okx = new OKXRestClient().UnifiedApi.SharedApi; ISpotTickerRestClient okx = new OKXRestClient().UnifiedApi.SharedClient;
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT"); var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var result = await binance.GetTickerAsync(new GetTickerRequest(symbol)); var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol));
``` ```
Choose the interface for the operation: for example, `IGetTickerRest`, `IGetOrderBookRest`, `IPlaceSpotOrderRest`, `ICancelFuturesOrderRest`, or `ISubscribeTradesSocket`. V1 broad interfaces remain on `.SharedClient` for migration, but new code should use `.SharedApi`. Same code works on every exchange that implements the interface. Use `Task.WhenAll` for concurrent multi-exchange calls.
Use `SharedSymbol`; do not hard-code exchange-native symbol formatting in shared code. ## Shared symbol metadata
## Runtime capability selection CryptoExchange.Net 12.2.0 classifies the base and quote sides of `SharedSpotSymbol` and `SharedFuturesSymbol` with `SharedAssetType` (`Crypto`, `Fiat`, `TradFi`) and optional `SharedAssetSubType` (`StableCoin`, `Equity`, `Commodity`). The models also expose `DisplayName`. Use the corresponding base/quote fields on `GetSymbolsRequest` to filter symbol discovery.
When the API surface is known, assign its typed `.SharedApi` directly. When support or the API surface is selected at runtime, use the exchange-wide `I[Exchange]SharedApiClient`: `ISpotSymbolRestClient.SpotSymbolCatalog` is populated by `GetSpotSymbolsAsync`; `IFuturesSymbolRestClient.FuturesSymbolCatalog` is populated by `GetFuturesSymbolsAsync`. Do not assume a catalog is available before that request. For exchange-library implementations, `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` offer best-effort classification and can be extended with exchange-specific values.
```csharp ## Shared market-data quantities
var match = sharedClient.GetCapability(
SharedCapabilities.Orders.Futures.PlaceOrder.Rest,
TradingMode.PerpetualLinear);
if (match is null) CryptoExchange.Net 12.4.0 uses `SharedOrderQuantity` for market-data quantities. Prefer `Volumes` on `SharedSpotTicker`, `SharedFuturesTicker`, and `SharedKline`, and `Quantities` on `SharedTrade`; the scalar `Volume`, `QuoteVolume`, and `Quantity` members are obsolete.
return;
var result = await match.Capability.PlaceFuturesOrderAsync(request); ## WebSocket order commands
```
`GetCapability` returns one preferred `SharedCapabilityResolution<T>` or `null`; `GetCapabilities` returns all matching implementations. Include `TradingMode` when an exchange can expose multiple futures surfaces. `SharedCapabilities` entries are lookup references, not guarantees of support. CryptoExchange.Net 12.5.0 adds optional `ISpotOrderManagementSocketClient` and `IFuturesOrderManagementSocketClient` interfaces for placing and canceling orders over WebSocket. These command methods return `QueryResult<SharedId>` rather than a subscription result. Check exchange support before relying on either interface.
Before constructing dynamic requests, inspect `match.Options.RequestParameterRules`, `ExchangeParameterRules`, and `SupportedTradingModes`. A capability may exist while a particular request field is unsupported. ## Single-exchange code uses the exchange's own client
## Results and transports For Binance-only code, use `BinanceRestClient` directly (see Binance.Net repo `AGENTS.md`). SharedApis is for portability — use it when you need that.
- Transport-agnostic operation: `IExchangeCallResult<T>` ## Result pattern
- REST capability: `HttpResult<T>`
- WebSocket command capability: `QueryResult<T>`
- WebSocket subscription capability: `WebSocketResult<UpdateSubscription>`
Select `.Rest` or `.Socket` when transport-specific behavior matters. Otherwise exchange-wide selection uses `PreferredTransport`, normally REST. Always check `.Success` before `.Data`; use `.Error` and `.Exchange` for diagnostics. REST methods return `HttpResult<T>` and websocket subscription methods return `WebSocketResult<UpdateSubscription>`. Check `.Success` before `.Data`. `.Error` has structured info. `.Exchange` on shared clients identifies which exchange responded.
## Current V2 semantics ## Available shared interfaces
- Ticker operations are `GetTickerAsync` and `GetAllTickersAsync`, returning `SharedTicker` for both spot and futures. REST: tickers, symbols, orderbook, klines, trades, orders (spot/futures, trigger, TP-SL), balances, positions, fees, deposits/withdrawals, transfers.
- Socket order streams use `SharedSpotOrderUpdate` and `SharedFuturesOrderUpdate`. WebSocket: tickers, book tickers, orderbook, trades, klines, user data, and optional spot/futures order management.
- `ICloseFullPosition` closes a complete position, not a partial quantity.
- Exchange support varies by operation, transport, trading mode, and request parameter.
## Engineering conventions Each exchange library implements a subset. Check exchange docs for support matrix.
- Reuse clients through dependency injection. ## Avoid
- Use `await`; never use `.Result` or `.Wait()`.
- Use `Task.WhenAll` for independent requests across exchanges.
- Keep exchange-native models out of portable services.
- Do not infer feature support from a broad interface or request model.
See `AGENTS.md`, `docs/ai-api-map.md`, and `docs/SHARED_API_V2_MIGRATION.md` for expanded guidance. - Installing `CryptoExchange.Net` alone and trying to call exchange APIs (need exchange-specific packages)
- Mixing exchange-native models in cross-exchange code (use Shared* types)
- Synchronous `.Result` / `.Wait()` (use `await`)
- Instantiating clients per-request (use DI, reuse instances)
- Sequential per-exchange calls when parallel is fine (`Task.WhenAll`)
## Reference
For detailed patterns see `AGENTS.md` and `llms.txt` in repo root, `examples/ai-friendly/` for compilable examples.
+126 -157
View File
@@ -1,39 +1,40 @@
--- ---
name: cryptoexchange-net name: cryptoexchange-net
description: Use CryptoExchange.Net Shared API V2 abstractions when generating C#/.NET code for multiple cryptocurrency exchanges, including arbitrage, routing, portfolio aggregation, exchange-agnostic bots, comparison tools, and new exchange-library implementations. description: Use CryptoExchange.Net abstractions when generating C#/.NET code that needs to work across MULTIPLE cryptocurrency exchanges (Binance + Bybit + OKX + Kraken + Coinbase + ...) — for arbitrage, best-execution routing, multi-exchange portfolio dashboards, exchange-agnostic trading bots, or comparison tools. Triggers on requests mentioning multi-exchange, cross-exchange, arbitrage, exchange-agnostic, or unified crypto API in C#. Also triggers when the user is implementing a new exchange library following the CryptoExchange.Net pattern.
--- ---
# CryptoExchange.Net Skill # CryptoExchange.Net Skill
## When to use ## When to use
CryptoExchange.Net is the base library behind exchange-specific libraries such as Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, and Coinbase.Net. Do not install it alone to call an exchange. CryptoExchange.Net is the **base library** powering 28+ exchange-specific libraries (Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, Coinbase.Net, etc.). You don't install it directly — you install the exchange libraries, which depend on it.
Choose one of these approaches: **Three usage modes:**
1. One exchange: install and use that exchange's library directly. 1. **You target ONE exchange** → use that exchange's library directly (e.g., Binance.Net), see its CLAUDE.md.
2. Multiple exchanges: install the required exchange libraries and use `CryptoExchange.Net.SharedApis` V2 capabilities. 2. **You target MULTIPLE exchanges** → install each library you need + use `CryptoExchange.Net.SharedApis` interfaces — write code once, runs against any exchange. **This is the main use case for this skill.**
3. All exchanges in one package: install `CryptoClients.Net` and use its combined clients and shared capability lookup. 3. **You want ALL exchanges in one package** → install `CryptoClients.Net`, get `ExchangeRestClient` and `ExchangeSocketClient` with everything bundled.
For new cross-exchange code, use Shared API V2. V1 aggregate interfaces remain available through `.SharedClient` for incremental migration.
## Installation ## Installation
For a multi-exchange project:
```bash ```bash
dotnet add package Binance.Net dotnet add package Binance.Net
dotnet add package JK.OKX.Net dotnet add package JK.OKX.Net
dotnet add package Bybit.Net dotnet add package Bybit.Net
# ... etc
``` ```
Or install the bundle: Or the bundle:
```bash ```bash
dotnet add package CryptoClients.Net dotnet add package CryptoClients.Net
``` ```
## Core pattern: fine-grained capabilities ## Core Pattern: Shared Interfaces
Each exchange API surface exposes a typed `.SharedApi` aggregate. Assign it to the capability for the single operation being used: Every exchange library exposes `.SharedClient` properties on its API surfaces. These implement the same interfaces from `CryptoExchange.Net.SharedApis`.
```csharp ```csharp
using Binance.Net.Clients; using Binance.Net.Clients;
@@ -41,187 +42,155 @@ using OKX.Net.Clients;
using Bybit.Net.Clients; using Bybit.Net.Clients;
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
IGetTickerRest binance = new BinanceRestClient().SpotApi.SharedApi; // All three implement ISpotTickerRestClient
IGetTickerRest okx = new OKXRestClient().UnifiedApi.SharedApi; ISpotTickerRestClient binance = new BinanceRestClient().SpotApi.SharedClient;
IGetTickerRest bybit = new BybitRestClient().V5Api.SharedApi; ISpotTickerRestClient okx = new OKXRestClient().UnifiedApi.SharedClient;
ISpotTickerRestClient bybit = new BybitRestClient().V5Api.SharedClient;
// Single agnostic call — works against any of them
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT"); var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var result = await binance.GetTickerAsync(new GetTickerRequest(symbol)); var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol));
// ticker.Data.LastPrice, ticker.Data.HighPrice, etc. — same model regardless of exchange
```
## Core Pattern: SharedSymbol
Different exchanges format symbols differently — Binance uses `BTCUSDT`, OKX uses `BTC-USDT`, others may have other formats. `SharedSymbol` normalizes this:
```csharp
var btcusdt = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
// Each exchange library translates SharedSymbol → its native format internally.
// For futures:
var btcusdtPerp = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
```
For exchanges that use exotic asset names, see the AssetAliases configuration.
## Symbol Metadata and Asset Classification
Since CryptoExchange.Net 12.2.0, shared symbol responses describe both sides of a market with `BaseAssetType`, `BaseAssetSubType`, `QuoteAssetType`, and `QuoteAssetSubType`. `SharedAssetType` distinguishes `Crypto`, `Fiat`, and `TradFi`; `SharedAssetSubType` distinguishes `StableCoin`, `Equity`, and `Commodity`. `SharedSpotSymbol` and `SharedFuturesSymbol` also expose `DisplayName`.
The same fields on `GetSymbolsRequest` filter spot or futures symbol discovery:
```csharp
var request = new GetSymbolsRequest(
baseAssetType: SharedAssetType.Crypto,
quoteAssetSubType: SharedAssetSubType.StableCoin);
var result = await symbolClient.GetSpotSymbolsAsync(request);
```
After calling `GetSpotSymbolsAsync` or `GetFuturesSymbolsAsync`, use the client's `SpotSymbolCatalog` or `FuturesSymbolCatalog` to look up normalized asset and symbol metadata by name. The catalog is unavailable until the corresponding symbol request has populated the cache.
For exchange-library implementations, `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` provide best-effort classification of known assets and accept exchange-specific additions. These helpers are heuristics, not an exhaustive source of truth.
## Shared Market-Data Quantities
Since CryptoExchange.Net 12.4.0, shared market-data models use `SharedOrderQuantity` so base-asset, quote-asset, and contract quantities remain explicit. Read `SharedSpotTicker.Volumes`, `SharedFuturesTicker.Volumes`, and `SharedKline.Volumes`; read `SharedTrade.Quantities`. The former scalar `Volume`, `QuoteVolume`, and `Quantity` members are obsolete.
## WebSocket Order Management
Since CryptoExchange.Net 12.5.0, exchanges can implement `ISpotOrderManagementSocketClient` and `IFuturesOrderManagementSocketClient` to place and cancel orders over WebSocket. These are command interfaces, not subscription interfaces: `Place*OrderAsync` and `Cancel*OrderAsync` return `QueryResult<SharedId>`. Check exchange support before using them.
## Available Shared Interfaces
**REST:**
- Market data: `ISpotTickerRestClient`, `IBookTickerRestClient`, `ISpotSymbolRestClient`, `IFuturesSymbolRestClient`, `IOrderBookRestClient`, `IRecentTradeRestClient`, `IKlineRestClient`
- Orders: `ISpotOrderRestClient`, `IFuturesOrderRestClient`, `ISpotOrderClientIdRestClient`, `IFuturesOrderClientIdRestClient`, `ISpotTriggerOrderRestClient`, `IFuturesTriggerOrderRestClient`, `IFuturesTpSlRestClient`
- Account: `IBalanceRestClient`, `IPositionRestClient`, `IFeeRestClient`, `ITransferRestClient`, `IDepositRestClient`, `IWithdrawalRestClient`
**WebSocket:**
- `ITickerSocketClient`, `IBookTickerSocketClient`
- `IOrderBookSocketClient`, `ITradeSocketClient`, `IKlineSocketClient`
- `IUserTradeSocketClient`, `ISpotOrderSocketClient`, `IFuturesOrderSocketClient`, `IPositionSocketClient`, `IBalanceSocketClient`
- Order commands: `ISpotOrderManagementSocketClient`, `IFuturesOrderManagementSocketClient`
Each exchange documents which interfaces it implements (some exchanges don't support every operation).
## Core Pattern: Result Handling
Same as exchange-specific libraries: REST calls return `HttpResult<T>` and websocket subscription calls return `WebSocketResult<UpdateSubscription>`, both with `.Success`, `.Data`, and `.Error`. Always check `.Success` first.
```csharp
var result = await sharedClient.GetSpotTickerAsync(new GetTickerRequest(symbol));
if (!result.Success) if (!result.Success)
Console.WriteLine($"[{result.Exchange}] {result.Error}"); {
else Console.WriteLine($"[{sharedClient.Exchange}] Error: {result.Error}");
Console.WriteLine($"[{result.Exchange}] {result.Data!.LastPrice}"); return;
}
Console.WriteLine($"[{sharedClient.Exchange}] {result.Data.LastPrice}");
``` ```
V2 interfaces describe operations, not broad feature groups. Examples include `IGetTickerRest`, `IGetOrderBookRest`, `IPlaceSpotOrderRest`, `ICancelFuturesOrderRest`, and `ISubscribeTradesSocket`. An exchange may implement one operation without implementing adjacent operations. `.Exchange` property on every shared client tells you which exchange you're talking to — useful for logging.
## Shared symbols ## Core Pattern: Multi-Exchange Aggregation
Use `SharedSymbol`; never pass exchange-native symbol strings to shared requests:
```csharp ```csharp
var spot = new SharedSymbol(TradingMode.Spot, "BTC", "USDT"); var clients = new ISpotTickerRestClient[]
var linearPerpetual = new SharedSymbol( {
TradingMode.PerpetualLinear, new BinanceRestClient().SpotApi.SharedClient,
"BTC", new OKXRestClient().UnifiedApi.SharedClient,
"USDT"); new BybitRestClient().V5Api.SharedClient,
``` };
Each exchange library converts the shared symbol to its native format. For unusual exchange asset names, configure asset aliases.
## Known surface versus runtime selection
Use the typed `.SharedApi` surface when the exchange and API are known. This is the simplest and most discoverable approach:
```csharp
IPlaceSpotOrderRest orders = restClient.SpotApi.SharedApi;
```
For runtime selection, inject the exchange-wide `I[Exchange]SharedApiClient` registered by `services.Add[Exchange](...)`. Resolve a capability with the strongly typed `SharedCapabilities` catalog:
```csharp
var match = sharedClient.GetCapability(
SharedCapabilities.Orders.Futures.PlaceOrder.Rest,
TradingMode.PerpetualLinear);
if (match is null)
return; // This exchange/API/mode does not provide the operation.
var result = await match.Capability.PlaceFuturesOrderAsync(request);
```
`GetCapability` returns one `SharedCapabilityResolution<T>` containing `Capability` and `Options`, or `null` if no supported implementation matches. `GetCapabilities` returns every match on that exchange-wide client. Always specify `TradingMode` when multiple spot, linear, inverse, or delivery surfaces could match.
`SharedCapabilities` references identify interface types; they do not guarantee that an exchange implements the operation and do not send a request.
## Transport selection and result types
Some commands have three interfaces:
- Transport-agnostic, such as `IPlaceSpotOrder`, returns `IExchangeCallResult<T>`.
- REST-specific, such as `IPlaceSpotOrderRest`, returns `HttpResult<T>`.
- socket-specific, such as `IPlaceSpotOrderSocket`, returns `QueryResult<T>`.
Socket subscription capabilities such as `ISubscribeTickerSocket` return `WebSocketResult<UpdateSubscription>`.
Use `.Rest` or `.Socket` on a capability reference when transport matters. Without a transport filter, exchange-wide lookup uses `PreferredTransport`, configured through the exchange's `SharedApi.PreferredTransport` option and normally defaulting to REST.
Always check `.Success` before `.Data`. Use `.Error` for failures and the result or capability `.Exchange` value for multi-exchange logging.
## Inspect parameter support
The presence of a capability does not mean every shared request property is accepted by every exchange. Inspect its `CapabilityOptions`:
- `RequestParameterRules`: whether each shared request field is `Required`, `Optional`, or `NotSupported`.
- `ExchangeParameterRules`: required or optional exchange-specific values supplied through the request's `ExchangeParameters`.
- `SupportedTradingModes`: the trading modes supported by this implementation.
```csharp
var match = sharedClient.GetCapability(
SharedCapabilities.Orders.Futures.PlaceOrder.Rest,
TradingMode.PerpetualLinear);
var leverageRule = match?.Options.RequestParameterRules
.FirstOrDefault(x => x.Name == nameof(PlaceFuturesOrderRequest.Leverage));
```
Do not infer support from the request model alone. Handle a `null` resolution and exchange-specific parameter rules before issuing dynamic trading calls.
## Common capabilities
- Market data: `IGetTickerRest`, `IGetAllTickersRest`, `IGetOrderBookRest`, `IGetKlinesRest`, `IGetRecentTradesRest`; ticker, trade, kline, and order-book socket subscriptions.
- Trading: fine-grained spot and futures place, edit, cancel, get, open-order, closed-order, and order-update capabilities.
- Account: balances, positions, user trades, fees, deposits, withdrawals, and transfers.
- Futures data: funding, open interest, leverage, mark price, and index price capabilities.
Check the typed exchange Shared API surface or use capability lookup instead of assuming universal support.
## Multi-exchange aggregation
Run independent exchange requests concurrently:
```csharp
var clients = new IGetTickerRest[] { binance, okx, bybit };
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT"); var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
var tasks = clients.Select(client => // Fetch concurrently from all exchanges
client.GetTickerAsync(new GetTickerRequest(symbol))); var tasks = clients.Select(c => c.GetSpotTickerAsync(new GetTickerRequest(symbol))).ToArray();
var results = await Task.WhenAll(tasks); var results = await Task.WhenAll(tasks);
foreach (var result in results.Where(x => x.Success)) for (int i = 0; i < clients.Length; i++)
Console.WriteLine($"{result.Exchange}: {result.Data!.LastPrice}");
```
Reuse clients through dependency injection; do not instantiate them per request.
## Dependency injection
Each exchange library provides `services.Add[Exchange](...)`. Registration includes its native clients, V1 shared interfaces, V2 operation capabilities, and exchange-wide shared client.
Inject a capability directly only when the container has one intended implementation:
```csharp
public sealed class TickerService(IGetTickerRest ticker)
{ {
public Task<HttpResult<SharedTicker>> GetAsync( if (results[i].Success)
SharedSymbol symbol, Console.WriteLine($"{clients[i].Exchange}: {results[i].Data!.LastPrice}");
CancellationToken ct = default)
=> ticker.GetTickerAsync(new GetTickerRequest(symbol), ct);
} }
``` ```
If multiple exchanges or multiple API surfaces register the same capability, inject the exchange-specific shared client and select its typed API property or call `GetCapability`. Plain single-service resolution does not express which implementation you want. ## Per-Exchange Setup
For the bundle, use `services.AddCryptoClients(...)`. CryptoClients.Net provides `IExchangeSharedApiClient` for capability lookup across exchanges. Each exchange library has its own credentials class and options. See each library's CLAUDE.md for specifics. The pattern is consistent: `XxxRestClient(options => { options.ApiCredentials = new XxxCredentials(...); })`.
## V1 migration ## Dependency Injection
V1 `.SharedClient` facades and broad interfaces such as `ISpotTickerRestClient` remain available. Migrate one operation at a time: Each exchange library has its own `services.AddXxx(...)` extension. They all share the same option-builder pattern. Register only the ones you use:
```csharp ```csharp
// V1 services.AddBinance(restOpts => { /*...*/ }, socketOpts => { /*...*/ });
await restClient.SpotApi.SharedClient.GetSpotTickerAsync(request); services.AddOKX(restOpts => { /*...*/ }, socketOpts => { /*...*/ });
// Inject IBinanceRestClient, IOKXRestClient, etc.
// V2
await restClient.SpotApi.SharedApi.GetTickerAsync(request);
``` ```
Important semantic changes: For one-package access: `services.AddCryptoClients(...)` from `CryptoClients.Net`.
- V2 ticker methods are `GetTickerAsync` and `GetAllTickersAsync` and return `SharedTicker` for both spot and futures. ## Common Pitfalls — AVOID
- WebSocket order updates use `SharedSpotOrderUpdate` and `SharedFuturesOrderUpdate`; REST order retrieval keeps the ordinary order models.
- `ICloseFullPosition` closes the complete position. Use an order capability for partial closes where supported.
- Transport-agnostic operations return `IExchangeCallResult<T>`; select the REST or socket interface when code needs a transport-specific result.
See `docs/SHARED_API_V2_MIGRATION.md` for detailed mappings. - **Do NOT install `CryptoExchange.Net` and try to call exchange APIs directly** — it's a base abstraction; you need an exchange library.
- **Do NOT try to use one exchange's models with another's client** — use the SharedApis types (`SharedSymbol`, `SharedSpotTicker`, `SharedSpotOrder`, etc.) for cross-exchange code.
- **Do NOT block on async operations** — use `await` throughout. `Task.WhenAll` for parallelism across exchanges.
- **Do NOT assume every exchange supports every operation** — check exchange docs or the library's implementation. Operations may return errors like "not supported on this exchange".
- **Do NOT instantiate clients per-request** — reuse via DI.
- **Do NOT iterate exchanges sequentially when concurrency is fine** — use `Task.WhenAll` for ~Nx speedup.
## Common pitfalls ## Implementing a New Exchange Library
- Do not install `CryptoExchange.Net` alone and expect exchange endpoints. If you're building a NEW exchange wrapper following the CryptoExchange.Net pattern (rare but valuable):
- Do not use `.SharedClient` for new V2 code; use `.SharedApi` and fine-grained capabilities.
- Do not mix exchange-native request or response models into cross-exchange services.
- Do not assume capability presence or request-parameter support; resolve and inspect it.
- Do not rely on preferred transport when REST or socket semantics matter.
- Do not block with `.Result` or `.Wait()`; use async calls throughout.
- Do not query exchanges sequentially when requests are independent.
## Implementing a new exchange library - Inherit from `RestApiClient` and `SocketApiClient` base classes
- Define your own `XxxCredentials` extending `ApiCredentials` (or use `ApiCredentials` directly)
- Implement `AuthenticationProvider` for the exchange's signing scheme
- Implement the relevant `Shared*` interfaces on your API client classes for cross-exchange support
- Follow the same `XxxRestOptions` / `XxxSocketOptions` pattern
- Derive API clients from `RestApiClient` and `SocketApiClient`. See existing libraries (Binance.Net, Bybit.Net) as reference implementations.
- Follow the exchange credentials and options patterns used by existing libraries.
- Implement the relevant fine-grained V2 capability interfaces on typed Shared API classes.
- Publish accurate `CapabilityOptions`, including supported trading modes and request/exchange parameter rules.
- Register REST/socket Shared APIs and the exchange-wide shared client with the library's DI extension.
- Retain V1 facades only where compatibility with existing consumers is required.
Use maintained exchange libraries such as Binance.Net and Bybit.Net as implementation references.
## Reference ## Reference
- Source: https://github.com/JKorf/CryptoExchange.Net - Source: https://github.com/JKorf/CryptoExchange.Net
- Documentation: https://cryptoexchange.jkorf.dev/ - Documentation: https://cryptoexchange.jkorf.dev/
- Shared API migration: https://github.com/JKorf/CryptoExchange.Net/blob/master/docs/SHARED_API_V2_MIGRATION.md - SharedApis docs: https://cryptoexchange.jkorf.dev/CryptoExchange.Net/idocs_shared.html
- Bundle: https://github.com/JKorf/CryptoClients.Net - Bundle (all exchanges): https://github.com/JKorf/CryptoClients.Net
- Demo app: https://github.com/JKorf/CryptoManager.Net - Demo app: https://github.com/JKorf/CryptoManager.Net
- Discord: https://discord.gg/MSpeEtSY8t - Discord: https://discord.gg/MSpeEtSY8t
@@ -62,31 +62,6 @@ namespace CryptoExchange.Net.UnitTests.ConverterTests
Assert.That(deserialized.Prop8!.Prop31, Is.EqualTo(5)); Assert.That(deserialized.Prop8!.Prop31, Is.EqualTo(5));
Assert.That(deserialized.Prop8.Prop32, Is.EqualTo("101")); Assert.That(deserialized.Prop8.Prop32, Is.EqualTo("101"));
} }
[TestCase("[\"4.803E-5\",\"9.623e-4\",\"0.0010025\"]", 0.00004803, 0.0009623, 0.0010025)]
[TestCase("[\"81251.5\",\"1E+2\",\"-2.5E-3\"]", 81251.5, 100, -0.0025)]
[TestCase("[4.803E-5,9.623e-4,0.0010025]", 0.00004803, 0.0009623, 0.0010025)]
public void TestArrayConverterDecimalInScientificNotation(string json, decimal expected1, decimal expected2, decimal expected3)
{
// Some exchanges send small prices as strings in scientific notation, for example the
// Kucoin futures kline stream: "candles":["1789830000","4.803E-5","4.807E-5",...]
var deserialized = JsonSerializer.Deserialize<TestDecimal>(json);
Assert.That(deserialized!.Prop1, Is.EqualTo(expected1));
Assert.That(deserialized.Prop2, Is.EqualTo(expected2));
Assert.That(deserialized.Prop3, Is.EqualTo(expected3));
}
}
[JsonConverter(typeof(ArrayConverter<TestDecimal>))]
public record TestDecimal
{
[ArrayProperty(0)]
public decimal Prop1 { get; set; }
[ArrayProperty(1)]
public decimal? Prop2 { get; set; }
[ArrayProperty(2)]
public decimal Prop3 { get; set; }
} }
[JsonConverter(typeof(ArrayConverter<Test>))] [JsonConverter(typeof(ArrayConverter<Test>))]
@@ -3,7 +3,6 @@ using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Filters; using CryptoExchange.Net.RateLimiting.Filters;
using CryptoExchange.Net.RateLimiting.Guards; using CryptoExchange.Net.RateLimiting.Guards;
using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.RateLimiting.Trackers;
using CryptoExchange.Net.UnitTests.Implementations; using CryptoExchange.Net.UnitTests.Implementations;
using NUnit.Framework; using NUnit.Framework;
using System; using System;
@@ -365,30 +364,5 @@ namespace CryptoExchange.Net.UnitTests
RateLimitingBehaviour.Fail, null, 1.0, default); RateLimitingBehaviour.Fail, null, 1.0, default);
Assert.That(overHardLimit.Error, Is.TypeOf<ClientRateLimitError>()); Assert.That(overHardLimit.Error, Is.TypeOf<ClientRateLimitError>());
} }
[TestCase(0.1, 10)]
[TestCase(1, 50)]
[TestCase(5, 250)]
[TestCase(60, 250)]
public void RateLimitSafetyMargin_DefaultIsProportionalAndCapped(double periodSeconds, int expectedMarginMilliseconds)
{
var margin = WindowTrackerHelpers.GetDefaultSafetyMargin(TimeSpan.FromSeconds(periodSeconds));
Assert.That(margin, Is.EqualTo(TimeSpan.FromMilliseconds(expectedMarginMilliseconds)));
}
[Test]
public void RateLimitGuard_ExplicitSafetyMarginIsUsed()
{
var guard = new RateLimitGuard(
RateLimitGuard.PerHost,
new LimitItemTypeFilter(RateLimitItemType.Request),
1,
TimeSpan.FromSeconds(1),
RateLimitWindowType.Sliding,
safetyMargin: TimeSpan.Zero);
Assert.That(guard.SafetyMargin, Is.EqualTo(TimeSpan.Zero));
}
} }
} }
@@ -173,9 +173,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (targetType.IsAssignableFrom(value?.GetType())) if (targetType.IsAssignableFrom(value?.GetType()))
attribute.PropertyInfo.SetValue(result, value); attribute.PropertyInfo.SetValue(result, value);
else if (value is string stringValue && targetType == typeof(decimal))
// Convert.ChangeType doesn't accept scientific notation, which some exchanges use for small values ("4.803E-5")
attribute.PropertyInfo.SetValue(result, ExchangeHelpers.ParseDecimal(stringValue));
else else
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture)); attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
} }
+3 -3
View File
@@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net</PackageId> <PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description> <Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>13.0.0</PackageVersion> <PackageVersion>12.5.1</PackageVersion>
<AssemblyVersion>13.0.0</AssemblyVersion> <AssemblyVersion>12.5.1</AssemblyVersion>
<FileVersion>13.0.0</FileVersion> <FileVersion>12.5.1</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags> <PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
@@ -97,7 +97,7 @@ public record CallResult<T> : CallResult, ICallResult<T>
/// Call result for an exchange /// Call result for an exchange
/// </summary> /// </summary>
/// <typeparam name="T">Data type</typeparam> /// <typeparam name="T">Data type</typeparam>
public record ExchangeCallResult<T> : CallResult<T>, IExchangeCallResult<T> public record ExchangeCallResult<T> : CallResult<T>
{ {
/// <summary> /// <summary>
/// Exchange name /// Exchange name
@@ -45,18 +45,11 @@ public interface ICallResult<T> : ICallResult
/// <summary> /// <summary>
/// Exchange call result /// Exchange call result
/// </summary> /// </summary>
public interface IExchangeCallResult : ICallResult /// <typeparam name="T">Result data type</typeparam>
public interface IExchangeCallResult<T> : ICallResult, ICallResult<T>
{ {
/// <summary> /// <summary>
/// Exchange name /// Exchange name
/// </summary> /// </summary>
public string Exchange { get; } public string Exchange { get; set; }
}
/// <summary>
/// Exchange call result
/// </summary>
/// <typeparam name="T">Result data type</typeparam>
public interface IExchangeCallResult<T> : IExchangeCallResult, ICallResult<T>
{
} }
@@ -10,8 +10,12 @@ namespace CryptoExchange.Net.Objects
/// <summary> /// <summary>
/// HTTP call result /// HTTP call result
/// </summary> /// </summary>
public interface IHttpResult : ICallResult, IExchangeCallResult public interface IHttpResult : ICallResult
{ {
/// <summary>
/// Exchange name
/// </summary>
string Exchange { get; init; }
/// <summary> /// <summary>
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options /// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
/// </summary> /// </summary>
@@ -10,8 +10,13 @@ namespace CryptoExchange.Net.Objects
/// <summary> /// <summary>
/// WebSocket call result /// WebSocket call result
/// </summary> /// </summary>
public interface IWebSocketResult : ICallResult, IExchangeCallResult public interface IWebSocketResult : ICallResult
{ {
/// <summary>
/// Exchange name
/// </summary>
string Exchange { get; init; }
/// <summary> /// <summary>
/// The request id /// The request id
/// </summary> /// </summary>
@@ -36,7 +36,6 @@ namespace CryptoExchange.Net.RateLimiting.Guards
private readonly Dictionary<string, IWindowTracker> _trackers; private readonly Dictionary<string, IWindowTracker> _trackers;
private readonly RateLimitWindowType _windowType; private readonly RateLimitWindowType _windowType;
private readonly double? _decayRate; private readonly double? _decayRate;
private readonly TimeSpan _safetyMargin;
private readonly int? _connectionWeight; private readonly int? _connectionWeight;
private readonly Func<RequestDefinition, string?, string> _keySelector; private readonly Func<RequestDefinition, string?, string> _keySelector;
private readonly SemaphoreSlim? _sharedGuardSemaphore; private readonly SemaphoreSlim? _sharedGuardSemaphore;
@@ -55,10 +54,6 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// The time period for the limit /// The time period for the limit
/// </summary> /// </summary>
public TimeSpan TimeSpan { get; } public TimeSpan TimeSpan { get; }
/// <summary>
/// Additional time to wait after a rate limit window expires to account for latency and timing differences
/// </summary>
public TimeSpan SafetyMargin => _safetyMargin;
/// <summary> /// <summary>
/// Whether this guard is shared between multiple gates /// Whether this guard is shared between multiple gates
@@ -73,12 +68,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <param name="limit">Limit per period</param> /// <param name="limit">Limit per period</param>
/// <param name="timeSpan">Timespan for the period</param> /// <param name="timeSpan">Timespan for the period</param>
/// <param name="windowType">Type of rate limit window</param> /// <param name="windowType">Type of rate limit window</param>
/// <param name="safetyMargin">Additional time to wait after a window expires, or null to use the default margin</param>
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param> /// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
/// <param name="connectionWeight">The weight of a new connection</param> /// <param name="connectionWeight">The weight of a new connection</param>
/// <param name="shared">Whether this guard is shared between multiple gates</param> /// <param name="shared">Whether this guard is shared between multiple gates</param>
public RateLimitGuard(Func<RequestDefinition, string?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false, TimeSpan? safetyMargin = null) public RateLimitGuard(Func<RequestDefinition, string?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight, shared, safetyMargin) : this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight, shared)
{ {
} }
@@ -90,11 +84,10 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <param name="limit">Limit per period</param> /// <param name="limit">Limit per period</param>
/// <param name="timeSpan">Timespan for the period</param> /// <param name="timeSpan">Timespan for the period</param>
/// <param name="windowType">Type of rate limit window</param> /// <param name="windowType">Type of rate limit window</param>
/// <param name="safetyMargin">Additional time to wait after a window expires, or null to use the default margin</param>
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param> /// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
/// <param name="connectionWeight">The weight of a new connection</param> /// <param name="connectionWeight">The weight of a new connection</param>
/// <param name="shared">Whether this guard is shared between multiple gates</param> /// <param name="shared">Whether this guard is shared between multiple gates</param>
public RateLimitGuard(Func<RequestDefinition, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false, TimeSpan? safetyMargin = null) public RateLimitGuard(Func<RequestDefinition, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false)
{ {
_filters = filters; _filters = filters;
_trackers = new Dictionary<string, IWindowTracker>(); _trackers = new Dictionary<string, IWindowTracker>();
@@ -104,7 +97,6 @@ namespace CryptoExchange.Net.RateLimiting.Guards
SharedGuard = shared; SharedGuard = shared;
_keySelector = keySelector; _keySelector = keySelector;
_decayRate = decayPerTimeSpan; _decayRate = decayPerTimeSpan;
_safetyMargin = safetyMargin ?? WindowTrackerHelpers.GetDefaultSafetyMargin(timeSpan); ;
_connectionWeight = connectionWeight; _connectionWeight = connectionWeight;
if (SharedGuard) if (SharedGuard)
@@ -213,9 +205,9 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <returns></returns> /// <returns></returns>
protected IWindowTracker CreateTracker() protected IWindowTracker CreateTracker()
{ {
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(Limit, TimeSpan, _safetyMargin) return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(Limit, TimeSpan)
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(Limit, TimeSpan, _safetyMargin) : _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(Limit, TimeSpan)
: _windowType == RateLimitWindowType.FixedAfterFirst ? new FixedAfterStartWindowTracker(Limit, TimeSpan, _safetyMargin) : : _windowType == RateLimitWindowType.FixedAfterFirst ? new FixedAfterStartWindowTracker(Limit, TimeSpan) :
new DecayWindowTracker(Limit, TimeSpan, _decayRate ?? throw new InvalidOperationException("Decay rate not provided")); new DecayWindowTracker(Limit, TimeSpan, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
} }
} }
@@ -24,7 +24,6 @@ namespace CryptoExchange.Net.RateLimiting.Guards
private readonly Dictionary<string, IWindowTracker> _trackers; private readonly Dictionary<string, IWindowTracker> _trackers;
private readonly RateLimitWindowType _windowType; private readonly RateLimitWindowType _windowType;
private readonly double? _decayRate; private readonly double? _decayRate;
private readonly TimeSpan _safetyMargin;
private readonly int _limit; private readonly int _limit;
private readonly TimeSpan _period; private readonly TimeSpan _period;
private readonly Func<RequestDefinition, string?, string> _keySelector; private readonly Func<RequestDefinition, string?, string> _keySelector;
@@ -35,11 +34,6 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <inheritdoc /> /// <inheritdoc />
public string Description => _windowType == RateLimitWindowType.Decay ? $"Endpoint limit of {_limit} with a decay rate of {_decayRate}" : $"Limit of {_limit} per {_period}"; public string Description => _windowType == RateLimitWindowType.Decay ? $"Endpoint limit of {_limit} with a decay rate of {_decayRate}" : $"Limit of {_limit} per {_period}";
/// <summary>
/// Additional time to wait after a rate limit window expires to account for latency and timing differences
/// </summary>
public TimeSpan SafetyMargin => _safetyMargin;
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
@@ -47,15 +41,13 @@ namespace CryptoExchange.Net.RateLimiting.Guards
int limit, int limit,
TimeSpan period, TimeSpan period,
RateLimitWindowType windowType, RateLimitWindowType windowType,
TimeSpan? safetyMargin = null, double? decayRate = null,
Func<RequestDefinition, string?, string>? keySelector = null, Func<RequestDefinition, string?, string>? keySelector = null)
double? decayRate = null)
{ {
_limit = limit; _limit = limit;
_period = period; _period = period;
_windowType = windowType; _windowType = windowType;
_decayRate = decayRate; _decayRate = decayRate;
_safetyMargin = safetyMargin ?? WindowTrackerHelpers.GetDefaultSafetyMargin(period);
_keySelector = keySelector ?? Default; _keySelector = keySelector ?? Default;
_trackers = new Dictionary<string, IWindowTracker>(); _trackers = new Dictionary<string, IWindowTracker>();
} }
@@ -92,8 +84,8 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <returns></returns> /// <returns></returns>
protected IWindowTracker CreateTracker() protected IWindowTracker CreateTracker()
{ {
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(_limit, _period, _safetyMargin) return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(_limit, _period)
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(_limit, _period, _safetyMargin) : : _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(_limit, _period) :
new DecayWindowTracker(_limit, _period, _decayRate ?? throw new InvalidOperationException("Decay rate not provided")); new DecayWindowTracker(_limit, _period, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
} }
@@ -18,18 +18,9 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
private int _currentWeight = 0; private int _currentWeight = 0;
private DateTime _lastDecrease = DateTime.UtcNow; private DateTime _lastDecrease = DateTime.UtcNow;
private double _decayProgress;
public DecayWindowTracker( public DecayWindowTracker(int limit, TimeSpan period, double decayRate)
int limit,
TimeSpan period,
double decayRate)
{ {
if (period <= TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(period));
if (decayRate <= 0)
throw new ArgumentOutOfRangeException(nameof(decayRate));
Limit = limit; Limit = limit;
TimePeriod = period; TimePeriod = period;
DecreaseRate = decayRate; DecreaseRate = decayRate;
@@ -40,118 +31,83 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
{ {
if (amount == null) if (amount == null)
{ {
ResetState(); _lastDecrease = DateTime.UtcNow;
return; _currentWeight = 0;
}
else
{
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
} }
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
if (_currentWeight == 0)
ResetState();
} }
/// <inheritdoc /> /// <inheritdoc />
public TimeSpan GetWaitTime(int weight, double allowedRateRatio) public TimeSpan GetWaitTime(int weight, double allowedRateRatio)
{ {
var now = DateTime.UtcNow; // Decrease the counter based on the last update time and decay rate
DecreaseCounter(now); DecreaseCounter(DateTime.UtcNow);
if ((Current + weight) / (double)Limit <= allowedRateRatio) if ((Current + weight) / (double)Limit > allowedRateRatio)
return TimeSpan.Zero;
if (Current == 0)
{ {
if (allowedRateRatio < 1) // The weight would cause the rate limit to be passed
if (Current == 0)
{ {
throw new Exception( if (allowedRateRatio < 1)
"Request limit reached max utilization. " + {
"This request can never execute with the current rate limiter configuration. " + throw new Exception("Request limit reached max utilization. " +
$"Request weight: {weight}, RateLimit: {Limit}, " + $"This request can never execute with the current rate limiter configuration. Request weight: {weight}, RateLimit: {Limit}, " +
$"Request ratio: {(Current + weight) / (double)Limit}, " + $"Request ratio: {(Current + weight) / (double)Limit}, AllowedRateRatio: {allowedRateRatio}");
$"AllowedRateRatio: {allowedRateRatio}"); }
else
{
throw new Exception("Request limit reached without any prior request. " +
$"This request can never execute with the current rate limiter. Request weight: {weight}, RateLimit: {Limit}");
}
} }
throw new Exception( // Determine the time to wait before this weight can be applied without going over the rate limit
"Request limit reached without any prior request. " + return DetermineWaitTime(weight, allowedRateRatio);
"This request can never execute with the current rate limiter. " +
$"Request weight: {weight}, RateLimit: {Limit}");
} }
return DetermineWaitTime( // Weight can fit without going over limit
weight, return TimeSpan.Zero;
allowedRateRatio);
} }
/// <inheritdoc />
public void ApplyWeight(int weight) public void ApplyWeight(int weight)
{ {
if (_currentWeight == 0) if (_currentWeight == 0)
{
_lastDecrease = DateTime.UtcNow; _lastDecrease = DateTime.UtcNow;
_decayProgress = 0;
}
_currentWeight += weight; _currentWeight += weight;
} }
private void DecreaseCounter(DateTime now) /// <summary>
/// Decrease the counter based on time passed since last update and the decay rate
/// </summary>
/// <param name="time"></param>
protected void DecreaseCounter(DateTime time)
{ {
if (_currentWeight == 0) var dif = (time - _lastDecrease).TotalMilliseconds / TimePeriod.TotalMilliseconds * DecreaseRate;
var decrease = (int)Math.Floor(dif);
if (decrease >= 1)
{ {
_lastDecrease = now; _currentWeight = Math.Max(0, _currentWeight - (int)Math.Floor(dif));
_decayProgress = 0; _lastDecrease = time;
return;
}
var elapsed = now - _lastDecrease;
if (elapsed <= TimeSpan.Zero)
return;
var elapsedDecay = elapsed.Ticks / (double)TimePeriod.Ticks * DecreaseRate;
var totalDecay = _decayProgress + elapsedDecay;
var completedDecay = (int)Math.Floor(totalDecay);
_lastDecrease = now;
if (completedDecay == 0)
{
_decayProgress = totalDecay;
return;
}
_currentWeight = Math.Max(0, _currentWeight - completedDecay);
if (_currentWeight == 0)
{
// Decay cannot accumulate as credit while the counter is empty.
_decayProgress = 0;
}
else
{
_decayProgress = totalDecay - completedDecay;
} }
} }
private TimeSpan DetermineWaitTime( /// <summary>
int requestWeight, /// Determine the time to wait before the weight would fit
double allowedRateRatio) /// </summary>
/// <param name="requestWeight"></param>
/// <param name="allowedRateRatio"></param>
/// <returns></returns>
private TimeSpan DetermineWaitTime(int requestWeight, double allowedRateRatio)
{ {
var weightToRemove = Current + requestWeight - Limit * allowedRateRatio; var weightToRemove = Math.Max(Current + requestWeight - Limit * allowedRateRatio, 0);
var result = TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds);
// The counter is integer-valued, so enough whole weight units if (result < TimeSpan.Zero)
// must decay before the request can be admitted. return TimeSpan.Zero;
var requiredDecay = Math.Ceiling(weightToRemove); return result;
var remainingDecay = Math.Max(0, requiredDecay - _decayProgress);
var waitTicks = Math.Ceiling(remainingDecay / DecreaseRate * TimePeriod.Ticks);
return waitTicks <= 0
? TimeSpan.Zero
: TimeSpan.FromTicks((long)waitTicks);
}
private void ResetState()
{
_currentWeight = 0;
_decayProgress = 0;
_lastDecrease = DateTime.UtcNow;
} }
} }
} }
@@ -17,18 +17,15 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
private int _currentWeight = 0; private int _currentWeight = 0;
private DateTime? _nextReset; private DateTime? _nextReset;
private readonly TimeSpan _safetyMargin; /// <summary>
/// Additional wait time to apply to account for time offset between server and client
/// </summary>
private static TimeSpan _fixedWindowBuffer = TimeSpan.FromMilliseconds(1000);
public FixedAfterStartWindowTracker(int limit, TimeSpan period) public FixedAfterStartWindowTracker(int limit, TimeSpan period)
: this(limit, period, WindowTrackerHelpers.GetDefaultSafetyMargin(period))
{
}
public FixedAfterStartWindowTracker(int limit, TimeSpan period, TimeSpan safetyMargin)
{ {
Limit = limit; Limit = limit;
TimePeriod = period; TimePeriod = period;
_safetyMargin = safetyMargin;
_entries = new Queue<LimitEntry>(); _entries = new Queue<LimitEntry>();
} }
@@ -60,7 +57,7 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
{ {
// Remove requests no longer in time period from the history // Remove requests no longer in time period from the history
var checkTime = DateTime.UtcNow; var checkTime = DateTime.UtcNow;
if (_nextReset != null && checkTime >= _nextReset + _safetyMargin) if (_nextReset != null && checkTime > _nextReset)
RemoveBefore(_nextReset.Value); RemoveBefore(_nextReset.Value);
if (Current == 0) if (Current == 0)
@@ -133,7 +130,7 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
private TimeSpan DetermineWaitTime() private TimeSpan DetermineWaitTime()
{ {
var checkTime = DateTime.UtcNow; var checkTime = DateTime.UtcNow;
var result = _nextReset!.Value + _safetyMargin - checkTime; var result = (_nextReset!.Value - checkTime) + _fixedWindowBuffer;
if (result < TimeSpan.Zero) if (result < TimeSpan.Zero)
return TimeSpan.Zero; return TimeSpan.Zero;
return result; return result;
@@ -16,18 +16,15 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
private readonly Queue<LimitEntry> _entries; private readonly Queue<LimitEntry> _entries;
private int _currentWeight = 0; private int _currentWeight = 0;
private readonly TimeSpan _safetyMargin; /// <summary>
/// Additional wait time to apply to account for time offset between server and client
/// </summary>
private static readonly TimeSpan _fixedWindowBuffer = TimeSpan.FromMilliseconds(1000);
public FixedWindowTracker(int limit, TimeSpan period) public FixedWindowTracker(int limit, TimeSpan period)
: this(limit, period, WindowTrackerHelpers.GetDefaultSafetyMargin(period))
{
}
public FixedWindowTracker(int limit, TimeSpan period, TimeSpan safetyMargin)
{ {
Limit = limit; Limit = limit;
TimePeriod = period; TimePeriod = period;
_safetyMargin = safetyMargin;
_entries = new Queue<LimitEntry>(); _entries = new Queue<LimitEntry>();
} }
@@ -59,11 +56,7 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
{ {
// Remove requests no longer in time period from the history // Remove requests no longer in time period from the history
var checkTime = DateTime.UtcNow; var checkTime = DateTime.UtcNow;
var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks)); RemoveBefore(checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks)));
var resetBoundary = checkTime >= startCurrentWindow + _safetyMargin
? startCurrentWindow
: startCurrentWindow - TimePeriod;
RemoveBefore(resetBoundary);
if ((Current + weight) / (double)Limit > allowedRateRatio) if ((Current + weight) / (double)Limit > allowedRateRatio)
{ {
@@ -131,9 +124,8 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
{ {
var checkTime = DateTime.UtcNow; var checkTime = DateTime.UtcNow;
var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks)); var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks));
var result = checkTime < startCurrentWindow + _safetyMargin var wait = startCurrentWindow.Add(TimePeriod) - checkTime;
? startCurrentWindow + _safetyMargin - checkTime var result = wait.Add(_fixedWindowBuffer);
: startCurrentWindow + TimePeriod + _safetyMargin - checkTime;
if (result < TimeSpan.Zero) if (result < TimeSpan.Zero)
return TimeSpan.Zero; return TimeSpan.Zero;
return result; return result;
@@ -16,18 +16,15 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
private readonly List<LimitEntry> _entries; private readonly List<LimitEntry> _entries;
private int _currentWeight = 0; private int _currentWeight = 0;
private readonly TimeSpan _safetyMargin; /// <summary>
/// Additional wait time to apply to account for fluctuating request times
/// </summary>
private static readonly TimeSpan _slidingWindowBuffer = TimeSpan.FromMilliseconds(1000);
public SlidingWindowTracker(int limit, TimeSpan period) public SlidingWindowTracker(int limit, TimeSpan period)
: this(limit, period, WindowTrackerHelpers.GetDefaultSafetyMargin(period))
{
}
public SlidingWindowTracker(int limit, TimeSpan period, TimeSpan safetyMargin)
{ {
Limit = limit; Limit = limit;
TimePeriod = period; TimePeriod = period;
_safetyMargin = safetyMargin;
_entries = new List<LimitEntry>(); _entries = new List<LimitEntry>();
} }
@@ -59,7 +56,7 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
public TimeSpan GetWaitTime(int weight, double allowedRateRatio) public TimeSpan GetWaitTime(int weight, double allowedRateRatio)
{ {
// Remove requests no longer in time period from the history // Remove requests no longer in time period from the history
RemoveBefore(DateTime.UtcNow - TimePeriod - _safetyMargin); RemoveBefore(DateTime.UtcNow - TimePeriod);
if ((Current + weight) / (double)Limit > allowedRateRatio) if ((Current + weight) / (double)Limit > allowedRateRatio)
{ {
@@ -102,7 +99,7 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
{ {
for (var i = 0; i < _entries.Count; i++) for (var i = 0; i < _entries.Count; i++)
{ {
if (_entries[i].Timestamp <= time) if (_entries[i].Timestamp < time)
{ {
var entry = _entries[i]; var entry = _entries[i];
_entries.Remove(entry); _entries.Remove(entry);
@@ -130,7 +127,7 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
removedWeight += entry.Weight; removedWeight += entry.Weight;
if (removedWeight >= weightToRemove) if (removedWeight >= weightToRemove)
{ {
var result = entry.Timestamp + TimePeriod + _safetyMargin - DateTime.UtcNow; var result = entry.Timestamp + TimePeriod + _slidingWindowBuffer - DateTime.UtcNow;
if (result < TimeSpan.Zero) if (result < TimeSpan.Zero)
return TimeSpan.Zero; return TimeSpan.Zero;
return result; return result;
@@ -1,20 +0,0 @@
using System;
namespace CryptoExchange.Net.RateLimiting.Trackers
{
internal static class WindowTrackerHelpers
{
private static readonly TimeSpan _minimumSafetyMargin = TimeSpan.FromMilliseconds(10);
private static readonly TimeSpan _maximumSafetyMargin = TimeSpan.FromMilliseconds(250);
public static TimeSpan GetDefaultSafetyMargin(TimeSpan period)
{
var margin = TimeSpan.FromTicks(period.Ticks / 20);
if (margin < _minimumSafetyMargin)
return _minimumSafetyMargin;
if (margin > _maximumSafetyMargin)
return _maximumSafetyMargin;
return margin;
}
}
}
+27 -496
View File
@@ -78,474 +78,6 @@ namespace CryptoExchange.Net.SharedApis
return services; return services;
} }
/// <summary>
/// Execute GetAssetAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<IExchangeCallResult<SharedAsset>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetAsset>> capabilities,
GetAssetRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetAssetAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetAssetAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<HttpResult<SharedAsset>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetAssetRest>> capabilities,
GetAssetRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetAssetAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetAllAssetsAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<IExchangeCallResult<SharedAsset[]>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetAllAssets>> capabilities,
GetAssetsRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetAllAssetsAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetAllAssetsAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<HttpResult<SharedAsset[]>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetAllAssetsRest>> capabilities,
GetAssetsRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetAllAssetsAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetBalancesAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<HttpResult<SharedBalance[]>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetBalancesRest>> capabilities,
GetBalancesRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetBalancesAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetBalancesAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<IExchangeCallResult<SharedBalance[]>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetBalances>> capabilities,
GetBalancesRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetBalancesAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetFeesAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<HttpResult<SharedFee>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetFeesRest>> capabilities,
GetFeeRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetFeesAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetFeesAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<IExchangeCallResult<SharedFee>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetFees>> capabilities,
GetFeeRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetFeesAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetFundingInfoAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<HttpResult<SharedFundingInfo>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetFundingInfoRest>> capabilities,
GetFundingInfoRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetFundingInfoAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetFundingInfoAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<IExchangeCallResult<SharedFundingInfo>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetFundingInfo>> capabilities,
GetFundingInfoRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetFundingInfoAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetIndexPriceAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<HttpResult<SharedIndexPrice>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetIndexPriceRest>> capabilities,
GetIndexPriceRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetIndexPriceAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetIndexPriceAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<IExchangeCallResult<SharedIndexPrice>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetIndexPrice>> capabilities,
GetIndexPriceRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetIndexPriceAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetMarkPriceAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<HttpResult<SharedMarkPrice>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetMarkPriceRest>> capabilities,
GetMarkPriceRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetMarkPriceAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetMarkPriceAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<IExchangeCallResult<SharedMarkPrice>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetMarkPrice>> capabilities,
GetMarkPriceRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetMarkPriceAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetAllMarkPricesAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<HttpResult<SharedMarkPrice[]>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetAllMarkPricesRest>> capabilities,
GetAllMarkPricesRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetAllMarkPricesAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetAllMarkPricesAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<IExchangeCallResult<SharedMarkPrice[]>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetAllMarkPrices>> capabilities,
GetAllMarkPricesRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetAllMarkPricesAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetAllIndexPricesAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<HttpResult<SharedIndexPrice[]>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetAllIndexPricesRest>> capabilities,
GetAllIndexPricesRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetAllIndexPricesAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetAllIndexPricesAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<IExchangeCallResult<SharedIndexPrice[]>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetAllIndexPrices>> capabilities,
GetAllIndexPricesRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetAllIndexPricesAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetLeverageAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<HttpResult<SharedLeverage>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetLeverageRest>> capabilities,
GetLeverageRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetLeverageAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetLeverageAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<IExchangeCallResult<SharedLeverage>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetLeverage>> capabilities,
GetLeverageRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetLeverageAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetLeverageTiersAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<HttpResult<SharedLeverageTier[]>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetLeverageTiersRest>> capabilities,
GetLeverageTiersRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetLeverageTiersAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetLeverageTiersAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<IExchangeCallResult<SharedLeverageTier[]>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetLeverageTiers>> capabilities,
GetLeverageTiersRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetLeverageTiersAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetOpenInterestAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<HttpResult<SharedOpenInterest>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetOpenInterestRest>> capabilities,
GetOpenInterestRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetOpenInterestAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetOpenInterestAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<IExchangeCallResult<SharedOpenInterest>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetOpenInterest>> capabilities,
GetOpenInterestRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetOpenInterestAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetBookTickerAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<HttpResult<SharedBookTicker>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetBookTickerRest>> capabilities,
GetBookTickerRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetBookTickerAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetBookTickerAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<IExchangeCallResult<SharedBookTicker>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetBookTicker>> capabilities,
GetBookTickerRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetBookTickerAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetOrderBookAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<HttpResult<SharedOrderBook>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetOrderBookRest>> capabilities,
GetOrderBookRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetOrderBookAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetOrderBookAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<IExchangeCallResult<SharedOrderBook>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetOrderBook>> capabilities,
GetOrderBookRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetOrderBookAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetPositionModeAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<HttpResult<SharedPositionModeResult>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetPositionModeRest>> capabilities,
GetPositionModeRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetPositionModeAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetPositionModeAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<IExchangeCallResult<SharedPositionModeResult>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetPositionMode>> capabilities,
GetPositionModeRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetPositionModeAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetPositionsAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<HttpResult<SharedPosition[]>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetPositionsRest>> capabilities,
GetPositionsRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetPositionsAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetPositionsAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<IExchangeCallResult<SharedPosition[]>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetPositions>> capabilities,
GetPositionsRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetPositionsAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetSpotSymbolsAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<HttpResult<SharedSpotSymbol[]>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetSpotSymbolsRest>> capabilities,
GetSymbolsRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetSpotSymbolsAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetSpotSymbolsAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<IExchangeCallResult<SharedSpotSymbol[]>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetSpotSymbols>> capabilities,
GetSymbolsRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetSpotSymbolsAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetFuturesSymbolsAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<HttpResult<SharedFuturesSymbol[]>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetFuturesSymbolsRest>> capabilities,
GetSymbolsRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetFuturesSymbolsAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary>
/// Execute GetFuturesSymbolsAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<IExchangeCallResult<SharedFuturesSymbol[]>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetFuturesSymbols>> capabilities,
GetSymbolsRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetFuturesSymbolsAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary> /// <summary>
/// Execute GetTickerAsync for all capabilities in parallel and return results as they arrive /// Execute GetTickerAsync for all capabilities in parallel and return results as they arrive
/// </summary> /// </summary>
@@ -559,19 +91,6 @@ namespace CryptoExchange.Net.SharedApis
.ParallelEnumerateAsync(); .ParallelEnumerateAsync();
} }
/// <summary>
/// Execute GetTickerAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<IExchangeCallResult<SharedTicker>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetTicker>> capabilities,
GetTickerRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetTickerAsync(request, ct))
.ParallelEnumerateAsync();
}
/// <summary> /// <summary>
/// Execute GetAllTickersAsync for all capabilities in parallel and return results as they arrive /// Execute GetAllTickersAsync for all capabilities in parallel and return results as they arrive
/// </summary> /// </summary>
@@ -586,43 +105,55 @@ namespace CryptoExchange.Net.SharedApis
} }
/// <summary> /// <summary>
/// Execute GetAllTickersAsync for all capabilities in parallel and return results as they arrive /// Execute GetAssetAsync for all capabilities in parallel and return results as they arrive
/// </summary> /// </summary>
public static IAsyncEnumerable<IExchangeCallResult<SharedTicker[]>> ExecuteAllAsync( public static IAsyncEnumerable<HttpResult<SharedAsset>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetAllTickers>> capabilities, this IEnumerable<SharedCapabilityResolution<IGetAssetRest>> capabilities,
GetTickersRequest request, GetAssetRequest request,
CancellationToken ct = default) CancellationToken ct = default)
{ {
return capabilities return capabilities
.Select(x => x.Capability.GetAllTickersAsync(request, ct)) .Select(x => x.Capability.GetAssetAsync(request, ct))
.ParallelEnumerateAsync(); .ParallelEnumerateAsync();
} }
/// <summary> /// <summary>
/// Execute GetRecentTradesAsync for all capabilities in parallel and return results as they arrive /// Execute GetAllAssetsAsync for all capabilities in parallel and return results as they arrive
/// </summary> /// </summary>
public static IAsyncEnumerable<HttpResult<SharedTrade[]>> ExecuteAllAsync( public static IAsyncEnumerable<HttpResult<SharedAsset[]>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetRecentTradesRest>> capabilities, this IEnumerable<SharedCapabilityResolution<IGetAllAssetsRest>> capabilities,
GetRecentTradesRequest request, GetAssetsRequest request,
CancellationToken ct = default) CancellationToken ct = default)
{ {
return capabilities return capabilities
.Select(x => x.Capability.GetRecentTradesAsync(request, ct)) .Select(x => x.Capability.GetAllAssetsAsync(request, ct))
.ParallelEnumerateAsync(); .ParallelEnumerateAsync();
} }
/// <summary> /// <summary>
/// Execute GetRecentTradesAsync for all capabilities in parallel and return results as they arrive /// Execute GetAllBalancesAsync for all capabilities in parallel and return results as they arrive
/// </summary> /// </summary>
public static IAsyncEnumerable<IExchangeCallResult<SharedTrade[]>> ExecuteAllAsync( public static IAsyncEnumerable<HttpResult<SharedBalance[]>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetRecentTrades>> capabilities, this IEnumerable<SharedCapabilityResolution<IGetBalancesRest>> capabilities,
GetRecentTradesRequest request, GetBalancesRequest request,
CancellationToken ct = default) CancellationToken ct = default)
{ {
return capabilities return capabilities
.Select(x => x.Capability.GetRecentTradesAsync(request, ct)) .Select(x => x.Capability.GetBalancesAsync(request, ct))
.ParallelEnumerateAsync(); .ParallelEnumerateAsync();
} }
/// <summary>
/// Execute GetAllBalancesAsync for all capabilities in parallel and return results as they arrive
/// </summary>
public static IAsyncEnumerable<ICallResult<SharedBalance[]>> ExecuteAllAsync(
this IEnumerable<SharedCapabilityResolution<IGetBalances>> capabilities,
GetBalancesRequest request,
CancellationToken ct = default)
{
return capabilities
.Select(x => x.Capability.GetBalancesAsync(request, ct))
.ParallelEnumerateAsync();
}
} }
} }
@@ -28,7 +28,7 @@ namespace CryptoExchange.Net.SharedApis
/// Get all futures symbols for a specific base asset /// Get all futures symbols for a specific base asset
/// </summary> /// </summary>
/// <param name="baseAsset">Asset, for example `ETH`</param> /// <param name="baseAsset">Asset, for example `ETH`</param>
Task<ExchangeCallResult<SharedSymbol[]>> GetFuturesSymbolsForBaseAssetAsync(string baseAsset); Task<IExchangeCallResult<SharedSymbol[]>> GetFuturesSymbolsForBaseAssetAsync(string baseAsset);
/// <summary> /// <summary>
/// Gets whether the client supports a futures symbol /// Gets whether the client supports a futures symbol
@@ -47,7 +47,7 @@ namespace CryptoExchange.Net.SharedApis
/// </summary> /// </summary>
/// <param name="request">Request info</param> /// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param> /// <param name="ct">Cancellation token</param>
Task<IExchangeCallResult<SharedFuturesSymbol[]>> GetFuturesSymbolsAsync(GetSymbolsRequest request, CancellationToken ct = default); Task<ICallResult<SharedFuturesSymbol[]>> GetFuturesSymbolsAsync(GetSymbolsRequest request, CancellationToken ct = default);
} }
/// <summary> /// <summary>
@@ -27,7 +27,7 @@ namespace CryptoExchange.Net.SharedApis
/// Gets whether the client supports a spot symbol /// Gets whether the client supports a spot symbol
/// </summary> /// </summary>
/// <param name="symbol">The symbol</param> /// <param name="symbol">The symbol</param>
Task<ExchangeCallResult<bool>> SupportsSpotSymbolAsync(SharedSymbol symbol); Task<IExchangeCallResult<bool>> SupportsSpotSymbolAsync(SharedSymbol symbol);
/// <summary> /// <summary>
/// Gets whether the client supports a spot symbol /// Gets whether the client supports a spot symbol
@@ -47,7 +47,7 @@ namespace CryptoExchange.Net.SharedApis
/// </summary> /// </summary>
/// <param name="request">Request info</param> /// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param> /// <param name="ct">Cancellation token</param>
Task<IExchangeCallResult<SharedSpotSymbol[]>> GetSpotSymbolsAsync(GetSymbolsRequest request, CancellationToken ct = default); Task<ICallResult<SharedSpotSymbol[]>> GetSpotSymbolsAsync(GetSymbolsRequest request, CancellationToken ct = default);
} }
/// <summary> /// <summary>
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
/// </summary> /// </summary>
/// <param name="request">Request info</param> /// <param name="request">Request info</param>
/// <param name="ct">Cancellation token</param> /// <param name="ct">Cancellation token</param>
Task<IExchangeCallResult<SharedFuturesTriggerOrder>> GetFuturesTriggerOrderAsync(GetOrderRequest request, CancellationToken ct = default); Task<IExchangeCallResultSharedFuturesTriggerOrder>> GetFuturesTriggerOrderAsync(GetOrderRequest request, CancellationToken ct = default);
} }
/// <summary> /// <summary>
+3 -37
View File
@@ -23,9 +23,9 @@ For single-exchange code, see also the AI files in each exchange's repository (B
See [cryptoexchange-skills-hub](https://github.com/JKorf/cryptoexchange-skills-hub) for installable skills. See [cryptoexchange-skills-hub](https://github.com/JKorf/cryptoexchange-skills-hub) for installable skills.
**Quick prompt to verify your assistant is using these:** **Quick prompt to verify your assistant is using these:**
> "Show me how to fetch BTC/USDT spot tickers from Binance and OKX concurrently in C# using the SharedApis V2 pattern." > "Show me how to fetch BTC/USDT spot tickers from Binance and OKX concurrently in C# using the SharedApis pattern."
The expected output should use `.SharedApi` properties, `SharedSymbol`, `IGetTickerRest`, and `Task.WhenAll`. The expected output should use `.SharedClient` properties, `SharedSymbol`, `ISpotTickerRestClient`, and `Task.WhenAll`.
### CryptoExchange.Net Ecosystem ### CryptoExchange.Net Ecosystem
Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider using a referral link to support development, as well as potentially get some trading fee discount! Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider using a referral link to support development, as well as potentially get some trading fee discount!
@@ -38,6 +38,7 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
|![Bitfinex](https://raw.githubusercontent.com/JKorf/Bitfinex.Net/refs/heads/master/Bitfinex.Net/Icon/icon.png)|Bitfinex|CEX|[JKorf/Bitfinex.Net](https://github.com/JKorf/Bitfinex.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bitfinex.net.svg?style=flat-square)](https://www.nuget.org/packages/Bitfinex.Net)|-|-| |![Bitfinex](https://raw.githubusercontent.com/JKorf/Bitfinex.Net/refs/heads/master/Bitfinex.Net/Icon/icon.png)|Bitfinex|CEX|[JKorf/Bitfinex.Net](https://github.com/JKorf/Bitfinex.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bitfinex.net.svg?style=flat-square)](https://www.nuget.org/packages/Bitfinex.Net)|-|-|
|![Bitget](https://raw.githubusercontent.com/JKorf/Bitget.Net/refs/heads/main/Bitget.Net/Icon/icon.png)|Bitget|CEX|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Bitget.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Bitget.Net)|[Link](https://partner.bitget.com/bg/1qlf6pj1)|20%| |![Bitget](https://raw.githubusercontent.com/JKorf/Bitget.Net/refs/heads/main/Bitget.Net/Icon/icon.png)|Bitget|CEX|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Bitget.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Bitget.Net)|[Link](https://partner.bitget.com/bg/1qlf6pj1)|20%|
|![BitMart](https://raw.githubusercontent.com/JKorf/BitMart.Net/refs/heads/main/BitMart.Net/Icon/icon.png)|BitMart|CEX|[JKorf/BitMart.Net](https://github.com/JKorf/BitMart.Net)|[![Nuget version](https://img.shields.io/nuget/v/BitMart.net.svg?style=flat-square)](https://www.nuget.org/packages/BitMart.Net)|[Link](https://www.bitmart.com/invite/JKorfAPI/en-US)|30%| |![BitMart](https://raw.githubusercontent.com/JKorf/BitMart.Net/refs/heads/main/BitMart.Net/Icon/icon.png)|BitMart|CEX|[JKorf/BitMart.Net](https://github.com/JKorf/BitMart.Net)|[![Nuget version](https://img.shields.io/nuget/v/BitMart.net.svg?style=flat-square)](https://www.nuget.org/packages/BitMart.Net)|[Link](https://www.bitmart.com/invite/JKorfAPI/en-US)|30%|
|![BitMEX](https://raw.githubusercontent.com/JKorf/BitMEX.Net/refs/heads/main/BitMEX.Net/Icon/icon.png)|BitMEX|CEX|[JKorf/BitMEX.Net](https://github.com/JKorf/BitMEX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.BitMEX.net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.BitMEX.Net)|[Link](https://www.bitmex.com/app/register/94f98e)|30%|
|![Bitstamp](https://raw.githubusercontent.com/JKorf/Bitstamp.Net/refs/heads/main/Bitstamp.Net/Icon/icon.png)|Bitstamp|CEX|[JKorf/Bitstamp.Net](https://github.com/JKorf/Bitstamp.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bitstamp.net.svg?style=flat-square)](https://www.nuget.org/packages/Bitstamp.Net)|-|-| |![Bitstamp](https://raw.githubusercontent.com/JKorf/Bitstamp.Net/refs/heads/main/Bitstamp.Net/Icon/icon.png)|Bitstamp|CEX|[JKorf/Bitstamp.Net](https://github.com/JKorf/Bitstamp.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bitstamp.net.svg?style=flat-square)](https://www.nuget.org/packages/Bitstamp.Net)|-|-|
|![BloFin](https://raw.githubusercontent.com/JKorf/BloFin.Net/refs/heads/main/BloFin.Net/Icon/icon.png)|BloFin|CEX|[JKorf/BloFin.Net](https://github.com/JKorf/BloFin.Net)|[![Nuget version](https://img.shields.io/nuget/v/BloFin.net.svg?style=flat-square)](https://www.nuget.org/packages/BloFin.Net)|-|-| |![BloFin](https://raw.githubusercontent.com/JKorf/BloFin.Net/refs/heads/main/BloFin.Net/Icon/icon.png)|BloFin|CEX|[JKorf/BloFin.Net](https://github.com/JKorf/BloFin.Net)|[![Nuget version](https://img.shields.io/nuget/v/BloFin.net.svg?style=flat-square)](https://www.nuget.org/packages/BloFin.Net)|-|-|
|![Bybit](https://raw.githubusercontent.com/JKorf/Bybit.Net/refs/heads/main/ByBit.Net/Icon/icon.png)|Bybit|CEX|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square)](https://www.nuget.org/packages/Bybit.Net)|[Link](https://partner.bybit.com/b/jkorf)|-| |![Bybit](https://raw.githubusercontent.com/JKorf/Bybit.Net/refs/heads/main/ByBit.Net/Icon/icon.png)|Bybit|CEX|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square)](https://www.nuget.org/packages/Bybit.Net)|[Link](https://partner.bybit.com/b/jkorf)|-|
@@ -127,41 +128,6 @@ Various:
* PlatformInfo now required support environment names in the constructor * PlatformInfo now required support environment names in the constructor
## Release notes ## Release notes
* Version 13.0.0 - 23 Sep 2026
* Shared APIs
* Added Shared API V2 with fine-grained capability interfaces for individual REST requests, WebSocket requests and subscriptions
* Added transport-agnostic capability interfaces with REST- and WebSocket-specific variants where applicable
* Added dynamic capability resolution support with optional transport preference option
* Added `RequestParameterRules` and `ExchangeParameterRules` properties on Shared API options making request parameter rules and support clearer
* Added new Shared API capabilities for ledger history, funding information and history, leverage tiers, transfers, mark prices and index prices
* Added new capabilities for placing and editing multiple spot and futures orders
* Added new capabilities for cancelling all open orders and all open symbol orders
* Added new incremental order book, mark price and index price subscriptions
* Added ReduceOnly parameter to PlaceFuturesTriggerOrderRequest
* Added new `SharedTicker` as the common V2 ticker model for spot and futures markets
* For additional futures ticker info like mark/index price and funding info separate interfaces are available in V2
* Added `SharedSpotOrderUpdate` and `SharedFuturesOrderUpdate` models for WebSocket order updates
* Added `IExchangeCallResult` and `IExchangeCallResult<T>` for transport-agnostic Shared API results
* Retained the V1 aggregate Shared API interfaces for backwards compatibility
* Renamed GetAssetsOptions to GetAllAssetsOptions
* Renamed GetWithdrawalsOptions to GetWithdrawalHistoryOptions
* Renamed GetDepositsOptions to GetDepositHistoryOptions
* GetFuturesTickerOptions and GetSpotTickerOptions have been replaced by GetTickerOptions
* Deprecated Subscribe request constructors using `params` for `SharedSymbol` parameter
* Rate limiting
* Added `RateLimitAdmission` for restricting requests to a configurable maximum rate limit utilization
* Added the `RateLimitAdmission` client option callback for assigning admission rules based on request definition and weight
* Added `WithRateLimitAdmissionAsync` for applying a rate limit admission rule to a specific REST, WebSocket or Shared API operation
* Added configurable safety margins to fixed, sliding and fixed-after-first rate limit windows
* Fixed decay rate limiter calculations and handling of partial decay progress
* Fixed fixed and sliding window boundary calculations
* Added automatic in-flight request coalescing for identical public REST GET requests
* Sending identical public GET requests on the same client will only send a single request to the server and use the same response
* Coalescing is enabled by default and can be disabled with the `RequestCoalescingEnabled` client option
* Added `DataTime`, `DataTimeLocal` and `SequenceNumber` propagation when converting `DataEvent<T>` instances
* Fixed form data URL encoding on .NET Framework when parameter values contain special characters
* Fixed array converter not correctly handling decimal parsing for certain notations
* Version 12.5.1 - 01 Sep 2026 * Version 12.5.1 - 01 Sep 2026
* Fixed KlineTracker reporting incorrect High/Low price on GetStats result * Fixed KlineTracker reporting incorrect High/Low price on GetStats result
* Fixed caching issue for auth requests * Fixed caching issue for auth requests
+1 -1
View File
@@ -35,7 +35,7 @@ var placementCapabilityResolution = sharedClient.GetCapability(
if (placementCapabilityResolution is not null) if (placementCapabilityResolution is not null)
{ {
IPlaceFuturesOrderRest api = placementCapabilityResolution.Capability; IPlaceFuturesOrderRest api = placement.Capability;
// placement.Options describes parameter and trading-mode support for this implementation. // placement.Options describes parameter and trading-mode support for this implementation.
} }
``` ```
+26 -23
View File
@@ -1,37 +1,40 @@
# CryptoExchange.Net # CryptoExchange.Net
> Base C#/.NET library used by 28+ cryptocurrency exchange clients. It standardizes REST, WebSocket, authentication, rate limiting, results, and exchange-agnostic Shared APIs. > Base C#/.NET library for cryptocurrency exchange API client implementations. Provides a standardized abstraction (REST, WebSocket, authentication, rate limiting, error handling, order book management, shared cross-exchange interfaces) that 28+ exchange-specific libraries are built on top of.
CryptoExchange.Net is not an exchange client by itself. Install exchange-specific packages such as Binance.Net, Bybit.Net, JK.OKX.Net, Kraken.Net, or Coinbase.Net, or install `CryptoClients.Net` for the bundle. Current release: 13.0.0. Targets netstandard2.0, netstandard2.1, net8.0, net9.0, and net10.0; Native AOT is supported. CryptoExchange.Net itself is not used directly — install one of the exchange-specific libraries (Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, Coinbase.Net, etc.) or `CryptoClients.Net` to access all exchanges via a single bundle. The base library is what makes the entire ecosystem feel consistent: same `HttpResult<T>` REST result pattern, same `WebSocketResult<UpdateSubscription>` websocket subscription pattern, same DI registration, same shared interfaces across all exchanges. Current version: 12.5.1. Targets netstandard2.0, netstandard2.1, net8.0, net9.0, net10.0. Native AOT supported.
## Shared API V2 The standout feature for cross-exchange code is `CryptoExchange.Net.SharedApis` — a set of interfaces (`ISpotTickerRestClient`, `ISpotOrderRestClient`, `IBalanceRestClient`, etc.) implemented by every exchange library. Same call signature works against any exchange.
For new cross-exchange code, use fine-grained capability interfaces from `CryptoExchange.Net.SharedApis` through each API surface's `.SharedApi` property. Examples: `IGetTickerRest`, `IGetOrderBookRest`, `IPlaceSpotOrderRest`, and `ISubscribeTradesSocket`. Version 12.2.0 adds typed asset metadata to shared symbol discovery. `SharedSpotSymbol` and `SharedFuturesSymbol` expose `DisplayName` plus base/quote `SharedAssetType` and `SharedAssetSubType` values. `GetSymbolsRequest` can filter on those four type fields. After symbol discovery, `ISpotSymbolRestClient.SpotSymbolCatalog` and `IFuturesSymbolRestClient.FuturesSymbolCatalog` provide asset and symbol dictionaries; each catalog is available only after the corresponding `Get*SymbolsAsync` call. `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` are best-effort helpers for exchange-library implementations.
Use `SharedSymbol` to normalize symbols and `Task.WhenAll` for independent calls across exchanges. Use an exchange-wide `I[Exchange]SharedApiClient` plus `SharedCapabilities` and `GetCapability` when selecting operations dynamically. A resolution can be `null`; its `Options` describes supported trading modes and request/exchange parameter rules. Version 12.4.0 represents market-data quantities with `SharedOrderQuantity`: use `Volumes` on `SharedSpotTicker`, `SharedFuturesTicker`, and `SharedKline`, and `Quantities` on `SharedTrade`. The old scalar `Volume`, `QuoteVolume`, and `Quantity` members are obsolete. Exchange-library implementations must pass `SharedOrderQuantity` to these model constructors.
Transport-agnostic capabilities return `IExchangeCallResult<T>`. REST capabilities return `HttpResult<T>`, socket commands return `QueryResult<T>`, and socket subscriptions return `WebSocketResult<UpdateSubscription>`. Always check `.Success` before `.Data`. Version 12.5.0 adds optional shared WebSocket order commands through `ISpotOrderManagementSocketClient` and `IFuturesOrderManagementSocketClient`. Their place/cancel methods return `QueryResult<SharedId>`; they are commands rather than update subscriptions, and support is exchange-specific.
V1 broad interfaces remain accessible through `.SharedClient` for incremental migration. Prefer `.SharedApi` for new code. V2 ticker calls use `GetTickerAsync`/`GetAllTickersAsync` and the common `SharedTicker` model.
## Documentation ## Documentation
- [README](https://github.com/JKorf/CryptoExchange.Net/blob/master/README.md): ecosystem overview, packages, and release notes - [README](https://github.com/JKorf/CryptoExchange.Net/blob/master/README.md): Overview, full ecosystem table (28+ exchange libraries), installation per exchange, complete release notes
- [Shared API V2 migration](https://github.com/JKorf/CryptoExchange.Net/blob/master/docs/SHARED_API_V2_MIGRATION.md): V1-to-V2 mapping, capability selection, transports, and parameter discovery - [Documentation Site](https://cryptoexchange.jkorf.dev/): Full documentation hub with sections per topic
- [Documentation site](https://cryptoexchange.jkorf.dev/): complete library documentation - [SharedApis Documentation](https://cryptoexchange.jkorf.dev/CryptoExchange.Net/idocs_shared.html): Cross-exchange shared interface guide
- [Shared API documentation](https://cryptoexchange.jkorf.dev/CryptoExchange.Net/idocs_shared.html): cross-exchange API guide
- [AI API map](https://github.com/JKorf/CryptoExchange.Net/blob/master/docs/ai-api-map.md): concise V2 interface and selection map
## AI context files ## Examples
- [AGENTS.md](https://github.com/JKorf/CryptoExchange.Net/blob/master/AGENTS.md): practical generation rules and examples - [AI-friendly examples directory](https://github.com/JKorf/CryptoExchange.Net/tree/master/Examples/ai-friendly): Compact, fully runnable examples optimized for AI assistants
- [llms-full.txt](https://github.com/JKorf/CryptoExchange.Net/blob/master/llms-full.txt): expanded library and Shared API V2 context - [Shared Clients Quickstart](https://github.com/JKorf/CryptoExchange.Net/blob/master/Examples/ai-friendly/01-shared-clients-quickstart.cs): Same code calling multiple exchanges via SharedApis
- [.github/copilot-instructions.md](https://github.com/JKorf/CryptoExchange.Net/blob/master/.github/copilot-instructions.md): compact repository conventions - [Multi-Exchange Tickers](https://github.com/JKorf/CryptoExchange.Net/blob/master/Examples/ai-friendly/02-multi-exchange-tickers.cs): Aggregating ticker data across N exchanges concurrently
- [.cursor/rules/CryptoExchange-Net.mdc](https://github.com/JKorf/CryptoExchange.Net/blob/master/.cursor/rules/CryptoExchange-Net.mdc): Cursor rules for cross-exchange C# code - [Cross-Exchange Arbitrage Skeleton](https://github.com/JKorf/CryptoExchange.Net/blob/master/Examples/ai-friendly/03-cross-exchange-arbitrage-skeleton.cs): Pattern for building a price difference scanner
- [Full Examples Repository](https://github.com/JKorf/CryptoExchange.Net/tree/master/Examples): ConsoleClient with multiple exchange exchanges, BlazorClient, SharedClients
## Related projects ## Reference
- [CryptoClients.Net](https://github.com/JKorf/CryptoClients.Net): bundle and cross-exchange capability lookup - [Ecosystem libraries list](https://github.com/JKorf/CryptoExchange.Net#cryptoexchangenet-ecosystem): Aster, Binance, BingX, Bitfinex, Bitget, BitMart, BitMEX, Bitstamp, BloFin, Bybit, Coinbase, CoinEx, CoinW, CoinGecko, Crypto.com, DeepCoin, Gate.io, HTX, HyperLiquid, Kraken, Kucoin, Mexc, OKX, Pionex, Polymarket, Toobit, Upbit, Weex, WhiteBit, XT
- [CryptoManager.Net](https://github.com/JKorf/CryptoManager.Net): full demo application - [CryptoClients.Net](https://github.com/JKorf/CryptoClients.Net): Single bundle package for all exchange libraries
- [NuGet](https://www.nuget.org/packages/CryptoExchange.Net): package releases - [CryptoManager.Net](https://github.com/JKorf/CryptoManager.Net): Full demo application using CryptoClients.Net
- [Discord](https://discord.gg/MSpeEtSY8t): maintainer-supported community - [NuGet Package](https://www.nuget.org/packages/CryptoExchange.Net): Latest stable release on NuGet
## Optional
- [Discord Community](https://discord.gg/MSpeEtSY8t): Maintainer-supported Discord for ecosystem-wide discussion
- [GitHub Issues](https://github.com/JKorf/CryptoExchange.Net/issues): Bug reports and feature requests
- [GitHub Sponsors](https://github.com/sponsors/JKorf): Support the maintainer