diff --git a/README.md b/README.md index 410d434d..85108892 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ The following cryptocurrency exchanges are supported: | Bittrex | x | x | T R | | BL3P | x | x | R B | Trades stream does not send trade's ids. | Bleutrade | x | x | | -| BTSE | x | | | +| BTSE | x | x | | | Coinbase | x | x | T R | | Digifinex | x | x | R B | | Gemini | x | x | R | @@ -157,4 +157,4 @@ jeff@digitalruby.com http://www.digitalruby.com [nuget]: https://www.nuget.org/packages/DigitalRuby.ExchangeSharp/ - [websocket4net]: https://github.com/kerryjiang/WebSocket4Net \ No newline at end of file + [websocket4net]: https://github.com/kerryjiang/WebSocket4Net diff --git a/src/ExchangeSharp/API/Exchanges/BTSE/ExchangeBTSEAPI.cs b/src/ExchangeSharp/API/Exchanges/BTSE/ExchangeBTSEAPI.cs index f81b8186..d334daa2 100644 --- a/src/ExchangeSharp/API/Exchanges/BTSE/ExchangeBTSEAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/BTSE/ExchangeBTSEAPI.cs @@ -1,11 +1,15 @@ -namespace ExchangeSharp { +using Newtonsoft.Json; + +namespace ExchangeSharp +{ using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; - public sealed partial class ExchangeBTSEAPI :ExchangeAPI { + public sealed partial class ExchangeBTSEAPI : ExchangeAPI + { public override string BaseUrl { get; set; } = "https://api.btse.com"; protected override async Task> OnGetMarketSymbolsAsync() @@ -16,7 +20,8 @@ protected override async Task> OnGetMarketSymbolsAsync() protected override async Task>> OnGetTickersAsync() { JToken allPairs = await MakeJsonRequestAsync("/spot/api/v3/market_summary"); - var tasks = allPairs.Select(async token => await this.ParseTickerAsync(token, token["symbol"].Value(), "lowestAsk", "highestBid", "last", "volume", null, + var tasks = allPairs.Select(async token => await this.ParseTickerAsync(token, + token["symbol"].Value(), "lowestAsk", "highestBid", "last", "volume", null, null, TimestampType.UnixMilliseconds, "base", "quote", "symbol")); return (await Task.WhenAll(tasks)).Select(ticker => @@ -25,15 +30,17 @@ protected override async Task>> protected override async Task OnGetTickerAsync(string marketSymbol) { - JToken ticker = await MakeJsonRequestAsync("/spot/api/v3/market_summary", null, new Dictionary() - { - {"symbol", marketSymbol} - }); + JToken ticker = await MakeJsonRequestAsync("/spot/api/v3/market_summary", null, + new Dictionary() + { + {"symbol", marketSymbol} + }); return await this.ParseTickerAsync(ticker, marketSymbol, "lowestAsk", "highestBid", "last", "volume", null, null, TimestampType.UnixMilliseconds, "base", "quote", "symbol"); } - protected override async Task> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, + protected override async Task> OnGetCandlesAsync(string marketSymbol, + int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) { var payload = new Dictionary() @@ -46,6 +53,7 @@ protected override async Task> OnGetCandlesAsync(strin { payload.Add("start", startDate.Value.UnixTimestampFromDateTimeMilliseconds()); } + if (endDate != null) { payload.Add("end", startDate.Value.UnixTimestampFromDateTimeMilliseconds()); @@ -56,15 +64,219 @@ protected override async Task> OnGetCandlesAsync(strin this.ParseCandle(token, marketSymbol, periodSeconds, 1, 2, 3, 4, 0, TimestampType.UnixMilliseconds, 5)); } + protected override async Task OnCancelOrderAsync(string orderId, string? marketSymbol = null) + { + var payload = await GetNoncePayloadAsync(); + + payload["order_id"] = orderId.ConvertInvariant(); + var url = new UriBuilder(BaseUrl) {Path = "/spot/api/v3/order"}; + url.AppendPayloadToQuery(new Dictionary() + { + {"symbol", marketSymbol}, + {"orderID", orderId} + }); + + await MakeJsonRequestAsync(url.ToStringInvariant().Replace(BaseUrl, ""), + requestMethod: "DELETE", payload: payload); + } + + protected override async Task> OnGetAmountsAsync() + { + var payload = await GetNoncePayloadAsync(); + + var result = await MakeJsonRequestAsync("/spot/api/v3/user/wallet", + requestMethod: "GET", payload: payload); + return Extract(result, token => (token["currency"].Value(), token["total"].Value())); + } + + protected override async Task> OnGetAmountsAvailableToTradeAsync() + { + var payload = await GetNoncePayloadAsync(); + + var result = await MakeJsonRequestAsync("/spot/api/v3/user/wallet", + requestMethod: "GET", payload: payload); + return Extract(result, token => (token["currency"].Value(), token["available"].Value())); + } + + protected override async Task> OnGetFeesAsync() + { + var payload = await GetNoncePayloadAsync(); + + var result = await MakeJsonRequestAsync("/spot/api/v3/user/fees", + requestMethod: "GET", payload: payload); + + //taker or maker fees in BTSE.. i chose take for here + return Extract(result, token => (token["symbol"].Value(), token["taker"].Value())); + } + + protected override async Task> OnGetOpenOrderDetailsAsync( + string? marketSymbol = null) + { + if (marketSymbol == null) throw new ArgumentNullException(nameof(marketSymbol)); + var payload = await GetNoncePayloadAsync(); + var url = new UriBuilder(BaseUrl) {Path = "/spot/api/v3/open_orders"}; + url.AppendPayloadToQuery(new Dictionary() + { + {"symbol", marketSymbol} + }); + var result = await MakeJsonRequestAsync(url.ToStringInvariant().Replace(BaseUrl, ""), + requestMethod: "GET", payload: payload); + + //taker or maker fees in BTSE.. i chose take for here + return Extract2(result, token => new ExchangeOrderResult() + { + Amount = token["size"].Value(), + AmountFilled = token["filledSize"].Value(), + OrderId = token["orderID"].Value(), + IsBuy = token["side"].Value() == "BUY", + Price = token["price"].Value(), + MarketSymbol = token["symbol"].Value(), + OrderDate = token["timestamp"].ConvertInvariant().UnixTimeStampToDateTimeMilliseconds() + }); + } + + public override async Task PlaceOrdersAsync(params ExchangeOrderRequest[] orders) + { + var payload = await GetNoncePayloadAsync(); + payload.Add("body", orders.Select(request => new + { + size = request.Amount, + side = request.IsBuy ? "BUY" : "SELL", + price = request.Price, + stopPrice = request.StopPrice, + symbol = request.MarketSymbol, + txType = request.OrderType == OrderType.Limit ? "LIMIT" : + request.OrderType == OrderType.Stop ? "STOP" : null, + type = request.OrderType == OrderType.Limit ? "LIMIT" : + request.OrderType == OrderType.Market ? "MARKET" : null + })); + var result = await MakeJsonRequestAsync("/spot/api/v3/order", + requestMethod: "POST", payload: payload); + return Extract2(result, token => + { + var status = ExchangeAPIOrderResult.Unknown; + switch (token["status"].Value()) + { + case 2: + status = ExchangeAPIOrderResult.Pending; + break; + case 4: + status = ExchangeAPIOrderResult.Filled; + break; + case 5: + status = ExchangeAPIOrderResult.FilledPartially; + break; + case 6: + status = ExchangeAPIOrderResult.Canceled; + break; + case 9: //trigger inserted + case 10: //trigger activated + status = ExchangeAPIOrderResult.Pending; + break; + case 15: //rejected + status = ExchangeAPIOrderResult.Error; + break; + case 16: //not found + status = ExchangeAPIOrderResult.Unknown; + break; + } + + return new ExchangeOrderResult() + { + Message = token["message"].Value(), + OrderId = token["orderID"].Value(), + IsBuy = token["orderType"].Value().ToLowerInvariant() == "buy", + Price = token["price"].Value(), + MarketSymbol = token["symbol"].Value(), + Result = status, + Amount = token["size"].Value(), + OrderDate = token["timestamp"].ConvertInvariant().UnixTimeStampToDateTimeMilliseconds(), + }; + }).ToArray(); + } + + private Dictionary Extract(JToken token, Func processor) + { + if (token is JArray resultArr) + { + return resultArr.Select(processor.Invoke) + .ToDictionary(tuple => tuple.Item1, tuple => tuple.Item2); + } + + var resItem = processor.Invoke(token); + return new Dictionary() + { + {resItem.Item1, resItem.Item2} + }; + } + + private IEnumerable Extract2(JToken token, Func processor) + { + if (token is JArray resultArr) + { + return resultArr.Select(processor.Invoke); + } + + return new List() + { + processor.Invoke(token) + }; + } + protected override Uri ProcessRequestUrl(UriBuilder url, Dictionary payload, string method) { - if ( method == "GET" && (payload?.Count??0) != 0) + if (method == "GET" && (payload?.Count ?? 0) != 0 && !payload.ContainsKey("nonce")) { url.AppendPayloadToQuery(payload); } + return base.ProcessRequestUrl(url, payload, method); } + + protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + { + if (CanMakeAuthenticatedRequest(payload)) + { + if (payload.TryGetValue("body", out var body)) + { + payload.Remove("body"); + } + + var nonce = payload["nonce"].ToString(); + payload.Remove("nonce"); + + + var json = JsonConvert.SerializeObject(body ?? payload); + if (json == "{}") + { + json = ""; + } + + var hexSha384 = CryptoUtility.SHA384Sign( + $"{request.RequestUri.PathAndQuery.Replace("/spot", string.Empty)}{nonce}{json}", + PrivateApiKey.ToUnsecureString()); + request.AddHeader("btse-sign", hexSha384); + request.AddHeader("btse-api", PublicApiKey.ToUnsecureString()); + await request.WriteToRequestAsync(json); + } + + await base.ProcessRequestAsync(request, payload); + } + + protected override async Task> GetNoncePayloadAsync() + { + var result = await base.GetNoncePayloadAsync(); + if (result.ContainsKey("recvWindow")) + { + result.Remove("recvWindow"); + } + + return result; + } } - public partial class ExchangeName { public const string BTSE = "BTSE"; } + public partial class ExchangeName + { + public const string BTSE = "BTSE"; + } }