diff --git a/src/ExchangeSharp/API/Exchanges/Aquanow/ExchangeAquanowAPI.cs b/src/ExchangeSharp/API/Exchanges/Aquanow/ExchangeAquanowAPI.cs index 11b4ec605..792cf002d 100644 --- a/src/ExchangeSharp/API/Exchanges/Aquanow/ExchangeAquanowAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Aquanow/ExchangeAquanowAPI.cs @@ -12,241 +12,232 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp { - using ExchangeSharp.Aquanow; - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; - using System; - using System.Collections.Generic; - using System.Threading.Tasks; + using ExchangeSharp.Aquanow; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + using System; + using System.Collections.Generic; + using System.Threading.Tasks; - public sealed partial class ExchangeAquanowAPI : ExchangeAPI - { - public override string BaseUrl { get; set; } = "https://api.aquanow.io"; + public sealed partial class ExchangeAquanowAPI : ExchangeAPI + { + public override string BaseUrl { get; set; } = "https://api.aquanow.io"; - public string MarketUrl { get; set; } = "https://market.aquanow.io"; - public override string BaseUrlWebSocket { get; set; } = "wss://market.aquanow.io/"; + public string MarketUrl { get; set; } = "https://market.aquanow.io"; + public override string BaseUrlWebSocket { get; set; } = "wss://market.aquanow.io/"; private ExchangeAquanowAPI() - { - NonceStyle = NonceStyle.UnixMilliseconds; - RequestContentType = "application/x-www-form-urlencoded"; - MarketSymbolSeparator = "-"; - MarketSymbolIsReversed = false; - WebSocketOrderBookType = WebSocketOrderBookType.DeltasOnly; - } - - protected override async Task> OnGetMarketSymbolsAsync() - { - List symbols = new List(); - JToken token = await MakeJsonRequestAsync("/availablesymbols", MarketUrl); - foreach (string symbol in token) - { - symbols.Add(symbol); - } - return symbols; - } - - - // NOT SUPPORTED - protected override async Task>> OnGetTickersAsync() - { - List> tickers = new List>(); - JToken symbols = await MakeJsonRequestAsync("/availablesymbols", MarketUrl); - foreach (string symbol in symbols) - { - JToken bestPriceSymbol = await MakeJsonRequestAsync($"/bestprice?symbol={symbol}", MarketUrl); - decimal bid = bestPriceSymbol["bestBid"].ConvertInvariant(); - decimal ask = bestPriceSymbol["bestAsk"].ConvertInvariant(); - ExchangeTicker ticker = new ExchangeTicker { MarketSymbol = symbol, Bid = bid, Ask = ask }; - tickers.Add(new KeyValuePair(symbol, ticker)); - - } - return tickers; - } - - protected override async Task> OnGetCurrenciesAsync() - { - - var currencies = new Dictionary(); - var symbols = await GetMarketSymbolsAsync(); - foreach (string symbol in symbols) - { - var currency = new ExchangeCurrency - { - Name = symbol - }; - currencies[currency.Name] = currency; - } - - return currencies; - } - - - - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) - { - - if (CanMakeAuthenticatedRequest(payload)) - { - request.AddHeader("content-type", "application/json"); - var sigContent = new signatureContent { httpMethod = request.Method, path = request.RequestUri.LocalPath, nonce = payload["nonce"].ToStringInvariant() }; - string json = JsonConvert.SerializeObject(sigContent); - string bodyRequest = JsonConvert.SerializeObject(payload); - string hexSha384 = CryptoUtility.SHA384Sign(json, PrivateApiKey.ToUnsecureString()); - request.AddHeader("x-api-key", PublicApiKey.ToUnsecureString()); - request.AddHeader("x-signature", hexSha384); - request.AddHeader("x-nonce", payload["nonce"].ToStringInvariant() - ); - if (request.Method == "GET") - { - await CryptoUtility.WriteToRequestAsync(request, null); - } - else - { - await CryptoUtility.WriteToRequestAsync(request, bodyRequest); - } - } - } - // DONE - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) - { - // In Aquanow market order, when buying crypto the amount of crypto that is bought is the receiveQuantity - // and when selling the amount of crypto that is sold is the deliverQuantity - string amountParameter = order.IsBuy ? "receiveQuantity" : "deliverQuantity"; - string amountReceived = order.IsBuy ? "deliverQuantity" : "receiveQuantity"; - string feesCurrency = order.IsBuy ? order.MarketSymbol.Substring(0, order.MarketSymbol.IndexOf('-')) : order.MarketSymbol.Substring(order.MarketSymbol.IndexOf('-') + 1); - var payload = await GetNoncePayloadAsync(); - payload["ticker"] = order.MarketSymbol; - payload["tradeSide"] = order.IsBuy ? "buy" : "sell"; - payload[amountParameter] = order.Amount; - order.ExtraParameters.CopyTo(payload); - JToken token = await MakeJsonRequestAsync("/trades/v1/market", null, payload, "POST"); - var orderDetailsPayload = await GetNoncePayloadAsync(); - - //{ - // "type": "marketOrderSubmitAck", - // "payload": { - // "orderId": "cfXXXXXX-56ce-4df8-9f1e-729e87bf54d8", - // "receiveCurrency": "BTC", - // "receiveQuantity": 0.00004, - // "deliverCurrency": "USD", - // "deliverQuantity": 0.369124, - // "fee": 0.000001 - // } - //} - - - - JToken result = await MakeJsonRequestAsync($"/trades/v1/order?orderId={token["payload"]["orderId"].ToStringInvariant()}", null, orderDetailsPayload, "GET"); - // { - // "priceArrival": 9223.5, - // "orderId": "24cf77ad-7e93-44d7-86f8-b9d9a046b008", - // "remainingQtyBase": 0, - // "tradeSize": 0.0004, - // "exchangeOrderId": "-", - // "tradePriceAvg": 9223.5, - // "fillPct": 100, - // "finalizeReturnedQtyBase": 0, - // "tradeSide": "buy", - // "exchangeClientOrderId": "-", - // "tradeTime": 1594681810754, - // "childOrderCount": 0, - // "fillFeeQuote": 0, - // "itemDateTime": 1594681811719, - // "baseSymbol": "USD", - // "strategy": "MARKET", - // "fillQtyQuote": 0.0004, - // "usernameRef": "-", - // "fillQtyBase": 3.6894, - // "priceMarket": "-", - // "symbol": "BTC-USD", - // "tradeStatus": "COMPLETE", - // "commissionRate": 20, - // "createdAt": 1594681810756, - // "message": "-", - // "priceLimit": 9223.5, - // "quoteSymbol": "BTC", - // "remainingQtyQuote": 0, - // "orderIdParent": "24cf77ad-7e93-44d7-86f8-b9d9a046b008", - // "orderType": "parentOrder", - // "updatedAt": 1594681811941, - // "tradeDuration": 0, - // "username": "XXXXXXX", - // "fillFeeQuoteAqua": 0.0000001 - // } - ExchangeOrderResult orderDetails = new ExchangeOrderResult - { - OrderId = result["orderId"].ToStringInvariant(), - AmountFilled = result["fillQtyQuote"].ToStringInvariant().ConvertInvariant(), - Amount = payload[amountParameter].ConvertInvariant(), - OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(result["tradeTime"].ConvertInvariant()), - Message = result["message"].ToStringInvariant(), - IsBuy = order.IsBuy, - Fees = token["payload"]["fee"].ConvertInvariant(), - FeesCurrency = feesCurrency, - MarketSymbol = order.MarketSymbol, - Price = result["priceArrival"].ToStringInvariant().ConvertInvariant(), - - }; - switch (result["tradeStatus"].ToStringInvariant()) - { - case "COMPLETE": - orderDetails.AveragePrice = result["tradePriceAvg"].ToStringInvariant().ConvertInvariant(); - orderDetails.Result = ExchangeAPIOrderResult.Filled; - break; - - default: - orderDetails.Result = ExchangeAPIOrderResult.Error; - break; - } - return orderDetails; - } - - protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null) - { - if (string.IsNullOrWhiteSpace(orderId)) - { - return null; - } - var payload = await GetNoncePayloadAsync(); - JToken result = await MakeJsonRequestAsync($"/trades/v1/order?orderId={orderId}", null, payload, "GET"); - bool isBuy = result["tradeSide"].ToStringInvariant() == "buy" ? true : false; - ExchangeOrderResult orderDetails = new ExchangeOrderResult - { - OrderId = result["orderId"].ToStringInvariant(), - AmountFilled = result["fillQtyQuote"].ToStringInvariant().ConvertInvariant(), - Amount = result["tradeSize"].ConvertInvariant(), - OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(result["tradeTime"].ConvertInvariant()), - Message = result["message"].ToStringInvariant(), - IsBuy = isBuy, - Fees = result["fillFeeQuote"].ConvertInvariant() + result["fillFeeQuotaAqua"].ConvertInvariant(), - FeesCurrency = result["quoteSymbol"].ToStringInvariant(), - MarketSymbol = result["symbol"].ToStringInvariant(), - Price = result["priceArrival"].ToStringInvariant().ConvertInvariant(), - }; - switch (result["tradeStatus"].ToStringInvariant()) - { - case "COMPLETE": - orderDetails.AveragePrice = result["tradePriceAvg"].ToStringInvariant().ConvertInvariant(); - orderDetails.Result = ExchangeAPIOrderResult.Filled; - break; - - default: - orderDetails.Result = ExchangeAPIOrderResult.Error; - break; - } - - return orderDetails; - } - - protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null) - { - var payload = await GetNoncePayloadAsync(); - payload["orderId"] = orderId; - JToken token = await MakeJsonRequestAsync("/trades/v1/order", null, payload, "DELETE"); - } - - } - - public partial class ExchangeName { public const string Aquanow = "Aquanow"; } + { + NonceStyle = NonceStyle.UnixMilliseconds; + RequestContentType = "application/x-www-form-urlencoded"; + MarketSymbolSeparator = "-"; + MarketSymbolIsReversed = false; + WebSocketOrderBookType = WebSocketOrderBookType.DeltasOnly; + } + + protected override async Task> OnGetMarketSymbolsAsync() + { + List symbols = new List(); + JToken token = await MakeJsonRequestAsync("/availablesymbols", MarketUrl); + foreach (string symbol in token) + { + symbols.Add(symbol); + } + return symbols; + } + + // NOT SUPPORTED + protected override async Task>> OnGetTickersAsync() + { + List> tickers = new List>(); + JToken symbols = await MakeJsonRequestAsync("/availablesymbols", MarketUrl); + foreach (string symbol in symbols) + { + JToken bestPriceSymbol = await MakeJsonRequestAsync($"/bestprice?symbol={symbol}", MarketUrl); + decimal bid = bestPriceSymbol["bestBid"].ConvertInvariant(); + decimal ask = bestPriceSymbol["bestAsk"].ConvertInvariant(); + ExchangeTicker ticker = new ExchangeTicker { MarketSymbol = symbol, Bid = bid, Ask = ask, ApiResponse = bestPriceSymbol }; + tickers.Add(new KeyValuePair(symbol, ticker)); + } + return tickers; + } + + protected override async Task> OnGetCurrenciesAsync() + { + var currencies = new Dictionary(); + var symbols = await GetMarketSymbolsAsync(); + foreach (string symbol in symbols) + { + var currency = new ExchangeCurrency + { + Name = symbol + }; + currencies[currency.Name] = currency; + } + + return currencies; + } + + protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + { + if (CanMakeAuthenticatedRequest(payload)) + { + request.AddHeader("content-type", "application/json"); + var sigContent = new signatureContent { httpMethod = request.Method, path = request.RequestUri.LocalPath, nonce = payload["nonce"].ToStringInvariant() }; + string json = JsonConvert.SerializeObject(sigContent); + string bodyRequest = JsonConvert.SerializeObject(payload); + string hexSha384 = CryptoUtility.SHA384Sign(json, PrivateApiKey.ToUnsecureString()); + request.AddHeader("x-api-key", PublicApiKey.ToUnsecureString()); + request.AddHeader("x-signature", hexSha384); + request.AddHeader("x-nonce", payload["nonce"].ToStringInvariant() + ); + if (request.Method == "GET") + { + await CryptoUtility.WriteToRequestAsync(request, null); + } + else + { + await CryptoUtility.WriteToRequestAsync(request, bodyRequest); + } + } + } + + // DONE + protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + { + // In Aquanow market order, when buying crypto the amount of crypto that is bought is the receiveQuantity + // and when selling the amount of crypto that is sold is the deliverQuantity + string amountParameter = order.IsBuy ? "receiveQuantity" : "deliverQuantity"; + string amountReceived = order.IsBuy ? "deliverQuantity" : "receiveQuantity"; + string feesCurrency = order.IsBuy ? order.MarketSymbol.Substring(0, order.MarketSymbol.IndexOf('-')) : order.MarketSymbol.Substring(order.MarketSymbol.IndexOf('-') + 1); + var payload = await GetNoncePayloadAsync(); + payload["ticker"] = order.MarketSymbol; + payload["tradeSide"] = order.IsBuy ? "buy" : "sell"; + payload[amountParameter] = order.Amount; + order.ExtraParameters.CopyTo(payload); + JToken token = await MakeJsonRequestAsync("/trades/v1/market", null, payload, "POST"); + var orderDetailsPayload = await GetNoncePayloadAsync(); + + //{ + // "type": "marketOrderSubmitAck", + // "payload": { + // "orderId": "cfXXXXXX-56ce-4df8-9f1e-729e87bf54d8", + // "receiveCurrency": "BTC", + // "receiveQuantity": 0.00004, + // "deliverCurrency": "USD", + // "deliverQuantity": 0.369124, + // "fee": 0.000001 + // } + //} + + JToken result = await MakeJsonRequestAsync($"/trades/v1/order?orderId={token["payload"]["orderId"].ToStringInvariant()}", null, orderDetailsPayload, "GET"); + // { + // "priceArrival": 9223.5, + // "orderId": "24cf77ad-7e93-44d7-86f8-b9d9a046b008", + // "remainingQtyBase": 0, + // "tradeSize": 0.0004, + // "exchangeOrderId": "-", + // "tradePriceAvg": 9223.5, + // "fillPct": 100, + // "finalizeReturnedQtyBase": 0, + // "tradeSide": "buy", + // "exchangeClientOrderId": "-", + // "tradeTime": 1594681810754, + // "childOrderCount": 0, + // "fillFeeQuote": 0, + // "itemDateTime": 1594681811719, + // "baseSymbol": "USD", + // "strategy": "MARKET", + // "fillQtyQuote": 0.0004, + // "usernameRef": "-", + // "fillQtyBase": 3.6894, + // "priceMarket": "-", + // "symbol": "BTC-USD", + // "tradeStatus": "COMPLETE", + // "commissionRate": 20, + // "createdAt": 1594681810756, + // "message": "-", + // "priceLimit": 9223.5, + // "quoteSymbol": "BTC", + // "remainingQtyQuote": 0, + // "orderIdParent": "24cf77ad-7e93-44d7-86f8-b9d9a046b008", + // "orderType": "parentOrder", + // "updatedAt": 1594681811941, + // "tradeDuration": 0, + // "username": "XXXXXXX", + // "fillFeeQuoteAqua": 0.0000001 + // } + ExchangeOrderResult orderDetails = new ExchangeOrderResult + { + OrderId = result["orderId"].ToStringInvariant(), + AmountFilled = result["fillQtyQuote"].ToStringInvariant().ConvertInvariant(), + Amount = payload[amountParameter].ConvertInvariant(), + OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(result["tradeTime"].ConvertInvariant()), + Message = result["message"].ToStringInvariant(), + IsBuy = order.IsBuy, + Fees = token["payload"]["fee"].ConvertInvariant(), + FeesCurrency = feesCurrency, + MarketSymbol = order.MarketSymbol, + Price = result["priceArrival"].ToStringInvariant().ConvertInvariant(), + }; + switch (result["tradeStatus"].ToStringInvariant()) + { + case "COMPLETE": + orderDetails.AveragePrice = result["tradePriceAvg"].ToStringInvariant().ConvertInvariant(); + orderDetails.Result = ExchangeAPIOrderResult.Filled; + break; + + default: + orderDetails.Result = ExchangeAPIOrderResult.Error; + break; + } + return orderDetails; + } + + protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null) + { + if (string.IsNullOrWhiteSpace(orderId)) + { + return null; + } + var payload = await GetNoncePayloadAsync(); + JToken result = await MakeJsonRequestAsync($"/trades/v1/order?orderId={orderId}", null, payload, "GET"); + bool isBuy = result["tradeSide"].ToStringInvariant() == "buy" ? true : false; + ExchangeOrderResult orderDetails = new ExchangeOrderResult + { + OrderId = result["orderId"].ToStringInvariant(), + AmountFilled = result["fillQtyQuote"].ToStringInvariant().ConvertInvariant(), + Amount = result["tradeSize"].ConvertInvariant(), + OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(result["tradeTime"].ConvertInvariant()), + Message = result["message"].ToStringInvariant(), + IsBuy = isBuy, + Fees = result["fillFeeQuote"].ConvertInvariant() + result["fillFeeQuotaAqua"].ConvertInvariant(), + FeesCurrency = result["quoteSymbol"].ToStringInvariant(), + MarketSymbol = result["symbol"].ToStringInvariant(), + Price = result["priceArrival"].ToStringInvariant().ConvertInvariant(), + }; + switch (result["tradeStatus"].ToStringInvariant()) + { + case "COMPLETE": + orderDetails.AveragePrice = result["tradePriceAvg"].ToStringInvariant().ConvertInvariant(); + orderDetails.Result = ExchangeAPIOrderResult.Filled; + break; + + default: + orderDetails.Result = ExchangeAPIOrderResult.Error; + break; + } + + return orderDetails; + } + + protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null) + { + var payload = await GetNoncePayloadAsync(); + payload["orderId"] = orderId; + JToken token = await MakeJsonRequestAsync("/trades/v1/order", null, payload, "DELETE"); + } + } + + public partial class ExchangeName { public const string Aquanow = "Aquanow"; } } diff --git a/src/ExchangeSharp/API/Exchanges/BitBank/ExchangeBitBankAPI.cs b/src/ExchangeSharp/API/Exchanges/BitBank/ExchangeBitBankAPI.cs index d4d0f402e..b36cf62e5 100644 --- a/src/ExchangeSharp/API/Exchanges/BitBank/ExchangeBitBankAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/BitBank/ExchangeBitBankAPI.cs @@ -7,320 +7,326 @@ namespace ExchangeSharp { - public sealed partial class ExchangeBitBankAPI : ExchangeAPI - { - public override string BaseUrl { get; set; } = "https://public.bitbank.cc"; - public string BaseUrlPrivate { get; set; } = "https://api.bitbank.cc/v1"; - public string ErrorCodeDescriptionUrl { get; set; } = "https://docs.bitbank.cc/error_code/"; + public sealed partial class ExchangeBitBankAPI : ExchangeAPI + { + public override string BaseUrl { get; set; } = "https://public.bitbank.cc"; + public string BaseUrlPrivate { get; set; } = "https://api.bitbank.cc/v1"; + public string ErrorCodeDescriptionUrl { get; set; } = "https://docs.bitbank.cc/error_code/"; + // bitbank trade fees are fixed + private const decimal MakerFee = -0.0005m; - // bitbank trade fees are fixed - private const decimal MakerFee = -0.0005m; - private const decimal TakerFee = 0.0015m; + private const decimal TakerFee = 0.0015m; private ExchangeBitBankAPI() - { - NonceStyle = NonceStyle.UnixMilliseconds; - NonceOffset = TimeSpan.FromSeconds(0.1); - WebSocketOrderBookType = WebSocketOrderBookType.DeltasOnly; - MarketSymbolSeparator = "_"; - MarketSymbolIsUppercase = false; + { + NonceStyle = NonceStyle.UnixMilliseconds; + NonceOffset = TimeSpan.FromSeconds(0.1); + WebSocketOrderBookType = WebSocketOrderBookType.DeltasOnly; + MarketSymbolSeparator = "_"; + MarketSymbolIsUppercase = false; ExchangeGlobalCurrencyReplacements["BCC"] = "BCH"; } - # region Public APIs - - protected override async Task OnGetTickerAsync(string marketSymbol) - { - JToken token = await MakeJsonRequestAsync($"/{marketSymbol}/ticker"); - return await ParseTickerAsync(marketSymbol, token); - } - - // Bitbank supports endpoint for getting all rates in one request, Using this endpoint is faster then ExchangeAPI's default implementation - // (which interate `OnGetTickerAsync` for each marketSymbols) - // Note: This doesn't give you a volume. if you want it, please specify marketSymbol. - protected override async Task>> OnGetTickersAsync() - { - JToken token = await MakeJsonRequestAsync($"/prices"); - var symbols = await OnGetMarketSymbolsAsync(); - var result = new List>(); - foreach (var symbol in symbols) - { - var data = token[GlobalMarketSymbolToExchangeMarketSymbolAsync(symbol)]; - var ticker = new ExchangeTicker() - { - Ask = data["sell"].ConvertInvariant(), - Bid = data["buy"].ConvertInvariant(), - Last = data["last"].ConvertInvariant(), - MarketSymbol = symbol - }; - result.Add(new KeyValuePair(symbol, ticker)); - } - return result; - } - - #endregion - - protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) - { - JToken token = await MakeJsonRequestAsync($"/{marketSymbol}/transactions"); - ExchangeOrderBook result = new ExchangeOrderBook(); - // we can not use `APIExtensions.ParseOrderBookFromJToken ...` here, because bid/ask is denoted by "side" property. - foreach (JToken tx in token["transactions"]) - { - var isBuy = (string)tx["side"] == "buy"; - decimal price = tx["price"].ConvertInvariant(); - decimal amount = tx["amount"].ConvertInvariant(); - if (isBuy) - { - result.Bids[price] = new ExchangeOrderPrice { Amount = amount, Price = price }; - } - else - { - result.Asks[price] = new ExchangeOrderPrice { Amount = amount, Price = price }; - } - result.MarketSymbol = NormalizeMarketSymbol(marketSymbol); - } - return result; - } - - protected override async Task> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) - { - var period = FormatPeriod(periodSeconds); - var url = $"/{marketSymbol}/candlestick/{period}/{startDate?.ToString("yyyyMMdd")}"; - JToken token = await MakeJsonRequestAsync(url); - List result = new List(); - // since it is impossible to convert by `CryptoUtility.ToDateTimeInvariant()` - foreach (var c in token["candlestick"]) - { - foreach (var data in c["ohlcv"]) - { - var open = data[0].ConvertInvariant(); - var timestamp = DateTime.SpecifyKind(data[5].ConvertInvariant().UnixTimeStampToDateTimeMilliseconds(), DateTimeKind.Utc); - var candle = new MarketCandle() - { - ExchangeName = "BitBank", - Name = url, - OpenPrice = open, - HighPrice = data[1].ConvertInvariant(), - LowPrice = data[2].ConvertInvariant(), - ClosePrice = data[3].ConvertInvariant(), - BaseCurrencyVolume = data[4].ConvertInvariant(), - Timestamp = timestamp, - }; - result.Add(candle); - } - } - return result; - } - - # region Private APIs - - protected override async Task> OnGetAmountsAsync() => await OnGetAmountsAsyncCore("onhand_amount"); - - protected override Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) - { - throw new NotImplementedException(); - } - - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) - { - if (order.OrderType == OrderType.Stop) - throw new InvalidOperationException("Bitbank does not support stop order"); - Dictionary payload = await GetNoncePayloadAsync(); - payload.Add("pair", NormalizeMarketSymbol(order.MarketSymbol)); - payload.Add("amount", order.Amount.ToStringInvariant()); - payload.Add("side", order.IsBuy ? "buy" : "sell"); - payload.Add("type", order.OrderType.ToStringLowerInvariant()); - payload.Add("price", order.Price); - JToken token = await MakeJsonRequestAsync("/user/spot/order", baseUrl: BaseUrlPrivate, payload: payload, requestMethod: "POST"); - return ParseOrder(token); - } - protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null) - { - Dictionary payload = await GetNoncePayloadAsync(); - if (marketSymbol == null) - throw new APIException("Bitbank requries market symbol when cancelling orders"); - payload.Add("pair", NormalizeMarketSymbol(marketSymbol)); - payload.Add("order_id", orderId); - await MakeJsonRequestAsync("/user/spot/cancel_order", baseUrl: BaseUrlPrivate, payload: payload, requestMethod: "POST"); - } - protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null) - { - var payload = await GetNoncePayloadAsync(); - payload.Add("order_id", orderId); - if (marketSymbol == null) - throw new InvalidOperationException($"BitBank API requires marketSymbol for {nameof(GetOrderDetailsAsync)}"); - payload.Add("pair", marketSymbol); - JToken token = await MakeJsonRequestAsync("/user/spot/order", baseUrl: BaseUrlPrivate, payload: payload); - return ParseOrder(token); - } - protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) - { - var payload = await GetNoncePayloadAsync(); - if (marketSymbol != null) - payload.Add("pair", NormalizeMarketSymbol(marketSymbol)); - JToken token = await MakeJsonRequestAsync("/user/spot/active_orders", baseUrl: BaseUrlPrivate, payload: payload); - return token["orders"].Select(o => ParseOrder(o)); - } - protected override async Task> OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) - { - var payload = await GetNoncePayloadAsync(); - if (marketSymbol == null) - throw new APIException("BitBank requires marketSymbol when getting completed orders"); - payload.Add("pair", NormalizeMarketSymbol(marketSymbol)); - if (afterDate != null) - payload.Add("since", afterDate.ConvertInvariant()); - JToken token = await MakeJsonRequestAsync($"/user/spot/trade_history", baseUrl: BaseUrlPrivate, payload: payload); - return token["trades"].Select(t => TradeHistoryToExchangeOrderResult(t)); - } - - /// - /// Bitbank does not support withdrawing to arbitrary address (for security reason). - /// We must first register address from its web form. - /// So we will call two methods here. - /// 1. Get address from already registered account. (fail if does not exist) - /// 2. Withdraw to that address. - /// - /// - /// - protected override async Task OnWithdrawAsync(ExchangeWithdrawalRequest withdrawalRequest) - { - var asset = withdrawalRequest.Currency.ToLowerInvariant(); - var payload1 = await GetNoncePayloadAsync(); - payload1.Add("asset", asset); - JToken token1 = await MakeJsonRequestAsync($"/user/withdrawal_account", baseUrl: BaseUrlPrivate, payload: payload1); - if (!token1["accounts"].ToArray().Any(a => a["address"].ToStringInvariant() == withdrawalRequest.Address)) - throw new APIException($"Could not withdraw to address {withdrawalRequest.Address}! You must register the address from web form first."); - - var uuid = token1["uuid"].ToStringInvariant(); - - var payload2 = await GetNoncePayloadAsync(); - payload2.Add("asset", asset); - payload2.Add("amount", withdrawalRequest.Amount); - payload2.Add("uuid", uuid); - JToken token2 = await MakeJsonRequestAsync($"/user/request_withdrawal", baseUrl: BaseUrlPrivate, payload: payload2, requestMethod: "POST"); - var resp = new ExchangeWithdrawalResponse - { - Id = token2["txid"].ToStringInvariant() - }; - var status = token2["status"].ToStringInvariant(); - resp.Success = status != "REJECTED" && status != "CANCELED"; - resp.Message = "{" + $"label:{token2["label"]}, fee:{token2["fee"]}" + "}"; - return resp; - } - - # endregion - - /// - /// BitBank does not support API for this one. So hard-code it. - /// - /// - protected override Task> OnGetMarketSymbolsAsync() - { - return Task.FromResult(new List { - "btc_jpy", - "xrp_jpy", - "ltc_btc", - "eth_btc", - "mona_jpy", - "mona_btc", - "bcc_jpy", - "bcc_btc" - }.AsEnumerable()); - } - - // protected override Task> OnGetCurrenciesAsync() => throw new NotImplementedException(); - - // protected override Task> OnGetMarketSymbolsMetadataAsync() => throw new NotImplementedException(); - // protected override Task OnGetDepositAddressAsync(string currency, bool forceRegenerate = false) => throw new NotImplementedException(); - // protected override Task> OnGetDepositHistoryAsync(string currency) => throw new NotImplementedException(); - // protected override Task> OnGetFeesAsync() => throw new NotImplementedException(); - protected override async Task> OnGetAmountsAvailableToTradeAsync() - => await OnGetAmountsAsyncCore("free_amount"); - - /// - /// Bitbank does not support placing several orders at once, so we will just run `PlaceOrderAsync` for each orders. - /// - /// - /// - protected override async Task OnPlaceOrdersAsync(params ExchangeOrderRequest[] order) - { - var resp = new List(); - foreach (var o in order) - resp.Add(await this.PlaceOrderAsync(o)); - return resp.ToArray(); - } - // protected override Task> OnGetWithdrawHistoryAsync(string currency) => throw new NotImplementedException(); - // protected override Task> OnGetMarginAmountsAvailableToTradeAsync(bool includeZeroBalances) => throw new NotImplementedException(); - // protected override Task OnGetOpenPositionAsync(string marketSymbol) => throw new NotImplementedException(); - // protected override Task OnCloseMarginPositionAsync(string marketSymbol) => throw new NotImplementedException(); - /* + #region Public APIs + + protected override async Task OnGetTickerAsync(string marketSymbol) + { + JToken token = await MakeJsonRequestAsync($"/{marketSymbol}/ticker"); + return await ParseTickerAsync(marketSymbol, token); + } + + // Bitbank supports endpoint for getting all rates in one request, Using this endpoint is faster then ExchangeAPI's default implementation + // (which interate `OnGetTickerAsync` for each marketSymbols) + // Note: This doesn't give you a volume. if you want it, please specify marketSymbol. + protected override async Task>> OnGetTickersAsync() + { + JToken token = await MakeJsonRequestAsync($"/prices"); + var symbols = await OnGetMarketSymbolsAsync(); + var result = new List>(); + foreach (var symbol in symbols) + { + var data = token[GlobalMarketSymbolToExchangeMarketSymbolAsync(symbol)]; + var ticker = new ExchangeTicker() + { + ApiResponse = token, + Ask = data["sell"].ConvertInvariant(), + Bid = data["buy"].ConvertInvariant(), + Last = data["last"].ConvertInvariant(), + MarketSymbol = symbol + }; + result.Add(new KeyValuePair(symbol, ticker)); + } + return result; + } + + #endregion Public APIs + + protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) + { + JToken token = await MakeJsonRequestAsync($"/{marketSymbol}/transactions"); + ExchangeOrderBook result = new ExchangeOrderBook(); + // we can not use `APIExtensions.ParseOrderBookFromJToken ...` here, because bid/ask is denoted by "side" property. + foreach (JToken tx in token["transactions"]) + { + var isBuy = (string)tx["side"] == "buy"; + decimal price = tx["price"].ConvertInvariant(); + decimal amount = tx["amount"].ConvertInvariant(); + if (isBuy) + { + result.Bids[price] = new ExchangeOrderPrice { Amount = amount, Price = price }; + } + else + { + result.Asks[price] = new ExchangeOrderPrice { Amount = amount, Price = price }; + } + result.MarketSymbol = NormalizeMarketSymbol(marketSymbol); + } + return result; + } + + protected override async Task> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + { + var period = FormatPeriod(periodSeconds); + var url = $"/{marketSymbol}/candlestick/{period}/{startDate?.ToString("yyyyMMdd")}"; + JToken token = await MakeJsonRequestAsync(url); + List result = new List(); + // since it is impossible to convert by `CryptoUtility.ToDateTimeInvariant()` + foreach (var c in token["candlestick"]) + { + foreach (var data in c["ohlcv"]) + { + var open = data[0].ConvertInvariant(); + var timestamp = DateTime.SpecifyKind(data[5].ConvertInvariant().UnixTimeStampToDateTimeMilliseconds(), DateTimeKind.Utc); + var candle = new MarketCandle() + { + ExchangeName = "BitBank", + Name = url, + OpenPrice = open, + HighPrice = data[1].ConvertInvariant(), + LowPrice = data[2].ConvertInvariant(), + ClosePrice = data[3].ConvertInvariant(), + BaseCurrencyVolume = data[4].ConvertInvariant(), + Timestamp = timestamp, + }; + result.Add(candle); + } + } + return result; + } + + #region Private APIs + + protected override async Task> OnGetAmountsAsync() => await OnGetAmountsAsyncCore("onhand_amount"); + + protected override Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + { + throw new NotImplementedException(); + } + + protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + { + if (order.OrderType == OrderType.Stop) + throw new InvalidOperationException("Bitbank does not support stop order"); + Dictionary payload = await GetNoncePayloadAsync(); + payload.Add("pair", NormalizeMarketSymbol(order.MarketSymbol)); + payload.Add("amount", order.Amount.ToStringInvariant()); + payload.Add("side", order.IsBuy ? "buy" : "sell"); + payload.Add("type", order.OrderType.ToStringLowerInvariant()); + payload.Add("price", order.Price); + JToken token = await MakeJsonRequestAsync("/user/spot/order", baseUrl: BaseUrlPrivate, payload: payload, requestMethod: "POST"); + return ParseOrder(token); + } + + protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null) + { + Dictionary payload = await GetNoncePayloadAsync(); + if (marketSymbol == null) + throw new APIException("Bitbank requries market symbol when cancelling orders"); + payload.Add("pair", NormalizeMarketSymbol(marketSymbol)); + payload.Add("order_id", orderId); + await MakeJsonRequestAsync("/user/spot/cancel_order", baseUrl: BaseUrlPrivate, payload: payload, requestMethod: "POST"); + } + + protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null) + { + var payload = await GetNoncePayloadAsync(); + payload.Add("order_id", orderId); + if (marketSymbol == null) + throw new InvalidOperationException($"BitBank API requires marketSymbol for {nameof(GetOrderDetailsAsync)}"); + payload.Add("pair", marketSymbol); + JToken token = await MakeJsonRequestAsync("/user/spot/order", baseUrl: BaseUrlPrivate, payload: payload); + return ParseOrder(token); + } + + protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) + { + var payload = await GetNoncePayloadAsync(); + if (marketSymbol != null) + payload.Add("pair", NormalizeMarketSymbol(marketSymbol)); + JToken token = await MakeJsonRequestAsync("/user/spot/active_orders", baseUrl: BaseUrlPrivate, payload: payload); + return token["orders"].Select(o => ParseOrder(o)); + } + + protected override async Task> OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) + { + var payload = await GetNoncePayloadAsync(); + if (marketSymbol == null) + throw new APIException("BitBank requires marketSymbol when getting completed orders"); + payload.Add("pair", NormalizeMarketSymbol(marketSymbol)); + if (afterDate != null) + payload.Add("since", afterDate.ConvertInvariant()); + JToken token = await MakeJsonRequestAsync($"/user/spot/trade_history", baseUrl: BaseUrlPrivate, payload: payload); + return token["trades"].Select(t => TradeHistoryToExchangeOrderResult(t)); + } + + /// + /// Bitbank does not support withdrawing to arbitrary address (for security reason). + /// We must first register address from its web form. + /// So we will call two methods here. + /// 1. Get address from already registered account. (fail if does not exist) + /// 2. Withdraw to that address. + /// + /// + /// + protected override async Task OnWithdrawAsync(ExchangeWithdrawalRequest withdrawalRequest) + { + var asset = withdrawalRequest.Currency.ToLowerInvariant(); + var payload1 = await GetNoncePayloadAsync(); + payload1.Add("asset", asset); + JToken token1 = await MakeJsonRequestAsync($"/user/withdrawal_account", baseUrl: BaseUrlPrivate, payload: payload1); + if (!token1["accounts"].ToArray().Any(a => a["address"].ToStringInvariant() == withdrawalRequest.Address)) + throw new APIException($"Could not withdraw to address {withdrawalRequest.Address}! You must register the address from web form first."); + + var uuid = token1["uuid"].ToStringInvariant(); + + var payload2 = await GetNoncePayloadAsync(); + payload2.Add("asset", asset); + payload2.Add("amount", withdrawalRequest.Amount); + payload2.Add("uuid", uuid); + JToken token2 = await MakeJsonRequestAsync($"/user/request_withdrawal", baseUrl: BaseUrlPrivate, payload: payload2, requestMethod: "POST"); + var resp = new ExchangeWithdrawalResponse + { + Id = token2["txid"].ToStringInvariant() + }; + var status = token2["status"].ToStringInvariant(); + resp.Success = status != "REJECTED" && status != "CANCELED"; + resp.Message = "{" + $"label:{token2["label"]}, fee:{token2["fee"]}" + "}"; + return resp; + } + + #endregion Private APIs + + /// + /// BitBank does not support API for this one. So hard-code it. + /// + /// + protected override Task> OnGetMarketSymbolsAsync() + { + return Task.FromResult(new List { + "btc_jpy", + "xrp_jpy", + "ltc_btc", + "eth_btc", + "mona_jpy", + "mona_btc", + "bcc_jpy", + "bcc_btc" + }.AsEnumerable()); + } + + // protected override Task> OnGetCurrenciesAsync() => throw new NotImplementedException(); + + // protected override Task> OnGetMarketSymbolsMetadataAsync() => throw new NotImplementedException(); + // protected override Task OnGetDepositAddressAsync(string currency, bool forceRegenerate = false) => throw new NotImplementedException(); + // protected override Task> OnGetDepositHistoryAsync(string currency) => throw new NotImplementedException(); + // protected override Task> OnGetFeesAsync() => throw new NotImplementedException(); + protected override async Task> OnGetAmountsAvailableToTradeAsync() + => await OnGetAmountsAsyncCore("free_amount"); + + /// + /// Bitbank does not support placing several orders at once, so we will just run `PlaceOrderAsync` for each orders. + /// + /// + /// + protected override async Task OnPlaceOrdersAsync(params ExchangeOrderRequest[] order) + { + var resp = new List(); + foreach (var o in order) + resp.Add(await this.PlaceOrderAsync(o)); + return resp.ToArray(); + } + + // protected override Task> OnGetWithdrawHistoryAsync(string currency) => throw new NotImplementedException(); + // protected override Task> OnGetMarginAmountsAvailableToTradeAsync(bool includeZeroBalances) => throw new NotImplementedException(); + // protected override Task OnGetOpenPositionAsync(string marketSymbol) => throw new NotImplementedException(); + // protected override Task OnCloseMarginPositionAsync(string marketSymbol) => throw new NotImplementedException(); + /* */ - protected override Uri ProcessRequestUrl(UriBuilder url, Dictionary payload, string method) - { - if (CanMakeAuthenticatedRequest(payload) && method == "GET" && payload.Count != 0) - { - var realPayload = new Dictionary(); - payload.CopyTo(realPayload); - realPayload.Remove("nonce"); - CryptoUtility.AppendPayloadToQuery(url, realPayload); - } - return base.ProcessRequestUrl(url, payload, method); - } - - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) - { - if (CanMakeAuthenticatedRequest(payload)) - { - // convert nonce to long, trim off milliseconds - var nonce = payload["nonce"].ConvertInvariant(); - payload.Remove("nonce"); - var stringToCommit = String.Empty; - if (request.Method == "POST") - { - var msg = CryptoUtility.GetJsonForPayload(payload); - stringToCommit = $"{nonce}{msg}"; - await request.WritePayloadJsonToRequestAsync(payload); - } - else if (request.Method == "GET") - { - stringToCommit = $"{nonce}{request.RequestUri.PathAndQuery}"; - } - else - { - throw new APIException($"BitBank does not support {request.Method} as its HTTP method!"); - } - string signature = CryptoUtility.SHA256Sign(stringToCommit, CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); - - request.AddHeader("ACCESS-NONCE", nonce.ToStringInvariant()); - request.AddHeader("ACCESS-KEY", PublicApiKey.ToUnsecureString()); - request.AddHeader("ACCESS-SIGNATURE", signature); - } - return; - } - - private async Task ParseTickerAsync(string symbol, JToken token) - { - return await this.ParseTickerAsync(token, symbol, "sell", "buy", "last", "vol", quoteVolumeKey: null, "timestamp", TimestampType.UnixMilliseconds); - } - - private string FormatPeriod(int ps) - { - if (ps < 0) - throw new APIException("Can not specify negative time for period"); - if (ps < 60) - return "1min"; - if (ps < 300) - return "5min"; - if (ps < 900) - return "15min"; - if (ps < 1800) - return "30min"; - else - return "1hour"; - /* These are not working + protected override Uri ProcessRequestUrl(UriBuilder url, Dictionary payload, string method) + { + if (CanMakeAuthenticatedRequest(payload) && method == "GET" && payload.Count != 0) + { + var realPayload = new Dictionary(); + payload.CopyTo(realPayload); + realPayload.Remove("nonce"); + CryptoUtility.AppendPayloadToQuery(url, realPayload); + } + return base.ProcessRequestUrl(url, payload, method); + } + + protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + { + if (CanMakeAuthenticatedRequest(payload)) + { + // convert nonce to long, trim off milliseconds + var nonce = payload["nonce"].ConvertInvariant(); + payload.Remove("nonce"); + var stringToCommit = String.Empty; + if (request.Method == "POST") + { + var msg = CryptoUtility.GetJsonForPayload(payload); + stringToCommit = $"{nonce}{msg}"; + await request.WritePayloadJsonToRequestAsync(payload); + } + else if (request.Method == "GET") + { + stringToCommit = $"{nonce}{request.RequestUri.PathAndQuery}"; + } + else + { + throw new APIException($"BitBank does not support {request.Method} as its HTTP method!"); + } + string signature = CryptoUtility.SHA256Sign(stringToCommit, CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); + + request.AddHeader("ACCESS-NONCE", nonce.ToStringInvariant()); + request.AddHeader("ACCESS-KEY", PublicApiKey.ToUnsecureString()); + request.AddHeader("ACCESS-SIGNATURE", signature); + } + return; + } + + private async Task ParseTickerAsync(string symbol, JToken token) + { + return await this.ParseTickerAsync(token, symbol, "sell", "buy", "last", "vol", quoteVolumeKey: null, "timestamp", TimestampType.UnixMilliseconds); + } + + private string FormatPeriod(int ps) + { + if (ps < 0) + throw new APIException("Can not specify negative time for period"); + if (ps < 60) + return "1min"; + if (ps < 300) + return "5min"; + if (ps < 900) + return "15min"; + if (ps < 1800) + return "30min"; + else + return "1hour"; + /* These are not working if (ps < 3600 * 4) return "4hour"; if (ps < 3600 * 8) @@ -330,81 +336,87 @@ private string FormatPeriod(int ps) else return "1day"; */ - } - - private ExchangeOrderResult ParseOrder(JToken token) - { - var res = ParseOrderCore(token); - res.Amount = token["executed_amount"].ConvertInvariant(); - res.AveragePrice = token["averate_price"].ConvertInvariant(); - res.AmountFilled = token["executed_amount"].ConvertInvariant(); - res.OrderDate = token["ordered_at"].ConvertInvariant().UnixTimeStampToDateTimeMilliseconds(); - switch (token["status"].ToStringInvariant()) - { - case "UNFILLED": - res.Result = ExchangeAPIOrderResult.Pending; - break; - case "PARTIALLY_FILLED": - res.Result = ExchangeAPIOrderResult.FilledPartially; - break; - case "FULLY_FILLED": - res.Result = ExchangeAPIOrderResult.Filled; - break; - case "CANCELED_UNFILLED": - res.Result = ExchangeAPIOrderResult.Canceled; - break; - case "CANCELED_PARTIALLY_FILLED": - res.Result = ExchangeAPIOrderResult.FilledPartiallyAndCancelled; - break; - default: - res.Result = ExchangeAPIOrderResult.Unknown; - break; - } - return res; - } - - private ExchangeOrderResult TradeHistoryToExchangeOrderResult(JToken token) - { - var res = ParseOrderCore(token); - res.TradeId = token["trade_id"].ToStringInvariant(); - res.Amount = token["amount"].ConvertInvariant(); - res.AmountFilled = res.Amount; - res.Fees = token["fee_amount_base"].ConvertInvariant(); - res.Result = ExchangeAPIOrderResult.Filled; - res.Message = token["maker_taker"].ToStringInvariant(); - return res; - } - - // Parse common part of two kinds of response - // 1. CompletedOrder details - // 2. GetOrder, PostOrder - private ExchangeOrderResult ParseOrderCore(JToken token) - { - var res = new ExchangeOrderResult - { - OrderId = token["order_id"].ToStringInvariant(), - MarketSymbol = token["pair"].ToStringInvariant(), - IsBuy = token["side"].ToStringInvariant() == "buy" - }; - res.Fees = token["type"].ToStringInvariant() == "limit" ? MakerFee * res.Amount : TakerFee * res.Amount; - res.Price = token["price"].ConvertInvariant(); - res.FillDate = token["executed_at"] == null ? default : token["executed_at"].ConvertInvariant().UnixTimeStampToDateTimeMilliseconds(); - res.FeesCurrency = res.MarketSymbol.Substring(0, 3); - return res; - } - - private async Task> OnGetAmountsAsyncCore(string type) - { - JToken token = await MakeJsonRequestAsync($"/user/assets", baseUrl: BaseUrlPrivate, payload: await GetNoncePayloadAsync(), requestMethod: "GET"); - Dictionary balances = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (JToken assets in token["assets"]) - { - decimal amount = assets[type].ConvertInvariant(); - if (amount > 0m) - balances[assets["assets"].ToStringInvariant()] = amount; - } - return balances; - } - } - public partial class ExchangeName { public const string BitBank = "BitBank"; } + } + + private ExchangeOrderResult ParseOrder(JToken token) + { + var res = ParseOrderCore(token); + res.Amount = token["executed_amount"].ConvertInvariant(); + res.AveragePrice = token["averate_price"].ConvertInvariant(); + res.AmountFilled = token["executed_amount"].ConvertInvariant(); + res.OrderDate = token["ordered_at"].ConvertInvariant().UnixTimeStampToDateTimeMilliseconds(); + switch (token["status"].ToStringInvariant()) + { + case "UNFILLED": + res.Result = ExchangeAPIOrderResult.Pending; + break; + + case "PARTIALLY_FILLED": + res.Result = ExchangeAPIOrderResult.FilledPartially; + break; + + case "FULLY_FILLED": + res.Result = ExchangeAPIOrderResult.Filled; + break; + + case "CANCELED_UNFILLED": + res.Result = ExchangeAPIOrderResult.Canceled; + break; + + case "CANCELED_PARTIALLY_FILLED": + res.Result = ExchangeAPIOrderResult.FilledPartiallyAndCancelled; + break; + + default: + res.Result = ExchangeAPIOrderResult.Unknown; + break; + } + return res; + } + + private ExchangeOrderResult TradeHistoryToExchangeOrderResult(JToken token) + { + var res = ParseOrderCore(token); + res.TradeId = token["trade_id"].ToStringInvariant(); + res.Amount = token["amount"].ConvertInvariant(); + res.AmountFilled = res.Amount; + res.Fees = token["fee_amount_base"].ConvertInvariant(); + res.Result = ExchangeAPIOrderResult.Filled; + res.Message = token["maker_taker"].ToStringInvariant(); + return res; + } + + // Parse common part of two kinds of response + // 1. CompletedOrder details + // 2. GetOrder, PostOrder + private ExchangeOrderResult ParseOrderCore(JToken token) + { + var res = new ExchangeOrderResult + { + OrderId = token["order_id"].ToStringInvariant(), + MarketSymbol = token["pair"].ToStringInvariant(), + IsBuy = token["side"].ToStringInvariant() == "buy" + }; + res.Fees = token["type"].ToStringInvariant() == "limit" ? MakerFee * res.Amount : TakerFee * res.Amount; + res.Price = token["price"].ConvertInvariant(); + res.FillDate = token["executed_at"] == null ? default : token["executed_at"].ConvertInvariant().UnixTimeStampToDateTimeMilliseconds(); + res.FeesCurrency = res.MarketSymbol.Substring(0, 3); + return res; + } + + private async Task> OnGetAmountsAsyncCore(string type) + { + JToken token = await MakeJsonRequestAsync($"/user/assets", baseUrl: BaseUrlPrivate, payload: await GetNoncePayloadAsync(), requestMethod: "GET"); + Dictionary balances = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (JToken assets in token["assets"]) + { + decimal amount = assets[type].ConvertInvariant(); + if (amount > 0m) + balances[assets["assets"].ToStringInvariant()] = amount; + } + return balances; + } + } + + public partial class ExchangeName { public const string BitBank = "BitBank"; } } diff --git a/src/ExchangeSharp/API/Exchanges/Bitfinex/ExchangeBitfinexAPI.cs b/src/ExchangeSharp/API/Exchanges/Bitfinex/ExchangeBitfinexAPI.cs index c0dda1ff3..4b25efc45 100644 --- a/src/ExchangeSharp/API/Exchanges/Bitfinex/ExchangeBitfinexAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Bitfinex/ExchangeBitfinexAPI.cs @@ -12,7 +12,8 @@ The above copyright notice and this permission notice shall be included in all c using System.Runtime.InteropServices; -namespace ExchangeSharp { +namespace ExchangeSharp +{ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; @@ -24,7 +25,7 @@ namespace ExchangeSharp { using System.Text.RegularExpressions; using System.Threading.Tasks; - public sealed partial class ExchangeBitfinexAPI :ExchangeAPI + public sealed partial class ExchangeBitfinexAPI : ExchangeAPI { public override string BaseUrl { get; set; } = "https://api.bitfinex.com/v2"; public override string BaseUrlWebSocket { get; set; } = "wss://api.bitfinex.com/ws"; @@ -39,7 +40,8 @@ private ExchangeBitfinexAPI() RateLimit = new RateGate(1, TimeSpan.FromSeconds(6.0)); // List is from "Withdrawal Types" section https://docs.bitfinex.com/v1/reference#rest-auth-withdrawal - DepositMethodLookup = new Dictionary(StringComparer.OrdinalIgnoreCase) { + DepositMethodLookup = new Dictionary(StringComparer.OrdinalIgnoreCase) + { ["AVT"] = "aventus", ["BCH"] = "bcash", ["BTC"] = "bitcoin", @@ -79,7 +81,8 @@ protected internal override async Task> OnGetMarketS var markets = new List(); JToken allPairs = await MakeJsonRequestAsync("/symbols_details", BaseUrlV1); Match m; - foreach(JToken pair in allPairs) { + foreach (JToken pair in allPairs) + { var market = new ExchangeMarket { IsActive = true, @@ -90,17 +93,21 @@ protected internal override async Task> OnGetMarketS }; var pairPropertyVal = pair["pair"].ToStringUpperInvariant(); m = Regex.Match(pairPropertyVal, "^(BTC|USD|ETH|GBP|JPY|EUR|EOS)"); - if(m.Success) { + if (m.Success) + { market.BaseCurrency = m.Value; market.QuoteCurrency = pairPropertyVal.Substring(m.Length); } - else { + else + { m = Regex.Match(pairPropertyVal, "(BTC|USD|ETH|GBP|JPY|EUR|EOS)$"); - if(m.Success) { + if (m.Success) + { market.BaseCurrency = pairPropertyVal.Substring(0, m.Index); market.QuoteCurrency = m.Value; } - else { + else + { // TODO: Figure out a nicer way to handle newly added pairs market.BaseCurrency = pairPropertyVal.Substring(0, 3); market.QuoteCurrency = pairPropertyVal.Substring(3); @@ -123,9 +130,11 @@ protected override async Task>> { List> tickers = new List>(); IReadOnlyDictionary marketsBySymbol = (await GetMarketSymbolsMetadataAsync()).ToDictionary(market => market.MarketSymbol, market => market); - if(marketsBySymbol != null && marketsBySymbol.Count != 0) { + if (marketsBySymbol != null && marketsBySymbol.Count != 0) + { StringBuilder symbolString = new StringBuilder(); - foreach(var marketSymbol in marketsBySymbol.Keys) { + foreach (var marketSymbol in marketsBySymbol.Keys) + { symbolString.Append('t'); symbolString.Append(marketSymbol.ToUpperInvariant()); symbolString.Append(','); @@ -133,8 +142,10 @@ protected override async Task>> symbolString.Length--; JToken token = await MakeJsonRequestAsync("/tickers?symbols=" + symbolString); DateTime now = CryptoUtility.UtcNow; - foreach(JArray array in token) { + foreach (JArray array in token) + { #region Return Values + //[ // SYMBOL, // BID, float Price of last highest bid @@ -148,15 +159,20 @@ protected override async Task>> // HIGH, float Daily high // LOW float Daily low //] - #endregion + + #endregion Return Values + var marketSymbol = array[0].ToStringInvariant().Substring(1); var market = marketsBySymbol[marketSymbol.ToLowerInvariant()]; - tickers.Add(new KeyValuePair(marketSymbol, new ExchangeTicker { + tickers.Add(new KeyValuePair(marketSymbol, new ExchangeTicker + { MarketSymbol = marketSymbol, + ApiResponse = token, Ask = array[3].ConvertInvariant(), Bid = array[1].ConvertInvariant(), Last = array[7].ConvertInvariant(), - Volume = new ExchangeVolume { + Volume = new ExchangeVolume + { QuoteCurrencyVolume = array[8].ConvertInvariant() * array[7].ConvertInvariant(), QuoteCurrency = market.QuoteCurrency, BaseCurrencyVolume = array[8].ConvertInvariant(), @@ -172,27 +188,35 @@ protected override async Task>> protected override async Task OnGetTickersWebSocketAsync(Action>> callback, params string[] marketSymbols) { Dictionary channelIdToSymbol = new Dictionary(); - return await ConnectWebSocketAsync(string.Empty, async (_socket, msg) => { + return await ConnectWebSocketAsync(string.Empty, async (_socket, msg) => + { JToken token = JToken.Parse(msg.ToStringFromUTF8()); - if(token is JArray array) { - if(array.Count > 10) { + if (token is JArray array) + { + if (array.Count > 10) + { List> tickerList = new List>(); - if(channelIdToSymbol.TryGetValue(array[0].ConvertInvariant(), out string symbol)) { + if (channelIdToSymbol.TryGetValue(array[0].ConvertInvariant(), out string symbol)) + { ExchangeTicker ticker = await ParseTickerWebSocketAsync(symbol, array); - if(ticker != null) { + if (ticker != null) + { callback(new KeyValuePair[] { new KeyValuePair(symbol, ticker) }); } } } } - else if(token["event"].ToStringInvariant() == "subscribed" && token["channel"].ToStringInvariant() == "ticker") { + else if (token["event"].ToStringInvariant() == "subscribed" && token["channel"].ToStringInvariant() == "ticker") + { // {"event":"subscribed","channel":"ticker","chanId":1,"pair":"BTCUSD"} int channelId = token["chanId"].ConvertInvariant(); channelIdToSymbol[channelId] = token["pair"].ToStringInvariant(); } - }, async (_socket) => { + }, async (_socket) => + { marketSymbols = marketSymbols == null || marketSymbols.Length == 0 ? (await GetMarketSymbolsAsync()).ToArray() : marketSymbols; - foreach(var marketSymbol in marketSymbols) { + foreach (var marketSymbol in marketSymbols) + { await _socket.SendMessageAsync(new { @event = "subscribe", channel = "ticker", pair = marketSymbol }); } }); @@ -201,37 +225,50 @@ protected override async Task OnGetTickersWebSocketAsync(Action OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) { Dictionary channelIdToSymbol = new Dictionary(); - if(marketSymbols == null || marketSymbols.Length == 0) { + if (marketSymbols == null || marketSymbols.Length == 0) + { marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); } return await ConnectWebSocketAsync("/2", async (_socket, msg) => //use websocket V2 (beta, but millisecond timestamp) { JToken token = JToken.Parse(msg.ToStringFromUTF8()); - if(token is JArray array) { - if(token[1].ToStringInvariant() == "hb") { + if (token is JArray array) + { + if (token[1].ToStringInvariant() == "hb") + { // heartbeat } - else if(token.Last.Last.HasValues == false) { + else if (token.Last.Last.HasValues == false) + { //[29654, "tu", [270343572, 1532012917722, -0.003, 7465.636738]] "te"=temp/intention to execute "tu"=confirmed and ID is definitive //chan id, -- , [ID , timestamp , amount, price ]] - if(channelIdToSymbol.TryGetValue(array[0].ConvertInvariant(), out string symbol)) { - if(token[1].ToStringInvariant() == "tu") { + if (channelIdToSymbol.TryGetValue(array[0].ConvertInvariant(), out string symbol)) + { + if (token[1].ToStringInvariant() == "tu") + { ExchangeTrade trade = ParseTradeWebSocket(token.Last); - if(trade != null) { + if (trade != null) + { await callback(new KeyValuePair(symbol, trade)); } } } } - else { + else + { //parse snapshot here if needed - if(channelIdToSymbol.TryGetValue(array[0].ConvertInvariant(), out string symbol)) { - if(array[1] is JArray subarray) { - for(int i = 0; i < subarray.Count - 1; i++) { + if (channelIdToSymbol.TryGetValue(array[0].ConvertInvariant(), out string symbol)) + { + if (array[1] is JArray subarray) + { + for (int i = 0; i < subarray.Count - 1; i++) + { ExchangeTrade trade = ParseTradeWebSocket(subarray[i]); - if(trade != null) { + if (trade != null) + { trade.Flags |= ExchangeTradeFlags.IsFromSnapshot; - if(i == subarray.Count - 1) { + if (i == subarray.Count - 1) + { trade.Flags |= ExchangeTradeFlags.IsLastFromSnapshot; } await callback(new KeyValuePair(symbol, trade)); @@ -241,13 +278,16 @@ protected override async Task OnGetTradesWebSocketAsync(Func(); channelIdToSymbol[channelId] = token["pair"].ToStringInvariant(); } - }, async (_socket) => { - foreach(var marketSymbol in marketSymbols) { + }, async (_socket) => + { + foreach (var marketSymbol in marketSymbols) + { await _socket.SendMessageAsync(new { @event = "subscribe", channel = "trades", symbol = marketSymbol }); } }); @@ -256,7 +296,8 @@ protected override async Task OnGetTradesWebSocketAsync(Func(); - return new ExchangeTrade { + return new ExchangeTrade + { Id = token[0].ToStringInvariant(), Timestamp = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(token[1].ConvertInvariant()), Amount = Math.Abs(amount), @@ -270,19 +311,20 @@ protected override async Task OnGetOrderBookAsync(string mark ExchangeOrderBook orders = new ExchangeOrderBook(); decimal[][] books = await MakeJsonRequestAsync("/book/t" + marketSymbol + "/P0?limit_bids=" + maxCount.ToStringInvariant() + "limit_asks=" + maxCount.ToStringInvariant()); - foreach(decimal[] book in books) { - if(book[2] > 0m) { + foreach (decimal[] book in books) + { + if (book[2] > 0m) + { orders.Bids[book[0]] = new ExchangeOrderPrice { Amount = book[2], Price = book[0] }; } - else { + else + { orders.Asks[book[0]] = new ExchangeOrderPrice { Amount = -book[2], Price = book[0] }; } } return orders; } - - protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null) { List trades = new List(); @@ -290,19 +332,20 @@ protected override async Task> OnGetRecentTradesAsync //https://docs.bitfinex.com/reference#rest-public-trades note bitfinex max limit = 10000 int requestLimit = (limit == null || limit < 1 || limit > 10000) ? 10000 : (int)limit; - string url = "/trades/t" + marketSymbol + "/hist?sort=" + "-1" + "&limit=" + requestLimit; + string url = "/trades/t" + marketSymbol + "/hist?sort=" + "-1" + "&limit=" + requestLimit; tradeChunk = await MakeJsonRequestAsync(url); - if(tradeChunk != null || tradeChunk.Length > 0) { - //tradeChunk = tradeChunk.Reverse(); - foreach(decimal[] tradeChunkPiece in tradeChunk) { + if (tradeChunk != null || tradeChunk.Length > 0) + { + //tradeChunk = tradeChunk.Reverse(); + foreach (decimal[] tradeChunkPiece in tradeChunk) + { trades.Add(new ExchangeTrade { Amount = Math.Abs(tradeChunkPiece[2]), IsBuy = tradeChunkPiece[2] > 0m, Price = tradeChunkPiece[3], Timestamp = CryptoUtility.UnixTimeStampToDateTimeMilliseconds((double)tradeChunkPiece[1]), Id = tradeChunkPiece[0].ToStringInvariant() }); } } return trades; } - protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) { const int maxCount = 100; @@ -310,27 +353,34 @@ protected override async Task OnGetHistoricalTradesAsync(Func trades = new List(); decimal[][] tradeChunk; - while(true) { + while (true) + { url = baseUrl; - if(startDate != null) { + if (startDate != null) + { url += "&start=" + (long)CryptoUtility.UnixTimestampFromDateTimeMilliseconds(startDate.Value); } tradeChunk = await MakeJsonRequestAsync(url); - if(tradeChunk == null || tradeChunk.Length == 0) { + if (tradeChunk == null || tradeChunk.Length == 0) + { break; } - if(startDate != null) { + if (startDate != null) + { startDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds((double)tradeChunk[tradeChunk.Length - 1][1]); } - foreach(decimal[] tradeChunkPiece in tradeChunk) { + foreach (decimal[] tradeChunkPiece in tradeChunk) + { trades.Add(new ExchangeTrade { Amount = Math.Abs(tradeChunkPiece[2]), IsBuy = tradeChunkPiece[2] > 0m, Price = tradeChunkPiece[3], Timestamp = CryptoUtility.UnixTimeStampToDateTimeMilliseconds((double)tradeChunkPiece[1]), Id = tradeChunkPiece[0].ToStringInvariant() }); } trades.Sort((t1, t2) => t1.Timestamp.CompareTo(t2.Timestamp)); - if(!callback(trades)) { + if (!callback(trades)) + { break; } trades.Clear(); - if(tradeChunk.Length < maxCount || startDate == null) { + if (tradeChunk.Length < maxCount || startDate == null) + { break; } await Task.Delay(5000); @@ -343,19 +393,22 @@ protected override async Task> OnGetCandlesAsync(strin List candles = new List(); string periodString = PeriodSecondsToString(periodSeconds); string url = "/candles/trade:" + periodString + ":t" + marketSymbol + "/hist?sort=1"; - if(startDate != null || endDate != null) { + if (startDate != null || endDate != null) + { endDate = endDate ?? CryptoUtility.UtcNow; startDate = startDate ?? endDate.Value.Subtract(TimeSpan.FromDays(1.0)); url += "&start=" + ((long)startDate.Value.UnixTimestampFromDateTimeMilliseconds()).ToStringInvariant(); url += "&end=" + ((long)endDate.Value.UnixTimestampFromDateTimeMilliseconds()).ToStringInvariant(); } - if(limit != null) { + if (limit != null) + { url += "&limit=" + (limit.Value.ToStringInvariant()); } JToken token = await MakeJsonRequestAsync(url); /* MTS, OPEN, CLOSE, HIGH, LOW, VOL */ - foreach(JToken candle in token) { + foreach (JToken candle in token) + { candles.Add(this.ParseCandle(candle, marketSymbol, periodSeconds, 1, 3, 4, 2, 0, TimestampType.UnixMilliseconds, 5)); } @@ -371,10 +424,13 @@ public async Task> OnGetAmountsAsync(string type) { Dictionary lookup = new Dictionary(StringComparer.OrdinalIgnoreCase); JArray obj = await MakeJsonRequestAsync("/balances", BaseUrlV1, await GetNoncePayloadAsync()); - foreach(JToken token in obj) { - if(token["type"].ToStringInvariant() == type) { + foreach (JToken token in obj) + { + if (token["type"].ToStringInvariant() == type) + { decimal amount = token["amount"].ConvertInvariant(); - if(amount > 0m) { + if (amount > 0m) + { lookup[token["currency"].ToStringInvariant()] = amount; } } @@ -392,10 +448,13 @@ protected override async Task> OnGetAmountsAvailable { Dictionary lookup = new Dictionary(StringComparer.OrdinalIgnoreCase); JArray obj = await MakeJsonRequestAsync("/balances", BaseUrlV1, await GetNoncePayloadAsync()); - foreach(JToken token in obj) { - if(token["type"].ToStringInvariant() == "exchange") { + foreach (JToken token in obj) + { + if (token["type"].ToStringInvariant() == "exchange") + { decimal amount = token["available"].ConvertInvariant(); - if(amount > 0m) { + if (amount > 0m) + { lookup[token["currency"].ToStringInvariant()] = amount; } } @@ -411,17 +470,21 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd payload["amount"] = (await ClampOrderQuantity(marketSymbol, order.Amount)).ToStringInvariant(); payload["side"] = (order.IsBuy ? "buy" : "sell"); - if(order.IsMargin) { + if (order.IsMargin) + { payload["type"] = order.OrderType == OrderType.Market ? "market" : "limit"; } - else { + else + { payload["type"] = order.OrderType == OrderType.Market ? "exchange market" : "exchange limit"; } - if(order.OrderType != OrderType.Market) { + if (order.OrderType != OrderType.Market) + { payload["price"] = (await ClampOrderPrice(marketSymbol, order.Price)).ToStringInvariant(); } - else { + else + { payload["price"] = "1"; } order.ExtraParameters.CopyTo(payload); @@ -431,7 +494,8 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null) { - if(string.IsNullOrWhiteSpace(orderId)) { + if (string.IsNullOrWhiteSpace(orderId)) + { return null; } @@ -448,7 +512,8 @@ protected override async Task> OnGetOpenOrderDe protected override async Task> OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) { - if(string.IsNullOrWhiteSpace(marketSymbol)) { + if (string.IsNullOrWhiteSpace(marketSymbol)) + { // HACK: Bitfinex does not provide a way to get all historical order details beyond a few days in one call, so we have to // get the historical details one by one for each symbol. var symbols = (await GetMarketSymbolsAsync()).Where(s => s.IndexOf("usd", StringComparison.OrdinalIgnoreCase) < 0 && s.IndexOf("btc", StringComparison.OrdinalIgnoreCase) >= 0); @@ -461,18 +526,23 @@ protected override async Task> OnGetCompletedOr protected override Task OnGetCompletedOrderDetailsWebSocketAsync(Action callback) { - return ConnectWebSocketAsync(string.Empty, (_socket, msg) => { + return ConnectWebSocketAsync(string.Empty, (_socket, msg) => + { JToken token = JToken.Parse(msg.ToStringFromUTF8()); - if(token[1].ToStringInvariant() == "hb") { + if (token[1].ToStringInvariant() == "hb") + { // heartbeat } - else if(token is JArray array && array.Count > 1 && array[2] is JArray && array[1].ToStringInvariant() == "os") { - foreach(JToken orderToken in array[2]) { + else if (token is JArray array && array.Count > 1 && array[2] is JArray && array[1].ToStringInvariant() == "os") + { + foreach (JToken orderToken in array[2]) + { callback.Invoke(ParseOrderWebSocket(orderToken)); } } return Task.CompletedTask; - }, async (_socket) => { + }, async (_socket) => + { object nonce = await GenerateNonceAsync(); string authPayload = "AUTH" + nonce; string signature = CryptoUtility.SHA384Sign(authPayload, PrivateApiKey.ToUnsecureString()); @@ -492,22 +562,25 @@ protected override async Task OnCancelOrderAsync(string orderId, string marketSy { Dictionary payload = await GetNoncePayloadAsync(); payload["order_id"] = orderId.ConvertInvariant(); - var token= await MakeJsonRequestAsync("/order/cancel", BaseUrlV1, payload); + var token = await MakeJsonRequestAsync("/order/cancel", BaseUrlV1, payload); } protected override async Task OnGetDepositAddressAsync(string currency, bool forceRegenerate = false) { - if(string.IsNullOrWhiteSpace(currency)) { + if (string.IsNullOrWhiteSpace(currency)) + { throw new ArgumentNullException(nameof(currency)); } // IOTA addresses should never be used more than once - if(currency.Equals("MIOTA", StringComparison.OrdinalIgnoreCase)) { + if (currency.Equals("MIOTA", StringComparison.OrdinalIgnoreCase)) + { forceRegenerate = true; } // symbol needs to be translated to full name of coin: bitcoin/litecoin/ethereum - if(!DepositMethodLookup.TryGetValue(currency, out string fullName)) { + if (!DepositMethodLookup.TryGetValue(currency, out string fullName)) + { fullName = currency.ToLowerInvariant(); } @@ -521,11 +594,13 @@ protected override async Task OnGetDepositAddressAsync(s { Currency = result["currency"].ToStringInvariant(), }; - if(result["address_pool"] != null) { + if (result["address_pool"] != null) + { details.Address = result["address_pool"].ToStringInvariant(); details.AddressTag = result["address"].ToStringLowerInvariant(); } - else { + else + { details.Address = result["address"].ToStringInvariant(); } @@ -537,7 +612,8 @@ protected override async Task OnGetDepositAddressAsync(s /// Collection of ExchangeCoinTransfers protected override async Task> OnGetDepositHistoryAsync(string currency) { - if(string.IsNullOrWhiteSpace(currency)) { + if (string.IsNullOrWhiteSpace(currency)) + { throw new ArgumentNullException(nameof(currency)); } @@ -546,8 +622,10 @@ protected override async Task> OnGetDepositHist JToken result = await MakeJsonRequestAsync("/history/movements", BaseUrlV1, payload, "POST"); var transactions = new List(); - foreach(JToken token in result) { - if(!string.Equals(token["type"].ToStringUpperInvariant(), "DEPOSIT")) { + foreach (JToken token in result) + { + if (!string.Equals(token["type"].ToStringUpperInvariant(), "DEPOSIT")) + { continue; } @@ -562,13 +640,16 @@ protected override async Task> OnGetDepositHist }; string status = token["status"].ToStringUpperInvariant(); - switch(status) { + switch (status) + { case "COMPLETED": transaction.Status = TransactionStatus.Complete; break; + case "UNCONFIRMED": transaction.Status = TransactionStatus.Processing; break; + default: transaction.Status = TransactionStatus.Unknown; transaction.Notes += ", Unknown transaction status " + status; @@ -590,7 +671,8 @@ protected override async Task> OnGetDepositHist /// Collection of ExchangeCoinTransfers protected override async Task> OnGetWithdrawHistoryAsync(string currency) { - if(string.IsNullOrWhiteSpace(currency)) { + if (string.IsNullOrWhiteSpace(currency)) + { throw new ArgumentNullException(nameof(currency)); } @@ -599,8 +681,10 @@ protected override async Task> OnGetWithdrawHis JToken result = await MakeJsonRequestAsync("/history/movements", BaseUrlV1, payload, "POST"); var transactions = new List(); - foreach(JToken token in result) { - if(!string.Equals(token["type"].ToStringUpperInvariant(), "WITHDRAWAL")) { + foreach (JToken token in result) + { + if (!string.Equals(token["type"].ToStringUpperInvariant(), "WITHDRAWAL")) + { continue; } @@ -615,13 +699,16 @@ protected override async Task> OnGetWithdrawHis }; string status = token["status"].ToStringUpperInvariant(); - switch(status) { + switch (status) + { case "COMPLETED": transaction.Status = TransactionStatus.Complete; break; + case "UNCONFIRMED": transaction.Status = TransactionStatus.Processing; break; + default: transaction.Status = TransactionStatus.Unknown; transaction.Notes += ", Unknown transaction status " + status; @@ -645,14 +732,17 @@ protected override async Task> OnGetWithdrawHis protected override async Task OnWithdrawAsync(ExchangeWithdrawalRequest withdrawalRequest) { // symbol needs to be translated to full name of coin: bitcoin/litecoin/ethereum - if(!DepositMethodLookup.TryGetValue(withdrawalRequest.Currency, out string fullName)) { + if (!DepositMethodLookup.TryGetValue(withdrawalRequest.Currency, out string fullName)) + { fullName = withdrawalRequest.Currency.ToLowerInvariant(); } // Bitfinex adds the fee on top of what you request to withdrawal - if(withdrawalRequest.TakeFeeFromAmount) { + if (withdrawalRequest.TakeFeeFromAmount) + { Dictionary fees = await GetWithdrawalFeesAsync(); - if(fees.TryGetValue(withdrawalRequest.Currency, out decimal feeAmt)) { + if (fees.TryGetValue(withdrawalRequest.Currency, out decimal feeAmt)) + { withdrawalRequest.Amount -= feeAmt; } } @@ -663,18 +753,21 @@ protected override async Task OnWithdrawAsync(Exchan payload["amount"] = withdrawalRequest.Amount.ToString(CultureInfo.InvariantCulture); // API throws if this is a number not a string payload["address"] = withdrawalRequest.Address; - if(!string.IsNullOrWhiteSpace(withdrawalRequest.AddressTag)) { + if (!string.IsNullOrWhiteSpace(withdrawalRequest.AddressTag)) + { payload["payment_id"] = withdrawalRequest.AddressTag; } - if(!string.IsNullOrWhiteSpace(withdrawalRequest.Description)) { + if (!string.IsNullOrWhiteSpace(withdrawalRequest.Description)) + { payload["account_name"] = withdrawalRequest.Description; } JToken result = await MakeJsonRequestAsync("/withdraw", BaseUrlV1, payload, "POST"); var resp = new ExchangeWithdrawalResponse(); - if(!string.Equals(result[0]["status"].ToStringInvariant(), "success", StringComparison.OrdinalIgnoreCase)) { + if (!string.Equals(result[0]["status"].ToStringInvariant(), "success", StringComparison.OrdinalIgnoreCase)) + { resp.Success = false; } @@ -685,11 +778,13 @@ protected override async Task OnWithdrawAsync(Exchan protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) { - if(CanMakeAuthenticatedRequest(payload)) { + if (CanMakeAuthenticatedRequest(payload)) + { request.Method = "POST"; request.AddHeader("content-type", "application/json"); request.AddHeader("accept", "application/json"); - if(request.RequestUri.AbsolutePath.StartsWith("/v2")) { + if (request.RequestUri.AbsolutePath.StartsWith("/v2")) + { string nonce = payload["nonce"].ToStringInvariant(); payload.Remove("nonce"); string json = JsonConvert.SerializeObject(payload); @@ -700,7 +795,8 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti request.AddHeader("bfx-signature", hexSha384); await CryptoUtility.WriteToRequestAsync(request, json); } - else { + else + { // bitfinex v1 doesn't put the payload in the post body it puts it in as a http header, so no need to write to request stream payload.Add("request", request.RequestUri.AbsolutePath); string json = JsonConvert.SerializeObject(payload); @@ -731,11 +827,15 @@ private async Task> GetOrderDetailsInternalV2(s payload["end"] = CryptoUtility.UtcNow.UnixTimestampFromDateTimeMilliseconds(); JToken result = await MakeJsonRequestAsync(url, null, payload); Dictionary> trades = new Dictionary>(StringComparer.OrdinalIgnoreCase); - if(result is JArray array) { - foreach(JToken token in array) { - if(string.IsNullOrWhiteSpace(marketSymbol) || token[1].ToStringInvariant() == "t" + marketSymbol) { + if (result is JArray array) + { + foreach (JToken token in array) + { + if (string.IsNullOrWhiteSpace(marketSymbol) || token[1].ToStringInvariant() == "t" + marketSymbol) + { string lookup = token[1].ToStringInvariant().Substring(1).ToLowerInvariant(); - if(!trades.TryGetValue(lookup, out List tradeList)) { + if (!trades.TryGetValue(lookup, out List tradeList)) + { tradeList = trades[lookup] = new List(); } tradeList.Add(token); @@ -750,9 +850,12 @@ private async Task> GetOrderDetailsInternalAsyn List orders = new List(); marketSymbol = NormalizeMarketSymbolV1(marketSymbol); JToken result = await MakeJsonRequestAsync(url, BaseUrlV1, await GetNoncePayloadAsync()); - if(result is JArray array) { - foreach(JToken token in array) { - if(marketSymbol == null || token["symbol"].ToStringInvariant() == marketSymbol) { + if (result is JArray array) + { + foreach (JToken token in array) + { + if (marketSymbol == null || token["symbol"].ToStringInvariant() == marketSymbol) + { orders.Add(ParseOrder(token)); } } @@ -763,23 +866,29 @@ private async Task> GetOrderDetailsInternalAsyn private async Task> GetOrderDetailsInternalV1(IEnumerable marketSymbols, DateTime? afterDate) { Dictionary orders = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach(string marketSymbol in marketSymbols) { + foreach (string marketSymbol in marketSymbols) + { string normalizedSymbol = NormalizeMarketSymbolV1(marketSymbol); Dictionary payload = await GetNoncePayloadAsync(); payload["symbol"] = normalizedSymbol; payload["limit_trades"] = 250; - if(afterDate != null) { + if (afterDate != null) + { payload["timestamp"] = afterDate.Value.UnixTimestampFromDateTimeSeconds().ToStringInvariant(); payload["until"] = CryptoUtility.UtcNow.UnixTimestampFromDateTimeSeconds().ToStringInvariant(); } JToken token = await MakeJsonRequestAsync("/mytrades", BaseUrlV1, payload); - foreach(JToken trade in token) { + foreach (JToken trade in token) + { ExchangeOrderResult subOrder = ParseTrade(trade, normalizedSymbol); - lock(orders) { - if(orders.TryGetValue(subOrder.OrderId, out ExchangeOrderResult baseOrder)) { + lock (orders) + { + if (orders.TryGetValue(subOrder.OrderId, out ExchangeOrderResult baseOrder)) + { baseOrder.AppendOrderWithOrder(subOrder); } - else { + else + { orders[subOrder.OrderId] = subOrder; } } @@ -793,7 +902,8 @@ private ExchangeOrderResult ParseOrder(JToken order) decimal amount = order["original_amount"].ConvertInvariant(); decimal amountFilled = order["executed_amount"].ConvertInvariant(); decimal price = order["price"].ConvertInvariant(); - return new ExchangeOrderResult { + return new ExchangeOrderResult + { Amount = amount, AmountFilled = amountFilled, Price = price, @@ -831,7 +941,8 @@ private ExchangeOrderResult ParseOrderWebSocket(JToken order) ACTIVE, EXECUTED @ PRICE(AMOUNT) e.g. "EXECUTED @ 107.6(-0.2)", PARTIALLY FILLED @ PRICE(AMOUNT), INSUFFICIENT MARGIN was: PARTIALLY FILLED @ PRICE(AMOUNT), CANCELED, CANCELED was: PARTIALLY FILLED @ PRICE(AMOUNT) */ string orderStatusString = order[5].ToStringInvariant().Split(' ')[0]; - return new ExchangeOrderResult { + return new ExchangeOrderResult + { Amount = amount, AmountFilled = amount, Price = order[6].ConvertInvariant(), @@ -851,7 +962,6 @@ private ExchangeOrderResult ParseOrderWebSocket(JToken order) private IEnumerable ParseOrderV2(Dictionary> trades) { - /* [ ID integer Trade database id @@ -868,9 +978,11 @@ FEE_CURRENCY string Fee currency ], */ - foreach(var kv in trades) { + foreach (var kv in trades) + { ExchangeOrderResult order = new ExchangeOrderResult { Result = ExchangeAPIOrderResult.Filled }; - foreach(JToken trade in kv.Value) { + foreach (JToken trade in kv.Value) + { ExchangeOrderResult append = new ExchangeOrderResult { MarketSymbol = kv.Key, OrderId = trade[3].ToStringInvariant() }; append.Amount = append.AmountFilled = Math.Abs(trade[4].ConvertInvariant()); append.Price = trade[7].ConvertInvariant(); @@ -899,7 +1011,8 @@ private ExchangeOrderResult ParseTrade(JToken trade, string symbol) "order_id":446913929 }] */ - return new ExchangeOrderResult { + return new ExchangeOrderResult + { Amount = trade["amount"].ConvertInvariant(), AmountFilled = trade["amount"].ConvertInvariant(), AveragePrice = trade["price"].ConvertInvariant(), @@ -922,7 +1035,8 @@ private async Task> GetWithdrawalFeesAsync() { JToken obj = await MakeJsonRequestAsync("/account_fees", BaseUrlV1, await GetNoncePayloadAsync()); var fees = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach(var jToken in obj["withdraw"]) { + foreach (var jToken in obj["withdraw"]) + { var prop = (JProperty)jToken; fees[prop.Name] = prop.Value.ConvertInvariant(); } diff --git a/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI_WebSocket.cs b/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI_WebSocket.cs index 243ca00c0..01f5c1ef2 100644 --- a/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI_WebSocket.cs +++ b/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI_WebSocket.cs @@ -32,7 +32,6 @@ namespace ExchangeSharp { public partial class ExchangeBittrexAPI { - #if HAS_SIGNALR /// @@ -78,7 +77,7 @@ public async Task SubscribeToExchangeDeltasAsync(Func } } - private BittrexWebSocketManager webSocket; + private BittrexWebSocketManager webSocket; public static string ReverseMarketNameForWS(string WebSocketFeedMarketName) { @@ -96,10 +95,11 @@ protected override async Task OnGetTickersWebSocketAsync(Action(StringComparer.OrdinalIgnoreCase); JToken token = JToken.Parse(json); token = token["D"]; foreach (JToken ticker in token) { - - string marketName = ReverseMarketNameForWS(ticker["M"].ToStringInvariant()); if (filter.Count != 0 && !filter.Contains(marketName)) { @@ -143,6 +142,7 @@ async Task innerCallback(string json) var t = new ExchangeTicker { MarketSymbol = marketName, + ApiResponse = ticker, Ask = ask, Bid = bid, Last = last, @@ -176,11 +176,12 @@ params string[] marketSymbols Task innerCallback(string json) { #region sample json + /* { MarketName : string, Nonce : int, - Buys: + Buys: [ { Type : int, @@ -188,7 +189,7 @@ Task innerCallback(string json) Quantity : decimal } ], - Sells: + Sells: [ { Type : int, @@ -196,7 +197,7 @@ Task innerCallback(string json) Quantity : decimal } ], - Fills: + Fills: [ { FillId : int, @@ -208,7 +209,8 @@ Task innerCallback(string json) ] } */ - #endregion + + #endregion sample json var ordersUpdates = JsonConvert.DeserializeObject(json); var book = new ExchangeOrderBook(); @@ -229,8 +231,8 @@ Task innerCallback(string json) return Task.CompletedTask; } - return await new BittrexWebSocketManager().SubscribeToExchangeDeltasAsync(innerCallback, marketSymbols); - } + return await new BittrexWebSocketManager().SubscribeToExchangeDeltasAsync(innerCallback, marketSymbols); + } protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) { @@ -261,19 +263,17 @@ async Task innerCallback(string json) #endif - protected override void OnDispose() - { - + protected override void OnDispose() + { #if HAS_SIGNALR - if (webSocket != null) - { - webSocket.Dispose(); - webSocket = null; - } + if (webSocket != null) + { + webSocket.Dispose(); + webSocket = null; + } #endif - - } - } + } + } } diff --git a/src/ExchangeSharp/API/Exchanges/Digifinex/ExchangeDigifinexAPI.cs b/src/ExchangeSharp/API/Exchanges/Digifinex/ExchangeDigifinexAPI.cs index b1f38094b..7f2459de9 100644 --- a/src/ExchangeSharp/API/Exchanges/Digifinex/ExchangeDigifinexAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Digifinex/ExchangeDigifinexAPI.cs @@ -9,587 +9,593 @@ namespace ExchangeSharp { - - public partial class ExchangeDigifinexAPI : ExchangeAPI - { - string[] Urls = - { - "openapi.digifinex.com", - "openapi.digifinex.vip", - "openapi.digifinex.xyz", - }; - string fastestUrl = null; - int failedUrlCount; - int successUrlCount; - - public override string BaseUrl { get; set; } = "https://openapi.digifinex.vip/v3"; - public override string BaseUrlWebSocket { get; set; } = "wss://openapi.digifinex.vip/ws/v1/"; - int websocketMessageId = 0; - string timeWindow; - TaskCompletionSource inited = new TaskCompletionSource(); + public partial class ExchangeDigifinexAPI : ExchangeAPI + { + private string[] Urls = + { + "openapi.digifinex.com", + "openapi.digifinex.vip", + "openapi.digifinex.xyz", + }; + + private string fastestUrl = null; + private int failedUrlCount; + private int successUrlCount; + + public override string BaseUrl { get; set; } = "https://openapi.digifinex.vip/v3"; + public override string BaseUrlWebSocket { get; set; } = "wss://openapi.digifinex.vip/ws/v1/"; + private int websocketMessageId = 0; + private string timeWindow; + private TaskCompletionSource inited = new TaskCompletionSource(); private ExchangeDigifinexAPI() - { - MarketSymbolSeparator = "_"; - MarketSymbolIsReversed = false; - MarketSymbolIsUppercase = true; - WebSocketOrderBookType = WebSocketOrderBookType.FullBookFirstThenDeltas; - NonceStyle = NonceStyle.UnixSeconds; - RateLimit = new RateGate(240, TimeSpan.FromMinutes(1)); - GetFastestUrl(); - } - - void GetFastestUrl() - { - var client = new HttpClient(); - foreach (var url in Urls) - { - var u = url; - client.GetAsync($"https://{u}").ContinueWith((t) => - { - if (t.Exception != null) - { - var count = Interlocked.Increment(ref failedUrlCount); - if (count == Urls.Length) - inited.SetException(new APIException("All digifinex URLs failed.")); - return; - } + { + MarketSymbolSeparator = "_"; + MarketSymbolIsReversed = false; + MarketSymbolIsUppercase = true; + WebSocketOrderBookType = WebSocketOrderBookType.FullBookFirstThenDeltas; + NonceStyle = NonceStyle.UnixSeconds; + RateLimit = new RateGate(240, TimeSpan.FromMinutes(1)); + GetFastestUrl(); + } + + private void GetFastestUrl() + { + var client = new HttpClient(); + foreach (var url in Urls) + { + var u = url; + client.GetAsync($"https://{u}").ContinueWith((t) => + { + if (t.Exception != null) + { + var count = Interlocked.Increment(ref failedUrlCount); + if (count == Urls.Length) + inited.SetException(new APIException("All digifinex URLs failed.")); + return; + } if (Interlocked.Increment(ref successUrlCount) == 1) - { - fastestUrl = u; - //Console.WriteLine($"Fastest url {GetHashCode()}: {u}"); - BaseUrl = $"https://{u}/v3"; - BaseUrlWebSocket = $"wss://{u}/ws/v1/"; - inited.SetResult(1); - } - }); - } - } - - #region ProcessRequest - - protected override async Task OnGetNonceOffset() - { - try - { - await inited.Task; - var start = CryptoUtility.UtcNow; - JToken token = await MakeJsonRequestAsync("/time"); - DateTime serverDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(token["server_time"].ConvertInvariant()); - var end = CryptoUtility.UtcNow; - var now = start + TimeSpan.FromMilliseconds((end - start).TotalMilliseconds); - var timeFaster = now - serverDate; - timeWindow = "30"; // max latency of 30s - NonceOffset = now - serverDate; // how much time to substract from Nonce when making a request - //Console.WriteLine($"NonceOffset {GetHashCode()}: {NonceOffset}"); - } - catch - { - throw; - } - } - - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) - { - await inited.Task; - var query = request.RequestUri.Query.TrimStart('?'); - if (CanMakeAuthenticatedRequest(payload)) - { - var nonce = payload["nonce"]; - payload.Remove("nonce"); - var body = string.Empty; - if (payload.Count > 0) - { - body = CryptoUtility.GetFormForPayload(payload); - if (query.Length > 0) - query += '&'; - query += body; - } - string signature = CryptoUtility.SHA256Sign(query, CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); - request.AddHeader("ACCESS-KEY", PublicApiKey.ToUnsecureString()); - request.AddHeader("ACCESS-SIGN", signature); - request.AddHeader("ACCESS-TIMESTAMP", nonce.ToStringInvariant()); - if (timeWindow != null) - request.AddHeader("ACCESS-RECV-WINDOW", timeWindow); - - if (request.Method == "POST") - { - await CryptoUtility.WriteToRequestAsync(request, body); - } - } - } - - protected override JToken CheckJsonResponse(JToken result) - { - if ((int)result["code"] != 0) - { - throw new APIException(result.ToStringInvariant()); - } - //var resultKeys = new string[] { "result", "data", "return", "list" }; - //foreach (string key in resultKeys) - //{ - // JToken possibleResult = result[key]; - // if (possibleResult != null && (possibleResult.Type == JTokenType.Object || possibleResult.Type == JTokenType.Array)) - // { - // result = possibleResult; - // break; - // } - //} - return result; - } - - #endregion - - #region Public APIs - - private async Task ParseExchangeMarketAsync(JToken x) - { - var symbol = x["market"].ToStringUpperInvariant(); - var (baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync(symbol); - return new ExchangeMarket - { - IsActive = true, - MarketSymbol = symbol, - BaseCurrency = baseCurrency, - QuoteCurrency = quoteCurrency, - PriceStepSize = new decimal(1, 0, 0, false, (byte)x["price_precision"]), - QuantityStepSize = new decimal(1, 0, 0, false, (byte)x["volume_precision"]), - MinTradeSize = x["min_volume"].ConvertInvariant(), - MinTradeSizeInQuoteCurrency = x["min_amount"].ConvertInvariant(), - }; - } - - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() - { - await inited.Task; - JToken obj = await MakeJsonRequestAsync("markets"); - JToken data = obj["data"]; - List results = new List(); - foreach (JToken token in data) - { - results.Add(await ParseExchangeMarketAsync(token)); - } - return results; - } - - protected override async Task> OnGetMarketSymbolsAsync() - { - return (await GetMarketSymbolsMetadataAsync()).Select(x => x.MarketSymbol); - } - - private async Task ParseTickerAsync(JToken x) - { - var t = x["ticker"][0]; - var symbol = t["symbol"].ToStringUpperInvariant(); - var (baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync(symbol); - - return new ExchangeTicker - { - Ask = t["sell"].ConvertInvariant(), - Bid = t["buy"].ConvertInvariant(), - Last = t["last"].ConvertInvariant(), - MarketSymbol = symbol, - Volume = new ExchangeVolume - { - BaseCurrency = baseCurrency, - QuoteCurrency = quoteCurrency, - QuoteCurrencyVolume = t["base_vol"].ConvertInvariant(), - BaseCurrencyVolume = t["vol"].ConvertInvariant(), - Timestamp = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["date"].ConvertInvariant()), - }, - }; - } - - protected override async Task OnGetTickerAsync(string marketSymbol) - { - JToken obj = await MakeJsonRequestAsync($"/ticker?symbol={marketSymbol}"); - return await ParseTickerAsync(obj); - } - - protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) - { - JToken obj = await MakeJsonRequestAsync($"/order_book?symbol={marketSymbol}&limit={maxCount}"); - var result = ExchangeAPIExtensions.ParseOrderBookFromJTokenArrays(obj, sequence: "date", maxCount: maxCount); - result.LastUpdatedUtc = CryptoUtility.UnixTimeStampToDateTimeSeconds(obj["date"].ConvertInvariant()); - result.MarketSymbol = marketSymbol; - return result; - } - - protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null) - { - JToken obj = await MakeJsonRequestAsync($"/trades?symbol={marketSymbol}&limit={limit??500}"); // maximum limit = 500 - return obj["data"].Select(x => new ExchangeTrade - { - Id = x["id"].ToStringInvariant(), - Amount = x["amount"].ConvertInvariant(), - Price = x["price"].ConvertInvariant(), - IsBuy = x["type"].ToStringLowerInvariant() != "sell", - Timestamp = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["date"].ConvertInvariant()), - Flags = x["type"].ToStringLowerInvariant() == "sell" ? default : ExchangeTradeFlags.IsBuy, - }); - } - - protected override async Task> OnGetCandlesAsync( - string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) - { - string period; - if (periodSeconds <= 60 * 720) - period = (periodSeconds / 60).ToStringInvariant(); - else if (periodSeconds == 24 * 60 * 60) - period = "1D"; - else if (periodSeconds == 7 * 24 * 60 * 60) - period = "1W"; - else - throw new ArgumentException($"Unsupported periodSeconds: {periodSeconds}", "periodSeconds"); - - var url = $"/kline?symbol={marketSymbol}&period={period}"; - if (startDate != null && endDate != null && limit != null) - throw new ArgumentException("Cannot specify `startDate`, `endDate` and `limit` all at the same time"); - if (limit != null) - { - if (startDate != null) - endDate = startDate + TimeSpan.FromSeconds(limit.Value * periodSeconds); - else - { - if (endDate == null) - endDate = DateTime.Now; - startDate = endDate - TimeSpan.FromSeconds((limit.Value-1) * periodSeconds); - } - } - - if (startDate != null) - url += $"&start_time={new DateTimeOffset(startDate.Value).ToUnixTimeSeconds()}"; - if (endDate != null) - url += $"&end_time={new DateTimeOffset(endDate.Value).ToUnixTimeSeconds()}"; - - JToken obj = await MakeJsonRequestAsync(url); - return obj["data"].Select(x => new MarketCandle - { - Timestamp = CryptoUtility.UnixTimeStampToDateTimeSeconds(x[0].ConvertInvariant()), - BaseCurrencyVolume = x[1].ConvertInvariant(), - ClosePrice = x[2].ConvertInvariant(), - HighPrice = x[3].ConvertInvariant(), - LowPrice = x[4].ConvertInvariant(), - OpenPrice = x[5].ConvertInvariant(), - }); - } - - - #endregion - - #region Private APIs - - ExchangeAPIOrderResult ParseOrderStatus(JToken token) - { - var x = (int)token; - switch (x) - { - case 0: - return ExchangeAPIOrderResult.Pending; - case 1: - return ExchangeAPIOrderResult.FilledPartially; - case 2: - return ExchangeAPIOrderResult.Filled; - case 3: - return ExchangeAPIOrderResult.Canceled; - case 4: - return ExchangeAPIOrderResult.FilledPartiallyAndCancelled; - default: - throw new APIException($"Unknown order result type {x}"); - } - } - - protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) - { - Dictionary payload = await GetNoncePayloadAsync(); - var url = "/spot/order/current"; - - if (marketSymbol?.Length > 0) - url += "?symbol=" + marketSymbol; - - JToken token = await MakeJsonRequestAsync(url, payload: payload); - var list = token["data"]; - return list.Select(x => new ExchangeOrderResult - { - MarketSymbol = x["symbol"].ToStringUpperInvariant(), - OrderId = x["order_id"].ToStringInvariant(), - OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["created_date"].ConvertInvariant()), - FillDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["finished_date"].ConvertInvariant()), - Price = x["price"].ConvertInvariant(), - AveragePrice = x["avg_price"].ConvertInvariant(), - Amount = x["amount"].ConvertInvariant(), - AmountFilled = x["executed_amount"].ConvertInvariant(), - IsBuy = x["type"].ToStringLowerInvariant() == "buy", - Result = ParseOrderStatus(x["status"]), - }); - } - - protected override async Task> OnGetCompletedOrderDetailsAsync( - string marketSymbol = null, DateTime? afterDate = null) - { - Dictionary payload = await GetNoncePayloadAsync(); - var url = "/spot/mytrades?limit=500"; - - if (marketSymbol?.Length > 0) - url += "&symbol=" + marketSymbol; - - if (afterDate != null) - { - var startTime = (long)afterDate.Value.UnixTimestampFromDateTimeSeconds(); - url += "&start_time=" + startTime.ToStringInvariant(); - } - - JToken token = await MakeJsonRequestAsync(url, payload: payload); - var list = token["list"]; - return list.Select(x => new ExchangeOrderResult - { - MarketSymbol = x["symbol"].ToStringUpperInvariant(), - OrderId = x["order_id"].ToStringInvariant(), - TradeId = x["id"].ToStringInvariant(), - Price = x["price"].ConvertInvariant(), - AmountFilled = x["amount"].ConvertInvariant(), - Fees = x["fee"].ConvertInvariant(), - FeesCurrency = x["fee_currency"].ToStringInvariant(), - FillDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["timestamp"].ConvertInvariant()), - IsBuy = x["side"].ToStringLowerInvariant() == "buy", - Result = ExchangeAPIOrderResult.Unknown, - }); - } - - protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null) - { - Dictionary payload = await GetNoncePayloadAsync(); - JToken token = await MakeJsonRequestAsync($"/spot/order?order_id={orderId}", payload: payload); - var x = token["data"]; - return new ExchangeOrderResult - { - MarketSymbol = x["symbol"].ToStringUpperInvariant(), - OrderId = x["order_id"].ToStringInvariant(), - OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["created_date"].ConvertInvariant()), - FillDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["finished_date"].ConvertInvariant()), - Price = x["price"].ConvertInvariant(), - AveragePrice = x["avg_price"].ConvertInvariant(), - Amount = x["amount"].ConvertInvariant(), - AmountFilled = x["executed_amount"].ConvertInvariant(), - IsBuy = x["type"].ToStringLowerInvariant() == "buy", - Result = ParseOrderStatus(x["status"]), - }; - } - - protected override async Task> OnGetAmountsAsync() - { - Dictionary payload = await GetNoncePayloadAsync(); - JToken token = await MakeJsonRequestAsync("/spot/assets", payload: payload); - var list = token["list"]; - return list.Where(x => x["total"].ConvertInvariant() != 0m).ToDictionary(x => x["currency"].ToStringUpperInvariant(), x => x["total"].ConvertInvariant()); - } - - protected override async Task> OnGetAmountsAvailableToTradeAsync() - { - Dictionary payload = await GetNoncePayloadAsync(); - JToken token = await MakeJsonRequestAsync("/spot/assets", payload: payload); - var list = token["list"]; - return list.Where(x => x["free"].ConvertInvariant() != 0m).ToDictionary(x => x["currency"].ToStringUpperInvariant(), x => x["free"].ConvertInvariant()); - } - - string GetOrderType(ExchangeOrderRequest order) - { - var result = order.IsBuy ? "buy" : "sell"; - switch (order.OrderType) - { - case OrderType.Limit: - break; - case OrderType.Market: - result += "_market"; - break; - default: - throw new ArgumentException($"Unsupported order type `{order.OrderType}`", "OrderType"); - } - return result; - } - - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) - { - Dictionary payload = await GetNoncePayloadAsync(); - payload["symbol"] = order.MarketSymbol; - payload["type"] = GetOrderType(order); - payload["price"] = order.Price; - payload["amount"] = order.Amount; - var market = order.IsMargin ? "margin" : "spot"; - JToken token = await MakeJsonRequestAsync($"/{market}/order/new", payload: payload, requestMethod: "POST"); - return new ExchangeOrderResult { OrderId = token["order_id"].ToStringInvariant() }; - } - - protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null) - { - Dictionary payload = await GetNoncePayloadAsync(); - payload["order_id"] = orderId; - JToken token = await MakeJsonRequestAsync("/spot/order/cancel", payload: payload, requestMethod: "POST"); - //{ - // "code": 0, - // "success": [ - // "198361cecdc65f9c8c9bb2fa68faec40", - // "3fb0d98e51c18954f10d439a9cf57de0" - // ], - // "error": [ - // "78a7104e3c65cc0c5a212a53e76d0205" - // ] - //} - } - - #endregion - - #region WebSocket APIs - - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) - { - await inited.Task; - if (callback == null) - { - return null; - } - else if (marketSymbols == null || marketSymbols.Length == 0) - { - marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); - } - return await ConnectWebSocketAsync(string.Empty, async (_socket, msg) => - { - // { - // "method": "trades.update", - // "params": - // [ - // true, - // [ - // { - // "id": 7172173, - // "time": 1523339279.761838, - // "price": "398.59", - // "amount": "0.027", - // "type": "buy" - // } - // ], - // "ETH_USDT" - // ], - // "id": null - // } - JToken token = JToken.Parse(CryptoUtility.DecompressDeflate((new ArraySegment(msg, 2, msg.Length - 2)).ToArray()).ToStringFromUTF8()); - if (token["method"].ToStringLowerInvariant() == "trades.update") - { - var args = token["params"]; - var clean = (bool)args[0]; - var trades = args[1]; - var symbol = args[2].ToStringUpperInvariant(); - - var x = trades as JArray; - for (int i = 0; i < x.Count; i++) - { - var trade = x[i]; - var isBuy = trade["type"].ToStringLowerInvariant() != "sell"; - var flags = default(ExchangeTradeFlags); - if (isBuy) - { - flags |= ExchangeTradeFlags.IsBuy; - if (clean) - { - flags |= ExchangeTradeFlags.IsFromSnapshot; - if (i == x.Count - 1) - { - flags |= ExchangeTradeFlags.IsLastFromSnapshot; - } - } - await callback.Invoke(new KeyValuePair - ( - symbol, - new ExchangeTrade - { - Id = trade["id"].ToStringInvariant(), - Timestamp = CryptoUtility.UnixTimeStampToDateTimeSeconds(0).AddSeconds(trade["time"].ConvertInvariant()), - Price = trade["price"].ConvertInvariant(), - Amount = trade["amount"].ConvertInvariant(), - IsBuy = isBuy, - Flags = flags, - } - )); - } - } - } - }, - async (_socket2) => - { - var id = Interlocked.Increment(ref websocketMessageId); - await _socket2.SendMessageAsync(new { id, method = "trades.subscribe", @params = marketSymbols }); - }); - } - - protected override async Task OnGetDeltaOrderBookWebSocketAsync(Action callback, int maxCount = 20, params string[] marketSymbols) - { - if (callback == null) - { - return null; - } - await inited.Task; - - return await ConnectWebSocketAsync(string.Empty, (_socket, msg) => - { - //{ - // "method": "depth.update", - // "params": [ - // true, - // { - // "asks": [ - // [ - // "10249.68000000", - // "0.00200000" - // ], - // [ - // "10249.67000000", - // "0.00110000" - // ] - // ], - // "bids": [ - // [ - // "10249.61000000", - // "0.86570000" - // ], - // [ - // "10248.44000000", - // "1.00190000" - // ] - // ] - // }, - // "BTC_USDT" - // ], - // "id": null - //} - JToken token = JToken.Parse(CryptoUtility.DecompressDeflate((new ArraySegment(msg, 2, msg.Length - 2)).ToArray()).ToStringFromUTF8()); - if (token["method"].ToStringLowerInvariant() == "depth.update") - { - var args = token["params"]; - var data = args[1]; - var book = new ExchangeOrderBook { LastUpdatedUtc = CryptoUtility.UtcNow, MarketSymbol = args[2].ToStringUpperInvariant() }; - foreach (var x in data["asks"]) - { - var price = x[0].ConvertInvariant(); - book.Asks[price] = new ExchangeOrderPrice { Price = price, Amount = x[1].ConvertInvariant() }; - } - foreach (var x in data["bids"]) - { - var price = x[0].ConvertInvariant(); - book.Bids[price] = new ExchangeOrderPrice { Price = price, Amount = x[1].ConvertInvariant() }; - } - callback(book); - } - return Task.CompletedTask; - }, async (_socket) => - { - var id = Interlocked.Increment(ref websocketMessageId); - await _socket.SendMessageAsync(new { id, method = "depth.subscribe", @params = marketSymbols }); - }); - } - - #endregion - } - - public partial class ExchangeName { public const string Digifinex = "Digifinex"; } - + { + fastestUrl = u; + //Console.WriteLine($"Fastest url {GetHashCode()}: {u}"); + BaseUrl = $"https://{u}/v3"; + BaseUrlWebSocket = $"wss://{u}/ws/v1/"; + inited.SetResult(1); + } + }); + } + } + + #region ProcessRequest + + protected override async Task OnGetNonceOffset() + { + try + { + await inited.Task; + var start = CryptoUtility.UtcNow; + JToken token = await MakeJsonRequestAsync("/time"); + DateTime serverDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(token["server_time"].ConvertInvariant()); + var end = CryptoUtility.UtcNow; + var now = start + TimeSpan.FromMilliseconds((end - start).TotalMilliseconds); + var timeFaster = now - serverDate; + timeWindow = "30"; // max latency of 30s + NonceOffset = now - serverDate; // how much time to substract from Nonce when making a request + //Console.WriteLine($"NonceOffset {GetHashCode()}: {NonceOffset}"); + } + catch + { + throw; + } + } + + protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + { + await inited.Task; + var query = request.RequestUri.Query.TrimStart('?'); + if (CanMakeAuthenticatedRequest(payload)) + { + var nonce = payload["nonce"]; + payload.Remove("nonce"); + var body = string.Empty; + if (payload.Count > 0) + { + body = CryptoUtility.GetFormForPayload(payload); + if (query.Length > 0) + query += '&'; + query += body; + } + string signature = CryptoUtility.SHA256Sign(query, CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); + request.AddHeader("ACCESS-KEY", PublicApiKey.ToUnsecureString()); + request.AddHeader("ACCESS-SIGN", signature); + request.AddHeader("ACCESS-TIMESTAMP", nonce.ToStringInvariant()); + if (timeWindow != null) + request.AddHeader("ACCESS-RECV-WINDOW", timeWindow); + + if (request.Method == "POST") + { + await CryptoUtility.WriteToRequestAsync(request, body); + } + } + } + + protected override JToken CheckJsonResponse(JToken result) + { + if ((int)result["code"] != 0) + { + throw new APIException(result.ToStringInvariant()); + } + //var resultKeys = new string[] { "result", "data", "return", "list" }; + //foreach (string key in resultKeys) + //{ + // JToken possibleResult = result[key]; + // if (possibleResult != null && (possibleResult.Type == JTokenType.Object || possibleResult.Type == JTokenType.Array)) + // { + // result = possibleResult; + // break; + // } + //} + return result; + } + + #endregion ProcessRequest + + #region Public APIs + + private async Task ParseExchangeMarketAsync(JToken x) + { + var symbol = x["market"].ToStringUpperInvariant(); + var (baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync(symbol); + return new ExchangeMarket + { + IsActive = true, + MarketSymbol = symbol, + BaseCurrency = baseCurrency, + QuoteCurrency = quoteCurrency, + PriceStepSize = new decimal(1, 0, 0, false, (byte)x["price_precision"]), + QuantityStepSize = new decimal(1, 0, 0, false, (byte)x["volume_precision"]), + MinTradeSize = x["min_volume"].ConvertInvariant(), + MinTradeSizeInQuoteCurrency = x["min_amount"].ConvertInvariant(), + }; + } + + protected internal override async Task> OnGetMarketSymbolsMetadataAsync() + { + await inited.Task; + JToken obj = await MakeJsonRequestAsync("markets"); + JToken data = obj["data"]; + List results = new List(); + foreach (JToken token in data) + { + results.Add(await ParseExchangeMarketAsync(token)); + } + return results; + } + + protected override async Task> OnGetMarketSymbolsAsync() + { + return (await GetMarketSymbolsMetadataAsync()).Select(x => x.MarketSymbol); + } + + private async Task ParseTickerAsync(JToken x) + { + var t = x["ticker"][0]; + var symbol = t["symbol"].ToStringUpperInvariant(); + var (baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync(symbol); + + return new ExchangeTicker + { + ApiResponse = t, + Ask = t["sell"].ConvertInvariant(), + Bid = t["buy"].ConvertInvariant(), + Last = t["last"].ConvertInvariant(), + MarketSymbol = symbol, + Volume = new ExchangeVolume + { + BaseCurrency = baseCurrency, + QuoteCurrency = quoteCurrency, + QuoteCurrencyVolume = t["base_vol"].ConvertInvariant(), + BaseCurrencyVolume = t["vol"].ConvertInvariant(), + Timestamp = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["date"].ConvertInvariant()), + }, + }; + } + + protected override async Task OnGetTickerAsync(string marketSymbol) + { + JToken obj = await MakeJsonRequestAsync($"/ticker?symbol={marketSymbol}"); + return await ParseTickerAsync(obj); + } + + protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) + { + JToken obj = await MakeJsonRequestAsync($"/order_book?symbol={marketSymbol}&limit={maxCount}"); + var result = ExchangeAPIExtensions.ParseOrderBookFromJTokenArrays(obj, sequence: "date", maxCount: maxCount); + result.LastUpdatedUtc = CryptoUtility.UnixTimeStampToDateTimeSeconds(obj["date"].ConvertInvariant()); + result.MarketSymbol = marketSymbol; + return result; + } + + protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null) + { + JToken obj = await MakeJsonRequestAsync($"/trades?symbol={marketSymbol}&limit={limit ?? 500}"); // maximum limit = 500 + return obj["data"].Select(x => new ExchangeTrade + { + Id = x["id"].ToStringInvariant(), + Amount = x["amount"].ConvertInvariant(), + Price = x["price"].ConvertInvariant(), + IsBuy = x["type"].ToStringLowerInvariant() != "sell", + Timestamp = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["date"].ConvertInvariant()), + Flags = x["type"].ToStringLowerInvariant() == "sell" ? default : ExchangeTradeFlags.IsBuy, + }); + } + + protected override async Task> OnGetCandlesAsync( + string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + { + string period; + if (periodSeconds <= 60 * 720) + period = (periodSeconds / 60).ToStringInvariant(); + else if (periodSeconds == 24 * 60 * 60) + period = "1D"; + else if (periodSeconds == 7 * 24 * 60 * 60) + period = "1W"; + else + throw new ArgumentException($"Unsupported periodSeconds: {periodSeconds}", "periodSeconds"); + + var url = $"/kline?symbol={marketSymbol}&period={period}"; + if (startDate != null && endDate != null && limit != null) + throw new ArgumentException("Cannot specify `startDate`, `endDate` and `limit` all at the same time"); + if (limit != null) + { + if (startDate != null) + endDate = startDate + TimeSpan.FromSeconds(limit.Value * periodSeconds); + else + { + if (endDate == null) + endDate = DateTime.Now; + startDate = endDate - TimeSpan.FromSeconds((limit.Value - 1) * periodSeconds); + } + } + + if (startDate != null) + url += $"&start_time={new DateTimeOffset(startDate.Value).ToUnixTimeSeconds()}"; + if (endDate != null) + url += $"&end_time={new DateTimeOffset(endDate.Value).ToUnixTimeSeconds()}"; + + JToken obj = await MakeJsonRequestAsync(url); + return obj["data"].Select(x => new MarketCandle + { + Timestamp = CryptoUtility.UnixTimeStampToDateTimeSeconds(x[0].ConvertInvariant()), + BaseCurrencyVolume = x[1].ConvertInvariant(), + ClosePrice = x[2].ConvertInvariant(), + HighPrice = x[3].ConvertInvariant(), + LowPrice = x[4].ConvertInvariant(), + OpenPrice = x[5].ConvertInvariant(), + }); + } + + #endregion Public APIs + + #region Private APIs + + private ExchangeAPIOrderResult ParseOrderStatus(JToken token) + { + var x = (int)token; + switch (x) + { + case 0: + return ExchangeAPIOrderResult.Pending; + + case 1: + return ExchangeAPIOrderResult.FilledPartially; + + case 2: + return ExchangeAPIOrderResult.Filled; + + case 3: + return ExchangeAPIOrderResult.Canceled; + + case 4: + return ExchangeAPIOrderResult.FilledPartiallyAndCancelled; + + default: + throw new APIException($"Unknown order result type {x}"); + } + } + + protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) + { + Dictionary payload = await GetNoncePayloadAsync(); + var url = "/spot/order/current"; + + if (marketSymbol?.Length > 0) + url += "?symbol=" + marketSymbol; + + JToken token = await MakeJsonRequestAsync(url, payload: payload); + var list = token["data"]; + return list.Select(x => new ExchangeOrderResult + { + MarketSymbol = x["symbol"].ToStringUpperInvariant(), + OrderId = x["order_id"].ToStringInvariant(), + OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["created_date"].ConvertInvariant()), + FillDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["finished_date"].ConvertInvariant()), + Price = x["price"].ConvertInvariant(), + AveragePrice = x["avg_price"].ConvertInvariant(), + Amount = x["amount"].ConvertInvariant(), + AmountFilled = x["executed_amount"].ConvertInvariant(), + IsBuy = x["type"].ToStringLowerInvariant() == "buy", + Result = ParseOrderStatus(x["status"]), + }); + } + + protected override async Task> OnGetCompletedOrderDetailsAsync( + string marketSymbol = null, DateTime? afterDate = null) + { + Dictionary payload = await GetNoncePayloadAsync(); + var url = "/spot/mytrades?limit=500"; + + if (marketSymbol?.Length > 0) + url += "&symbol=" + marketSymbol; + + if (afterDate != null) + { + var startTime = (long)afterDate.Value.UnixTimestampFromDateTimeSeconds(); + url += "&start_time=" + startTime.ToStringInvariant(); + } + + JToken token = await MakeJsonRequestAsync(url, payload: payload); + var list = token["list"]; + return list.Select(x => new ExchangeOrderResult + { + MarketSymbol = x["symbol"].ToStringUpperInvariant(), + OrderId = x["order_id"].ToStringInvariant(), + TradeId = x["id"].ToStringInvariant(), + Price = x["price"].ConvertInvariant(), + AmountFilled = x["amount"].ConvertInvariant(), + Fees = x["fee"].ConvertInvariant(), + FeesCurrency = x["fee_currency"].ToStringInvariant(), + FillDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["timestamp"].ConvertInvariant()), + IsBuy = x["side"].ToStringLowerInvariant() == "buy", + Result = ExchangeAPIOrderResult.Unknown, + }); + } + + protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null) + { + Dictionary payload = await GetNoncePayloadAsync(); + JToken token = await MakeJsonRequestAsync($"/spot/order?order_id={orderId}", payload: payload); + var x = token["data"]; + return new ExchangeOrderResult + { + MarketSymbol = x["symbol"].ToStringUpperInvariant(), + OrderId = x["order_id"].ToStringInvariant(), + OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["created_date"].ConvertInvariant()), + FillDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["finished_date"].ConvertInvariant()), + Price = x["price"].ConvertInvariant(), + AveragePrice = x["avg_price"].ConvertInvariant(), + Amount = x["amount"].ConvertInvariant(), + AmountFilled = x["executed_amount"].ConvertInvariant(), + IsBuy = x["type"].ToStringLowerInvariant() == "buy", + Result = ParseOrderStatus(x["status"]), + }; + } + + protected override async Task> OnGetAmountsAsync() + { + Dictionary payload = await GetNoncePayloadAsync(); + JToken token = await MakeJsonRequestAsync("/spot/assets", payload: payload); + var list = token["list"]; + return list.Where(x => x["total"].ConvertInvariant() != 0m).ToDictionary(x => x["currency"].ToStringUpperInvariant(), x => x["total"].ConvertInvariant()); + } + + protected override async Task> OnGetAmountsAvailableToTradeAsync() + { + Dictionary payload = await GetNoncePayloadAsync(); + JToken token = await MakeJsonRequestAsync("/spot/assets", payload: payload); + var list = token["list"]; + return list.Where(x => x["free"].ConvertInvariant() != 0m).ToDictionary(x => x["currency"].ToStringUpperInvariant(), x => x["free"].ConvertInvariant()); + } + + private string GetOrderType(ExchangeOrderRequest order) + { + var result = order.IsBuy ? "buy" : "sell"; + switch (order.OrderType) + { + case OrderType.Limit: + break; + + case OrderType.Market: + result += "_market"; + break; + + default: + throw new ArgumentException($"Unsupported order type `{order.OrderType}`", "OrderType"); + } + return result; + } + + protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + { + Dictionary payload = await GetNoncePayloadAsync(); + payload["symbol"] = order.MarketSymbol; + payload["type"] = GetOrderType(order); + payload["price"] = order.Price; + payload["amount"] = order.Amount; + var market = order.IsMargin ? "margin" : "spot"; + JToken token = await MakeJsonRequestAsync($"/{market}/order/new", payload: payload, requestMethod: "POST"); + return new ExchangeOrderResult { OrderId = token["order_id"].ToStringInvariant() }; + } + + protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null) + { + Dictionary payload = await GetNoncePayloadAsync(); + payload["order_id"] = orderId; + JToken token = await MakeJsonRequestAsync("/spot/order/cancel", payload: payload, requestMethod: "POST"); + //{ + // "code": 0, + // "success": [ + // "198361cecdc65f9c8c9bb2fa68faec40", + // "3fb0d98e51c18954f10d439a9cf57de0" + // ], + // "error": [ + // "78a7104e3c65cc0c5a212a53e76d0205" + // ] + //} + } + + #endregion Private APIs + + #region WebSocket APIs + + protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + { + await inited.Task; + if (callback == null) + { + return null; + } + else if (marketSymbols == null || marketSymbols.Length == 0) + { + marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); + } + return await ConnectWebSocketAsync(string.Empty, async (_socket, msg) => + { + // { + // "method": "trades.update", + // "params": + // [ + // true, + // [ + // { + // "id": 7172173, + // "time": 1523339279.761838, + // "price": "398.59", + // "amount": "0.027", + // "type": "buy" + // } + // ], + // "ETH_USDT" + // ], + // "id": null + // } + JToken token = JToken.Parse(CryptoUtility.DecompressDeflate((new ArraySegment(msg, 2, msg.Length - 2)).ToArray()).ToStringFromUTF8()); + if (token["method"].ToStringLowerInvariant() == "trades.update") + { + var args = token["params"]; + var clean = (bool)args[0]; + var trades = args[1]; + var symbol = args[2].ToStringUpperInvariant(); + + var x = trades as JArray; + for (int i = 0; i < x.Count; i++) + { + var trade = x[i]; + var isBuy = trade["type"].ToStringLowerInvariant() != "sell"; + var flags = default(ExchangeTradeFlags); + if (isBuy) + { + flags |= ExchangeTradeFlags.IsBuy; + if (clean) + { + flags |= ExchangeTradeFlags.IsFromSnapshot; + if (i == x.Count - 1) + { + flags |= ExchangeTradeFlags.IsLastFromSnapshot; + } + } + await callback.Invoke(new KeyValuePair + ( + symbol, + new ExchangeTrade + { + Id = trade["id"].ToStringInvariant(), + Timestamp = CryptoUtility.UnixTimeStampToDateTimeSeconds(0).AddSeconds(trade["time"].ConvertInvariant()), + Price = trade["price"].ConvertInvariant(), + Amount = trade["amount"].ConvertInvariant(), + IsBuy = isBuy, + Flags = flags, + } + )); + } + } + } + }, + async (_socket2) => + { + var id = Interlocked.Increment(ref websocketMessageId); + await _socket2.SendMessageAsync(new { id, method = "trades.subscribe", @params = marketSymbols }); + }); + } + + protected override async Task OnGetDeltaOrderBookWebSocketAsync(Action callback, int maxCount = 20, params string[] marketSymbols) + { + if (callback == null) + { + return null; + } + await inited.Task; + + return await ConnectWebSocketAsync(string.Empty, (_socket, msg) => + { + //{ + // "method": "depth.update", + // "params": [ + // true, + // { + // "asks": [ + // [ + // "10249.68000000", + // "0.00200000" + // ], + // [ + // "10249.67000000", + // "0.00110000" + // ] + // ], + // "bids": [ + // [ + // "10249.61000000", + // "0.86570000" + // ], + // [ + // "10248.44000000", + // "1.00190000" + // ] + // ] + // }, + // "BTC_USDT" + // ], + // "id": null + //} + JToken token = JToken.Parse(CryptoUtility.DecompressDeflate((new ArraySegment(msg, 2, msg.Length - 2)).ToArray()).ToStringFromUTF8()); + if (token["method"].ToStringLowerInvariant() == "depth.update") + { + var args = token["params"]; + var data = args[1]; + var book = new ExchangeOrderBook { LastUpdatedUtc = CryptoUtility.UtcNow, MarketSymbol = args[2].ToStringUpperInvariant() }; + foreach (var x in data["asks"]) + { + var price = x[0].ConvertInvariant(); + book.Asks[price] = new ExchangeOrderPrice { Price = price, Amount = x[1].ConvertInvariant() }; + } + foreach (var x in data["bids"]) + { + var price = x[0].ConvertInvariant(); + book.Bids[price] = new ExchangeOrderPrice { Price = price, Amount = x[1].ConvertInvariant() }; + } + callback(book); + } + return Task.CompletedTask; + }, async (_socket) => + { + var id = Interlocked.Increment(ref websocketMessageId); + await _socket.SendMessageAsync(new { id, method = "depth.subscribe", @params = marketSymbols }); + }); + } + + #endregion WebSocket APIs + } + + public partial class ExchangeName { public const string Digifinex = "Digifinex"; } } diff --git a/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs b/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs index a6a760160..b5c817267 100644 --- a/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs @@ -43,7 +43,7 @@ private async Task ParseVolumeAsync(JToken token, string symbol) JProperty[] props = token.Children().ToArray(); if (props.Length == 3) { - var(baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync(symbol); + var (baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync(symbol); vol.QuoteCurrency = quoteCurrency.ToUpperInvariant(); vol.QuoteCurrencyVolume = token[quoteCurrency.ToUpperInvariant()].ConvertInvariant(); vol.BaseCurrency = baseCurrency.ToUpperInvariant(); @@ -61,15 +61,15 @@ private ExchangeOrderResult ParseOrder(JToken result) return new ExchangeOrderResult { Amount = amount, - AmountFilled = amountFilled, - Price = result["price"].ConvertInvariant(), - AveragePrice = result["avg_execution_price"].ConvertInvariant(), - Message = string.Empty, - OrderId = result["id"].ToStringInvariant(), - Result = (amountFilled == amount ? ExchangeAPIOrderResult.Filled : (amountFilled == 0 ? ExchangeAPIOrderResult.Pending : ExchangeAPIOrderResult.FilledPartially)), - OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(result["timestampms"].ConvertInvariant()), - MarketSymbol = result["symbol"].ToStringInvariant(), - IsBuy = result["side"].ToStringInvariant() == "buy" + AmountFilled = amountFilled, + Price = result["price"].ConvertInvariant(), + AveragePrice = result["avg_execution_price"].ConvertInvariant(), + Message = string.Empty, + OrderId = result["id"].ToStringInvariant(), + Result = (amountFilled == amount ? ExchangeAPIOrderResult.Filled : (amountFilled == 0 ? ExchangeAPIOrderResult.Pending : ExchangeAPIOrderResult.FilledPartially)), + OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(result["timestampms"].ConvertInvariant()), + MarketSymbol = result["symbol"].ToStringInvariant(), + IsBuy = result["side"].ToStringInvariant() == "buy" }; } @@ -197,22 +197,22 @@ protected internal override async Task> OnGetMarketS List tasks = new List(); foreach (string symbol in symbols) { - tasks.Add(Task.Run(async() => + tasks.Add(Task.Run(async () => { JToken token = await MakeJsonRequestAsync("/symbols/details/" + HttpUtility.UrlEncode(symbol)); // {"symbol":"BTCUSD","base_currency":"BTC","quote_currency":"USD","tick_size":1E-8,"quote_increment":0.01,"min_order_size":"0.00001","status":"open"} - lock(markets) + lock (markets) { markets.Add(new ExchangeMarket { BaseCurrency = token["base_currency"].ToStringInvariant(), - IsActive = token["status"].ToStringInvariant().Equals("open", StringComparison.OrdinalIgnoreCase), - MarketSymbol = token["symbol"].ToStringInvariant(), - MinTradeSize = token["min_order_size"].ConvertInvariant(), - QuantityStepSize = token["tick_size"].ConvertInvariant(), - QuoteCurrency = token["quote_currency"].ToStringInvariant(), - PriceStepSize = token["quote_increment"].ConvertInvariant() + IsActive = token["status"].ToStringInvariant().Equals("open", StringComparison.OrdinalIgnoreCase), + MarketSymbol = token["symbol"].ToStringInvariant(), + MinTradeSize = token["min_order_size"].ConvertInvariant(), + QuantityStepSize = token["tick_size"].ConvertInvariant(), + QuoteCurrency = token["quote_currency"].ToStringInvariant(), + PriceStepSize = token["quote_increment"].ConvertInvariant() }); } })); @@ -234,9 +234,10 @@ protected override async Task OnGetTickerAsync(string marketSymb ExchangeTicker t = new ExchangeTicker { MarketSymbol = marketSymbol, - Ask = obj["ask"].ConvertInvariant(), - Bid = obj["bid"].ConvertInvariant(), - Last = obj["last"].ConvertInvariant() + ApiResponse = obj, + Ask = obj["ask"].ConvertInvariant(), + Bid = obj["bid"].ConvertInvariant(), + Last = obj["last"].ConvertInvariant() }; t.Volume = await ParseVolumeAsync(obj["volume"], marketSymbol); return t; @@ -245,7 +246,7 @@ protected override async Task OnGetTickerAsync(string marketSymb protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) { JToken obj = await MakeJsonRequestAsync("/book/" + marketSymbol + "?limit_bids=" + maxCount + "&limit_asks=" + maxCount); - return ExchangeAPIExtensions.ParseOrderBookFromJTokenDictionaries(obj, maxCount : maxCount); + return ExchangeAPIExtensions.ParseOrderBookFromJTokenDictionaries(obj, maxCount: maxCount); } protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) @@ -253,13 +254,13 @@ protected override async Task OnGetHistoricalTradesAsync(Func token.ParseTrade("amount", "price", "type", "timestampms", TimestampType.UnixMilliseconds, idKey: "tid"), - StartDate = startDate, - MarketSymbol = marketSymbol, - TimestampFunction = (DateTime dt) => ((long)CryptoUtility.UnixTimestampFromDateTimeMilliseconds(dt)).ToStringInvariant(), - Url = "/trades/[marketSymbol]?limit_trades=100×tamp={0}" + DirectionIsBackwards = false, + EndDate = endDate, + ParseFunction = (JToken token) => token.ParseTrade("amount", "price", "type", "timestampms", TimestampType.UnixMilliseconds, idKey: "tid"), + StartDate = startDate, + MarketSymbol = marketSymbol, + TimestampFunction = (DateTime dt) => ((long)CryptoUtility.UnixTimestampFromDateTimeMilliseconds(dt)).ToStringInvariant(), + Url = "/trades/[marketSymbol]?limit_trades=100×tamp={0}" }; await state.ProcessHistoricalTrades(); } @@ -269,7 +270,7 @@ protected override async Task> OnGetAmountsAsync() Dictionary lookup = new Dictionary(StringComparer.OrdinalIgnoreCase); JArray obj = await MakeJsonRequestAsync("/balances", null, await GetNoncePayloadAsync()); var q = from JToken token in obj - select new { Currency = token["currency"].ToStringInvariant(), Available = token["amount"].ConvertInvariant() }; + select new { Currency = token["currency"].ToStringInvariant(), Available = token["amount"].ConvertInvariant() }; foreach (var kv in q) { if (kv.Available > 0m) @@ -285,7 +286,7 @@ protected override async Task> OnGetAmountsAvailable Dictionary lookup = new Dictionary(StringComparer.OrdinalIgnoreCase); JArray obj = await MakeJsonRequestAsync("/balances", null, await GetNoncePayloadAsync()); var q = from JToken token in obj - select new { Currency = token["currency"].ToStringInvariant(), Available = token["available"].ConvertInvariant() }; + select new { Currency = token["currency"].ToStringInvariant(), Available = token["available"].ConvertInvariant() }; foreach (var kv in q) { if (kv.Available > 0m) @@ -371,11 +372,11 @@ static ExchangeTicker GetTicker(ConcurrentDictionary tic return new ExchangeTicker { MarketSymbol = _marketSymbol, - Volume = new ExchangeVolume - { - BaseCurrency = baseCurrency, - QuoteCurrency = quoteCurrency - } + Volume = new ExchangeVolume + { + BaseCurrency = baseCurrency, + QuoteCurrency = quoteCurrency + } }; }); } @@ -412,7 +413,7 @@ static void PublishTicker(ExchangeTicker ticker, string marketSymbol, Concurrent if (changesToken != null) { string marketSymbol = token["symbol"].ToStringInvariant(); - if (changesToken.FirstOrDefault()is JArray candleArray) + if (changesToken.FirstOrDefault() is JArray candleArray) { decimal volume = candleArray[5].ConvertInvariant(); volumeDict[marketSymbol] = volume; @@ -422,6 +423,7 @@ static void PublishTicker(ExchangeTicker ticker, string marketSymbol, Concurrent } } break; + case "l2_updates": { // fetch the last bid/ask/last prices @@ -446,6 +448,7 @@ static void PublishTicker(ExchangeTicker ticker, string marketSymbol, Concurrent } } break; + case "trade": { //{ "type":"trade","symbol":"ETHUSD","event_id":35899433249,"timestamp":1619191314701,"price":"2261.65","quantity":"0.010343","side":"buy"} @@ -468,16 +471,16 @@ static void PublishTicker(ExchangeTicker ticker, string marketSymbol, Concurrent break; } return Task.CompletedTask; - }, connectCallback : async(_socket) => - { - volumeDict.Clear(); - tickerDict.Clear(); - await _socket.SendMessageAsync(new - { - type = "subscribe", - subscriptions = new [] { new { name = "candles_1d", symbols = marketSymbols }, new { name = "l2", symbols = marketSymbols } } - }); - }); + }, connectCallback: async (_socket) => + { + volumeDict.Clear(); + tickerDict.Clear(); + await _socket.SendMessageAsync(new + { + type = "subscribe", + subscriptions = new[] { new { name = "candles_1d", symbols = marketSymbols }, new { name = "l2", symbols = marketSymbols } } + }); + }); } protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) @@ -555,48 +558,48 @@ protected override async Task OnGetTradesWebSocketAsync(Func - { - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - if (token["result"].ToStringInvariant() == "error") - { - // {{ "result": "error", "reason": "InvalidJson"}} - Logger.Info(token["reason"].ToStringInvariant()); - } - else if (token["type"].ToStringInvariant() == "l2_updates") - { - string marketSymbol = token["symbol"].ToStringInvariant(); - var tradesToken = token["trades"]; - if (tradesToken != null) - foreach (var tradeToken in tradesToken) - { - var trade = ParseWebSocketTrade(tradeToken); - trade.Flags |= ExchangeTradeFlags.IsFromSnapshot; - await callback(new KeyValuePair(marketSymbol, trade)); - } - } - else if (token["type"].ToStringInvariant() == "trade") - { - string marketSymbol = token["symbol"].ToStringInvariant(); - var trade = ParseWebSocketTrade(token); - await callback(new KeyValuePair(marketSymbol, trade)); - } - }, connectCallback : async(_socket) => - { - //{ "type": "subscribe","subscriptions":[{ "name":"l2","symbols":["BTCUSD","ETHUSD","ETHBTC"]}]} - await _socket.SendMessageAsync(new - { - type = "subscribe", - subscriptions = new [] - { + return await ConnectWebSocketAsync(BaseUrlWebSocket, messageCallback: async (_socket, msg) => + { + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + if (token["result"].ToStringInvariant() == "error") + { + // {{ "result": "error", "reason": "InvalidJson"}} + Logger.Info(token["reason"].ToStringInvariant()); + } + else if (token["type"].ToStringInvariant() == "l2_updates") + { + string marketSymbol = token["symbol"].ToStringInvariant(); + var tradesToken = token["trades"]; + if (tradesToken != null) + foreach (var tradeToken in tradesToken) + { + var trade = ParseWebSocketTrade(tradeToken); + trade.Flags |= ExchangeTradeFlags.IsFromSnapshot; + await callback(new KeyValuePair(marketSymbol, trade)); + } + } + else if (token["type"].ToStringInvariant() == "trade") + { + string marketSymbol = token["symbol"].ToStringInvariant(); + var trade = ParseWebSocketTrade(token); + await callback(new KeyValuePair(marketSymbol, trade)); + } + }, connectCallback: async (_socket) => + { + //{ "type": "subscribe","subscriptions":[{ "name":"l2","symbols":["BTCUSD","ETHUSD","ETHBTC"]}]} + await _socket.SendMessageAsync(new + { + type = "subscribe", + subscriptions = new[] + { new { name = "l2", symbols = marketSymbols } - } - }); - }); + } + }); + }); } protected override async Task OnGetDeltaOrderBookWebSocketAsync( @@ -618,7 +621,7 @@ params string[] marketSymbols if (message.Contains("l2_updates")) { // parse delta update - var delta = JsonConvert.DeserializeObject(message)as JObject; + var delta = JsonConvert.DeserializeObject(message) as JObject; var symbol = delta["symbol"].ToString(); book.MarketSymbol = symbol; @@ -649,17 +652,17 @@ params string[] marketSymbols } return Task.CompletedTask; - }, connectCallback : async(_socket) => - { - await _socket.SendMessageAsync(new - { - type = "subscribe", - subscriptions = new [] { new { + }, connectCallback: async (_socket) => + { + await _socket.SendMessageAsync(new + { + type = "subscribe", + subscriptions = new[] { new { name = "l2", symbols = marketSymbols } } - }); - }); + }); + }); } private static ExchangeTrade ParseWebSocketTrade(JToken token) => token.ParseTrade( diff --git a/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs b/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs index 608ca1773..5bd5989bb 100644 --- a/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs @@ -51,7 +51,7 @@ private ExchangeKrakenAPI() /// Task private async Task PopulateLookupTables() { - await Cache.GetOrCreate(nameof(PopulateLookupTables), async() => + await Cache.GetOrCreate(nameof(PopulateLookupTables), async () => { IReadOnlyDictionary currencies = await GetCurrenciesAsync(); ExchangeMarket[] markets = (await GetMarketSymbolsMetadataAsync())?.ToArray(); @@ -75,6 +75,7 @@ await Cache.GetOrCreate(nameof(PopulateLookupTables), async() => case "xbt": altName = "BTC"; break; + case "xdg": altName = "DOGE"; break; @@ -107,7 +108,7 @@ await Cache.GetOrCreate(nameof(PopulateLookupTables), async() => }); } - public override async Task < (string baseCurrency, string quoteCurrency) > ExchangeMarketSymbolToCurrenciesAsync(string marketSymbol) + public override async Task<(string baseCurrency, string quoteCurrency)> ExchangeMarketSymbolToCurrenciesAsync(string marketSymbol) { ExchangeMarket market = await GetExchangeMarketFromCacheAsync(marketSymbol); if (market == null) @@ -124,7 +125,7 @@ await Cache.GetOrCreate(nameof(PopulateLookupTables), async() => public override async Task ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol) { await PopulateLookupTables(); - var(baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync(marketSymbol); + var (baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync(marketSymbol); if (!exchangeCurrencyToNormalizedCurrency.TryGetValue(baseCurrency, out string baseCurrencyNormalized)) { baseCurrencyNormalized = baseCurrency; @@ -177,16 +178,20 @@ private ExchangeOrderResult ParseOrder(string orderId, JToken order) case "pending": orderResult.Result = ExchangeAPIOrderResult.Pending; break; + case "open": orderResult.Result = ExchangeAPIOrderResult.FilledPartially; break; + case "closed": orderResult.Result = ExchangeAPIOrderResult.Filled; break; + case "canceled": case "expired": orderResult.Result = ExchangeAPIOrderResult.Canceled; break; + default: orderResult.Result = ExchangeAPIOrderResult.Error; break; @@ -353,7 +358,7 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti string form = CryptoUtility.GetFormForPayload(payload); // nonce must be first on Kraken form = "nonce=" + nonce + (string.IsNullOrWhiteSpace(form) ? string.Empty : "&" + form); - using(SHA256 sha256 = SHA256Managed.Create()) + using (SHA256 sha256 = SHA256Managed.Create()) { string hashString = nonce + form; byte[] sha256Bytes = sha256.ComputeHash(hashString.ToBytesUTF8()); @@ -362,7 +367,7 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti pathBytes.CopyTo(sigBytes, 0); sha256Bytes.CopyTo(sigBytes, pathBytes.Length); byte[] privateKey = System.Convert.FromBase64String(CryptoUtility.ToUnsecureString(PrivateApiKey)); - using(System.Security.Cryptography.HMACSHA512 hmac = new System.Security.Cryptography.HMACSHA512(privateKey)) + using (System.Security.Cryptography.HMACSHA512 hmac = new System.Security.Cryptography.HMACSHA512(privateKey)) { string sign = System.Convert.ToBase64String(hmac.ComputeHash(sigBytes)); request.AddHeader("API-Sign", sign); @@ -385,9 +390,9 @@ protected override async Task> OnG var coin = new ExchangeCurrency { CoinType = token.Value["aclass"].ToStringInvariant(), - Name = token.Name, - FullName = token.Name, - AltName = token.Value["altname"].ToStringInvariant() + Name = token.Name, + FullName = token.Name, + AltName = token.Value["altname"].ToStringInvariant() }; currencies[coin.Name] = coin; @@ -415,101 +420,101 @@ protected override async Task> OnGetMarketSymbolsAsync(bool protected internal override async Task> OnGetMarketSymbolsMetadataAsync() { //{"ADACAD": { - // "altname": "ADACAD", - // "wsname": "ADA/CAD", - // "aclass_base": "currency", - // "base": "ADA", - // "aclass_quote": "currency", - // "quote": "ZCAD", - // "lot": "unit", - // "pair_decimals": 6, - // "lot_decimals": 8, - // "lot_multiplier": 1, - // "leverage_buy": [], - // "leverage_sell": [], - // "fees": [ - // [ - // 0, - // 0.26 - // ], - // [ - // 50000, - // 0.24 - // ], - // [ - // 100000, - // 0.22 - // ], - // [ - // 250000, - // 0.2 - // ], - // [ - // 500000, - // 0.18 - // ], - // [ - // 1000000, - // 0.16 - // ], - // [ - // 2500000, - // 0.14 - // ], - // [ - // 5000000, - // 0.12 - // ], - // [ - // 10000000, - // 0.1 - // ] - // ], - // "fees_maker": [ - // [ - // 0, - // 0.16 - // ], - // [ - // 50000, - // 0.14 - // ], - // [ - // 100000, - // 0.12 - // ], - // [ - // 250000, - // 0.1 - // ], - // [ - // 500000, - // 0.08 - // ], - // [ - // 1000000, - // 0.06 - // ], - // [ - // 2500000, - // 0.04 - // ], - // [ - // 5000000, - // 0.02 - // ], - // [ - // 10000000, - // 0 - // ] - // ], - // "fee_volume_currency": "ZUSD", - // "margin_call": 80, - // "margin_stop": 40 - //}} + // "altname": "ADACAD", + // "wsname": "ADA/CAD", + // "aclass_base": "currency", + // "base": "ADA", + // "aclass_quote": "currency", + // "quote": "ZCAD", + // "lot": "unit", + // "pair_decimals": 6, + // "lot_decimals": 8, + // "lot_multiplier": 1, + // "leverage_buy": [], + // "leverage_sell": [], + // "fees": [ + // [ + // 0, + // 0.26 + // ], + // [ + // 50000, + // 0.24 + // ], + // [ + // 100000, + // 0.22 + // ], + // [ + // 250000, + // 0.2 + // ], + // [ + // 500000, + // 0.18 + // ], + // [ + // 1000000, + // 0.16 + // ], + // [ + // 2500000, + // 0.14 + // ], + // [ + // 5000000, + // 0.12 + // ], + // [ + // 10000000, + // 0.1 + // ] + // ], + // "fees_maker": [ + // [ + // 0, + // 0.16 + // ], + // [ + // 50000, + // 0.14 + // ], + // [ + // 100000, + // 0.12 + // ], + // [ + // 250000, + // 0.1 + // ], + // [ + // 500000, + // 0.08 + // ], + // [ + // 1000000, + // 0.06 + // ], + // [ + // 2500000, + // 0.04 + // ], + // [ + // 5000000, + // 0.02 + // ], + // [ + // 10000000, + // 0 + // ] + // ], + // "fee_volume_currency": "ZUSD", + // "margin_call": 80, + // "margin_stop": 40 + //}} var markets = new List(); JToken allPairs = await MakeJsonRequestAsync("/0/public/AssetPairs"); - var res = (from prop in allPairs.Children()select prop).ToArray(); + var res = (from prop in allPairs.Children() select prop).ToArray(); foreach (JProperty prop in res.Where(p => !p.Name.EndsWith(".d"))) { @@ -519,15 +524,15 @@ protected internal override async Task> OnGetMarketS var market = new ExchangeMarket { IsActive = true, - MarketSymbol = prop.Name, - AltMarketSymbol = child["altname"].ToStringInvariant(), - AltMarketSymbol2 = child["wsname"].ToStringInvariant(), - MinTradeSize = quantityStepSize, - MarginEnabled = pair["leverage_buy"].Children().Any() || pair["leverage_sell"].Children().Any(), - BaseCurrency = pair["base"].ToStringInvariant(), - QuoteCurrency = pair["quote"].ToStringInvariant(), - QuantityStepSize = quantityStepSize, - PriceStepSize = Math.Pow(0.1, pair["pair_decimals"].ConvertInvariant()).ConvertInvariant() + MarketSymbol = prop.Name, + AltMarketSymbol = child["altname"].ToStringInvariant(), + AltMarketSymbol2 = child["wsname"].ToStringInvariant(), + MinTradeSize = quantityStepSize, + MarginEnabled = pair["leverage_buy"].Children().Any() || pair["leverage_sell"].Children().Any(), + BaseCurrency = pair["base"].ToStringInvariant(), + QuoteCurrency = pair["quote"].ToStringInvariant(), + QuantityStepSize = quantityStepSize, + PriceStepSize = Math.Pow(0.1, pair["pair_decimals"].ConvertInvariant()).ConvertInvariant() }; markets.Add(market); } @@ -547,6 +552,7 @@ protected override async Task>> JToken ticker = apiTickers[marketSymbol]; #region Fix for pairs that are not found like USDTZUSD + if (ticker == null) { // Some pairs like USDTZUSD are not found, but they can be found using Metadata. @@ -554,7 +560,8 @@ protected override async Task>> var symbol = symbols.FirstOrDefault(a => a.MarketSymbol.Replace("/", "").Equals(marketSymbol)); ticker = apiTickers[symbol.BaseCurrency + symbol.QuoteCurrency]; } - #endregion + + #endregion Fix for pairs that are not found like USDTZUSD try { @@ -578,21 +585,22 @@ protected override async Task OnGetTickerAsync(string marketSymb private async Task ConvertToExchangeTickerAsync(string symbol, JToken ticker) { decimal last = ticker["c"][0].ConvertInvariant(); - var(baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync(symbol); + var (baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync(symbol); return new ExchangeTicker { MarketSymbol = symbol, - Ask = ticker["a"][0].ConvertInvariant(), - Bid = ticker["b"][0].ConvertInvariant(), - Last = last, - Volume = new ExchangeVolume - { - QuoteCurrencyVolume = ticker["v"][1].ConvertInvariant(), - QuoteCurrency = quoteCurrency, - BaseCurrencyVolume = ticker["v"][1].ConvertInvariant() * ticker["p"][1].ConvertInvariant(), - BaseCurrency = baseCurrency, - Timestamp = CryptoUtility.UtcNow - } + ApiResponse = ticker, + Ask = ticker["a"][0].ConvertInvariant(), + Bid = ticker["b"][0].ConvertInvariant(), + Last = last, + Volume = new ExchangeVolume + { + QuoteCurrencyVolume = ticker["v"][1].ConvertInvariant(), + QuoteCurrency = quoteCurrency, + BaseCurrencyVolume = ticker["v"][1].ConvertInvariant() * ticker["p"][1].ConvertInvariant(), + BaseCurrency = baseCurrency, + Timestamp = CryptoUtility.UtcNow + } }; } @@ -604,7 +612,7 @@ protected override Task OnInitializeAsync() protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) { JToken obj = await MakeJsonRequestAsync("/0/public/Depth?pair=" + marketSymbol + "&count=" + maxCount); - return ExchangeAPIExtensions.ParseOrderBookFromJTokenArrays(obj[marketSymbol], maxCount : maxCount); + return ExchangeAPIExtensions.ParseOrderBookFromJTokenArrays(obj[marketSymbol], maxCount: maxCount); } protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null) @@ -693,7 +701,7 @@ protected override async Task> OnGetCandlesAsync(strin List candles = new List(); if (json.Children().Count() != 0) { - JProperty prop = json.Children().First()as JProperty; + JProperty prop = json.Children().First() as JProperty; foreach (JToken jsonCandle in prop.Value) { MarketCandle candle = this.ParseCandle(jsonCandle, marketSymbol, periodSeconds, 1, 2, 3, 4, 0, TimestampType.UnixSeconds, 6, null, 5); @@ -780,7 +788,7 @@ protected override async Task OnGetOrderDetailsAsync(string return orderResult; } - return ParseOrder(orderId, result[orderId]);; + return ParseOrder(orderId, result[orderId]); ; } protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) @@ -835,24 +843,24 @@ protected override async Task OnGetTickersWebSocketAsync(Action - { - if (JToken.Parse(msg.ToStringFromUTF8())is JArray token) - { - var exchangeTicker = await ConvertToExchangeTickerAsync(token[3].ToString(), token[1]); - var kv = new KeyValuePair(exchangeTicker.MarketSymbol, exchangeTicker); - tickers(new List> { kv }); - } - }, connectCallback : async(_socket) => - { - List marketSymbolList = await GetMarketSymbolList(marketSymbols); - await _socket.SendMessageAsync(new - { - @event = "subscribe", - pair = marketSymbolList, - subscription = new { name = "ticker" } - }); - }); + return await ConnectWebSocketAsync(null, messageCallback: async (_socket, msg) => + { + if (JToken.Parse(msg.ToStringFromUTF8()) is JArray token) + { + var exchangeTicker = await ConvertToExchangeTickerAsync(token[3].ToString(), token[1]); + var kv = new KeyValuePair(exchangeTicker.MarketSymbol, exchangeTicker); + tickers(new List> { kv }); + } + }, connectCallback: async (_socket) => + { + List marketSymbolList = await GetMarketSymbolList(marketSymbols); + await _socket.SendMessageAsync(new + { + @event = "subscribe", + pair = marketSymbolList, + subscription = new { name = "ticker" } + }); + }); } protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) @@ -861,91 +869,91 @@ protected override async Task OnGetTradesWebSocketAsync(Func - { - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - if (token.Type == JTokenType.Array && token[2].ToStringInvariant() == "trade") - { //[ - // 0, - // [ - - // [ - // "5541.20000", - // "0.15850568", - // "1534614057.321597", - // "s", - // "l", - // "" - // ], - - // [ - // "6060.00000", - // "0.02455000", - // "1534614057.324998", - // "b", - // "l", - // "" - // ] - // ], - // "trade", - // "XBT/USD" - //] - string marketSymbol = token[3].ToStringInvariant(); - foreach (var tradesToken in token[1]) - { - var trade = tradesToken.ParseTradeKraken(amountKey: 1, priceKey: 0, - typeKey: 3, timestampKey: 2, - TimestampType.UnixSecondsDouble, idKey : null, - typeKeyIsBuyValue: "b"); - await callback(new KeyValuePair(marketSymbol, trade)); - } - } - else if (token["event"].ToStringInvariant() == "heartbeat") {} - else if (token["status"].ToStringInvariant() == "error") - { //{{ - // "errorMessage": "Currency pair not in ISO 4217-A3 format ADACAD", - // "event": "subscriptionStatus", - // "pair": "ADACAD", - // "status": "error", - // "subscription": { - // "name": "trade" - // } - //}} - Logger.Info(token["errorMessage"].ToStringInvariant()); - } - else if (token["status"].ToStringInvariant() == "online") - { //{{ - // "connectionID": 9077277725533272053, - // "event": "systemStatus", - // "status": "online", - // "version": "0.2.0" - //}} - } - }, connectCallback : async(_socket) => - { - //{ - // "event": "subscribe", - // "pair": [ - // "XBT/USD","XBT/EUR" - // ], - // "subscription": { - // "name": "ticker" - // } - //} - List marketSymbolList = await GetMarketSymbolList(marketSymbols); - await _socket.SendMessageAsync(new - { - @event = "subscribe", - pair = marketSymbolList, - subscription = new { name = "trade" } - }); - }); + return await ConnectWebSocketAsync(null, messageCallback: async (_socket, msg) => + { + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + if (token.Type == JTokenType.Array && token[2].ToStringInvariant() == "trade") + { //[ + // 0, + // [ + + // [ + // "5541.20000", + // "0.15850568", + // "1534614057.321597", + // "s", + // "l", + // "" + // ], + + // [ + // "6060.00000", + // "0.02455000", + // "1534614057.324998", + // "b", + // "l", + // "" + // ] + // ], + // "trade", + // "XBT/USD" + //] + string marketSymbol = token[3].ToStringInvariant(); + foreach (var tradesToken in token[1]) + { + var trade = tradesToken.ParseTradeKraken(amountKey: 1, priceKey: 0, + typeKey: 3, timestampKey: 2, + TimestampType.UnixSecondsDouble, idKey: null, + typeKeyIsBuyValue: "b"); + await callback(new KeyValuePair(marketSymbol, trade)); + } + } + else if (token["event"].ToStringInvariant() == "heartbeat") { } + else if (token["status"].ToStringInvariant() == "error") + { //{{ + // "errorMessage": "Currency pair not in ISO 4217-A3 format ADACAD", + // "event": "subscriptionStatus", + // "pair": "ADACAD", + // "status": "error", + // "subscription": { + // "name": "trade" + // } + //}} + Logger.Info(token["errorMessage"].ToStringInvariant()); + } + else if (token["status"].ToStringInvariant() == "online") + { //{{ + // "connectionID": 9077277725533272053, + // "event": "systemStatus", + // "status": "online", + // "version": "0.2.0" + //}} + } + }, connectCallback: async (_socket) => + { + //{ + // "event": "subscribe", + // "pair": [ + // "XBT/USD","XBT/EUR" + // ], + // "subscription": { + // "name": "ticker" + // } + //} + List marketSymbolList = await GetMarketSymbolList(marketSymbols); + await _socket.SendMessageAsync(new + { + @event = "subscribe", + pair = marketSymbolList, + subscription = new { name = "trade" } + }); + }); } private async Task> GetMarketSymbolList(string[] marketSymbols) { await PopulateLookupTables(); // prime cache - Task[] marketSymbolsArray = marketSymbols.Select(async(m) => + Task[] marketSymbolsArray = marketSymbols.Select(async (m) => { ExchangeMarket market = await GetExchangeMarketFromCacheAsync(m); if (market == null) @@ -985,7 +993,7 @@ params string[] marketSymbols if (message.Contains("\"as\"") || message.Contains("\"bs\"")) { // parse delta update - var delta = JsonConvert.DeserializeObject(message)as JArray; + var delta = JsonConvert.DeserializeObject(message) as JArray; book.MarketSymbol = delta[3].ToString(); @@ -1028,7 +1036,7 @@ params string[] marketSymbols else if (message.Contains("\"a\"") || message.Contains("\"b\"")) { // parse delta update - var delta = JsonConvert.DeserializeObject(message)as JArray; + var delta = JsonConvert.DeserializeObject(message) as JArray; book.MarketSymbol = delta[3].ToString(); @@ -1079,21 +1087,21 @@ params string[] marketSymbols } return Task.CompletedTask; - }, connectCallback : async(_socket) => - { - // subscribe to order book channel for each symbol - var channelAction = new ChannelAction - { - Event = ActionType.Subscribe, - Pairs = marketSymbols.ToList(), - SubscriptionSettings = new Subscription - { - Name = "book", - Depth = 100 - } - }; - await _socket.SendMessageAsync(channelAction); - }); + }, connectCallback: async (_socket) => + { + // subscribe to order book channel for each symbol + var channelAction = new ChannelAction + { + Event = ActionType.Subscribe, + Pairs = marketSymbols.ToList(), + SubscriptionSettings = new Subscription + { + Name = "book", + Depth = 100 + } + }; + await _socket.SendMessageAsync(channelAction); + }); } } diff --git a/src/ExchangeSharp/API/Exchanges/LBank/ExchangeLBankAPI.cs b/src/ExchangeSharp/API/Exchanges/LBank/ExchangeLBankAPI.cs index 9330dacf4..c0f3b70fc 100644 --- a/src/ExchangeSharp/API/Exchanges/LBank/ExchangeLBankAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/LBank/ExchangeLBankAPI.cs @@ -29,152 +29,152 @@ namespace ExchangeSharp /// WebSockets address: ws://api.lbank.info/ws/v2/ /// public class ExchangeLBankAPI : ExchangeAPI - { - private const int ORDER_BOOK_MAX_SIZE = 60; - private const int RECENT_TRADS_MAX_SIZE = 600; - private const int WITHDRAW_PAGE_MAX_SIZE = 100; + { + private const int ORDER_BOOK_MAX_SIZE = 60; + private const int RECENT_TRADS_MAX_SIZE = 600; + private const int WITHDRAW_PAGE_MAX_SIZE = 100; - /// - /// Base URL for the API. - /// - public override string BaseUrl { get; set; } = "https://api.lbank.info/v1"; + /// + /// Base URL for the API. + /// + public override string BaseUrl { get; set; } = "https://api.lbank.info/v1"; - /// - /// Gets the name of the API. - /// - public override string Name => ExchangeName.LBank; + /// + /// Gets the name of the API. + /// + public override string Name => ExchangeName.LBank; /// /// Constructor /// private ExchangeLBankAPI() - { - RequestContentType = "application/x-www-form-urlencoded"; - MarketSymbolSeparator = "_"; - MarketSymbolIsUppercase = false; - } - - #region PUBLIC API********************************************* - //GetSymbolsMetadata - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() - { - var currencyPairs = await OnGetMarketSymbolsAsync(); - return ParseMarket(currencyPairs); - } - - //GetSymbols - protected override async Task> OnGetMarketSymbolsAsync() - { - JArray resp = await this.MakeJsonRequestAsync("/currencyPairs.do"); - CheckResponseToken(resp); - return resp.ToObject(); - } - - //GetTicker - protected override async Task OnGetTickerAsync(string symbol) - { - //https://api.lbank.info/v1/ticker.do?symbol=eth_btc - JToken resp = await this.MakeJsonRequestAsync($"/ticker.do?symbol={symbol}"); - CheckResponseToken(resp); - return ParseTicker(resp); - } + { + RequestContentType = "application/x-www-form-urlencoded"; + MarketSymbolSeparator = "_"; + MarketSymbolIsUppercase = false; + } - //GetTickers 4 - protected override async Task>> OnGetTickersAsync() - { - //https://api.lbank.info/v1/ticker.do?symbol=all + #region PUBLIC API********************************************* - JToken resp = await MakeJsonRequestAsync($"/ticker.do?symbol=all"); + //GetSymbolsMetadata + protected internal override async Task> OnGetMarketSymbolsMetadataAsync() + { + var currencyPairs = await OnGetMarketSymbolsAsync(); + return ParseMarket(currencyPairs); + } - CheckResponseToken(resp); + //GetSymbols + protected override async Task> OnGetMarketSymbolsAsync() + { + JArray resp = await this.MakeJsonRequestAsync("/currencyPairs.do"); + CheckResponseToken(resp); + return resp.ToObject(); + } - return ParseTickers(resp); - } + //GetTicker + protected override async Task OnGetTickerAsync(string symbol) + { + //https://api.lbank.info/v1/ticker.do?symbol=eth_btc + JToken resp = await this.MakeJsonRequestAsync($"/ticker.do?symbol={symbol}"); + CheckResponseToken(resp); + return ParseTicker(resp); + } + //GetTickers 4 + protected override async Task>> OnGetTickersAsync() + { + //https://api.lbank.info/v1/ticker.do?symbol=all - //GetOrderBook 5 - protected override async Task OnGetOrderBookAsync(string symbol, int maxCount = 100) - { + JToken resp = await MakeJsonRequestAsync($"/ticker.do?symbol=all"); - //https://api.lbank.info/v1/depth.do?symbol=eth_btc&size=60&merge=1 + CheckResponseToken(resp); - maxCount = Math.Min(maxCount, ORDER_BOOK_MAX_SIZE); - JToken resp = await this.MakeJsonRequestAsync($"/depth.do?symbol={symbol}&size={maxCount}&merge=0"); - CheckResponseToken(resp); - ExchangeOrderBook book = ExchangeAPIExtensions.ParseOrderBookFromJTokenArrays(resp, maxCount: maxCount); - book.SequenceId = resp["timestamp"].ConvertInvariant(); - return book; - } - - //GetRecentTrades 6 - protected override async Task> OnGetRecentTradesAsync(string symbol, int? limit = null) - { + return ParseTickers(resp); + } + + //GetOrderBook 5 + protected override async Task OnGetOrderBookAsync(string symbol, int maxCount = 100) + { + //https://api.lbank.info/v1/depth.do?symbol=eth_btc&size=60&merge=1 + + maxCount = Math.Min(maxCount, ORDER_BOOK_MAX_SIZE); + JToken resp = await this.MakeJsonRequestAsync($"/depth.do?symbol={symbol}&size={maxCount}&merge=0"); + CheckResponseToken(resp); + ExchangeOrderBook book = ExchangeAPIExtensions.ParseOrderBookFromJTokenArrays(resp, maxCount: maxCount); + book.SequenceId = resp["timestamp"].ConvertInvariant(); + return book; + } + + //GetRecentTrades 6 + protected override async Task> OnGetRecentTradesAsync(string symbol, int? limit = null) + { //https://api.lbank.info/v1/trades.do?symbol=eth_btc&size=600 int requestLimit = (limit == null || limit < 1 || limit > RECENT_TRADS_MAX_SIZE) ? RECENT_TRADS_MAX_SIZE : (int)limit; JToken resp = await this.MakeJsonRequestAsync($"/trades.do?symbol={symbol}&size={requestLimit}"); - CheckResponseToken(resp); - return ParseRecentTrades(resp, symbol); - } - - //GetCandles 7 - protected override async Task> OnGetCandlesAsync(string symbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) - { - //Get http://api.lbank.info/v1/kline.do - limit = limit ?? 100; - DateTime fromDate = startDate ?? CryptoUtility.UtcNow.AddDays(-1); - string type = CryptoUtility.SecondsToPeriodString(periodSeconds); - long timestamp = CryptoUtility.UnixTimestampFromDateTimeSeconds(fromDate).ConvertInvariant(); - JToken resp = await MakeJsonRequestAsync($"/kline.do?symbol={symbol}&size={limit}&type={type}&time={timestamp}"); - CheckResponseToken(resp); - return ParseMarketCandle(resp); - } - #endregion + CheckResponseToken(resp); + return ParseRecentTrades(resp, symbol); + } + //GetCandles 7 + protected override async Task> OnGetCandlesAsync(string symbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + { + //Get http://api.lbank.info/v1/kline.do + limit = limit ?? 100; + DateTime fromDate = startDate ?? CryptoUtility.UtcNow.AddDays(-1); + string type = CryptoUtility.SecondsToPeriodString(periodSeconds); + long timestamp = CryptoUtility.UnixTimestampFromDateTimeSeconds(fromDate).ConvertInvariant(); + JToken resp = await MakeJsonRequestAsync($"/kline.do?symbol={symbol}&size={limit}&type={type}&time={timestamp}"); + CheckResponseToken(resp); + return ParseMarketCandle(resp); + } - #region PARSERS PublicAPI - private List ParseMarket(IEnumerable array) - { - List markets = new List(array.Count()); + #endregion PUBLIC API********************************************* - foreach (string item in array) - { - string[] pair = item.ToUpperInvariant().Split(this.MarketSymbolSeparator[0]); + #region PARSERS PublicAPI - if (pair.Length != 2) - { - continue; - } + private List ParseMarket(IEnumerable array) + { + List markets = new List(array.Count()); + + foreach (string item in array) + { + string[] pair = item.ToUpperInvariant().Split(this.MarketSymbolSeparator[0]); - markets.Add( - new ExchangeMarket - { - MarketId = item, - MarketSymbol = item, - BaseCurrency = pair[0], - QuoteCurrency = pair[1], - IsActive = true, - }); - } + if (pair.Length != 2) + { + continue; + } - return markets; - } + markets.Add( + new ExchangeMarket + { + MarketId = item, + MarketSymbol = item, + BaseCurrency = pair[0], + QuoteCurrency = pair[1], + IsActive = true, + }); + } + + return markets; + } - private List> ParseTickers(JToken obj) - { - List> tickerList = new List>(); + private List> ParseTickers(JToken obj) + { + List> tickerList = new List>(); - foreach (JObject token in obj) - { - string symbol = token["symbol"].ConvertInvariant(); + foreach (JObject token in obj) + { + string symbol = token["symbol"].ConvertInvariant(); - ExchangeTicker ticker = ParseTicker(token); + ExchangeTicker ticker = ParseTicker(token); - tickerList.Add(new KeyValuePair(symbol, ticker)); - } + tickerList.Add(new KeyValuePair(symbol, ticker)); + } - return tickerList; - } + return tickerList; + } private ExchangeTicker ParseTicker(JToken resp) { @@ -212,6 +212,7 @@ private ExchangeTicker ParseTicker(JToken resp) ExchangeTicker ticker = new ExchangeTicker { MarketSymbol = symbol, + ApiResponse = obj, Ask = obj["high"].ConvertInvariant(), Bid = obj["low"].ConvertInvariant(), Last = obj["latest"].ConvertInvariant(), @@ -232,187 +233,179 @@ private ExchangeTicker ParseTicker(JToken resp) } private List ParseRecentTrades(JToken trades, string symbol) - { - List exTradeList = new List(trades.Count()); - - foreach (JToken token in trades) - { - long ms = token["date_ms"].ConvertInvariant(); - DateTime timestamp = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(ms); - - exTradeList.Add( - new ExchangeTrade - { - Id = token["tid"].ToStringInvariant(), - Timestamp = timestamp, - Price = token["price"].ConvertInvariant(), - Amount = token["amount"].ConvertInvariant(), - IsBuy = token["type"].ToStringLowerInvariant() == "buy" - }); - - } - - return exTradeList; - } - - private List ParseMarketCandle(JToken array) - { - List candles = new List(); - - foreach (JArray item in array) - { - MarketCandle candle = new MarketCandle - { - Timestamp = CryptoUtility.UnixTimeStampToDateTimeSeconds(item[0].ConvertInvariant()), - OpenPrice = item[1].ConvertInvariant(), - HighPrice = item[2].ConvertInvariant(), - LowPrice = item[3].ConvertInvariant(), - ClosePrice = item[4].ConvertInvariant(), - BaseCurrencyVolume = item[5].ConvertInvariant() - }; - - candles.Add(candle); - } - - return candles; - } - #endregion - - - #region TRADING API********************************************* - - //GetAmounts 8 - protected override async Task> OnGetAmountsAsync() - { - Dictionary payload = new Dictionary - { - { "api_key", PublicApiKey.ToUnsecureString() } - }; - JToken resp = await MakeJsonRequestAsync("/user_info.do", null, (Dictionary)payload, "POST"); - CheckResponseToken(resp); - return ParseAmounts(resp, true); - } + { + List exTradeList = new List(trades.Count()); - //PlaceOrder 9 - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) - { - Dictionary payload = new Dictionary - { - { "amount", order.Amount }, - { "api_key", PublicApiKey.ToUnsecureString() }, - { "price", order.Price }, - { "symbol", order.MarketSymbol }, - { "type", order.IsBuy ? "buy" : "sell"} - }; + foreach (JToken token in trades) + { + long ms = token["date_ms"].ConvertInvariant(); + DateTime timestamp = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(ms); + + exTradeList.Add( + new ExchangeTrade + { + Id = token["tid"].ToStringInvariant(), + Timestamp = timestamp, + Price = token["price"].ConvertInvariant(), + Amount = token["amount"].ConvertInvariant(), + IsBuy = token["type"].ToStringLowerInvariant() == "buy" + }); + } + + return exTradeList; + } - JToken resp = await MakeJsonRequestAsync("/create_order.do", null, payload, "POST"); + private List ParseMarketCandle(JToken array) + { + List candles = new List(); - CheckResponseToken(resp); + foreach (JArray item in array) + { + MarketCandle candle = new MarketCandle + { + Timestamp = CryptoUtility.UnixTimeStampToDateTimeSeconds(item[0].ConvertInvariant()), + OpenPrice = item[1].ConvertInvariant(), + HighPrice = item[2].ConvertInvariant(), + LowPrice = item[3].ConvertInvariant(), + ClosePrice = item[4].ConvertInvariant(), + BaseCurrencyVolume = item[5].ConvertInvariant() + }; + + candles.Add(candle); + } + + return candles; + } - return ParsePlaceOrder(resp, payload); - } + #endregion PARSERS PublicAPI - //GetOpenOrderDetails 10 - protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol) - { + #region TRADING API********************************************* - Dictionary payload = new Dictionary - { - { "api_key", PublicApiKey.ToUnsecureString() }, - { "symbol", marketSymbol } - }; + //GetAmounts 8 + protected override async Task> OnGetAmountsAsync() + { + Dictionary payload = new Dictionary + { + { "api_key", PublicApiKey.ToUnsecureString() } + }; + JToken resp = await MakeJsonRequestAsync("/user_info.do", null, (Dictionary)payload, "POST"); + CheckResponseToken(resp); + return ParseAmounts(resp, true); + } - JToken resp = await MakeJsonRequestAsync("/orders_info_no_deal.do", null, payload, "POST"); + //PlaceOrder 9 + protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + { + Dictionary payload = new Dictionary + { + { "amount", order.Amount }, + { "api_key", PublicApiKey.ToUnsecureString() }, + { "price", order.Price }, + { "symbol", order.MarketSymbol }, + { "type", order.IsBuy ? "buy" : "sell"} + }; - CheckResponseToken(resp); + JToken resp = await MakeJsonRequestAsync("/create_order.do", null, payload, "POST"); - return ParseOrderList(resp, ExchangeAPIOrderResult.Pending); - } + CheckResponseToken(resp); - //GetCompletedOrderDetails 11 - protected override async Task> OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) - { - Dictionary payload = new Dictionary - { - { "api_key", PublicApiKey.ToUnsecureString() }, - { "symbol", marketSymbol } - }; + return ParsePlaceOrder(resp, payload); + } - JToken resp = await MakeJsonRequestAsync("/orders_info_history.do", null, payload, "POST"); - CheckResponseToken(resp); - return ParseOrderList(resp, ExchangeAPIOrderResult.Filled); + //GetOpenOrderDetails 10 + protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol) + { + Dictionary payload = new Dictionary + { + { "api_key", PublicApiKey.ToUnsecureString() }, + { "symbol", marketSymbol } + }; - } - - //CancelOrder 12 - protected override async Task OnCancelOrderAsync(string orderId, string symbol = null) - { - Dictionary payload = new Dictionary - { - { "api_key", PublicApiKey.ToUnsecureString() }, - { "order_id", orderId }, - { "symbol", symbol }, - }; - JToken resp = await MakeJsonRequestAsync("/cancel_order.do", null, payload, "POST"); - CheckResponseToken(resp); - } + JToken resp = await MakeJsonRequestAsync("/orders_info_no_deal.do", null, payload, "POST"); + CheckResponseToken(resp); - //GetOrderDetails 13 - protected override async Task OnGetOrderDetailsAsync(string orderId, string symbol = null) - { + return ParseOrderList(resp, ExchangeAPIOrderResult.Pending); + } - Dictionary payload = new Dictionary - { - { "api_key", PublicApiKey.ToUnsecureString() }, - { "order_id", orderId }, - { "symbol", symbol } - }; + //GetCompletedOrderDetails 11 + protected override async Task> OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) + { + Dictionary payload = new Dictionary + { + { "api_key", PublicApiKey.ToUnsecureString() }, + { "symbol", marketSymbol } + }; - JToken resp = await MakeJsonRequestAsync("/orders_info.do", null, payload, "POST"); - CheckResponseToken(resp); - var orderResultList = ParseOrderList(resp, ExchangeAPIOrderResult.Unknown); - CheckResponseList(orderResultList, orderId); + JToken resp = await MakeJsonRequestAsync("/orders_info_history.do", null, payload, "POST"); + CheckResponseToken(resp); + return ParseOrderList(resp, ExchangeAPIOrderResult.Filled); + } - return orderResultList[0]; - } + //CancelOrder 12 + protected override async Task OnCancelOrderAsync(string orderId, string symbol = null) + { + Dictionary payload = new Dictionary + { + { "api_key", PublicApiKey.ToUnsecureString() }, + { "order_id", orderId }, + { "symbol", symbol }, + }; + JToken resp = await MakeJsonRequestAsync("/cancel_order.do", null, payload, "POST"); + CheckResponseToken(resp); + } + //GetOrderDetails 13 + protected override async Task OnGetOrderDetailsAsync(string orderId, string symbol = null) + { + Dictionary payload = new Dictionary + { + { "api_key", PublicApiKey.ToUnsecureString() }, + { "order_id", orderId }, + { "symbol", symbol } + }; - //Withdraw 14 - protected override async Task OnWithdrawAsync(ExchangeWithdrawalRequest withdrawalRequest) - { + JToken resp = await MakeJsonRequestAsync("/orders_info.do", null, payload, "POST"); + CheckResponseToken(resp); + var orderResultList = ParseOrderList(resp, ExchangeAPIOrderResult.Unknown); + CheckResponseList(orderResultList, orderId); - if (string.IsNullOrWhiteSpace(withdrawalRequest.Currency)) - { - throw new APIException("Symbol empty"); - } - if (string.IsNullOrWhiteSpace(withdrawalRequest.Address)) - { - throw new APIException("Address empty"); - } + return orderResultList[0]; + } - Dictionary payload = new Dictionary - { - { "account", withdrawalRequest.Address }, - { "amount", withdrawalRequest.Amount }, - { "api_key", PublicApiKey.ToUnsecureString() }, - { "assetCode", withdrawalRequest.Currency }, - { "fee", withdrawalRequest.TakeFeeFromAmount } - }; + //Withdraw 14 + protected override async Task OnWithdrawAsync(ExchangeWithdrawalRequest withdrawalRequest) + { + if (string.IsNullOrWhiteSpace(withdrawalRequest.Currency)) + { + throw new APIException("Symbol empty"); + } + if (string.IsNullOrWhiteSpace(withdrawalRequest.Address)) + { + throw new APIException("Address empty"); + } - JObject resp = await MakeJsonRequestAsync("/withdraw.do", null, payload, "POST"); + Dictionary payload = new Dictionary + { + { "account", withdrawalRequest.Address }, + { "amount", withdrawalRequest.Amount }, + { "api_key", PublicApiKey.ToUnsecureString() }, + { "assetCode", withdrawalRequest.Currency }, + { "fee", withdrawalRequest.TakeFeeFromAmount } + }; - CheckResponseToken(resp); + JObject resp = await MakeJsonRequestAsync("/withdraw.do", null, payload, "POST"); - return ParseWithdrawalResponse(resp); - } + CheckResponseToken(resp); + return ParseWithdrawalResponse(resp); + } - //Withdraws 15 - protected override Task> OnGetWithdrawHistoryAsync(string currency) - { - throw new NotImplementedException(); - /* + //Withdraws 15 + protected override Task> OnGetWithdrawHistoryAsync(string currency) + { + throw new NotImplementedException(); + /* Dictionary payload = new Dictionary { { "api_key", PublicApiKey.ToUnsecureString() }, @@ -426,254 +419,254 @@ protected override Task> OnGetWithdrawHistoryAs return ParseWithdrawListResponse(resp); */ - } + } + #endregion TRADING API********************************************* + #region PARSERS PrivateAPI - #endregion + private Dictionary ParseAmounts(JToken obj, bool isAll) + { + Dictionary balance = new Dictionary(); - #region PARSERS PrivateAPI - private Dictionary ParseAmounts(JToken obj, bool isAll) - { - Dictionary balance = new Dictionary(); + JToken freeAssets = obj["info"]["free"]; - JToken freeAssets = obj["info"]["free"]; + foreach (JProperty item in freeAssets) + { + string symbol = item.Name.ToStringInvariant(); + decimal amount = item.Value.ConvertInvariant(); - foreach (JProperty item in freeAssets) - { - string symbol = item.Name.ToStringInvariant(); - decimal amount = item.Value.ConvertInvariant(); + if (isAll) + { + balance[symbol] = amount; + } + else + { + if (amount > 0m) + { + balance[symbol] = amount; + } + } + } - if (isAll) - { - balance[symbol] = amount; - } - else - { - if (amount > 0m) - { - balance[symbol] = amount; - } - } - } - - return balance; - } - private ExchangeOrderResult ParsePlaceOrder(JToken obj, Dictionary payload) - { - - ExchangeOrderResult orderResult = new ExchangeOrderResult - { - Amount = payload["amount"].ConvertInvariant(), - MarketSymbol = payload["symbol"].ToStringInvariant(), - OrderId = obj["order_id"].ToStringInvariant(), - IsBuy = payload["type"].ToString().Equals("buy"), - Price = payload["price"].ConvertInvariant(), - OrderDate = CryptoUtility.UtcNow, - Result = ExchangeAPIOrderResult.Pending - }; - - return orderResult; - } - private List ParseOrderList(JToken orderList, ExchangeAPIOrderResult status) - { - JToken orders = orderList["orders"]; - - List orderResultList = new List(); - - foreach (JToken order in orders) - { - ExchangeOrderResult orderResult = ParseOrder(order); - - if (orderResult.Result == status || status == ExchangeAPIOrderResult.Unknown) //ApiOrderResult.Unknown - any states - { - orderResultList.Add(orderResult); - } - } - - return orderResultList; - } - private ExchangeOrderResult ParseOrder(JToken obj) - { - long ms = obj["create_time"].ConvertInvariant(); - - ExchangeOrderResult orderResult = new ExchangeOrderResult - { - Amount = obj["amount"].ConvertInvariant(), - MarketSymbol = obj["symbol"].ToStringInvariant(), - OrderId = obj["order_id"].ToStringInvariant(), - IsBuy = obj["type"].ToString().Equals("buy"), - AveragePrice = obj["avg_price"].ConvertInvariant(), - Price = obj["price"].ConvertInvariant(), - AmountFilled = obj["deal_amount"].ConvertInvariant(), - OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(ms), - Result = GetApiOrderResultFrom(obj["status"].ConvertInvariant()) - }; - - return orderResult; - } - private ExchangeWithdrawalResponse ParseWithdrawalResponse(JToken obj) - { - long ms = obj["time"].ConvertInvariant(); - - return new ExchangeWithdrawalResponse - { - Id = obj["id"].ConvertInvariant(), - Success = obj["success"].ConvertInvariant() - }; - } - - - private List ParseWithdrawListResponse(JToken withdrawList) - { - List withdrawResponseList = new List(); - - JToken withdraws = withdrawList["list"]; - - foreach (JToken item in withdraws) - { - ExchangeWithdrawalResponse withdrawResponse = ParseWithdrawalResponse(item); - withdrawResponseList.Add(withdrawResponse); - } - - return withdrawResponseList; - } - #endregion - - #region HELPERS - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) - { - if (payload == null || request.Method == "GET") - { - return; - } - - string secret = this.PrivateApiKey.ToUnsecureString(); - - payload.Add("secret_key", secret); - - string body = CryptoUtility.GetFormForPayload(payload); - string sign = CryptoUtility.MD5Sign(body, PrivateApiKey.ToUnsecureBytesUTF8()); - - payload.Remove("secret_key"); - payload.Add("sign", sign); - body = payload.GetFormForPayload(); - await CryptoUtility.WriteToRequestAsync(request, body); - } - - /// - /// -1: Revoked - /// 0: Unfilled (Pending) - /// 1: partial deal - /// 2: The complete deal (Filled) - /// 4: Withdrawal process - /// - /// - /// - private ExchangeAPIOrderResult GetApiOrderResultFrom(int status) - { - switch (status) - { - case -1: - return ExchangeAPIOrderResult.Canceled; - - case 0: - return ExchangeAPIOrderResult.Pending; - - case 1: - return ExchangeAPIOrderResult.FilledPartially; - - case 2: - return ExchangeAPIOrderResult.Filled; - - case 4: - return ExchangeAPIOrderResult.PendingCancel; - - default: - return ExchangeAPIOrderResult.Unknown; - } - } - - - private void CheckResponseToken(JToken token, string orderId = null) - { - if (token == null || !token.HasValues) - { - throw new APIException("Missing response"); - } - - else if (!(token is JArray) && !token["result"].ConvertInvariant() && token["error_code"] != null) - { - int errorCode = token["error_code"].ConvertInvariant(); - string errMsg = GetErrorMsg(errorCode); - throw new APIException($"ErrorCode: {errorCode} {errMsg}"); - } - - if (orderId != null && token["order_id"].ConvertInvariant() != orderId) - { - - throw new APIException($"Response order_id mismatch with {orderId}"); - } - } - - private void CheckResponseList(List orderResultList, string orderId) - { - if (orderResultList.Count == 0 || (orderResultList.Count > 0 && orderResultList[0].OrderId != orderId)) - { - throw new APIException("Missing response"); - } - } - - private static string GetErrorMsg(int errorCode) - { - string errMsg = ""; - - switch (errorCode) - { - case 10000: errMsg = "Internal error"; break; - case 10001: errMsg = "Required parameters cannot be empty"; break; - case 10002: errMsg = "Verification failed"; break; - case 10003: errMsg = "illegal parameters"; break; - case 10004: errMsg = "User requests are too frequent"; break; - case 10005: errMsg = "Key does not exist"; break; - case 10006: errMsg = "User does not exist"; break; - case 10007: errMsg = "Invalid signature"; break; - case 10008: errMsg = "This currency pair does not support"; break; - - case 10009: errMsg = "Limit order can not be missing the order price and order quantity"; break; - case 10010: errMsg = "Order price or order quantity must be greater than 0"; break; - case 10013: errMsg = "Minimum trading amount less than position 0.001"; break; - case 10014: errMsg = "Insufficient amount of account currency"; break; - case 10015: errMsg = "Order type error"; break; - case 10016: errMsg = "Account balance is insufficient"; break; - case 10017: errMsg = "Server exception"; break; - case 10018: errMsg = "The number of order inquiry cannot be greater than 50 and less than 1"; break; - - case 10019: errMsg = "The number of withdrawals cannot be greater than 3 and less than 1"; break; - case 10020: errMsg = "Minimum trading amount less than the amount of 0.001"; break; - case 10021: errMsg = "Minimum transaction amount less than the limit order transaction price 0.01"; break; - case 10022: errMsg = "Insufficient key authority"; break; - case 10023: errMsg = "Does not support market price trading"; break; - case 10024: errMsg = "Users cannot trade the pair"; break; - case 10025: errMsg = "Order has been dealt"; break; - case 10026: errMsg = "Order has been revoked"; break; - case 10027: errMsg = "Order is being revoked"; break; - - case 10100: errMsg = "No coin rights"; break; - case 10101: errMsg = "The coin rate is wrong"; break; - case 10102: errMsg = "The amount of the coin is less than the single minimum"; break; - case 10103: errMsg = "The amount of the coin exceeds the daily limit"; break; - case 10104: errMsg = "The order has been processed and cannot be revoked"; break; - case 10105: errMsg = "The order has been cancelled"; break; - - default: errMsg = $"Unknown error code: {errorCode}"; break; - } - - return errMsg; - } - - #endregion - } - - public partial class ExchangeName { public const string LBank = "LBank"; } + return balance; + } + + private ExchangeOrderResult ParsePlaceOrder(JToken obj, Dictionary payload) + { + ExchangeOrderResult orderResult = new ExchangeOrderResult + { + Amount = payload["amount"].ConvertInvariant(), + MarketSymbol = payload["symbol"].ToStringInvariant(), + OrderId = obj["order_id"].ToStringInvariant(), + IsBuy = payload["type"].ToString().Equals("buy"), + Price = payload["price"].ConvertInvariant(), + OrderDate = CryptoUtility.UtcNow, + Result = ExchangeAPIOrderResult.Pending + }; + + return orderResult; + } + + private List ParseOrderList(JToken orderList, ExchangeAPIOrderResult status) + { + JToken orders = orderList["orders"]; + + List orderResultList = new List(); + + foreach (JToken order in orders) + { + ExchangeOrderResult orderResult = ParseOrder(order); + + if (orderResult.Result == status || status == ExchangeAPIOrderResult.Unknown) //ApiOrderResult.Unknown - any states + { + orderResultList.Add(orderResult); + } + } + + return orderResultList; + } + + private ExchangeOrderResult ParseOrder(JToken obj) + { + long ms = obj["create_time"].ConvertInvariant(); + + ExchangeOrderResult orderResult = new ExchangeOrderResult + { + Amount = obj["amount"].ConvertInvariant(), + MarketSymbol = obj["symbol"].ToStringInvariant(), + OrderId = obj["order_id"].ToStringInvariant(), + IsBuy = obj["type"].ToString().Equals("buy"), + AveragePrice = obj["avg_price"].ConvertInvariant(), + Price = obj["price"].ConvertInvariant(), + AmountFilled = obj["deal_amount"].ConvertInvariant(), + OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(ms), + Result = GetApiOrderResultFrom(obj["status"].ConvertInvariant()) + }; + + return orderResult; + } + + private ExchangeWithdrawalResponse ParseWithdrawalResponse(JToken obj) + { + long ms = obj["time"].ConvertInvariant(); + + return new ExchangeWithdrawalResponse + { + Id = obj["id"].ConvertInvariant(), + Success = obj["success"].ConvertInvariant() + }; + } + + private List ParseWithdrawListResponse(JToken withdrawList) + { + List withdrawResponseList = new List(); + + JToken withdraws = withdrawList["list"]; + + foreach (JToken item in withdraws) + { + ExchangeWithdrawalResponse withdrawResponse = ParseWithdrawalResponse(item); + withdrawResponseList.Add(withdrawResponse); + } + + return withdrawResponseList; + } + + #endregion PARSERS PrivateAPI + + #region HELPERS + + protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + { + if (payload == null || request.Method == "GET") + { + return; + } + + string secret = this.PrivateApiKey.ToUnsecureString(); + + payload.Add("secret_key", secret); + + string body = CryptoUtility.GetFormForPayload(payload); + string sign = CryptoUtility.MD5Sign(body, PrivateApiKey.ToUnsecureBytesUTF8()); + + payload.Remove("secret_key"); + payload.Add("sign", sign); + body = payload.GetFormForPayload(); + await CryptoUtility.WriteToRequestAsync(request, body); + } + + /// + /// -1: Revoked + /// 0: Unfilled (Pending) + /// 1: partial deal + /// 2: The complete deal (Filled) + /// 4: Withdrawal process + /// + /// + /// + private ExchangeAPIOrderResult GetApiOrderResultFrom(int status) + { + switch (status) + { + case -1: + return ExchangeAPIOrderResult.Canceled; + + case 0: + return ExchangeAPIOrderResult.Pending; + + case 1: + return ExchangeAPIOrderResult.FilledPartially; + + case 2: + return ExchangeAPIOrderResult.Filled; + + case 4: + return ExchangeAPIOrderResult.PendingCancel; + + default: + return ExchangeAPIOrderResult.Unknown; + } + } + + private void CheckResponseToken(JToken token, string orderId = null) + { + if (token == null || !token.HasValues) + { + throw new APIException("Missing response"); + } + else if (!(token is JArray) && !token["result"].ConvertInvariant() && token["error_code"] != null) + { + int errorCode = token["error_code"].ConvertInvariant(); + string errMsg = GetErrorMsg(errorCode); + throw new APIException($"ErrorCode: {errorCode} {errMsg}"); + } + + if (orderId != null && token["order_id"].ConvertInvariant() != orderId) + { + throw new APIException($"Response order_id mismatch with {orderId}"); + } + } + + private void CheckResponseList(List orderResultList, string orderId) + { + if (orderResultList.Count == 0 || (orderResultList.Count > 0 && orderResultList[0].OrderId != orderId)) + { + throw new APIException("Missing response"); + } + } + + private static string GetErrorMsg(int errorCode) + { + string errMsg = ""; + + switch (errorCode) + { + case 10000: errMsg = "Internal error"; break; + case 10001: errMsg = "Required parameters cannot be empty"; break; + case 10002: errMsg = "Verification failed"; break; + case 10003: errMsg = "illegal parameters"; break; + case 10004: errMsg = "User requests are too frequent"; break; + case 10005: errMsg = "Key does not exist"; break; + case 10006: errMsg = "User does not exist"; break; + case 10007: errMsg = "Invalid signature"; break; + case 10008: errMsg = "This currency pair does not support"; break; + + case 10009: errMsg = "Limit order can not be missing the order price and order quantity"; break; + case 10010: errMsg = "Order price or order quantity must be greater than 0"; break; + case 10013: errMsg = "Minimum trading amount less than position 0.001"; break; + case 10014: errMsg = "Insufficient amount of account currency"; break; + case 10015: errMsg = "Order type error"; break; + case 10016: errMsg = "Account balance is insufficient"; break; + case 10017: errMsg = "Server exception"; break; + case 10018: errMsg = "The number of order inquiry cannot be greater than 50 and less than 1"; break; + + case 10019: errMsg = "The number of withdrawals cannot be greater than 3 and less than 1"; break; + case 10020: errMsg = "Minimum trading amount less than the amount of 0.001"; break; + case 10021: errMsg = "Minimum transaction amount less than the limit order transaction price 0.01"; break; + case 10022: errMsg = "Insufficient key authority"; break; + case 10023: errMsg = "Does not support market price trading"; break; + case 10024: errMsg = "Users cannot trade the pair"; break; + case 10025: errMsg = "Order has been dealt"; break; + case 10026: errMsg = "Order has been revoked"; break; + case 10027: errMsg = "Order is being revoked"; break; + + case 10100: errMsg = "No coin rights"; break; + case 10101: errMsg = "The coin rate is wrong"; break; + case 10102: errMsg = "The amount of the coin is less than the single minimum"; break; + case 10103: errMsg = "The amount of the coin exceeds the daily limit"; break; + case 10104: errMsg = "The order has been processed and cannot be revoked"; break; + case 10105: errMsg = "The order has been cancelled"; break; + + default: errMsg = $"Unknown error code: {errorCode}"; break; + } + + return errMsg; + } + + #endregion HELPERS + } + + public partial class ExchangeName { public const string LBank = "LBank"; } } diff --git a/src/ExchangeSharp/API/Exchanges/NDAX/Models/Level1Data.cs b/src/ExchangeSharp/API/Exchanges/NDAX/Models/Level1Data.cs index 51dfc6aa9..81881afad 100644 --- a/src/ExchangeSharp/API/Exchanges/NDAX/Models/Level1Data.cs +++ b/src/ExchangeSharp/API/Exchanges/NDAX/Models/Level1Data.cs @@ -8,7 +8,7 @@ public sealed partial class ExchangeNDAXAPI /// /// For use in SubscribeLevel1 OnGetTickersWebSocketAsync() /// - class Level1Data + private class Level1Data { [JsonProperty("OMSId")] public long OmsId { get; set; } @@ -85,5 +85,5 @@ public ExchangeTicker ToExchangeTicker(string currencyPair) }; } } - } + } } diff --git a/src/ExchangeSharp/API/Exchanges/NDAX/Models/NDAXTicker.cs b/src/ExchangeSharp/API/Exchanges/NDAX/Models/NDAXTicker.cs index 632ddf065..368738835 100644 --- a/src/ExchangeSharp/API/Exchanges/NDAX/Models/NDAXTicker.cs +++ b/src/ExchangeSharp/API/Exchanges/NDAX/Models/NDAXTicker.cs @@ -5,7 +5,7 @@ namespace ExchangeSharp { public sealed partial class ExchangeNDAXAPI { - class NDAXTicker + private class NDAXTicker { [JsonProperty("isFrozen")] [JsonConverter(typeof(BoolConverter))] diff --git a/src/ExchangeSharp/API/Exchanges/UfoDex/ExchangeUfoDexAPI.cs b/src/ExchangeSharp/API/Exchanges/UfoDex/ExchangeUfoDexAPI.cs index 8603014f8..0bee1f72e 100644 --- a/src/ExchangeSharp/API/Exchanges/UfoDex/ExchangeUfoDexAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/UfoDex/ExchangeUfoDexAPI.cs @@ -19,65 +19,66 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp { - public sealed partial class ExchangeUfoDexAPI : ExchangeAPI - { - // TODO: Set correct base url - public override string BaseUrl { get; set; } = "https://ufodex.io/dexsrv/mainnet/api/v1"; + public sealed partial class ExchangeUfoDexAPI : ExchangeAPI + { + // TODO: Set correct base url + public override string BaseUrl { get; set; } = "https://ufodex.io/dexsrv/mainnet/api/v1"; - private ExchangeTicker ParseTicker(JToken token) - { - string pair = token["Label"].ToStringInvariant(); - string[] symbols = pair.Split('/'); - return new ExchangeTicker - { - // TODO: Parse out fields... - // Ticker JSON { "GenTime":12345678901234 "Label":"UFO/BTC", "Ask":0.00000005, "Bid":0.00000003, "Open":0.00000006, "High":0.00000007, "Low":0.00000004, "Close":0.00000003, "Volume":3240956.04453450, "BaseVolume":455533325.98457433 } - Id = token["GenTime"].ConvertInvariant(), // ???? - Ask = token["Ask"].ConvertInvariant(), - Bid = token["Bid"].ConvertInvariant(), - Last = token["Close"].ConvertInvariant(), - MarketSymbol = pair, - Volume = new ExchangeVolume - { - QuoteCurrency = symbols[0], - BaseCurrency = symbols[1], - QuoteCurrencyVolume = token["Volume"].ConvertInvariant(), - BaseCurrencyVolume = token["BaseVolume"].ConvertInvariant(), - Timestamp = token["GenTime"].ConvertInvariant().UnixTimeStampToDateTimeMilliseconds() - } - }; - } + private ExchangeTicker ParseTicker(JToken token) + { + string pair = token["Label"].ToStringInvariant(); + string[] symbols = pair.Split('/'); + return new ExchangeTicker + { + // TODO: Parse out fields... + // Ticker JSON { "GenTime":12345678901234 "Label":"UFO/BTC", "Ask":0.00000005, "Bid":0.00000003, "Open":0.00000006, "High":0.00000007, "Low":0.00000004, "Close":0.00000003, "Volume":3240956.04453450, "BaseVolume":455533325.98457433 } + Id = token["GenTime"].ConvertInvariant(), // ???? + ApiResponse = token, + Ask = token["Ask"].ConvertInvariant(), + Bid = token["Bid"].ConvertInvariant(), + Last = token["Close"].ConvertInvariant(), + MarketSymbol = pair, + Volume = new ExchangeVolume + { + QuoteCurrency = symbols[0], + BaseCurrency = symbols[1], + QuoteCurrencyVolume = token["Volume"].ConvertInvariant(), + BaseCurrencyVolume = token["BaseVolume"].ConvertInvariant(), + Timestamp = token["GenTime"].ConvertInvariant().UnixTimeStampToDateTimeMilliseconds() + } + }; + } private ExchangeUfoDexAPI() - { - RequestContentType = "application/json"; + { + RequestContentType = "application/json"; - // TODO: Verify these are correct - NonceStyle = NonceStyle.UnixMillisecondsString; - MarketSymbolSeparator = "/"; - MarketSymbolIsUppercase = true; - } + // TODO: Verify these are correct + NonceStyle = NonceStyle.UnixMillisecondsString; + MarketSymbolSeparator = "/"; + MarketSymbolIsUppercase = true; + } - protected override async Task OnGetTickerAsync(string marketSymbol) - { - // marketSymbol like "UFO/BTC" - JToken result = await MakeJsonRequestAsync("/getticker/" + marketSymbol); - return ParseTicker(result); - } + protected override async Task OnGetTickerAsync(string marketSymbol) + { + // marketSymbol like "UFO/BTC" + JToken result = await MakeJsonRequestAsync("/getticker/" + marketSymbol); + return ParseTicker(result); + } - protected override async Task>> OnGetTickersAsync() - { - List> tickers = new List>(); + protected override async Task>> OnGetTickersAsync() + { + List> tickers = new List>(); - JToken result = await MakeJsonRequestAsync("/gettickers"); - foreach (JProperty token in result) - { - // {"UFO/BTC":{Ticker JSON}, "UFO/LTC":{Ticker JSON}, ...} - tickers.Add(new KeyValuePair(token.Name, ParseTicker(token.Value))); - } - return tickers; - } - } + JToken result = await MakeJsonRequestAsync("/gettickers"); + foreach (JProperty token in result) + { + // {"UFO/BTC":{Ticker JSON}, "UFO/LTC":{Ticker JSON}, ...} + tickers.Add(new KeyValuePair(token.Name, ParseTicker(token.Value))); + } + return tickers; + } + } - public partial class ExchangeName { public const string UfoDex = "UfoDex"; } + public partial class ExchangeName { public const string UfoDex = "UfoDex"; } } diff --git a/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs b/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs index 529c1b194..252dd0b92 100644 --- a/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs +++ b/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs @@ -10,6 +10,7 @@ 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. */ #nullable enable + using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -24,503 +25,508 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp { - /// Contains useful extension methods and parsing for the ExchangeAPI classes - public static class ExchangeAPIExtensions - { - /// - /// Get full order book bids and asks via web socket. This is efficient and will - /// only use the order book deltas (if supported by the exchange). This method deals - /// with the complexity of different exchanges sending order books that are full, - /// partial or otherwise. - /// - /// Callback containing full order book - /// Max count of bids and asks - not all exchanges will honor this - /// parameter - /// Order book symbols or null/empty for all of them (if supported) - /// Web socket, call Dispose to close - public static async Task GetFullOrderBookWebSocketAsync(this IOrderBookProvider api, Action callback, int maxCount = 20, params string[] symbols) - { - if (api.WebSocketOrderBookType == WebSocketOrderBookType.None) - { - throw new NotSupportedException(api.GetType().Name + " does not support web socket order books"); - } - - // Notes: - // * Confirm with the Exchange's API docs whether the data in each event is the absolute quantity or differential quantity - // * Receiving an event that removes a price level that is not in your local order book can happen and is normal. - ConcurrentDictionary fullBooks = new ConcurrentDictionary(); - Dictionary> partialOrderBookQueues = new Dictionary>(); - - static void applyDelta(SortedDictionary deltaValues, SortedDictionary bookToEdit) - { - foreach (ExchangeOrderPrice record in deltaValues.Values) - { - if (record.Amount <= 0 || record.Price <= 0) - { - bookToEdit.Remove(record.Price); - } - else - { - bookToEdit[record.Price] = record; - } - } - } - - static void updateOrderBook(ExchangeOrderBook fullOrderBook, ExchangeOrderBook freshBook) - { - lock (fullOrderBook) - { - // update deltas as long as the full book is at or before the delta timestamp - if (fullOrderBook.SequenceId <= freshBook.SequenceId) - { - applyDelta(freshBook.Asks, fullOrderBook.Asks); - applyDelta(freshBook.Bids, fullOrderBook.Bids); - fullOrderBook.SequenceId = freshBook.SequenceId; - } - } - } - - async Task innerCallback(ExchangeOrderBook newOrderBook) - { - // depending on the exchange, newOrderBook may be a complete or partial order book - // ideally all exchanges would send the full order book on first message, followed by delta order books - // but this is not the case - - bool foundFullBook = fullBooks.TryGetValue(newOrderBook.MarketSymbol, out ExchangeOrderBook fullOrderBook); - switch (api.WebSocketOrderBookType) - { - case WebSocketOrderBookType.DeltasOnly: - { - // Fetch an initial book the first time and apply deltas on top - // send these exchanges scathing support tickets that they should send - // the full book for the first web socket callback message - Queue partialOrderBookQueue; - bool requestFullOrderBook = false; - - // attempt to find the right queue to put the partial order book in to be processed later - lock (partialOrderBookQueues) - { - if (!partialOrderBookQueues.TryGetValue(newOrderBook.MarketSymbol, out partialOrderBookQueue)) - { - // no queue found, make a new one - partialOrderBookQueues[newOrderBook.MarketSymbol] = partialOrderBookQueue = new Queue(); - requestFullOrderBook = !foundFullBook; - } - - // always enqueue the partial order book, they get dequeued down below - partialOrderBookQueue.Enqueue(newOrderBook); - } - - // request the entire order book if we need it - if (requestFullOrderBook) - { - fullOrderBook = await api.GetOrderBookAsync(newOrderBook.MarketSymbol, maxCount); - fullOrderBook.MarketSymbol = newOrderBook.MarketSymbol; - fullBooks[newOrderBook.MarketSymbol] = fullOrderBook; - } - else if (!foundFullBook) - { - // we got a partial book while the full order book was being requested - // return out, the full order book loop will process this item in the queue - return; - } - // else new partial book with full order book available, will get dequeued below - - // check if any old books for this symbol, if so process them first - // lock dictionary of queues for lookup only - lock (partialOrderBookQueues) - { - partialOrderBookQueues.TryGetValue(newOrderBook.MarketSymbol, out partialOrderBookQueue); - } - - if (partialOrderBookQueue != null) - { - // lock the individual queue for processing, fifo queue - lock (partialOrderBookQueue) - { - while (partialOrderBookQueue.Count != 0) - { - updateOrderBook(fullOrderBook, partialOrderBookQueue.Dequeue()); - } - } - } - } break; - - case WebSocketOrderBookType.FullBookFirstThenDeltas: - { - // First response from exchange will be the full order book. - // Subsequent updates will be deltas, at least some exchanges have their heads on straight - if (!foundFullBook) - { - fullBooks[newOrderBook.MarketSymbol] = fullOrderBook = newOrderBook; - } - else - { - updateOrderBook(fullOrderBook, newOrderBook); - } - } break; - - case WebSocketOrderBookType.FullBookAlways: - { - // Websocket always returns full order book, some exchanges think CPU and bandwidth are free... - fullBooks[newOrderBook.MarketSymbol] = fullOrderBook = newOrderBook; - } break; - } - - fullOrderBook.LastUpdatedUtc = CryptoUtility.UtcNow; - callback(fullOrderBook); - } - - IWebSocket socket = await api.GetDeltaOrderBookWebSocketAsync(async (b) => - { - try - { - await innerCallback(b); - } - catch - { - } - }, maxCount, symbols); - socket.Connected += (s) => - { - // when we re-connect, we must invalidate the order books, who knows how long we were disconnected - // and how out of date the order books are - fullBooks.Clear(); - lock (partialOrderBookQueues) - { - partialOrderBookQueues.Clear(); - } - return Task.CompletedTask; - }; - return socket; - } - - /// - /// Get cache of symbols metadata and put into a dictionary. This method looks in the cache first, and if found, returns immediately, otherwise makes a network request and puts it in the cache - /// - /// Exchange API - /// Dictionary of symbol name and market, or null if there was an error - public static async Task> GetExchangeMarketDictionaryFromCacheAsync(this ExchangeAPI api) - { - await new SynchronizationContextRemover(); - CachedItem> cacheResult = await api.Cache.GetOrCreate>(nameof(GetExchangeMarketDictionaryFromCacheAsync), async () => - { - try - { - Dictionary symbolsMetadataDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); - IEnumerable symbolsMetadata = await api.GetMarketSymbolsMetadataAsync(); - - // build a new lookup dictionary - foreach (ExchangeMarket symbolMetadata in symbolsMetadata) - { - symbolsMetadataDictionary[symbolMetadata.MarketSymbol] = symbolMetadata; - } - - // return the cached dictionary for 4 hours - return new CachedItem>(symbolsMetadataDictionary, CryptoUtility.UtcNow.AddHours(4.0)); - } - catch// (Exception ex) - { - // if the network goes down this could log quite a lot of exceptions... - //Logger.Error(ex); - return new CachedItem>(); - } - }); - if (cacheResult.Found) - { - return cacheResult.Value; - } - return null; - } - - /// - /// Place a limit order by first querying the order book and then placing the order for a threshold below the bid or above the ask that would fully fulfill the amount. - /// The order book is scanned until an amount of bids or asks that will fulfill the order is found and then the order is placed at the lowest bid or highest ask price multiplied - /// by priceThreshold. - /// - /// Symbol to sell - /// Amount to sell - /// True for buy, false for sell - /// Amount of bids/asks to request in the order book - /// Threshold below the lowest bid or above the highest ask to set the limit order price at. For buys, this is converted to 1 / priceThreshold. - /// This can be set to 0 if you want to set the price like a market order. - /// If the lowest bid/highest ask price divided by the highest bid/lowest ask price is below this threshold, throw an exception. - /// This ensures that your order does not buy or sell at an extreme margin. - /// Whether to abort if the order book does not have enough bids or ask amounts to fulfill the order. - /// Order result - public static async Task PlaceSafeMarketOrderAsync(this ExchangeAPI api, string symbol, decimal amount, bool isBuy, int orderBookCount = 100, decimal priceThreshold = 0.9m, - decimal thresholdToAbort = 0.75m, bool abortIfOrderBookTooSmall = false) - { - if (priceThreshold > 0.9m) - { - throw new APIException("You cannot specify a price threshold above 0.9m, otherwise there is a chance your order will never be fulfilled. For buys, this is " + - "converted to 1.0m / priceThreshold, so always specify the value below 0.9m"); - } - else if (priceThreshold <= 0m) - { - priceThreshold = 1m; - } - else if (isBuy && priceThreshold > 0m) - { - priceThreshold = 1.0m / priceThreshold; - } - ExchangeOrderBook book = await api.GetOrderBookAsync(symbol, orderBookCount); - if (book == null || (isBuy && book.Asks.Count == 0) || (!isBuy && book.Bids.Count == 0)) - { - throw new APIException($"Error getting order book for {symbol}"); - } - decimal counter = 0m; - decimal highPrice = decimal.MinValue; - decimal lowPrice = decimal.MaxValue; - if (isBuy) - { - foreach (ExchangeOrderPrice ask in book.Asks.Values) - { - counter += ask.Amount; - highPrice = Math.Max(highPrice, ask.Price); - lowPrice = Math.Min(lowPrice, ask.Price); - if (counter >= amount) - { - break; - } - } - } - else - { - foreach (ExchangeOrderPrice bid in book.Bids.Values) - { - counter += bid.Amount; - highPrice = Math.Max(highPrice, bid.Price); - lowPrice = Math.Min(lowPrice, bid.Price); - if (counter >= amount) - { - break; - } - } - } - if (abortIfOrderBookTooSmall && counter < amount) - { - throw new APIException($"{(isBuy ? "Buy" : "Sell") } order for {symbol} and amount {amount} cannot be fulfilled because the order book is too thin."); - } - else if (lowPrice / highPrice < thresholdToAbort) - { - throw new APIException($"{(isBuy ? "Buy" : "Sell")} order for {symbol} and amount {amount} would place for a price below threshold of {thresholdToAbort}, aborting."); - } - ExchangeOrderRequest request = new ExchangeOrderRequest - { - Amount = amount, + /// Contains useful extension methods and parsing for the ExchangeAPI classes + public static class ExchangeAPIExtensions + { + /// + /// Get full order book bids and asks via web socket. This is efficient and will + /// only use the order book deltas (if supported by the exchange). This method deals + /// with the complexity of different exchanges sending order books that are full, + /// partial or otherwise. + /// + /// Callback containing full order book + /// Max count of bids and asks - not all exchanges will honor this + /// parameter + /// Order book symbols or null/empty for all of them (if supported) + /// Web socket, call Dispose to close + public static async Task GetFullOrderBookWebSocketAsync(this IOrderBookProvider api, Action callback, int maxCount = 20, params string[] symbols) + { + if (api.WebSocketOrderBookType == WebSocketOrderBookType.None) + { + throw new NotSupportedException(api.GetType().Name + " does not support web socket order books"); + } + + // Notes: + // * Confirm with the Exchange's API docs whether the data in each event is the absolute quantity or differential quantity + // * Receiving an event that removes a price level that is not in your local order book can happen and is normal. + ConcurrentDictionary fullBooks = new ConcurrentDictionary(); + Dictionary> partialOrderBookQueues = new Dictionary>(); + + static void applyDelta(SortedDictionary deltaValues, SortedDictionary bookToEdit) + { + foreach (ExchangeOrderPrice record in deltaValues.Values) + { + if (record.Amount <= 0 || record.Price <= 0) + { + bookToEdit.Remove(record.Price); + } + else + { + bookToEdit[record.Price] = record; + } + } + } + + static void updateOrderBook(ExchangeOrderBook fullOrderBook, ExchangeOrderBook freshBook) + { + lock (fullOrderBook) + { + // update deltas as long as the full book is at or before the delta timestamp + if (fullOrderBook.SequenceId <= freshBook.SequenceId) + { + applyDelta(freshBook.Asks, fullOrderBook.Asks); + applyDelta(freshBook.Bids, fullOrderBook.Bids); + fullOrderBook.SequenceId = freshBook.SequenceId; + } + } + } + + async Task innerCallback(ExchangeOrderBook newOrderBook) + { + // depending on the exchange, newOrderBook may be a complete or partial order book + // ideally all exchanges would send the full order book on first message, followed by delta order books + // but this is not the case + + bool foundFullBook = fullBooks.TryGetValue(newOrderBook.MarketSymbol, out ExchangeOrderBook fullOrderBook); + switch (api.WebSocketOrderBookType) + { + case WebSocketOrderBookType.DeltasOnly: + { + // Fetch an initial book the first time and apply deltas on top + // send these exchanges scathing support tickets that they should send + // the full book for the first web socket callback message + Queue partialOrderBookQueue; + bool requestFullOrderBook = false; + + // attempt to find the right queue to put the partial order book in to be processed later + lock (partialOrderBookQueues) + { + if (!partialOrderBookQueues.TryGetValue(newOrderBook.MarketSymbol, out partialOrderBookQueue)) + { + // no queue found, make a new one + partialOrderBookQueues[newOrderBook.MarketSymbol] = partialOrderBookQueue = new Queue(); + requestFullOrderBook = !foundFullBook; + } + + // always enqueue the partial order book, they get dequeued down below + partialOrderBookQueue.Enqueue(newOrderBook); + } + + // request the entire order book if we need it + if (requestFullOrderBook) + { + fullOrderBook = await api.GetOrderBookAsync(newOrderBook.MarketSymbol, maxCount); + fullOrderBook.MarketSymbol = newOrderBook.MarketSymbol; + fullBooks[newOrderBook.MarketSymbol] = fullOrderBook; + } + else if (!foundFullBook) + { + // we got a partial book while the full order book was being requested + // return out, the full order book loop will process this item in the queue + return; + } + // else new partial book with full order book available, will get dequeued below + + // check if any old books for this symbol, if so process them first + // lock dictionary of queues for lookup only + lock (partialOrderBookQueues) + { + partialOrderBookQueues.TryGetValue(newOrderBook.MarketSymbol, out partialOrderBookQueue); + } + + if (partialOrderBookQueue != null) + { + // lock the individual queue for processing, fifo queue + lock (partialOrderBookQueue) + { + while (partialOrderBookQueue.Count != 0) + { + updateOrderBook(fullOrderBook, partialOrderBookQueue.Dequeue()); + } + } + } + } + break; + + case WebSocketOrderBookType.FullBookFirstThenDeltas: + { + // First response from exchange will be the full order book. + // Subsequent updates will be deltas, at least some exchanges have their heads on straight + if (!foundFullBook) + { + fullBooks[newOrderBook.MarketSymbol] = fullOrderBook = newOrderBook; + } + else + { + updateOrderBook(fullOrderBook, newOrderBook); + } + } + break; + + case WebSocketOrderBookType.FullBookAlways: + { + // Websocket always returns full order book, some exchanges think CPU and bandwidth are free... + fullBooks[newOrderBook.MarketSymbol] = fullOrderBook = newOrderBook; + } + break; + } + + fullOrderBook.LastUpdatedUtc = CryptoUtility.UtcNow; + callback(fullOrderBook); + } + + IWebSocket socket = await api.GetDeltaOrderBookWebSocketAsync(async (b) => + { + try + { + await innerCallback(b); + } + catch + { + } + }, maxCount, symbols); + socket.Connected += (s) => + { + // when we re-connect, we must invalidate the order books, who knows how long we were disconnected + // and how out of date the order books are + fullBooks.Clear(); + lock (partialOrderBookQueues) + { + partialOrderBookQueues.Clear(); + } + return Task.CompletedTask; + }; + return socket; + } + + /// + /// Get cache of symbols metadata and put into a dictionary. This method looks in the cache first, and if found, returns immediately, otherwise makes a network request and puts it in the cache + /// + /// Exchange API + /// Dictionary of symbol name and market, or null if there was an error + public static async Task> GetExchangeMarketDictionaryFromCacheAsync(this ExchangeAPI api) + { + await new SynchronizationContextRemover(); + CachedItem> cacheResult = await api.Cache.GetOrCreate>(nameof(GetExchangeMarketDictionaryFromCacheAsync), async () => + { + try + { + Dictionary symbolsMetadataDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + IEnumerable symbolsMetadata = await api.GetMarketSymbolsMetadataAsync(); + + // build a new lookup dictionary + foreach (ExchangeMarket symbolMetadata in symbolsMetadata) + { + symbolsMetadataDictionary[symbolMetadata.MarketSymbol] = symbolMetadata; + } + + // return the cached dictionary for 4 hours + return new CachedItem>(symbolsMetadataDictionary, CryptoUtility.UtcNow.AddHours(4.0)); + } + catch// (Exception ex) + { + // if the network goes down this could log quite a lot of exceptions... + //Logger.Error(ex); + return new CachedItem>(); + } + }); + if (cacheResult.Found) + { + return cacheResult.Value; + } + return null; + } + + /// + /// Place a limit order by first querying the order book and then placing the order for a threshold below the bid or above the ask that would fully fulfill the amount. + /// The order book is scanned until an amount of bids or asks that will fulfill the order is found and then the order is placed at the lowest bid or highest ask price multiplied + /// by priceThreshold. + /// + /// Symbol to sell + /// Amount to sell + /// True for buy, false for sell + /// Amount of bids/asks to request in the order book + /// Threshold below the lowest bid or above the highest ask to set the limit order price at. For buys, this is converted to 1 / priceThreshold. + /// This can be set to 0 if you want to set the price like a market order. + /// If the lowest bid/highest ask price divided by the highest bid/lowest ask price is below this threshold, throw an exception. + /// This ensures that your order does not buy or sell at an extreme margin. + /// Whether to abort if the order book does not have enough bids or ask amounts to fulfill the order. + /// Order result + public static async Task PlaceSafeMarketOrderAsync(this ExchangeAPI api, string symbol, decimal amount, bool isBuy, int orderBookCount = 100, decimal priceThreshold = 0.9m, + decimal thresholdToAbort = 0.75m, bool abortIfOrderBookTooSmall = false) + { + if (priceThreshold > 0.9m) + { + throw new APIException("You cannot specify a price threshold above 0.9m, otherwise there is a chance your order will never be fulfilled. For buys, this is " + + "converted to 1.0m / priceThreshold, so always specify the value below 0.9m"); + } + else if (priceThreshold <= 0m) + { + priceThreshold = 1m; + } + else if (isBuy && priceThreshold > 0m) + { + priceThreshold = 1.0m / priceThreshold; + } + ExchangeOrderBook book = await api.GetOrderBookAsync(symbol, orderBookCount); + if (book == null || (isBuy && book.Asks.Count == 0) || (!isBuy && book.Bids.Count == 0)) + { + throw new APIException($"Error getting order book for {symbol}"); + } + decimal counter = 0m; + decimal highPrice = decimal.MinValue; + decimal lowPrice = decimal.MaxValue; + if (isBuy) + { + foreach (ExchangeOrderPrice ask in book.Asks.Values) + { + counter += ask.Amount; + highPrice = Math.Max(highPrice, ask.Price); + lowPrice = Math.Min(lowPrice, ask.Price); + if (counter >= amount) + { + break; + } + } + } + else + { + foreach (ExchangeOrderPrice bid in book.Bids.Values) + { + counter += bid.Amount; + highPrice = Math.Max(highPrice, bid.Price); + lowPrice = Math.Min(lowPrice, bid.Price); + if (counter >= amount) + { + break; + } + } + } + if (abortIfOrderBookTooSmall && counter < amount) + { + throw new APIException($"{(isBuy ? "Buy" : "Sell") } order for {symbol} and amount {amount} cannot be fulfilled because the order book is too thin."); + } + else if (lowPrice / highPrice < thresholdToAbort) + { + throw new APIException($"{(isBuy ? "Buy" : "Sell")} order for {symbol} and amount {amount} would place for a price below threshold of {thresholdToAbort}, aborting."); + } + ExchangeOrderRequest request = new ExchangeOrderRequest + { + Amount = amount, IsBuy = isBuy, - OrderType = OrderType.Limit, - Price = CryptoUtility.RoundAmount((isBuy ? highPrice : lowPrice) * priceThreshold), - ShouldRoundAmount = true, - MarketSymbol = symbol - }; - ExchangeOrderResult result = await api.PlaceOrderAsync(request); - - // wait about 10 seconds until the order is fulfilled - int i = 0; - const int maxTries = 20; // 500 ms for each try - for (; i < maxTries; i++) - { - await System.Threading.Tasks.Task.Delay(500); - result = await api.GetOrderDetailsAsync(result.OrderId, symbol); - switch (result.Result) - { - case ExchangeAPIOrderResult.Filled: - case ExchangeAPIOrderResult.Canceled: - case ExchangeAPIOrderResult.Error: - i = maxTries + 1; - break; - } - } - - if (i == maxTries) - { - throw new APIException($"{(isBuy ? "Buy" : "Sell")} order for {symbol} and amount {amount} timed out and may not have been fulfilled"); - } - - return result; - } - - /// Common order book parsing method, most exchanges use "asks" and "bids" with - /// arrays of length 2 for price and amount (or amount and price) - /// Token - /// Asks key - /// Bids key - /// Max count - /// Order book - internal static ExchangeOrderBook ParseOrderBookFromJTokenArrays - ( - this JToken token, - string asks = "asks", - string bids = "bids", - string sequence = "ts", - int maxCount = 100 - ) - { - var book = new ExchangeOrderBook { SequenceId = token[sequence].ConvertInvariant() }; - foreach (JArray array in token[asks]) - { - var depth = new ExchangeOrderPrice { Price = array[0].ConvertInvariant(), Amount = array[1].ConvertInvariant() }; - book.Asks[depth.Price] = depth; - if (book.Asks.Count == maxCount) - { - break; - } - } - - foreach (JArray array in token[bids]) - { - var depth = new ExchangeOrderPrice { Price = array[0].ConvertInvariant(), Amount = array[1].ConvertInvariant() }; - book.Bids[depth.Price] = depth; - if (book.Bids.Count == maxCount) - { - break; - } - } - - return book; - } - - /// Common order book parsing method, checks for "amount" or "quantity" and "price" - /// elements - /// Token - /// Asks key - /// Bids key - /// Price key - /// Quantity key - /// Sequence key - /// Max count - /// Order book - internal static ExchangeOrderBook ParseOrderBookFromJTokenDictionaries - ( - this JToken token, - string asks = "asks", - string bids = "bids", - string price = "price", - string amount = "amount", - string sequence = "ts", - int maxCount = 100 - ) - { - var book = new ExchangeOrderBook { SequenceId = token[sequence].ConvertInvariant() }; - foreach (JToken ask in token[asks]) - { - var depth = new ExchangeOrderPrice { Price = ask[price].ConvertInvariant(), Amount = ask[amount].ConvertInvariant() }; - book.Asks[depth.Price] = depth; - if (book.Asks.Count == maxCount) - { - break; - } - } - - foreach (JToken bid in token[bids]) - { - var depth = new ExchangeOrderPrice { Price = bid[price].ConvertInvariant(), Amount = bid[amount].ConvertInvariant() }; - book.Bids[depth.Price] = depth; - if (book.Bids.Count == maxCount) - { - break; - } - } - - return book; - } - - /// - /// Parse a JToken into a ticker - /// - /// ExchangeAPI - /// Token - /// Symbol - /// Ask key - /// Bid key - /// Last key - /// Base currency volume key - /// Quote currency volume key - /// Timestamp key - /// Timestamp type - /// Base currency key - /// Quote currency key - /// Id key - /// ExchangeTicker - internal static async Task ParseTickerAsync(this ExchangeAPI api, JToken token, string marketSymbol, - object askKey, object bidKey, object lastKey, object baseVolumeKey, - object? quoteVolumeKey = null, object? timestampKey = null, TimestampType timestampType = TimestampType.None, - object? baseCurrencyKey = null, object? quoteCurrencyKey = null, object? idKey = null) - { - if (token == null || !token.HasValues) - { - return null; - } - decimal last = token[lastKey].ConvertInvariant(); - - // parse out volumes, handle cases where one or both do not exist - token.ParseVolumes(baseVolumeKey, quoteVolumeKey, last, out decimal baseCurrencyVolume, out decimal quoteCurrencyVolume); - - // pull out timestamp - DateTime timestamp = timestampKey == null - ? CryptoUtility.UtcNow - : CryptoUtility.ParseTimestamp(token[timestampKey], timestampType); - - // split apart the symbol if we have a separator, otherwise just put the symbol for base and convert symbol - string baseCurrency; - string quoteCurrency; - if (baseCurrencyKey != null && quoteCurrencyKey != null) - { - baseCurrency = token[baseCurrencyKey].ToStringInvariant(); - quoteCurrency = token[quoteCurrencyKey].ToStringInvariant(); - } - else if (string.IsNullOrWhiteSpace(marketSymbol)) - { - throw new ArgumentNullException(nameof(marketSymbol)); - } - else - { - (baseCurrency, quoteCurrency) = await api.ExchangeMarketSymbolToCurrenciesAsync(marketSymbol); - } - - // create the ticker and return it - decimal ask = 0m; - decimal bid = 0m; - if (askKey != null) - { - JToken askValue = token[askKey]; - if (askValue is JArray) - { - askValue = askValue[0]; - } - ask = askValue.ConvertInvariant(); - } - if (bidKey != null) - { - JToken bidValue = token[bidKey]; - if (bidValue is JArray) - { - bidValue = bidValue[0]; - } - bid = bidValue.ConvertInvariant(); - } - ExchangeTicker ticker = new ExchangeTicker - { - MarketSymbol = marketSymbol, - Ask = ask, - Bid = bid, - Id = (idKey == null ? null : token[idKey].ToStringInvariant()), - Last = last, - Volume = new ExchangeVolume - { - BaseCurrencyVolume = baseCurrencyVolume, - BaseCurrency = baseCurrency, - QuoteCurrencyVolume = quoteCurrencyVolume, - QuoteCurrency = quoteCurrency, - Timestamp = timestamp - } - }; - return ticker; - } + OrderType = OrderType.Limit, + Price = CryptoUtility.RoundAmount((isBuy ? highPrice : lowPrice) * priceThreshold), + ShouldRoundAmount = true, + MarketSymbol = symbol + }; + ExchangeOrderResult result = await api.PlaceOrderAsync(request); + + // wait about 10 seconds until the order is fulfilled + int i = 0; + const int maxTries = 20; // 500 ms for each try + for (; i < maxTries; i++) + { + await System.Threading.Tasks.Task.Delay(500); + result = await api.GetOrderDetailsAsync(result.OrderId, symbol); + switch (result.Result) + { + case ExchangeAPIOrderResult.Filled: + case ExchangeAPIOrderResult.Canceled: + case ExchangeAPIOrderResult.Error: + i = maxTries + 1; + break; + } + } + + if (i == maxTries) + { + throw new APIException($"{(isBuy ? "Buy" : "Sell")} order for {symbol} and amount {amount} timed out and may not have been fulfilled"); + } + + return result; + } + + /// Common order book parsing method, most exchanges use "asks" and "bids" with + /// arrays of length 2 for price and amount (or amount and price) + /// Token + /// Asks key + /// Bids key + /// Max count + /// Order book + internal static ExchangeOrderBook ParseOrderBookFromJTokenArrays + ( + this JToken token, + string asks = "asks", + string bids = "bids", + string sequence = "ts", + int maxCount = 100 + ) + { + var book = new ExchangeOrderBook { SequenceId = token[sequence].ConvertInvariant() }; + foreach (JArray array in token[asks]) + { + var depth = new ExchangeOrderPrice { Price = array[0].ConvertInvariant(), Amount = array[1].ConvertInvariant() }; + book.Asks[depth.Price] = depth; + if (book.Asks.Count == maxCount) + { + break; + } + } + + foreach (JArray array in token[bids]) + { + var depth = new ExchangeOrderPrice { Price = array[0].ConvertInvariant(), Amount = array[1].ConvertInvariant() }; + book.Bids[depth.Price] = depth; + if (book.Bids.Count == maxCount) + { + break; + } + } + + return book; + } + + /// Common order book parsing method, checks for "amount" or "quantity" and "price" + /// elements + /// Token + /// Asks key + /// Bids key + /// Price key + /// Quantity key + /// Sequence key + /// Max count + /// Order book + internal static ExchangeOrderBook ParseOrderBookFromJTokenDictionaries + ( + this JToken token, + string asks = "asks", + string bids = "bids", + string price = "price", + string amount = "amount", + string sequence = "ts", + int maxCount = 100 + ) + { + var book = new ExchangeOrderBook { SequenceId = token[sequence].ConvertInvariant() }; + foreach (JToken ask in token[asks]) + { + var depth = new ExchangeOrderPrice { Price = ask[price].ConvertInvariant(), Amount = ask[amount].ConvertInvariant() }; + book.Asks[depth.Price] = depth; + if (book.Asks.Count == maxCount) + { + break; + } + } + + foreach (JToken bid in token[bids]) + { + var depth = new ExchangeOrderPrice { Price = bid[price].ConvertInvariant(), Amount = bid[amount].ConvertInvariant() }; + book.Bids[depth.Price] = depth; + if (book.Bids.Count == maxCount) + { + break; + } + } + + return book; + } + + /// + /// Parse a JToken into a ticker + /// + /// ExchangeAPI + /// Token + /// Symbol + /// Ask key + /// Bid key + /// Last key + /// Base currency volume key + /// Quote currency volume key + /// Timestamp key + /// Timestamp type + /// Base currency key + /// Quote currency key + /// Id key + /// ExchangeTicker + internal static async Task ParseTickerAsync(this ExchangeAPI api, JToken token, string marketSymbol, + object askKey, object bidKey, object lastKey, object baseVolumeKey, + object? quoteVolumeKey = null, object? timestampKey = null, TimestampType timestampType = TimestampType.None, + object? baseCurrencyKey = null, object? quoteCurrencyKey = null, object? idKey = null) + { + if (token == null || !token.HasValues) + { + return null; + } + decimal last = token[lastKey].ConvertInvariant(); + + // parse out volumes, handle cases where one or both do not exist + token.ParseVolumes(baseVolumeKey, quoteVolumeKey, last, out decimal baseCurrencyVolume, out decimal quoteCurrencyVolume); + + // pull out timestamp + DateTime timestamp = timestampKey == null + ? CryptoUtility.UtcNow + : CryptoUtility.ParseTimestamp(token[timestampKey], timestampType); + + // split apart the symbol if we have a separator, otherwise just put the symbol for base and convert symbol + string baseCurrency; + string quoteCurrency; + if (baseCurrencyKey != null && quoteCurrencyKey != null) + { + baseCurrency = token[baseCurrencyKey].ToStringInvariant(); + quoteCurrency = token[quoteCurrencyKey].ToStringInvariant(); + } + else if (string.IsNullOrWhiteSpace(marketSymbol)) + { + throw new ArgumentNullException(nameof(marketSymbol)); + } + else + { + (baseCurrency, quoteCurrency) = await api.ExchangeMarketSymbolToCurrenciesAsync(marketSymbol); + } + + // create the ticker and return it + decimal ask = 0m; + decimal bid = 0m; + if (askKey != null) + { + JToken askValue = token[askKey]; + if (askValue is JArray) + { + askValue = askValue[0]; + } + ask = askValue.ConvertInvariant(); + } + if (bidKey != null) + { + JToken bidValue = token[bidKey]; + if (bidValue is JArray) + { + bidValue = bidValue[0]; + } + bid = bidValue.ConvertInvariant(); + } + ExchangeTicker ticker = new ExchangeTicker + { + MarketSymbol = marketSymbol, + ApiResponse = token, + Ask = ask, + Bid = bid, + Id = (idKey == null ? null : token[idKey].ToStringInvariant()), + Last = last, + Volume = new ExchangeVolume + { + BaseCurrencyVolume = baseCurrencyVolume, + BaseCurrency = baseCurrency, + QuoteCurrencyVolume = quoteCurrencyVolume, + QuoteCurrency = quoteCurrency, + Timestamp = timestamp + } + }; + return ticker; + } #region ParseTrade() methods + /// /// Parse a trade /// @@ -538,7 +544,6 @@ internal static ExchangeTrade ParseTrade(this JToken token, object amountKey, ob { return ParseTradeComponents(token, amountKey, priceKey, typeKey, timestampKey, timestampType, idKey, typeKeyIsBuyValue); - } internal static ExchangeTrade ParseTradeBinance(this JToken token, object amountKey, object priceKey, object typeKey, @@ -654,7 +659,8 @@ internal static T ParseTradeComponents(this JToken token, object amountKey, o trade.Flags = isBuy ? ExchangeTradeFlags.IsBuy : default; return trade; } - #endregion + + #endregion ParseTrade() methods /// /// Parse volume from JToken @@ -666,77 +672,77 @@ internal static T ParseTradeComponents(this JToken token, object amountKey, o /// Receive base currency volume /// Receive quote currency volume internal static void ParseVolumes(this JToken token, object baseVolumeKey, object? quoteVolumeKey, decimal last, out decimal baseCurrencyVolume, out decimal quoteCurrencyVolume) - { - // parse out volumes, handle cases where one or both do not exist - if (baseVolumeKey == null) - { - if (quoteVolumeKey == null) - { - baseCurrencyVolume = quoteCurrencyVolume = 0m; - } - else - { - quoteCurrencyVolume = token[quoteVolumeKey].ConvertInvariant(); - baseCurrencyVolume = (last <= 0m ? 0m : quoteCurrencyVolume / last); - } - } - else - { - baseCurrencyVolume = (token is JObject jObj - ? jObj.SelectToken((string) baseVolumeKey) - : token[baseVolumeKey] - ).ConvertInvariant(); - if (quoteVolumeKey == null) - { - quoteCurrencyVolume = baseCurrencyVolume * last; - } - else - { - quoteCurrencyVolume = token[quoteVolumeKey].ConvertInvariant(); - } - } - } - - /// - /// Parse market candle from JToken - /// - /// Named item - /// JToken - /// Symbol - /// Period seconds - /// Open key - /// High key - /// Low key - /// Close key - /// Timestamp key - /// Timestamp type - /// Base currency volume key - /// Quote currency volume key - /// Weighted average key - /// MarketCandle - internal static MarketCandle ParseCandle(this INamed named, JToken token, string marketSymbol, int periodSeconds, object openKey, object highKey, object lowKey, - object closeKey, object timestampKey, TimestampType timestampType, object baseVolumeKey, object? quoteVolumeKey = null, object? weightedAverageKey = null) - { - MarketCandle candle = new MarketCandle - { - ClosePrice = token[closeKey].ConvertInvariant(), - ExchangeName = named.Name, - HighPrice = token[highKey].ConvertInvariant(), - LowPrice = token[lowKey].ConvertInvariant(), - Name = marketSymbol, - OpenPrice = token[openKey].ConvertInvariant(), - PeriodSeconds = periodSeconds, - Timestamp = CryptoUtility.ParseTimestamp(token[timestampKey], timestampType) - }; - - token.ParseVolumes(baseVolumeKey, quoteVolumeKey, candle.ClosePrice, out decimal baseVolume, out decimal convertVolume); - candle.BaseCurrencyVolume = (double)baseVolume; - candle.QuoteCurrencyVolume = (double)convertVolume; - if (weightedAverageKey != null) - { - candle.WeightedAverage = token[weightedAverageKey].ConvertInvariant(); - } - return candle; - } + { + // parse out volumes, handle cases where one or both do not exist + if (baseVolumeKey == null) + { + if (quoteVolumeKey == null) + { + baseCurrencyVolume = quoteCurrencyVolume = 0m; + } + else + { + quoteCurrencyVolume = token[quoteVolumeKey].ConvertInvariant(); + baseCurrencyVolume = (last <= 0m ? 0m : quoteCurrencyVolume / last); + } + } + else + { + baseCurrencyVolume = (token is JObject jObj + ? jObj.SelectToken((string)baseVolumeKey) + : token[baseVolumeKey] + ).ConvertInvariant(); + if (quoteVolumeKey == null) + { + quoteCurrencyVolume = baseCurrencyVolume * last; + } + else + { + quoteCurrencyVolume = token[quoteVolumeKey].ConvertInvariant(); + } + } + } + + /// + /// Parse market candle from JToken + /// + /// Named item + /// JToken + /// Symbol + /// Period seconds + /// Open key + /// High key + /// Low key + /// Close key + /// Timestamp key + /// Timestamp type + /// Base currency volume key + /// Quote currency volume key + /// Weighted average key + /// MarketCandle + internal static MarketCandle ParseCandle(this INamed named, JToken token, string marketSymbol, int periodSeconds, object openKey, object highKey, object lowKey, + object closeKey, object timestampKey, TimestampType timestampType, object baseVolumeKey, object? quoteVolumeKey = null, object? weightedAverageKey = null) + { + MarketCandle candle = new MarketCandle + { + ClosePrice = token[closeKey].ConvertInvariant(), + ExchangeName = named.Name, + HighPrice = token[highKey].ConvertInvariant(), + LowPrice = token[lowKey].ConvertInvariant(), + Name = marketSymbol, + OpenPrice = token[openKey].ConvertInvariant(), + PeriodSeconds = periodSeconds, + Timestamp = CryptoUtility.ParseTimestamp(token[timestampKey], timestampType) + }; + + token.ParseVolumes(baseVolumeKey, quoteVolumeKey, candle.ClosePrice, out decimal baseVolume, out decimal convertVolume); + candle.BaseCurrencyVolume = (double)baseVolume; + candle.QuoteCurrencyVolume = (double)convertVolume; + if (weightedAverageKey != null) + { + candle.WeightedAverage = token[weightedAverageKey].ConvertInvariant(); + } + return candle; + } } } diff --git a/src/ExchangeSharp/Model/ExchangeTicker.cs b/src/ExchangeSharp/Model/ExchangeTicker.cs index 92127a12e..308b58957 100644 --- a/src/ExchangeSharp/Model/ExchangeTicker.cs +++ b/src/ExchangeSharp/Model/ExchangeTicker.cs @@ -18,112 +18,118 @@ The above copyright notice and this permission notice shall be included in all c using System.Threading.Tasks; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace ExchangeSharp { - /// - /// Details of the current price of an exchange asset - /// - public sealed class ExchangeTicker - { - /// - /// An exchange specific id if known, otherwise null - /// - public string Id { get; set; } - - /// - /// The currency pair symbol that this ticker is in reference to - /// - public string MarketSymbol { get; set; } - - /// - /// The bid is the price to sell at - /// - public decimal Bid { get; set; } - - /// - /// The ask is the price to buy at - /// - public decimal Ask { get; set; } - - /// - /// The last trade purchase price - /// - public decimal Last { get; set; } - - /// - /// Volume info - /// - public ExchangeVolume Volume { get; set; } - - /// - /// Get a string for this ticker - /// - /// String - public override string ToString() - { - return string.Format("Bid: {0}, Ask: {1}, Last: {2}, Vol: {3}", Bid, Ask, Last, Volume); - } - - /// - /// Write to writer - /// - /// Writer - public void ToBinary(BinaryWriter writer) - { - writer.Write((double)Bid); - writer.Write((double)Ask); - writer.Write((double)Last); - Volume.ToBinary(writer); - } - - /// - /// Read from reader - /// - /// Reader - public void FromBinary(BinaryReader reader) - { - Bid = (decimal)reader.ReadDouble(); - Ask = (decimal)reader.ReadDouble(); - Last = (decimal)reader.ReadDouble(); - Volume = (Volume ?? new ExchangeVolume()); - Volume.FromBinary(reader); - } - } - - /// - /// Info about exchange volume - /// - public sealed class ExchangeVolume - { - /// - /// Last volume update timestamp - /// - public DateTime Timestamp { get; set; } - - /// - /// Quote / Price currency - will equal base currency if exchange doesn't break it out by price unit and quantity unit - /// In BTC-USD, this would be USD - /// - public string QuoteCurrency { get; set; } - - /// - /// Amount in units of the QuoteCurrency - will equal BaseCurrencyVolume if exchange doesn't break it out by price unit and quantity unit - /// In BTC-USD, this would be USD volume - /// - public decimal QuoteCurrencyVolume { get; set; } - - /// - /// Base currency - /// In BTC-USD, this would be BTC - /// - public string BaseCurrency { get; set; } - - /// - /// Base currency amount (this many units total) - /// In BTC-USD this would be BTC volume - /// - public decimal BaseCurrencyVolume { get; set; } + /// + /// Details of the current price of an exchange asset + /// + public sealed class ExchangeTicker + { + /// + /// An exchange specific id if known, otherwise null + /// + public string Id { get; set; } + + /// + /// The currency pair symbol that this ticker is in reference to + /// + public string MarketSymbol { get; set; } + + /// + /// The bid is the price to sell at + /// + public decimal Bid { get; set; } + + /// + /// The ask is the price to buy at + /// + public decimal Ask { get; set; } + + /// + /// The last trade purchase price + /// + public decimal Last { get; set; } + + /// + /// This property contains the content of the complete api response. This may contain additional information + /// + public JToken ApiResponse { get; set; } + + /// + /// Volume info + /// + public ExchangeVolume Volume { get; set; } + + /// + /// Get a string for this ticker + /// + /// String + public override string ToString() + { + return string.Format("Bid: {0}, Ask: {1}, Last: {2}, Vol: {3}", Bid, Ask, Last, Volume); + } + + /// + /// Write to writer + /// + /// Writer + public void ToBinary(BinaryWriter writer) + { + writer.Write((double)Bid); + writer.Write((double)Ask); + writer.Write((double)Last); + Volume.ToBinary(writer); + } + + /// + /// Read from reader + /// + /// Reader + public void FromBinary(BinaryReader reader) + { + Bid = (decimal)reader.ReadDouble(); + Ask = (decimal)reader.ReadDouble(); + Last = (decimal)reader.ReadDouble(); + Volume = (Volume ?? new ExchangeVolume()); + Volume.FromBinary(reader); + } + } + + /// + /// Info about exchange volume + /// + public sealed class ExchangeVolume + { + /// + /// Last volume update timestamp + /// + public DateTime Timestamp { get; set; } + + /// + /// Quote / Price currency - will equal base currency if exchange doesn't break it out by price unit and quantity unit + /// In BTC-USD, this would be USD + /// + public string QuoteCurrency { get; set; } + + /// + /// Amount in units of the QuoteCurrency - will equal BaseCurrencyVolume if exchange doesn't break it out by price unit and quantity unit + /// In BTC-USD, this would be USD volume + /// + public decimal QuoteCurrencyVolume { get; set; } + + /// + /// Base currency + /// In BTC-USD, this would be BTC + /// + public string BaseCurrency { get; set; } + + /// + /// Base currency amount (this many units total) + /// In BTC-USD this would be BTC volume + /// + public decimal BaseCurrencyVolume { get; set; } /// public override string ToString() @@ -136,25 +142,25 @@ public override string ToString() /// /// Binary writer public void ToBinary(BinaryWriter writer) - { - writer.Write(Timestamp.ToUniversalTime().Ticks); - writer.Write(QuoteCurrency); - writer.Write((double)QuoteCurrencyVolume); - writer.Write(BaseCurrency); - writer.Write((double)BaseCurrencyVolume); - } - - /// - /// Read from a binary reader - /// - /// Binary reader - public void FromBinary(BinaryReader reader) - { - Timestamp = new DateTime(reader.ReadInt64(), DateTimeKind.Utc); - QuoteCurrency = reader.ReadString(); - QuoteCurrencyVolume = (decimal)reader.ReadDouble(); - BaseCurrency = reader.ReadString(); - BaseCurrencyVolume = (decimal)reader.ReadDouble(); - } - } + { + writer.Write(Timestamp.ToUniversalTime().Ticks); + writer.Write(QuoteCurrency); + writer.Write((double)QuoteCurrencyVolume); + writer.Write(BaseCurrency); + writer.Write((double)BaseCurrencyVolume); + } + + /// + /// Read from a binary reader + /// + /// Binary reader + public void FromBinary(BinaryReader reader) + { + Timestamp = new DateTime(reader.ReadInt64(), DateTimeKind.Utc); + QuoteCurrency = reader.ReadString(); + QuoteCurrencyVolume = (decimal)reader.ReadDouble(); + BaseCurrency = reader.ReadString(); + BaseCurrencyVolume = (decimal)reader.ReadDouble(); + } + } }