diff --git a/README.md b/README.md index 08ca05cc..6252ed48 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ The following cryptocurrency exchanges are supported: | Bybit | x | x | R | Has public method for Websocket Positions | Coinbase | x | x | T R | | Digifinex | x | x | R B | +| FTX | x | x | T | | Gemini | x | x | T R B | | HitBTC | x | x | R | | Huobi | x | x | R B | diff --git a/src/ExchangeSharp/API/Exchanges/Aquanow/ExchangeAquanowAPI.cs b/src/ExchangeSharp/API/Exchanges/Aquanow/ExchangeAquanowAPI.cs index 8eed8c4c..fc774e6c 100644 --- a/src/ExchangeSharp/API/Exchanges/Aquanow/ExchangeAquanowAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Aquanow/ExchangeAquanowAPI.cs @@ -56,7 +56,7 @@ protected override async Task>> 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 }; + ExchangeTicker ticker = new ExchangeTicker { Exchange = Name, MarketSymbol = symbol, Bid = bid, Ask = ask, ApiResponse = bestPriceSymbol }; tickers.Add(new KeyValuePair(symbol, ticker)); } return tickers; diff --git a/src/ExchangeSharp/API/Exchanges/BitBank/ExchangeBitBankAPI.cs b/src/ExchangeSharp/API/Exchanges/BitBank/ExchangeBitBankAPI.cs index 5cd35e21..75694a64 100644 --- a/src/ExchangeSharp/API/Exchanges/BitBank/ExchangeBitBankAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/BitBank/ExchangeBitBankAPI.cs @@ -49,6 +49,7 @@ protected override async Task>> var data = token[GlobalMarketSymbolToExchangeMarketSymbolAsync(symbol)]; var ticker = new ExchangeTicker() { + Exchange = Name, ApiResponse = token, Ask = data["sell"].ConvertInvariant(), Bid = data["buy"].ConvertInvariant(), diff --git a/src/ExchangeSharp/API/Exchanges/Bitfinex/ExchangeBitfinexAPI.cs b/src/ExchangeSharp/API/Exchanges/Bitfinex/ExchangeBitfinexAPI.cs index 309cd31f..e1bc82f5 100644 --- a/src/ExchangeSharp/API/Exchanges/Bitfinex/ExchangeBitfinexAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Bitfinex/ExchangeBitfinexAPI.cs @@ -166,6 +166,7 @@ protected override async Task>> var market = marketsBySymbol[marketSymbol.ToLowerInvariant()]; tickers.Add(new KeyValuePair(marketSymbol, new ExchangeTicker { + Exchange = Name, MarketSymbol = marketSymbol, ApiResponse = token, Ask = array[3].ConvertInvariant(), diff --git a/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI_WebSocket.cs b/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI_WebSocket.cs index 01f5c1ef..fe712c02 100644 --- a/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI_WebSocket.cs +++ b/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI_WebSocket.cs @@ -141,6 +141,7 @@ async Task innerCallback(string json) DateTime timestamp = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(ticker["T"].ConvertInvariant()); var t = new ExchangeTicker { + Exchange = Name, MarketSymbol = marketName, ApiResponse = ticker, Ask = ask, diff --git a/src/ExchangeSharp/API/Exchanges/Digifinex/ExchangeDigifinexAPI.cs b/src/ExchangeSharp/API/Exchanges/Digifinex/ExchangeDigifinexAPI.cs index eb601ab0..6c4bc20a 100644 --- a/src/ExchangeSharp/API/Exchanges/Digifinex/ExchangeDigifinexAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Digifinex/ExchangeDigifinexAPI.cs @@ -185,6 +185,7 @@ private async Task ParseTickerAsync(JToken x) return new ExchangeTicker { + Exchange = Name, ApiResponse = t, Ask = t["sell"].ConvertInvariant(), Bid = t["buy"].ConvertInvariant(), diff --git a/src/ExchangeSharp/API/Exchanges/FTX/ExchangeFTXAPI.cs b/src/ExchangeSharp/API/Exchanges/FTX/ExchangeFTXAPI.cs new file mode 100644 index 00000000..5db80bcd --- /dev/null +++ b/src/ExchangeSharp/API/Exchanges/FTX/ExchangeFTXAPI.cs @@ -0,0 +1,498 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace ExchangeSharp.API.Exchanges.FTX +{ + public sealed partial class ExchangeFTXAPI : ExchangeAPI + { + public override string BaseUrl { get; set; } = "https://ftx.com/api"; + public override string BaseUrlWebSocket { get; set; } = "wss://ftx.com/ws/"; + + #region [ Constructor(s) ] + + public ExchangeFTXAPI() + { + NonceStyle = NonceStyle.UnixMillisecondsString; + MarketSymbolSeparator = "/"; + RequestContentType = "application/json"; + } + + #endregion + + #region [ Implementation ] + + /// + protected async override Task OnCancelOrderAsync(string orderId, string marketSymbol = null) + { + await MakeJsonRequestAsync($"/orders/{orderId}", null, await GetNoncePayloadAsync(), "DELETE"); + } + + /// + protected async override Task> OnGetAmountsAsync() + { + var balances = new Dictionary(); + + JToken result = await MakeJsonRequestAsync("/wallet/balances", null, await GetNoncePayloadAsync()); + + foreach (JObject obj in result) + { + decimal amount = obj["total"].ConvertInvariant(); + + balances[obj["coin"].ToStringInvariant()] = amount; + } + + return balances; + } + + /// + protected async override Task> OnGetAmountsAvailableToTradeAsync() + { + // https://docs.ftx.com/#get-balances + // NOTE there is also is "Get balances of all accounts"? + // "coin": "USDTBEAR", + // "free": 2320.2, + // "spotBorrow": 0.0, + // "total": 2340.2, + // "usdValue": 2340.2, + // "availableWithoutBorrow": 2320.2 + + var balances = new Dictionary(); + + JToken result = await MakeJsonRequestAsync($"/wallet/balances", null, await GetNoncePayloadAsync()); + + foreach (JToken token in result.Children()) + { + balances.Add(token["coin"].ToStringInvariant(), + token["availableWithoutBorrow"].ConvertInvariant()); + } + + return balances; + } + + + /// + protected async override Task> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + { + + //period options: 15, 60, 300, 900, 3600, 14400, 86400, or any multiple of 86400 up to 30*86400 + + var queryUrl = $"/markets/{marketSymbol}/candles?resolution={periodSeconds}"; + + if (startDate.HasValue) + { + queryUrl += $"&start_time={startDate?.UnixTimestampFromDateTimeSeconds()}"; + } + + if (endDate.HasValue) + { + queryUrl += $"&end_time={endDate?.UnixTimestampFromDateTimeSeconds()}"; + } + + var candles = new List(); + + var response = await MakeJsonRequestAsync(queryUrl, null, await GetNoncePayloadAsync()); + + foreach (JToken candle in response.Children()) + { + var parsedCandle = this.ParseCandle(candle, marketSymbol, periodSeconds, "open", "high", "low", "close", "startTime", TimestampType.Iso8601, "volume"); + + candles.Add(parsedCandle); + } + + return candles; + } + + /// + protected async override Task> OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) + { + string query = "/orders/history"; + + string parameters = ""; + + if (!string.IsNullOrEmpty(marketSymbol)) + { + parameters += $"&market={marketSymbol}"; + } + + if (afterDate != null) + { + parameters += $"&start_time={afterDate?.UnixTimestampFromDateTimeSeconds()}"; + } + + if (!string.IsNullOrEmpty(parameters)) + { + query += $"?{parameters}"; + } + + JToken response = await MakeJsonRequestAsync(query, null, await GetNoncePayloadAsync()); + + var orders = new List(); + + foreach (JToken token in response.Children()) + { + var symbol = token["market"].ToStringInvariant(); + + if (!Regex.Match(symbol, @"[\w\d]*\/[[\w\d]]*").Success) + { + continue; + } + + orders.Add(ParseOrder(token)); + } + + return orders; + } + + /// + protected async override Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + { + string baseUrl = $"/markets/{marketSymbol}/trades?"; + + if (startDate != null) + { + baseUrl += $"&start_time={startDate?.UnixTimestampFromDateTimeMilliseconds()}"; + } + + if (endDate != null) + { + baseUrl += $"&end_time={endDate?.UnixTimestampFromDateTimeMilliseconds()}"; + } + + List trades = new List(); + + while (true) + { + JToken result = await MakeJsonRequestAsync(baseUrl); + + foreach (JToken trade in result.Children()) + { + trades.Add(trade.ParseTrade("size", "price", "side", "time", TimestampType.Iso8601, "id", "buy")); + } + + if (!callback(trades)) + { + break; + } + + Task.Delay(1000).Wait(); + } + } + + /// + protected async override Task> OnGetMarketSymbolsAsync(bool isWebSocket = false) + { + JToken result = await MakeJsonRequestAsync("/markets"); + + //FTX contains futures which we are not interested in so we filter them out. + var names = result.Children().Select(x => x["name"].ToStringInvariant()).Where(x => Regex.Match(x, @"[\w\d]*\/[[\w\d]]*").Success).ToList(); + + names.Sort(); + + return names; + } + + /// + protected async internal override Task> OnGetMarketSymbolsMetadataAsync() + { + //{ + // "name": "BTC-0628", + // "baseCurrency": null, + // "quoteCurrency": null, + // "quoteVolume24h": 28914.76, + // "change1h": 0.012, + // "change24h": 0.0299, + // "changeBod": 0.0156, + // "highLeverageFeeExempt": false, + // "minProvideSize": 0.001, + // "type": "future", + // "underlying": "BTC", + // "enabled": true, + // "ask": 3949.25, + // "bid": 3949, + // "last": 10579.52, + // "postOnly": false, + // "price": 10579.52, + // "priceIncrement": 0.25, + // "sizeIncrement": 0.0001, + // "restricted": false, + // "volumeUsd24h": 28914.76 + //} + + var markets = new List(); + + JToken result = await MakeJsonRequestAsync("/markets"); + + foreach (JToken token in result.Children()) + { + var symbol = token["name"].ToStringInvariant(); + + if (!Regex.Match(symbol, @"[\w\d]*\/[[\w\d]]*").Success) + { + continue; + } + + var market = new ExchangeMarket() + { + MarketSymbol = symbol, + BaseCurrency = token["baseCurrency"].ToStringInvariant(), + QuoteCurrency = token["quoteCurrency"].ToStringInvariant(), + PriceStepSize = token["priceIncrement"].ConvertInvariant(), + QuantityStepSize = token["sizeIncrement"].ConvertInvariant(), + MinTradeSize = token["minProvideSize"].ConvertInvariant(), + IsActive = token["enabled"].ConvertInvariant(), + }; + + markets.Add(market); + } + + return markets; + } + + /// + protected async override Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) + { + // https://docs.ftx.com/#get-open-orders + + + var markets = new List(); + + JToken result = await MakeJsonRequestAsync($"/orders?market={marketSymbol}", null, await GetNoncePayloadAsync()); + + foreach (JToken token in result.Children()) + { + var symbol = token["market"].ToStringInvariant(); + + if (!Regex.Match(symbol, @"[\w\d]*\/[[\w\d]]*").Success) + { + continue; + } + + markets.Add(ParseOrder(token)); + } + + return markets; + } + + /// + protected async override Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) + { + JToken response = await MakeJsonRequestAsync($"/markets/{marketSymbol}/orderbook?depth={maxCount}"); + + return ExchangeAPIExtensions.ParseOrderBookFromJTokenArrays(response, maxCount: maxCount); + } + + /// + protected async override Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + { + // https://docs.ftx.com/#get-order-status + + var url = "/orders/"; + + if (isClientOrderId) + { + url += "by_client_id/"; + } + + JToken result = await MakeJsonRequestAsync($"{url}{orderId}", null, await GetNoncePayloadAsync()); + + return ParseOrder(result); + } + + /// + protected async override Task>> OnGetTickersAsync() + { + JToken result = await MakeJsonRequestAsync("/markets"); + + var tickers = new Dictionary(); + + foreach (JToken token in result.Children()) + { + var symbol = token["name"].ToStringInvariant(); + + if (!Regex.Match(symbol, @"[\w\d]*\/[[\w\d]]*").Success) + { + continue; + } + + var ticker = await this.ParseTickerAsync(token, symbol, "ask", "bid", "last", null, null, "time", TimestampType.UnixSecondsDouble); + + tickers.Add(symbol, ticker); + } + + return tickers; + } + + /// + protected override async Task OnGetTickersWebSocketAsync(Action>> tickers, params string[] marketSymbols) + { + if (marketSymbols == null || marketSymbols.Length == 0) + { + marketSymbols = (await GetMarketSymbolsAsync(true)).ToArray(); + } + return await ConnectPublicWebSocketAsync(null, messageCallback: async (_socket, msg) => + { + JToken parsedMsg = JToken.Parse(msg.ToStringFromUTF8()); + + if (parsedMsg["channel"].ToStringInvariant().Equals("ticker") && !parsedMsg["type"].ToStringInvariant().Equals("subscribed")) + { + JToken data = parsedMsg["data"]; + + var exchangeTicker = await this.ParseTickerAsync(data, parsedMsg["market"].ToStringInvariant(), "ask", "bid", "last", null, null, "time", TimestampType.UnixSecondsDouble); + + var kv = new KeyValuePair(exchangeTicker.MarketSymbol, exchangeTicker); + + tickers(new List> { kv }); + } + }, connectCallback: async (_socket) => + { + List marketSymbolList = marketSymbols.ToList(); + + //{'op': 'subscribe', 'channel': 'trades', 'market': 'BTC-PERP'} + + for (int i = 0; i < marketSymbolList.Count; i++) + { + await _socket.SendMessageAsync(new + { + op = "subscribe", + market = marketSymbolList[i], + channel = "ticker" + }); + } + }); + } + + /// + protected async override Task OnPlaceOrderAsync(ExchangeOrderRequest order) + { + //{ + // "market": "XRP-PERP", + // "side": "sell", + // "price": 0.306525, + // "type": "limit", + // "size": 31431.0, + // "reduceOnly": false, + // "ioc": false, + // "postOnly": false, + // "clientId": null + //} + + IEnumerable markets = await OnGetMarketSymbolsMetadataAsync(); + ExchangeMarket market = markets.Where(m => m.MarketSymbol == order.MarketSymbol).First(); + + var payload = await GetNoncePayloadAsync(); + + var parameters = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + {"market", market.MarketSymbol}, + {"side", order.IsBuy ? "buy" : "sell" }, + {"type", order.OrderType.ToStringLowerInvariant() }, + {"size", order.RoundAmount() }, + {"postOnly", order.IsPostOnly } + }; + + if (!string.IsNullOrEmpty(order.ClientOrderId)) + { + parameters.Add("clientId", order.ClientOrderId); + } + + if (order.OrderType != OrderType.Market) + { + int precision = BitConverter.GetBytes(decimal.GetBits((decimal)market.PriceStepSize)[3])[2]; + + if (order.Price == null) throw new ArgumentNullException(nameof(order.Price)); + + parameters.Add("price", Math.Round(order.Price.Value, precision)); + } + else + { + parameters.Add("price", null); + } + + parameters.CopyTo(payload); + + order.ExtraParameters.CopyTo(payload); + + var response = await MakeJsonRequestAsync("/orders", null, payload, "POST"); + + ExchangeOrderResult result = new ExchangeOrderResult + { + OrderId = response["id"].ToStringInvariant(), + ClientOrderId = response["clientId"].ToStringInvariant(), + OrderDate = CryptoUtility.ToDateTimeInvariant(response["createdAt"]), + Price = CryptoUtility.ConvertInvariant(response["price"]), + AmountFilled = CryptoUtility.ConvertInvariant(response["filledSize"]), + AveragePrice = CryptoUtility.ConvertInvariant(response["avgFillPrice"]), + Amount = CryptoUtility.ConvertInvariant(response["size"]), + MarketSymbol = response["market"].ToStringInvariant(), + IsBuy = response["side"].ToStringInvariant() == "buy" + }; + + return result; + } + + /// + protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + { + if (CanMakeAuthenticatedRequest(payload)) + { + string timestamp = payload["nonce"].ToStringInvariant(); + + payload.Remove("nonce"); + + string form = CryptoUtility.GetJsonForPayload(payload); + + //Create the signature payload + string toHash = $"{timestamp}{request.Method.ToUpperInvariant()}{request.RequestUri.PathAndQuery}"; + + if (request.Method == "POST") + { + toHash += form; + + await CryptoUtility.WriteToRequestAsync(request, form); + } + + byte[] secret = CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey); + + string signatureHexString = CryptoUtility.SHA256Sign(toHash, secret); + + request.AddHeader("FTX-KEY", PublicApiKey.ToUnsecureString()); + request.AddHeader("FTX-SIGN", signatureHexString); + request.AddHeader("FTX-TS", timestamp); + } + } + + #endregion + + #region Private Methods + + /// + /// Parses the json of an order. + /// + /// Json token to parse the order from. + /// Parsed exchange order result. + private ExchangeOrderResult ParseOrder(JToken token) + { + return new ExchangeOrderResult() + { + MarketSymbol = token["market"].ToStringInvariant(), + Price = token["price"].ConvertInvariant(), + AveragePrice = token["avgFillPrice"].ConvertInvariant(), + OrderDate = token["createdAt"].ConvertInvariant(), + IsBuy = token["side"].ToStringInvariant().Equals("buy"), + OrderId = token["id"].ToStringInvariant(), + Amount = token["size"].ConvertInvariant(), + AmountFilled = token["filledSize"].ConvertInvariant(), + ClientOrderId = token["clientId"].ToStringInvariant(), + Result = token["status"].ToStringInvariant().ToExchangeAPIOrderResult(), + ResultCode = token["status"].ToStringInvariant() + }; + } + + #endregion + + } +} diff --git a/src/ExchangeSharp/API/Exchanges/FTX/Extensions.cs b/src/ExchangeSharp/API/Exchanges/FTX/Extensions.cs new file mode 100644 index 00000000..5fbead56 --- /dev/null +++ b/src/ExchangeSharp/API/Exchanges/FTX/Extensions.cs @@ -0,0 +1,23 @@ +namespace ExchangeSharp.API.Exchanges.FTX +{ + /// + /// Extension helper methods. + /// + internal static class Extensions + { + /// + /// Cnvert FTX order status string to . + /// + /// FTX order status string. + /// + internal static ExchangeAPIOrderResult ToExchangeAPIOrderResult(this string status) + { + return status switch + { + "open" => ExchangeAPIOrderResult.Pending, + "closed" => ExchangeAPIOrderResult.Filled, + _ => ExchangeAPIOrderResult.Unknown, + }; + } + } +} diff --git a/src/ExchangeSharp/API/Exchanges/GateIo/ExchangeGateIoAPI.cs b/src/ExchangeSharp/API/Exchanges/GateIo/ExchangeGateIoAPI.cs index 5783e977..cc3319fd 100644 --- a/src/ExchangeSharp/API/Exchanges/GateIo/ExchangeGateIoAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/GateIo/ExchangeGateIoAPI.cs @@ -166,6 +166,7 @@ private ExchangeTicker ParseTicker(JToken tickerToken) return new ExchangeTicker { + Exchange = Name, MarketSymbol = tickerToken["currency_pair"].ToStringInvariant(), Bid = IsEmptyString(tickerToken["lowest_ask"]) ? default : tickerToken["lowest_ask"].ConvertInvariant(), Ask = IsEmptyString(tickerToken["highest_bid"]) ? default : tickerToken["highest_bid"].ConvertInvariant(), diff --git a/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs b/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs index a3d5a2c9..6c74f597 100644 --- a/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs @@ -234,6 +234,7 @@ protected override async Task OnGetTickerAsync(string marketSymb } ExchangeTicker t = new ExchangeTicker { + Exchange = Name, MarketSymbol = marketSymbol, ApiResponse = obj, Ask = obj["ask"].ConvertInvariant(), @@ -375,6 +376,7 @@ static ExchangeTicker GetTicker(ConcurrentDictionary tic (string baseCurrency, string quoteCurrency) = api.ExchangeMarketSymbolToCurrenciesAsync(_marketSymbol).Sync(); return new ExchangeTicker { + Exchange = api.Name, MarketSymbol = _marketSymbol, Volume = new ExchangeVolume { diff --git a/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs b/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs index a56dc6f1..6d6743e8 100644 --- a/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs @@ -604,6 +604,7 @@ private async Task ConvertToExchangeTickerAsync(string symbol, J var (baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync(symbol); return new ExchangeTicker { + Exchange = Name, MarketSymbol = symbol, ApiResponse = ticker, Ask = ticker["a"][0].ConvertInvariant(), diff --git a/src/ExchangeSharp/API/Exchanges/LBank/ExchangeLBankAPI.cs b/src/ExchangeSharp/API/Exchanges/LBank/ExchangeLBankAPI.cs index 5936860e..ae82dc70 100644 --- a/src/ExchangeSharp/API/Exchanges/LBank/ExchangeLBankAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/LBank/ExchangeLBankAPI.cs @@ -211,6 +211,7 @@ private ExchangeTicker ParseTicker(JToken resp) ExchangeTicker ticker = new ExchangeTicker { + Exchange = Name, MarketSymbol = symbol, ApiResponse = obj, Ask = obj["high"].ConvertInvariant(), diff --git a/src/ExchangeSharp/API/Exchanges/NDAX/ExchangeNDAXAPI.cs b/src/ExchangeSharp/API/Exchanges/NDAX/ExchangeNDAXAPI.cs index b8547931..b6e7c0a7 100644 --- a/src/ExchangeSharp/API/Exchanges/NDAX/ExchangeNDAXAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/NDAX/ExchangeNDAXAPI.cs @@ -33,7 +33,7 @@ protected override async Task>> await MakeJsonRequestAsync>("ticker", "https://core.ndax.io/v1", null, "GET"); _marketSymbolToInstrumentIdMapping = result.ToDictionary(pair => pair.Key.Replace("_", ""), pair => pair.Value.Id); // remove the _ return result.Select(pair => - new KeyValuePair(pair.Key, pair.Value.ToExchangeTicker(pair.Key))); + new KeyValuePair(pair.Key, pair.Value.ToExchangeTicker(Name, pair.Key))); } protected override async Task OnGetTickerAsync(string symbol) @@ -395,7 +395,7 @@ protected override async Task OnGetTickersWebSocketAsync(Action(symbol, rawPayload.ToExchangeTicker(symbol)), + new KeyValuePair(symbol, rawPayload.ToExchangeTicker(Name, symbol)), }); } else // "{\"result\":false,\"errormsg\":\"Resource Not Found\",\"errorcode\":104,\"detail\":\"Instrument not Found\"}" diff --git a/src/ExchangeSharp/API/Exchanges/NDAX/Models/Level1Data.cs b/src/ExchangeSharp/API/Exchanges/NDAX/Models/Level1Data.cs index 81881afa..747815d7 100644 --- a/src/ExchangeSharp/API/Exchanges/NDAX/Models/Level1Data.cs +++ b/src/ExchangeSharp/API/Exchanges/NDAX/Models/Level1Data.cs @@ -67,11 +67,12 @@ private class Level1Data [JsonProperty("TimeStamp")] public string TimeStamp { get; set; } - public ExchangeTicker ToExchangeTicker(string currencyPair) + public ExchangeTicker ToExchangeTicker(string exchangeName, string currencyPair) { var currencyParts = currencyPair.Split(new[] { "_" }, StringSplitOptions.RemoveEmptyEntries); return new ExchangeTicker() { + Exchange = exchangeName, Bid = BestBid.GetValueOrDefault(), Ask = BestOffer.GetValueOrDefault(), Id = InstrumentId.ToString(), diff --git a/src/ExchangeSharp/API/Exchanges/NDAX/Models/NDAXTicker.cs b/src/ExchangeSharp/API/Exchanges/NDAX/Models/NDAXTicker.cs index 36873883..5fea6a0d 100644 --- a/src/ExchangeSharp/API/Exchanges/NDAX/Models/NDAXTicker.cs +++ b/src/ExchangeSharp/API/Exchanges/NDAX/Models/NDAXTicker.cs @@ -21,11 +21,12 @@ private class NDAXTicker [JsonProperty("baseVolume")] public decimal? BaseVolume { get; set; } [JsonProperty("quoteVolume")] public decimal? QuoteVolume { get; set; } - public ExchangeTicker ToExchangeTicker(string currencyPair) + public ExchangeTicker ToExchangeTicker(string exchangeName, string currencyPair) { var currencyParts = currencyPair.Split(new[] { "_" }, StringSplitOptions.RemoveEmptyEntries); return new ExchangeTicker() { + Exchange = exchangeName, MarketSymbol = currencyPair, Ask = LowestAsk.GetValueOrDefault(), Bid = HighestBid.GetValueOrDefault(), diff --git a/src/ExchangeSharp/API/Exchanges/UfoDex/ExchangeUfoDexAPI.cs b/src/ExchangeSharp/API/Exchanges/UfoDex/ExchangeUfoDexAPI.cs index 0bee1f72..95eef760 100644 --- a/src/ExchangeSharp/API/Exchanges/UfoDex/ExchangeUfoDexAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/UfoDex/ExchangeUfoDexAPI.cs @@ -33,6 +33,7 @@ private ExchangeTicker ParseTicker(JToken token) // 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(), // ???? + Exchange = Name, ApiResponse = token, Ask = token["Ask"].ConvertInvariant(), Bid = token["Bid"].ConvertInvariant(), diff --git a/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs b/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs index 07b9fd80..13b63815 100644 --- a/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs +++ b/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs @@ -507,6 +507,7 @@ internal static async Task ParseTickerAsync(this ExchangeAPI api } ExchangeTicker ticker = new ExchangeTicker { + Exchange = api.Name, MarketSymbol = marketSymbol, ApiResponse = token, Ask = ask, diff --git a/src/ExchangeSharp/API/Exchanges/_Base/ExchangeLogger.cs b/src/ExchangeSharp/API/Exchanges/_Base/ExchangeLogger.cs index 87dac80b..ab956eb7 100644 --- a/src/ExchangeSharp/API/Exchanges/_Base/ExchangeLogger.cs +++ b/src/ExchangeSharp/API/Exchanges/_Base/ExchangeLogger.cs @@ -1,4 +1,4 @@ -/* +/* MIT LICENSE Copyright 2017 Digital Ruby, LLC - http://www.digitalruby.com diff --git a/src/ExchangeSharp/ExchangeSharp.csproj b/src/ExchangeSharp/ExchangeSharp.csproj index 324b2882..49354715 100644 --- a/src/ExchangeSharp/ExchangeSharp.csproj +++ b/src/ExchangeSharp/ExchangeSharp.csproj @@ -22,7 +22,7 @@ git true - + diff --git a/src/ExchangeSharp/Model/ExchangeTicker.cs b/src/ExchangeSharp/Model/ExchangeTicker.cs index 308b5895..282b9e57 100644 --- a/src/ExchangeSharp/Model/ExchangeTicker.cs +++ b/src/ExchangeSharp/Model/ExchangeTicker.cs @@ -32,6 +32,11 @@ public sealed class ExchangeTicker /// public string Id { get; set; } + /// + /// The name of the exchange the tick was sent from. + /// + public string Exchange { get; set; } + /// /// The currency pair symbol that this ticker is in reference to ///