Skip to content

Commit

Permalink
Trackers (#2)
Browse files Browse the repository at this point in the history
Updated examples
Updated CryptoExchange.Net to v8.1.0
Moved FormatSymbol to CoinbaseExchange class
Added support Side setting on SharedTrade model
Added CoinbaseTrackerFactory
Added overload to Create method on CoinbaseOrderBookFactory support SharedSymbol parameter
Added GetKlinesAsync to Shared rest client
Fixed exception on restClient.AdvncedTrading.Trading.CancelOrderAynsc when order not found
Fixed exception on restClient.AdvncedTrading.Trading.CancelOrdersAynsc when request fails
Removed incorrect rate limit of 100 message per second per ip for websockets
Fixed issue with concurrent websocket subscription acknowledgements
  • Loading branch information
JKorf authored Oct 28, 2024
1 parent 5e168fe commit f9fa448
Show file tree
Hide file tree
Showing 23 changed files with 533 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -127,19 +127,8 @@ protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
=> _timeSyncState.TimeOffset;

/// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null)
{
if (tradingMode == TradingMode.Spot)
return $"{baseAsset.ToUpperInvariant()}-{quoteAsset.ToUpperInvariant()}";

if (tradingMode.IsPerpetual())
return $"{baseAsset.ToUpperInvariant()}-PERP-INTX";

if (deliverDate == null)
throw new ArgumentException("DeliverDate required for delivery futures symbol");

return $"{baseAsset.ToUpperInvariant()}-{deliverDate.Value:dd}{deliverDate.Value.ToString("MMM").ToUpper()}{deliverDate.Value:yy}-CDE";
}
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null)
=> CoinbaseExchange.FormatSymbol(baseAsset, quoteAsset, tradingMode, deliverTime);

/// <inheritdoc />
public ICoinbaseRestClientAdvancedTradeApiShared SharedClient => this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,10 @@ async Task<ExchangeWebResult<IEnumerable<SharedTrade>>> IRecentTradeRestClient.G
return result.AsExchangeResult<IEnumerable<SharedTrade>>(Exchange, null, default);

// Return
return result.AsExchangeResult<IEnumerable<SharedTrade>>(Exchange, request.Symbol.TradingMode, result.Data.Trades.Select(x => new SharedTrade(x.Quantity, x.Price, x.Timestamp)).ToArray());
return result.AsExchangeResult<IEnumerable<SharedTrade>>(Exchange, request.Symbol.TradingMode, result.Data.Trades.Select(x => new SharedTrade(x.Quantity, x.Price, x.Timestamp)
{
Side = x.OrderSide == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell
}).ToArray());
}
#endregion

Expand Down Expand Up @@ -264,7 +267,10 @@ async Task<ExchangeWebResult<IEnumerable<SharedTrade>>> ITradeHistoryRestClient.
nextToken = new DateTimeToken(result.Data.Trades.Min(x => x.Timestamp.AddMilliseconds(-1)));

// Return
return result.AsExchangeResult<IEnumerable<SharedTrade>>(Exchange, TradingMode.Spot, result.Data.Trades.Select(x => new SharedTrade(x.Quantity, x.Price, x.Timestamp)).ToArray(), nextToken);
return result.AsExchangeResult<IEnumerable<SharedTrade>>(Exchange, TradingMode.Spot, result.Data.Trades.Select(x => new SharedTrade(x.Quantity, x.Price, x.Timestamp)
{
Side = x.OrderSide == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell
}).ToArray(), nextToken);
}
#endregion

Expand Down Expand Up @@ -683,7 +689,7 @@ async Task<ExchangeWebResult<SharedFuturesTicker>> IFuturesTickerRestClient.GetF
return resultTicker.AsExchangeResult<SharedFuturesTicker>(Exchange, null, default);

