diff --git a/src/ExchangeSharp/API/Exchanges/BinanceGroup/BinanceGroupCommon.cs b/src/ExchangeSharp/API/Exchanges/BinanceGroup/BinanceGroupCommon.cs index 17126519..fc80e6d2 100644 --- a/src/ExchangeSharp/API/Exchanges/BinanceGroup/BinanceGroupCommon.cs +++ b/src/ExchangeSharp/API/Exchanges/BinanceGroup/BinanceGroupCommon.cs @@ -332,7 +332,7 @@ protected override async Task OnGetOrderBookAsync(string mark return ExchangeAPIExtensions.ParseOrderBookFromJTokenArrays(obj, sequence: "lastUpdateId", maxCount: maxCount); } - protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null) + protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) { /* [ { "a": 26129, // Aggregate tradeId @@ -345,17 +345,44 @@ protected override async Task OnGetHistoricalTradesAsync(Func token.ParseTrade("q", "p", "m", "T", TimestampType.UnixMilliseconds, "a", "false"), - StartDate = startDate, - MarketSymbol = marketSymbol, - TimestampFunction = (DateTime dt) => ((long)CryptoUtility.UnixTimestampFromDateTimeMilliseconds(dt)).ToStringInvariant(), - Url = "/aggTrades?symbol=[marketSymbol]&startTime={0}&endTime={1}", - }; - await state.ProcessHistoricalTrades(); + Callback = callback, + EndDate = endDate, + ParseFunction = (JToken token) => token.ParseTrade("q", "p", "m", "T", TimestampType.UnixMilliseconds, "a", "false"), + StartDate = startDate, + MarketSymbol = marketSymbol, + TimestampFunction = (DateTime dt) => ((long)CryptoUtility.UnixTimestampFromDateTimeMilliseconds(dt)).ToStringInvariant(), + Url = "/aggTrades?symbol=[marketSymbol]&startTime={0}&endTime={1}", + }; + await state.ProcessHistoricalTrades(); + //} + } + + protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null) + { + List trades = new List(); + //var maxRequestLimit = 1000; //hard coded for now, should add limit as an argument + //https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#compressedaggregate-trades-list + int maxRequestLimit = (limit == null || limit < 1 || limit > 1000) ? 1000 : (int)limit; + + JToken obj = await MakeJsonRequestAsync($"/aggTrades?symbol={marketSymbol}&limit={maxRequestLimit}"); + //JToken obj = await MakeJsonRequestAsync("/public/trades/" + marketSymbol + "?limit=" + maxRequestLimit + "?sort=DESC"); + if(obj.HasValues) { // + foreach(JToken token in obj) { + var trade = token.ParseTrade("q", "p", "m", "T", TimestampType.UnixMilliseconds, "a", "false"); + trades.Add(trade); + } + } + return trades.AsEnumerable().Reverse(); //Descending order (ie newest trades first) + //return trades; } public async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, long startId, long? endId = null) @@ -407,6 +434,56 @@ public async Task OnGetHistoricalTradesAsync(Func, bo } while (callback(trades) && trades.Count > 0); } + public async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, int limit = 100) + { + /* [ { + "a": 26129, // Aggregate tradeId + "p": "0.01633102", // Price + "q": "4.70443515", // Quantity + "f": 27781, // First tradeId + "l": 27781, // Last tradeId + "T": 1498793709153, // Timestamp + "m": true, // Was the buyer the maker? + "M": true // Was the trade the best price match? + } ] */ + + // TODO : Refactor into a common layer once more Exchanges implement this pattern + // https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#compressedaggregate-trades-list + if(limit > 1000) limit = 1000; //Binance max = 1000 + var maxRequestLimit = 1000; + var trades = new List(); + var processedIds = new HashSet(); + marketSymbol = NormalizeMarketSymbol(marketSymbol); + + do { + //if(fromId > endId) + // break; + + trades.Clear(); + //var limit = Math.Min(endId - fromId ?? maxRequestLimit, maxRequestLimit); + var obj = await MakeJsonRequestAsync($"/aggTrades?symbol={marketSymbol}&limit={limit}"); + + foreach(var token in obj) { + var trade = token.ParseTrade("q", "p", "m", "T", TimestampType.UnixMilliseconds, "a", "false"); + //long tradeId = (long)trade.Id.ConvertInvariant(); + //if(tradeId < fromId) + // continue; + //if(tradeId > endId) + // continue; + //if(!processedIds.Add(tradeId)) + // continue; + + trades.Add(trade); + //fromId = tradeId; + } + + //fromId++; + } while(callback(trades) && trades.Count > 0); + } + + + + protected override async Task> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) { /* [ diff --git a/src/ExchangeSharp/API/Exchanges/BitBank/ExchangeBitBankAPI.cs b/src/ExchangeSharp/API/Exchanges/BitBank/ExchangeBitBankAPI.cs index ce30bf80..c081f5bb 100644 --- a/src/ExchangeSharp/API/Exchanges/BitBank/ExchangeBitBankAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/BitBank/ExchangeBitBankAPI.cs @@ -125,7 +125,7 @@ protected override async Task> OnGetCandlesAsync(strin protected override async Task> OnGetAmountsAsync() => await OnGetAmountsAsyncCore("onhand_amount"); - protected override Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null) + protected override Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) { throw new NotImplementedException(); } diff --git a/src/ExchangeSharp/API/Exchanges/Bitfinex/ExchangeBitfinexAPI.cs b/src/ExchangeSharp/API/Exchanges/Bitfinex/ExchangeBitfinexAPI.cs index f000a4e2..fa0255b5 100644 --- a/src/ExchangeSharp/API/Exchanges/Bitfinex/ExchangeBitfinexAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Bitfinex/ExchangeBitfinexAPI.cs @@ -1,4 +1,4 @@ -/* +/* MIT LICENSE Copyright 2017 Digital Ruby, LLC - http://www.digitalruby.com @@ -12,259 +12,225 @@ The above copyright notice and this permission notice shall be included in all c using System.Runtime.InteropServices; -namespace ExchangeSharp -{ - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; - using System; - using System.Collections.Generic; - using System.Globalization; - using System.Linq; - using System.Net; - using System.Text; - using System.Text.RegularExpressions; - using System.Threading.Tasks; - - public sealed partial class ExchangeBitfinexAPI : ExchangeAPI - { - public override string BaseUrl { get; set; } = "https://api.bitfinex.com/v2"; - public override string BaseUrlWebSocket { get; set; } = "wss://api.bitfinex.com/ws"; - - public Dictionary DepositMethodLookup { get; } - - public string BaseUrlV1 { get; set; } = "https://api.bitfinex.com/v1"; - - public ExchangeBitfinexAPI() - { - NonceStyle = NonceStyle.UnixMillisecondsString; - RateLimit = new RateGate(1, TimeSpan.FromSeconds(6.0)); - - // List is from "Withdrawal Types" section https://docs.bitfinex.com/v1/reference#rest-auth-withdrawal - DepositMethodLookup = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - ["AVT"] = "aventus", - ["BCH"] = "bcash", - ["BTC"] = "bitcoin", - ["BTG"] = "bgold", - ["DASH"] = "dash", // TODO: Bitfinex returns "DSH" as the symbol name in the API but on the site it is "DASH". How to normalize? - ["EDO"] = "eidoo", - ["ETC"] = "ethereumc", - ["ETH"] = "ethereum", - ["GNT"] = "golem", - ["LTC"] = "litecoin", - ["MIOTA"] = "iota", - ["OMG"] = "omisego", - ["SAN"] = "santiment", - ["SNT"] = "status", - ["XMR"] = "monero", - ["XRP"] = "ripple", - ["YYW"] = "yoyow", - ["ZEC"] = "zcash", - }; - - MarketSymbolSeparator = string.Empty; - } - - public override string PeriodSecondsToString(int seconds) - { - return base.PeriodSecondsToString(seconds).Replace("d", "D"); // WTF Bitfinex, capital D??? - } - - protected override async Task> OnGetMarketSymbolsAsync() - { - var m = await GetMarketSymbolsMetadataAsync(); - return m.Select(x => NormalizeMarketSymbol(x.MarketSymbol)); - } - - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() - { - var markets = new List(); - JToken allPairs = await MakeJsonRequestAsync("/symbols_details", BaseUrlV1); - Match m; - foreach (JToken pair in allPairs) - { - var market = new ExchangeMarket - { - IsActive = true, - MarketSymbol = pair["pair"].ToStringInvariant(), - MinTradeSize = pair["minimum_order_size"].ConvertInvariant(), - MaxTradeSize = pair["maximum_order_size"].ConvertInvariant(), - MarginEnabled = pair["margin"].ConvertInvariant(false) - }; - var pairPropertyVal = pair["pair"].ToStringUpperInvariant(); - m = Regex.Match(pairPropertyVal, "^(BTC|USD|ETH|GBP|JPY|EUR|EOS)"); - if (m.Success) - { - market.BaseCurrency = m.Value; - market.QuoteCurrency = pairPropertyVal.Substring(m.Length); - } - else - { - m = Regex.Match(pairPropertyVal, "(BTC|USD|ETH|GBP|JPY|EUR|EOS)$"); - if (m.Success) - { - market.BaseCurrency = pairPropertyVal.Substring(0, m.Index); - market.QuoteCurrency = m.Value; - } - else - { - // TODO: Figure out a nicer way to handle newly added pairs - market.BaseCurrency = pairPropertyVal.Substring(0, 3); - market.QuoteCurrency = pairPropertyVal.Substring(3); - } - } - int pricePrecision = pair["price_precision"].ConvertInvariant(); - market.PriceStepSize = (decimal)Math.Pow(0.1, pricePrecision); - markets.Add(market); - } - return markets; - } - - protected override async Task OnGetTickerAsync(string marketSymbol) - { - JToken ticker = await MakeJsonRequestAsync("/ticker/t" + marketSymbol); - return await this.ParseTickerAsync(ticker, marketSymbol, 2, 0, 6, 7); - } - - protected override async Task>> OnGetTickersAsync() - { - List> tickers = new List>(); - IReadOnlyDictionary marketsBySymbol = (await GetMarketSymbolsMetadataAsync()).ToDictionary(market => market.MarketSymbol, market => market); - if (marketsBySymbol != null && marketsBySymbol.Count != 0) - { - StringBuilder symbolString = new StringBuilder(); - foreach (var marketSymbol in marketsBySymbol.Keys) - { - symbolString.Append('t'); - symbolString.Append(marketSymbol.ToUpperInvariant()); - symbolString.Append(','); - } - symbolString.Length--; - JToken token = await MakeJsonRequestAsync("/tickers?symbols=" + symbolString); - DateTime now = CryptoUtility.UtcNow; - foreach (JArray array in token) - { - #region Return Values - //[ - // SYMBOL, - // BID, float Price of last highest bid - // BID_SIZE, float Sum of the 25 highest bid sizes - // ASK, float Price of last lowest ask - // ASK_SIZE, float Sum of the 25 lowest ask sizes - // DAILY_CHANGE, float Amount that the last price has changed since yesterday - // DAILY_CHANGE_PERC, float Amount that the price has changed expressed in percentage terms - // LAST_PRICE, float Price of the last trade - // VOLUME, float Daily volume - // HIGH, float Daily high - // LOW float Daily low - //] - #endregion - var marketSymbol = array[0].ToStringInvariant().Substring(1); - var market = marketsBySymbol[marketSymbol.ToLowerInvariant()]; - tickers.Add(new KeyValuePair(marketSymbol, new ExchangeTicker - { - MarketSymbol = marketSymbol, - Ask = array[3].ConvertInvariant(), - Bid = array[1].ConvertInvariant(), - Last = array[7].ConvertInvariant(), - Volume = new ExchangeVolume - { - QuoteCurrencyVolume = array[8].ConvertInvariant() * array[7].ConvertInvariant(), - QuoteCurrency = market.QuoteCurrency, - BaseCurrencyVolume = array[8].ConvertInvariant(), - BaseCurrency = market.BaseCurrency, - Timestamp = now - } - })); - } - } - return tickers; - } - - protected override async Task OnGetTickersWebSocketAsync(Action>> callback, params string[] marketSymbols) - { - Dictionary channelIdToSymbol = new Dictionary(); - return await ConnectWebSocketAsync(string.Empty, async (_socket, msg) => - { - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - if (token is JArray array) - { - if (array.Count > 10) - { - List> tickerList = new List>(); - if (channelIdToSymbol.TryGetValue(array[0].ConvertInvariant(), out string symbol)) - { - ExchangeTicker ticker = await ParseTickerWebSocketAsync(symbol, array); - if (ticker != null) - { - callback(new KeyValuePair[] { new KeyValuePair(symbol, ticker) }); - } - } - } - } - else if (token["event"].ToStringInvariant() == "subscribed" && token["channel"].ToStringInvariant() == "ticker") - { - // {"event":"subscribed","channel":"ticker","chanId":1,"pair":"BTCUSD"} - int channelId = token["chanId"].ConvertInvariant(); - channelIdToSymbol[channelId] = token["pair"].ToStringInvariant(); - } - }, async (_socket) => - { - marketSymbols = marketSymbols == null || marketSymbols.Length == 0 ? (await GetMarketSymbolsAsync()).ToArray() : marketSymbols; - foreach (var marketSymbol in marketSymbols) - { - await _socket.SendMessageAsync(new { @event = "subscribe", channel = "ticker", pair = marketSymbol }); - } - }); - } - - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) - { - Dictionary channelIdToSymbol = new Dictionary(); - if (marketSymbols == null || marketSymbols.Length == 0) - { +namespace ExchangeSharp { + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Net; + using System.Text; + using System.Text.RegularExpressions; + using System.Threading.Tasks; + + public sealed partial class ExchangeBitfinexAPI :ExchangeAPI { + public override string BaseUrl { get; set; } = "https://api.bitfinex.com/v2"; + public override string BaseUrlWebSocket { get; set; } = "wss://api.bitfinex.com/ws"; + + public Dictionary DepositMethodLookup { get; } + + public string BaseUrlV1 { get; set; } = "https://api.bitfinex.com/v1"; + + public ExchangeBitfinexAPI() + { + NonceStyle = NonceStyle.UnixMillisecondsString; + RateLimit = new RateGate(1, TimeSpan.FromSeconds(6.0)); + + // List is from "Withdrawal Types" section https://docs.bitfinex.com/v1/reference#rest-auth-withdrawal + DepositMethodLookup = new Dictionary(StringComparer.OrdinalIgnoreCase) { + ["AVT"] = "aventus", + ["BCH"] = "bcash", + ["BTC"] = "bitcoin", + ["BTG"] = "bgold", + ["DASH"] = "dash", // TODO: Bitfinex returns "DSH" as the symbol name in the API but on the site it is "DASH". How to normalize? + ["EDO"] = "eidoo", + ["ETC"] = "ethereumc", + ["ETH"] = "ethereum", + ["GNT"] = "golem", + ["LTC"] = "litecoin", + ["MIOTA"] = "iota", + ["OMG"] = "omisego", + ["SAN"] = "santiment", + ["SNT"] = "status", + ["XMR"] = "monero", + ["XRP"] = "ripple", + ["YYW"] = "yoyow", + ["ZEC"] = "zcash", + }; + + MarketSymbolSeparator = string.Empty; + } + + public override string PeriodSecondsToString(int seconds) + { + return base.PeriodSecondsToString(seconds).Replace("d", "D"); // WTF Bitfinex, capital D??? + } + + protected override async Task> OnGetMarketSymbolsAsync() + { + var m = await GetMarketSymbolsMetadataAsync(); + return m.Select(x => NormalizeMarketSymbol(x.MarketSymbol)); + } + + protected internal override async Task> OnGetMarketSymbolsMetadataAsync() + { + var markets = new List(); + JToken allPairs = await MakeJsonRequestAsync("/symbols_details", BaseUrlV1); + Match m; + foreach(JToken pair in allPairs) { + var market = new ExchangeMarket + { + IsActive = true, + MarketSymbol = pair["pair"].ToStringInvariant(), + MinTradeSize = pair["minimum_order_size"].ConvertInvariant(), + MaxTradeSize = pair["maximum_order_size"].ConvertInvariant(), + MarginEnabled = pair["margin"].ConvertInvariant(false) + }; + var pairPropertyVal = pair["pair"].ToStringUpperInvariant(); + m = Regex.Match(pairPropertyVal, "^(BTC|USD|ETH|GBP|JPY|EUR|EOS)"); + if(m.Success) { + market.BaseCurrency = m.Value; + market.QuoteCurrency = pairPropertyVal.Substring(m.Length); + } + else { + m = Regex.Match(pairPropertyVal, "(BTC|USD|ETH|GBP|JPY|EUR|EOS)$"); + if(m.Success) { + market.BaseCurrency = pairPropertyVal.Substring(0, m.Index); + market.QuoteCurrency = m.Value; + } + else { + // TODO: Figure out a nicer way to handle newly added pairs + market.BaseCurrency = pairPropertyVal.Substring(0, 3); + market.QuoteCurrency = pairPropertyVal.Substring(3); + } + } + int pricePrecision = pair["price_precision"].ConvertInvariant(); + market.PriceStepSize = (decimal)Math.Pow(0.1, pricePrecision); + markets.Add(market); + } + return markets; + } + + protected override async Task OnGetTickerAsync(string marketSymbol) + { + JToken ticker = await MakeJsonRequestAsync("/ticker/t" + marketSymbol); + return await this.ParseTickerAsync(ticker, marketSymbol, 2, 0, 6, 7); + } + + protected override async Task>> OnGetTickersAsync() + { + List> tickers = new List>(); + IReadOnlyDictionary marketsBySymbol = (await GetMarketSymbolsMetadataAsync()).ToDictionary(market => market.MarketSymbol, market => market); + if(marketsBySymbol != null && marketsBySymbol.Count != 0) { + StringBuilder symbolString = new StringBuilder(); + foreach(var marketSymbol in marketsBySymbol.Keys) { + symbolString.Append('t'); + symbolString.Append(marketSymbol.ToUpperInvariant()); + symbolString.Append(','); + } + symbolString.Length--; + JToken token = await MakeJsonRequestAsync("/tickers?symbols=" + symbolString); + DateTime now = CryptoUtility.UtcNow; + foreach(JArray array in token) { + #region Return Values + //[ + // SYMBOL, + // BID, float Price of last highest bid + // BID_SIZE, float Sum of the 25 highest bid sizes + // ASK, float Price of last lowest ask + // ASK_SIZE, float Sum of the 25 lowest ask sizes + // DAILY_CHANGE, float Amount that the last price has changed since yesterday + // DAILY_CHANGE_PERC, float Amount that the price has changed expressed in percentage terms + // LAST_PRICE, float Price of the last trade + // VOLUME, float Daily volume + // HIGH, float Daily high + // LOW float Daily low + //] + #endregion + var marketSymbol = array[0].ToStringInvariant().Substring(1); + var market = marketsBySymbol[marketSymbol.ToLowerInvariant()]; + tickers.Add(new KeyValuePair(marketSymbol, new ExchangeTicker { + MarketSymbol = marketSymbol, + Ask = array[3].ConvertInvariant(), + Bid = array[1].ConvertInvariant(), + Last = array[7].ConvertInvariant(), + Volume = new ExchangeVolume { + QuoteCurrencyVolume = array[8].ConvertInvariant() * array[7].ConvertInvariant(), + QuoteCurrency = market.QuoteCurrency, + BaseCurrencyVolume = array[8].ConvertInvariant(), + BaseCurrency = market.BaseCurrency, + Timestamp = now + } + })); + } + } + return tickers; + } + + protected override async Task OnGetTickersWebSocketAsync(Action>> callback, params string[] marketSymbols) + { + Dictionary channelIdToSymbol = new Dictionary(); + return await ConnectWebSocketAsync(string.Empty, async (_socket, msg) => { + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + if(token is JArray array) { + if(array.Count > 10) { + List> tickerList = new List>(); + if(channelIdToSymbol.TryGetValue(array[0].ConvertInvariant(), out string symbol)) { + ExchangeTicker ticker = await ParseTickerWebSocketAsync(symbol, array); + if(ticker != null) { + callback(new KeyValuePair[] { new KeyValuePair(symbol, ticker) }); + } + } + } + } + else if(token["event"].ToStringInvariant() == "subscribed" && token["channel"].ToStringInvariant() == "ticker") { + // {"event":"subscribed","channel":"ticker","chanId":1,"pair":"BTCUSD"} + int channelId = token["chanId"].ConvertInvariant(); + channelIdToSymbol[channelId] = token["pair"].ToStringInvariant(); + } + }, async (_socket) => { + marketSymbols = marketSymbols == null || marketSymbols.Length == 0 ? (await GetMarketSymbolsAsync()).ToArray() : marketSymbols; + foreach(var marketSymbol in marketSymbols) { + await _socket.SendMessageAsync(new { @event = "subscribe", channel = "ticker", pair = marketSymbol }); + } + }); + } + + protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + { + Dictionary channelIdToSymbol = new Dictionary(); + if(marketSymbols == null || marketSymbols.Length == 0) { marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); } - return await ConnectWebSocketAsync("/2", async (_socket, msg) => //use websocket V2 (beta, but millisecond timestamp) - { - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - if (token is JArray array) - { - if (token[1].ToStringInvariant() == "hb") - { - // heartbeat - } - else if (token.Last.Last.HasValues == false) - { - //[29654, "tu", [270343572, 1532012917722, -0.003, 7465.636738]] "te"=temp/intention to execute "tu"=confirmed and ID is definitive - //chan id, -- , [ID , timestamp , amount, price ]] - if (channelIdToSymbol.TryGetValue(array[0].ConvertInvariant(), out string symbol)) - { - if (token[1].ToStringInvariant() == "tu") - { - ExchangeTrade trade = ParseTradeWebSocket(token.Last); - if (trade != null) - { - await callback(new KeyValuePair(symbol, trade)); - } - } - } - } - else - { + return await ConnectWebSocketAsync("/2", async (_socket, msg) => //use websocket V2 (beta, but millisecond timestamp) + { + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + if(token is JArray array) { + if(token[1].ToStringInvariant() == "hb") { + // heartbeat + } + else if(token.Last.Last.HasValues == false) { + //[29654, "tu", [270343572, 1532012917722, -0.003, 7465.636738]] "te"=temp/intention to execute "tu"=confirmed and ID is definitive + //chan id, -- , [ID , timestamp , amount, price ]] + if(channelIdToSymbol.TryGetValue(array[0].ConvertInvariant(), out string symbol)) { + if(token[1].ToStringInvariant() == "tu") { + ExchangeTrade trade = ParseTradeWebSocket(token.Last); + if(trade != null) { + await callback(new KeyValuePair(symbol, trade)); + } + } + } + } + else { //parse snapshot here if needed - if (channelIdToSymbol.TryGetValue(array[0].ConvertInvariant(), out string symbol)) - { - if (array[1] is JArray subarray) - { - for (int i = 0; i < subarray.Count - 1; i++) - { + if(channelIdToSymbol.TryGetValue(array[0].ConvertInvariant(), out string symbol)) { + if(array[1] is JArray subarray) { + for(int i = 0; i < subarray.Count - 1; i++) { ExchangeTrade trade = ParseTradeWebSocket(subarray[i]); - if (trade != null) - { + if(trade != null) { trade.Flags |= ExchangeTradeFlags.IsFromSnapshot; - if (i == subarray.Count - 1) - { + if(i == subarray.Count - 1) { trade.Flags |= ExchangeTradeFlags.IsLastFromSnapshot; } await callback(new KeyValuePair(symbol, trade)); @@ -274,619 +240,571 @@ protected override async Task OnGetTradesWebSocketAsync(Func(); - channelIdToSymbol[channelId] = token["pair"].ToStringInvariant(); - } - }, async (_socket) => - { - foreach (var marketSymbol in marketSymbols) - { - await _socket.SendMessageAsync(new { @event = "subscribe", channel = "trades", symbol = marketSymbol }); - } - }); - } - - private ExchangeTrade ParseTradeWebSocket(JToken token) - { - decimal amount = token[2].ConvertInvariant(); - return new ExchangeTrade - { - Id = token[0].ToStringInvariant(), - Timestamp = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(token[1].ConvertInvariant()), - Amount = Math.Abs(amount), - IsBuy = amount > 0, - Price = token[3].ConvertInvariant() - }; - } - - protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) - { - ExchangeOrderBook orders = new ExchangeOrderBook(); - decimal[][] books = await MakeJsonRequestAsync("/book/t" + marketSymbol + - "/P0?limit_bids=" + maxCount.ToStringInvariant() + "limit_asks=" + maxCount.ToStringInvariant()); - foreach (decimal[] book in books) - { - if (book[2] > 0m) - { - orders.Bids[book[0]] = new ExchangeOrderPrice { Amount = book[2], Price = book[0] }; - } - else - { - orders.Asks[book[0]] = new ExchangeOrderPrice { Amount = -book[2], Price = book[0] }; - } - } - return orders; - } - - protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null) - { - const int maxCount = 100; - string baseUrl = "/trades/t" + marketSymbol + "/hist?sort=" + (startDate == null ? "-1" : "1") + "&limit=" + maxCount; - string url; - List trades = new List(); - decimal[][] tradeChunk; - while (true) - { - url = baseUrl; - if (startDate != null) - { - url += "&start=" + (long)CryptoUtility.UnixTimestampFromDateTimeMilliseconds(startDate.Value); - } - tradeChunk = await MakeJsonRequestAsync(url); - if (tradeChunk == null || tradeChunk.Length == 0) - { - break; - } - if (startDate != null) - { - startDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds((double)tradeChunk[tradeChunk.Length - 1][1]); - } - foreach (decimal[] tradeChunkPiece in tradeChunk) - { - trades.Add(new ExchangeTrade { Amount = Math.Abs(tradeChunkPiece[2]), IsBuy = tradeChunkPiece[2] > 0m, Price = tradeChunkPiece[3], Timestamp = CryptoUtility.UnixTimeStampToDateTimeMilliseconds((double)tradeChunkPiece[1]), Id = tradeChunkPiece[0].ToStringInvariant() }); - } - trades.Sort((t1, t2) => t1.Timestamp.CompareTo(t2.Timestamp)); - if (!callback(trades)) - { - break; - } - trades.Clear(); - if (tradeChunk.Length < maxCount || startDate == null) - { - break; - } - await Task.Delay(5000); - } - } - - protected override async Task> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) - { - // https://api.bitfinex.com/v2/candles/trade:1d:btcusd/hist?start=ms_start&end=ms_end - List candles = new List(); - string periodString = PeriodSecondsToString(periodSeconds); - string url = "/candles/trade:" + periodString + ":t" + marketSymbol + "/hist?sort=1"; - if (startDate != null || endDate != null) - { - endDate = endDate ?? CryptoUtility.UtcNow; - startDate = startDate ?? endDate.Value.Subtract(TimeSpan.FromDays(1.0)); - url += "&start=" + ((long)startDate.Value.UnixTimestampFromDateTimeMilliseconds()).ToStringInvariant(); - url += "&end=" + ((long)endDate.Value.UnixTimestampFromDateTimeMilliseconds()).ToStringInvariant(); - } - if (limit != null) - { - url += "&limit=" + (limit.Value.ToStringInvariant()); - } - JToken token = await MakeJsonRequestAsync(url); - - /* MTS, OPEN, CLOSE, HIGH, LOW, VOL */ - foreach (JToken candle in token) - { - candles.Add(this.ParseCandle(candle, marketSymbol, periodSeconds, 1, 3, 4, 2, 0, TimestampType.UnixMilliseconds, 5)); - } - - return candles; - } - - protected override async Task> OnGetAmountsAsync() - { - return await OnGetAmountsAsync("exchange"); - } - - public async Task> OnGetAmountsAsync(string type) - { - Dictionary lookup = new Dictionary(StringComparer.OrdinalIgnoreCase); - JArray obj = await MakeJsonRequestAsync("/balances", BaseUrlV1, await GetNoncePayloadAsync()); - foreach (JToken token in obj) - { - if (token["type"].ToStringInvariant() == type) - { - decimal amount = token["amount"].ConvertInvariant(); - if (amount > 0m) - { - lookup[token["currency"].ToStringInvariant()] = amount; - } - } - } - return lookup; - } - - protected override async Task> OnGetMarginAmountsAvailableToTradeAsync( - bool includeZeroBalances = false) - { - return await OnGetAmountsAsync("trading"); - } - - protected override async Task> OnGetAmountsAvailableToTradeAsync() - { - Dictionary lookup = new Dictionary(StringComparer.OrdinalIgnoreCase); - JArray obj = await MakeJsonRequestAsync("/balances", BaseUrlV1, await GetNoncePayloadAsync()); - foreach (JToken token in obj) - { - if (token["type"].ToStringInvariant() == "exchange") - { - decimal amount = token["available"].ConvertInvariant(); - if (amount > 0m) - { - lookup[token["currency"].ToStringInvariant()] = amount; - } - } - } - return lookup; - } - - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) - { - string marketSymbol = NormalizeMarketSymbolV1(order.MarketSymbol); - Dictionary payload = await GetNoncePayloadAsync(); - payload["symbol"] = marketSymbol; - payload["amount"] = (await ClampOrderQuantity(marketSymbol, order.Amount)).ToStringInvariant(); - payload["side"] = (order.IsBuy ? "buy" : "sell"); - - if (order.IsMargin) - { - payload["type"] = order.OrderType == OrderType.Market ? "market" : "limit"; - } - else - { - payload["type"] = order.OrderType == OrderType.Market ? "exchange market" : "exchange limit"; - } - - if (order.OrderType != OrderType.Market) - { - payload["price"] = (await ClampOrderPrice(marketSymbol, order.Price)).ToStringInvariant(); - } - else - { - payload["price"] = "1"; - } - order.ExtraParameters.CopyTo(payload); - JToken obj = await MakeJsonRequestAsync("/order/new", BaseUrlV1, payload); - return ParseOrder(obj); - } - - protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null) - { - if (string.IsNullOrWhiteSpace(orderId)) - { - return null; - } - - Dictionary payload = await GetNoncePayloadAsync(); - payload["order_id"] = orderId.ConvertInvariant(); - JToken result = await MakeJsonRequestAsync("/order/status", BaseUrlV1, payload); - return ParseOrder(result); - } - - protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) - { - return await GetOrderDetailsInternalAsync("/orders", marketSymbol); - } - - protected override async Task> OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) - { - if (string.IsNullOrWhiteSpace(marketSymbol)) - { - // HACK: Bitfinex does not provide a way to get all historical order details beyond a few days in one call, so we have to - // get the historical details one by one for each symbol. - var symbols = (await GetMarketSymbolsAsync()).Where(s => s.IndexOf("usd", StringComparison.OrdinalIgnoreCase) < 0 && s.IndexOf("btc", StringComparison.OrdinalIgnoreCase) >= 0); - return await GetOrderDetailsInternalV1(symbols, afterDate); - } - - // retrieve orders for the one symbol - return await GetOrderDetailsInternalV1(new string[] { marketSymbol }, afterDate); - } - - protected override Task OnGetCompletedOrderDetailsWebSocketAsync(Action callback) - { - return ConnectWebSocketAsync(string.Empty, (_socket, msg) => - { - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - if (token[1].ToStringInvariant() == "hb") - { + else if(token["event"].ToStringInvariant() == "subscribed" && token["channel"].ToStringInvariant() == "trades") { + //{"event": "subscribed","channel": "trades","chanId": 29654,"symbol": "tBTCUSD","pair": "BTCUSD"} + int channelId = token["chanId"].ConvertInvariant(); + channelIdToSymbol[channelId] = token["pair"].ToStringInvariant(); + } + }, async (_socket) => { + foreach(var marketSymbol in marketSymbols) { + await _socket.SendMessageAsync(new { @event = "subscribe", channel = "trades", symbol = marketSymbol }); + } + }); + } + + private ExchangeTrade ParseTradeWebSocket(JToken token) + { + decimal amount = token[2].ConvertInvariant(); + return new ExchangeTrade { + Id = token[0].ToStringInvariant(), + Timestamp = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(token[1].ConvertInvariant()), + Amount = Math.Abs(amount), + IsBuy = amount > 0, + Price = token[3].ConvertInvariant() + }; + } + + protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) + { + ExchangeOrderBook orders = new ExchangeOrderBook(); + decimal[][] books = await MakeJsonRequestAsync("/book/t" + marketSymbol + + "/P0?limit_bids=" + maxCount.ToStringInvariant() + "limit_asks=" + maxCount.ToStringInvariant()); + foreach(decimal[] book in books) { + if(book[2] > 0m) { + orders.Bids[book[0]] = new ExchangeOrderPrice { Amount = book[2], Price = book[0] }; + } + else { + orders.Asks[book[0]] = new ExchangeOrderPrice { Amount = -book[2], Price = book[0] }; + } + } + return orders; + } + + + + protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null) + { + List trades = new List(); + decimal[][] tradeChunk; + + //https://docs.bitfinex.com/reference#rest-public-trades note bitfinex max limit = 10000 + int requestLimit = (limit == null || limit < 1 || limit > 10000) ? 10000 : (int)limit; + string url = "/trades/t" + marketSymbol + "/hist?sort=" + "-1" + "&limit=" + requestLimit; + + tradeChunk = await MakeJsonRequestAsync(url); + if(tradeChunk != null || tradeChunk.Length > 0) { + //tradeChunk = tradeChunk.Reverse(); + foreach(decimal[] tradeChunkPiece in tradeChunk) { + trades.Add(new ExchangeTrade { Amount = Math.Abs(tradeChunkPiece[2]), IsBuy = tradeChunkPiece[2] > 0m, Price = tradeChunkPiece[3], Timestamp = CryptoUtility.UnixTimeStampToDateTimeMilliseconds((double)tradeChunkPiece[1]), Id = tradeChunkPiece[0].ToStringInvariant() }); + } + } + return trades; + } + + + protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + { + const int maxCount = 100; + string baseUrl = "/trades/t" + marketSymbol + "/hist?sort=" + (startDate == null ? "-1" : "1") + "&limit=" + maxCount; + string url; + List trades = new List(); + decimal[][] tradeChunk; + while(true) { + url = baseUrl; + if(startDate != null) { + url += "&start=" + (long)CryptoUtility.UnixTimestampFromDateTimeMilliseconds(startDate.Value); + } + tradeChunk = await MakeJsonRequestAsync(url); + if(tradeChunk == null || tradeChunk.Length == 0) { + break; + } + if(startDate != null) { + startDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds((double)tradeChunk[tradeChunk.Length - 1][1]); + } + foreach(decimal[] tradeChunkPiece in tradeChunk) { + trades.Add(new ExchangeTrade { Amount = Math.Abs(tradeChunkPiece[2]), IsBuy = tradeChunkPiece[2] > 0m, Price = tradeChunkPiece[3], Timestamp = CryptoUtility.UnixTimeStampToDateTimeMilliseconds((double)tradeChunkPiece[1]), Id = tradeChunkPiece[0].ToStringInvariant() }); + } + trades.Sort((t1, t2) => t1.Timestamp.CompareTo(t2.Timestamp)); + if(!callback(trades)) { + break; + } + trades.Clear(); + if(tradeChunk.Length < maxCount || startDate == null) { + break; + } + await Task.Delay(5000); + } + } + + protected override async Task> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + { + // https://api.bitfinex.com/v2/candles/trade:1d:btcusd/hist?start=ms_start&end=ms_end + List candles = new List(); + string periodString = PeriodSecondsToString(periodSeconds); + string url = "/candles/trade:" + periodString + ":t" + marketSymbol + "/hist?sort=1"; + if(startDate != null || endDate != null) { + endDate = endDate ?? CryptoUtility.UtcNow; + startDate = startDate ?? endDate.Value.Subtract(TimeSpan.FromDays(1.0)); + url += "&start=" + ((long)startDate.Value.UnixTimestampFromDateTimeMilliseconds()).ToStringInvariant(); + url += "&end=" + ((long)endDate.Value.UnixTimestampFromDateTimeMilliseconds()).ToStringInvariant(); + } + if(limit != null) { + url += "&limit=" + (limit.Value.ToStringInvariant()); + } + JToken token = await MakeJsonRequestAsync(url); + + /* MTS, OPEN, CLOSE, HIGH, LOW, VOL */ + foreach(JToken candle in token) { + candles.Add(this.ParseCandle(candle, marketSymbol, periodSeconds, 1, 3, 4, 2, 0, TimestampType.UnixMilliseconds, 5)); + } + + return candles; + } + + protected override async Task> OnGetAmountsAsync() + { + return await OnGetAmountsAsync("exchange"); + } + + public async Task> OnGetAmountsAsync(string type) + { + Dictionary lookup = new Dictionary(StringComparer.OrdinalIgnoreCase); + JArray obj = await MakeJsonRequestAsync("/balances", BaseUrlV1, await GetNoncePayloadAsync()); + foreach(JToken token in obj) { + if(token["type"].ToStringInvariant() == type) { + decimal amount = token["amount"].ConvertInvariant(); + if(amount > 0m) { + lookup[token["currency"].ToStringInvariant()] = amount; + } + } + } + return lookup; + } + + protected override async Task> OnGetMarginAmountsAvailableToTradeAsync( + bool includeZeroBalances = false) + { + return await OnGetAmountsAsync("trading"); + } + + protected override async Task> OnGetAmountsAvailableToTradeAsync() + { + Dictionary lookup = new Dictionary(StringComparer.OrdinalIgnoreCase); + JArray obj = await MakeJsonRequestAsync("/balances", BaseUrlV1, await GetNoncePayloadAsync()); + foreach(JToken token in obj) { + if(token["type"].ToStringInvariant() == "exchange") { + decimal amount = token["available"].ConvertInvariant(); + if(amount > 0m) { + lookup[token["currency"].ToStringInvariant()] = amount; + } + } + } + return lookup; + } + + protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + { + string marketSymbol = NormalizeMarketSymbolV1(order.MarketSymbol); + Dictionary payload = await GetNoncePayloadAsync(); + payload["symbol"] = marketSymbol; + payload["amount"] = (await ClampOrderQuantity(marketSymbol, order.Amount)).ToStringInvariant(); + payload["side"] = (order.IsBuy ? "buy" : "sell"); + + if(order.IsMargin) { + payload["type"] = order.OrderType == OrderType.Market ? "market" : "limit"; + } + else { + payload["type"] = order.OrderType == OrderType.Market ? "exchange market" : "exchange limit"; + } + + if(order.OrderType != OrderType.Market) { + payload["price"] = (await ClampOrderPrice(marketSymbol, order.Price)).ToStringInvariant(); + } + else { + payload["price"] = "1"; + } + order.ExtraParameters.CopyTo(payload); + JToken obj = await MakeJsonRequestAsync("/order/new", BaseUrlV1, payload); + return ParseOrder(obj); + } + + protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null) + { + if(string.IsNullOrWhiteSpace(orderId)) { + return null; + } + + Dictionary payload = await GetNoncePayloadAsync(); + payload["order_id"] = orderId.ConvertInvariant(); + JToken result = await MakeJsonRequestAsync("/order/status", BaseUrlV1, payload); + return ParseOrder(result); + } + + protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) + { + return await GetOrderDetailsInternalAsync("/orders", marketSymbol); + } + + protected override async Task> OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) + { + if(string.IsNullOrWhiteSpace(marketSymbol)) { + // HACK: Bitfinex does not provide a way to get all historical order details beyond a few days in one call, so we have to + // get the historical details one by one for each symbol. + var symbols = (await GetMarketSymbolsAsync()).Where(s => s.IndexOf("usd", StringComparison.OrdinalIgnoreCase) < 0 && s.IndexOf("btc", StringComparison.OrdinalIgnoreCase) >= 0); + return await GetOrderDetailsInternalV1(symbols, afterDate); + } + + // retrieve orders for the one symbol + return await GetOrderDetailsInternalV1(new string[] { marketSymbol }, afterDate); + } + + protected override Task OnGetCompletedOrderDetailsWebSocketAsync(Action callback) + { + return ConnectWebSocketAsync(string.Empty, (_socket, msg) => { + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + if(token[1].ToStringInvariant() == "hb") { // heartbeat } - else if (token is JArray array && array.Count > 1 && array[2] is JArray && array[1].ToStringInvariant() == "os") - { - foreach (JToken orderToken in array[2]) - { - callback.Invoke(ParseOrderWebSocket(orderToken)); - } - } - return Task.CompletedTask; - }, async (_socket) => - { - object nonce = await GenerateNonceAsync(); - string authPayload = "AUTH" + nonce; - string signature = CryptoUtility.SHA384Sign(authPayload, PrivateApiKey.ToUnsecureString()); - Dictionary payload = new Dictionary - { - { "apiKey", PublicApiKey.ToUnsecureString() }, - { "event", "auth" }, - { "authPayload", authPayload }, - { "authSig", signature } - }; - string payloadJSON = CryptoUtility.GetJsonForPayload(payload); - await _socket.SendMessageAsync(payloadJSON); - }); - } - - protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null) - { - Dictionary payload = await GetNoncePayloadAsync(); - payload["order_id"] = orderId.ConvertInvariant(); - var token= await MakeJsonRequestAsync("/order/cancel", BaseUrlV1, payload); - } - - protected override async Task OnGetDepositAddressAsync(string currency, bool forceRegenerate = false) - { - if (string.IsNullOrWhiteSpace(currency)) - { - throw new ArgumentNullException(nameof(currency)); - } - - // IOTA addresses should never be used more than once - if (currency.Equals("MIOTA", StringComparison.OrdinalIgnoreCase)) - { - forceRegenerate = true; - } - - // symbol needs to be translated to full name of coin: bitcoin/litecoin/ethereum - if (!DepositMethodLookup.TryGetValue(currency, out string fullName)) - { - fullName = currency.ToLowerInvariant(); - } - - Dictionary payload = await GetNoncePayloadAsync(); - payload["method"] = fullName; - payload["wallet_name"] = "exchange"; - payload["renew"] = forceRegenerate ? 1 : 0; - - JToken result = await MakeJsonRequestAsync("/deposit/new", BaseUrlV1, payload, "POST"); - var details = new ExchangeDepositDetails - { - Currency = result["currency"].ToStringInvariant(), - }; - if (result["address_pool"] != null) - { - details.Address = result["address_pool"].ToStringInvariant(); - details.AddressTag = result["address"].ToStringLowerInvariant(); - } - else - { - details.Address = result["address"].ToStringInvariant(); - } - - return details; - } - - /// Gets the deposit history for a symbol - /// The symbol to check. Must be specified. - /// Collection of ExchangeCoinTransfers - protected override async Task> OnGetDepositHistoryAsync(string currency) - { - if (string.IsNullOrWhiteSpace(currency)) - { - throw new ArgumentNullException(nameof(currency)); - } - - Dictionary payload = await GetNoncePayloadAsync(); - payload["currency"] = currency; - - JToken result = await MakeJsonRequestAsync("/history/movements", BaseUrlV1, payload, "POST"); - var transactions = new List(); - foreach (JToken token in result) - { - if (!string.Equals(token["type"].ToStringUpperInvariant(), "DEPOSIT")) - { - continue; - } - - var transaction = new ExchangeTransaction - { - PaymentId = token["id"].ToStringInvariant(), - BlockchainTxId = token["txid"].ToStringInvariant(), - Currency = token["currency"].ToStringUpperInvariant(), - Notes = token["description"].ToStringInvariant() + ", method: " + token["method"].ToStringInvariant(), - Amount = token["amount"].ConvertInvariant(), - Address = token["address"].ToStringInvariant() - }; - - string status = token["status"].ToStringUpperInvariant(); - switch (status) - { - case "COMPLETED": - transaction.Status = TransactionStatus.Complete; - break; - case "UNCONFIRMED": - transaction.Status = TransactionStatus.Processing; - break; - default: - transaction.Status = TransactionStatus.Unknown; - transaction.Notes += ", Unknown transaction status " + status; - break; - } - - double unixTimestamp = token["timestamp"].ConvertInvariant(); - transaction.Timestamp = unixTimestamp.UnixTimeStampToDateTimeSeconds(); - transaction.TxFee = token["fee"].ConvertInvariant(); - - transactions.Add(transaction); - } - - return transactions; - } - - /// Gets the deposit history for a symbol - /// The symbol to check. Must be specified. - /// Collection of ExchangeCoinTransfers - protected override async Task> OnGetWithdrawHistoryAsync(string currency) - { - if (string.IsNullOrWhiteSpace(currency)) - { - throw new ArgumentNullException(nameof(currency)); - } - - Dictionary payload = await GetNoncePayloadAsync(); - payload["currency"] = currency; - - JToken result = await MakeJsonRequestAsync("/history/movements", BaseUrlV1, payload, "POST"); - var transactions = new List(); - foreach (JToken token in result) - { - if (!string.Equals(token["type"].ToStringUpperInvariant(), "WITHDRAWAL")) - { - continue; - } - - var transaction = new ExchangeTransaction - { - PaymentId = token["id"].ToStringInvariant(), - BlockchainTxId = token["txid"].ToStringInvariant(), - Currency = token["currency"].ToStringUpperInvariant(), - Notes = token["description"].ToStringInvariant() + ", method: " + token["method"].ToStringInvariant(), - Amount = token["amount"].ConvertInvariant(), - Address = token["address"].ToStringInvariant() - }; - - string status = token["status"].ToStringUpperInvariant(); - switch (status) - { - case "COMPLETED": - transaction.Status = TransactionStatus.Complete; - break; - case "UNCONFIRMED": - transaction.Status = TransactionStatus.Processing; - break; - default: - transaction.Status = TransactionStatus.Unknown; - transaction.Notes += ", Unknown transaction status " + status; - break; - } - - double unixTimestamp = token["timestamp"].ConvertInvariant(); - transaction.Timestamp = unixTimestamp.UnixTimeStampToDateTimeSeconds(); - transaction.TxFee = token["fee"].ConvertInvariant(); - - transactions.Add(transaction); - } - - return transactions; - } - - /// A withdrawal request. - /// The withdrawal request. - /// NOTE: Network fee must be subtracted from amount or withdrawal will fail - /// The withdrawal response - protected override async Task OnWithdrawAsync(ExchangeWithdrawalRequest withdrawalRequest) - { - // symbol needs to be translated to full name of coin: bitcoin/litecoin/ethereum - if (!DepositMethodLookup.TryGetValue(withdrawalRequest.Currency, out string fullName)) - { - fullName = withdrawalRequest.Currency.ToLowerInvariant(); - } - - // Bitfinex adds the fee on top of what you request to withdrawal - if (withdrawalRequest.TakeFeeFromAmount) - { - Dictionary fees = await GetWithdrawalFeesAsync(); - if (fees.TryGetValue(withdrawalRequest.Currency, out decimal feeAmt)) - { - withdrawalRequest.Amount -= feeAmt; - } - } - - Dictionary payload = await GetNoncePayloadAsync(); - payload["withdraw_type"] = fullName; - payload["walletselected"] = "exchange"; - payload["amount"] = withdrawalRequest.Amount.ToString(CultureInfo.InvariantCulture); // API throws if this is a number not a string - payload["address"] = withdrawalRequest.Address; - - if (!string.IsNullOrWhiteSpace(withdrawalRequest.AddressTag)) - { - payload["payment_id"] = withdrawalRequest.AddressTag; - } - - if (!string.IsNullOrWhiteSpace(withdrawalRequest.Description)) - { - payload["account_name"] = withdrawalRequest.Description; - } - - JToken result = await MakeJsonRequestAsync("/withdraw", BaseUrlV1, payload, "POST"); - - var resp = new ExchangeWithdrawalResponse(); - if (!string.Equals(result[0]["status"].ToStringInvariant(), "success", StringComparison.OrdinalIgnoreCase)) - { - resp.Success = false; - } - - resp.Id = result[0]["withdrawal_id"].ToStringInvariant(); - resp.Message = result[0]["message"].ToStringInvariant(); - return resp; - } - - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) - { - if (CanMakeAuthenticatedRequest(payload)) - { - request.Method = "POST"; - request.AddHeader("content-type", "application/json"); - request.AddHeader("accept", "application/json"); - if (request.RequestUri.AbsolutePath.StartsWith("/v2")) - { - string nonce = payload["nonce"].ToStringInvariant(); - payload.Remove("nonce"); - string json = JsonConvert.SerializeObject(payload); - string toSign = "/api" + request.RequestUri.PathAndQuery + nonce + json; - string hexSha384 = CryptoUtility.SHA384Sign(toSign, PrivateApiKey.ToUnsecureString()); - request.AddHeader("bfx-nonce", nonce); - request.AddHeader("bfx-apikey", PublicApiKey.ToUnsecureString()); - request.AddHeader("bfx-signature", hexSha384); - await CryptoUtility.WriteToRequestAsync(request, json); - } - else - { - // bitfinex v1 doesn't put the payload in the post body it puts it in as a http header, so no need to write to request stream - payload.Add("request", request.RequestUri.AbsolutePath); - string json = JsonConvert.SerializeObject(payload); - string json64 = System.Convert.ToBase64String(json.ToBytesUTF8()); - string hexSha384 = CryptoUtility.SHA384Sign(json64, PrivateApiKey.ToUnsecureString()); - request.AddHeader("X-BFX-PAYLOAD", json64); - request.AddHeader("X-BFX-SIGNATURE", hexSha384); - request.AddHeader("X-BFX-APIKEY", PublicApiKey.ToUnsecureString()); - } - } - } - - protected override Task> OnGetCurrenciesAsync() - { - throw new NotSupportedException("Bitfinex does not provide data about its currencies via the API"); - } - - private string NormalizeMarketSymbolV1(string marketSymbol) - { - return (marketSymbol ?? string.Empty).Replace("-", string.Empty).ToLowerInvariant(); - } - - private async Task> GetOrderDetailsInternalV2(string url, string marketSymbol = null) - { - Dictionary payload = await GetNoncePayloadAsync(); - payload["limit"] = 250; - payload["start"] = CryptoUtility.UtcNow.Subtract(TimeSpan.FromDays(365.0)).UnixTimestampFromDateTimeMilliseconds(); - payload["end"] = CryptoUtility.UtcNow.UnixTimestampFromDateTimeMilliseconds(); - JToken result = await MakeJsonRequestAsync(url, null, payload); - Dictionary> trades = new Dictionary>(StringComparer.OrdinalIgnoreCase); - if (result is JArray array) - { - foreach (JToken token in array) - { - if (string.IsNullOrWhiteSpace(marketSymbol) || token[1].ToStringInvariant() == "t" + marketSymbol) - { - string lookup = token[1].ToStringInvariant().Substring(1).ToLowerInvariant(); - if (!trades.TryGetValue(lookup, out List tradeList)) - { - tradeList = trades[lookup] = new List(); - } - tradeList.Add(token); - } - } - } - return ParseOrderV2(trades); - } - - private async Task> GetOrderDetailsInternalAsync(string url, string marketSymbol = null) - { - List orders = new List(); - marketSymbol = NormalizeMarketSymbolV1(marketSymbol); - JToken result = await MakeJsonRequestAsync(url, BaseUrlV1, await GetNoncePayloadAsync()); - if (result is JArray array) - { - foreach (JToken token in array) - { - if (marketSymbol == null || token["symbol"].ToStringInvariant() == marketSymbol) - { - orders.Add(ParseOrder(token)); - } - } - } - return orders; - } - - private async Task> GetOrderDetailsInternalV1(IEnumerable marketSymbols, DateTime? afterDate) - { - Dictionary orders = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (string marketSymbol in marketSymbols) - { - string normalizedSymbol = NormalizeMarketSymbolV1(marketSymbol); - Dictionary payload = await GetNoncePayloadAsync(); - payload["symbol"] = normalizedSymbol; - payload["limit_trades"] = 250; - if (afterDate != null) - { - payload["timestamp"] = afterDate.Value.UnixTimestampFromDateTimeSeconds().ToStringInvariant(); - payload["until"] = CryptoUtility.UtcNow.UnixTimestampFromDateTimeSeconds().ToStringInvariant(); - } - JToken token = await MakeJsonRequestAsync("/mytrades", BaseUrlV1, payload); - foreach (JToken trade in token) - { - ExchangeOrderResult subOrder = ParseTrade(trade, normalizedSymbol); - lock (orders) - { - if (orders.TryGetValue(subOrder.OrderId, out ExchangeOrderResult baseOrder)) - { - baseOrder.AppendOrderWithOrder(subOrder); - } - else - { - orders[subOrder.OrderId] = subOrder; - } - } - } - } - return orders.Values.OrderByDescending(o => o.OrderDate); - } - - private ExchangeOrderResult ParseOrder(JToken order) - { - decimal amount = order["original_amount"].ConvertInvariant(); - decimal amountFilled = order["executed_amount"].ConvertInvariant(); - decimal price = order["price"].ConvertInvariant(); - return new ExchangeOrderResult - { - Amount = amount, - AmountFilled = amountFilled, - Price = price, - AveragePrice = order["avg_execution_price"].ConvertInvariant(order["price"].ConvertInvariant()), - Message = string.Empty, - OrderId = order["id"].ToStringInvariant(), - Result = (amountFilled == amount ? ExchangeAPIOrderResult.Filled : (amountFilled == 0 ? ExchangeAPIOrderResult.Pending : ExchangeAPIOrderResult.FilledPartially)), - OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(order["timestamp"].ConvertInvariant()), - MarketSymbol = order["symbol"].ToStringInvariant(), - IsBuy = order["side"].ToStringInvariant() == "buy" - }; - } + else if(token is JArray array && array.Count > 1 && array[2] is JArray && array[1].ToStringInvariant() == "os") { + foreach(JToken orderToken in array[2]) { + callback.Invoke(ParseOrderWebSocket(orderToken)); + } + } + return Task.CompletedTask; + }, async (_socket) => { + object nonce = await GenerateNonceAsync(); + string authPayload = "AUTH" + nonce; + string signature = CryptoUtility.SHA384Sign(authPayload, PrivateApiKey.ToUnsecureString()); + Dictionary payload = new Dictionary + { + { "apiKey", PublicApiKey.ToUnsecureString() }, + { "event", "auth" }, + { "authPayload", authPayload }, + { "authSig", signature } + }; + string payloadJSON = CryptoUtility.GetJsonForPayload(payload); + await _socket.SendMessageAsync(payloadJSON); + }); + } + + protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null) + { + Dictionary payload = await GetNoncePayloadAsync(); + payload["order_id"] = orderId.ConvertInvariant(); + var token= await MakeJsonRequestAsync("/order/cancel", BaseUrlV1, payload); + } + + protected override async Task OnGetDepositAddressAsync(string currency, bool forceRegenerate = false) + { + if(string.IsNullOrWhiteSpace(currency)) { + throw new ArgumentNullException(nameof(currency)); + } + + // IOTA addresses should never be used more than once + if(currency.Equals("MIOTA", StringComparison.OrdinalIgnoreCase)) { + forceRegenerate = true; + } + + // symbol needs to be translated to full name of coin: bitcoin/litecoin/ethereum + if(!DepositMethodLookup.TryGetValue(currency, out string fullName)) { + fullName = currency.ToLowerInvariant(); + } + + Dictionary payload = await GetNoncePayloadAsync(); + payload["method"] = fullName; + payload["wallet_name"] = "exchange"; + payload["renew"] = forceRegenerate ? 1 : 0; + + JToken result = await MakeJsonRequestAsync("/deposit/new", BaseUrlV1, payload, "POST"); + var details = new ExchangeDepositDetails + { + Currency = result["currency"].ToStringInvariant(), + }; + if(result["address_pool"] != null) { + details.Address = result["address_pool"].ToStringInvariant(); + details.AddressTag = result["address"].ToStringLowerInvariant(); + } + else { + details.Address = result["address"].ToStringInvariant(); + } + + return details; + } + + /// Gets the deposit history for a symbol + /// The symbol to check. Must be specified. + /// Collection of ExchangeCoinTransfers + protected override async Task> OnGetDepositHistoryAsync(string currency) + { + if(string.IsNullOrWhiteSpace(currency)) { + throw new ArgumentNullException(nameof(currency)); + } + + Dictionary payload = await GetNoncePayloadAsync(); + payload["currency"] = currency; + + JToken result = await MakeJsonRequestAsync("/history/movements", BaseUrlV1, payload, "POST"); + var transactions = new List(); + foreach(JToken token in result) { + if(!string.Equals(token["type"].ToStringUpperInvariant(), "DEPOSIT")) { + continue; + } + + var transaction = new ExchangeTransaction + { + PaymentId = token["id"].ToStringInvariant(), + BlockchainTxId = token["txid"].ToStringInvariant(), + Currency = token["currency"].ToStringUpperInvariant(), + Notes = token["description"].ToStringInvariant() + ", method: " + token["method"].ToStringInvariant(), + Amount = token["amount"].ConvertInvariant(), + Address = token["address"].ToStringInvariant() + }; + + string status = token["status"].ToStringUpperInvariant(); + switch(status) { + case "COMPLETED": + transaction.Status = TransactionStatus.Complete; + break; + case "UNCONFIRMED": + transaction.Status = TransactionStatus.Processing; + break; + default: + transaction.Status = TransactionStatus.Unknown; + transaction.Notes += ", Unknown transaction status " + status; + break; + } + + double unixTimestamp = token["timestamp"].ConvertInvariant(); + transaction.Timestamp = unixTimestamp.UnixTimeStampToDateTimeSeconds(); + transaction.TxFee = token["fee"].ConvertInvariant(); + + transactions.Add(transaction); + } + + return transactions; + } + + /// Gets the deposit history for a symbol + /// The symbol to check. Must be specified. + /// Collection of ExchangeCoinTransfers + protected override async Task> OnGetWithdrawHistoryAsync(string currency) + { + if(string.IsNullOrWhiteSpace(currency)) { + throw new ArgumentNullException(nameof(currency)); + } + + Dictionary payload = await GetNoncePayloadAsync(); + payload["currency"] = currency; + + JToken result = await MakeJsonRequestAsync("/history/movements", BaseUrlV1, payload, "POST"); + var transactions = new List(); + foreach(JToken token in result) { + if(!string.Equals(token["type"].ToStringUpperInvariant(), "WITHDRAWAL")) { + continue; + } + + var transaction = new ExchangeTransaction + { + PaymentId = token["id"].ToStringInvariant(), + BlockchainTxId = token["txid"].ToStringInvariant(), + Currency = token["currency"].ToStringUpperInvariant(), + Notes = token["description"].ToStringInvariant() + ", method: " + token["method"].ToStringInvariant(), + Amount = token["amount"].ConvertInvariant(), + Address = token["address"].ToStringInvariant() + }; + + string status = token["status"].ToStringUpperInvariant(); + switch(status) { + case "COMPLETED": + transaction.Status = TransactionStatus.Complete; + break; + case "UNCONFIRMED": + transaction.Status = TransactionStatus.Processing; + break; + default: + transaction.Status = TransactionStatus.Unknown; + transaction.Notes += ", Unknown transaction status " + status; + break; + } + + double unixTimestamp = token["timestamp"].ConvertInvariant(); + transaction.Timestamp = unixTimestamp.UnixTimeStampToDateTimeSeconds(); + transaction.TxFee = token["fee"].ConvertInvariant(); + + transactions.Add(transaction); + } + + return transactions; + } + + /// A withdrawal request. + /// The withdrawal request. + /// NOTE: Network fee must be subtracted from amount or withdrawal will fail + /// The withdrawal response + protected override async Task OnWithdrawAsync(ExchangeWithdrawalRequest withdrawalRequest) + { + // symbol needs to be translated to full name of coin: bitcoin/litecoin/ethereum + if(!DepositMethodLookup.TryGetValue(withdrawalRequest.Currency, out string fullName)) { + fullName = withdrawalRequest.Currency.ToLowerInvariant(); + } + + // Bitfinex adds the fee on top of what you request to withdrawal + if(withdrawalRequest.TakeFeeFromAmount) { + Dictionary fees = await GetWithdrawalFeesAsync(); + if(fees.TryGetValue(withdrawalRequest.Currency, out decimal feeAmt)) { + withdrawalRequest.Amount -= feeAmt; + } + } + + Dictionary payload = await GetNoncePayloadAsync(); + payload["withdraw_type"] = fullName; + payload["walletselected"] = "exchange"; + payload["amount"] = withdrawalRequest.Amount.ToString(CultureInfo.InvariantCulture); // API throws if this is a number not a string + payload["address"] = withdrawalRequest.Address; + + if(!string.IsNullOrWhiteSpace(withdrawalRequest.AddressTag)) { + payload["payment_id"] = withdrawalRequest.AddressTag; + } + + if(!string.IsNullOrWhiteSpace(withdrawalRequest.Description)) { + payload["account_name"] = withdrawalRequest.Description; + } + + JToken result = await MakeJsonRequestAsync("/withdraw", BaseUrlV1, payload, "POST"); + + var resp = new ExchangeWithdrawalResponse(); + if(!string.Equals(result[0]["status"].ToStringInvariant(), "success", StringComparison.OrdinalIgnoreCase)) { + resp.Success = false; + } + + resp.Id = result[0]["withdrawal_id"].ToStringInvariant(); + resp.Message = result[0]["message"].ToStringInvariant(); + return resp; + } + + protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + { + if(CanMakeAuthenticatedRequest(payload)) { + request.Method = "POST"; + request.AddHeader("content-type", "application/json"); + request.AddHeader("accept", "application/json"); + if(request.RequestUri.AbsolutePath.StartsWith("/v2")) { + string nonce = payload["nonce"].ToStringInvariant(); + payload.Remove("nonce"); + string json = JsonConvert.SerializeObject(payload); + string toSign = "/api" + request.RequestUri.PathAndQuery + nonce + json; + string hexSha384 = CryptoUtility.SHA384Sign(toSign, PrivateApiKey.ToUnsecureString()); + request.AddHeader("bfx-nonce", nonce); + request.AddHeader("bfx-apikey", PublicApiKey.ToUnsecureString()); + request.AddHeader("bfx-signature", hexSha384); + await CryptoUtility.WriteToRequestAsync(request, json); + } + else { + // bitfinex v1 doesn't put the payload in the post body it puts it in as a http header, so no need to write to request stream + payload.Add("request", request.RequestUri.AbsolutePath); + string json = JsonConvert.SerializeObject(payload); + string json64 = System.Convert.ToBase64String(json.ToBytesUTF8()); + string hexSha384 = CryptoUtility.SHA384Sign(json64, PrivateApiKey.ToUnsecureString()); + request.AddHeader("X-BFX-PAYLOAD", json64); + request.AddHeader("X-BFX-SIGNATURE", hexSha384); + request.AddHeader("X-BFX-APIKEY", PublicApiKey.ToUnsecureString()); + } + } + } + + protected override Task> OnGetCurrenciesAsync() + { + throw new NotSupportedException("Bitfinex does not provide data about its currencies via the API"); + } + + private string NormalizeMarketSymbolV1(string marketSymbol) + { + return (marketSymbol ?? string.Empty).Replace("-", string.Empty).ToLowerInvariant(); + } + + private async Task> GetOrderDetailsInternalV2(string url, string marketSymbol = null) + { + Dictionary payload = await GetNoncePayloadAsync(); + payload["limit"] = 250; + payload["start"] = CryptoUtility.UtcNow.Subtract(TimeSpan.FromDays(365.0)).UnixTimestampFromDateTimeMilliseconds(); + payload["end"] = CryptoUtility.UtcNow.UnixTimestampFromDateTimeMilliseconds(); + JToken result = await MakeJsonRequestAsync(url, null, payload); + Dictionary> trades = new Dictionary>(StringComparer.OrdinalIgnoreCase); + if(result is JArray array) { + foreach(JToken token in array) { + if(string.IsNullOrWhiteSpace(marketSymbol) || token[1].ToStringInvariant() == "t" + marketSymbol) { + string lookup = token[1].ToStringInvariant().Substring(1).ToLowerInvariant(); + if(!trades.TryGetValue(lookup, out List tradeList)) { + tradeList = trades[lookup] = new List(); + } + tradeList.Add(token); + } + } + } + return ParseOrderV2(trades); + } + + private async Task> GetOrderDetailsInternalAsync(string url, string marketSymbol = null) + { + List orders = new List(); + marketSymbol = NormalizeMarketSymbolV1(marketSymbol); + JToken result = await MakeJsonRequestAsync(url, BaseUrlV1, await GetNoncePayloadAsync()); + if(result is JArray array) { + foreach(JToken token in array) { + if(marketSymbol == null || token["symbol"].ToStringInvariant() == marketSymbol) { + orders.Add(ParseOrder(token)); + } + } + } + return orders; + } + + private async Task> GetOrderDetailsInternalV1(IEnumerable marketSymbols, DateTime? afterDate) + { + Dictionary orders = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach(string marketSymbol in marketSymbols) { + string normalizedSymbol = NormalizeMarketSymbolV1(marketSymbol); + Dictionary payload = await GetNoncePayloadAsync(); + payload["symbol"] = normalizedSymbol; + payload["limit_trades"] = 250; + if(afterDate != null) { + payload["timestamp"] = afterDate.Value.UnixTimestampFromDateTimeSeconds().ToStringInvariant(); + payload["until"] = CryptoUtility.UtcNow.UnixTimestampFromDateTimeSeconds().ToStringInvariant(); + } + JToken token = await MakeJsonRequestAsync("/mytrades", BaseUrlV1, payload); + foreach(JToken trade in token) { + ExchangeOrderResult subOrder = ParseTrade(trade, normalizedSymbol); + lock(orders) { + if(orders.TryGetValue(subOrder.OrderId, out ExchangeOrderResult baseOrder)) { + baseOrder.AppendOrderWithOrder(subOrder); + } + else { + orders[subOrder.OrderId] = subOrder; + } + } + } + } + return orders.Values.OrderByDescending(o => o.OrderDate); + } + + private ExchangeOrderResult ParseOrder(JToken order) + { + decimal amount = order["original_amount"].ConvertInvariant(); + decimal amountFilled = order["executed_amount"].ConvertInvariant(); + decimal price = order["price"].ConvertInvariant(); + return new ExchangeOrderResult { + Amount = amount, + AmountFilled = amountFilled, + Price = price, + AveragePrice = order["avg_execution_price"].ConvertInvariant(order["price"].ConvertInvariant()), + Message = string.Empty, + OrderId = order["id"].ToStringInvariant(), + Result = (amountFilled == amount ? ExchangeAPIOrderResult.Filled : (amountFilled == 0 ? ExchangeAPIOrderResult.Pending : ExchangeAPIOrderResult.FilledPartially)), + OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(order["timestamp"].ConvertInvariant()), + MarketSymbol = order["symbol"].ToStringInvariant(), + IsBuy = order["side"].ToStringInvariant() == "buy" + }; + } private ExchangeOrderResult ParseOrderWebSocket(JToken order) { @@ -912,8 +830,7 @@ private ExchangeOrderResult ParseOrderWebSocket(JToken order) ACTIVE, EXECUTED @ PRICE(AMOUNT) e.g. "EXECUTED @ 107.6(-0.2)", PARTIALLY FILLED @ PRICE(AMOUNT), INSUFFICIENT MARGIN was: PARTIALLY FILLED @ PRICE(AMOUNT), CANCELED, CANCELED was: PARTIALLY FILLED @ PRICE(AMOUNT) */ string orderStatusString = order[5].ToStringInvariant().Split(' ')[0]; - return new ExchangeOrderResult - { + return new ExchangeOrderResult { Amount = amount, AmountFilled = amount, Price = order[6].ConvertInvariant(), @@ -932,9 +849,9 @@ private ExchangeOrderResult ParseOrderWebSocket(JToken order) } private IEnumerable ParseOrderV2(Dictionary> trades) - { + { - /* + /* [ ID integer Trade database id PAIR string Pair (BTCUSD, …) @@ -950,27 +867,25 @@ FEE_CURRENCY string Fee currency ], */ - foreach (var kv in trades) - { - ExchangeOrderResult order = new ExchangeOrderResult { Result = ExchangeAPIOrderResult.Filled }; - foreach (JToken trade in kv.Value) - { - ExchangeOrderResult append = new ExchangeOrderResult { MarketSymbol = kv.Key, OrderId = trade[3].ToStringInvariant() }; - append.Amount = append.AmountFilled = Math.Abs(trade[4].ConvertInvariant()); - append.Price = trade[7].ConvertInvariant(); - append.AveragePrice = trade[5].ConvertInvariant(); - append.IsBuy = trade[4].ConvertInvariant() >= 0m; - append.OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(trade[2].ConvertInvariant()); - append.OrderId = trade[3].ToStringInvariant(); - order.AppendOrderWithOrder(append); - } - yield return order; - } - } - - private ExchangeOrderResult ParseTrade(JToken trade, string symbol) - { - /* + foreach(var kv in trades) { + ExchangeOrderResult order = new ExchangeOrderResult { Result = ExchangeAPIOrderResult.Filled }; + foreach(JToken trade in kv.Value) { + ExchangeOrderResult append = new ExchangeOrderResult { MarketSymbol = kv.Key, OrderId = trade[3].ToStringInvariant() }; + append.Amount = append.AmountFilled = Math.Abs(trade[4].ConvertInvariant()); + append.Price = trade[7].ConvertInvariant(); + append.AveragePrice = trade[5].ConvertInvariant(); + append.IsBuy = trade[4].ConvertInvariant() >= 0m; + append.OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(trade[2].ConvertInvariant()); + append.OrderId = trade[3].ToStringInvariant(); + order.AppendOrderWithOrder(append); + } + yield return order; + } + } + + private ExchangeOrderResult ParseTrade(JToken trade, string symbol) + { + /* [{ "price":"246.94", "amount":"1.0", @@ -983,39 +898,37 @@ private ExchangeOrderResult ParseTrade(JToken trade, string symbol) "order_id":446913929 }] */ - return new ExchangeOrderResult - { - Amount = trade["amount"].ConvertInvariant(), - AmountFilled = trade["amount"].ConvertInvariant(), - AveragePrice = trade["price"].ConvertInvariant(), - IsBuy = trade["type"].ToStringUpperInvariant() == "BUY", - OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(trade["timestamp"].ConvertInvariant()), - OrderId = trade["order_id"].ToStringInvariant(), - Result = ExchangeAPIOrderResult.Filled, - MarketSymbol = symbol - }; - } - - private async Task ParseTickerWebSocketAsync(string symbol, JToken token) - { - return await this.ParseTickerAsync(token, symbol, 3, 1, 7, 8); - } - - /// Gets the withdrawal fees for various currencies. - /// A dictionary of symbol-fee pairs - private async Task> GetWithdrawalFeesAsync() - { - JToken obj = await MakeJsonRequestAsync("/account_fees", BaseUrlV1, await GetNoncePayloadAsync()); - var fees = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var jToken in obj["withdraw"]) - { - var prop = (JProperty)jToken; - fees[prop.Name] = prop.Value.ConvertInvariant(); - } - - return fees; - } - } - - public partial class ExchangeName { public const string Bitfinex = "Bitfinex"; } + return new ExchangeOrderResult { + Amount = trade["amount"].ConvertInvariant(), + AmountFilled = trade["amount"].ConvertInvariant(), + AveragePrice = trade["price"].ConvertInvariant(), + IsBuy = trade["type"].ToStringUpperInvariant() == "BUY", + OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(trade["timestamp"].ConvertInvariant()), + OrderId = trade["order_id"].ToStringInvariant(), + Result = ExchangeAPIOrderResult.Filled, + MarketSymbol = symbol + }; + } + + private async Task ParseTickerWebSocketAsync(string symbol, JToken token) + { + return await this.ParseTickerAsync(token, symbol, 3, 1, 7, 8); + } + + /// Gets the withdrawal fees for various currencies. + /// A dictionary of symbol-fee pairs + private async Task> GetWithdrawalFeesAsync() + { + JToken obj = await MakeJsonRequestAsync("/account_fees", BaseUrlV1, await GetNoncePayloadAsync()); + var fees = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach(var jToken in obj["withdraw"]) { + var prop = (JProperty)jToken; + fees[prop.Name] = prop.Value.ConvertInvariant(); + } + + return fees; + } + } + + public partial class ExchangeName { public const string Bitfinex = "Bitfinex"; } } diff --git a/src/ExchangeSharp/API/Exchanges/Bitstamp/ExchangeBitstampAPI.cs b/src/ExchangeSharp/API/Exchanges/Bitstamp/ExchangeBitstampAPI.cs index 932fa693..60a93bb5 100644 --- a/src/ExchangeSharp/API/Exchanges/Bitstamp/ExchangeBitstampAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Bitstamp/ExchangeBitstampAPI.cs @@ -1,4 +1,4 @@ -/* +/* MIT LICENSE Copyright 2017 Digital Ruby, LLC - http://www.digitalruby.com @@ -136,7 +136,7 @@ protected override async Task OnGetOrderBookAsync(string mark return ExchangeAPIExtensions.ParseOrderBookFromJTokenArrays(token, maxCount: maxCount); } - protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null) + protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) { // [{"date": "1513387997", "tid": "33734815", "price": "0.01724547", "type": "1", "amount": "5.56481714"}] JToken token = await MakeBitstampRequestAsync("/transactions/" + marketSymbol); diff --git a/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI.cs b/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI.cs index cce1609b..13f5c02c 100644 --- a/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI.cs @@ -293,13 +293,13 @@ protected override async Task> OnGetDepositHist } protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, - DateTime? startDate = null, DateTime? endDate = null) + DateTime? startDate = null, DateTime? endDate = null, int? limit = null) { throw new APIException( "Bittrex does not allow querying trades by dates. Consider using either GetRecentTradesAsync() or GetCandlesAsync() w/ a period of 1 min. See issue #508."); } - protected override async Task> OnGetRecentTradesAsync(string marketSymbol) + protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null) { List trades = new List(); string baseUrl = "/public/getmarkethistory?market=" + marketSymbol; diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs index 913fe386..9698a944 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs @@ -1,4 +1,4 @@ -/* +/* MIT LICENSE Copyright 2017 Digital Ruby, LLC - http://www.digitalruby.com @@ -435,7 +435,7 @@ private ExchangeTrade ParseTradeWebSocket(JToken token) return token.ParseTradeCoinbase("size", "price", "side", "time", TimestampType.Iso8601, "trade_id"); } - protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null) + protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) { /* [{ @@ -469,9 +469,12 @@ protected override async Task OnGetHistoricalTradesAsync(Func> OnGetRecentTradesAsync(string marketSymbol) + protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null) { - string baseUrl = "/products/" + marketSymbol.ToUpperInvariant() + "/trades"; + //https://docs.pro.coinbase.com/#pagination Coinbase limit is 100, however pagination can return more (4 later) + int requestLimit = (limit == null || limit < 1 || limit > 100) ? 100 : (int)limit; + + string baseUrl = "/products/" + marketSymbol.ToUpperInvariant() + "/trades" + "?limit=" + requestLimit; JToken trades = await MakeJsonRequestAsync(baseUrl); List tradeList = new List(); foreach (JToken trade in trades) diff --git a/src/ExchangeSharp/API/Exchanges/Digifinex/ExchangeDigifinexAPI.cs b/src/ExchangeSharp/API/Exchanges/Digifinex/ExchangeDigifinexAPI.cs index ffcd717e..21431477 100644 --- a/src/ExchangeSharp/API/Exchanges/Digifinex/ExchangeDigifinexAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Digifinex/ExchangeDigifinexAPI.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; @@ -177,7 +177,7 @@ protected override async Task OnGetOrderBookAsync(string mark return result; } - protected override async Task> OnGetRecentTradesAsync(string marketSymbol) + protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null) { JToken obj = await MakeJsonRequestAsync($"/trades?symbol={marketSymbol}&limit=500"); // maximum limit = 500 return obj["data"].Select(x => new ExchangeTrade diff --git a/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs b/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs index ac3293c8..ad808012 100644 --- a/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs @@ -1,4 +1,4 @@ -/* +/* MIT LICENSE Copyright 2017 Digital Ruby, LLC - http://www.digitalruby.com @@ -176,7 +176,7 @@ protected override async Task OnGetOrderBookAsync(string mark return ExchangeAPIExtensions.ParseOrderBookFromJTokenDictionaries(obj, maxCount: maxCount); } - protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null) + protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) { ExchangeHistoricalTradeHelper state = new ExchangeHistoricalTradeHelper(this) { diff --git a/src/ExchangeSharp/API/Exchanges/Hitbtc/ExchangeHitbtcAPI.cs b/src/ExchangeSharp/API/Exchanges/Hitbtc/ExchangeHitbtcAPI.cs index 2519dd8f..d977383a 100644 --- a/src/ExchangeSharp/API/Exchanges/Hitbtc/ExchangeHitbtcAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Hitbtc/ExchangeHitbtcAPI.cs @@ -165,15 +165,21 @@ protected override async Task> OnGetCandlesAsync(strin return candles; } - protected override async Task> OnGetRecentTradesAsync(string marketSymbol) + protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null) { List trades = new List(); - // Putting an arbitrary limit of 10 for 'recent' - JToken obj = await MakeJsonRequestAsync("/public/trades/" + marketSymbol + "?limit=10"); - foreach (JToken token in obj) - { - trades.Add(ParseExchangeTrade(token)); - } + // Putting an arbitrary limit of 10 for 'recent' + // UPDATE: Putting an arbitrary limit of 100 for 'recent' + + //var maxRequestLimit = 1000; //hard coded for now, should add limit as an argument + var maxRequestLimit = (limit == null || limit < 1 || limit > 1000) ? 1000 : (int)limit; + + JToken obj = await MakeJsonRequestAsync("/public/trades/" + marketSymbol + "?limit=" + maxRequestLimit + "?sort=DESC"); + if(obj.HasValues) { // + foreach(JToken token in obj) { + trades.Add(ParseExchangeTrade(token)); + } + } return trades; } @@ -183,13 +189,19 @@ protected override async Task OnGetOrderBookAsync(string mark return ExchangeAPIExtensions.ParseOrderBookFromJTokenDictionaries(token, asks: "ask", bids: "bid", amount: "size", maxCount: maxCount); } - protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null) + protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) { List trades = new List(); - // TODO: Can't get Hitbtc to return other than the last 50 trades even though their API says it should (by orderid or timestamp). When passing either of these parms, it still returns the last 50 - // So until there is an update, that's what we'll go with - JToken obj = await MakeJsonRequestAsync("/public/trades/" + marketSymbol); - if (obj.HasValues) + // TODO: Can't get Hitbtc to return other than the last 50 trades even though their API says it should (by orderid or timestamp). When passing either of these parms, it still returns the last 50 + // So until there is an update, that's what we'll go with + // UPDATE: 2020/01/19 https://api.hitbtc.com/ GET /api/2/public/trades/{symbol} limit default: 100 max value:1000 + // + //var maxRequestLimit = 1000; //hard coded for now, should add limit as an argument + var maxRequestLimit = (limit == null || limit < 1 || limit > 1000) ? 1000 : (int)limit; + //note that sort must come after limit, else returns default 100 trades, sort default is DESC + JToken obj = await MakeJsonRequestAsync("/public/trades/" + marketSymbol + "?limit=" + maxRequestLimit + "?sort=DESC"); + //JToken obj = await MakeJsonRequestAsync("/public/trades/" + marketSymbol); + if (obj.HasValues) { foreach (JToken token in obj) { @@ -201,8 +213,9 @@ protected override async Task OnGetHistoricalTradesAsync(Func t.Timestamp)); - } + callback(trades); //no need to OrderBy or OrderByDescending, handled by sort=DESC or sort=ASC + //callback(trades.OrderBy(t => t.Timestamp)); + } } } diff --git a/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs b/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs index d7e85f18..3ea56914 100644 --- a/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs @@ -587,7 +587,32 @@ protected override async Task OnGetOrderBookAsync(string mark return ExchangeAPIExtensions.ParseOrderBookFromJTokenArrays(obj[marketSymbol], maxCount: maxCount); } - protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null) + 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 + //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 = "/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()) { + trades.Add(trade.ParseTrade(1, 0, 3, 2, TimestampType.UnixSecondsDouble, null, "b")); + } + } + + return trades.AsEnumerable().Reverse(); //Descending order (ie newest trades first) + } + + 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; diff --git a/src/ExchangeSharp/API/Exchanges/KuCoin/ExchangeKuCoinAPI.cs b/src/ExchangeSharp/API/Exchanges/KuCoin/ExchangeKuCoinAPI.cs index 7b8d3f7c..f46c7005 100644 --- a/src/ExchangeSharp/API/Exchanges/KuCoin/ExchangeKuCoinAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/KuCoin/ExchangeKuCoinAPI.cs @@ -208,7 +208,7 @@ protected override async Task>> return tickers; } - protected override async Task> OnGetRecentTradesAsync(string marketSymbol) + protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null) { List trades = new List(); // [0]-Timestamp [1]-OrderType [2]-Price [3]-Amount [4]-Volume @@ -222,7 +222,7 @@ protected override async Task> OnGetRecentTradesAsync return trades; } - protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null) + protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) { List trades = new List(); JToken token = await MakeJsonRequestAsync("/market/histories?symbol=" + marketSymbol + (startDate == null ? string.Empty : "&since=" + startDate.Value.UnixTimestampFromDateTimeMilliseconds())); diff --git a/src/ExchangeSharp/API/Exchanges/LBank/ExchangeLBankAPI.cs b/src/ExchangeSharp/API/Exchanges/LBank/ExchangeLBankAPI.cs index aa033968..a552f1d8 100644 --- a/src/ExchangeSharp/API/Exchanges/LBank/ExchangeLBankAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/LBank/ExchangeLBankAPI.cs @@ -1,4 +1,4 @@ -/* +/* MIT LICENSE Copyright 2017 Digital Ruby, LLC - http://www.digitalruby.com @@ -107,10 +107,12 @@ protected override async Task OnGetOrderBookAsync(string symb } //GetRecentTrades 6 - protected override async Task> OnGetRecentTradesAsync(string symbol) + protected override async Task> OnGetRecentTradesAsync(string symbol, int? limit = null) { - //https://api.lbank.info/v1/trades.do?symbol=eth_btc&size=600 - JToken resp = await this.MakeJsonRequestAsync($"/trades.do?symbol={symbol}&size={RECENT_TRADS_MAX_SIZE}"); + //https://api.lbank.info/v1/trades.do?symbol=eth_btc&size=600 + int requestLimit = (limit == null || limit < 1 || limit > RECENT_TRADS_MAX_SIZE) ? RECENT_TRADS_MAX_SIZE : (int)limit; + + JToken resp = await this.MakeJsonRequestAsync($"/trades.do?symbol={symbol}&size={requestLimit}"); CheckResponseToken(resp); return ParseRecentTrades(resp, symbol); } diff --git a/src/ExchangeSharp/API/Exchanges/Livecoin/ExchangeLivecoinAPI.cs b/src/ExchangeSharp/API/Exchanges/Livecoin/ExchangeLivecoinAPI.cs index 3594852d..6495cee1 100644 --- a/src/ExchangeSharp/API/Exchanges/Livecoin/ExchangeLivecoinAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Livecoin/ExchangeLivecoinAPI.cs @@ -1,4 +1,4 @@ -/* +/* MIT LICENSE Copyright 2017 Digital Ruby, LLC - http://www.digitalruby.com @@ -131,7 +131,7 @@ protected override async Task OnGetOrderBookAsync(string mark /// /// /// - protected override async Task> OnGetRecentTradesAsync(string marketSymbol) + protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null) { List trades = new List(); JToken token = await MakeJsonRequestAsync("/exchange/last_trades?currencyPair=" + marketSymbol.UrlEncode()); @@ -149,7 +149,7 @@ protected override async Task> OnGetRecentTradesAsync /// /// /// - protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null) + protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) { List trades = new List(); // Not directly supported so we'll return what they have and filter if necessary diff --git a/src/ExchangeSharp/API/Exchanges/NDAX/ExchangeNDAXAPI.cs b/src/ExchangeSharp/API/Exchanges/NDAX/ExchangeNDAXAPI.cs index 8f67f7a2..ebd9a8a1 100644 --- a/src/ExchangeSharp/API/Exchanges/NDAX/ExchangeNDAXAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/NDAX/ExchangeNDAXAPI.cs @@ -86,7 +86,7 @@ protected override async Task> OnGetCompletedOr return result.Select(order => order.ToExchangeOrderResult(_marketSymbolToInstrumentIdMapping)); } protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, - DateTime? endDate = null) + DateTime? endDate = null, int? limit = null) { var payload = new Dictionary() diff --git a/src/ExchangeSharp/API/Exchanges/OKGroup/OKGroupCommon.cs b/src/ExchangeSharp/API/Exchanges/OKGroup/OKGroupCommon.cs index 6d1ff724..2582fcda 100644 --- a/src/ExchangeSharp/API/Exchanges/OKGroup/OKGroupCommon.cs +++ b/src/ExchangeSharp/API/Exchanges/OKGroup/OKGroupCommon.cs @@ -1,4 +1,4 @@ -/* +/* MIT LICENSE Copyright 2017 Digital Ruby, LLC - http://www.digitalruby.com @@ -291,7 +291,7 @@ protected override async Task OnGetOrderBookAsync(string mark return ExchangeAPIExtensions.ParseOrderBookFromJTokenArrays(token.Item1, maxCount: maxCount); } - protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null) + protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) { List allTrades = new List(); var trades = await MakeRequestOkexAsync(marketSymbol, "/trades.do?symbol=$SYMBOL$"); diff --git a/src/ExchangeSharp/API/Exchanges/Poloniex/ExchangePoloniexAPI.cs b/src/ExchangeSharp/API/Exchanges/Poloniex/ExchangePoloniexAPI.cs index d0527678..ebca9521 100644 --- a/src/ExchangeSharp/API/Exchanges/Poloniex/ExchangePoloniexAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Poloniex/ExchangePoloniexAPI.cs @@ -1,4 +1,4 @@ -/* +/* MIT LICENSE Copyright 2017 Digital Ruby, LLC - http://www.digitalruby.com @@ -584,7 +584,27 @@ protected override async Task, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null) + protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null) + { + List trades = new List(); + //https://docs.poloniex.com/#returnorderbook note poloniex limit = 1000 + int requestLimit = (limit == null || limit < 1 || limit > 1000) ? 1000 : (int)limit; + string url = "/public?command=returnTradeHistory¤cyPair=" + marketSymbol + "&limit=" + requestLimit ; + + //JToken obj = await MakeJsonRequestAsync($"/aggTrades?symbol={marketSymbol}&limit={maxRequestLimit}"); + JToken obj = await MakeJsonRequestAsync(url); + + //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"); + trades.Add(trade); + } + } + return trades; + } + + protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) { // [{"globalTradeID":245321705,"tradeID":11501281,"date":"2017-10-20 17:39:17","type":"buy","rate":"0.01022188","amount":"0.00954454","total":"0.00009756"},...] ExchangeHistoricalTradeHelper state = new ExchangeHistoricalTradeHelper(this) diff --git a/src/ExchangeSharp/API/Exchanges/Yobit/ExchangeYobitAPI.cs b/src/ExchangeSharp/API/Exchanges/Yobit/ExchangeYobitAPI.cs index ae2c829f..7f3a7e24 100644 --- a/src/ExchangeSharp/API/Exchanges/Yobit/ExchangeYobitAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Yobit/ExchangeYobitAPI.cs @@ -1,4 +1,4 @@ -/* +/* MIT LICENSE Copyright 2017 Digital Ruby, LLC - http://www.digitalruby.com @@ -130,7 +130,7 @@ protected override async Task OnGetOrderBookAsync(string mark return ExchangeAPIExtensions.ParseOrderBookFromJTokenArrays(token[marketSymbol]); } - protected override async Task> OnGetRecentTradesAsync(string marketSymbol) + protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null) { List trades = new List(); JToken token = await MakeJsonRequestAsync("/trades/" + marketSymbol + "?limit=10", null, null, "POST"); // default is 150, max: 2000, let's do another arbitrary 10 for consistency @@ -138,11 +138,11 @@ protected override async Task> OnGetRecentTradesAsync return trades; } - protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null) + protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) { List trades = new List(); // Not directly supported, but we'll return the max and filter if necessary - JToken token = await MakeJsonRequestAsync("/trades/" + marketSymbol + "?limit=2000", null, null, "POST"); + JToken token = await MakeJsonRequestAsync("/trades/" + marketSymbol + "?limit=2000", null, null, "POST"); token = token.First.First; // bunch of nested foreach (JToken prop in token) { diff --git a/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPI.cs b/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPI.cs index a198b8a2..16e66df7 100644 --- a/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPI.cs @@ -78,7 +78,13 @@ protected virtual async Task return orderBooks.ToDictionary(k => k.MarketSymbol, v => v); } - protected virtual async Task> OnGetRecentTradesAsync(string marketSymbol) + /// + /// When possible, the sort order will be Descending (ie newest trades first) + /// + /// name of symbol + /// max number of results returned, if limiting is supported by the exchange + /// + protected virtual async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null) { marketSymbol = NormalizeMarketSymbol(marketSymbol); List trades = new List(); @@ -86,7 +92,7 @@ await GetHistoricalTradesAsync((e) => { trades.AddRange(e); return true; - }, marketSymbol); + }, marketSymbol, limit:limit); //KK2020 return trades; } @@ -95,7 +101,8 @@ await GetHistoricalTradesAsync((e) => protected internal virtual Task> OnGetMarketSymbolsMetadataAsync() => throw new NotImplementedException(); protected virtual Task OnGetTickerAsync(string marketSymbol) => throw new NotImplementedException(); protected virtual Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) => throw new NotImplementedException(); - protected virtual Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null) => throw new NotImplementedException(); + protected virtual Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) => throw new NotImplementedException(); + //protected virtual Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null) => throw new NotImplementedException(); protected virtual Task OnGetDepositAddressAsync(string currency, bool forceRegenerate = false) => throw new NotImplementedException(); protected virtual Task> OnGetDepositHistoryAsync(string currency) => throw new NotImplementedException(); protected virtual Task> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) => throw new NotImplementedException(); @@ -618,11 +625,11 @@ public virtual async Task>> /// Symbol to get historical data for /// Optional UTC start date time to start getting the historical data at, null for the most recent data. Not all exchanges support this. /// Optional UTC end date time to start getting the historical data at, null for the most recent data. Not all exchanges support this. - public virtual async Task GetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null) + public virtual async Task GetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) { // *NOTE*: Do not wrap in CacheMethodCall, uses a callback with custom queries, not easy to cache await new SynchronizationContextRemover(); - await OnGetHistoricalTradesAsync(callback, NormalizeMarketSymbol(marketSymbol), startDate, endDate); + await OnGetHistoricalTradesAsync(callback, NormalizeMarketSymbol(marketSymbol), startDate, endDate, limit); } /// @@ -630,10 +637,11 @@ public virtual async Task GetHistoricalTradesAsync(Func /// Symbol to get recent trades for /// An enumerator that loops through all recent trades - public virtual async Task> GetRecentTradesAsync(string marketSymbol) + public virtual async Task> GetRecentTradesAsync(string marketSymbol, int? limit = null) { marketSymbol = NormalizeMarketSymbol(marketSymbol); - return await Cache.CacheMethod(MethodCachePolicy, async () => await OnGetRecentTradesAsync(marketSymbol), nameof(GetRecentTradesAsync), nameof(marketSymbol), marketSymbol); + return await Cache.CacheMethod(MethodCachePolicy, async () => await OnGetRecentTradesAsync(marketSymbol, limit), nameof(GetRecentTradesAsync), nameof(marketSymbol), marketSymbol, nameof(limit), limit); + //return await Cache.CacheMethod(MethodCachePolicy, async () => await OnGetRecentTradesAsync(marketSymbol), nameof(GetRecentTradesAsync), nameof(marketSymbol), marketSymbol); } /// diff --git a/src/ExchangeSharp/API/Exchanges/_Base/IExchangeAPI.cs b/src/ExchangeSharp/API/Exchanges/_Base/IExchangeAPI.cs index 0dc7ebaf..8614a5a2 100644 --- a/src/ExchangeSharp/API/Exchanges/_Base/IExchangeAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/_Base/IExchangeAPI.cs @@ -112,25 +112,27 @@ public interface IExchangeAPI : IDisposable, IBaseAPI, IOrderBookProvider /// Symbol to get historical data for /// Optional start date time to start getting the historical data at, null for the most recent data. Not all exchanges support this. /// Optional UTC end date time to start getting the historical data at, null for the most recent data. Not all exchanges support this. - Task GetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null); + Task GetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null); + //Task GetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null); /// /// Get the latest trades /// /// Market Symbol /// Trades - Task> GetRecentTradesAsync(string marketSymbol); - - /// - /// Get candles (open, high, low, close) - /// - /// Market symbol to get candles for - /// Period in seconds to get candles for. Use 60 for minute, 3600 for hour, 3600*24 for day, 3600*24*30 for month. - /// Optional start date to get candles for - /// Optional end date to get candles for - /// Max results, can be used instead of startDate and endDate if desired - /// Candles - Task> GetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null); + Task> GetRecentTradesAsync(string marketSymbol, int? limit = null); + //Task> GetRecentTradesAsync(string marketSymbol); + + /// + /// Get candles (open, high, low, close) + /// + /// Market symbol to get candles for + /// Period in seconds to get candles for. Use 60 for minute, 3600 for hour, 3600*24 for day, 3600*24*30 for month. + /// Optional start date to get candles for + /// Optional end date to get candles for + /// Max results, can be used instead of startDate and endDate if desired + /// Candles + Task> GetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null); /// /// Get total amounts, symbol / amount dictionary