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

Compare commits

...

8 Commits

16 changed files with 247 additions and 39 deletions
@@ -62,6 +62,31 @@ namespace CryptoExchange.Net.UnitTests.ConverterTests
Assert.That(deserialized.Prop8!.Prop31, Is.EqualTo(5)); Assert.That(deserialized.Prop8!.Prop31, Is.EqualTo(5));
Assert.That(deserialized.Prop8.Prop32, Is.EqualTo("101")); Assert.That(deserialized.Prop8.Prop32, Is.EqualTo("101"));
} }
[TestCase("[\"4.803E-5\",\"9.623e-4\",\"0.0010025\"]", 0.00004803, 0.0009623, 0.0010025)]
[TestCase("[\"81251.5\",\"1E+2\",\"-2.5E-3\"]", 81251.5, 100, -0.0025)]
[TestCase("[4.803E-5,9.623e-4,0.0010025]", 0.00004803, 0.0009623, 0.0010025)]
public void TestArrayConverterDecimalInScientificNotation(string json, decimal expected1, decimal expected2, decimal expected3)
{
// Some exchanges send small prices as strings in scientific notation, for example the
// Kucoin futures kline stream: "candles":["1789830000","4.803E-5","4.807E-5",...]
var deserialized = JsonSerializer.Deserialize<TestDecimal>(json);
Assert.That(deserialized!.Prop1, Is.EqualTo(expected1));
Assert.That(deserialized.Prop2, Is.EqualTo(expected2));
Assert.That(deserialized.Prop3, Is.EqualTo(expected3));
}
}
[JsonConverter(typeof(ArrayConverter<TestDecimal>))]
public record TestDecimal
{
[ArrayProperty(0)]
public decimal Prop1 { get; set; }
[ArrayProperty(1)]
public decimal? Prop2 { get; set; }
[ArrayProperty(2)]
public decimal Prop3 { get; set; }
} }
[JsonConverter(typeof(ArrayConverter<Test>))] [JsonConverter(typeof(ArrayConverter<Test>))]
@@ -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;
}
}
}
+14 -1
View File
@@ -198,7 +198,7 @@ namespace CryptoExchange.Net.Clients
string? cacheKey = null; string? cacheKey = null;
if (ShouldCache(definition)) if (ShouldCache(definition))
{ {
cacheKey = definition.FullUrl + definition + uriParameters?.ToFormData(); cacheKey = GetCacheKey(definition, uriParameters);
_logger.CheckingCache(cacheKey); _logger.CheckingCache(cacheKey);
var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge); var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge);
if (cachedValue != null) if (cachedValue != null)
@@ -800,6 +800,13 @@ namespace CryptoExchange.Net.Clients
&& definition.Method == HttpMethod.Get && definition.Method == HttpMethod.Get
&& !definition.PreventCaching; && !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();
}
/// <inheritdoc /> /// <inheritdoc />
public virtual void SetOptions(UpdateOptions options) public virtual void SetOptions(UpdateOptions options)
@@ -889,6 +896,12 @@ namespace CryptoExchange.Net.Clients
ApiCredentials = (TApiCredentials?)options.ApiCredentials?.Copy() ?? ApiCredentials; ApiCredentials = (TApiCredentials?)options.ApiCredentials?.Copy() ?? ApiCredentials;
} }
/// <inheritdoc />
protected override string GetCacheKey(RequestDefinition definition, Parameters? parameters)
{
return definition.FullUrl + definition + parameters?.ToFormData() + GetAuthenticationProvider()?.Key;
}
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -173,6 +173,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (targetType.IsAssignableFrom(value?.GetType())) if (targetType.IsAssignableFrom(value?.GetType()))
attribute.PropertyInfo.SetValue(result, value); attribute.PropertyInfo.SetValue(result, value);
else if (value is string stringValue && targetType == typeof(decimal))
// Convert.ChangeType doesn't accept scientific notation, which some exchanges use for small values ("4.803E-5")
attribute.PropertyInfo.SetValue(result, ExchangeHelpers.ParseDecimal(stringValue));
else else
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture)); attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
} }
+3 -3
View File
@@ -6,9 +6,9 @@
<PackageId>CryptoExchange.Net</PackageId> <PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors> <Authors>JKorf</Authors>
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description> <Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
<PackageVersion>12.5.0</PackageVersion> <PackageVersion>12.5.1</PackageVersion>
<AssemblyVersion>12.5.0</AssemblyVersion> <AssemblyVersion>12.5.1</AssemblyVersion>
<FileVersion>12.5.0</FileVersion> <FileVersion>12.5.1</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags> <PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
@@ -289,6 +289,7 @@ namespace CryptoExchange.Net.OrderBook
if (!startResult.Success) if (!startResult.Success)
{ {
Status = OrderBookStatus.Disconnected; Status = OrderBookStatus.Disconnected;
_queueEvent.Set();
return CallResult.Fail(startResult.Error!); return CallResult.Fail(startResult.Error!);
} }
@@ -25,6 +25,9 @@ namespace CryptoExchange.Net.Sockets.Default.Routing
Routes = routes; Routes = routes;
} }
private RouteCollection? GetRoutes(string typeIdentifier)
=> (_routingTable ?? throw new NullReferenceException("Routing table not build before handling")).GetRoutes(typeIdentifier);
/// <summary> /// <summary>
/// Build the route mapping /// Build the route mapping
/// </summary> /// </summary>
@@ -46,13 +49,21 @@ namespace CryptoExchange.Net.Sockets.Default.Routing
/// </summary> /// </summary>
public bool Handle(string typeIdentifier, string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, out CallResult? result) public bool Handle(string typeIdentifier, string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, out CallResult? result)
{ {
var routeCollection = (_routingTable ?? throw new NullReferenceException("Routing table not build before handling")).GetRoutes(typeIdentifier); var routeCollection = GetRoutes(typeIdentifier);
if (routeCollection == null) if (routeCollection == null)
throw new InvalidOperationException($"No routes for {typeIdentifier} message type"); throw new InvalidOperationException($"No routes for {typeIdentifier} message type");
return routeCollection.Handle(topicFilter, connection, receiveTime, originalData, data, out result); return routeCollection.Handle(topicFilter, connection, receiveTime, originalData, data, out result);
} }
/// <summary>
/// Whether the router has a matching route for the specified message type and topic
/// </summary>
public bool CanHandle(string typeIdentifier, string? topicFilter)
{
return GetRoutes(typeIdentifier)?.CanHandle(topicFilter) == true;
}
/// <summary> /// <summary>
/// Create a void handler /// Create a void handler
/// </summary> /// </summary>
@@ -60,6 +60,15 @@ namespace CryptoExchange.Net.Sockets.Default.Routing
return matchingTopicRoutes; return matchingTopicRoutes;
} }
public bool CanHandle(string? topicFilter)
{
if (_routesWithoutTopicFilter.Count > 0)
return true;
return topicFilter != null
&& GetRoutesWithMatchingTopicFilter(topicFilter) != null;
}
public abstract bool Handle(string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, out CallResult? result); public abstract bool Handle(string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, out CallResult? result);
} }
} }
@@ -219,9 +219,13 @@ namespace CryptoExchange.Net.Sockets.Default
ConnectionInvocations++; ConnectionInvocations++;
TotalInvocations++; TotalInvocations++;
if (SubscriptionQuery != null && !SubscriptionQuery.Completed && SubscriptionQuery.TimeoutBehavior == TimeoutBehavior.Succeed) if (SubscriptionQuery != null
&& !SubscriptionQuery.Completed
&& SubscriptionQuery.TimeoutBehavior == TimeoutBehavior.Succeed
&& MessageRouter.CanHandle(typeIdentifier, topicFilter))
{ {
// The subscription query is one where it is successful if there is no error returned // The subscription query is one where it is successful if there is no error returned
// The connection routes by type, so only a matching subscription route proves the query was successful
// Since we've received a data update for the subscription we can assume the subscribe query was successful // Since we've received a data update for the subscription we can assume the subscribe query was successful
// Call timeout to complete // Call timeout to complete
SubscriptionQuery.Timeout(); SubscriptionQuery.Timeout();
@@ -282,8 +282,8 @@ namespace CryptoExchange.Net.Trackers.Klines
KlineCount = klines.Count(), KlineCount = klines.Count(),
FirstOpenTime = klines.First().OpenTime, FirstOpenTime = klines.First().OpenTime,
LastOpenTime = klines.Last().OpenTime, LastOpenTime = klines.Last().OpenTime,
HighPrice = klines.Select(d => d.LowPrice).Max(), HighPrice = klines.Select(d => d.HighPrice).Max(),
LowPrice = klines.Select(d => d.HighPrice).Min(), LowPrice = klines.Select(d => d.LowPrice).Min(),
#pragma warning disable CS0618 // Type or member is obsolete | Temporary to maintain previous behavior #pragma warning disable CS0618 // Type or member is obsolete | Temporary to maintain previous behavior
Volume = klines.Select(d => d.Volume).Sum(), Volume = klines.Select(d => d.Volume).Sum(),
AverageVolume = Math.Round(klines.OrderByDescending(d => d.OpenTime).Skip(1).Select(d => d.Volume).DefaultIfEmpty().Average(), 8) AverageVolume = Math.Round(klines.OrderByDescending(d => d.OpenTime).Skip(1).Select(d => d.Volume).DefaultIfEmpty().Average(), 8)
+30 -29
View File
@@ -5,36 +5,37 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Binance.Net" Version="13.3.0" /> <PackageReference Include="Binance.Net" Version="13.5.0" />
<PackageReference Include="Bitfinex.Net" Version="11.3.0" /> <PackageReference Include="Bitfinex.Net" Version="11.4.0" />
<PackageReference Include="BitMart.Net" Version="4.3.0" /> <PackageReference Include="BitMart.Net" Version="4.4.0" />
<PackageReference Include="BloFin.Net" Version="3.3.0" /> <PackageReference Include="BloFin.Net" Version="3.4.0" />
<PackageReference Include="Bybit.Net" Version="7.3.0" /> <PackageReference Include="Bybit.Net" Version="7.5.0" />
<PackageReference Include="CoinEx.Net" Version="11.3.0" /> <PackageReference Include="CoinEx.Net" Version="11.4.0" />
<PackageReference Include="CoinW.Net" Version="3.3.0" /> <PackageReference Include="CoinW.Net" Version="3.4.0" />
<PackageReference Include="CryptoCom.Net" Version="4.3.0" /> <PackageReference Include="CryptoCom.Net" Version="4.4.0" />
<PackageReference Include="DeepCoin.Net" Version="4.3.0" /> <PackageReference Include="DeepCoin.Net" Version="4.4.0" />
<PackageReference Include="GateIo.Net" Version="4.4.0" /> <PackageReference Include="GateIo.Net" Version="4.6.0" />
<PackageReference Include="HyperLiquid.Net" Version="5.4.0" /> <PackageReference Include="HyperLiquid.Net" Version="5.7.0" />
<PackageReference Include="JK.BingX.Net" Version="4.3.0" /> <PackageReference Include="JK.BingX.Net" Version="4.5.0" />
<PackageReference Include="JK.Bitget.Net" Version="4.3.2" /> <PackageReference Include="JK.Bitget.Net" Version="4.5.0" />
<PackageReference Include="JK.Mexc.Net" Version="6.4.0" /> <PackageReference Include="JK.Mexc.Net" Version="6.6.0" />
<PackageReference Include="JK.OKX.Net" Version="5.3.1" /> <PackageReference Include="JK.OKX.Net" Version="5.4.0" />
<PackageReference Include="Jkorf.Aster.Net" Version="4.3.0" /> <PackageReference Include="Jkorf.Aster.Net" Version="4.4.0" />
<PackageReference Include="JKorf.BitMEX.Net" Version="4.3.0" /> <PackageReference Include="JKorf.BitMEX.Net" Version="4.4.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="4.4.0" /> <PackageReference Include="JKorf.Coinbase.Net" Version="4.5.0" />
<PackageReference Include="JKorf.HTX.Net" Version="9.3.0" /> <PackageReference Include="JKorf.HTX.Net" Version="9.5.0" />
<PackageReference Include="JKorf.Lighter.Net" Version="1.4.0" /> <PackageReference Include="JKorf.Lighter.Net" Version="1.5.0" />
<PackageReference Include="JKorf.Upbit.Net" Version="3.3.0" /> <PackageReference Include="JKorf.Upbit.Net" Version="3.4.0" />
<PackageReference Include="KrakenExchange.Net" Version="8.3.0" /> <PackageReference Include="KrakenExchange.Net" Version="8.4.0" />
<PackageReference Include="Kucoin.Net" Version="9.3.0" /> <PackageReference Include="Kucoin.Net" Version="9.5.0" />
<PackageReference Include="LBank.Net" Version="1.0.0" /> <PackageReference Include="LBank.Net" Version="1.1.0" />
<PackageReference Include="Pionex.Net" Version="1.1.0" /> <PackageReference Include="Pionex.Net" Version="1.2.0" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Toobit.Net" Version="4.3.0" /> <PackageReference Include="Tapbit.Net" Version="1.0.0" />
<PackageReference Include="Weex.Net" Version="2.3.0" /> <PackageReference Include="Toobit.Net" Version="4.4.0" />
<PackageReference Include="WhiteBit.Net" Version="4.3.0" /> <PackageReference Include="Weex.Net" Version="2.4.0" />
<PackageReference Include="XT.Net" Version="4.3.0" /> <PackageReference Include="WhiteBit.Net" Version="4.4.0" />
<PackageReference Include="XT.Net" Version="4.5.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+3 -1
View File
@@ -23,6 +23,7 @@
@inject IMexcRestClient mexcClient @inject IMexcRestClient mexcClient
@inject IOKXRestClient okxClient @inject IOKXRestClient okxClient
@inject IPionexRestClient pionexClient @inject IPionexRestClient pionexClient
@inject ITapbitRestClient tapbitClient
@inject IToobitRestClient toobitClient @inject IToobitRestClient toobitClient
@inject IUpbitRestClient upbitClient @inject IUpbitRestClient upbitClient
@inject IWeexRestClient weexClient @inject IWeexRestClient weexClient
@@ -70,7 +71,8 @@
var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync(); var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync();
var xtTask = xtClient.SpotApi.ExchangeData.GetTickersAsync("btc_usdt"); var xtTask = xtClient.SpotApi.ExchangeData.GetTickersAsync("btc_usdt");
await Task.WhenAll(asterTask, binanceTask, bingXTask, bitfinexTask, bitgetTask, bitmartTask, bloFinTask, bitmexTask, bybitTask, coinexTask, coinWTask, deepCoinTask, gateioTask, htxTask, krakenTask, kucoinTask, lBankTask, mexcTask, okxTask, pionexTask); await Task.WhenAll(asterTask, binanceTask, bingXTask, bitfinexTask, bitgetTask, bitmartTask, bloFinTask, bitmexTask, bybitTask, coinexTask, coinWTask, deepCoinTask, gateioTask,
htxTask, krakenTask, kucoinTask, lBankTask, mexcTask, okxTask, pionexTask, toobitTask, upbitTask, weexTask, whitebitTask, xtTask);
if (asterTask.Result.Success) if (asterTask.Result.Success)
_prices.Add("Aster", asterTask.Result.Data.LastPrice); _prices.Add("Aster", asterTask.Result.Data.LastPrice);
+1
View File
@@ -57,6 +57,7 @@ namespace BlazorClient
services.AddMexc(); services.AddMexc();
services.AddOKX(); services.AddOKX();
services.AddPionex(); services.AddPionex();
services.AddTapbit();
services.AddToobit(); services.AddToobit();
services.AddUpbit(); services.AddUpbit();
services.AddWeex(); services.AddWeex();
+1
View File
@@ -33,6 +33,7 @@
@using OKX.Net.Interfaces.Clients; @using OKX.Net.Interfaces.Clients;
@using Pionex.Net.Interfaces.Clients; @using Pionex.Net.Interfaces.Clients;
@using Upbit.Net.Interfaces.Clients; @using Upbit.Net.Interfaces.Clients;
@using Tapbit.Net.Interfaces.Clients;
@using Toobit.Net.Interfaces.Clients; @using Toobit.Net.Interfaces.Clients;
@using Weex.Net.Interfaces.Clients @using Weex.Net.Interfaces.Clients
@using WhiteBit.Net.Interfaces.Clients @using WhiteBit.Net.Interfaces.Clients
+7
View File
@@ -59,6 +59,7 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
|![OKX](https://raw.githubusercontent.com/JKorf/OKX.Net/refs/heads/main/OKX.Net/Icon/icon.png)|OKX|CEX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.OKX.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.OKX.Net)|[Link](https://www.okx.com/join/14592495)|20%| |![OKX](https://raw.githubusercontent.com/JKorf/OKX.Net/refs/heads/main/OKX.Net/Icon/icon.png)|OKX|CEX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.OKX.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.OKX.Net)|[Link](https://www.okx.com/join/14592495)|20%|
|![Pionex](https://raw.githubusercontent.com/JKorf/Pionex.Net/refs/heads/main/Pionex.Net/Icon/icon.png)|Pionex|CEX|[JKorf/Pionex.Net](https://github.com/JKorf/Pionex.Net)|[![Nuget version](https://img.shields.io/nuget/v/Pionex.net.svg?style=flat-square)](https://www.nuget.org/packages/Pionex.Net)|-|-| |![Pionex](https://raw.githubusercontent.com/JKorf/Pionex.Net/refs/heads/main/Pionex.Net/Icon/icon.png)|Pionex|CEX|[JKorf/Pionex.Net](https://github.com/JKorf/Pionex.Net)|[![Nuget version](https://img.shields.io/nuget/v/Pionex.net.svg?style=flat-square)](https://www.nuget.org/packages/Pionex.Net)|-|-|
|![Polymarket](https://raw.githubusercontent.com/JKorf/Polymarket.Net/main/Polymarket.Net/Icon/icon.png)|Polymarket|DEX|[JKorf/Polymarket.Net](https://github.com/JKorf/Polymarket.Net)|[![Nuget version](https://img.shields.io/nuget/v/Polymarket.net.svg?style=flat-square)](https://www.nuget.org/packages/Polymarket.Net)|-|-| |![Polymarket](https://raw.githubusercontent.com/JKorf/Polymarket.Net/main/Polymarket.Net/Icon/icon.png)|Polymarket|DEX|[JKorf/Polymarket.Net](https://github.com/JKorf/Polymarket.Net)|[![Nuget version](https://img.shields.io/nuget/v/Polymarket.net.svg?style=flat-square)](https://www.nuget.org/packages/Polymarket.Net)|-|-|
|![Tapbit](https://raw.githubusercontent.com/JKorf/Tapbit.Net/refs/heads/main/Tapbit.Net/Icon/icon.png)|Tapbit|CEX|[JKorf/Tapbit.Net](https://github.com/JKorf/Tapbit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Tapbit.net.svg?style=flat-square)](https://www.nuget.org/packages/Tapbit.Net)|[Link](https://www.tapbit.com/en/invite/SPBGMSK)|-|
|![Toobit](https://raw.githubusercontent.com/JKorf/Toobit.Net/refs/heads/main/Toobit.Net/Icon/icon.png)|Toobit|CEX|[JKorf/Toobit.Net](https://github.com/JKorf/Toobit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Toobit.net.svg?style=flat-square)](https://www.nuget.org/packages/Toobit.Net)|[Link](https://www.toobit.com/en-US/register?invite_code=zsV19h)|-| |![Toobit](https://raw.githubusercontent.com/JKorf/Toobit.Net/refs/heads/main/Toobit.Net/Icon/icon.png)|Toobit|CEX|[JKorf/Toobit.Net](https://github.com/JKorf/Toobit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Toobit.net.svg?style=flat-square)](https://www.nuget.org/packages/Toobit.Net)|[Link](https://www.toobit.com/en-US/register?invite_code=zsV19h)|-|
|![Upbit](https://raw.githubusercontent.com/JKorf/Upbit.Net/refs/heads/main/Upbit.Net/Icon/icon.png)|Upbit|CEX|[JKorf/Upbit.Net](https://github.com/JKorf/Upbit.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.Upbit.net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.Upbit.Net)|-|-| |![Upbit](https://raw.githubusercontent.com/JKorf/Upbit.Net/refs/heads/main/Upbit.Net/Icon/icon.png)|Upbit|CEX|[JKorf/Upbit.Net](https://github.com/JKorf/Upbit.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.Upbit.net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.Upbit.Net)|-|-|
|![Weex](https://raw.githubusercontent.com/JKorf/Weex.Net/refs/heads/main/Weex.Net/Icon/icon.png)|Weex|CEX|[JKorf/Weex.Net](https://github.com/JKorf/Weex.Net)|[![Nuget version](https://img.shields.io/nuget/v/Weex.net.svg?style=flat-square)](https://www.nuget.org/packages/Weex.Net)|-|-| |![Weex](https://raw.githubusercontent.com/JKorf/Weex.Net/refs/heads/main/Weex.Net/Icon/icon.png)|Weex|CEX|[JKorf/Weex.Net](https://github.com/JKorf/Weex.Net)|[![Nuget version](https://img.shields.io/nuget/v/Weex.net.svg?style=flat-square)](https://www.nuget.org/packages/Weex.Net)|-|-|
@@ -127,6 +128,12 @@ Various:
* PlatformInfo now required support environment names in the constructor * PlatformInfo now required support environment names in the constructor
## Release notes ## Release notes
* Version 12.5.1 - 01 Sep 2026
* Fixed KlineTracker reporting incorrect High/Low price on GetStats result
* Fixed caching issue for auth requests
* Fixed failed ISymbolOrderBook start potentially leaving a pending background task
* Fixed subscription query completion potentially reporting success too early for topic-filtered routes
* Version 12.5.0 - 21 Aug 2026 * Version 12.5.0 - 21 Aug 2026
* Shared APIs * Shared APIs
* Added ISpotOrderManagementSocketClient for placing/canceling Spot orders via Shared websocket implementation * Added ISpotOrderManagementSocketClient for placing/canceling Spot orders via Shared websocket implementation
+1 -1
View File
@@ -2,7 +2,7 @@
> Base C#/.NET library for cryptocurrency exchange API client implementations. Provides a standardized abstraction (REST, WebSocket, authentication, rate limiting, error handling, order book management, shared cross-exchange interfaces) that 28+ exchange-specific libraries are built on top of. > Base C#/.NET library for cryptocurrency exchange API client implementations. Provides a standardized abstraction (REST, WebSocket, authentication, rate limiting, error handling, order book management, shared cross-exchange interfaces) that 28+ exchange-specific libraries are built on top of.
CryptoExchange.Net itself is not used directly — install one of the exchange-specific libraries (Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, Coinbase.Net, etc.) or `CryptoClients.Net` to access all exchanges via a single bundle. The base library is what makes the entire ecosystem feel consistent: same `HttpResult<T>` REST result pattern, same `WebSocketResult<UpdateSubscription>` websocket subscription pattern, same DI registration, same shared interfaces across all exchanges. Current version: 12.5.0. Targets netstandard2.0, netstandard2.1, net8.0, net9.0, net10.0. Native AOT supported. CryptoExchange.Net itself is not used directly — install one of the exchange-specific libraries (Binance.Net, Bybit.Net, OKX.Net, Kraken.Net, Coinbase.Net, etc.) or `CryptoClients.Net` to access all exchanges via a single bundle. The base library is what makes the entire ecosystem feel consistent: same `HttpResult<T>` REST result pattern, same `WebSocketResult<UpdateSubscription>` websocket subscription pattern, same DI registration, same shared interfaces across all exchanges. Current version: 12.5.1. Targets netstandard2.0, netstandard2.1, net8.0, net9.0, net10.0. Native AOT supported.
The standout feature for cross-exchange code is `CryptoExchange.Net.SharedApis` — a set of interfaces (`ISpotTickerRestClient`, `ISpotOrderRestClient`, `IBalanceRestClient`, etc.) implemented by every exchange library. Same call signature works against any exchange. The standout feature for cross-exchange code is `CryptoExchange.Net.SharedApis` — a set of interfaces (`ISpotTickerRestClient`, `ISpotOrderRestClient`, `IBalanceRestClient`, etc.) implemented by every exchange library. Same call signature works against any exchange.