return resultTicker.AsExchangeResult(Exchange, request.Symbol.TradingMode,
new SharedFuturesTicker(resultTicker.Data.Symbol, resultTicker.Data.LastPrice ?? 0, 0, 0, resultTicker.Data.Volume24h ?? 0, resultTicker.Data.PricePercentageChange24h)
new SharedFuturesTicker(resultTicker.Data.Symbol, resultTicker.Data.LastPrice, null, null, resultTicker.Data.Volume24h ?? 0, resultTicker.Data.PricePercentageChange24h)
{
FundingRate = resultTicker.Data.FutureProductDetails!.PerpetualDetails!.FundingRate,
NextFundingTime = resultTicker.Data.FutureProductDetails.PerpetualDetails.FundingTime
Expand All @@ -705,7 +711,7 @@ async Task<ExchangeWebResult<IEnumerable<SharedFuturesTicker>>> IFuturesTickerRe
var data = resultTicker.Data;
return resultTicker.AsExchangeResult<IEnumerable<SharedFuturesTicker>>(Exchange, request.TradingMode.HasValue ? [request.TradingMode.Value]: SupportedTradingModes,
data.Select(x =>
new SharedFuturesTicker(x.Symbol, x.LastPrice ?? 0, 0, 0, x.Volume24h ?? 0, x.PricePercentageChange24h)
new SharedFuturesTicker(x.Symbol, x.LastPrice, null, null, x.Volume24h ?? 0, x.PricePercentageChange24h)
{
FundingRate = x.FutureProductDetails!.PerpetualDetails?.FundingRate,
NextFundingTime = x.FutureProductDetails.PerpetualDetails?.FundingTime
Expand Down Expand Up @@ -1082,6 +1088,66 @@ async Task<ExchangeWebResult<SharedId>> IFuturesOrderRestClient.ClosePositionAsy

#endregion

#region Klines Client

GetKlinesOptions IKlineRestClient.GetKlinesOptions { get; } = new GetKlinesOptions(SharedPaginationSupport.Descending, false)
{
MaxRequestDataPoints = 350
};

async Task<ExchangeWebResult<IEnumerable<SharedKline>>> IKlineRestClient.GetKlinesAsync(GetKlinesRequest request, INextPageToken? pageToken, CancellationToken ct)
{
var interval = (Enums.KlineInterval)request.Interval;
if (!Enum.IsDefined(typeof(Enums.KlineInterval), interval))
return new ExchangeWebResult<IEnumerable<SharedKline>>(Exchange, new ArgumentError("Interval not supported"));

var validationError = ((IKlineRestClient)this).GetKlinesOptions.ValidateRequest(Exchange, request, request.Symbol.TradingMode, SupportedTradingModes);
if (validationError != null)
return new ExchangeWebResult<IEnumerable<SharedKline>>(Exchange, validationError);

// Determine pagination
// Data is normally returned oldest first, so to do newest first pagination we have to do some calc
DateTime endTime = request.EndTime ?? DateTime.UtcNow;
DateTime? startTime = request.StartTime;
if (pageToken is DateTimeToken dateTimeToken)
endTime = dateTimeToken.LastTime;

var limit = request.Limit ?? 350;
if (startTime == null || startTime < endTime)
{
var offset = (int)interval * limit;
startTime = endTime.AddSeconds(-offset);
}

if (startTime < request.StartTime)
startTime = request.StartTime;

// Get data
var result = await ExchangeData.GetKlinesAsync(
request.Symbol.GetSymbol(FormatSymbol),
interval,
startTime,
endTime,
limit,
ct: ct
).ConfigureAwait(false);
if (!result)
return new ExchangeWebResult<IEnumerable<SharedKline>>(Exchange, TradingMode.Spot, result.As<IEnumerable<SharedKline>>(default));

// Get next token
DateTimeToken? nextToken = null;
if (result.Data.Count() == limit)
{
var minOpenTime = result.Data.Min(x => x.OpenTime);
if (request.StartTime == null || minOpenTime > request.StartTime.Value)
nextToken = new DateTimeToken(minOpenTime.AddSeconds(-(int)(interval - 1)));
}

return result.AsExchangeResult<IEnumerable<SharedKline>>(Exchange, request.Symbol.TradingMode, result.Data.Reverse().Select(x => new SharedKline(x.OpenTime, x.ClosePrice, x.HighPrice, x.LowPrice, x.OpenPrice, x.Volume)).ToArray(), nextToken);
}

#endregion

private async Task<string?> GetAccountIdAsync(ExchangeParameters? parameters, string? asset, CancellationToken ct)
{
var accountId = ExchangeParameters.GetValue<string>(parameters, Exchange, "AccountId");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ public async Task<WebCallResult<CoinbaseCancelResult>> CancelOrderAsync(string o
if (!result)
return result.As<CoinbaseCancelResult>(default);

var cancelResult = result.Data.Single();
var cancelResult = result.Data.SingleOrDefault();
if (cancelResult == null)
return result.AsError<CoinbaseCancelResult>(new ServerError("Order not found"));

if (!cancelResult.Success)
return result.AsError<CoinbaseCancelResult>(new ServerError(cancelResult.ErrorMessage!));

Expand All @@ -96,7 +99,7 @@ public async Task<WebCallResult<IEnumerable<CoinbaseCancelResult>>> CancelOrders
parameters.Add("order_ids", orderIds.ToArray());
var request = _definitions.GetOrCreate(HttpMethod.Post, "api/v3/brokerage/orders/batch_cancel", CoinbaseExchange.RateLimiter.CoinbaseRestPrivate, 1, true);
var result = await _baseClient.SendAsync<CoinbaseCancelResultWrapper>(request, parameters, ct).ConfigureAwait(false);
return result.As(result.Data.Results);
return result.As<IEnumerable<CoinbaseCancelResult>>(result.Data?.Results);
}

#endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,18 +188,7 @@ public async Task<CallResult<UpdateSubscription>> SubscribeToFuturesBalanceUpdat
public ICoinbaseSocketClientAdvancedTradeApiShared SharedClient => this;

/// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null)
{
if (tradingMode == TradingMode.Spot)
return $"{baseAsset.ToUpperInvariant()}-{quoteAsset.ToUpperInvariant()}";

if (tradingMode.IsPerpetual())
return $"{baseAsset.ToUpperInvariant()}-PERP-INTX";

if (deliverDate == null)
throw new ArgumentException("DeliverDate required for delivery futures symbol");

return $"{baseAsset.ToUpperInvariant()}-{deliverDate.Value:dd}{deliverDate.Value.ToString("MMM").ToUpper()}{deliverDate.Value:yy}-CDE";
}
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null)
=> CoinbaseExchange.FormatSymbol(baseAsset, quoteAsset, tradingMode, deliverTime);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,11 @@ async Task<ExchangeResult<UpdateSubscription>> ITradeSocketClient.SubscribeToTra
return;

foreach (var item in update.Data)
handler(update.AsExchangeEvent<IEnumerable<SharedTrade>>(Exchange, new[] { new SharedTrade(item.Quantity, item.Price, item.Timestamp) }));
{
handler(update.AsExchangeEvent<IEnumerable<SharedTrade>>(Exchange, new[] { new SharedTrade(item.Quantity, item.Price, item.Timestamp){
Side = item.OrderSide == OrderSide.Buy ? SharedOrderSide.Buy : SharedOrderSide.Sell
} }));
}

}, ct).ConfigureAwait(false);

Expand Down
2 changes: 1 addition & 1 deletion Coinbase.Net/Coinbase.Net.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="CryptoExchange.Net" Version="8.0.3" />
<PackageReference Include="CryptoExchange.Net" Version="8.1.0" />
<PackageReference Include="jose-jwt" Version="5.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
Expand Down
65 changes: 65 additions & 0 deletions Coinbase.Net/Coinbase.Net.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 26 additions & 2 deletions Coinbase.Net/CoinbaseExchange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.RateLimiting;
using System;
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net;

namespace Coinbase.Net
{
Expand All @@ -30,6 +32,28 @@ public static class CoinbaseExchange
"https://docs.cdp.coinbase.com/coinbase-app/docs/welcome"
};

/// <summary>
/// Format a base and quote asset to a Coinbase recognized symbol
/// </summary>
/// <param name="baseAsset">Base asset</param>
/// <param name="quoteAsset">Quote asset</param>
/// <param name="tradingMode">Trading mode</param>
/// <param name="deliverTime">Delivery time for delivery futures</param>
/// <returns></returns>
public static string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverTime = null)
{
if (tradingMode == TradingMode.Spot)
return $"{baseAsset.ToUpperInvariant()}-{quoteAsset.ToUpperInvariant()}";

if (tradingMode.IsPerpetual())
return $"{baseAsset.ToUpperInvariant()}-PERP-INTX";

if (deliverTime == null)
throw new ArgumentException("DeliverDate required for delivery futures symbol");

return $"{baseAsset.ToUpperInvariant()}-{deliverTime.Value:dd}{deliverTime.Value.ToString("MMM").ToUpper()}{deliverTime.Value:yy}-CDE";
}

/// <summary>
/// Rate limiter configuration for the Coinbase API
/// </summary>
Expand Down Expand Up @@ -60,8 +84,8 @@ private void Initialize()
CoinbaseRestPrivate = new RateLimitGate("Coinbase Private")
.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, Array.Empty<IGuardFilter>(), 30, TimeSpan.FromSeconds(1), RateLimitWindowType.Sliding));
CoinbaseSocket = new RateLimitGate("Coinbase Socket")
.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Request), 100, TimeSpan.FromSeconds(1), RateLimitWindowType.Sliding))
.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new LimitItemTypeFilter(RateLimitItemType.Request), 8, TimeSpan.FromSeconds(1), RateLimitWindowType.Sliding));
.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 750, TimeSpan.FromSeconds(1), RateLimitWindowType.Sliding))
.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Request), 8, TimeSpan.FromSeconds(1), RateLimitWindowType.Sliding));
CoinbaseRestPublic.RateLimitTriggered += (x) => RateLimitTriggered?.Invoke(x);
CoinbaseRestPrivate.RateLimitTriggered += (x) => RateLimitTriggered?.Invoke(x);
CoinbaseSocket.RateLimitTriggered += (x) => RateLimitTriggered?.Invoke(x);
Expand Down
Loading

0 comments on commit f9fa448

Please sign in to comment.