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

Compare commits

..

50 Commits

Author SHA1 Message Date
Jkorf bda81d8170 Updated to version 13.0.0 2026-09-23 10:07:50 +02:00
Jkorf 666ca76776 Fixed incorrect example 2026-09-23 09:47:15 +02:00
Jan Korf ae7504c26c Shared API's V2 (#289)
Added V2 Shared API's infrastructure
Added request coalescing
Added rate limiting admission logic
Fixed some rate limiting issues
2026-09-23 09:15:39 +02:00
Maxxx555 cecfbcba48 ArrayConverter: parse string decimals in scientific notation (#292) 2026-09-21 11:54:15 +02:00
Jkorf 4a4930d658 Updated to version 12.5.1 2026-09-01 09:03:50 +02:00
nils2525 ca245959a2 Fix subscription query completion for topic-filtered routes (#288) 2026-08-31 21:07:33 +02:00
Jkorf ff43d202d4 Add Tapbit to examples 2026-08-24 11:52:37 +02:00
Jkorf d1947e643b Added Tapbit reference 2026-08-24 10:55:38 +02:00
JKorf 6fec65cd6a Fixed failed ISymbolOrderBook start potentially leaving a pending background task 2026-08-23 20:36:47 +02:00
JKorf 053bcf6ecc Fixed caching issue for auth requests 2026-08-23 20:22:10 +02:00
JKorf 6957ba44f5 Fixed KlineTracker reporting incorrect High/Low price on GetStats result 2026-08-23 20:21:54 +02:00
Jkorf c1af97dd4a Updated to version 12.5.0 2026-08-21 10:21:54 +02:00
Jkorf ecfe3cf392 Added ISpotOrderManagementSocketClient and IFuturesOrderManagementSocketClient for placing/canceling orders via websocket 2026-08-21 09:56:56 +02:00
Jkorf ac1e9cf2ab Added Description property to EndpointOptions classes. Updated client info string representation, added additional validation for Spot endpoints 2026-08-21 09:55:53 +02:00
Jkorf 4987e6be70 Added WithCalculatedQuantities to SharedQuantity to retrieve a copy with calculated quantities 2026-08-21 09:52:54 +02:00
Jkorf 9100c0d7f3 Added UpperPriceLimitPerecentage, LowerPriceLimitPercentage, MakerFeePercentage and TakerFeePercentage to SharedSpotSymbol model 2026-08-21 09:52:29 +02:00
Jkorf 5b1ae30e4b Updated SharedId value to be nullable 2026-08-21 09:52:00 +02:00
Jkorf 36a06e8a9a Added UpperFundingCap, LowerFundingCap to SharedFuturesSymbol model 2026-08-21 09:51:43 +02:00
Jkorf e7eb559074 Updated remaining quantity/volumes to SharedOrderQuantity model in Shared APIs, added QuantityType to SharedOrderBook, added auto calculated quote quantity where for SharedOrderQuantity where it makes sense 2026-08-21 09:51:17 +02:00
Jkorf 79e2efec70 Added IQueryResult interface to QueryResult, moved OriginalData to the base class 2026-08-21 09:48:57 +02:00
Jkorf c8c6d14329 Split UserClientProvider into base class with RestClient and derived class also containing SocketClient 2026-08-21 09:47:19 +02:00
Jkorf b8bd30a09d Added UsePublicConnectionForAuth property to SocketApiClient 2026-08-21 09:44:30 +02:00
Jkorf c2b58ab19a Removed unnecessary log from token manager 2026-08-21 09:43:57 +02:00
Jkorf 90a031c595 Added support for order polling where Shared order implementation doesn't support time filtering to UserDataTracker implementations 2026-08-17 14:17:43 +02:00
Jkorf 860d753ad6 Update README.md 2026-08-07 09:09:51 +02:00
Jkorf e16c91792c Added LBank reference 2026-08-06 10:29:02 +02:00
Jkorf a3d95da9fa Updated examples 2026-08-06 10:26:39 +02:00
JKorf 907399b878 Added ManualUpdateSubscription and UpdateSubscription additional constructor to allow producing websocket events without actual connection 2026-08-05 21:31:59 +02:00
JKorf 9eab0d967e Merge branch 'master' of https://github.com/JKorf/CryptoExchange.Net 2026-08-05 17:53:51 +02:00
JKorf 0dee68e8ae Fixed timing issue in test 2026-08-05 17:53:43 +02:00
Jkorf d838d3377f Updated logging unhandled websocket message 2026-08-05 13:13:48 +02:00
Jkorf 2141ad9061 Fixed incorrect check test output 2026-07-29 13:05:42 +02:00
Jkorf 9be8798ccf Updated to version 12.4.0 2026-07-28 12:45:36 +02:00
Jkorf 4803ed91cd Updated SharedTrade to use SharedOrderQuantity for quantities 2026-07-28 11:47:59 +02:00
Jkorf 20bddd5c37 Updated SharedFuturesTicker, SharedSpotTicker and SharedKline to use SharedOrderQuantity for volumes. Added AveragePrice property to SharedQuantity model 2026-07-28 09:05:48 +02:00
Jkorf 0e75ddb3d0 Updated json deserialization error for empty response 2026-07-24 15:42:37 +02:00
Jkorf 0e5b46002c Added DebuggerDisplay Result objects 2026-07-24 15:42:11 +02:00
Jkorf 8f7c71f9ce Add Pionex reference 2026-07-24 15:40:56 +02:00
Jkorf 73377fbb87 Updated to version 12.3.0 2026-07-23 13:47:24 +02:00
Jkorf 3a00d6371a Updated some Shared errors 2026-07-23 13:17:08 +02:00
Jkorf caf6d36bcd Extracted ConnectionCanBeUsedFor method in SocketApiClient for easier custom logic implementation 2026-07-23 13:16:59 +02:00
Jkorf e078a373da Remove duplicate warnings from testing output 2026-07-21 16:39:30 +02:00
Jkorf 007743f5a1 Added calculation of AveragePrice on Shared order models if data is available and AveragePrice is not set 2026-07-21 11:35:59 +02:00
Jkorf d06f891cee Updated to version 12.2.0 2026-07-20 14:02:03 +02:00
Jkorf 8dcbb687f5 Fixed warning 2026-07-20 13:45:32 +02:00
Jan Korf fcb36f7ee0 Shared asset and symbol types
Added SpotSymbolCatalog to Shared ISpotSymbolRestClient interface
Added FuturesSymbolCatalog to Shared IFuturesSymbolRestClient interface
Added BaseAssetType, BaseAssetSubType, QuoteAssetType and QuoteAssetSubType to GetSymbolsRequest model
Added DisplayName to SharedSpotSymbol and SharedFuturesSymbol models
Added BaseAssetType, BaseAssetSubType, QuoteAssetType and QuoteAssetSubType to SharedSpotSymbol and SharedFuturesSymbol models
Added IsStableCoin, IsCommodity and IsEquity helper methods to LibraryHelpers
2026-07-20 13:40:58 +02:00
nils2525 3cffd67518 Fixed socket individual subscription target calculation (#282)
* Fixed socket individual subscription target calculation

* Fixed socket selection when the least-loaded connection has reached its individual subscription limit
2026-07-20 09:24:01 +02:00
Jkorf ecd00ea707 Added DebuggerDisplay attributes to Shared models 2026-07-14 09:59:02 +02:00
JKorf 38a7b981ce Updated to version 12.1.1 2026-07-11 15:34:31 +02:00
JKorf c41cc3c4c7 Added timestamp deserialization support for yyyy-MM-dd HH:mm:ss.ffffff+00:00:00 2026-07-11 15:19:02 +02:00
545 changed files with 16184 additions and 4143 deletions
+63 -42
View File
@@ -1,71 +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
## 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
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
+45 -27
View File
@@ -1,52 +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`.
## Single-exchange code uses the exchange's own client
Use `SharedSymbol`; do not hard-code exchange-native symbol formatting in shared code.
For Binance-only code, use `BinanceRestClient` directly (see Binance.Net repo `AGENTS.md`). SharedApis is for portability — use it when you need that.
## Runtime capability selection
## Result pattern
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`:
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.
```csharp
var match = sharedClient.GetCapability(
SharedCapabilities.Orders.Futures.PlaceOrder.Rest,
TradingMode.PerpetualLinear);
## Available shared interfaces
if (match is null)
return;
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.
var result = await match.Capability.PlaceFuturesOrderAsync(request);
```
Each exchange library implements a subset. Check exchange docs for support matrix.
`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.
## Avoid
Before constructing dynamic requests, inspect `match.Options.RequestParameterRules`, `ExchangeParameterRules`, and `SupportedTradingModes`. A capability may exist while a particular request field is unsupported.
- 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`)
## Results and transports
## Reference
- Transport-agnostic operation: `IExchangeCallResult<T>`
- REST capability: `HttpResult<T>`
- WebSocket command capability: `QueryResult<T>`
- WebSocket subscription capability: `WebSocketResult<UpdateSubscription>`
For detailed patterns see `AGENTS.md` and `llms.txt` in repo root, `examples/ai-friendly/` for compilable examples.
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.
## Current V2 semantics
- 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.
## Engineering conventions
- 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.
See `AGENTS.md`, `docs/ai-api-map.md`, and `docs/SHARED_API_V2_MIGRATION.md` for expanded guidance.
+157 -99
View File
@@ -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,128 +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.
## 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`
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
@@ -91,28 +91,25 @@ namespace CryptoExchange.Net.UnitTests
var evnt = new AsyncResetEvent(false, true);
var waiters = new List<Task<bool>>();
for(var i = 0; i < 10; i++)
for (var i = 0; i < 10; i++)
{
waiters.Add(evnt.WaitAsync());
}
List<bool>? results = null;
var resultsWaiter = Task.Run(async () =>
{
await Task.WhenAll(waiters);
results = waiters.Select(w => w.Result).ToList();
});
var remaining = waiters.ToList();
for(var i = 1; i <= 10; i++)
for (var i = 0; i < 10; i++)
{
evnt.Set();
await Task.Delay(1); // Wait for the continuation.
Assert.That(10 - i == waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
var completed = await Task.WhenAny(remaining);
Assert.That(await completed, Is.True);
remaining.Remove(completed);
Assert.That(remaining.Count(w => w.IsCompleted), Is.Zero);
}
await resultsWaiter;
Assert.That(10 == results?.Count(r => r));
Assert.That(remaining, Is.Empty);
}
[Test]
@@ -138,6 +138,68 @@ namespace CryptoExchange.Net.UnitTests.ClientTests
Assert.That(socket2.Connected == false);
}
[TestCase()]
public async Task BatchedSubscription_Should_NotExceedIndividualCombineTarget()
{
// arrange
var client = new TestSocketClient(options =>
{
options.SocketSubscriptionsCombineTarget = 10;
options.SocketIndividualSubscriptionCombineTarget = 10;
});
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
// act
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default, individualSubscriptionCount: 6);
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default, individualSubscriptionCount: 6);
// assert
Assert.That(client.ApiClient1._socketConnections.Count == 2);
Assert.That(client.ApiClient1._socketConnections.Values.All(connection => connection.Subscriptions.Sum(subscription => subscription.IndividualSubscriptionCount) <= 10));
}
[TestCase()]
public async Task BatchedSubscription_FullIndividualConnection_Should_NotPreventEligibleConnectionReuse()
{
// arrange
var client = new TestSocketClient(options =>
{
options.SocketSubscriptionsCombineTarget = 5;
options.SocketIndividualSubscriptionCombineTarget = 10;
});
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default, individualSubscriptionCount: 10);
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
// act
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
// assert
Assert.That(
client.ApiClient1._socketConnections.Count,
Is.EqualTo(2),
"The eligible connection should be reused instead of opening a new connection after selecting a full individual-subscription connection");
var fullConnection = client.ApiClient1._socketConnections.Values
.Single(connection => connection.Subscriptions.Sum(subscription => subscription.IndividualSubscriptionCount) == 10);
Assert.That(
fullConnection.UserSubscriptionCount,
Is.EqualTo(1),
"The full connection should not receive the normal subscription");
var eligibleConnection = client.ApiClient1._socketConnections.Values.Single(connection => connection != fullConnection);
Assert.That(
eligibleConnection.UserSubscriptionCount,
Is.EqualTo(3),
"The existing eligible connection should receive the normal subscription");
}
[TestCase()]
public async Task ErrorResponse_ShouldNot_ConfirmSubscription()
{
@@ -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>))]
@@ -15,6 +15,7 @@ namespace CryptoExchange.Net.UnitTests.ConverterTests
[TestCase("1620777600000")]
[TestCase("2021-05-12T00:00:00.000Z")]
[TestCase("2021-05-12T00:00:00.000000000Z")]
[TestCase("2021-05-12 00:00:00.000000+00:00:00")]
[TestCase("0.000000", true)]
[TestCase("0", true)]
[TestCase("", true)]
@@ -36,9 +36,13 @@ namespace CryptoExchange.Net.UnitTests.Implementations
protected override TestAuthenticationProvider CreateAuthenticationProvider(TestCredentials credentials) =>
new TestAuthenticationProvider(credentials);
public async Task<WebSocketResult<UpdateSubscription>> SubscribeToUpdatesAsync<T>(Action<DataEvent<T>> handler, bool subQuery, CancellationToken ct)
public async Task<WebSocketResult<UpdateSubscription>> SubscribeToUpdatesAsync<T>(Action<DataEvent<T>> handler, bool subQuery, CancellationToken ct, int individualSubscriptionCount = 1)
{
return await base.SubscribeAsync(new TestSubscription<T>(_logger, handler, subQuery, false), ct);
var subscription = new TestSubscription<T>(_logger, handler, subQuery, false)
{
IndividualSubscriptionCount = individualSubscriptionCount
};
return await base.SubscribeAsync(subscription, ct);
}
}
}
@@ -0,0 +1,136 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets.Default;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture]
public class ManualUpdateSubscriptionTests
{
[Test]
public void Constructor_Should_CreateSubscribedVirtualSubscription()
{
var controller = new ManualUpdateSubscription(socketId: 12);
Assert.That(controller.Subscription.SocketId, Is.EqualTo(12));
Assert.That(controller.Subscription.Id, Is.GreaterThan(0));
Assert.That(controller.Subscription.SocketStatus, Is.EqualTo(SocketStatus.Connected));
Assert.That(controller.Subscription.SubscriptionStatus, Is.EqualTo(SubscriptionStatus.Subscribed));
Assert.That(controller.Subscription.LastReceiveTime, Is.Null);
}
[Test]
public void StateChanges_Should_BeVisibleOnSubscription()
{
var controller = new ManualUpdateSubscription();
var timestamp = new DateTime(2026, 8, 5, 12, 0, 0, DateTimeKind.Utc);
var statuses = new List<SubscriptionStatus>();
controller.Subscription.SubscriptionStatusChanged += statuses.Add;
controller.SetLastReceiveTime(timestamp);
controller.SetSocketStatus(SocketStatus.Reconnecting);
controller.SetSubscriptionStatus(SubscriptionStatus.Subscribing);
controller.SetSubscriptionStatus(SubscriptionStatus.Subscribed);
Assert.That(controller.Subscription.LastReceiveTime, Is.EqualTo(timestamp));
Assert.That(controller.Subscription.SocketStatus, Is.EqualTo(SocketStatus.Reconnecting));
Assert.That(controller.Subscription.SubscriptionStatus, Is.EqualTo(SubscriptionStatus.Subscribed));
Assert.That(statuses, Is.EqualTo(new[]
{
SubscriptionStatus.Subscribing,
SubscriptionStatus.Subscribed
}));
}
[Test]
public void LifecycleMethods_Should_InvokeSubscriptionEvents()
{
var controller = new ManualUpdateSubscription();
var error = new ServerError("Test error", ErrorInfo.Unknown);
var exception = new InvalidOperationException("Test exception");
var disconnectedPeriod = TimeSpan.FromMinutes(2);
var lost = 0;
var restored = TimeSpan.Zero;
Error? resubscribeError = null;
var paused = 0;
var unpaused = 0;
Exception? receivedException = null;
controller.Subscription.ConnectionLost += () => lost++;
controller.Subscription.ConnectionRestored += x => restored = x;
controller.Subscription.ResubscribingFailed += x => resubscribeError = x;
controller.Subscription.ActivityPaused += () => paused++;
controller.Subscription.ActivityUnpaused += () => unpaused++;
controller.Subscription.Exception += x => receivedException = x;
controller.InvokeConnectionLost();
controller.InvokeConnectionRestored(disconnectedPeriod);
controller.InvokeResubscribingFailed(error);
controller.InvokeActivityPaused();
controller.InvokeActivityUnpaused();
controller.InvokeException(exception);
Assert.That(lost, Is.EqualTo(1));
Assert.That(restored, Is.EqualTo(disconnectedPeriod));
Assert.That(resubscribeError, Is.SameAs(error));
Assert.That(paused, Is.EqualTo(1));
Assert.That(unpaused, Is.EqualTo(1));
Assert.That(receivedException, Is.SameAs(exception));
}
[Test]
public void InvokeConnectionClosed_Should_CloseAndOnlyInvokeOnce()
{
var controller = new ManualUpdateSubscription();
var closed = 0;
controller.Subscription.ConnectionClosed += () => closed++;
controller.InvokeConnectionClosed();
controller.InvokeConnectionClosed();
Assert.That(closed, Is.EqualTo(1));
Assert.That(controller.Subscription.SocketStatus, Is.EqualTo(SocketStatus.Closed));
Assert.That(controller.Subscription.SubscriptionStatus, Is.EqualTo(SubscriptionStatus.Closed));
}
[Test]
public async Task SubscriptionOperations_Should_InvokeCallbacks()
{
var closes = 0;
var reconnects = 0;
var resubscribes = 0;
var controller = new ManualUpdateSubscription(
closeAsync: () =>
{
closes++;
return Task.CompletedTask;
},
reconnectAsync: () =>
{
reconnects++;
return Task.CompletedTask;
},
resubscribeAsync: () =>
{
resubscribes++;
return Task.FromResult(CallResult.Ok());
});
await controller.Subscription.ReconnectAsync();
var resubscribeResult = await controller.Subscription.ResubscribeAsync();
await controller.Subscription.CloseAsync();
await controller.Subscription.CloseAsync();
Assert.That(reconnects, Is.EqualTo(1));
Assert.That(resubscribes, Is.EqualTo(1));
Assert.That(resubscribeResult.Success, Is.True);
Assert.That(closes, Is.EqualTo(1));
Assert.That(controller.Subscription.SubscriptionStatus, Is.EqualTo(SubscriptionStatus.Closed));
}
}
}
+93 -24
View File
@@ -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));
}
}
}
@@ -0,0 +1,281 @@
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging.Abstractions;
using NUnit.Framework;
using System;
using System.Collections;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture]
public class RequestCoalescerTests
{
private static readonly TimeSpan _testTimeout = TimeSpan.FromSeconds(5);
[Test]
public async Task ExecuteAsync_WithIdenticalRequests_ShouldExecuteRequestOnce()
{
var coalescer = CreateCoalescer();
var key = CreateKey();
var requestCompletion = CreateCompletionSource<string>();
var requestStarted = CreateCompletionSource<bool>();
var requestCount = 0;
async Task<string> ExecuteRequest(CancellationToken cancellationToken)
{
Interlocked.Increment(ref requestCount);
requestStarted.TrySetResult(true);
return await requestCompletion.Task;
}
var first = coalescer.ExecuteAsync(key, ExecuteRequest, () => "canceled", CancellationToken.None);
await requestStarted.Task.WaitAsync(_testTimeout);
var second = coalescer.ExecuteAsync(key, ExecuteRequest, () => "canceled", CancellationToken.None);
Assert.That(requestCount, Is.EqualTo(1));
requestCompletion.SetResult("result");
Assert.That(await first.WaitAsync(_testTimeout), Is.EqualTo("result"));
Assert.That(await second.WaitAsync(_testTimeout), Is.EqualTo("result"));
Assert.That(requestCount, Is.EqualTo(1));
}
[Test]
public async Task ExecuteAsync_WithDifferentKeys_ShouldExecuteEachRequest()
{
var coalescer = CreateCoalescer();
var firstCompletion = CreateCompletionSource<string>();
var secondCompletion = CreateCompletionSource<string>();
var firstStarted = CreateCompletionSource<bool>();
var secondStarted = CreateCompletionSource<bool>();
var requestCount = 0;
var first = coalescer.ExecuteAsync(
CreateKey(parameters: "symbol=ETH"),
async cancellationToken =>
{
Interlocked.Increment(ref requestCount);
firstStarted.TrySetResult(true);
return await firstCompletion.Task;
},
() => "canceled",
CancellationToken.None);
var second = coalescer.ExecuteAsync(
CreateKey(parameters: "symbol=BTC"),
async cancellationToken =>
{
Interlocked.Increment(ref requestCount);
secondStarted.TrySetResult(true);
return await secondCompletion.Task;
},
() => "canceled",
CancellationToken.None);
await Task.WhenAll(firstStarted.Task, secondStarted.Task).WaitAsync(_testTimeout);
Assert.That(requestCount, Is.EqualTo(2));
firstCompletion.SetResult("first");
secondCompletion.SetResult("second");
Assert.That(await first.WaitAsync(_testTimeout), Is.EqualTo("first"));
Assert.That(await second.WaitAsync(_testTimeout), Is.EqualTo("second"));
}
[Test]
public async Task ExecuteAsync_WhenOneListenerCancels_ShouldKeepSharedRequestRunning()
{
var coalescer = CreateCoalescer();
var requestCompletion = CreateCompletionSource<string>();
var requestStarted = CreateCompletionSource<CancellationToken>();
var requestCount = 0;
using var firstCancellationSource = new CancellationTokenSource();
async Task<string> ExecuteRequest(CancellationToken cancellationToken)
{
Interlocked.Increment(ref requestCount);
requestStarted.TrySetResult(cancellationToken);
return await requestCompletion.Task;
}
var first = coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "canceled", firstCancellationSource.Token);
var requestCancellationToken = await requestStarted.Task.WaitAsync(_testTimeout);
var second = coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "canceled", CancellationToken.None);
firstCancellationSource.Cancel();
Assert.That(await first.WaitAsync(_testTimeout), Is.EqualTo("canceled"));
Assert.That(requestCancellationToken.IsCancellationRequested, Is.False);
Assert.That(requestCount, Is.EqualTo(1));
requestCompletion.SetResult("result");
Assert.That(await second.WaitAsync(_testTimeout), Is.EqualTo("result"));
}
[Test]
public async Task ExecuteAsync_WhenAllListenersCancel_ShouldCancelSharedRequest()
{
var coalescer = CreateCoalescer();
var requestStarted = CreateCompletionSource<bool>();
var requestCanceled = CreateCompletionSource<bool>();
using var firstCancellationSource = new CancellationTokenSource();
using var secondCancellationSource = new CancellationTokenSource();
async Task<string> ExecuteRequest(CancellationToken cancellationToken)
{
using (cancellationToken.Register(() => requestCanceled.TrySetResult(true)))
{
requestStarted.TrySetResult(true);
await requestCanceled.Task;
return "request canceled";
}
}
var first = coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "listener canceled", firstCancellationSource.Token);
await requestStarted.Task.WaitAsync(_testTimeout);
var second = coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "listener canceled", secondCancellationSource.Token);
firstCancellationSource.Cancel();
Assert.That(await first.WaitAsync(_testTimeout), Is.EqualTo("listener canceled"));
Assert.That(requestCanceled.Task.IsCompleted, Is.False);
secondCancellationSource.Cancel();
Assert.That(await second.WaitAsync(_testTimeout), Is.EqualTo("listener canceled"));
Assert.That(await requestCanceled.Task.WaitAsync(_testTimeout), Is.True);
}
[Test]
public async Task ExecuteAsync_WithAlreadyCanceledListener_ShouldNotStartRequest()
{
var coalescer = CreateCoalescer();
var requestCount = 0;
using var cancellationSource = new CancellationTokenSource();
cancellationSource.Cancel();
var result = await coalescer.ExecuteAsync(
CreateKey(),
cancellationToken =>
{
Interlocked.Increment(ref requestCount);
return Task.FromResult("result");
},
() => "canceled",
cancellationSource.Token);
Assert.That(result, Is.EqualTo("canceled"));
Assert.That(requestCount, Is.Zero);
}
[Test]
public async Task ExecuteAsync_AfterRequestCompletes_ShouldStartNewRequest()
{
var coalescer = CreateCoalescer();
var requestCount = 0;
Task<int> ExecuteRequest(CancellationToken cancellationToken)
=> Task.FromResult(Interlocked.Increment(ref requestCount));
var first = await coalescer.ExecuteAsync(CreateKey(typeof(int)), ExecuteRequest, () => -1, CancellationToken.None);
var second = await coalescer.ExecuteAsync(CreateKey(typeof(int)), ExecuteRequest, () => -1, CancellationToken.None);
Assert.That(first, Is.EqualTo(1));
Assert.That(second, Is.EqualTo(2));
Assert.That(requestCount, Is.EqualTo(2));
}
[Test]
public void ExecuteAsync_AfterRequestFails_ShouldStartNewRequest()
{
var coalescer = CreateCoalescer();
var requestCount = 0;
Task<string> ExecuteRequest(CancellationToken cancellationToken)
{
if (Interlocked.Increment(ref requestCount) == 1)
return Task.FromException<string>(new InvalidOperationException("Request failed"));
return Task.FromResult("result");
}
Assert.ThrowsAsync<InvalidOperationException>(async () =>
await coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "canceled", CancellationToken.None));
Assert.That(
coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "canceled", CancellationToken.None).GetAwaiter().GetResult(),
Is.EqualTo("result"));
Assert.That(requestCount, Is.EqualTo(2));
}
[Test]
public async Task ExecuteAsync_WhenAbandonedRequestCompletes_ShouldNotRemoveReplacementRequest()
{
var coalescer = CreateCoalescer();
var key = CreateKey();
var oldRequestCompletion = CreateCompletionSource<string>();
var oldRequestStarted = CreateCompletionSource<bool>();
var replacementCompletion = CreateCompletionSource<string>();
var replacementStarted = CreateCompletionSource<bool>();
var requestCount = 0;
using var cancellationSource = new CancellationTokenSource();
async Task<string> ExecuteOldRequest(CancellationToken cancellationToken)
{
Interlocked.Increment(ref requestCount);
oldRequestStarted.TrySetResult(true);
return await oldRequestCompletion.Task;
}
async Task<string> ExecuteReplacementRequest(CancellationToken cancellationToken)
{
Interlocked.Increment(ref requestCount);
replacementStarted.TrySetResult(true);
return await replacementCompletion.Task;
}
var abandonedListener = coalescer.ExecuteAsync(key, ExecuteOldRequest, () => "canceled", cancellationSource.Token);
await oldRequestStarted.Task.WaitAsync(_testTimeout);
var oldPhysicalRequest = GetPhysicalRequest(coalescer, key);
cancellationSource.Cancel();
Assert.That(await abandonedListener.WaitAsync(_testTimeout), Is.EqualTo("canceled"));
var replacementListener = coalescer.ExecuteAsync(key, ExecuteReplacementRequest, () => "canceled", CancellationToken.None);
await replacementStarted.Task.WaitAsync(_testTimeout);
oldRequestCompletion.SetResult("old result");
await oldPhysicalRequest.WaitAsync(_testTimeout);
var joinedReplacementListener = coalescer.ExecuteAsync(key, ExecuteReplacementRequest, () => "canceled", CancellationToken.None);
Assert.That(requestCount, Is.EqualTo(2));
replacementCompletion.SetResult("replacement result");
Assert.That(await replacementListener.WaitAsync(_testTimeout), Is.EqualTo("replacement result"));
Assert.That(await joinedReplacementListener.WaitAsync(_testTimeout), Is.EqualTo("replacement result"));
Assert.That(requestCount, Is.EqualTo(2));
}
private static RequestCoalescer CreateCoalescer()
=> new RequestCoalescer(NullLogger.Instance);
private static RequestCoalescingKey CreateKey(Type? responseType = null, string? parameters = null)
=> new RequestCoalescingKey("GET", "https://localhost/test", parameters, responseType ?? typeof(string));
private static TaskCompletionSource<T> CreateCompletionSource<T>()
=> new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
private static Task GetPhysicalRequest(RequestCoalescer coalescer, RequestCoalescingKey key)
{
var requestsField = typeof(RequestCoalescer).GetField("_requests", BindingFlags.Instance | BindingFlags.NonPublic)!;
var requests = (IDictionary)requestsField.GetValue(coalescer)!;
var entry = requests[key]!;
var requestProperty = entry.GetType().GetProperty("Request")!;
return ((Lazy<Task<object>>)requestProperty.GetValue(entry)!).Value;
}
}
}
@@ -0,0 +1,355 @@
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
{
[TestFixture]
internal class SharedApiClientTests
{
[Test]
public void GetCapability_ShouldUseCapabilityTradingModes()
{
var sharedApi = new TestSharedApi(
[TradingMode.Spot, TradingMode.PerpetualLinear],
[TradingMode.Spot]);
var client = new TestSharedApiClient(sharedApi);
Assert.Multiple(() =>
{
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);
});
}
[Test]
public void CapabilityTradingModes_ShouldDefaultToSharedApiTradingModes()
{
var sharedApi = new TestSharedApi(
[TradingMode.Spot, TradingMode.PerpetualLinear],
null);
var client = new TestSharedApiClient(sharedApi);
Assert.Multiple(() =>
{
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));
});
}
[Test]
public void CapabilityTradingModes_ShouldIntersectApiApplicableAndOverrideModes()
{
var sharedApi = new TestSharedApi(
[TradingMode.Spot, TradingMode.PerpetualLinear, TradingMode.DeliveryLinear],
[TradingMode.Spot, TradingMode.PerpetualLinear],
[TradingMode.PerpetualLinear, TradingMode.DeliveryLinear]);
Assert.That(
sharedApi.CapabilityOptions.SupportedTradingModes,
Is.EqualTo(new[] { TradingMode.PerpetualLinear }));
}
[Test]
public void CapabilityOptions_ShouldApplyIntrinsicTradingModes()
{
var apiModes = new[]
{
TradingMode.Spot,
TradingMode.PerpetualLinear,
TradingMode.DeliveryLinear,
TradingMode.PerpetualInverse,
TradingMode.DeliveryInverse
};
var spotOptions = new PlaceSpotOrderOptions("TestExchange");
var futuresOptions = new GetPositionsOptions("TestExchange", false);
var perpetualOptions = new GetFundingRateHistoryOptions(
"TestExchange",
true,
true,
true,
100,
false);
var unrestrictedOptions = new GetTickerOptions("TestExchange");
_ = new TestOptionsHost(apiModes, spotOptions);
_ = new TestOptionsHost(apiModes, futuresOptions);
_ = new TestOptionsHost(apiModes, perpetualOptions);
_ = new TestOptionsHost(apiModes, unrestrictedOptions);
Assert.Multiple(() =>
{
Assert.That(spotOptions.SupportedTradingModes, Is.EqualTo(new[] { TradingMode.Spot }));
Assert.That(futuresOptions.SupportedTradingModes, Is.EqualTo(new[]
{
TradingMode.PerpetualLinear,
TradingMode.DeliveryLinear,
TradingMode.PerpetualInverse,
TradingMode.DeliveryInverse
}));
Assert.That(perpetualOptions.SupportedTradingModes, Is.EqualTo(new[]
{
TradingMode.PerpetualLinear,
TradingMode.PerpetualInverse
}));
Assert.That(unrestrictedOptions.SupportedTradingModes, Is.EqualTo(apiModes));
});
}
private interface ITestCapability : ISharedApiCapability
{
}
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";
public TestCapabilityOptions(TradingMode[]? applicableTradingModes)
: base("TestExchange", false, "TestOperation", [], applicableTradingModes)
{
}
}
private sealed class TestSharedApi : SharedApiBase, ITestCapability
{
public TestCapabilityOptions CapabilityOptions { get; }
public TestSharedApi(TradingMode[] apiTradingModes, TradingMode[]? capabilityTradingModes)
: this(apiTradingModes, capabilityTradingModes, null)
{
}
public TestSharedApi(
TradingMode[] apiTradingModes,
TradingMode[]? capabilityTradingModes,
TradingMode[]? applicableTradingModes)
: base(
SharedTransport.Rest,
new TestApiClient(),
apiTradingModes,
() => false,
(baseAsset, quoteAsset, tradingMode, deliverDate) => $"{baseAsset}{quoteAsset}")
{
var options = new TestCapabilityOptions(applicableTradingModes)
{
SupportedTradingModeOverrides = capabilityTradingModes
};
SetCapabilities(options);
CapabilityOptions = options;
}
public override SharedClientInfo Discover() => new();
}
private sealed class TestOptionsHost : SharedApiBase
{
public TestOptionsHost(TradingMode[] apiTradingModes, CapabilityOptions capabilityOptions)
: base(
SharedTransport.Rest,
new TestApiClient(),
apiTradingModes,
() => false,
(baseAsset, quoteAsset, tradingMode, deliverDate) => $"{baseAsset}{quoteAsset}")
{
SetCapabilities(capabilityOptions);
}
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, sharedApis)
{
}
}
}
}
@@ -0,0 +1,89 @@
using CryptoExchange.Net.SharedApis;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture]
internal class SharedCapabilitiesTests
{
[Test]
public void AllSharedApiCapabilities_ShouldBeListedInSharedCapabilities()
{
var markerTypes = new[]
{
typeof(ISharedApiCapability),
typeof(ISharedRest),
typeof(ISharedSocket),
typeof(ISharedSubscription)
};
var expectedCapabilities = typeof(ISharedApiCapability).Assembly
.GetTypes()
.Where(x => x.IsInterface
&& typeof(ISharedApiCapability).IsAssignableFrom(x)
&& !markerTypes.Contains(x))
.OrderBy(x => x.FullName)
.ToArray();
var listedCapabilities = GetTypeAndNestedTypes(typeof(SharedCapabilities))
.SelectMany(x => x.GetProperties(
BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly))
.SelectMany(x => x.PropertyType.GetGenericArguments())
.Where(x => x.IsInterface
&& typeof(ISharedApiCapability).IsAssignableFrom(x))
.ToArray();
var missingCapabilities = expectedCapabilities
.Except(listedCapabilities)
.Select(x => x.Name)
.ToArray();
var unknownCapabilities = listedCapabilities
.Except(expectedCapabilities)
.Select(x => x.Name)
.ToArray();
var duplicateCapabilities = listedCapabilities
.GroupBy(x => x)
.Where(x => x.Count() > 1)
.Select(x => x.Key.Name)
.ToArray();
Assert.Multiple(() =>
{
Assert.That(
missingCapabilities,
Is.Empty,
$"Capabilities missing from SharedCapabilities: " +
string.Join(", ", missingCapabilities));
Assert.That(
unknownCapabilities,
Is.Empty,
$"Unknown capabilities listed in SharedCapabilities: " +
string.Join(", ", unknownCapabilities));
Assert.That(
duplicateCapabilities,
Is.Empty,
$"Capabilities listed multiple times in SharedCapabilities: " +
string.Join(", ", duplicateCapabilities));
});
}
private static IEnumerable<Type> GetTypeAndNestedTypes(Type type)
{
yield return type;
foreach (var nestedType in type.GetNestedTypes(BindingFlags.Public))
{
foreach (var result in GetTypeAndNestedTypes(nestedType))
yield return result;
}
}
}
}
@@ -0,0 +1,180 @@
using NUnit.Framework;
using CryptoExchange.Net.SharedApis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture]
internal class SharedParametersTests
{
[Test]
public void AllSharedAPIOptions_ShouldListAllDefaultParametersInRequestParameterRules()
{
var failures = new List<string>();
var optionsTypes = typeof(CapabilityOptions).Assembly
.GetTypes()
.Where(x => x.IsClass
&& !x.IsAbstract
&& typeof(CapabilityOptions).IsAssignableFrom(x))
.OrderBy(x => x.FullName)
.ToArray();
foreach (var optionsType in optionsTypes)
{
var genericOptionsType = GetGenericOptionsType(optionsType);
if (genericOptionsType == null)
{
if (optionsType == typeof(ClosePositionOptions))
continue;
failures.Add($"{optionsType.Name}: unable to determine request type");
continue;
}
var requestType = genericOptionsType.GetGenericArguments()[0];
var defaultRulesField = GetDefaultParameterRulesField(optionsType);
if (defaultRulesField == null)
{
failures.Add($"{optionsType.Name}: no _defaultParameterRules field found");
continue;
}
var rules = (RequestParameterDescription[]?)defaultRulesField.GetValue(null);
if (rules == null)
{
failures.Add($"{optionsType.Name}: _defaultParameterRules is null");
continue;
}
var constructorParameterNames = requestType
.GetConstructors(BindingFlags.Instance | BindingFlags.Public)
.SelectMany(x => x.GetParameters())
.Where(x => !string.Equals(
x.Name,
"exchangeParameters",
StringComparison.OrdinalIgnoreCase))
.Select(x => x.Name!)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var parameterProperties = requestType
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.DeclaringType == requestType
|| constructorParameterNames.Contains(x.Name))
.Where(x => x.Name != nameof(SharedRequest.ExchangeParameters))
.Select(x => x.Name)
.OrderBy(x => x)
.ToArray();
var ruleNames = rules
.Select(x => x.Name)
.ToHashSet(StringComparer.Ordinal);
foreach (var parameterProperty in parameterProperties)
{
if (!ruleNames.Contains(parameterProperty))
{
failures.Add(
$"{optionsType.Name}: request property " +
$"{requestType.Name}.{parameterProperty} has no default request parameter rule");
}
}
}
Assert.That(
failures,
Is.Empty,
$"Missing default request parameter rules:{Environment.NewLine}" +
string.Join(Environment.NewLine, failures));
}
[Test]
public void RequiredExchangeParameter_ShouldAcceptNameOrAnyAlias()
{
const string exchange = "TestExchange";
var options = new GetTickerOptions(exchange)
{
ExchangeParameterRules =
[
ExchangeParameterRule.Required(
"Parameter",
"Test parameter",
1,
"Alias1",
"Alias2")
]
};
var missingResult = options.ValidateRequest(null, null, [TradingMode.Spot]);
var nameResult = options.ValidateRequest(
new ExchangeParameters(new ExchangeParameter(exchange, "Parameter", 1)),
null,
[TradingMode.Spot]);
var firstAliasResult = options.ValidateRequest(
new ExchangeParameters(new ExchangeParameter(exchange, "Alias1", 1)),
null,
[TradingMode.Spot]);
var secondAliasResult = options.ValidateRequest(
new ExchangeParameters(new ExchangeParameter(exchange, "Alias2", 1)),
null,
[TradingMode.Spot]);
Assert.Multiple(() =>
{
Assert.That(missingResult, Is.Not.Null);
Assert.That(nameResult, Is.Null);
Assert.That(firstAliasResult, Is.Null);
Assert.That(secondAliasResult, Is.Null);
});
}
[Test]
public void RequiredExchangeParameterWithoutAliases_ShouldBeRequired()
{
const string exchange = "TestExchange";
var options = new GetTickerOptions(exchange)
{
ExchangeParameterRules =
[
ExchangeParameterRule.Required(
"Parameter",
"Test parameter",
1)
]
};
var result = options.ValidateRequest(null, null, [TradingMode.Spot]);
Assert.That(result, Is.Not.Null);
}
private static Type? GetGenericOptionsType(Type optionsType)
{
for (var type = optionsType; type != null; type = type.BaseType)
{
if (type.IsGenericType
&& type.GetGenericTypeDefinition() == typeof(CapabilityOptions<,>))
return type;
}
return null;
}
private static FieldInfo? GetDefaultParameterRulesField(Type optionsType)
{
for (var type = optionsType; type != null; type = type.BaseType)
{
var field = type.GetField(
"_defaultParameterRules",
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
if (field != null)
return field;
}
return null;
}
}
}
@@ -0,0 +1,130 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Routing;
using CryptoExchange.Net.UnitTests.Implementations;
using Microsoft.Extensions.Logging.Abstractions;
using NUnit.Framework;
using System;
namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
[TestFixture]
public class SubscriptionTests
{
[Test]
public void Handle_Should_OnlyCompleteSubscriptionQuery_ForMatchingTopic()
{
// arrange
var topicASubscription = new TopicSubscription("topic-a");
var topicBSubscription = new TopicSubscription("topic-b");
var topicAQuery = topicASubscription.CreateSubscriptionQuery(null!)!;
var topicBQuery = topicBSubscription.CreateSubscriptionQuery(null!)!;
// act
var topicAHandled = topicASubscription.Handle("type", "topic-a", null!, DateTime.UtcNow, "original", "data");
var topicBHandled = topicBSubscription.Handle("type", "topic-a", null!, DateTime.UtcNow, "original", "data");
// assert
Assert.That(topicAHandled, Is.True);
Assert.That(topicBHandled, Is.False);
Assert.That(topicAQuery.Completed, Is.True);
Assert.That(topicAQuery.Success, Is.True);
Assert.That(topicBQuery.Completed, Is.False);
}
[Test]
public void Handle_Should_CompleteSubscriptionQuery_BeforeInvokingMatchingHandler()
{
// arrange
Query? query = null;
var queryCompletedWhenHandlerInvoked = false;
var subscription = new TopicSubscription("topic", () => queryCompletedWhenHandlerInvoked = query!.Completed);
query = subscription.CreateSubscriptionQuery(null!)!;
// act
subscription.Handle("type", "topic", null!, DateTime.UtcNow, "original", "data");
// assert
Assert.That(queryCompletedWhenHandlerInvoked, Is.True);
}
[TestCase(null)]
[TestCase("topic")]
public void Handle_Should_CompleteSubscriptionQuery_ForUnfilteredRoute(string? topicFilter)
{
// arrange
var subscription = new TopicSubscription(topic: null);
var query = subscription.CreateSubscriptionQuery(null!)!;
// act
var handled = subscription.Handle("type", topicFilter, null!, DateTime.UtcNow, "original", "data");
// assert
Assert.That(handled, Is.True);
Assert.That(query.Completed, Is.True);
Assert.That(query.Success, Is.True);
}
[Test]
public void Handle_Should_TreatEmptyTopicFilterAsUnfilteredRoute()
{
// arrange
var subscription = new TopicSubscription(string.Empty);
var query = subscription.CreateSubscriptionQuery(null!)!;
// act
var handled = subscription.Handle("type", "topic", null!, DateTime.UtcNow, "original", "data");
// assert
Assert.That(handled, Is.True);
Assert.That(query.Completed, Is.True);
Assert.That(query.Success, Is.True);
}
[Test]
public void Handle_Should_CompleteSubscriptionQuery_ForAnyMatchingTopic()
{
// arrange
var subscription = new TopicSubscription(["topic-a", "topic-b"]);
var query = subscription.CreateSubscriptionQuery(null!)!;
// act
var handled = subscription.Handle("type", "topic-b", null!, DateTime.UtcNow, "original", "data");
// assert
Assert.That(handled, Is.True);
Assert.That(query.Completed, Is.True);
Assert.That(query.Success, Is.True);
}
private sealed class TopicSubscription : Subscription
{
public TopicSubscription(string? topic, Action? handler = null)
: base(NullLogger.Instance, false)
{
MessageRouter = MessageRouter.CreateForEvent<string>("type", topic, (_, _, _, _) =>
{
handler?.Invoke();
return CallResult.Ok();
});
}
public TopicSubscription(string[] topics)
: base(NullLogger.Instance, false)
{
MessageRouter = MessageRouter.CreateForEvent<string>("type", topics, (_, _, _, _) => CallResult.Ok());
}
protected override Query? GetSubQuery(SocketConnection connection)
{
return new TestQuery(new TestSocketMessage { Id = 1, Data = "Sub" }, false)
{
TimeoutBehavior = TimeoutBehavior.Succeed
};
}
protected override Query? GetUnsubQuery(SocketConnection connection) => null;
}
}
}
+32 -1
View File
@@ -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>
+114 -3
View File
@@ -98,6 +98,10 @@ namespace CryptoExchange.Net.Clients
/// Configured environment name
/// </summary>
public abstract string EnvironmentName { get; }
/// <summary>
/// Request coalescer
/// </summary>
protected RequestCoalescer RequestCoalescer { get; }
/// <summary>
/// ctor
@@ -123,6 +127,7 @@ namespace CryptoExchange.Net.Clients
{
TimeOffsetManager.RegisterRestApi(ClientName);
RequestCoalescer = new RequestCoalescer(_logger);
RequestFactory.Configure(options, httpClient);
}
@@ -177,8 +182,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <returns></returns>
protected virtual async Task<HttpResult<T>> SendAsync<T>(
protected virtual Task<HttpResult<T>> SendAsync<T>(
RequestDefinition definition,
Parameters? uriParameters,
Parameters? bodyParameters,
@@ -187,6 +191,61 @@ namespace CryptoExchange.Net.Clients
int? weight = null,
int? weightSingleLimiter = null,
string? rateLimitKeySuffix = null)
{
if (!ShouldCoalesce(definition, additionalHeaders))
{
return SendCoreAsync<T>(
definition,
uriParameters,
bodyParameters,
cancellationToken,
additionalHeaders,
weight,
weightSingleLimiter,
rateLimitKeySuffix);
}
var key = GetRequestCoalescingKey<T>(
definition,
uriParameters);
return RequestCoalescer.ExecuteAsync(
key,
requestCancellationToken => SendCoreAsync<T>(
definition,
uriParameters,
bodyParameters,
requestCancellationToken,
additionalHeaders,
weight,
weightSingleLimiter,
rateLimitKeySuffix),
() => HttpResult.Fail<T>(
Exchange,
new CancellationRequestedError()),
cancellationToken);
}
/// <summary>
/// Send a request to the base address based on the request definition
/// </summary>
/// <typeparam name="T">Response type</typeparam>
/// <param name="definition">Request definition</param>
/// <param name="uriParameters">Request query parameters</param>
/// <param name="bodyParameters">Request body parameters</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
protected virtual async Task<HttpResult<T>> SendCoreAsync<T>(RequestDefinition definition,
Parameters? uriParameters,
Parameters? bodyParameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null,
int? weightSingleLimiter = null,
string? rateLimitKeySuffix = null)
{
var requestId = ExchangeHelpers.NextId();
if (definition.Authenticated && GetAuthenticationProvider() == null)
@@ -198,7 +257,7 @@ namespace CryptoExchange.Net.Clients
string? cacheKey = null;
if (ShouldCache(definition))
{
cacheKey = definition.FullUrl + definition + uriParameters?.ToFormData();
cacheKey = GetCacheKey(definition, uriParameters);
_logger.CheckingCache(cacheKey);
var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge);
if (cachedValue != null)
@@ -291,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,
@@ -300,6 +362,7 @@ namespace CryptoExchange.Net.Clients
requestWeight,
ClientOptions.RateLimitingBehaviour,
rateLimitKeySuffix + ClientOptions.RateLimitGroup,
rateRatio,
cancellationToken).ConfigureAwait(false);
if (!limitResult.Success)
return limitResult.Error!;
@@ -315,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,
@@ -325,6 +391,7 @@ namespace CryptoExchange.Net.Clients
singleRequestWeight,
ClientOptions.RateLimitingBehaviour,
rateLimitKeySuffix,
rateRatio,
cancellationToken).ConfigureAwait(false);
if (!limitResult.Success)
return limitResult.Error!;
@@ -688,7 +755,10 @@ namespace CryptoExchange.Net.Clients
stringData = stringSerializer.Serialize(parameters.BodyValue);
}
else
{
stringData = stringSerializer.Serialize(parameters);
}
request.SetContent(stringData, RequestBodyContentEncoding, contentType);
}
else if (contentType == Constants.FormContentHeader)
@@ -800,6 +870,41 @@ namespace CryptoExchange.Net.Clients
&& definition.Method == HttpMethod.Get
&& !definition.PreventCaching;
/// <summary>
/// Produce a unique cache key for the request based on the request definition and parameters.
/// </summary>
protected virtual string GetCacheKey(RequestDefinition definition, Parameters? parameters)
{
return definition.FullUrl + definition + parameters?.ToFormData();
}
/// <summary>
/// Whether a request can be coalesced or not
/// </summary>
protected virtual bool ShouldCoalesce(
RequestDefinition definition,
Dictionary<string, string>? additionalHeaders)
{
return ClientOptions.RequestCoalescingEnabled // Enabled in client options
&& !definition.PreventRequestCoalescing // Enabled in request definition
&& definition.Method == HttpMethod.Get // Is a GET request
&& !definition.Authenticated // Not an authenticated request
&& (additionalHeaders == null || additionalHeaders.Count == 0); // No additional headers that might change the response
}
/// <summary>
/// Get a unique key for coalescing requests based on the request definition, parameters, and the type of the expected response.
/// </summary>
protected virtual RequestCoalescingKey GetRequestCoalescingKey<T>(
RequestDefinition definition,
Parameters? uriParameters)
{
return new RequestCoalescingKey(
definition.Method.Method,
definition.FullUrl,
uriParameters?.ToFormData(),
typeof(T));
}
/// <inheritdoc />
public virtual void SetOptions(UpdateOptions options)
@@ -889,6 +994,12 @@ namespace CryptoExchange.Net.Clients
ApiCredentials = (TApiCredentials?)options.ApiCredentials?.Copy() ?? ApiCredentials;
}
/// <inheritdoc />
protected override string GetCacheKey(RequestDefinition definition, Parameters? parameters)
{
return definition.FullUrl + definition + parameters?.ToFormData() + GetAuthenticationProvider()?.Key;
}
}
/// <inheritdoc />
+54 -35
View File
@@ -97,6 +97,11 @@ namespace CryptoExchange.Net.Clients
/// </summary>
protected bool AllowTopicsOnTheSameConnection { get; set; } = true;
/// <summary>
/// Whether to allow usage of a current public WebSocket connection for an authenticated request
/// </summary>
protected bool UsePublicConnectionForAuth { get; set; } = false;
/// <inheritdoc />
public double IncomingKbps
{
@@ -625,6 +630,21 @@ namespace CryptoExchange.Net.Clients
return Task.FromResult(CallResult.Ok());
}
/// <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="topic">Topic of the request</param>
/// <returns>True if connection can be used</returns>
protected virtual bool ConnectionCanBeUsedFor(SocketConnection connection, string address, bool authenticated, string? topic = null)
{
return connection.ConnectionUriString.Equals(address.TrimEnd('/'), StringComparison.Ordinal)
&& connection.ApiClient.ClientName.Equals(ClientName, StringComparison.Ordinal)
&& (AllowTopicsOnTheSameConnection || !connection.Topics.Contains(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>
@@ -643,10 +663,7 @@ namespace CryptoExchange.Net.Clients
string? topic = null,
int individualSubscriptionCount = 1)
{
var socketQuery = _socketConnections.Where(s => s.Value.ConnectionUriString.Equals(address.TrimEnd('/'), StringComparison.Ordinal)
&& s.Value.ApiClient.ClientName.Equals(ClientName, StringComparison.Ordinal)
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(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, 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;
@@ -676,49 +693,32 @@ namespace CryptoExchange.Net.Clients
_logger.WaitedForReconnectingSocket((long)(DateTime.UtcNow - delayStart).TotalMilliseconds);
socketQuery = socketQuery.Where(s => (s.Status == SocketStatus.None || s.Status == SocketStatus.Connected)
&& (s.Authenticated == authenticated || !authenticated)
&& (s.Authenticated == authenticated || !authenticated || UsePublicConnectionForAuth)
&& s.Connected).ToList();
SocketConnection? connection;
if (!dedicatedRequestConnection)
{
connection = socketQuery
.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection)
.OrderBy(s => s.UserSubscriptionCount)
.FirstOrDefault();
}
else
bool maxConnectionsReached = _socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections);
SocketConnection? connection = null;
if (dedicatedRequestConnection)
{
connection = socketQuery.Where(s => s.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault();
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
// Mark dedicated request connection as authenticated if the request is authenticated
connection.DedicatedRequestConnection.Authenticated = authenticated;
if (connection == null)
// Fall back to an existing connection if there is no dedicated request connection available
connection = socketQuery.OrderBy(s => s.UserSubscriptionCount).FirstOrDefault();
}
bool maxConnectionsReached = _socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections);
if (connection != null)
if (connection == null)
{
bool lessThanBatchSubCombineTarget = connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget;
bool lessThanIndividualSubCombineTarget = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount) < ClientOptions.SocketIndividualSubscriptionCombineTarget;
if ((lessThanBatchSubCombineTarget && lessThanIndividualSubCombineTarget)
|| maxConnectionsReached)
{
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
// If there is a max subscriptions per connection limit also only use existing if the new subscription doesn't go over the limit
if (MaxIndividualSubscriptionsPerConnection == null)
return CallResult.Ok(connection);
var currentCount = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount);
if (currentCount + individualSubscriptionCount <= MaxIndividualSubscriptionsPerConnection)
return CallResult.Ok(connection);
}
// Use an eligible non-dedicated connection for subscriptions, or as fallback when no dedicated request connection is available
connection = socketQuery
.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection)
.Where(s => IsConnectionEligible(s, individualSubscriptionCount, maxConnectionsReached))
.OrderBy(s => s.UserSubscriptionCount)
.FirstOrDefault();
}
if (connection != null)
return CallResult.Ok(connection);
if (maxConnectionsReached)
return CallResult.Fail<SocketConnection>(new InvalidOperationError("Max amount of socket connections reached"));
@@ -784,6 +784,23 @@ namespace CryptoExchange.Net.Clients
return CallResult.Ok(socketConnection);
}
private bool IsConnectionEligible(SocketConnection socketConnection, int individualSubscriptionCount, bool maxConnectionsReached)
{
var currentIndividualSubscriptionCount = socketConnection.Subscriptions.Sum(x => x.IndividualSubscriptionCount);
bool lessThanBatchSubCombineTarget = socketConnection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget;
// Include the incoming batch so batched subscriptions cannot overshoot the configured socket target.
bool lessThanIndividualSubCombineTarget = currentIndividualSubscriptionCount + individualSubscriptionCount <= ClientOptions.SocketIndividualSubscriptionCombineTarget;
if ((!lessThanBatchSubCombineTarget || !lessThanIndividualSubCombineTarget)
&& !maxConnectionsReached)
{
return false;
}
return MaxIndividualSubscriptionsPerConnection == null
|| currentIndividualSubscriptionCount + individualSubscriptionCount <= MaxIndividualSubscriptionsPerConnection;
}
/// <summary>
/// Process an unhandled message
/// </summary>
@@ -841,6 +858,8 @@ 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,
+100 -60
View File
@@ -14,21 +14,19 @@ using System.Text;
namespace CryptoExchange.Net.Clients
{
/// <inheritdoc />
public abstract class UserClientProvider<TRestClient, TSocketClient, TRestOptions, TSocketOptions, TCredentials, TEnvironment>
public abstract class UserClientProvider<TRestClient, TRestOptions, TCredentials, TEnvironment>
where TRestClient : IRestClient<TCredentials>
where TSocketClient : ISocketClient<TCredentials>
where TRestOptions : RestExchangeOptions<TEnvironment, TCredentials>, new()
where TSocketOptions : SocketExchangeOptions<TEnvironment, TCredentials>, new()
where TCredentials : ApiCredentials
where TEnvironment : TradeEnvironment
{
private ConcurrentDictionary<string, TRestClient> _restClients = new ConcurrentDictionary<string, TRestClient>();
private ConcurrentDictionary<string, TSocketClient> _socketClients = new ConcurrentDictionary<string, TSocketClient>();
private readonly IOptions<TRestOptions> _restOptions;
private readonly IOptions<TSocketOptions> _socketOptions;
private readonly HttpClient _httpClient;
private readonly ILoggerFactory? _loggerFactory;
/// <summary>
/// Logger factory
/// </summary>
protected readonly ILoggerFactory? _loggerFactory;
/// <inheritdoc />
public abstract string ExchangeName { get; }
@@ -39,17 +37,14 @@ namespace CryptoExchange.Net.Clients
public UserClientProvider(
HttpClient? httpClient,
ILoggerFactory? loggerFactory,
IOptions<TRestOptions> restOptions,
IOptions<TSocketOptions> socketOptions)
IOptions<TRestOptions> restOptions)
{
_httpClient = httpClient ?? new HttpClient();
_httpClient.Timeout = restOptions.Value.RequestTimeout;
_loggerFactory = loggerFactory;
_restOptions = restOptions;
_socketOptions = socketOptions;
}
private IOptions<TRestOptions> SetRestEnvironment(IOptions<TRestOptions> options, TEnvironment? environment)
{
if (environment == null)
@@ -61,22 +56,10 @@ namespace CryptoExchange.Net.Clients
return Options.Create(newRestClientOptions);
}
private IOptions<TSocketOptions> SetSocketEnvironment(IOptions<TSocketOptions> options, TEnvironment? environment)
{
if (environment == null)
return options;
var newSocketClientOptions = new TSocketOptions();
options.Value.Set(newSocketClientOptions);
newSocketClientOptions.Environment = environment;
return Options.Create(newSocketClientOptions);
}
/// <inheritdoc />
public void InitializeUserClient(string userIdentifier, TCredentials credentials, TEnvironment? environment = null)
public virtual void InitializeUserClient(string userIdentifier, TCredentials credentials, TEnvironment? environment = null)
{
CreateRestClient(userIdentifier, credentials, environment);
CreateSocketClient(userIdentifier, credentials, environment);
}
/// <inheritdoc />
@@ -88,15 +71,6 @@ namespace CryptoExchange.Net.Clients
return client;
}
/// <inheritdoc />
public TSocketClient GetSocketClient(string userIdentifier, TCredentials? credentials = null, TEnvironment? environment = null)
{
if (!_socketClients.TryGetValue(userIdentifier, out var client) || client.Disposed)
client = CreateSocketClient(userIdentifier, credentials, environment);
return client;
}
private TRestClient CreateRestClient(string userIdentifier, TCredentials? credentials, TEnvironment? environment)
{
var clientRestOptions = SetRestEnvironment(_restOptions, environment);
@@ -109,6 +83,94 @@ namespace CryptoExchange.Net.Clients
return client;
}
/// <summary>
/// Constructs a new instance of the rest client
/// </summary>
protected abstract TRestClient ConstructRestClient(
HttpClient client,
ILoggerFactory? loggerFactory,
IOptions<TRestOptions> options);
/// <inheritdoc />
public virtual void ClearUserClients(string userIdentifier)
{
_restClients.TryRemove(userIdentifier, out var restClient);
restClient?.Dispose();
}
/// <inheritdoc />
public virtual void Clear()
{
foreach (var client in _restClients.Values)
client.Dispose();
_restClients.Clear();
}
/// <summary>
/// Applies the provided options delegate to a new instance of the specified type.
/// </summary>
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T : new()
{
var opts = new T();
del?.Invoke(opts);
return opts;
}
}
/// <inheritdoc />
public abstract class UserClientProvider<TRestClient, TSocketClient, TRestOptions, TSocketOptions, TCredentials, TEnvironment>
: UserClientProvider<TRestClient, TRestOptions, TCredentials, TEnvironment>
where TRestClient : IRestClient<TCredentials>
where TSocketClient : ISocketClient<TCredentials>
where TRestOptions : RestExchangeOptions<TEnvironment, TCredentials>, new()
where TSocketOptions : SocketExchangeOptions<TEnvironment, TCredentials>, new()
where TCredentials : ApiCredentials
where TEnvironment : TradeEnvironment
{
private ConcurrentDictionary<string, TSocketClient> _socketClients = new ConcurrentDictionary<string, TSocketClient>();
private readonly IOptions<TSocketOptions> _socketOptions;
/// <summary>
/// ctor
/// </summary>
public UserClientProvider(
HttpClient? httpClient,
ILoggerFactory? loggerFactory,
IOptions<TRestOptions> restOptions,
IOptions<TSocketOptions> socketOptions)
: base(httpClient, loggerFactory, restOptions)
{
_socketOptions = socketOptions;
}
private IOptions<TSocketOptions> SetSocketEnvironment(IOptions<TSocketOptions> options, TEnvironment? environment)
{
if (environment == null)
return options;
var newSocketClientOptions = new TSocketOptions();
options.Value.Set(newSocketClientOptions);
newSocketClientOptions.Environment = environment;
return Options.Create(newSocketClientOptions);
}
/// <inheritdoc />
public override void InitializeUserClient(string userIdentifier, TCredentials credentials, TEnvironment? environment = null)
{
base.InitializeUserClient(userIdentifier, credentials, environment);
CreateSocketClient(userIdentifier, credentials, environment);
}
/// <inheritdoc />
public TSocketClient GetSocketClient(string userIdentifier, TCredentials? credentials = null, TEnvironment? environment = null)
{
if (!_socketClients.TryGetValue(userIdentifier, out var client) || client.Disposed)
client = CreateSocketClient(userIdentifier, credentials, environment);
return client;
}
private TSocketClient CreateSocketClient(string userIdentifier, TCredentials? credentials, TEnvironment? environment)
{
var clientSocketOptions = SetSocketEnvironment(_socketOptions, environment);
@@ -121,15 +183,6 @@ namespace CryptoExchange.Net.Clients
return client;
}
/// <summary>
/// Constructs a new instance of the rest client
/// </summary>
protected abstract TRestClient ConstructRestClient(
HttpClient client,
ILoggerFactory? loggerFactory,
IOptions<TRestOptions> options);
/// <summary>
/// Constructs a new instance of the socket client
/// </summary>
@@ -137,36 +190,23 @@ namespace CryptoExchange.Net.Clients
ILoggerFactory? loggerFactory,
IOptions<TSocketOptions> options);
/// <inheritdoc />
public void ClearUserClients(string userIdentifier)
public override void ClearUserClients(string userIdentifier)
{
_restClients.TryRemove(userIdentifier, out var restClient);
base.ClearUserClients(userIdentifier);
_socketClients.TryRemove(userIdentifier, out var socketClient);
restClient?.Dispose();
socketClient?.Dispose();
}
/// <inheritdoc />
public void Clear()
public override void Clear()
{
foreach (var client in _restClients.Values)
client.Dispose();
_restClients.Clear();
base.Clear();
foreach (var client in _socketClients.Values)
client.Dispose();
_socketClients.Clear();
}
/// <summary>
/// Applies the provided options delegate to a new instance of the specified type.
/// </summary>
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T : new()
{
var opts = new T();
del?.Invoke(opts);
return opts;
}
}
}
@@ -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));
}
@@ -198,6 +198,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
}
if (stringValue.EndsWith("+00:00:00"))
return DateTime.Parse(stringValue.Substring(0, stringValue.Length - 9), CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
}
@@ -88,7 +88,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
stream.Seek(0, SeekOrigin.Begin);
var written = new StreamReader(stream).ReadBlock(dataSnippet, 0, _errorResponseSnippetLimit);
var data = new string(dataSnippet, 0, written);
errorMsg += $": {data}";
errorMsg += $": {(string.IsNullOrEmpty(data) ? "(empty)" : data)}";
if (data.Length == _errorResponseSnippetLimit)
errorMsg += " (truncated)";
}
+3 -3
View File
@@ -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.1.0</PackageVersion>
<AssemblyVersion>12.1.0</AssemblyVersion>
<FileVersion>12.1.0</FileVersion>
<PackageVersion>13.0.0</PackageVersion>
<AssemblyVersion>13.0.0</AssemblyVersion>
<FileVersion>13.0.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>
+71 -10
View File
@@ -33,7 +33,7 @@ namespace CryptoExchange.Net
if (keyedCache != null && DateTime.UtcNow - keyedCache.UpdateTime < TimeSpan.FromMinutes(60))
return;
exchangeInfo.Set(key, new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol)));
exchangeInfo.Set(key, new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x)));
}
/// <summary>
@@ -118,6 +118,22 @@ namespace CryptoExchange.Net
return exchangeInfo.ParseSymbol(key, symbolName);
}
/// <summary>
/// Get a symbol catalog for a specific exchange(topic) and environment. Only available if <see cref="UpdateSymbolInfo(string, string, string?, SharedSpotSymbol[])"/> has been called previously.
/// </summary>
/// <param name="exchange">Exchange name</param>
/// <param name="topicId">Id for the provided data</param>
/// <param name="environmentName">Trade environment</param>
/// <param name="key">Additional data set identification key</param>
public static SharedSymbolCatalog? GetSymbolCatalog(string exchange, string topicId, string environmentName, string? key)
{
var id = topicId + environmentName;
if (!_symbolInfos.TryGetValue(id, out var exchangeInfo))
return null;
return exchangeInfo.GetSymbolCatalog(exchange, key);
}
class ExchangeKeyedCache
{
private ExchangeInfo? _noKeyCache;
@@ -163,7 +179,7 @@ namespace CryptoExchange.Net
public SharedSymbol? ParseSymbol(string? key, string symbolName)
{
SharedSymbol? symbolInfo = null;
SharedSpotSymbol? symbolInfo = null;
if (key == null)
{
if (_noKeyCache != null)
@@ -173,7 +189,7 @@ namespace CryptoExchange.Net
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
{
DeliverTime = symbolInfo.DeliverTime
DeliverTime = (symbolInfo as SharedFuturesSymbol)?.DeliveryTime
};
}
@@ -183,7 +199,7 @@ namespace CryptoExchange.Net
{
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
{
DeliverTime = symbolInfo.DeliverTime
DeliverTime = (symbolInfo as SharedFuturesSymbol)?.DeliveryTime
};
}
}
@@ -199,7 +215,7 @@ namespace CryptoExchange.Net
{
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
{
DeliverTime = symbolInfo.DeliverTime
DeliverTime = (symbolInfo as SharedFuturesSymbol)?.DeliveryTime
};
}
@@ -265,7 +281,7 @@ namespace CryptoExchange.Net
{
return _noKeyCache.Symbols
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
.Select(x => x.Value)
.Select(x => x.Value.SharedSymbol)
.ToArray();
}
@@ -274,7 +290,7 @@ namespace CryptoExchange.Net
{
result.AddRange(cache.Symbols
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
.Select(x => x.Value));
.Select(x => x.Value.SharedSymbol));
}
return result.ToArray();
@@ -286,18 +302,63 @@ namespace CryptoExchange.Net
return exchangeInfo.Symbols
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
.Select(x => x.Value)
.Select(x => x.Value.SharedSymbol)
.ToArray();
}
internal SharedSymbolCatalog? GetSymbolCatalog(string exchange, string? key)
{
IEnumerable<SharedSpotSymbol> cachedSymbols;
if (key == null)
{
if (_noKeyCache != null)
cachedSymbols = _noKeyCache.Symbols.Values;
else
cachedSymbols = _keyedCache.Values.SelectMany(x => x.Symbols.Values);
}
else
{
if (!_keyedCache.TryGetValue(key, out var exchangeInfo) || exchangeInfo == null)
return null;
cachedSymbols = exchangeInfo.Symbols.Values;
}
var assets = new Dictionary<string, SharedAssetInfo>();
var symbols = new Dictionary<string, SharedSpotSymbol>();
foreach (var symbol in cachedSymbols)
{
if (!assets.TryGetValue(symbol.BaseAsset, out var baseAssetInfo))
{
baseAssetInfo = new SharedAssetInfo(symbol.BaseAsset, symbol.BaseAssetType, symbol.BaseAssetSubType);
assets.Add(symbol.BaseAsset, baseAssetInfo);
}
if (!assets.TryGetValue(symbol.QuoteAsset, out var quoteAssetInfo))
{
quoteAssetInfo = new SharedAssetInfo(symbol.QuoteAsset, symbol.QuoteAssetType, symbol.QuoteAssetSubType);
assets.Add(symbol.QuoteAsset, quoteAssetInfo);
}
symbols.Add(symbol.Name, symbol);
}
return new SharedSymbolCatalog
{
Exchange = exchange,
Assets = assets,
Symbols = symbols
};
}
}
class ExchangeInfo
{
public DateTime UpdateTime { get; set; }
public Dictionary<string, SharedSymbol> Symbols { get; set; }
public Dictionary<string, SharedSpotSymbol> Symbols { get; set; }
public ExchangeInfo(DateTime updateTime, Dictionary<string, SharedSymbol> symbols)
public ExchangeInfo(DateTime updateTime, Dictionary<string, SharedSpotSymbol> symbols)
{
UpdateTime = updateTime;
Symbols = symbols;
+68 -8
View File
@@ -1,15 +1,20 @@
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;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Web;
namespace CryptoExchange.Net
@@ -145,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>
@@ -395,7 +417,6 @@ namespace CryptoExchange.Net
services.AddTransient(x => (IBookTickerRestClient)client(x)!);
if (typeof(ITransferRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ITransferRestClient)client(x)!);
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
if (typeof(ISpotSymbolRestClient).IsAssignableFrom(typeof(T)))
@@ -461,11 +482,15 @@ namespace CryptoExchange.Net
if (typeof(ISpotOrderSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotOrderSocketClient)client(x)!);
if (typeof(ISpotOrderManagementSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotOrderManagementSocketClient)client(x)!);
if (typeof(IFuturesOrderSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFuturesOrderSocketClient)client(x)!);
if (typeof(IPositionSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IPositionSocketClient)client(x)!);
if (typeof(IFuturesOrderManagementSocketClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IFuturesOrderManagementSocketClient)client(x)!);
return services;
}
@@ -489,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);
}
}
+118
View File
@@ -4,6 +4,8 @@ using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO.Pipelines;
using System.Linq;
using System.Net;
using System.Net.Http;
@@ -14,6 +16,61 @@ namespace CryptoExchange.Net
/// </summary>
public static class LibraryHelpers
{
private static readonly HashSet<string> _stableCoins = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
// USD
"USDT", "USDC", "DAI", "FDUSD", "USDE", "TUSD", "USDP", "PYUSD", "GUSD",
"USDD", "LUSD", "USDJ", "SUSD", "ZUSD", "BUSD", "USTC", "USDX", "USDK",
"CUSD", "USD1", "USD0", "XUSD", "BFUSD", "USDS", "RLUSD", "OUSD", "USDH",
"APXUSD", "USDQ", "USDPT", "FIDD", "AUSD",
// EUR
"EURS", "EURC", "EURI", "EURT", "AGEUR", "CEUR", "AEUR", "EURQ", "EUROP",
// Other
"CNYT", // CNY
"CREAL", "BRL1", // BRL
"XSGD", // SGD
"GYEN", // JPY
"KGST", // KGS
"QCAD", // CAD
"TGBP", // GBP
"AUDX", // AUD
"MXNB", // MXN
};
private static readonly HashSet<string> _commodities = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
// Metals
"XAU", "XAUT", "XAG", "XPT", "XPD", "COPPER", "PAXG", "XNI", "XCU", "XAL", "GOLD", "SILVER",
// Energy
"BZ", "NATGAS", "NGAS", "CL", "XTI", "UKOIL", "USOIL", "BRENTOIL"
};
private static readonly HashSet<string> _stocks = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
// Top stocks, will need to update periodically
"AAAU", "AADR", "AAPL", "ACWI", "ACWX", "AGG", "AMD", "AMLP", "AMZN", "ARKF",
"ARKG", "ARKK", "ARKQ", "ARKW", "AVGO", "BA", "BABA", "BND", "BNDX", "BOTZ",
"CIBR", "COIN", "DIA", "DIVB", "DVY", "EEM", "EFA", "EFAV", "ESGU", "EWG",
"EWJ", "EWT", "EWU", "EWW", "EWY", "EWZ", "FDN", "FEZ", "GLDM", "GOOGL",
"HDV", "HOOD", "HYG", "IAU", "IBB", "ICLN", "IEFA", "IEMG", "IGSB", "IJH",
"IJR", "INTC", "ITOT", "IUSB", "IUSG", "IUSV", "IWM", "IWO", "IWR", "IYR",
"JETS", "JPM", "LIT", "MCHI", "META", "MGK", "MSTR", "MTUM", "MU", "NET",
"NFLX", "NOBL", "NVDA", "OIH", "ORCL", "PAVE", "PBW", "PLTR", "QQQ", "QQQM",
"SCHB", "SCHD", "SCHF", "SCHG", "SCHH", "SCHV", "SCHX", "SKHY", "SPCX", "SPCXD",
"SPLG", "SPY", "SPYG", "SPYV", "SQQQ", "TSLA", "TSM", "TQQQ", "USMV", "VBR",
"VCIT", "VCSH", "VEA", "VEU", "VGIT", "VGK", "VGT", "VHT", "VIG", "VNQ",
"VOO", "VOT", "VTI", "VTV", "VUG", "VXUS", "XBI", "XLC", "XLE", "XLF",
"XLI", "XLK", "XLP", "XLU", "XLV", "XLY", "CSCO", "UBER", "MRVL", "RKLB",
"COHR", "SOXL", "HD", "DIS", "CBRS", "V", "BRKB", "FLNC", "LLY", "COST",
"ARM", "BMNR", "NBIS", "ASML", "AAOI", "GLW", "SHLD", "BE", "QNTX", "IBM",
"AMAT", "NOK", "ASTS", "BBX", "SLX", "SKHYNIX", "SAMSUNG", "HYUNDAI", "NVO",
"IREN", "ONDS", "CRM" , "VRT", "ZEST", "BTW", "HPE", "AXTI", "BX", "CRWD",
"CRDO", "NOW", "ZM", "DKNG", "RIVN", "URNM", "EBAY", "ADBE", "UVXY", "RDW",
"CIEN","PANW", "WIN", "PAYP", "HIMS", "CRWV", "QCOM", "LITE", "DRAM", "ANTHROPIC",
"OPENAI", "USAR", "BILL", "SNDK", "NASDAQ100", "SPX500", "BSB", "CRCL", "STRC",
"MSFT", "WDC"
};
private static ILogger? _staticLogger;
/// <summary>
/// Static logger
@@ -105,6 +162,67 @@ namespace CryptoExchange.Net
return _defaultClientReferences.TryGetValue(key, out var id) ? id : throw new KeyNotFoundException($"{exchange} not found in configuration");
}
/// <summary>
/// Check whether an asset is a known stablecoin. Note that this is not definitive, only large known stocks are checked
/// </summary>
/// <param name="asset">Asset name</param>
/// <param name="additionalStableCoins">Additional stablecoin names for the specific exchange</param>
public static bool IsStableCoin(string asset, params HashSet<string> additionalStableCoins)
{
if (string.IsNullOrEmpty(asset))
return false;
return _stableCoins.Contains(asset) || (additionalStableCoins != null && additionalStableCoins.Contains(asset, StringComparer.OrdinalIgnoreCase));
}
/// <summary>
/// Check whether an asset is a known commodity. Note that this is not definitive, only large known stocks are checked
/// </summary>
/// <param name="asset">Asset name</param>
/// <param name="additionalCommodities">Additional commodity names for the specific exchange</param>
public static bool IsCommodity(string asset, params HashSet<string> additionalCommodities)
{
if (string.IsNullOrEmpty(asset))
return false;
return _commodities.Contains(asset) || (additionalCommodities != null && additionalCommodities.Contains(asset, StringComparer.OrdinalIgnoreCase));
}
/// <summary>
/// Check whether an asset is a known stock. Note that this is not definitive, only large known stocks are checked
/// </summary>
/// <param name="asset">Asset name</param>
/// <param name="additionalStocks">Additional stock names for the specific exchange</param>
public static bool IsEquity(string asset, params HashSet<string> additionalStocks)
=> IsEquity(asset, [], additionalStocks);
/// <summary>
/// Check whether an asset is a known stock.
/// </summary>
/// <param name="asset">Asset name</param>
/// <param name="potentialSuffixes">Suffixes to check, for example when `X` is a potential suffix both `TSLA` and `TSLAX` will be checked</param>
/// <param name="additionalStocks">Additional stock names for the specific exchange</param>
public static bool IsEquity(string asset, string[] potentialSuffixes, params HashSet<string> additionalStocks)
{
if (string.IsNullOrEmpty(asset))
return false;
if (_stocks.Contains(asset) || (additionalStocks != null && additionalStocks.Contains(asset, StringComparer.OrdinalIgnoreCase)))
return true;
foreach (var suffix in potentialSuffixes)
{
if (!asset.EndsWith(suffix))
continue;
var suffixAsset = asset.Substring(0, asset.Length - suffix.Length);
if (_stocks.Contains(suffixAsset) || (additionalStocks != null && additionalStocks.Contains(suffixAsset, StringComparer.OrdinalIgnoreCase)))
return true;
}
return false;
}
/// <summary>
/// Create a new HttpMessageHandler instance
/// </summary>
@@ -22,6 +22,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, string, Exception?> _restApiCacheHit;
private static readonly Action<ILogger, string, Exception?> _restApiCacheNotHit;
private static readonly Action<ILogger, int?, Exception?> _restApiCancellationRequested;
private static readonly Action<ILogger, string, string, Exception?> _restApiRequestsJoined;
static RestApiClientLoggingExtensions()
{
@@ -89,7 +90,11 @@ namespace CryptoExchange.Net.Logging.Extensions
LogLevel.Debug,
new EventId(4012, "RestApiCancellationRequested"),
"[Req {RequestId}] request cancelled by user");
_restApiRequestsJoined = LoggerMessage.Define<string, string>(
LogLevel.Debug,
new EventId(4013, "RestApiRequestsJoined"),
"Request for {Method} {Path} joined with in flight request");
}
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error, string? originalData, Exception? exception)
@@ -151,9 +156,15 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_restApiCacheNotHit(logger, key, null);
}
public static void RestApiCancellationRequested(this ILogger logger, int? requestId)
{
_restApiCancellationRequested(logger, requestId, null);
}
public static void RestApiRequestsJoined(this ILogger logger, string method, string path)
{
_restApiRequestsJoined(logger, method, path, null);
}
}
}
@@ -180,7 +180,7 @@ namespace CryptoExchange.Net.Logging.Extensions
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string, string>(
LogLevel.Warning,
new EventId(2029, "ReceivedMessageNotMatchedToAnyListener"),
"[Sckt {SocketId}] received message not matched to any listener. TypeIdentifier: {TypeIdentifier}, ListenId: {ListenId}, current listeners: [{ListenIds}]");
"[Sckt {SocketId}] received message not matched to any listener. TypeIdentifier: {TypeIdentifier}, TopicFilter: {ListenId}, registered TopicFilters for type: [{TopicFilters}]");
_failedToParse = LoggerMessage.Define<int, string>(
LogLevel.Warning,
@@ -326,9 +326,9 @@ namespace CryptoExchange.Net.Logging.Extensions
_sendingData(logger, socketId, requestId, data, null);
}
public static void ReceivedMessageNotMatchedToAnyListener(this ILogger logger, int socketId, string typeIdentifier, string listenId, string listenIds)
public static void ReceivedMessageNotMatchedToAnyListener(this ILogger logger, int socketId, string typeIdentifier, string topicFilter, string topicFilters)
{
_receivedMessageNotMatchedToAnyListener(logger, socketId, typeIdentifier, listenId, listenIds, null);
_receivedMessageNotMatchedToAnyListener(logger, socketId, typeIdentifier, topicFilter, topicFilters, null);
}
public static void SendingByteData(this ILogger logger, int socketId, int requestId, int length)
@@ -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()
{
@@ -34,6 +34,11 @@ namespace CryptoExchange.Net.Objects.Options
= new Version(1, 1);
#endif
/// <summary>
/// Whether concurrent identical REST requests may share the same in-flight request when allowed by the request definition.
/// </summary>
public bool RequestCoalescingEnabled { get; set; } = true;
/// <summary>
/// Http client keep alive interval for keeping connections open. Only applied when using dotnet8.0 or higher and dependency injection
/// </summary>
@@ -70,9 +75,11 @@ 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;
item.RequestCoalescingEnabled = RequestCoalescingEnabled;
item.HttpKeepAliveInterval = HttpKeepAliveInterval;
#if NET5_0_OR_GREATER
item.HttpMaxConnectionsPerServer = HttpMaxConnectionsPerServer;
@@ -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;
}
@@ -0,0 +1,277 @@
using CryptoExchange.Net.Logging.Extensions;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Request coalescer, used to detect multiple identical requests and wait for and return only a single actual request result
/// </summary>
public class RequestCoalescer
{
private sealed class RequestEntry
{
public RequestCoalescingKey Key { get; }
public CancellationTokenSource CancellationSource { get; } = new();
public Lazy<Task<object>> Request { get; set; } = null!;
public int ListenerCount { get; set; }
public bool Completed { get; set; }
public bool CancellationInProgress { get; set; }
public bool Disposed { get; set; }
public RequestEntry(RequestCoalescingKey key)
{
Key = key;
}
}
private readonly object _sync = new();
private readonly ILogger _logger;
private readonly Dictionary<RequestCoalescingKey, RequestEntry> _requests = new();
/// <summary>
/// ctor
/// </summary>
public RequestCoalescer(ILogger logger)
{
_logger = logger;
}
/// <summary>
/// Execute a request, coalescing identical requests into a single request
/// </summary>
public Task<TResult> ExecuteAsync<TResult>(
RequestCoalescingKey key,
Func<CancellationToken, Task<TResult>> requestFactory,
Func<TResult> cancellationResultFactory,
CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
return Task.FromResult(cancellationResultFactory());
RequestEntry entry;
bool joined;
lock (_sync)
{
joined = _requests.TryGetValue(key, out entry!);
if (!joined)
{
entry = CreateEntry(key, requestFactory);
_requests.Add(key, entry);
}
entry.ListenerCount++;
}
if (joined)
_logger.RestApiRequestsJoined(key.Method, key.Url);
return WaitAsync(
entry,
cancellationResultFactory,
cancellationToken);
}
private RequestEntry CreateEntry<TResult>(
RequestCoalescingKey key,
Func<CancellationToken, Task<TResult>> requestFactory)
{
var entry = new RequestEntry(key);
entry.Request = new Lazy<Task<object>>(
() => ExecuteCoreAsync(
entry,
async requestCancellationToken =>
(await requestFactory(requestCancellationToken).ConfigureAwait(false))!),
LazyThreadSafetyMode.ExecutionAndPublication);
return entry;
}
private async Task<object> ExecuteCoreAsync(
RequestEntry entry,
Func<CancellationToken, Task<object>> requestFactory)
{
try
{
return await requestFactory(entry.CancellationSource.Token).ConfigureAwait(false);
}
finally
{
CompleteRequest(entry);
}
}
private async Task<TResult> WaitAsync<TResult>(
RequestEntry entry,
Func<TResult> cancellationResultFactory,
CancellationToken cancellationToken)
{
try
{
var requestTask = entry.Request.Value;
if (!cancellationToken.CanBeCanceled)
return (TResult)await requestTask.ConfigureAwait(false);
var cancellationCompletion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
using (cancellationToken.Register(
state => ((TaskCompletionSource<bool>)state!).TrySetResult(true),
cancellationCompletion))
{
var completedTask = await Task.WhenAny(requestTask, cancellationCompletion.Task).ConfigureAwait(false);
if (completedTask != requestTask)
return cancellationResultFactory();
}
return (TResult)await requestTask.ConfigureAwait(false);
}
finally
{
ReleaseListener(entry);
}
}
private void ReleaseListener(RequestEntry entry)
{
var cancelRequest = false;
var disposeCancellationSource = false;
lock (_sync)
{
entry.ListenerCount--;
if (entry.ListenerCount == 0)
{
if (!entry.Completed)
{
// Remove the entry before canceling it. A caller arriving
// after this point must create a new physical request
// instead of joining one that is being canceled.
RemoveEntry(entry);
entry.CancellationInProgress = true;
cancelRequest = true;
}
else if (!entry.Disposed)
{
entry.Disposed = true;
disposeCancellationSource = true;
}
}
}
if (cancelRequest)
{
try
{
entry.CancellationSource.Cancel();
}
catch (Exception exception)
{
// Cancellation callbacks are external to the coalescer and
// must not cause an exception to escape the library.
_logger.LogWarning(
exception,
"Error canceling in-flight request {Method} {Url}",
entry.Key.Method,
entry.Key.Url);
}
finally
{
CompleteCancellation(entry);
}
}
else if (disposeCancellationSource)
{
entry.CancellationSource.Dispose();
}
}
private void CompleteCancellation(RequestEntry entry)
{
var disposeCancellationSource = false;
lock (_sync)
{
entry.CancellationInProgress = false;
if (entry.Completed
&& entry.ListenerCount == 0
&& !entry.Disposed)
{
entry.Disposed = true;
disposeCancellationSource = true;
}
}
if (disposeCancellationSource)
entry.CancellationSource.Dispose();
}
private void CompleteRequest(RequestEntry entry)
{
var disposeCancellationSource = false;
lock (_sync)
{
entry.Completed = true;
// Only remove this specific entry. If all listeners canceled,
// a replacement request may already have been created for the
// same key.
RemoveEntry(entry);
if (entry.ListenerCount == 0
&& !entry.CancellationInProgress
&& !entry.Disposed)
{
entry.Disposed = true;
disposeCancellationSource = true;
}
}
if (disposeCancellationSource)
entry.CancellationSource.Dispose();
}
/// <summary>
/// Remove an entry when it is still the current entry for its key.
/// Must only be called while holding <see cref="_sync"/>.
/// </summary>
private void RemoveEntry(RequestEntry entry)
{
if (_requests.TryGetValue(entry.Key, out var current)
&& ReferenceEquals(current, entry))
{
_requests.Remove(entry.Key);
}
}
}
/// <summary>
/// Key used to identify identical requests for coalescing
/// </summary>
/// <param name="Method">Request method</param>
/// <param name="Url">Request URL</param>
/// <param name="Parameters">Request parameter string</param>
/// <param name="ResponseType">Response type</param>
public sealed record RequestCoalescingKey(
string Method,
string Url,
string? Parameters,
Type ResponseType);
}
@@ -101,6 +101,11 @@ namespace CryptoExchange.Net.Objects
}
}
/// <summary>
/// Whether concurrent identical requests should be prevented from sharing the same request/response
/// </summary>
public bool PreventRequestCoalescing { get; set; }
/// <summary>
/// ctor
/// </summary>
@@ -53,6 +53,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
/// <param name="forcePathEndWithSlash">Force trailing `/`</param>
/// <param name="identifier">Optional request identifier override</param>
/// <param name="preventRequestCoalescing">Whether concurrent identical requests should be prevented from sharing the same request/response</param>
/// <returns></returns>
public RequestDefinition GetOrCreate(
HttpMethod method,
@@ -68,7 +69,8 @@ namespace CryptoExchange.Net.Objects
bool? preventCaching = null,
bool? tryParseOnNonSuccess = null,
bool? forcePathEndWithSlash = null,
string? identifier = null)
string? identifier = null,
bool? preventRequestCoalescing = null)
{
var identifierToUse = identifier ?? $"{path}{method.Method}{baseAddress}";
if (!_definitions.TryGetValue(identifierToUse, out var def))
@@ -85,6 +87,7 @@ namespace CryptoExchange.Net.Objects
PreventCaching = preventCaching ?? false,
TryParseOnNonSuccess = tryParseOnNonSuccess ?? false,
ForcePathEndWithSlash = forcePathEndWithSlash ?? false,
PreventRequestCoalescing = preventRequestCoalescing ?? false,
};
_definitions.TryAdd(identifierToUse, def);
}
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
@@ -8,8 +9,11 @@ namespace CryptoExchange.Net.Objects;
/// <summary>
/// Call result
/// </summary>
[DebuggerDisplay("{DebugView,nq}")]
public record CallResult : ICallResult
{
private string DebugView => Success ? "Success" : $"Error: {Error}";
private static CallResult _successResult = new CallResult();
/// <inheritdoc />
@@ -93,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
@@ -1,6 +1,7 @@
using CryptoExchange.Net.SharedApis;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http;
@@ -12,8 +13,11 @@ namespace CryptoExchange.Net.Objects;
/// <summary>
/// HTTP call result
/// </summary>
[DebuggerDisplay("{DebugView,nq}")]
public record HttpResult : IHttpResult
{
private string DebugView => $"[Req {RequestId}] " + (Success ? "Success" : $"Error: {Error}");
/// <summary>
/// Create a new success HTTP result
/// </summary>
@@ -251,8 +255,32 @@ public record HttpResult : IHttpResult
/// <inheritdoc />
[DebuggerDisplay("{DebugView,nq}")]
public record HttpResult<T> : HttpResult, IHttpResult<T>
{
private string DebugView
{
get
{
var result = new StringBuilder($"[Req {RequestId}] " + (Success ? "Success" : $"Error: {Error}"));
if (Data != null)
{
result.Append(", ");
var typeName = typeof(T).Name;
if (Data is Array ar)
{
result.Append($"{ar.Length} {typeName.Substring(0, typeName.Length - 2)}");
}
else
{
result.Append(typeName);
}
}
return result.ToString();
}
}
/// <summary>
/// ctor
/// </summary>
@@ -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,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
@@ -8,8 +9,11 @@ namespace CryptoExchange.Net.Objects;
/// <summary>
/// WebSocket call result
/// </summary>
[DebuggerDisplay("{DebugView,nq}")]
public record WebSocketResult : IWebSocketResult
{
private string DebugView => $"[Sckt {ConnectionId}] " + (RequestId == null ? "" : $"[Req {RequestId}] ") + (Success ? "Success" : $"Error: {Error}");
/// <summary>
/// ctor
/// </summary>
@@ -183,7 +187,7 @@ public record WebSocketResult<T> : WebSocketResult, IWebSocketResult<T>
}
/// <inheritdoc />
public record QueryResult : WebSocketResult
public record QueryResult : WebSocketResult, IQueryResult
{
/// <summary>
/// ctor
@@ -208,6 +212,8 @@ public record QueryResult : WebSocketResult
Url = result.Url,
RequestId = result.RequestId,
ResponseTime = result.ResponseTime,
OriginalData = result.OriginalData,
RequestBody = result.RequestBody
};
/// <summary>
@@ -288,6 +294,9 @@ public record QueryResult : WebSocketResult
/// <inheritdoc />
public string? RequestBody { get; init; }
/// <inheritdoc />
public string? OriginalData { get; init; }
}
/// <inheritdoc />
@@ -314,7 +323,4 @@ public record QueryResult<T> : QueryResult, IQueryResult<T>
public new bool Success => Error == null;
/// <inheritdoc />
public T? Data { get; set; }
/// <inheritdoc />
public string? OriginalData { get; init; }
}
@@ -165,7 +165,10 @@ namespace CryptoExchange.Net.Objects.Sockets
{
StreamId = StreamId,
UpdateType = UpdateType,
Symbol = Symbol
Symbol = Symbol,
DataTime = DataTime,
DataTimeLocal = DataTimeLocal,
SequenceNumber = SequenceNumber
};
}
@@ -0,0 +1,209 @@
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Routing;
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Objects.Sockets
{
/// <summary>
/// Controller for an update subscription which isn't backed by a websocket connection. Can be used for testing.
/// </summary>
public class ManualUpdateSubscription
{
private readonly Func<Task> _closeAsync;
private readonly Func<Task> _reconnectAsync;
private readonly Func<Task<CallResult>> _resubscribeAsync;
private readonly ManualSubscription _manualSubscription;
private int _closedEventInvoked;
/// <summary>
/// The update subscription
/// </summary>
public UpdateSubscription Subscription { get; }
/// <summary>
/// The virtual socket id
/// </summary>
public int SocketId { get; }
/// <summary>
/// The last timestamp anything was received by the subscription
/// </summary>
public DateTime? LastReceiveTime { get; private set; }
/// <summary>
/// The current virtual websocket status
/// </summary>
public SocketStatus SocketStatus { get; private set; }
/// <summary>
/// Create a manually controlled update subscription
/// </summary>
/// <param name="socketId">The virtual socket id</param>
/// <param name="closeAsync">Callback when the subscription is closed</param>
/// <param name="reconnectAsync">Callback when a reconnect is requested</param>
/// <param name="resubscribeAsync">Callback when a resubscribe is requested</param>
public ManualUpdateSubscription(
int socketId = 0,
Func<Task>? closeAsync = null,
Func<Task>? reconnectAsync = null,
Func<Task<CallResult>>? resubscribeAsync = null)
{
SocketId = socketId;
SocketStatus = SocketStatus.Connected;
_closeAsync = closeAsync ?? (() => Task.CompletedTask);
_reconnectAsync = reconnectAsync ?? (() => Task.CompletedTask);
_resubscribeAsync = resubscribeAsync ?? (() => Task.FromResult(CallResult.Ok()));
_manualSubscription = new ManualSubscription();
_manualSubscription.Status = SubscriptionStatus.Subscribed;
Subscription = new UpdateSubscription(this, _manualSubscription);
}
/// <summary>
/// Set the last timestamp anything was received by the subscription
/// </summary>
/// <param name="timestamp">The receive timestamp</param>
public void SetLastReceiveTime(DateTime? timestamp)
{
LastReceiveTime = timestamp;
}
/// <summary>
/// Set the virtual websocket status
/// </summary>
/// <param name="status">The status</param>
public void SetSocketStatus(SocketStatus status)
{
SocketStatus = status;
}
/// <summary>
/// Set the subscription status
/// </summary>
/// <param name="status">The status</param>
public void SetSubscriptionStatus(SubscriptionStatus status)
{
_manualSubscription.Status = status;
}
/// <summary>
/// Invoke the connection lost event
/// </summary>
public void InvokeConnectionLost()
{
Subscription.HandleConnectionLostEvent();
}
/// <summary>
/// Invoke the connection restored event
/// </summary>
/// <param name="disconnectedPeriod">The period the connection was disconnected</param>
public void InvokeConnectionRestored(TimeSpan disconnectedPeriod)
{
Subscription.HandleConnectionRestoredEvent(disconnectedPeriod);
}
/// <summary>
/// Invoke the connection closed event
/// </summary>
public void InvokeConnectionClosed()
{
if (Interlocked.Exchange(ref _closedEventInvoked, 1) != 0)
return;
SocketStatus = SocketStatus.Closed;
_manualSubscription.Status = SubscriptionStatus.Closed;
Subscription.HandleConnectionClosedEvent();
}
/// <summary>
/// Invoke the resubscribing failed event
/// </summary>
/// <param name="error">The resubscribe error</param>
public void InvokeResubscribingFailed(Error error)
{
if (error == null)
throw new ArgumentNullException(nameof(error));
Subscription.HandleResubscribeFailedEvent(error);
}
/// <summary>
/// Invoke the activity paused event
/// </summary>
public void InvokeActivityPaused()
{
Subscription.HandlePausedEvent();
}
/// <summary>
/// Invoke the activity unpaused event
/// </summary>
public void InvokeActivityUnpaused()
{
Subscription.HandleUnpausedEvent();
}
/// <summary>
/// Invoke the exception event
/// </summary>
/// <param name="exception">The exception</param>
public void InvokeException(Exception exception)
{
if (exception == null)
throw new ArgumentNullException(nameof(exception));
_manualSubscription.InvokeExceptionHandler(exception);
}
internal async Task CloseAsync()
{
if (_manualSubscription.Status == SubscriptionStatus.Closed
|| _manualSubscription.Status == SubscriptionStatus.Closing)
return;
_manualSubscription.Status = SubscriptionStatus.Closing;
try
{
await _closeAsync().ConfigureAwait(false);
}
finally
{
_manualSubscription.Status = SubscriptionStatus.Closed;
}
}
internal Task ReconnectAsync()
{
return _reconnectAsync();
}
internal Task<CallResult> ResubscribeAsync()
{
return _resubscribeAsync();
}
private class ManualSubscription : Subscription
{
public ManualSubscription()
: base(NullLogger.Instance, false)
{
MessageRouter = MessageRouter.Create();
}
protected override Query? GetSubQuery(SocketConnection connection)
{
return null;
}
protected override Query? GetUnsubQuery(SocketConnection connection)
{
return null;
}
}
}
}
@@ -12,7 +12,8 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
public class UpdateSubscription
{
private readonly SocketConnection _connection;
private readonly SocketConnection? _connection;
private readonly ManualUpdateSubscription? _manualSubscription;
internal readonly Subscription _subscription;
#if NET9_0_OR_GREATER
@@ -102,7 +103,7 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <summary>
/// The id of the socket
/// </summary>
public int SocketId => _connection.SocketId;
public int SocketId => _connection?.SocketId ?? _manualSubscription!.SocketId;
/// <summary>
/// The id of the subscription
@@ -112,12 +113,12 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <summary>
/// The last timestamp anything was received from the server
/// </summary>
public DateTime? LastReceiveTime => _connection.LastReceiveTime;
public DateTime? LastReceiveTime => _connection?.LastReceiveTime ?? _manualSubscription!.LastReceiveTime;
/// <summary>
/// The current websocket status
/// </summary>
public SocketStatus SocketStatus => _connection.Status;
public SocketStatus SocketStatus => _connection?.Status ?? _manualSubscription!.SocketStatus;
/// <summary>
/// The current subscription status
@@ -143,6 +144,18 @@ namespace CryptoExchange.Net.Objects.Sockets
_subscription.StatusChanged += (x) => SubscriptionStatusChanged?.Invoke(x);
}
/// <summary>
/// ctor
/// </summary>
/// <param name="manualSubscription">The manual subscription for controlling events and data</param>
/// <param name="subscription">The subscription</param>
internal UpdateSubscription(ManualUpdateSubscription manualSubscription, Subscription subscription)
{
_manualSubscription = manualSubscription;
_subscription = subscription;
_subscription.StatusChanged += (x) => SubscriptionStatusChanged?.Invoke(x);
}
private void UnsubscribeConnectionEvents()
{
lock (_eventLock)
@@ -150,22 +163,26 @@ namespace CryptoExchange.Net.Objects.Sockets
if (!_connectionEventsSubscribed)
return;
_connection.ConnectionClosed -= HandleConnectionClosedEvent;
_connection.ConnectionLost -= HandleConnectionLostEvent;
_connection.ConnectionRestored -= HandleConnectionRestoredEvent;
_connection.ResubscribingFailed -= HandleResubscribeFailedEvent;
_connection.ActivityPaused -= HandlePausedEvent;
_connection.ActivityUnpaused -= HandleUnpausedEvent;
if (_connection != null)
{
_connection.ConnectionClosed -= HandleConnectionClosedEvent;
_connection.ConnectionLost -= HandleConnectionLostEvent;
_connection.ConnectionRestored -= HandleConnectionRestoredEvent;
_connection.ResubscribingFailed -= HandleResubscribeFailedEvent;
_connection.ActivityPaused -= HandlePausedEvent;
_connection.ActivityUnpaused -= HandleUnpausedEvent;
}
_connectionEventsSubscribed = false;
}
}
private void HandleConnectionClosedEvent()
internal void HandleConnectionClosedEvent()
{
UnsubscribeConnectionEvents();
// If we're not the subscription closing this connection don't bother emitting
if (!_subscription.IsClosingConnection)
if (_connection != null && !_subscription.IsClosingConnection)
return;
List<Action> handlers;
@@ -176,7 +193,7 @@ namespace CryptoExchange.Net.Objects.Sockets
callback();
}
private void HandleConnectionLostEvent()
internal void HandleConnectionLostEvent()
{
if (!_subscription.Active)
{
@@ -192,7 +209,7 @@ namespace CryptoExchange.Net.Objects.Sockets
callback();
}
private void HandleConnectionRestoredEvent(TimeSpan period)
internal void HandleConnectionRestoredEvent(TimeSpan period)
{
if (!_subscription.Active)
{
@@ -208,7 +225,7 @@ namespace CryptoExchange.Net.Objects.Sockets
callback(period);
}
private void HandleResubscribeFailedEvent(Error error)
internal void HandleResubscribeFailedEvent(Error error)
{
if (!_subscription.Active)
{
@@ -224,7 +241,7 @@ namespace CryptoExchange.Net.Objects.Sockets
callback(error);
}
private void HandlePausedEvent()
internal void HandlePausedEvent()
{
if (!_subscription.Active)
{
@@ -240,7 +257,7 @@ namespace CryptoExchange.Net.Objects.Sockets
callback();
}
private void HandleUnpausedEvent()
internal void HandleUnpausedEvent()
{
if (!_subscription.Active)
{
@@ -262,7 +279,10 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <returns></returns>
public Task CloseAsync()
{
return _connection.CloseAsync(_subscription);
if (_connection != null)
return _connection.CloseAsync(_subscription);
return _manualSubscription!.CloseAsync();
}
/// <summary>
@@ -271,7 +291,10 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <returns></returns>
public Task ReconnectAsync()
{
return _connection.TriggerReconnectAsync();
if (_connection != null)
return _connection.TriggerReconnectAsync();
return _manualSubscription!.ReconnectAsync();
}
/// <summary>
@@ -280,7 +303,13 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <returns></returns>
internal async Task UnsubscribeAsync()
{
await _connection.UnsubscribeAsync(_subscription).ConfigureAwait(false);
if (_connection != null)
{
await _connection.UnsubscribeAsync(_subscription).ConfigureAwait(false);
return;
}
await _manualSubscription!.CloseAsync().ConfigureAwait(false);
}
/// <summary>
@@ -289,7 +318,10 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <returns></returns>
internal async Task<CallResult> ResubscribeAsync()
{
return await _connection.ResubscribeAsync(_subscription).ConfigureAwait(false);
if (_connection != null)
return await _connection.ResubscribeAsync(_subscription).ConfigureAwait(false);
return await _manualSubscription!.ResubscribeAsync().ConfigureAwait(false);
}
}
}
@@ -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>
@@ -289,6 +289,7 @@ namespace CryptoExchange.Net.OrderBook
if (!startResult.Success)
{
Status = OrderBookStatus.Disconnected;
_queueEvent.Set();
return CallResult.Fail(startResult.Error!);
}
@@ -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;
}
}
}
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Catalog of Shared API capability references.
/// </summary>
public static partial class SharedCapabilities
{
/// <summary>
/// Asset capabilities.
/// </summary>
public static class Assets
{
/// <summary>
/// Get asset capability.
/// </summary>
public static SharedRestCapability<IGetAsset, IGetAssetRest> GetAsset { get; } = new();
/// <summary>
/// Get all assets capability.
/// </summary>
public static SharedRestCapability<IGetAllAssets, IGetAllAssetsRest> GetAllAssets { get; } = new();
}
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Balance capabilities.
/// </summary>
public static class Balances
{
/// <summary>
/// Get balances capability.
/// </summary>
public static SharedRestCapability<IGetBalances, IGetBalancesRest> GetBalances { get; } = new();
/// <summary>
/// Subscribe to balance updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeBalancesSocket> SubscribeBalances { get; } = new();
}
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Deposit capabilities.
/// </summary>
public static class Deposits
{
/// <summary>
/// Get deposit addresses capability.
/// </summary>
public static SharedRestCapability<IGetDepositAddresses, IGetDepositAddressesRest> GetDepositAddresses { get; } = new();
/// <summary>
/// Get deposit history capability.
/// </summary>
public static SharedRestCapability<IGetDepositHistory, IGetDepositHistoryRest> GetDepositHistory { get; } = new();
}
}
}
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Fee capabilities.
/// </summary>
public static class Fees
{
/// <summary>
/// Get fees capability.
/// </summary>
public static SharedRestCapability<IGetFees, IGetFeesRest> GetFees { get; } = new();
}
}
}
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Funding capabilities.
/// </summary>
public static class Funding
{
/// <summary>
/// Get current funding information capability.
/// </summary>
public static SharedRestCapability<IGetFundingInfo, IGetFundingInfoRest> GetFundingInfo { get; } = new();
/// <summary>
/// Get funding rate history capability.
/// </summary>
public static SharedRestCapability<IGetFundingRateHistory, IGetFundingRateHistoryRest> GetFundingRateHistory { get; } = new();
/// <summary>
/// Get user funding history capability.
/// </summary>
public static SharedRestCapability<IGetUserFundingHistory, IGetUserFundingHistoryRest> GetUserFundingHistory { get; } = new();
}
}
}
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Index price capabilities.
/// </summary>
public static class IndexPrices
{
/// <summary>
/// Get an index price capability.
/// </summary>
public static SharedRestCapability<IGetIndexPrice, IGetIndexPriceRest> GetIndexPrice { get; } = new();
/// <summary>
/// Get all index prices capability.
/// </summary>
public static SharedRestCapability<IGetAllIndexPrices, IGetAllIndexPricesRest> GetAllIndexPrices { get; } = new();
/// <summary>
/// Subscribe to index price updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeIndexPriceSocket> SubscribeIndexPrice { get; } = new();
}
}
}
@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Kline capabilities.
/// </summary>
public static class Klines
{
/// <summary>
/// Get klines capability.
/// </summary>
public static SharedRestCapability<IGetKlines, IGetKlinesRest> GetKlines { get; } = new();
/// <summary>
/// Get mark price klines capability.
/// </summary>
public static SharedRestCapability<IGetMarkPriceKlines, IGetMarkPriceKlinesRest> GetMarkPriceKlines { get; } = new();
/// <summary>
/// Get index price klines capability.
/// </summary>
public static SharedRestCapability<IGetIndexPriceKlines, IGetIndexPriceKlinesRest> GetIndexPriceKlines { get; } = new();
/// <summary>
/// Subscribe to kline updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeKlinesSocket> SubscribeKlines { get; } = new();
}
}
}
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Ledger capabilities.
/// </summary>
public static class Ledger
{
/// <summary>
/// Get ledger entries capability.
/// </summary>
public static SharedRestCapability<IGetLedger, IGetLedgerRest> GetLedger { get; } = new();
}
}
}
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Leverage capabilities.
/// </summary>
public static class Leverage
{
/// <summary>
/// Get leverage capability.
/// </summary>
public static SharedRestCapability<IGetLeverage, IGetLeverageRest> GetLeverage { get; } = new();
/// <summary>
/// Set leverage capability.
/// </summary>
public static SharedRestCapability<ISetLeverage, ISetLeverageRest> SetLeverage { get; } = new();
/// <summary>
/// Get leverage tiers capability.
/// </summary>
public static SharedRestCapability<IGetLeverageTiers, IGetLeverageTiersRest> GetLeverageTiers { get; } = new();
}
}
}
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Mark price capabilities.
/// </summary>
public static class MarkPrices
{
/// <summary>
/// Get a mark price capability.
/// </summary>
public static SharedRestCapability<IGetMarkPrice, IGetMarkPriceRest> GetMarkPrice { get; } = new();
/// <summary>
/// Get all mark prices capability.
/// </summary>
public static SharedRestCapability<IGetAllMarkPrices, IGetAllMarkPricesRest> GetAllMarkPrices { get; } = new();
/// <summary>
/// Subscribe to mark price updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeMarkPriceSocket> SubscribeMarkPrice { get; } = new();
}
}
}
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Open interest capabilities.
/// </summary>
public static class OpenInterest
{
/// <summary>
/// Get open interest capability.
/// </summary>
public static SharedRestCapability<IGetOpenInterest, IGetOpenInterestRest> GetOpenInterest { get; } = new();
}
}
}
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Order book capabilities.
/// </summary>
public static class OrderBooks
{
/// <summary>
/// Get an order book capability.
/// </summary>
public static SharedRestCapability<IGetOrderBook, IGetOrderBookRest> GetOrderBook { get; } = new();
/// <summary>
/// Get a book ticker capability.
/// </summary>
public static SharedRestCapability<IGetBookTicker, IGetBookTickerRest> GetBookTicker { get; } = new();
/// <summary>
/// Subscribe to order book updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeOrderBookSocket> SubscribeOrderBook { get; } = new();
/// <summary>
/// Subscribe to incremental order book updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeIncrementalOrderBookSocket> SubscribeIncrementalOrderBook { get; } = new();
}
}
}
@@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Order capabilities.
/// </summary>
public static class Orders
{
/// <summary>
/// Spot order capabilities.
/// </summary>
public static class Spot
{
/// <summary>
/// Place a spot order capability.
/// </summary>
public static SharedRestSocketCapability<IPlaceSpotOrder, IPlaceSpotOrderRest, IPlaceSpotOrderSocket> PlaceOrder { get; } = new();
/// <summary>
/// Place multiple spot orders capability.
/// </summary>
public static SharedRestSocketCapability<IPlaceMultipleSpotOrders, IPlaceMultipleSpotOrdersRest, IPlaceMultipleSpotOrdersSocket> PlaceMultipleOrders { get; } = new();
/// <summary>
/// Edit a spot order capability.
/// </summary>
public static SharedRestSocketCapability<IEditSpotOrder, IEditSpotOrderRest, IEditSpotOrderSocket> EditOrder { get; } = new();
/// <summary>
/// Edit a spot order by client order id capability.
/// </summary>
public static SharedRestSocketCapability<IEditSpotOrderByClientOrderId, IEditSpotOrderByClientOrderIdRest, IEditSpotOrderByClientOrderIdSocket> EditOrderByClientOrderId { get; } = new();
/// <summary>
/// Cancel a spot order capability.
/// </summary>
public static SharedRestSocketCapability<ICancelSpotOrder, ICancelSpotOrderRest, ICancelSpotOrderSocket> CancelOrder { get; } = new();
/// <summary>
/// Cancel a spot order by client order id capability.
/// </summary>
public static SharedRestSocketCapability<ICancelSpotOrderByClientOrderId, ICancelSpotOrderByClientOrderIdRest, ICancelSpotOrderByClientOrderIdSocket> CancelOrderByClientOrderId { get; } = new();
/// <summary>
/// Cancel all spot orders capability.
/// </summary>
public static SharedRestSocketCapability<ICancelAllSpotOrders, ICancelAllSpotOrdersRest, ICancelAllSpotOrdersSocket> CancelAllOrders { get; } = new();
/// <summary>
/// Cancel all spot orders for a symbol capability.
/// </summary>
public static SharedRestSocketCapability<ICancelAllSpotSymbolOrders, ICancelAllSpotSymbolOrdersRest, ICancelAllSpotSymbolOrdersSocket> CancelAllSymbolOrders { get; } = new();
/// <summary>
/// Get a spot order capability.
/// </summary>
public static SharedRestCapability<IGetSpotOrder, IGetSpotOrderRest> GetOrder { get; } = new();
/// <summary>
/// Get a spot order by client order id capability.
/// </summary>
public static SharedRestCapability<IGetSpotOrderByClientOrderId, IGetSpotOrderByClientOrderIdRest> GetOrderByClientOrderId { get; } = new();
/// <summary>
/// Get open spot orders capability.
/// </summary>
public static SharedRestCapability<IGetOpenSpotOrders, IGetOpenSpotOrdersRest> GetOpenOrders { get; } = new();
/// <summary>
/// Get closed spot orders capability.
/// </summary>
public static SharedRestCapability<IGetClosedSpotOrders, IGetClosedSpotOrdersRest> GetClosedOrders { get; } = new();
/// <summary>
/// Subscribe to spot order updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeSpotOrdersSocket> SubscribeOrders { get; } = new();
}
/// <summary>
/// Futures order capabilities.
/// </summary>
public static class Futures
{
/// <summary>
/// Place a futures order capability.
/// </summary>
public static SharedRestSocketCapability<IPlaceFuturesOrder, IPlaceFuturesOrderRest, IPlaceFuturesOrderSocket> PlaceOrder { get; } = new();
/// <summary>
/// Place multiple futures orders capability.
/// </summary>
public static SharedRestSocketCapability<IPlaceMultipleFuturesOrders, IPlaceMultipleFuturesOrdersRest, IPlaceMultipleFuturesOrdersSocket> PlaceMultipleOrders { get; } = new();
/// <summary>
/// Edit a futures order capability.
/// </summary>
public static SharedRestSocketCapability<IEditFuturesOrder, IEditFuturesOrderRest, IEditFuturesOrderSocket> EditOrder { get; } = new();
/// <summary>
/// Edit a futures order by client order id capability.
/// </summary>
public static SharedRestSocketCapability<IEditFuturesOrderByClientOrderId, IEditFuturesOrderByClientOrderIdRest, IEditFuturesOrderByClientOrderIdSocket> EditOrderByClientOrderId { get; } = new();
/// <summary>
/// Cancel a futures order capability.
/// </summary>
public static SharedRestSocketCapability<ICancelFuturesOrder, ICancelFuturesOrderRest, ICancelFuturesOrderSocket> CancelOrder { get; } = new();
/// <summary>
/// Cancel a futures order by client order id capability.
/// </summary>
public static SharedRestSocketCapability<ICancelFuturesOrderByClientOrderId, ICancelFuturesOrderByClientOrderIdRest, ICancelFuturesOrderByClientOrderIdSocket> CancelOrderByClientOrderId { get; } = new();
/// <summary>
/// Cancel all futures orders capability.
/// </summary>
public static SharedRestSocketCapability<ICancelAllFuturesOrders, ICancelAllFuturesOrdersRest, ICancelAllFuturesOrdersSocket> CancelAllOrders { get; } = new();
/// <summary>
/// Cancel all futures orders for a symbol capability.
/// </summary>
public static SharedRestSocketCapability<ICancelAllFuturesSymbolOrders, ICancelAllFuturesSymbolOrdersRest, ICancelAllFuturesSymbolOrdersSocket> CancelAllSymbolOrders { get; } = new();
/// <summary>
/// Get a futures order capability.
/// </summary>
public static SharedRestCapability<IGetFuturesOrder, IGetFuturesOrderRest> GetOrder { get; } = new();
/// <summary>
/// Get a futures order by client order id capability.
/// </summary>
public static SharedRestCapability<IGetFuturesOrderByClientOrderId, IGetFuturesOrderByClientOrderIdRest> GetOrderByClientOrderId { get; } = new();
/// <summary>
/// Get open futures orders capability.
/// </summary>
public static SharedRestCapability<IGetOpenFuturesOrders, IGetOpenFuturesOrdersRest> GetOpenOrders { get; } = new();
/// <summary>
/// Get closed futures orders capability.
/// </summary>
public static SharedRestCapability<IGetClosedFuturesOrders, IGetClosedFuturesOrdersRest> GetClosedOrders { get; } = new();
/// <summary>
/// Subscribe to futures order updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeFuturesOrdersSocket> SubscribeOrders { get; } = new();
}
}
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Position mode capabilities.
/// </summary>
public static class PositionMode
{
/// <summary>
/// Get position mode capability.
/// </summary>
public static SharedRestCapability<IGetPositionMode, IGetPositionModeRest> GetPositionMode { get; } = new();
/// <summary>
/// Set position mode capability.
/// </summary>
public static SharedRestCapability<ISetPositionMode, ISetPositionModeRest> SetPositionMode { get; } = new();
}
}
}
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Position capabilities.
/// </summary>
public static class Positions
{
/// <summary>
/// Get positions capability.
/// </summary>
public static SharedRestCapability<IGetPositions, IGetPositionsRest> GetPositions { get; } = new();
/// <summary>
/// Get position history capability.
/// </summary>
public static SharedRestCapability<IGetPositionHistory, IGetPositionHistoryRest> GetPositionHistory { get; } = new();
/// <summary>
/// Close a full position capability.
/// </summary>
public static SharedRestCapability<ICloseFullPosition, ICloseFullPositionRest> CloseFullPosition { get; } = new();
/// <summary>
/// Subscribe to position updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribePositionsSocket> SubscribePositions { get; } = new();
}
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Symbol capabilities.
/// </summary>
public static class Symbols
{
/// <summary>
/// Get spot symbols capability.
/// </summary>
public static SharedRestCapability<IGetSpotSymbols, IGetSpotSymbolsRest> GetSpotSymbols { get; } = new();
/// <summary>
/// Get futures symbols capability.
/// </summary>
public static SharedRestCapability<IGetFuturesSymbols, IGetFuturesSymbolsRest> GetFuturesSymbols { get; } = new();
}
}
}
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Ticker capabilities.
/// </summary>
public static class Tickers
{
/// <summary>
/// Get a ticker capability.
/// </summary>
public static SharedRestCapability<IGetTicker, IGetTickerRest> GetTicker { get; } = new();
/// <summary>
/// Get all tickers capability.
/// </summary>
public static SharedRestCapability<IGetAllTickers, IGetAllTickersRest> GetAllTickers { get; } = new();
/// <summary>
/// Subscribe to ticker updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeTickerSocket> SubscribeTicker { get; } = new();
/// <summary>
/// Subscribe to all ticker updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeAllTickersSocket> SubscribeAllTickers { get; } = new();
/// <summary>
/// Subscribe to book ticker updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeBookTickerSocket> SubscribeBookTicker { get; } = new();
}
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Take profit and stop loss capabilities.
/// </summary>
public static class TpSl
{
/// <summary>
/// Set futures take profit or stop loss capability.
/// </summary>
public static SharedRestCapability<ISetFuturesTpSl, ISetFuturesTpSlRest> SetFuturesTpSl { get; } = new();
/// <summary>
/// Cancel futures take profit or stop loss capability.
/// </summary>
public static SharedRestCapability<ICancelFuturesTpSl, ICancelFuturesTpSlRest> CancelFuturesTpSl { get; } = new();
}
}
}
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Public trade capabilities.
/// </summary>
public static class Trades
{
/// <summary>
/// Get recent trades capability.
/// </summary>
public static SharedRestCapability<IGetRecentTrades, IGetRecentTradesRest> GetRecentTrades { get; } = new();
/// <summary>
/// Get trade history capability.
/// </summary>
public static SharedRestCapability<IGetTradeHistory, IGetTradeHistoryRest> GetTradeHistory { get; } = new();
/// <summary>
/// Subscribe to trade updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeTradesSocket> SubscribeTrades { get; } = new();
}
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Transfer capabilities.
/// </summary>
public static class Transfers
{
/// <summary>
/// Transfer assets capability.
/// </summary>
public static SharedRestCapability<ITransfer, ITransferRest> Transfer { get; } = new();
/// <summary>
/// Get transfer history capability.
/// </summary>
public static SharedRestCapability<IGetTransferHistory, IGetTransferHistoryRest> GetTransferHistory { get; } = new();
}
}
}
@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Trigger order capabilities.
/// </summary>
public static class TriggerOrders
{
/// <summary>
/// Spot trigger order capabilities.
/// </summary>
public static class Spot
{
/// <summary>
/// Place a spot trigger order capability.
/// </summary>
public static SharedRestCapability<IPlaceSpotTriggerOrder, IPlaceSpotTriggerOrderRest> PlaceOrder { get; } = new();
/// <summary>
/// Get a spot trigger order capability.
/// </summary>
public static SharedRestCapability<IGetSpotTriggerOrder, IGetSpotTriggerOrderRest> GetOrder { get; } = new();
/// <summary>
/// Cancel a spot trigger order capability.
/// </summary>
public static SharedRestCapability<ICancelSpotTriggerOrder, ICancelSpotTriggerOrderRest> CancelOrder { get; } = new();
}
/// <summary>
/// Futures trigger order capabilities.
/// </summary>
public static class Futures
{
/// <summary>
/// Place a futures trigger order capability.
/// </summary>
public static SharedRestCapability<IPlaceFuturesTriggerOrder, IPlaceFuturesTriggerOrderRest> PlaceOrder { get; } = new();
/// <summary>
/// Get a futures trigger order capability.
/// </summary>
public static SharedRestCapability<IGetFuturesTriggerOrder, IGetFuturesTriggerOrderRest> GetOrder { get; } = new();
/// <summary>
/// Cancel a futures trigger order capability.
/// </summary>
public static SharedRestCapability<ICancelFuturesTriggerOrder, ICancelFuturesTriggerOrderRest> CancelOrder { get; } = new();
}
}
}
}
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// User trade capabilities.
/// </summary>
public static class UserTrades
{
/// <summary>
/// Get trades for a spot order capability.
/// </summary>
public static SharedRestCapability<IGetSpotOrderTrades, IGetSpotOrderTradesRest> GetSpotOrderTrades { get; } = new();
/// <summary>
/// Get spot user trade history capability.
/// </summary>
public static SharedRestCapability<IGetSpotUserTradeHistory, IGetSpotUserTradeHistoryRest> GetSpotTradeHistory { get; } = new();
/// <summary>
/// Get trades for a futures order capability.
/// </summary>
public static SharedRestCapability<IGetFuturesOrderTrades, IGetFuturesOrderTradesRest> GetFuturesOrderTrades { get; } = new();
/// <summary>
/// Get futures user trade history capability.
/// </summary>
public static SharedRestCapability<IGetFuturesUserTradeHistory, IGetFuturesUserTradeHistoryRest> GetFuturesTradeHistory { get; } = new();
/// <summary>
/// Subscribe to user trade updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeUserTradesSocket> SubscribeUserTrades { get; } = new();
}
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Withdrawal capabilities.
/// </summary>
public static class Withdrawals
{
/// <summary>
/// Withdraw assets capability.
/// </summary>
public static SharedRestCapability<IWithdraw, IWithdrawRest> Withdraw { get; } = new();
/// <summary>
/// Get withdrawal history capability.
/// </summary>
public static SharedRestCapability<IGetWithdrawalHistory, IGetWithdrawalHistoryRest> GetWithdrawalHistory { get; } = new();
}
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Strongly typed reference to a Shared API capability.
/// </summary>
public class SharedCapabilityReference<T>
where T : ISharedApiCapability
{
/// <summary>
/// Capability interface type.
/// </summary>
public Type CapabilityType => typeof(T);
internal SharedCapabilityReference()
{
}
/// <inheritdoc />
public override string ToString() => typeof(T).Name;
}
}
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Capability lookup result
/// </summary>
/// <typeparam name="T">Capability type</typeparam>
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>
public T Capability { get; }
/// <summary>
/// The capability options
/// </summary>
public CapabilityOptions Options { get; }
internal SharedCapabilityResolution(
T capability,
CapabilityOptions options)
{
Capability = capability;
Options = options;
}
}
}
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// References for a capability available through REST.
/// </summary>
public class SharedRestCapability<TCapability, TRest>
: SharedCapabilityReference<TCapability>
where TCapability : ISharedApiCapability
where TRest : TCapability, ISharedRest
{
/// <summary>
/// Transport-agnostic capability.
/// </summary>
public SharedCapabilityReference<TCapability> Any { get; } = new();
/// <summary>
/// REST capability.
/// </summary>
public SharedCapabilityReference<TRest> Rest { get; } = new();
internal SharedRestCapability()
{
}
}
}
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// References for a capability available through REST and WebSocket.
/// </summary>
public class SharedRestSocketCapability<TCapability, TRest, TSocket>
: SharedCapabilityReference<TCapability>
where TCapability : ISharedApiCapability
where TRest : TCapability, ISharedRest
where TSocket : TCapability, ISharedSocket
{
/// <summary>
/// REST capability.
/// </summary>
public SharedCapabilityReference<TRest> Rest { get; } = new();
/// <summary>
/// WebSocket capability.
/// </summary>
public SharedCapabilityReference<TSocket> Socket { get; } = new();
internal SharedRestSocketCapability()
{
}
}
}
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Asset type
/// </summary>
public enum SharedAssetType
{
/// <summary>
/// Unknown or unspecified asset type
/// </summary>
Unspecified,
/// <summary>
/// Cryptocurrency asset type
/// </summary>
Crypto,
/// <summary>
/// Fiat currency asset type
/// </summary>
Fiat,
/// <summary>
/// Traditional finance asset type
/// </summary>
TradFi
}
/// <summary>
/// Asset sub type
/// </summary>
public enum SharedAssetSubType
{
// --- Crypto sub types ---
/// <summary>
/// Stable coin, can be for different fiat currencies
/// </summary>
StableCoin,
// --- TradFi sub types ---
/// <summary>
/// Equity, can be stocks, ETFs, or indices
/// </summary>
Equity,
/// <summary>
/// Commodity, can be oil, gas, metals, etc.
/// </summary>
Commodity
}
}
@@ -0,0 +1,42 @@
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Ledger entry type
/// </summary>
public enum SharedLedgerEntryType
{
/// <summary>
/// Trade execution
/// </summary>
Trade,
/// <summary>
/// Withdrawal
/// </summary>
Withdrawal,
/// <summary>
/// Deposit
/// </summary>
Deposit,
/// <summary>
/// Transfer
/// </summary>
Transfer,
/// <summary>
/// Fee payment
/// </summary>
Fee,
/// <summary>
/// Funding fee payment
/// </summary>
FundingFee,
/// <summary>
/// Rebate
/// </summary>
Rebate,
/// <summary>
/// Other or unknown
/// </summary>
Unknown
}
}
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// The type of updates the incremental order book subscription produces
/// </summary>
public enum SharedOrderBookSubscriptionType
{
/// <summary>
/// Subscription produces an initial snapshot update, followed by incremental change updates
/// </summary>
SnapshotThenIncremental,
/// <summary>
/// Subscription produces only incremental change updates
/// </summary>
OnlyIncremental
}
}
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// The type of transport used for the shared API
/// </summary>
public enum SharedTransport
{
/// <summary>
/// REST transport
/// </summary>
Rest,
/// <summary>
/// WebSocket transport
/// </summary>
Socket
}
}
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// A shared/common client interface
/// </summary>
public interface ISharedClient : ISharedApi
{
}
}
@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Options for canceling a futures order
/// </summary>
public class CancelFuturesOrderSocketOptions : CancelFuturesOrderOptions
{
/// <inheritdoc />
public override string Description => "Cancel a futures order over a socket connection";
/// <summary>
/// ctor
/// </summary>
public CancelFuturesOrderSocketOptions(string exchange, bool authenticated) : base(exchange, authenticated)
{
}
}
}
@@ -0,0 +1,23 @@
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Options for canceling a spot order
/// </summary>
public class CancelSpotOrderSocketOptions : CancelSpotOrderOptions
{
/// <inheritdoc />
public override string Description => "Cancel a spot order over a socket connection";
/// <summary>
/// ctor
/// </summary>
public CancelSpotOrderSocketOptions(string exchange, bool authenticated) : base(exchange, authenticated)
{
}
}
}
@@ -0,0 +1,82 @@
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Options for closing position
/// </summary>
#pragma warning disable CS0618 // Type or member is obsolete
public class ClosePositionOptions : EndpointOptions
#pragma warning restore CS0618 // Type or member is obsolete
{
private static PropertyInfo[] _requestProperties = typeof(ClosePositionRequest).GetProperties();
/// <inheritdoc />
public override string Description => "Close an open futures position";
/// <inheritdoc />
public override Type CapabilityType => typeof(IFuturesOrderRestClient);
private static readonly RequestParameterDescription[] _defaultParameterRules = new[]
{
RequestParameterRule<ClosePositionRequest>.Required(x => x.Symbol, "The symbol of the position to close", new SharedSymbol(TradingMode.PerpetualLinear, "ETH", "USDT")),
RequestParameterRule<ClosePositionRequest>.Required(x => x.PositionMode, "The current position mode of the account", SharedPositionMode.OneWay),
RequestParameterRule<ClosePositionRequest>.Optional(x => x.PositionSide, "The side of the position to close", SharedPositionSide.Long),
RequestParameterRule<ClosePositionRequest>.Optional(x => x.MarginMode, "The margin mode of the position", SharedMarginMode.Cross),
RequestParameterRule<ClosePositionRequest>.Optional(x => x.Quantity, "The quantity of the position to close", 1m),
};
/// <summary>
/// ctor
/// </summary>
public ClosePositionOptions(string exchange, bool authenticated) : base(exchange, nameof(IFuturesOrderRestClient.ClosePositionAsync), true, _defaultParameterRules, SharedTradingModeSets.Futures)
{
}
/// <summary>
/// Validate request
/// </summary>
public Error? ValidateRequest(ClosePositionRequest request, IFuturesOrderRestClient client)
{
if (NeedsAuthentication && !client.Authenticated)
return new NoApiCredentialsError();
foreach (var param in RequestParameterRules)
{
var property = _requestProperties.Single(x => x.Name == param.Name);
var value = property.GetValue(request);
if (param.Support == RequestParameterSupport.Required)
{
if (value == null)
{
return ArgumentError.Invalid(
param.Name,
$"Request parameter `{param.Name}` for exchange `{Exchange}` is required and should be provided. Example: {param.ExampleValue}");
}
}
}
if (request is SharedSymbolRequest symbolsRequest)
{
if (symbolsRequest.Symbols != null)
{
if (!SupportsMultipleSymbols)
return ArgumentError.Invalid(nameof(SharedSymbolRequest.Symbols), $"Only a single symbol parameter is allowed, multiple symbols are not supported");
if (symbolsRequest.Symbols.Length > MaxSymbolCount)
return ArgumentError.Invalid(nameof(SharedSymbolRequest.Symbols), $"Max number of symbols is {MaxSymbolCount} but {symbolsRequest.Symbols.Length} were passed");
}
}
return ValidateRequest(request.ExchangeParameters, request.TradingMode, client.SupportedTradingModes);
}
}
}
@@ -0,0 +1,23 @@
using CryptoExchange.Net.Objects;
using System;
using System.Linq;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Options for placing a new futures order
/// </summary>
public class PlaceFuturesOrderSocketOptions : PlaceFuturesOrderOptions
{
/// <inheritdoc />
public override string Description => "Place a new futures order over a socket connection";
/// <summary>
/// ctor
/// </summary>
public PlaceFuturesOrderSocketOptions(string exchange, bool supportsTpSl) : base(exchange, supportsTpSl)
{
}
}
}
@@ -0,0 +1,23 @@
using CryptoExchange.Net.Objects;
using System;
using System.Linq;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Options for placing a new spot order
/// </summary>
public class PlaceSpotOrderSocketOptions : PlaceSpotOrderOptions
{
/// <inheritdoc />
public override string Description => "Place a new spot order over a socket connection";
/// <summary>
/// ctor
/// </summary>
public PlaceSpotOrderSocketOptions(string exchange) : base(exchange)
{
}
}
}
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Funding rate request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
GetFundingRateHistoryOptions GetFundingRateHistoryOptions { get; }
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Futures get order by client order id request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
GetFuturesOrderByClientOrderIdOptions GetFuturesOrderByClientOrderIdOptions { get; }
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary>
/// Futures cancel order by client order id request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary>
CancelFuturesOrderByClientOrderIdOptions CancelFuturesOrderByClientOrderIdOptions { get; }

Some files were not shown because too many files have changed in this diff Show More