mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 17:03:10 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 73377fbb87 | |||
| 3a00d6371a | |||
| caf6d36bcd | |||
| e078a373da | |||
| 007743f5a1 | |||
| d06f891cee | |||
| 8dcbb687f5 | |||
| fcb36f7ee0 | |||
| 3cffd67518 | |||
| ecd00ea707 | |||
| 38a7b981ce | |||
| c41cc3c4c7 | |||
| 4accc8039b | |||
| 87c86ec0c0 | |||
| d9850da282 | |||
| c4a8b02054 |
@@ -30,6 +30,14 @@ 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -24,6 +24,12 @@ 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -67,6 +67,24 @@ var btcusdtPerp = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
|
||||
|
||||
For exchanges that use exotic asset names, see the AssetAliases configuration.
|
||||
|
||||
## Symbol Metadata and Asset Classification
|
||||
|
||||
Since CryptoExchange.Net 12.2.0, shared symbol responses describe both sides of a market with `BaseAssetType`, `BaseAssetSubType`, `QuoteAssetType`, and `QuoteAssetSubType`. `SharedAssetType` distinguishes `Crypto`, `Fiat`, and `TradFi`; `SharedAssetSubType` distinguishes `StableCoin`, `Equity`, and `Commodity`. `SharedSpotSymbol` and `SharedFuturesSymbol` also expose `DisplayName`.
|
||||
|
||||
The same fields on `GetSymbolsRequest` filter spot or futures symbol discovery:
|
||||
|
||||
```csharp
|
||||
var request = new GetSymbolsRequest(
|
||||
baseAssetType: SharedAssetType.Crypto,
|
||||
quoteAssetSubType: SharedAssetSubType.StableCoin);
|
||||
|
||||
var result = await symbolClient.GetSpotSymbolsAsync(request);
|
||||
```
|
||||
|
||||
After calling `GetSpotSymbolsAsync` or `GetFuturesSymbolsAsync`, use the client's `SpotSymbolCatalog` or `FuturesSymbolCatalog` to look up normalized asset and symbol metadata by name. The catalog is unavailable until the corresponding symbol request has populated the cache.
|
||||
|
||||
For exchange-library implementations, `LibraryHelpers.IsStableCoin`, `IsCommodity`, and `IsEquity` provide best-effort classification of known assets and accept exchange-specific additions. These helpers are heuristics, not an exhaustive source of truth.
|
||||
|
||||
## Available Shared Interfaces
|
||||
|
||||
**REST:**
|
||||
|
||||
@@ -138,6 +138,68 @@ namespace CryptoExchange.Net.UnitTests.ClientTests
|
||||
Assert.That(socket2.Connected == false);
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public async Task BatchedSubscription_Should_NotExceedIndividualCombineTarget()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(options =>
|
||||
{
|
||||
options.SocketSubscriptionsCombineTarget = 10;
|
||||
options.SocketIndividualSubscriptionCombineTarget = 10;
|
||||
});
|
||||
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
|
||||
|
||||
// act
|
||||
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default, individualSubscriptionCount: 6);
|
||||
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
|
||||
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default, individualSubscriptionCount: 6);
|
||||
|
||||
// assert
|
||||
Assert.That(client.ApiClient1._socketConnections.Count == 2);
|
||||
Assert.That(client.ApiClient1._socketConnections.Values.All(connection => connection.Subscriptions.Sum(subscription => subscription.IndividualSubscriptionCount) <= 10));
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public async Task BatchedSubscription_FullIndividualConnection_Should_NotPreventEligibleConnectionReuse()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(options =>
|
||||
{
|
||||
options.SocketSubscriptionsCombineTarget = 5;
|
||||
options.SocketIndividualSubscriptionCombineTarget = 10;
|
||||
});
|
||||
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
|
||||
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
|
||||
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
|
||||
|
||||
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
|
||||
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default, individualSubscriptionCount: 10);
|
||||
|
||||
TestHelpers.ConfigureSocketClient(client, "wss://localhost");
|
||||
|
||||
// act
|
||||
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
|
||||
|
||||
// assert
|
||||
Assert.That(
|
||||
client.ApiClient1._socketConnections.Count,
|
||||
Is.EqualTo(2),
|
||||
"The eligible connection should be reused instead of opening a new connection after selecting a full individual-subscription connection");
|
||||
|
||||
var fullConnection = client.ApiClient1._socketConnections.Values
|
||||
.Single(connection => connection.Subscriptions.Sum(subscription => subscription.IndividualSubscriptionCount) == 10);
|
||||
Assert.That(
|
||||
fullConnection.UserSubscriptionCount,
|
||||
Is.EqualTo(1),
|
||||
"The full connection should not receive the normal subscription");
|
||||
|
||||
var eligibleConnection = client.ApiClient1._socketConnections.Values.Single(connection => connection != fullConnection);
|
||||
Assert.That(
|
||||
eligibleConnection.UserSubscriptionCount,
|
||||
Is.EqualTo(3),
|
||||
"The existing eligible connection should receive the normal subscription");
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public async Task ErrorResponse_ShouldNot_ConfirmSubscription()
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace CryptoExchange.Net.UnitTests.ConverterTests
|
||||
[TestCase("1620777600000")]
|
||||
[TestCase("2021-05-12T00:00:00.000Z")]
|
||||
[TestCase("2021-05-12T00:00:00.000000000Z")]
|
||||
[TestCase("2021-05-12 00:00:00.000000+00:00:00")]
|
||||
[TestCase("0.000000", true)]
|
||||
[TestCase("0", true)]
|
||||
[TestCase("", true)]
|
||||
|
||||
@@ -36,9 +36,13 @@ namespace CryptoExchange.Net.UnitTests.Implementations
|
||||
protected override TestAuthenticationProvider CreateAuthenticationProvider(TestCredentials credentials) =>
|
||||
new TestAuthenticationProvider(credentials);
|
||||
|
||||
public async Task<WebSocketResult<UpdateSubscription>> SubscribeToUpdatesAsync<T>(Action<DataEvent<T>> handler, bool subQuery, CancellationToken ct)
|
||||
public async Task<WebSocketResult<UpdateSubscription>> SubscribeToUpdatesAsync<T>(Action<DataEvent<T>> handler, bool subQuery, CancellationToken ct, int individualSubscriptionCount = 1)
|
||||
{
|
||||
return await base.SubscribeAsync(new TestSubscription<T>(_logger, handler, subQuery, false), ct);
|
||||
var subscription = new TestSubscription<T>(_logger, handler, subQuery, false)
|
||||
{
|
||||
IndividualSubscriptionCount = individualSubscriptionCount
|
||||
};
|
||||
return await base.SubscribeAsync(subscription, ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,6 +625,21 @@ namespace CryptoExchange.Net.Clients
|
||||
return Task.FromResult(CallResult.Ok());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the connection can be used for a new subscription or query with the provided parameters
|
||||
/// </summary>
|
||||
/// <param name="connection">The connection to check</param>
|
||||
/// <param name="address">The address set by the request</param>
|
||||
/// <param name="authenticated">Whether the request needs an authenticated connection</param>
|
||||
/// <param name="topic">Topic of the request</param>
|
||||
/// <returns>True if connection can be used</returns>
|
||||
protected virtual bool ConnectionCanBeUsedFor(SocketConnection connection, string address, bool authenticated, string? topic = null)
|
||||
{
|
||||
return connection.ConnectionUriString.Equals(address.TrimEnd('/'), StringComparison.Ordinal)
|
||||
&& connection.ApiClient.ClientName.Equals(ClientName, StringComparison.Ordinal)
|
||||
&& (AllowTopicsOnTheSameConnection || !connection.Topics.Contains(topic));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
|
||||
/// </summary>
|
||||
@@ -643,10 +658,7 @@ namespace CryptoExchange.Net.Clients
|
||||
string? topic = null,
|
||||
int individualSubscriptionCount = 1)
|
||||
{
|
||||
var socketQuery = _socketConnections.Where(s => s.Value.ConnectionUriString.Equals(address.TrimEnd('/'), StringComparison.Ordinal)
|
||||
&& s.Value.ApiClient.ClientName.Equals(ClientName, StringComparison.Ordinal)
|
||||
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic)))
|
||||
.Select(x => x.Value); // Don't ToList this so the query is executed again when called
|
||||
var socketQuery = _socketConnections.Where(s => ConnectionCanBeUsedFor(s.Value, address, authenticated, topic)).Select(x => x.Value); // Don't ToList this so the query is executed again when called
|
||||
|
||||
// If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection
|
||||
var delayStart = DateTime.UtcNow;
|
||||
@@ -679,45 +691,26 @@ namespace CryptoExchange.Net.Clients
|
||||
&& (s.Authenticated == authenticated || !authenticated)
|
||||
&& s.Connected).ToList();
|
||||
|
||||
SocketConnection? connection;
|
||||
if (!dedicatedRequestConnection)
|
||||
{
|
||||
connection = socketQuery
|
||||
.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection)
|
||||
.OrderBy(s => s.UserSubscriptionCount)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
else
|
||||
bool maxConnectionsReached = _socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections);
|
||||
SocketConnection? connection = null;
|
||||
if (dedicatedRequestConnection)
|
||||
{
|
||||
connection = socketQuery.Where(s => s.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault();
|
||||
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
|
||||
// Mark dedicated request connection as authenticated if the request is authenticated
|
||||
connection.DedicatedRequestConnection.Authenticated = authenticated;
|
||||
|
||||
if (connection == null)
|
||||
// Fall back to an existing connection if there is no dedicated request connection available
|
||||
connection = socketQuery.OrderBy(s => s.UserSubscriptionCount).FirstOrDefault();
|
||||
}
|
||||
|
||||
bool maxConnectionsReached = _socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections);
|
||||
if (connection == null)
|
||||
// Use an eligible non-dedicated connection for subscriptions, or as fallback when no dedicated request connection is available
|
||||
connection = socketQuery
|
||||
.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection)
|
||||
.Where(s => IsConnectionEligible(s, individualSubscriptionCount, maxConnectionsReached))
|
||||
.OrderBy(s => s.UserSubscriptionCount)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (connection != null)
|
||||
{
|
||||
bool lessThanBatchSubCombineTarget = connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget;
|
||||
bool lessThanIndividualSubCombineTarget = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount) < ClientOptions.SocketIndividualSubscriptionCombineTarget;
|
||||
|
||||
if ((lessThanBatchSubCombineTarget && lessThanIndividualSubCombineTarget)
|
||||
|| maxConnectionsReached)
|
||||
{
|
||||
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
||||
// If there is a max subscriptions per connection limit also only use existing if the new subscription doesn't go over the limit
|
||||
if (MaxIndividualSubscriptionsPerConnection == null)
|
||||
return CallResult.Ok(connection);
|
||||
|
||||
var currentCount = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount);
|
||||
if (currentCount + individualSubscriptionCount <= MaxIndividualSubscriptionsPerConnection)
|
||||
return CallResult.Ok(connection);
|
||||
}
|
||||
}
|
||||
return CallResult.Ok(connection);
|
||||
|
||||
if (maxConnectionsReached)
|
||||
return CallResult.Fail<SocketConnection>(new InvalidOperationError("Max amount of socket connections reached"));
|
||||
@@ -784,6 +777,21 @@ namespace CryptoExchange.Net.Clients
|
||||
return CallResult.Ok(socketConnection);
|
||||
}
|
||||
|
||||
private bool IsConnectionEligible(SocketConnection socketConnection, int individualSubscriptionCount, bool maxConnectionsReached)
|
||||
{
|
||||
var currentIndividualSubscriptionCount = socketConnection.Subscriptions.Sum(x => x.IndividualSubscriptionCount);
|
||||
bool lessThanBatchSubCombineTarget = socketConnection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget;
|
||||
// Include the incoming batch so batched subscriptions cannot overshoot the configured socket target.
|
||||
bool lessThanIndividualSubCombineTarget = currentIndividualSubscriptionCount + individualSubscriptionCount <= ClientOptions.SocketIndividualSubscriptionCombineTarget;
|
||||
|
||||
if ((!lessThanBatchSubCombineTarget || !lessThanIndividualSubCombineTarget)
|
||||
&& !maxConnectionsReached)
|
||||
return false;
|
||||
|
||||
return MaxIndividualSubscriptionsPerConnection == null
|
||||
|| currentIndividualSubscriptionCount + individualSubscriptionCount <= MaxIndividualSubscriptionsPerConnection;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process an unhandled message
|
||||
/// </summary>
|
||||
|
||||
@@ -198,6 +198,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
if (stringValue.EndsWith("+00:00:00"))
|
||||
return DateTime.Parse(stringValue.Substring(0, stringValue.Length - 9), CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
||||
|
||||
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||
<PackageVersion>12.0.2</PackageVersion>
|
||||
<AssemblyVersion>12.0.2</AssemblyVersion>
|
||||
<FileVersion>12.0.2</FileVersion>
|
||||
<PackageVersion>12.3.0</PackageVersion>
|
||||
<AssemblyVersion>12.3.0</AssemblyVersion>
|
||||
<FileVersion>12.3.0</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace CryptoExchange.Net
|
||||
if (keyedCache != null && DateTime.UtcNow - keyedCache.UpdateTime < TimeSpan.FromMinutes(60))
|
||||
return;
|
||||
|
||||
exchangeInfo.Set(key, new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol)));
|
||||
exchangeInfo.Set(key, new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -118,6 +118,22 @@ namespace CryptoExchange.Net
|
||||
return exchangeInfo.ParseSymbol(key, symbolName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a symbol catalog for a specific exchange(topic) and environment. Only available if <see cref="UpdateSymbolInfo(string, string, string?, SharedSpotSymbol[])"/> has been called previously.
|
||||
/// </summary>
|
||||
/// <param name="exchange">Exchange name</param>
|
||||
/// <param name="topicId">Id for the provided data</param>
|
||||
/// <param name="environmentName">Trade environment</param>
|
||||
/// <param name="key">Additional data set identification key</param>
|
||||
public static SharedSymbolCatalog? GetSymbolCatalog(string exchange, string topicId, string environmentName, string? key)
|
||||
{
|
||||
var id = topicId + environmentName;
|
||||
if (!_symbolInfos.TryGetValue(id, out var exchangeInfo))
|
||||
return null;
|
||||
|
||||
return exchangeInfo.GetSymbolCatalog(exchange, key);
|
||||
}
|
||||
|
||||
class ExchangeKeyedCache
|
||||
{
|
||||
private ExchangeInfo? _noKeyCache;
|
||||
@@ -163,7 +179,7 @@ namespace CryptoExchange.Net
|
||||
|
||||
public SharedSymbol? ParseSymbol(string? key, string symbolName)
|
||||
{
|
||||
SharedSymbol? symbolInfo = null;
|
||||
SharedSpotSymbol? symbolInfo = null;
|
||||
if (key == null)
|
||||
{
|
||||
if (_noKeyCache != null)
|
||||
@@ -173,7 +189,7 @@ namespace CryptoExchange.Net
|
||||
|
||||
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
|
||||
{
|
||||
DeliverTime = symbolInfo.DeliverTime
|
||||
DeliverTime = (symbolInfo as SharedFuturesSymbol)?.DeliveryTime
|
||||
};
|
||||
}
|
||||
|
||||
@@ -183,7 +199,7 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
|
||||
{
|
||||
DeliverTime = symbolInfo.DeliverTime
|
||||
DeliverTime = (symbolInfo as SharedFuturesSymbol)?.DeliveryTime
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -199,7 +215,7 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
|
||||
{
|
||||
DeliverTime = symbolInfo.DeliverTime
|
||||
DeliverTime = (symbolInfo as SharedFuturesSymbol)?.DeliveryTime
|
||||
};
|
||||
}
|
||||
|
||||
@@ -265,7 +281,7 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
return _noKeyCache.Symbols
|
||||
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
|
||||
.Select(x => x.Value)
|
||||
.Select(x => x.Value.SharedSymbol)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
@@ -274,7 +290,7 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
result.AddRange(cache.Symbols
|
||||
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
|
||||
.Select(x => x.Value));
|
||||
.Select(x => x.Value.SharedSymbol));
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
@@ -286,18 +302,63 @@ namespace CryptoExchange.Net
|
||||
|
||||
return exchangeInfo.Symbols
|
||||
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
|
||||
.Select(x => x.Value)
|
||||
.Select(x => x.Value.SharedSymbol)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
internal SharedSymbolCatalog? GetSymbolCatalog(string exchange, string? key)
|
||||
{
|
||||
IEnumerable<SharedSpotSymbol> cachedSymbols;
|
||||
if (key == null)
|
||||
{
|
||||
if (_noKeyCache != null)
|
||||
cachedSymbols = _noKeyCache.Symbols.Values;
|
||||
else
|
||||
cachedSymbols = _keyedCache.Values.SelectMany(x => x.Symbols.Values);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_keyedCache.TryGetValue(key, out var exchangeInfo) || exchangeInfo == null)
|
||||
return null;
|
||||
|
||||
cachedSymbols = exchangeInfo.Symbols.Values;
|
||||
}
|
||||
|
||||
var assets = new Dictionary<string, SharedAssetInfo>();
|
||||
var symbols = new Dictionary<string, SharedSpotSymbol>();
|
||||
foreach (var symbol in cachedSymbols)
|
||||
{
|
||||
if (!assets.TryGetValue(symbol.BaseAsset, out var baseAssetInfo))
|
||||
{
|
||||
baseAssetInfo = new SharedAssetInfo(symbol.BaseAsset, symbol.BaseAssetType, symbol.BaseAssetSubType);
|
||||
assets.Add(symbol.BaseAsset, baseAssetInfo);
|
||||
}
|
||||
|
||||
if (!assets.TryGetValue(symbol.QuoteAsset, out var quoteAssetInfo))
|
||||
{
|
||||
quoteAssetInfo = new SharedAssetInfo(symbol.QuoteAsset, symbol.QuoteAssetType, symbol.QuoteAssetSubType);
|
||||
assets.Add(symbol.QuoteAsset, quoteAssetInfo);
|
||||
}
|
||||
|
||||
symbols.Add(symbol.Name, symbol);
|
||||
}
|
||||
|
||||
return new SharedSymbolCatalog
|
||||
{
|
||||
Exchange = exchange,
|
||||
Assets = assets,
|
||||
Symbols = symbols
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ExchangeInfo
|
||||
{
|
||||
public DateTime UpdateTime { get; set; }
|
||||
public Dictionary<string, SharedSymbol> Symbols { get; set; }
|
||||
public Dictionary<string, SharedSpotSymbol> Symbols { get; set; }
|
||||
|
||||
public ExchangeInfo(DateTime updateTime, Dictionary<string, SharedSymbol> symbols)
|
||||
public ExchangeInfo(DateTime updateTime, Dictionary<string, SharedSpotSymbol> symbols)
|
||||
{
|
||||
UpdateTime = updateTime;
|
||||
Symbols = symbols;
|
||||
|
||||
@@ -24,8 +24,9 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <param name="interval">Kline interval</param>
|
||||
/// <param name="limit">The max amount of klines to retain</param>
|
||||
/// <param name="period">The max period the data should be retained</param>
|
||||
/// <param name="exchangeParameters">Exchange parameters</param>
|
||||
/// <returns></returns>
|
||||
IKlineTracker CreateKlineTracker(SharedSymbol symbol, SharedKlineInterval interval, int? limit = null, TimeSpan? period = null);
|
||||
IKlineTracker CreateKlineTracker(SharedSymbol symbol, SharedKlineInterval interval, int? limit = null, TimeSpan? period = null, ExchangeParameters? exchangeParameters = null);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the factory supports creating a TradeTracker instance for this symbol
|
||||
@@ -39,7 +40,8 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <param name="symbol">The symbol</param>
|
||||
/// <param name="limit">The max amount of trades to retain</param>
|
||||
/// <param name="period">The max period the data should be retained</param>
|
||||
/// <param name="exchangeParameters">Exchange parameters</param>
|
||||
/// <returns></returns>
|
||||
ITradeTracker CreateTradeTracker(SharedSymbol symbol, int? limit = null, TimeSpan? period = null);
|
||||
ITradeTracker CreateTradeTracker(SharedSymbol symbol, int? limit = null, TimeSpan? period = null, ExchangeParameters? exchangeParameters = null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Pipelines;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
|
||||
@@ -14,6 +16,61 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
public static class LibraryHelpers
|
||||
{
|
||||
private static readonly HashSet<string> _stableCoins = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// USD
|
||||
"USDT", "USDC", "DAI", "FDUSD", "USDE", "TUSD", "USDP", "PYUSD", "GUSD",
|
||||
"USDD", "LUSD", "USDJ", "SUSD", "ZUSD", "BUSD", "USTC", "USDX", "USDK",
|
||||
"CUSD", "USD1", "USD0", "XUSD", "BFUSD", "USDS", "RLUSD", "OUSD", "USDH",
|
||||
"APXUSD", "USDQ", "USDPT", "FIDD", "AUSD",
|
||||
// EUR
|
||||
"EURS", "EURC", "EURI", "EURT", "AGEUR", "CEUR", "AEUR", "EURQ", "EUROP",
|
||||
// Other
|
||||
"CNYT", // CNY
|
||||
"CREAL", "BRL1", // BRL
|
||||
"XSGD", // SGD
|
||||
"GYEN", // JPY
|
||||
"KGST", // KGS
|
||||
"QCAD", // CAD
|
||||
"TGBP", // GBP
|
||||
"AUDX", // AUD
|
||||
"MXNB", // MXN
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> _commodities = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// Metals
|
||||
"XAU", "XAUT", "XAG", "XPT", "XPD", "COPPER", "PAXG", "XNI", "XCU", "XAL", "GOLD", "SILVER",
|
||||
// Energy
|
||||
"BZ", "NATGAS", "NGAS", "CL", "XTI", "UKOIL", "USOIL", "BRENTOIL"
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> _stocks = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// Top stocks, will need to update periodically
|
||||
"AAAU", "AADR", "AAPL", "ACWI", "ACWX", "AGG", "AMD", "AMLP", "AMZN", "ARKF",
|
||||
"ARKG", "ARKK", "ARKQ", "ARKW", "AVGO", "BA", "BABA", "BND", "BNDX", "BOTZ",
|
||||
"CIBR", "COIN", "DIA", "DIVB", "DVY", "EEM", "EFA", "EFAV", "ESGU", "EWG",
|
||||
"EWJ", "EWT", "EWU", "EWW", "EWY", "EWZ", "FDN", "FEZ", "GLDM", "GOOGL",
|
||||
"HDV", "HOOD", "HYG", "IAU", "IBB", "ICLN", "IEFA", "IEMG", "IGSB", "IJH",
|
||||
"IJR", "INTC", "ITOT", "IUSB", "IUSG", "IUSV", "IWM", "IWO", "IWR", "IYR",
|
||||
"JETS", "JPM", "LIT", "MCHI", "META", "MGK", "MSTR", "MTUM", "MU", "NET",
|
||||
"NFLX", "NOBL", "NVDA", "OIH", "ORCL", "PAVE", "PBW", "PLTR", "QQQ", "QQQM",
|
||||
"SCHB", "SCHD", "SCHF", "SCHG", "SCHH", "SCHV", "SCHX", "SKHY", "SPCX", "SPCXD",
|
||||
"SPLG", "SPY", "SPYG", "SPYV", "SQQQ", "TSLA", "TSM", "TQQQ", "USMV", "VBR",
|
||||
"VCIT", "VCSH", "VEA", "VEU", "VGIT", "VGK", "VGT", "VHT", "VIG", "VNQ",
|
||||
"VOO", "VOT", "VTI", "VTV", "VUG", "VXUS", "XBI", "XLC", "XLE", "XLF",
|
||||
"XLI", "XLK", "XLP", "XLU", "XLV", "XLY", "CSCO", "UBER", "MRVL", "RKLB",
|
||||
"COHR", "SOXL", "HD", "DIS", "CBRS", "V", "BRKB", "FLNC", "LLY", "COST",
|
||||
"ARM", "BMNR", "NBIS", "ASML", "AAOI", "GLW", "SHLD", "BE", "QNTX", "IBM",
|
||||
"AMAT", "NOK", "ASTS", "BBX", "SLX", "SKHYNIX", "SAMSUNG", "HYUNDAI", "NVO",
|
||||
"IREN", "ONDS", "CRM" , "VRT", "ZEST", "BTW", "HPE", "AXTI", "BX", "CRWD",
|
||||
"CRDO", "NOW", "ZM", "DKNG", "RIVN", "URNM", "EBAY", "ADBE", "UVXY", "RDW",
|
||||
"CIEN","PANW", "WIN", "PAYP", "HIMS", "CRWV", "QCOM", "LITE", "DRAM", "ANTHROPIC",
|
||||
"OPENAI", "USAR", "BILL", "SNDK", "NASDAQ100", "SPX500", "BSB", "CRCL", "STRC",
|
||||
"MSFT", "WDC"
|
||||
};
|
||||
|
||||
private static ILogger? _staticLogger;
|
||||
/// <summary>
|
||||
/// Static logger
|
||||
@@ -105,6 +162,67 @@ namespace CryptoExchange.Net
|
||||
return _defaultClientReferences.TryGetValue(key, out var id) ? id : throw new KeyNotFoundException($"{exchange} not found in configuration");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether an asset is a known stablecoin. Note that this is not definitive, only large known stocks are checked
|
||||
/// </summary>
|
||||
/// <param name="asset">Asset name</param>
|
||||
/// <param name="additionalStableCoins">Additional stablecoin names for the specific exchange</param>
|
||||
public static bool IsStableCoin(string asset, params HashSet<string> additionalStableCoins)
|
||||
{
|
||||
if (string.IsNullOrEmpty(asset))
|
||||
return false;
|
||||
|
||||
return _stableCoins.Contains(asset) || (additionalStableCoins != null && additionalStableCoins.Contains(asset, StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether an asset is a known commodity. Note that this is not definitive, only large known stocks are checked
|
||||
/// </summary>
|
||||
/// <param name="asset">Asset name</param>
|
||||
/// <param name="additionalCommodities">Additional commodity names for the specific exchange</param>
|
||||
public static bool IsCommodity(string asset, params HashSet<string> additionalCommodities)
|
||||
{
|
||||
if (string.IsNullOrEmpty(asset))
|
||||
return false;
|
||||
|
||||
return _commodities.Contains(asset) || (additionalCommodities != null && additionalCommodities.Contains(asset, StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether an asset is a known stock. Note that this is not definitive, only large known stocks are checked
|
||||
/// </summary>
|
||||
/// <param name="asset">Asset name</param>
|
||||
/// <param name="additionalStocks">Additional stock names for the specific exchange</param>
|
||||
public static bool IsEquity(string asset, params HashSet<string> additionalStocks)
|
||||
=> IsEquity(asset, [], additionalStocks);
|
||||
|
||||
/// <summary>
|
||||
/// Check whether an asset is a known stock.
|
||||
/// </summary>
|
||||
/// <param name="asset">Asset name</param>
|
||||
/// <param name="potentialSuffixes">Suffixes to check, for example when `X` is a potential suffix both `TSLA` and `TSLAX` will be checked</param>
|
||||
/// <param name="additionalStocks">Additional stock names for the specific exchange</param>
|
||||
public static bool IsEquity(string asset, string[] potentialSuffixes, params HashSet<string> additionalStocks)
|
||||
{
|
||||
if (string.IsNullOrEmpty(asset))
|
||||
return false;
|
||||
|
||||
if (_stocks.Contains(asset) || (additionalStocks != null && additionalStocks.Contains(asset, StringComparer.OrdinalIgnoreCase)))
|
||||
return true;
|
||||
|
||||
foreach (var suffix in potentialSuffixes)
|
||||
{
|
||||
if (!asset.EndsWith(suffix))
|
||||
continue;
|
||||
|
||||
var suffixAsset = asset.Substring(0, asset.Length - suffix.Length);
|
||||
if (_stocks.Contains(suffixAsset) || (additionalStocks != null && additionalStocks.Contains(suffixAsset, StringComparer.OrdinalIgnoreCase)))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new HttpMessageHandler instance
|
||||
/// </summary>
|
||||
|
||||
@@ -233,7 +233,7 @@ namespace CryptoExchange.Net.Objects
|
||||
/// Add key as comma separated values
|
||||
/// </summary>
|
||||
#if NET5_0_OR_GREATER
|
||||
public void AddCommaSeparated<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, IEnumerable<T> values)
|
||||
public void AddCommaSeparated<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, IEnumerable<T>? values)
|
||||
#else
|
||||
public void AddCommaSeparated<T>(string key, IEnumerable<T>? values)
|
||||
#endif
|
||||
|
||||
@@ -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>
|
||||
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>
|
||||
/// 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 />
|
||||
|
||||
@@ -9,6 +9,11 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
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>
|
||||
/// 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 />
|
||||
|
||||
@@ -68,9 +68,14 @@ namespace CryptoExchange.Net.SharedApis
|
||||
return ArgumentError.Invalid("TradingMode", $"TradingMode.{tradingMode} is not supported, supported types: {string.Join(", ", supportedTradingModes)}");
|
||||
|
||||
foreach (var param in RequiredExchangeParameters)
|
||||
{
|
||||
{
|
||||
if (param.Names!.All(x => ExchangeParameters.HasValue(exchangeParameters, Exchange, x, param.ValueType) != true))
|
||||
return ArgumentError.Invalid(string.Join("/", param.Names!), $"One of exchange parameters `{string.Join(", ", param.Names!)}` for exchange `{Exchange}` should be provided. Example: {param.ExampleValue}");
|
||||
{
|
||||
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 null;
|
||||
@@ -149,7 +154,12 @@ namespace CryptoExchange.Net.SharedApis
|
||||
foreach (var param in RequiredOptionalParameters)
|
||||
{
|
||||
if (param.Names!.All(x => _requestProperties.Single(p => p.Name == x).GetValue(request, null) == null))
|
||||
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 (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}");
|
||||
}
|
||||
}
|
||||
|
||||
if (request is SharedSymbolRequest symbolsRequest)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
@@ -15,5 +16,43 @@ namespace CryptoExchange.Net.SharedApis
|
||||
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.Text;
|
||||
|
||||
@@ -15,5 +16,44 @@ namespace CryptoExchange.Net.SharedApis
|
||||
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>
|
||||
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>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <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>
|
||||
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.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Asset info
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Name,nq} - {Networks.Length} network(s)")]
|
||||
public record SharedAsset
|
||||
{
|
||||
/// <summary>
|
||||
@@ -32,6 +34,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// Asset network info
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Name,nq}")]
|
||||
public record SharedAssetNetwork
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Balance info
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Available} {Asset, nq}")]
|
||||
public record SharedBalance
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Book ticker
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Symbol,nq} - {BestBidPrice} / {BestAskPrice}")]
|
||||
public record SharedBookTicker : SharedSymbolModel
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Deposit info
|
||||
/// </summary>
|
||||
[DebuggerDisplay("[{Timestamp}] {Quantity} {Asset,nq}")]
|
||||
public record SharedDeposit
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Deposit address info
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Asset,nq} - {Address,nq}")]
|
||||
public record SharedDepositAddress
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Trading fee info
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{MakerFee} / {TakerFee}")]
|
||||
public record SharedFee
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Funding rate
|
||||
/// </summary>
|
||||
[DebuggerDisplay("[{Timestamp}] {FundingRate}")]
|
||||
public record SharedFundingRate
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Mark/index price kline
|
||||
/// </summary>
|
||||
[DebuggerDisplay("[{OpenTime}] O: {OpenPrice} H: {HighPrice} L: {LowPrice} C: {ClosePrice}")]
|
||||
public record SharedFuturesKline : SharedSymbolModel
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Futures order info
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{DebugView,nq}")]
|
||||
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>
|
||||
/// Id of the order
|
||||
/// </summary>
|
||||
@@ -47,10 +54,18 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Order price
|
||||
/// </summary>
|
||||
public decimal? OrderPrice { get; set; }
|
||||
private decimal? _averagePrice;
|
||||
/// <summary>
|
||||
/// Average price
|
||||
/// Average fill price
|
||||
/// </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>
|
||||
/// Client order id
|
||||
/// </summary>
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Futures symbol info
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{DebugView,nq}")]
|
||||
public record SharedFuturesSymbol : SharedSpotSymbol
|
||||
{
|
||||
private string DebugView => $"{TradingMode} {(DisplayName ?? Name)} - {BaseAssetType}{(BaseAssetSubType == null ? "" : " " + BaseAssetSubType)}{(DeliveryTime != null ? $" Delivery: {DeliveryTime:yyyy-MM-dd}": "")}";
|
||||
|
||||
/// <summary>
|
||||
/// The size of a single contract
|
||||
/// </summary>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Futures ticker info
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Symbol,nq} High: {HighPrice}, Low: {LowPrice}, Last: {LastPrice}, Change: {ChangePercentage}%")]
|
||||
public record SharedFuturesTicker: SharedSymbolModel
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Kline info
|
||||
/// </summary>
|
||||
[DebuggerDisplay("[{OpenTime}] O: {OpenPrice} H: {HighPrice} L: {LowPrice} C: {ClosePrice} V: {Volume}")]
|
||||
public record SharedKline : SharedSymbolModel
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Position info
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Symbol,nq} {PositionSide}: {PositionSize} {AverageOpenPrice}")]
|
||||
public record SharedPosition : SharedSymbolModel
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Position history
|
||||
/// </summary>
|
||||
[DebuggerDisplay("[{Timestamp}] {Symbol,nq} {PositionSide}: {RealizedPnl}")]
|
||||
public record SharedPositionHistory : SharedSymbolModel
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Spot order info
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{DebugView,nq}")]
|
||||
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>
|
||||
/// The id of the order
|
||||
/// </summary>
|
||||
@@ -39,10 +46,18 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Order price
|
||||
/// </summary>
|
||||
public decimal? OrderPrice { get; set; }
|
||||
private decimal? _averagePrice;
|
||||
/// <summary>
|
||||
/// Average fill price
|
||||
/// </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>
|
||||
/// Client order id
|
||||
/// </summary>
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol info
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{DebugView,nq}")]
|
||||
public record SharedSpotSymbol
|
||||
{
|
||||
private string DebugView => $"{TradingMode} {(DisplayName ?? Name)} - {BaseAssetType} {BaseAssetSubType}";
|
||||
|
||||
/// <summary>
|
||||
/// The trading mode of the symbol
|
||||
/// </summary>
|
||||
@@ -22,6 +29,10 @@
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
/// <summary>
|
||||
/// The display name of the symbol
|
||||
/// </summary>
|
||||
public string? DisplayName { get; set; }
|
||||
/// <summary>
|
||||
/// Minimal quantity of an order in the base asset
|
||||
/// </summary>
|
||||
public decimal? MinTradeQuantity { get; set; }
|
||||
@@ -57,6 +68,22 @@
|
||||
/// Whether the symbol is currently available for trading
|
||||
/// </summary>
|
||||
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>
|
||||
/// ctor
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Ticker info
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Symbol,nq} High: {HighPrice}, Low: {LowPrice}, Last: {LastPrice}, Change: {ChangePercentage}%")]
|
||||
public record SharedSpotTicker: SharedSymbolModel
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Public trade info
|
||||
/// </summary>
|
||||
[DebuggerDisplay("[{Timestamp}] {Symbol,nq} {Side.ToString(),nq} {Quantity} @ {Price}")]
|
||||
public record SharedTrade : SharedSymbolModel
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// A user trade
|
||||
/// </summary>
|
||||
[DebuggerDisplay("[{Timestamp}] {Id,nq} {Symbol,nq} {Side.ToString(),nq} {Quantity} @ {Price}")]
|
||||
public record SharedUserTrade : SharedSymbolModel
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// A withdrawal record
|
||||
/// </summary>
|
||||
[DebuggerDisplay("[{Timestamp}] {Quantity} {Asset,nq}")]
|
||||
public record SharedWithdrawal
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
@@ -35,6 +36,33 @@ namespace CryptoExchange.Net.SharedApis
|
||||
QuantityInQuoteAsset = quoteAssetQuantity;
|
||||
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>
|
||||
@@ -53,6 +81,9 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
public SharedQuantity() : base(null, null, null) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => base.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Specify quantity in base asset
|
||||
/// </summary>
|
||||
@@ -123,5 +154,8 @@ namespace CryptoExchange.Net.SharedApis
|
||||
: base(baseAssetQuantity, quoteAssetQuantity, contractQuantity)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => base.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
@@ -176,5 +177,24 @@ namespace CryptoExchange.Net.SharedApis
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,10 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
foreach (var dictProp in jObj.EnumerateObject())
|
||||
{
|
||||
if (!dict.Contains(dictProp.Name))
|
||||
{
|
||||
outputExceptions.Add(new Exception($"{method}: Dictionary has no value for {dictProp.Name} while input json `{dictProp.Name}` has value {dictProp.Value}"));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dictProp.Value.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
@@ -80,6 +83,7 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
|
||||
// Property value not correct
|
||||
outputExceptions.Add(new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {dictProp.Value}"));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,7 +135,10 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
{
|
||||
var value = enumerator.Current;
|
||||
if (value == default && jObj.ValueKind != JsonValueKind.Null)
|
||||
{
|
||||
outputExceptions.Add(new Exception($"{method}: Array has no value while input json array has value {jObj}"));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,7 +174,7 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
}
|
||||
|
||||
Debug.WriteLine($"Successfully validated {method}");
|
||||
return outputExceptions;
|
||||
return outputExceptions.Distinct().ToList();
|
||||
}
|
||||
|
||||
private static void CheckObject(string method, JsonProperty prop, object obj, List<string>? ignoreProperties, List<Exception> outputExceptions)
|
||||
@@ -225,7 +232,10 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
|
||||
// Property value not correct
|
||||
if (propValue.ToString() != "0")
|
||||
{
|
||||
outputExceptions.Add(new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ((propertyValue == default && (propValue.ValueKind == JsonValueKind.Null || string.IsNullOrEmpty(propValue.ToString()))) || propValue.ToString() == "0")
|
||||
@@ -237,7 +247,10 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
foreach (var dictProp in propValue.EnumerateObject())
|
||||
{
|
||||
if (!dict.Contains(dictProp.Name))
|
||||
{
|
||||
outputExceptions.Add(new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}"));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dictProp.Value.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
@@ -246,8 +259,11 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
else
|
||||
{
|
||||
if (dict[dictProp.Name] == default && dictProp.Value.ValueKind != JsonValueKind.Null)
|
||||
{
|
||||
// Property value not correct
|
||||
outputExceptions.Add(new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {propValue} for"));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -304,7 +320,10 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
{
|
||||
var value = enumerator.Current;
|
||||
if (value == default && jToken.ValueKind != JsonValueKind.Null)
|
||||
{
|
||||
outputExceptions.Add(new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {jToken}"));
|
||||
return;
|
||||
}
|
||||
|
||||
CheckValues(method, propertyName!, propertyType, jToken, value!, outputExceptions);
|
||||
}
|
||||
@@ -368,7 +387,10 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
{
|
||||
var value = enumerator.Current;
|
||||
if (value == default && jObj.ValueKind != JsonValueKind.Null)
|
||||
{
|
||||
outputExceptions.Add(new Exception($"{method}: Array has no value while input json array has value {jObj}"));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ namespace CryptoExchange.Net.Testing
|
||||
var issues = SystemTextJsonComparer.CompareData(expressionBody.Method.Name, data, originalData, compareNestedProperty, ignoreProperties, useSingleArrayItem ?? false);
|
||||
foreach(var issue in issues)
|
||||
{
|
||||
if (issue is MissingPropertyException)
|
||||
if (issue is MissingPropertyException && !warnings?.Any(x => x.Message == issue.Message) == true)
|
||||
warnings?.Add(issue);
|
||||
else
|
||||
errors.Add(issue);
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace CryptoExchange.Net.Trackers.Klines
|
||||
private readonly IKlineRestClient _restClient;
|
||||
private SyncStatus _status;
|
||||
private bool _startWithSnapshot;
|
||||
private ExchangeParameters? _exchangeParameters;
|
||||
|
||||
/// <summary>
|
||||
/// The internal data structure
|
||||
@@ -157,9 +158,11 @@ namespace CryptoExchange.Net.Trackers.Klines
|
||||
SharedSymbol symbol,
|
||||
SharedKlineInterval interval,
|
||||
int? limit = null,
|
||||
TimeSpan? period = null)
|
||||
TimeSpan? period = null,
|
||||
ExchangeParameters? exchangeParameters = null)
|
||||
{
|
||||
_logger = logger ?? new NullLogger<KlineTracker>();
|
||||
_exchangeParameters = exchangeParameters;
|
||||
Symbol = symbol;
|
||||
SymbolName = socketClient.FormatSymbol(symbol.BaseAsset, symbol.QuoteAsset, symbol.TradingMode, symbol.DeliverTime);
|
||||
Exchange = restClient.Exchange;
|
||||
@@ -180,7 +183,7 @@ namespace CryptoExchange.Net.Trackers.Klines
|
||||
Status = SyncStatus.Syncing;
|
||||
_logger.KlineTrackerStarting(SymbolName);
|
||||
|
||||
var subResult = await _socketClient.SubscribeToKlineUpdatesAsync(new SubscribeKlineRequest(Symbol, Interval),
|
||||
var subResult = await _socketClient.SubscribeToKlineUpdatesAsync(new SubscribeKlineRequest(Symbol, Interval, exchangeParameters: _exchangeParameters),
|
||||
update =>
|
||||
{
|
||||
AddOrUpdate(update.Data);
|
||||
@@ -237,7 +240,7 @@ namespace CryptoExchange.Net.Trackers.Klines
|
||||
|
||||
var limit = Math.Min(_restClient.GetKlinesOptions.MaxLimit, Limit ?? 100);
|
||||
|
||||
var request = new GetKlinesRequest(Symbol, Interval, startTime, DateTime.UtcNow, limit: limit);
|
||||
var request = new GetKlinesRequest(Symbol, Interval, startTime, DateTime.UtcNow, limit: limit, exchangeParameters: _exchangeParameters);
|
||||
var data = new List<SharedKline>();
|
||||
await foreach (var result in ExchangeHelpers.ExecutePages(_restClient.GetKlinesAsync, request).ConfigureAwait(false))
|
||||
{
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace CryptoExchange.Net.Trackers.Trades
|
||||
private SyncStatus _status;
|
||||
private long _snapshotId;
|
||||
private bool _startWithSnapshot;
|
||||
private ExchangeParameters? _exchangeParameters;
|
||||
|
||||
/// <summary>
|
||||
/// The internal data structure
|
||||
@@ -154,12 +155,14 @@ namespace CryptoExchange.Net.Trackers.Trades
|
||||
ITradeSocketClient socketClient,
|
||||
SharedSymbol symbol,
|
||||
int? limit = null,
|
||||
TimeSpan? period = null)
|
||||
TimeSpan? period = null,
|
||||
ExchangeParameters? exchangeParameters = null)
|
||||
{
|
||||
_logger = logger ?? new NullLogger<TradeTracker>();
|
||||
_recentRestClient = recentRestClient;
|
||||
_historyRestClient = historyRestClient;
|
||||
_socketClient = socketClient;
|
||||
_exchangeParameters = exchangeParameters;
|
||||
Exchange = socketClient.Exchange;
|
||||
Symbol = symbol;
|
||||
SymbolName = socketClient.FormatSymbol(symbol.BaseAsset, symbol.QuoteAsset, symbol.TradingMode, symbol.DeliverTime);
|
||||
@@ -203,7 +206,7 @@ namespace CryptoExchange.Net.Trackers.Trades
|
||||
_startWithSnapshot = startWithSnapshot;
|
||||
Status = SyncStatus.Syncing;
|
||||
_logger.TradeTrackerStarting(SymbolName);
|
||||
var subResult = await _socketClient.SubscribeToTradeUpdatesAsync(new SubscribeTradeRequest(Symbol),
|
||||
var subResult = await _socketClient.SubscribeToTradeUpdatesAsync(new SubscribeTradeRequest(Symbol, exchangeParameters: _exchangeParameters),
|
||||
update =>
|
||||
{
|
||||
AddData(update.Data);
|
||||
@@ -257,7 +260,7 @@ namespace CryptoExchange.Net.Trackers.Trades
|
||||
if (_historyRestClient != null)
|
||||
{
|
||||
var startTime = Period == null ? DateTime.UtcNow.AddMinutes(-5) : DateTime.UtcNow.Add(-Period.Value);
|
||||
var request = new GetTradeHistoryRequest(Symbol, startTime, DateTime.UtcNow);
|
||||
var request = new GetTradeHistoryRequest(Symbol, startTime, DateTime.UtcNow, exchangeParameters: _exchangeParameters);
|
||||
var data = new List<SharedTrade>();
|
||||
await foreach (var result in ExchangeHelpers.ExecutePages(_historyRestClient.GetTradeHistoryAsync, request).ConfigureAwait(false))
|
||||
{
|
||||
@@ -278,7 +281,7 @@ namespace CryptoExchange.Net.Trackers.Trades
|
||||
if (Limit.HasValue)
|
||||
limit = Math.Min(_recentRestClient.GetRecentTradesOptions.MaxLimit, Limit.Value);
|
||||
|
||||
var snapshot = await _recentRestClient.GetRecentTradesAsync(new GetRecentTradesRequest(Symbol, limit)).ConfigureAwait(false);
|
||||
var snapshot = await _recentRestClient.GetRecentTradesAsync(new GetRecentTradesRequest(Symbol, limit, exchangeParameters: _exchangeParameters)).ConfigureAwait(false);
|
||||
if (!snapshot.Success)
|
||||
{
|
||||
return CallResult.Fail(snapshot.Error);
|
||||
|
||||
@@ -127,6 +127,30 @@ Various:
|
||||
* PlatformInfo now required support environment names in the constructor
|
||||
|
||||
## Release notes
|
||||
* Version 12.3.0 - 23 Jul 2026
|
||||
* Added calculation of AveragePrice on Shared order models if data is available and AveragePrice is not set
|
||||
* Extracted ConnectionCanBeUsedFor method in SocketApiClient for easier custom logic implementation
|
||||
* Updated some Shared APIs error messages
|
||||
* Remove duplicate warnings from testing output
|
||||
|
||||
* Version 12.2.0 - 20 Jul 2026
|
||||
* 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
|
||||
* Added DebuggerDisplay attributes to Shared models
|
||||
* Fixed socket connection combine calculations
|
||||
|
||||
* Version 12.1.1 - 11 Jul 2026
|
||||
* Added timestamp deserialization support for yyyy-MM-dd HH:mm:ss.ffffff+00:00:00
|
||||
|
||||
* Version 12.1.0 - 09 Jul 2026
|
||||
* Added ExchangeParameters parameter to KlineTracker, TradeTracker and ITrackerFactory methods
|
||||
* Updated some testing logic
|
||||
* Fixed nullability operator on Parameters.AddCommaSeperated
|
||||
|
||||
* Version 12.0.2 - 01 Jul 2026
|
||||
* Updated test validation to output a list of issues instead of throwing on the first
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
> 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.3.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.
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
Reference in New Issue
Block a user