1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-12 17:03:10 +00:00

Compare commits

...

26 Commits

Author SHA1 Message Date
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
63 changed files with 1474 additions and 1347 deletions
+12
View File
@@ -30,6 +30,18 @@ var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol));
`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. `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.
## Symbol metadata and catalogs
In 12.2.0, `SharedSpotSymbol` and `SharedFuturesSymbol` include `DisplayName` and base/quote asset classification through `SharedAssetType` (`Crypto`, `Fiat`, `TradFi`) and `SharedAssetSubType` (`StableCoin`, `Equity`, `Commodity`). Pass the matching base/quote filters to `GetSymbolsRequest` when discovery should return only a class of markets.
After calling `GetSpotSymbolsAsync`, `ISpotSymbolRestClient.SpotSymbolCatalog` maps asset and symbol names to shared metadata. `IFuturesSymbolRestClient.FuturesSymbolCatalog` works the same way after `GetFuturesSymbolsAsync`. Treat either property as unavailable before its corresponding request has populated the cache.
When implementing an exchange library, use `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` only as best-effort classifiers and supply exchange-specific additions where needed.
## Shared market-data quantities
In 12.4.0, use `SharedOrderQuantity`-valued `Volumes` on shared spot/futures tickers and klines, and `Quantities` on shared trades. The scalar `Volume`, `QuoteVolume`, and `Quantity` members are obsolete.
## Result pattern ## 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. REST methods return `HttpResult<T>` and websocket subscription methods return `WebSocketResult<UpdateSubscription>`. Always check `.Success`. `.Exchange` property identifies which exchange responded — useful for logging.
+10
View File
@@ -24,6 +24,16 @@ var ticker = await binance.GetSpotTickerAsync(new GetTickerRequest(symbol));
Same code works on every exchange that implements the interface. Use `Task.WhenAll` for concurrent multi-exchange calls. Same code works on every exchange that implements the interface. Use `Task.WhenAll` for concurrent multi-exchange calls.
## Shared symbol metadata
CryptoExchange.Net 12.2.0 classifies the base and quote sides of `SharedSpotSymbol` and `SharedFuturesSymbol` with `SharedAssetType` (`Crypto`, `Fiat`, `TradFi`) and optional `SharedAssetSubType` (`StableCoin`, `Equity`, `Commodity`). The models also expose `DisplayName`. Use the corresponding base/quote fields on `GetSymbolsRequest` to filter symbol discovery.
`ISpotSymbolRestClient.SpotSymbolCatalog` is populated by `GetSpotSymbolsAsync`; `IFuturesSymbolRestClient.FuturesSymbolCatalog` is populated by `GetFuturesSymbolsAsync`. Do not assume a catalog is available before that request. For exchange-library implementations, `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` offer best-effort classification and can be extended with exchange-specific values.
## Shared market-data quantities
CryptoExchange.Net 12.4.0 uses `SharedOrderQuantity` for market-data quantities. Prefer `Volumes` on `SharedSpotTicker`, `SharedFuturesTicker`, and `SharedKline`, and `Quantities` on `SharedTrade`; the scalar `Volume`, `QuoteVolume`, and `Quantity` members are obsolete.
## Single-exchange code uses the exchange's own client ## Single-exchange code uses the exchange's own client
For Binance-only code, use `BinanceRestClient` directly (see Binance.Net repo `AGENTS.md`). SharedApis is for portability — use it when you need that. For Binance-only code, use `BinanceRestClient` directly (see Binance.Net repo `AGENTS.md`). SharedApis is for portability — use it when you need that.
+22
View File
@@ -67,6 +67,28 @@ var btcusdtPerp = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
For exchanges that use exotic asset names, see the AssetAliases configuration. For exchanges that use exotic asset names, see the AssetAliases configuration.
## Symbol Metadata and Asset Classification
Since CryptoExchange.Net 12.2.0, shared symbol responses describe both sides of a market with `BaseAssetType`, `BaseAssetSubType`, `QuoteAssetType`, and `QuoteAssetSubType`. `SharedAssetType` distinguishes `Crypto`, `Fiat`, and `TradFi`; `SharedAssetSubType` distinguishes `StableCoin`, `Equity`, and `Commodity`. `SharedSpotSymbol` and `SharedFuturesSymbol` also expose `DisplayName`.
The same fields on `GetSymbolsRequest` filter spot or futures symbol discovery:
```csharp
var request = new GetSymbolsRequest(
baseAssetType: SharedAssetType.Crypto,
quoteAssetSubType: SharedAssetSubType.StableCoin);
var result = await symbolClient.GetSpotSymbolsAsync(request);
```
After calling `GetSpotSymbolsAsync` or `GetFuturesSymbolsAsync`, use the client's `SpotSymbolCatalog` or `FuturesSymbolCatalog` to look up normalized asset and symbol metadata by name. The catalog is unavailable until the corresponding symbol request has populated the cache.
For exchange-library implementations, `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` provide best-effort classification of known assets and accept exchange-specific additions. These helpers are heuristics, not an exhaustive source of truth.
## Shared Market-Data Quantities
Since CryptoExchange.Net 12.4.0, shared market-data models use `SharedOrderQuantity` so base-asset, quote-asset, and contract quantities remain explicit. Read `SharedSpotTicker.Volumes`, `SharedFuturesTicker.Volumes`, and `SharedKline.Volumes`; read `SharedTrade.Quantities`. The former scalar `Volume`, `QuoteVolume`, and `Quantity` members are obsolete.
## Available Shared Interfaces ## Available Shared Interfaces
**REST:** **REST:**
@@ -91,28 +91,25 @@ namespace CryptoExchange.Net.UnitTests
var evnt = new AsyncResetEvent(false, true); var evnt = new AsyncResetEvent(false, true);
var waiters = new List<Task<bool>>(); var waiters = new List<Task<bool>>();
for(var i = 0; i < 10; i++) for (var i = 0; i < 10; i++)
{ {
waiters.Add(evnt.WaitAsync()); waiters.Add(evnt.WaitAsync());
} }
List<bool>? results = null; var remaining = waiters.ToList();
var resultsWaiter = Task.Run(async () =>
{
await Task.WhenAll(waiters);
results = waiters.Select(w => w.Result).ToList();
});
for(var i = 1; i <= 10; i++) for (var i = 0; i < 10; i++)
{ {
evnt.Set(); 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(remaining, Is.Empty);
Assert.That(10 == results?.Count(r => r));
} }
[Test] [Test]
@@ -138,6 +138,68 @@ namespace CryptoExchange.Net.UnitTests.ClientTests
Assert.That(socket2.Connected == false); 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()] [TestCase()]
public async Task ErrorResponse_ShouldNot_ConfirmSubscription() public async Task ErrorResponse_ShouldNot_ConfirmSubscription()
{ {
@@ -15,6 +15,7 @@ namespace CryptoExchange.Net.UnitTests.ConverterTests
[TestCase("1620777600000")] [TestCase("1620777600000")]
[TestCase("2021-05-12T00:00:00.000Z")] [TestCase("2021-05-12T00:00:00.000Z")]
[TestCase("2021-05-12T00:00:00.000000000Z")] [TestCase("2021-05-12T00:00:00.000000000Z")]
[TestCase("2021-05-12 00:00:00.000000+00:00:00")]
[TestCase("0.000000", true)] [TestCase("0.000000", true)]
[TestCase("0", true)] [TestCase("0", true)]
[TestCase("", true)] [TestCase("", true)]
@@ -36,9 +36,13 @@ namespace CryptoExchange.Net.UnitTests.Implementations
protected override TestAuthenticationProvider CreateAuthenticationProvider(TestCredentials credentials) => protected override TestAuthenticationProvider CreateAuthenticationProvider(TestCredentials credentials) =>
new TestAuthenticationProvider(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));
}
}
}
+41 -33
View File
@@ -625,6 +625,21 @@ namespace CryptoExchange.Net.Clients
return Task.FromResult(CallResult.Ok()); 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> /// <summary>
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one. /// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
/// </summary> /// </summary>
@@ -643,10 +658,7 @@ namespace CryptoExchange.Net.Clients
string? topic = null, string? topic = null,
int individualSubscriptionCount = 1) int individualSubscriptionCount = 1)
{ {
var socketQuery = _socketConnections.Where(s => s.Value.ConnectionUriString.Equals(address.TrimEnd('/'), StringComparison.Ordinal) 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
&& 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
// If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection // 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; var delayStart = DateTime.UtcNow;
@@ -679,46 +691,27 @@ namespace CryptoExchange.Net.Clients
&& (s.Authenticated == authenticated || !authenticated) && (s.Authenticated == authenticated || !authenticated)
&& s.Connected).ToList(); && s.Connected).ToList();
SocketConnection? connection; bool maxConnectionsReached = _socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections);
if (!dedicatedRequestConnection) SocketConnection? connection = null;
{ if (dedicatedRequestConnection)
connection = socketQuery
.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection)
.OrderBy(s => s.UserSubscriptionCount)
.FirstOrDefault();
}
else
{ {
connection = socketQuery.Where(s => s.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault(); connection = socketQuery.Where(s => s.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault();
if (connection != null && !connection.DedicatedRequestConnection.Authenticated) if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
// Mark dedicated request connection as authenticated if the request is authenticated // Mark dedicated request connection as authenticated if the request is authenticated
connection.DedicatedRequestConnection.Authenticated = authenticated; connection.DedicatedRequestConnection.Authenticated = authenticated;
}
if (connection == null) if (connection == null)
// Fall back to an existing connection if there is no dedicated request connection available // Use an eligible non-dedicated connection for subscriptions, or as fallback when no dedicated request connection is available
connection = socketQuery.OrderBy(s => s.UserSubscriptionCount).FirstOrDefault(); connection = socketQuery
} .Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection)
.Where(s => IsConnectionEligible(s, individualSubscriptionCount, maxConnectionsReached))
.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); return CallResult.Ok(connection);
var currentCount = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount);
if (currentCount + individualSubscriptionCount <= MaxIndividualSubscriptionsPerConnection)
return CallResult.Ok(connection);
}
}
if (maxConnectionsReached) if (maxConnectionsReached)
return CallResult.Fail<SocketConnection>(new InvalidOperationError("Max amount of socket connections reached")); return CallResult.Fail<SocketConnection>(new InvalidOperationError("Max amount of socket connections reached"));
@@ -784,6 +777,21 @@ namespace CryptoExchange.Net.Clients
return CallResult.Ok(socketConnection); 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> /// <summary>
/// Process an unhandled message /// Process an unhandled message
/// </summary> /// </summary>
@@ -198,6 +198,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc); 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); 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); stream.Seek(0, SeekOrigin.Begin);
var written = new StreamReader(stream).ReadBlock(dataSnippet, 0, _errorResponseSnippetLimit); var written = new StreamReader(stream).ReadBlock(dataSnippet, 0, _errorResponseSnippetLimit);
var data = new string(dataSnippet, 0, written); var data = new string(dataSnippet, 0, written);
errorMsg += $": {data}"; errorMsg += $": {(string.IsNullOrEmpty(data) ? "(empty)" : data)}";
if (data.Length == _errorResponseSnippetLimit) if (data.Length == _errorResponseSnippetLimit)
errorMsg += " (truncated)"; errorMsg += " (truncated)";
} }
+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.1.0</PackageVersion> <PackageVersion>12.4.0</PackageVersion>
<AssemblyVersion>12.1.0</AssemblyVersion> <AssemblyVersion>12.4.0</AssemblyVersion>
<FileVersion>12.1.0</FileVersion> <FileVersion>12.4.0</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>
+71 -10
View File
@@ -33,7 +33,7 @@ namespace CryptoExchange.Net
if (keyedCache != null && DateTime.UtcNow - keyedCache.UpdateTime < TimeSpan.FromMinutes(60)) if (keyedCache != null && DateTime.UtcNow - keyedCache.UpdateTime < TimeSpan.FromMinutes(60))
return; 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> /// <summary>
@@ -118,6 +118,22 @@ namespace CryptoExchange.Net
return exchangeInfo.ParseSymbol(key, symbolName); 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 class ExchangeKeyedCache
{ {
private ExchangeInfo? _noKeyCache; private ExchangeInfo? _noKeyCache;
@@ -163,7 +179,7 @@ namespace CryptoExchange.Net
public SharedSymbol? ParseSymbol(string? key, string symbolName) public SharedSymbol? ParseSymbol(string? key, string symbolName)
{ {
SharedSymbol? symbolInfo = null; SharedSpotSymbol? symbolInfo = null;
if (key == null) if (key == null)
{ {
if (_noKeyCache != null) if (_noKeyCache != null)
@@ -173,7 +189,7 @@ namespace CryptoExchange.Net
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName) 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) 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) 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 return _noKeyCache.Symbols
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase)) .Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
.Select(x => x.Value) .Select(x => x.Value.SharedSymbol)
.ToArray(); .ToArray();
} }
@@ -274,7 +290,7 @@ namespace CryptoExchange.Net
{ {
result.AddRange(cache.Symbols result.AddRange(cache.Symbols
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase)) .Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
.Select(x => x.Value)); .Select(x => x.Value.SharedSymbol));
} }
return result.ToArray(); return result.ToArray();
@@ -286,18 +302,63 @@ namespace CryptoExchange.Net
return exchangeInfo.Symbols return exchangeInfo.Symbols
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase)) .Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
.Select(x => x.Value) .Select(x => x.Value.SharedSymbol)
.ToArray(); .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 class ExchangeInfo
{ {
public DateTime UpdateTime { get; set; } 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; UpdateTime = updateTime;
Symbols = symbols; Symbols = symbols;
+118
View File
@@ -4,6 +4,8 @@ using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO.Pipelines;
using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
@@ -14,6 +16,61 @@ namespace CryptoExchange.Net
/// </summary> /// </summary>
public static class LibraryHelpers 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; private static ILogger? _staticLogger;
/// <summary> /// <summary>
/// Static logger /// 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"); 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> /// <summary>
/// Create a new HttpMessageHandler instance /// Create a new HttpMessageHandler instance
/// </summary> /// </summary>
@@ -180,7 +180,7 @@ namespace CryptoExchange.Net.Logging.Extensions
_receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string, string>( _receivedMessageNotMatchedToAnyListener = LoggerMessage.Define<int, string, string, string>(
LogLevel.Warning, LogLevel.Warning,
new EventId(2029, "ReceivedMessageNotMatchedToAnyListener"), 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>( _failedToParse = LoggerMessage.Define<int, string>(
LogLevel.Warning, LogLevel.Warning,
@@ -326,9 +326,9 @@ namespace CryptoExchange.Net.Logging.Extensions
_sendingData(logger, socketId, requestId, data, null); _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) public static void SendingByteData(this ILogger logger, int socketId, int requestId, int length)
@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text; using System.Text;
@@ -8,8 +9,11 @@ namespace CryptoExchange.Net.Objects;
/// <summary> /// <summary>
/// Call result /// Call result
/// </summary> /// </summary>
[DebuggerDisplay("{DebugView,nq}")]
public record CallResult : ICallResult public record CallResult : ICallResult
{ {
private string DebugView => Success ? "Success" : $"Error: {Error}";
private static CallResult _successResult = new CallResult(); private static CallResult _successResult = new CallResult();
/// <inheritdoc /> /// <inheritdoc />
@@ -1,6 +1,7 @@
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
@@ -12,8 +13,11 @@ namespace CryptoExchange.Net.Objects;
/// <summary> /// <summary>
/// HTTP call result /// HTTP call result
/// </summary> /// </summary>
[DebuggerDisplay("{DebugView,nq}")]
public record HttpResult : IHttpResult public record HttpResult : IHttpResult
{ {
private string DebugView => $"[Req {RequestId}] " + (Success ? "Success" : $"Error: {Error}");
/// <summary> /// <summary>
/// Create a new success HTTP result /// Create a new success HTTP result
/// </summary> /// </summary>
@@ -251,8 +255,32 @@ public record HttpResult : IHttpResult
/// <inheritdoc /> /// <inheritdoc />
[DebuggerDisplay("{DebugView,nq}")]
public record HttpResult<T> : HttpResult, IHttpResult<T> 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> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text; using System.Text;
@@ -8,8 +9,11 @@ namespace CryptoExchange.Net.Objects;
/// <summary> /// <summary>
/// WebSocket call result /// WebSocket call result
/// </summary> /// </summary>
[DebuggerDisplay("{DebugView,nq}")]
public record WebSocketResult : IWebSocketResult public record WebSocketResult : IWebSocketResult
{ {
private string DebugView => $"[Sckt {ConnectionId}] " + (RequestId == null ? "" : $"[Req {RequestId}] ") + (Success ? "Success" : $"Error: {Error}");
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
@@ -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> /// </summary>
public class UpdateSubscription public class UpdateSubscription
{ {
private readonly SocketConnection _connection; private readonly SocketConnection? _connection;
private readonly ManualUpdateSubscription? _manualSubscription;
internal readonly Subscription _subscription; internal readonly Subscription _subscription;
#if NET9_0_OR_GREATER #if NET9_0_OR_GREATER
@@ -102,7 +103,7 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <summary> /// <summary>
/// The id of the socket /// The id of the socket
/// </summary> /// </summary>
public int SocketId => _connection.SocketId; public int SocketId => _connection?.SocketId ?? _manualSubscription!.SocketId;
/// <summary> /// <summary>
/// The id of the subscription /// The id of the subscription
@@ -112,12 +113,12 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <summary> /// <summary>
/// The last timestamp anything was received from the server /// The last timestamp anything was received from the server
/// </summary> /// </summary>
public DateTime? LastReceiveTime => _connection.LastReceiveTime; public DateTime? LastReceiveTime => _connection?.LastReceiveTime ?? _manualSubscription!.LastReceiveTime;
/// <summary> /// <summary>
/// The current websocket status /// The current websocket status
/// </summary> /// </summary>
public SocketStatus SocketStatus => _connection.Status; public SocketStatus SocketStatus => _connection?.Status ?? _manualSubscription!.SocketStatus;
/// <summary> /// <summary>
/// The current subscription status /// The current subscription status
@@ -143,6 +144,18 @@ namespace CryptoExchange.Net.Objects.Sockets
_subscription.StatusChanged += (x) => SubscriptionStatusChanged?.Invoke(x); _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() private void UnsubscribeConnectionEvents()
{ {
lock (_eventLock) lock (_eventLock)
@@ -150,22 +163,26 @@ namespace CryptoExchange.Net.Objects.Sockets
if (!_connectionEventsSubscribed) if (!_connectionEventsSubscribed)
return; return;
if (_connection != null)
{
_connection.ConnectionClosed -= HandleConnectionClosedEvent; _connection.ConnectionClosed -= HandleConnectionClosedEvent;
_connection.ConnectionLost -= HandleConnectionLostEvent; _connection.ConnectionLost -= HandleConnectionLostEvent;
_connection.ConnectionRestored -= HandleConnectionRestoredEvent; _connection.ConnectionRestored -= HandleConnectionRestoredEvent;
_connection.ResubscribingFailed -= HandleResubscribeFailedEvent; _connection.ResubscribingFailed -= HandleResubscribeFailedEvent;
_connection.ActivityPaused -= HandlePausedEvent; _connection.ActivityPaused -= HandlePausedEvent;
_connection.ActivityUnpaused -= HandleUnpausedEvent; _connection.ActivityUnpaused -= HandleUnpausedEvent;
}
_connectionEventsSubscribed = false; _connectionEventsSubscribed = false;
} }
} }
private void HandleConnectionClosedEvent() internal void HandleConnectionClosedEvent()
{ {
UnsubscribeConnectionEvents(); UnsubscribeConnectionEvents();
// If we're not the subscription closing this connection don't bother emitting // If we're not the subscription closing this connection don't bother emitting
if (!_subscription.IsClosingConnection) if (_connection != null && !_subscription.IsClosingConnection)
return; return;
List<Action> handlers; List<Action> handlers;
@@ -176,7 +193,7 @@ namespace CryptoExchange.Net.Objects.Sockets
callback(); callback();
} }
private void HandleConnectionLostEvent() internal void HandleConnectionLostEvent()
{ {
if (!_subscription.Active) if (!_subscription.Active)
{ {
@@ -192,7 +209,7 @@ namespace CryptoExchange.Net.Objects.Sockets
callback(); callback();
} }
private void HandleConnectionRestoredEvent(TimeSpan period) internal void HandleConnectionRestoredEvent(TimeSpan period)
{ {
if (!_subscription.Active) if (!_subscription.Active)
{ {
@@ -208,7 +225,7 @@ namespace CryptoExchange.Net.Objects.Sockets
callback(period); callback(period);
} }
private void HandleResubscribeFailedEvent(Error error) internal void HandleResubscribeFailedEvent(Error error)
{ {
if (!_subscription.Active) if (!_subscription.Active)
{ {
@@ -224,7 +241,7 @@ namespace CryptoExchange.Net.Objects.Sockets
callback(error); callback(error);
} }
private void HandlePausedEvent() internal void HandlePausedEvent()
{ {
if (!_subscription.Active) if (!_subscription.Active)
{ {
@@ -240,7 +257,7 @@ namespace CryptoExchange.Net.Objects.Sockets
callback(); callback();
} }
private void HandleUnpausedEvent() internal void HandleUnpausedEvent()
{ {
if (!_subscription.Active) if (!_subscription.Active)
{ {
@@ -262,7 +279,10 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <returns></returns> /// <returns></returns>
public Task CloseAsync() public Task CloseAsync()
{ {
if (_connection != null)
return _connection.CloseAsync(_subscription); return _connection.CloseAsync(_subscription);
return _manualSubscription!.CloseAsync();
} }
/// <summary> /// <summary>
@@ -271,7 +291,10 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <returns></returns> /// <returns></returns>
public Task ReconnectAsync() public Task ReconnectAsync()
{ {
if (_connection != null)
return _connection.TriggerReconnectAsync(); return _connection.TriggerReconnectAsync();
return _manualSubscription!.ReconnectAsync();
} }
/// <summary> /// <summary>
@@ -279,8 +302,14 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
internal async Task UnsubscribeAsync() internal async Task UnsubscribeAsync()
{
if (_connection != null)
{ {
await _connection.UnsubscribeAsync(_subscription).ConfigureAwait(false); await _connection.UnsubscribeAsync(_subscription).ConfigureAwait(false);
return;
}
await _manualSubscription!.CloseAsync().ConfigureAwait(false);
} }
/// <summary> /// <summary>
@@ -289,7 +318,10 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <returns></returns> /// <returns></returns>
internal async Task<CallResult> ResubscribeAsync() internal async Task<CallResult> ResubscribeAsync()
{ {
if (_connection != null)
return await _connection.ResubscribeAsync(_subscription).ConfigureAwait(false); return await _connection.ResubscribeAsync(_subscription).ConfigureAwait(false);
return await _manualSubscription!.ResubscribeAsync().ConfigureAwait(false);
} }
} }
} }
@@ -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
}
}
@@ -9,6 +9,11 @@ namespace CryptoExchange.Net.SharedApis
/// </summary> /// </summary>
public interface IFuturesSymbolRestClient : ISharedClient public interface IFuturesSymbolRestClient : ISharedClient
{ {
/// <summary>
/// Get the futures symbol catalog. Only available if <see cref="GetFuturesSymbolsAsync(GetSymbolsRequest, CancellationToken)"/> has been called previously.
/// </summary>
SharedSymbolCatalog? FuturesSymbolCatalog { get; }
/// <summary> /// <summary>
/// Futures symbol request options.<br /> /// Futures symbol 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="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
@@ -9,6 +9,11 @@ namespace CryptoExchange.Net.SharedApis
/// </summary> /// </summary>
public interface ISpotSymbolRestClient : ISharedClient public interface ISpotSymbolRestClient : ISharedClient
{ {
/// <summary>
/// Get the spot symbol catalog. Only available if <see cref="GetSpotSymbolsAsync(GetSymbolsRequest, CancellationToken)"/> has been called previously.
/// </summary>
SharedSymbolCatalog? SpotSymbolCatalog { get; }
/// <summary> /// <summary>
/// Spot symbols request options.<br /> /// Spot symbols 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="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
@@ -70,8 +70,13 @@ namespace CryptoExchange.Net.SharedApis
foreach (var param in RequiredExchangeParameters) foreach (var param in RequiredExchangeParameters)
{ {
if (param.Names!.All(x => ExchangeParameters.HasValue(exchangeParameters, Exchange, x, param.ValueType) != true)) if (param.Names!.All(x => ExchangeParameters.HasValue(exchangeParameters, Exchange, x, param.ValueType) != true))
{
if (param.Names.Length == 1)
return ArgumentError.Invalid(string.Join("/", param.Names!), $"Exchange parameter `{param.Names[0]}` for exchange `{Exchange}` should be provided. Example: {param.ExampleValue}");
else
return ArgumentError.Invalid(string.Join("/", param.Names!), $"One of exchange parameters `{string.Join(", ", param.Names!)}` for exchange `{Exchange}` should be provided. Example: {param.ExampleValue}"); return ArgumentError.Invalid(string.Join("/", param.Names!), $"One of exchange parameters `{string.Join(", ", param.Names!)}` for exchange `{Exchange}` should be provided. Example: {param.ExampleValue}");
} }
}
return null; return null;
} }
@@ -149,8 +154,13 @@ namespace CryptoExchange.Net.SharedApis
foreach (var param in RequiredOptionalParameters) foreach (var param in RequiredOptionalParameters)
{ {
if (param.Names!.All(x => _requestProperties.Single(p => p.Name == x).GetValue(request, null) == null)) if (param.Names!.All(x => _requestProperties.Single(p => p.Name == x).GetValue(request, null) == null))
{
if (param.Names.Length == 1)
return ArgumentError.Invalid(string.Join("/", param.Names!), $"Optional parameter `{param.Names[0]}` for exchange `{Exchange}` should be provided. Example: {param.ExampleValue}");
else
return ArgumentError.Invalid(string.Join("/", param.Names!), $"One of optional parameters `{string.Join(", ", param.Names!)}` for exchange `{Exchange}` should be provided. Example: {param.ExampleValue}"); return ArgumentError.Invalid(string.Join("/", param.Names!), $"One of optional parameters `{string.Join(", ", param.Names!)}` for exchange `{Exchange}` should be provided. Example: {param.ExampleValue}");
} }
}
if (request is SharedSymbolRequest symbolsRequest) if (request is SharedSymbolRequest symbolsRequest)
{ {
@@ -1,4 +1,5 @@
using System; using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
@@ -15,5 +16,43 @@ namespace CryptoExchange.Net.SharedApis
public GetFuturesSymbolsOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(IFuturesSymbolRestClient.GetFuturesSymbolsAsync)) public GetFuturesSymbolsOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(IFuturesSymbolRestClient.GetFuturesSymbolsAsync))
{ {
} }
/// <inheritdoc />
public override Error? ValidateRequest(GetSymbolsRequest request, IFuturesSymbolRestClient client)
{
if (request.BaseAssetType != null && request.BaseAssetSubType != null)
{
var error = ValidateAssetTypeCombination(request.BaseAssetType.Value, request.BaseAssetSubType.Value);
if (error != null)
return error;
}
if (request.QuoteAssetType != null && request.QuoteAssetSubType != null)
{
var error = ValidateAssetTypeCombination(request.QuoteAssetType.Value, request.QuoteAssetSubType.Value);
if (error != null)
return error;
}
return base.ValidateRequest(request, client);
}
private Error? ValidateAssetTypeCombination(SharedAssetType type, SharedAssetSubType subType)
{
if (type == SharedAssetType.Crypto
&& (subType == SharedAssetSubType.Commodity
|| (subType == SharedAssetSubType.Equity)))
{
return ArgumentError.Invalid(nameof(GetSymbolsRequest.BaseAssetSubType), $"Invalid combination of asset type filters: {type} and {subType}");
}
if (type == SharedAssetType.TradFi && subType == SharedAssetSubType.StableCoin)
return ArgumentError.Invalid(nameof(GetSymbolsRequest.BaseAssetSubType), $"Invalid combination of asset type filters: {type} and {subType}");
if (type == SharedAssetType.Fiat)
return ArgumentError.Invalid(nameof(GetSymbolsRequest.BaseAssetSubType), $"Invalid combination of asset type filters: {type} and {subType}");
return null;
}
} }
} }
@@ -1,4 +1,5 @@
using System; using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
@@ -15,5 +16,44 @@ namespace CryptoExchange.Net.SharedApis
public GetSpotSymbolsOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(ISpotSymbolRestClient.GetSpotSymbolsAsync)) public GetSpotSymbolsOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(ISpotSymbolRestClient.GetSpotSymbolsAsync))
{ {
} }
/// <inheritdoc />
public override Error? ValidateRequest(GetSymbolsRequest request, ISpotSymbolRestClient client)
{
if (request.BaseAssetType != null && request.BaseAssetSubType != null)
{
var error = ValidateAssetTypeCombination(request.BaseAssetType.Value, request.BaseAssetSubType.Value);
if (error != null)
return error;
}
if (request.QuoteAssetType != null && request.QuoteAssetSubType != null)
{
var error = ValidateAssetTypeCombination(request.QuoteAssetType.Value, request.QuoteAssetSubType.Value);
if (error != null)
return error;
}
return base.ValidateRequest(request, client);
}
private Error? ValidateAssetTypeCombination(SharedAssetType type, SharedAssetSubType subType)
{
if (type == SharedAssetType.Crypto
&& (subType == SharedAssetSubType.Commodity
|| (subType == SharedAssetSubType.Equity)))
{
return ArgumentError.Invalid(nameof(GetSymbolsRequest.BaseAssetSubType), $"Invalid combination of asset type filters: {type} and {subType}");
}
if (type == SharedAssetType.TradFi && subType == SharedAssetSubType.StableCoin)
return ArgumentError.Invalid(nameof(GetSymbolsRequest.BaseAssetSubType), $"Invalid combination of asset type filters: {type} and {subType}");
if (type == SharedAssetType.Fiat)
return ArgumentError.Invalid(nameof(GetSymbolsRequest.BaseAssetSubType), $"Invalid combination of asset type filters: {type} and {subType}");
return null;
}
} }
} }
@@ -5,13 +5,44 @@
/// </summary> /// </summary>
public record GetSymbolsRequest : SharedRequest public record GetSymbolsRequest : SharedRequest
{ {
/// <summary>
/// Base asset type filter
/// </summary>
public SharedAssetType? BaseAssetType { get; }
/// <summary>
/// Base asset subtype filter
/// </summary>
public SharedAssetSubType? BaseAssetSubType { get; }
/// <summary>
/// Quote asset type filter
/// </summary>
public SharedAssetType? QuoteAssetType { get; }
/// <summary>
/// Quote asset subtype filter
/// </summary>
public SharedAssetSubType? QuoteAssetSubType { get; }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
/// <param name="tradingMode">Trading mode filter</param> /// <param name="tradingMode">Trading mode filter</param>
/// <param name="baseAssetType">Filter by base asset type</param>
/// <param name="baseAssetSubType">Filter by base asset subtype</param>
/// <param name="quoteAssetType">Filter by quote asset type</param>
/// <param name="quoteAssetSubType">Filter by quote asset subtype</param>
/// <param name="exchangeParameters">Exchange specific parameters</param> /// <param name="exchangeParameters">Exchange specific parameters</param>
public GetSymbolsRequest(TradingMode? tradingMode = null, ExchangeParameters? exchangeParameters = null) : base(tradingMode, exchangeParameters) public GetSymbolsRequest(
TradingMode? tradingMode = null,
SharedAssetType? baseAssetType = null,
SharedAssetSubType? baseAssetSubType = null,
SharedAssetType? quoteAssetType = null,
SharedAssetSubType? quoteAssetSubType = null,
ExchangeParameters? exchangeParameters = null) : base(tradingMode, exchangeParameters)
{ {
BaseAssetType = baseAssetType;
BaseAssetSubType = baseAssetSubType;
QuoteAssetType = quoteAssetType;
QuoteAssetSubType = quoteAssetSubType;
} }
} }
} }
@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Symbol and asset catalog for a shared client
/// </summary>
public class SharedSymbolCatalog
{
/// <summary>
/// Exchange name
/// </summary>
public string Exchange { get; set; } = string.Empty;
/// <summary>
/// Assets supported
/// </summary>
public IReadOnlyDictionary<string, SharedAssetInfo> Assets { get; set; } = new Dictionary<string, SharedAssetInfo>();
/// <summary>
/// Symbols supported
/// </summary>
public IReadOnlyDictionary<string, SharedSpotSymbol> Symbols { get; set; } = new Dictionary<string, SharedSpotSymbol>();
}
/// <summary>
/// Asset info
/// </summary>
[DebuggerDisplay("{DebugView,nq}")]
public class SharedAssetInfo
{
private string DebugView => $"{Name} - {Type}{(SubType == null ? "": $" {SubType}")}";
/// <summary>
/// Asset name
/// </summary>
public string Name { get; set; }
/// <summary>
/// Asset type
/// </summary>
public SharedAssetType Type { get; set; }
/// <summary>
/// Asset sub type
/// </summary>
public SharedAssetSubType? SubType { get; set; }
/// <summary>
/// ctor
/// </summary>
public SharedAssetInfo(string name, SharedAssetType type, SharedAssetSubType? subType)
{
Name = name;
Type = type;
SubType = subType;
}
}
}
@@ -1,10 +1,12 @@
using System; using System;
using System.Diagnostics;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Asset info /// Asset info
/// </summary> /// </summary>
[DebuggerDisplay("{Name,nq} - {Networks.Length} network(s)")]
public record SharedAsset public record SharedAsset
{ {
/// <summary> /// <summary>
@@ -32,6 +34,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Asset network info /// Asset network info
/// </summary> /// </summary>
[DebuggerDisplay("{Name,nq}")]
public record SharedAssetNetwork public record SharedAssetNetwork
{ {
/// <summary> /// <summary>
@@ -1,8 +1,11 @@
namespace CryptoExchange.Net.SharedApis using System.Diagnostics;
namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Balance info /// Balance info
/// </summary> /// </summary>
[DebuggerDisplay("{Available} {Asset, nq}")]
public record SharedBalance public record SharedBalance
{ {
/// <summary> /// <summary>
@@ -1,8 +1,11 @@
namespace CryptoExchange.Net.SharedApis using System.Diagnostics;
namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Book ticker /// Book ticker
/// </summary> /// </summary>
[DebuggerDisplay("{Symbol,nq} - {BestBidPrice} / {BestAskPrice}")]
public record SharedBookTicker : SharedSymbolModel public record SharedBookTicker : SharedSymbolModel
{ {
/// <summary> /// <summary>
@@ -1,10 +1,12 @@
using System; using System;
using System.Diagnostics;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Deposit info /// Deposit info
/// </summary> /// </summary>
[DebuggerDisplay("[{Timestamp}] {Quantity} {Asset,nq}")]
public record SharedDeposit public record SharedDeposit
{ {
/// <summary> /// <summary>
@@ -1,8 +1,11 @@
namespace CryptoExchange.Net.SharedApis using System.Diagnostics;
namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Deposit address info /// Deposit address info
/// </summary> /// </summary>
[DebuggerDisplay("{Asset,nq} - {Address,nq}")]
public record SharedDepositAddress public record SharedDepositAddress
{ {
/// <summary> /// <summary>
@@ -1,8 +1,11 @@
namespace CryptoExchange.Net.SharedApis using System.Diagnostics;
namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Trading fee info /// Trading fee info
/// </summary> /// </summary>
[DebuggerDisplay("{MakerFee} / {TakerFee}")]
public record SharedFee public record SharedFee
{ {
/// <summary> /// <summary>
@@ -1,10 +1,12 @@
using System; using System;
using System.Diagnostics;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Funding rate /// Funding rate
/// </summary> /// </summary>
[DebuggerDisplay("[{Timestamp}] {FundingRate}")]
public record SharedFundingRate public record SharedFundingRate
{ {
/// <summary> /// <summary>
@@ -1,10 +1,12 @@
using System; using System;
using System.Diagnostics;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Mark/index price kline /// Mark/index price kline
/// </summary> /// </summary>
[DebuggerDisplay("[{OpenTime}] O: {OpenPrice} H: {HighPrice} L: {LowPrice} C: {ClosePrice}")]
public record SharedFuturesKline : SharedSymbolModel public record SharedFuturesKline : SharedSymbolModel
{ {
/// <summary> /// <summary>
@@ -1,12 +1,19 @@
using System; using System;
using System.Diagnostics;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Futures order info /// Futures order info
/// </summary> /// </summary>
[DebuggerDisplay("{DebugView,nq}")]
public record SharedFuturesOrder : SharedSymbolModel public record SharedFuturesOrder : SharedSymbolModel
{ {
private string DebugView =>
$"[{CreateTime}] {OrderId} {(PositionSide != null ? $"{PositionSide} " : "")}{Symbol} - " +
$"{OrderType} {Side} {OrderQuantity}{(OrderPrice != null ? " @ " + OrderPrice : "")}, " +
$"{Status}{(QuantityFilled != null && Status != SharedOrderStatus.Canceled ? $" {QuantityFilled}" : "")}{(AveragePrice != null ? " @ " + AveragePrice : "")}";
/// <summary> /// <summary>
/// Id of the order /// Id of the order
/// </summary> /// </summary>
@@ -47,10 +54,18 @@ namespace CryptoExchange.Net.SharedApis
/// Order price /// Order price
/// </summary> /// </summary>
public decimal? OrderPrice { get; set; } public decimal? OrderPrice { get; set; }
private decimal? _averagePrice;
/// <summary> /// <summary>
/// Average price /// Average fill price
/// </summary> /// </summary>
public decimal? AveragePrice { get; set; } public decimal? AveragePrice
{
get => _averagePrice > 0 ? _averagePrice
: (QuantityFilled?.QuantityInBaseAsset > 0 && QuantityFilled?.QuantityInQuoteAsset > 0
? QuantityFilled.QuantityInQuoteAsset / QuantityFilled.QuantityInBaseAsset
: null);
set => _averagePrice = value;
}
/// <summary> /// <summary>
/// Client order id /// Client order id
/// </summary> /// </summary>
@@ -1,12 +1,17 @@
using System; using System;
using System.Diagnostics;
using System.Drawing;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Futures symbol info /// Futures symbol info
/// </summary> /// </summary>
[DebuggerDisplay("{DebugView,nq}")]
public record SharedFuturesSymbol : SharedSpotSymbol public record SharedFuturesSymbol : SharedSpotSymbol
{ {
private string DebugView => $"{TradingMode} {(DisplayName ?? Name)} - {BaseAssetType}{(BaseAssetSubType == null ? "" : " " + BaseAssetSubType)}{(DeliveryTime != null ? $" Delivery: {DeliveryTime:yyyy-MM-dd}": "")}";
/// <summary> /// <summary>
/// The size of a single contract /// The size of a single contract
/// </summary> /// </summary>
@@ -1,10 +1,12 @@
using System; using System;
using System.Diagnostics;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Futures ticker info /// Futures ticker info
/// </summary> /// </summary>
[DebuggerDisplay("{Symbol,nq} High: {HighPrice}, Low: {LowPrice}, Last: {LastPrice}, Change: {ChangePercentage}%")]
public record SharedFuturesTicker: SharedSymbolModel public record SharedFuturesTicker: SharedSymbolModel
{ {
/// <summary> /// <summary>
@@ -22,7 +24,24 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// The volume in the last 24h /// The volume in the last 24h
/// </summary> /// </summary>
public decimal Volume { get; set; } public SharedOrderQuantity Volumes { get; set; }
private decimal? _volume;
/// <summary>
/// The volume in the last 24h
/// </summary>
[Obsolete("Use `Volumes` instead")]
public decimal Volume
{
get
{
if (_volume.HasValue)
return _volume.Value;
return Volumes.QuantityInBaseAsset ?? Volumes.QuantityInContracts ?? 0;
}
set => _volume = value;
}
/// <summary> /// <summary>
/// Change percentage in the last 24h /// Change percentage in the last 24h
/// </summary> /// </summary>
@@ -47,13 +66,20 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
public SharedFuturesTicker(SharedSymbol? sharedSymbol, string symbol, decimal? lastPrice, decimal? highPrice, decimal? lowPrice, decimal volume, decimal? changePercentage) public SharedFuturesTicker(
SharedSymbol? sharedSymbol,
string symbol,
decimal? lastPrice,
decimal? highPrice,
decimal? lowPrice,
SharedOrderQuantity volumes,
decimal? changePercentage)
:base(sharedSymbol, symbol) :base(sharedSymbol, symbol)
{ {
LastPrice = lastPrice; LastPrice = lastPrice;
HighPrice = highPrice; HighPrice = highPrice;
LowPrice = lowPrice; LowPrice = lowPrice;
Volume = volume; Volumes = volumes;
ChangePercentage = changePercentage; ChangePercentage = changePercentage;
} }
} }
@@ -1,10 +1,12 @@
using System; using System;
using System.Diagnostics;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Kline info /// Kline info
/// </summary> /// </summary>
[DebuggerDisplay("[{OpenTime}] O: {OpenPrice} H: {HighPrice} L: {LowPrice} C: {ClosePrice} V: {Volumes}")]
public record SharedKline : SharedSymbolModel public record SharedKline : SharedSymbolModel
{ {
/// <summary> /// <summary>
@@ -27,15 +29,40 @@ namespace CryptoExchange.Net.SharedApis
/// Open price /// Open price
/// </summary> /// </summary>
public decimal OpenPrice { get; set; } public decimal OpenPrice { get; set; }
private decimal? _volume;
/// <summary> /// <summary>
/// Volume in the base asset /// Volume in the base asset
/// </summary> /// </summary>
public decimal Volume { get; set; } [Obsolete("Use `Volumes` instead")]
public decimal Volume
{
get
{
if (_volume.HasValue)
return _volume.Value;
return Volumes.QuantityInBaseAsset ?? Volumes.QuantityInContracts ?? 0;
}
set => _volume = value;
}
/// <summary>
/// The volume in the last 24h
/// </summary>
public SharedOrderQuantity Volumes { get; set; }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
public SharedKline(SharedSymbol? sharedSymbol, string symbol, DateTime openTime, decimal closePrice, decimal highPrice, decimal lowPrice, decimal openPrice, decimal volume) public SharedKline(
SharedSymbol? sharedSymbol,
string symbol,
DateTime openTime,
decimal closePrice,
decimal highPrice,
decimal lowPrice,
decimal openPrice,
SharedOrderQuantity volumes)
: base(sharedSymbol, symbol) : base(sharedSymbol, symbol)
{ {
OpenTime = openTime; OpenTime = openTime;
@@ -43,7 +70,7 @@ namespace CryptoExchange.Net.SharedApis
HighPrice = highPrice; HighPrice = highPrice;
LowPrice = lowPrice; LowPrice = lowPrice;
OpenPrice = openPrice; OpenPrice = openPrice;
Volume = volume; Volumes = volumes;
} }
} }
} }
@@ -1,10 +1,12 @@
using System; using System;
using System.Diagnostics;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Position info /// Position info
/// </summary> /// </summary>
[DebuggerDisplay("{Symbol,nq} {PositionSide}: {PositionSize} {AverageOpenPrice}")]
public record SharedPosition : SharedSymbolModel public record SharedPosition : SharedSymbolModel
{ {
/// <summary> /// <summary>
@@ -1,10 +1,12 @@
using System; using System;
using System.Diagnostics;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Position history /// Position history
/// </summary> /// </summary>
[DebuggerDisplay("[{Timestamp}] {Symbol,nq} {PositionSide}: {RealizedPnl}")]
public record SharedPositionHistory : SharedSymbolModel public record SharedPositionHistory : SharedSymbolModel
{ {
/// <summary> /// <summary>
@@ -1,12 +1,19 @@
using System; using System;
using System.Diagnostics;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Spot order info /// Spot order info
/// </summary> /// </summary>
[DebuggerDisplay("{DebugView,nq}")]
public record SharedSpotOrder : SharedSymbolModel public record SharedSpotOrder : SharedSymbolModel
{ {
private string DebugView =>
$"[{CreateTime}] {OrderId} {Symbol} - " +
$"{OrderType} {Side} {OrderQuantity}{(OrderPrice != null ? " @ " + OrderPrice : "")}, " +
$"{Status}{(QuantityFilled != null && Status != SharedOrderStatus.Canceled ? $" {QuantityFilled}" : "")}{(AveragePrice != null ? " @ " + AveragePrice : "")}";
/// <summary> /// <summary>
/// The id of the order /// The id of the order
/// </summary> /// </summary>
@@ -39,10 +46,18 @@ namespace CryptoExchange.Net.SharedApis
/// Order price /// Order price
/// </summary> /// </summary>
public decimal? OrderPrice { get; set; } public decimal? OrderPrice { get; set; }
private decimal? _averagePrice;
/// <summary> /// <summary>
/// Average fill price /// Average fill price
/// </summary> /// </summary>
public decimal? AveragePrice { get; set; } public decimal? AveragePrice
{
get => _averagePrice > 0 ? _averagePrice
: (QuantityFilled?.QuantityInBaseAsset > 0 && QuantityFilled?.QuantityInQuoteAsset > 0
? QuantityFilled.QuantityInQuoteAsset / QuantityFilled.QuantityInBaseAsset
: null);
set => _averagePrice = value;
}
/// <summary> /// <summary>
/// Client order id /// Client order id
/// </summary> /// </summary>
@@ -1,10 +1,17 @@
namespace CryptoExchange.Net.SharedApis using System;
using System.Data;
using System.Diagnostics;
namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Symbol info /// Symbol info
/// </summary> /// </summary>
[DebuggerDisplay("{DebugView,nq}")]
public record SharedSpotSymbol public record SharedSpotSymbol
{ {
private string DebugView => $"{TradingMode} {(DisplayName ?? Name)} - {BaseAssetType} {BaseAssetSubType}";
/// <summary> /// <summary>
/// The trading mode of the symbol /// The trading mode of the symbol
/// </summary> /// </summary>
@@ -22,6 +29,10 @@
/// </summary> /// </summary>
public string Name { get; set; } public string Name { get; set; }
/// <summary> /// <summary>
/// The display name of the symbol
/// </summary>
public string? DisplayName { get; set; }
/// <summary>
/// Minimal quantity of an order in the base asset /// Minimal quantity of an order in the base asset
/// </summary> /// </summary>
public decimal? MinTradeQuantity { get; set; } public decimal? MinTradeQuantity { get; set; }
@@ -57,6 +68,22 @@
/// Whether the symbol is currently available for trading /// Whether the symbol is currently available for trading
/// </summary> /// </summary>
public bool Trading { get; set; } public bool Trading { get; set; }
/// <summary>
/// Base asset type
/// </summary>
public SharedAssetType BaseAssetType { get; set; }
/// <summary>
/// Base asset sub type
/// </summary>
public SharedAssetSubType? BaseAssetSubType { get; set; }
/// <summary>
/// Quote asset type
/// </summary>
public SharedAssetType QuoteAssetType { get; set; }
/// <summary>
/// Quote asset sub type
/// </summary>
public SharedAssetSubType? QuoteAssetSubType { get; set; }
/// <summary> /// <summary>
/// ctor /// ctor
@@ -1,8 +1,12 @@
namespace CryptoExchange.Net.SharedApis using System;
using System.Diagnostics;
namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Ticker info /// Ticker info
/// </summary> /// </summary>
[DebuggerDisplay("{Symbol,nq} High: {HighPrice}, Low: {LowPrice}, Last: {LastPrice}, Change: {ChangePercentage}%")]
public record SharedSpotTicker: SharedSymbolModel public record SharedSpotTicker: SharedSymbolModel
{ {
/// <summary> /// <summary>
@@ -18,13 +22,31 @@
/// </summary> /// </summary>
public decimal? LowPrice { get; set; } public decimal? LowPrice { get; set; }
/// <summary> /// <summary>
/// The volume in the last 24h
/// </summary>
public SharedOrderQuantity Volumes { get; set; }
private decimal? _volume;
/// <summary>
/// Trade volume in base asset in the last 24h /// Trade volume in base asset in the last 24h
/// </summary> /// </summary>
public decimal Volume { get; set; } [Obsolete("Use `Volumes` instead")]
public decimal Volume
{
get
{
if (_volume.HasValue)
return _volume.Value;
return Volumes.QuantityInBaseAsset ?? Volumes.QuantityInContracts ?? 0;
}
set => _volume = value;
}
/// <summary> /// <summary>
/// Trade volume in quote asset in the last 24h /// Trade volume in quote asset in the last 24h
/// </summary> /// </summary>
public decimal? QuoteVolume { get; set; } [Obsolete("Use `Volumes` instead")]
public decimal? QuoteVolume => Volumes?.QuantityInQuoteAsset;
/// <summary> /// <summary>
/// Change percentage in the last 24h /// Change percentage in the last 24h
/// </summary> /// </summary>
@@ -33,13 +55,20 @@
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
public SharedSpotTicker(SharedSymbol? sharedSymbol, string symbol, decimal? lastPrice, decimal? highPrice, decimal? lowPrice, decimal volume, decimal? changePercentage) public SharedSpotTicker(
SharedSymbol? sharedSymbol,
string symbol,
decimal? lastPrice,
decimal? highPrice,
decimal? lowPrice,
SharedOrderQuantity volumes,
decimal? changePercentage)
: base(sharedSymbol, symbol) : base(sharedSymbol, symbol)
{ {
LastPrice = lastPrice; LastPrice = lastPrice;
HighPrice = highPrice; HighPrice = highPrice;
LowPrice = lowPrice; LowPrice = lowPrice;
Volume = volume; Volumes = volumes;
ChangePercentage = changePercentage; ChangePercentage = changePercentage;
} }
} }
@@ -1,16 +1,23 @@
using System; using System;
using System.Diagnostics;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Public trade info /// Public trade info
/// </summary> /// </summary>
[DebuggerDisplay("[{Timestamp}] {Symbol,nq} {Side.ToString(),nq} {Quantity} @ {Price}")]
public record SharedTrade : SharedSymbolModel public record SharedTrade : SharedSymbolModel
{ {
/// <summary> /// <summary>
/// Quantity of the trade /// Quantity of the trade
/// </summary> /// </summary>
public decimal Quantity { get; set; } [Obsolete("Use `Quantities` instead")]
public decimal Quantity => Quantities.QuantityInBaseAsset ?? Quantities.QuantityInContracts ?? 0;
/// <summary>
/// The quantities of the trade
/// </summary>
public SharedOrderQuantity Quantities { get; set; }
/// <summary> /// <summary>
/// Price of the trade /// Price of the trade
/// </summary> /// </summary>
@@ -27,9 +34,9 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
public SharedTrade(SharedSymbol? sharedSymbol, string symbol, decimal quantity, decimal price, DateTime timestamp) : base(sharedSymbol, symbol) public SharedTrade(SharedSymbol? sharedSymbol, string symbol, SharedOrderQuantity quantities, decimal price, DateTime timestamp) : base(sharedSymbol, symbol)
{ {
Quantity = quantity; Quantities = quantities;
Price = price; Price = price;
Timestamp = timestamp; Timestamp = timestamp;
} }
@@ -1,10 +1,12 @@
using System; using System;
using System.Diagnostics;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// A user trade /// A user trade
/// </summary> /// </summary>
[DebuggerDisplay("[{Timestamp}] {Id,nq} {Symbol,nq} {Side.ToString(),nq} {Quantity} @ {Price}")]
public record SharedUserTrade : SharedSymbolModel public record SharedUserTrade : SharedSymbolModel
{ {
/// <summary> /// <summary>
@@ -1,10 +1,12 @@
using System; using System;
using System.Diagnostics;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// A withdrawal record /// A withdrawal record
/// </summary> /// </summary>
[DebuggerDisplay("[{Timestamp}] {Quantity} {Asset,nq}")]
public record SharedWithdrawal public record SharedWithdrawal
{ {
/// <summary> /// <summary>
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Converters.SystemTextJson; using CryptoExchange.Net.Converters.SystemTextJson;
using System.Text;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
@@ -35,6 +36,33 @@ namespace CryptoExchange.Net.SharedApis
QuantityInQuoteAsset = quoteAssetQuantity; QuantityInQuoteAsset = quoteAssetQuantity;
QuantityInContracts = contractQuantity; QuantityInContracts = contractQuantity;
} }
/// <inheritdoc />
public override string ToString()
{
var sb = new StringBuilder("[");
if (QuantityInBaseAsset != null)
sb.Append($"{QuantityInBaseAsset} base");
if (QuantityInQuoteAsset != null)
{
if (sb.Length > 1)
sb.Append(", ");
sb.Append($"{QuantityInQuoteAsset} quote");
}
if (QuantityInContracts != null)
{
if (sb.Length > 1)
sb.Append(", ");
sb.Append($"{QuantityInContracts} contracts");
}
sb.Append("]");
return sb.ToString();
}
} }
/// <summary> /// <summary>
@@ -53,6 +81,9 @@ namespace CryptoExchange.Net.SharedApis
/// </summary> /// </summary>
public SharedQuantity() : base(null, null, null) { } public SharedQuantity() : base(null, null, null) { }
/// <inheritdoc />
public override string ToString() => base.ToString();
/// <summary> /// <summary>
/// Specify quantity in base asset /// Specify quantity in base asset
/// </summary> /// </summary>
@@ -111,6 +142,11 @@ namespace CryptoExchange.Net.SharedApis
[JsonConverter(typeof(SharedOrderQuantityConverter))] [JsonConverter(typeof(SharedOrderQuantityConverter))]
public record SharedOrderQuantity : SharedQuantityReference public record SharedOrderQuantity : SharedQuantityReference
{ {
/// <summary>
/// The average price based on the base and quote asset quantities
/// </summary>
public decimal? AveragePrice => QuantityInBaseAsset == 0 ? null : QuantityInQuoteAsset / QuantityInBaseAsset;
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
@@ -123,5 +159,24 @@ namespace CryptoExchange.Net.SharedApis
: base(baseAssetQuantity, quoteAssetQuantity, contractQuantity) : base(baseAssetQuantity, quoteAssetQuantity, contractQuantity)
{ {
} }
/// <summary>
/// Get the quantity in quote asset. Will use the set `QuantityInQuoteAsset` property if it has a value, or `QuantityInBaseAsset` * `price` if not. Null otherwise.
/// </summary>
/// <param name="price">The price to use for the QuantityInBaseAsset to quote asset quantity calculation</param>
/// <returns>Quantity in quote asset if it's available or can be calculated, null otherwise</returns>
public decimal? GetQuantityInQuoteAsset(decimal? price)
{
if (QuantityInQuoteAsset != null)
return QuantityInQuoteAsset;
if (QuantityInBaseAsset != null && price != null)
return QuantityInBaseAsset * price;
return null;
}
/// <inheritdoc />
public override string ToString() => base.ToString();
} }
} }
@@ -1,5 +1,6 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
{ {
@@ -176,5 +177,24 @@ namespace CryptoExchange.Net.SharedApis
return result.ToArray(); return result.ToArray();
} }
/// <summary>
/// Apply symbols request filter for asset type and trading mode
/// </summary>
public static T[] ApplySymbolFilter<T>(T[] symbols, GetSymbolsRequest request) where T : SharedSpotSymbol
{
IEnumerable<T> resultData = symbols;
if (request.TradingMode != null)
resultData = resultData.Where(x => x.TradingMode == request.TradingMode);
if (request.BaseAssetType != null)
resultData = resultData.Where(x => x.BaseAssetType == request.BaseAssetType);
if (request.QuoteAssetType != null)
resultData = resultData.Where(x => x.QuoteAssetType == request.QuoteAssetType);
if (request.BaseAssetSubType != null)
resultData = resultData.Where(x => x.BaseAssetSubType == request.BaseAssetSubType);
if (request.QuoteAssetSubType != null)
resultData = resultData.Where(x => x.QuoteAssetSubType == request.QuoteAssetSubType);
return resultData.ToArray();
}
} }
} }
@@ -607,7 +607,7 @@ namespace CryptoExchange.Net.Sockets.Default
SocketId, SocketId,
typeIdentifier, typeIdentifier,
topicFilter!, topicFilter!,
string.Join(",", _listeners.Select(x => string.Join(",", x.MessageRouter.Routes.Where(x => x.TypeIdentifier == typeIdentifier).Select(x => x.TopicFilter != null ? string.Join(",", x.TopicFilter) : "[null]"))))); string.Join(",", _listeners.SelectMany(x => x.MessageRouter.Routes.Where(x => x.TypeIdentifier == typeIdentifier).Select(x => x.TopicFilter ?? "[null]"))));
} }
} }
} }
@@ -143,10 +143,15 @@ namespace CryptoExchange.Net.Testing
foreach(var issue in issues) foreach(var issue in issues)
{ {
if (issue is MissingPropertyException) if (issue is MissingPropertyException)
{
if (!warnings?.Any(x => x.Message == issue.Message) == true)
warnings?.Add(issue); warnings?.Add(issue);
}
else else
{
errors.Add(issue); errors.Add(issue);
} }
}
if (errors.Count > 0) if (errors.Count > 0)
throw new AggregateException(errors); throw new AggregateException(errors);
@@ -284,8 +284,10 @@ namespace CryptoExchange.Net.Trackers.Klines
LastOpenTime = klines.Last().OpenTime, LastOpenTime = klines.Last().OpenTime,
HighPrice = klines.Select(d => d.LowPrice).Max(), HighPrice = klines.Select(d => d.LowPrice).Max(),
LowPrice = klines.Select(d => d.HighPrice).Min(), LowPrice = klines.Select(d => d.HighPrice).Min(),
#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)
#pragma warning restore
}; };
} }
@@ -138,6 +138,11 @@ namespace CryptoExchange.Net.Trackers.Trades
} }
} }
/// <summary>
/// The type of quantity the trades and stats are denoted in
/// </summary>
public TradeQuantityType QuantityType { get; }
/// <inheritdoc /> /// <inheritdoc />
public event Func<SharedTrade, Task>? OnAdded; public event Func<SharedTrade, Task>? OnAdded;
/// <inheritdoc /> /// <inheritdoc />
@@ -156,12 +161,14 @@ namespace CryptoExchange.Net.Trackers.Trades
SharedSymbol symbol, SharedSymbol symbol,
int? limit = null, int? limit = null,
TimeSpan? period = null, TimeSpan? period = null,
TradeQuantityType tradeQuantityType = TradeQuantityType.BaseAsset,
ExchangeParameters? exchangeParameters = null) ExchangeParameters? exchangeParameters = null)
{ {
_logger = logger ?? new NullLogger<TradeTracker>(); _logger = logger ?? new NullLogger<TradeTracker>();
_recentRestClient = recentRestClient; _recentRestClient = recentRestClient;
_historyRestClient = historyRestClient; _historyRestClient = historyRestClient;
_socketClient = socketClient; _socketClient = socketClient;
QuantityType = tradeQuantityType;
_exchangeParameters = exchangeParameters; _exchangeParameters = exchangeParameters;
Exchange = socketClient.Exchange; Exchange = socketClient.Exchange;
Symbol = symbol; Symbol = symbol;
@@ -170,22 +177,41 @@ namespace CryptoExchange.Net.Trackers.Trades
Period = period; Period = period;
} }
private static TradesStats GetStats(IEnumerable<SharedTrade> trades) private TradesStats GetStats(IEnumerable<SharedTrade> trades)
{ {
if (!trades.Any()) if (!trades.Any())
return new TradesStats(); return new TradesStats();
return new TradesStats
var stats = new TradesStats
{ {
TradeCount = trades.Count(), TradeCount = trades.Count(),
FirstTradeTime = trades.First().Timestamp, FirstTradeTime = trades.First().Timestamp,
LastTradeTime = trades.Last().Timestamp, LastTradeTime = trades.Last().Timestamp,
AveragePrice = Math.Round(trades.Select(d => d.Price).DefaultIfEmpty().Average(), 8), AveragePrice = Math.Round(trades.Select(d => d.Price).DefaultIfEmpty().Average(), 8),
VolumeWeightedAveragePrice = trades.Any() ? Math.Round(trades.Select(d => d.Price * d.Quantity).DefaultIfEmpty().Sum() / trades.Select(d => d.Quantity).DefaultIfEmpty().Sum(), 8) : null, QuoteVolume = Math.Round(trades.Sum(d => d.Quantities.GetQuantityInQuoteAsset(d.Price) ?? 0), 8),
Volume = Math.Round(trades.Sum(d => d.Quantity), 8),
QuoteVolume = Math.Round(trades.Sum(d => d.Quantity * d.Price), 8),
BuySellRatio = Math.Round(trades.Where(x => x.Side == SharedOrderSide.Buy).Sum(x => x.Quantity) / trades.Sum(x => x.Quantity), 8)
}; };
if (QuantityType == TradeQuantityType.BaseAsset)
{
stats.VolumeWeightedAveragePrice =
trades.Any()
? Math.Round(trades.Select(d => d.Quantities.GetQuantityInQuoteAsset(d.Price) ?? 0).DefaultIfEmpty().Sum() / trades.Select(d => d.Quantities.QuantityInBaseAsset!.Value).DefaultIfEmpty().Sum(), 8)
: null;
stats.Volume = Math.Round(trades.Sum(d => d.Quantities.QuantityInBaseAsset!.Value), 8);
stats.BuySellRatio = Math.Round(trades.Where(x => x.Side == SharedOrderSide.Buy).Sum(x => x.Quantities.QuantityInBaseAsset!.Value) / trades.Sum(x => x.Quantities.QuantityInBaseAsset!.Value), 8);
}
else
{
stats.VolumeWeightedAveragePrice =
trades.Any()
? Math.Round(trades.Select(d => d.Quantities.GetQuantityInQuoteAsset(d.Price) ?? 0).DefaultIfEmpty().Sum() / trades.Select(d => d.Quantities.QuantityInContracts!.Value).DefaultIfEmpty().Sum(), 8)
: null;
stats.Volume = Math.Round(trades.Sum(d => d.Quantities.QuantityInContracts!.Value), 8);
stats.BuySellRatio = Math.Round(trades.Where(x => x.Side == SharedOrderSide.Buy).Sum(x => x.Quantities.QuantityInContracts!.Value) / trades.Sum(x => x.Quantities.QuantityInContracts!.Value), 8);
}
return stats;
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -498,4 +524,19 @@ namespace CryptoExchange.Net.Trackers.Trades
Status = SyncStatus.Synced; Status = SyncStatus.Synced;
} }
} }
/// <summary>
/// The quantities to use for trade tracking
/// </summary>
public enum TradeQuantityType
{
/// <summary>
/// Base asset
/// </summary>
BaseAsset,
/// <summary>
/// Contracts
/// </summary>
Contracts
}
} }
+29 -27
View File
@@ -5,34 +5,36 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Binance.Net" Version="13.0.0" /> <PackageReference Include="Binance.Net" Version="13.3.0" />
<PackageReference Include="Bitfinex.Net" Version="11.0.0" /> <PackageReference Include="Bitfinex.Net" Version="11.3.0" />
<PackageReference Include="BitMart.Net" Version="4.0.0" /> <PackageReference Include="BitMart.Net" Version="4.3.0" />
<PackageReference Include="BloFin.Net" Version="3.0.0" /> <PackageReference Include="BloFin.Net" Version="3.3.0" />
<PackageReference Include="Bybit.Net" Version="7.0.0" /> <PackageReference Include="Bybit.Net" Version="7.3.0" />
<PackageReference Include="CoinEx.Net" Version="11.0.0" /> <PackageReference Include="CoinEx.Net" Version="11.3.0" />
<PackageReference Include="CoinW.Net" Version="3.0.0" /> <PackageReference Include="CoinW.Net" Version="3.3.0" />
<PackageReference Include="CryptoCom.Net" Version="4.0.0" /> <PackageReference Include="CryptoCom.Net" Version="4.3.0" />
<PackageReference Include="DeepCoin.Net" Version="4.0.0" /> <PackageReference Include="DeepCoin.Net" Version="4.3.0" />
<PackageReference Include="GateIo.Net" Version="4.0.0" /> <PackageReference Include="GateIo.Net" Version="4.4.0" />
<PackageReference Include="HyperLiquid.Net" Version="5.0.0" /> <PackageReference Include="HyperLiquid.Net" Version="5.4.0" />
<PackageReference Include="JK.BingX.Net" Version="4.0.0" /> <PackageReference Include="JK.BingX.Net" Version="4.3.0" />
<PackageReference Include="JK.Bitget.Net" Version="4.0.0" /> <PackageReference Include="JK.Bitget.Net" Version="4.3.2" />
<PackageReference Include="JK.Mexc.Net" Version="6.0.0" /> <PackageReference Include="JK.Mexc.Net" Version="6.4.0" />
<PackageReference Include="JK.OKX.Net" Version="5.0.1" /> <PackageReference Include="JK.OKX.Net" Version="5.3.1" />
<PackageReference Include="Jkorf.Aster.Net" Version="4.0.0" /> <PackageReference Include="Jkorf.Aster.Net" Version="4.3.0" />
<PackageReference Include="JKorf.BitMEX.Net" Version="4.0.0" /> <PackageReference Include="JKorf.BitMEX.Net" Version="4.3.0" />
<PackageReference Include="JKorf.Coinbase.Net" Version="4.0.0" /> <PackageReference Include="JKorf.Coinbase.Net" Version="4.4.0" />
<PackageReference Include="JKorf.HTX.Net" Version="9.0.0" /> <PackageReference Include="JKorf.HTX.Net" Version="9.3.0" />
<PackageReference Include="JKorf.Lighter.Net" Version="1.0.0" /> <PackageReference Include="JKorf.Lighter.Net" Version="1.4.0" />
<PackageReference Include="JKorf.Upbit.Net" Version="3.0.0" /> <PackageReference Include="JKorf.Upbit.Net" Version="3.3.0" />
<PackageReference Include="KrakenExchange.Net" Version="8.0.0" /> <PackageReference Include="KrakenExchange.Net" Version="8.3.0" />
<PackageReference Include="Kucoin.Net" Version="9.0.0" /> <PackageReference Include="Kucoin.Net" Version="9.3.0" />
<PackageReference Include="LBank.Net" Version="1.0.0" />
<PackageReference Include="Pionex.Net" Version="1.1.0" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Toobit.Net" Version="4.0.0" /> <PackageReference Include="Toobit.Net" Version="4.3.0" />
<PackageReference Include="Weex.Net" Version="2.0.0" /> <PackageReference Include="Weex.Net" Version="2.3.0" />
<PackageReference Include="WhiteBit.Net" Version="4.0.0" /> <PackageReference Include="WhiteBit.Net" Version="4.3.0" />
<PackageReference Include="XT.Net" Version="4.0.0" /> <PackageReference Include="XT.Net" Version="4.3.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+11 -1
View File
@@ -18,9 +18,11 @@
@inject IHyperLiquidRestClient hyperLiquidClient @inject IHyperLiquidRestClient hyperLiquidClient
@inject IKrakenRestClient krakenClient @inject IKrakenRestClient krakenClient
@inject IKucoinRestClient kucoinClient @inject IKucoinRestClient kucoinClient
@inject ILBankRestClient lbankClient
@inject ILighterRestClient lighterClient @inject ILighterRestClient lighterClient
@inject IMexcRestClient mexcClient @inject IMexcRestClient mexcClient
@inject IOKXRestClient okxClient @inject IOKXRestClient okxClient
@inject IPionexRestClient pionexClient
@inject IToobitRestClient toobitClient @inject IToobitRestClient toobitClient
@inject IUpbitRestClient upbitClient @inject IUpbitRestClient upbitClient
@inject IWeexRestClient weexClient @inject IWeexRestClient weexClient
@@ -57,16 +59,18 @@
var hyperLiquidTask = hyperLiquidClient.FuturesApi.ExchangeData.GetExchangeInfoAndTickersAsync(); var hyperLiquidTask = hyperLiquidClient.FuturesApi.ExchangeData.GetExchangeInfoAndTickersAsync();
var krakenTask = krakenClient.SpotApi.ExchangeData.GetTickerAsync("XBTUSD"); var krakenTask = krakenClient.SpotApi.ExchangeData.GetTickerAsync("XBTUSD");
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT"); var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
var lBankTask = lbankClient.SpotApi.ExchangeData.GetTickersAsync("btc_usdt");
var lighterTask = lighterClient.ExchangeApi.ExchangeData.GetSymbolDetailsAsync("BTC"); var lighterTask = lighterClient.ExchangeApi.ExchangeData.GetSymbolDetailsAsync("BTC");
var mexcTask = mexcClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT"); var mexcTask = mexcClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
var okxTask = okxClient.UnifiedApi.ExchangeData.GetTickerAsync("BTC-USDT"); var okxTask = okxClient.UnifiedApi.ExchangeData.GetTickerAsync("BTC-USDT");
var pionexTask = pionexClient.SpotApi.ExchangeData.GetTickersAsync("BTC_USDT");
var toobitTask = toobitClient.SpotApi.ExchangeData.GetTickersAsync("BTCUSDT"); var toobitTask = toobitClient.SpotApi.ExchangeData.GetTickersAsync("BTCUSDT");
var upbitTask = upbitClient.SpotApi.ExchangeData.GetTickerAsync("USDT-BTC"); var upbitTask = upbitClient.SpotApi.ExchangeData.GetTickerAsync("USDT-BTC");
var weexTask = weexClient.SpotApi.ExchangeData.GetTickersAsync(["BTCUSDT"]); var weexTask = weexClient.SpotApi.ExchangeData.GetTickersAsync(["BTCUSDT"]);
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, mexcTask, okxTask); await Task.WhenAll(asterTask, binanceTask, bingXTask, bitfinexTask, bitgetTask, bitmartTask, bloFinTask, bitmexTask, bybitTask, coinexTask, coinWTask, deepCoinTask, gateioTask, htxTask, krakenTask, kucoinTask, lBankTask, mexcTask, okxTask, pionexTask);
if (asterTask.Result.Success) if (asterTask.Result.Success)
_prices.Add("Aster", asterTask.Result.Data.LastPrice); _prices.Add("Aster", asterTask.Result.Data.LastPrice);
@@ -133,6 +137,9 @@
if (kucoinTask.Result.Success) if (kucoinTask.Result.Success)
_prices.Add("Kucoin", kucoinTask.Result.Data.LastPrice ?? 0); _prices.Add("Kucoin", kucoinTask.Result.Data.LastPrice ?? 0);
if (lBankTask.Result.Success)
_prices.Add("LBank", lBankTask.Result.Data.Single().Ticker.LastPrice);
if (lighterTask.Result.Success) if (lighterTask.Result.Success)
_prices.Add("Lighter", lighterTask.Result.Data.PerpSymbols[0].LastPrice); _prices.Add("Lighter", lighterTask.Result.Data.PerpSymbols[0].LastPrice);
@@ -142,6 +149,9 @@
if (okxTask.Result.Success) if (okxTask.Result.Success)
_prices.Add("OKX", okxTask.Result.Data.LastPrice ?? 0); _prices.Add("OKX", okxTask.Result.Data.LastPrice ?? 0);
if (pionexTask.Result.Success)
_prices.Add("Pionex", pionexTask.Result.Data.Single().ClosePrice);
if (toobitTask.Result.Success) if (toobitTask.Result.Success)
_prices.Add("Toobit", toobitTask.Result.Data.Single().LastPrice ?? 0); _prices.Add("Toobit", toobitTask.Result.Data.Single().LastPrice ?? 0);
+4 -1
View File
@@ -18,9 +18,11 @@
@inject IHyperLiquidSocketClient hyperLiquidSocketClient @inject IHyperLiquidSocketClient hyperLiquidSocketClient
@inject IKrakenSocketClient krakenSocketClient @inject IKrakenSocketClient krakenSocketClient
@inject IKucoinSocketClient kucoinSocketClient @inject IKucoinSocketClient kucoinSocketClient
@inject ILBankSocketClient lBankSocketClient
@inject ILighterSocketClient lighterSocketClient @inject ILighterSocketClient lighterSocketClient
@inject IMexcSocketClient mexcSocketClient @inject IMexcSocketClient mexcSocketClient
@inject IOKXSocketClient okxSocketClient @inject IOKXSocketClient okxSocketClient
@inject IPionexSocketClient pionexSocketClient
@inject IToobitSocketClient toobitSocketClient @inject IToobitSocketClient toobitSocketClient
@inject IUpbitSocketClient upbitSocketClient @inject IUpbitSocketClient upbitSocketClient
@inject IWeexSocketClient weexSocketClient @inject IWeexSocketClient weexSocketClient
@@ -65,11 +67,11 @@
deepCoinSocketClient.ExchangeApi.SubscribeToSymbolUpdatesAsync("ETH-BTC", data => UpdateData("DeepCoin", data.Data.LastPrice ?? 0)), deepCoinSocketClient.ExchangeApi.SubscribeToSymbolUpdatesAsync("ETH-BTC", data => UpdateData("DeepCoin", data.Data.LastPrice ?? 0)),
gateioSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("GateIo", data.Data.LastPrice)), gateioSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("GateIo", data.Data.LastPrice)),
htxSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ethbtc", data => UpdateData("HTX", data.Data.ClosePrice ?? 0)), htxSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ethbtc", data => UpdateData("HTX", data.Data.ClosePrice ?? 0)),
xtSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("eth_btc", data => UpdateData("XT", data.Data.LastPrice ?? 0)),
// HyperLiquid doesn't support the ETH/BTC pair // HyperLiquid doesn't support the ETH/BTC pair
//hyperLiquidSocketClient.SpotApi.SubscribeToSymbolUpdatesAsync("ETH", data => UpdateData("HyperLiquid", data.Data.MidPrice ?? 0)), //hyperLiquidSocketClient.SpotApi.SubscribeToSymbolUpdatesAsync("ETH", data => UpdateData("HyperLiquid", data.Data.MidPrice ?? 0)),
krakenSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH/BTC", data => UpdateData("Kraken", data.Data.LastPrice)), krakenSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH/BTC", data => UpdateData("Kraken", data.Data.LastPrice)),
kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)), kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
lBankSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("eth_btc", data => UpdateData("LBank", data.Data.LastPrice)),
// Mexc doesn't offer a ticker stream currently // Mexc doesn't offer a ticker stream currently
//mexcSocketClient.SpotApi.SubscribeToMiniTickerUpdatesAsync("ETHBTC", data => UpdateData("Mexc", data.Data.LastPrice)), //mexcSocketClient.SpotApi.SubscribeToMiniTickerUpdatesAsync("ETHBTC", data => UpdateData("Mexc", data.Data.LastPrice)),
okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.Data.LastPrice ?? 0)), okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.Data.LastPrice ?? 0)),
@@ -77,6 +79,7 @@
//toobitSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Toobit", data.Data.LastPrice ?? 0)), //toobitSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Toobit", data.Data.LastPrice ?? 0)),
upbitSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("BTC-ETH", data => UpdateData("Upbit", data.Data.LastPrice)), upbitSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("BTC-ETH", data => UpdateData("Upbit", data.Data.LastPrice)),
whitebitSocketClient.V4Api.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("WhiteBit", data.Data.Ticker.LastPrice)), whitebitSocketClient.V4Api.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("WhiteBit", data.Data.Ticker.LastPrice)),
xtSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("eth_btc", data => UpdateData("XT", data.Data.LastPrice ?? 0)),
}; };
await Task.WhenAll(tasks); await Task.WhenAll(tasks);
@@ -24,9 +24,11 @@
@using Kucoin.Net @using Kucoin.Net
@using Kucoin.Net.Clients @using Kucoin.Net.Clients
@using Kucoin.Net.Interfaces @using Kucoin.Net.Interfaces
@using LBank.Net.Interfaces
@using Lighter.Net.Interfaces @using Lighter.Net.Interfaces
@using Mexc.Net.Interfaces @using Mexc.Net.Interfaces
@using OKX.Net.Interfaces; @using OKX.Net.Interfaces;
@using Pionex.Net.Interfaces;
@using Upbit.Net.Interfaces; @using Upbit.Net.Interfaces;
@using Toobit.Net.Interfaces; @using Toobit.Net.Interfaces;
@using Weex.Net.Interfaces @using Weex.Net.Interfaces
@@ -51,9 +53,11 @@
@inject IHyperLiquidOrderBookFactory hyperLiquidFactory @inject IHyperLiquidOrderBookFactory hyperLiquidFactory
@inject IKrakenOrderBookFactory krakenFactory @inject IKrakenOrderBookFactory krakenFactory
@inject IKucoinOrderBookFactory kucoinFactory @inject IKucoinOrderBookFactory kucoinFactory
@inject ILBankOrderBookFactory lBankFactory
@inject ILighterOrderBookFactory lighterFactory @inject ILighterOrderBookFactory lighterFactory
@inject IMexcOrderBookFactory mexcFactory @inject IMexcOrderBookFactory mexcFactory
@inject IOKXOrderBookFactory okxFactory @inject IOKXOrderBookFactory okxFactory
@inject IPionexOrderBookFactory pionexFactory
@inject IToobitOrderBookFactory toobitFactory @inject IToobitOrderBookFactory toobitFactory
@inject IUpbitOrderBookFactory upbitFactory @inject IUpbitOrderBookFactory upbitFactory
@inject IWeexOrderBookFactory weexFactory @inject IWeexOrderBookFactory weexFactory
@@ -112,9 +116,11 @@
{ "HyperLiquid", hyperLiquidFactory.Create("UETH/USDC") }, { "HyperLiquid", hyperLiquidFactory.Create("UETH/USDC") },
{ "Kraken", krakenFactory.CreateSpot("ETH/BTC") }, { "Kraken", krakenFactory.CreateSpot("ETH/BTC") },
{ "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") }, { "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") },
{ "LBank", lBankFactory.CreateSpot("eth_usdt") },
{ "Lighter", lighterFactory.Create("ETH/USDC") }, { "Lighter", lighterFactory.Create("ETH/USDC") },
{ "Mexc", mexcFactory.CreateSpot("ETHBTC") }, { "Mexc", mexcFactory.CreateSpot("ETHBTC") },
{ "OKX", okxFactory.Create("ETH-BTC") }, { "OKX", okxFactory.Create("ETH-BTC") },
{ "Pionex", pionexFactory.CreateSpot("ETH_USDT") },
{ "Toobit", toobitFactory.CreateSpot("ETHUSDT") }, { "Toobit", toobitFactory.CreateSpot("ETHUSDT") },
{ "Upbit", upbitFactory.CreateSpot("BTC-ETH") }, { "Upbit", upbitFactory.CreateSpot("BTC-ETH") },
{ "Weex", weexFactory.CreateSpot("ETHUSDT") }, { "Weex", weexFactory.CreateSpot("ETHUSDT") },
@@ -24,9 +24,11 @@
@using Kraken.Net.Interfaces @using Kraken.Net.Interfaces
@using Kucoin.Net.Clients @using Kucoin.Net.Clients
@using Kucoin.Net.Interfaces @using Kucoin.Net.Interfaces
@using LBank.Net.Interfaces
@using Lighter.Net.Interfaces @using Lighter.Net.Interfaces
@using Mexc.Net.Interfaces @using Mexc.Net.Interfaces
@using OKX.Net.Interfaces; @using OKX.Net.Interfaces;
@using Pionex.Net.Interfaces;
@using Upbit.Net.Interfaces; @using Upbit.Net.Interfaces;
@using Toobit.Net.Interfaces; @using Toobit.Net.Interfaces;
@using Weex.Net.Interfaces @using Weex.Net.Interfaces
@@ -51,9 +53,11 @@
@inject IHyperLiquidTrackerFactory hyperLiquidFactory @inject IHyperLiquidTrackerFactory hyperLiquidFactory
@inject IKrakenTrackerFactory krakenFactory @inject IKrakenTrackerFactory krakenFactory
@inject IKucoinTrackerFactory kucoinFactory @inject IKucoinTrackerFactory kucoinFactory
@inject ILBankTrackerFactory lBankFactory
@inject ILighterTrackerFactory lighterFactory @inject ILighterTrackerFactory lighterFactory
@inject IMexcTrackerFactory mexcFactory @inject IMexcTrackerFactory mexcFactory
@inject IOKXTrackerFactory okxFactory @inject IOKXTrackerFactory okxFactory
@inject IPionexTrackerFactory pionexFactory
@inject IToobitTrackerFactory toobitFactory @inject IToobitTrackerFactory toobitFactory
@inject IUpbitTrackerFactory upbitFactory @inject IUpbitTrackerFactory upbitFactory
@inject IWeexTrackerFactory weexFactory @inject IWeexTrackerFactory weexFactory
@@ -105,9 +109,11 @@
{ hyperLiquidFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { hyperLiquidFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ krakenFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { krakenFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ kucoinFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { kucoinFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ lBankFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ lighterFactory.CreateTradeTracker(futuresSymbol, period: TimeSpan.FromMinutes(5)) }, { lighterFactory.CreateTradeTracker(futuresSymbol, period: TimeSpan.FromMinutes(5)) },
{ mexcFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { mexcFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ okxFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { okxFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ pionexFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ toobitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { toobitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ upbitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { upbitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ weexFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { weexFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
+2
View File
@@ -52,9 +52,11 @@ namespace BlazorClient
services.AddHTX(); services.AddHTX();
services.AddKraken(); services.AddKraken();
services.AddKucoin(); services.AddKucoin();
services.AddLBank();
services.AddLighter(); services.AddLighter();
services.AddMexc(); services.AddMexc();
services.AddOKX(); services.AddOKX();
services.AddPionex();
services.AddToobit(); services.AddToobit();
services.AddUpbit(); services.AddUpbit();
services.AddWeex(); services.AddWeex();
+2
View File
@@ -27,9 +27,11 @@
@using HyperLiquid.Net.Interfaces.Clients; @using HyperLiquid.Net.Interfaces.Clients;
@using Kraken.Net.Interfaces.Clients; @using Kraken.Net.Interfaces.Clients;
@using Kucoin.Net.Interfaces.Clients; @using Kucoin.Net.Interfaces.Clients;
@using LBank.Net.Interfaces.Clients
@using Lighter.Net.Interfaces.Clients @using Lighter.Net.Interfaces.Clients
@using Mexc.Net.Interfaces.Clients; @using Mexc.Net.Interfaces.Clients;
@using OKX.Net.Interfaces.Clients; @using OKX.Net.Interfaces.Clients;
@using Pionex.Net.Interfaces.Clients;
@using Upbit.Net.Interfaces.Clients; @using Upbit.Net.Interfaces.Clients;
@using Toobit.Net.Interfaces.Clients; @using Toobit.Net.Interfaces.Clients;
@using Weex.Net.Interfaces.Clients @using Weex.Net.Interfaces.Clients
+28 -1189
View File
File diff suppressed because it is too large Load Diff
+6 -2
View File
@@ -2,10 +2,14 @@
> 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.x. 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.4.0. 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.
Version 12.2.0 adds typed asset metadata to shared symbol discovery. `SharedSpotSymbol` and `SharedFuturesSymbol` expose `DisplayName` plus base/quote `SharedAssetType` and `SharedAssetSubType` values. `GetSymbolsRequest` can filter on those four type fields. After symbol discovery, `ISpotSymbolRestClient.SpotSymbolCatalog` and `IFuturesSymbolRestClient.FuturesSymbolCatalog` provide asset and symbol dictionaries; each catalog is available only after the corresponding `Get*SymbolsAsync` call. `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` are best-effort helpers for exchange-library implementations.
Version 12.4.0 represents market-data quantities with `SharedOrderQuantity`: use `Volumes` on `SharedSpotTicker`, `SharedFuturesTicker`, and `SharedKline`, and `Quantities` on `SharedTrade`. The old scalar `Volume`, `QuoteVolume`, and `Quantity` members are obsolete. Exchange-library implementations must pass `SharedOrderQuantity` to these model constructors.
## Documentation ## Documentation
- [README](https://github.com/JKorf/CryptoExchange.Net/blob/master/README.md): Overview, full ecosystem table (28+ exchange libraries), installation per exchange, complete release notes - [README](https://github.com/JKorf/CryptoExchange.Net/blob/master/README.md): Overview, full ecosystem table (28+ exchange libraries), installation per exchange, complete release notes
@@ -22,7 +26,7 @@ The standout feature for cross-exchange code is `CryptoExchange.Net.SharedApis`
## Reference ## Reference
- [Ecosystem libraries list](https://github.com/JKorf/CryptoExchange.Net#cryptoexchangenet-ecosystem): Aster, Binance, BingX, Bitfinex, Bitget, BitMart, BitMEX, Bitstamp, BloFin, Bybit, Coinbase, CoinEx, CoinW, CoinGecko, Crypto.com, DeepCoin, Gate.io, HTX, HyperLiquid, Kraken, Kucoin, Mexc, OKX, Polymarket, Toobit, Upbit, Weex, WhiteBit, XT - [Ecosystem libraries list](https://github.com/JKorf/CryptoExchange.Net#cryptoexchangenet-ecosystem): Aster, Binance, BingX, Bitfinex, Bitget, BitMart, BitMEX, Bitstamp, BloFin, Bybit, Coinbase, CoinEx, CoinW, CoinGecko, Crypto.com, DeepCoin, Gate.io, HTX, HyperLiquid, Kraken, Kucoin, Mexc, OKX, Pionex, Polymarket, Toobit, Upbit, Weex, WhiteBit, XT
- [CryptoClients.Net](https://github.com/JKorf/CryptoClients.Net): Single bundle package for all exchange libraries - [CryptoClients.Net](https://github.com/JKorf/CryptoClients.Net): Single bundle package for all exchange libraries
- [CryptoManager.Net](https://github.com/JKorf/CryptoManager.Net): Full demo application using CryptoClients.Net - [CryptoManager.Net](https://github.com/JKorf/CryptoManager.Net): Full demo application using CryptoClients.Net
- [NuGet Package](https://www.nuget.org/packages/CryptoExchange.Net): Latest stable release on NuGet - [NuGet Package](https://www.nuget.org/packages/CryptoExchange.Net): Latest stable release on NuGet