diff --git a/src/ExchangeSharp/API/Exchanges/BitMEX/ExchangeBitMEXAPI.cs b/src/ExchangeSharp/API/Exchanges/BitMEX/ExchangeBitMEXAPI.cs index 2ac30179..764ac681 100644 --- a/src/ExchangeSharp/API/Exchanges/BitMEX/ExchangeBitMEXAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/BitMEX/ExchangeBitMEXAPI.cs @@ -255,7 +255,7 @@ protected override Task OnGetTradesWebSocketAsync(Func(marketSymbol, t.ParseTrade("size", "price", "side", "timestamp", TimestampType.Iso8601, "trdMatchID"))); + await callback(new KeyValuePair(marketSymbol, t.ParseTrade("size", "price", "side", "timestamp", TimestampType.Iso8601UTC, "trdMatchID"))); } }, async (_socket) => { @@ -426,7 +426,7 @@ protected override async Task> OnGetCandlesAsync(strin var obj = await MakeJsonRequestAsync(url); foreach (var t in obj) { - candles.Add(this.ParseCandle(t, marketSymbol, periodSeconds, "open", "high", "low", "close", "timestamp", TimestampType.Iso8601, "volume", "turnover", "vwap")); + candles.Add(this.ParseCandle(t, marketSymbol, periodSeconds, "open", "high", "low", "close", "timestamp", TimestampType.Iso8601UTC, "volume", "turnover", "vwap")); } candles.Reverse(); @@ -468,7 +468,7 @@ public async Task> GetHistoricalTradesAsync( var obj = await MakeJsonRequestAsync(url); foreach (var t in obj) { - trades.Add(t.ParseTrade("size", "price", "side", "timestamp", TimestampType.Iso8601, "trdMatchID")); + trades.Add(t.ParseTrade("size", "price", "side", "timestamp", TimestampType.Iso8601UTC, "trdMatchID")); } return trades; @@ -709,7 +709,7 @@ private ExchangePosition ParsePosition(JToken token) LiquidationPrice = token["liquidationPrice"].ConvertInvariant(), Leverage = token["leverage"].ConvertInvariant(), LastPrice = token["lastPrice"].ConvertInvariant(), - TimeStamp = CryptoUtility.ParseTimestamp(token["currentTimestamp"], TimestampType.Iso8601) + TimeStamp = CryptoUtility.ParseTimestamp(token["currentTimestamp"], TimestampType.Iso8601UTC) }; return result; } diff --git a/src/ExchangeSharp/API/Exchanges/Bithumb/ExchangeBithumbAPI.cs b/src/ExchangeSharp/API/Exchanges/Bithumb/ExchangeBithumbAPI.cs index fe34860d..d8247fc6 100644 --- a/src/ExchangeSharp/API/Exchanges/Bithumb/ExchangeBithumbAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Bithumb/ExchangeBithumbAPI.cs @@ -12,6 +12,7 @@ The above copyright notice and this permission notice shall be included in all c using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json.Linq; @@ -21,6 +22,8 @@ namespace ExchangeSharp public sealed partial class ExchangeBithumbAPI : ExchangeAPI { public override string BaseUrl { get; set; } = "https://api.bithumb.com"; + public override string BaseUrlWebSocket { get; set; } = "wss://pubwss.bithumb.com/pub/ws"; + private ExchangeBithumbAPI() { @@ -112,15 +115,16 @@ protected override (string baseCurrency, string quoteCurrency) OnSplitMarketSymb protected override async Task> OnGetMarketSymbolsAsync() { List marketSymbols = new List(); - string marketSymbol = "all"; + string marketSymbol = "all_BTC"; var data = await MakeRequestBithumbAsync(marketSymbol, "/public/ticker/$SYMBOL$"); foreach (JProperty token in data.Item1) { if (token.Name != "date") { - marketSymbols.Add(token.Name); - } - } + marketSymbols.Add($"{token.Name}_KRW"); + if (token.Name != "BTC") marketSymbols.Add($"{token.Name}_BTC"); + } + } return marketSymbols; } @@ -169,7 +173,60 @@ protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + { + /* + { + "type" : "transaction", + "content" : { + "list" : [ + { + "symbol" : "BTC_KRW", // currency code + "buySellGb" : "1", // Execution type (1: sell execution, 2: buy execution) + "contPrice" : "10579000", // Execution price + "contQty" : "0.01", // contract quantity + "contAmt" : "105790.00", // Execution amount + "contDtm" : "2020-01-29 12:24:18.830039", // Execution time + "updn" : "dn" // Compare with the previous price: up-up, dn-down + } + ] + } + } + */ + 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["status"].ToStringInvariant().Equals("0000")) + return; // either "Connected Successfully" or "Filter Registered Successfully" + else if (parsedMsg["status"].ToStringInvariant().Equals("5100")) + { + Logger.Error("Error in exchange {0} OnGetTradesWebSocketAsync(): {1}", Name, parsedMsg["resmsg"].ToStringInvariant()); + return; + } + else if (parsedMsg["type"].ToStringInvariant().Equals("transaction")) + { + foreach (var data in parsedMsg["content"]["list"]) + { + var exchangeTrade = data.ParseTrade("contQty", "contPrice", "buySellGb", "contDtm", TimestampType.Iso8601UTC, null, typeKeyIsBuyValue: "2"); + + await callback(new KeyValuePair(parsedMsg["market"].ToStringInvariant(), exchangeTrade)); + } + } + }, connectCallback: async (_socket) => + { + await _socket.SendMessageAsync(new + { + type = "transaction", + symbols = marketSymbols, + }); + }); + } + } public partial class ExchangeName { public const string Bithumb = "Bithumb"; } } diff --git a/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI.cs b/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI.cs index 77f414a5..2c0b693d 100644 --- a/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI.cs @@ -230,7 +230,7 @@ protected override async Task OnGetTickerAsync(string marketSymb { JToken ticker = await MakeJsonRequestAsync("/markets/" + marketSymbol + "/ticker"); //NOTE: Bittrex uses the term "BaseVolume" when referring to the QuoteCurrencyVolume - return await this.ParseTickerAsync(ticker, marketSymbol, "askRate", "bidRate", "lastTradeRate", "volume", "quoteVolume", "updatedAt", TimestampType.Iso8601); + return await this.ParseTickerAsync(ticker, marketSymbol, "askRate", "bidRate", "lastTradeRate", "volume", "quoteVolume", "updatedAt", TimestampType.Iso8601UTC); } protected override async Task>> OnGetTickersAsync() @@ -241,7 +241,7 @@ protected override async Task>> foreach (JToken ticker in tickers) { marketSymbol = ticker["symbol"].ToStringInvariant(); - ExchangeTicker tickerObj = await this.ParseTickerAsync(ticker, marketSymbol, "askRate", "bidRate", "lastTradeRate", "volume", "quoteVolume", "updatedAt", TimestampType.Iso8601); + ExchangeTicker tickerObj = await this.ParseTickerAsync(ticker, marketSymbol, "askRate", "bidRate", "lastTradeRate", "volume", "quoteVolume", "updatedAt", TimestampType.Iso8601UTC); tickerList.Add(new KeyValuePair(marketSymbol, tickerObj)); } return tickerList; @@ -319,7 +319,7 @@ protected override async Task> OnGetRecentTradesAsync JToken array = await MakeJsonRequestAsync(baseUrl); foreach (JToken token in array) { - trades.Add(token.ParseTrade("quantity", "rate", "takerSide", "executedAt", TimestampType.Iso8601, "id")); + trades.Add(token.ParseTrade("quantity", "rate", "takerSide", "executedAt", TimestampType.Iso8601UTC, "id")); } return trades; } @@ -488,7 +488,7 @@ protected override async Task> OnGetCandlesAsync(strin { //NOTE: Bittrex uses the term "BaseVolume" when referring to the QuoteCurrencyVolume MarketCandle candle = this.ParseCandle(token: jsonCandle, marketSymbol: marketSymbol, periodSeconds: periodSeconds, - openKey: "open", highKey: "high", lowKey: "low", closeKey: "close", timestampKey: "startsAt", timestampType: TimestampType.Iso8601, + openKey: "open", highKey: "high", lowKey: "low", closeKey: "close", timestampKey: "startsAt", timestampType: TimestampType.Iso8601UTC, baseVolumeKey: "volume", quoteVolumeKey: "quoteVolume"); if (startDate != null && endDate != null) { diff --git a/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs b/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs index 10e1527e..5ef30e21 100644 --- a/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs @@ -279,7 +279,7 @@ protected override async Task OnGetTradesWebSocketAsync(Func(dataRow["symbol"].ToStringInvariant(), trade)); } @@ -927,7 +927,7 @@ private ExchangePosition ParsePosition(JToken token) AveragePrice = token["entry_price"].ConvertInvariant(), LiquidationPrice = token["liq_price"].ConvertInvariant(), Leverage = token["effective_leverage"].ConvertInvariant(), - TimeStamp = CryptoUtility.ParseTimestamp(token["updated_at"], TimestampType.Iso8601) + TimeStamp = CryptoUtility.ParseTimestamp(token["updated_at"], TimestampType.Iso8601UTC) }; if (token["side"].ToStringInvariant() == "Sell") result.Amount *= -1; diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs index 685e973e..8a5cd5e0 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs @@ -236,7 +236,7 @@ protected override async Task> OnG protected override async Task OnGetTickerAsync(string marketSymbol) { JToken ticker = await MakeJsonRequestAsync("/products/" + marketSymbol + "/ticker"); - return await this.ParseTickerAsync(ticker, marketSymbol, "ask", "bid", "price", "volume", null, "time", TimestampType.Iso8601); + return await this.ParseTickerAsync(ticker, marketSymbol, "ask", "bid", "price", "volume", null, "time", TimestampType.Iso8601UTC); } protected override async Task OnGetDepositAddressAsync(string symbol, bool forceRegenerate = false) @@ -375,7 +375,7 @@ protected override async Task OnGetTickersWebSocketAsync(Action>() { new KeyValuePair(token["product_id"].ToStringInvariant(), ticker) }); } }, async (_socket) => @@ -438,7 +438,7 @@ protected override async Task OnGetTradesWebSocketAsync(Func OnUserDataWebSocketAsync(Action callback) @@ -556,7 +556,7 @@ protected override async Task OnGetHistoricalTradesAsync(Func token.ParseTrade("size", "price", "side", "time", TimestampType.Iso8601, "trade_id"), + ParseFunction = (JToken token) => token.ParseTrade("size", "price", "side", "time", TimestampType.Iso8601UTC, "trade_id"), StartDate = startDate, MarketSymbol = marketSymbol, Url = "/products/[marketSymbol]/trades", @@ -578,7 +578,7 @@ protected override async Task> OnGetRecentTradesAsync List tradeList = new List(); foreach (JToken trade in trades) { - tradeList.Add(trade.ParseTrade("size", "price", "side", "time", TimestampType.Iso8601, "trade_id")); + tradeList.Add(trade.ParseTrade("size", "price", "side", "time", TimestampType.Iso8601UTC, "trade_id")); } return tradeList; } diff --git a/src/ExchangeSharp/API/Exchanges/FTX/ExchangeFTXAPI.cs b/src/ExchangeSharp/API/Exchanges/FTX/ExchangeFTXAPI.cs index 494ee2f8..9ab8a7c4 100644 --- a/src/ExchangeSharp/API/Exchanges/FTX/ExchangeFTXAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/FTX/ExchangeFTXAPI.cs @@ -1,529 +1,15 @@ -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; +using System.Text; namespace ExchangeSharp { - public sealed partial class ExchangeFTXAPI : ExchangeAPI + public sealed class ExchangeFTXAPI : FTXGroupCommon { 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 response.ParseOrderBookFromJTokenArrays(); - } - - /// - protected async override Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) - { // https://docs.ftx.com/#get-order-status and https://docs.ftx.com/#get-order-status-by-client-id - if (!string.IsNullOrEmpty(marketSymbol)) throw new NotImplementedException("Searching by marketSymbol is either not implemented by or supported by this exchange. Please submit a PR if you are interested in this feature"); - - 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 OnGetTickerAsync(string marketSymbol) - { - var result = await MakeJsonRequestAsync($"/markets/{marketSymbol}"); - - return await this.ParseTickerAsync(result, marketSymbol, "ask", "bid", "last", null, null, "time", TimestampType.UnixSecondsDouble); - } - - protected override async Task OnWithdrawAsync(ExchangeWithdrawalRequest request) - { - var parameters = new Dictionary - { - { "coin", request.Currency }, - { "size", request.Amount }, - { "address", request.Address }, - { "nonce", await GenerateNonceAsync() }, - { "password", request.Password }, - { "code", request.Code } - }; - - var result = await MakeJsonRequestAsync("/wallet/withdrawals", null, parameters, "POST"); - - return new ExchangeWithdrawalResponse - { - Id = result["id"].ToString(), - Fee = result.Value("fee") - }; - } - - /// - 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() } - }; - - if (!string.IsNullOrEmpty(order.ClientOrderId)) - { - parameters.Add("clientId", order.ClientOrderId); - } - - if (order.IsPostOnly != null) - { - parameters.Add("postOnly", order.IsPostOnly); - } - - 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(token["size"].ConvertInvariant() - token["filledSize"].ConvertInvariant()), - ResultCode = token["status"].ToStringInvariant() - }; - } - - #endregion + public partial class ExchangeName { public const string FTX = "FTX"; } - } } diff --git a/src/ExchangeSharp/API/Exchanges/FTX/ExchangeFTXUSAPI.cs b/src/ExchangeSharp/API/Exchanges/FTX/ExchangeFTXUSAPI.cs new file mode 100644 index 00000000..4f977b44 --- /dev/null +++ b/src/ExchangeSharp/API/Exchanges/FTX/ExchangeFTXUSAPI.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ExchangeSharp +{ + public sealed class ExchangeFTXUSAPI : FTXGroupCommon + { + public override string BaseUrl { get; set; } = "https://ftx.us/api"; + public override string BaseUrlWebSocket { get; set; } = "wss://ftx.us/ws/"; + } + + public partial class ExchangeName { public const string FTXUS = "FTXUS"; } +} diff --git a/src/ExchangeSharp/API/Exchanges/FTX/FTXGroupCommon.cs b/src/ExchangeSharp/API/Exchanges/FTX/FTXGroupCommon.cs new file mode 100644 index 00000000..51407e8d --- /dev/null +++ b/src/ExchangeSharp/API/Exchanges/FTX/FTXGroupCommon.cs @@ -0,0 +1,561 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace ExchangeSharp +{ + public abstract class FTXGroupCommon : ExchangeAPI + { + #region [ Constructor(s) ] + + public FTXGroupCommon() + { + 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.Iso8601UTC, "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.Iso8601UTC, "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 response.ParseOrderBookFromJTokenArrays(); + } + + /// + protected async override Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + { // https://docs.ftx.com/#get-order-status and https://docs.ftx.com/#get-order-status-by-client-id + if (!string.IsNullOrEmpty(marketSymbol)) throw new NotImplementedException("Searching by marketSymbol is either not implemented by or supported by this exchange. Please submit a PR if you are interested in this feature"); + + 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 OnGetTickerAsync(string marketSymbol) + { + var result = await MakeJsonRequestAsync($"/markets/{marketSymbol}"); + + return await this.ParseTickerAsync(result, marketSymbol, "ask", "bid", "last", null, null, "time", TimestampType.UnixSecondsDouble); + } + + protected override async Task OnWithdrawAsync(ExchangeWithdrawalRequest request) + { + var parameters = new Dictionary + { + { "coin", request.Currency }, + { "size", request.Amount }, + { "address", request.Address }, + { "nonce", await GenerateNonceAsync() }, + { "password", request.Password }, + { "code", request.Code } + }; + + var result = await MakeJsonRequestAsync("/wallet/withdrawals", null, parameters, "POST"); + + return new ExchangeWithdrawalResponse + { + Id = result["id"].ToString(), + Fee = result.Value("fee") + }; + } + + /// + 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) => + { + //{'op': 'subscribe', 'channel': 'trades', 'market': 'BTC-PERP'} + + for (int i = 0; i < marketSymbols.Length; i++) + { + await _socket.SendMessageAsync(new + { + op = "subscribe", + market = marketSymbols[i], + channel = "ticker" + }); + } + }); + } + + /// + protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, 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("trades") + && !parsedMsg["type"].ToStringInvariant().Equals("subscribed")) + { + foreach (var data in parsedMsg["data"]) + { + var exchangeTrade = data.ParseTradeFTX("size", "price", "side", "time", TimestampType.Iso8601Local, "id"); + + await callback(new KeyValuePair(parsedMsg["market"].ToStringInvariant(), exchangeTrade)); + } + } + }, connectCallback: async (_socket) => + { + //{'op': 'subscribe', 'channel': 'trades', 'market': 'BTC-PERP'} + + for (int i = 0; i < marketSymbols.Length; i++) + { + await _socket.SendMessageAsync(new + { + op = "subscribe", + market = marketSymbols[i], + channel = "trades" + }); + } + }); + } + + /// + 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() } + }; + + if (!string.IsNullOrEmpty(order.ClientOrderId)) + { + parameters.Add("clientId", order.ClientOrderId); + } + + if (order.IsPostOnly != null) + { + parameters.Add("postOnly", order.IsPostOnly); + } + + 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(token["size"].ConvertInvariant() - token["filledSize"].ConvertInvariant()), + ResultCode = token["status"].ToStringInvariant() + }; + } + + #endregion + + } +} diff --git a/src/ExchangeSharp/API/Exchanges/FTX/Models/FTXTrade.cs b/src/ExchangeSharp/API/Exchanges/FTX/Models/FTXTrade.cs new file mode 100644 index 00000000..e3280b2b --- /dev/null +++ b/src/ExchangeSharp/API/Exchanges/FTX/Models/FTXTrade.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ExchangeSharp.API.Exchanges.FTX.Models +{ + public class FTXTrade : ExchangeTrade + { + /// + /// if the trade involved a liquidation order + /// + public bool IsLiquidationOrder { get; set; } + + public override string ToString() + { + return string.Format("{0}, IsLiquidation: {1}", base.ToString(), IsLiquidationOrder); + } + } +} diff --git a/src/ExchangeSharp/API/Exchanges/Hitbtc/ExchangeHitbtcAPI.cs b/src/ExchangeSharp/API/Exchanges/Hitbtc/ExchangeHitbtcAPI.cs index e6d9fd8f..88763e6e 100644 --- a/src/ExchangeSharp/API/Exchanges/Hitbtc/ExchangeHitbtcAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Hitbtc/ExchangeHitbtcAPI.cs @@ -160,7 +160,7 @@ protected override async Task> OnGetCandlesAsync(strin JToken obj = await MakeJsonRequestAsync("/public/candles/" + marketSymbol + "?period=" + periodString + "&limit=" + limit); foreach (JToken token in obj) { - candles.Add(this.ParseCandle(token, marketSymbol, periodSeconds, "open", "max", "min", "close", "timestamp", TimestampType.Iso8601, "volume", "volumeQuote")); + candles.Add(this.ParseCandle(token, marketSymbol, periodSeconds, "open", "max", "min", "close", "timestamp", TimestampType.Iso8601UTC, "volume", "volumeQuote")); } return candles; } @@ -575,7 +575,7 @@ await _socket.SendMessageAsync(new }); ExchangeTrade parseTrade(JToken token) => token.ParseTrade(amountKey: "quantity", priceKey: "price", typeKey: "side", timestampKey: "timestamp", - timestampType: TimestampType.Iso8601, idKey: "id"); + timestampType: TimestampType.Iso8601UTC, idKey: "id"); } #endregion @@ -617,13 +617,13 @@ public async Task AccountTransfer(string Symbol, decimal Amount, bool ToBa private async Task ParseTickerAsync(JToken token, string symbol) { // [ {"ask": "0.050043","bid": "0.050042","last": "0.050042","open": "0.047800","low": "0.047052","high": "0.051679","volume": "36456.720","volumeQuote": "1782.625000","timestamp": "2017-05-12T14:57:19.999Z","symbol": "ETHBTC"} ] - return await this.ParseTickerAsync(token, symbol, "ask", "bid", "last", "volume", "volumeQuote", "timestamp", TimestampType.Iso8601); + return await this.ParseTickerAsync(token, symbol, "ask", "bid", "last", "volume", "volumeQuote", "timestamp", TimestampType.Iso8601UTC); } private ExchangeTrade ParseExchangeTrade(JToken token) { // [ { "id": 9533117, "price": "0.046001", "quantity": "0.220", "side": "sell", "timestamp": "2017-04-14T12:18:40.426Z" }, ... ] - return token.ParseTrade("quantity", "price", "side", "timestamp", TimestampType.Iso8601, "id"); + return token.ParseTrade("quantity", "price", "side", "timestamp", TimestampType.Iso8601UTC, "id"); } private ExchangeOrderResult ParseCompletedOrder(JToken token) diff --git a/src/ExchangeSharp/API/Exchanges/OKGroup/OKGroupCommon.cs b/src/ExchangeSharp/API/Exchanges/OKGroup/OKGroupCommon.cs index df6fcbda..9d5bea29 100644 --- a/src/ExchangeSharp/API/Exchanges/OKGroup/OKGroupCommon.cs +++ b/src/ExchangeSharp/API/Exchanges/OKGroup/OKGroupCommon.cs @@ -233,13 +233,13 @@ protected override async Task OnGetTradesWebSocketAsync(Func(symbol, trade)); }); @@ -673,7 +673,7 @@ private async Task ParseTickerV3Async(string symbol, JToken tick */ return await this.ParseTickerAsync(ticker, symbol, askKey: "best_ask", bidKey: "best_bid", lastKey: "last", baseVolumeKey: "base_volume_24h", quoteVolumeKey: "quote_volume_24h", - timestampKey: "timestamp", timestampType: TimestampType.Iso8601); + timestampKey: "timestamp", timestampType: TimestampType.Iso8601UTC); } private Dictionary ParseAmounts(JToken token, Dictionary amounts) diff --git a/src/ExchangeSharp/API/Exchanges/Poloniex/ExchangePoloniexAPI.cs b/src/ExchangeSharp/API/Exchanges/Poloniex/ExchangePoloniexAPI.cs index 587baf3d..7806de56 100644 --- a/src/ExchangeSharp/API/Exchanges/Poloniex/ExchangePoloniexAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Poloniex/ExchangePoloniexAPI.cs @@ -625,7 +625,7 @@ protected override async Task> OnGetRecentTradesAsync //JToken obj = await MakeJsonRequestAsync("/public/trades/" + marketSymbol + "?limit=" + maxRequestLimit + "?sort=DESC"); if(obj.HasValues) { // foreach(JToken token in obj) { - var trade = token.ParseTrade("amount", "rate", "type", "date", TimestampType.Iso8601, "globalTradeID"); + var trade = token.ParseTrade("amount", "rate", "type", "date", TimestampType.Iso8601UTC, "globalTradeID"); trades.Add(trade); } } @@ -640,7 +640,7 @@ protected override async Task OnGetHistoricalTradesAsync(Func token.ParseTrade("amount", "rate", "type", "date", TimestampType.Iso8601, "globalTradeID"), + ParseFunction = (JToken token) => token.ParseTrade("amount", "rate", "type", "date", TimestampType.Iso8601UTC, "globalTradeID"), StartDate = startDate, MarketSymbol = marketSymbol, TimestampFunction = (DateTime dt) => ((long)CryptoUtility.UnixTimestampFromDateTimeSeconds(dt)).ToStringInvariant(), diff --git a/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs b/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs index b59db732..908c4d83 100644 --- a/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs +++ b/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPIExtensions.cs @@ -23,6 +23,7 @@ The above copyright notice and this permission notice shall be included in all c using ExchangeSharp.KuCoin; using Newtonsoft.Json.Linq; using ExchangeSharp.NDAX; +using ExchangeSharp.API.Exchanges.FTX.Models; namespace ExchangeSharp { @@ -516,7 +517,7 @@ internal static async Task ParseTickerAsync(this ExchangeAPI api /// Token /// Amount key /// Price key - /// Type key + /// Type key (aka Side - Buy/Sell) /// Timestamp key /// Timestamp type /// Id key @@ -572,6 +573,15 @@ internal static ExchangeTrade ParseTradeCoinbase(this JToken token, object amoun return trade; } + internal static ExchangeTrade ParseTradeFTX(this JToken token, object amountKey, object priceKey, object typeKey, + object timestampKey, TimestampType timestampType, object idKey, string typeKeyIsBuyValue = "buy") + { + var trade = ParseTradeComponents(token, amountKey, priceKey, typeKey, + timestampKey, timestampType, idKey, typeKeyIsBuyValue); + trade.IsLiquidationOrder = ((bool)token["liquidation"]); + return trade; + } + internal static ExchangeTrade ParseTradeKraken(this JToken token, object amountKey, object priceKey, object typeKey, object timestampKey, TimestampType timestampType, object idKey, string typeKeyIsBuyValue = "buy") { diff --git a/src/ExchangeSharp/ExchangeSharp.csproj b/src/ExchangeSharp/ExchangeSharp.csproj index 3081345f..bb2ec1cf 100644 --- a/src/ExchangeSharp/ExchangeSharp.csproj +++ b/src/ExchangeSharp/ExchangeSharp.csproj @@ -27,7 +27,7 @@ - + diff --git a/src/ExchangeSharp/Utility/CryptoUtility.cs b/src/ExchangeSharp/Utility/CryptoUtility.cs index b6e6f9ed..0869f80f 100644 --- a/src/ExchangeSharp/Utility/CryptoUtility.cs +++ b/src/ExchangeSharp/Utility/CryptoUtility.cs @@ -182,12 +182,12 @@ public static byte[] DecompressDeflate(byte[] bytes) } /// - /// Convert a DateTime and set the kind to UTC using the DateTimeKind property. + /// Convert object to a UTC DateTime /// /// Object to convert /// Default value if no conversion is possible - /// DateTime with DateTimeKind kind or defaultValue if no conversion possible - public static DateTime ToDateTimeInvariant(this object obj, DateTime defaultValue = default) + /// DateTime in UTC or defaultValue if no conversion possible + public static DateTime ToDateTimeInvariant(this object obj, bool isSourceObjUTC = true, DateTime defaultValue = default) { if (obj == null) { @@ -196,10 +196,12 @@ public static DateTime ToDateTimeInvariant(this object obj, DateTime defaultValu JValue? jValue = obj as JValue; if (jValue != null && jValue.Value == null) { + Logger.Error("Failed parsing of datetime - setting to default value"); return defaultValue; } DateTime dt = (DateTime)Convert.ChangeType(jValue == null ? obj : jValue.Value, typeof(DateTime), CultureInfo.InvariantCulture); - return DateTime.SpecifyKind(dt, DateTimeKind.Utc); + if (dt.Kind == DateTimeKind.Utc || isSourceObjUTC) return dt; + else return dt.ToUniversalTime(); // convert to UTC } /// @@ -695,10 +697,13 @@ public static DateTime ParseTimestamp(object value, TimestampType type) switch (type) { - case TimestampType.Iso8601: - return value.ToDateTimeInvariant(); + case TimestampType.Iso8601Local: + return value.ToDateTimeInvariant(false); - case TimestampType.UnixNanoseconds: + case TimestampType.Iso8601UTC: + return value.ToDateTimeInvariant(true); + + case TimestampType.UnixNanoseconds: return UnixTimeStampToDateTimeNanoseconds(value.ConvertInvariant()); case TimestampType.UnixMicroeconds: @@ -1475,9 +1480,14 @@ public enum TimestampType /// UnixSeconds, - /// - /// ISO 8601 - /// - Iso8601 - } + /// + /// ISO 8601 in UTC + /// + Iso8601UTC, + + /// + /// ISO 8601 in local time + /// + Iso8601Local, + } }