Skip to content

Commit 6a21325

Browse files
authored
chore: Adding better errors for coinbase order errors (#826)
* chore: Adding better errors for coinbase order errors * Please round if requested * Helper already checks flag
1 parent 33be336 commit 6a21325

File tree

1 file changed

+50
-38
lines changed

1 file changed

+50
-38
lines changed

src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs

Lines changed: 50 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ public sealed partial class ExchangeCoinbaseAPI : ExchangeAPI
2929
public override string BaseUrl { get; set; } = "https://api.coinbase.com/api/v3/brokerage";
3030
private readonly string BaseUrlV2 = "https://api.coinbase.com/v2"; // For Wallet Support
3131
public override string BaseUrlWebSocket { get; set; } = "wss://advanced-trade-ws.coinbase.com";
32-
32+
3333
private enum PaginationType { None, V2, V3}
3434
private PaginationType pagination = PaginationType.None;
35-
private string cursorNext;
35+
private string cursorNext;
3636

3737
private Dictionary<string, string> Accounts = null; // Cached Account IDs
3838

@@ -62,7 +62,7 @@ private void ProcessResponse(IAPIRequestMaker maker, RequestMakerState state, ob
6262
JToken token = JsonConvert.DeserializeObject<JToken>((string)response);
6363
if (token == null) return;
6464
switch(pagination)
65-
{
65+
{
6666
case PaginationType.V2: cursorNext = token["pagination"]?["next_starting_after"]?.ToStringInvariant(); break;
6767
case PaginationType.V3: cursorNext = token[CURSOR]?.ToStringInvariant(); break;
6868
}
@@ -77,7 +77,7 @@ private void ProcessResponse(IAPIRequestMaker maker, RequestMakerState state, ob
7777
/// <param name="payload"></param>
7878
/// <returns></returns>
7979
protected override bool CanMakeAuthenticatedRequest(IReadOnlyDictionary<string, object> payload)
80-
{
80+
{
8181
return (PrivateApiKey != null && PublicApiKey != null);
8282
}
8383

@@ -90,7 +90,7 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti
9090

9191
// V2 wants PathAndQuery, V3 wants LocalPath for the sig (I guess they wanted to shave a nano-second or two - silly)
9292
string path = request.RequestUri.AbsoluteUri.StartsWith(BaseUrlV2) ? request.RequestUri.PathAndQuery : request.RequestUri.LocalPath;
93-
string signature = CryptoUtility.SHA256Sign(timestamp + request.Method.ToUpperInvariant() + path + body, PrivateApiKey.ToUnsecureString());
93+
string signature = CryptoUtility.SHA256Sign(timestamp + request.Method.ToUpperInvariant() + path + body, PrivateApiKey.ToUnsecureString());
9494

9595
request.AddHeader("CB-ACCESS-KEY", PublicApiKey.ToUnsecureString());
9696
request.AddHeader("CB-ACCESS-SIGN", signature);
@@ -141,7 +141,7 @@ protected internal override async Task<IEnumerable<ExchangeMarket>> OnGetMarketS
141141

142142
protected override async Task<IEnumerable<string>> OnGetMarketSymbolsAsync()
143143
{
144-
return (await GetMarketSymbolsMetadataAsync()).Select(market => market.MarketSymbol);
144+
return (await GetMarketSymbolsMetadataAsync()).Select(market => market.MarketSymbol);
145145
}
146146

147147
protected override async Task<IReadOnlyDictionary<string, ExchangeCurrency>> OnGetCurrenciesAsync()
@@ -176,7 +176,7 @@ protected override async Task<IReadOnlyDictionary<string, ExchangeCurrency>> OnG
176176
currencies[currency.Name] = currency;
177177
}
178178
}
179-
return currencies;
179+
return currencies;
180180
}
181181

182182
protected override async Task<IEnumerable<KeyValuePair<string, ExchangeTicker>>> OnGetTickersAsync()
@@ -187,7 +187,7 @@ protected override async Task<IEnumerable<KeyValuePair<string, ExchangeTicker>>>
187187
foreach (JToken book in books[PRICEBOOKS])
188188
{
189189
var split = book[PRODUCTID].ToString().Split(GlobalMarketSymbolSeparator);
190-
// This endpoint does not provide a last or open for the ExchangeTicker
190+
// This endpoint does not provide a last or open for the ExchangeTicker
191191
tickers.Add(new KeyValuePair<string, ExchangeTicker>(book[PRODUCTID].ToString(), new ExchangeTicker()
192192
{
193193
MarketSymbol = book[PRODUCTID].ToString(),
@@ -224,7 +224,7 @@ protected override async Task<ExchangeTicker> OnGetTickerAsync(string marketSymb
224224
QuoteCurrencyVolume = book[ASKS][0][SIZE].ConvertInvariant<decimal>(),
225225
Timestamp = DateTime.UtcNow
226226
}
227-
};
227+
};
228228
}
229229

230230
protected override async Task<ExchangeOrderBook> OnGetOrderBookAsync(string marketSymbol, int maxCount = 50)
@@ -267,8 +267,8 @@ protected override async Task<IEnumerable<MarketCandle>> OnGetCandlesAsync(strin
267267
if ((RangeEnd - RangeStart).TotalSeconds / periodSeconds > 300) RangeStart = RangeEnd.AddSeconds(-(periodSeconds * 300));
268268

269269
List<MarketCandle> candles = new List<MarketCandle>();
270-
while (true)
271-
{
270+
while (true)
271+
{
272272
JToken token = await MakeJsonRequestAsync<JToken>(string.Format("/products/{0}/candles?start={1}&end={2}&granularity={3}", marketSymbol, ((DateTimeOffset)RangeStart).ToUnixTimeSeconds(), ((DateTimeOffset)RangeEnd).ToUnixTimeSeconds(), granularity));
273273
foreach (JToken candle in token["candles"]) candles.Add(this.ParseCandle(candle, marketSymbol, periodSeconds, "open", "high", "low", "close", "start", TimestampType.UnixSeconds, "volume"));
274274
if (RangeStart > startDate)
@@ -278,7 +278,7 @@ protected override async Task<IEnumerable<MarketCandle>> OnGetCandlesAsync(strin
278278
RangeEnd = RangeEnd.AddSeconds(-(periodSeconds * 300));
279279
}
280280
else break;
281-
}
281+
}
282282
return candles.Where(c => c.Timestamp >= startDate).OrderBy(c => c.Timestamp);
283283
}
284284

@@ -301,7 +301,7 @@ protected override async Task<Dictionary<string, decimal>> OnGetFeesAsync()
301301

302302
#region AccountSpecificEndpoints
303303

304-
// WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call.
304+
// WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call.
305305
protected override async Task<ExchangeDepositDetails> OnGetDepositAddressAsync(string symbol, bool forceRegenerate = false)
306306
{
307307
if (Accounts == null) await GetAmounts(true); // Populate Accounts Cache
@@ -323,13 +323,13 @@ protected override async Task<Dictionary<string, decimal>> OnGetAmountsAvailable
323323
return await GetAmounts(true);
324324
}
325325

326-
// WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call.
326+
// WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call.
327327
protected override async Task<IEnumerable<ExchangeTransaction>> OnGetWithdrawHistoryAsync(string currency)
328328
{
329329
return await GetTx(true, currency);
330330
}
331331

332-
// WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call.
332+
// WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call.
333333
protected override async Task<IEnumerable<ExchangeTransaction>> OnGetDepositHistoryAsync(string currency)
334334
{
335335
return await GetTx(false, currency);
@@ -344,7 +344,7 @@ protected override async Task<IEnumerable<ExchangeOrderResult>> OnGetOpenOrderDe
344344
string uri = string.IsNullOrEmpty(marketSymbol) ? "/orders/historical/batch?order_status=OPEN" : $"/orders/historical/batch?product_id={marketSymbol}&order_status=OPEN"; // Parameter order is critical
345345
JToken token = await MakeJsonRequestAsync<JToken>(uri);
346346
while(true)
347-
{
347+
{
348348
foreach (JToken order in token[ORDERS]) if (order[TYPE].ToStringInvariant().Equals(ADVFILL)) orders.Add(ParseOrder(order));
349349
if (string.IsNullOrEmpty(cursorNext)) break;
350350
token = await MakeJsonRequestAsync<JToken>(uri + "&cursor=" + cursorNext);
@@ -360,7 +360,7 @@ protected override async Task<IEnumerable<ExchangeOrderResult>> OnGetCompletedOr
360360
string uri = string.IsNullOrEmpty(marketSymbol) ? "/orders/historical/batch?order_status=FILLED" : $"/orders/historical/batch?product_id={marketSymbol}&order_status=OPEN"; // Parameter order is critical
361361
JToken token = await MakeJsonRequestAsync<JToken>(uri);
362362
while(true)
363-
{
363+
{
364364
foreach (JToken order in token[ORDERS]) orders.Add(ParseOrder(order));
365365
if (string.IsNullOrEmpty(cursorNext)) break;
366366
token = await MakeJsonRequestAsync<JToken>(uri + "&cursor=" + cursorNext);
@@ -403,17 +403,17 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
403403
{
404404
orderConfig.Add("limit_limit_gtd", new Dictionary<string, object>()
405405
{
406-
{"base_size", order.Amount.ToStringInvariant() },
406+
{"base_size", order.RoundAmount().ToStringInvariant() },
407407
{"limit_price", order.Price.ToStringInvariant() },
408-
{"end_time", order.ExtraParameters["gtd_timestamp"] },
408+
{"end_time", order.ExtraParameters["gtd_timestamp"] },
409409
{"post_only", order.ExtraParameters.TryGetValueOrDefault( "post_only", false) }
410410
});
411411
}
412412
else
413-
{
413+
{
414414
orderConfig.Add("limit_limit_gtc", new Dictionary<string, object>()
415415
{
416-
{"base_size", order.Amount.ToStringInvariant() },
416+
{"base_size", order.RoundAmount().ToStringInvariant() },
417417
{"limit_price", order.Price.ToStringInvariant() },
418418
{"post_only", order.ExtraParameters.TryGetValueOrDefault( "post_only", "false") }
419419
});
@@ -424,7 +424,7 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
424424
{
425425
orderConfig.Add("stop_limit_stop_limit_gtd", new Dictionary<string, object>()
426426
{
427-
{"base_size", order.Amount.ToStringInvariant() },
427+
{"base_size", order.RoundAmount().ToStringInvariant() },
428428
{"limit_price", order.Price.ToStringInvariant() },
429429
{"stop_price", order.StopPrice.ToStringInvariant() },
430430
{"end_time", order.ExtraParameters["gtd_timestamp"] },
@@ -434,15 +434,15 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
434434
{
435435
orderConfig.Add("stop_limit_stop_limit_gtc", new Dictionary<string, object>()
436436
{
437-
{"base_size", order.Amount.ToStringInvariant() },
437+
{"base_size", order.RoundAmount().ToStringInvariant() },
438438
{"limit_price", order.Price.ToStringInvariant() },
439439
{"stop_price", order.StopPrice.ToStringInvariant() },
440440
});
441441
}
442442
break;
443443
case OrderType.Market:
444-
if (order.IsBuy) orderConfig.Add("market_market_ioc", new Dictionary<string, object>() { { "quote_size", order.Amount.ToStringInvariant() }});
445-
else orderConfig.Add("market_market_ioc", new Dictionary<string, object>() { { "base_size", order.Amount.ToStringInvariant() }});
444+
if (order.IsBuy) orderConfig.Add("market_market_ioc", new Dictionary<string, object>() { { "quote_size", order.RoundAmount().ToStringInvariant() }});
445+
else orderConfig.Add("market_market_ioc", new Dictionary<string, object>() { { "base_size", order.RoundAmount().ToStringInvariant() }});
446446
break;
447447
}
448448

@@ -454,10 +454,22 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
454454
// The Post doesn't return with any status, just a new OrderId. To get the Order Details we have to reQuery.
455455
return await OnGetOrderDetailsAsync(result[ORDERID].ToStringInvariant());
456456
}
457-
catch (Exception ex) // All fails come back with an exception.
457+
catch (Exception ex) // All fails come back with an exception.
458458
{
459+
Logger.Error(ex, "Failed to place coinbase error");
459460
var token = JToken.Parse(ex.Message);
460-
return new ExchangeOrderResult(){ Result = ExchangeAPIOrderResult.Rejected, ClientOrderId = order.ClientOrderId, ResultCode = token["error_response"]["error"].ToStringInvariant() };
461+
return new ExchangeOrderResult(){
462+
Result = ExchangeAPIOrderResult.Rejected,
463+
IsBuy = payload["side"].ToStringInvariant().Equals(BUY),
464+
MarketSymbol = payload["product_id"].ToStringInvariant(),
465+
ClientOrderId = order.ClientOrderId,
466+
ResultCode = $"{token["error_response"]["error"].ToStringInvariant()} - {token["error_response"]["preview_failure_reason"].ToStringInvariant()}",
467+
AmountFilled = 0,
468+
Amount = order.RoundAmount(),
469+
AveragePrice = 0,
470+
Fees = 0,
471+
FeesCurrency = "USDT"
472+
};
461473
}
462474
}
463475

@@ -509,8 +521,8 @@ protected override Task<IWebSocket> OnGetDeltaOrderBookWebSocketAsync(Action<Exc
509521
if (askCount >= maxCount && bidCount >=maxCount) break;
510522
}
511523
callback?.Invoke(book);
512-
}
513-
return Task.CompletedTask;
524+
}
525+
return Task.CompletedTask;
514526
}, async (_socket) =>
515527
{
516528
string timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToStringInvariant();
@@ -551,7 +563,7 @@ protected override async Task<IWebSocket> OnGetTickersWebSocketAsync(Action<IRea
551563
BaseCurrency = split[0],
552564
QuoteCurrency = split[1],
553565
BaseCurrencyVolume = token["volume_24_h"].ConvertInvariant<decimal>(),
554-
Timestamp = timestamp
566+
Timestamp = timestamp
555567
}
556568
} ));
557569
}
@@ -630,7 +642,7 @@ private async Task<Dictionary<string, decimal>> GetAmounts(bool AvailableOnly)
630642
}
631643
if (string.IsNullOrEmpty(cursorNext)) break;
632644
token = await MakeJsonRequestAsync<JToken>("/accounts?starting_after=" + cursorNext);
633-
}
645+
}
634646
pagination = PaginationType.None;
635647
return amounts;
636648
}
@@ -643,12 +655,12 @@ private async Task<Dictionary<string, decimal>> GetAmounts(bool AvailableOnly)
643655
/// <returns></returns>
644656
private async Task<List<ExchangeTransaction>> GetTx(bool Withdrawals, string currency)
645657
{
646-
if (Accounts == null) await GetAmounts(true);
658+
if (Accounts == null) await GetAmounts(true);
647659
pagination = PaginationType.V2;
648660
List<ExchangeTransaction> transfers = new List<ExchangeTransaction>();
649661
JToken tokens = await MakeJsonRequestAsync<JToken>($"accounts/{Accounts[currency]}/transactions", BaseUrlV2);
650662
while(true)
651-
{
663+
{
652664
foreach (JToken token in tokens)
653665
{
654666
// A "send" to Coinbase is when someone "sent" you coin - or a receive to the rest of the world
@@ -658,7 +670,7 @@ private async Task<List<ExchangeTransaction>> GetTx(bool Withdrawals, string cur
658670
}
659671
if (string.IsNullOrEmpty(cursorNext)) break;
660672
tokens = await MakeJsonRequestAsync<JToken>($"accounts/{Accounts[currency]}/transactions?starting_after={cursorNext}", BaseUrlV2);
661-
}
673+
}
662674
pagination = PaginationType.None;
663675
return transfers;
664676
}
@@ -672,17 +684,17 @@ private ExchangeTransaction ParseTransaction(JToken token)
672684
{
673685
// The Coin Address/TxFee isn't available but can be retrieved using the Network Hash/BlockChainId
674686
return new ExchangeTransaction()
675-
{
687+
{
676688
PaymentId = token["id"].ToStringInvariant(), // Not sure how this is used elsewhere but here it is the Coinbase TransactionID
677689
BlockchainTxId = token["network"]["hash"].ToStringInvariant(),
678690
Currency = token[AMOUNT][CURRENCY].ToStringInvariant(),
679691
Amount = token[AMOUNT][AMOUNT].ConvertInvariant<decimal>(),
680692
Timestamp = token["created_at"].ToObject<DateTime>(),
681693
Status = token[STATUS].ToStringInvariant() == "completed" ? TransactionStatus.Complete : TransactionStatus.Unknown,
682694
Notes = token["description"].ToStringInvariant()
683-
// Address
684-
// AddressTag
685-
// TxFee
695+
// Address
696+
// AddressTag
697+
// TxFee
686698
};
687699
}
688700

0 commit comments

Comments
 (0)