mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-10-14 07:01:46 +00:00
Compare commits
18 Commits
911e09be61
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| f079e6f0f4 | |||
| c6647f6815 | |||
| 5135047097 | |||
| 4f25a6e7d6 | |||
| a0f3dcaa4c | |||
| 550afd41c3 | |||
| afdced3bb8 | |||
| f8c657f308 | |||
| c9ae1bf379 | |||
| 59acbd648e | |||
| 7a11bd0c5e | |||
| 9502db6d58 | |||
| e6ab899654 | |||
| a8517d2c76 | |||
| bda81d8170 | |||
| 666ca76776 | |||
| ae7504c26c | |||
| cecfbcba48 |
@@ -1,87 +1,92 @@
|
||||
---
|
||||
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.
|
||||
description: Conventions for cross-exchange C# code using CryptoExchange.Net Shared API V2 capabilities.
|
||||
globs:
|
||||
- "**/*.cs"
|
||||
- "**/*.csproj"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# CryptoExchange.Net Conventions
|
||||
# CryptoExchange.Net conventions
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Multi-exchange pattern
|
||||
## Use Shared API V2
|
||||
|
||||
For new cross-exchange code, use the fine-grained interfaces in `CryptoExchange.Net.SharedApis` and each API surface's `.SharedApi` property:
|
||||
|
||||
```csharp
|
||||
using Binance.Net.Clients;
|
||||
using OKX.Net.Clients;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
|
||||
ISpotTickerRestClient binance = new BinanceRestClient().SpotApi.SharedClient;
|
||||
ISpotTickerRestClient okx = new OKXRestClient().UnifiedApi.SharedClient;
|
||||
IGetTickerRest binance = new BinanceRestClient().SpotApi.SharedApi;
|
||||
IGetTickerRest okx = new OKXRestClient().UnifiedApi.SharedApi;
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||
|
||||
var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol));
|
||||
// ticker.Data.LastPrice — same model regardless of exchange
|
||||
var result = await binance.GetTickerAsync(new GetTickerRequest(symbol));
|
||||
```
|
||||
|
||||
## Symbol normalization
|
||||
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.
|
||||
|
||||
`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.
|
||||
## Capability discovery
|
||||
|
||||
## 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.
|
||||
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:
|
||||
|
||||
```csharp
|
||||
var clients = new ISpotTickerRestClient[] { binance, okx, bybit };
|
||||
var tasks = clients.Select(c => c.GetSpotTickerAsync(new GetTickerRequest(symbol)));
|
||||
var match = sharedClient.GetCapability(
|
||||
SharedCapabilities.Orders.Futures.PlaceOrder.Rest,
|
||||
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);
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
- ❌ Never install `CryptoExchange.Net` alone and expect to call exchanges — it's a base library; you need exchange-specific packages
|
||||
- ❌ Never mix exchange-specific models in cross-exchange code (use `SharedSymbol`, `SharedSpotTicker`, etc.)
|
||||
- ❌ Never use `.Result` / `.Wait()` — async-only
|
||||
- ❌ Never iterate sequentially when concurrency is fine — `Task.WhenAll` is your friend
|
||||
- ❌ Never instantiate clients per-request — reuse via DI
|
||||
- ✅ Always use `.SharedClient` for cross-exchange 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
|
||||
- Never install CryptoExchange.Net alone and expect exchange endpoints.
|
||||
- Never pass exchange-native models or symbols through shared services.
|
||||
- Never assume a capability or request field is supported; resolve and inspect it.
|
||||
- Never use `.Result` or `.Wait()`.
|
||||
- Never instantiate exchange clients per request.
|
||||
- Prefer `Task.WhenAll` for independent cross-exchange calls.
|
||||
- Prefer `.SharedApi` and fine-grained V2 interfaces for new code.
|
||||
|
||||
## Reference
|
||||
|
||||
- `AGENTS.md` in repo root has fuller examples
|
||||
- `llms.txt` for AI context
|
||||
- `Examples/ai-friendly/` for compilable examples
|
||||
- For single-exchange code, see that exchange's library (e.g., Binance.Net `AGENTS.md`)
|
||||
- `AGENTS.md` for fuller examples
|
||||
- `docs/ai-api-map.md` for the V2 interface map
|
||||
- `docs/SHARED_API_V2_MIGRATION.md` for V1 migration details
|
||||
- `llms-full.txt` for expanded AI context
|
||||
|
||||
@@ -1,66 +1,70 @@
|
||||
# Copilot Instructions for CryptoExchange.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.).
|
||||
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.
|
||||
|
||||
When generating code in this ecosystem, follow these conventions:
|
||||
## Package choice
|
||||
|
||||
## You don't install CryptoExchange.Net directly
|
||||
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`.
|
||||
|
||||
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.
|
||||
## Generate Shared API V2 code
|
||||
|
||||
## 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:
|
||||
Use fine-grained V2 capabilities through `.SharedApi`:
|
||||
|
||||
```csharp
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
|
||||
ISpotTickerRestClient binance = new BinanceRestClient().SpotApi.SharedClient;
|
||||
ISpotTickerRestClient okx = new OKXRestClient().UnifiedApi.SharedClient;
|
||||
IGetTickerRest binance = new BinanceRestClient().SpotApi.SharedApi;
|
||||
IGetTickerRest okx = new OKXRestClient().UnifiedApi.SharedApi;
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||
var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol));
|
||||
var result = await binance.GetTickerAsync(new GetTickerRequest(symbol));
|
||||
```
|
||||
|
||||
Same code works on every exchange that implements the interface. Use `Task.WhenAll` for concurrent multi-exchange calls.
|
||||
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`.
|
||||
|
||||
## Shared symbol metadata
|
||||
Use `SharedSymbol`; do not hard-code exchange-native symbol formatting in shared code.
|
||||
|
||||
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.
|
||||
## Runtime capability selection
|
||||
|
||||
`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.
|
||||
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`:
|
||||
|
||||
## Shared market-data quantities
|
||||
```csharp
|
||||
var match = sharedClient.GetCapability(
|
||||
SharedCapabilities.Orders.Futures.PlaceOrder.Rest,
|
||||
TradingMode.PerpetualLinear);
|
||||
|
||||
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.
|
||||
if (match is null)
|
||||
return;
|
||||
|
||||
## WebSocket order commands
|
||||
var result = await match.Capability.PlaceFuturesOrderAsync(request);
|
||||
```
|
||||
|
||||
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.
|
||||
`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.
|
||||
|
||||
## Single-exchange code uses the exchange's own client
|
||||
Before constructing dynamic requests, inspect `match.Options.RequestParameterRules`, `ExchangeParameterRules`, and `SupportedTradingModes`. A capability may exist while a particular request field is unsupported.
|
||||
|
||||
For Binance-only code, use `BinanceRestClient` directly (see Binance.Net repo `AGENTS.md`). SharedApis is for portability — use it when you need that.
|
||||
## Results and transports
|
||||
|
||||
## Result pattern
|
||||
- Transport-agnostic operation: `IExchangeCallResult<T>`
|
||||
- REST capability: `HttpResult<T>`
|
||||
- WebSocket command capability: `QueryResult<T>`
|
||||
- WebSocket subscription capability: `WebSocketResult<UpdateSubscription>`
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Available shared interfaces
|
||||
## Current V2 semantics
|
||||
|
||||
REST: tickers, symbols, orderbook, klines, trades, orders (spot/futures, trigger, TP-SL), balances, positions, fees, deposits/withdrawals, transfers.
|
||||
WebSocket: tickers, book tickers, orderbook, trades, klines, user data, and optional spot/futures order management.
|
||||
- Ticker operations are `GetTickerAsync` and `GetAllTickersAsync`, returning `SharedTicker` for both spot and futures.
|
||||
- Socket order streams use `SharedSpotOrderUpdate` and `SharedFuturesOrderUpdate`.
|
||||
- `ICloseFullPosition` closes a complete position, not a partial quantity.
|
||||
- Exchange support varies by operation, transport, trading mode, and request parameter.
|
||||
|
||||
Each exchange library implements a subset. Check exchange docs for support matrix.
|
||||
## Engineering conventions
|
||||
|
||||
## Avoid
|
||||
- Reuse clients through dependency injection.
|
||||
- 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.
|
||||
|
||||
- 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.
|
||||
See `AGENTS.md`, `docs/ai-api-map.md`, and `docs/SHARED_API_V2_MIGRATION.md` for expanded guidance.
|
||||
|
||||
@@ -1,40 +1,39 @@
|
||||
---
|
||||
name: cryptoexchange-net
|
||||
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.
|
||||
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.
|
||||
---
|
||||
|
||||
# CryptoExchange.Net Skill
|
||||
|
||||
## When to use
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
**Three usage modes:**
|
||||
Choose one of these approaches:
|
||||
|
||||
1. **You target ONE exchange** → use that exchange's library directly (e.g., Binance.Net), see its CLAUDE.md.
|
||||
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. **You want ALL exchanges in one package** → install `CryptoClients.Net`, get `ExchangeRestClient` and `ExchangeSocketClient` with everything bundled.
|
||||
1. One exchange: install and use that exchange's library directly.
|
||||
2. Multiple exchanges: install the required exchange libraries and use `CryptoExchange.Net.SharedApis` V2 capabilities.
|
||||
3. All exchanges in one package: install `CryptoClients.Net` and use its combined clients and shared capability lookup.
|
||||
|
||||
For new cross-exchange code, use Shared API V2. V1 aggregate interfaces remain available through `.SharedClient` for incremental migration.
|
||||
|
||||
## Installation
|
||||
|
||||
For a multi-exchange project:
|
||||
|
||||
```bash
|
||||
dotnet add package Binance.Net
|
||||
dotnet add package JK.OKX.Net
|
||||
dotnet add package Bybit.Net
|
||||
# ... etc
|
||||
```
|
||||
|
||||
Or the bundle:
|
||||
Or install the bundle:
|
||||
|
||||
```bash
|
||||
dotnet add package CryptoClients.Net
|
||||
```
|
||||
|
||||
## Core Pattern: Shared Interfaces
|
||||
## Core pattern: fine-grained capabilities
|
||||
|
||||
Every exchange library exposes `.SharedClient` properties on its API surfaces. These implement the same interfaces from `CryptoExchange.Net.SharedApis`.
|
||||
Each exchange API surface exposes a typed `.SharedApi` aggregate. Assign it to the capability for the single operation being used:
|
||||
|
||||
```csharp
|
||||
using Binance.Net.Clients;
|
||||
@@ -42,155 +41,187 @@ using OKX.Net.Clients;
|
||||
using Bybit.Net.Clients;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
|
||||
// All three implement ISpotTickerRestClient
|
||||
ISpotTickerRestClient binance = new BinanceRestClient().SpotApi.SharedClient;
|
||||
ISpotTickerRestClient okx = new OKXRestClient().UnifiedApi.SharedClient;
|
||||
ISpotTickerRestClient bybit = new BybitRestClient().V5Api.SharedClient;
|
||||
IGetTickerRest binance = new BinanceRestClient().SpotApi.SharedApi;
|
||||
IGetTickerRest okx = new OKXRestClient().UnifiedApi.SharedApi;
|
||||
IGetTickerRest bybit = new BybitRestClient().V5Api.SharedApi;
|
||||
|
||||
// Single agnostic call — works against any of them
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||
var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol));
|
||||
// ticker.Data.LastPrice, ticker.Data.HighPrice, etc. — same model regardless of exchange
|
||||
```
|
||||
var result = await binance.GetTickerAsync(new GetTickerRequest(symbol));
|
||||
|
||||
## 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)
|
||||
{
|
||||
Console.WriteLine($"[{sharedClient.Exchange}] Error: {result.Error}");
|
||||
return;
|
||||
}
|
||||
Console.WriteLine($"[{sharedClient.Exchange}] {result.Data.LastPrice}");
|
||||
Console.WriteLine($"[{result.Exchange}] {result.Error}");
|
||||
else
|
||||
Console.WriteLine($"[{result.Exchange}] {result.Data!.LastPrice}");
|
||||
```
|
||||
|
||||
`.Exchange` property on every shared client tells you which exchange you're talking to — useful for logging.
|
||||
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.
|
||||
|
||||
## Core Pattern: Multi-Exchange Aggregation
|
||||
## Shared symbols
|
||||
|
||||
Use `SharedSymbol`; never pass exchange-native symbol strings to shared requests:
|
||||
|
||||
```csharp
|
||||
var clients = new ISpotTickerRestClient[]
|
||||
{
|
||||
new BinanceRestClient().SpotApi.SharedClient,
|
||||
new OKXRestClient().UnifiedApi.SharedClient,
|
||||
new BybitRestClient().V5Api.SharedClient,
|
||||
};
|
||||
var spot = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||
var linearPerpetual = new SharedSymbol(
|
||||
TradingMode.PerpetualLinear,
|
||||
"BTC",
|
||||
"USDT");
|
||||
```
|
||||
|
||||
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");
|
||||
|
||||
// Fetch concurrently from all exchanges
|
||||
var tasks = clients.Select(c => c.GetSpotTickerAsync(new GetTickerRequest(symbol))).ToArray();
|
||||
var tasks = clients.Select(client =>
|
||||
client.GetTickerAsync(new GetTickerRequest(symbol)));
|
||||
var results = await Task.WhenAll(tasks);
|
||||
|
||||
for (int i = 0; i < clients.Length; i++)
|
||||
foreach (var result in results.Where(x => x.Success))
|
||||
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)
|
||||
{
|
||||
if (results[i].Success)
|
||||
Console.WriteLine($"{clients[i].Exchange}: {results[i].Data!.LastPrice}");
|
||||
public Task<HttpResult<SharedTicker>> GetAsync(
|
||||
SharedSymbol symbol,
|
||||
CancellationToken ct = default)
|
||||
=> ticker.GetTickerAsync(new GetTickerRequest(symbol), ct);
|
||||
}
|
||||
```
|
||||
|
||||
## Per-Exchange Setup
|
||||
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.
|
||||
|
||||
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(...); })`.
|
||||
For the bundle, use `services.AddCryptoClients(...)`. CryptoClients.Net provides `IExchangeSharedApiClient` for capability lookup across exchanges.
|
||||
|
||||
## Dependency Injection
|
||||
## V1 migration
|
||||
|
||||
Each exchange library has its own `services.AddXxx(...)` extension. They all share the same option-builder pattern. Register only the ones you use:
|
||||
V1 `.SharedClient` facades and broad interfaces such as `ISpotTickerRestClient` remain available. Migrate one operation at a time:
|
||||
|
||||
```csharp
|
||||
services.AddBinance(restOpts => { /*...*/ }, socketOpts => { /*...*/ });
|
||||
services.AddOKX(restOpts => { /*...*/ }, socketOpts => { /*...*/ });
|
||||
// Inject IBinanceRestClient, IOKXRestClient, etc.
|
||||
// V1
|
||||
await restClient.SpotApi.SharedClient.GetSpotTickerAsync(request);
|
||||
|
||||
// V2
|
||||
await restClient.SpotApi.SharedApi.GetTickerAsync(request);
|
||||
```
|
||||
|
||||
For one-package access: `services.AddCryptoClients(...)` from `CryptoClients.Net`.
|
||||
Important semantic changes:
|
||||
|
||||
## Common Pitfalls — AVOID
|
||||
- V2 ticker methods are `GetTickerAsync` and `GetAllTickersAsync` and return `SharedTicker` for both spot and futures.
|
||||
- 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.
|
||||
|
||||
- **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.
|
||||
See `docs/SHARED_API_V2_MIGRATION.md` for detailed mappings.
|
||||
|
||||
## Implementing a New Exchange Library
|
||||
## Common pitfalls
|
||||
|
||||
If you're building a NEW exchange wrapper following the CryptoExchange.Net pattern (rare but valuable):
|
||||
- Do not install `CryptoExchange.Net` alone and expect exchange endpoints.
|
||||
- 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.
|
||||
|
||||
- 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
|
||||
## Implementing a new exchange library
|
||||
|
||||
See existing libraries (Binance.Net, Bybit.Net) as reference implementations.
|
||||
- Derive API clients from `RestApiClient` and `SocketApiClient`.
|
||||
- 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
|
||||
|
||||
- Source: https://github.com/JKorf/CryptoExchange.Net
|
||||
- Documentation: https://cryptoexchange.jkorf.dev/
|
||||
- SharedApis docs: https://cryptoexchange.jkorf.dev/CryptoExchange.Net/idocs_shared.html
|
||||
- Bundle (all exchanges): https://github.com/JKorf/CryptoClients.Net
|
||||
- Shared API migration: https://github.com/JKorf/CryptoExchange.Net/blob/master/docs/SHARED_API_V2_MIGRATION.md
|
||||
- Bundle: https://github.com/JKorf/CryptoClients.Net
|
||||
- Demo app: https://github.com/JKorf/CryptoManager.Net
|
||||
- Discord: https://discord.gg/MSpeEtSY8t
|
||||
|
||||
@@ -62,6 +62,31 @@ namespace CryptoExchange.Net.UnitTests.ConverterTests
|
||||
Assert.That(deserialized.Prop8!.Prop31, Is.EqualTo(5));
|
||||
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>))]
|
||||
|
||||
@@ -3,6 +3,7 @@ using CryptoExchange.Net.RateLimiting;
|
||||
using CryptoExchange.Net.RateLimiting.Filters;
|
||||
using CryptoExchange.Net.RateLimiting.Guards;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
using CryptoExchange.Net.RateLimiting.Trackers;
|
||||
using CryptoExchange.Net.UnitTests.Implementations;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
@@ -33,12 +34,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
for (var i = 0; i < requests + 1; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
|
||||
Assert.That(i == requests ? triggered : !triggered);
|
||||
}
|
||||
triggered = false;
|
||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
|
||||
Assert.That(!triggered);
|
||||
}
|
||||
|
||||
@@ -58,7 +59,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
|
||||
bool expected = i == 1 ? expectLimiting ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
|
||||
Assert.That(expected);
|
||||
}
|
||||
@@ -79,9 +80,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
|
||||
Assert.That(expectLimiting ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
@@ -100,12 +101,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
for (var i = 0; i < requests + 1; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
|
||||
Assert.That(i == requests ? triggered : !triggered);
|
||||
}
|
||||
triggered = false;
|
||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
|
||||
Assert.That(!triggered);
|
||||
}
|
||||
|
||||
@@ -123,7 +124,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
|
||||
bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
|
||||
Assert.That(expected);
|
||||
}
|
||||
@@ -143,7 +144,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
|
||||
bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
|
||||
Assert.That(expected);
|
||||
}
|
||||
@@ -166,9 +167,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, key1, 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, key1, 1, RateLimitingBehaviour.Wait, null, 1, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, key2, 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, key2, 1, RateLimitingBehaviour.Wait, null, 1, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
@@ -185,9 +186,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, null, 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, null, 1, RateLimitingBehaviour.Wait, null, 1, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
@@ -205,9 +206,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
@@ -222,9 +223,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
RateLimitEvent? evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition(host1, "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition(host1, "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition(host2, "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition(host2, "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
@@ -238,8 +239,8 @@ namespace CryptoExchange.Net.UnitTests
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("https://test.com", "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("https://test.com", "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("https://test.com", "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, 1, ct.Token);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("https://test.com", "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, 1, ct.Token);
|
||||
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
|
||||
}
|
||||
|
||||
@@ -257,9 +258,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
|
||||
|
||||
// act
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, null, 1, RateLimitingBehaviour.Fail, null, ct.Token);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, null, 1, RateLimitingBehaviour.Fail, null, 1, ct.Token);
|
||||
await rateLimiter.ResetAsync(RateLimitItemType.Request, definition, null, null, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, null, 1, RateLimitingBehaviour.Fail, null, ct.Token);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, null, 1, RateLimitingBehaviour.Fail, null, 1, ct.Token);
|
||||
|
||||
// assert
|
||||
Assert.That(evnt, Is.Null);
|
||||
@@ -279,10 +280,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
// act
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition1, null, 1, RateLimitingBehaviour.Fail, null, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, null, 1, RateLimitingBehaviour.Fail, null, default);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition1, null, 1, RateLimitingBehaviour.Fail, null, 1, default);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, null, 1, RateLimitingBehaviour.Fail, null, 1, default);
|
||||
await rateLimiter.ResetAsync(RateLimitItemType.Request, definition1, null, null, null, default);
|
||||
var result3 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, null, 1, RateLimitingBehaviour.Fail, null, default);
|
||||
var result3 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, null, 1, RateLimitingBehaviour.Fail, null, 1, default);
|
||||
|
||||
// assert
|
||||
Assert.That(evnt, Is.Not.Null);
|
||||
@@ -321,5 +322,73 @@ namespace CryptoExchange.Net.UnitTests
|
||||
// assert
|
||||
Assert.That(evnt != null, Is.EqualTo(expectLimited));
|
||||
}
|
||||
|
||||
[TestCase(RateLimitWindowType.Fixed)]
|
||||
[TestCase(RateLimitWindowType.FixedAfterFirst)]
|
||||
[TestCase(RateLimitWindowType.Sliding)]
|
||||
[TestCase(RateLimitWindowType.Decay)]
|
||||
public async Task LowerThreshold_ReservesCapacityForFullThreshold(RateLimitWindowType windowType)
|
||||
{
|
||||
var gate = new RateLimitGate("Test");
|
||||
gate.AddGuard(new RateLimitGuard(
|
||||
RateLimitGuard.PerHost,
|
||||
new LimitItemTypeFilter(RateLimitItemType.Request),
|
||||
10,
|
||||
TimeSpan.FromHours(1),
|
||||
windowType,
|
||||
decayPerTimeSpan: 1));
|
||||
|
||||
var definition = new RequestDefinition(
|
||||
"https://test.com", "/ticker", HttpMethod.Get);
|
||||
var logger = new TraceLogger();
|
||||
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
var result = await gate.ProcessAsync(
|
||||
logger, i, RateLimitItemType.Request, definition, null, 1,
|
||||
RateLimitingBehaviour.Fail, null, 0.8, default);
|
||||
Assert.That(result.Success, Is.True);
|
||||
}
|
||||
|
||||
var marketData = await gate.ProcessAsync(
|
||||
logger, 9, RateLimitItemType.Request, definition, null, 1,
|
||||
RateLimitingBehaviour.Fail, null, 0.8, default);
|
||||
Assert.That(marketData.Error, Is.TypeOf<ClientRateLimitError>());
|
||||
|
||||
var order = await gate.ProcessAsync(
|
||||
logger, 10, RateLimitItemType.Request, definition, null, 2,
|
||||
RateLimitingBehaviour.Fail, null, 1.0, default);
|
||||
Assert.That(order.Success, Is.True);
|
||||
|
||||
var overHardLimit = await gate.ProcessAsync(
|
||||
logger, 11, RateLimitItemType.Request, definition, null, 1,
|
||||
RateLimitingBehaviour.Fail, null, 1.0, default);
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using CryptoExchange.Net.Interfaces.Clients;
|
||||
using CryptoExchange.Net.RateLimiting;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
@@ -16,7 +22,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(client.GetCapability<ITestCapability>(TradingMode.Spot), Is.SameAs(sharedApi));
|
||||
Assert.That(client.GetCapability<ITestCapability>(TradingMode.Spot)?.Capability, Is.SameAs(sharedApi));
|
||||
Assert.That(client.GetCapability<ITestCapability>(TradingMode.PerpetualLinear), Is.Null);
|
||||
Assert.That(client.GetCapabilities<ITestCapability>(TradingMode.Spot), Has.Count.EqualTo(1));
|
||||
Assert.That(client.GetCapabilities<ITestCapability>(TradingMode.PerpetualLinear), Is.Empty);
|
||||
@@ -33,8 +39,94 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(client.GetCapability<ITestCapability>(TradingMode.Spot), Is.SameAs(sharedApi));
|
||||
Assert.That(client.GetCapability<ITestCapability>(TradingMode.PerpetualLinear), Is.SameAs(sharedApi));
|
||||
Assert.That(client.GetCapability<ITestCapability>(TradingMode.Spot)?.Capability, Is.SameAs(sharedApi));
|
||||
Assert.That(client.GetCapability<ITestCapability>(TradingMode.PerpetualLinear)?.Capability, Is.SameAs(sharedApi));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetCapabilities_WithReference_ShouldReturnMatchingCapabilities()
|
||||
{
|
||||
var sharedApi = new TestSharedApi(
|
||||
[TradingMode.Spot],
|
||||
[TradingMode.Spot]);
|
||||
var client = new TestSharedApiClient(sharedApi);
|
||||
|
||||
var result = client.GetCapabilities(
|
||||
new SharedCapabilityReference<ITestCapability>(),
|
||||
TradingMode.Spot);
|
||||
|
||||
Assert.That(result, Has.Count.EqualTo(1));
|
||||
Assert.That(result[0].Capability, Is.SameAs(sharedApi));
|
||||
Assert.That(result[0].Options, Is.SameAs(sharedApi.CapabilityOptions));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Discover_ShouldReturnAllSharedApiInformation()
|
||||
{
|
||||
var restApi = new TestRestSharedApi();
|
||||
var socketApi = new TestSocketSharedApi();
|
||||
var client = new TestDiSharedApiClient(
|
||||
restApi,
|
||||
socketApi);
|
||||
|
||||
var result = client.Discover();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(result.PreferredTransport, Is.EqualTo(SharedTransport.Socket));
|
||||
Assert.That(result.SharedApis, Has.Length.EqualTo(2));
|
||||
Assert.That(result.SharedApis[0].Transport, Is.EqualTo(SharedTransport.Rest));
|
||||
Assert.That(result.SharedApis[1].Transport, Is.EqualTo(SharedTransport.Socket));
|
||||
Assert.That(result.SharedApis[0].SupportedTradingModes, Is.EqualTo(new[] { TradingMode.Spot }));
|
||||
Assert.That(result.SharedApis[0].Capabilities, Has.Length.EqualTo(1));
|
||||
Assert.That(result.SharedApis[0].Authenticated, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterSharedApiClient_ShouldResolvePreferredTransport()
|
||||
{
|
||||
var restApi = new TestRestSharedApi();
|
||||
var socketApi = new TestSocketSharedApi();
|
||||
var services = new ServiceCollection();
|
||||
|
||||
services.AddSingleton(restApi);
|
||||
services.AddSingleton(socketApi);
|
||||
|
||||
services.RegisterSharedApiClient<
|
||||
ITestSharedApiClient,
|
||||
TestDiSharedApiClient>(sharedApis => sharedApis
|
||||
.Add(client => client.Rest)
|
||||
.Add(client => client.Socket));
|
||||
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(
|
||||
provider.GetRequiredService<ITestSharedApiClient>(),
|
||||
Is.Not.Null);
|
||||
|
||||
Assert.That(
|
||||
provider.GetRequiredService<ISharedApiClientBase>(),
|
||||
Is.InstanceOf<TestDiSharedApiClient>());
|
||||
|
||||
Assert.That(
|
||||
provider.GetRequiredService<ITestCapability>(),
|
||||
Is.SameAs(socketApi));
|
||||
|
||||
Assert.That(
|
||||
provider.GetServices<ITestCapability>().ToArray(),
|
||||
Is.EqualTo(new[] { socketApi }));
|
||||
|
||||
Assert.That(
|
||||
provider.GetRequiredService<ITestRestCapability>(),
|
||||
Is.SameAs(restApi));
|
||||
|
||||
Assert.That(
|
||||
provider.GetRequiredService<ITestSocketCapability>(),
|
||||
Is.SameAs(socketApi));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -101,6 +193,46 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class TestApiClient : IBaseApiClient
|
||||
{
|
||||
public string Exchange => "TestExchange";
|
||||
public string BaseAddress => "https://test.invalid";
|
||||
|
||||
public string FormatSymbol(
|
||||
string baseAsset,
|
||||
string quoteAsset,
|
||||
TradingMode tradingMode,
|
||||
DateTime? deliverDate = null)
|
||||
=> $"{baseAsset}{quoteAsset}";
|
||||
|
||||
public Task<TResult> WithRateLimitAdmissionAsync<TResult>(
|
||||
RateLimitAdmission admission,
|
||||
Func<Task<TResult>> operation)
|
||||
=> operation();
|
||||
}
|
||||
|
||||
private interface ITestRestCapability : ITestCapability, ISharedRest
|
||||
{
|
||||
}
|
||||
|
||||
private interface ITestSocketCapability : ITestCapability, ISharedSocket
|
||||
{
|
||||
}
|
||||
|
||||
private interface ITestRestSharedApi : ITestRestCapability
|
||||
{
|
||||
}
|
||||
|
||||
private interface ITestSocketSharedApi : ITestSocketCapability
|
||||
{
|
||||
}
|
||||
|
||||
private interface ITestSharedApiClient : ISharedApiClientBase
|
||||
{
|
||||
ITestRestSharedApi Rest { get; }
|
||||
ITestSocketSharedApi Socket { get; }
|
||||
}
|
||||
|
||||
private sealed class TestCapabilityOptions : CapabilityOptions<SharedRequest, ITestCapability>
|
||||
{
|
||||
public override string Description => "Test capability";
|
||||
@@ -126,7 +258,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
TradingMode[]? applicableTradingModes)
|
||||
: base(
|
||||
SharedTransport.Rest,
|
||||
"TestExchange",
|
||||
new TestApiClient(),
|
||||
apiTradingModes,
|
||||
() => false,
|
||||
(baseAsset, quoteAsset, tradingMode, deliverDate) => $"{baseAsset}{quoteAsset}")
|
||||
@@ -148,7 +280,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public TestOptionsHost(TradingMode[] apiTradingModes, CapabilityOptions capabilityOptions)
|
||||
: base(
|
||||
SharedTransport.Rest,
|
||||
"TestExchange",
|
||||
new TestApiClient(),
|
||||
apiTradingModes,
|
||||
() => false,
|
||||
(baseAsset, quoteAsset, tradingMode, deliverDate) => $"{baseAsset}{quoteAsset}")
|
||||
@@ -159,10 +291,63 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public override SharedClientInfo Discover() => new();
|
||||
}
|
||||
|
||||
private abstract class TestTransportSharedApi : SharedApiBase
|
||||
{
|
||||
protected TestTransportSharedApi(SharedTransport transport)
|
||||
: base(
|
||||
transport,
|
||||
new TestApiClient(),
|
||||
[TradingMode.Spot],
|
||||
() => false,
|
||||
(baseAsset, quoteAsset, tradingMode, deliverDate) => $"{baseAsset}{quoteAsset}")
|
||||
{
|
||||
SetCapabilities(new TestCapabilityOptions([TradingMode.Spot]));
|
||||
}
|
||||
|
||||
public override SharedClientInfo Discover() => new()
|
||||
{
|
||||
Exchange = this.Exchange,
|
||||
TypeName = GetType().Name,
|
||||
SupportedTradingModes = this.SupportedTradingModes,
|
||||
Transport = this.Transport,
|
||||
Authenticated = this.Authenticated,
|
||||
Capabilities = ((ISharedApi)this).Capabilities.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class TestRestSharedApi : TestTransportSharedApi, ITestRestSharedApi
|
||||
{
|
||||
public TestRestSharedApi() : base(SharedTransport.Rest)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestSocketSharedApi : TestTransportSharedApi, ITestSocketSharedApi
|
||||
{
|
||||
public TestSocketSharedApi() : base(SharedTransport.Socket)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestDiSharedApiClient : SharedApiClientBase, ITestSharedApiClient
|
||||
{
|
||||
public ITestRestSharedApi Rest { get; }
|
||||
public ITestSocketSharedApi Socket { get; }
|
||||
|
||||
public TestDiSharedApiClient(
|
||||
TestRestSharedApi rest,
|
||||
TestSocketSharedApi socket)
|
||||
: base(SharedTransport.Socket, rest, socket)
|
||||
{
|
||||
Rest = rest;
|
||||
Socket = socket;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestSharedApiClient : SharedApiClientBase
|
||||
{
|
||||
public TestSharedApiClient(params ISharedApiCapability[] sharedApis)
|
||||
: base([SharedTransport.Rest, SharedTransport.Socket], sharedApis)
|
||||
: base(SharedTransport.Rest, sharedApis)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.Interfaces.Clients;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.RateLimiting;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
@@ -78,6 +81,11 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
|
||||
|
||||
/// <summary>
|
||||
/// Current rate limit admission override
|
||||
/// </summary>
|
||||
protected readonly AsyncLocal<RateLimitAdmission?> AdmissionOverride = new();
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
@@ -122,6 +130,29 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<TResult> WithRateLimitAdmissionAsync<TResult>(
|
||||
RateLimitAdmission admission,
|
||||
Func<Task<TResult>> operation)
|
||||
{
|
||||
if (admission == null)
|
||||
throw new ArgumentNullException(nameof(admission));
|
||||
if (operation == null)
|
||||
throw new ArgumentNullException(nameof(operation));
|
||||
|
||||
var previous = AdmissionOverride.Value;
|
||||
AdmissionOverride.Value = admission;
|
||||
|
||||
try
|
||||
{
|
||||
return await operation().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
AdmissionOverride.Value = previous;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
|
||||
@@ -350,6 +350,9 @@ namespace CryptoExchange.Net.Clients
|
||||
|
||||
if (ClientOptions.RateLimiterEnabled)
|
||||
{
|
||||
var admissionOverride = AdmissionOverride.Value ?? ClientOptions.RateLimitAdmission?.Invoke(definition, requestWeight);
|
||||
var rateRatio = admissionOverride?.MaxUtilizationRatio ?? 1.0;
|
||||
|
||||
var limitResult = await definition.RateLimitGate.ProcessAsync(
|
||||
_logger,
|
||||
requestId,
|
||||
@@ -359,6 +362,7 @@ namespace CryptoExchange.Net.Clients
|
||||
requestWeight,
|
||||
ClientOptions.RateLimitingBehaviour,
|
||||
rateLimitKeySuffix + ClientOptions.RateLimitGroup,
|
||||
rateRatio,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
if (!limitResult.Success)
|
||||
return limitResult.Error!;
|
||||
@@ -374,6 +378,9 @@ namespace CryptoExchange.Net.Clients
|
||||
if (ClientOptions.RateLimiterEnabled)
|
||||
{
|
||||
var singleRequestWeight = weightSingleLimiter ?? 1;
|
||||
var admissionOverride = AdmissionOverride.Value ?? ClientOptions.RateLimitAdmission?.Invoke(definition, singleRequestWeight);
|
||||
var rateRatio = admissionOverride?.MaxUtilizationRatio ?? 1.0;
|
||||
|
||||
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(
|
||||
_logger,
|
||||
requestId,
|
||||
@@ -384,6 +391,7 @@ namespace CryptoExchange.Net.Clients
|
||||
singleRequestWeight,
|
||||
ClientOptions.RateLimitingBehaviour,
|
||||
rateLimitKeySuffix,
|
||||
rateRatio,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
if (!limitResult.Success)
|
||||
return limitResult.Error!;
|
||||
|
||||
@@ -286,7 +286,7 @@ namespace CryptoExchange.Net.Clients
|
||||
while (true)
|
||||
{
|
||||
// Get a new or existing socket connection
|
||||
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic, subscription.IndividualSubscriptionCount).ConfigureAwait(false);
|
||||
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription, subscription.Topic, subscription.IndividualSubscriptionCount).ConfigureAwait(false);
|
||||
if (!socketResult.Success)
|
||||
return WebSocketResult.Fail<UpdateSubscription>(Exchange, socketResult.Error);
|
||||
|
||||
@@ -309,7 +309,7 @@ namespace CryptoExchange.Net.Clients
|
||||
|
||||
var needsConnecting = !socketConnection.Connected;
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated, ct).ConfigureAwait(false);
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated, ct, subscription).ConfigureAwait(false);
|
||||
if (!connectResult.Success)
|
||||
return WebSocketResult.Fail<UpdateSubscription>(Exchange, connectResult.Error!);
|
||||
|
||||
@@ -482,7 +482,7 @@ namespace CryptoExchange.Net.Clients
|
||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct).ConfigureAwait(false);
|
||||
var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct, processor: query).ConfigureAwait(false);
|
||||
if (!socketResult.Success)
|
||||
return QueryResult.Fail<THandlerResponse>(Exchange, socketResult.Error);
|
||||
|
||||
@@ -495,7 +495,7 @@ namespace CryptoExchange.Net.Clients
|
||||
released = true;
|
||||
}
|
||||
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct).ConfigureAwait(false);
|
||||
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated, ct, query).ConfigureAwait(false);
|
||||
if (!connectResult.Success)
|
||||
return QueryResult.Fail<THandlerResponse>(Exchange, connectResult.Error!);
|
||||
}
|
||||
@@ -524,7 +524,18 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="authenticated">Whether the socket should authenticated</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult> ConnectIfNeededAsync(ISocketConnection socket, bool authenticated, CancellationToken ct)
|
||||
protected virtual Task<CallResult> ConnectIfNeededAsync(ISocketConnection socket, bool authenticated, CancellationToken ct)
|
||||
=> ConnectIfNeededAsync(socket, authenticated, ct, null);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a socket needs to be connected and does so if needed. Also authenticates on the socket if needed
|
||||
/// </summary>
|
||||
/// <param name="socket">The connection to check</param>
|
||||
/// <param name="authenticated">Whether the socket should authenticated</param>
|
||||
/// <param name="processor">The message processor requesting a connection</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult> ConnectIfNeededAsync(ISocketConnection socket, bool authenticated, CancellationToken ct, IMessageProcessor? processor)
|
||||
{
|
||||
if (socket.Connected)
|
||||
return CallResult.Ok();
|
||||
@@ -533,16 +544,13 @@ namespace CryptoExchange.Net.Clients
|
||||
if (!connectResult.Success)
|
||||
return connectResult;
|
||||
|
||||
if (ClientOptions.DelayAfterConnect != TimeSpan.Zero)
|
||||
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
|
||||
|
||||
if (!authenticated || socket.Authenticated)
|
||||
return CallResult.Ok();
|
||||
|
||||
if (socket is not SocketConnection sc)
|
||||
throw new InvalidOperationException("HighPerfSocketConnection not supported for authentication");
|
||||
|
||||
var result = await AuthenticateSocketAsync(sc).ConfigureAwait(false);
|
||||
var result = await AuthenticateSocketAsync(sc, processor).ConfigureAwait(false);
|
||||
if (!result.Success)
|
||||
await socket.CloseAsync().ConfigureAwait(false);
|
||||
|
||||
@@ -554,13 +562,22 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
/// <param name="socket">Socket to authenticate</param>
|
||||
/// <returns></returns>
|
||||
public virtual async Task<CallResult> AuthenticateSocketAsync(SocketConnection socket)
|
||||
public virtual Task<CallResult> AuthenticateSocketAsync(SocketConnection socket)
|
||||
=> AuthenticateSocketAsync(socket, null);
|
||||
|
||||
/// <summary>
|
||||
/// Authenticate a socket connection
|
||||
/// </summary>
|
||||
/// <param name="socket">Socket to authenticate</param>
|
||||
/// <param name="processor">The message processor requesting authentication</param>
|
||||
/// <returns></returns>
|
||||
public virtual async Task<CallResult> AuthenticateSocketAsync(SocketConnection socket, IMessageProcessor? processor)
|
||||
{
|
||||
if (GetAuthenticationProvider() == null)
|
||||
return CallResult.Fail(new NoApiCredentialsError());
|
||||
|
||||
_logger.AttemptingToAuthenticate(socket.SocketId);
|
||||
var authRequest = await GetAuthenticationRequestAsync(socket).ConfigureAwait(false);
|
||||
var authRequest = await GetAuthenticationRequestAsync(socket, processor).ConfigureAwait(false);
|
||||
if (authRequest != null)
|
||||
{
|
||||
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
|
||||
@@ -581,13 +598,21 @@ namespace CryptoExchange.Net.Clients
|
||||
return CallResult.Ok();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Should return the request which can be used to authenticate a socket connection
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) =>
|
||||
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) =>
|
||||
Task.FromResult(GetAuthenticationProvider()!.GetAuthenticationQuery(this, connection));
|
||||
|
||||
/// <summary>
|
||||
/// Should return the request which can be used to authenticate a socket connection
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection, IMessageProcessor? processor) =>
|
||||
GetAuthenticationRequestAsync(connection);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a system subscription. Used for example to reply to ping requests
|
||||
/// </summary>
|
||||
@@ -645,6 +670,18 @@ namespace CryptoExchange.Net.Clients
|
||||
&& (AllowTopicsOnTheSameConnection || !connection.Topics.Contains(topic));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the connection can be used for a new subscription or query with the provided parameters
|
||||
/// </summary>
|
||||
/// <param name="connection">The connection to check</param>
|
||||
/// <param name="address">The address set by the request</param>
|
||||
/// <param name="authenticated">Whether the request needs an authenticated connection</param>
|
||||
/// <param name="processor">The processor requesting a connection</param>
|
||||
/// <param name="topic">Topic of the request</param>
|
||||
/// <returns>True if connection can be used</returns>
|
||||
protected virtual bool ConnectionCanBeUsedFor(SocketConnection connection, string address, bool authenticated, IMessageProcessor? processor, string? topic = null)
|
||||
=> ConnectionCanBeUsedFor(connection, address, authenticated, topic);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
|
||||
/// </summary>
|
||||
@@ -655,15 +692,36 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
|
||||
/// <param name="individualSubscriptionCount">The number of individual subscriptions in this subscribe request</param>
|
||||
/// <returns></returns>
|
||||
protected virtual Task<CallResult<SocketConnection>> GetSocketConnection(
|
||||
string address,
|
||||
bool authenticated,
|
||||
bool dedicatedRequestConnection,
|
||||
CancellationToken ct,
|
||||
string? topic = null,
|
||||
int individualSubscriptionCount = 1)
|
||||
=> GetSocketConnection(address, authenticated, dedicatedRequestConnection, ct, null, topic, individualSubscriptionCount);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
|
||||
/// </summary>
|
||||
/// <param name="address">The address the socket is for</param>
|
||||
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
|
||||
/// <param name="individualSubscriptionCount">The number of individual subscriptions in this subscribe request</param>
|
||||
/// <param name="processor">The processor requesting a connection</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(
|
||||
string address,
|
||||
bool authenticated,
|
||||
bool dedicatedRequestConnection,
|
||||
CancellationToken ct,
|
||||
IMessageProcessor? processor,
|
||||
string? topic = null,
|
||||
int individualSubscriptionCount = 1)
|
||||
{
|
||||
var socketQuery = _socketConnections.Where(s => ConnectionCanBeUsedFor(s.Value, address, authenticated, topic)).Select(x => x.Value); // Don't ToList this so the query is executed again when called
|
||||
var socketQuery = _socketConnections.Where(s => ConnectionCanBeUsedFor(s.Value, address, authenticated, processor, topic)).Select(x => x.Value); // Don't ToList this so the query is executed again when called
|
||||
|
||||
// If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection
|
||||
var delayStart = DateTime.UtcNow;
|
||||
@@ -858,9 +916,11 @@ namespace CryptoExchange.Net.Clients
|
||||
ReconnectInterval = ClientOptions.ReconnectInterval,
|
||||
RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null,
|
||||
RateLimitingBehavior = ClientOptions.RateLimitingBehaviour,
|
||||
RateLimitAdmissionCallbackOptions = (def, weight) => ClientOptions.RateLimitAdmission?.Invoke(def, weight),
|
||||
RateLimitAdmissionCallbackRequest = () => AdmissionOverride.Value,
|
||||
Proxy = ClientOptions.Proxy,
|
||||
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout,
|
||||
ReceiveBufferSize = ClientOptions.ReceiveBufferSize,
|
||||
ReceiveBufferSize = ClientOptions.ReceiveBufferSize
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -173,6 +173,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
|
||||
if (targetType.IsAssignableFrom(value?.GetType()))
|
||||
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
|
||||
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<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>
|
||||
<PackageVersion>12.5.1</PackageVersion>
|
||||
<AssemblyVersion>12.5.1</AssemblyVersion>
|
||||
<FileVersion>12.5.1</FileVersion>
|
||||
<PackageVersion>13.2.0</PackageVersion>
|
||||
<AssemblyVersion>13.2.0</AssemblyVersion>
|
||||
<FileVersion>13.2.0</FileVersion>
|
||||
<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>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Interfaces.Clients;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.RateLimiting;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
@@ -11,6 +14,7 @@ using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
@@ -146,25 +150,42 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
public static string ToFormData(this IDictionary<string, object> parameters)
|
||||
{
|
||||
var formData = HttpUtility.ParseQueryString(string.Empty);
|
||||
var result = new StringBuilder();
|
||||
var first = true;
|
||||
|
||||
void Append(string key, object value)
|
||||
{
|
||||
if (!first)
|
||||
result.Append('&');
|
||||
first = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(key))
|
||||
{
|
||||
result.Append(HttpUtility.UrlEncode(key, Encoding.UTF8));
|
||||
result.Append('=');
|
||||
}
|
||||
|
||||
var text = string.Format(CultureInfo.InvariantCulture, "{0}", value);
|
||||
result.Append(HttpUtility.UrlEncode(text, Encoding.UTF8));
|
||||
}
|
||||
|
||||
foreach (var kvp in parameters)
|
||||
{
|
||||
if (kvp.Value is null)
|
||||
continue;
|
||||
|
||||
if (kvp.Value.GetType().IsArray)
|
||||
if (kvp.Value is Array array)
|
||||
{
|
||||
var array = (Array)kvp.Value;
|
||||
foreach (var value in array)
|
||||
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", value));
|
||||
Append(kvp.Key, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value));
|
||||
Append(kvp.Key, kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return formData.ToString()!;
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -474,47 +495,6 @@ namespace CryptoExchange.Net
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a strict Shared API aggregate and all capability interfaces implemented by it.
|
||||
/// </summary>
|
||||
public static IServiceCollection RegisterSharedApi<
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)]
|
||||
#endif
|
||||
TSharedApi>(
|
||||
this IServiceCollection services,
|
||||
Func<IServiceProvider, TSharedApi> factory)
|
||||
where TSharedApi : class, ISharedApi
|
||||
{
|
||||
services.AddTransient<TSharedApi>(factory);
|
||||
|
||||
var markerInterfaces = new[]
|
||||
{
|
||||
typeof(ISharedApiCapability),
|
||||
typeof(ISharedRest),
|
||||
typeof(ISharedSocket),
|
||||
typeof(ISharedSubscription)
|
||||
};
|
||||
|
||||
var capabilityInterfaces = typeof(TSharedApi)
|
||||
.GetInterfaces()
|
||||
.Where(x =>
|
||||
typeof(ISharedApiCapability).IsAssignableFrom(x)
|
||||
&& !markerInterfaces.Contains(x))
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
foreach (var capabilityInterface in capabilityInterfaces)
|
||||
{
|
||||
services.AddTransient(
|
||||
capabilityInterface,
|
||||
serviceProvider =>
|
||||
serviceProvider.GetRequiredService<TSharedApi>());
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a hex encoded string to byte array
|
||||
/// </summary>
|
||||
@@ -534,6 +514,41 @@ namespace CryptoExchange.Net
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute an operation with a specific rate limit admission rule
|
||||
/// </summary>
|
||||
/// <param name="client">Client</param>
|
||||
/// <param name="admission">Rate limit admission rule</param>
|
||||
/// <param name="operation">Operation to execute</param>
|
||||
public static Task<TResult> WithRateLimitAdmissionAsync<TClient, TResult>(
|
||||
this TClient client,
|
||||
RateLimitAdmission admission,
|
||||
Func<TClient, Task<TResult>> operation)
|
||||
where TClient : IRateLimitAdmissionClient
|
||||
{
|
||||
return client.WithRateLimitAdmissionAsync(
|
||||
admission,
|
||||
() => operation(client));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Return the task results in the form of an IAsyncEnumerable, returning the first completed task first
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of task result</typeparam>
|
||||
/// <param name="tasks">Task list</param>
|
||||
public static async IAsyncEnumerable<T> ParallelEnumerateAsync<T>(this IEnumerable<Task<T>> tasks)
|
||||
{
|
||||
var remaining = new List<Task<T>>(tasks);
|
||||
|
||||
while (remaining.Count != 0)
|
||||
{
|
||||
var task = await Task.WhenAny(remaining).ConfigureAwait(false);
|
||||
remaining.Remove(task);
|
||||
yield return await task.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces.Clients
|
||||
@@ -8,7 +6,7 @@ namespace CryptoExchange.Net.Interfaces.Clients
|
||||
/// <summary>
|
||||
/// Base api client
|
||||
/// </summary>
|
||||
public interface IBaseApiClient
|
||||
public interface IBaseApiClient : IRateLimitAdmissionClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Exchange name
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
using CryptoExchange.Net.RateLimiting;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// A client supporting rate limit admission rules
|
||||
/// </summary>
|
||||
public interface IRateLimitAdmissionClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Execute an operation with a specific rate limit admission rule
|
||||
/// </summary>
|
||||
Task<TResult> WithRateLimitAdmissionAsync<TResult>(
|
||||
RateLimitAdmission admission,
|
||||
Func<Task<TResult>> operation);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.RateLimiting;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
@@ -41,6 +42,12 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// </summary>
|
||||
public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait;
|
||||
|
||||
/// <summary>
|
||||
/// A delegate receiving the request definition and the request weight that can be used to determine whether a request should be admitted or rejected based
|
||||
/// on the request definition and the current rate limit usage ratio. This allows for custom rate limiting logic to be implemented.
|
||||
/// </summary>
|
||||
public Func<RequestDefinition, int, RateLimitAdmission>? RateLimitAdmission { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
|
||||
@@ -75,6 +75,7 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
item.RateLimiterEnabled = RateLimiterEnabled;
|
||||
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
||||
item.RateLimitGroup = RateLimitGroup;
|
||||
item.RateLimitAdmission = RateLimitAdmission;
|
||||
item.CachingEnabled = CachingEnabled;
|
||||
item.CachingMaxAge = CachingMaxAge;
|
||||
item.HttpVersion = HttpVersion;
|
||||
|
||||
@@ -19,6 +19,11 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// </summary>
|
||||
public int? MaxSocketConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time to wait after connecting a socket before sending messages. Can be used for API's which will rate limit if you subscribe directly after connecting.
|
||||
/// </summary>
|
||||
public TimeSpan? DelayAfterConnect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set the values of this options on the target options
|
||||
/// </summary>
|
||||
@@ -28,6 +33,7 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
item.SocketNoDataTimeout = SocketNoDataTimeout;
|
||||
item.AutoTimestamp = AutoTimestamp;
|
||||
item.MaxSocketConnections = MaxSocketConnections;
|
||||
item.DelayAfterConnect = DelayAfterConnect;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
||||
item.RateLimiterEnabled = RateLimiterEnabled;
|
||||
item.RateLimitGroup = RateLimitGroup;
|
||||
item.RateLimitAdmission = RateLimitAdmission;
|
||||
item.ReceiveBufferSize = ReceiveBufferSize;
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ public record CallResult<T> : CallResult, ICallResult<T>
|
||||
/// Call result for an exchange
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Data type</typeparam>
|
||||
public record ExchangeCallResult<T> : CallResult<T>
|
||||
public record ExchangeCallResult<T> : CallResult<T>, IExchangeCallResult<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Exchange name
|
||||
|
||||
@@ -40,4 +40,23 @@ public interface ICallResult<T> : ICallResult
|
||||
/// The result data, only available when Success = true
|
||||
/// </summary>
|
||||
T? Data { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exchange call result
|
||||
/// </summary>
|
||||
public interface IExchangeCallResult : ICallResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Exchange name
|
||||
/// </summary>
|
||||
public string Exchange { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exchange call result
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Result data type</typeparam>
|
||||
public interface IExchangeCallResult<T> : IExchangeCallResult, ICallResult<T>
|
||||
{
|
||||
}
|
||||
@@ -10,12 +10,8 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// HTTP call result
|
||||
/// </summary>
|
||||
public interface IHttpResult : ICallResult
|
||||
public interface IHttpResult : ICallResult, IExchangeCallResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Exchange name
|
||||
/// </summary>
|
||||
string Exchange { get; init; }
|
||||
/// <summary>
|
||||
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
|
||||
/// </summary>
|
||||
@@ -79,7 +75,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// HTTP call result
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Result data type</typeparam>
|
||||
public interface IHttpResult<T> : IHttpResult, ICallResult<T>
|
||||
public interface IHttpResult<T> : IHttpResult, IExchangeCallResult<T>
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,8 @@ namespace CryptoExchange.Net.Objects
|
||||
/// <summary>
|
||||
/// WebSocket call result
|
||||
/// </summary>
|
||||
public interface IWebSocketResult : ICallResult
|
||||
public interface IWebSocketResult : ICallResult, IExchangeCallResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Exchange name
|
||||
/// </summary>
|
||||
string Exchange { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The request id
|
||||
/// </summary>
|
||||
@@ -42,7 +37,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// WebSocket call result
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Data result type</typeparam>
|
||||
public interface IWebSocketResult<T> : IWebSocketResult, ICallResult<T>
|
||||
public interface IWebSocketResult<T> : IWebSocketResult, IExchangeCallResult<T>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
using CryptoExchange.Net.RateLimiting;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
@@ -64,6 +65,17 @@ namespace CryptoExchange.Net.Objects.Sockets
|
||||
/// </summary>
|
||||
public RateLimitingBehaviour RateLimitingBehavior { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A delegate receiving the request definition and the request weight that can be used to determine whether a request should be admitted or rejected based
|
||||
/// on the request definition and the current rate limit usage ratio. This allows for custom rate limiting logic to be implemented.
|
||||
/// </summary>
|
||||
public Func<RequestDefinition, int, RateLimitAdmission?>? RateLimitAdmissionCallbackOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A delegate for retrieving the current rate limit admission decision for a specific request. This allows for custom rate limiting logic to be implemented.
|
||||
/// </summary>
|
||||
public Func<RateLimitAdmission?>? RateLimitAdmissionCallbackRequest { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Encoding for sending/receiving data
|
||||
/// </summary>
|
||||
|
||||
@@ -36,6 +36,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
private readonly Dictionary<string, IWindowTracker> _trackers;
|
||||
private readonly RateLimitWindowType _windowType;
|
||||
private readonly double? _decayRate;
|
||||
private readonly TimeSpan _safetyMargin;
|
||||
private readonly int? _connectionWeight;
|
||||
private readonly Func<RequestDefinition, string?, string> _keySelector;
|
||||
private readonly SemaphoreSlim? _sharedGuardSemaphore;
|
||||
@@ -54,6 +55,10 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// The time period for the limit
|
||||
/// </summary>
|
||||
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>
|
||||
/// Whether this guard is shared between multiple gates
|
||||
@@ -68,11 +73,12 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// <param name="limit">Limit per period</param>
|
||||
/// <param name="timeSpan">Timespan for the period</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="connectionWeight">The weight of a new connection</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)
|
||||
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight, shared)
|
||||
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)
|
||||
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight, shared, safetyMargin)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -84,10 +90,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// <param name="limit">Limit per period</param>
|
||||
/// <param name="timeSpan">Timespan for the period</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="connectionWeight">The weight of a new connection</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)
|
||||
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)
|
||||
{
|
||||
_filters = filters;
|
||||
_trackers = new Dictionary<string, IWindowTracker>();
|
||||
@@ -97,6 +104,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
SharedGuard = shared;
|
||||
_keySelector = keySelector;
|
||||
_decayRate = decayPerTimeSpan;
|
||||
_safetyMargin = safetyMargin ?? WindowTrackerHelpers.GetDefaultSafetyMargin(timeSpan); ;
|
||||
_connectionWeight = connectionWeight;
|
||||
|
||||
if (SharedGuard)
|
||||
@@ -104,7 +112,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix)
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix, double allowedRateRatio)
|
||||
{
|
||||
foreach (var filter in _filters)
|
||||
{
|
||||
@@ -127,7 +135,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
_trackers.Add(key, tracker);
|
||||
}
|
||||
|
||||
var delay = tracker.GetWaitTime(requestWeight);
|
||||
var delay = tracker.GetWaitTime(requestWeight, allowedRateRatio);
|
||||
if (delay == default)
|
||||
return LimitCheck.NotNeeded(Limit, TimeSpan, tracker.Current);
|
||||
|
||||
@@ -205,9 +213,9 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// <returns></returns>
|
||||
protected IWindowTracker CreateTracker()
|
||||
{
|
||||
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(Limit, TimeSpan)
|
||||
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(Limit, TimeSpan)
|
||||
: _windowType == RateLimitWindowType.FixedAfterFirst ? new FixedAfterStartWindowTracker(Limit, TimeSpan) :
|
||||
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(Limit, TimeSpan, _safetyMargin)
|
||||
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(Limit, TimeSpan, _safetyMargin)
|
||||
: _windowType == RateLimitWindowType.FixedAfterFirst ? new FixedAfterStartWindowTracker(Limit, TimeSpan, _safetyMargin) :
|
||||
new DecayWindowTracker(Limit, TimeSpan, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix)
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix, double allowedRateRatio)
|
||||
{
|
||||
if (type != Type)
|
||||
return LimitCheck.NotApplicable;
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
private readonly Dictionary<string, IWindowTracker> _trackers;
|
||||
private readonly RateLimitWindowType _windowType;
|
||||
private readonly double? _decayRate;
|
||||
private readonly TimeSpan _safetyMargin;
|
||||
private readonly int _limit;
|
||||
private readonly TimeSpan _period;
|
||||
private readonly Func<RequestDefinition, string?, string> _keySelector;
|
||||
@@ -32,7 +33,12 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
public string Name => "EndpointLimitGuard";
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Description => $"Limit requests to endpoint";
|
||||
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>
|
||||
/// ctor
|
||||
@@ -41,19 +47,21 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
int limit,
|
||||
TimeSpan period,
|
||||
RateLimitWindowType windowType,
|
||||
double? decayRate = null,
|
||||
Func<RequestDefinition, string?, string>? keySelector = null)
|
||||
TimeSpan? safetyMargin = null,
|
||||
Func<RequestDefinition, string?, string>? keySelector = null,
|
||||
double? decayRate = null)
|
||||
{
|
||||
_limit = limit;
|
||||
_period = period;
|
||||
_windowType = windowType;
|
||||
_decayRate = decayRate;
|
||||
_safetyMargin = safetyMargin ?? WindowTrackerHelpers.GetDefaultSafetyMargin(period);
|
||||
_keySelector = keySelector ?? Default;
|
||||
_trackers = new Dictionary<string, IWindowTracker>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix)
|
||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix, double allowedRateRatio)
|
||||
{
|
||||
var key = _keySelector(definition, apiKey) + keySuffix;
|
||||
if (!_trackers.TryGetValue(key, out var tracker))
|
||||
@@ -62,7 +70,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
_trackers.Add(key, tracker);
|
||||
}
|
||||
|
||||
var delay = tracker.GetWaitTime(requestWeight);
|
||||
var delay = tracker.GetWaitTime(requestWeight, allowedRateRatio);
|
||||
if (delay == default)
|
||||
return LimitCheck.NotNeeded(_limit, _period, tracker.Current);
|
||||
|
||||
@@ -84,8 +92,8 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// <returns></returns>
|
||||
protected IWindowTracker CreateTracker()
|
||||
{
|
||||
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(_limit, _period)
|
||||
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(_limit, _period) :
|
||||
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(_limit, _period, _safetyMargin)
|
||||
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(_limit, _period, _safetyMargin) :
|
||||
new DecayWindowTracker(_limit, _period, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
|
||||
}
|
||||
|
||||
|
||||
@@ -53,9 +53,20 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
/// <param name="requestWeight">Request weight</param>
|
||||
/// <param name="behaviour">Behaviour when rate limit is hit</param>
|
||||
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
|
||||
/// <param name="ct">Cancelation token</param>
|
||||
/// <param name="allowedRateRatio">The allowed rate ratio</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
|
||||
ValueTask<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct);
|
||||
ValueTask<CallResult> ProcessAsync(
|
||||
ILogger logger,
|
||||
int itemId,
|
||||
RateLimitItemType type,
|
||||
RequestDefinition definition,
|
||||
string? apiKey,
|
||||
int requestWeight,
|
||||
RateLimitingBehaviour behaviour,
|
||||
string? keySuffix,
|
||||
double allowedRateRatio,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Enforces the rate limit as defined in the request definition. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail
|
||||
@@ -69,9 +80,21 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
/// <param name="behaviour">Behaviour when rate limit is hit</param>
|
||||
/// <param name="requestWeight">The weight to apply to the limit guard</param>
|
||||
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
|
||||
/// <param name="allowedRateRatio">The allowed rate ratio</param>
|
||||
/// <param name="ct">Cancelation token</param>
|
||||
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
|
||||
ValueTask<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct);
|
||||
ValueTask<CallResult> ProcessSingleAsync(
|
||||
ILogger logger,
|
||||
int itemId,
|
||||
IRateLimitGuard guard,
|
||||
RateLimitItemType type,
|
||||
RequestDefinition definition,
|
||||
string? apiKey,
|
||||
int requestWeight,
|
||||
RateLimitingBehaviour behaviour,
|
||||
string? keySuffix,
|
||||
double allowedRateRatio,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Reset the limit for the specified parameters
|
||||
|
||||
@@ -25,8 +25,9 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
/// <param name="apiKey">The API key</param>
|
||||
/// <param name="requestWeight">The request weight</param>
|
||||
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
|
||||
/// <param name="allowedRateRatio">The allowed rate ratio</param>
|
||||
/// <returns></returns>
|
||||
LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix);
|
||||
LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix, double allowedRateRatio);
|
||||
|
||||
/// <summary>
|
||||
/// Apply the request to this guard with the specified weight
|
||||
|
||||
@@ -22,9 +22,7 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
/// <summary>
|
||||
/// Get the time to wait to fit the weight
|
||||
/// </summary>
|
||||
/// <param name="weight"></param>
|
||||
/// <returns></returns>
|
||||
TimeSpan GetWaitTime(int weight);
|
||||
TimeSpan GetWaitTime(int weight, double allowedRateRatio);
|
||||
/// <summary>
|
||||
/// Register the weight in this window
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting
|
||||
{
|
||||
/// <summary>
|
||||
/// Rate limit admission decision result
|
||||
/// </summary>
|
||||
public record RateLimitAdmission
|
||||
{
|
||||
/// <summary>
|
||||
/// Value ratio between 0 and 1
|
||||
/// </summary>
|
||||
public double MaxUtilizationRatio { get; }
|
||||
|
||||
private RateLimitAdmission(double maxUtilizationValue) {
|
||||
if (maxUtilizationValue <= 0 || maxUtilizationValue > 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(maxUtilizationValue), "Max utilization value must be bigger than 0 and less than or equal to 1");
|
||||
|
||||
MaxUtilizationRatio = maxUtilizationValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Only allow the request when below a certain ratio of the rate limit, for example 0.5 means it can use a max of 50% of the rate limit,
|
||||
/// 1 means it's allowed to use the full rate limit.
|
||||
/// </summary>
|
||||
/// <param name="value">0.5 means a max use 50% of the rate limit, 1 means the request is allowed to use the full rate limit</param>
|
||||
public static RateLimitAdmission WithMaxUtilizationRatio(double value)
|
||||
=> new RateLimitAdmission(value);
|
||||
}
|
||||
}
|
||||
@@ -37,14 +37,24 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct)
|
||||
public async ValueTask<CallResult> ProcessAsync(
|
||||
ILogger logger,
|
||||
int itemId,
|
||||
RateLimitItemType type,
|
||||
RequestDefinition definition,
|
||||
string? apiKey,
|
||||
int requestWeight,
|
||||
RateLimitingBehaviour rateLimitingBehaviour,
|
||||
string? keySuffix,
|
||||
double allowedRateRatio,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||
bool release = true;
|
||||
_waitingCount++;
|
||||
try
|
||||
{
|
||||
return await CheckGuardsAsync(_guards, logger, itemId, type, definition, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
|
||||
return await CheckGuardsAsync(_guards, logger, itemId, type, definition, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, allowedRateRatio, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (TaskCanceledException tce)
|
||||
{
|
||||
@@ -71,6 +81,7 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
int requestWeight,
|
||||
RateLimitingBehaviour rateLimitingBehaviour,
|
||||
string? keySuffix,
|
||||
double allowedRateRatio,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||
@@ -78,7 +89,7 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
_waitingCount++;
|
||||
try
|
||||
{
|
||||
return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
|
||||
return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, allowedRateRatio, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (TaskCanceledException tce)
|
||||
{
|
||||
@@ -94,12 +105,23 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask<CallResult> CheckGuardsAsync(IEnumerable<IRateLimitGuard> guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct)
|
||||
private async ValueTask<CallResult> CheckGuardsAsync(
|
||||
IEnumerable<IRateLimitGuard> guards,
|
||||
ILogger logger,
|
||||
int itemId,
|
||||
RateLimitItemType type,
|
||||
RequestDefinition definition,
|
||||
string? apiKey,
|
||||
int requestWeight,
|
||||
RateLimitingBehaviour rateLimitingBehaviour,
|
||||
string? keySuffix,
|
||||
double allowedRateRatio,
|
||||
CancellationToken ct)
|
||||
{
|
||||
foreach (var guard in guards)
|
||||
{
|
||||
// Check if a wait is needed for this guard
|
||||
var result = guard.Check(type, definition, apiKey, requestWeight, keySuffix);
|
||||
var result = guard.Check(type, definition, apiKey, requestWeight, keySuffix, allowedRateRatio);
|
||||
if (result.Delay != TimeSpan.Zero && rateLimitingBehaviour == RateLimitingBehaviour.Fail)
|
||||
{
|
||||
// Delay is needed and limit behaviour is to fail the request
|
||||
@@ -109,7 +131,10 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
logger.RateLimitRequestFailed(itemId, definition.Path, guard.Name, guard.Description);
|
||||
|
||||
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
||||
return CallResult.Fail(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}"));
|
||||
var errorMsg = $"Rate limit check failed on guard {guard.Name}; {guard.Description}";
|
||||
if (allowedRateRatio < 1)
|
||||
errorMsg += $" with allowed ratio {allowedRateRatio}";
|
||||
return CallResult.Fail(new ClientRateLimitError(errorMsg));
|
||||
}
|
||||
|
||||
if (result.Delay != TimeSpan.Zero)
|
||||
@@ -117,7 +142,14 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
// Delay is needed and limit behaviour is to wait for the request to be under the limit
|
||||
_semaphore.Release();
|
||||
|
||||
var description = result.Limit == null ? guard.Description : $"{guard.Description}, Request weight: {requestWeight}, Current: {result.Current}, Limit: {result.Limit}, requests now being limited: {_waitingCount}";
|
||||
var description =
|
||||
result.Limit == null
|
||||
? guard.Description
|
||||
: $"{guard.Description}, Request weight: {requestWeight}," +
|
||||
$" Current: {result.Current}" +
|
||||
$", Limit: {result.Limit}" +
|
||||
(allowedRateRatio < 1 ? $", Allowed ratio: {allowedRateRatio}" : "") +
|
||||
$", requests now being limited: {_waitingCount}";
|
||||
if (type == RateLimitItemType.Connection)
|
||||
logger.RateLimitDelayingConnection(itemId, result.Delay, guard.Name, description);
|
||||
else
|
||||
@@ -126,7 +158,7 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
||||
await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false);
|
||||
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||
return await CheckGuardsAsync(guards, logger, itemId, type, definition, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
|
||||
return await CheckGuardsAsync(guards, logger, itemId, type, definition, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, allowedRateRatio, ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,18 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
|
||||
private int _currentWeight = 0;
|
||||
private DateTime _lastDecrease = DateTime.UtcNow;
|
||||
private double _decayProgress;
|
||||
|
||||
public DecayWindowTracker(int limit, TimeSpan period, double decayRate)
|
||||
public DecayWindowTracker(
|
||||
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;
|
||||
TimePeriod = period;
|
||||
DecreaseRate = decayRate;
|
||||
@@ -31,73 +40,118 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
{
|
||||
if (amount == null)
|
||||
{
|
||||
_lastDecrease = DateTime.UtcNow;
|
||||
_currentWeight = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
|
||||
ResetState();
|
||||
return;
|
||||
}
|
||||
|
||||
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
|
||||
if (_currentWeight == 0)
|
||||
ResetState();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan GetWaitTime(int weight)
|
||||
public TimeSpan GetWaitTime(int weight, double allowedRateRatio)
|
||||
{
|
||||
// Decrease the counter based on the last update time and decay rate
|
||||
DecreaseCounter(DateTime.UtcNow);
|
||||
var now = DateTime.UtcNow;
|
||||
DecreaseCounter(now);
|
||||
|
||||
if (Current + weight > Limit)
|
||||
if ((Current + weight) / (double)Limit <= allowedRateRatio)
|
||||
return TimeSpan.Zero;
|
||||
|
||||
if (Current == 0)
|
||||
{
|
||||
// The weight would cause the rate limit to be passed
|
||||
if (Current == 0)
|
||||
if (allowedRateRatio < 1)
|
||||
{
|
||||
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(
|
||||
"Request limit reached max utilization. " +
|
||||
"This request can never execute with the current rate limiter configuration. " +
|
||||
$"Request weight: {weight}, RateLimit: {Limit}, " +
|
||||
$"Request ratio: {(Current + weight) / (double)Limit}, " +
|
||||
$"AllowedRateRatio: {allowedRateRatio}");
|
||||
}
|
||||
|
||||
// Determine the time to wait before this weight can be applied without going over the rate limit
|
||||
return DetermineWaitTime(weight);
|
||||
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}");
|
||||
}
|
||||
|
||||
// Weight can fit without going over limit
|
||||
return TimeSpan.Zero;
|
||||
return DetermineWaitTime(
|
||||
weight,
|
||||
allowedRateRatio);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ApplyWeight(int weight)
|
||||
{
|
||||
if (_currentWeight == 0)
|
||||
{
|
||||
_lastDecrease = DateTime.UtcNow;
|
||||
_decayProgress = 0;
|
||||
}
|
||||
|
||||
_currentWeight += weight;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
private void DecreaseCounter(DateTime now)
|
||||
{
|
||||
var dif = (time - _lastDecrease).TotalMilliseconds / TimePeriod.TotalMilliseconds * DecreaseRate;
|
||||
var decrease = (int)Math.Floor(dif);
|
||||
if (decrease >= 1)
|
||||
if (_currentWeight == 0)
|
||||
{
|
||||
_currentWeight = Math.Max(0, _currentWeight - (int)Math.Floor(dif));
|
||||
_lastDecrease = time;
|
||||
_lastDecrease = now;
|
||||
_decayProgress = 0;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine the time to wait before the weight would fit
|
||||
/// </summary>
|
||||
/// <param name="requestWeight"></param>
|
||||
/// <returns></returns>
|
||||
private TimeSpan DetermineWaitTime(int requestWeight)
|
||||
private TimeSpan DetermineWaitTime(
|
||||
int requestWeight,
|
||||
double allowedRateRatio)
|
||||
{
|
||||
var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0);
|
||||
var result = TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds);
|
||||
if (result < TimeSpan.Zero)
|
||||
return TimeSpan.Zero;
|
||||
return result;
|
||||
var weightToRemove = Current + requestWeight - Limit * allowedRateRatio;
|
||||
|
||||
// The counter is integer-valued, so enough whole weight units
|
||||
// must decay before the request can be admitted.
|
||||
var requiredDecay = Math.Ceiling(weightToRemove);
|
||||
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,15 +17,18 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
private int _currentWeight = 0;
|
||||
private DateTime? _nextReset;
|
||||
|
||||
/// <summary>
|
||||
/// Additional wait time to apply to account for time offset between server and client
|
||||
/// </summary>
|
||||
private static TimeSpan _fixedWindowBuffer = TimeSpan.FromMilliseconds(1000);
|
||||
private readonly TimeSpan _safetyMargin;
|
||||
|
||||
public FixedAfterStartWindowTracker(int limit, TimeSpan period)
|
||||
: this(limit, period, WindowTrackerHelpers.GetDefaultSafetyMargin(period))
|
||||
{
|
||||
}
|
||||
|
||||
public FixedAfterStartWindowTracker(int limit, TimeSpan period, TimeSpan safetyMargin)
|
||||
{
|
||||
Limit = limit;
|
||||
TimePeriod = period;
|
||||
_safetyMargin = safetyMargin;
|
||||
_entries = new Queue<LimitEntry>();
|
||||
}
|
||||
|
||||
@@ -53,23 +56,32 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
}
|
||||
}
|
||||
|
||||
public TimeSpan GetWaitTime(int weight)
|
||||
public TimeSpan GetWaitTime(int weight, double allowedRateRatio)
|
||||
{
|
||||
// Remove requests no longer in time period from the history
|
||||
var checkTime = DateTime.UtcNow;
|
||||
if (_nextReset != null && checkTime > _nextReset)
|
||||
if (_nextReset != null && checkTime >= _nextReset + _safetyMargin)
|
||||
RemoveBefore(_nextReset.Value);
|
||||
|
||||
if (Current == 0)
|
||||
_nextReset = null;
|
||||
|
||||
if (Current + weight > Limit)
|
||||
if ((Current + weight) / (double)Limit > allowedRateRatio)
|
||||
{
|
||||
// The weight would cause the rate limit to be passed
|
||||
if (Current == 0)
|
||||
{
|
||||
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}");
|
||||
if (allowedRateRatio < 1)
|
||||
{
|
||||
throw new Exception("Request limit reached max utilization. " +
|
||||
$"This request can never execute with the current rate limiter configuration. Request weight: {weight}, RateLimit: {Limit}, " +
|
||||
$"Request ratio: {(Current + weight) / (double)Limit}, 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}");
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the time to wait before this weight can be applied without going over the rate limit
|
||||
@@ -121,7 +133,7 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
private TimeSpan DetermineWaitTime()
|
||||
{
|
||||
var checkTime = DateTime.UtcNow;
|
||||
var result = (_nextReset!.Value - checkTime) + _fixedWindowBuffer;
|
||||
var result = _nextReset!.Value + _safetyMargin - checkTime;
|
||||
if (result < TimeSpan.Zero)
|
||||
return TimeSpan.Zero;
|
||||
return result;
|
||||
|
||||
@@ -16,15 +16,18 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
private readonly Queue<LimitEntry> _entries;
|
||||
private int _currentWeight = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Additional wait time to apply to account for time offset between server and client
|
||||
/// </summary>
|
||||
private static readonly TimeSpan _fixedWindowBuffer = TimeSpan.FromMilliseconds(1000);
|
||||
private readonly TimeSpan _safetyMargin;
|
||||
|
||||
public FixedWindowTracker(int limit, TimeSpan period)
|
||||
: this(limit, period, WindowTrackerHelpers.GetDefaultSafetyMargin(period))
|
||||
{
|
||||
}
|
||||
|
||||
public FixedWindowTracker(int limit, TimeSpan period, TimeSpan safetyMargin)
|
||||
{
|
||||
Limit = limit;
|
||||
TimePeriod = period;
|
||||
_safetyMargin = safetyMargin;
|
||||
_entries = new Queue<LimitEntry>();
|
||||
}
|
||||
|
||||
@@ -52,19 +55,32 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan GetWaitTime(int weight)
|
||||
public TimeSpan GetWaitTime(int weight, double allowedRateRatio)
|
||||
{
|
||||
// Remove requests no longer in time period from the history
|
||||
var checkTime = DateTime.UtcNow;
|
||||
RemoveBefore(checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks)));
|
||||
var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks));
|
||||
var resetBoundary = checkTime >= startCurrentWindow + _safetyMargin
|
||||
? startCurrentWindow
|
||||
: startCurrentWindow - TimePeriod;
|
||||
RemoveBefore(resetBoundary);
|
||||
|
||||
if (Current + weight > Limit)
|
||||
if ((Current + weight) / (double)Limit > allowedRateRatio)
|
||||
{
|
||||
// The weight would cause the rate limit to be passed
|
||||
if (Current == 0)
|
||||
{
|
||||
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}");
|
||||
if (allowedRateRatio < 1)
|
||||
{
|
||||
throw new Exception("Request limit reached max utilization. " +
|
||||
$"This request can never execute with the current rate limiter configuration. Request weight: {weight}, RateLimit: {Limit}, " +
|
||||
$"Request ratio: {(Current + weight) / (double)Limit}, 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}");
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the time to wait before this weight can be applied without going over the rate limit
|
||||
@@ -115,8 +131,9 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
{
|
||||
var checkTime = DateTime.UtcNow;
|
||||
var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks));
|
||||
var wait = startCurrentWindow.Add(TimePeriod) - checkTime;
|
||||
var result = wait.Add(_fixedWindowBuffer);
|
||||
var result = checkTime < startCurrentWindow + _safetyMargin
|
||||
? startCurrentWindow + _safetyMargin - checkTime
|
||||
: startCurrentWindow + TimePeriod + _safetyMargin - checkTime;
|
||||
if (result < TimeSpan.Zero)
|
||||
return TimeSpan.Zero;
|
||||
return result;
|
||||
|
||||
@@ -16,15 +16,18 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
private readonly List<LimitEntry> _entries;
|
||||
private int _currentWeight = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Additional wait time to apply to account for fluctuating request times
|
||||
/// </summary>
|
||||
private static readonly TimeSpan _slidingWindowBuffer = TimeSpan.FromMilliseconds(1000);
|
||||
private readonly TimeSpan _safetyMargin;
|
||||
|
||||
public SlidingWindowTracker(int limit, TimeSpan period)
|
||||
: this(limit, period, WindowTrackerHelpers.GetDefaultSafetyMargin(period))
|
||||
{
|
||||
}
|
||||
|
||||
public SlidingWindowTracker(int limit, TimeSpan period, TimeSpan safetyMargin)
|
||||
{
|
||||
Limit = limit;
|
||||
TimePeriod = period;
|
||||
_safetyMargin = safetyMargin;
|
||||
_entries = new List<LimitEntry>();
|
||||
}
|
||||
|
||||
@@ -53,22 +56,31 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TimeSpan GetWaitTime(int weight)
|
||||
public TimeSpan GetWaitTime(int weight, double allowedRateRatio)
|
||||
{
|
||||
// Remove requests no longer in time period from the history
|
||||
RemoveBefore(DateTime.UtcNow - TimePeriod);
|
||||
RemoveBefore(DateTime.UtcNow - TimePeriod - _safetyMargin);
|
||||
|
||||
if (Current + weight > Limit)
|
||||
if ((Current + weight) / (double)Limit > allowedRateRatio)
|
||||
{
|
||||
// The weight would cause the rate limit to be passed
|
||||
if (Current == 0)
|
||||
{
|
||||
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}");
|
||||
if (allowedRateRatio < 1)
|
||||
{
|
||||
throw new Exception("Request limit reached max utilization. " +
|
||||
$"This request can never execute with the current rate limiter configuration. Request weight: {weight}, RateLimit: {Limit}, " +
|
||||
$"Request ratio: {(Current + weight) / (double)Limit}, 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}");
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the time to wait before this weight can be applied without going over the rate limit
|
||||
return DetermineWaitTime(weight);
|
||||
return DetermineWaitTime(weight, allowedRateRatio);
|
||||
}
|
||||
|
||||
// Weight can fit without going over limit
|
||||
@@ -90,7 +102,7 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
{
|
||||
for (var i = 0; i < _entries.Count; i++)
|
||||
{
|
||||
if (_entries[i].Timestamp < time)
|
||||
if (_entries[i].Timestamp <= time)
|
||||
{
|
||||
var entry = _entries[i];
|
||||
_entries.Remove(entry);
|
||||
@@ -108,9 +120,9 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
/// Determine the time to wait before the weight would fit
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private TimeSpan DetermineWaitTime(int requestWeight)
|
||||
private TimeSpan DetermineWaitTime(int requestWeight, double allowedRateRatio)
|
||||
{
|
||||
var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0);
|
||||
var weightToRemove = Math.Max(Current + requestWeight - Limit * allowedRateRatio, 0);
|
||||
var removedWeight = 0;
|
||||
for (var i = 0; i < _entries.Count; i++)
|
||||
{
|
||||
@@ -118,7 +130,7 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
||||
removedWeight += entry.Weight;
|
||||
if (removedWeight >= weightToRemove)
|
||||
{
|
||||
var result = entry.Timestamp + TimePeriod + _slidingWindowBuffer - DateTime.UtcNow;
|
||||
var result = entry.Timestamp + TimePeriod + _safetyMargin - DateTime.UtcNow;
|
||||
if (result < TimeSpan.Zero)
|
||||
return TimeSpan.Zero;
|
||||
return result;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,10 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Get user funding history capability.
|
||||
/// </summary>
|
||||
public static SharedRestCapability<IGetUserFundingHistory, IGetUserFundingHistoryRest> GetUserFundingHistory { get; } = new();
|
||||
/// <summary>
|
||||
/// Subscribe to funding info updates capability.
|
||||
/// </summary>
|
||||
public static SharedCapabilityReference<ISubscribeFundingInfoSocket> SubscribeFundingInfo { get; } = new();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,16 @@ namespace CryptoExchange.Net.SharedApis
|
||||
public class SharedCapabilityResolution<T>
|
||||
where T : ISharedApiCapability
|
||||
{
|
||||
/// <summary>
|
||||
/// The exchange name
|
||||
/// </summary>
|
||||
public string Exchange => Capability.Exchange;
|
||||
|
||||
/// <summary>
|
||||
/// The transport type
|
||||
/// </summary>
|
||||
public SharedTransport Transport => Capability.Transport;
|
||||
|
||||
/// <summary>
|
||||
/// The capability
|
||||
/// </summary>
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// References for a capability available through REST.
|
||||
/// </summary>
|
||||
public sealed class SharedRestCapability<TCapability, TRest>
|
||||
public class SharedRestCapability<TCapability, TRest>
|
||||
: SharedCapabilityReference<TCapability>
|
||||
where TCapability : ISharedApiCapability
|
||||
where TRest : TCapability, ISharedRest
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// References for a capability available through REST and WebSocket.
|
||||
/// </summary>
|
||||
public sealed class SharedRestSocketCapability<TCapability, TRest, TSocket>
|
||||
public class SharedRestSocketCapability<TCapability, TRest, TSocket>
|
||||
: SharedCapabilityReference<TCapability>
|
||||
where TCapability : ISharedApiCapability
|
||||
where TRest : TCapability, ISharedRest
|
||||
|
||||
@@ -7,15 +7,15 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// The type of transport used for the shared API
|
||||
/// </summary>
|
||||
public record SharedTransport(string Name)
|
||||
public enum SharedTransport
|
||||
{
|
||||
/// <summary>
|
||||
/// REST transport
|
||||
/// </summary>
|
||||
public static SharedTransport Rest { get; } = new("REST");
|
||||
Rest,
|
||||
/// <summary>
|
||||
/// WebSocket transport
|
||||
/// </summary>
|
||||
public static SharedTransport Socket { get; } = new("WebSocket");
|
||||
Socket
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Discovery information for a Shared API client.
|
||||
/// </summary>
|
||||
public class SharedApiClientInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Preferred transport when multiple implementations of a capability are available.
|
||||
/// </summary>
|
||||
public SharedTransport PreferredTransport { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Shared APIs available on the client.
|
||||
/// </summary>
|
||||
public SharedClientInfo[] SharedApis { get; }
|
||||
|
||||
internal SharedApiClientInfo(
|
||||
SharedTransport preferredTransport,
|
||||
SharedClientInfo[] sharedApis)
|
||||
{
|
||||
PreferredTransport = preferredTransport;
|
||||
SharedApis = sharedApis;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => ToString(false);
|
||||
|
||||
/// <summary>
|
||||
/// Create a string representation of the available Shared APIs.
|
||||
/// </summary>
|
||||
/// <param name="detailed">Whether to include detailed capability information.</param>
|
||||
public string ToString(bool detailed)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine($"Preferred transport: {PreferredTransport}");
|
||||
|
||||
foreach (var sharedApi in SharedApis)
|
||||
{
|
||||
builder.AppendLine();
|
||||
builder.Append(sharedApi.ToString(detailed));
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,14 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
public CentralizationType CentralizationType { get; set; }
|
||||
/// <summary>
|
||||
/// The underlying transport type used by this client
|
||||
/// </summary>
|
||||
public SharedTransport Transport { get; init; }
|
||||
/// <summary>
|
||||
/// Whether API credentials have been configured for this client. Does not check whether the credentials are valid.
|
||||
/// </summary>
|
||||
public bool Authenticated { get; init; }
|
||||
/// <summary>
|
||||
/// Use Capabilities instead
|
||||
/// </summary>
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
@@ -74,6 +82,8 @@ namespace CryptoExchange.Net.SharedApis
|
||||
sb.AppendLine($"Supported environments: {string.Join(", ", SupportedEnvironments)}");
|
||||
sb.AppendLine($"Supported trading modes: {string.Join(", ", SupportedTradingModes)}");
|
||||
sb.AppendLine($"Centralization type: {CentralizationType}");
|
||||
sb.AppendLine($"Transport: {Transport}");
|
||||
sb.AppendLine($"Authenticated: {Authenticated}");
|
||||
sb.AppendLine($"Capabilities:");
|
||||
foreach (var capability in Capabilities)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Order book info
|
||||
/// </summary>
|
||||
public record SharedIncrementalOrderBook : SharedOrderBook
|
||||
{
|
||||
/// <summary>
|
||||
/// The sequence number of the first book update in this update.
|
||||
/// </summary>
|
||||
public long? StartSequenceNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SharedIncrementalOrderBook(SharedQuantityType quantityType, long? startSequenceNumber, long? endSequenceNumber, ISymbolOrderBookEntry[] asks, ISymbolOrderBookEntry[] bids)
|
||||
:base(quantityType, endSequenceNumber, asks, bids)
|
||||
{
|
||||
StartSequenceNumber = startSequenceNumber;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for Shared API client instances
|
||||
/// </summary>
|
||||
public class SharedApiOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The preferred transport type when requesting transport agnostic capabilities
|
||||
/// </summary>
|
||||
public SharedTransport PreferredTransport { get; set; }
|
||||
= SharedTransport.Rest;
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[{ValueType.Name}] \"{Name}\" {string.Join(" / ", Aliases.Select(x => $"\"{x}\""))}: {Description}";
|
||||
return $"[{(Nullable.GetUnderlyingType(ValueType) ?? ValueType).Name}] \"{Name}\" {string.Join(" / ", Aliases.Select(x => $"\"{x}\""))}: {Description}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[{Support}] [{ValueType.Name}] \"{Name}\" {string.Join(" / ", Aliases.Select(x => $"\"{x}\""))}: {Description}";
|
||||
return $"[{Support}] [{(Nullable.GetUnderlyingType(ValueType) ?? ValueType).Name}] \"{Name}\" {string.Join(" / ", Aliases.Select(x => $"\"{x}\""))}: {Description}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[{Requirement}] [{ValueType.Name}] \"{Name}\" {string.Join(" / ", Aliases.Select(x => $"\"{x}\""))}: {Description}";
|
||||
return $"[{Requirement}] [{(Nullable.GetUnderlyingType(ValueType) ?? ValueType).Name}] \"{Name}\" {string.Join(" / ", Aliases.Select(x => $"\"{x}\""))}: {Description}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.Interfaces.Clients;
|
||||
using CryptoExchange.Net.RateLimiting;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
@@ -12,7 +15,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
private readonly Func<string, string, TradingMode, DateTime?, string> _symbolFormatter;
|
||||
private readonly Func<bool> _authDelegate;
|
||||
|
||||
private readonly IBaseApiClient _apiClient;
|
||||
private IReadOnlyCollection<CapabilityOptions> _capabilities = Array.Empty<CapabilityOptions>();
|
||||
IReadOnlyCollection<CapabilityOptions> ISharedApi.Capabilities => _capabilities;
|
||||
|
||||
@@ -33,16 +36,17 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
public SharedApiBase(
|
||||
SharedTransport transport,
|
||||
string exchange,
|
||||
IBaseApiClient apiClient,
|
||||
TradingMode[] supportedTradingModes,
|
||||
Func<bool> authenticated,
|
||||
Func<string, string, TradingMode, DateTime?, string> formatSymbol)
|
||||
{
|
||||
Transport = transport;
|
||||
Exchange = exchange;
|
||||
SupportedTradingModes = supportedTradingModes;
|
||||
_apiClient = apiClient;
|
||||
_authDelegate = authenticated;
|
||||
_symbolFormatter = formatSymbol;
|
||||
Transport = transport;
|
||||
Exchange = apiClient.Exchange;
|
||||
SupportedTradingModes = supportedTradingModes;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -64,6 +68,12 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <inheritdoc />
|
||||
public void ResetDefaultExchangeParameters() => ExchangeParameters.ResetStaticExchangeParameters(Exchange);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<TResult> WithRateLimitAdmissionAsync<TResult>(
|
||||
RateLimitAdmission admission,
|
||||
Func<Task<TResult>> operation)
|
||||
=> _apiClient.WithRateLimitAdmissionAsync(admission, operation);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract SharedClientInfo Discover();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
internal interface ISharedApiClientResolver
|
||||
{
|
||||
object? GetCapability(Type capabilityType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base interface for Shared API clients
|
||||
/// </summary>
|
||||
@@ -14,6 +20,19 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Shared API's available on this client
|
||||
/// </summary>
|
||||
IReadOnlyList<ISharedApi> SharedApis { get; }
|
||||
/// <summary>
|
||||
/// Preferred transport selection when multiple transport options are available
|
||||
/// </summary>
|
||||
SharedTransport PreferredTransport { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get discovery information for the Shared APIs available on this client.
|
||||
/// </summary>
|
||||
SharedApiClientInfo Discover();
|
||||
/// <summary>
|
||||
/// The exchange name of this client
|
||||
/// </summary>
|
||||
string Exchange { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get all capabilities matching the provided capability type.
|
||||
@@ -26,6 +45,17 @@ namespace CryptoExchange.Net.SharedApis
|
||||
IReadOnlyList<SharedCapabilityResolution<T>> GetCapabilities<T>(TradingMode? tradingMode = null, SharedTransport? transport = null)
|
||||
where T : ISharedApiCapability;
|
||||
|
||||
/// <summary>
|
||||
/// Get all capabilities matching the provided capability reference.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The capability type.</typeparam>
|
||||
/// <param name="capability">The capability reference.</param>
|
||||
/// <param name="tradingMode">Filter by supported trading mode.</param>
|
||||
IReadOnlyList<SharedCapabilityResolution<T>> GetCapabilities<T>(
|
||||
SharedCapabilityReference<T> capability,
|
||||
TradingMode? tradingMode = null)
|
||||
where T : ISharedApiCapability;
|
||||
|
||||
/// <summary>
|
||||
/// Get the requested capability type. If more than one implementation is available the first one is returned.
|
||||
/// In this case prefer using <see cref="GetCapability{T}(TradingMode, SharedTransport?)"/> to select a specific trading mode.
|
||||
@@ -74,30 +104,48 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <returns>The requested capability type or null if not found</returns>
|
||||
SharedCapabilityResolution<T>? GetCapability<T>(SharedCapabilityReference<T> capability, TradingMode tradingMode)
|
||||
where T : ISharedApiCapability;
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes all subscriptions created by the underlying socket API
|
||||
/// client and closes its subscription connections.
|
||||
/// </summary>
|
||||
Task UnsubscribeAllAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base client for Shared API clients
|
||||
/// </summary>
|
||||
public abstract class SharedApiClientBase : ISharedApiClientBase
|
||||
public abstract class SharedApiClientBase : ISharedApiClientBase, ISharedApiClientResolver
|
||||
{
|
||||
private readonly ISharedApi[] _sharedApis;
|
||||
private readonly SharedTransport[] _preferredTransports;
|
||||
private readonly SharedTransport _preferredTransport;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<ISharedApi> SharedApis => _sharedApis;
|
||||
/// <inheritdoc />
|
||||
public SharedTransport PreferredTransport => _preferredTransport;
|
||||
/// <inheritdoc />
|
||||
public string Exchange => _sharedApis[0].Exchange;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SharedApiClientBase(
|
||||
IEnumerable<SharedTransport> transportPreference,
|
||||
SharedTransport transportPreference,
|
||||
params ISharedApi[] sharedApis)
|
||||
{
|
||||
_preferredTransports = transportPreference.Distinct().ToArray();
|
||||
_preferredTransport = transportPreference;
|
||||
_sharedApis = sharedApis.Distinct().ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public SharedApiClientInfo Discover()
|
||||
{
|
||||
return new SharedApiClientInfo(
|
||||
PreferredTransport,
|
||||
_sharedApis.Select(x => x.Discover()).ToArray());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public SharedCapabilityResolution<T>? GetCapability<T>(SharedTransport? transport = null)
|
||||
where T : ISharedApiCapability
|
||||
@@ -126,6 +174,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
return GetCapabilityCore<T>(tradingMode, null);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<SharedCapabilityResolution<T>> GetCapabilities<T>(
|
||||
TradingMode? tradingMode = null,
|
||||
@@ -157,6 +206,38 @@ namespace CryptoExchange.Net.SharedApis
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<SharedCapabilityResolution<T>> GetCapabilities<T>(
|
||||
SharedCapabilityReference<T> capability,
|
||||
TradingMode? tradingMode = null)
|
||||
where T : ISharedApiCapability
|
||||
{
|
||||
return GetCapabilities<T>(tradingMode);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UnsubscribeAllAsync()
|
||||
{
|
||||
return Task.WhenAll(
|
||||
_sharedApis
|
||||
.OfType<ISharedSubscription>()
|
||||
.Select(x => x.UnsubscribeAllAsync()));
|
||||
}
|
||||
|
||||
object? ISharedApiClientResolver.GetCapability(Type capabilityType)
|
||||
{
|
||||
return _sharedApis
|
||||
.Where(sharedApi =>
|
||||
capabilityType.IsInstanceOfType(sharedApi)
|
||||
&& GetMatchingOptions(
|
||||
sharedApi,
|
||||
capabilityType,
|
||||
null) != null)
|
||||
.OrderBy(sharedApi =>
|
||||
GetTransportPriority(sharedApi.Transport))
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private SharedCapabilityResolution<T>? GetCapabilityCore<T>(
|
||||
TradingMode? tradingMode,
|
||||
SharedTransport? selectedTransport = null)
|
||||
@@ -171,8 +252,17 @@ namespace CryptoExchange.Net.SharedApis
|
||||
TradingMode? tradingMode)
|
||||
where T : ISharedApiCapability
|
||||
{
|
||||
var requestedType = typeof(T);
|
||||
return GetMatchingOptions(
|
||||
sharedApi,
|
||||
typeof(T),
|
||||
tradingMode);
|
||||
}
|
||||
|
||||
private static CapabilityOptions? GetMatchingOptions(
|
||||
ISharedApi sharedApi,
|
||||
Type requestedType,
|
||||
TradingMode? tradingMode)
|
||||
{
|
||||
return sharedApi.Capabilities
|
||||
.Where(options =>
|
||||
options.Supported
|
||||
@@ -182,7 +272,6 @@ namespace CryptoExchange.Net.SharedApis
|
||||
&& (tradingMode == null
|
||||
|| options.SupportedTradingModes.Contains(
|
||||
tradingMode.Value)))
|
||||
// Prefer options declared for the exact requested interface.
|
||||
.OrderByDescending(options =>
|
||||
options.CapabilityType == requestedType)
|
||||
.FirstOrDefault();
|
||||
@@ -198,8 +287,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
|
||||
private int GetTransportPriority(SharedTransport transport)
|
||||
{
|
||||
var index = Array.IndexOf(_preferredTransports, transport);
|
||||
return index == -1 ? int.MaxValue : index;
|
||||
return transport == _preferredTransport ? 0 : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared API dependency injection registration builder
|
||||
/// </summary>
|
||||
/// <typeparam name="TSharedApiClient"></typeparam>
|
||||
public class SharedApiClientRegistrationBuilder<TSharedApiClient>
|
||||
where TSharedApiClient : class, ISharedApiClientBase
|
||||
{
|
||||
private readonly IServiceCollection _services;
|
||||
private readonly HashSet<Type> _baseCapabilityTypes = new();
|
||||
|
||||
internal SharedApiClientRegistrationBuilder(
|
||||
IServiceCollection services)
|
||||
{
|
||||
_services = services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a shared API
|
||||
/// </summary>
|
||||
public SharedApiClientRegistrationBuilder<TSharedApiClient> Add<
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(
|
||||
DynamicallyAccessedMemberTypes.Interfaces)]
|
||||
#endif
|
||||
TSharedApi>(
|
||||
Func<TSharedApiClient, TSharedApi> selector)
|
||||
where TSharedApi : class, ISharedApi
|
||||
{
|
||||
if (selector is null)
|
||||
throw new ArgumentNullException(nameof(selector));
|
||||
|
||||
_services.AddTransient<TSharedApi>(
|
||||
serviceProvider =>
|
||||
{
|
||||
var client = serviceProvider
|
||||
.GetRequiredService<TSharedApiClient>();
|
||||
|
||||
return selector(client);
|
||||
});
|
||||
|
||||
var interfaces = typeof(TSharedApi).GetInterfaces();
|
||||
|
||||
var transportCapabilities = interfaces
|
||||
.Where(type =>
|
||||
typeof(ISharedApiCapability).IsAssignableFrom(type)
|
||||
&& (typeof(ISharedRest).IsAssignableFrom(type)
|
||||
|| typeof(ISharedSocket).IsAssignableFrom(type))
|
||||
&& type != typeof(ISharedRest)
|
||||
&& type != typeof(ISharedSocket)
|
||||
&& type != typeof(ISharedSubscription))
|
||||
.Distinct();
|
||||
|
||||
foreach (var capabilityType in transportCapabilities)
|
||||
{
|
||||
_services.AddTransient(
|
||||
capabilityType,
|
||||
serviceProvider =>
|
||||
{
|
||||
var client = serviceProvider
|
||||
.GetRequiredService<TSharedApiClient>();
|
||||
|
||||
return selector(client);
|
||||
});
|
||||
}
|
||||
|
||||
var baseCapabilities = interfaces
|
||||
.Where(type =>
|
||||
type != typeof(ISharedApiCapability)
|
||||
&& typeof(ISharedApiCapability).IsAssignableFrom(type)
|
||||
&& !typeof(ISharedRest).IsAssignableFrom(type)
|
||||
&& !typeof(ISharedSocket).IsAssignableFrom(type));
|
||||
|
||||
foreach (var capabilityType in baseCapabilities)
|
||||
_baseCapabilityTypes.Add(capabilityType);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
internal void RegisterTransportAgnosticCapabilities()
|
||||
{
|
||||
foreach (var capabilityType in _baseCapabilityTypes)
|
||||
{
|
||||
_services.AddTransient(
|
||||
capabilityType,
|
||||
serviceProvider =>
|
||||
{
|
||||
var client = serviceProvider
|
||||
.GetRequiredService<TSharedApiClient>();
|
||||
|
||||
var resolver = (ISharedApiClientResolver)client;
|
||||
|
||||
return resolver.GetCapability(capabilityType)
|
||||
?? throw new InvalidOperationException(
|
||||
$"No implementation of " +
|
||||
$"{capabilityType.Name} is available on " +
|
||||
$"{typeof(TSharedApiClient).Name}");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
@@ -21,6 +27,8 @@ namespace CryptoExchange.Net.SharedApis
|
||||
SupportedEnvironments = platformInfo.SupportedEnvironments,
|
||||
SupportedTradingModes = client.SupportedTradingModes,
|
||||
CentralizationType = platformInfo.CentralizationType,
|
||||
Transport = client.Transport,
|
||||
Authenticated = client.Authenticated,
|
||||
Capabilities = client.Capabilities.Where(x => x.Supported).ToArray()
|
||||
};
|
||||
}
|
||||
@@ -43,5 +51,787 @@ namespace CryptoExchange.Net.SharedApis
|
||||
resultData = resultData.Where(x => x.QuoteAssetSubType == request.QuoteAssetSubType);
|
||||
return resultData.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register Shared API client in DI container
|
||||
/// </summary>
|
||||
public static IServiceCollection RegisterSharedApiClient<
|
||||
TSharedApiClient,
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors)]
|
||||
#endif
|
||||
TImplementation
|
||||
>(this IServiceCollection services, Action<SharedApiClientRegistrationBuilder<TSharedApiClient>> configure)
|
||||
where TImplementation : class, TSharedApiClient
|
||||
where TSharedApiClient : class, ISharedApiClientBase
|
||||
{
|
||||
services.AddTransient<TSharedApiClient, TImplementation>();
|
||||
services.AddTransient<ISharedApiClientBase>(
|
||||
serviceProvider =>
|
||||
serviceProvider.GetRequiredService<TSharedApiClient>());
|
||||
|
||||
var builder =
|
||||
new SharedApiClientRegistrationBuilder<TSharedApiClient>(services);
|
||||
|
||||
configure(builder);
|
||||
builder.RegisterTransportAgnosticCapabilities();
|
||||
|
||||
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>
|
||||
/// Execute GetTickerAsync for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<HttpResult<SharedTicker>> ExecuteAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<IGetTickerRest>> capabilities,
|
||||
GetTickerRequest request,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.GetTickerAsync(request, ct))
|
||||
.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>
|
||||
/// Execute GetAllTickersAsync for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<HttpResult<SharedTicker[]>> ExecuteAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<IGetAllTickersRest>> capabilities,
|
||||
GetTickersRequest request,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.GetAllTickersAsync(request, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute GetAllTickersAsync for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<IExchangeCallResult<SharedTicker[]>> ExecuteAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<IGetAllTickers>> capabilities,
|
||||
GetTickersRequest request,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.GetAllTickersAsync(request, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute GetRecentTradesAsync for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<HttpResult<SharedTrade[]>> ExecuteAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<IGetRecentTradesRest>> capabilities,
|
||||
GetRecentTradesRequest request,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.GetRecentTradesAsync(request, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute GetRecentTradesAsync for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<IExchangeCallResult<SharedTrade[]>> ExecuteAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<IGetRecentTrades>> capabilities,
|
||||
GetRecentTradesRequest request,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.GetRecentTradesAsync(request, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to trade updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeTradesSocket>> capabilities,
|
||||
SubscribeTradeRequest request,
|
||||
Action<DataEvent<SharedTrade[]>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToTradeUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to balance updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeBalancesSocket>> capabilities,
|
||||
SubscribeBalancesRequest request,
|
||||
Action<DataEvent<SharedBalance[]>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToBalanceUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to index price updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeIndexPriceSocket>> capabilities,
|
||||
SubscribeIndexPriceRequest request,
|
||||
Action<DataEvent<SharedIndexPrice>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToIndexPriceUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to funding info updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeFundingInfoSocket>> capabilities,
|
||||
SubscribeFundingInfoRequest request,
|
||||
Action<DataEvent<SharedFundingInfo>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToFundingInfoUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to kline updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeKlinesSocket>> capabilities,
|
||||
SubscribeKlineRequest request,
|
||||
Action<DataEvent<SharedKline>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToKlineUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to mark price updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeMarkPriceSocket>> capabilities,
|
||||
SubscribeMarkPriceRequest request,
|
||||
Action<DataEvent<SharedMarkPrice>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToMarkPriceUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to order book updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeOrderBookSocket>> capabilities,
|
||||
SubscribeOrderBookRequest request,
|
||||
Action<DataEvent<SharedOrderBook>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToOrderBookUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to incremental order book updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeIncrementalOrderBookSocket>> capabilities,
|
||||
SubscribeOrderBookRequest request,
|
||||
Action<DataEvent<SharedIncrementalOrderBook>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToIncrementalOrderBookUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to futures order updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeFuturesOrdersSocket>> capabilities,
|
||||
SubscribeFuturesOrderRequest request,
|
||||
Action<DataEvent<SharedFuturesOrderUpdate[]>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToFuturesOrderUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to spot order updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeSpotOrdersSocket>> capabilities,
|
||||
SubscribeSpotOrderRequest request,
|
||||
Action<DataEvent<SharedSpotOrderUpdate[]>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToSpotOrderUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to position updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribePositionsSocket>> capabilities,
|
||||
SubscribePositionRequest request,
|
||||
Action<DataEvent<SharedPosition[]>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToPositionUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to ticker updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeTickerSocket>> capabilities,
|
||||
SubscribeTickerRequest request,
|
||||
Action<DataEvent<SharedTicker>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToTickerUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to all ticker updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeAllTickersSocket>> capabilities,
|
||||
SubscribeAllTickersRequest request,
|
||||
Action<DataEvent<SharedTicker[]>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToAllTickersUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to user trade updates for all capabilities in parallel and return results as they arrive
|
||||
/// </summary>
|
||||
public static IAsyncEnumerable<WebSocketResult<UpdateSubscription>> SubscribeAllAsync(
|
||||
this IEnumerable<SharedCapabilityResolution<ISubscribeUserTradesSocket>> capabilities,
|
||||
SubscribeUserTradeRequest request,
|
||||
Action<DataEvent<SharedUserTrade[]>> onData,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return capabilities
|
||||
.Select(x => x.Capability.SubscribeToUserTradeUpdatesAsync(request, onData, ct))
|
||||
.ParallelEnumerateAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for all results from an IAsyncEnumerable and return them as an array
|
||||
/// </summary>
|
||||
public static async Task<T[]> WaitAllAsync<T>(this IAsyncEnumerable<T> enumerable)
|
||||
{
|
||||
var result = new List<T>();
|
||||
await foreach (var itemResult in enumerable.ConfigureAwait(false))
|
||||
result.Add(itemResult);
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedAsset[]>> GetAllAssetsAsync(GetAssetsRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedAsset[]>> GetAllAssetsAsync(GetAssetsRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedAsset>> GetAssetAsync(GetAssetRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedAsset>> GetAssetAsync(GetAssetRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
Task<ICallResult<SharedBalance[]>> GetBalancesAsync(GetBalancesRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedBalance[]>> GetBalancesAsync(GetBalancesRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
Task<ICallResult<SharedDepositAddress[]>> GetDepositAddressesAsync(GetDepositAddressesRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedDepositAddress[]>> GetDepositAddressesAsync(GetDepositAddressesRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="nextPageToken">The pagination request from the previous request result <see cref="HttpResult{T}.NextPageRequest"/> property to continue pagination</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
Task<ICallResult<SharedDeposit[]>> GetDepositHistoryAsync(GetDepositsRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedDeposit[]>> GetDepositHistoryAsync(GetDepositsRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedFee>> GetFeesAsync(GetFeeRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedFee>> GetFeesAsync(GetFeeRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedFundingInfo>> GetFundingInfoAsync(GetFundingInfoRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedFundingInfo>> GetFundingInfoAsync(GetFundingInfoRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="nextPageToken">The pagination request from the previous request result <see cref="HttpResult{T}.NextPageRequest"/> property to continue pagination</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedFundingRate[]>> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedFundingRate[]>> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="nextPageToken">The pagination request from the previous request result <see cref="HttpResult{T}.NextPageRequest"/> property to continue pagination</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedFundingFee[]>> GetUserFundingHistoryAsync(GetUserFundingHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedFundingFee[]>> GetUserFundingHistoryAsync(GetUserFundingHistoryRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Operation for subscribing to funding info updates
|
||||
/// </summary>
|
||||
public interface ISubscribeFundingInfoSocket : ISharedSubscription
|
||||
{
|
||||
/// <summary>
|
||||
/// Funding info subscription options
|
||||
/// </summary>
|
||||
SubscribeFundingInfoOptions SubscribeFundingInfoOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to funding info updates
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="handler">Update handler</param>
|
||||
/// <param name="ct">Cancellation token, can be used to stop the updates</param>
|
||||
/// <returns></returns>
|
||||
Task<WebSocketResult<UpdateSubscription>> SubscribeToFundingInfoUpdatesAsync(SubscribeFundingInfoRequest request, Action<DataEvent<SharedFundingInfo>> handler, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for subscribing to funding info updates
|
||||
/// </summary>
|
||||
public class SubscribeFundingInfoOptions : CapabilityOptions<SubscribeFundingInfoRequest, ISubscribeFundingInfoSocket>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string Description => "Subscribe to funding info updates for a symbol";
|
||||
|
||||
private static readonly RequestParameterDescription[] _defaultParameterRules = new[]
|
||||
{
|
||||
RequestParameterRule<SubscribeFundingInfoRequest>.Optional(x => x.Symbol, "The symbol to subscribe to", new SharedSymbol(TradingMode.PerpetualLinear, "ETH", "USDT")),
|
||||
RequestParameterRule<SubscribeFundingInfoRequest>.Optional(x => x.Symbols, "The symbols to subscribe to", new[] { new SharedSymbol(TradingMode.PerpetualLinear, "ETH", "USDT") }),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SubscribeFundingInfoOptions(string exchange, bool needsAuthentication)
|
||||
: base(exchange, needsAuthentication, nameof(ISubscribeFundingInfoSocket.SubscribeToFundingInfoUpdatesAsync), _defaultParameterRules, SharedTradingModeSets.Futures)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Request to subscribe to funding info updates for a symbol
|
||||
/// </summary>
|
||||
public record SubscribeFundingInfoRequest : SharedSymbolRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol to subscribe to</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public SubscribeFundingInfoRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null)
|
||||
: base(symbol, exchangeParameters)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="symbols">The symbols to subscribe to</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public SubscribeFundingInfoRequest(IEnumerable<SharedSymbol> symbols, ExchangeParameters? exchangeParameters = null)
|
||||
: base(symbols, exchangeParameters)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.RateLimiting;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared API
|
||||
/// </summary>
|
||||
public interface ISharedApi
|
||||
public interface ISharedApi : IRateLimitAdmissionClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the exchange
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared WebSocket API subscription capabitlity
|
||||
/// Shared WebSocket API subscription capability
|
||||
/// </summary>
|
||||
public interface ISharedSubscription : ISharedSocket
|
||||
{
|
||||
/// <summary>
|
||||
/// Unsubscribes all subscriptions created by the underlying socket API
|
||||
/// client and closes its subscription connections.
|
||||
/// </summary>
|
||||
Task UnsubscribeAllAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedIndexPrice[]>> GetAllIndexPricesAsync(GetAllIndexPricesRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedIndexPrice[]>> GetAllIndexPricesAsync(GetAllIndexPricesRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedIndexPrice>> GetIndexPriceAsync(GetIndexPriceRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedIndexPrice>> GetIndexPriceAsync(GetIndexPriceRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="nextPageToken">The pagination request from the previous request result <see cref="HttpResult{T}.NextPageRequest"/> property to continue pagination</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedFuturesKline[]>> GetIndexPriceKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedFuturesKline[]>> GetIndexPriceKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="nextPageToken">The pagination request from the previous request result <see cref="HttpResult{T}.NextPageRequest"/> property to continue pagination</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedFuturesKline[]>> GetMarkPriceKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedFuturesKline[]>> GetMarkPriceKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="nextPageToken">The pagination request from the previous request result <see cref="HttpResult{T}.NextPageRequest"/> property to continue pagination</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
Task<ICallResult<SharedKline[]>> GetKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedKline[]>> GetKlinesAsync(GetKlinesRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="nextPageToken">The pagination request from the previous request result <see cref="HttpResult{T}.NextPageRequest"/> property to continue pagination</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedLedgerEntry[]>> GetLedgerAsync(GetLedgerRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedLedgerEntry[]>> GetLedgerAsync(GetLedgerRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedLeverage>> GetLeverageAsync(GetLeverageRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedLeverage>> GetLeverageAsync(GetLeverageRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedLeverageTier[]>> GetLeverageTiersAsync(GetLeverageTiersRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedLeverageTier[]>> GetLeverageTiersAsync(GetLeverageTiersRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedLeverage>> SetLeverageAsync(SetLeverageRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedLeverage>> SetLeverageAsync(SetLeverageRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedMarkPrice[]>> GetAllMarkPricesAsync(GetAllMarkPricesRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedMarkPrice[]>> GetAllMarkPricesAsync(GetAllMarkPricesRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedMarkPrice>> GetMarkPriceAsync(GetMarkPriceRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedMarkPrice>> GetMarkPriceAsync(GetMarkPriceRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedOpenInterest>> GetOpenInterestAsync(GetOpenInterestRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedOpenInterest>> GetOpenInterestAsync(GetOpenInterestRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
Task<ICallResult<SharedBookTicker>> GetBookTickerAsync(GetBookTickerRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedBookTicker>> GetBookTickerAsync(GetBookTickerRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
Task<ICallResult<SharedOrderBook>> GetOrderBookAsync(GetOrderBookRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedOrderBook>> GetOrderBookAsync(GetOrderBookRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// Order book subscription options
|
||||
/// </summary>
|
||||
SubscribeOrderBookOptions SubscribeOrderBookOptions { get; }
|
||||
SubscribeIncrementalOrderBookOptions SubscribeIncrementalOrderBookOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to incremental order book updates for a symbol
|
||||
@@ -23,6 +23,6 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="handler">Update handler</param>
|
||||
/// <param name="ct">Cancellation token, can be used to stop the updates</param>
|
||||
/// <returns></returns>
|
||||
Task<WebSocketResult<UpdateSubscription>> SubscribeToOrderBookUpdatesAsync(SubscribeOrderBookRequest request, Action<DataEvent<SharedOrderBook>> handler, CancellationToken ct = default);
|
||||
Task<WebSocketResult<UpdateSubscription>> SubscribeToIncrementalOrderBookUpdatesAsync(SubscribeOrderBookRequest request, Action<DataEvent<SharedIncrementalOrderBook>> handler, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SubscribeIncrementalOrderBookOptions(string exchange, bool needsAuthentication, int[] limits, SharedOrderBookSubscriptionType updateType)
|
||||
: base(exchange, needsAuthentication, nameof(ISubscribeIncrementalOrderBookSocket.SubscribeToOrderBookUpdatesAsync), _defaultParameterRules)
|
||||
: base(exchange, needsAuthentication, nameof(ISubscribeIncrementalOrderBookSocket.SubscribeToIncrementalOrderBookUpdatesAsync), _defaultParameterRules)
|
||||
{
|
||||
SupportedLimits = limits;
|
||||
UpdateType = updateType;
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult> CancelAllFuturesOrdersAsync(CancelAllOrdersRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult> CancelAllFuturesOrdersAsync(CancelAllOrdersRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult> CancelAllFuturesSymbolOrdersAsync(CancelAllSymbolOrdersRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult> CancelAllFuturesSymbolOrdersAsync(CancelAllSymbolOrdersRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedId>> CancelFuturesOrderAsync(CancelOrderRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedId>> CancelFuturesOrderAsync(CancelOrderRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedId>> CancelFuturesOrderByClientOrderIdAsync(CancelOrderRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedId>> CancelFuturesOrderByClientOrderIdAsync(CancelOrderRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedId>> EditFuturesOrderAsync(
|
||||
Task<IExchangeCallResult<SharedId>> EditFuturesOrderAsync(
|
||||
EditOrderRequest request,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedId>> EditFuturesOrderByClientOrderIdAsync(EditOrderRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedId>> EditFuturesOrderByClientOrderIdAsync(EditOrderRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="nextPageToken">The pagination request from the previous request result <see cref="HttpResult{T}.NextPageRequest"/> property to continue pagination</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedFuturesOrder[]>> GetClosedFuturesOrdersAsync(GetClosedOrdersRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedFuturesOrder[]>> GetClosedFuturesOrdersAsync(GetClosedOrdersRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedFuturesOrder>> GetFuturesOrderAsync(GetOrderRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedFuturesOrder>> GetFuturesOrderAsync(GetOrderRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedFuturesOrder>> GetFuturesOrderByClientOrderIdAsync(GetOrderRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedFuturesOrder>> GetFuturesOrderByClientOrderIdAsync(GetOrderRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedFuturesOrder[]>> GetOpenFuturesOrdersAsync(GetOpenOrdersRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedFuturesOrder[]>> GetOpenFuturesOrdersAsync(GetOpenOrdersRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedId>> PlaceFuturesOrderAsync(PlaceFuturesOrderRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedId>> PlaceFuturesOrderAsync(PlaceFuturesOrderRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<CallResult<SharedId>[]>> PlaceMultipleFuturesOrdersAsync(
|
||||
Task<IExchangeCallResult<CallResult<SharedId>[]>> PlaceMultipleFuturesOrdersAsync(
|
||||
PlaceMultipleFuturesOrdersRequest request,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult> CancelAllSpotOrdersAsync(CancelAllOrdersRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult> CancelAllSpotOrdersAsync(CancelAllOrdersRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult> CancelAllSpotSymbolOrdersAsync(CancelAllSymbolOrdersRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult> CancelAllSpotSymbolOrdersAsync(CancelAllSymbolOrdersRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedId>> CancelSpotOrderAsync(CancelOrderRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedId>> CancelSpotOrderAsync(CancelOrderRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedId>> CancelSpotOrderByClientOrderIdAsync(CancelOrderRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedId>> CancelSpotOrderByClientOrderIdAsync(CancelOrderRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedId>> EditSpotOrderAsync(
|
||||
Task<IExchangeCallResult<SharedId>> EditSpotOrderAsync(
|
||||
EditOrderRequest request,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedId>> EditSpotOrderByClientOrderIdAsync(EditOrderRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedId>> EditSpotOrderByClientOrderIdAsync(EditOrderRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="nextPageToken">The pagination request from the previous request result <see cref="HttpResult{T}.NextPageRequest"/> property to continue pagination</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedSpotOrder[]>> GetClosedSpotOrdersAsync(GetClosedOrdersRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedSpotOrder[]>> GetClosedSpotOrdersAsync(GetClosedOrdersRequest request, PageRequest? nextPageToken = null, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ICallResult<SharedSpotOrder[]>> GetOpenSpotOrdersAsync(GetOpenOrdersRequest request, CancellationToken ct = default);
|
||||
Task<IExchangeCallResult<SharedSpotOrder[]>> GetOpenSpotOrdersAsync(GetOpenOrdersRequest request, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user