diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs
index e494523c..41fafc0a 100644
--- a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs
+++ b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs
@@ -10,7 +10,6 @@ The above copyright notice and this permission notice shall be included in all c
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
-
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
@@ -20,59 +19,68 @@ The above copyright notice and this permission notice shall be included in all c
namespace ExchangeSharp
{
- ///
- /// Warning: This API now uses Coinbase Advanced Trade V2/V3.
- /// If you are using legacy API keys from previous Coinbase versions they must be upgraded to Advanced Trade on the Coinbase site.
- /// These keys must be set before using the Coinbase API (sorry).
- ///
+
+ ///
+ /// Warning: This API now uses Coinbase Advanced Trade V2/V3.
+ /// If you are using legacy API keys from previous Coinbase versions they must be upgraded to Advanced Trade on the Coinbase site.
+ /// These keys must be set before using the Coinbase API (sorry).
+ ///
public sealed class ExchangeCoinbaseAPI : ExchangeAPI
- {
- private const string ADVFILL = "advanced_trade_fill";
- private const string CURRENCY = "currency";
- private const string PRODUCTID = "product_id";
- private const string PRODUCTS = "products";
- private const string PRICEBOOK = "pricebook";
- private const string PRICEBOOKS = "pricebooks";
- private const string ASKS = "asks";
- private const string BIDS = "bids";
- private const string PRICE = "price";
- private const string AMOUNT = "amount";
- private const string VALUE = "value";
- private const string SIZE = "size";
- private const string CURSOR = "cursor";
-
-
- public override string BaseUrl { get; set; } = "https://api.coinbase.com/api/v3/brokerage";
- private readonly string BaseURLV2 = "https://api.coinbase.com/v2"; // For Wallet Support
- public override string BaseUrlWebSocket { get; set; } = "wss://advanced-trade-ws.coinbase.com";
+ {
+ private const string ADVFILL = "advanced_trade_fill";
+ private const string CURRENCY = "currency";
+ private const string PRODUCTID = "product_id";
+ private const string PRODUCTS = "products";
+ private const string PRICEBOOK = "pricebook";
+ private const string PRICEBOOKS = "pricebooks";
+ private const string ASKS = "asks";
+ private const string BIDS = "bids";
+ private const string PRICE = "price";
+ private const string AMOUNT = "amount";
+ private const string VALUE = "value";
+ private const string SIZE = "size";
+ private const string CURSOR = "cursor";
+ private const string TYPE = "type";
+ private const string SUBSCRIBE = "subscribe";
+ private const string MARKETTRADES = "market_trades";
+ private const string TICKER = "ticker";
+ private const string EVENTS = "events";
+ private const string LEVEL2 = "level2";
+ private const string PRICELEVEL = "price_level";
+ private const string SIDE = "side";
+ private const string BUY = "buy";
+
+ public override string BaseUrl { get; set; } = "https://api.coinbase.com/api/v3/brokerage";
+ private readonly string BaseURLV2 = "https://api.coinbase.com/v2"; // For Wallet Support
+ public override string BaseUrlWebSocket { get; set; } = "wss://advanced-trade-ws.coinbase.com";
- private enum PaginationType { None, V2, V3, V3Cursor}
- private PaginationType pagination = PaginationType.None;
- private string cursorNext;
-
- private Dictionary Accounts = null; // Cached Account IDs
-
- private ExchangeCoinbaseAPI()
- {
- MarketSymbolIsReversed = false;
- RequestContentType = "application/json";
- NonceStyle = NonceStyle.None;
- WebSocketOrderBookType = WebSocketOrderBookType.FullBookFirstThenDeltas;
- RateLimit = new RateGate(10, TimeSpan.FromSeconds(1));
- base.RequestMaker.RequestStateChanged = ProcessResponse;
- }
-
- ///
- /// This is used to capture Pagination instead of overriding the ProcessResponse
- /// because the Pagination info is no longer in the Headers and ProcessResponse does not return the required Content
- ///
- ///
- ///
- ///
- private void ProcessResponse(IAPIRequestMaker maker, RequestMakerState state, object response)
- {
- // We can bypass serialization if we already know the last call isn't paginated
- if (state == RequestMakerState.Finished && pagination != PaginationType.None)
+ private enum PaginationType { None, V2, V3, V3Cursor}
+ private PaginationType pagination = PaginationType.None;
+ private string cursorNext;
+
+ private Dictionary Accounts = null; // Cached Account IDs
+
+ private ExchangeCoinbaseAPI()
+ {
+ MarketSymbolIsReversed = false;
+ RequestContentType = "application/json";
+ NonceStyle = NonceStyle.None;
+ WebSocketOrderBookType = WebSocketOrderBookType.FullBookFirstThenDeltas;
+ RateLimit = new RateGate(30, TimeSpan.FromSeconds(1));
+ base.RequestMaker.RequestStateChanged = ProcessResponse;
+ }
+
+ ///
+ /// This is used to capture Pagination instead of overriding the ProcessResponse
+ /// because the Pagination info is no longer in the Headers and ProcessResponse does not return the required Content
+ ///
+ ///
+ ///
+ ///
+ private void ProcessResponse(IAPIRequestMaker maker, RequestMakerState state, object response)
+ {
+ // We can bypass serialization if we already know the last call isn't paginated
+ if (state == RequestMakerState.Finished && pagination != PaginationType.None)
{
cursorNext = null;
JToken token = JsonConvert.DeserializeObject((string)response);
@@ -84,486 +92,536 @@ private void ProcessResponse(IAPIRequestMaker maker, RequestMakerState state, ob
case PaginationType.V3Cursor: cursorNext = token[CURSOR]?.ToStringInvariant(); break; // Only used for V3 Fills - go figure.
}
}
- }
-
- #region BaseOverrides
-
- ///
- /// Overridden because we no longer need a nonce in the payload and passphrase is no longer used
- ///
- ///
- ///
- protected override bool CanMakeAuthenticatedRequest(IReadOnlyDictionary payload)
- {
- return (PrivateApiKey != null && PublicApiKey != null);
- }
-
- ///
- /// Sometimes the Fiat pairs are reported backwards, but Coinbase requires the fiat to be last of the pair
- /// Only three Fiat Currencies are supported
- ///
- ///
- ///
- public override Task GlobalMarketSymbolToExchangeMarketSymbolAsync(string marketSymbol)
- {
- if (marketSymbol.StartsWith("USD-") || marketSymbol.StartsWith("EUR-") || marketSymbol.StartsWith("GRP-"))
- {
- var split = marketSymbol.Split(GlobalMarketSymbolSeparator);
- return Task.FromResult(split[1] + GlobalMarketSymbolSeparator + split[0]);
- }
- else return Task.FromResult(marketSymbol);
- }
+ }
- protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload)
- {
- if (CanMakeAuthenticatedRequest(payload))
- {
- string timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToStringInvariant(); // If you're skittish about the local clock, you may retrieve the timestamp from the Coinbase Site
- string body = CryptoUtility.GetJsonForPayload(payload);
+ #region BaseOverrides
- // V2 wants PathAndQuery, V3 wants LocalPath for the sig
- string path = request.RequestUri.AbsoluteUri.StartsWith(BaseURLV2) ? request.RequestUri.PathAndQuery : request.RequestUri.LocalPath;
- string signature = CryptoUtility.SHA256Sign(timestamp + request.Method.ToUpperInvariant() + path + body, PrivateApiKey.ToUnsecureString());
+ ///
+ /// Overridden because we no longer need a nonce in the payload and passphrase is no longer used
+ ///
+ ///
+ ///
+ protected override bool CanMakeAuthenticatedRequest(IReadOnlyDictionary payload)
+ {
+ return (PrivateApiKey != null && PublicApiKey != null);
+ }
- request.AddHeader("CB-ACCESS-KEY", PublicApiKey.ToUnsecureString());
- request.AddHeader("CB-ACCESS-SIGN", signature);
- request.AddHeader("CB-ACCESS-TIMESTAMP", timestamp);
- if (request.Method == "POST") await CryptoUtility.WriteToRequestAsync(request, body);
- }
- }
+ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload)
+ {
+ if (CanMakeAuthenticatedRequest(payload))
+ {
+ string timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToStringInvariant(); // If you're skittish about the local clock, you may retrieve the timestamp from the Coinbase Site
+ string body = CryptoUtility.GetJsonForPayload(payload);
- #endregion
+ // V2 wants PathAndQuery, V3 wants LocalPath for the sig (I guess they wanted to shave a nano-second or two - silly)
+ string path = request.RequestUri.AbsoluteUri.StartsWith(BaseURLV2) ? request.RequestUri.PathAndQuery : request.RequestUri.LocalPath;
+ string signature = CryptoUtility.SHA256Sign(timestamp + request.Method.ToUpperInvariant() + path + body, PrivateApiKey.ToUnsecureString());
- #region GeneralProductEndpoints
+ request.AddHeader("CB-ACCESS-KEY", PublicApiKey.ToUnsecureString());
+ request.AddHeader("CB-ACCESS-SIGN", signature);
+ request.AddHeader("CB-ACCESS-TIMESTAMP", timestamp);
+ if (request.Method == "POST") await CryptoUtility.WriteToRequestAsync(request, body);
+ }
+ }
- protected internal override async Task> OnGetMarketSymbolsMetadataAsync()
- {
- var markets = new List();
- JToken products = await MakeJsonRequestAsync("/products");
- foreach (JToken product in products[PRODUCTS])
- {
- markets.Add(new ExchangeMarket()
- {
- MarketSymbol = product[PRODUCTID].ToStringUpperInvariant(),
- BaseCurrency = product["base_currency_id"].ToStringUpperInvariant(),
- QuoteCurrency = product["quote_currency_id"].ToStringUpperInvariant(),
- IsActive = string.Equals(product["status"].ToStringInvariant(), "online", StringComparison.OrdinalIgnoreCase),
- MinTradeSize = product["base_min_size"].ConvertInvariant(),
- MaxTradeSize = product["base_max_size"].ConvertInvariant(),
- PriceStepSize = product["quote_increment"].ConvertInvariant()
- });
- }
- return markets.OrderBy(market => market.MarketSymbol); // Ordered for Convenience
- }
+ ///
+ /// Sometimes the Fiat pairs are reported backwards, but Coinbase requires the fiat to be last of the pair
+ /// Only three Fiat Currencies are supported
+ ///
+ ///
+ ///
+ public override Task GlobalMarketSymbolToExchangeMarketSymbolAsync(string marketSymbol)
+ {
+ if (marketSymbol.StartsWith("USD-") || marketSymbol.StartsWith("EUR-") || marketSymbol.StartsWith("GRP-"))
+ {
+ var split = marketSymbol.Split(GlobalMarketSymbolSeparator);
+ marketSymbol = split[1] + GlobalMarketSymbolSeparator + split[0];
+ }
+ return Task.FromResult(marketSymbol);
+ }
- protected override async Task> OnGetMarketSymbolsAsync()
- {
- return (await GetMarketSymbolsMetadataAsync()).Select(market => market.MarketSymbol);
- }
+ #endregion
- protected override async Task> OnGetCurrenciesAsync()
- {
- var currencies = new Dictionary(); // We could order the return (like Market Symbols are) if we populate as a list then sort and select into a dictionary before return, but is it worth the overhead?
+ #region GeneralProductEndpoints
- // We don't have a currencies endpoint, but we can derive the currencies by splitting the products (includes fiat - filter if you wish)
- JToken products = await MakeJsonRequestAsync("/products");
- foreach (JToken product in products[PRODUCTS])
- {
- var split = product[PRODUCTID].ToString().Split(GlobalMarketSymbolSeparator);
- if (!currencies.ContainsKey(split[0]))
- {
- var currency = new ExchangeCurrency
- {
- Name = split[0],
- FullName = product["base_name"].ToStringInvariant(),
- DepositEnabled = true,
- WithdrawalEnabled = true
- };
- currencies[currency.Name] = currency;
- }
- if (!currencies.ContainsKey(split[1]))
- {
- var currency = new ExchangeCurrency
- {
- Name = split[1],
- FullName = product["quote_name"].ToStringInvariant(),
- DepositEnabled = true,
- WithdrawalEnabled = true
- };
- currencies[currency.Name] = currency;
- }
- }
- return currencies;
- }
-
- protected override async Task>> OnGetTickersAsync()
- {
- var tickers = new List>();
- JToken books = await MakeJsonRequestAsync("/best_bid_ask");
- var Timestamp = CryptoUtility.ParseTimestamp(books["time"], TimestampType.Iso8601UTC);
- foreach (JToken book in books[PRICEBOOKS])
- {
- var split = book[PRODUCTID].ToString().Split(GlobalMarketSymbolSeparator);
- // This endpoint does not provide a last or open for the ExchangeTicker. We might get this from the sockets, but this call is extremely fast?
- tickers.Add(new KeyValuePair(book[PRODUCTID].ToString(), new ExchangeTicker()
- {
- MarketSymbol = book[PRODUCTID].ToString(),
- Ask = book[ASKS][0][PRICE].ConvertInvariant(),
- Bid = book[BIDS][0][PRICE].ConvertInvariant(),
- Volume = new ExchangeVolume()
- {
- BaseCurrency = split[0],
- BaseCurrencyVolume = book[BIDS][0][SIZE].ConvertInvariant(),
- QuoteCurrency = split[1],
- QuoteCurrencyVolume = book[ASKS][0][SIZE].ConvertInvariant(),
- Timestamp = Timestamp
- }
- }));
- }
- return tickers;
- }
-
- protected override async Task OnGetTickerAsync(string marketSymbol)
- {
- // Again, me might also get this from the sockets, but this seems preferable for now.
- JToken ticker = await MakeJsonRequestAsync("/best_bid_ask?product_ids=" + marketSymbol.ToUpperInvariant());
- JToken book = ticker[PRICEBOOKS][0];
- var split = book[PRODUCTID].ToString().Split(GlobalMarketSymbolSeparator);
- return new ExchangeTicker()
- {
- MarketSymbol = book[PRODUCTID].ToString(),
- Ask = book[ASKS][0][PRICE].ConvertInvariant(),
- Bid = book[BIDS][0][PRICE].ConvertInvariant(),
- Volume = new ExchangeVolume()
- {
- BaseCurrency = split[0],
- BaseCurrencyVolume = book[BIDS][0][SIZE].ConvertInvariant(),
- QuoteCurrency = split[1],
- QuoteCurrencyVolume = book[ASKS][0][SIZE].ConvertInvariant(),
- Timestamp = DateTime.UtcNow
- }
- };
- }
-
- protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 50)
- {
- JToken token = await MakeJsonRequestAsync("/product_book?product_id=" + marketSymbol.ToUpperInvariant() + "&limit=" + maxCount);
- ExchangeOrderBook orderBook = new ExchangeOrderBook();
- foreach(JToken bid in token[PRICEBOOK][BIDS]) orderBook.Bids.Add(bid[PRICE].ConvertInvariant(), new ExchangeOrderPrice(){ Price = bid[PRICE].ConvertInvariant(), Amount = bid[SIZE].ConvertInvariant() });
- foreach(JToken ask in token[PRICEBOOK][ASKS]) orderBook.Asks.Add(ask[PRICE].ConvertInvariant(), new ExchangeOrderPrice(){ Price = ask[PRICE].ConvertInvariant(), Amount = ask[SIZE].ConvertInvariant() });
- return orderBook;
- }
-
- protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = 100)
- {
- // Limit is required but maxed at 100 with no pagination available. Check Sockets?
- limit = (limit == null || limit < 1 || limit > 100) ? 100 : (int)limit;
- JToken trades = await MakeJsonRequestAsync("/products/" + marketSymbol.ToUpperInvariant() + "/ticker?limit=" + limit);
- List tradeList = new List();
- foreach (JToken trade in trades["trades"]) tradeList.Add(trade.ParseTrade(SIZE, PRICE, "side", "time", TimestampType.Iso8601UTC, "trade_id"));
- return tradeList;
- }
-
- protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null)
- {
- // There is no Historical Trades endpoint. The best we can do is get the last 100 trades and filter.
- // Check for this data on the sockets?
- var trades = await OnGetRecentTradesAsync(marketSymbol.ToUpperInvariant());
-
- if (startDate != null) trades = trades.Where(t => t.Timestamp >= startDate);
- if (endDate != null) trades = trades.Where(t => t.Timestamp <= endDate);;
- if (limit != null) trades = trades.Take((int)limit);
-
- callback(trades);
- }
-
- protected override async Task> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null)
- {
- if (endDate == null) endDate = CryptoUtility.UtcNow;
-
- string granularity = "UNKNOWN_GRANULARITY";
- if (periodSeconds <= 60) { granularity = "ONE_MINUTE"; periodSeconds = 60; }
- else if (periodSeconds <= 300) { granularity = "FIVE_MINUTE"; periodSeconds = 300; }
- else if (periodSeconds <= 900) { granularity = "FIFTEEN_MINUTE"; periodSeconds = 900; }
- else if (periodSeconds <= 1800) { granularity = "THIRTY_MINUTE"; periodSeconds = 1800; }
- else if (periodSeconds <= 3600) { granularity = "ONE_HOUR"; periodSeconds = 3600; }
- else if (periodSeconds <= 21600) { granularity = "SIX_HOUR"; periodSeconds = 21600; }
- else { granularity = "ONE_DAY"; periodSeconds = 86400; }
-
- // Returned Candle count is restricted to 300 - and they don't paginate this call
- // We're going to keep retrieving candles 300 at a time until we get our date range for the granularity
- if (startDate == null) startDate = CryptoUtility.UtcNow.AddMinutes(-(periodSeconds * 300));
- if (startDate >= endDate) throw new APIException("Invalid Date Range");
- DateTime RangeStart = (DateTime)startDate, RangeEnd = (DateTime)endDate;
- if ((RangeEnd - RangeStart).TotalSeconds / periodSeconds > 300) RangeStart = RangeEnd.AddSeconds(-(periodSeconds * 300));
-
- List candles = new List();
- while (true)
- {
- JToken token = await MakeJsonRequestAsync(string.Format("/products/{0}/candles?start={1}&end={2}&granularity={3}", marketSymbol.ToUpperInvariant(), ((DateTimeOffset)RangeStart).ToUnixTimeSeconds(), ((DateTimeOffset)RangeEnd).ToUnixTimeSeconds(), granularity));
- foreach (JToken candle in token["candles"]) candles.Add(this.ParseCandle(candle, marketSymbol, periodSeconds, "open", "high", "low", "close", "start", TimestampType.UnixSeconds, "volume"));
- if (RangeStart > startDate)
- {
- // For simplicity, we'll go back 300 each iteration and sort/filter date range before return
- RangeStart = RangeStart.AddSeconds(-(periodSeconds * 300));
- RangeEnd = RangeEnd.AddSeconds(-(periodSeconds * 300));
- }
- else break;
- }
- return candles.Where(c => c.Timestamp >= startDate).OrderBy(c => c.Timestamp);
- }
-
-
- protected override async Task> OnGetFeesAsync()
- {
- var symbols = await OnGetMarketSymbolsAsync();
- JToken token = await this.MakeJsonRequestAsync("/transaction_summary");
- Dictionary fees = new Dictionary();
-
- // We can chose between maker and taker fee, but currently ExchangeSharp only supports 1 fee rate per symbol.
- // Here, we choose taker fee, which is usually higher
- decimal makerRate = token["fee_tier"]["taker_fee_rate"].Value(); //percentage between 0 and 1
-
- return symbols.Select(symbol => new KeyValuePair(symbol, makerRate)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
- }
-
-
- #endregion
-
- #region AccountSpecificEndpoints
-
- // WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call.
- protected override async Task OnGetDepositAddressAsync(string symbol, bool forceRegenerate = false)
- {
- if (Accounts == null) await GetAmounts(true); // Populate Accounts Cache
- if (Accounts.ContainsKey(symbol))
- {
- JToken accountWalletAddress = await this.MakeJsonRequestAsync($"/accounts/{Accounts[symbol]}/addresses", BaseURLV2);
- return new ExchangeDepositDetails { Address = accountWalletAddress[0]["address"].ToStringInvariant(), Currency = symbol }; // We only support a single Wallet/Address (Coinbase is the only Exchange that has multiple)
- }
- throw new APIException($"Address not found for {symbol}");
- }
-
- protected override async Task> OnGetAmountsAsync()
- {
- return await GetAmounts(false);
- }
-
- protected override async Task> OnGetAmountsAvailableToTradeAsync()
- {
- return await GetAmounts(true);
- }
-
- // WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call.
- protected override async Task> OnGetWithdrawHistoryAsync(string currency)
- {
- return await GetTx(true, currency);
- }
-
- // WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call.
- protected override async Task> OnGetDepositHistoryAsync(string currency)
- {
- return await GetTx(false, currency);
- }
-
- ///
- /// WARNING: Only Advanced Trade Open Orders are supported.
- ///
- ///
- ///
- protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null)
- {
- List orders = new List();
- // Max return count is 1000 with no pagination available
- JToken array = await MakeJsonRequestAsync("/orders/historical/batch?order_status=OPEN" + marketSymbol == null || marketSymbol == string.Empty ? string.Empty : "&product_id=" + marketSymbol );
- foreach (JToken order in array) if (order["type"].ToStringInvariant().Equals(ADVFILL)) orders.Add(ParseOrder(order));
- return orders;
- }
-
- ///
- /// WARNING: Only Advanced Trade Completed Orders are supported.
- ///
- ///
- ///
- ///
- protected override async Task> OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null)
- {
- // Legacy Orders may be retrieved using V2 (not implemented here - see GetTx in code below)
- List orders = new List();
- pagination = PaginationType.V3Cursor;
- string startURL = "/orders/historical/fills";
-
- if (!string.IsNullOrEmpty(marketSymbol)) startURL += "?product_id=" + marketSymbol.ToStringUpperInvariant();
- if (afterDate != null) startURL += marketSymbol == null ? "?" : "&" + "start_sequence_timestamp=" + ((DateTimeOffset)afterDate).ToUnixTimeSeconds();
- JToken token = await MakeJsonRequestAsync(startURL);
- startURL += marketSymbol == null && afterDate == null ? "?" : "&" + "cursor=";
- while(true)
- {
- foreach (JToken fill in token["fills"])
- {
- orders.Add(new ExchangeOrderResult()
- {
- MarketSymbol = fill[PRODUCTID].ToStringInvariant(),
- TradeId = fill["trade_id"].ToStringInvariant(),
- OrderId = fill["order_id"].ToStringInvariant(),
- OrderDate = fill["trade_time"].ToDateTimeInvariant(),
- IsBuy = fill["side"].ToStringInvariant() == "buy",
- Amount = fill[SIZE].ConvertInvariant(),
- AmountFilled = fill[SIZE].ConvertInvariant(),
- Price = fill[PRICE].ConvertInvariant(),
- Fees = fill["commission"].ConvertInvariant(),
- AveragePrice = fill[PRICE].ConvertInvariant()
- });
- }
- if (string.IsNullOrEmpty(cursorNext)) break;
- token = await MakeJsonRequestAsync(startURL + cursorNext);
- }
- pagination = PaginationType.None;
- return orders;
- }
-
- protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false)
- {
- JToken obj = await MakeJsonRequestAsync("/orders/historical/" + orderId);
- return ParseOrder(obj);
- }
+ protected internal override async Task> OnGetMarketSymbolsMetadataAsync()
+ {
+ var markets = new List();
+ JToken products = await MakeJsonRequestAsync("/products");
+ foreach (JToken product in products[PRODUCTS])
+ {
+ markets.Add(new ExchangeMarket
+ {
+ MarketSymbol = product[PRODUCTID].ToStringUpperInvariant(),
+ BaseCurrency = product["base_currency_id"].ToStringUpperInvariant(),
+ QuoteCurrency = product["quote_currency_id"].ToStringUpperInvariant(),
+ IsActive = string.Equals(product["status"].ToStringInvariant(), "online", StringComparison.OrdinalIgnoreCase),
+ MinTradeSize = product["base_min_size"].ConvertInvariant(),
+ MaxTradeSize = product["base_max_size"].ConvertInvariant(),
+ PriceStepSize = product["quote_increment"].ConvertInvariant()
+ });
+ }
+ return markets.OrderBy(market => market.MarketSymbol); // Ordered for Convenience
+ }
- protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false)
- {
- Dictionary payload = new Dictionary() {{ "order_ids", new [] { orderId } } };
- await MakeJsonRequestAsync("/orders/batch_cancel", payload: payload, requestMethod: "POST");
- }
+ protected override async Task> OnGetMarketSymbolsAsync()
+ {
+ return (await GetMarketSymbolsMetadataAsync()).Select(market => market.MarketSymbol);
+ }
- ///
- /// This supports two Entries in the Order ExtraParameters:
- /// "post_only" : true/false (defaults to false if does not exist)
- /// "gtd_timestamp : datetime (determines GTD order type if exists, otherwise GTC
- ///
- ///
- ///
- protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order)
- {
- Dictionary configuration = new Dictionary();
- switch (order.OrderType)
+ protected override async Task> OnGetCurrenciesAsync()
{
- case OrderType.Limit:
- if (order.ExtraParameters.ContainsKey("gtd_timestamp"))
+ var currencies = new Dictionary();
+
+ // We don't have a currencies endpoint, but we can derive the currencies by splitting the products (includes fiat - filter if you wish)
+ JToken products = await MakeJsonRequestAsync("/products");
+ foreach (JToken product in products[PRODUCTS])
+ {
+ var split = product[PRODUCTID].ToString().Split(GlobalMarketSymbolSeparator);
+ if (!currencies.ContainsKey(split[0]))
{
- configuration.Add("limit_limit_gtd", new Dictionary()
+ var currency = new ExchangeCurrency
{
- {"base_size", order.Amount.ToStringInvariant() },
- {"limit_price", order.Price.ToStringInvariant() },
- {"end_time", ((DateTimeOffset)order.ExtraParameters["gtd_timestamp"].ToDateTimeInvariant()).ToUnixTimeSeconds().ToString() }, // This is a bit convoluted? Is this the right format?
- {"post_only", order.ExtraParameters.TryGetValueOrDefault( "post_only", "false") }
- });
+ Name = split[0],
+ FullName = product["base_name"].ToStringInvariant(),
+ DepositEnabled = true,
+ WithdrawalEnabled = true
+ };
+ currencies[currency.Name] = currency;
}
- else
- {
- configuration.Add("limit_limit_gtc", new Dictionary()
+ if (!currencies.ContainsKey(split[1]))
+ {
+ var currency = new ExchangeCurrency
{
- {"base_size", order.Amount.ToStringInvariant() },
- {"limit_price", order.Price.ToStringInvariant() },
- {"post_only", order.ExtraParameters.TryGetValueOrDefault( "post_only", "false") }
- });
+ Name = split[1],
+ FullName = product["quote_name"].ToStringInvariant(),
+ DepositEnabled = true,
+ WithdrawalEnabled = true
+ };
+ currencies[currency.Name] = currency;
}
- break;
- case OrderType.Stop:
- if (order.ExtraParameters.ContainsKey("gtd_timestamp"))
+ }
+ return currencies;
+ }
+
+ protected override async Task>> OnGetTickersAsync()
+ {
+ var tickers = new List>();
+ JToken books = await MakeJsonRequestAsync("/best_bid_ask");
+ var Timestamp = CryptoUtility.ParseTimestamp(books["time"], TimestampType.Iso8601UTC);
+ foreach (JToken book in books[PRICEBOOKS])
+ {
+ var split = book[PRODUCTID].ToString().Split(GlobalMarketSymbolSeparator);
+ // This endpoint does not provide a last or open for the ExchangeTicker
+ tickers.Add(new KeyValuePair(book[PRODUCTID].ToString(), new ExchangeTicker()
{
- configuration.Add("stop_limit_stop_limit_gtd", new Dictionary()
+ MarketSymbol = book[PRODUCTID].ToString(),
+ Ask = book[ASKS][0][PRICE].ConvertInvariant(),
+ Bid = book[BIDS][0][PRICE].ConvertInvariant(),
+ Volume = new ExchangeVolume()
{
- {"base_size", order.Amount.ToStringInvariant() },
- {"limit_price", order.Price.ToStringInvariant() },
- {"stop_price", order.StopPrice.ToStringInvariant() },
- {"end_time", ((DateTimeOffset)order.ExtraParameters["gtd_timestamp"].ToDateTimeInvariant()).ToUnixTimeSeconds().ToString() }, // This is a bit convoluted? Is this the right format?
- {"post_only", order.ExtraParameters.TryGetValueOrDefault( "post_only", "false") }
- //{"stop_direction", "UNKNOWN_STOP_DIRECTION" } // set stop direction?
- });
+ BaseCurrency = split[0],
+ BaseCurrencyVolume = book[BIDS][0][SIZE].ConvertInvariant(),
+ QuoteCurrency = split[1],
+ QuoteCurrencyVolume = book[ASKS][0][SIZE].ConvertInvariant(),
+ Timestamp = Timestamp
+ }
+ }));
+ }
+ return tickers;
+ }
+
+ protected override async Task OnGetTickerAsync(string marketSymbol)
+ {
+ JToken ticker = await MakeJsonRequestAsync("/best_bid_ask?product_ids=" + marketSymbol.ToUpperInvariant());
+ JToken book = ticker[PRICEBOOKS][0];
+ var split = book[PRODUCTID].ToString().Split(GlobalMarketSymbolSeparator);
+ return new ExchangeTicker()
+ {
+ MarketSymbol = book[PRODUCTID].ToString(),
+ Ask = book[ASKS][0][PRICE].ConvertInvariant(),
+ Bid = book[BIDS][0][PRICE].ConvertInvariant(),
+ Volume = new ExchangeVolume()
+ {
+ BaseCurrency = split[0],
+ BaseCurrencyVolume = book[BIDS][0][SIZE].ConvertInvariant(),
+ QuoteCurrency = split[1],
+ QuoteCurrencyVolume = book[ASKS][0][SIZE].ConvertInvariant(),
+ Timestamp = DateTime.UtcNow
}
- else
+ };
+ }
+
+ protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 50)
+ {
+ JToken token = await MakeJsonRequestAsync("/product_book?product_id=" + marketSymbol.ToUpperInvariant() + "&limit=" + maxCount);
+ ExchangeOrderBook orderBook = new ExchangeOrderBook();
+ foreach(JToken bid in token[PRICEBOOK][BIDS]) orderBook.Bids.Add(bid[PRICE].ConvertInvariant(), new ExchangeOrderPrice(){ Price = bid[PRICE].ConvertInvariant(), Amount = bid[SIZE].ConvertInvariant() });
+ foreach(JToken ask in token[PRICEBOOK][ASKS]) orderBook.Asks.Add(ask[PRICE].ConvertInvariant(), new ExchangeOrderPrice(){ Price = ask[PRICE].ConvertInvariant(), Amount = ask[SIZE].ConvertInvariant() });
+ return orderBook;
+ }
+
+ protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = 100)
+ {
+ // Limit is required but maxed at 100 with no pagination available
+ limit = (limit == null || limit < 1 || limit > 100) ? 100 : (int)limit;
+ JToken trades = await MakeJsonRequestAsync("/products/" + marketSymbol.ToUpperInvariant() + "/ticker?limit=" + limit);
+ List tradeList = new List();
+ foreach (JToken trade in trades["trades"]) tradeList.Add(trade.ParseTrade(SIZE, PRICE, SIDE, "time", TimestampType.Iso8601UTC, "trade_id"));
+ return tradeList;
+ }
+
+ protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null)
+ {
+ // There is no Historical Trades endpoint. The best we can do is get the last 100 trades and filter.
+ // Check for this data on the sockets?
+ var trades = await OnGetRecentTradesAsync(marketSymbol.ToUpperInvariant());
+
+ if (startDate != null) trades = trades.Where(t => t.Timestamp >= startDate);
+ if (endDate != null) trades = trades.Where(t => t.Timestamp <= endDate);;
+ if (limit != null) trades = trades.Take((int)limit);
+
+ callback(trades);
+ }
+
+ protected override async Task> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null)
+ {
+ if (endDate == null) endDate = CryptoUtility.UtcNow;
+
+ string granularity = "UNKNOWN_GRANULARITY";
+ if (periodSeconds <= 60) { granularity = "ONE_MINUTE"; periodSeconds = 60; }
+ else if (periodSeconds <= 300) { granularity = "FIVE_MINUTE"; periodSeconds = 300; }
+ else if (periodSeconds <= 900) { granularity = "FIFTEEN_MINUTE"; periodSeconds = 900; }
+ else if (periodSeconds <= 1800) { granularity = "THIRTY_MINUTE"; periodSeconds = 1800; }
+ else if (periodSeconds <= 3600) { granularity = "ONE_HOUR"; periodSeconds = 3600; }
+ else if (periodSeconds <= 21600) { granularity = "SIX_HOUR"; periodSeconds = 21600; }
+ else { granularity = "ONE_DAY"; periodSeconds = 86400; }
+
+ // Returned Candle count is restricted to 300 and they don't paginate this call
+ // We're going to keep retrieving candles 300 at a time until we get our date range for the granularity
+ if (startDate == null) startDate = CryptoUtility.UtcNow.AddMinutes(-(periodSeconds * 300));
+ if (startDate >= endDate) throw new APIException("Invalid Date Range");
+ DateTime RangeStart = (DateTime)startDate, RangeEnd = (DateTime)endDate;
+ if ((RangeEnd - RangeStart).TotalSeconds / periodSeconds > 300) RangeStart = RangeEnd.AddSeconds(-(periodSeconds * 300));
+
+ List candles = new List();
+ while (true)
+ {
+ JToken token = await MakeJsonRequestAsync(string.Format("/products/{0}/candles?start={1}&end={2}&granularity={3}", marketSymbol.ToUpperInvariant(), ((DateTimeOffset)RangeStart).ToUnixTimeSeconds(), ((DateTimeOffset)RangeEnd).ToUnixTimeSeconds(), granularity));
+ foreach (JToken candle in token["candles"]) candles.Add(this.ParseCandle(candle, marketSymbol, periodSeconds, "open", "high", "low", "close", "start", TimestampType.UnixSeconds, "volume"));
+ if (RangeStart > startDate)
{
- configuration.Add("stop_limit_stop_limit_gtc", new Dictionary()
+ // For simplicity, we'll go back 300 each iteration and sort/filter date range before return
+ RangeStart = RangeStart.AddSeconds(-(periodSeconds * 300));
+ RangeEnd = RangeEnd.AddSeconds(-(periodSeconds * 300));
+ }
+ else break;
+ }
+ return candles.Where(c => c.Timestamp >= startDate).OrderBy(c => c.Timestamp);
+ }
+
+
+ protected override async Task> OnGetFeesAsync()
+ {
+ var symbols = await OnGetMarketSymbolsAsync();
+ JToken token = await this.MakeJsonRequestAsync("/transaction_summary");
+ Dictionary fees = new Dictionary();
+
+ // We can chose between maker and taker fee, but currently ExchangeSharp only supports 1 fee rate per market symbol.
+ // Here, we choose taker fee, which is usually higher
+ decimal makerRate = token["fee_tier"]["taker_fee_rate"].Value(); //percentage between 0 and 1
+
+ return symbols.Select(symbol => new KeyValuePair(symbol, makerRate)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
+ }
+
+ #endregion
+
+ #region AccountSpecificEndpoints
+
+ // WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call.
+ protected override async Task OnGetDepositAddressAsync(string symbol, bool forceRegenerate = false)
+ {
+ if (Accounts == null) await GetAmounts(true); // Populate Accounts Cache
+ if (Accounts.ContainsKey(symbol))
+ {
+ JToken accountWalletAddress = await this.MakeJsonRequestAsync($"/accounts/{Accounts[symbol]}/addresses", BaseURLV2);
+ return new ExchangeDepositDetails { Address = accountWalletAddress[0]["address"].ToStringInvariant(), Currency = symbol }; // We only support a single Wallet/Address (Coinbase is the only Exchange that has multiple)
+ }
+ throw new APIException($"Address not found for {symbol}");
+ }
+
+ protected override async Task> OnGetAmountsAsync()
+ {
+ return await GetAmounts(false);
+ }
+
+ protected override async Task> OnGetAmountsAvailableToTradeAsync()
+ {
+ return await GetAmounts(true);
+ }
+
+ // WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call.
+ protected override async Task> OnGetWithdrawHistoryAsync(string currency)
+ {
+ return await GetTx(true, currency);
+ }
+
+ // WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call.
+ protected override async Task> OnGetDepositHistoryAsync(string currency)
+ {
+ return await GetTx(false, currency);
+ }
+
+ ///
+ /// WARNING: Only Advanced Trade Open Orders are supported.
+ ///
+ ///
+ ///
+ protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null)
+ {
+ List orders = new List();
+ // Max return count is 1000 with no pagination available
+ JToken array = await MakeJsonRequestAsync("/orders/historical/batch?order_status=OPEN" + marketSymbol == null || marketSymbol == string.Empty ? string.Empty : "&product_id=" + marketSymbol );
+ foreach (JToken order in array) if (order[TYPE].ToStringInvariant().Equals(ADVFILL)) orders.Add(ParseOrder(order));
+ return orders;
+ }
+
+ ///
+ /// WARNING: Only Advanced Trade Completed Orders are supported.
+ ///
+ ///
+ ///
+ ///
+ protected override async Task> OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null)
+ {
+ // Legacy Orders may be retrieved using V2 (not implemented here - see GetTx in code below)
+ List orders = new List();
+ pagination = PaginationType.V3Cursor;
+ string startURL = "/orders/historical/fills";
+
+ if (!string.IsNullOrEmpty(marketSymbol)) startURL += "?product_id=" + marketSymbol;
+ if (afterDate != null) startURL += marketSymbol == null ? "?" : "&" + "start_sequence_timestamp=" + ((DateTimeOffset)afterDate).ToUnixTimeSeconds();
+ JToken token = await MakeJsonRequestAsync(startURL);
+ startURL += marketSymbol == null && afterDate == null ? "?" : "&" + "cursor=";
+ while(true)
+ {
+ foreach (JToken fill in token["fills"])
+ {
+ orders.Add(new ExchangeOrderResult()
{
- {"base_size", order.Amount.ToStringInvariant() },
- {"limit_price", order.Price.ToStringInvariant() },
- {"stop_price", order.StopPrice.ToStringInvariant() },
- {"post_only", order.ExtraParameters.TryGetValueOrDefault( "post_only", "false") }
- //{"stop_direction", "UNKNOWN_STOP_DIRECTION" } // set stop direction?
+ MarketSymbol = fill[PRODUCTID].ToStringInvariant(),
+ TradeId = fill["trade_id"].ToStringInvariant(),
+ OrderId = fill["order_id"].ToStringInvariant(),
+ OrderDate = fill["trade_time"].ToDateTimeInvariant(),
+ IsBuy = fill[SIDE].ToStringInvariant() == BUY,
+ Amount = fill[SIZE].ConvertInvariant(),
+ AmountFilled = fill[SIZE].ConvertInvariant(),
+ Price = fill[PRICE].ConvertInvariant(),
+ Fees = fill["commission"].ConvertInvariant(),
+ AveragePrice = fill[PRICE].ConvertInvariant()
});
}
+ if (string.IsNullOrEmpty(cursorNext)) break;
+ token = await MakeJsonRequestAsync(startURL + cursorNext);
+ }
+ pagination = PaginationType.None;
+ return orders;
+ }
+
+ protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false)
+ {
+ JToken obj = await MakeJsonRequestAsync("/orders/historical/" + orderId);
+ return ParseOrder(obj);
+ }
+
+ ///
+ /// This supports two Entries in the Order ExtraParameters:
+ /// "post_only" : true/false (defaults to false if does not exist)
+ /// "gtd_timestamp : datetime (determines GTD order type if exists, otherwise GTC
+ ///
+ ///
+ ///
+ protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order)
+ {
+ Dictionary configuration = new Dictionary();
+ switch (order.OrderType)
+ {
+ case OrderType.Limit:
+ if (order.ExtraParameters.ContainsKey("gtd_timestamp"))
+ {
+ configuration.Add("limit_limit_gtd", new Dictionary()
+ {
+ {"base_size", order.Amount.ToStringInvariant() },
+ {"limit_price", order.Price.ToStringInvariant() },
+ {"end_time", ((DateTimeOffset)order.ExtraParameters["gtd_timestamp"].ToDateTimeInvariant()).ToUnixTimeSeconds().ToString() }, // This is a bit convoluted? Is this the right format?
+ {"post_only", order.ExtraParameters.TryGetValueOrDefault( "post_only", "false") }
+ });
+ }
+ else
+ {
+ configuration.Add("limit_limit_gtc", new Dictionary()
+ {
+ {"base_size", order.Amount.ToStringInvariant() },
+ {"limit_price", order.Price.ToStringInvariant() },
+ {"post_only", order.ExtraParameters.TryGetValueOrDefault( "post_only", "false") }
+ });
+ }
break;
- case OrderType.Market:
- configuration.Add("market_market_ioc", new Dictionary()
- {
- {"base_size", order.Amount.ToStringInvariant() }
- });
+ case OrderType.Stop:
+ if (order.ExtraParameters.ContainsKey("gtd_timestamp"))
+ {
+ configuration.Add("stop_limit_stop_limit_gtd", new Dictionary()
+ {
+ {"base_size", order.Amount.ToStringInvariant() },
+ {"limit_price", order.Price.ToStringInvariant() },
+ {"stop_price", order.StopPrice.ToStringInvariant() },
+ {"end_time", ((DateTimeOffset)order.ExtraParameters["gtd_timestamp"].ToDateTimeInvariant()).ToUnixTimeSeconds().ToString() }, // This is a bit convoluted? Is this the right format?
+ {"post_only", order.ExtraParameters.TryGetValueOrDefault( "post_only", "false") }
+ //{"stop_direction", "UNKNOWN_STOP_DIRECTION" } // set stop direction?
+ });
+ }
+ else
+ {
+ configuration.Add("stop_limit_stop_limit_gtc", new Dictionary()
+ {
+ {"base_size", order.Amount.ToStringInvariant() },
+ {"limit_price", order.Price.ToStringInvariant() },
+ {"stop_price", order.StopPrice.ToStringInvariant() },
+ {"post_only", order.ExtraParameters.TryGetValueOrDefault( "post_only", "false") }
+ //{"stop_direction", "UNKNOWN_STOP_DIRECTION" } // set stop direction?
+ });
+ }
break;
- }
+ case OrderType.Market:
+ configuration.Add("market_market_ioc", new Dictionary()
+ {
+ {"base_size", order.Amount.ToStringInvariant() }
+ });
+ break;
+ }
- Dictionary payload = new Dictionary { { "order_configuration", configuration} };
- string side = order.IsBuy ? "buy" : "sell";
- JToken result = await MakeJsonRequestAsync($"/orders?product_id={order.MarketSymbol.ToUpperInvariant()}&side={side}", payload: payload, requestMethod: "POST");
+ Dictionary payload = new Dictionary
+ {
+ { "order_configuration", configuration}
+ };
- // We don't have the proper return type for the POST - will probably require a separate parsing function and return Success/Fail
- return ParseOrder(result);
- }
- protected override Task OnWithdrawAsync(ExchangeWithdrawalRequest withdrawalRequest)
- {
- return base.OnWithdrawAsync(withdrawalRequest);
- }
+ string side = order.IsBuy ? BUY : "sell";
+ JToken result = await MakeJsonRequestAsync($"/orders?product_id={order.MarketSymbol.ToUpperInvariant()}&side={side}", payload: payload, requestMethod: "POST");
+ // We don't have the proper return type for a successful POST - will probably require a separate parsing function and return Success/Fail
+ return ParseOrder(result);
+ }
- #endregion
+ protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false)
+ {
+ Dictionary payload = new Dictionary() {{ "order_ids", new [] { orderId } } };
+ await MakeJsonRequestAsync("/orders/batch_cancel", payload: payload, requestMethod: "POST");
+ }
- #region SocketEndpoints
+ protected override Task OnWithdrawAsync(ExchangeWithdrawalRequest withdrawalRequest)
+ {
+ return base.OnWithdrawAsync(withdrawalRequest);
+ }
- protected override Task OnGetDeltaOrderBookWebSocketAsync(Action callback, int maxCount = 100, params string[] marketSymbols)
- {
- return base.OnGetDeltaOrderBookWebSocketAsync(callback);
- }
+ #endregion
- protected override async Task OnGetTickersWebSocketAsync(Action>> callback, params string[] marketSymbols)
- {
+ #region SocketEndpoints
+
+ protected override Task OnGetDeltaOrderBookWebSocketAsync(Action callback, int maxCount = 100, params string[] marketSymbols)
+ {
+ return ConnectWebSocketAsync("/", (_socket, msg) =>
+ {
+ JToken tokens = JToken.Parse(msg.ToStringFromUTF8());
+ string type = tokens[EVENTS][0][TYPE].ToStringInvariant();
+ if (type.Equals("update") || type.Equals("snapshot"))
+ {
+ var book = new ExchangeOrderBook(){ MarketSymbol = tokens[EVENTS][0][PRODUCTID].ToStringInvariant(), LastUpdatedUtc = DateTime.UtcNow, SequenceId = tokens["sequence_num"].ConvertInvariant() };
+ int askCount = 0, bidCount = 0;
+ foreach(var token in tokens[EVENTS][0]["updates"])
+ {
+ if (token[SIDE].ToStringInvariant().Equals("bid"))
+ {
+ if (bidCount++ < maxCount)
+ {
+ decimal price = token[PRICELEVEL].ConvertInvariant();
+ book.Bids.Add( price, new ExchangeOrderPrice(){ Price = price, Amount=token["new_quantity"].ConvertInvariant()} );
+ }
+ }
+ else
+ {
+ if (askCount++ < maxCount)
+ {
+ decimal price = token[PRICELEVEL].ConvertInvariant();
+ book.Asks.Add( price, new ExchangeOrderPrice(){ Price = price, Amount=token["new_quantity"].ConvertInvariant()} );
+ }
+ }
+ if (askCount >= maxCount && bidCount >=maxCount) break;
+ }
+ callback?.Invoke(book);
+ }
+ return Task.CompletedTask;
+ }, async (_socket) =>
+ {
+ string timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToStringInvariant();
+ string signature = CryptoUtility.SHA256Sign(timestamp + LEVEL2 + string.Join(",", marketSymbols), PrivateApiKey.ToUnsecureString());
+ var subscribeRequest = new
+ {
+ type = SUBSCRIBE,
+ product_ids = marketSymbols,
+ channel = LEVEL2,
+ api_key = PublicApiKey.ToUnsecureString(),
+ timestamp,
+ signature
+ };
+ await _socket.SendMessageAsync(subscribeRequest);
+ });
+ }
+
+ protected override async Task OnGetTickersWebSocketAsync(Action>> callback, params string[] marketSymbols)
+ {
return await ConnectWebSocketAsync("/", async (_socket, msg) =>
{
JToken tokens = JToken.Parse(msg.ToStringFromUTF8());
var timestamp = tokens["timestamp"].ConvertInvariant();
List> ticks = new List>();
- foreach(var token in tokens["events"]?[0]?["tickers"])
+ foreach(var token in tokens[EVENTS]?[0]?["tickers"])
{
- string product = token["product_id"].ToStringInvariant();
+ string product = token[PRODUCTID].ToStringInvariant();
var split = product.Split(GlobalMarketSymbolSeparator);
ticks.Add(new KeyValuePair(product, new ExchangeTicker()
{
- // We don't have Bid or Ask info on this feed
- ApiResponse = token,
- Last = token["price"].ConvertInvariant(),
- Volume = new ExchangeVolume()
- {
- BaseCurrency = split[0],
- QuoteCurrency = split[1],
- BaseCurrencyVolume = token["volume_24_h"].ConvertInvariant(),
- Timestamp = timestamp
- }
+ // We don't have Bid or Ask info on this feed
+ MarketSymbol = product,
+ ApiResponse = token,
+ Exchange = this.Name,
+ Last = token[PRICE].ConvertInvariant(),
+ Volume = new ExchangeVolume()
+ {
+ BaseCurrency = split[0],
+ QuoteCurrency = split[1],
+ BaseCurrencyVolume = token["volume_24_h"].ConvertInvariant(),
+ Timestamp = timestamp
+ }
} ));
- }
- callback?.Invoke(ticks);
- }, async (_socket) =>
- {
- string timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToStringInvariant();
- string signature = CryptoUtility.SHA256Sign(timestamp + "ticker" + string.Join(",", marketSymbols), PrivateApiKey.ToUnsecureString());
- var subscribeRequest = new
+ }
+ callback?.Invoke(ticks);
+ }, async (_socket) =>
{
- type = "subscribe",
- product_ids = marketSymbols,
- channel = "ticker",
- api_key = PublicApiKey.ToUnsecureString(),
- timestamp,
- signature
- };
- await _socket.SendMessageAsync(subscribeRequest);
- }); }
+ string timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToStringInvariant();
+ string signature = CryptoUtility.SHA256Sign(timestamp + TICKER + string.Join(",", marketSymbols), PrivateApiKey.ToUnsecureString());
+ var subscribeRequest = new
+ {
+ type = SUBSCRIBE,
+ product_ids = marketSymbols,
+ channel = TICKER,
+ api_key = PublicApiKey.ToUnsecureString(),
+ timestamp,
+ signature
+ };
+ await _socket.SendMessageAsync(subscribeRequest);
+ });
+ }
protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols)
{
@@ -571,13 +629,13 @@ protected override async Task OnGetTradesWebSocketAsync(Func
{
JToken tokens = JToken.Parse(msg.ToStringFromUTF8());
- foreach(var token in tokens["events"]?[0]?["trades"])
+ foreach(var token in tokens[EVENTS]?[0]?["trades"])
{
- await callback?.Invoke(new KeyValuePair(token["product_id"].ToStringInvariant(), new ExchangeTrade()
+ callback?.Invoke(new KeyValuePair(token[PRODUCTID].ToStringInvariant(), new ExchangeTrade()
{
- Amount = token["size"].ConvertInvariant(),
- Price = token["price"].ConvertInvariant(),
- IsBuy = token["side"].ToStringInvariant().Equals("buy"),
+ Amount = token[SIZE].ConvertInvariant(),
+ Price = token[PRICE].ConvertInvariant(),
+ IsBuy = token[SIDE].ToStringInvariant().Equals(BUY),
Id = token["trade_id"].ToStringInvariant(),
Timestamp = token["time"].ConvertInvariant()
}));
@@ -585,185 +643,183 @@ protected override async Task OnGetTradesWebSocketAsync(Func
{
string timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToStringInvariant();
- string signature = CryptoUtility.SHA256Sign(timestamp + "market_trades" + string.Join(",", marketSymbols), PrivateApiKey.ToUnsecureString());
+ string signature = CryptoUtility.SHA256Sign(timestamp + MARKETTRADES + string.Join(",", marketSymbols), PrivateApiKey.ToUnsecureString());
var subscribeRequest = new
{
- type = "subscribe",
+ type = SUBSCRIBE,
product_ids = marketSymbols,
- channel = "market_trades",
+ channel = MARKETTRADES,
api_key = PublicApiKey.ToUnsecureString(),
timestamp,
signature
};
await _socket.SendMessageAsync(subscribeRequest);
- });
- }
+ });
+ }
- #endregion
+ #endregion
#region PrivateFunctions
- private async Task> GetAmounts(bool AvailableOnly)
- {
- Accounts ??= new Dictionary(); // This function is the only place where Accounts cache is populated
+ private async Task> GetAmounts(bool AvailableOnly)
+ {
+ Accounts ??= new Dictionary(); // This function is the only place where Accounts cache is populated
- Dictionary amounts = new Dictionary(StringComparer.OrdinalIgnoreCase);
- pagination = PaginationType.V3;
- JToken token = await MakeJsonRequestAsync("/accounts");
- while(true)
- {
- foreach (JToken account in token["accounts"])
- {
- Accounts[account[CURRENCY].ToString()] = account["uuid"].ToString(); // populate Accounts cache as we go
- decimal amount = AvailableOnly ? account["available_balance"][VALUE].ConvertInvariant() : account["available_balance"][VALUE].ConvertInvariant() + account["hold"][VALUE].ConvertInvariant();
- if (amount > 0.0m) amounts[account[CURRENCY].ToStringInvariant()] = amount;
- }
- if (cursorNext == null) break;
- token = await MakeJsonRequestAsync("/accounts?starting_after=" + cursorNext);
- }
- pagination = PaginationType.None;
- return amounts;
- }
-
- ///
- /// Warning: This call uses V2 Transactions
- ///
- ///
- ///
- ///
- private async Task> GetTx(bool Withdrawals, string currency)
- {
- if (Accounts == null) await GetAmounts(true);
- pagination = PaginationType.V2;
- List transfers = new List();
- JToken tokens = await MakeJsonRequestAsync($"accounts/{Accounts[currency]}/transactions", BaseURLV2);
- while(true)
- {
- foreach (JToken token in tokens)
- {
- // A "send" to Coinbase is when someone "sent" you coin - or a receive to the rest of the world
- // Likewise, a "receive" is when someone "received" coin from you. In other words, it's back-asswards.
- if (!Withdrawals && token["type"].ToStringInvariant().Equals("send")) transfers.Add(ParseTransaction(token));
- else if (Withdrawals && token["type"].ToStringInvariant().Equals("receive")) transfers.Add(ParseTransaction(token));
-
- // Legacy Order and other Coinbase Tx Types can be parsed using this V2 code block
- //var tmp = ParseOrder(token);
- }
- if (string.IsNullOrEmpty(cursorNext)) break;
- tokens = await MakeJsonRequestAsync($"accounts/{Accounts[currency]}/transactions?starting_after={cursorNext}", BaseURLV2);
- }
- pagination = PaginationType.None;
- return transfers;
- }
-
- ///
- /// Parse V2 Transaction of type of either "Send" or "Receive"
- ///
- ///
- ///
- private ExchangeTransaction ParseTransaction(JToken token)
- {
- // The Coin Address/TxFee isn't available but can be retrieved using the Network Hash/BlockChainId
- return new ExchangeTransaction()
- {
- PaymentId = token["id"].ToStringInvariant(), // Not sure how this is used elsewhere but here it is the Coinbase TransactionID
- BlockchainTxId = token["network"]["hash"].ToStringInvariant(),
- Currency = token[AMOUNT][CURRENCY].ToStringInvariant(),
- Amount = token[AMOUNT][AMOUNT].ConvertInvariant(),
- Timestamp = token["created_at"].ToObject(),
- Status = token["status"].ToStringInvariant() == "completed" ? TransactionStatus.Complete : TransactionStatus.Unknown,
- Notes = token["description"].ToStringInvariant()
- // Address
- // AddressTag
- // TxFee
- };
- }
-
-
- ///
- /// Parse both Advanced Trade and Legacy Transactions
- ///
- ///
- ///
- private ExchangeOrderResult ParseOrder(JToken result)
- {
- decimal amount = 0, amountFilled = 0, price = 0, fees = 0;
- string marketSymbol = string.Empty;
- bool isBuy = true;
-
- //Debug.WriteLine(result["type"].ToStringInvariant());
- switch(result["type"].ToStringInvariant())
- {
- case ADVFILL:
- // Buys/Sells have reversed amounts?
-
-
- break;
- case "send":
- case "receive":
- return new ExchangeOrderResult {OrderId = result["id"].ToStringInvariant(), Message = result["type"].ToStringInvariant(), };
- case "buy":
- case "sell":
- case "trade":
- case "request":
- case "transfer":
-
- case "exchange_deposit":
- case "fiat_deposit":
- case "fiat_withdrawal":
- case "pro_withdrawal":
- case "vault_withdrawal":
- default:
- return new ExchangeOrderResult {OrderId = result["id"].ToStringInvariant(), Message = result["type"].ToStringInvariant(), };
- }
+ Dictionary amounts = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ pagination = PaginationType.V3;
+ JToken token = await MakeJsonRequestAsync("/accounts");
+ while(true)
+ {
+ foreach (JToken account in token["accounts"])
+ {
+ Accounts[account[CURRENCY].ToString()] = account["uuid"].ToString(); // populate Accounts cache as we go
+ decimal amount = AvailableOnly ? account["available_balance"][VALUE].ConvertInvariant() : account["available_balance"][VALUE].ConvertInvariant() + account["hold"][VALUE].ConvertInvariant();
+ if (amount > 0.0m) amounts[account[CURRENCY].ToStringInvariant()] = amount;
+ }
+ if (string.IsNullOrEmpty(cursorNext)) break;
+ token = await MakeJsonRequestAsync("/accounts?starting_after=" + cursorNext);
+ }
+ pagination = PaginationType.None;
+ return amounts;
+ }
- amount = result[AMOUNT][AMOUNT].ConvertInvariant(amountFilled);
- amountFilled = amount;
+ ///
+ /// Warning: This call uses V2 Transactions
+ ///
+ ///
+ ///
+ ///
+ private async Task> GetTx(bool Withdrawals, string currency)
+ {
+ if (Accounts == null) await GetAmounts(true);
+ pagination = PaginationType.V2;
+ List transfers = new List();
+ JToken tokens = await MakeJsonRequestAsync($"accounts/{Accounts[currency]}/transactions", BaseURLV2);
+ while(true)
+ {
+ foreach (JToken token in tokens)
+ {
+ // A "send" to Coinbase is when someone "sent" you coin - or a receive to the rest of the world
+ // Likewise, a "receive" is when someone "received" coin from you. In other words, it's back-asswards.
+ if (!Withdrawals && token[TYPE].ToStringInvariant().Equals("send")) transfers.Add(ParseTransaction(token));
+ else if (Withdrawals && token[TYPE].ToStringInvariant().Equals("receive")) transfers.Add(ParseTransaction(token));
- price = result[ADVFILL]["fill_price"].ConvertInvariant();
- fees = result[ADVFILL]["commission"].ConvertInvariant();
- marketSymbol = result[ADVFILL][PRODUCTID].ToStringInvariant(result["id"].ToStringInvariant());
- isBuy = (result[ADVFILL]["order_side"].ToStringInvariant() == "buy");
+ // Legacy Order and other Coinbase Tx Types can be parsed using this V2 code
+ //var tmp = ParseOrder(token);
+ }
+ if (string.IsNullOrEmpty(cursorNext)) break;
+ tokens = await MakeJsonRequestAsync($"accounts/{Accounts[currency]}/transactions?starting_after={cursorNext}", BaseURLV2);
+ }
+ pagination = PaginationType.None;
+ return transfers;
+ }
- ExchangeOrderResult order = new ExchangeOrderResult()
- {
- IsBuy = isBuy,
- Amount = amount,
- AmountFilled = amountFilled,
- Price = price,
- Fees = fees,
- FeesCurrency = result["native_amount"]["currency"].ToStringInvariant(),
- OrderDate = result["created_at"].ToDateTimeInvariant(),
- CompletedDate = result["updated_at"].ToDateTimeInvariant(),
- MarketSymbol = marketSymbol,
- OrderId = result["id"].ToStringInvariant(),
- Message = result["type"].ToStringInvariant()
- };
-
- switch (result["status"].ToStringInvariant())
- {
- case "completed":
- order.Result = ExchangeAPIOrderResult.Filled;
- break;
- case "waiting_for_clearing":
- case "waiting_for_signature":
- case "pending":
- order.Result = ExchangeAPIOrderResult.PendingOpen;
- break;
- case "expired":
- case "canceled":
- order.Result = ExchangeAPIOrderResult.Canceled;
- break;
- default:
- order.Result = ExchangeAPIOrderResult.Unknown;
- break;
- }
- return order;
- }
+ ///
+ /// Parse V2 Transaction of type of either "Send" or "Receive"
+ ///
+ ///
+ ///
+ private ExchangeTransaction ParseTransaction(JToken token)
+ {
+ // The Coin Address/TxFee isn't available but can be retrieved using the Network Hash/BlockChainId
+ return new ExchangeTransaction()
+ {
+ PaymentId = token["id"].ToStringInvariant(), // Not sure how this is used elsewhere but here it is the Coinbase TransactionID
+ BlockchainTxId = token["network"]["hash"].ToStringInvariant(),
+ Currency = token[AMOUNT][CURRENCY].ToStringInvariant(),
+ Amount = token[AMOUNT][AMOUNT].ConvertInvariant(),
+ Timestamp = token["created_at"].ToObject(),
+ Status = token["status"].ToStringInvariant() == "completed" ? TransactionStatus.Complete : TransactionStatus.Unknown,
+ Notes = token["description"].ToStringInvariant()
+ // Address
+ // AddressTag
+ // TxFee
+ };
+ }
- #endregion
- }
+ ///
+ /// Parse both Advanced Trade and Legacy Transactions
+ ///
+ ///
+ ///
+ private ExchangeOrderResult ParseOrder(JToken result)
+ {
+ decimal amount = 0, amountFilled = 0, price = 0, fees = 0;
+ string marketSymbol = string.Empty;
+ bool isBuy = true;
- public partial class ExchangeName { public const string Coinbase = "Coinbase"; }
+ //Debug.WriteLine(result["type"].ToStringInvariant());
+ switch(result[TYPE].ToStringInvariant())
+ {
+ case ADVFILL:
+ // Buys/Sells have reversed amounts?
+ break;
+ case BUY:
+ case "sell":
+ case "send":
+ case "receive":
+ return new ExchangeOrderResult {OrderId = result["id"].ToStringInvariant(), Message = result["type"].ToStringInvariant(), };
+ case "trade":
+ case "request":
+ case "transfer":
+
+ case "exchange_deposit":
+ case "fiat_deposit":
+ case "fiat_withdrawal":
+ case "pro_withdrawal":
+ case "vault_withdrawal":
+ default:
+ return new ExchangeOrderResult {OrderId = result["id"].ToStringInvariant(), Message = result["type"].ToStringInvariant(), };
+ }
+
+ amount = result[AMOUNT][AMOUNT].ConvertInvariant(amountFilled);
+ amountFilled = amount;
+
+ price = result[ADVFILL]["fill_price"].ConvertInvariant();
+ fees = result[ADVFILL]["commission"].ConvertInvariant();
+ marketSymbol = result[ADVFILL][PRODUCTID].ToStringInvariant(result["id"].ToStringInvariant());
+ isBuy = (result[ADVFILL]["order_side"].ToStringInvariant() == BUY);
+
+ ExchangeOrderResult order = new ExchangeOrderResult()
+ {
+ IsBuy = isBuy,
+ Amount = amount,
+ AmountFilled = amountFilled,
+ Price = price,
+ Fees = fees,
+ FeesCurrency = result["native_amount"][CURRENCY].ToStringInvariant(),
+ OrderDate = result["created_at"].ToDateTimeInvariant(),
+ CompletedDate = result["updated_at"].ToDateTimeInvariant(),
+ MarketSymbol = marketSymbol,
+ OrderId = result["id"].ToStringInvariant(),
+ Message = result[TYPE].ToStringInvariant()
+ };
+
+ switch (result["status"].ToStringInvariant())
+ {
+ case "completed":
+ order.Result = ExchangeAPIOrderResult.Filled;
+ break;
+ case "waiting_for_clearing":
+ case "waiting_for_signature":
+ case "pending":
+ order.Result = ExchangeAPIOrderResult.PendingOpen;
+ break;
+ case "expired":
+ case "canceled":
+ order.Result = ExchangeAPIOrderResult.Canceled;
+ break;
+ default:
+ order.Result = ExchangeAPIOrderResult.Unknown;
+ break;
+ }
+ return order;
+ }
+
+ #endregion
+
+ }
}
+
+public partial class ExchangeName { public const string Coinbase = "Coinbase"; }