From 61fd46c44a7cd550c2c709ed7cfe6d981e36bc84 Mon Sep 17 00:00:00 2001 From: EmergentCybernetics Date: Wed, 5 May 2021 09:08:15 -0500 Subject: [PATCH 1/2] Linting. Consistent tabs for code formatting. --- .../API/Exchanges/Gemini/ExchangeGeminiAPI.cs | 556 +++---- .../API/Exchanges/Kraken/ExchangeKrakenAPI.cs | 1346 ++++++++-------- .../API/Exchanges/_Base/ExchangeAPI.cs | 1361 +++++++++-------- .../API/Exchanges/_Base/IExchangeAPI.cs | 426 +++--- 4 files changed, 1861 insertions(+), 1828 deletions(-) diff --git a/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs b/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs index 772d7dfdc..58a789d75 100644 --- a/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs @@ -19,82 +19,81 @@ The above copyright notice and this permission notice shall be included in all c using System.Threading.Tasks; using System.Web; using System.Xml; - using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace ExchangeSharp { - public sealed partial class ExchangeGeminiAPI : ExchangeAPI - { - public override string BaseUrl { get; set; } = "https://api.gemini.com/v1"; + public sealed partial class ExchangeGeminiAPI : ExchangeAPI + { + public override string BaseUrl { get; set; } = "https://api.gemini.com/v1"; public override string BaseUrlWebSocket { get; set; } = "wss://api.gemini.com/v2/marketdata"; private ExchangeGeminiAPI() - { - MarketSymbolIsUppercase = false; - MarketSymbolSeparator = string.Empty; + { + MarketSymbolIsUppercase = false; + MarketSymbolSeparator = string.Empty; RateLimit = new RateGate(1, TimeSpan.FromSeconds(0.5)); - } - - private async Task ParseVolumeAsync(JToken token, string symbol) - { - ExchangeVolume vol = new ExchangeVolume(); - JProperty[] props = token.Children().ToArray(); - if (props.Length == 3) - { - var (baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync(symbol); - vol.QuoteCurrency = quoteCurrency.ToUpperInvariant(); - vol.QuoteCurrencyVolume = token[quoteCurrency.ToUpperInvariant()].ConvertInvariant(); - vol.BaseCurrency = baseCurrency.ToUpperInvariant(); - vol.BaseCurrencyVolume = token[baseCurrency.ToUpperInvariant()].ConvertInvariant(); - vol.Timestamp = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(props[2].Value.ConvertInvariant()); - } - - return vol; - } - - private ExchangeOrderResult ParseOrder(JToken result) - { - decimal amount = result["original_amount"].ConvertInvariant(); - decimal amountFilled = result["executed_amount"].ConvertInvariant(); - 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" - }; - } - - protected override Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) - { - if (CanMakeAuthenticatedRequest(payload)) - { - payload.Add("request", request.RequestUri.AbsolutePath); - string json = JsonConvert.SerializeObject(payload); - string json64 = System.Convert.ToBase64String(json.ToBytesUTF8()); - string hexSha384 = CryptoUtility.SHA384Sign(json64, CryptoUtility.ToUnsecureString(PrivateApiKey)); - request.AddHeader("X-GEMINI-PAYLOAD", json64); - request.AddHeader("X-GEMINI-SIGNATURE", hexSha384); - request.AddHeader("X-GEMINI-APIKEY", CryptoUtility.ToUnsecureString(PublicApiKey)); - request.Method = "POST"; - - // gemini 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 - } - return base.ProcessRequestAsync(request, payload); - } - - protected override async Task> OnGetMarketSymbolsAsync() - { - return await MakeJsonRequestAsync("/symbols"); - } + } + + private async Task ParseVolumeAsync(JToken token, string symbol) + { + ExchangeVolume vol = new ExchangeVolume(); + JProperty[] props = token.Children().ToArray(); + if (props.Length == 3) + { + var(baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync(symbol); + vol.QuoteCurrency = quoteCurrency.ToUpperInvariant(); + vol.QuoteCurrencyVolume = token[quoteCurrency.ToUpperInvariant()].ConvertInvariant(); + vol.BaseCurrency = baseCurrency.ToUpperInvariant(); + vol.BaseCurrencyVolume = token[baseCurrency.ToUpperInvariant()].ConvertInvariant(); + vol.Timestamp = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(props[2].Value.ConvertInvariant()); + } + + return vol; + } + + private ExchangeOrderResult ParseOrder(JToken result) + { + decimal amount = result["original_amount"].ConvertInvariant(); + decimal amountFilled = result["executed_amount"].ConvertInvariant(); + 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" + }; + } + + protected override Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + { + if (CanMakeAuthenticatedRequest(payload)) + { + payload.Add("request", request.RequestUri.AbsolutePath); + string json = JsonConvert.SerializeObject(payload); + string json64 = System.Convert.ToBase64String(json.ToBytesUTF8()); + string hexSha384 = CryptoUtility.SHA384Sign(json64, CryptoUtility.ToUnsecureString(PrivateApiKey)); + request.AddHeader("X-GEMINI-PAYLOAD", json64); + request.AddHeader("X-GEMINI-SIGNATURE", hexSha384); + request.AddHeader("X-GEMINI-APIKEY", CryptoUtility.ToUnsecureString(PublicApiKey)); + request.Method = "POST"; + + // gemini 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 + } + return base.ProcessRequestAsync(request, payload); + } + + protected override async Task> OnGetMarketSymbolsAsync() + { + return await MakeJsonRequestAsync("/symbols"); + } protected internal override async Task> OnGetMarketSymbolsMetadataAsync() { @@ -197,22 +196,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() }); } })); @@ -225,136 +224,135 @@ protected internal override async Task> OnGetMarketS } protected override async Task OnGetTickerAsync(string marketSymbol) - { - JToken obj = await MakeJsonRequestAsync("/pubticker/" + marketSymbol); - if (obj == null || obj.Count() == 0) - { - return null; - } - ExchangeTicker t = new ExchangeTicker - { - MarketSymbol = marketSymbol, - Ask = obj["ask"].ConvertInvariant(), - Bid = obj["bid"].ConvertInvariant(), - Last = obj["last"].ConvertInvariant() - }; - t.Volume = await ParseVolumeAsync(obj["volume"], marketSymbol); - return t; - } - - 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); - } - - protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) - { - ExchangeHistoricalTradeHelper state = new ExchangeHistoricalTradeHelper(this) - { - Callback = callback, - 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(); - } - - 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() }; - foreach (var kv in q) - { - if (kv.Available > 0m) - { - lookup[kv.Currency] = kv.Available; - } - } - return lookup; - } - - protected override async Task> OnGetAmountsAvailableToTradeAsync() - { - 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() }; - foreach (var kv in q) - { - if (kv.Available > 0m) - { - lookup[kv.Currency] = kv.Available; - } - } - return lookup; - } - - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) - { - if (order.OrderType == OrderType.Market) - { - throw new NotSupportedException("Order type " + order.OrderType + " not supported"); - } - - object nonce = await GenerateNonceAsync(); - Dictionary payload = new Dictionary - { - { "nonce", nonce }, - { "client_order_id", "ExchangeSharp_" + CryptoUtility.UtcNow.ToString("s", System.Globalization.CultureInfo.InvariantCulture) }, - { "symbol", order.MarketSymbol }, - { "amount", order.RoundAmount().ToStringInvariant() }, - { "price", order.Price.ToStringInvariant() }, - { "side", (order.IsBuy ? "buy" : "sell") }, - { "type", "exchange limit" } - }; - order.ExtraParameters.CopyTo(payload); - JToken obj = await MakeJsonRequestAsync("/order/new", null, payload); - return ParseOrder(obj); - } - - protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null) - { - if (string.IsNullOrWhiteSpace(orderId)) - { - return null; - } - - object nonce = await GenerateNonceAsync(); - JToken result = await MakeJsonRequestAsync("/order/status", null, new Dictionary { { "nonce", nonce }, { "order_id", orderId } }); - return ParseOrder(result); - } - - protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) - { - List orders = new List(); - object nonce = await GenerateNonceAsync(); - JToken result = await MakeJsonRequestAsync("/orders", null, new Dictionary { { "nonce", nonce } }); - if (result is JArray array) - { - foreach (JToken token in array) - { - if (marketSymbol == null || token["symbol"].ToStringInvariant() == marketSymbol) - { - orders.Add(ParseOrder(token)); - } - } - } - - return orders; - } - - protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null) - { - object nonce = await GenerateNonceAsync(); - await MakeJsonRequestAsync("/order/cancel", null, new Dictionary{ { "nonce", nonce }, { "order_id", orderId } }); - } + { + JToken obj = await MakeJsonRequestAsync("/pubticker/" + marketSymbol); + if (obj == null || obj.Count() == 0) + { + return null; + } + ExchangeTicker t = new ExchangeTicker + { + MarketSymbol = marketSymbol, + Ask = obj["ask"].ConvertInvariant(), + Bid = obj["bid"].ConvertInvariant(), + Last = obj["last"].ConvertInvariant() + }; + t.Volume = await ParseVolumeAsync(obj["volume"], marketSymbol); + return t; + } + + 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); + } + + protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + { + ExchangeHistoricalTradeHelper state = new ExchangeHistoricalTradeHelper(this) + { + Callback = callback, + 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(); + } + + 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() }; + foreach (var kv in q) + { + if (kv.Available > 0m) + { + lookup[kv.Currency] = kv.Available; + } + } + return lookup; + } + + protected override async Task> OnGetAmountsAvailableToTradeAsync() + { + 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() }; + foreach (var kv in q) + { + if (kv.Available > 0m) + { + lookup[kv.Currency] = kv.Available; + } + } + return lookup; + } + + protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + { + if (order.OrderType == OrderType.Market) + { + throw new NotSupportedException("Order type " + order.OrderType + " not supported"); + } + + object nonce = await GenerateNonceAsync(); + Dictionary payload = new Dictionary + { { "nonce", nonce }, + { "client_order_id", "ExchangeSharp_" + CryptoUtility.UtcNow.ToString("s", System.Globalization.CultureInfo.InvariantCulture) }, + { "symbol", order.MarketSymbol }, + { "amount", order.RoundAmount().ToStringInvariant() }, + { "price", order.Price.ToStringInvariant() }, + { "side", (order.IsBuy ? "buy" : "sell") }, + { "type", "exchange limit" } + }; + order.ExtraParameters.CopyTo(payload); + JToken obj = await MakeJsonRequestAsync("/order/new", null, payload); + return ParseOrder(obj); + } + + protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null) + { + if (string.IsNullOrWhiteSpace(orderId)) + { + return null; + } + + object nonce = await GenerateNonceAsync(); + JToken result = await MakeJsonRequestAsync("/order/status", null, new Dictionary { { "nonce", nonce }, { "order_id", orderId } }); + return ParseOrder(result); + } + + protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) + { + List orders = new List(); + object nonce = await GenerateNonceAsync(); + JToken result = await MakeJsonRequestAsync("/orders", null, new Dictionary { { "nonce", nonce } }); + if (result is JArray array) + { + foreach (JToken token in array) + { + if (marketSymbol == null || token["symbol"].ToStringInvariant() == marketSymbol) + { + orders.Add(ParseOrder(token)); + } + } + } + + return orders; + } + + protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null) + { + object nonce = await GenerateNonceAsync(); + await MakeJsonRequestAsync("/order/cancel", null, new Dictionary { { "nonce", nonce }, { "order_id", orderId } }); + } protected override async Task OnGetTickersWebSocketAsync(Action>> tickerCallback, params string[] marketSymbols) { @@ -372,11 +370,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 + } }; }); } @@ -385,8 +383,8 @@ static void PublishTicker(ExchangeTicker ticker, string marketSymbol, Concurrent Action>> callback) { // if we are fully populated... - if (ticker.Bid > 0m && ticker.Ask > 0m && ticker.Bid <= ticker.Ask && - _volumeDict.TryGetValue(marketSymbol, out decimal tickerVolume)) + if (ticker.Bid > 0m && ticker.Ask > 0m && ticker.Bid <= ticker.Ask + && _volumeDict.TryGetValue(marketSymbol, out decimal tickerVolume)) { ticker.Volume.BaseCurrencyVolume = tickerVolume; ticker.Volume.QuoteCurrencyVolume = tickerVolume * ticker.Last; @@ -408,72 +406,75 @@ static void PublishTicker(ExchangeTicker ticker, string marketSymbol, Concurrent switch (type) { case "candles_1d_updates": - { - JToken changesToken = token["changes"]; - if (changesToken != null) { - string marketSymbol = token["symbol"].ToStringInvariant(); - if (changesToken.FirstOrDefault() is JArray candleArray) + JToken changesToken = token["changes"]; + if (changesToken != null) + { + string marketSymbol = token["symbol"].ToStringInvariant(); + if (changesToken.FirstOrDefault()is JArray candleArray) + { + decimal volume = candleArray[5].ConvertInvariant(); + volumeDict[marketSymbol] = volume; + ExchangeTicker ticker = GetTicker(tickerDict, this, marketSymbol); + PublishTicker(ticker, marketSymbol, volumeDict, tickerCallback); + } + } + } + break; + case "l2_updates": + { + // fetch the last bid/ask/last prices + if (token["trades"] is JArray tradesToken) { - decimal volume = candleArray[5].ConvertInvariant(); - volumeDict[marketSymbol] = volume; + string marketSymbol = token["symbol"].ToStringInvariant(); ExchangeTicker ticker = GetTicker(tickerDict, this, marketSymbol); + JToken lastSell = tradesToken.FirstOrDefault(t => t["side"].ToStringInvariant().Equals("sell", StringComparison.OrdinalIgnoreCase)); + if (lastSell != null) + { + decimal lastTradePrice = lastSell["price"].ConvertInvariant(); + ticker.Bid = ticker.Last = lastTradePrice; + } + JToken lastBuy = tradesToken.FirstOrDefault(t => t["side"].ToStringInvariant().Equals("buy", StringComparison.OrdinalIgnoreCase)); + if (lastBuy != null) + { + decimal lastTradePrice = lastBuy["price"].ConvertInvariant(); + ticker.Ask = ticker.Last = lastTradePrice; + } + PublishTicker(ticker, marketSymbol, volumeDict, tickerCallback); } } - } break; - case "l2_updates": - { - // fetch the last bid/ask/last prices - if (token["trades"] is JArray tradesToken) + break; + case "trade": { + //{ "type":"trade","symbol":"ETHUSD","event_id":35899433249,"timestamp":1619191314701,"price":"2261.65","quantity":"0.010343","side":"buy"} + + // fetch the active ticker metadata for this symbol string marketSymbol = token["symbol"].ToStringInvariant(); ExchangeTicker ticker = GetTicker(tickerDict, this, marketSymbol); - JToken lastSell = tradesToken.FirstOrDefault(t => t["side"].ToStringInvariant().Equals("sell", StringComparison.OrdinalIgnoreCase)); - if (lastSell != null) + string side = token["side"].ToStringInvariant(); + decimal price = token["price"].ConvertInvariant(); + if (side == "sell") { - decimal lastTradePrice = lastSell["price"].ConvertInvariant(); - ticker.Bid = ticker.Last = lastTradePrice; + ticker.Bid = ticker.Last = price; } - JToken lastBuy = tradesToken.FirstOrDefault(t => t["side"].ToStringInvariant().Equals("buy", StringComparison.OrdinalIgnoreCase)); - if (lastBuy != null) + else { - decimal lastTradePrice = lastBuy["price"].ConvertInvariant(); - ticker.Ask = ticker.Last = lastTradePrice; + ticker.Ask = ticker.Last = price; } - PublishTicker(ticker, marketSymbol, volumeDict, tickerCallback); } - } break; - case "trade": - { - //{ "type":"trade","symbol":"ETHUSD","event_id":35899433249,"timestamp":1619191314701,"price":"2261.65","quantity":"0.010343","side":"buy"} - - // fetch the active ticker metadata for this symbol - string marketSymbol = token["symbol"].ToStringInvariant(); - ExchangeTicker ticker = GetTicker(tickerDict, this, marketSymbol); - string side = token["side"].ToStringInvariant(); - decimal price = token["price"].ConvertInvariant(); - if (side == "sell") - { - ticker.Bid = ticker.Last = price; - } - else - { - ticker.Ask = ticker.Last = price; - } - PublishTicker(ticker, marketSymbol, volumeDict, tickerCallback); - } break; + break; } return Task.CompletedTask; - }, connectCallback: async (_socket) => + }, 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 } } + subscriptions = new [] { new { name = "candles_1d", symbols = marketSymbols }, new { name = "l2", symbols = marketSymbols } } }); }); } @@ -553,7 +554,7 @@ protected override async Task OnGetTradesWebSocketAsync(Func + return await ConnectWebSocketAsync(BaseUrlWebSocket, messageCallback : async(_socket, msg) => { JToken token = JToken.Parse(msg.ToStringFromUTF8()); if (token["result"].ToStringInvariant() == "error") @@ -565,12 +566,13 @@ protected override async Task OnGetTradesWebSocketAsync(Func(marketSymbol, trade)); - } + 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") { @@ -578,20 +580,20 @@ protected override async Task OnGetTradesWebSocketAsync(Func(marketSymbol, trade)); } - }, connectCallback: async (_socket) => + }, connectCallback : async(_socket) => { //{ "type": "subscribe","subscriptions":[{ "name":"l2","symbols":["BTCUSD","ETHUSD","ETHBTC"]}]} await _socket.SendMessageAsync(new { type = "subscribe", - subscriptions = new[] - { - new + subscriptions = new [] { - name = "l2", - symbols = marketSymbols + new + { + name = "l2", + symbols = marketSymbols + } } - } }); }); } @@ -639,21 +641,21 @@ protected override async Task OnGetDeltaOrderBookWebSocketAsync(Acti } } return Task.CompletedTask; - }, connectCallback: async (_socket) => + }, connectCallback : async(_socket) => { //{ "type": "subscribe","subscriptions":[{ "name":"l2","symbols":["BTCUSD","ETHUSD","ETHBTC"]}]} books.Clear(); await _socket.SendMessageAsync(new { type = "subscribe", - subscriptions = new[] - { - new + subscriptions = new [] { - name = "l2", - symbols = marketSymbols + new + { + name = "l2", + symbols = marketSymbols + } } - } }); }); } @@ -664,5 +666,5 @@ private static ExchangeTrade ParseWebSocketTrade(JToken token) => token.ParseTra TimestampType.UnixMilliseconds, idKey: "event_id"); } - public partial class ExchangeName { public const string Gemini = "Gemini"; } + public partial class ExchangeName { public const string Gemini = "Gemini"; } } diff --git a/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs b/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs index 90ff39bf9..46b933a22 100644 --- a/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs @@ -18,375 +18,387 @@ The above copyright notice and this permission notice shall be included in all c using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; - using Newtonsoft.Json.Linq; namespace ExchangeSharp { - public sealed partial class ExchangeKrakenAPI : ExchangeAPI - { - public override string BaseUrl { get; set; } = "https://api.kraken.com"; + public sealed partial class ExchangeKrakenAPI : ExchangeAPI + { + public override string BaseUrl { get; set; } = "https://api.kraken.com"; public override string BaseUrlWebSocket { get; set; } = "wss://ws.kraken.com"; private ExchangeKrakenAPI() - { - RequestMethod = "POST"; - RequestContentType = "application/x-www-form-urlencoded"; - MarketSymbolSeparator = string.Empty; - NonceStyle = NonceStyle.UnixMilliseconds; - } - - private IReadOnlyDictionary exchangeCurrencyToNormalizedCurrency = new Dictionary(); - private IReadOnlyDictionary normalizedCurrencyToExchangeCurrency = new Dictionary(); - private IReadOnlyDictionary exchangeSymbolToNormalizedSymbol = new Dictionary(); - private IReadOnlyDictionary normalizedSymbolToExchangeSymbol = new Dictionary(); - private IReadOnlyDictionary exchangeCurrenciesToMarketSymbol = new Dictionary(); - - /// - /// Populate dictionaries to deal with Kraken weirdness in currency and market names, will use cache if it exists - /// - /// Task - private async Task PopulateLookupTables() - { - await Cache.GetOrCreate(nameof(PopulateLookupTables), async () => - { - IReadOnlyDictionary currencies = await GetCurrenciesAsync(); - ExchangeMarket[] markets = (await GetMarketSymbolsMetadataAsync())?.ToArray(); - if (markets == null || markets.Length == 0) - { - return new CachedItem(); - } - - Dictionary exchangeCurrencyToNormalizedCurrencyNew = new Dictionary(StringComparer.OrdinalIgnoreCase); - Dictionary normalizedCurrencyToExchangeCurrencyNew = new Dictionary(StringComparer.OrdinalIgnoreCase); - Dictionary exchangeSymbolToNormalizedSymbolNew = new Dictionary(StringComparer.OrdinalIgnoreCase); - Dictionary normalizedSymbolToExchangeSymbolNew = new Dictionary(StringComparer.OrdinalIgnoreCase); - Dictionary exchangeCurrenciesToMarketSymbolNew = new Dictionary(StringComparer.OrdinalIgnoreCase); - - foreach (KeyValuePair kv in currencies) - { - string altName = kv.Value.AltName; - switch (altName.ToLowerInvariant()) - { - // wtf kraken... - case "xbt": altName = "BTC"; break; - case "xdg": altName = "DOGE"; break; - } - exchangeCurrencyToNormalizedCurrencyNew[kv.Value.Name] = altName; - normalizedCurrencyToExchangeCurrencyNew[altName] = kv.Value.Name; - } - - foreach (ExchangeMarket market in markets.Where(m => !m.MarketSymbol.Contains(".d"))) - { - string baseSymbol = market.BaseCurrency; - string quoteSymbol = market.QuoteCurrency; - string baseNorm = exchangeCurrencyToNormalizedCurrencyNew[market.BaseCurrency]; - string quoteNorm = exchangeCurrencyToNormalizedCurrencyNew[market.QuoteCurrency]; - string marketSymbolNorm = baseNorm + quoteNorm; - string marketSymbol = market.MarketSymbol; - exchangeSymbolToNormalizedSymbolNew[marketSymbol] = marketSymbolNorm; - normalizedSymbolToExchangeSymbolNew[marketSymbolNorm] = marketSymbol; - exchangeCurrenciesToMarketSymbolNew[baseSymbol + quoteSymbol] = marketSymbol; - exchangeCurrenciesToMarketSymbolNew[quoteSymbol + baseSymbol] = marketSymbol; - } + { + RequestMethod = "POST"; + RequestContentType = "application/x-www-form-urlencoded"; + MarketSymbolSeparator = string.Empty; + NonceStyle = NonceStyle.UnixMilliseconds; + } + + private IReadOnlyDictionary exchangeCurrencyToNormalizedCurrency = new Dictionary(); + private IReadOnlyDictionary normalizedCurrencyToExchangeCurrency = new Dictionary(); + private IReadOnlyDictionary exchangeSymbolToNormalizedSymbol = new Dictionary(); + private IReadOnlyDictionary normalizedSymbolToExchangeSymbol = new Dictionary(); + private IReadOnlyDictionary exchangeCurrenciesToMarketSymbol = new Dictionary(); + + /// + /// Populate dictionaries to deal with Kraken weirdness in currency and market names, will use cache if it exists + /// + /// Task + private async Task PopulateLookupTables() + { + await Cache.GetOrCreate(nameof(PopulateLookupTables), async() => + { + IReadOnlyDictionary currencies = await GetCurrenciesAsync(); + ExchangeMarket[] markets = (await GetMarketSymbolsMetadataAsync())?.ToArray(); + if (markets == null || markets.Length == 0) + { + return new CachedItem(); + } + + Dictionary exchangeCurrencyToNormalizedCurrencyNew = new Dictionary(StringComparer.OrdinalIgnoreCase); + Dictionary normalizedCurrencyToExchangeCurrencyNew = new Dictionary(StringComparer.OrdinalIgnoreCase); + Dictionary exchangeSymbolToNormalizedSymbolNew = new Dictionary(StringComparer.OrdinalIgnoreCase); + Dictionary normalizedSymbolToExchangeSymbolNew = new Dictionary(StringComparer.OrdinalIgnoreCase); + Dictionary exchangeCurrenciesToMarketSymbolNew = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (KeyValuePair kv in currencies) + { + string altName = kv.Value.AltName; + switch (altName.ToLowerInvariant()) + { + // wtf kraken... + case "xbt": + altName = "BTC"; + break; + case "xdg": + altName = "DOGE"; + break; + } + exchangeCurrencyToNormalizedCurrencyNew[kv.Value.Name] = altName; + normalizedCurrencyToExchangeCurrencyNew[altName] = kv.Value.Name; + } + + foreach (ExchangeMarket market in markets.Where(m => !m.MarketSymbol.Contains(".d"))) + { + string baseSymbol = market.BaseCurrency; + string quoteSymbol = market.QuoteCurrency; + string baseNorm = exchangeCurrencyToNormalizedCurrencyNew[market.BaseCurrency]; + string quoteNorm = exchangeCurrencyToNormalizedCurrencyNew[market.QuoteCurrency]; + string marketSymbolNorm = baseNorm + quoteNorm; + string marketSymbol = market.MarketSymbol; + exchangeSymbolToNormalizedSymbolNew[marketSymbol] = marketSymbolNorm; + normalizedSymbolToExchangeSymbolNew[marketSymbolNorm] = marketSymbol; + exchangeCurrenciesToMarketSymbolNew[baseSymbol + quoteSymbol] = marketSymbol; + exchangeCurrenciesToMarketSymbolNew[quoteSymbol + baseSymbol] = marketSymbol; + } exchangeCurrencyToNormalizedCurrency = ExchangeGlobalCurrencyReplacements = exchangeCurrencyToNormalizedCurrencyNew; - normalizedCurrencyToExchangeCurrency = normalizedCurrencyToExchangeCurrencyNew; - exchangeSymbolToNormalizedSymbol = exchangeSymbolToNormalizedSymbolNew; - normalizedSymbolToExchangeSymbol = normalizedSymbolToExchangeSymbolNew; - exchangeCurrenciesToMarketSymbol = exchangeCurrenciesToMarketSymbolNew; + normalizedCurrencyToExchangeCurrency = normalizedCurrencyToExchangeCurrencyNew; + exchangeSymbolToNormalizedSymbol = exchangeSymbolToNormalizedSymbolNew; + normalizedSymbolToExchangeSymbol = normalizedSymbolToExchangeSymbolNew; + exchangeCurrenciesToMarketSymbol = exchangeCurrenciesToMarketSymbolNew; return new CachedItem(new object(), CryptoUtility.UtcNow.AddHours(4.0)); - }); - } - - public override async Task<(string baseCurrency, string quoteCurrency)> ExchangeMarketSymbolToCurrenciesAsync(string marketSymbol) - { - ExchangeMarket market = await GetExchangeMarketFromCacheAsync(marketSymbol); - if (market == null) - { + }); + } + + public override async Task < (string baseCurrency, string quoteCurrency) > ExchangeMarketSymbolToCurrenciesAsync(string marketSymbol) + { + ExchangeMarket market = await GetExchangeMarketFromCacheAsync(marketSymbol); + if (market == null) + { market = await GetExchangeMarketFromCacheAsync(marketSymbol.Replace("/", string.Empty)); if (market == null) { throw new ArgumentException("Unable to get currencies for market symbol " + marketSymbol); } - } - return (market.BaseCurrency, market.QuoteCurrency); - } - - public override async Task ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol) - { - await PopulateLookupTables(); - var (baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync(marketSymbol); - if (!exchangeCurrencyToNormalizedCurrency.TryGetValue(baseCurrency, out string baseCurrencyNormalized)) - { - baseCurrencyNormalized = baseCurrency; - } - if (!exchangeCurrencyToNormalizedCurrency.TryGetValue(quoteCurrency, out string quoteCurrencyNormalized)) - { - quoteCurrencyNormalized = quoteCurrency; - } - return baseCurrencyNormalized + GlobalMarketSymbolSeparatorString + quoteCurrencyNormalized; - } - - public override async Task GlobalMarketSymbolToExchangeMarketSymbolAsync(string marketSymbol) - { - await PopulateLookupTables(); - string[] pieces = marketSymbol.Split('-'); - if (pieces.Length < 2) - { - throw new ArgumentException("Market symbol must be at least two pieces"); - } - if (!normalizedCurrencyToExchangeCurrency.TryGetValue(pieces[0], out string baseCurrencyExchange)) - { - baseCurrencyExchange = pieces[0]; - } - if (!normalizedCurrencyToExchangeCurrency.TryGetValue(pieces[1], out string quoteCurrencyExchange)) - { - quoteCurrencyExchange = pieces[1]; - } - if (!exchangeCurrenciesToMarketSymbol.TryGetValue(baseCurrencyExchange + quoteCurrencyExchange, out string exchangeMarketSymbol)) - { - throw new ArgumentException("Unable to find exchange market for global market symbol " + marketSymbol); - } - return exchangeMarketSymbol; - } - - protected override JToken CheckJsonResponse(JToken json) - { - if (!(json is JArray) && json["error"] is JArray error && error.Count != 0) - { - throw new APIException(error[0].ToStringInvariant()); - } - return json["result"] ?? json; - } - - private ExchangeOrderResult ParseOrder(string orderId, JToken order) - { - ExchangeOrderResult orderResult = new ExchangeOrderResult { OrderId = orderId }; - - switch (order["status"].ToStringInvariant()) - { - 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; - } - orderResult.Message = (orderResult.Message ?? order["reason"].ToStringInvariant()); - orderResult.OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(order["opentm"].ConvertInvariant()); - orderResult.MarketSymbol = order["descr"]["pair"].ToStringInvariant(); - orderResult.IsBuy = (order["descr"]["type"].ToStringInvariant() == "buy"); - orderResult.Amount = order["vol"].ConvertInvariant(); - orderResult.AmountFilled = order["vol_exec"].ConvertInvariant(); - orderResult.Price = order["descr"]["price"].ConvertInvariant(); - orderResult.AveragePrice = order["price"].ConvertInvariant(); - - return orderResult; - } - - private async Task ParseHistoryOrder(string orderId, JToken order) - { -// //{{ -// "ordertxid": "ONKWWN-3LWZ7-4SDZVJ", -// "postxid": "TKH2SE-M7IF5-CFI7LT", -// "pair": "XXRPZUSD", -// "time": 1537779676.7525, -// "type": "buy", -// "ordertype": "limit", -// "price": "0.54160000", -// "cost": "16.22210000", -// "fee": "0.02595536", -// "vol": "29.95217873", -// "margin": "0.00000000", -// "misc": "" -//} -// } - - ExchangeOrderResult orderResult = new ExchangeOrderResult { OrderId = orderId }; - orderResult.Result = ExchangeAPIOrderResult.Filled; - orderResult.Message = ""; - orderResult.OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(order["time"].ConvertInvariant()); - orderResult.MarketSymbol = order["pair"].ToStringInvariant(); - orderResult.IsBuy = (order["type"].ToStringInvariant() == "buy"); - orderResult.Amount = order["vol"].ConvertInvariant(); - orderResult.Fees = order["fee"].ConvertInvariant(); - orderResult.Price = order["price"].ConvertInvariant(); - orderResult.AveragePrice = order["price"].ConvertInvariant(); - orderResult.TradeId = order["postxid"].ToStringInvariant(); //verify which is orderid & tradeid - orderResult.OrderId = order["ordertxid"].ToStringInvariant(); //verify which is orderid & tradeid - orderResult.AmountFilled = order["vol"].ConvertInvariant(); - orderResult.FillDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(order["time"].ConvertInvariant()); - - string[] pairs = (await ExchangeMarketSymbolToGlobalMarketSymbolAsync(order["pair"].ToStringInvariant())).Split('-'); - orderResult.FeesCurrency = pairs[1]; - - return orderResult; - } - - private async Task> QueryOrdersAsync(string symbol, string path) - { - await PopulateLookupTables(); - List orders = new List(); - JToken result = await MakeJsonRequestAsync(path, null, await GetNoncePayloadAsync()); - result = result["open"]; - if (exchangeSymbolToNormalizedSymbol.TryGetValue(symbol, out string normalizedSymbol)) - { - foreach (JProperty order in result) - { - if (normalizedSymbol == null || order.Value["descr"]["pair"].ToStringInvariant() == normalizedSymbol.ToUpperInvariant()) - { - orders.Add(ParseOrder(order.Name, order.Value)); - } - } - } - - return orders; - } - - private async Task> QueryClosedOrdersAsync(string symbol, string path) - { - await PopulateLookupTables(); - List orders = new List(); - JToken result = await MakeJsonRequestAsync(path, null, await GetNoncePayloadAsync()); - result = result["closed"]; - if (exchangeSymbolToNormalizedSymbol.TryGetValue(symbol, out string normalizedSymbol)) - { - foreach (JProperty order in result) - { - if (normalizedSymbol == null || order.Value["descr"]["pair"].ToStringInvariant() == normalizedSymbol.ToUpperInvariant()) - { - orders.Add(ParseOrder(order.Name, order.Value)); - } - } - } - else - { - foreach (JProperty order in result) - { - orders.Add(ParseOrder(order.Name, order.Value)); - } - } - - return orders; - } - - private async Task> QueryHistoryOrdersAsync(string symbol, string path) - { - await PopulateLookupTables(); - List orders = new List(); - JToken result = await MakeJsonRequestAsync(path, null, await GetNoncePayloadAsync()); - result = result["trades"]; - if (exchangeSymbolToNormalizedSymbol.TryGetValue(symbol, out string normalizedSymbol)) - { - foreach (JProperty order in result) - { - if (normalizedSymbol == null || order.Value["pair"].ToStringInvariant() == symbol.ToUpperInvariant()) - { - orders.Add(await ParseHistoryOrder(order.Name, order.Value)); - } - } - } - else - { - foreach (JProperty order in result) - { - orders.Add(await ParseHistoryOrder(order.Name, order.Value)); - } - } - - return orders; - } - - //private async Task> QueryClosedOrdersAsync(string symbol, string path) - //{ - // List orders = new List(); - // JToken result = await MakeJsonRequestAsync(path, null, await GetNoncePayloadAsync()); - // //result = result["closed"]; - // foreach (JProperty order in result) - // { - // orders.Add(ParseOrder(order.Name, order.Value)); - // } - - - // //if (exchangeSymbolToNormalizedSymbol.TryGetValue(symbol, out string normalizedSymbol)) - // //{ - // // foreach (JProperty order in result) - // // { - // // if (normalizedSymbol == null || order.Value["descr"]["pair"].ToStringInvariant() == normalizedSymbol.ToUpperInvariant()) - // // { - // // orders.Add(ParseOrder(order.Name, order.Value)); - // // } - // // } - // //} - - // return orders; - //} - - - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) - { - if (payload == null || PrivateApiKey == null || PublicApiKey == null || !payload.ContainsKey("nonce")) - { - await CryptoUtility.WritePayloadFormToRequestAsync(request, payload); - } - else - { - string nonce = payload["nonce"].ToStringInvariant(); - payload.Remove("nonce"); - 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()) - { - string hashString = nonce + form; - byte[] sha256Bytes = sha256.ComputeHash(hashString.ToBytesUTF8()); - byte[] pathBytes = request.RequestUri.AbsolutePath.ToBytesUTF8(); - byte[] sigBytes = new byte[sha256Bytes.Length + pathBytes.Length]; - 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)) - { - string sign = System.Convert.ToBase64String(hmac.ComputeHash(sigBytes)); - request.AddHeader("API-Sign", sign); - } - } - request.AddHeader("API-Key", CryptoUtility.ToUnsecureString(PublicApiKey)); - await CryptoUtility.WriteToRequestAsync(request, form); - } - } - - protected override async Task> OnGetCurrenciesAsync() - { - // https://api.kraken.com/0/public/Assets - Dictionary allCoins = new Dictionary(StringComparer.OrdinalIgnoreCase); - - var currencies = new Dictionary(StringComparer.OrdinalIgnoreCase); - JToken array = await MakeJsonRequestAsync("/0/public/Assets"); - foreach (JProperty token in array) - { - var coin = new ExchangeCurrency - { - CoinType = token.Value["aclass"].ToStringInvariant(), - Name = token.Name, - FullName = token.Name, - AltName = token.Value["altname"].ToStringInvariant() - }; - - currencies[coin.Name] = coin; - } - - return currencies; - } - - protected override async Task> OnGetMarketSymbolsAsync() - { - JToken result = await MakeJsonRequestAsync("/0/public/AssetPairs"); - return result.Children().Where(p => !p.Name.Contains(".d")).Select(p => p.Name).ToArray(); - } - - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() - { //{"ADACAD": { + } + return (market.BaseCurrency, market.QuoteCurrency); + } + + public override async Task ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol) + { + await PopulateLookupTables(); + var(baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync(marketSymbol); + if (!exchangeCurrencyToNormalizedCurrency.TryGetValue(baseCurrency, out string baseCurrencyNormalized)) + { + baseCurrencyNormalized = baseCurrency; + } + if (!exchangeCurrencyToNormalizedCurrency.TryGetValue(quoteCurrency, out string quoteCurrencyNormalized)) + { + quoteCurrencyNormalized = quoteCurrency; + } + return baseCurrencyNormalized + GlobalMarketSymbolSeparatorString + quoteCurrencyNormalized; + } + + public override async Task GlobalMarketSymbolToExchangeMarketSymbolAsync(string marketSymbol) + { + await PopulateLookupTables(); + string[] pieces = marketSymbol.Split('-'); + if (pieces.Length < 2) + { + throw new ArgumentException("Market symbol must be at least two pieces"); + } + if (!normalizedCurrencyToExchangeCurrency.TryGetValue(pieces[0], out string baseCurrencyExchange)) + { + baseCurrencyExchange = pieces[0]; + } + if (!normalizedCurrencyToExchangeCurrency.TryGetValue(pieces[1], out string quoteCurrencyExchange)) + { + quoteCurrencyExchange = pieces[1]; + } + if (!exchangeCurrenciesToMarketSymbol.TryGetValue(baseCurrencyExchange + quoteCurrencyExchange, out string exchangeMarketSymbol)) + { + throw new ArgumentException("Unable to find exchange market for global market symbol " + marketSymbol); + } + return exchangeMarketSymbol; + } + + protected override JToken CheckJsonResponse(JToken json) + { + if (!(json is JArray) && json["error"] is JArray error && error.Count != 0) + { + throw new APIException(error[0].ToStringInvariant()); + } + return json["result"] ?? json; + } + + private ExchangeOrderResult ParseOrder(string orderId, JToken order) + { + ExchangeOrderResult orderResult = new ExchangeOrderResult { OrderId = orderId }; + + switch (order["status"].ToStringInvariant()) + { + 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; + } + orderResult.Message = (orderResult.Message ?? order["reason"].ToStringInvariant()); + orderResult.OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(order["opentm"].ConvertInvariant()); + orderResult.MarketSymbol = order["descr"]["pair"].ToStringInvariant(); + orderResult.IsBuy = (order["descr"]["type"].ToStringInvariant() == "buy"); + orderResult.Amount = order["vol"].ConvertInvariant(); + orderResult.AmountFilled = order["vol_exec"].ConvertInvariant(); + orderResult.Price = order["descr"]["price"].ConvertInvariant(); + orderResult.AveragePrice = order["price"].ConvertInvariant(); + + return orderResult; + } + + private async Task ParseHistoryOrder(string orderId, JToken order) + { + // //{{ + // "ordertxid": "ONKWWN-3LWZ7-4SDZVJ", + // "postxid": "TKH2SE-M7IF5-CFI7LT", + // "pair": "XXRPZUSD", + // "time": 1537779676.7525, + // "type": "buy", + // "ordertype": "limit", + // "price": "0.54160000", + // "cost": "16.22210000", + // "fee": "0.02595536", + // "vol": "29.95217873", + // "margin": "0.00000000", + // "misc": "" + //} + // } + + ExchangeOrderResult orderResult = new ExchangeOrderResult { OrderId = orderId }; + orderResult.Result = ExchangeAPIOrderResult.Filled; + orderResult.Message = ""; + orderResult.OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(order["time"].ConvertInvariant()); + orderResult.MarketSymbol = order["pair"].ToStringInvariant(); + orderResult.IsBuy = (order["type"].ToStringInvariant() == "buy"); + orderResult.Amount = order["vol"].ConvertInvariant(); + orderResult.Fees = order["fee"].ConvertInvariant(); + orderResult.Price = order["price"].ConvertInvariant(); + orderResult.AveragePrice = order["price"].ConvertInvariant(); + orderResult.TradeId = order["postxid"].ToStringInvariant(); //verify which is orderid & tradeid + orderResult.OrderId = order["ordertxid"].ToStringInvariant(); //verify which is orderid & tradeid + orderResult.AmountFilled = order["vol"].ConvertInvariant(); + orderResult.FillDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(order["time"].ConvertInvariant()); + + string[] pairs = (await ExchangeMarketSymbolToGlobalMarketSymbolAsync(order["pair"].ToStringInvariant())).Split('-'); + orderResult.FeesCurrency = pairs[1]; + + return orderResult; + } + + private async Task> QueryOrdersAsync(string symbol, string path) + { + await PopulateLookupTables(); + List orders = new List(); + JToken result = await MakeJsonRequestAsync(path, null, await GetNoncePayloadAsync()); + result = result["open"]; + if (exchangeSymbolToNormalizedSymbol.TryGetValue(symbol, out string normalizedSymbol)) + { + foreach (JProperty order in result) + { + if (normalizedSymbol == null || order.Value["descr"]["pair"].ToStringInvariant() == normalizedSymbol.ToUpperInvariant()) + { + orders.Add(ParseOrder(order.Name, order.Value)); + } + } + } + + return orders; + } + + private async Task> QueryClosedOrdersAsync(string symbol, string path) + { + await PopulateLookupTables(); + List orders = new List(); + JToken result = await MakeJsonRequestAsync(path, null, await GetNoncePayloadAsync()); + result = result["closed"]; + if (exchangeSymbolToNormalizedSymbol.TryGetValue(symbol, out string normalizedSymbol)) + { + foreach (JProperty order in result) + { + if (normalizedSymbol == null || order.Value["descr"]["pair"].ToStringInvariant() == normalizedSymbol.ToUpperInvariant()) + { + orders.Add(ParseOrder(order.Name, order.Value)); + } + } + } + else + { + foreach (JProperty order in result) + { + orders.Add(ParseOrder(order.Name, order.Value)); + } + } + + return orders; + } + + private async Task> QueryHistoryOrdersAsync(string symbol, string path) + { + await PopulateLookupTables(); + List orders = new List(); + JToken result = await MakeJsonRequestAsync(path, null, await GetNoncePayloadAsync()); + result = result["trades"]; + if (exchangeSymbolToNormalizedSymbol.TryGetValue(symbol, out string normalizedSymbol)) + { + foreach (JProperty order in result) + { + if (normalizedSymbol == null || order.Value["pair"].ToStringInvariant() == symbol.ToUpperInvariant()) + { + orders.Add(await ParseHistoryOrder(order.Name, order.Value)); + } + } + } + else + { + foreach (JProperty order in result) + { + orders.Add(await ParseHistoryOrder(order.Name, order.Value)); + } + } + + return orders; + } + + //private async Task> QueryClosedOrdersAsync(string symbol, string path) + //{ + // List orders = new List(); + // JToken result = await MakeJsonRequestAsync(path, null, await GetNoncePayloadAsync()); + // //result = result["closed"]; + // foreach (JProperty order in result) + // { + // orders.Add(ParseOrder(order.Name, order.Value)); + // } + + // //if (exchangeSymbolToNormalizedSymbol.TryGetValue(symbol, out string normalizedSymbol)) + // //{ + // // foreach (JProperty order in result) + // // { + // // if (normalizedSymbol == null || order.Value["descr"]["pair"].ToStringInvariant() == normalizedSymbol.ToUpperInvariant()) + // // { + // // orders.Add(ParseOrder(order.Name, order.Value)); + // // } + // // } + // //} + + // return orders; + //} + + protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + { + if (payload == null || PrivateApiKey == null || PublicApiKey == null || !payload.ContainsKey("nonce")) + { + await CryptoUtility.WritePayloadFormToRequestAsync(request, payload); + } + else + { + string nonce = payload["nonce"].ToStringInvariant(); + payload.Remove("nonce"); + 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()) + { + string hashString = nonce + form; + byte[] sha256Bytes = sha256.ComputeHash(hashString.ToBytesUTF8()); + byte[] pathBytes = request.RequestUri.AbsolutePath.ToBytesUTF8(); + byte[] sigBytes = new byte[sha256Bytes.Length + pathBytes.Length]; + 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)) + { + string sign = System.Convert.ToBase64String(hmac.ComputeHash(sigBytes)); + request.AddHeader("API-Sign", sign); + } + } + request.AddHeader("API-Key", CryptoUtility.ToUnsecureString(PublicApiKey)); + await CryptoUtility.WriteToRequestAsync(request, form); + } + } + + protected override async Task> OnGetCurrenciesAsync() + { + // https://api.kraken.com/0/public/Assets + Dictionary allCoins = new Dictionary(StringComparer.OrdinalIgnoreCase); + + var currencies = new Dictionary(StringComparer.OrdinalIgnoreCase); + JToken array = await MakeJsonRequestAsync("/0/public/Assets"); + foreach (JProperty token in array) + { + var coin = new ExchangeCurrency + { + CoinType = token.Value["aclass"].ToStringInvariant(), + Name = token.Name, + FullName = token.Name, + AltName = token.Value["altname"].ToStringInvariant() + }; + + currencies[coin.Name] = coin; + } + + return currencies; + } + + protected override async Task> OnGetMarketSymbolsAsync() + { + JToken result = await MakeJsonRequestAsync("/0/public/AssetPairs"); + return result.Children().Where(p => !p.Name.Contains(".d")).Select(p => p.Name).ToArray(); + } + + protected internal override async Task> OnGetMarketSymbolsMetadataAsync() + { //{"ADACAD": { // "altname": "ADACAD", // "wsname": "ADA/CAD", // "aclass_base": "currency", @@ -480,93 +492,93 @@ protected internal override async Task> OnGetMarketS // "margin_stop": 40 //}} var markets = new List(); - JToken allPairs = await MakeJsonRequestAsync("/0/public/AssetPairs"); - var res = (from prop in allPairs.Children() select prop).ToArray(); - - foreach (JProperty prop in res.Where(p => !p.Name.EndsWith(".d"))) - { - JToken pair = prop.Value; - JToken child = prop.Children().FirstOrDefault(); - var quantityStepSize = Math.Pow(0.1, pair["lot_decimals"].ConvertInvariant()).ConvertInvariant(); - 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() - }; - markets.Add(market); - } - - return markets; - } - - protected override async Task>> OnGetTickersAsync() - { - var marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); - var normalizedPairsList = marketSymbols.Select(symbol => NormalizeMarketSymbol(symbol)).ToList(); - var csvPairsList = string.Join(",", normalizedPairsList); - JToken apiTickers = await MakeJsonRequestAsync("/0/public/Ticker", null, new Dictionary { { "pair", csvPairsList } }); - var tickers = new List>(); - foreach (string marketSymbol in normalizedPairsList) - { - 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. - var symbols = (await GetMarketSymbolsMetadataAsync()).ToList(); - var symbol = symbols.FirstOrDefault(a => a.MarketSymbol.Replace("/", "").Equals(marketSymbol)); - ticker = apiTickers[symbol.BaseCurrency + symbol.QuoteCurrency]; - } - #endregion - - try - { - tickers.Add(new KeyValuePair(marketSymbol, await ConvertToExchangeTickerAsync(marketSymbol, ticker))); - } - catch - { - // if Kraken throws bogus json at us, just eat it - } - } - return tickers; - } - - protected override async Task OnGetTickerAsync(string marketSymbol) - { - JToken apiTickers = await MakeJsonRequestAsync("/0/public/Ticker", null, new Dictionary { { "pair", NormalizeMarketSymbol(marketSymbol) } }); - JToken ticker = apiTickers[marketSymbol]; - return await ConvertToExchangeTickerAsync(marketSymbol, ticker); - } - - private async Task ConvertToExchangeTickerAsync(string symbol, JToken ticker) - { - decimal last = ticker["c"][0].ConvertInvariant(); - 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 - } - }; - } + JToken allPairs = await MakeJsonRequestAsync("/0/public/AssetPairs"); + var res = (from prop in allPairs.Children()select prop).ToArray(); + + foreach (JProperty prop in res.Where(p => !p.Name.EndsWith(".d"))) + { + JToken pair = prop.Value; + JToken child = prop.Children().FirstOrDefault(); + var quantityStepSize = Math.Pow(0.1, pair["lot_decimals"].ConvertInvariant()).ConvertInvariant(); + 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() + }; + markets.Add(market); + } + + return markets; + } + + protected override async Task>> OnGetTickersAsync() + { + var marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); + var normalizedPairsList = marketSymbols.Select(symbol => NormalizeMarketSymbol(symbol)).ToList(); + var csvPairsList = string.Join(",", normalizedPairsList); + JToken apiTickers = await MakeJsonRequestAsync("/0/public/Ticker", null, new Dictionary { { "pair", csvPairsList } }); + var tickers = new List>(); + foreach (string marketSymbol in normalizedPairsList) + { + 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. + var symbols = (await GetMarketSymbolsMetadataAsync()).ToList(); + var symbol = symbols.FirstOrDefault(a => a.MarketSymbol.Replace("/", "").Equals(marketSymbol)); + ticker = apiTickers[symbol.BaseCurrency + symbol.QuoteCurrency]; + } + #endregion + + try + { + tickers.Add(new KeyValuePair(marketSymbol, await ConvertToExchangeTickerAsync(marketSymbol, ticker))); + } + catch + { + // if Kraken throws bogus json at us, just eat it + } + } + return tickers; + } + + protected override async Task OnGetTickerAsync(string marketSymbol) + { + JToken apiTickers = await MakeJsonRequestAsync("/0/public/Ticker", null, new Dictionary { { "pair", NormalizeMarketSymbol(marketSymbol) } }); + JToken ticker = apiTickers[marketSymbol]; + return await ConvertToExchangeTickerAsync(marketSymbol, ticker); + } + + private async Task ConvertToExchangeTickerAsync(string symbol, JToken ticker) + { + decimal last = ticker["c"][0].ConvertInvariant(); + 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 + } + }; + } protected override Task OnInitializeAsync() { @@ -574,29 +586,31 @@ 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); - } + { + JToken obj = await MakeJsonRequestAsync("/0/public/Depth?pair=" + marketSymbol + "&count=" + maxCount); + return ExchangeAPIExtensions.ParseOrderBookFromJTokenArrays(obj[marketSymbol], maxCount : maxCount); + } protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null) { List trades = new List(); //https://www.kraken.com/features/api#public-market-data note kraken does not specify but it appears the limit is around 1860 (weird) - //https://api.kraken.com/0/public/Trades?pair=BCHUSD&count=1860 + //https://api.kraken.com/0/public/Trades?pair=BCHUSD&count=1860 //needs testing of different marketsymbols to establish if limit varies //gonna use 1500 for now int requestLimit = (limit == null || limit < 1 || limit > 1500) ? 1500 : (int)limit; - string url = "/0/public/Trades?pair=" + marketSymbol + "&count=" + requestLimit; + string url = "/0/public/Trades?pair=" + marketSymbol + "&count=" + requestLimit; //string url = "/trades/t" + marketSymbol + "/hist?sort=" + "-1" + "&limit=" + requestLimit; JToken result = await MakeJsonRequestAsync(url); //if (result != null && (!(result[marketSymbol] is JArray outerArray) || outerArray.Count == 0)) { - if(result != null && result[marketSymbol] is JArray outerArray && outerArray.Count > 0) { - foreach(JToken trade in outerArray.Children()) { + if (result != null && result[marketSymbol] is JArray outerArray && outerArray.Count > 0) + { + foreach (JToken trade in outerArray.Children()) + { trades.Add(trade.ParseTrade(1, 0, 3, 2, TimestampType.UnixSecondsDouble, null, "b")); } } @@ -605,209 +619,199 @@ protected override async Task> OnGetRecentTradesAsync } protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) - { - string baseUrl = "/0/public/Trades?pair=" + marketSymbol; - string url; - List trades = new List(); - while (true) - { - url = baseUrl; - if (startDate != null) - { - url += "&since=" + (long)(CryptoUtility.UnixTimestampFromDateTimeMilliseconds(startDate.Value) * 1000000.0); - } - JToken result = await MakeJsonRequestAsync(url); - if (result == null) - { - break; - } - if (!(result[marketSymbol] is JArray outerArray) || outerArray.Count == 0) - { - break; - } - if (startDate != null) - { - startDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(result["last"].ConvertInvariant() / 1000000.0d); - } - foreach (JToken trade in outerArray.Children()) - { - trades.Add(trade.ParseTrade(1, 0, 3, 2, TimestampType.UnixSecondsDouble, null, "b")); - } - trades.Sort((t1, t2) => t1.Timestamp.CompareTo(t2.Timestamp)); - if (!callback(trades)) - { - break; - } - trades.Clear(); - if (startDate == null) - { - break; - } - Task.Delay(1000).Wait(); - } - } - - protected override async Task> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) - { - if (limit != null) - { - throw new APIException("Limit parameter not supported"); - } - - // https://api.kraken.com/0/public/OHLC - // pair = asset pair to get OHLC data for, interval = time frame interval in minutes(optional):, 1(default), 5, 15, 30, 60, 240, 1440, 10080, 21600, since = return committed OHLC data since given id(optional.exclusive) - // array of array entries(