diff --git a/.editorconfig b/.editorconfig index 1e7cbe749..d14dbc6ed 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,7 +2,7 @@ root = true [*.cs] indent_style = tab -indent_size = 4 +indent_size = 2 charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..e4c255ede --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "josefpihrt-vscode.roslynator", + "ms-dotnettools.csharp" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..559ab8958 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,22 @@ +{ + "workbench.colorCustomizations": { + "activityBar.background": "#753ca1", + "activityBar.foreground": "#ffffff", + "activityBar.inactiveForeground": "#ffffff99", + "activityBarBadge.background": "#d211fd", + "activityBarBadge.foreground": "#ffffff", + "statusBar.noFolderBackground": "#642596", + "statusBar.background": "#753ca1", + "statusBar.foreground": "#ffffff", + "statusBarItem.hoverBackground": "#642596", + "titleBar.activeBackground": "#753ca1", + "titleBar.activeForeground": "#ffffff" + }, + "[csharp]": { + "editor.tabSize": 2, + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.defaultFormatter": "ms-dotnettools.csharp" + }, + "editor.renderWhitespace": "all" +} diff --git a/README.md b/README.md index f27bdce6d..ce573649d 100644 --- a/README.md +++ b/README.md @@ -6,67 +6,67 @@ ## Overview -ExchangeSharp is a C# **framework/lib** and [console app](#Installing-the-CLI) for trading and communicating with [various](#Exchanges) exchange API end points for cryptocurrency assets. Many exchanges are supported, along with [web sockets](#Websockets), withdraws and more! +ExchangeSharp is a C# **framework/lib** and [console app](#installing-the-cli) for trading and communicating with [various](#exchanges) exchange API end points for cryptocurrency assets. Many exchanges are supported, along with [web sockets](#websockets), withdraws and more! -Feel free to visit the discord channel at https://discord.gg/58ktxXuTVK and chat with other developers. +Feel free to visit the discord channel at and chat with other developers. ### Features -- Many exchanges supported with public, private and web socket API +- Many [exchanges](/src//ExchangeSharp/API/Exchanges/) supported with public, private and web socket API - Easy to use code and API - Optional global market symbol normalization, since each exchange has their own way of doing market symbols - Runs anywhere .NET runs. (Windows, Mac, Linux, Containers, Serverless, iOS, Android, [etc.](https://docs.microsoft.com/en-us/dotnet/core/about)) - Can be used from [many different C# platforms](https://github.com/dotnet/standard/blob/master/docs/versions/netstandard2.0.md#platform-support) -- Has a great [CLI](#Installing-the-CLI) that enables you to use all features from all exchanges right from your command line. +- Has a great [CLI](#installing-the-cli) that enables you to use all features from all exchanges right from your command line. ### Exchanges The following cryptocurrency exchanges are supported: (Web socket key: T = tickers, R = trades, B = orderbook / delta orderbook, O = private orders, U = user data) -| Exchange Name | Public REST | Private REST | Web Socket | Notes | -| -------------- | ----------- | ------------ | ---------- | ---------------------------------------- | -| ApolloX | x | x | T R B O U | -| Aquanow | wip | x | | -| Binance | x | x | T R B O U | -| Binance Jersey | x | x | T R B O U | Ceased operations -| Binance.US | x | x | T R B O U | -| Binance DEX | | | R | -| Bitbank | x | x | | -| Bitfinex | x | x | T R O | -| Bitflyer | | | R | -| Bithumb | x | | R | -| BitMEX | x | x | R O | -| Bitstamp | x | x | R | -| Bittrex | x | x | T R | -| BL3P | x | x | R B | Trades stream does not send trade's ids. | -| Bleutrade | x | x | | -| BtcTurk | | | R | -| BTSE | x | x | | -| Bybit | x | x | R | Has public method for Websocket Positions -| Coinbase | x | x | T R O U | -| Coincheck | | | R | -| Coinmate | x | x | | -| Crypto.com | | | R | -| Digifinex | x | x | R B | -| Dydx | | | R | -| FTX | x | x | T R | -| FTX.us | x | x | T R | -| gate.io | x | x | R | -| Gemini | x | x | T R B | -| HitBTC | x | x | R | -| Huobi | x | x | R B | -| Kraken | x | x | R | Dark order symbols not supported | -| KuCoin | x | x | T R | -| LBank | x | x | R | -| Livecoin | x | x | | -| NDAX | x | x | T R | -| OKCoin | x | x | R B | -| OKEx | x | x | T R B O | -| Poloniex | x | x | T R B | -| UPbit | | | R | -| YoBit | x | x | | -| ZB.com | wip | | R | +| Exchange Name | Public REST | Private REST | Web Socket | Notes | +| ----------------------- | ----------- | ------------ | ------------- | ------------------------------------------- | +| ApolloX | x | x | T R B O U | | +| Aquanow | wip | x | | | +| Binance | x | x | T R B O U | | +| ~~Binance Jersey~~ | ~~x~~ | ~~x~~ | ~~T R B O U~~ | Ceased operations | +| Binance.US | x | x | T R B O U | | +| Binance DEX | | | R | | +| Bitbank | x | x | | | +| Bitfinex | x | x | T R O | | +| Bitflyer | | | R | | +| Bithumb | x | | R | | +| BitMEX | x | x | R O | | +| Bitstamp | x | x | R | | +| Bittrex | x | x | T R | | +| BL3P | x | x | R B | Trades stream does not send trade's ids. | +| Bleutrade | x | x | | | +| BtcTurk | | | R | | +| BTSE | x | x | | | +| Bybit | x | x | R | Has public method for Websocket Positions | +| Coinbase (Pro) | x | x | T R O U | | +| Coincheck | | | R | | +| Coinmate | x | x | | | +| Crypto.com | | | R | | +| Digifinex | x | x | R B | | +| Dydx | | | R | | +| FTX | x | x | T R | | +| FTX.us | x | x | T R | | +| gate.io | x | x | R | | +| Gemini | x | x | T R B | | +| HitBTC | x | x | R | | +| Huobi | x | x | R B | | +| Kraken | x | x | R | Dark order symbols not supported | +| KuCoin | x | x | T R | | +| LBank | x | x | R | | +| Livecoin | x | x | | | +| NDAX | x | x | T R | | +| OKCoin | x | x | R B | | +| OKEx | x | x | T R B O | | +| Poloniex | x | x | T R B | | +| UPbit | | | R | | +| YoBit | x | x | | | +| ZB.com | wip | | R | | The following cryptocurrency services are supported: @@ -169,7 +169,7 @@ Please read the [contributing guideline](CONTRIBUTING.md) **before** submitting ### Consulting -I'm happy to make customizations to the software for you and keep in private repo, email exchangesharp@digitalruby.com. +I'm happy to make customizations to the software for you and keep in private repo, email . ### Donations Gratefully Accepted @@ -189,9 +189,8 @@ Donation totals: Thanks for visiting! Jeff Johnson -jeff@digitalruby.com -http://www.digitalruby.com + + [nuget]: https://www.nuget.org/packages/DigitalRuby.ExchangeSharp/ [websocket4net]: https://github.com/kerryjiang/WebSocket4Net - diff --git a/src/ExchangeSharp.Forms/Forms/PlotForm.Designer.cs b/src/ExchangeSharp.Forms/Forms/PlotForm.Designer.cs index 39f8aad15..8013b400d 100644 --- a/src/ExchangeSharp.Forms/Forms/PlotForm.Designer.cs +++ b/src/ExchangeSharp.Forms/Forms/PlotForm.Designer.cs @@ -22,7 +22,7 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - #region Windows Form Designer generated code + #region Windows Form Designer generated code /// /// Required method for Designer support - do not modify @@ -58,7 +58,7 @@ private void InitializeComponent() } - #endregion + #endregion private System.Windows.Forms.DataVisualization.Charting.Chart PlotChart; } diff --git a/src/ExchangeSharp.Forms/Forms/PlotForm.cs b/src/ExchangeSharp.Forms/Forms/PlotForm.cs index 5ebc81e2c..c1e9bb323 100644 --- a/src/ExchangeSharp.Forms/Forms/PlotForm.cs +++ b/src/ExchangeSharp.Forms/Forms/PlotForm.cs @@ -81,7 +81,11 @@ public PlotForm() InitializeComponent(); } - public void SetPlotPoints(List>> points, List> buyPrices = null, List> sellPrices = null) + public void SetPlotPoints( + List>> points, + List> buyPrices = null, + List> sellPrices = null + ) { // clear the chart PlotChart.Series.Clear(); @@ -152,10 +156,7 @@ public static class PlotFormExtensions { public static void ShowPlotForm(this Trader trader) { - PlotForm form = new PlotForm - { - WindowState = FormWindowState.Maximized - }; + PlotForm form = new PlotForm { WindowState = FormWindowState.Maximized }; form.SetPlotPoints(trader.PlotPoints, trader.BuyPrices, trader.SellPrices); form.ShowDialog(); } diff --git a/src/ExchangeSharp/API/Common/APIException.cs b/src/ExchangeSharp/API/Common/APIException.cs index 9fde84442..9733629af 100644 --- a/src/ExchangeSharp/API/Common/APIException.cs +++ b/src/ExchangeSharp/API/Common/APIException.cs @@ -14,22 +14,24 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp { - /// - /// Exception class for API calls - /// - public class APIException : Exception - { - /// - /// Constructor - /// - /// Message - public APIException(string message) : base(message) { } + /// + /// Exception class for API calls + /// + public class APIException : Exception + { + /// + /// Constructor + /// + /// Message + public APIException(string message) + : base(message) { } - /// - /// Constructor - /// - /// - /// Inner exception - public APIException(string message, Exception innerException) : base(message, innerException) { } - } -} \ No newline at end of file + /// + /// Constructor + /// + /// + /// Inner exception + public APIException(string message, Exception innerException) + : base(message, innerException) { } + } +} diff --git a/src/ExchangeSharp/API/Common/APIRequestMaker.cs b/src/ExchangeSharp/API/Common/APIRequestMaker.cs index 1236561b5..4992491b6 100644 --- a/src/ExchangeSharp/API/Common/APIRequestMaker.cs +++ b/src/ExchangeSharp/API/Common/APIRequestMaker.cs @@ -37,10 +37,7 @@ public sealed class APIRequestMaker : IAPIRequestMaker public static IWebProxy? Proxy { get => ClientHandler.Proxy; - set - { - ClientHandler.Proxy = value; - } + set { ClientHandler.Proxy = value; } } public static readonly HttpClient Client = new HttpClient(ClientHandler); @@ -105,7 +102,6 @@ public int ReadWriteTimeout set => Timeout = value; } - public Task WriteAllAsync(byte[] data, int index, int length) { Request.Content = new ByteArrayContent(data, index, length); @@ -139,7 +135,10 @@ public Dictionary> Headers { get { - return response.Headers.ToDictionary(x => x.Key, x => (IReadOnlyList)x.Value.ToArray()); + return response.Headers.ToDictionary( + x => x.Key, + x => (IReadOnlyList)x.Value.ToArray() + ); } } } @@ -162,7 +161,12 @@ public APIRequestMaker(IAPIRequestHandler api) /// The encoding of payload is API dependant but is typically json. /// Request method or null for default. Example: 'GET' or 'POST'. /// Raw response - public async Task> MakeRequestAsync(string url, string? baseUrl = null, Dictionary? payload = null, string? method = null) + public async Task> MakeRequestAsync( + string url, + string? baseUrl = null, + Dictionary? payload = null, + string? method = null + ) { await new SynchronizationContextRemover(); await api.RateLimit.WaitToProceedAsync(); @@ -189,7 +193,7 @@ public APIRequestMaker(IAPIRequestHandler api) using var cancel = new CancellationTokenSource(request.Timeout); try { - RequestStateChanged?.Invoke(this, RequestMakerState.Begin, uri.AbsoluteUri);// when start make a request we send the uri, this helps developers to track the http requests. + RequestStateChanged?.Invoke(this, RequestMakerState.Begin, uri.AbsoluteUri); // when start make a request we send the uri, this helps developers to track the http requests. response = await Client.SendAsync(request.Request, cancel.Token); if (response == null) { @@ -197,12 +201,21 @@ public APIRequestMaker(IAPIRequestHandler api) } responseString = await response.Content.ReadAsStringAsync(); - if (response.StatusCode != HttpStatusCode.OK && response.StatusCode != HttpStatusCode.Created) + if ( + response.StatusCode != HttpStatusCode.OK + && response.StatusCode != HttpStatusCode.Created + ) { // 404 maybe return empty responseString if (string.IsNullOrWhiteSpace(responseString)) { - throw new APIException(string.Format("{0} - {1}", response.StatusCode.ConvertInvariant(), response.StatusCode)); + throw new APIException( + string.Format( + "{0} - {1}", + response.StatusCode.ConvertInvariant(), + response.StatusCode + ) + ); } throw new APIException(responseString); @@ -225,12 +238,21 @@ public APIRequestMaker(IAPIRequestHandler api) { response?.Dispose(); } - return new IAPIRequestMaker.RequestResult() { Response = responseString, HTTPHeaderDate = response.Headers.Date }; + return new IAPIRequestMaker.RequestResult() + { + Response = responseString, + HTTPHeaderDate = response.Headers.Date + }; } /// /// An action to execute when a request has been made (this request and state and object (response or exception)) /// - public Action? RequestStateChanged { get; set; } + public Action< + IAPIRequestMaker, + RequestMakerState, + object + >? RequestStateChanged + { get; set; } } } diff --git a/src/ExchangeSharp/API/Common/BaseAPI.cs b/src/ExchangeSharp/API/Common/BaseAPI.cs index f9cc983b2..60727522d 100644 --- a/src/ExchangeSharp/API/Common/BaseAPI.cs +++ b/src/ExchangeSharp/API/Common/BaseAPI.cs @@ -130,9 +130,11 @@ public abstract class BaseAPI : IAPIRequestHandler, INamed /// /// User agent for requests /// - public const string RequestUserAgent = "ExchangeSharp (https://github.com/jjxtra/ExchangeSharp)"; + public const string RequestUserAgent = + "ExchangeSharp (https://github.com/jjxtra/ExchangeSharp)"; private IAPIRequestMaker requestMaker; + /// /// API request maker /// @@ -140,8 +142,8 @@ public IAPIRequestMaker RequestMaker { get { return requestMaker; } set { requestMaker = value ?? new APIRequestMaker(this); } - } + /// /// Base URL for the API /// @@ -178,15 +180,21 @@ public IAPIRequestMaker RequestMaker /// public System.Security.SecureString? Passphrase { get; set; } - private static readonly ConcurrentDictionary rateLimiters = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary rateLimiters = + new ConcurrentDictionary(); private RateGate rateGate; + /// /// Rate limiter - set this to a new limit if you are seeing your ip get blocked by the API /// One rate limiter is created per type of api /// public RateGate RateLimit { - get => rateGate ??= rateLimiters.GetOrAdd(GetType(), v => new RateGate(5, TimeSpan.FromSeconds(15.0d))); + get => + rateGate ??= rateLimiters.GetOrAdd( + GetType(), + v => new RateGate(5, TimeSpan.FromSeconds(15.0d)) + ); set => rateLimiters[GetType()] = rateGate = value; } @@ -239,25 +247,31 @@ public RateGate RateLimit /// /// Cache policy - defaults to no cache, don't change unless you have specific needs /// - public System.Net.Cache.RequestCachePolicy RequestCachePolicy { get; set; } = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.NoCacheNoStore); + public System.Net.Cache.RequestCachePolicy RequestCachePolicy { get; set; } = + new System.Net.Cache.RequestCachePolicy( + System.Net.Cache.RequestCacheLevel.NoCacheNoStore + ); /// /// Method cache policy (method name, time to cache) /// Can be cleared for no caching, or you can put in custom cache times using nameof(method) and timespan. /// - public Dictionary MethodCachePolicy { get; } = new Dictionary(); + public Dictionary MethodCachePolicy { get; } = + new Dictionary(); - public static JsonSerializerSettings SerializerSettings { get; } = new JsonSerializerSettings - { - FloatParseHandling = FloatParseHandling.Decimal, - NullValueHandling = NullValueHandling.Ignore, - ContractResolver = new DefaultContractResolver - { - NamingStrategy = new SnakeCaseNamingStrategy() - }, - }; + public static JsonSerializerSettings SerializerSettings { get; } = + new JsonSerializerSettings + { + FloatParseHandling = FloatParseHandling.Decimal, + NullValueHandling = NullValueHandling.Ignore, + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new SnakeCaseNamingStrategy() + }, + }; private ICache cache = new MemoryCache(); + /// /// Get or set the current cache. Defaults to MemoryCache. /// @@ -273,37 +287,42 @@ public ICache Cache private decimal lastNonce; - private readonly string[] resultKeys = new string[] { "result", "data", "return", "Result", "Data", "Return" }; + private readonly string[] resultKeys = new string[] + { + "result", + "data", + "return", + "Result", + "Data", + "Return" + }; /// /// Static constructor /// static BaseAPI() { - #pragma warning disable CS0618 try { - #if HAS_WINDOWS_FORMS // NET47 - ServicePointManager.SecurityProtocol = SecurityProtocolType.SystemDefault | SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12; + ServicePointManager.SecurityProtocol = + SecurityProtocolType.SystemDefault + | SecurityProtocolType.Tls + | SecurityProtocolType.Tls11 + | SecurityProtocolType.Tls12; #else ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; #endif - - } - catch - { - } + catch { } #pragma warning restore CS0618 - } /// @@ -317,7 +336,12 @@ public BaseAPI() object[] nameAttributes = GetType().GetCustomAttributes(typeof(ApiNameAttribute), true); if (nameAttributes == null || nameAttributes.Length == 0) { - Name = Regex.Replace(className, "^Exchange|API$", string.Empty, RegexOptions.CultureInvariant); + Name = Regex.Replace( + className, + "^Exchange|API$", + string.Empty, + RegexOptions.CultureInvariant + ); } else { @@ -373,7 +397,9 @@ public async Task GenerateNonceAsync() break; case NonceStyle.UnixMillisecondsString: - nonce = ((long)now.UnixTimestampFromDateTimeMilliseconds()).ToStringInvariant(); + nonce = ( + (long)now.UnixTimestampFromDateTimeMilliseconds() + ).ToStringInvariant(); break; case NonceStyle.UnixMillisecondsThenIncrement: @@ -400,19 +426,29 @@ public async Task GenerateNonceAsync() { // why an API would use a persistent incrementing counter for nonce is beyond me, ticks is so much better with a sliding window... // making it required to increment by 1 is also a pain - especially when restarting a process or rebooting. - string tempFile = Path.Combine(Path.GetTempPath(), (PublicApiKey?.ToUnsecureString() ?? "unknown_pub_key") + ".nonce"); + string tempFile = Path.Combine( + Path.GetTempPath(), + (PublicApiKey?.ToUnsecureString() ?? "unknown_pub_key") + ".nonce" + ); if (!File.Exists(tempFile)) { File.WriteAllText(tempFile, "0"); } unchecked { - long longNonce = File.ReadAllText(tempFile).ConvertInvariant() + 1; - long maxValue = (NonceStyle == NonceStyle.Int32File ? int.MaxValue : long.MaxValue); + long longNonce = + File.ReadAllText(tempFile).ConvertInvariant() + 1; + long maxValue = ( + NonceStyle == NonceStyle.Int32File + ? int.MaxValue + : long.MaxValue + ); if (longNonce < 1 || longNonce > maxValue) { - throw new APIException($"Nonce {longNonce.ToStringInvariant()} is out of bounds, valid ranges are 1 to {maxValue.ToStringInvariant()}, " + - $"please regenerate new API keys. Please contact {Name} API support and ask them to change to a sensible nonce algorithm."); + throw new APIException( + $"Nonce {longNonce.ToStringInvariant()} is out of bounds, valid ranges are 1 to {maxValue.ToStringInvariant()}, " + + $"please regenerate new API keys. Please contact {Name} API support and ask them to change to a sensible nonce algorithm." + ); } File.WriteAllText(tempFile, longNonce.ToStringInvariant()); nonce = longNonce; @@ -434,7 +470,11 @@ public async Task GenerateNonceAsync() // check for duplicate nonce decimal convertedNonce = nonce.ConvertInvariant(); - if (lastNonce != convertedNonce || NonceStyle == NonceStyle.ExpiresUnixSeconds || NonceStyle == NonceStyle.ExpiresUnixMilliseconds) + if ( + lastNonce != convertedNonce + || NonceStyle == NonceStyle.ExpiresUnixSeconds + || NonceStyle == NonceStyle.ExpiresUnixMilliseconds + ) { lastNonce = convertedNonce; break; @@ -464,7 +504,7 @@ public void LoadAPIKeys(string encryptedFile) if (strings.Length < 2) { throw new InvalidOperationException( - "Encrypted keys file should have at least a public and private key, and an optional pass phrase" + "Encrypted keys file should have at least a public and private key, and an optional pass phrase" ); } @@ -482,7 +522,11 @@ public void LoadAPIKeys(string encryptedFile) /// Public Api Key /// Private Api Key /// Pass phrase, null for none - public void LoadAPIKeysUnsecure(string publicApiKey, string privateApiKey, string? passPhrase = null) + public void LoadAPIKeysUnsecure( + string publicApiKey, + string privateApiKey, + string? passPhrase = null + ) { PublicApiKey = publicApiKey.ToSecureString(); PrivateApiKey = privateApiKey.ToSecureString(); @@ -498,7 +542,18 @@ public void LoadAPIKeysUnsecure(string publicApiKey, string privateApiKey, strin /// The encoding of payload is API dependant but is typically json. /// Request method or null for default /// Raw response - public async Task> MakeRequestAsync(string url, string? baseUrl = null, Dictionary? payload = null, string? method = null) => await requestMaker.MakeRequestAsync(url, baseUrl: baseUrl, payload: payload, method: method); + public async Task> MakeRequestAsync( + string url, + string? baseUrl = null, + Dictionary? payload = null, + string? method = null + ) => + await requestMaker.MakeRequestAsync( + url, + baseUrl: baseUrl, + payload: payload, + method: method + ); /// /// Make a JSON request to an API end point @@ -509,7 +564,20 @@ public void LoadAPIKeysUnsecure(string publicApiKey, string privateApiKey, strin /// Payload, can be null. For private API end points, the payload must contain a 'nonce' key set to GenerateNonce value. /// Request method or null for default /// Result decoded from JSON response - public async Task MakeJsonRequestAsync(string url, string? baseUrl = null, Dictionary? payload = null, string? requestMethod = null) => (await MakeJsonRequestFullAsync(url, baseUrl: baseUrl, payload: payload, requestMethod: requestMethod)).Response; + public async Task MakeJsonRequestAsync( + string url, + string? baseUrl = null, + Dictionary? payload = null, + string? requestMethod = null + ) => + ( + await MakeJsonRequestFullAsync( + url, + baseUrl: baseUrl, + payload: payload, + requestMethod: requestMethod + ) + ).Response; /// /// Make a JSON request to an API end point, with full retun result @@ -520,17 +588,35 @@ public void LoadAPIKeysUnsecure(string publicApiKey, string privateApiKey, strin /// Payload, can be null. For private API end points, the payload must contain a 'nonce' key set to GenerateNonce value. /// Request method or null for default /// full return result, including result decoded from JSON response - public async Task> MakeJsonRequestFullAsync(string url, string? baseUrl = null, Dictionary? payload = null, string? requestMethod = null) + public async Task> MakeJsonRequestFullAsync( + string url, + string? baseUrl = null, + Dictionary? payload = null, + string? requestMethod = null + ) { await new SynchronizationContextRemover(); - var result = await MakeRequestAsync(url, baseUrl: baseUrl, payload: payload, method: requestMethod); + var result = await MakeRequestAsync( + url, + baseUrl: baseUrl, + payload: payload, + method: requestMethod + ); T jsonResult = JsonConvert.DeserializeObject(result.Response, SerializerSettings); if (jsonResult is JToken token) { - return new IAPIRequestMaker.RequestResult() { Response = (T)(object)CheckJsonResponse(token), HTTPHeaderDate = result.HTTPHeaderDate }; + return new IAPIRequestMaker.RequestResult() + { + Response = (T)(object)CheckJsonResponse(token), + HTTPHeaderDate = result.HTTPHeaderDate + }; } - return new IAPIRequestMaker.RequestResult() { Response = jsonResult, HTTPHeaderDate = result.HTTPHeaderDate }; + return new IAPIRequestMaker.RequestResult() + { + Response = jsonResult, + HTTPHeaderDate = result.HTTPHeaderDate + }; } /// @@ -540,12 +626,11 @@ public void LoadAPIKeysUnsecure(string publicApiKey, string privateApiKey, strin /// Callback for messages /// Connect callback /// Web socket - dispose of the wrapper to shutdown the socket - public virtual Task ConnectWebSocketAsync - ( - string url, - Func messageCallback, - WebSocketConnectionDelegate? connectCallback = null, - WebSocketConnectionDelegate? disconnectCallback = null + public virtual Task ConnectWebSocketAsync( + string url, + Func messageCallback, + WebSocketConnectionDelegate? connectCallback = null, + WebSocketConnectionDelegate? disconnectCallback = null ) { if (messageCallback == null) @@ -581,12 +666,11 @@ public virtual Task ConnectWebSocketAsync /// Callback for messages /// Connect callback /// Web socket - dispose of the wrapper to shutdown the socket - public virtual Task ConnectPublicWebSocketAsync - ( - string url, - Func messageCallback, - WebSocketConnectionDelegate? connectCallback = null, - WebSocketConnectionDelegate? disconnectCallback = null + public virtual Task ConnectPublicWebSocketAsync( + string url, + Func messageCallback, + WebSocketConnectionDelegate? connectCallback = null, + WebSocketConnectionDelegate? disconnectCallback = null ) { if (messageCallback == null) @@ -596,7 +680,12 @@ public virtual Task ConnectPublicWebSocketAsync string fullUrl = BaseUrlWebSocket + (url ?? string.Empty); - return ConnectWebSocketAsync(fullUrl, messageCallback, connectCallback, disconnectCallback); + return ConnectWebSocketAsync( + fullUrl, + messageCallback, + connectCallback, + disconnectCallback + ); } /// @@ -606,12 +695,11 @@ public virtual Task ConnectPublicWebSocketAsync /// Callback for messages /// Connect callback /// Web socket - dispose of the wrapper to shutdown the socket - public virtual Task ConnectPrivateWebSocketAsync - ( - string url, - Func messageCallback, - WebSocketConnectionDelegate? connectCallback = null, - WebSocketConnectionDelegate? disconnectCallback = null + public virtual Task ConnectPrivateWebSocketAsync( + string url, + Func messageCallback, + WebSocketConnectionDelegate? connectCallback = null, + WebSocketConnectionDelegate? disconnectCallback = null ) { if (messageCallback == null) @@ -619,9 +707,17 @@ public virtual Task ConnectPrivateWebSocketAsync throw new ArgumentNullException(nameof(messageCallback)); } - string fullUrl = BaseUrlPrivateWebSocket == string.Empty ? BaseUrlWebSocket : BaseUrlPrivateWebSocket + (url ?? string.Empty); - - return ConnectWebSocketAsync(fullUrl, messageCallback, connectCallback, disconnectCallback); + string fullUrl = + BaseUrlPrivateWebSocket == string.Empty + ? BaseUrlWebSocket + : BaseUrlPrivateWebSocket + (url ?? string.Empty); + + return ConnectWebSocketAsync( + fullUrl, + messageCallback, + connectCallback, + disconnectCallback + ); } /// @@ -629,9 +725,16 @@ public virtual Task ConnectPrivateWebSocketAsync /// /// Payload to potentially send /// True if an authenticated request can be made with the payload, false otherwise - protected virtual bool CanMakeAuthenticatedRequest(IReadOnlyDictionary? payload) + protected virtual bool CanMakeAuthenticatedRequest( + IReadOnlyDictionary? payload + ) { - return (PrivateApiKey != null && PublicApiKey != null && payload != null && payload.ContainsKey("nonce")); + return ( + PrivateApiKey != null + && PublicApiKey != null + && payload != null + && payload.ContainsKey("nonce") + ); } /// @@ -640,7 +743,10 @@ protected virtual bool CanMakeAuthenticatedRequest(IReadOnlyDictionary /// Request /// Payload - protected virtual Task ProcessRequestAsync(IHttpWebRequest request, Dictionary? payload) + protected virtual Task ProcessRequestAsync( + IHttpWebRequest request, + Dictionary? payload + ) { return Task.CompletedTask; } @@ -649,10 +755,7 @@ protected virtual Task ProcessRequestAsync(IHttpWebRequest request, Dictionary /// Response - protected virtual void ProcessResponse(IHttpWebResponse response) - { - - } + protected virtual void ProcessResponse(IHttpWebResponse response) { } /// /// Process a request url @@ -661,7 +764,11 @@ protected virtual void ProcessResponse(IHttpWebResponse response) /// Payload /// Method /// Updated url - protected virtual Uri ProcessRequestUrl(UriBuilder url, Dictionary? payload, string? method) + protected virtual Uri ProcessRequestUrl( + UriBuilder url, + Dictionary? payload, + string? method + ) { return url.Uri; } @@ -685,16 +792,18 @@ protected virtual JToken CheckJsonResponse(JToken result) } else if (!(result is JArray) && result.Type == JTokenType.Object) { - if - ( - (!string.IsNullOrWhiteSpace(result["error"].ToStringInvariant())) || - (!string.IsNullOrWhiteSpace(result["errorCode"].ToStringInvariant())) || - (!string.IsNullOrWhiteSpace(result["error_code"].ToStringInvariant())) || - (result["status"].ToStringInvariant() == "error") || - (result["Status"].ToStringInvariant() == "error") || - (result["success"] != null && !result["success"].ConvertInvariant()) || - (result["Success"] != null && !result["Success"].ConvertInvariant()) || - (!string.IsNullOrWhiteSpace(result["ok"].ToStringInvariant()) && result["ok"].ToStringInvariant().ToLowerInvariant() != "ok") + if ( + (!string.IsNullOrWhiteSpace(result["error"].ToStringInvariant())) + || (!string.IsNullOrWhiteSpace(result["errorCode"].ToStringInvariant())) + || (!string.IsNullOrWhiteSpace(result["error_code"].ToStringInvariant())) + || (result["status"].ToStringInvariant() == "error") + || (result["Status"].ToStringInvariant() == "error") + || (result["success"] != null && !result["success"].ConvertInvariant()) + || (result["Success"] != null && !result["Success"].ConvertInvariant()) + || ( + !string.IsNullOrWhiteSpace(result["ok"].ToStringInvariant()) + && result["ok"].ToStringInvariant().ToLowerInvariant() != "ok" + ) ) { throw new APIException(result.ToStringInvariant()); @@ -704,7 +813,13 @@ protected virtual JToken CheckJsonResponse(JToken result) foreach (string key in resultKeys) { JToken possibleResult = result[key]; - if (possibleResult != null && (possibleResult.Type == JTokenType.Object || possibleResult.Type == JTokenType.Array)) + if ( + possibleResult != null + && ( + possibleResult.Type == JTokenType.Object + || possibleResult.Type == JTokenType.Array + ) + ) { result = possibleResult; break; @@ -749,8 +864,16 @@ protected virtual async Task OnGetNonceOffset() DateTime serverDate = NonceEndPointStyle switch { NonceStyle.Iso8601 => value.ToDateTimeInvariant(), - NonceStyle.UnixMilliseconds => value.ConvertInvariant().UnixTimeStampToDateTimeMilliseconds(), - _ => throw new ArgumentException("Invalid nonce end point style '" + NonceEndPointStyle + "' for exchange '" + Name + "'"), + NonceStyle.UnixMilliseconds + => value.ConvertInvariant().UnixTimeStampToDateTimeMilliseconds(), + _ + => throw new ArgumentException( + "Invalid nonce end point style '" + + NonceEndPointStyle + + "' for exchange '" + + Name + + "'" + ), }; NonceOffset = (CryptoUtility.UtcNow - serverDate); } @@ -761,7 +884,10 @@ protected virtual async Task OnGetNonceOffset() } } - async Task IAPIRequestHandler.ProcessRequestAsync(IHttpWebRequest request, Dictionary? payload) + async Task IAPIRequestHandler.ProcessRequestAsync( + IHttpWebRequest request, + Dictionary? payload + ) { await ProcessRequestAsync(request, payload); } @@ -771,7 +897,11 @@ void IAPIRequestHandler.ProcessResponse(IHttpWebResponse response) ProcessResponse(response); } - Uri IAPIRequestHandler.ProcessRequestUrl(UriBuilder url, Dictionary? payload, string? method) + Uri IAPIRequestHandler.ProcessRequestUrl( + UriBuilder url, + Dictionary? payload, + string? method + ) { return ProcessRequestUrl(url, payload, method); } diff --git a/src/ExchangeSharp/API/Common/IAPIRequestMaker.cs b/src/ExchangeSharp/API/Common/IAPIRequestMaker.cs index 859679627..a00b8ab85 100644 --- a/src/ExchangeSharp/API/Common/IAPIRequestMaker.cs +++ b/src/ExchangeSharp/API/Common/IAPIRequestMaker.cs @@ -17,32 +17,32 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp { - /// - /// Request maker states - /// - public enum RequestMakerState - { - /// - /// About to begin request - /// - Begin, - - /// - /// Request finished successfully - /// - Finished, - - /// - /// Request error - /// - Error - } - - /// - /// Interface for making API requests - /// - public interface IAPIRequestMaker - { + /// + /// Request maker states + /// + public enum RequestMakerState + { + /// + /// About to begin request + /// + Begin, + + /// + /// Request finished successfully + /// + Finished, + + /// + /// Request error + /// + Error + } + + /// + /// Interface for making API requests + /// + public interface IAPIRequestMaker + { public class RequestResult { /// request response @@ -62,129 +62,134 @@ public class RequestResult /// Request method or null for default /// Raw response /// Request fails - Task> MakeRequestAsync(string url, string? baseUrl = null, Dictionary? payload = null, string? method = null); - - /// - /// An action to execute when a request has been made (this request and state and object (response or exception)) - /// - Action? RequestStateChanged { get; set; } - } - - /// - /// Http web request - /// - public interface IHttpWebRequest - { - /// - /// Request uri - /// - Uri RequestUri { get; } - - /// - /// Request method (GET, POST, PUT, DELETE, etc.) - /// - string Method { get; set; } - - /// - /// Response timeout - /// - int Timeout { get; set; } - - /// - /// Read/write timeout - /// - int ReadWriteTimeout { get; set; } - - /// - /// Add a header - /// - /// Header - /// Value - void AddHeader(string header, string value); - - /// - /// Write data to the request and then flush, get ready for reading response - /// - /// Data - /// Offset - /// Length - /// - Task WriteAllAsync(byte[] data, int index, int length); - } - - /// - /// Http web response - /// - public interface IHttpWebResponse - { - /// - /// Get header by name - /// - /// Header name - /// Header values, count of 0 if header not exist, will never return null - IReadOnlyList GetHeader(string name); - - /// - /// Headers - /// - Dictionary> Headers { get; } - } - - /// - /// Interface for setting up and handling API request and response - /// - public interface IAPIRequestHandler - { - /// - /// Additional handling for request - /// - /// Request - /// Payload - Task ProcessRequestAsync(IHttpWebRequest request, Dictionary? payload); - - /// - /// Additional handling for response - /// - /// Response - void ProcessResponse(IHttpWebResponse response); - - /// - /// Process a request url - /// - /// Url - /// Payload - /// Method - /// Updated url - Uri ProcessRequestUrl(UriBuilder url, Dictionary? payload, string method); - - /// - /// Base url for the request - /// - string BaseUrl { get; set; } - - /// - /// Request method, i.e. GET - /// - string RequestMethod { get; set; } - - /// - /// Request content type, i.e. application/json - /// - string RequestContentType { get; set; } - - /// - /// Request cache policy - /// - System.Net.Cache.RequestCachePolicy RequestCachePolicy { get; set; } - - /// - /// Request timeout, this will get assigned to the request before sending it off - /// - TimeSpan RequestTimeout { get; set; } - - /// - /// Rate limiter - /// - RateGate RateLimit { get; set; } - } + Task> MakeRequestAsync( + string url, + string? baseUrl = null, + Dictionary? payload = null, + string? method = null + ); + + /// + /// An action to execute when a request has been made (this request and state and object (response or exception)) + /// + Action? RequestStateChanged { get; set; } + } + + /// + /// Http web request + /// + public interface IHttpWebRequest + { + /// + /// Request uri + /// + Uri RequestUri { get; } + + /// + /// Request method (GET, POST, PUT, DELETE, etc.) + /// + string Method { get; set; } + + /// + /// Response timeout + /// + int Timeout { get; set; } + + /// + /// Read/write timeout + /// + int ReadWriteTimeout { get; set; } + + /// + /// Add a header + /// + /// Header + /// Value + void AddHeader(string header, string value); + + /// + /// Write data to the request and then flush, get ready for reading response + /// + /// Data + /// Offset + /// Length + /// + Task WriteAllAsync(byte[] data, int index, int length); + } + + /// + /// Http web response + /// + public interface IHttpWebResponse + { + /// + /// Get header by name + /// + /// Header name + /// Header values, count of 0 if header not exist, will never return null + IReadOnlyList GetHeader(string name); + + /// + /// Headers + /// + Dictionary> Headers { get; } + } + + /// + /// Interface for setting up and handling API request and response + /// + public interface IAPIRequestHandler + { + /// + /// Additional handling for request + /// + /// Request + /// Payload + Task ProcessRequestAsync(IHttpWebRequest request, Dictionary? payload); + + /// + /// Additional handling for response + /// + /// Response + void ProcessResponse(IHttpWebResponse response); + + /// + /// Process a request url + /// + /// Url + /// Payload + /// Method + /// Updated url + Uri ProcessRequestUrl(UriBuilder url, Dictionary? payload, string method); + + /// + /// Base url for the request + /// + string BaseUrl { get; set; } + + /// + /// Request method, i.e. GET + /// + string RequestMethod { get; set; } + + /// + /// Request content type, i.e. application/json + /// + string RequestContentType { get; set; } + + /// + /// Request cache policy + /// + System.Net.Cache.RequestCachePolicy RequestCachePolicy { get; set; } + + /// + /// Request timeout, this will get assigned to the request before sending it off + /// + TimeSpan RequestTimeout { get; set; } + + /// + /// Rate limiter + /// + RateGate RateLimit { get; set; } + } } diff --git a/src/ExchangeSharp/API/Common/IBaseAPI.cs b/src/ExchangeSharp/API/Common/IBaseAPI.cs index c9f1bd3f8..5aee6a973 100644 --- a/src/ExchangeSharp/API/Common/IBaseAPI.cs +++ b/src/ExchangeSharp/API/Common/IBaseAPI.cs @@ -28,10 +28,10 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp { - /// - /// Base interface for all API implementations - /// - public interface IBaseAPI : IAPIRequestHandler, INamed + /// + /// Base interface for all API implementations + /// + public interface IBaseAPI : IAPIRequestHandler, INamed { #region Properties /// @@ -58,30 +58,30 @@ public interface IBaseAPI : IAPIRequestHandler, INamed /// SecureString? PublicApiKey { get; set; } - /// - /// Optional private API key - /// - SecureString? PrivateApiKey { get; set; } + /// + /// Optional private API key + /// + SecureString? PrivateApiKey { get; set; } - /// - /// Pass phrase API key - only needs to be set if you are using private authenticated end points. Please use CryptoUtility.SaveUnprotectedStringsToFile to store your API keys, never store them in plain text! - /// Most exchanges do not require this, but Coinbase is an example of one that does - /// - System.Security.SecureString? Passphrase { get; set; } + /// + /// Pass phrase API key - only needs to be set if you are using private authenticated end points. Please use CryptoUtility.SaveUnprotectedStringsToFile to store your API keys, never store them in plain text! + /// Most exchanges do not require this, but Coinbase is an example of one that does + /// + System.Security.SecureString? Passphrase { get; set; } // RateLimit is in IAPIRequestHandler // RequestTimeout is in IAPIRequestHandler - /// - /// Request window - most services do not use this, but Binance API is an example of one that does - /// - TimeSpan RequestWindow { get; set; } + /// + /// Request window - most services do not use this, but Binance API is an example of one that does + /// + TimeSpan RequestWindow { get; set; } - /// - /// Nonce style - /// - NonceStyle NonceStyle { get; } + /// + /// Nonce style + /// + NonceStyle NonceStyle { get; } /// /// The nonce end point for pulling down a server timestamp - override OnGetNonceOffset if you need custom handling @@ -121,31 +121,40 @@ public interface IBaseAPI : IAPIRequestHandler, INamed /// Encrypted file to load keys from void LoadAPIKeys(string encryptedFile); - /// - /// Load API keys from unsecure strings - /// Public Api Key - /// Private Api Key - /// Pass phrase, null for none - /// - void LoadAPIKeysUnsecure(string publicApiKey, string privateApiKey, string? passPhrase = null); - - /// - /// Generate a nonce - /// - /// Nonce (can be string, long, double, etc., so object is used) - Task GenerateNonceAsync(); - - /// - /// Make a JSON request to an API end point - /// - /// Type of object to parse JSON as - /// Path and query - /// Override the base url, null for the default BaseUrl - /// Payload, can be null. For private API end points, the payload must contain a 'nonce' key set to GenerateNonce value. - /// Request method or null for default - /// Result decoded from JSON response - Task MakeJsonRequestAsync(string url, string? baseUrl = null, Dictionary? payload = null, string? requestMethod = null); - - #endregion Methods - } + /// + /// Load API keys from unsecure strings + /// Public Api Key + /// Private Api Key + /// Pass phrase, null for none + /// + void LoadAPIKeysUnsecure( + string publicApiKey, + string privateApiKey, + string? passPhrase = null + ); + + /// + /// Generate a nonce + /// + /// Nonce (can be string, long, double, etc., so object is used) + Task GenerateNonceAsync(); + + /// + /// Make a JSON request to an API end point + /// + /// Type of object to parse JSON as + /// Path and query + /// Override the base url, null for the default BaseUrl + /// Payload, can be null. For private API end points, the payload must contain a 'nonce' key set to GenerateNonce value. + /// Request method or null for default + /// Result decoded from JSON response + Task MakeJsonRequestAsync( + string url, + string? baseUrl = null, + Dictionary? payload = null, + string? requestMethod = null + ); + + #endregion Methods + } } diff --git a/src/ExchangeSharp/API/Exchanges/ApolloX/ExchangeApolloXApi.cs b/src/ExchangeSharp/API/Exchanges/ApolloX/ExchangeApolloXApi.cs index 72adef6f3..b1848e6fc 100644 --- a/src/ExchangeSharp/API/Exchanges/ApolloX/ExchangeApolloXApi.cs +++ b/src/ExchangeSharp/API/Exchanges/ApolloX/ExchangeApolloXApi.cs @@ -1,10 +1,10 @@ -using ExchangeSharp.BinanceGroup; -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using ExchangeSharp.BinanceGroup; +using Newtonsoft.Json.Linq; namespace ExchangeSharp { @@ -16,5 +16,8 @@ public sealed partial class ExchangeApolloXApi : BinanceGroupCommon public override string BaseUrlApi => $"{BaseUrl}/fapi/v1"; } - public partial class ExchangeName { public const string ApolloX = "ApolloX"; } + public partial class ExchangeName + { + public const string ApolloX = "ApolloX"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/Aquanow/ExchangeAquanowAPI.cs b/src/ExchangeSharp/API/Exchanges/Aquanow/ExchangeAquanowAPI.cs index ebf39afe9..f4525da40 100644 --- a/src/ExchangeSharp/API/Exchanges/Aquanow/ExchangeAquanowAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Aquanow/ExchangeAquanowAPI.cs @@ -12,12 +12,12 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp { - using ExchangeSharp.Aquanow; - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Threading.Tasks; + using ExchangeSharp.Aquanow; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; public sealed partial class ExchangeAquanowAPI : ExchangeAPI { @@ -47,50 +47,69 @@ protected override async Task> OnGetMarketSymbolsAsync() } // NOT SUPPORTED - protected override async Task>> OnGetTickersAsync() + protected override async Task< + IEnumerable> + > OnGetTickersAsync() { - List> tickers = new List>(); + List> tickers = + new List>(); JToken symbols = await MakeJsonRequestAsync("/availablesymbols", MarketUrl); foreach (string symbol in symbols) { - JToken bestPriceSymbol = await MakeJsonRequestAsync($"/bestprice?symbol={symbol}", MarketUrl); + JToken bestPriceSymbol = await MakeJsonRequestAsync( + $"/bestprice?symbol={symbol}", + MarketUrl + ); decimal bid = bestPriceSymbol["bestBid"].ConvertInvariant(); decimal ask = bestPriceSymbol["bestAsk"].ConvertInvariant(); - ExchangeTicker ticker = new ExchangeTicker { Exchange = Name, MarketSymbol = symbol, Bid = bid, Ask = ask, ApiResponse = bestPriceSymbol }; + ExchangeTicker ticker = new ExchangeTicker + { + Exchange = Name, + MarketSymbol = symbol, + Bid = bid, + Ask = ask, + ApiResponse = bestPriceSymbol + }; tickers.Add(new KeyValuePair(symbol, ticker)); } return tickers; } - protected override async Task> OnGetCurrenciesAsync() + protected override async Task< + IReadOnlyDictionary + > OnGetCurrenciesAsync() { var currencies = new Dictionary(); var symbols = await GetMarketSymbolsAsync(); foreach (string symbol in symbols) { - var currency = new ExchangeCurrency - { - Name = symbol - }; + var currency = new ExchangeCurrency { Name = symbol }; currencies[currency.Name] = currency; } return currencies; } - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + protected override async Task ProcessRequestAsync( + IHttpWebRequest request, + Dictionary payload + ) { if (CanMakeAuthenticatedRequest(payload)) { request.AddHeader("content-type", "application/json"); - var sigContent = new signatureContent { httpMethod = request.Method, path = request.RequestUri.LocalPath, nonce = payload["nonce"].ToStringInvariant() }; + var sigContent = new signatureContent + { + httpMethod = request.Method, + path = request.RequestUri.LocalPath, + nonce = payload["nonce"].ToStringInvariant() + }; string json = JsonConvert.SerializeObject(sigContent); string bodyRequest = JsonConvert.SerializeObject(payload); string hexSha384 = CryptoUtility.SHA384Sign(json, PrivateApiKey.ToUnsecureString()); request.AddHeader("x-api-key", PublicApiKey.ToUnsecureString()); request.AddHeader("x-signature", hexSha384); - request.AddHeader("x-nonce", payload["nonce"].ToStringInvariant() - ); + request.AddHeader("x-nonce", payload["nonce"].ToStringInvariant()); if (request.Method == "GET") { await CryptoUtility.WriteToRequestAsync(request, null); @@ -103,20 +122,32 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti } // DONE - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + protected override async Task OnPlaceOrderAsync( + ExchangeOrderRequest order + ) { - if (order.IsPostOnly != null) throw new NotSupportedException("Post Only orders are not supported by this exchange or not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature."); + if (order.IsPostOnly != null) + throw new NotSupportedException( + "Post Only orders are not supported by this exchange or not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature." + ); // In Aquanow market order, when buying crypto the amount of crypto that is bought is the receiveQuantity // and when selling the amount of crypto that is sold is the deliverQuantity string amountParameter = order.IsBuy ? "receiveQuantity" : "deliverQuantity"; string amountReceived = order.IsBuy ? "deliverQuantity" : "receiveQuantity"; - string feesCurrency = order.IsBuy ? order.MarketSymbol.Substring(0, order.MarketSymbol.IndexOf('-')) : order.MarketSymbol.Substring(order.MarketSymbol.IndexOf('-') + 1); + string feesCurrency = order.IsBuy + ? order.MarketSymbol.Substring(0, order.MarketSymbol.IndexOf('-')) + : order.MarketSymbol.Substring(order.MarketSymbol.IndexOf('-') + 1); var payload = await GetNoncePayloadAsync(); payload["ticker"] = order.MarketSymbol; payload["tradeSide"] = order.IsBuy ? "buy" : "sell"; payload[amountParameter] = order.Amount; order.ExtraParameters.CopyTo(payload); - JToken token = await MakeJsonRequestAsync("/trades/v1/market", null, payload, "POST"); + JToken token = await MakeJsonRequestAsync( + "/trades/v1/market", + null, + payload, + "POST" + ); var orderDetailsPayload = await GetNoncePayloadAsync(); //{ @@ -131,7 +162,12 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd // } //} - JToken result = await MakeJsonRequestAsync($"/trades/v1/order?orderId={token["payload"]["orderId"].ToStringInvariant()}", null, orderDetailsPayload, "GET"); + JToken result = await MakeJsonRequestAsync( + $"/trades/v1/order?orderId={token["payload"]["orderId"].ToStringInvariant()}", + null, + orderDetailsPayload, + "GET" + ); // { // "priceArrival": 9223.5, // "orderId": "24cf77ad-7e93-44d7-86f8-b9d9a046b008", @@ -171,9 +207,13 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd ExchangeOrderResult orderDetails = new ExchangeOrderResult { OrderId = result["orderId"].ToStringInvariant(), - AmountFilled = result["fillQtyQuote"].ToStringInvariant().ConvertInvariant(), + AmountFilled = result["fillQtyQuote"] + .ToStringInvariant() + .ConvertInvariant(), Amount = payload[amountParameter].ConvertInvariant(), - OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(result["tradeTime"].ConvertInvariant()), + OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds( + result["tradeTime"].ConvertInvariant() + ), Message = result["message"].ToStringInvariant(), IsBuy = order.IsBuy, Fees = token["payload"]["fee"].ConvertInvariant(), @@ -184,7 +224,9 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd switch (result["tradeStatus"].ToStringInvariant()) { case "COMPLETE": - orderDetails.AveragePrice = result["tradePriceAvg"].ToStringInvariant().ConvertInvariant(); + orderDetails.AveragePrice = result["tradePriceAvg"] + .ToStringInvariant() + .ConvertInvariant(); orderDetails.Result = ExchangeAPIOrderResult.Filled; break; @@ -195,25 +237,43 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd return orderDetails; } - protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnGetOrderDetailsAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { if (string.IsNullOrWhiteSpace(orderId)) { return null; } - if (isClientOrderId) throw new NotSupportedException("Querying by client order ID is not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature"); + if (isClientOrderId) + throw new NotSupportedException( + "Querying by client order ID is not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature" + ); var payload = await GetNoncePayloadAsync(); - JToken result = await MakeJsonRequestAsync($"/trades/v1/order?orderId={orderId}", null, payload, "GET"); + JToken result = await MakeJsonRequestAsync( + $"/trades/v1/order?orderId={orderId}", + null, + payload, + "GET" + ); bool isBuy = result["tradeSide"].ToStringInvariant() == "buy" ? true : false; ExchangeOrderResult orderDetails = new ExchangeOrderResult { OrderId = result["orderId"].ToStringInvariant(), - AmountFilled = result["fillQtyQuote"].ToStringInvariant().ConvertInvariant(), + AmountFilled = result["fillQtyQuote"] + .ToStringInvariant() + .ConvertInvariant(), Amount = result["tradeSize"].ConvertInvariant(), - OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(result["tradeTime"].ConvertInvariant()), + OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds( + result["tradeTime"].ConvertInvariant() + ), Message = result["message"].ToStringInvariant(), IsBuy = isBuy, - Fees = result["fillFeeQuote"].ConvertInvariant() + result["fillFeeQuotaAqua"].ConvertInvariant(), + Fees = + result["fillFeeQuote"].ConvertInvariant() + + result["fillFeeQuotaAqua"].ConvertInvariant(), FeesCurrency = result["quoteSymbol"].ToStringInvariant(), MarketSymbol = result["symbol"].ToStringInvariant(), Price = result["priceArrival"].ToStringInvariant().ConvertInvariant(), @@ -221,7 +281,9 @@ protected override async Task OnGetOrderDetailsAsync(string switch (result["tradeStatus"].ToStringInvariant()) { case "COMPLETE": - orderDetails.AveragePrice = result["tradePriceAvg"].ToStringInvariant().ConvertInvariant(); + orderDetails.AveragePrice = result["tradePriceAvg"] + .ToStringInvariant() + .ConvertInvariant(); orderDetails.Result = ExchangeAPIOrderResult.Filled; break; @@ -233,14 +295,29 @@ protected override async Task OnGetOrderDetailsAsync(string return orderDetails; } - protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnCancelOrderAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { - if (isClientOrderId) throw new NotSupportedException("Cancelling by client order ID is not supported in ExchangeSharp. Please submit a PR if you are interested in this feature"); + if (isClientOrderId) + throw new NotSupportedException( + "Cancelling by client order ID is not supported in ExchangeSharp. Please submit a PR if you are interested in this feature" + ); var payload = await GetNoncePayloadAsync(); payload["orderId"] = orderId; - JToken token = await MakeJsonRequestAsync("/trades/v1/order", null, payload, "DELETE"); + JToken token = await MakeJsonRequestAsync( + "/trades/v1/order", + null, + payload, + "DELETE" + ); } } - public partial class ExchangeName { public const string Aquanow = "Aquanow"; } + public partial class ExchangeName + { + public const string Aquanow = "Aquanow"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/Aquanow/Models/Response/SignatureContent.cs b/src/ExchangeSharp/API/Exchanges/Aquanow/Models/Response/SignatureContent.cs index 563944623..f26d83cb6 100644 --- a/src/ExchangeSharp/API/Exchanges/Aquanow/Models/Response/SignatureContent.cs +++ b/src/ExchangeSharp/API/Exchanges/Aquanow/Models/Response/SignatureContent.cs @@ -12,10 +12,10 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp.Aquanow { - internal class signatureContent - { - public string httpMethod { get; set; } - public string path { get; set; } - public object nonce { get; set; } - } -} \ No newline at end of file + internal class signatureContent + { + public string httpMethod { get; set; } + public string path { get; set; } + public object nonce { get; set; } + } +} diff --git a/src/ExchangeSharp/API/Exchanges/BL3P/BL3PException.cs b/src/ExchangeSharp/API/Exchanges/BL3P/BL3PException.cs index 952775246..98706ff99 100644 --- a/src/ExchangeSharp/API/Exchanges/BL3P/BL3PException.cs +++ b/src/ExchangeSharp/API/Exchanges/BL3P/BL3PException.cs @@ -9,7 +9,7 @@ internal class BL3PException : Exception public string ErrorCode { get; } internal BL3PException(BL3PResponsePayloadError error) - : this(error?.Message) + : this(error?.Message) { if (error == null) throw new ArgumentNullException(nameof(error)); @@ -17,20 +17,12 @@ internal BL3PException(BL3PResponsePayloadError error) } public BL3PException(string message) - : base(message) - { - } + : base(message) { } public BL3PException(string message, Exception inner) - : base(message, inner) - { - } + : base(message, inner) { } - protected BL3PException( - SerializationInfo info, - StreamingContext context - ) : base(info, context) - { - } + protected BL3PException(SerializationInfo info, StreamingContext context) + : base(info, context) { } } } diff --git a/src/ExchangeSharp/API/Exchanges/BL3P/Converters/BL3PResponseConverter.cs b/src/ExchangeSharp/API/Exchanges/BL3P/Converters/BL3PResponseConverter.cs index 147177e9c..b02e5dd7a 100644 --- a/src/ExchangeSharp/API/Exchanges/BL3P/Converters/BL3PResponseConverter.cs +++ b/src/ExchangeSharp/API/Exchanges/BL3P/Converters/BL3PResponseConverter.cs @@ -4,7 +4,7 @@ namespace ExchangeSharp.BL3P { internal class BL3PResponseConverter : JsonComplexObjectConverter - where TSuccess : BL3PResponsePayload, new() + where TSuccess : BL3PResponsePayload, new() { protected override BL3PResponsePayload Create(JsonReader reader) { @@ -15,7 +15,7 @@ protected override BL3PResponsePayload Create(JsonReader reader) continue; } - var prop = (string) reader.Value; + var prop = (string)reader.Value; switch (prop) { diff --git a/src/ExchangeSharp/API/Exchanges/BL3P/ExchangeBL3PAPI.cs b/src/ExchangeSharp/API/Exchanges/BL3P/ExchangeBL3PAPI.cs index 55ae48668..f024c98b0 100644 --- a/src/ExchangeSharp/API/Exchanges/BL3P/ExchangeBL3PAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/BL3P/ExchangeBL3PAPI.cs @@ -47,7 +47,7 @@ private ExchangeBL3PAPI() } public ExchangeBL3PAPI(ref string publicApiKey, ref string privateApiKey) - : this() + : this() { if (publicApiKey == null) throw new ArgumentNullException(nameof(publicApiKey)); @@ -62,8 +62,7 @@ public ExchangeBL3PAPI(ref string publicApiKey, ref string privateApiKey) protected override async Task> OnGetMarketSymbolsAsync() { - return (await OnGetMarketSymbolsMetadataAsync()) - .Select(em => em.MarketSymbol); + return (await OnGetMarketSymbolsMetadataAsync()).Select(em => em.MarketSymbol); } protected override async Task OnGetTickerAsync(string marketSymbol) @@ -71,54 +70,58 @@ protected override async Task OnGetTickerAsync(string marketSymb var result = await MakeJsonRequestAsync($"/{marketSymbol}/ticker"); return await this.ParseTickerAsync( - result, - marketSymbol, - askKey: "ask", - bidKey: "bid", - lastKey: "last", - baseVolumeKey: "volume.24h", - timestampKey: "timestamp", - timestampType: TimestampType.UnixSeconds + result, + marketSymbol, + askKey: "ask", + bidKey: "bid", + lastKey: "last", + baseVolumeKey: "volume.24h", + timestampKey: "timestamp", + timestampType: TimestampType.UnixSeconds ); } - protected internal override Task> OnGetMarketSymbolsMetadataAsync() + protected internal override Task< + IEnumerable + > OnGetMarketSymbolsMetadataAsync() { - return Task.FromResult(new[] - { - // For now we only support these two coins - new ExchangeMarket - { - BaseCurrency = "BTC", - IsActive = true, - MarketSymbol = "BTCEUR", - QuoteCurrency = "EUR" - }, - new ExchangeMarket - { - BaseCurrency = "LTC", - IsActive = true, - MarketSymbol = "LTCEUR", - QuoteCurrency = "EUR" - } - } as IEnumerable); + return Task.FromResult( + new[] + { + // For now we only support these two coins + new ExchangeMarket + { + BaseCurrency = "BTC", + IsActive = true, + MarketSymbol = "BTCEUR", + QuoteCurrency = "EUR" + }, + new ExchangeMarket + { + BaseCurrency = "LTC", + IsActive = true, + MarketSymbol = "LTCEUR", + QuoteCurrency = "EUR" + } + } as IEnumerable + ); } - protected override Task>> OnGetTickersAsync() + protected override Task< + IEnumerable> + > OnGetTickersAsync() { - return Task.WhenAll( - OnGetTickerAsync("BTCEUR"), - OnGetTickerAsync("LTCEUR") - ).ContinueWith( - r => r.Result.ToDictionary(t => t.MarketSymbol, t => t).AsEnumerable(), - TaskContinuationOptions.OnlyOnRanToCompletion - ); + return Task.WhenAll(OnGetTickerAsync("BTCEUR"), OnGetTickerAsync("LTCEUR")) + .ContinueWith( + r => r.Result.ToDictionary(t => t.MarketSymbol, t => t).AsEnumerable(), + TaskContinuationOptions.OnlyOnRanToCompletion + ); } protected override async Task OnGetDeltaOrderBookWebSocketAsync( - Action callback, - int maxCount = 20, - params string[] marketSymbols + Action callback, + int maxCount = 20, + params string[] marketSymbols ) { if (marketSymbols == null || marketSymbols.Length == 0) @@ -128,7 +131,9 @@ params string[] marketSymbols Task MessageCallback(IWebSocket _, byte[] msg) { - var bl3POrderBook = JsonConvert.DeserializeObject(msg.ToStringFromUTF8()); + var bl3POrderBook = JsonConvert.DeserializeObject( + msg.ToStringFromUTF8() + ); var exchangeOrderBook = ConvertToExchangeOrderBook(maxCount, bl3POrderBook); @@ -138,13 +143,18 @@ Task MessageCallback(IWebSocket _, byte[] msg) } return new MultiWebsocketWrapper( - await Task.WhenAll( - marketSymbols.Select(ms => ConnectPublicWebSocketAsync($"{ms}/orderbook", MessageCallback)) - ) + await Task.WhenAll( + marketSymbols.Select( + ms => ConnectPublicWebSocketAsync($"{ms}/orderbook", MessageCallback) + ) + ) ); } - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + protected override async Task OnGetTradesWebSocketAsync( + Func, Task> callback, + params string[] marketSymbols + ) { if (marketSymbols == null || marketSymbols.Length == 0) { @@ -154,22 +164,32 @@ Task MessageCallback(IWebSocket _, byte[] msg) { // {{ "date": 1573255932, "marketplace": "BTCEUR", "price_int": 802466000, "type": "buy", "amount_int": 6193344 } } var token = JToken.Parse(msg.ToStringFromUTF8()); var symbol = token["marketplace"].ToStringInvariant(); - var trade = token.ParseTrade(amountKey: "amount_int", priceKey: "price_int", typeKey: "type", timestampKey: "date", - timestampType: TimestampType.UnixSeconds, - idKey: null, // + TODO: add Id Key when BL3P starts providing this info - typeKeyIsBuyValue: "buy"); + var trade = token.ParseTrade( + amountKey: "amount_int", + priceKey: "price_int", + typeKey: "type", + timestampKey: "date", + timestampType: TimestampType.UnixSeconds, + idKey: null, // + TODO: add Id Key when BL3P starts providing this info + typeKeyIsBuyValue: "buy" + ); callback(new KeyValuePair(symbol, trade)); return Task.CompletedTask; } return new MultiWebsocketWrapper( - await Task.WhenAll( - marketSymbols.Select(ms => ConnectPublicWebSocketAsync($"{ms}/trades", MessageCallback)) - ) + await Task.WhenAll( + marketSymbols.Select( + ms => ConnectPublicWebSocketAsync($"{ms}/trades", MessageCallback) + ) + ) ); } - private static ExchangeOrderBook ConvertToExchangeOrderBook(int maxCount, BL3POrderBook bl3POrderBook) + private static ExchangeOrderBook ConvertToExchangeOrderBook( + int maxCount, + BL3POrderBook bl3POrderBook + ) { var exchangeOrderBook = new ExchangeOrderBook { @@ -178,16 +198,16 @@ private static ExchangeOrderBook ConvertToExchangeOrderBook(int maxCount, BL3POr }; var asks = bl3POrderBook.Asks - .OrderBy(b => b.Price, exchangeOrderBook.Asks.Comparer) - .Take(maxCount); + .OrderBy(b => b.Price, exchangeOrderBook.Asks.Comparer) + .Take(maxCount); foreach (var ask in asks) { exchangeOrderBook.Asks.Add(ask.Price, ask.ToExchangeOrder()); } var bids = bl3POrderBook.Bids - .OrderBy(b => b.Price, exchangeOrderBook.Bids.Comparer) - .Take(maxCount); + .OrderBy(b => b.Price, exchangeOrderBook.Bids.Comparer) + .Take(maxCount); foreach (var bid in bids) { exchangeOrderBook.Bids.Add(bid.Price, bid.ToExchangeOrder()); @@ -196,15 +216,21 @@ private static ExchangeOrderBook ConvertToExchangeOrderBook(int maxCount, BL3POr return exchangeOrderBook; } - protected override bool CanMakeAuthenticatedRequest(IReadOnlyDictionary payload) + protected override bool CanMakeAuthenticatedRequest( + IReadOnlyDictionary payload + ) { return !(PublicApiKey is null) && !(PrivateApiKey is null); } - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + protected override async Task ProcessRequestAsync( + IHttpWebRequest request, + Dictionary payload + ) { - var formData = await request.WritePayloadFormToRequestAsync(payload) - .ConfigureAwait(false); + var formData = await request + .WritePayloadFormToRequestAsync(payload) + .ConfigureAwait(false); if (CanMakeAuthenticatedRequest(payload)) { @@ -218,7 +244,8 @@ private string GetSignKey(IHttpWebRequest request, string formData) { //TODO: Use csharp8 ranges var index = Array.IndexOf(request.RequestUri.Segments, "1/"); - var callPath = string.Join(string.Empty, request.RequestUri.Segments.Skip(index + 1)).TrimStart('/'); + var callPath = string.Join(string.Empty, request.RequestUri.Segments.Skip(index + 1)) + .TrimStart('/'); var postData = $"{callPath}\0{formData}"; var privateKeyBase64 = Convert.FromBase64String(PrivateApiKey.ToUnsecureString()); @@ -231,92 +258,135 @@ private string GetSignKey(IHttpWebRequest request, string formData) return Convert.ToBase64String(hashBytes); } - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + protected override async Task OnPlaceOrderAsync( + ExchangeOrderRequest order + ) { - if (order.IsPostOnly != null) throw new NotSupportedException("Post Only orders are not supported by this exchange or not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature."); + if (order.IsPostOnly != null) + throw new NotSupportedException( + "Post Only orders are not supported by this exchange or not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature." + ); var roundedAmount = order.RoundAmount(); var amountInt = converterToEight.FromDecimal(roundedAmount); - var feeCurrency = (order.ExtraParameters.ContainsKey("fee_currency") - ? order.ExtraParameters["fee_currency"] ?? DefaultFeeCurrency - : DefaultFeeCurrency) - .ToString() - .ToUpperInvariant(); + var feeCurrency = ( + order.ExtraParameters.ContainsKey("fee_currency") + ? order.ExtraParameters["fee_currency"] ?? DefaultFeeCurrency + : DefaultFeeCurrency + ) + .ToString() + .ToUpperInvariant(); var data = new Dictionary - { - {"amount_int", amountInt}, - {"type", order.IsBuy ? "bid" : "ask"}, - {"fee_currency", feeCurrency}, - }; + { + { "amount_int", amountInt }, + { "type", order.IsBuy ? "bid" : "ask" }, + { "fee_currency", feeCurrency }, + }; switch (order.OrderType) { case OrderType.Limit: - if (order.Price == null) throw new ArgumentNullException(nameof(order.Price)); + if (order.Price == null) + throw new ArgumentNullException(nameof(order.Price)); data["price_int"] = converterToFive.FromDecimal(order.Price.Value); break; case OrderType.Market: - if (order.Price == null) throw new ArgumentNullException(nameof(order.Price)); - data["amount_funds_int"] = converterToFive.FromDecimal(roundedAmount * order.Price.Value); + if (order.Price == null) + throw new ArgumentNullException(nameof(order.Price)); + data["amount_funds_int"] = converterToFive.FromDecimal( + roundedAmount * order.Price.Value + ); break; default: throw new NotSupportedException($"{order.OrderType} is not supported"); } - var result = (await MakeJsonRequestAsync( - $"/{order.MarketSymbol}/money/order/add", - payload: data - )).Except(); + var result = ( + await MakeJsonRequestAsync( + $"/{order.MarketSymbol}/money/order/add", + payload: data + ) + ).Except(); - var orderDetails = await GetOrderDetailsAsync(result.OrderId, marketSymbol: order.MarketSymbol); + var orderDetails = await GetOrderDetailsAsync( + result.OrderId, + marketSymbol: order.MarketSymbol + ); return orderDetails; } - protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) + protected override async Task OnGetOrderBookAsync( + string marketSymbol, + int maxCount = 100 + ) { if (string.IsNullOrWhiteSpace(marketSymbol)) - throw new ArgumentException("Value cannot be null or whitespace.", nameof(marketSymbol)); + throw new ArgumentException( + "Value cannot be null or whitespace.", + nameof(marketSymbol) + ); - var bl3pOrderBook = (await MakeJsonRequestAsync($"/{marketSymbol}/money/depth/full")) - .Except(); + var bl3pOrderBook = ( + await MakeJsonRequestAsync( + $"/{marketSymbol}/money/depth/full" + ) + ).Except(); - bl3pOrderBook.MarketSymbol??= marketSymbol; + bl3pOrderBook.MarketSymbol ??= marketSymbol; return ConvertToExchangeOrderBook(maxCount, bl3pOrderBook); } - protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnCancelOrderAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { if (string.IsNullOrWhiteSpace(marketSymbol)) - throw new ArgumentException("Value cannot be null or whitespace.", nameof(marketSymbol)); - if (isClientOrderId) throw new NotSupportedException("Cancelling by client order ID is not supported in ExchangeSharp. Please submit a PR if you are interested in this feature"); - - (await MakeJsonRequestAsync( - $"/{marketSymbol}/money/order/cancel", - payload: new Dictionary - { - {"order_id", orderId} - } - )).Except(); + throw new ArgumentException( + "Value cannot be null or whitespace.", + nameof(marketSymbol) + ); + if (isClientOrderId) + throw new NotSupportedException( + "Cancelling by client order ID is not supported in ExchangeSharp. Please submit a PR if you are interested in this feature" + ); + + ( + await MakeJsonRequestAsync( + $"/{marketSymbol}/money/order/cancel", + payload: new Dictionary { { "order_id", orderId } } + ) + ).Except(); } - protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnGetOrderDetailsAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { if (string.IsNullOrWhiteSpace(marketSymbol)) - throw new ArgumentException("Value cannot be null or whitespace.", nameof(marketSymbol)); - if (isClientOrderId) throw new NotSupportedException("Querying by client order ID is not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature"); - - var data = new Dictionary - { - {"order_id", orderId} - }; - - var result = (await MakeJsonRequestAsync( - $"/{marketSymbol}/money/order/result", - payload: data - )).Except(); + throw new ArgumentException( + "Value cannot be null or whitespace.", + nameof(marketSymbol) + ); + if (isClientOrderId) + throw new NotSupportedException( + "Querying by client order ID is not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature" + ); + + var data = new Dictionary { { "order_id", orderId } }; + + var result = ( + await MakeJsonRequestAsync( + $"/{marketSymbol}/money/order/result", + payload: data + ) + ).Except(); return new ExchangeOrderResult { @@ -338,11 +408,13 @@ protected override async Task OnGetOrderDetailsAsync(string }; } - protected override Task OnPlaceOrdersAsync(params ExchangeOrderRequest[] order) + protected override Task OnPlaceOrdersAsync( + params ExchangeOrderRequest[] order + ) { Debug.WriteLine( - "Splitting orders in single order calls as BL3P does not support batch operations yet", - "WARN" + "Splitting orders in single order calls as BL3P does not support batch operations yet", + "WARN" ); return Task.WhenAll(order.Select(OnPlaceOrderAsync)); } diff --git a/src/ExchangeSharp/API/Exchanges/BL3P/Extensions/BL3PExtensions.cs b/src/ExchangeSharp/API/Exchanges/BL3P/Extensions/BL3PExtensions.cs index e5b1fed55..117d15a0d 100644 --- a/src/ExchangeSharp/API/Exchanges/BL3P/Extensions/BL3PExtensions.cs +++ b/src/ExchangeSharp/API/Exchanges/BL3P/Extensions/BL3PExtensions.cs @@ -2,13 +2,17 @@ namespace ExchangeSharp.BL3P { internal static class BL3PExtensions { - public static ExchangeAPIOrderResult ToResult(this BL3POrderStatus status, BL3PAmount amount) + public static ExchangeAPIOrderResult ToResult( + this BL3POrderStatus status, + BL3PAmount amount + ) { return status switch { BL3POrderStatus.Cancelled => ExchangeAPIOrderResult.Canceled, BL3POrderStatus.Closed => ExchangeAPIOrderResult.Filled, - BL3POrderStatus.Open when amount.Value > 0 => ExchangeAPIOrderResult.FilledPartially, + BL3POrderStatus.Open when amount.Value > 0 + => ExchangeAPIOrderResult.FilledPartially, BL3POrderStatus.Open => ExchangeAPIOrderResult.Open, BL3POrderStatus.Pending => ExchangeAPIOrderResult.PendingOpen, BL3POrderStatus.Placed => ExchangeAPIOrderResult.Open, diff --git a/src/ExchangeSharp/API/Exchanges/BL3P/Models/BL3PAmount.cs b/src/ExchangeSharp/API/Exchanges/BL3P/Models/BL3PAmount.cs index 5c9b3975a..941e83b86 100644 --- a/src/ExchangeSharp/API/Exchanges/BL3P/Models/BL3PAmount.cs +++ b/src/ExchangeSharp/API/Exchanges/BL3P/Models/BL3PAmount.cs @@ -4,14 +4,19 @@ namespace ExchangeSharp.BL3P { internal class BL3PAmount { - [JsonProperty("value_int")] public long ValueInt { get; set; } + [JsonProperty("value_int")] + public long ValueInt { get; set; } - [JsonProperty("display_short")] public string DisplayShort { get; set; } + [JsonProperty("display_short")] + public string DisplayShort { get; set; } - [JsonProperty("display")] public string Display { get; set; } + [JsonProperty("display")] + public string Display { get; set; } - [JsonProperty("currency")] public string Currency { get; set; } + [JsonProperty("currency")] + public string Currency { get; set; } - [JsonProperty("value")] public decimal Value { get; set; } + [JsonProperty("value")] + public decimal Value { get; set; } } } diff --git a/src/ExchangeSharp/API/Exchanges/BL3P/Models/BL3POrderRequest.cs b/src/ExchangeSharp/API/Exchanges/BL3P/Models/BL3POrderRequest.cs index b01fb6079..3f6a95f8e 100644 --- a/src/ExchangeSharp/API/Exchanges/BL3P/Models/BL3POrderRequest.cs +++ b/src/ExchangeSharp/API/Exchanges/BL3P/Models/BL3POrderRequest.cs @@ -9,18 +9,13 @@ internal class BL3POrderRequest [JsonConverter(typeof(FixedIntDecimalJsonConverter), 5)] public decimal Price { get; set; } - [JsonProperty("amount_int")] [JsonConverter(typeof(FixedIntDecimalJsonConverter), 8)] public decimal Amount { get; set; } public ExchangeOrderPrice ToExchangeOrder() { - return new ExchangeOrderPrice - { - Amount = Amount, - Price = Price - }; + return new ExchangeOrderPrice { Amount = Amount, Price = Price }; } } } diff --git a/src/ExchangeSharp/API/Exchanges/BL3P/Models/BL3PResponse.cs b/src/ExchangeSharp/API/Exchanges/BL3P/Models/BL3PResponse.cs index d32b22b75..c6d3dba68 100644 --- a/src/ExchangeSharp/API/Exchanges/BL3P/Models/BL3PResponse.cs +++ b/src/ExchangeSharp/API/Exchanges/BL3P/Models/BL3PResponse.cs @@ -2,22 +2,20 @@ namespace ExchangeSharp.BL3P { - internal class BL3PEmptyResponse - : BL3PResponse + internal class BL3PEmptyResponse : BL3PResponse { [JsonProperty("data")] protected override BL3PResponsePayload Data { get; set; } } internal abstract class BL3PResponse - : BL3PResponse - where TSuccess : BL3PResponsePayload - { - } + : BL3PResponse + where TSuccess : BL3PResponsePayload + { } internal abstract class BL3PResponse - where TSuccess : BL3PResponsePayload - where TFail : BL3PResponsePayloadError + where TSuccess : BL3PResponsePayload + where TFail : BL3PResponsePayloadError { [JsonProperty("result", Required = Required.Always)] public BL3PResponseType Result { get; set; } @@ -26,10 +24,10 @@ internal abstract class BL3PResponse protected abstract BL3PResponsePayload Data { get; set; } [JsonIgnore] - public virtual TSuccess Success => (TSuccess) Data; + public virtual TSuccess Success => (TSuccess)Data; [JsonIgnore] - public virtual TFail Error => (TFail) Data; + public virtual TFail Error => (TFail)Data; /// /// Returns TSuccess or nothing diff --git a/src/ExchangeSharp/API/Exchanges/BL3P/Models/BL3PResponsePayload.cs b/src/ExchangeSharp/API/Exchanges/BL3P/Models/BL3PResponsePayload.cs index 41c9babd3..def4a776c 100644 --- a/src/ExchangeSharp/API/Exchanges/BL3P/Models/BL3PResponsePayload.cs +++ b/src/ExchangeSharp/API/Exchanges/BL3P/Models/BL3PResponsePayload.cs @@ -1,6 +1,4 @@ namespace ExchangeSharp.BL3P { - internal class BL3PResponsePayload - { - } + internal class BL3PResponsePayload { } } diff --git a/src/ExchangeSharp/API/Exchanges/BL3P/MultiWebsocketWrapper.cs b/src/ExchangeSharp/API/Exchanges/BL3P/MultiWebsocketWrapper.cs index 24e393eb4..a6bb4fb6a 100644 --- a/src/ExchangeSharp/API/Exchanges/BL3P/MultiWebsocketWrapper.cs +++ b/src/ExchangeSharp/API/Exchanges/BL3P/MultiWebsocketWrapper.cs @@ -74,8 +74,10 @@ public void Dispose() public async Task SendMessageAsync(object message) { - var tasks = await Task.WhenAll(webSockets.Select(ws => ws.Key.SendMessageAsync(message))) - .ConfigureAwait(false); + var tasks = await Task.WhenAll( + webSockets.Select(ws => ws.Key.SendMessageAsync(message)) + ) + .ConfigureAwait(false); return tasks.All(r => r); } diff --git a/src/ExchangeSharp/API/Exchanges/BTSE/ExchangeBTSEAPI.cs b/src/ExchangeSharp/API/Exchanges/BTSE/ExchangeBTSEAPI.cs index 1374bc7b7..612bcfe5c 100644 --- a/src/ExchangeSharp/API/Exchanges/BTSE/ExchangeBTSEAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/BTSE/ExchangeBTSEAPI.cs @@ -2,11 +2,11 @@ namespace ExchangeSharp { - using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; + using Newtonsoft.Json.Linq; public sealed partial class ExchangeBTSEAPI : ExchangeAPI { @@ -17,40 +17,52 @@ private ExchangeBTSEAPI() { NonceStyle = NonceStyle.UnixMillisecondsString; } + protected override async Task> OnGetMarketSymbolsAsync() { return (await GetTickersAsync()).Select(pair => pair.Key); } - protected override async Task>> OnGetTickersAsync() + protected override async Task< + IEnumerable> + > OnGetTickersAsync() { - JToken allPairs = await MakeJsonRequestAsync("/api/v3.1/market_summary", BaseUrl); - var tasks = allPairs.Select(async token => await ParseBTSETicker(token, - token["symbol"].Value())); - - return (await Task.WhenAll(tasks)).Select(ticker => - new KeyValuePair(ticker.MarketSymbol, ticker)); + JToken allPairs = await MakeJsonRequestAsync( + "/api/v3.1/market_summary", + BaseUrl + ); + var tasks = allPairs.Select( + async token => await ParseBTSETicker(token, token["symbol"].Value()) + ); + + return (await Task.WhenAll(tasks)).Select( + ticker => new KeyValuePair(ticker.MarketSymbol, ticker) + ); } protected override async Task OnGetTickerAsync(string marketSymbol) { - JToken ticker = await MakeJsonRequestAsync("/api/v3.1/market_summary", BaseUrl, - new Dictionary() - { - {"symbol", marketSymbol} - }); + JToken ticker = await MakeJsonRequestAsync( + "/api/v3.1/market_summary", + BaseUrl, + new Dictionary() { { "symbol", marketSymbol } } + ); return await ParseBTSETicker(ticker, marketSymbol); } - protected override async Task> OnGetCandlesAsync(string marketSymbol, - int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, - int? limit = null) + protected override async Task> OnGetCandlesAsync( + string marketSymbol, + int periodSeconds, + DateTime? startDate = null, + DateTime? endDate = null, + int? limit = null + ) { var payload = new Dictionary() - { - {"symbol", marketSymbol}, - {"resolution", periodSeconds} - }; + { + { "symbol", marketSymbol }, + { "resolution", periodSeconds } + }; if (startDate != null) { @@ -62,25 +74,52 @@ protected override async Task> OnGetCandlesAsync(strin payload.Add("end", startDate.Value.UnixTimestampFromDateTimeMilliseconds()); } - JToken ticker = await MakeJsonRequestAsync("/api/v3.1/ohlcv", null, payload, "GET"); - return ticker.Select(token => - this.ParseCandle(token, marketSymbol, periodSeconds, 1, 2, 3, 4, 0, TimestampType.UnixMilliseconds, 5)); + JToken ticker = await MakeJsonRequestAsync( + "/api/v3.1/ohlcv", + null, + payload, + "GET" + ); + return ticker.Select( + token => + this.ParseCandle( + token, + marketSymbol, + periodSeconds, + 1, + 2, + 3, + 4, + 0, + TimestampType.UnixMilliseconds, + 5 + ) + ); } - protected override async Task OnCancelOrderAsync(string orderId, string? marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnCancelOrderAsync( + string orderId, + string? marketSymbol = null, + bool isClientOrderId = false + ) { var payload = await GetNoncePayloadAsync(); payload["order_id"] = orderId.ConvertInvariant(); - var url = new UriBuilder(BaseUrl) {Path = "/api/v3.1/order"}; - url.AppendPayloadToQuery(new Dictionary() - { - {"symbol", marketSymbol}, - {"orderID", orderId} - }); - - await MakeJsonRequestAsync($"/api/v3.1/order{url.Query}", - requestMethod: "DELETE", payload: payload); + var url = new UriBuilder(BaseUrl) { Path = "/api/v3.1/order" }; + url.AppendPayloadToQuery( + new Dictionary() + { + { "symbol", marketSymbol }, + { "orderID", orderId } + } + ); + + await MakeJsonRequestAsync( + $"/api/v3.1/order{url.Query}", + requestMethod: "DELETE", + payload: payload + ); } protected override Task> OnGetAmountsAsync() @@ -97,40 +136,56 @@ protected override async Task> OnGetFeesAsync() { var payload = await GetNoncePayloadAsync(); - var result = await MakeJsonRequestAsync("/api/v3.1/user/fees", - requestMethod: "GET", payload: payload); + var result = await MakeJsonRequestAsync( + "/api/v3.1/user/fees", + requestMethod: "GET", + payload: payload + ); //taker or maker fees in BTSE.. i chose maker for here - return Extract(result, token => (token["symbol"].Value(), token["makerFee"].Value())); + return Extract( + result, + token => (token["symbol"].Value(), token["makerFee"].Value()) + ); } protected override async Task> OnGetOpenOrderDetailsAsync( - string? marketSymbol = null) + string? marketSymbol = null + ) { - if (marketSymbol == null) throw new ArgumentNullException(nameof(marketSymbol)); + if (marketSymbol == null) + throw new ArgumentNullException(nameof(marketSymbol)); var payload = await GetNoncePayloadAsync(); - var url = new UriBuilder(BaseUrl) {Path = "/api/v3.1/user/open_orders"}; - url.AppendPayloadToQuery(new Dictionary() - { - {"symbol", marketSymbol} - }); - - var result = await MakeJsonRequestAsync("/api/v3.1/user/open_orders"+url.Query, - requestMethod: "GET", payload: payload); - - 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(), - ClientOrderId = token["clOrderID"].Value(), - Result = FromOrderState(token["orderState"].Value()) - }); + var url = new UriBuilder(BaseUrl) { Path = "/api/v3.1/user/open_orders" }; + url.AppendPayloadToQuery( + new Dictionary() { { "symbol", marketSymbol } } + ); + + var result = await MakeJsonRequestAsync( + "/api/v3.1/user/open_orders" + url.Query, + requestMethod: "GET", + payload: payload + ); + + 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(), + ClientOrderId = token["clOrderID"].Value(), + Result = FromOrderState(token["orderState"].Value()) + } + ); } private ExchangeAPIOrderResult FromOrderState(string s) @@ -150,24 +205,27 @@ private ExchangeAPIOrderResult FromOrderState(string s) } } - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest request) - {var payload = await GetNoncePayloadAsync(); + protected override async Task OnPlaceOrderAsync( + ExchangeOrderRequest request + ) + { + var payload = await GetNoncePayloadAsync(); var dict = new Dictionary(); var id = request.OrderId ?? request.ClientOrderId; if (!string.IsNullOrEmpty(id)) { - dict.Add("clOrderID",id); - + dict.Add("clOrderID", id); } dict.Add("size", request.Amount); dict.Add("side", request.IsBuy ? "BUY" : "SELL"); dict.Add("symbol", request.MarketSymbol); - if (request.IsPostOnly != null) payload["postOnly"] = request.IsPostOnly; + if (request.IsPostOnly != null) + payload["postOnly"] = request.IsPostOnly; - switch (request.OrderType ) + switch (request.OrderType) { case OrderType.Limit: dict.Add("txType", "LIMIT"); @@ -192,62 +250,73 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd } } - payload.Add("body", dict); - var result = await MakeJsonRequestAsync("/api/v3.1/order", - requestMethod: "POST", payload: payload); - return Extract2(result, token => - { - var status = ExchangeAPIOrderResult.Unknown; - switch (token["status"].Value()) - { - case 2: // ORDER_INSERTED - status = ExchangeAPIOrderResult.Open; - break; - case 4: // ORDER_FULLY_TRANSACTED - status = ExchangeAPIOrderResult.Filled; - break; - case 5: // ORDER_PARTIALLY_TRANSACTED - status = ExchangeAPIOrderResult.FilledPartially; - break; - case 6: // ORDER_CANCELLED - status = ExchangeAPIOrderResult.Canceled; - break; - case 8: // INSUFFICIENT_BALANCE - status = ExchangeAPIOrderResult.Rejected; - break; - case 9: //trigger inserted - case 10: //trigger activated - status = ExchangeAPIOrderResult.Open; - break; - case 15: //rejected - status = ExchangeAPIOrderResult.Rejected; - break; - case 16: //not found - status = ExchangeAPIOrderResult.Unknown; - break; - } - - return new ExchangeOrderResult() - { - Message = token["message"].Value(), - OrderId = token["orderID"].Value(), - IsBuy = token["side"].Value().ToLowerInvariant() == "buy", - Price = token["price"].Value(), - MarketSymbol = token["symbol"].Value(), - Result = status, - Amount = token["size"].Value(), - OrderDate = token["timestamp"].ConvertInvariant().UnixTimeStampToDateTimeMilliseconds(), - ClientOrderId = token["clOrderID"].Value(), - AveragePrice = token["averageFillPrice"].Value(), - AmountFilled = token["fillSize"].Value(), - - }; - }).First(); + var result = await MakeJsonRequestAsync( + "/api/v3.1/order", + requestMethod: "POST", + payload: payload + ); + return Extract2( + result, + token => + { + var status = ExchangeAPIOrderResult.Unknown; + switch (token["status"].Value()) + { + case 2: // ORDER_INSERTED + status = ExchangeAPIOrderResult.Open; + break; + case 4: // ORDER_FULLY_TRANSACTED + status = ExchangeAPIOrderResult.Filled; + break; + case 5: // ORDER_PARTIALLY_TRANSACTED + status = ExchangeAPIOrderResult.FilledPartially; + break; + case 6: // ORDER_CANCELLED + status = ExchangeAPIOrderResult.Canceled; + break; + case 8: // INSUFFICIENT_BALANCE + status = ExchangeAPIOrderResult.Rejected; + break; + case 9: //trigger inserted + case 10: //trigger activated + status = ExchangeAPIOrderResult.Open; + break; + case 15: //rejected + status = ExchangeAPIOrderResult.Rejected; + break; + case 16: //not found + status = ExchangeAPIOrderResult.Unknown; + break; + } + + return new ExchangeOrderResult() + { + Message = token["message"].Value(), + OrderId = token["orderID"].Value(), + IsBuy = token["side"].Value().ToLowerInvariant() == "buy", + Price = token["price"].Value(), + MarketSymbol = token["symbol"].Value(), + Result = status, + Amount = token["size"].Value(), + OrderDate = token["timestamp"] + .ConvertInvariant() + .UnixTimeStampToDateTimeMilliseconds(), + ClientOrderId = token["clOrderID"].Value(), + AveragePrice = token["averageFillPrice"].Value(), + AmountFilled = token["fillSize"].Value(), + }; + } + ) + .First(); } - protected override Uri ProcessRequestUrl(UriBuilder url, Dictionary payload, string method) + protected override Uri ProcessRequestUrl( + UriBuilder url, + Dictionary payload, + string method + ) { if (method == "GET" && (payload?.Count ?? 0) != 0 && !payload.ContainsKey("nonce")) { @@ -256,7 +325,10 @@ protected override Uri ProcessRequestUrl(UriBuilder url, Dictionary payload) + protected override async Task ProcessRequestAsync( + IHttpWebRequest request, + Dictionary payload + ) { if (CanMakeAuthenticatedRequest(payload)) { @@ -281,8 +353,9 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti } var hexSha384 = CryptoUtility.SHA384Sign( - $"{request.RequestUri.AbsolutePath.Replace("/spot", string.Empty)}{nonce}{json}", - passphrase); + $"{request.RequestUri.AbsolutePath.Replace("/spot", string.Empty)}{nonce}{json}", + passphrase + ); request.AddHeader("btse-sign", hexSha384); request.AddHeader("btse-nonce", nonce); request.AddHeader("btse-api", PublicApiKey.ToUnsecureString()); @@ -303,19 +376,20 @@ protected override async Task> GetNoncePayloadAsync() return result; } - private Dictionary Extract(JToken token, Func processor) + private Dictionary Extract( + JToken token, + Func processor + ) { if (token is JArray resultArr) { - return resultArr.Select(processor.Invoke) - .ToDictionary(tuple => tuple.Item1, tuple => tuple.Item2); + return resultArr + .Select(processor.Invoke) + .ToDictionary(tuple => tuple.Item1, tuple => tuple.Item2); } var resItem = processor.Invoke(token); - return new Dictionary() - { - {resItem.Item1, resItem.Item2} - }; + return new Dictionary() { { resItem.Item1, resItem.Item2 } }; } private IEnumerable Extract2(JToken token, Func processor) @@ -325,27 +399,45 @@ private IEnumerable Extract2(JToken token, Func return resultArr.Select(processor.Invoke); } - return new List() - { - processor.Invoke(token) - }; + return new List() { processor.Invoke(token) }; } private async Task ParseBTSETicker(JToken ticker, string marketSymbol) { - return await this.ParseTickerAsync(ticker, marketSymbol, "lowestAsk", "highestBid", "last", "volume", null, - null, TimestampType.UnixMilliseconds, "base", "quote", "symbol"); + return await this.ParseTickerAsync( + ticker, + marketSymbol, + "lowestAsk", + "highestBid", + "last", + "volume", + null, + null, + TimestampType.UnixMilliseconds, + "base", + "quote", + "symbol" + ); } private async Task> GetBTSEBalance(bool availableOnly) { var payload = await GetNoncePayloadAsync(); - var result = await MakeJsonRequestAsync("/api/v3.1/user/wallet", - requestMethod: "GET", payload: payload); - return Extract(result, token => (token["currency"].Value(), token[availableOnly?"available": "total"].Value())); + var result = await MakeJsonRequestAsync( + "/api/v3.1/user/wallet", + requestMethod: "GET", + payload: payload + ); + return Extract( + result, + token => + ( + token["currency"].Value(), + token[availableOnly ? "available" : "total"].Value() + ) + ); } - } public partial class ExchangeName diff --git a/src/ExchangeSharp/API/Exchanges/BinanceGroup/BinanceGroupCommon.cs b/src/ExchangeSharp/API/Exchanges/BinanceGroup/BinanceGroupCommon.cs index cae9a8919..eba28d9aa 100644 --- a/src/ExchangeSharp/API/Exchanges/BinanceGroup/BinanceGroupCommon.cs +++ b/src/ExchangeSharp/API/Exchanges/BinanceGroup/BinanceGroupCommon.cs @@ -10,13 +10,13 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #nullable enable -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace ExchangeSharp.BinanceGroup { @@ -26,7 +26,10 @@ public abstract class BinanceGroupCommon : ExchangeAPI public string BaseUrlSApi => $"{BaseUrl}/sapi/v1"; - protected async Task GetWebSocketStreamUrlForSymbolsAsync(string suffix, params string[] marketSymbols) + protected async Task GetWebSocketStreamUrlForSymbolsAsync( + string suffix, + params string[] marketSymbols + ) { if (marketSymbols == null || marketSymbols.Length == 0) { @@ -54,7 +57,7 @@ protected async Task GetWebSocketStreamUrlForSymbolsAsync(string suffix, protected BinanceGroupCommon() { // give binance plenty of room to accept requests - RequestWindow = TimeSpan.FromMilliseconds(60000); // 60000 is max value = max request time window of 60 seconds + RequestWindow = TimeSpan.FromMilliseconds(60000); // 60000 is max value = max request time window of 60 seconds NonceStyle = NonceStyle.UnixMilliseconds; NonceOffset = TimeSpan.FromSeconds(15); // 15 seconds are deducted from current UTCTime as base of the request time window MarketSymbolSeparator = string.Empty; @@ -71,21 +74,46 @@ protected BinanceGroupCommon() RateLimit = new RateGate(40, TimeSpan.FromSeconds(10)); } - public override Task ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol) + public override Task ExchangeMarketSymbolToGlobalMarketSymbolAsync( + string marketSymbol + ) { // All pairs in Binance end with BTC, ETH, BNB or USDT - if (marketSymbol.EndsWith("BTC") || marketSymbol.EndsWith("ETH") || marketSymbol.EndsWith("BNB")) + if ( + marketSymbol.EndsWith("BTC") + || marketSymbol.EndsWith("ETH") + || marketSymbol.EndsWith("BNB") + ) { string baseSymbol = marketSymbol.Substring(marketSymbol.Length - 3); - return ExchangeMarketSymbolToGlobalMarketSymbolWithSeparatorAsync((marketSymbol.Replace(baseSymbol, "") + GlobalMarketSymbolSeparator + baseSymbol), GlobalMarketSymbolSeparator); + return ExchangeMarketSymbolToGlobalMarketSymbolWithSeparatorAsync( + ( + marketSymbol.Replace(baseSymbol, "") + + GlobalMarketSymbolSeparator + + baseSymbol + ), + GlobalMarketSymbolSeparator + ); } if (marketSymbol.EndsWith("USDT")) { string baseSymbol = marketSymbol.Substring(marketSymbol.Length - 4); - return ExchangeMarketSymbolToGlobalMarketSymbolWithSeparatorAsync((marketSymbol.Replace(baseSymbol, "") + GlobalMarketSymbolSeparator + baseSymbol), GlobalMarketSymbolSeparator); + return ExchangeMarketSymbolToGlobalMarketSymbolWithSeparatorAsync( + ( + marketSymbol.Replace(baseSymbol, "") + + GlobalMarketSymbolSeparator + + baseSymbol + ), + GlobalMarketSymbolSeparator + ); } - return ExchangeMarketSymbolToGlobalMarketSymbolWithSeparatorAsync(marketSymbol.Substring(0, marketSymbol.Length - 3) + GlobalMarketSymbolSeparator + (marketSymbol.Substring(marketSymbol.Length - 3, 3)), GlobalMarketSymbolSeparator); + return ExchangeMarketSymbolToGlobalMarketSymbolWithSeparatorAsync( + marketSymbol.Substring(0, marketSymbol.Length - 3) + + GlobalMarketSymbolSeparator + + (marketSymbol.Substring(marketSymbol.Length - 3, 3)), + GlobalMarketSymbolSeparator + ); } /// @@ -93,85 +121,103 @@ public override Task ExchangeMarketSymbolToGlobalMarketSymbolAsync(strin /// /// Symbol to get trades for or null for all /// All trades for the specified symbol, or all if null symbol - public async Task> GetMyTradesAsync(string? marketSymbol = null, DateTime? afterDate = null) + public async Task> GetMyTradesAsync( + string? marketSymbol = null, + DateTime? afterDate = null + ) { await new SynchronizationContextRemover(); return await OnGetMyTradesAsync(marketSymbol, afterDate); } - protected override async Task> OnGetCurrenciesAsync() + protected override async Task< + IReadOnlyDictionary + > OnGetCurrenciesAsync() { Dictionary payload = await GetNoncePayloadAsync(); - var result = await MakeJsonRequestAsync>("/capital/config/getall", BaseUrlSApi, payload); - - return result.Where(x => !string.IsNullOrWhiteSpace(x.AssetCode)) - .ToDictionary(x => x.AssetCode, x => new ExchangeCurrency - { - Name = x.AssetCode, - FullName = x.AssetName, - DepositEnabled = x.EnableCharge ?? false, - WithdrawalEnabled = x.EnableWithdraw.GetValueOrDefault(false), - MinConfirmations = x.ConfirmTimes.GetValueOrDefault(0), - MinWithdrawalSize = x.MinProductWithdraw.GetValueOrDefault(decimal.Zero), - TxFee = x.FeeRate.GetValueOrDefault(decimal.Zero), - CoinType = x.ParentCode - }); + var result = await MakeJsonRequestAsync>( + "/capital/config/getall", + BaseUrlSApi, + payload + ); + + return result + .Where(x => !string.IsNullOrWhiteSpace(x.AssetCode)) + .ToDictionary( + x => x.AssetCode, + x => + new ExchangeCurrency + { + Name = x.AssetCode, + FullName = x.AssetName, + DepositEnabled = x.EnableCharge ?? false, + WithdrawalEnabled = x.EnableWithdraw.GetValueOrDefault(false), + MinConfirmations = x.ConfirmTimes.GetValueOrDefault(0), + MinWithdrawalSize = x.MinProductWithdraw.GetValueOrDefault( + decimal.Zero + ), + TxFee = x.FeeRate.GetValueOrDefault(decimal.Zero), + CoinType = x.ParentCode + } + ); } protected override async Task> OnGetMarketSymbolsAsync() { List symbols = new List(); JToken? obj = await MakeJsonRequestAsync("/ticker/price", BaseUrlApi); - if (!(obj is null)) - { - foreach (JToken token in obj) - { - symbols.Add(token["symbol"].ToStringInvariant()); - } - } + if (!(obj is null)) + { + foreach (JToken token in obj) + { + symbols.Add(token["symbol"].ToStringInvariant()); + } + } return symbols; } - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() + protected internal override async Task< + IEnumerable + > OnGetMarketSymbolsMetadataAsync() { /* - * { - "symbol": "ETHBTC", - "status": "TRADING", - "baseAsset": "ETH", - "baseAssetPrecision": 8, - "quoteAsset": "BTC", - "quotePrecision": 8, - "orderTypes": [ - "LIMIT", - "MARKET", - "STOP_LOSS", - "STOP_LOSS_LIMIT", - "TAKE_PROFIT", - "TAKE_PROFIT_LIMIT", - "LIMIT_MAKER" - ], - "icebergAllowed": false, - "filters": [ - { - "filterType": "PRICE_FILTER", - "minPrice": "0.00000100", - "maxPrice": "100000.00000000", - "tickSize": "0.00000100" - }, - { - "filterType": "LOT_SIZE", - "minQty": "0.00100000", - "maxQty": "100000.00000000", - "stepSize": "0.00100000" - }, - { - "filterType": "MIN_NOTIONAL", - "minNotional": "0.00100000" - } - ] - }, - */ + * { + "symbol": "ETHBTC", + "status": "TRADING", + "baseAsset": "ETH", + "baseAssetPrecision": 8, + "quoteAsset": "BTC", + "quotePrecision": 8, + "orderTypes": [ + "LIMIT", + "MARKET", + "STOP_LOSS", + "STOP_LOSS_LIMIT", + "TAKE_PROFIT", + "TAKE_PROFIT_LIMIT", + "LIMIT_MAKER" + ], + "icebergAllowed": false, + "filters": [ + { + "filterType": "PRICE_FILTER", + "minPrice": "0.00000100", + "maxPrice": "100000.00000000", + "tickSize": "0.00000100" + }, + { + "filterType": "LOT_SIZE", + "minQty": "0.00100000", + "maxQty": "100000.00000000", + "stepSize": "0.00100000" + }, + { + "filterType": "MIN_NOTIONAL", + "minNotional": "0.00100000" + } + ] + }, + */ var markets = new List(); JToken obj = await MakeJsonRequestAsync("/exchangeInfo", BaseUrlApi); @@ -182,20 +228,30 @@ protected internal override async Task> OnGetMarketS { // common ExchangeMarket properties MarketSymbol = marketSymbolToken["symbol"].ToStringUpperInvariant(), - IsActive = ParseMarketStatus(marketSymbolToken["status"].ToStringUpperInvariant()), + IsActive = ParseMarketStatus( + marketSymbolToken["status"].ToStringUpperInvariant() + ), QuoteCurrency = marketSymbolToken["quoteAsset"].ToStringUpperInvariant(), BaseCurrency = marketSymbolToken["baseAsset"].ToStringUpperInvariant(), - // Binance specific properties - Status = (BinanceSymbolStatus)Enum.Parse(typeof(BinanceSymbolStatus), marketSymbolToken["status"].ToStringInvariant(), true), - BaseAssetPrecision = marketSymbolToken["baseAssetPrecision"].ConvertInvariant(), + Status = (BinanceSymbolStatus) + Enum.Parse( + typeof(BinanceSymbolStatus), + marketSymbolToken["status"].ToStringInvariant(), + true + ), + BaseAssetPrecision = marketSymbolToken[ + "baseAssetPrecision" + ].ConvertInvariant(), QuotePrecision = marketSymbolToken["quotePrecision"].ConvertInvariant(), IsIceBergAllowed = marketSymbolToken["icebergAllowed"].ConvertInvariant(), }; // "LOT_SIZE" JToken filters = marketSymbolToken["filters"]; - JToken? lotSizeFilter = filters?.FirstOrDefault(x => string.Equals(x["filterType"].ToStringUpperInvariant(), "LOT_SIZE")); + JToken? lotSizeFilter = filters?.FirstOrDefault( + x => string.Equals(x["filterType"].ToStringUpperInvariant(), "LOT_SIZE") + ); if (lotSizeFilter != null) { market.MaxTradeSize = lotSizeFilter["maxQty"].ConvertInvariant(); @@ -204,7 +260,9 @@ protected internal override async Task> OnGetMarketS } // PRICE_FILTER - JToken? priceFilter = filters?.FirstOrDefault(x => string.Equals(x["filterType"].ToStringUpperInvariant(), "PRICE_FILTER")); + JToken? priceFilter = filters?.FirstOrDefault( + x => string.Equals(x["filterType"].ToStringUpperInvariant(), "PRICE_FILTER") + ); if (priceFilter != null) { market.MaxPrice = priceFilter["maxPrice"].ConvertInvariant(); @@ -213,17 +271,25 @@ protected internal override async Task> OnGetMarketS } // MIN_NOTIONAL - JToken? minNotionalFilter = filters?.FirstOrDefault(x => string.Equals(x["filterType"].ToStringUpperInvariant(), "MIN_NOTIONAL")); + JToken? minNotionalFilter = filters?.FirstOrDefault( + x => string.Equals(x["filterType"].ToStringUpperInvariant(), "MIN_NOTIONAL") + ); if (minNotionalFilter != null) { - market.MinTradeSizeInQuoteCurrency = minNotionalFilter["minNotional"].ConvertInvariant(); + market.MinTradeSizeInQuoteCurrency = minNotionalFilter[ + "minNotional" + ].ConvertInvariant(); } // MAX_NUM_ORDERS - JToken? maxOrdersFilter = filters?.FirstOrDefault(x => string.Equals(x["filterType"].ToStringUpperInvariant(), "MAX_NUM_ORDERS")); + JToken? maxOrdersFilter = filters?.FirstOrDefault( + x => string.Equals(x["filterType"].ToStringUpperInvariant(), "MAX_NUM_ORDERS") + ); if (maxOrdersFilter != null) { - market.MaxNumOrders = maxOrdersFilter["maxNumberOrders"].ConvertInvariant(); + market.MaxNumOrders = maxOrdersFilter[ + "maxNumberOrders" + ].ConvertInvariant(); } markets.Add(market); } @@ -233,20 +299,31 @@ protected internal override async Task> OnGetMarketS protected override async Task OnGetTickerAsync(string marketSymbol) { - JToken obj = await MakeJsonRequestAsync("/ticker/24hr?symbol=" + marketSymbol, BaseUrlApi); + JToken obj = await MakeJsonRequestAsync( + "/ticker/24hr?symbol=" + marketSymbol, + BaseUrlApi + ); return await ParseTickerAsync(marketSymbol, obj); } - protected override async Task>> OnGetTickersAsync() + protected override async Task< + IEnumerable> + > OnGetTickersAsync() { - List> tickers = new List>(); + List> tickers = + new List>(); JToken obj = await MakeJsonRequestAsync("/ticker/24hr", BaseUrlApi); foreach (JToken child in obj) { var marketSymbol = child["symbol"].ToStringInvariant(); try { - tickers.Add(new KeyValuePair(marketSymbol, await ParseTickerAsync(marketSymbol, child))); + tickers.Add( + new KeyValuePair( + marketSymbol, + await ParseTickerAsync(marketSymbol, child) + ) + ); } catch { @@ -256,126 +333,201 @@ protected override async Task>> return tickers; } - protected override async Task OnGetTickersWebSocketAsync(Action>> callback, params string[] symbols) + protected override async Task OnGetTickersWebSocketAsync( + Action>> callback, + params string[] symbols + ) { string url = null; if (symbols == null || symbols.Length == 0) { url = "/stream?streams=!ticker@arr"; } - else url = await GetWebSocketStreamUrlForSymbolsAsync("@ticker", symbols); - return await ConnectPublicWebSocketAsync(url, async (_socket, msg) => - { - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - List> tickerList = new List>(); - ExchangeTicker ticker; - - // if it is the tickers stream, data will be an array, else it is an direct item. - var data = token["data"]; - if (data is JArray) - foreach (JToken childToken in data) + else + url = await GetWebSocketStreamUrlForSymbolsAsync("@ticker", symbols); + return await ConnectPublicWebSocketAsync( + url, + async (_socket, msg) => { - ticker = await ParseTickerWebSocketAsync(childToken); - tickerList.Add(new KeyValuePair(ticker.MarketSymbol, ticker)); - } - else - { - ticker = await ParseTickerWebSocketAsync(data); - tickerList.Add(new KeyValuePair(ticker.MarketSymbol, ticker)); - } + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + List> tickerList = + new List>(); + ExchangeTicker ticker; + + // if it is the tickers stream, data will be an array, else it is an direct item. + var data = token["data"]; + if (data is JArray) + foreach (JToken childToken in data) + { + ticker = await ParseTickerWebSocketAsync(childToken); + tickerList.Add( + new KeyValuePair( + ticker.MarketSymbol, + ticker + ) + ); + } + else + { + ticker = await ParseTickerWebSocketAsync(data); + tickerList.Add( + new KeyValuePair(ticker.MarketSymbol, ticker) + ); + } - if (tickerList.Count != 0) - { - callback(tickerList); - } - }); + if (tickerList.Count != 0) + { + callback(tickerList); + } + } + ); } - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + protected override async Task OnGetTradesWebSocketAsync( + Func, Task> callback, + params string[] marketSymbols + ) { /* - { - "e": "aggTrade", // Event type - "E": 123456789, // Event time - "s": "BNBBTC", // Symbol - "a": 12345, // Aggregate trade ID - "p": "0.001", // Price - "q": "100", // Quantity - "f": 100, // First trade ID - "l": 105, // Last trade ID - "T": 123456785, // Trade time - "m": true, // Is the buyer the market maker? - "M": true // Ignore - } - */ + { + "e": "aggTrade", // Event type + "E": 123456789, // Event time + "s": "BNBBTC", // Symbol + "a": 12345, // Aggregate trade ID + "p": "0.001", // Price + "q": "100", // Quantity + "f": 100, // First trade ID + "l": 105, // Last trade ID + "T": 123456785, // Trade time + "m": true, // Is the buyer the market maker? + "M": true // Ignore + } + */ if (marketSymbols == null || marketSymbols.Length == 0) { marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); } string url = await GetWebSocketStreamUrlForSymbolsAsync("@aggTrade", marketSymbols); - return await ConnectPublicWebSocketAsync(url, messageCallback: async (_socket, msg) => - { - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - string name = token["stream"].ToStringInvariant(); - token = token["data"]; - string marketSymbol = NormalizeMarketSymbol(name.Substring(0, name.IndexOf('@'))); - - // buy=0 -> m = true (The buyer is maker, while the seller is taker). - // buy=1 -> m = false(The seller is maker, while the buyer is taker). - await callback(new KeyValuePair(marketSymbol, - token.ParseTradeBinance(amountKey: "q", priceKey: "p", typeKey: "m", - timestampKey: "T", // use trade time (T) instead of event time (E) - timestampType: TimestampType.UnixMilliseconds, idKey: "a", typeKeyIsBuyValue: "false"))); - }); + return await ConnectPublicWebSocketAsync( + url, + messageCallback: async (_socket, msg) => + { + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + string name = token["stream"].ToStringInvariant(); + token = token["data"]; + string marketSymbol = NormalizeMarketSymbol( + name.Substring(0, name.IndexOf('@')) + ); + + // buy=0 -> m = true (The buyer is maker, while the seller is taker). + // buy=1 -> m = false(The seller is maker, while the buyer is taker). + await callback( + new KeyValuePair( + marketSymbol, + token.ParseTradeBinance( + amountKey: "q", + priceKey: "p", + typeKey: "m", + timestampKey: "T", // use trade time (T) instead of event time (E) + timestampType: TimestampType.UnixMilliseconds, + idKey: "a", + typeKeyIsBuyValue: "false" + ) + ) + ); + } + ); } - protected override async Task OnGetDeltaOrderBookWebSocketAsync(Action callback, int maxCount = 20, params string[] marketSymbols) + protected override async Task OnGetDeltaOrderBookWebSocketAsync( + Action callback, + int maxCount = 20, + params string[] marketSymbols + ) { if (marketSymbols == null || marketSymbols.Length == 0) { marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); } - string combined = string.Join("/", marketSymbols.Select(s => this.NormalizeMarketSymbol(s).ToLowerInvariant() + "@depth@100ms")); - return await ConnectPublicWebSocketAsync($"/stream?streams={combined}", (_socket, msg) => - { - string json = msg.ToStringFromUTF8(); - var update = JsonConvert.DeserializeObject(json, SerializerSettings); - string marketSymbol = update.Data.MarketSymbol; - ExchangeOrderBook book = new ExchangeOrderBook { SequenceId = update.Data.FinalUpdate, MarketSymbol = marketSymbol, LastUpdatedUtc = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(update.Data.EventTime) }; - foreach (List ask in update.Data.Asks) - { - var depth = new ExchangeOrderPrice { Price = ask[0].ConvertInvariant(), Amount = ask[1].ConvertInvariant() }; - book.Asks[depth.Price] = depth; - } - foreach (List bid in update.Data.Bids) - { - var depth = new ExchangeOrderPrice { Price = bid[0].ConvertInvariant(), Amount = bid[1].ConvertInvariant() }; - book.Bids[depth.Price] = depth; - } - callback(book); - return Task.CompletedTask; - }); + string combined = string.Join( + "/", + marketSymbols.Select( + s => this.NormalizeMarketSymbol(s).ToLowerInvariant() + "@depth@100ms" + ) + ); + return await ConnectPublicWebSocketAsync( + $"/stream?streams={combined}", + (_socket, msg) => + { + string json = msg.ToStringFromUTF8(); + var update = JsonConvert.DeserializeObject( + json, + SerializerSettings + ); + string marketSymbol = update.Data.MarketSymbol; + ExchangeOrderBook book = new ExchangeOrderBook + { + SequenceId = update.Data.FinalUpdate, + MarketSymbol = marketSymbol, + LastUpdatedUtc = CryptoUtility.UnixTimeStampToDateTimeMilliseconds( + update.Data.EventTime + ) + }; + foreach (List ask in update.Data.Asks) + { + var depth = new ExchangeOrderPrice + { + Price = ask[0].ConvertInvariant(), + Amount = ask[1].ConvertInvariant() + }; + book.Asks[depth.Price] = depth; + } + foreach (List bid in update.Data.Bids) + { + var depth = new ExchangeOrderPrice + { + Price = bid[0].ConvertInvariant(), + Amount = bid[1].ConvertInvariant() + }; + book.Bids[depth.Price] = depth; + } + callback(book); + return Task.CompletedTask; + } + ); } - protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) + protected override async Task OnGetOrderBookAsync( + string marketSymbol, + int maxCount = 100 + ) { - JToken obj = await MakeJsonRequestAsync($"/depth?symbol={marketSymbol}&limit={maxCount}", BaseUrlApi); + JToken obj = await MakeJsonRequestAsync( + $"/depth?symbol={marketSymbol}&limit={maxCount}", + BaseUrlApi + ); return obj.ParseOrderBookFromJTokenArrays(sequence: "lastUpdateId"); } - protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + protected override async Task OnGetHistoricalTradesAsync( + Func, bool> callback, + string marketSymbol, + DateTime? startDate = null, + DateTime? endDate = null, + int? limit = null + ) { /* [ { - "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? - } ] */ + "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? + } ] */ //if(startDate == null && endDate == null) { // await OnGetRecentTradesAsync(marketSymbol, limit); @@ -384,47 +536,82 @@ 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) + protected override async Task> OnGetRecentTradesAsync( + string marketSymbol, + int? limit = null + ) { //https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md var trades = new List(); var maxRequestLimit = (limit == null || limit < 1 || limit > 1000) ? 1000 : (int)limit; - JToken obj = await MakeJsonRequestAsync($"/aggTrades?symbol={marketSymbol}&limit={maxRequestLimit}", BaseUrlApi); + JToken obj = await MakeJsonRequestAsync( + $"/aggTrades?symbol={marketSymbol}&limit={maxRequestLimit}", + BaseUrlApi + ); if (obj.HasValues) { - trades.AddRange(obj.Select(token => - token.ParseTrade("q", "p", "m", "T", TimestampType.UnixMilliseconds, "a", "false"))); + trades.AddRange( + obj.Select( + token => + token.ParseTrade( + "q", + "p", + "m", + "T", + TimestampType.UnixMilliseconds, + "a", + "false" + ) + ) + ); } return trades.AsEnumerable().Reverse(); //Descending order (ie newest trades first) } - public async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, long startId, long? endId = null) + public async Task OnGetHistoricalTradesAsync( + Func, bool> callback, + string marketSymbol, + long startId, + long? endId = null + ) { /* [ { - "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? - } ] */ + "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 @@ -441,11 +628,21 @@ public async Task OnGetHistoricalTradesAsync(Func, bo trades.Clear(); var limit = Math.Min(endId - fromId ?? maxRequestLimit, maxRequestLimit); - var obj = await MakeJsonRequestAsync($"/aggTrades?symbol={marketSymbol}&fromId={fromId}&limit={limit}"); + var obj = await MakeJsonRequestAsync( + $"/aggTrades?symbol={marketSymbol}&fromId={fromId}&limit={limit}" + ); foreach (var token in obj) { - var trade = token.ParseTrade("q", "p", "m", "T", TimestampType.UnixMilliseconds, "a", "false"); + var trade = token.ParseTrade( + "q", + "p", + "m", + "T", + TimestampType.UnixMilliseconds, + "a", + "false" + ); long tradeId = (long)trade.Id.ConvertInvariant(); if (tradeId < fromId) continue; @@ -462,37 +659,54 @@ public async Task OnGetHistoricalTradesAsync(Func, bo } while (callback(trades) && trades.Count > 0); } - public async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, int limit = 100) + 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? - } ] */ + "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 + if (limit > 1000) + limit = 1000; //Binance max = 1000 var maxRequestLimit = 1000; var trades = new List(); var processedIds = new HashSet(); marketSymbol = NormalizeMarketSymbol(marketSymbol); - do { + do + { //if(fromId > endId) // break; trades.Clear(); //var limit = Math.Min(endId - fromId ?? maxRequestLimit, maxRequestLimit); - var obj = await MakeJsonRequestAsync($"/aggTrades?symbol={marketSymbol}&limit={limit}"); + 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"); + 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; @@ -506,36 +720,47 @@ public async Task OnGetHistoricalTradesAsync(Func, bo } //fromId++; - } while(callback(trades) && trades.Count > 0); + } while (callback(trades) && trades.Count > 0); } - - protected override async Task> OnGetCandlesAsync(string marketSymbol, - int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + protected override async Task> OnGetCandlesAsync( + string marketSymbol, + int periodSeconds, + DateTime? startDate = null, + DateTime? endDate = null, + int? limit = null + ) { /* [ - [ - 1499040000000, // Open time - "0.01634790", // Open - "0.80000000", // High - "0.01575800", // Low - "0.01577100", // Close - "148976.11427815", // Volume - 1499644799999, // Close time - "2434.19055334", // Quote asset volume - 308, // Number of trades - "1756.87402397", // Taker buy base asset volume - "28.46694368", // Taker buy quote asset volume - "17928899.62484339" // Can be ignored - ]] */ + [ + 1499040000000, // Open time + "0.01634790", // Open + "0.80000000", // High + "0.01575800", // Low + "0.01577100", // Close + "148976.11427815", // Volume + 1499644799999, // Close time + "2434.19055334", // Quote asset volume + 308, // Number of trades + "1756.87402397", // Taker buy base asset volume + "28.46694368", // Taker buy quote asset volume + "17928899.62484339" // Can be ignored + ]] */ string url = "/klines?symbol=" + marketSymbol; if (startDate != null) { - url += "&startTime=" + (long)startDate.Value.UnixTimestampFromDateTimeMilliseconds(); - url += "&endTime=" + - ((endDate == null ? long.MaxValue : (long)endDate.Value.UnixTimestampFromDateTimeMilliseconds())) - .ToStringInvariant(); + url += + "&startTime=" + (long)startDate.Value.UnixTimestampFromDateTimeMilliseconds(); + url += + "&endTime=" + + ( + ( + endDate == null + ? long.MaxValue + : (long)endDate.Value.UnixTimestampFromDateTimeMilliseconds() + ) + ).ToStringInvariant(); } if (limit != null) @@ -546,17 +771,40 @@ protected override async Task> OnGetCandlesAsync(strin url += "&interval=" + PeriodSecondsToString(periodSeconds); JToken obj = await MakeJsonRequestAsync(url, BaseUrlApi); - return obj.Select(token => this.ParseCandle(token, marketSymbol, periodSeconds, 1, 2, 3, 4, 0, - TimestampType.UnixMilliseconds, 5, 7)).ToList(); + return obj.Select( + token => + this.ParseCandle( + token, + marketSymbol, + periodSeconds, + 1, + 2, + 3, + 4, + 0, + TimestampType.UnixMilliseconds, + 5, + 7 + ) + ) + .ToList(); } protected override async Task> OnGetAmountsAsync() { - JToken token = await MakeJsonRequestAsync("/account", BaseUrlApi, await GetNoncePayloadAsync()); - Dictionary balances = new Dictionary(StringComparer.OrdinalIgnoreCase); + JToken token = await MakeJsonRequestAsync( + "/account", + BaseUrlApi, + await GetNoncePayloadAsync() + ); + Dictionary balances = new Dictionary( + StringComparer.OrdinalIgnoreCase + ); foreach (JToken balance in token["balances"]) { - decimal amount = balance["free"].ConvertInvariant() + balance["locked"].ConvertInvariant(); + decimal amount = + balance["free"].ConvertInvariant() + + balance["locked"].ConvertInvariant(); if (amount > 0m) { balances[balance["asset"].ToStringInvariant()] = amount; @@ -565,10 +813,18 @@ protected override async Task> OnGetAmountsAsync() return balances; } - protected override async Task> OnGetAmountsAvailableToTradeAsync() + protected override async Task< + Dictionary + > OnGetAmountsAvailableToTradeAsync() { - JToken token = await MakeJsonRequestAsync("/account", BaseUrlApi, await GetNoncePayloadAsync()); - Dictionary balances = new Dictionary(StringComparer.OrdinalIgnoreCase); + JToken token = await MakeJsonRequestAsync( + "/account", + BaseUrlApi, + await GetNoncePayloadAsync() + ); + Dictionary balances = new Dictionary( + StringComparer.OrdinalIgnoreCase + ); foreach (JToken balance in token["balances"]) { decimal amount = balance["free"].ConvertInvariant(); @@ -580,20 +836,27 @@ protected override async Task> OnGetAmountsAvailable return balances; } - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + protected override async Task OnPlaceOrderAsync( + ExchangeOrderRequest order + ) { - if (order.Price == null && order.OrderType != OrderType.Market) throw new ArgumentNullException(nameof(order.Price)); + if (order.Price == null && order.OrderType != OrderType.Market) + throw new ArgumentNullException(nameof(order.Price)); Dictionary payload = await GetNoncePayloadAsync(); payload["symbol"] = order.MarketSymbol; payload["newClientOrderId"] = order.ClientOrderId; payload["side"] = order.IsBuy ? "BUY" : "SELL"; if (order.OrderType == OrderType.Stop) - payload["type"] = "STOP_LOSS";//if order type is stop loss/limit, then binance expect word 'STOP_LOSS' inestead of 'STOP' + payload["type"] = "STOP_LOSS"; //if order type is stop loss/limit, then binance expect word 'STOP_LOSS' inestead of 'STOP' else if (order.IsPostOnly == true) { - if (order.OrderType == OrderType.Limit) payload["type"] = "LIMIT_MAKER"; // LIMIT_MAKER are LIMIT orders that will be rejected if they would immediately match and trade as a taker. - else throw new NotSupportedException("PostOnly with non limit orders are not currently supported on Binance. Please submit a PR if you are interested in this feature"); + if (order.OrderType == OrderType.Limit) + payload["type"] = "LIMIT_MAKER"; // LIMIT_MAKER are LIMIT orders that will be rejected if they would immediately match and trade as a taker. + else + throw new NotSupportedException( + "PostOnly with non limit orders are not currently supported on Binance. Please submit a PR if you are interested in this feature" + ); } else payload["type"] = order.OrderType.ToStringUpperInvariant(); @@ -614,20 +877,31 @@ protected override async Task> OnGetAmountsAvailable } order.ExtraParameters.CopyTo(payload); - JToken? token = await MakeJsonRequestAsync("/order", BaseUrlApi, payload, "POST"); - if (token is null) - { - return null; - } + JToken? token = await MakeJsonRequestAsync( + "/order", + BaseUrlApi, + payload, + "POST" + ); + if (token is null) + { + return null; + } return ParseOrder(token); } - protected override async Task OnGetOrderDetailsAsync(string orderId, string? marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnGetOrderDetailsAsync( + string orderId, + string? marketSymbol = null, + bool isClientOrderId = false + ) { Dictionary payload = await GetNoncePayloadAsync(); if (string.IsNullOrWhiteSpace(marketSymbol)) { - throw new ArgumentNullException("Binance single order details request requires symbol"); + throw new ArgumentNullException( + "Binance single order details request requires symbol" + ); } payload["symbol"] = marketSymbol!; @@ -642,10 +916,15 @@ protected override async Task OnGetOrderDetailsAsync(string // Add up the fees from each trade in the order Dictionary feesPayload = await GetNoncePayloadAsync(); feesPayload["symbol"] = marketSymbol!; - if (!isClientOrderId) { - feesPayload["orderId"] = orderId; - } - JToken feesToken = await MakeJsonRequestAsync("/myTrades", BaseUrlApi, feesPayload); + if (!isClientOrderId) + { + feesPayload["orderId"] = orderId; + } + JToken feesToken = await MakeJsonRequestAsync( + "/myTrades", + BaseUrlApi, + feesPayload + ); ParseFees(feesToken, result); return result; @@ -656,7 +935,9 @@ protected override async Task OnGetOrderDetailsAsync(string /// The result object to append to. private static void ParseFees(JToken feesToken, ExchangeOrderResult result) { - var tradesInOrder = feesToken.Where(x => x["orderId"].ToStringInvariant() == result.OrderId); + var tradesInOrder = feesToken.Where( + x => x["orderId"].ToStringInvariant() == result.OrderId + ); bool currencySet = false; foreach (var trade in tradesInOrder) @@ -673,7 +954,9 @@ private static void ParseFees(JToken feesToken, ExchangeOrderResult result) } } - protected override async Task> OnGetOpenOrderDetailsAsync(string? marketSymbol = null) + protected override async Task> OnGetOpenOrderDetailsAsync( + string? marketSymbol = null + ) { List orders = new List(); Dictionary payload = await GetNoncePayloadAsync(); @@ -690,45 +973,63 @@ protected override async Task> OnGetOpenOrderDe return orders; } - private async Task> GetCompletedOrdersForAllSymbolsAsync(DateTime? afterDate) + private async Task> GetCompletedOrdersForAllSymbolsAsync( + DateTime? afterDate + ) { // TODO: This is a HACK, Binance API needs to add a single API call to get all orders for all symbols, terrible... List orders = new List(); Exception? ex = null; string? failedSymbol = null; - Parallel.ForEach((await GetMarketSymbolsAsync()).Where(s => s.IndexOf("BTC", StringComparison.OrdinalIgnoreCase) >= 0), async (s) => - { - try - { - foreach (ExchangeOrderResult order in (await GetCompletedOrderDetailsAsync(s, afterDate))) + Parallel.ForEach( + (await GetMarketSymbolsAsync()).Where( + s => s.IndexOf("BTC", StringComparison.OrdinalIgnoreCase) >= 0 + ), + async (s) => { - lock (orders) + try { - orders.Add(order); + foreach ( + ExchangeOrderResult order in ( + await GetCompletedOrderDetailsAsync(s, afterDate) + ) + ) + { + lock (orders) + { + orders.Add(order); + } + } + } + catch (Exception _ex) + { + failedSymbol = s; + ex = _ex; } } - } - catch (Exception _ex) - { - failedSymbol = s; - ex = _ex; - } - }); + ); if (ex != null) { - throw new APIException("Failed to get completed order details for symbol " + failedSymbol, ex); + throw new APIException( + "Failed to get completed order details for symbol " + failedSymbol, + ex + ); } // sort timestamp desc - orders.Sort((o1, o2) => - { - return o2.OrderDate.CompareTo(o1.OrderDate); - }); + orders.Sort( + (o1, o2) => + { + return o2.OrderDate.CompareTo(o1.OrderDate); + } + ); return orders; } - protected override async Task> OnGetCompletedOrderDetailsAsync(string? marketSymbol = null, DateTime? afterDate = null) + protected override async Task< + IEnumerable + > OnGetCompletedOrderDetailsAsync(string? marketSymbol = null, DateTime? afterDate = null) { //new way List trades = new List(); @@ -753,7 +1054,10 @@ protected override async Task> OnGetCompletedOr return trades; } - private async Task> OnGetMyTradesAsync(string? marketSymbol = null, DateTime? afterDate = null) + private async Task> OnGetMyTradesAsync( + string? marketSymbol = null, + DateTime? afterDate = null + ) { List trades = new List(); if (string.IsNullOrWhiteSpace(marketSymbol)) @@ -777,7 +1081,11 @@ private async Task> OnGetMyTradesAsync(string? return trades; } - protected override async Task OnCancelOrderAsync(string orderId, string? marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnCancelOrderAsync( + string orderId, + string? marketSymbol = null, + bool isClientOrderId = false + ) { Dictionary payload = await GetNoncePayloadAsync(); if (string.IsNullOrWhiteSpace(marketSymbol)) @@ -789,16 +1097,20 @@ protected override async Task OnCancelOrderAsync(string orderId, string? marketS payload["origClientOrderId"] = orderId; else payload["orderId"] = orderId; - var token = await MakeJsonRequestAsync("/order", BaseUrlApi, payload, "DELETE"); + var token = await MakeJsonRequestAsync("/order", BaseUrlApi, payload, "DELETE"); var cancelledOrder = ParseOrder(token); if (cancelledOrder.OrderId != orderId) - throw new APIException($"Cancelled {cancelledOrder.OrderId} when trying to cancel {orderId}"); + throw new APIException( + $"Cancelled {cancelledOrder.OrderId} when trying to cancel {orderId}" + ); } /// A withdrawal request. Fee is automatically subtracted from the amount. /// The withdrawal request. /// Withdrawal response from Binance - protected override async Task OnWithdrawAsync(ExchangeWithdrawalRequest withdrawalRequest) + protected override async Task OnWithdrawAsync( + ExchangeWithdrawalRequest withdrawalRequest + ) { if (string.IsNullOrWhiteSpace(withdrawalRequest.Currency)) { @@ -828,7 +1140,12 @@ protected override async Task OnWithdrawAsync(Exchan payload["addressTag"] = withdrawalRequest.AddressTag; } - JToken response = await MakeJsonRequestAsync("/capital/withdraw/apply", BaseUrlSApi, payload, "POST"); + JToken response = await MakeJsonRequestAsync( + "/capital/withdraw/apply", + BaseUrlSApi, + payload, + "POST" + ); ExchangeWithdrawalResponse withdrawalResponse = new ExchangeWithdrawalResponse { Id = response["id"].ToStringInvariant(), @@ -849,12 +1166,12 @@ private static bool ParseMarketStatus(string status) isActive = true; break; /* - case "PRE_TRADING": - case "POST_TRADING": - case "END_OF_DAY": - case "HALT": - case "AUCTION_MATCH": - case "BREAK": */ + case "PRE_TRADING": + case "POST_TRADING": + case "END_OF_DAY": + case "HALT": + case "AUCTION_MATCH": + case "BREAK": */ } } @@ -864,82 +1181,112 @@ private static bool ParseMarketStatus(string status) private async Task ParseTickerAsync(string symbol, JToken token) { // {"priceChange":"-0.00192300","priceChangePercent":"-4.735","weightedAvgPrice":"0.03980955","prevClosePrice":"0.04056700","lastPrice":"0.03869000","lastQty":"0.69300000","bidPrice":"0.03858500","bidQty":"38.35000000","askPrice":"0.03869000","askQty":"31.90700000","openPrice":"0.04061300","highPrice":"0.04081900","lowPrice":"0.03842000","volume":"128015.84300000","quoteVolume":"5096.25362239","openTime":1512403353766,"closeTime":1512489753766,"firstId":4793094,"lastId":4921546,"count":128453} - return await this.ParseTickerAsync(token, symbol, "askPrice", "bidPrice", "lastPrice", "volume", "quoteVolume", "closeTime", TimestampType.UnixMilliseconds); + return await this.ParseTickerAsync( + token, + symbol, + "askPrice", + "bidPrice", + "lastPrice", + "volume", + "quoteVolume", + "closeTime", + TimestampType.UnixMilliseconds + ); } private async Task ParseTickerWebSocketAsync(JToken token) { string marketSymbol = token["s"].ToStringInvariant(); - return await this.ParseTickerAsync(token, marketSymbol, "a", "b", "c", "v", "q", "E", TimestampType.UnixMilliseconds); + return await this.ParseTickerAsync( + token, + marketSymbol, + "a", + "b", + "c", + "v", + "q", + "E", + TimestampType.UnixMilliseconds + ); } private static ExchangeOrderResult ParseOrder(JToken token) { /* - "symbol": "IOTABTC", - "orderId": 1, - "clientOrderId": "12345", - "transactTime": 1510629334993, - "price": "1.00000000", - "origQty": "1.00000000", - "executedQty": "0.00000000", - "status": "NEW", - "timeInForce": "GTC", - "type": "LIMIT", - "side": "SELL", - "fills": [ - { - "price": "4000.00000000", - "qty": "1.00000000", - "commission": "4.00000000", - "commissionAsset": "USDT" - }, - { - "price": "3999.00000000", - "qty": "5.00000000", - "commission": "19.99500000", - "commissionAsset": "USDT" - }, - { - "price": "3998.00000000", - "qty": "2.00000000", - "commission": "7.99600000", - "commissionAsset": "USDT" - }, - { - "price": "3997.00000000", - "qty": "1.00000000", - "commission": "3.99700000", - "commissionAsset": "USDT" - }, - { - "price": "3995.00000000", - "qty": "1.00000000", - "commission": "3.99500000", - "commissionAsset": "USDT" - } - ] - */ + "symbol": "IOTABTC", + "orderId": 1, + "clientOrderId": "12345", + "transactTime": 1510629334993, + "price": "1.00000000", + "origQty": "1.00000000", + "executedQty": "0.00000000", + "status": "NEW", + "timeInForce": "GTC", + "type": "LIMIT", + "side": "SELL", + "fills": [ + { + "price": "4000.00000000", + "qty": "1.00000000", + "commission": "4.00000000", + "commissionAsset": "USDT" + }, + { + "price": "3999.00000000", + "qty": "5.00000000", + "commission": "19.99500000", + "commissionAsset": "USDT" + }, + { + "price": "3998.00000000", + "qty": "2.00000000", + "commission": "7.99600000", + "commissionAsset": "USDT" + }, + { + "price": "3997.00000000", + "qty": "1.00000000", + "commission": "3.99700000", + "commissionAsset": "USDT" + }, + { + "price": "3995.00000000", + "qty": "1.00000000", + "commission": "3.99500000", + "commissionAsset": "USDT" + } + ] + */ ExchangeOrderResult result = new ExchangeOrderResult { Amount = token["origQty"].ConvertInvariant(), AmountFilled = token["executedQty"].ConvertInvariant(), Price = token["price"].ConvertInvariant(), IsBuy = token["side"].ToStringInvariant() == "BUY", - OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(token["time"].ConvertInvariant(token["transactTime"].ConvertInvariant())), + OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds( + token["time"].ConvertInvariant( + token["transactTime"].ConvertInvariant() + ) + ), OrderId = token["orderId"].ToStringInvariant(), MarketSymbol = token["symbol"].ToStringInvariant(), ClientOrderId = token["clientOrderId"].ToStringInvariant() }; result.ResultCode = token["status"].ToStringInvariant(); - result.Result = ParseExchangeAPIOrderResult(result.ResultCode, result.AmountFilled.Value); + result.Result = ParseExchangeAPIOrderResult( + result.ResultCode, + result.AmountFilled.Value + ); ParseAveragePriceAndFeesFromFills(result, token["fills"]); return result; } - internal static ExchangeAPIOrderResult ParseExchangeAPIOrderResult(string status, decimal amountFilled) + internal static ExchangeAPIOrderResult ParseExchangeAPIOrderResult( + string status, + decimal amountFilled + ) { switch (status) { @@ -950,7 +1297,9 @@ internal static ExchangeAPIOrderResult ParseExchangeAPIOrderResult(string status case "FILLED": return ExchangeAPIOrderResult.Filled; case "CANCELED": - return amountFilled > 0 ? ExchangeAPIOrderResult.FilledPartiallyAndCancelled : ExchangeAPIOrderResult.Canceled; + return amountFilled > 0 + ? ExchangeAPIOrderResult.FilledPartiallyAndCancelled + : ExchangeAPIOrderResult.Canceled; case "PENDING_CANCEL": return ExchangeAPIOrderResult.PendingCancel; case "EXPIRED": @@ -965,21 +1314,21 @@ internal static ExchangeAPIOrderResult ParseExchangeAPIOrderResult(string status private static ExchangeOrderResult ParseTrade(JToken token, string symbol) { /* - [ - { - "id": 28457, - "orderId": 100234, - "price": "4.00000100", - "qty": "12.00000000", - "commission": "10.10000000", - "commissionAsset": "BNB", - "time": 1499865549590, - "isBuyer": true, - "isMaker": false, - "isBestMatch": true - } - ] - */ + [ + { + "id": 28457, + "orderId": 100234, + "price": "4.00000100", + "qty": "12.00000000", + "commission": "10.10000000", + "commissionAsset": "BNB", + "time": 1499865549590, + "isBuyer": true, + "isMaker": false, + "isBestMatch": true + } + ] + */ ExchangeOrderResult result = new ExchangeOrderResult { Result = ExchangeAPIOrderResult.Filled, @@ -990,7 +1339,9 @@ private static ExchangeOrderResult ParseTrade(JToken token, string symbol) IsBuy = token["isBuyer"].ConvertInvariant() == true, // OrderDate - not provided here. ideally would be null but ExchangeOrderResult.OrderDate is not nullable CompletedDate = null, // order not necessarily fullly filled at this point - TradeDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(token["time"].ConvertInvariant()), + TradeDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds( + token["time"].ConvertInvariant() + ), OrderId = token["orderId"].ToStringInvariant(), TradeId = token["id"].ToStringInvariant(), Fees = token["commission"].ConvertInvariant(), @@ -1001,7 +1352,10 @@ private static ExchangeOrderResult ParseTrade(JToken token, string symbol) return result; } - private static void ParseAveragePriceAndFeesFromFills(ExchangeOrderResult result, JToken fillsToken) + private static void ParseAveragePriceAndFeesFromFills( + ExchangeOrderResult result, + JToken fillsToken + ) { decimal totalCost = 0; decimal totalQuantity = 0; @@ -1027,28 +1381,49 @@ private static void ParseAveragePriceAndFeesFromFills(ExchangeOrderResult result } } - result.AveragePrice = (totalQuantity == 0 ? null : (decimal?)(totalCost / totalQuantity)); + result.AveragePrice = ( + totalQuantity == 0 ? null : (decimal?)(totalCost / totalQuantity) + ); } - protected override Task ProcessRequestAsync(IHttpWebRequest request, Dictionary? payload) + protected override Task ProcessRequestAsync( + IHttpWebRequest request, + Dictionary? payload + ) { - if (CanMakeAuthenticatedRequest(payload) || - (payload == null && request.RequestUri.AbsoluteUri.Contains("userDataStream"))) - { - request.AddHeader("X-MBX-APIKEY", PublicApiKey!.ToUnsecureString()); - } + if ( + CanMakeAuthenticatedRequest(payload) + || (payload == null && request.RequestUri.AbsoluteUri.Contains("userDataStream")) + ) + { + request.AddHeader("X-MBX-APIKEY", PublicApiKey!.ToUnsecureString()); + } return base.ProcessRequestAsync(request, payload); } - protected override Uri ProcessRequestUrl(UriBuilder url, Dictionary? payload, string? method) + protected override Uri ProcessRequestUrl( + UriBuilder url, + Dictionary? payload, + string? method + ) { if (CanMakeAuthenticatedRequest(payload)) { // payload is ignored, except for the nonce which is added to the url query - bittrex puts all the "post" parameters in the url query instead of the request body var query = (url.Query ?? string.Empty).Trim('?', '&'); - string newQuery = "timestamp=" + payload!["nonce"].ToStringInvariant() + (query.Length != 0 ? "&" + query : string.Empty) + - (payload.Count > 1 ? "&" + CryptoUtility.GetFormForPayload(payload, false) : string.Empty); - string signature = CryptoUtility.SHA256Sign(newQuery, CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); + string newQuery = + "timestamp=" + + payload!["nonce"].ToStringInvariant() + + (query.Length != 0 ? "&" + query : string.Empty) + + ( + payload.Count > 1 + ? "&" + CryptoUtility.GetFormForPayload(payload, false) + : string.Empty + ); + string signature = CryptoUtility.SHA256Sign( + newQuery, + CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey) + ); newQuery += "&signature=" + signature; url.Query = newQuery; return url.Uri; @@ -1064,18 +1439,25 @@ protected override Uri ProcessRequestUrl(UriBuilder url, Dictionary /// Deposit address details (including tag if applicable, such as XRP) /// - protected override async Task OnGetDepositAddressAsync(string currency, bool forceRegenerate = false) + protected override async Task OnGetDepositAddressAsync( + string currency, + bool forceRegenerate = false + ) { /* - * TODO: Binance does not offer a "regenerate" option in the API, but a second IOTA deposit to the same address will not be credited - * How does Binance handle GetDepositAddress for IOTA after it's been used once? - * Need to test calling this API after depositing IOTA. - */ + * TODO: Binance does not offer a "regenerate" option in the API, but a second IOTA deposit to the same address will not be credited + * How does Binance handle GetDepositAddress for IOTA after it's been used once? + * Need to test calling this API after depositing IOTA. + */ Dictionary payload = await GetNoncePayloadAsync(); payload["coin"] = currency; - JToken response = await MakeJsonRequestAsync("/capital/deposit/address", BaseUrlSApi, payload); + JToken response = await MakeJsonRequestAsync( + "/capital/deposit/address", + BaseUrlSApi, + payload + ); ExchangeDepositDetails depositDetails = new ExchangeDepositDetails { Currency = response["coin"].ToStringInvariant(), @@ -1089,7 +1471,9 @@ protected override async Task OnGetDepositAddressAsync(s /// Gets the deposit history for a symbol /// The currency to check. Null for all symbols. /// Collection of ExchangeCoinTransfers - protected override async Task> OnGetDepositHistoryAsync(string currency) + protected override async Task> OnGetDepositHistoryAsync( + string currency + ) { // TODO: API supports searching on status, startTime, endTime var payload = await GetNoncePayloadAsync(); @@ -1099,26 +1483,32 @@ protected override async Task> OnGetDepositHist payload["coin"] = currency; } - var response = await MakeJsonRequestAsync>("/capital/deposit/hisrec", BaseUrlSApi, payload); + var response = await MakeJsonRequestAsync>( + "/capital/deposit/hisrec", + BaseUrlSApi, + payload + ); var transactions = new List(); foreach (var item in response) { - transactions.Add(new ExchangeTransaction - { - Timestamp = item.InsertTime.UnixTimeStampToDateTimeMilliseconds(), - Amount = decimal.Parse(item.Amount), - Currency = item.Coin.ToUpperInvariant(), - Address = item.Address, - AddressTag = item.AddressTag, - BlockchainTxId = item.TxId, - Status = item.Status switch - { - 0 => TransactionStatus.Processing, - 1 => TransactionStatus.Complete, - _ => TransactionStatus.Unknown - } - }); + transactions.Add( + new ExchangeTransaction + { + Timestamp = item.InsertTime.UnixTimeStampToDateTimeMilliseconds(), + Amount = decimal.Parse(item.Amount), + Currency = item.Coin.ToUpperInvariant(), + Address = item.Address, + AddressTag = item.AddressTag, + BlockchainTxId = item.TxId, + Status = item.Status switch + { + 0 => TransactionStatus.Processing, + 1 => TransactionStatus.Complete, + _ => TransactionStatus.Unknown + } + } + ); } return transactions; @@ -1127,78 +1517,120 @@ protected override async Task> OnGetDepositHist protected override async Task OnUserDataWebSocketAsync(Action callback) { var listenKey = await GetListenKeyAsync(); - return await ConnectPublicWebSocketAsync($"/ws/{listenKey}", (_socket, msg) => - { - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - var eventType = token["e"].ToStringInvariant(); - switch (eventType) - { - case "executionReport": // systematically check to make sure we are dealing with expected cases here + return await ConnectPublicWebSocketAsync( + $"/ws/{listenKey}", + (_socket, msg) => + { + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + var eventType = token["e"].ToStringInvariant(); + switch (eventType) { - var update = JsonConvert.DeserializeObject(token.ToStringInvariant(), SerializerSettings); - switch (update.CurrentExecutionType) - { - case "NEW ": // The order has been accepted into the engine. + case "executionReport": // systematically check to make sure we are dealing with expected cases here + { + var update = JsonConvert.DeserializeObject( + token.ToStringInvariant(), + SerializerSettings + ); + switch (update.CurrentExecutionType) + { + case "NEW ": // The order has been accepted into the engine. + break; + case "CANCELED": // The order has been canceled by the user. + break; + case "REPLACED": // (currently unused) + throw new NotImplementedException( + $"ExecutionType {update.CurrentExecutionType} is currently unused" + ); + ; + case "REJECTED": // The order has been rejected and was not processed. (This is never pushed into the User Data Stream) + throw new NotImplementedException( + $"ExecutionType {update.CurrentExecutionType} is never pushed into the User Data Stream" + ); + ; + case "TRADE": // Part of the order or all of the order's quantity has filled. + break; + case "EXPIRED": // The order was canceled according to the order type's rules (e.g. LIMIT FOK orders with no fill, LIMIT IOC or MARKET orders that partially fill) or by the exchange, (e.g. orders canceled during liquidation, orders canceled during maintenance) + break; + default: + throw new NotImplementedException( + $"Unexpected ExecutionType {update.CurrentExecutionType}" + ); + } + callback(update.ExchangeOrderResult); break; - case "CANCELED": // The order has been canceled by the user. + } + case "outboundAccountInfo": + throw new NotImplementedException( + "has been removed (per binance 2021-01-01)" + ); + case "outboundAccountPosition": + { + var update = JsonConvert.DeserializeObject( + token.ToStringInvariant(), + SerializerSettings + ); + callback( + new ExchangeBalances() + { + EventTime = CryptoUtility.UnixTimeStampToDateTimeMilliseconds( + update.EventTime + ), + BalancesUpdateType = BalancesUpdateType.Total, + Balances = update.BalancesAsTotalDictionary + } + ); + callback( + new ExchangeBalances() + { + EventTime = CryptoUtility.UnixTimeStampToDateTimeMilliseconds( + update.EventTime + ), + BalancesUpdateType = BalancesUpdateType.AvailableToTrade, + Balances = update.BalancesAsAvailableToTradeDictionary + } + ); break; - case "REPLACED": // (currently unused) - throw new NotImplementedException($"ExecutionType {update.CurrentExecutionType} is currently unused"); ; - case "REJECTED": // The order has been rejected and was not processed. (This is never pushed into the User Data Stream) - throw new NotImplementedException($"ExecutionType {update.CurrentExecutionType} is never pushed into the User Data Stream"); ; - case "TRADE": // Part of the order or all of the order's quantity has filled. + } + case "listStatus": + { // fired as part of OCO order update + // var update = JsonConvert.DeserializeObject(token.ToStringInvariant()); + // no need to parse or call callback(), since OCO updates also send "executionReport" break; - case "EXPIRED": // The order was canceled according to the order type's rules (e.g. LIMIT FOK orders with no fill, LIMIT IOC or MARKET orders that partially fill) or by the exchange, (e.g. orders canceled during liquidation, orders canceled during maintenance) + } + case "balanceUpdate": + { + var update = JsonConvert.DeserializeObject( + token.ToStringInvariant(), + SerializerSettings + ); + callback( + new ExchangeBalances() + { + EventTime = CryptoUtility.UnixTimeStampToDateTimeMilliseconds( + update.EventTime + ), + BalancesUpdateType = BalancesUpdateType.Delta, + Balances = new Dictionary() + { + { update.Asset, update.BalanceDelta } + } + } + ); break; - default: throw new NotImplementedException($"Unexpected ExecutionType {update.CurrentExecutionType}"); - } - callback(update.ExchangeOrderResult); - break; - } - case "outboundAccountInfo": - throw new NotImplementedException("has been removed (per binance 2021-01-01)"); - case "outboundAccountPosition": - { - var update = JsonConvert.DeserializeObject(token.ToStringInvariant(), SerializerSettings); - callback(new ExchangeBalances() - { - EventTime = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(update.EventTime), - BalancesUpdateType = BalancesUpdateType.Total, - Balances = update.BalancesAsTotalDictionary - }); - callback(new ExchangeBalances() - { - EventTime = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(update.EventTime), - BalancesUpdateType = BalancesUpdateType.AvailableToTrade, - Balances = update.BalancesAsAvailableToTradeDictionary - }); - break; + } + default: + throw new NotImplementedException($"Unexpected event type {eventType}"); } - case "listStatus": - { // fired as part of OCO order update - // var update = JsonConvert.DeserializeObject(token.ToStringInvariant()); - // no need to parse or call callback(), since OCO updates also send "executionReport" - break; - } - case "balanceUpdate": - { - var update = JsonConvert.DeserializeObject(token.ToStringInvariant(), SerializerSettings); - callback(new ExchangeBalances() - { - EventTime = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(update.EventTime), - BalancesUpdateType = BalancesUpdateType.Delta, - Balances = new Dictionary() { { update.Asset, update.BalanceDelta } } - }); - break; - } - default: throw new NotImplementedException($"Unexpected event type {eventType}"); - } - return Task.CompletedTask; - }); + return Task.CompletedTask; + } + ); } - protected override async Task OnGetCandlesWebSocketAsync(Func callbackAsync, - int periodSeconds, params string[] marketSymbols) + protected override async Task OnGetCandlesWebSocketAsync( + Func callbackAsync, + int periodSeconds, + params string[] marketSymbols + ) { if (!marketSymbols.Any()) { @@ -1211,27 +1643,46 @@ protected override async Task OnGetCandlesWebSocketAsync(Func 2048) { throw new InvalidOperationException( - $"URL length over the limit of 2048 characters. Consider splitting instruments in multiple connections."); + $"URL length over the limit of 2048 characters. Consider splitting instruments in multiple connections." + ); } - return await ConnectPublicWebSocketAsync(url, async (_socket, msg) => - { - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - - if (token?["data"]?["k"] != null && token["data"]["s"] != null) - { - var candle = this.ParseCandle(token["data"]["k"], token["data"]["s"].ToStringInvariant(), - periodSeconds, "o", "h", "l", "c", "t", - TimestampType.UnixMilliseconds, "v", "q"); + return await ConnectPublicWebSocketAsync( + url, + async (_socket, msg) => + { + JToken token = JToken.Parse(msg.ToStringFromUTF8()); - await callbackAsync(candle); - } - }); + if (token?["data"]?["k"] != null && token["data"]["s"] != null) + { + var candle = this.ParseCandle( + token["data"]["k"], + token["data"]["s"].ToStringInvariant(), + periodSeconds, + "o", + "h", + "l", + "c", + "t", + TimestampType.UnixMilliseconds, + "v", + "q" + ); + + await callbackAsync(candle); + } + } + ); } public async Task GetListenKeyAsync() { - JToken response = await MakeJsonRequestAsync("/userDataStream", BaseUrlApi, null, "POST"); + JToken response = await MakeJsonRequestAsync( + "/userDataStream", + BaseUrlApi, + null, + "POST" + ); var listenKey = response["listenKey"].ToStringInvariant(); return listenKey; } diff --git a/src/ExchangeSharp/API/Exchanges/BinanceGroup/ExchangeBinanceAPI.cs b/src/ExchangeSharp/API/Exchanges/BinanceGroup/ExchangeBinanceAPI.cs index 81221609e..2e15dabec 100644 --- a/src/ExchangeSharp/API/Exchanges/BinanceGroup/ExchangeBinanceAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/BinanceGroup/ExchangeBinanceAPI.cs @@ -20,5 +20,8 @@ public sealed class ExchangeBinanceAPI : BinanceGroupCommon public override string BaseUrlWebSocket { get; set; } = "wss://stream.binance.com:9443"; } - public partial class ExchangeName { public const string Binance = "Binance"; } + public partial class ExchangeName + { + public const string Binance = "Binance"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/BinanceGroup/ExchangeBinanceDEXAPI.cs b/src/ExchangeSharp/API/Exchanges/BinanceGroup/ExchangeBinanceDEXAPI.cs index bc58f11c6..faec00238 100644 --- a/src/ExchangeSharp/API/Exchanges/BinanceGroup/ExchangeBinanceDEXAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/BinanceGroup/ExchangeBinanceDEXAPI.cs @@ -10,12 +10,12 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -using ExchangeSharp.BinanceGroup; -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using ExchangeSharp.BinanceGroup; +using Newtonsoft.Json.Linq; namespace ExchangeSharp { @@ -23,11 +23,12 @@ public sealed class ExchangeBinanceDEXAPI : ExchangeAPI { // unfortunately, Binance DEX doesn't share the same API as Binance and Binance.US, so it shouldn't inherit from BinanceGroupCommon public override string BaseUrl { get; set; } = "https://dex.binance.org/api/v1"; public override string BaseUrlWebSocket { get; set; } = "wss://dex.binance.org/api/ws"; + //public override string BaseUrlPrivate { get; set; } = "https://dex.binance.org/api/v3"; //public override string WithdrawalUrlPrivate { get; set; } = "https://dex.binance.org/wapi/v3"; protected override async Task> OnGetMarketSymbolsAsync() => - (await GetMarketSymbolsMetadataAsync()).Select(msm => msm.MarketSymbol); + (await GetMarketSymbolsMetadataAsync()).Select(msm => msm.MarketSymbol); public override async Task> GetMarketSymbolsMetadataAsync() { @@ -36,7 +37,9 @@ public override async Task> GetMarketSymbolsMetadata JToken allSymbols = await MakeJsonRequestAsync("/markets"); foreach (JToken marketSymbolToken in allSymbols) { - var QuoteCurrency = marketSymbolToken["quote_asset_symbol"].ToStringUpperInvariant(); + var QuoteCurrency = marketSymbolToken[ + "quote_asset_symbol" + ].ToStringUpperInvariant(); var BaseCurrency = marketSymbolToken["base_asset_symbol"].ToStringUpperInvariant(); var market = new ExchangeMarket { @@ -63,72 +66,99 @@ public override async Task> GetMarketSymbolsMetadata /// /// /// - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + protected override async Task OnGetTradesWebSocketAsync( + Func, Task> callback, + params string[] marketSymbols + ) { /* actual data, followed by example data { - "stream": "trades", - "data": [{ - "e": "trade", - "E": 47515444, - "s": "CBM-4B2_BNB", - "t": "47515444-0", - "p": "0.00000722", - "q": "24000.00000000", - "b": "F36B58E668004610B5D1DB23A40B496DF27A4D91-91340", - "a": "E9F71B7AFD3325590624A692B7DF5B449AA9BA19-91463", - "T": 1573417196130225520, - "sa": "bnb1a8m3k7haxvj4jp3y56ft0h6mgjd2nwsel6xsx8", - "ba": "bnb17d443engqprppdw3mv36gz6fdhe85nv37p2xq4", - "tt": 1 - }, - { - "e": "trade", // Event type - "E": 123456795, // Event time - "s": "BNB_BTC", // Symbol - "t": "12348", // Trade ID - "p": "0.001", // Price - "q": "100", // Quantity - "b": "88", // Buyer order ID - "a": "52", // Seller order ID - "T": 123456795, // Trade time - "sa": "bnb1me5u083m2spzt8pw8vunprnctc8syy64hegrcp", // SellerAddress - "ba": "bnb1kdr00ydr8xj3ydcd3a8ej2xxn8lkuja7mdunr5" // BuyerAddress - "tt": 1 //tiekertype 0: Unknown 1: SellTaker 2: BuyTaker 3: BuySurplus 4: SellSurplus 5: Neutral - }] + "stream": "trades", + "data": [{ + "e": "trade", + "E": 47515444, + "s": "CBM-4B2_BNB", + "t": "47515444-0", + "p": "0.00000722", + "q": "24000.00000000", + "b": "F36B58E668004610B5D1DB23A40B496DF27A4D91-91340", + "a": "E9F71B7AFD3325590624A692B7DF5B449AA9BA19-91463", + "T": 1573417196130225520, + "sa": "bnb1a8m3k7haxvj4jp3y56ft0h6mgjd2nwsel6xsx8", + "ba": "bnb17d443engqprppdw3mv36gz6fdhe85nv37p2xq4", + "tt": 1 + }, + { + "e": "trade", // Event type + "E": 123456795, // Event time + "s": "BNB_BTC", // Symbol + "t": "12348", // Trade ID + "p": "0.001", // Price + "q": "100", // Quantity + "b": "88", // Buyer order ID + "a": "52", // Seller order ID + "T": 123456795, // Trade time + "sa": "bnb1me5u083m2spzt8pw8vunprnctc8syy64hegrcp", // SellerAddress + "ba": "bnb1kdr00ydr8xj3ydcd3a8ej2xxn8lkuja7mdunr5" // BuyerAddress + "tt": 1 //tiekertype 0: Unknown 1: SellTaker 2: BuyTaker 3: BuySurplus 4: SellSurplus 5: Neutral + }] } - */ + */ if (marketSymbols == null || marketSymbols.Length == 0) { marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); } - return await ConnectPublicWebSocketAsync(string.Empty, messageCallback: async (_socket, msg) => - { - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - if (token["stream"].ToStringLowerInvariant() == "trades") - { - foreach (var data in token["data"]) + return await ConnectPublicWebSocketAsync( + string.Empty, + messageCallback: async (_socket, msg) => { - string name = data["s"].ToStringInvariant(); - string marketSymbol = NormalizeMarketSymbol(name); + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + if (token["stream"].ToStringLowerInvariant() == "trades") + { + foreach (var data in token["data"]) + { + string name = data["s"].ToStringInvariant(); + string marketSymbol = NormalizeMarketSymbol(name); - await callback(new KeyValuePair(marketSymbol, - data.ParseTradeBinanceDEX(amountKey: "q", priceKey: "p", typeKey: "tt", - timestampKey: "T", // use trade time (T) instead of event time (E) - timestampType: TimestampType.UnixNanoseconds, idKey: "t", typeKeyIsBuyValue: "BuyTaker"))); + await callback( + new KeyValuePair( + marketSymbol, + data.ParseTradeBinanceDEX( + amountKey: "q", + priceKey: "p", + typeKey: "tt", + timestampKey: "T", // use trade time (T) instead of event time (E) + timestampType: TimestampType.UnixNanoseconds, + idKey: "t", + typeKeyIsBuyValue: "BuyTaker" + ) + ) + ); + } + } + else if (token["error"] != null) + { // {{ "method": "subscribe", "error": { "error": "Invalid symbol(s)" }}} + Logger.Info(token["error"]["error"].ToStringInvariant()); + } + }, + connectCallback: async (_socket) => + { + await _socket.SendMessageAsync( + new + { + method = "subscribe", + topic = "trades", + symbols = marketSymbols + } + ); } - } - else if (token["error"] != null) - { // {{ "method": "subscribe", "error": { "error": "Invalid symbol(s)" }}} - Logger.Info(token["error"]["error"].ToStringInvariant()); - } - }, connectCallback: async (_socket) => - { - await _socket.SendMessageAsync(new { method = "subscribe", topic = "trades", symbols = marketSymbols }); - }); + ); } } - public partial class ExchangeName { public const string BinanceDEX = "BinanceDEX"; } + public partial class ExchangeName + { + public const string BinanceDEX = "BinanceDEX"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/BinanceGroup/ExchangeBinanceJerseyAPI.cs b/src/ExchangeSharp/API/Exchanges/BinanceGroup/ExchangeBinanceJerseyAPI.cs index 3f53b4eea..386c54b8a 100644 --- a/src/ExchangeSharp/API/Exchanges/BinanceGroup/ExchangeBinanceJerseyAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/BinanceGroup/ExchangeBinanceJerseyAPI.cs @@ -20,5 +20,8 @@ public class ExchangeBinanceJerseyAPI : BinanceGroupCommon public override string BaseUrlWebSocket { get; set; } = "wss://stream.binance.je:9443"; } - public partial class ExchangeName { public const string BinanceJersey = "BinanceJersey"; } + public partial class ExchangeName + { + public const string BinanceJersey = "BinanceJersey"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/BinanceGroup/ExchangeBinanceUSAPI.cs b/src/ExchangeSharp/API/Exchanges/BinanceGroup/ExchangeBinanceUSAPI.cs index 24ddace38..b38c13fa3 100644 --- a/src/ExchangeSharp/API/Exchanges/BinanceGroup/ExchangeBinanceUSAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/BinanceGroup/ExchangeBinanceUSAPI.cs @@ -20,5 +20,8 @@ public sealed class ExchangeBinanceUSAPI : BinanceGroupCommon public override string BaseUrlWebSocket { get; set; } = "wss://stream.binance.us:9443"; } - public partial class ExchangeName { public const string BinanceUS = "BinanceUS"; } + public partial class ExchangeName + { + public const string BinanceUS = "BinanceUS"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/BinanceAggregateTrade.cs b/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/BinanceAggregateTrade.cs index f3d90872f..50db782ae 100644 --- a/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/BinanceAggregateTrade.cs +++ b/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/BinanceAggregateTrade.cs @@ -16,6 +16,7 @@ public sealed class BinanceAggregateTrade : ExchangeTrade { public long FirstTradeId { get; set; } public long LastTradeId { get; set; } + public override string ToString() { return string.Format("{0},{1},{2}", base.ToString(), FirstTradeId, LastTradeId); diff --git a/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/BinanceDEXTrade.cs b/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/BinanceDEXTrade.cs index de4699efa..a16ddce20 100644 --- a/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/BinanceDEXTrade.cs +++ b/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/BinanceDEXTrade.cs @@ -22,14 +22,28 @@ public sealed class BinanceDEXTrade : ExchangeTrade public string BuyerAddress { get; set; } public string SellerAddress { get; set; } public TickerType TickerType { get; set; } + public override string ToString() { - return string.Format("{0},{1},{2},{3},{4},{5}", base.ToString(), BuyerOrderId, SellerOrderId, BuyerAddress, SellerAddress, TickerType); + return string.Format( + "{0},{1},{2},{3},{4},{5}", + base.ToString(), + BuyerOrderId, + SellerOrderId, + BuyerAddress, + SellerAddress, + TickerType + ); } } public enum TickerType : byte { // tiekertype 0: Unknown 1: SellTaker 2: BuyTaker 3: BuySurplus 4: SellSurplus 5: Neutral - Unknown = 0, SellTaker = 1, BuyTaker = 2, BuySurplus = 3, SellSurplus = 4, Neutral = 5 + Unknown = 0, + SellTaker = 1, + BuyTaker = 2, + BuySurplus = 3, + SellSurplus = 4, + Neutral = 5 } } diff --git a/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/HistoryRecord.cs b/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/HistoryRecord.cs index 2a91b68ca..0d6b72c05 100644 --- a/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/HistoryRecord.cs +++ b/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/HistoryRecord.cs @@ -36,5 +36,5 @@ public class HistoryRecord [JsonProperty("confirmTimes")] public string ConfirmTimes { get; set; } - } + } } diff --git a/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/MarketDepthDiffUpdate.cs b/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/MarketDepthDiffUpdate.cs index 37da179bf..d246ff985 100644 --- a/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/MarketDepthDiffUpdate.cs +++ b/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/MarketDepthDiffUpdate.cs @@ -12,31 +12,31 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp.BinanceGroup { - using System.Collections.Generic; + using System.Collections.Generic; - using Newtonsoft.Json; + using Newtonsoft.Json; - internal class MarketDepthDiffUpdate - { - [JsonProperty("e")] - public string EventType { get; set; } + internal class MarketDepthDiffUpdate + { + [JsonProperty("e")] + public string EventType { get; set; } - [JsonProperty("E")] - public long EventTime { get; set; } + [JsonProperty("E")] + public long EventTime { get; set; } - [JsonProperty("s")] - public string MarketSymbol { get; set; } + [JsonProperty("s")] + public string MarketSymbol { get; set; } - [JsonProperty("U")] - public long FirstUpdate { get; set; } + [JsonProperty("U")] + public long FirstUpdate { get; set; } - [JsonProperty("u")] - public long FinalUpdate { get; set; } + [JsonProperty("u")] + public long FinalUpdate { get; set; } - [JsonProperty("b")] - public List> Bids { get; set; } + [JsonProperty("b")] + public List> Bids { get; set; } - [JsonProperty("a")] - public List> Asks { get; set; } - } -} \ No newline at end of file + [JsonProperty("a")] + public List> Asks { get; set; } + } +} diff --git a/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/MultiDepthStream.cs b/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/MultiDepthStream.cs index e771f4ab3..9c6f1573a 100644 --- a/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/MultiDepthStream.cs +++ b/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/MultiDepthStream.cs @@ -12,14 +12,14 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp.BinanceGroup { - using Newtonsoft.Json; + using Newtonsoft.Json; - internal class MultiDepthStream - { - [JsonProperty("stream")] - public string Stream { get; set; } + internal class MultiDepthStream + { + [JsonProperty("stream")] + public string Stream { get; set; } - [JsonProperty("data")] - public MarketDepthDiffUpdate Data { get; set; } - } -} \ No newline at end of file + [JsonProperty("data")] + public MarketDepthDiffUpdate Data { get; set; } + } +} diff --git a/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/UserDataStream.cs b/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/UserDataStream.cs index ac8693c23..00ccb4fbc 100644 --- a/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/UserDataStream.cs +++ b/src/ExchangeSharp/API/Exchanges/BinanceGroup/Models/UserDataStream.cs @@ -11,60 +11,88 @@ internal class ExecutionReport { [JsonProperty("e")] public string EventType { get; set; } + [JsonProperty("E")] public long EventTime { get; set; } + [JsonProperty("s")] public string Symbol { get; set; } + [JsonProperty("c")] public string ClientOrderId { get; set; } + [JsonProperty("S")] public string Side { get; set; } + [JsonProperty("o")] public string OrderType { get; set; } + [JsonProperty("f")] public string TimeInForce { get; set; } + [JsonProperty("q")] public decimal OrderQuantity { get; set; } + [JsonProperty("p")] public decimal OrderPrice { get; set; } + [JsonProperty("P")] public decimal StopPrice { get; set; } + [JsonProperty("F")] public decimal IcebergQuantity { get; set; } + [JsonProperty("g")] public int OrderListId { get; set; } + [JsonProperty("C")] public string OriginalClientOrderId { get; set; } + [JsonProperty("x")] public string CurrentExecutionType { get; set; } + [JsonProperty("X")] public string CurrentOrderStatus { get; set; } + [JsonProperty("r")] public string OrderRejectReason { get; set; } + [JsonProperty("i")] public int OrderId { get; set; } + [JsonProperty("l")] public decimal LastExecutedQuantity { get; set; } + [JsonProperty("z")] public decimal CumulativeFilledQuantity { get; set; } + [JsonProperty("L")] public decimal LastExecutedPrice { get; set; } + [JsonProperty("n")] public decimal CommissionAmount { get; set; } + [JsonProperty("N")] public string CommissionAsset { get; set; } + [JsonProperty("T")] public long TransactionTime { get; set; } + [JsonProperty("t")] public string TradeId { get; set; } + [JsonProperty("w")] public string IsTheOrderWorking { get; set; } + [JsonProperty("m")] public string IsThisTradeTheMakerSide { get; set; } + [JsonProperty("O")] public long OrderCreationTime { get; set; } + [JsonProperty("Z")] public decimal CumulativeQuoteAssetTransactedQuantity { get; set; } + [JsonProperty("Y")] public decimal LastQuoteAssetTransactedQuantity { get; set; } @@ -80,7 +108,10 @@ public ExchangeOrderResult ExchangeOrderResult { get { - var status = BinanceGroupCommon.ParseExchangeAPIOrderResult(status: CurrentOrderStatus, amountFilled: CumulativeFilledQuantity); + var status = BinanceGroupCommon.ParseExchangeAPIOrderResult( + status: CurrentOrderStatus, + amountFilled: CumulativeFilledQuantity + ); return new ExchangeOrderResult() { OrderId = OrderId.ToString(), @@ -88,11 +119,18 @@ public ExchangeOrderResult ExchangeOrderResult Result = status, ResultCode = CurrentOrderStatus, Message = OrderRejectReason, // can use for multiple things in the future if needed - AmountFilled = TradeId != null ? LastExecutedQuantity : CumulativeFilledQuantity, + AmountFilled = + TradeId != null ? LastExecutedQuantity : CumulativeFilledQuantity, Price = OrderPrice, - AveragePrice = CumulativeQuoteAssetTransactedQuantity / CumulativeFilledQuantity, // Average price can be found by doing Z divided by z. - OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(OrderCreationTime), - CompletedDate = status.IsCompleted() ? (DateTime?)CryptoUtility.UnixTimeStampToDateTimeMilliseconds(TransactionTime) : null, + AveragePrice = + CumulativeQuoteAssetTransactedQuantity / CumulativeFilledQuantity, // Average price can be found by doing Z divided by z. + OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds( + OrderCreationTime + ), + CompletedDate = status.IsCompleted() + ? (DateTime?) + CryptoUtility.UnixTimeStampToDateTimeMilliseconds(TransactionTime) + : null, TradeDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(TransactionTime), UpdateSequence = EventTime, // in Binance, the sequence nymber is also the EventTime MarketSymbol = Symbol, @@ -109,8 +147,10 @@ internal class Order { [JsonProperty("s")] public string Symbol { get; set; } + [JsonProperty("i")] public int OrderId { get; set; } + [JsonProperty("c")] public string ClientOrderId { get; set; } @@ -124,24 +164,34 @@ internal class ListStatus { [JsonProperty("e")] public string EventType { get; set; } + [JsonProperty("E")] public long EventTime { get; set; } + [JsonProperty("s")] public string Symbol { get; set; } + [JsonProperty("g")] public int OrderListId { get; set; } + [JsonProperty("c")] public string ContingencyType { get; set; } + [JsonProperty("l")] public string ListStatusType { get; set; } + [JsonProperty("L")] public string ListOrderStatus { get; set; } + [JsonProperty("r")] public string ListRejectReason { get; set; } + [JsonProperty("C")] public string ListClientOrderId { get; set; } + [JsonProperty("T")] public long TransactionTime { get; set; } + [JsonProperty("O")] public List Orders { get; set; } @@ -160,12 +210,16 @@ internal class BalanceUpdate { [JsonProperty("e")] public string EventType { get; set; } + [JsonProperty("E")] public long EventTime { get; set; } + [JsonProperty("a")] public string Asset { get; set; } + [JsonProperty("d")] public decimal BalanceDelta { get; set; } + [JsonProperty("T")] public long ClearTime { get; set; } } @@ -177,8 +231,10 @@ internal class Balance { [JsonProperty("a")] public string Asset { get; set; } + [JsonProperty("f")] public decimal Free { get; set; } + [JsonProperty("l")] public decimal Locked { get; set; } @@ -196,10 +252,13 @@ internal class OutboundAccount { [JsonProperty("e")] public string EventType { get; set; } + [JsonProperty("E")] public long EventTime { get; set; } + [JsonProperty("u")] public long LastAccountUpdate { get; set; } + [JsonProperty("B")] public List Balances { get; set; } @@ -230,6 +289,5 @@ public Dictionary BalancesAsAvailableToTradeDictionary return dict; } } - } } diff --git a/src/ExchangeSharp/API/Exchanges/BitBank/ExchangeBitBankAPI.cs b/src/ExchangeSharp/API/Exchanges/BitBank/ExchangeBitBankAPI.cs index d6fb2da14..8f187c47d 100644 --- a/src/ExchangeSharp/API/Exchanges/BitBank/ExchangeBitBankAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/BitBank/ExchangeBitBankAPI.cs @@ -39,7 +39,9 @@ protected override async Task OnGetTickerAsync(string marketSymb // Bitbank supports endpoint for getting all rates in one request, Using this endpoint is faster then ExchangeAPI's default implementation // (which interate `OnGetTickerAsync` for each marketSymbols) // Note: This doesn't give you a volume. if you want it, please specify marketSymbol. - protected override async Task>> OnGetTickersAsync() + protected override async Task< + IEnumerable> + > OnGetTickersAsync() { JToken token = await MakeJsonRequestAsync($"/prices"); var symbols = await OnGetMarketSymbolsAsync(); @@ -63,7 +65,10 @@ protected override async Task>> #endregion Public APIs - protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) + protected override async Task OnGetOrderBookAsync( + string marketSymbol, + int maxCount = 100 + ) { JToken token = await MakeJsonRequestAsync($"/{marketSymbol}/transactions"); ExchangeOrderBook result = new ExchangeOrderBook(); @@ -86,7 +91,13 @@ protected override async Task OnGetOrderBookAsync(string mark return result; } - protected override async Task> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + protected override async Task> OnGetCandlesAsync( + string marketSymbol, + int periodSeconds, + DateTime? startDate = null, + DateTime? endDate = null, + int? limit = null + ) { var period = FormatPeriod(periodSeconds); var url = $"/{marketSymbol}/candlestick/{period}/{startDate?.ToString("yyyyMMdd")}"; @@ -98,7 +109,10 @@ protected override async Task> OnGetCandlesAsync(strin foreach (var data in c["ohlcv"]) { var open = data[0].ConvertInvariant(); - var timestamp = DateTime.SpecifyKind(data[5].ConvertInvariant().UnixTimeStampToDateTimeMilliseconds(), DateTimeKind.Utc); + var timestamp = DateTime.SpecifyKind( + data[5].ConvertInvariant().UnixTimeStampToDateTimeMilliseconds(), + DateTimeKind.Utc + ); var candle = new MarketCandle() { ExchangeName = "BitBank", @@ -118,14 +132,23 @@ protected override async Task> OnGetCandlesAsync(strin #region Private APIs - protected override async Task> OnGetAmountsAsync() => await OnGetAmountsAsyncCore("onhand_amount"); + protected override async Task> OnGetAmountsAsync() => + await OnGetAmountsAsyncCore("onhand_amount"); - protected override Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + protected override Task OnGetHistoricalTradesAsync( + Func, bool> callback, + string marketSymbol, + DateTime? startDate = null, + DateTime? endDate = null, + int? limit = null + ) { throw new NotImplementedException(); } - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + protected override async Task OnPlaceOrderAsync( + ExchangeOrderRequest order + ) { if (order.OrderType == OrderType.Stop) throw new NotSupportedException("Bitbank does not support stop order"); @@ -137,51 +160,95 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd payload.Add("side", order.IsBuy ? "buy" : "sell"); payload.Add("type", order.OrderType.ToStringLowerInvariant()); payload.Add("price", order.Price); - JToken token = await MakeJsonRequestAsync("/user/spot/order", baseUrl: BaseUrlPrivate, payload: payload, requestMethod: "POST"); + JToken token = await MakeJsonRequestAsync( + "/user/spot/order", + baseUrl: BaseUrlPrivate, + payload: payload, + requestMethod: "POST" + ); return ParseOrder(token); } - protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnCancelOrderAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { - if (isClientOrderId) throw new NotSupportedException("Cancelling by client order ID is not supported in ExchangeSharp. Please submit a PR if you are interested in this feature"); + if (isClientOrderId) + throw new NotSupportedException( + "Cancelling by client order ID is not supported in ExchangeSharp. Please submit a PR if you are interested in this feature" + ); Dictionary payload = await GetNoncePayloadAsync(); if (marketSymbol == null) throw new APIException("Bitbank requries market symbol when cancelling orders"); payload.Add("pair", NormalizeMarketSymbol(marketSymbol)); payload.Add("order_id", orderId); - await MakeJsonRequestAsync("/user/spot/cancel_order", baseUrl: BaseUrlPrivate, payload: payload, requestMethod: "POST"); + await MakeJsonRequestAsync( + "/user/spot/cancel_order", + baseUrl: BaseUrlPrivate, + payload: payload, + requestMethod: "POST" + ); } - protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnGetOrderDetailsAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { - if (isClientOrderId) throw new NotSupportedException("Querying by client order ID is not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature"); + if (isClientOrderId) + throw new NotSupportedException( + "Querying by client order ID is not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature" + ); var payload = await GetNoncePayloadAsync(); payload.Add("order_id", orderId); if (string.IsNullOrWhiteSpace(marketSymbol)) - throw new ArgumentNullException($"BitBank API requires marketSymbol for {nameof(GetOrderDetailsAsync)}"); + throw new ArgumentNullException( + $"BitBank API requires marketSymbol for {nameof(GetOrderDetailsAsync)}" + ); payload.Add("pair", marketSymbol); - JToken token = await MakeJsonRequestAsync("/user/spot/order", baseUrl: BaseUrlPrivate, payload: payload); + JToken token = await MakeJsonRequestAsync( + "/user/spot/order", + baseUrl: BaseUrlPrivate, + payload: payload + ); return ParseOrder(token); } - protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) + protected override async Task> OnGetOpenOrderDetailsAsync( + string marketSymbol = null + ) { var payload = await GetNoncePayloadAsync(); if (marketSymbol != null) payload.Add("pair", NormalizeMarketSymbol(marketSymbol)); - JToken token = await MakeJsonRequestAsync("/user/spot/active_orders", baseUrl: BaseUrlPrivate, payload: payload); + JToken token = await MakeJsonRequestAsync( + "/user/spot/active_orders", + baseUrl: BaseUrlPrivate, + payload: payload + ); return token["orders"].Select(o => ParseOrder(o)); } - protected override async Task> OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) + protected override async Task< + IEnumerable + > OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) { var payload = await GetNoncePayloadAsync(); if (marketSymbol == null) - throw new APIException("BitBank requires marketSymbol when getting completed orders"); + throw new APIException( + "BitBank requires marketSymbol when getting completed orders" + ); payload.Add("pair", NormalizeMarketSymbol(marketSymbol)); if (afterDate != null) payload.Add("since", afterDate.ConvertInvariant()); - JToken token = await MakeJsonRequestAsync($"/user/spot/trade_history", baseUrl: BaseUrlPrivate, payload: payload); + JToken token = await MakeJsonRequestAsync( + $"/user/spot/trade_history", + baseUrl: BaseUrlPrivate, + payload: payload + ); return token["trades"].Select(t => TradeHistoryToExchangeOrderResult(t)); } @@ -194,14 +261,26 @@ protected override async Task> OnGetCompletedOr /// /// /// - protected override async Task OnWithdrawAsync(ExchangeWithdrawalRequest withdrawalRequest) + protected override async Task OnWithdrawAsync( + ExchangeWithdrawalRequest withdrawalRequest + ) { var asset = withdrawalRequest.Currency.ToLowerInvariant(); var payload1 = await GetNoncePayloadAsync(); payload1.Add("asset", asset); - JToken token1 = await MakeJsonRequestAsync($"/user/withdrawal_account", baseUrl: BaseUrlPrivate, payload: payload1); - if (!token1["accounts"].ToArray().Any(a => a["address"].ToStringInvariant() == withdrawalRequest.Address)) - throw new APIException($"Could not withdraw to address {withdrawalRequest.Address}! You must register the address from web form first."); + JToken token1 = await MakeJsonRequestAsync( + $"/user/withdrawal_account", + baseUrl: BaseUrlPrivate, + payload: payload1 + ); + if ( + !token1["accounts"] + .ToArray() + .Any(a => a["address"].ToStringInvariant() == withdrawalRequest.Address) + ) + throw new APIException( + $"Could not withdraw to address {withdrawalRequest.Address}! You must register the address from web form first." + ); var uuid = token1["uuid"].ToStringInvariant(); @@ -209,11 +288,13 @@ protected override async Task OnWithdrawAsync(Exchan payload2.Add("asset", asset); payload2.Add("amount", withdrawalRequest.Amount); payload2.Add("uuid", uuid); - JToken token2 = await MakeJsonRequestAsync($"/user/request_withdrawal", baseUrl: BaseUrlPrivate, payload: payload2, requestMethod: "POST"); - var resp = new ExchangeWithdrawalResponse - { - Id = token2["txid"].ToStringInvariant() - }; + JToken token2 = await MakeJsonRequestAsync( + $"/user/request_withdrawal", + baseUrl: BaseUrlPrivate, + payload: payload2, + requestMethod: "POST" + ); + var resp = new ExchangeWithdrawalResponse { Id = token2["txid"].ToStringInvariant() }; var status = token2["status"].ToStringInvariant(); resp.Success = status != "REJECTED" && status != "CANCELED"; resp.Message = "{" + $"label:{token2["label"]}, fee:{token2["fee"]}" + "}"; @@ -228,16 +309,19 @@ protected override async Task OnWithdrawAsync(Exchan /// protected override Task> OnGetMarketSymbolsAsync() { - return Task.FromResult(new List { - "btc_jpy", - "xrp_jpy", - "ltc_btc", - "eth_btc", - "mona_jpy", - "mona_btc", - "bcc_jpy", - "bcc_btc" - }.AsEnumerable()); + return Task.FromResult( + new List + { + "btc_jpy", + "xrp_jpy", + "ltc_btc", + "eth_btc", + "mona_jpy", + "mona_btc", + "bcc_jpy", + "bcc_btc" + }.AsEnumerable() + ); } // protected override Task> OnGetCurrenciesAsync() => throw new NotImplementedException(); @@ -246,15 +330,18 @@ protected override Task> OnGetMarketSymbolsAsync() // protected override Task OnGetDepositAddressAsync(string currency, bool forceRegenerate = false) => throw new NotImplementedException(); // protected override Task> OnGetDepositHistoryAsync(string currency) => throw new NotImplementedException(); // protected override Task> OnGetFeesAsync() => throw new NotImplementedException(); - protected override async Task> OnGetAmountsAvailableToTradeAsync() - => await OnGetAmountsAsyncCore("free_amount"); + protected override async Task< + Dictionary + > OnGetAmountsAvailableToTradeAsync() => await OnGetAmountsAsyncCore("free_amount"); /// /// Bitbank does not support placing several orders at once, so we will just run `PlaceOrderAsync` for each orders. /// /// /// - protected override async Task OnPlaceOrdersAsync(params ExchangeOrderRequest[] order) + protected override async Task OnPlaceOrdersAsync( + params ExchangeOrderRequest[] order + ) { var resp = new List(); foreach (var o in order) @@ -267,9 +354,13 @@ protected override async Task OnPlaceOrdersAsync(params E // protected override Task OnGetOpenPositionAsync(string marketSymbol) => throw new NotImplementedException(); // protected override Task OnCloseMarginPositionAsync(string marketSymbol) => throw new NotImplementedException(); /* - */ + */ - protected override Uri ProcessRequestUrl(UriBuilder url, Dictionary payload, string method) + protected override Uri ProcessRequestUrl( + UriBuilder url, + Dictionary payload, + string method + ) { if (CanMakeAuthenticatedRequest(payload) && method == "GET" && payload.Count != 0) { @@ -281,7 +372,10 @@ protected override Uri ProcessRequestUrl(UriBuilder url, Dictionary payload) + protected override async Task ProcessRequestAsync( + IHttpWebRequest request, + Dictionary payload + ) { if (CanMakeAuthenticatedRequest(payload)) { @@ -301,9 +395,14 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti } else { - throw new APIException($"BitBank does not support {request.Method} as its HTTP method!"); + throw new APIException( + $"BitBank does not support {request.Method} as its HTTP method!" + ); } - string signature = CryptoUtility.SHA256Sign(stringToCommit, CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); + string signature = CryptoUtility.SHA256Sign( + stringToCommit, + CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey) + ); request.AddHeader("ACCESS-NONCE", nonce.ToStringInvariant()); request.AddHeader("ACCESS-KEY", PublicApiKey.ToUnsecureString()); @@ -314,7 +413,17 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti private async Task ParseTickerAsync(string symbol, JToken token) { - return await this.ParseTickerAsync(token, symbol, "sell", "buy", "last", "vol", quoteVolumeKey: null, "timestamp", TimestampType.UnixMilliseconds); + return await this.ParseTickerAsync( + token, + symbol, + "sell", + "buy", + "last", + "vol", + quoteVolumeKey: null, + "timestamp", + TimestampType.UnixMilliseconds + ); } private string FormatPeriod(int ps) @@ -332,15 +441,15 @@ private string FormatPeriod(int ps) else return "1hour"; /* These are not working - if (ps < 3600 * 4) - return "4hour"; - if (ps < 3600 * 8) - return "8hour"; - if (ps < 3600 * 12) - return "12hour"; - else - return "1day"; - */ + if (ps < 3600 * 4) + return "4hour"; + if (ps < 3600 * 8) + return "8hour"; + if (ps < 3600 * 12) + return "12hour"; + else + return "1day"; + */ } private ExchangeOrderResult ParseOrder(JToken token) @@ -349,7 +458,9 @@ private ExchangeOrderResult ParseOrder(JToken token) res.Amount = token["executed_amount"].ConvertInvariant(); res.AveragePrice = token["averate_price"].ConvertInvariant(); res.AmountFilled = token["executed_amount"].ConvertInvariant(); - res.OrderDate = token["ordered_at"].ConvertInvariant().UnixTimeStampToDateTimeMilliseconds(); + res.OrderDate = token["ordered_at"] + .ConvertInvariant() + .UnixTimeStampToDateTimeMilliseconds(); switch (token["status"].ToStringInvariant()) { // status enum: INACTIVE, UNFILLED, PARTIALLY_FILLED, FULLY_FILLED, CANCELED_UNFILLED, CANCELED_PARTIALLY_FILLED case "INACTIVE": @@ -377,7 +488,9 @@ private ExchangeOrderResult ParseOrder(JToken token) break; default: - throw new NotImplementedException($"Unexpected status type: {token["status"].ToStringInvariant()}"); + throw new NotImplementedException( + $"Unexpected status type: {token["status"].ToStringInvariant()}" + ); } return res; } @@ -386,7 +499,9 @@ private ExchangeOrderResult TradeHistoryToExchangeOrderResult(JToken token) { var res = ParseOrderCore(token); res.TradeId = token["trade_id"].ToStringInvariant(); - res.TradeDate = token["executed_at"].ConvertInvariant().UnixTimeStampToDateTimeMilliseconds(); + res.TradeDate = token["executed_at"] + .ConvertInvariant() + .UnixTimeStampToDateTimeMilliseconds(); res.Amount = token["amount"].ConvertInvariant(); res.AmountFilled = res.Amount; res.Fees = token["fee_amount_base"].ConvertInvariant(); @@ -406,17 +521,32 @@ private ExchangeOrderResult ParseOrderCore(JToken token) MarketSymbol = token["pair"].ToStringInvariant(), IsBuy = token["side"].ToStringInvariant() == "buy" }; - res.Fees = token["type"].ToStringInvariant() == "limit" ? MakerFee * res.Amount : TakerFee * res.Amount; + res.Fees = + token["type"].ToStringInvariant() == "limit" + ? MakerFee * res.Amount + : TakerFee * res.Amount; res.Price = token["price"].ConvertInvariant(); - res.CompletedDate = token["executed_at"] == null ? default : token["executed_at"].ConvertInvariant().UnixTimeStampToDateTimeMilliseconds(); + res.CompletedDate = + token["executed_at"] == null + ? default + : token["executed_at"] + .ConvertInvariant() + .UnixTimeStampToDateTimeMilliseconds(); res.FeesCurrency = res.MarketSymbol.Substring(0, 3); return res; } private async Task> OnGetAmountsAsyncCore(string type) { - JToken token = await MakeJsonRequestAsync($"/user/assets", baseUrl: BaseUrlPrivate, payload: await GetNoncePayloadAsync(), requestMethod: "GET"); - Dictionary balances = new Dictionary(StringComparer.OrdinalIgnoreCase); + JToken token = await MakeJsonRequestAsync( + $"/user/assets", + baseUrl: BaseUrlPrivate, + payload: await GetNoncePayloadAsync(), + requestMethod: "GET" + ); + Dictionary balances = new Dictionary( + StringComparer.OrdinalIgnoreCase + ); foreach (JToken assets in token["assets"]) { decimal amount = assets[type].ConvertInvariant(); @@ -427,5 +557,8 @@ private async Task> OnGetAmountsAsyncCore(string typ } } - public partial class ExchangeName { public const string BitBank = "BitBank"; } + public partial class ExchangeName + { + public const string BitBank = "BitBank"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/BitMEX/ExchangeBitMEXAPI.cs b/src/ExchangeSharp/API/Exchanges/BitMEX/ExchangeBitMEXAPI.cs index 398ffb59b..76ce7bb1c 100644 --- a/src/ExchangeSharp/API/Exchanges/BitMEX/ExchangeBitMEXAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/BitMEX/ExchangeBitMEXAPI.cs @@ -27,11 +27,14 @@ public sealed partial class ExchangeBitMEXAPI : ExchangeAPI { public override string BaseUrl { get; set; } = "https://www.bitmex.com/api/v1"; public override string BaseUrlWebSocket { get; set; } = "wss://www.bitmex.com/realtime"; + //public override string BaseUrl { get; set; } = "https://testnet.bitmex.com/api/v1"; //public override string BaseUrlWebSocket { get; set; } = "wss://testnet.bitmex.com/realtime"; - private SortedDictionary dict_long_decimal = new SortedDictionary(); - private SortedDictionary dict_decimal_long = new SortedDictionary(); + private SortedDictionary dict_long_decimal = + new SortedDictionary(); + private SortedDictionary dict_decimal_long = + new SortedDictionary(); private ExchangeBitMEXAPI() { @@ -49,17 +52,24 @@ private ExchangeBitMEXAPI() RateLimit = new RateGate(300, TimeSpan.FromMinutes(5)); } - public override Task ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol) + public override Task ExchangeMarketSymbolToGlobalMarketSymbolAsync( + string marketSymbol + ) { throw new NotImplementedException(); } - public override Task GlobalMarketSymbolToExchangeMarketSymbolAsync(string marketSymbol) + public override Task GlobalMarketSymbolToExchangeMarketSymbolAsync( + string marketSymbol + ) { throw new NotImplementedException(); } - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + protected override async Task ProcessRequestAsync( + IHttpWebRequest request, + Dictionary payload + ) { if (CanMakeAuthenticatedRequest(payload)) { @@ -67,8 +77,12 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti var nonce = payload["nonce"].ConvertInvariant(); payload.Remove("nonce"); var msg = CryptoUtility.GetJsonForPayload(payload); - var sign = $"{request.Method}{request.RequestUri.AbsolutePath}{request.RequestUri.Query}{nonce}{msg}"; - string signature = CryptoUtility.SHA256Sign(sign, CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); + var sign = + $"{request.Method}{request.RequestUri.AbsolutePath}{request.RequestUri.Query}{nonce}{msg}"; + string signature = CryptoUtility.SHA256Sign( + sign, + CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey) + ); request.AddHeader("api-expires", nonce.ToStringInvariant()); request.AddHeader("api-key", PublicApiKey.ToUnsecureString()); @@ -84,144 +98,160 @@ protected override async Task> OnGetMarketSymbolsAsync() return m.Select(x => x.MarketSymbol); } - - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() + protected internal override async Task< + IEnumerable + > OnGetMarketSymbolsMetadataAsync() { /* [ - { - "symbol": "XBTZ14", - "rootSymbol": "XBT", - "state": "Settled", - "typ": "FXXXS", - "listing": "2014-11-21T20:00:00.000Z", - "front": "2014-11-28T12:00:00.000Z", - "expiry": "2014-12-26T12:00:00.000Z", - "settle": "2014-12-26T12:00:00.000Z", - "listedSettle": "2014-12-26T12:00:00.000Z", - "relistInterval": null, - "inverseLeg": "", - "sellLeg": "", - "buyLeg": "", - "optionStrikePcnt": null, - "optionStrikeRound": null, - "optionStrikePrice": null, - "optionMultiplier": null, - "positionCurrency": "", - "underlying": "XBT", - "quoteCurrency": "USD", - "underlyingSymbol": "XBT=", - "reference": "BMEX", - "referenceSymbol": ".XBT2H", - "calcInterval": null, - "publishInterval": null, - "publishTime": null, - "maxOrderQty": 10000000, - "maxPrice": 1000000, - "lotSize": 1, - "tickSize": 0.01, - "multiplier": 1000, - "settlCurrency": "XBt", - "underlyingToPositionMultiplier": null, - "underlyingToSettleMultiplier": 100000000, - "quoteToSettleMultiplier": null, - "isQuanto": true, - "isInverse": false, - "initMargin": 0.3, - "maintMargin": 0.2, - "riskLimit": 25000000000, - "riskStep": 5000000000, - "limit": 0.2, - "capped": false, - "taxed": false, - "deleverage": false, - "makerFee": 0.00005, - "takerFee": 0.00005, - "settlementFee": 0.00005, - "insuranceFee": 0.00015, - "fundingBaseSymbol": "", - "fundingQuoteSymbol": "", - "fundingPremiumSymbol": "", - "fundingTimestamp": null, - "fundingInterval": null, - "fundingRate": null, - "indicativeFundingRate": null, - "rebalanceTimestamp": null, - "rebalanceInterval": null, - "openingTimestamp": "2014-12-26T12:00:00.000Z", - "closingTimestamp": "2014-12-26T12:00:00.000Z", - "sessionInterval": "2000-01-01T08:00:00.000Z", - "prevClosePrice": 319, - "limitDownPrice": 255.2, - "limitUpPrice": 382.8, - "bankruptLimitDownPrice": null, - "bankruptLimitUpPrice": null, - "prevTotalVolume": 323564, - "totalVolume": 348271, - "volume": 0, - "volume24h": 0, - "prevTotalTurnover": 0, - "totalTurnover": 0, - "turnover": 0, - "turnover24h": 0, - "homeNotional24h": 0, - "foreignNotional24h": 0, - "prevPrice24h": 323.33, - "vwap": null, - "highPrice": null, - "lowPrice": null, - "lastPrice": 323.33, - "lastPriceProtected": 323.33, - "lastTickDirection": "PlusTick", - "lastChangePcnt": 0, - "bidPrice": null, - "midPrice": null, - "askPrice": null, - "impactBidPrice": null, - "impactMidPrice": null, - "impactAskPrice": null, - "hasLiquidity": false, - "openInterest": 0, - "openValue": 0, - "fairMethod": "", - "fairBasisRate": null, - "fairBasis": 0, - "fairPrice": 323.33, - "markMethod": "LastPrice", - "markPrice": 323.33, - "indicativeTaxRate": null, - "indicativeSettlePrice": 323.33, - "optionUnderlyingPrice": null, - "settledPriceAdjustmentRate": null, - "settledPrice": 323.33, - "timestamp": "2014-11-21T21:00:02.409Z" - }, +{ +"symbol": "XBTZ14", +"rootSymbol": "XBT", +"state": "Settled", +"typ": "FXXXS", +"listing": "2014-11-21T20:00:00.000Z", +"front": "2014-11-28T12:00:00.000Z", +"expiry": "2014-12-26T12:00:00.000Z", +"settle": "2014-12-26T12:00:00.000Z", +"listedSettle": "2014-12-26T12:00:00.000Z", +"relistInterval": null, +"inverseLeg": "", +"sellLeg": "", +"buyLeg": "", +"optionStrikePcnt": null, +"optionStrikeRound": null, +"optionStrikePrice": null, +"optionMultiplier": null, +"positionCurrency": "", +"underlying": "XBT", +"quoteCurrency": "USD", +"underlyingSymbol": "XBT=", +"reference": "BMEX", +"referenceSymbol": ".XBT2H", +"calcInterval": null, +"publishInterval": null, +"publishTime": null, +"maxOrderQty": 10000000, +"maxPrice": 1000000, +"lotSize": 1, +"tickSize": 0.01, +"multiplier": 1000, +"settlCurrency": "XBt", +"underlyingToPositionMultiplier": null, +"underlyingToSettleMultiplier": 100000000, +"quoteToSettleMultiplier": null, +"isQuanto": true, +"isInverse": false, +"initMargin": 0.3, +"maintMargin": 0.2, +"riskLimit": 25000000000, +"riskStep": 5000000000, +"limit": 0.2, +"capped": false, +"taxed": false, +"deleverage": false, +"makerFee": 0.00005, +"takerFee": 0.00005, +"settlementFee": 0.00005, +"insuranceFee": 0.00015, +"fundingBaseSymbol": "", +"fundingQuoteSymbol": "", +"fundingPremiumSymbol": "", +"fundingTimestamp": null, +"fundingInterval": null, +"fundingRate": null, +"indicativeFundingRate": null, +"rebalanceTimestamp": null, +"rebalanceInterval": null, +"openingTimestamp": "2014-12-26T12:00:00.000Z", +"closingTimestamp": "2014-12-26T12:00:00.000Z", +"sessionInterval": "2000-01-01T08:00:00.000Z", +"prevClosePrice": 319, +"limitDownPrice": 255.2, +"limitUpPrice": 382.8, +"bankruptLimitDownPrice": null, +"bankruptLimitUpPrice": null, +"prevTotalVolume": 323564, +"totalVolume": 348271, +"volume": 0, +"volume24h": 0, +"prevTotalTurnover": 0, +"totalTurnover": 0, +"turnover": 0, +"turnover24h": 0, +"homeNotional24h": 0, +"foreignNotional24h": 0, +"prevPrice24h": 323.33, +"vwap": null, +"highPrice": null, +"lowPrice": null, +"lastPrice": 323.33, +"lastPriceProtected": 323.33, +"lastTickDirection": "PlusTick", +"lastChangePcnt": 0, +"bidPrice": null, +"midPrice": null, +"askPrice": null, +"impactBidPrice": null, +"impactMidPrice": null, +"impactAskPrice": null, +"hasLiquidity": false, +"openInterest": 0, +"openValue": 0, +"fairMethod": "", +"fairBasisRate": null, +"fairBasis": 0, +"fairPrice": 323.33, +"markMethod": "LastPrice", +"markPrice": 323.33, +"indicativeTaxRate": null, +"indicativeSettlePrice": 323.33, +"optionUnderlyingPrice": null, +"settledPriceAdjustmentRate": null, +"settledPrice": 323.33, +"timestamp": "2014-11-21T21:00:02.409Z" +}, */ List markets = new List(); - JToken allSymbols = await MakeJsonRequestAsync("/instrument?count=500&reverse=false"); + JToken allSymbols = await MakeJsonRequestAsync( + "/instrument?count=500&reverse=false" + ); foreach (JToken marketSymbolToken in allSymbols) { var market = new ExchangeMarket { MarketSymbol = marketSymbolToken["symbol"].ToStringUpperInvariant(), - IsActive = marketSymbolToken["state"].ToStringInvariant().EqualsWithOption("Open"), + IsActive = marketSymbolToken["state"] + .ToStringInvariant() + .EqualsWithOption("Open"), QuoteCurrency = marketSymbolToken["quoteCurrency"].ToStringUpperInvariant(), BaseCurrency = marketSymbolToken["underlying"].ToStringUpperInvariant(), }; try { - market.PriceStepSize = marketSymbolToken["tickSize"].ConvertInvariant(); + market.PriceStepSize = marketSymbolToken[ + "tickSize" + ].ConvertInvariant(); market.MaxPrice = marketSymbolToken["maxPrice"].ConvertInvariant(); //market.MinPrice = symbol["minPrice"].ConvertInvariant(); - BitMex does not provide min price - market.MaxTradeSize = marketSymbolToken["maxOrderQty"].ConvertInvariant(); - var underlyingToPositionMultiplier = marketSymbolToken["underlyingToPositionMultiplier"].Type == JTokenType.Null ? 1 : marketSymbolToken["underlyingToPositionMultiplier"].ConvertInvariant(); + market.MaxTradeSize = marketSymbolToken[ + "maxOrderQty" + ].ConvertInvariant(); + var underlyingToPositionMultiplier = + marketSymbolToken["underlyingToPositionMultiplier"].Type == JTokenType.Null + ? 1 + : marketSymbolToken[ + "underlyingToPositionMultiplier" + ].ConvertInvariant(); var contractSize = 1 / underlyingToPositionMultiplier; var lotSize = marketSymbolToken["lotSize"].ConvertInvariant(); var minTradeAmt = contractSize * lotSize; - var positionCurrency = marketSymbolToken["positionCurrency"]?.ToStringUpperInvariant(); + var positionCurrency = marketSymbolToken[ + "positionCurrency" + ]?.ToStringUpperInvariant(); if (positionCurrency == market.BaseCurrency) { market.MinTradeSize = market.QuantityStepSize = minTradeAmt; @@ -232,16 +262,16 @@ protected internal override async Task> OnGetMarketS } // else positionCurrency is probably null and the intrument is not active } - catch - { - - } + catch { } markets.Add(market); } return markets; } - protected override Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + protected override Task OnGetTradesWebSocketAsync( + Func, Task> callback, + params string[] marketSymbols + ) { /* {"table":"trade","action":"partial","keys":[], @@ -252,42 +282,70 @@ protected override Task OnGetTradesWebSocketAsync(Func - { - var str = msg.ToStringFromUTF8(); - JToken token = JToken.Parse(str); + return ConnectPublicWebSocketAsync( + string.Empty, + async (_socket, msg) => + { + var str = msg.ToStringFromUTF8(); + JToken token = JToken.Parse(str); - if (token["error"] != null) - { - Logger.Info(token["error"].ToStringInvariant()); - return; - } - else if (token["table"] == null) - { - return; - } + if (token["error"] != null) + { + Logger.Info(token["error"].ToStringInvariant()); + return; + } + else if (token["table"] == null) + { + return; + } - var action = token["action"].ToStringInvariant(); - JArray data = token["data"] as JArray; - foreach (var t in data) - { - var marketSymbol = t["symbol"].ToStringInvariant(); - await callback(new KeyValuePair(marketSymbol, t.ParseTrade("size", "price", "side", "timestamp", TimestampType.Iso8601UTC, "trdMatchID"))); - } - }, async (_socket) => - { - if (marketSymbols == null || marketSymbols.Length == 0) - { - await _socket.SendMessageAsync(new { op = "subscribe", args = "trade" }); - } - else - { - await _socket.SendMessageAsync(new { op = "subscribe", args = marketSymbols.Select(s => "trade:" + this.NormalizeMarketSymbol(s)).ToArray() }); - } - }); + var action = token["action"].ToStringInvariant(); + JArray data = token["data"] as JArray; + foreach (var t in data) + { + var marketSymbol = t["symbol"].ToStringInvariant(); + await callback( + new KeyValuePair( + marketSymbol, + t.ParseTrade( + "size", + "price", + "side", + "timestamp", + TimestampType.Iso8601UTC, + "trdMatchID" + ) + ) + ); + } + }, + async (_socket) => + { + if (marketSymbols == null || marketSymbols.Length == 0) + { + await _socket.SendMessageAsync(new { op = "subscribe", args = "trade" }); + } + else + { + await _socket.SendMessageAsync( + new + { + op = "subscribe", + args = marketSymbols + .Select(s => "trade:" + this.NormalizeMarketSymbol(s)) + .ToArray() + } + ); + } + } + ); } - protected override async Task OnGetDeltaOrderBookWebSocketAsync(Action callback, int maxCount = 20, params string[] marketSymbols) + protected override async Task OnGetDeltaOrderBookWebSocketAsync( + Action callback, + int maxCount = 20, + params string[] marketSymbols + ) { /* {"info":"Welcome to the BitMEX Realtime API.","version":"2018-06-29T18:05:14.000Z","timestamp":"2018-07-05T14:22:26.267Z","docs":"https://www.bitmex.com/app/wsAPI","limit":{"remaining":39}} @@ -299,80 +357,94 @@ protected override async Task OnGetDeltaOrderBookWebSocketAsync(Acti { marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); } - return await ConnectPublicWebSocketAsync(string.Empty, (_socket, msg) => - { - var str = msg.ToStringFromUTF8(); - JToken token = JToken.Parse(str); - - if (token["table"] == null) - { - return Task.CompletedTask; - } - - var action = token["action"].ToStringInvariant(); - JArray data = token["data"] as JArray; - - ExchangeOrderBook book = new ExchangeOrderBook(); - var price = 0m; - var size = 0m; - foreach (var d in data) - { - var marketSymbol = d["symbol"].ToStringInvariant(); - var id = d["id"].ConvertInvariant(); - if (d["price"] == null) + return await ConnectPublicWebSocketAsync( + string.Empty, + (_socket, msg) => { - if (!dict_long_decimal.TryGetValue(id, out price)) + var str = msg.ToStringFromUTF8(); + JToken token = JToken.Parse(str); + + if (token["table"] == null) { - continue; + return Task.CompletedTask; } - } - else - { - price = d["price"].ConvertInvariant(); - dict_long_decimal[id] = price; - dict_decimal_long[price] = id; - } - var side = d["side"].ToStringInvariant(); - - if (d["size"] == null) - { - size = 0m; - } - else - { - size = d["size"].ConvertInvariant(); - } + var action = token["action"].ToStringInvariant(); + JArray data = token["data"] as JArray; - var depth = new ExchangeOrderPrice { Price = price, Amount = size }; + ExchangeOrderBook book = new ExchangeOrderBook(); + var price = 0m; + var size = 0m; + foreach (var d in data) + { + var marketSymbol = d["symbol"].ToStringInvariant(); + var id = d["id"].ConvertInvariant(); + if (d["price"] == null) + { + if (!dict_long_decimal.TryGetValue(id, out price)) + { + continue; + } + } + else + { + price = d["price"].ConvertInvariant(); + dict_long_decimal[id] = price; + dict_decimal_long[price] = id; + } + + var side = d["side"].ToStringInvariant(); + + if (d["size"] == null) + { + size = 0m; + } + else + { + size = d["size"].ConvertInvariant(); + } + + var depth = new ExchangeOrderPrice { Price = price, Amount = size }; + + if (side.EqualsWithOption("Buy")) + { + book.Bids[depth.Price] = depth; + } + else + { + book.Asks[depth.Price] = depth; + } + book.MarketSymbol = marketSymbol; + } - if (side.EqualsWithOption("Buy")) - { - book.Bids[depth.Price] = depth; - } - else + if (!string.IsNullOrEmpty(book.MarketSymbol)) + { + callback(book); + } + return Task.CompletedTask; + }, + async (_socket) => { - book.Asks[depth.Price] = depth; + if (marketSymbols.Length == 0) + { + marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); + } + await _socket.SendMessageAsync( + new + { + op = "subscribe", + args = marketSymbols + .Select(s => "orderBookL2:" + this.NormalizeMarketSymbol(s)) + .ToArray() + } + ); } - book.MarketSymbol = marketSymbol; - } - - if (!string.IsNullOrEmpty(book.MarketSymbol)) - { - callback(book); - } - return Task.CompletedTask; - }, async (_socket) => - { - if (marketSymbols.Length == 0) - { - marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); - } - await _socket.SendMessageAsync(new { op = "subscribe", args = marketSymbols.Select(s => "orderBookL2:" + this.NormalizeMarketSymbol(s)).ToArray() }); - }); + ); } - protected override async Task OnGetPositionsWebSocketAsync(Action callback) + protected override async Task OnGetPositionsWebSocketAsync( + Action callback + ) { /* "{\"info\":\"Welcome to the BitMEX Realtime API.\",\"version\":\"2020-04-08T01:10:16.000Z\",\"timestamp\":\"2020-04-11T11:43:31.856Z\",\"docs\":\"https://testnet.bitmex.com/app/wsAPI\",\"limit\":{\"remaining\":39}}" @@ -381,42 +453,58 @@ protected override async Task OnGetPositionsWebSocketAsync(Action - { - var str = msg.ToStringFromUTF8(); - JToken token = JToken.Parse(str); - if (token["error"] != null) - { - Logger.Info(token["error"].ToStringInvariant()); - return Task.CompletedTask; - } - else if (token["table"] == null) - { - return Task.CompletedTask; - } - - JArray data = token["data"] as JArray; - foreach (var d in data) - { - var position = ParsePosition(d); - callback(position); - } - return Task.CompletedTask; + return await ConnectPublicWebSocketAsync( + string.Empty, + (_socket, msg) => + { + var str = msg.ToStringFromUTF8(); + JToken token = JToken.Parse(str); + if (token["error"] != null) + { + Logger.Info(token["error"].ToStringInvariant()); + return Task.CompletedTask; + } + else if (token["table"] == null) + { + return Task.CompletedTask; + } - }, async (_socket) => - { - long nonce = (await GenerateNonceAsync()).ConvertInvariant(); - var authPayload = $"GET/realtime{nonce}"; - string signature = CryptoUtility.SHA256Sign(authPayload, CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); - - var authArgs = new object[]{PublicApiKey.ToUnsecureString(), nonce, signature}; - await _socket.SendMessageAsync(new { op = "authKeyExpires", args = authArgs }); - await _socket.SendMessageAsync(new { op = "subscribe", args = "position" }); - }); + JArray data = token["data"] as JArray; + foreach (var d in data) + { + var position = ParsePosition(d); + callback(position); + } + return Task.CompletedTask; + }, + async (_socket) => + { + long nonce = (await GenerateNonceAsync()).ConvertInvariant(); + var authPayload = $"GET/realtime{nonce}"; + string signature = CryptoUtility.SHA256Sign( + authPayload, + CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey) + ); + + var authArgs = new object[] + { + PublicApiKey.ToUnsecureString(), + nonce, + signature + }; + await _socket.SendMessageAsync(new { op = "authKeyExpires", args = authArgs }); + await _socket.SendMessageAsync(new { op = "subscribe", args = "position" }); + } + ); } - - protected override async Task> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + protected override async Task> OnGetCandlesAsync( + string marketSymbol, + int periodSeconds, + DateTime? startDate = null, + DateTime? endDate = null, + int? limit = null + ) { /* [ @@ -426,7 +514,9 @@ protected override async Task> OnGetCandlesAsync(strin List candles = new List(); string periodString = PeriodSecondsToString(periodSeconds); - string url = $"/trade/bucketed?binSize={periodString}&partial=false&symbol={marketSymbol}&reverse=true" + marketSymbol; + string url = + $"/trade/bucketed?binSize={periodString}&partial=false&symbol={marketSymbol}&reverse=true" + + marketSymbol; if (startDate != null) { url += "&startTime=" + startDate.Value.ToString("yyyy-MM-ddTHH:mm:ss"); @@ -443,7 +533,22 @@ protected override async Task> OnGetCandlesAsync(strin var obj = await MakeJsonRequestAsync(url); foreach (var t in obj) { - candles.Add(this.ParseCandle(t, marketSymbol, periodSeconds, "open", "high", "low", "close", "timestamp", TimestampType.Iso8601UTC, "volume", "turnover", "vwap")); + candles.Add( + this.ParseCandle( + t, + marketSymbol, + periodSeconds, + "open", + "high", + "low", + "close", + "timestamp", + TimestampType.Iso8601UTC, + "volume", + "turnover", + "vwap" + ) + ); } candles.Reverse(); @@ -451,16 +556,18 @@ protected override async Task> OnGetCandlesAsync(strin } public async Task> GetHistoricalTradesAsync( - string marketSymbol = null, - DateTime? startDate = null, - DateTime? endDate = null, - int? startingIndex = null, - int? limit = 1000) + string marketSymbol = null, + DateTime? startDate = null, + DateTime? endDate = null, + int? startingIndex = null, + int? limit = 1000 + ) { List trades = new List(); Dictionary payload = await GetNoncePayloadAsync(); string url = "/trade?"; - url += "&columns=[\"symbol\", \"size\", \"price\", \"side\", \"timestamp\", \"trdMatchID\"]"; + url += + "&columns=[\"symbol\", \"size\", \"price\", \"side\", \"timestamp\", \"trdMatchID\"]"; if (!string.IsNullOrWhiteSpace(marketSymbol)) { url += "&symbol=" + NormalizeMarketSymbol(marketSymbol); @@ -485,7 +592,16 @@ public async Task> GetHistoricalTradesAsync( var obj = await MakeJsonRequestAsync(url); foreach (var t in obj) { - trades.Add(t.ParseTrade("size", "price", "side", "timestamp", TimestampType.Iso8601UTC, "trdMatchID")); + trades.Add( + t.ParseTrade( + "size", + "price", + "side", + "timestamp", + TimestampType.Iso8601UTC, + "trdMatchID" + ) + ); } return trades; @@ -495,56 +611,60 @@ protected override async Task> OnGetAmountsAsync() { /* {[ - { - "account": 93592, - "currency": "XBt", - "riskLimit": 1000000000000, - "prevState": "", - "state": "", - "action": "", - "amount": 141755795, - "pendingCredit": 0, - "pendingDebit": 0, - "confirmedDebit": 0, - "prevRealisedPnl": 0, - "prevUnrealisedPnl": 0, - "grossComm": 0, - "grossOpenCost": 0, - "grossOpenPremium": 0, - "grossExecCost": 0, - "grossMarkValue": 0, - "riskValue": 0, - "taxableMargin": 0, - "initMargin": 0, - "maintMargin": 0, - "sessionMargin": 0, - "targetExcessMargin": 0, - "varMargin": 0, - "realisedPnl": 0, - "unrealisedPnl": 0, - "indicativeTax": 0, - "unrealisedProfit": 0, - "syntheticMargin": 0, - "walletBalance": 141755795, - "marginBalance": 141755795, - "marginBalancePcnt": 1, - "marginLeverage": 0, - "marginUsedPcnt": 0, - "excessMargin": 141755795, - "excessMarginPcnt": 1, - "availableMargin": 141755795, - "withdrawableMargin": 141755795, - "timestamp": "2018-07-08T07:40:24.395Z", - "grossLastValue": 0, - "commission": null - } +{ +"account": 93592, +"currency": "XBt", +"riskLimit": 1000000000000, +"prevState": "", +"state": "", +"action": "", +"amount": 141755795, +"pendingCredit": 0, +"pendingDebit": 0, +"confirmedDebit": 0, +"prevRealisedPnl": 0, +"prevUnrealisedPnl": 0, +"grossComm": 0, +"grossOpenCost": 0, +"grossOpenPremium": 0, +"grossExecCost": 0, +"grossMarkValue": 0, +"riskValue": 0, +"taxableMargin": 0, +"initMargin": 0, +"maintMargin": 0, +"sessionMargin": 0, +"targetExcessMargin": 0, +"varMargin": 0, +"realisedPnl": 0, +"unrealisedPnl": 0, +"indicativeTax": 0, +"unrealisedProfit": 0, +"syntheticMargin": 0, +"walletBalance": 141755795, +"marginBalance": 141755795, +"marginBalancePcnt": 1, +"marginLeverage": 0, +"marginUsedPcnt": 0, +"excessMargin": 141755795, +"excessMarginPcnt": 1, +"availableMargin": 141755795, +"withdrawableMargin": 141755795, +"timestamp": "2018-07-08T07:40:24.395Z", +"grossLastValue": 0, +"commission": null +} ]} */ Dictionary amounts = new Dictionary(); var payload = await GetNoncePayloadAsync(); - JToken token = await MakeJsonRequestAsync($"/user/margin?currency=all", BaseUrl, payload); + JToken token = await MakeJsonRequestAsync( + $"/user/margin?currency=all", + BaseUrl, + payload + ); foreach (var item in token) { var balance = item["marginBalance"].ConvertInvariant(); @@ -562,11 +682,17 @@ protected override async Task> OnGetAmountsAsync() return amounts; } - protected override async Task> OnGetAmountsAvailableToTradeAsync() + protected override async Task< + Dictionary + > OnGetAmountsAvailableToTradeAsync() { Dictionary amounts = new Dictionary(); var payload = await GetNoncePayloadAsync(); - JToken token = await MakeJsonRequestAsync($"/user/margin?currency=all", BaseUrl, payload); + JToken token = await MakeJsonRequestAsync( + $"/user/margin?currency=all", + BaseUrl, + payload + ); foreach (var item in token) { var balance = item["availableMargin"].ConvertInvariant(); @@ -588,7 +714,8 @@ public async Task> GetCurrentPositionsAsync() { var payload = await GetNoncePayloadAsync(); string url = "/position?"; - url += "&columns=[\"symbol\", \"currentQty\", \"avgEntryPrice\", \"liquidationPrice\", \"leverage\", \"lastPrice\", \"currentTimestamp\"]"; + url += + "&columns=[\"symbol\", \"currentQty\", \"avgEntryPrice\", \"liquidationPrice\", \"leverage\", \"lastPrice\", \"currentTimestamp\"]"; JToken token = await MakeJsonRequestAsync(url, BaseUrl, payload); List positions = new List(); foreach (var item in token) @@ -598,7 +725,9 @@ public async Task> GetCurrentPositionsAsync() return positions; } - protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) + protected override async Task> OnGetOpenOrderDetailsAsync( + string marketSymbol = null + ) { List orders = new List(); Dictionary payload = await GetNoncePayloadAsync(); @@ -617,11 +746,16 @@ protected override async Task> OnGetOpenOrderDe return orders; } - protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnGetOrderDetailsAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { List orders = new List(); Dictionary payload = await GetNoncePayloadAsync(); - string query = $"/order?filter={{\"{(isClientOrderId ? "clOrdID" : "orderID")}\": \"{orderId}\"}}"; + string query = + $"/order?filter={{\"{(isClientOrderId ? "clOrdID" : "orderID")}\": \"{orderId}\"}}"; JToken token = await MakeJsonRequestAsync(query, BaseUrl, payload, "GET"); foreach (JToken order in token) { @@ -631,13 +765,17 @@ protected override async Task OnGetOrderDetailsAsync(string return orders[0]; } - protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnCancelOrderAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { Dictionary payload = await GetNoncePayloadAsync(); payload[isClientOrderId ? "clOrdID" : "orderID"] = orderId; JToken token = await MakeJsonRequestAsync("/order", BaseUrl, payload, "DELETE"); } - + public async Task CancelAllOrdersAsync(string marketSymbol = null) { Dictionary payload = await GetNoncePayloadAsync(); @@ -653,10 +791,17 @@ public async Task DeadmanAsync(int timeoutMS) { Dictionary payload = await GetNoncePayloadAsync(); payload["timeout"] = timeoutMS; - JToken token = await MakeJsonRequestAsync("/order/cancelAllAfter", BaseUrl, payload, "POST"); + JToken token = await MakeJsonRequestAsync( + "/order/cancelAllAfter", + BaseUrl, + payload, + "POST" + ); } - - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + + protected override async Task OnPlaceOrderAsync( + ExchangeOrderRequest order + ) { Dictionary payload = await GetNoncePayloadAsync(); AddOrderToPayload(order, payload); @@ -664,7 +809,10 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd return ParseOrder(token); } - private async Task PlaceOrdersAsync(string requestMethod, params ExchangeOrderRequest[] orders) + private async Task PlaceOrdersAsync( + string requestMethod, + params ExchangeOrderRequest[] orders + ) { List results = new List(); Dictionary payload = await GetNoncePayloadAsync(); @@ -676,7 +824,12 @@ private async Task PlaceOrdersAsync(string requestMethod, orderRequests.Add(subPayload); } payload["orders"] = orderRequests; - JToken token = await MakeJsonRequestAsync("/order/bulk", BaseUrl, payload, requestMethod); + JToken token = await MakeJsonRequestAsync( + "/order/bulk", + BaseUrl, + payload, + requestMethod + ); foreach (JToken orderResultToken in token) { results.Add(ParseOrder(orderResultToken)); @@ -684,30 +837,37 @@ private async Task PlaceOrdersAsync(string requestMethod, return results.ToArray(); } - protected override async Task OnPlaceOrdersAsync(params ExchangeOrderRequest[] orders) + protected override async Task OnPlaceOrdersAsync( + params ExchangeOrderRequest[] orders + ) { return await PlaceOrdersAsync("POST", orders); } - public async Task AmendOrdersAsync(params ExchangeOrderRequest[] orders) + public async Task AmendOrdersAsync( + params ExchangeOrderRequest[] orders + ) { return await PlaceOrdersAsync("PUT", orders); } - private void AddOrderToPayload(ExchangeOrderRequest order, Dictionary payload) + private void AddOrderToPayload( + ExchangeOrderRequest order, + Dictionary payload + ) { payload["symbol"] = order.MarketSymbol; payload["ordType"] = order.OrderType.ToStringInvariant(); payload["side"] = order.IsBuy ? "Buy" : "Sell"; payload["orderQty"] = order.Amount; - if(order.OrderId != null) + if (order.OrderId != null) payload["orderID"] = order.OrderId; - if(order.ClientOrderId != null) + if (order.ClientOrderId != null) payload["clOrdID"] = order.ClientOrderId; - if(order.OrderType!=OrderType.Market) + if (order.OrderType != OrderType.Market) payload["price"] = order.Price; if (order.IsPostOnly == true) @@ -726,7 +886,10 @@ private ExchangePosition ParsePosition(JToken token) LiquidationPrice = token["liquidationPrice"].ConvertInvariant(), Leverage = token["leverage"].ConvertInvariant(), LastPrice = token["lastPrice"].ConvertInvariant(), - TimeStamp = CryptoUtility.ParseTimestamp(token["currentTimestamp"], TimestampType.Iso8601UTC) + TimeStamp = CryptoUtility.ParseTimestamp( + token["currentTimestamp"], + TimestampType.Iso8601UTC + ) }; return result; } @@ -735,41 +898,41 @@ private ExchangeOrderResult ParseOrder(JToken token) { /* {[ - { - "orderID": "b7b8518a-c0d8-028d-bb6e-d843f8f723a3", - "clOrdID": "", - "clOrdLinkID": "", - "account": 93592, - "symbol": "XBTUSD", - "side": "Buy", - "simpleOrderQty": null, - "orderQty": 1, - "price": 5500, - "displayQty": null, - "stopPx": null, - "pegOffsetValue": null, - "pegPriceType": "", - "currency": "USD", - "settlCurrency": "XBt", - "ordType": "Limit", - "timeInForce": "GoodTillCancel", - "execInst": "ParticipateDoNotInitiate", - "contingencyType": "", - "exDestination": "XBME", - "ordStatus": "Canceled", - "triggered": "", - "workingIndicator": false, - "ordRejReason": "", - "simpleLeavesQty": 0, - "leavesQty": 0, - "simpleCumQty": 0, - "cumQty": 0, - "avgPx": null, - "multiLegReportingType": "SingleSecurity", - "text": "Canceled: Canceled via API.\nSubmission from testnet.bitmex.com", - "transactTime": "2018-07-08T09:20:39.428Z", - "timestamp": "2018-07-08T11:35:05.334Z" - } +{ +"orderID": "b7b8518a-c0d8-028d-bb6e-d843f8f723a3", +"clOrdID": "", +"clOrdLinkID": "", +"account": 93592, +"symbol": "XBTUSD", +"side": "Buy", +"simpleOrderQty": null, +"orderQty": 1, +"price": 5500, +"displayQty": null, +"stopPx": null, +"pegOffsetValue": null, +"pegPriceType": "", +"currency": "USD", +"settlCurrency": "XBt", +"ordType": "Limit", +"timeInForce": "GoodTillCancel", +"execInst": "ParticipateDoNotInitiate", +"contingencyType": "", +"exDestination": "XBME", +"ordStatus": "Canceled", +"triggered": "", +"workingIndicator": false, +"ordRejReason": "", +"simpleLeavesQty": 0, +"leavesQty": 0, +"simpleCumQty": 0, +"cumQty": 0, +"avgPx": null, +"multiLegReportingType": "SingleSecurity", +"text": "Canceled: Canceled via API.\nSubmission from testnet.bitmex.com", +"transactTime": "2018-07-08T09:20:39.428Z", +"timestamp": "2018-07-08T11:35:05.334Z" +} ]} */ ExchangeOrderResult result = new ExchangeOrderResult @@ -817,7 +980,6 @@ private ExchangeOrderResult ParseOrder(JToken token) return result; } - //private decimal GetInstrumentTickSize(ExchangeMarket market) //{ // if (market.MarketName == "XBTUSD") @@ -844,5 +1006,8 @@ private ExchangeOrderResult ParseOrder(JToken token) //} } - public partial class ExchangeName { public const string BitMEX = "BitMEX"; } + public partial class ExchangeName + { + public const string BitMEX = "BitMEX"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/Bitfinex/ExchangeBitfinexAPI.cs b/src/ExchangeSharp/API/Exchanges/Bitfinex/ExchangeBitfinexAPI.cs index ecf97b4ae..38eb3989d 100644 --- a/src/ExchangeSharp/API/Exchanges/Bitfinex/ExchangeBitfinexAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Bitfinex/ExchangeBitfinexAPI.cs @@ -14,8 +14,6 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp { - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Globalization; @@ -24,6 +22,8 @@ namespace ExchangeSharp using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; public sealed partial class ExchangeBitfinexAPI : ExchangeAPI { @@ -76,7 +76,9 @@ protected override async Task> OnGetMarketSymbolsAsync() return m.Select(x => NormalizeMarketSymbol(x.MarketSymbol)); } - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() + protected internal override async Task< + IEnumerable + > OnGetMarketSymbolsMetadataAsync() { var markets = new List(); JToken allPairs = await MakeJsonRequestAsync("/symbols_details", BaseUrlV1); @@ -123,13 +125,25 @@ protected internal override async Task> OnGetMarketS protected override async Task OnGetTickerAsync(string marketSymbol) { JToken ticker = await MakeJsonRequestAsync("/ticker/t" + marketSymbol); - return await this.ParseTickerAsync(ticker, NormalizeMarketSymbolV1(marketSymbol), 2, 0, 6, 7); + return await this.ParseTickerAsync( + ticker, + NormalizeMarketSymbolV1(marketSymbol), + 2, + 0, + 6, + 7 + ); } - protected override async Task>> OnGetTickersAsync() + protected override async Task< + IEnumerable> + > OnGetTickersAsync() { - List> tickers = new List>(); - IReadOnlyDictionary marketsBySymbol = (await GetMarketSymbolsMetadataAsync()).ToDictionary(market => market.MarketSymbol, market => market); + List> tickers = + new List>(); + IReadOnlyDictionary marketsBySymbol = ( + await GetMarketSymbolsMetadataAsync() + ).ToDictionary(market => market.MarketSymbol, market => market); if (marketsBySymbol != null && marketsBySymbol.Count != 0) { StringBuilder symbolString = new StringBuilder(); @@ -140,7 +154,9 @@ protected override async Task>> symbolString.Append(','); } symbolString.Length--; - JToken token = await MakeJsonRequestAsync("/tickers?symbols=" + symbolString); + JToken token = await MakeJsonRequestAsync( + "/tickers?symbols=" + symbolString + ); DateTime now = CryptoUtility.UtcNow; foreach (JArray array in token) { @@ -164,134 +180,210 @@ protected override async Task>> var marketSymbol = array[0].ToStringInvariant().Substring(1); var market = marketsBySymbol[marketSymbol.ToLowerInvariant()]; - tickers.Add(new KeyValuePair(marketSymbol, new ExchangeTicker - { - Exchange = Name, - MarketSymbol = marketSymbol, - ApiResponse = token, - 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 - } - })); + tickers.Add( + new KeyValuePair( + marketSymbol, + new ExchangeTicker + { + Exchange = Name, + MarketSymbol = marketSymbol, + ApiResponse = token, + 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) + protected override async Task OnGetTickersWebSocketAsync( + Action>> callback, + params string[] marketSymbols + ) { Dictionary channelIdToSymbol = new Dictionary(); - return await ConnectPublicWebSocketAsync(string.Empty, async (_socket, msg) => - { - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - if (token is JArray array) - { - if (array.Count > 10) + return await ConnectPublicWebSocketAsync( + string.Empty, + async (_socket, msg) => { - List> tickerList = new List>(); - if (channelIdToSymbol.TryGetValue(array[0].ConvertInvariant(), out string symbol)) + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + if (token is JArray array) { - ExchangeTicker ticker = await ParseTickerWebSocketAsync(symbol, array); - if (ticker != null) + if (array.Count > 10) { - callback(new KeyValuePair[] { new KeyValuePair(symbol, ticker) }); + 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 + } + ); + } } - } - 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) + 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 ConnectPublicWebSocketAsync("/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) + return await ConnectPublicWebSocketAsync( + "/2", + async (_socket, msg) => //use websocket V2 (beta, but millisecond timestamp) { - //[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)) + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + if (token is JArray array) { - if (token[1].ToStringInvariant() == "tu") + if (token[1].ToStringInvariant() == "hb") + { + // heartbeat + } + else if (token.Last.Last.HasValues == false) { - ExchangeTrade trade = ParseTradeWebSocket(token.Last); - if (trade != null) + //[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 + ) + ) { - await callback(new KeyValuePair(symbol, trade)); + 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) + else { - for (int i = 0; i < subarray.Count - 1; i++) + //parse snapshot here if needed + if ( + channelIdToSymbol.TryGetValue( + array[0].ConvertInvariant(), + out string symbol + ) + ) { - ExchangeTrade trade = ParseTradeWebSocket(subarray[i]); - if (trade != null) + if (array[1] is JArray subarray) { - trade.Flags |= ExchangeTradeFlags.IsFromSnapshot; - if (i == subarray.Count - 1) + for (int i = 0; i < subarray.Count - 1; i++) { - trade.Flags |= ExchangeTradeFlags.IsLastFromSnapshot; + ExchangeTrade trade = ParseTradeWebSocket(subarray[i]); + if (trade != null) + { + trade.Flags |= ExchangeTradeFlags.IsFromSnapshot; + if (i == subarray.Count - 1) + { + trade.Flags |= + ExchangeTradeFlags.IsLastFromSnapshot; + } + await callback( + new KeyValuePair( + symbol, + trade + ) + ); + } } - await callback(new KeyValuePair(symbol, trade)); } } } } + 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 + } + ); + } } - } - 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) @@ -300,40 +392,63 @@ private ExchangeTrade ParseTradeWebSocket(JToken token) return new ExchangeTrade { Id = token[0].ToStringInvariant(), - Timestamp = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(token[1].ConvertInvariant()), + 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) + 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()); + 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] }; + 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] }; + orders.Asks[book[0]] = new ExchangeOrderPrice + { + Amount = -book[2], + Price = book[0] + }; } } return orders; } - protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null) + 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; + string url = + "/trades/t" + marketSymbol + "/hist?sort=" + "-1" + "&limit=" + requestLimit; tradeChunk = await MakeJsonRequestAsync(url); if (tradeChunk != null || tradeChunk.Length > 0) @@ -341,16 +456,39 @@ protected override async Task> OnGetRecentTradesAsync //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() }); + 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) + 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 baseUrl = + "/trades/t" + + marketSymbol + + "/hist?sort=" + + (startDate == null ? "-1" : "1") + + "&limit=" + + maxCount; string url; List trades = new List(); decimal[][] tradeChunk; @@ -359,7 +497,10 @@ protected override async Task OnGetHistoricalTradesAsync(Func lookup = new Dictionary( + StringComparer.OrdinalIgnoreCase + ); + JArray obj = await MakeJsonRequestAsync( + "/balances", + BaseUrlV1, + await GetNoncePayloadAsync() + ); foreach (JToken token in obj) { if (token["type"].ToStringInvariant() == type) @@ -439,16 +626,25 @@ public async Task> OnGetAmountsAsync(string type) return lookup; } - protected override async Task> OnGetMarginAmountsAvailableToTradeAsync( - bool includeZeroBalances = false) + protected override async Task< + Dictionary + > OnGetMarginAmountsAvailableToTradeAsync(bool includeZeroBalances = false) { return await OnGetAmountsAsync("trading"); } - protected override async Task> OnGetAmountsAvailableToTradeAsync() + protected override async Task< + Dictionary + > OnGetAmountsAvailableToTradeAsync() { - Dictionary lookup = new Dictionary(StringComparer.OrdinalIgnoreCase); - JArray obj = await MakeJsonRequestAsync("/balances", BaseUrlV1, await GetNoncePayloadAsync()); + Dictionary lookup = new Dictionary( + StringComparer.OrdinalIgnoreCase + ); + JArray obj = await MakeJsonRequestAsync( + "/balances", + BaseUrlV1, + await GetNoncePayloadAsync() + ); foreach (JToken token in obj) { if (token["type"].ToStringInvariant() == "exchange") @@ -463,32 +659,45 @@ protected override async Task> OnGetAmountsAvailable return lookup; } - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + 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["amount"] = ( + await ClampOrderQuantity(marketSymbol, order.Amount) + ).ToStringInvariant(); payload["side"] = (order.IsBuy ? "buy" : "sell"); switch (order.OrderType) { case OrderType.Market: - if (order.Price == null) throw new ArgumentNullException(nameof(order.Price)); + if (order.Price == null) + throw new ArgumentNullException(nameof(order.Price)); payload["type"] = "market"; - payload["price"] = (await ClampOrderPrice(marketSymbol, order.Price.Value)).ToStringInvariant(); + payload["price"] = ( + await ClampOrderPrice(marketSymbol, order.Price.Value) + ).ToStringInvariant(); break; case OrderType.Limit: - if (order.Price == null) throw new ArgumentNullException(nameof(order.Price)); + if (order.Price == null) + throw new ArgumentNullException(nameof(order.Price)); payload["type"] = "limit"; - payload["price"] = (await ClampOrderPrice(marketSymbol, order.Price.Value)).ToStringInvariant(); + payload["price"] = ( + await ClampOrderPrice(marketSymbol, order.Price.Value) + ).ToStringInvariant(); break; case OrderType.Stop: - if (order.StopPrice < 0) throw new ArgumentOutOfRangeException(nameof(order.StopPrice)); + if (order.StopPrice < 0) + throw new ArgumentOutOfRangeException(nameof(order.StopPrice)); payload["type"] = "stop"; - payload["price"] = (await ClampOrderPrice(marketSymbol, order.StopPrice)).ToStringInvariant(); + payload["price"] = ( + await ClampOrderPrice(marketSymbol, order.StopPrice) + ).ToStringInvariant(); break; } @@ -496,15 +705,23 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd { payload["type"] = $"exchange {payload["type"]}"; } - if (order.IsPostOnly == true) payload["flags"] = "4096"; // The post-only limit order option ensures the limit order will be added to the order book and not match with a pre-existing order unless the pre-existing order is a hidden order. + if (order.IsPostOnly == true) + payload["flags"] = "4096"; // The post-only limit order option ensures the limit order will be added to the order book and not match with a pre-existing order unless the pre-existing order is a hidden order. 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, bool isClientOrderId = false) + protected override async Task OnGetOrderDetailsAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { - if (isClientOrderId) throw new NotSupportedException("Querying by client order ID is not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature"); + if (isClientOrderId) + throw new NotSupportedException( + "Querying by client order ID is not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature" + ); if (string.IsNullOrWhiteSpace(orderId)) { return null; @@ -516,18 +733,26 @@ protected override async Task OnGetOrderDetailsAsync(string return ParseOrder(result); } - protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) + protected override async Task> OnGetOpenOrderDetailsAsync( + string marketSymbol = null + ) { return await GetOrderDetailsInternalAsync("/orders", marketSymbol); } - protected override async Task> OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) + protected override async Task< + IEnumerable + > 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); + var symbols = (await GetMarketSymbolsAsync()).Where( + s => + s.IndexOf("usd", StringComparison.OrdinalIgnoreCase) < 0 + && s.IndexOf("btc", StringComparison.OrdinalIgnoreCase) >= 0 + ); return await GetOrderDetailsInternalV1(symbols, afterDate); } @@ -535,49 +760,73 @@ protected override async Task> OnGetCompletedOr return await GetOrderDetailsInternalV1(new string[] { marketSymbol }, afterDate); } - protected override Task OnGetCompletedOrderDetailsWebSocketAsync(Action callback) + protected override Task OnGetCompletedOrderDetailsWebSocketAsync( + Action callback + ) { - return ConnectPublicWebSocketAsync(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]) + return ConnectPublicWebSocketAsync( + 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) => { - callback.Invoke(ParseOrderWebSocket(orderToken)); + 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); } - } - 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, bool isClientOrderId = false) + protected override async Task OnCancelOrderAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { - if (isClientOrderId) throw new NotSupportedException("Cancelling by client order ID is not supported in ExchangeSharp. Please submit a PR if you are interested in this feature"); + if (isClientOrderId) + throw new NotSupportedException( + "Cancelling by client order ID is not supported in ExchangeSharp. Please submit a PR if you are interested in this feature" + ); 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) + protected override async Task OnGetDepositAddressAsync( + string currency, + bool forceRegenerate = false + ) { if (string.IsNullOrWhiteSpace(currency)) { @@ -601,7 +850,12 @@ protected override async Task OnGetDepositAddressAsync(s payload["wallet_name"] = "exchange"; payload["renew"] = forceRegenerate ? 1 : 0; - JToken result = await MakeJsonRequestAsync("/deposit/new", BaseUrlV1, payload, "POST"); + JToken result = await MakeJsonRequestAsync( + "/deposit/new", + BaseUrlV1, + payload, + "POST" + ); var details = new ExchangeDepositDetails { Currency = result["currency"].ToStringInvariant(), @@ -622,7 +876,9 @@ protected override async Task OnGetDepositAddressAsync(s /// Gets the deposit history for a symbol /// The symbol to check. Must be specified. /// Collection of ExchangeCoinTransfers - protected override async Task> OnGetDepositHistoryAsync(string currency) + protected override async Task> OnGetDepositHistoryAsync( + string currency + ) { if (string.IsNullOrWhiteSpace(currency)) { @@ -632,7 +888,12 @@ protected override async Task> OnGetDepositHist Dictionary payload = await GetNoncePayloadAsync(); payload["currency"] = currency; - JToken result = await MakeJsonRequestAsync("/history/movements", BaseUrlV1, payload, "POST"); + JToken result = await MakeJsonRequestAsync( + "/history/movements", + BaseUrlV1, + payload, + "POST" + ); var transactions = new List(); foreach (JToken token in result) { @@ -646,7 +907,10 @@ protected override async Task> OnGetDepositHist PaymentId = token["id"].ToStringInvariant(), BlockchainTxId = token["txid"].ToStringInvariant(), Currency = token["currency"].ToStringUpperInvariant(), - Notes = token["description"].ToStringInvariant() + ", method: " + token["method"].ToStringInvariant(), + Notes = + token["description"].ToStringInvariant() + + ", method: " + + token["method"].ToStringInvariant(), Amount = token["amount"].ConvertInvariant(), Address = token["address"].ToStringInvariant() }; @@ -681,7 +945,9 @@ protected override async Task> OnGetDepositHist /// Gets the deposit history for a symbol /// The symbol to check. Must be specified. /// Collection of ExchangeCoinTransfers - protected override async Task> OnGetWithdrawHistoryAsync(string currency) + protected override async Task> OnGetWithdrawHistoryAsync( + string currency + ) { if (string.IsNullOrWhiteSpace(currency)) { @@ -691,7 +957,12 @@ protected override async Task> OnGetWithdrawHis Dictionary payload = await GetNoncePayloadAsync(); payload["currency"] = currency; - JToken result = await MakeJsonRequestAsync("/history/movements", BaseUrlV1, payload, "POST"); + JToken result = await MakeJsonRequestAsync( + "/history/movements", + BaseUrlV1, + payload, + "POST" + ); var transactions = new List(); foreach (JToken token in result) { @@ -705,7 +976,10 @@ protected override async Task> OnGetWithdrawHis PaymentId = token["id"].ToStringInvariant(), BlockchainTxId = token["txid"].ToStringInvariant(), Currency = token["currency"].ToStringUpperInvariant(), - Notes = token["description"].ToStringInvariant() + ", method: " + token["method"].ToStringInvariant(), + Notes = + token["description"].ToStringInvariant() + + ", method: " + + token["method"].ToStringInvariant(), Amount = token["amount"].ConvertInvariant(), Address = token["address"].ToStringInvariant() }; @@ -741,7 +1015,9 @@ protected override async Task> OnGetWithdrawHis /// 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) + 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)) @@ -775,10 +1051,21 @@ protected override async Task OnWithdrawAsync(Exchan payload["account_name"] = withdrawalRequest.Description; } - JToken result = await MakeJsonRequestAsync("/withdraw", BaseUrlV1, payload, "POST"); + JToken result = await MakeJsonRequestAsync( + "/withdraw", + BaseUrlV1, + payload, + "POST" + ); var resp = new ExchangeWithdrawalResponse(); - if (!string.Equals(result[0]["status"].ToStringInvariant(), "success", StringComparison.OrdinalIgnoreCase)) + if ( + !string.Equals( + result[0]["status"].ToStringInvariant(), + "success", + StringComparison.OrdinalIgnoreCase + ) + ) { resp.Success = false; } @@ -790,7 +1077,10 @@ protected override async Task OnWithdrawAsync(Exchan return resp; } - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + protected override async Task ProcessRequestAsync( + IHttpWebRequest request, + Dictionary payload + ) { if (CanMakeAuthenticatedRequest(payload)) { @@ -803,7 +1093,10 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti payload.Remove("nonce"); string json = JsonConvert.SerializeObject(payload); string toSign = "/api" + request.RequestUri.PathAndQuery + nonce + json; - string hexSha384 = CryptoUtility.SHA384Sign(toSign, PrivateApiKey.ToUnsecureString()); + string hexSha384 = CryptoUtility.SHA384Sign( + toSign, + PrivateApiKey.ToUnsecureString() + ); request.AddHeader("bfx-nonce", nonce); request.AddHeader("bfx-apikey", PublicApiKey.ToUnsecureString()); request.AddHeader("bfx-signature", hexSha384); @@ -815,7 +1108,10 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti 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()); + 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()); @@ -823,9 +1119,13 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti } } - protected override Task> OnGetCurrenciesAsync() + protected override Task< + IReadOnlyDictionary + > OnGetCurrenciesAsync() { - throw new NotSupportedException("Bitfinex does not provide data about its currencies via the API"); + throw new NotSupportedException( + "Bitfinex does not provide data about its currencies via the API" + ); } private string NormalizeMarketSymbolV1(string marketSymbol) @@ -833,21 +1133,34 @@ private string NormalizeMarketSymbolV1(string marketSymbol) return (marketSymbol ?? string.Empty).Replace("-", string.Empty).ToLowerInvariant(); } - private async Task> GetOrderDetailsInternalV2(string url, string marketSymbol = null) + 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["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); + 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) + if ( + string.IsNullOrWhiteSpace(marketSymbol) + || token[1].ToStringInvariant() == "t" + marketSymbol + ) { - string lookup = token[1].ToStringInvariant().Substring(1).ToLowerInvariant(); + string lookup = token[1] + .ToStringInvariant() + .Substring(1) + .ToLowerInvariant(); if (!trades.TryGetValue(lookup, out List tradeList)) { tradeList = trades[lookup] = new List(); @@ -859,11 +1172,18 @@ private async Task> GetOrderDetailsInternalV2(s return ParseOrderV2(trades); } - private async Task> GetOrderDetailsInternalAsync(string url, string marketSymbol = null) + private async Task> GetOrderDetailsInternalAsync( + string url, + string marketSymbol = null + ) { List orders = new List(); marketSymbol = NormalizeMarketSymbolV1(marketSymbol); - JToken result = await MakeJsonRequestAsync(url, BaseUrlV1, await GetNoncePayloadAsync()); + JToken result = await MakeJsonRequestAsync( + url, + BaseUrlV1, + await GetNoncePayloadAsync() + ); if (result is JArray array) { foreach (JToken token in array) @@ -877,9 +1197,15 @@ private async Task> GetOrderDetailsInternalAsyn return orders; } - private async Task> GetOrderDetailsInternalV1(IEnumerable marketSymbols, DateTime? afterDate) + private async Task> GetOrderDetailsInternalV1( + IEnumerable marketSymbols, + DateTime? afterDate + ) { - Dictionary orders = new Dictionary(StringComparer.OrdinalIgnoreCase); + Dictionary orders = new Dictionary< + string, + ExchangeOrderResult + >(StringComparer.OrdinalIgnoreCase); foreach (string marketSymbol in marketSymbols) { string normalizedSymbol = NormalizeMarketSymbolV1(marketSymbol); @@ -888,8 +1214,12 @@ private async Task> GetOrderDetailsInternalV1(I payload["limit_trades"] = 250; if (afterDate != null) { - payload["timestamp"] = afterDate.Value.UnixTimestampFromDateTimeSeconds().ToStringInvariant(); - payload["until"] = CryptoUtility.UtcNow.UnixTimestampFromDateTimeSeconds().ToStringInvariant(); + payload["timestamp"] = afterDate.Value + .UnixTimestampFromDateTimeSeconds() + .ToStringInvariant(); + payload["until"] = CryptoUtility.UtcNow + .UnixTimestampFromDateTimeSeconds() + .ToStringInvariant(); } JToken token = await MakeJsonRequestAsync("/mytrades", BaseUrlV1, payload); foreach (JToken trade in token) @@ -921,11 +1251,23 @@ private ExchangeOrderResult ParseOrder(JToken order) Amount = amount, AmountFilled = amountFilled, Price = price, - AveragePrice = order["avg_execution_price"].ConvertInvariant(order["price"].ConvertInvariant()), + AveragePrice = order["avg_execution_price"].ConvertInvariant( + order["price"].ConvertInvariant() + ), Message = string.Empty, OrderId = order["id"].ToStringInvariant(), - Result = (amountFilled == amount ? ExchangeAPIOrderResult.Filled : (amountFilled == 0 ? ExchangeAPIOrderResult.Open : ExchangeAPIOrderResult.FilledPartially)), - OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(order["timestamp"].ConvertInvariant()), + Result = ( + amountFilled == amount + ? ExchangeAPIOrderResult.Filled + : ( + amountFilled == 0 + ? ExchangeAPIOrderResult.Open + : ExchangeAPIOrderResult.FilledPartially + ) + ), + OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds( + order["timestamp"].ConvertInvariant() + ), MarketSymbol = order["symbol"].ToStringInvariant(), IsBuy = order["side"].ToStringInvariant() == "buy" }; @@ -934,26 +1276,26 @@ private ExchangeOrderResult ParseOrder(JToken order) private ExchangeOrderResult ParseOrderWebSocket(JToken order) { /* - [ 0, "os", [ [ - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "" - ] ] ]; - */ + [ 0, "os", [ [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ] ] ]; + */ decimal amount = order[2].ConvertInvariant(); /* 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 { @@ -964,46 +1306,63 @@ private ExchangeOrderResult ParseOrderWebSocket(JToken order) IsBuy = (amount > 0m), OrderDate = order[8].ToDateTimeInvariant(), OrderId = order[0].ToStringInvariant(), - Result = orderStatusString == "ACTIVE" ? ExchangeAPIOrderResult.Open - : orderStatusString == "EXECUTED" ? ExchangeAPIOrderResult.Filled - : orderStatusString == "PARTIALLY" ? ExchangeAPIOrderResult.FilledPartially - : orderStatusString == "INSUFFICIENT" ? ExchangeAPIOrderResult.Rejected - : orderStatusString == "CANCELED" ? ExchangeAPIOrderResult.Canceled - : ExchangeAPIOrderResult.Unknown, + Result = + orderStatusString == "ACTIVE" + ? ExchangeAPIOrderResult.Open + : orderStatusString == "EXECUTED" + ? ExchangeAPIOrderResult.Filled + : orderStatusString == "PARTIALLY" + ? ExchangeAPIOrderResult.FilledPartially + : orderStatusString == "INSUFFICIENT" + ? ExchangeAPIOrderResult.Rejected + : orderStatusString == "CANCELED" + ? ExchangeAPIOrderResult.Canceled + : ExchangeAPIOrderResult.Unknown, MarketSymbol = order[1].ToStringInvariant(), }; } - private IEnumerable ParseOrderV2(Dictionary> trades) + private IEnumerable ParseOrderV2( + Dictionary> trades + ) { /* - [ - ID integer Trade database id - PAIR string Pair (BTCUSD, …) - MTS_CREATE integer Execution timestamp - ORDER_ID integer Order id - EXEC_AMOUNT float Positive means buy, negative means sell - EXEC_PRICE float Execution price - ORDER_TYPE string Order type - ORDER_PRICE float Order price - MAKER int 1 if true, 0 if false - FEE float Fee - FEE_CURRENCY string Fee currency - ], - */ + [ + ID integer Trade database id + PAIR string Pair (BTCUSD, …) + MTS_CREATE integer Execution timestamp + ORDER_ID integer Order id + EXEC_AMOUNT float Positive means buy, negative means sell + EXEC_PRICE float Execution price + ORDER_TYPE string Order type + ORDER_PRICE float Order price + MAKER int 1 if true, 0 if false + FEE float Fee + FEE_CURRENCY string Fee currency + ], + */ foreach (var kv in trades) { - ExchangeOrderResult order = new ExchangeOrderResult { Result = ExchangeAPIOrderResult.Filled }; + ExchangeOrderResult order = new ExchangeOrderResult + { + Result = ExchangeAPIOrderResult.Filled + }; foreach (JToken trade in kv.Value) { - ExchangeOrderResult append = new ExchangeOrderResult { MarketSymbol = kv.Key, OrderId = trade[3].ToStringInvariant() }; + ExchangeOrderResult append = new ExchangeOrderResult + { + MarketSymbol = kv.Key, + OrderId = trade[3].ToStringInvariant() + }; append.Amount = Math.Abs(trade[4].ConvertInvariant()); 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.OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds( + trade[2].ConvertInvariant() + ); append.OrderId = trade[3].ToStringInvariant(); order.AppendOrderWithOrder(append); } @@ -1014,25 +1373,27 @@ FEE_CURRENCY string Fee currency private ExchangeOrderResult ParseTrade(JToken trade, string symbol) { /* - [{ - "price":"246.94", - "amount":"1.0", - "timestamp":"1444141857.0", - "exchange":"", - "type":"Buy", - "fee_currency":"USD", - "fee_amount":"-0.49388", - "tid":11970839, - "order_id":446913929 - }] - */ + [{ + "price":"246.94", + "amount":"1.0", + "timestamp":"1444141857.0", + "exchange":"", + "type":"Buy", + "fee_currency":"USD", + "fee_amount":"-0.49388", + "tid":11970839, + "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()), + OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds( + trade["timestamp"].ConvertInvariant() + ), OrderId = trade["order_id"].ToStringInvariant(), Result = ExchangeAPIOrderResult.Filled, MarketSymbol = symbol @@ -1048,7 +1409,11 @@ private async Task ParseTickerWebSocketAsync(string symbol, JTok /// A dictionary of symbol-fee pairs private async Task> GetWithdrawalFeesAsync() { - JToken obj = await MakeJsonRequestAsync("/account_fees", BaseUrlV1, await GetNoncePayloadAsync()); + JToken obj = await MakeJsonRequestAsync( + "/account_fees", + BaseUrlV1, + await GetNoncePayloadAsync() + ); var fees = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var jToken in obj["withdraw"]) { @@ -1060,5 +1425,8 @@ private async Task> GetWithdrawalFeesAsync() } } - public partial class ExchangeName { public const string Bitfinex = "Bitfinex"; } + public partial class ExchangeName + { + public const string Bitfinex = "Bitfinex"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/Bitflyer/ExchangeBitflyerApi.cs b/src/ExchangeSharp/API/Exchanges/Bitflyer/ExchangeBitflyerApi.cs index 6260ec1d9..eb4a513d2 100644 --- a/src/ExchangeSharp/API/Exchanges/Bitflyer/ExchangeBitflyerApi.cs +++ b/src/ExchangeSharp/API/Exchanges/Bitflyer/ExchangeBitflyerApi.cs @@ -1,18 +1,19 @@ -using Newtonsoft.Json.Linq; -using SocketIOClient; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using SocketIOClient; namespace ExchangeSharp { public sealed partial class ExchangeBitflyerApi : ExchangeAPI { public override string BaseUrl { get; set; } = "https://api.bitflyer.com"; - public override string BaseUrlWebSocket { get; set; } = "https://io.lightstream.bitflyer.com"; + public override string BaseUrlWebSocket { get; set; } = + "https://io.lightstream.bitflyer.com"; public ExchangeBitflyerApi() { @@ -28,69 +29,75 @@ protected override async Task> OnGetMarketSymbolsAsync() { /* [ - { - "product_code": "BTC_JPY", - "market_type": "Spot" - }, - { - "product_code": "XRP_JPY", - "market_type": "Spot" - }, - { - "product_code": "ETH_JPY", - "market_type": "Spot" - }, - { - "product_code": "XLM_JPY", - "market_type": "Spot" - }, - { - "product_code": "MONA_JPY", - "market_type": "Spot" - }, - { - "product_code": "ETH_BTC", - "market_type": "Spot" - }, - { - "product_code": "BCH_BTC", - "market_type": "Spot" - }, - { - "product_code": "FX_BTC_JPY", - "market_type": "FX" - }, - { - "product_code": "BTCJPY12MAR2021", - "alias": "BTCJPY_MAT1WK", - "market_type": "Futures" - }, - { - "product_code": "BTCJPY19MAR2021", - "alias": "BTCJPY_MAT2WK", - "market_type": "Futures" - }, - { - "product_code": "BTCJPY26MAR2021", - "alias": "BTCJPY_MAT3M", - "market_type": "Futures" - } - ] + { + "product_code": "BTC_JPY", + "market_type": "Spot" + }, + { + "product_code": "XRP_JPY", + "market_type": "Spot" + }, + { + "product_code": "ETH_JPY", + "market_type": "Spot" + }, + { + "product_code": "XLM_JPY", + "market_type": "Spot" + }, + { + "product_code": "MONA_JPY", + "market_type": "Spot" + }, + { + "product_code": "ETH_BTC", + "market_type": "Spot" + }, + { + "product_code": "BCH_BTC", + "market_type": "Spot" + }, + { + "product_code": "FX_BTC_JPY", + "market_type": "FX" + }, + { + "product_code": "BTCJPY12MAR2021", + "alias": "BTCJPY_MAT1WK", + "market_type": "Futures" + }, + { + "product_code": "BTCJPY19MAR2021", + "alias": "BTCJPY_MAT2WK", + "market_type": "Futures" + }, + { + "product_code": "BTCJPY26MAR2021", + "alias": "BTCJPY_MAT3M", + "market_type": "Futures" + } + ] */ JToken instruments = await MakeJsonRequestAsync("v1/getmarkets"); var markets = new List(); foreach (JToken instrument in instruments) { - markets.Add(new ExchangeMarket - { - MarketSymbol = instrument["product_code"].ToStringUpperInvariant(), - AltMarketSymbol = instrument["alias"].ToStringInvariant(), - AltMarketSymbol2 = instrument["market_type"].ToStringInvariant(), - }); + markets.Add( + new ExchangeMarket + { + MarketSymbol = instrument["product_code"].ToStringUpperInvariant(), + AltMarketSymbol = instrument["alias"].ToStringInvariant(), + AltMarketSymbol2 = instrument["market_type"].ToStringInvariant(), + } + ); } return markets.Select(m => m.MarketSymbol); } - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + + protected override async Task OnGetTradesWebSocketAsync( + Func, Task> callback, + params string[] marketSymbols + ) { if (marketSymbols == null || marketSymbols.Length == 0) { @@ -100,9 +107,11 @@ protected override async Task OnGetTradesWebSocketAsync(Func - { /* [[ { + // BTC/JPY (Spot): lightning_executions_BTC_JPY + client.socketIO.On( + $"lightning_executions_{marketSymbol}", + response => + { /* [[ { "id": 39361, "side": "SELL", "price": 35100, @@ -111,18 +120,27 @@ protected override async Task OnGetTradesWebSocketAsync(Func(marketSymbol, trade)).Wait(); - } - }); + var token = JToken.Parse(response.ToStringInvariant()); + foreach (var tradeToken in token[0]) + { + var trade = tradeToken.ParseTradeBitflyer( + "size", + "price", + "side", + "exec_date", + TimestampType.Iso8601UTC, + "id" + ); + + // If it is executed during an Itayose, it will be an empty string. + if (string.IsNullOrWhiteSpace(tradeToken["side"].ToStringInvariant())) + trade.Flags |= ExchangeTradeFlags.HasNoSide; + + callback(new KeyValuePair(marketSymbol, trade)) + .Wait(); + } + } + ); } client.socketIO.OnConnected += async (sender, e) => @@ -130,7 +148,10 @@ protected override async Task OnGetTradesWebSocketAsync(Func OnGetTradesWebSocketAsync(Func throw new NotSupportedException(); - set => socketIO.Options.Reconnection = value > TimeSpan.Zero; } + { + get => throw new NotSupportedException(); + set => socketIO.Options.Reconnection = value > TimeSpan.Zero; + } public TimeSpan KeepAlive - { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } public event WebSocketConnectionDelegate Connected; public event WebSocketConnectionDelegate Disconnected; @@ -164,5 +191,8 @@ public TimeSpan KeepAlive public void Dispose() => socketIO.Dispose(); } - public partial class ExchangeName { public const string Bitflyer = "Bitflyer"; } + public partial class ExchangeName + { + public const string Bitflyer = "Bitflyer"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/Bitflyer/Models/BitflyerTrade.cs b/src/ExchangeSharp/API/Exchanges/Bitflyer/Models/BitflyerTrade.cs index a3da41425..93ffd6bd1 100644 --- a/src/ExchangeSharp/API/Exchanges/Bitflyer/Models/BitflyerTrade.cs +++ b/src/ExchangeSharp/API/Exchanges/Bitflyer/Models/BitflyerTrade.cs @@ -11,7 +11,12 @@ public class BitflyerTrade : ExchangeTrade public override string ToString() { - return string.Format("{0},{1}, {2}", base.ToString(), BuyChildOrderAcceptanceId, SellChildOrderAcceptanceId); + return string.Format( + "{0},{1}, {2}", + base.ToString(), + BuyChildOrderAcceptanceId, + SellChildOrderAcceptanceId + ); } } } diff --git a/src/ExchangeSharp/API/Exchanges/Bithumb/ExchangeBithumbAPI.cs b/src/ExchangeSharp/API/Exchanges/Bithumb/ExchangeBithumbAPI.cs index a41374e12..45633b4bd 100644 --- a/src/ExchangeSharp/API/Exchanges/Bithumb/ExchangeBithumbAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Bithumb/ExchangeBithumbAPI.cs @@ -19,214 +19,294 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp { - public sealed partial class ExchangeBithumbAPI : ExchangeAPI - { - public override string BaseUrl { get; set; } = "https://api.bithumb.com"; + public sealed partial class ExchangeBithumbAPI : ExchangeAPI + { + public override string BaseUrl { get; set; } = "https://api.bithumb.com"; public override string BaseUrlWebSocket { get; set; } = "wss://pubwss.bithumb.com/pub/ws"; - private ExchangeBithumbAPI() - { - MarketSymbolIsUppercase = true; - } - - public override string NormalizeMarketSymbol(string marketSymbol) - { - marketSymbol = base.NormalizeMarketSymbol(marketSymbol); - int pos = marketSymbol.IndexOf(MarketSymbolSeparator); - if (pos >= 0) - { - marketSymbol = marketSymbol.Substring(0, pos); - } - return marketSymbol; - } - - public override Task ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol) - { - return Task.FromResult(marketSymbol + GlobalMarketSymbolSeparator + "KRW"); //e.g. 1 btc worth 9.7m KRW - } - - public override Task GlobalMarketSymbolToExchangeMarketSymbolAsync(string marketSymbol) - { - var values = marketSymbol.Split(GlobalMarketSymbolSeparator); //for Bitthumb, e.g. "BTC-KRW", 1 btc worth about 9.7m won. Market symbol is BTC. - - return Task.FromResult(values[0]); - } - - private string StatusToError(string status) - { - switch (status) - { - case "5100": return "Bad Request"; - case "5200": return "Not Member"; - case "5300": return "Invalid Apikey"; - case "5302": return "Method Not Allowed"; - case "5400": return "Database Fail"; - case "5500": return "Invalid Parameter"; - case "5600": return "Custom Notice"; - case "5900": return "Unknown Error"; - default: return status; - } - } - - protected override JToken CheckJsonResponse(JToken result) - { - if (result != null && !(result is JArray) && result["status"] != null && result["status"].ToStringInvariant() != "0000") - { - throw new APIException(result["status"].ToStringInvariant() + ": " + result["message"].ToStringInvariant()); - } - return result["data"]; - } - - private async Task> MakeRequestBithumbAsync(string marketSymbol, string subUrl) - { - marketSymbol = NormalizeMarketSymbol(marketSymbol); - JToken obj = await MakeJsonRequestAsync(subUrl.Replace("$SYMBOL$", marketSymbol ?? string.Empty)); - return new Tuple(obj, marketSymbol); - } - - private async Task ParseTickerAsync(string marketSymbol, JToken data) - { - /* - { - "opening_price": "12625000", - "closing_price": "12636000", - "min_price": "12550000", - "max_price": "12700000", - "units_traded": "866.21", - "acc_trade_value": "10930847017.53", - "prev_closing_price": "12625000", - "units_traded_24H": "16767.54", - "acc_trade_value_24H": "211682650507.99", - "fluctate_24H": "3,000", - "fluctate_rate_24H": "0.02" - } - */ - ExchangeTicker ticker = await this.ParseTickerAsync(data, marketSymbol, "max_price", "min_price", "min_price", "min_price", "units_traded_24H"); - ticker.Volume.Timestamp = data.Parent.Parent["date"].ConvertInvariant().UnixTimeStampToDateTimeMilliseconds(); - return ticker; - } - - protected override (string baseCurrency, string quoteCurrency) OnSplitMarketSymbolToCurrencies(string marketSymbol) - { - return (marketSymbol, "KRW"); - } - - protected override async Task> OnGetMarketSymbolsAsync() - { - List marketSymbols = new List(); - string marketSymbol = "all_BTC"; - var data = await MakeRequestBithumbAsync(marketSymbol, "/public/ticker/$SYMBOL$"); - foreach (JProperty token in data.Item1) - { - if (token.Name != "date") - { - marketSymbols.Add($"{token.Name}_KRW"); - if (token.Name != "BTC") marketSymbols.Add($"{token.Name}_BTC"); - } + { + MarketSymbolIsUppercase = true; + } + + public override string NormalizeMarketSymbol(string marketSymbol) + { + marketSymbol = base.NormalizeMarketSymbol(marketSymbol); + int pos = marketSymbol.IndexOf(MarketSymbolSeparator); + if (pos >= 0) + { + marketSymbol = marketSymbol.Substring(0, pos); + } + return marketSymbol; + } + + public override Task ExchangeMarketSymbolToGlobalMarketSymbolAsync( + string marketSymbol + ) + { + return Task.FromResult(marketSymbol + GlobalMarketSymbolSeparator + "KRW"); //e.g. 1 btc worth 9.7m KRW + } + + public override Task GlobalMarketSymbolToExchangeMarketSymbolAsync( + string marketSymbol + ) + { + var values = marketSymbol.Split(GlobalMarketSymbolSeparator); //for Bitthumb, e.g. "BTC-KRW", 1 btc worth about 9.7m won. Market symbol is BTC. + + return Task.FromResult(values[0]); + } + + private string StatusToError(string status) + { + switch (status) + { + case "5100": + return "Bad Request"; + case "5200": + return "Not Member"; + case "5300": + return "Invalid Apikey"; + case "5302": + return "Method Not Allowed"; + case "5400": + return "Database Fail"; + case "5500": + return "Invalid Parameter"; + case "5600": + return "Custom Notice"; + case "5900": + return "Unknown Error"; + default: + return status; + } + } + + protected override JToken CheckJsonResponse(JToken result) + { + if ( + result != null + && !(result is JArray) + && result["status"] != null + && result["status"].ToStringInvariant() != "0000" + ) + { + throw new APIException( + result["status"].ToStringInvariant() + + ": " + + result["message"].ToStringInvariant() + ); } - return marketSymbols; - } - - protected override async Task OnGetTickerAsync(string marketSymbol) - { - var data = await MakeRequestBithumbAsync(marketSymbol, "/public/ticker/$SYMBOL$"); - return await ParseTickerAsync(data.Item2, data.Item1); - } - - protected override async Task>> OnGetTickersAsync() - { - string symbol = "all"; - List> tickers = new List>(); - var data = await MakeRequestBithumbAsync(symbol, "/public/ticker/$SYMBOL$"); - DateTime date = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(data.Item1["date"].ConvertInvariant()); - foreach (JProperty token in data.Item1) - { - if (token.Name != "date") - { - ExchangeTicker ticker = await ParseTickerAsync(token.Name, token.Value); - ticker.Volume.Timestamp = date; - tickers.Add(new KeyValuePair(token.Name, ticker)); - } - } - return tickers; - } - - protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) - { - var data = await MakeRequestBithumbAsync(marketSymbol, "/public/orderbook/$SYMBOL$"); - return data.Item1.ParseOrderBookFromJTokenDictionaries(amount: "quantity", sequence: "timestamp"); - } - - protected override async Task>> OnGetOrderBooksAsync(int maxCount = 100) - { - string symbol = "all"; - List> books = new List>(); - var data = await MakeRequestBithumbAsync(symbol, "/public/orderbook/$SYMBOL$"); - foreach (JProperty book in data.Item1) - { - if (book.Name != "timestamp" && book.Name != "payment_currency") - { - ExchangeOrderBook orderBook = ExchangeAPIExtensions.ParseOrderBookFromJTokenArrays(book.Value); - books.Add(new KeyValuePair(book.Name, orderBook)); - } - } - return books; - } - - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + return result["data"]; + } + + private async Task> MakeRequestBithumbAsync( + string marketSymbol, + string subUrl + ) + { + marketSymbol = NormalizeMarketSymbol(marketSymbol); + JToken obj = await MakeJsonRequestAsync( + subUrl.Replace("$SYMBOL$", marketSymbol ?? string.Empty) + ); + return new Tuple(obj, marketSymbol); + } + + private async Task ParseTickerAsync(string marketSymbol, JToken data) { /* + { + "opening_price": "12625000", + "closing_price": "12636000", + "min_price": "12550000", + "max_price": "12700000", + "units_traded": "866.21", + "acc_trade_value": "10930847017.53", + "prev_closing_price": "12625000", + "units_traded_24H": "16767.54", + "acc_trade_value_24H": "211682650507.99", + "fluctate_24H": "3,000", + "fluctate_rate_24H": "0.02" + } + */ + ExchangeTicker ticker = await this.ParseTickerAsync( + data, + marketSymbol, + "max_price", + "min_price", + "min_price", + "min_price", + "units_traded_24H" + ); + ticker.Volume.Timestamp = data.Parent.Parent["date"] + .ConvertInvariant() + .UnixTimeStampToDateTimeMilliseconds(); + return ticker; + } + + protected override ( + string baseCurrency, + string quoteCurrency + ) OnSplitMarketSymbolToCurrencies(string marketSymbol) + { + return (marketSymbol, "KRW"); + } + + protected override async Task> OnGetMarketSymbolsAsync() + { + List marketSymbols = new List(); + string marketSymbol = "all_BTC"; + var data = await MakeRequestBithumbAsync(marketSymbol, "/public/ticker/$SYMBOL$"); + foreach (JProperty token in data.Item1) + { + if (token.Name != "date") { - "type" : "transaction", - "content" : { - "list" : [ - { - "symbol" : "BTC_KRW", // currency code - "buySellGb" : "1", // Execution type (1: sell execution, 2: buy execution) - "contPrice" : "10579000", // Execution price - "contQty" : "0.01", // contract quantity - "contAmt" : "105790.00", // Execution amount - "contDtm" : "2020-01-29 12:24:18.830039", // Execution time - "updn" : "dn" // Compare with the previous price: up-up, dn-down + marketSymbols.Add($"{token.Name}_KRW"); + if (token.Name != "BTC") + marketSymbols.Add($"{token.Name}_BTC"); + } + } + return marketSymbols; + } + + protected override async Task OnGetTickerAsync(string marketSymbol) + { + var data = await MakeRequestBithumbAsync(marketSymbol, "/public/ticker/$SYMBOL$"); + return await ParseTickerAsync(data.Item2, data.Item1); + } + + protected override async Task< + IEnumerable> + > OnGetTickersAsync() + { + string symbol = "all"; + List> tickers = + new List>(); + var data = await MakeRequestBithumbAsync(symbol, "/public/ticker/$SYMBOL$"); + DateTime date = CryptoUtility.UnixTimeStampToDateTimeMilliseconds( + data.Item1["date"].ConvertInvariant() + ); + foreach (JProperty token in data.Item1) + { + if (token.Name != "date") + { + ExchangeTicker ticker = await ParseTickerAsync(token.Name, token.Value); + ticker.Volume.Timestamp = date; + tickers.Add(new KeyValuePair(token.Name, ticker)); + } + } + return tickers; + } + + protected override async Task OnGetOrderBookAsync( + string marketSymbol, + int maxCount = 100 + ) + { + var data = await MakeRequestBithumbAsync(marketSymbol, "/public/orderbook/$SYMBOL$"); + return data.Item1.ParseOrderBookFromJTokenDictionaries( + amount: "quantity", + sequence: "timestamp" + ); + } + + protected override async Task< + IEnumerable> + > OnGetOrderBooksAsync(int maxCount = 100) + { + string symbol = "all"; + List> books = + new List>(); + var data = await MakeRequestBithumbAsync(symbol, "/public/orderbook/$SYMBOL$"); + foreach (JProperty book in data.Item1) + { + if (book.Name != "timestamp" && book.Name != "payment_currency") + { + ExchangeOrderBook orderBook = + ExchangeAPIExtensions.ParseOrderBookFromJTokenArrays(book.Value); + books.Add(new KeyValuePair(book.Name, orderBook)); + } + } + return books; + } + + protected override async Task OnGetTradesWebSocketAsync( + Func, Task> callback, + params string[] marketSymbols + ) + { + /* + { + "type" : "transaction", + "content" : { + "list" : [ + { + "symbol" : "BTC_KRW", // currency code + "buySellGb" : "1", // Execution type (1: sell execution, 2: buy execution) + "contPrice" : "10579000", // Execution price + "contQty" : "0.01", // contract quantity + "contAmt" : "105790.00", // Execution amount + "contDtm" : "2020-01-29 12:24:18.830039", // Execution time + "updn" : "dn" // Compare with the previous price: up-up, dn-down + } + ] } - ] } - } */ if (marketSymbols == null || marketSymbols.Length == 0) { marketSymbols = (await GetMarketSymbolsAsync(true)).ToArray(); } - return await ConnectPublicWebSocketAsync(null, messageCallback: async (_socket, msg) => - { - JToken parsedMsg = JToken.Parse(msg.ToStringFromUTF8()); - if (parsedMsg["status"].ToStringInvariant().Equals("0000")) - return; // either "Connected Successfully" or "Filter Registered Successfully" - else if (parsedMsg["status"].ToStringInvariant().Equals("5100")) - { - Logger.Error("Error in exchange {0} OnGetTradesWebSocketAsync(): {1}", Name, parsedMsg["resmsg"].ToStringInvariant()); - return; - } - else if (parsedMsg["type"].ToStringInvariant().Equals("transaction")) - { - foreach (var data in parsedMsg["content"]["list"]) + return await ConnectPublicWebSocketAsync( + null, + messageCallback: async (_socket, msg) => { - var exchangeTrade = data.ParseTrade("contQty", "contPrice", "buySellGb", "contDtm", TimestampType.Iso8601Korea, null, typeKeyIsBuyValue: "2"); + JToken parsedMsg = JToken.Parse(msg.ToStringFromUTF8()); + if (parsedMsg["status"].ToStringInvariant().Equals("0000")) + return; // either "Connected Successfully" or "Filter Registered Successfully" + else if (parsedMsg["status"].ToStringInvariant().Equals("5100")) + { + Logger.Error( + "Error in exchange {0} OnGetTradesWebSocketAsync(): {1}", + Name, + parsedMsg["resmsg"].ToStringInvariant() + ); + return; + } + else if (parsedMsg["type"].ToStringInvariant().Equals("transaction")) + { + foreach (var data in parsedMsg["content"]["list"]) + { + var exchangeTrade = data.ParseTrade( + "contQty", + "contPrice", + "buySellGb", + "contDtm", + TimestampType.Iso8601Korea, + null, + typeKeyIsBuyValue: "2" + ); - await callback(new KeyValuePair(data["symbol"].ToStringInvariant(), exchangeTrade)); + await callback( + new KeyValuePair( + data["symbol"].ToStringInvariant(), + exchangeTrade + ) + ); + } + } + }, + connectCallback: async (_socket) => + { + await _socket.SendMessageAsync( + new { type = "transaction", symbols = marketSymbols, } + ); } - } - }, connectCallback: async (_socket) => - { - await _socket.SendMessageAsync(new - { - type = "transaction", - symbols = marketSymbols, - }); - }); + ); } } - public partial class ExchangeName { public const string Bithumb = "Bithumb"; } + public partial class ExchangeName + { + public const string Bithumb = "Bithumb"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/Bitstamp/ExchangeBitstampAPI.cs b/src/ExchangeSharp/API/Exchanges/Bitstamp/ExchangeBitstampAPI.cs index 596b8f221..075b75073 100644 --- a/src/ExchangeSharp/API/Exchanges/Bitstamp/ExchangeBitstampAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Bitstamp/ExchangeBitstampAPI.cs @@ -21,82 +21,90 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp { - public sealed partial class ExchangeBitstampAPI : ExchangeAPI - { - public override string BaseUrl { get; set; } = "https://www.bitstamp.net/api/v2"; + public sealed partial class ExchangeBitstampAPI : ExchangeAPI + { + public override string BaseUrl { get; set; } = "https://www.bitstamp.net/api/v2"; public override string BaseUrlWebSocket { get; set; } = "wss://ws.bitstamp.net"; /// /// Bitstamp private API requires a customer id. Internally this is secured in the PassPhrase property. /// public string CustomerId - { - get { return Passphrase.ToUnsecureString(); } - set { Passphrase = value.ToSecureString(); } - } + { + get { return Passphrase.ToUnsecureString(); } + set { Passphrase = value.ToSecureString(); } + } /// /// In order to use private functions of the API, you must set CustomerId by calling constructor with parameter, /// or setting it later in the ExchangeBitstampAPI object. /// private ExchangeBitstampAPI() - { - RequestContentType = "application/x-www-form-urlencoded"; - NonceStyle = NonceStyle.UnixMilliseconds; - MarketSymbolIsUppercase = false; - MarketSymbolSeparator = string.Empty; - } - - /// - /// In order to use private functions of the API, you must set CustomerId by calling this constructor with parameter, - /// or setting it later in the ExchangeBitstampAPI object. - /// - /// Customer Id can be found by the link "https://www.bitstamp.net/account/balance/" - public ExchangeBitstampAPI(string customerId) : this() - { - CustomerId = customerId; - } - - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) - { - if (CanMakeAuthenticatedRequest(payload)) - { - if (string.IsNullOrWhiteSpace(CustomerId)) - { - throw new APIException("Customer ID is not set for Bitstamp"); - } - - // messageToSign = nonce + customer_id + api_key - string apiKey = PublicApiKey.ToUnsecureString(); - string messageToSign = payload["nonce"].ToStringInvariant() + CustomerId + apiKey; - string signature = CryptoUtility.SHA256Sign(messageToSign, PrivateApiKey.ToUnsecureString()).ToUpperInvariant(); - payload["signature"] = signature; - payload["key"] = apiKey; - await CryptoUtility.WritePayloadFormToRequestAsync(request, payload); - } - } - - private async Task MakeBitstampRequestAsync(string subUrl) - { - JToken token = await MakeJsonRequestAsync(subUrl); - if (!(token is JArray) && token["error"] != null) - { - throw new APIException(token["error"].ToStringInvariant()); - } - return token; - } - - protected override async Task> OnGetMarketSymbolsAsync() - { - List symbols = new List(); - foreach (JToken token in (await MakeBitstampRequestAsync("/trading-pairs-info"))) - { - symbols.Add(token["url_symbol"].ToStringInvariant()); - } - return symbols; - } - - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() + { + RequestContentType = "application/x-www-form-urlencoded"; + NonceStyle = NonceStyle.UnixMilliseconds; + MarketSymbolIsUppercase = false; + MarketSymbolSeparator = string.Empty; + } + + /// + /// In order to use private functions of the API, you must set CustomerId by calling this constructor with parameter, + /// or setting it later in the ExchangeBitstampAPI object. + /// + /// Customer Id can be found by the link "https://www.bitstamp.net/account/balance/" + public ExchangeBitstampAPI(string customerId) + : this() + { + CustomerId = customerId; + } + + protected override async Task ProcessRequestAsync( + IHttpWebRequest request, + Dictionary payload + ) + { + if (CanMakeAuthenticatedRequest(payload)) + { + if (string.IsNullOrWhiteSpace(CustomerId)) + { + throw new APIException("Customer ID is not set for Bitstamp"); + } + + // messageToSign = nonce + customer_id + api_key + string apiKey = PublicApiKey.ToUnsecureString(); + string messageToSign = payload["nonce"].ToStringInvariant() + CustomerId + apiKey; + string signature = CryptoUtility + .SHA256Sign(messageToSign, PrivateApiKey.ToUnsecureString()) + .ToUpperInvariant(); + payload["signature"] = signature; + payload["key"] = apiKey; + await CryptoUtility.WritePayloadFormToRequestAsync(request, payload); + } + } + + private async Task MakeBitstampRequestAsync(string subUrl) + { + JToken token = await MakeJsonRequestAsync(subUrl); + if (!(token is JArray) && token["error"] != null) + { + throw new APIException(token["error"].ToStringInvariant()); + } + return token; + } + + protected override async Task> OnGetMarketSymbolsAsync() + { + List symbols = new List(); + foreach (JToken token in (await MakeBitstampRequestAsync("/trading-pairs-info"))) + { + symbols.Add(token["url_symbol"].ToStringInvariant()); + } + return symbols; + } + + protected internal override async Task< + IEnumerable + > OnGetMarketSymbolsMetadataAsync() { // {"base_decimals": 8, "minimum_order": "5.0 USD", "name": "LTC/USD", "counter_decimals": 2, "trading": "Enabled", "url_symbol": "ltcusd", "description": "Litecoin / U.S. dollar"} List symbols = new List(); foreach (JToken token in (await MakeBitstampRequestAsync("/trading-pairs-info"))) @@ -107,141 +115,193 @@ protected internal override async Task> OnGetMarketS var counterNumDecimals = token["counter_decimals"].Value(); var counterDecimals = (decimal)Math.Pow(0.1, counterNumDecimals); var minOrderString = token["minimum_order"].ToStringInvariant(); - symbols.Add(new ExchangeMarket() - { - MarketSymbol = token["url_symbol"].ToStringInvariant(), - BaseCurrency = split[0], - QuoteCurrency = split[1], - MinTradeSize = baseDecimals, // will likely get overriden by MinTradeSizeInQuoteCurrency - QuantityStepSize = baseDecimals, - MinTradeSizeInQuoteCurrency = minOrderString.Split(' ')[0].ConvertInvariant(), - MinPrice = counterDecimals, - PriceStepSize = counterDecimals, - IsActive = token["trading"].ToStringLowerInvariant() == "enabled", - }); + symbols.Add( + new ExchangeMarket() + { + MarketSymbol = token["url_symbol"].ToStringInvariant(), + BaseCurrency = split[0], + QuoteCurrency = split[1], + MinTradeSize = baseDecimals, // will likely get overriden by MinTradeSizeInQuoteCurrency + QuantityStepSize = baseDecimals, + MinTradeSizeInQuoteCurrency = minOrderString.Split(' ')[ + 0 + ].ConvertInvariant(), + MinPrice = counterDecimals, + PriceStepSize = counterDecimals, + IsActive = token["trading"].ToStringLowerInvariant() == "enabled", + } + ); } return symbols; } protected override async Task OnGetTickerAsync(string marketSymbol) - { - // {"high": "0.10948945", "last": "0.10121817", "timestamp": "1513387486", "bid": "0.10112165", "vwap": "0.09958913", "volume": "9954.37332614", "low": "0.09100000", "ask": "0.10198408", "open": "0.10250028"} - JToken token = await MakeBitstampRequestAsync("/ticker/" + marketSymbol); - return await this.ParseTickerAsync(token, marketSymbol, "ask", "bid", "last", "volume", null, "timestamp", TimestampType.UnixSeconds); - } - - protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) - { - JToken token = await MakeBitstampRequestAsync("/order_book/" + marketSymbol); - return token.ParseOrderBookFromJTokenArrays(); - } - - 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); - List trades = new List(); - foreach (JToken trade in token) - { - trades.Add(trade.ParseTrade("amount", "price", "type", "date", TimestampType.UnixSeconds, "tid", "0")); - } - callback(trades); - } - - protected override async Task> OnGetAmountsAsync() - { - string url = "/balance/"; - var payload = await GetNoncePayloadAsync(); - var responseObject = await MakeJsonRequestAsync(url, null, payload, "POST"); - return ExtractDictionary(responseObject, "balance"); - } - - - protected override async Task> OnGetFeesAsync() - { - string url = "/balance/"; - var payload = await GetNoncePayloadAsync(); - var responseObject = await MakeJsonRequestAsync(url, null, payload, "POST"); - return ExtractDictionary(responseObject, "fee"); - } - - protected override async Task> OnGetAmountsAvailableToTradeAsync() - { - string url = "/balance/"; - var payload = await GetNoncePayloadAsync(); - var responseObject = await MakeJsonRequestAsync(url, null, payload, "POST"); - return ExtractDictionary(responseObject, "available"); - } - - private static Dictionary ExtractDictionary(JObject responseObject, string key) - { - var result = new Dictionary(); - var suffix = $"_{key}"; - foreach (var property in responseObject) - { - if (property.Key.Contains(suffix)) - { - decimal value = property.Value.ConvertInvariant(); - if (value == 0) - { - continue; - } - - result.Add(property.Key.Replace(suffix, "").Trim(), value); - } - } - return result; - } - - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) - { - if (order.IsPostOnly != null) throw new NotSupportedException("Post Only orders are not supported by this exchange or not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature."); + { + // {"high": "0.10948945", "last": "0.10121817", "timestamp": "1513387486", "bid": "0.10112165", "vwap": "0.09958913", "volume": "9954.37332614", "low": "0.09100000", "ask": "0.10198408", "open": "0.10250028"} + JToken token = await MakeBitstampRequestAsync("/ticker/" + marketSymbol); + return await this.ParseTickerAsync( + token, + marketSymbol, + "ask", + "bid", + "last", + "volume", + null, + "timestamp", + TimestampType.UnixSeconds + ); + } + + protected override async Task OnGetOrderBookAsync( + string marketSymbol, + int maxCount = 100 + ) + { + JToken token = await MakeBitstampRequestAsync("/order_book/" + marketSymbol); + return token.ParseOrderBookFromJTokenArrays(); + } + + 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); + List trades = new List(); + foreach (JToken trade in token) + { + trades.Add( + trade.ParseTrade( + "amount", + "price", + "type", + "date", + TimestampType.UnixSeconds, + "tid", + "0" + ) + ); + } + callback(trades); + } + + protected override async Task> OnGetAmountsAsync() + { + string url = "/balance/"; + var payload = await GetNoncePayloadAsync(); + var responseObject = await MakeJsonRequestAsync(url, null, payload, "POST"); + return ExtractDictionary(responseObject, "balance"); + } + + protected override async Task> OnGetFeesAsync() + { + string url = "/balance/"; + var payload = await GetNoncePayloadAsync(); + var responseObject = await MakeJsonRequestAsync(url, null, payload, "POST"); + return ExtractDictionary(responseObject, "fee"); + } + + protected override async Task< + Dictionary + > OnGetAmountsAvailableToTradeAsync() + { + string url = "/balance/"; + var payload = await GetNoncePayloadAsync(); + var responseObject = await MakeJsonRequestAsync(url, null, payload, "POST"); + return ExtractDictionary(responseObject, "available"); + } + + private static Dictionary ExtractDictionary( + JObject responseObject, + string key + ) + { + var result = new Dictionary(); + var suffix = $"_{key}"; + foreach (var property in responseObject) + { + if (property.Key.Contains(suffix)) + { + decimal value = property.Value.ConvertInvariant(); + if (value == 0) + { + continue; + } + + result.Add(property.Key.Replace(suffix, "").Trim(), value); + } + } + return result; + } + + protected override async Task OnPlaceOrderAsync( + ExchangeOrderRequest order + ) + { + if (order.IsPostOnly != null) + throw new NotSupportedException( + "Post Only orders are not supported by this exchange or not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature." + ); string action = order.IsBuy ? "buy" : "sell"; - string market = order.OrderType == OrderType.Market ? "/market" : ""; - string url = $"/{action}{market}/{order.MarketSymbol}/"; - Dictionary payload = await GetNoncePayloadAsync(); + string market = order.OrderType == OrderType.Market ? "/market" : ""; + string url = $"/{action}{market}/{order.MarketSymbol}/"; + Dictionary payload = await GetNoncePayloadAsync(); - if (order.OrderType != OrderType.Market) - { - if (order.Price == null) throw new ArgumentNullException(nameof(order.Price)); + if (order.OrderType != OrderType.Market) + { + if (order.Price == null) + throw new ArgumentNullException(nameof(order.Price)); payload["price"] = order.Price.ToStringInvariant(); - } - - payload["amount"] = order.RoundAmount().ToStringInvariant(); - order.ExtraParameters.CopyTo(payload); - - JObject responseObject = await MakeJsonRequestAsync(url, null, payload, "POST"); - return new ExchangeOrderResult - { - OrderDate = CryptoUtility.UtcNow, - OrderId = responseObject["id"].ToStringInvariant(), - IsBuy = order.IsBuy, - MarketSymbol = order.MarketSymbol - }; - } - - protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + } + + payload["amount"] = order.RoundAmount().ToStringInvariant(); + order.ExtraParameters.CopyTo(payload); + + JObject responseObject = await MakeJsonRequestAsync( + url, + null, + payload, + "POST" + ); + return new ExchangeOrderResult + { + OrderDate = CryptoUtility.UtcNow, + OrderId = responseObject["id"].ToStringInvariant(), + IsBuy = order.IsBuy, + MarketSymbol = order.MarketSymbol + }; + } + + protected override async Task OnGetOrderDetailsAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { - //{ - // "status": "Finished", - // "id": 1022694747, - // "transactions": [ - // { - // "fee": "0.000002", - // "bch": "0.00882714", - // "price": "0.12120000", - // "datetime": "2018-02-24 14:15:29.133824", - // "btc": "0.0010698493680000", - // "tid": 56293144, - // "type": 2 - // }] - //} - if (string.IsNullOrWhiteSpace(orderId)) - { - return null; - } - string url = "/order_status/"; - Dictionary payload = await GetNoncePayloadAsync(); + //{ + // "status": "Finished", + // "id": 1022694747, + // "transactions": [ + // { + // "fee": "0.000002", + // "bch": "0.00882714", + // "price": "0.12120000", + // "datetime": "2018-02-24 14:15:29.133824", + // "btc": "0.0010698493680000", + // "tid": 56293144, + // "type": 2 + // }] + //} + if (string.IsNullOrWhiteSpace(orderId)) + { + return null; + } + string url = "/order_status/"; + Dictionary payload = await GetNoncePayloadAsync(); if (isClientOrderId) // Order can be fetched by using either id or client_order_id parameter. payload["client_order_id"] = orderId; else @@ -255,9 +315,9 @@ protected override async Task OnGetOrderDetailsAsync(string var statusCode = result.Value("status"); var status = GetOrderResultFromStatus(statusCode, anyTransaction); - // empty transaction array means that order is InQueue or Open and AmountFilled == 0 - // return empty order in this case. no any additional info available at this point - if (!anyTransaction) + // empty transaction array means that order is InQueue or Open and AmountFilled == 0 + // return empty order in this case. no any additional info available at this point + if (!anyTransaction) { return new ExchangeOrderResult { @@ -266,347 +326,512 @@ protected override async Task OnGetOrderDetailsAsync(string ResultCode = statusCode }; } - JObject first = transactions.First() as JObject; - List excludeStrings = new List() { "tid", "price", "fee", "datetime", "type", "btc", "usd", "eur" }; - - string quoteCurrency; - string baseCurrency = first.Properties().FirstOrDefault(p => !excludeStrings.Contains(p.Name, StringComparer.InvariantCultureIgnoreCase))?.Name; - if (string.IsNullOrWhiteSpace(baseCurrency)) - { - // the only 2 cases are BTC-USD and BTC-EUR - baseCurrency = "btc"; - excludeStrings.RemoveAll(s => s.Equals("usd") || s.Equals("eur")); - quoteCurrency = first.Properties().FirstOrDefault(p => !excludeStrings.Contains(p.Name, StringComparer.InvariantCultureIgnoreCase))?.Name; - } - else - { - excludeStrings.RemoveAll(s => s.Equals("usd") || s.Equals("eur") || s.Equals("btc")); - excludeStrings.Add(baseCurrency); - quoteCurrency = first.Properties().FirstOrDefault(p => !excludeStrings.Contains(p.Name, StringComparer.InvariantCultureIgnoreCase))?.Name; - } - string _symbol = $"{baseCurrency}-{quoteCurrency}"; - - decimal amountFilled = 0; - decimal spentQuoteCurrency = 0; - decimal price = 0; - decimal fees = 0; - - foreach (var t in transactions) - { - int type = t["type"].ConvertInvariant(); - if (type != 2) { continue; } - spentQuoteCurrency += t[quoteCurrency].ConvertInvariant(); - amountFilled += t[baseCurrency].ConvertInvariant(); - fees += t["fee"].ConvertInvariant(); - //set price only one time - if (price == 0) - { - price = t["price"].ConvertInvariant(); - } - } - - // No way to know if order IsBuy, Amount, OrderDate - return new ExchangeOrderResult() - { + JObject first = transactions.First() as JObject; + List excludeStrings = new List() + { + "tid", + "price", + "fee", + "datetime", + "type", + "btc", + "usd", + "eur" + }; + + string quoteCurrency; + string baseCurrency = first + .Properties() + .FirstOrDefault( + p => !excludeStrings.Contains(p.Name, StringComparer.InvariantCultureIgnoreCase) + ) + ?.Name; + if (string.IsNullOrWhiteSpace(baseCurrency)) + { + // the only 2 cases are BTC-USD and BTC-EUR + baseCurrency = "btc"; + excludeStrings.RemoveAll(s => s.Equals("usd") || s.Equals("eur")); + quoteCurrency = first + .Properties() + .FirstOrDefault( + p => + !excludeStrings.Contains( + p.Name, + StringComparer.InvariantCultureIgnoreCase + ) + ) + ?.Name; + } + else + { + excludeStrings.RemoveAll( + s => s.Equals("usd") || s.Equals("eur") || s.Equals("btc") + ); + excludeStrings.Add(baseCurrency); + quoteCurrency = first + .Properties() + .FirstOrDefault( + p => + !excludeStrings.Contains( + p.Name, + StringComparer.InvariantCultureIgnoreCase + ) + ) + ?.Name; + } + string _symbol = $"{baseCurrency}-{quoteCurrency}"; + + decimal amountFilled = 0; + decimal spentQuoteCurrency = 0; + decimal price = 0; + decimal fees = 0; + + foreach (var t in transactions) + { + int type = t["type"].ConvertInvariant(); + if (type != 2) + { + continue; + } + spentQuoteCurrency += t[quoteCurrency].ConvertInvariant(); + amountFilled += t[baseCurrency].ConvertInvariant(); + fees += t["fee"].ConvertInvariant(); + //set price only one time + if (price == 0) + { + price = t["price"].ConvertInvariant(); + } + } + + // No way to know if order IsBuy, Amount, OrderDate + return new ExchangeOrderResult() + { OrderId = orderId, - AmountFilled = amountFilled, - MarketSymbol = _symbol, - AveragePrice = spentQuoteCurrency / amountFilled, - Price = price, - Fees = fees, - FeesCurrency = quoteCurrency, + AmountFilled = amountFilled, + MarketSymbol = _symbol, + AveragePrice = spentQuoteCurrency / amountFilled, + Price = price, + Fees = fees, + FeesCurrency = quoteCurrency, Result = status, ResultCode = statusCode }; - } - - protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) - { - List orders = new List(); - // TODO: Bitstamp bug: bad request if url contains symbol, so temporarily using url for all symbols - // string url = string.IsNullOrWhiteSpace(symbol) ? "/open_orders/all/" : "/open_orders/" + symbol; - string url = "/open_orders/all/"; - JArray result = await MakeJsonRequestAsync(url, null, await GetNoncePayloadAsync(), "POST"); - foreach (JToken token in result) - { - //This request doesn't give info about amount filled, use GetOrderDetails(orderId) - string tokenSymbol = token["currency_pair"].ToStringLowerInvariant().Replace("/", ""); - if (!string.IsNullOrWhiteSpace(tokenSymbol) && !string.IsNullOrWhiteSpace(marketSymbol) && !tokenSymbol.Equals(marketSymbol, StringComparison.InvariantCultureIgnoreCase)) - { - continue; - } - orders.Add(new ExchangeOrderResult() - { - OrderId = token["id"].ToStringInvariant(), - OrderDate = token["datetime"].ToDateTimeInvariant(), - IsBuy = token["type"].ConvertInvariant() == 0, - Price = token["price"].ConvertInvariant(), - Amount = token["amount"].ConvertInvariant(), - MarketSymbol = tokenSymbol ?? marketSymbol - }); - } - return orders; - } + } + + protected override async Task> OnGetOpenOrderDetailsAsync( + string marketSymbol = null + ) + { + List orders = new List(); + // TODO: Bitstamp bug: bad request if url contains symbol, so temporarily using url for all symbols + // string url = string.IsNullOrWhiteSpace(symbol) ? "/open_orders/all/" : "/open_orders/" + symbol; + string url = "/open_orders/all/"; + JArray result = await MakeJsonRequestAsync( + url, + null, + await GetNoncePayloadAsync(), + "POST" + ); + foreach (JToken token in result) + { + //This request doesn't give info about amount filled, use GetOrderDetails(orderId) + string tokenSymbol = token["currency_pair"] + .ToStringLowerInvariant() + .Replace("/", ""); + if ( + !string.IsNullOrWhiteSpace(tokenSymbol) + && !string.IsNullOrWhiteSpace(marketSymbol) + && !tokenSymbol.Equals( + marketSymbol, + StringComparison.InvariantCultureIgnoreCase + ) + ) + { + continue; + } + orders.Add( + new ExchangeOrderResult() + { + OrderId = token["id"].ToStringInvariant(), + OrderDate = token["datetime"].ToDateTimeInvariant(), + IsBuy = token["type"].ConvertInvariant() == 0, + Price = token["price"].ConvertInvariant(), + Amount = token["amount"].ConvertInvariant(), + MarketSymbol = tokenSymbol ?? marketSymbol + } + ); + } + return orders; + } private ExchangeAPIOrderResult GetOrderResultFromStatus(string status, bool anyTransactions) { switch (status?.ToLower()) { - case "finished": return ExchangeAPIOrderResult.Filled; - case "open": return anyTransactions - ? ExchangeAPIOrderResult.FilledPartially - : ExchangeAPIOrderResult.Open; - case "canceled": return ExchangeAPIOrderResult.Canceled; - default: return ExchangeAPIOrderResult.Unknown; + case "finished": + return ExchangeAPIOrderResult.Filled; + case "open": + return anyTransactions + ? ExchangeAPIOrderResult.FilledPartially + : ExchangeAPIOrderResult.Open; + case "canceled": + return ExchangeAPIOrderResult.Canceled; + default: + return ExchangeAPIOrderResult.Unknown; } } public class BitstampTransaction - { - public BitstampTransaction(string id, DateTime dateTime, int type, string symbol, decimal fees, string orderId, decimal quantity, decimal price, bool isBuy) - { - Id = id; - DateTime = dateTime; - Type = type; - Symbol = symbol; - Fees = fees; - OrderId = orderId; - Quantity = quantity; - Price = price; - IsBuy = isBuy; - } - - public string Id { get; } - public DateTime DateTime { get; } - public int Type { get; } // Transaction type: 0 - deposit; 1 - withdrawal; 2 - market trade; 14 - sub account transfer. - public string Symbol { get; } - public decimal Fees { get; } - public string OrderId { get; } - public decimal Quantity { get; } - public decimal Price { get; } - public bool IsBuy { get; } - } - - public async Task> GetUserTransactionsAsync(string marketSymbol = null, DateTime? afterDate = null) - { - await new SynchronizationContextRemover(); - - marketSymbol = NormalizeMarketSymbol(marketSymbol); - // TODO: Bitstamp bug: bad request if url contains symbol, so temporarily using url for all symbols - // string url = string.IsNullOrWhiteSpace(symbol) ? "/user_transactions/" : "/user_transactions/" + symbol; - string url = "/user_transactions/"; - var payload = await GetNoncePayloadAsync(); - payload["limit"] = 1000; - JToken result = await MakeJsonRequestAsync(url, null, payload, "POST"); - - List transactions = new List(); - - foreach (var transaction in result as JArray) - { - int type = transaction["type"].ConvertInvariant(); - // only type 2 is order transaction type, so we discard all other transactions - if (type != 2) { continue; } - - string tradingPair = ((JObject)transaction).Properties().FirstOrDefault(p => - !p.Name.Equals("order_id", StringComparison.InvariantCultureIgnoreCase) - && p.Name.Contains("_"))?.Name.Replace("_", "-"); - tradingPair = NormalizeMarketSymbol(tradingPair); - if (!string.IsNullOrWhiteSpace(tradingPair) && !string.IsNullOrWhiteSpace(marketSymbol) && !tradingPair.Equals(marketSymbol)) - { - continue; - } - - var baseCurrency = tradingPair.Trim().Substring(tradingPair.Length - 3).ToLowerInvariant(); - var marketCurrency = tradingPair.Trim().ToLowerInvariant().Replace(baseCurrency, "").Replace("-", "").Replace("_", ""); - - decimal amount = transaction[baseCurrency].ConvertInvariant(); - decimal signedQuantity = transaction[marketCurrency].ConvertInvariant(); - decimal quantity = Math.Abs(signedQuantity); - decimal price = Math.Abs(amount / signedQuantity); - bool isBuy = signedQuantity > 0; - var id = transaction["id"].ToStringInvariant(); - var datetime = transaction["datetime"].ToDateTimeInvariant(); - var fee = transaction["fee"].ConvertInvariant(); - var orderId = transaction["order_id"].ToStringInvariant(); - - var bitstampTransaction = new BitstampTransaction(id, datetime, type, tradingPair, fee, orderId, quantity, price, isBuy); - transactions.Add(bitstampTransaction); - } - - return transactions; - } - - protected override async Task> OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) - { - // TODO: Bitstamp bug: bad request if url contains symbol, so temporarily using url for all symbols - // string url = string.IsNullOrWhiteSpace(symbol) ? "/user_transactions/" : "/user_transactions/" + symbol; - string url = "/user_transactions/"; - var payload = await GetNoncePayloadAsync(); - payload["limit"] = 1000; - JToken result = await MakeJsonRequestAsync(url, null, payload, "POST"); - List orders = new List(); - foreach (var transaction in result as JArray) - { - int type = transaction["type"].ConvertInvariant(); - // only type 2 is order transaction type, so we discard all other transactions - if (type != 2) { continue; } - - string tradingPair = ((JObject)transaction).Properties().FirstOrDefault(p => - !p.Name.Equals("order_id", StringComparison.InvariantCultureIgnoreCase) - && p.Name.Contains("_"))?.Name.Replace("_", "-"); - if (!string.IsNullOrWhiteSpace(tradingPair) && !string.IsNullOrWhiteSpace(marketSymbol) && !NormalizeMarketSymbol(tradingPair).Equals(marketSymbol)) - { - continue; - } - - var quoteCurrency = tradingPair.Trim().Substring(tradingPair.Length - 3).ToLowerInvariant(); - var baseCurrency = tradingPair.Trim().ToLowerInvariant().Replace(quoteCurrency, "").Replace("-", "").Replace("_", ""); - - decimal resultBaseCurrency = transaction[baseCurrency].ConvertInvariant(); - ExchangeOrderResult order = new ExchangeOrderResult() - { - OrderId = transaction["order_id"].ToStringInvariant(), - IsBuy = resultBaseCurrency > 0, - Fees = transaction["fee"].ConvertInvariant(), - FeesCurrency = quoteCurrency.ToStringUpperInvariant(), - MarketSymbol = NormalizeMarketSymbol(tradingPair), - OrderDate = transaction["datetime"].ToDateTimeInvariant(), - AmountFilled = Math.Abs(resultBaseCurrency), - AveragePrice = transaction[$"{baseCurrency}_{quoteCurrency}"].ConvertInvariant() - }; - orders.Add(order); - } - // at this point one transaction transformed into one order, we need to consolidate parts into order - // group by order id - var groupings = orders.GroupBy(o => o.OrderId); - List orders2 = new List(); - foreach (var group in groupings) - { - decimal spentQuoteCurrency = group.Sum(o => o.AveragePrice.Value * o.AmountFilled.Value); - ExchangeOrderResult order = group.First(); - order.AmountFilled = group.Sum(o => o.AmountFilled); - order.AveragePrice = spentQuoteCurrency / order.AmountFilled; - order.Price = order.AveragePrice; - orders2.Add(order); - } - - return orders2; - } - - protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) { - if (isClientOrderId) throw new NotSupportedException("Cancelling by client order ID is not supported in ExchangeSharp. Please submit a PR if you are interested in this feature"); - if (string.IsNullOrWhiteSpace(orderId)) - { - throw new APIException("OrderId is needed for canceling order"); - } - Dictionary payload = await GetNoncePayloadAsync(); - payload["id"] = orderId; - await MakeJsonRequestAsync("/cancel_order/", null, payload, "POST"); - } - - /// - /// Function to withdraw from Bitsamp exchange. At the moment only XRP is supported. - /// - /// - /// - protected override async Task OnWithdrawAsync(ExchangeWithdrawalRequest withdrawalRequest) - { - string baseurl = null; - string url; - switch (withdrawalRequest.Currency) - { - case "BTC": - // use old API for Bitcoin withdraw - baseurl = "https://www.bitstamp.net/api/"; - url = "/bitcoin_withdrawal/"; - break; - default: - // this will work for some currencies and fail for others, caller must be aware of the supported currencies - url = "/" + withdrawalRequest.Currency.ToLowerInvariant() + "_withdrawal/"; - break; - } - - Dictionary payload = await GetNoncePayloadAsync(); - payload["address"] = withdrawalRequest.Address.ToStringInvariant(); - payload["amount"] = withdrawalRequest.Amount.ToStringInvariant(); - payload["destination_tag"] = withdrawalRequest.AddressTag.ToStringInvariant(); - - JObject responseObject = await MakeJsonRequestAsync(url, baseurl, payload, "POST"); - CheckJsonResponse(responseObject); - return new ExchangeWithdrawalResponse - { - Id = responseObject["id"].ToStringInvariant(), - Message = responseObject["message"].ToStringInvariant(), - Success = responseObject["success"].ConvertInvariant() - }; - } - - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) - { - if (marketSymbols == null || marketSymbols.Length == 0) + public BitstampTransaction( + string id, + DateTime dateTime, + int type, + string symbol, + decimal fees, + string orderId, + decimal quantity, + decimal price, + bool isBuy + ) { - marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); + Id = id; + DateTime = dateTime; + Type = type; + Symbol = symbol; + Fees = fees; + OrderId = orderId; + Quantity = quantity; + Price = price; + IsBuy = isBuy; } - return await ConnectPublicWebSocketAsync(null, messageCallback: async (_socket, msg) => + + public string Id { get; } + public DateTime DateTime { get; } + public int Type { get; } // Transaction type: 0 - deposit; 1 - withdrawal; 2 - market trade; 14 - sub account transfer. + public string Symbol { get; } + public decimal Fees { get; } + public string OrderId { get; } + public decimal Quantity { get; } + public decimal Price { get; } + public bool IsBuy { get; } + } + + public async Task> GetUserTransactionsAsync( + string marketSymbol = null, + DateTime? afterDate = null + ) + { + await new SynchronizationContextRemover(); + + marketSymbol = NormalizeMarketSymbol(marketSymbol); + // TODO: Bitstamp bug: bad request if url contains symbol, so temporarily using url for all symbols + // string url = string.IsNullOrWhiteSpace(symbol) ? "/user_transactions/" : "/user_transactions/" + symbol; + string url = "/user_transactions/"; + var payload = await GetNoncePayloadAsync(); + payload["limit"] = 1000; + JToken result = await MakeJsonRequestAsync(url, null, payload, "POST"); + + List transactions = new List(); + + foreach (var transaction in result as JArray) { - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - if (token["event"].ToStringInvariant() == "bts:error") - { // {{"event": "bts:error", "channel": "", - // "data": {"code": null, "message": "Bad subscription string." } }} - token = token["data"]; - Logger.Info(token["code"].ToStringInvariant() + " " - + token["message"].ToStringInvariant()); - } - else if (token["event"].ToStringInvariant() == "trade") + int type = transaction["type"].ConvertInvariant(); + // only type 2 is order transaction type, so we discard all other transactions + if (type != 2) { - //{{ - // "data": { - // "microtimestamp": "1563418286809203", - // "amount": 0.141247, - // "buy_order_id": 3785916113, - // "sell_order_id": 3785915893, - // "amount_str": "0.14124700", - // "price_str": "9754.23", - // "timestamp": "1563418286", - // "price": 9754.23, - // "type": 0, // Trade type (0 - buy; 1 - sell). - // "id": 94160906 - // }, - // "event": "trade", - // "channel": "live_trades_btcusd" - //}} - string marketSymbol = token["channel"].ToStringInvariant().Split('_')[2]; - var trade = token["data"].ParseTradeBitstamp(amountKey: "amount", priceKey: "price", - typeKey: "type", timestampKey: "microtimestamp", - TimestampType.UnixMicroeconds, idKey: "id", - typeKeyIsBuyValue: "0"); - await callback(new KeyValuePair(marketSymbol, trade)); + continue; } - else if (token["event"].ToStringInvariant() == "bts:subscription_succeeded") - { // {{ "event": "bts:subscription_succeeded", - //"channel": "live_trades_btcusd", - //"data": { } }} + + string tradingPair = ((JObject)transaction) + .Properties() + .FirstOrDefault( + p => + !p.Name.Equals("order_id", StringComparison.InvariantCultureIgnoreCase) + && p.Name.Contains("_") + ) + ?.Name.Replace("_", "-"); + tradingPair = NormalizeMarketSymbol(tradingPair); + if ( + !string.IsNullOrWhiteSpace(tradingPair) + && !string.IsNullOrWhiteSpace(marketSymbol) + && !tradingPair.Equals(marketSymbol) + ) + { + continue; } - }, connectCallback: async (_socket) => + + var baseCurrency = tradingPair + .Trim() + .Substring(tradingPair.Length - 3) + .ToLowerInvariant(); + var marketCurrency = tradingPair + .Trim() + .ToLowerInvariant() + .Replace(baseCurrency, "") + .Replace("-", "") + .Replace("_", ""); + + decimal amount = transaction[baseCurrency].ConvertInvariant(); + decimal signedQuantity = transaction[marketCurrency].ConvertInvariant(); + decimal quantity = Math.Abs(signedQuantity); + decimal price = Math.Abs(amount / signedQuantity); + bool isBuy = signedQuantity > 0; + var id = transaction["id"].ToStringInvariant(); + var datetime = transaction["datetime"].ToDateTimeInvariant(); + var fee = transaction["fee"].ConvertInvariant(); + var orderId = transaction["order_id"].ToStringInvariant(); + + var bitstampTransaction = new BitstampTransaction( + id, + datetime, + type, + tradingPair, + fee, + orderId, + quantity, + price, + isBuy + ); + transactions.Add(bitstampTransaction); + } + + return transactions; + } + + protected override async Task< + IEnumerable + > OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) + { + // TODO: Bitstamp bug: bad request if url contains symbol, so temporarily using url for all symbols + // string url = string.IsNullOrWhiteSpace(symbol) ? "/user_transactions/" : "/user_transactions/" + symbol; + string url = "/user_transactions/"; + var payload = await GetNoncePayloadAsync(); + payload["limit"] = 1000; + JToken result = await MakeJsonRequestAsync(url, null, payload, "POST"); + List orders = new List(); + foreach (var transaction in result as JArray) { - //{ - // "event": "bts:subscribe", - // "data": { - // "channel": "[channel_name]" - // } - //} - foreach (var marketSymbol in marketSymbols) + int type = transaction["type"].ConvertInvariant(); + // only type 2 is order transaction type, so we discard all other transactions + if (type != 2) { - await _socket.SendMessageAsync(new - { - @event = "bts:subscribe", - data = new { channel = $"live_trades_{marketSymbol}" } - }); + continue; + } + + string tradingPair = ((JObject)transaction) + .Properties() + .FirstOrDefault( + p => + !p.Name.Equals("order_id", StringComparison.InvariantCultureIgnoreCase) + && p.Name.Contains("_") + ) + ?.Name.Replace("_", "-"); + if ( + !string.IsNullOrWhiteSpace(tradingPair) + && !string.IsNullOrWhiteSpace(marketSymbol) + && !NormalizeMarketSymbol(tradingPair).Equals(marketSymbol) + ) + { + continue; } - }); + + var quoteCurrency = tradingPair + .Trim() + .Substring(tradingPair.Length - 3) + .ToLowerInvariant(); + var baseCurrency = tradingPair + .Trim() + .ToLowerInvariant() + .Replace(quoteCurrency, "") + .Replace("-", "") + .Replace("_", ""); + + decimal resultBaseCurrency = transaction[baseCurrency].ConvertInvariant(); + ExchangeOrderResult order = new ExchangeOrderResult() + { + OrderId = transaction["order_id"].ToStringInvariant(), + IsBuy = resultBaseCurrency > 0, + Fees = transaction["fee"].ConvertInvariant(), + FeesCurrency = quoteCurrency.ToStringUpperInvariant(), + MarketSymbol = NormalizeMarketSymbol(tradingPair), + OrderDate = transaction["datetime"].ToDateTimeInvariant(), + AmountFilled = Math.Abs(resultBaseCurrency), + AveragePrice = transaction[ + $"{baseCurrency}_{quoteCurrency}" + ].ConvertInvariant() + }; + orders.Add(order); + } + // at this point one transaction transformed into one order, we need to consolidate parts into order + // group by order id + var groupings = orders.GroupBy(o => o.OrderId); + List orders2 = new List(); + foreach (var group in groupings) + { + decimal spentQuoteCurrency = group.Sum( + o => o.AveragePrice.Value * o.AmountFilled.Value + ); + ExchangeOrderResult order = group.First(); + order.AmountFilled = group.Sum(o => o.AmountFilled); + order.AveragePrice = spentQuoteCurrency / order.AmountFilled; + order.Price = order.AveragePrice; + orders2.Add(order); + } + + return orders2; + } + + protected override async Task OnCancelOrderAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) + { + if (isClientOrderId) + throw new NotSupportedException( + "Cancelling by client order ID is not supported in ExchangeSharp. Please submit a PR if you are interested in this feature" + ); + if (string.IsNullOrWhiteSpace(orderId)) + { + throw new APIException("OrderId is needed for canceling order"); + } + Dictionary payload = await GetNoncePayloadAsync(); + payload["id"] = orderId; + await MakeJsonRequestAsync("/cancel_order/", null, payload, "POST"); + } + + /// + /// Function to withdraw from Bitsamp exchange. At the moment only XRP is supported. + /// + /// + /// + protected override async Task OnWithdrawAsync( + ExchangeWithdrawalRequest withdrawalRequest + ) + { + string baseurl = null; + string url; + switch (withdrawalRequest.Currency) + { + case "BTC": + // use old API for Bitcoin withdraw + baseurl = "https://www.bitstamp.net/api/"; + url = "/bitcoin_withdrawal/"; + break; + default: + // this will work for some currencies and fail for others, caller must be aware of the supported currencies + url = "/" + withdrawalRequest.Currency.ToLowerInvariant() + "_withdrawal/"; + break; + } + + Dictionary payload = await GetNoncePayloadAsync(); + payload["address"] = withdrawalRequest.Address.ToStringInvariant(); + payload["amount"] = withdrawalRequest.Amount.ToStringInvariant(); + payload["destination_tag"] = withdrawalRequest.AddressTag.ToStringInvariant(); + + JObject responseObject = await MakeJsonRequestAsync( + url, + baseurl, + payload, + "POST" + ); + CheckJsonResponse(responseObject); + return new ExchangeWithdrawalResponse + { + Id = responseObject["id"].ToStringInvariant(), + Message = responseObject["message"].ToStringInvariant(), + Success = responseObject["success"].ConvertInvariant() + }; + } + + protected override async Task OnGetTradesWebSocketAsync( + Func, Task> callback, + params string[] marketSymbols + ) + { + if (marketSymbols == null || marketSymbols.Length == 0) + { + marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); + } + return await ConnectPublicWebSocketAsync( + null, + messageCallback: async (_socket, msg) => + { + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + if (token["event"].ToStringInvariant() == "bts:error") + { // {{"event": "bts:error", "channel": "", + // "data": {"code": null, "message": "Bad subscription string." } }} + token = token["data"]; + Logger.Info( + token["code"].ToStringInvariant() + + " " + + token["message"].ToStringInvariant() + ); + } + else if (token["event"].ToStringInvariant() == "trade") + { + //{{ + // "data": { + // "microtimestamp": "1563418286809203", + // "amount": 0.141247, + // "buy_order_id": 3785916113, + // "sell_order_id": 3785915893, + // "amount_str": "0.14124700", + // "price_str": "9754.23", + // "timestamp": "1563418286", + // "price": 9754.23, + // "type": 0, // Trade type (0 - buy; 1 - sell). + // "id": 94160906 + // }, + // "event": "trade", + // "channel": "live_trades_btcusd" + //}} + string marketSymbol = token["channel"].ToStringInvariant().Split('_')[2]; + var trade = token["data"].ParseTradeBitstamp( + amountKey: "amount", + priceKey: "price", + typeKey: "type", + timestampKey: "microtimestamp", + TimestampType.UnixMicroeconds, + idKey: "id", + typeKeyIsBuyValue: "0" + ); + await callback( + new KeyValuePair(marketSymbol, trade) + ); + } + else if (token["event"].ToStringInvariant() == "bts:subscription_succeeded") + { // {{ "event": "bts:subscription_succeeded", + //"channel": "live_trades_btcusd", + //"data": { } }} + } + }, + connectCallback: async (_socket) => + { + //{ + // "event": "bts:subscribe", + // "data": { + // "channel": "[channel_name]" + // } + //} + foreach (var marketSymbol in marketSymbols) + { + await _socket.SendMessageAsync( + new + { + @event = "bts:subscribe", + data = new { channel = $"live_trades_{marketSymbol}" } + } + ); + } + } + ); } } - public partial class ExchangeName { public const string Bitstamp = "Bitstamp"; } + public partial class ExchangeName + { + public const string Bitstamp = "Bitstamp"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/Bitstamp/Models/BitstampTrade.cs b/src/ExchangeSharp/API/Exchanges/Bitstamp/Models/BitstampTrade.cs index 4ae7add53..57026a979 100644 --- a/src/ExchangeSharp/API/Exchanges/Bitstamp/Models/BitstampTrade.cs +++ b/src/ExchangeSharp/API/Exchanges/Bitstamp/Models/BitstampTrade.cs @@ -22,6 +22,7 @@ public class BitstampTrade : ExchangeTrade { public Int64 BuyOrderId { get; set; } public Int64 SellOrderId { get; set; } + public override string ToString() { return string.Format("{0},{1},{2}", base.ToString(), BuyOrderId, SellOrderId); diff --git a/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI.cs b/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI.cs index b0ae8179b..3e40913ba 100644 --- a/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI.cs @@ -12,8 +12,6 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp { - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; @@ -22,17 +20,19 @@ namespace ExchangeSharp using System.Text; using System.Threading.Tasks; using System.Web; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; public sealed partial class ExchangeBittrexAPI : ExchangeAPI { public override string BaseUrl { get; set; } = "https://api.bittrex.com/v3"; + private ExchangeBittrexAPI() { RateLimit = new RateGate(60, TimeSpan.FromSeconds(60)); RequestContentType = "application/json"; MarketSymbolIsReversed = false; WebSocketOrderBookType = WebSocketOrderBookType.FullBookAlways; - } #region Utilities @@ -41,42 +41,50 @@ public override string PeriodSecondsToString(int seconds) string periodString; switch (seconds) { - case 60: periodString = "MINUTE_1"; break; - case 300: periodString = "MINUTE_5"; break; - case 3600: periodString = "HOUR_1"; break; - case 86400: periodString = "DAY_1"; break; + case 60: + periodString = "MINUTE_1"; + break; + case 300: + periodString = "MINUTE_5"; + break; + case 3600: + periodString = "HOUR_1"; + break; + case 86400: + periodString = "DAY_1"; + break; default: - throw new ArgumentException($"{nameof(seconds)} must be one of 60 (min), 300 (fiveMin) 3600 (hour), 86400 (day)"); + throw new ArgumentException( + $"{nameof(seconds)} must be one of 60 (min), 300 (fiveMin) 3600 (hour), 86400 (day)" + ); } return periodString; } - private ExchangeOrderResult ParseOrder(JToken token) { - /* { - "id": "string (uuid)", - "marketSymbol": "string", - "direction": "string", - "type": "string", - "quantity": "number (double)", - "limit": "number (double)", - "ceiling": "number (double)", - "timeInForce": "string", - "clientOrderId": "string (uuid)", - "fillQuantity": "number (double)", - "commission": "number (double)", - "proceeds": "number (double)", - "status": "string", - "createdAt": "string (date-time)", - "updatedAt": "string (date-time)", - "closedAt": "string (date-time)", - "orderToCancel": { - "type": "string", - "id": "string (uuid)" - } + "id": "string (uuid)", + "marketSymbol": "string", + "direction": "string", + "type": "string", + "quantity": "number (double)", + "limit": "number (double)", + "ceiling": "number (double)", + "timeInForce": "string", + "clientOrderId": "string (uuid)", + "fillQuantity": "number (double)", + "commission": "number (double)", + "proceeds": "number (double)", + "status": "string", + "createdAt": "string (date-time)", + "updatedAt": "string (date-time)", + "closedAt": "string (date-time)", + "orderToCancel": { + "type": "string", + "id": "string (uuid)" + } } */ ExchangeOrderResult order = new ExchangeOrderResult(); @@ -126,7 +134,10 @@ private string ByteToString(byte[] buff) #endregion #region RequestHelper - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + protected override async Task ProcessRequestAsync( + IHttpWebRequest request, + Dictionary payload + ) { if (CanMakeAuthenticatedRequest(payload)) { @@ -147,9 +158,15 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti request.AddHeader("Api-Key", PublicApiKey.ToUnsecureString()); request.AddHeader("Api-Timestamp", timeStamp.ToStringInvariant()); request.AddHeader("Api-Content-Hash", hash); - request.AddHeader("Api-Signature", CryptoUtility.SHA512Sign(sign, PrivateApiKey.ToUnsecureString())); + request.AddHeader( + "Api-Signature", + CryptoUtility.SHA512Sign(sign, PrivateApiKey.ToUnsecureString()) + ); if (request.Method == "POST") - await CryptoUtility.WriteToRequestAsync(request, JsonConvert.SerializeObject(payload)); + await CryptoUtility.WriteToRequestAsync( + request, + JsonConvert.SerializeObject(payload) + ); } //Console.WriteLine(request.RequestUri); //return base.ProcessRequestAsync(request, payload); @@ -157,9 +174,13 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti #endregion #region CurrencyData - protected override async Task> OnGetCurrenciesAsync() + protected override async Task< + IReadOnlyDictionary + > OnGetCurrenciesAsync() { - var currencies = new Dictionary(StringComparer.OrdinalIgnoreCase); + var currencies = new Dictionary( + StringComparer.OrdinalIgnoreCase + ); JToken array = await MakeJsonRequestAsync("/currencies"); foreach (JToken token in array) { @@ -188,7 +209,9 @@ protected override async Task> OnG /// Get exchange symbols including available metadata such as min trade size and whether the market is active /// /// Collection of ExchangeMarkets - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() + protected internal override async Task< + IEnumerable + > OnGetMarketSymbolsMetadataAsync() { var markets = new List(); JToken array = await MakeJsonRequestAsync("/markets"); @@ -228,20 +251,45 @@ protected override async Task> OnGetMarketSymbolsAsync() #region Tickers protected override async Task OnGetTickerAsync(string marketSymbol) { - JToken ticker = await MakeJsonRequestAsync("/markets/" + marketSymbol + "/ticker"); + JToken ticker = await MakeJsonRequestAsync( + "/markets/" + marketSymbol + "/ticker" + ); //NOTE: Bittrex uses the term "BaseVolume" when referring to the QuoteCurrencyVolume - return await this.ParseTickerAsync(ticker, marketSymbol, "askRate", "bidRate", "lastTradeRate", "volume", "quoteVolume", "updatedAt", TimestampType.Iso8601UTC); + return await this.ParseTickerAsync( + ticker, + marketSymbol, + "askRate", + "bidRate", + "lastTradeRate", + "volume", + "quoteVolume", + "updatedAt", + TimestampType.Iso8601UTC + ); } - protected override async Task>> OnGetTickersAsync() + protected override async Task< + IEnumerable> + > OnGetTickersAsync() { JToken tickers = await MakeJsonRequestAsync("/markets/tickers"); string marketSymbol; - List> tickerList = new List>(); + List> tickerList = + new List>(); foreach (JToken ticker in tickers) { marketSymbol = ticker["symbol"].ToStringInvariant(); - ExchangeTicker tickerObj = await this.ParseTickerAsync(ticker, marketSymbol, "askRate", "bidRate", "lastTradeRate", "volume", "quoteVolume", "updatedAt", TimestampType.Iso8601UTC); + ExchangeTicker tickerObj = await this.ParseTickerAsync( + ticker, + marketSymbol, + "askRate", + "bidRate", + "lastTradeRate", + "volume", + "quoteVolume", + "updatedAt", + TimestampType.Iso8601UTC + ); tickerList.Add(new KeyValuePair(marketSymbol, tickerObj)); } return tickerList; @@ -250,7 +298,10 @@ protected override async Task>> #endregion #region OrderBooks - protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 25) + protected override async Task OnGetOrderBookAsync( + string marketSymbol, + int maxCount = 25 + ) { // Bittrex API allowed values are [1, 25, 500], default is 25. if (maxCount > 100) @@ -270,18 +321,26 @@ protected override async Task OnGetOrderBookAsync(string mark maxCount = 1; } - JToken token = await MakeJsonRequestAsync("/markets/" + marketSymbol + "/orderbook" + "?depth=" + maxCount); + JToken token = await MakeJsonRequestAsync( + "/markets/" + marketSymbol + "/orderbook" + "?depth=" + maxCount + ); return token.ParseOrderBookFromJTokenDictionaries("ask", "bid", "rate", "quantity"); } /// Gets the deposit history for a symbol /// The symbol to check. May be null. /// Collection of ExchangeTransactions - protected override async Task> OnGetDepositHistoryAsync(string currencyNotNeeded = null) + protected override async Task> OnGetDepositHistoryAsync( + string currencyNotNeeded = null + ) { var transactions = new List(); string url = "/deposits/closed"; - JToken result = await MakeJsonRequestAsync(url, null, await GetNoncePayloadAsync()); + JToken result = await MakeJsonRequestAsync( + url, + null, + await GetNoncePayloadAsync() + ); foreach (JToken token in result) { var deposit = new ExchangeTransaction @@ -291,8 +350,7 @@ protected override async Task> OnGetDepositHist Currency = token["currencySymbol"].ToStringInvariant(), PaymentId = token["id"].ToStringInvariant(), BlockchainTxId = token["txId"].ToStringInvariant(), - Status = TransactionStatus.Complete,// As soon as it shows up in this list it is complete (verified manually) - + Status = TransactionStatus.Complete, // As soon as it shows up in this list it is complete (verified manually) }; DateTime.TryParse(token["updatedAt"].ToStringInvariant(), out DateTime timestamp); @@ -306,20 +364,39 @@ protected override async Task> OnGetDepositHist #endregion #region Trades - protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + protected override async Task OnGetHistoricalTradesAsync( + Func, bool> callback, + string marketSymbol, + 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."); + "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, int? limit = null) + protected override async Task> OnGetRecentTradesAsync( + string marketSymbol, + int? limit = null + ) { List trades = new List(); string baseUrl = "/markets/" + marketSymbol + "/trades"; JToken array = await MakeJsonRequestAsync(baseUrl); foreach (JToken token in array) { - trades.Add(token.ParseTrade("quantity", "rate", "takerSide", "executedAt", TimestampType.Iso8601UTC, "id")); + trades.Add( + token.ParseTrade( + "quantity", + "rate", + "takerSide", + "executedAt", + TimestampType.Iso8601UTC, + "id" + ) + ); } return trades; } @@ -328,9 +405,15 @@ protected override async Task> OnGetRecentTradesAsync #region AmountMethods protected override async Task> OnGetAmountsAsync() { - Dictionary currencies = new Dictionary(StringComparer.OrdinalIgnoreCase); + Dictionary currencies = new Dictionary( + StringComparer.OrdinalIgnoreCase + ); string url = "/balances"; - JToken array = await MakeJsonRequestAsync(url, null, await GetNoncePayloadAsync()); + JToken array = await MakeJsonRequestAsync( + url, + null, + await GetNoncePayloadAsync() + ); foreach (JToken token in array) { decimal amount = token["total"].ConvertInvariant(); @@ -342,11 +425,19 @@ protected override async Task> OnGetAmountsAsync() return currencies; } - protected override async Task> OnGetAmountsAvailableToTradeAsync() + protected override async Task< + Dictionary + > OnGetAmountsAvailableToTradeAsync() { - Dictionary currencies = new Dictionary(StringComparer.OrdinalIgnoreCase); + Dictionary currencies = new Dictionary( + StringComparer.OrdinalIgnoreCase + ); string url = "/balances"; - JToken array = await MakeJsonRequestAsync(url, null, await GetNoncePayloadAsync()); + JToken array = await MakeJsonRequestAsync( + url, + null, + await GetNoncePayloadAsync() + ); foreach (JToken token in array) { decimal amount = token["available"].ConvertInvariant(); @@ -358,39 +449,55 @@ protected override async Task> OnGetAmountsAvailable return currencies; } - protected override async Task> OnGetMarginAmountsAvailableToTradeAsync(bool includeZeroBalances) + protected override async Task< + Dictionary + > OnGetMarginAmountsAvailableToTradeAsync(bool includeZeroBalances) { Dictionary marginAmounts = new Dictionary(); string url = "/balances"; - JToken response = await MakeJsonRequestAsync(url, null, await GetNoncePayloadAsync()); + JToken response = await MakeJsonRequestAsync( + url, + null, + await GetNoncePayloadAsync() + ); var result = response - .Where(i => includeZeroBalances || i["available"].ConvertInvariant() != 0) - .ToDictionary(i => i["currencySymbol"].ToStringInvariant(), i => i["available"].ConvertInvariant()); + .Where(i => includeZeroBalances || i["available"].ConvertInvariant() != 0) + .ToDictionary( + i => i["currencySymbol"].ToStringInvariant(), + i => i["available"].ConvertInvariant() + ); return result; } #endregion #region OrderMethods - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + protected override async Task OnPlaceOrderAsync( + ExchangeOrderRequest order + ) { - decimal orderAmount = await ClampOrderQuantity(order.MarketSymbol, order.Amount); - if (order.Price == null) throw new ArgumentNullException(nameof(order.Price)); + if (order.Price == null) + throw new ArgumentNullException(nameof(order.Price)); decimal orderPrice = await ClampOrderPrice(order.MarketSymbol, order.Price.Value); string url = "/orders"; Dictionary orderParams = await GetNoncePayloadAsync(); orderParams.Add("marketSymbol", order.MarketSymbol); orderParams.Add("direction", order.IsBuy ? "BUY" : "SELL"); - orderParams.Add("type", order.OrderType == ExchangeSharp.OrderType.Market ? "MARKET" : "LIMIT"); + orderParams.Add( + "type", + order.OrderType == ExchangeSharp.OrderType.Market ? "MARKET" : "LIMIT" + ); orderParams.Add("quantity", orderAmount); if (order.OrderType == ExchangeSharp.OrderType.Limit) { orderParams.Add("limit", orderPrice); - if (order.IsPostOnly == true) orderParams.Add("timeInForce", "POST_ONLY_GOOD_TIL_CANCELLED"); // This option allows market makers to ensure that their orders are making it to the order book instead of matching with a pre-existing order. Note: If the order is not a maker order, you will return an error and the order will be cancelled - else orderParams.Add("timeInForce", "GOOD_TIL_CANCELLED"); + if (order.IsPostOnly == true) + orderParams.Add("timeInForce", "POST_ONLY_GOOD_TIL_CANCELLED"); // This option allows market makers to ensure that their orders are making it to the order book instead of matching with a pre-existing order. Note: If the order is not a maker order, you will return an error and the order will be cancelled + else + orderParams.Add("timeInForce", "GOOD_TIL_CANCELLED"); } foreach (KeyValuePair kv in order.ExtraParameters) @@ -412,25 +519,47 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd }; } - - protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnGetOrderDetailsAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { - if (isClientOrderId) throw new NotSupportedException("Querying by client order ID is not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature"); + if (isClientOrderId) + throw new NotSupportedException( + "Querying by client order ID is not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature" + ); if (string.IsNullOrWhiteSpace(orderId)) { return null; } string url = "/orders/" + orderId; - JToken result = await MakeJsonRequestAsync(url, null, await GetNoncePayloadAsync()); + JToken result = await MakeJsonRequestAsync( + url, + null, + await GetNoncePayloadAsync() + ); return ParseOrder(result); } - protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) + protected override async Task> OnGetOpenOrderDetailsAsync( + string marketSymbol = null + ) { List orders = new List(); - string url = "/orders/open" + (string.IsNullOrWhiteSpace(marketSymbol) ? string.Empty : "?marketSymbol=" + NormalizeMarketSymbol(marketSymbol)); - JToken result = await MakeJsonRequestAsync(url, null, await GetNoncePayloadAsync()); + string url = + "/orders/open" + + ( + string.IsNullOrWhiteSpace(marketSymbol) + ? string.Empty + : "?marketSymbol=" + NormalizeMarketSymbol(marketSymbol) + ); + JToken result = await MakeJsonRequestAsync( + url, + null, + await GetNoncePayloadAsync() + ); foreach (JToken token in result.Children()) { orders.Add(ParseOrder(token)); @@ -439,11 +568,23 @@ protected override async Task> OnGetOpenOrderDe return orders; } - protected override async Task> OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) + protected override async Task< + IEnumerable + > OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) { List orders = new List(); - string url = "/orders/closed" + (string.IsNullOrWhiteSpace(marketSymbol) ? string.Empty : "?marketSymbol=" + NormalizeMarketSymbol(marketSymbol)); - JToken result = await MakeJsonRequestAsync(url, null, await GetNoncePayloadAsync()); + string url = + "/orders/closed" + + ( + string.IsNullOrWhiteSpace(marketSymbol) + ? string.Empty + : "?marketSymbol=" + NormalizeMarketSymbol(marketSymbol) + ); + JToken result = await MakeJsonRequestAsync( + url, + null, + await GetNoncePayloadAsync() + ); foreach (JToken token in result.Children()) { ExchangeOrderResult order = ParseOrder(token); @@ -456,16 +597,34 @@ protected override async Task> OnGetCompletedOr return orders; } - protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnCancelOrderAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { - if (isClientOrderId) throw new NotSupportedException("Cancelling by client order ID is not supported in ExchangeSharp. Please submit a PR if you are interested in this feature"); - await MakeJsonRequestAsync("/orders/" + orderId, null, await GetNoncePayloadAsync(), "DELETE"); + if (isClientOrderId) + throw new NotSupportedException( + "Cancelling by client order ID is not supported in ExchangeSharp. Please submit a PR if you are interested in this feature" + ); + await MakeJsonRequestAsync( + "/orders/" + orderId, + null, + await GetNoncePayloadAsync(), + "DELETE" + ); } #endregion #region Candles - protected override async Task> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + protected override async Task> OnGetCandlesAsync( + string marketSymbol, + int periodSeconds, + DateTime? startDate = null, + DateTime? endDate = null, + int? limit = null + ) { if (limit != null) { @@ -480,17 +639,28 @@ protected override async Task> OnGetCandlesAsync(strin JToken result; - - result = await MakeJsonRequestAsync("/markets/" + marketSymbol + "/candles/" + periodString + "/recent"); + result = await MakeJsonRequestAsync( + "/markets/" + marketSymbol + "/candles/" + periodString + "/recent" + ); if (result is JArray array) { foreach (JToken jsonCandle in array) { //NOTE: Bittrex uses the term "BaseVolume" when referring to the QuoteCurrencyVolume - MarketCandle candle = this.ParseCandle(token: jsonCandle, marketSymbol: marketSymbol, periodSeconds: periodSeconds, - openKey: "open", highKey: "high", lowKey: "low", closeKey: "close", timestampKey: "startsAt", timestampType: TimestampType.Iso8601UTC, - baseVolumeKey: "volume", quoteVolumeKey: "quoteVolume"); + MarketCandle candle = this.ParseCandle( + token: jsonCandle, + marketSymbol: marketSymbol, + periodSeconds: periodSeconds, + openKey: "open", + highKey: "high", + lowKey: "low", + closeKey: "close", + timestampKey: "startsAt", + timestampType: TimestampType.Iso8601UTC, + baseVolumeKey: "volume", + quoteVolumeKey: "quoteVolume" + ); if (startDate != null && endDate != null) { if (candle.Timestamp >= startDate && candle.Timestamp <= endDate) @@ -498,7 +668,6 @@ protected override async Task> OnGetCandlesAsync(strin } else candles.Add(candle); - } } @@ -507,14 +676,16 @@ protected override async Task> OnGetCandlesAsync(strin #endregion #region Withdraw Methods - protected override async Task OnWithdrawAsync(ExchangeWithdrawalRequest withdrawalRequest) + protected override async Task OnWithdrawAsync( + ExchangeWithdrawalRequest withdrawalRequest + ) { /* - "currencySymbol": "string", - "quantity": "number (double)", - "cryptoAddress": "string", - "cryptoAddressTag": "string", - "clientWithdrawalId": "string (uuid)" + "currencySymbol": "string", + "quantity": "number (double)", + "cryptoAddress": "string", + "cryptoAddressTag": "string", + "clientWithdrawalId": "string (uuid)" */ string url = "/withdrawals"; @@ -525,24 +696,23 @@ protected override async Task OnWithdrawAsync(Exchan if (withdrawalRequest.AddressTag != null) payload.Add("cryptoAddressTag", withdrawalRequest.AddressTag); - JToken result = await MakeJsonRequestAsync(url, null, payload, "POST"); /* - { - "id": "string (uuid)", - "currencySymbol": "string", - "quantity": "number (double)", - "cryptoAddress": "string", - "cryptoAddressTag": "string", - "txCost": "number (double)", - "txId": "string", - "status": "string", - "createdAt": "string (date-time)", - "completedAt": "string (date-time)", - "clientWithdrawalId": "string (uuid)", - "accountId": "string (uuid)" - } + { + "id": "string (uuid)", + "currencySymbol": "string", + "quantity": "number (double)", + "cryptoAddress": "string", + "cryptoAddressTag": "string", + "txCost": "number (double)", + "txId": "string", + "status": "string", + "createdAt": "string (date-time)", + "completedAt": "string (date-time)", + "clientWithdrawalId": "string (uuid)", + "accountId": "string (uuid)" + } */ ExchangeWithdrawalResponse withdrawalResponse = new ExchangeWithdrawalResponse { @@ -554,25 +724,33 @@ protected override async Task OnWithdrawAsync(Exchan return withdrawalResponse; } - protected override async Task> OnGetWithdrawHistoryAsync(string currency) + protected override async Task> OnGetWithdrawHistoryAsync( + string currency + ) { - string url = $"/withdrawals/closed{(string.IsNullOrWhiteSpace(currency) ? string.Empty : $"?currencySymbol={currency}")}"; - JToken result = await MakeJsonRequestAsync(url, null, await GetNoncePayloadAsync()); - - var transactions = result.Select(t => new ExchangeTransaction - { - Amount = t["quantity"].ConvertInvariant(), - Address = t["cryptoAddress"].ToStringInvariant(), - AddressTag = t["cryptoAddressTag"].ToStringInvariant(), - TxFee = t["txCost"].ConvertInvariant(), - Currency = t["currencySymbol"].ToStringInvariant(), - PaymentId = t["id"].ToStringInvariant(), - BlockchainTxId = t["txId"].ToStringInvariant(), - Timestamp = DateTime.Parse(t["createdAt"].ToStringInvariant()), - Status = ToStatus(t["status"].ToStringInvariant()) - - }); - + string url = + $"/withdrawals/closed{(string.IsNullOrWhiteSpace(currency) ? string.Empty : $"?currencySymbol={currency}")}"; + JToken result = await MakeJsonRequestAsync( + url, + null, + await GetNoncePayloadAsync() + ); + + var transactions = result.Select( + t => + new ExchangeTransaction + { + Amount = t["quantity"].ConvertInvariant(), + Address = t["cryptoAddress"].ToStringInvariant(), + AddressTag = t["cryptoAddressTag"].ToStringInvariant(), + TxFee = t["txCost"].ConvertInvariant(), + Currency = t["currencySymbol"].ToStringInvariant(), + PaymentId = t["id"].ToStringInvariant(), + BlockchainTxId = t["txId"].ToStringInvariant(), + Timestamp = DateTime.Parse(t["createdAt"].ToStringInvariant()), + Status = ToStatus(t["status"].ToStringInvariant()) + } + ); return transactions; } @@ -610,7 +788,10 @@ private TransactionStatus ToStatus(string status) /// /// Deposit address details (including tag if applicable, such as with XRP) /// - protected override async Task OnGetDepositAddressAsync(string currency, bool forceRegenerate = false) + protected override async Task OnGetDepositAddressAsync( + string currency, + bool forceRegenerate = false + ) { if (forceRegenerate) { @@ -618,14 +799,18 @@ protected override async Task OnGetDepositAddressAsync(s } string url = "/addresses/" + NormalizeMarketSymbol(currency); - JToken result = await MakeJsonRequestAsync(url, null, await GetNoncePayloadAsync()); + JToken result = await MakeJsonRequestAsync( + url, + null, + await GetNoncePayloadAsync() + ); /* { - "status": "string", - "currencySymbol": "string", - "cryptoAddress": "string", - "cryptoAddressTag": "string" + "status": "string", + "currencySymbol": "string", + "cryptoAddress": "string", + "cryptoAddressTag": "string" } */ ExchangeDepositDetails depositDetails = new ExchangeDepositDetails @@ -637,8 +822,10 @@ protected override async Task OnGetDepositAddressAsync(s return depositDetails; } #endregion - } - public partial class ExchangeName { public const string Bittrex = "Bittrex"; } + public partial class ExchangeName + { + public const string Bittrex = "Bittrex"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI_WebSocket.cs b/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI_WebSocket.cs index fe712c02f..581e70a54 100644 --- a/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI_WebSocket.cs +++ b/src/ExchangeSharp/API/Exchanges/Bittrex/ExchangeBittrexAPI_WebSocket.cs @@ -20,11 +20,10 @@ The above copyright notice and this permission notice shall be included in all c using System.Linq; using System.Net; using System.Net.WebSockets; +using System.Security.Cryptography; using System.Text; -using System.Threading.Tasks; using System.Threading; -using System.Security.Cryptography; - +using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -39,7 +38,8 @@ public partial class ExchangeBittrexAPI /// public sealed class BittrexWebSocketManager : SignalrManager { - public BittrexWebSocketManager() : base("https://socket.bittrex.com/signalr", "c2") + public BittrexWebSocketManager() + : base("https://socket.bittrex.com/signalr", "c2") { FunctionNamesToFullNames["uS"] = "SubscribeToSummaryDeltas"; FunctionNamesToFullNames["uE"] = "SubscribeToExchangeDeltas"; @@ -51,9 +51,13 @@ public BittrexWebSocketManager() : base("https://socket.bittrex.com/signalr", "c /// Callback /// Symbols /// IDisposable to close the socket - public async Task SubscribeToSummaryDeltasAsync(Func callback, params string[] marketSymbols) + public async Task SubscribeToSummaryDeltasAsync( + Func callback, + params string[] marketSymbols + ) { - SignalrManager.SignalrSocketConnection conn = new SignalrManager.SignalrSocketConnection(this); + SignalrManager.SignalrSocketConnection conn = + new SignalrManager.SignalrSocketConnection(this); await conn.OpenAsync("uS", callback); return conn; } @@ -64,13 +68,19 @@ public async Task SubscribeToSummaryDeltasAsync(Func c /// Callback /// The market symbols to subscribe to /// IDisposable to close the socket - public async Task SubscribeToExchangeDeltasAsync(Func callback, params string[] marketSymbols) + public async Task SubscribeToExchangeDeltasAsync( + Func callback, + params string[] marketSymbols + ) { - SignalrManager.SignalrSocketConnection conn = new SignalrManager.SignalrSocketConnection(this); + SignalrManager.SignalrSocketConnection conn = + new SignalrManager.SignalrSocketConnection(this); List paramList = new List(); foreach (string marketSymbol in marketSymbols) { - paramList.Add(new object[] { ReverseMarketNameForWS((marketSymbol).ToStringInvariant()) }); + paramList.Add( + new object[] { ReverseMarketNameForWS((marketSymbol).ToStringInvariant()) } + ); } await conn.OpenAsync("uE", callback, 0, paramList.ToArray()); return conn; @@ -85,7 +95,10 @@ public static string ReverseMarketNameForWS(string WebSocketFeedMarketName) return (pair[1] + '-' + pair[0]).ToUpperInvariant(); } - protected override async Task OnGetTickersWebSocketAsync(Action>> callback, params string[] marketSymbols) + protected override async Task OnGetTickersWebSocketAsync( + Action>> callback, + params string[] marketSymbols + ) { HashSet filter = new HashSet(); foreach (string marketSymbol in marketSymbols) @@ -97,32 +110,34 @@ async Task innerCallback(string json) #region sample json /* - { - Nonce : int, - Deltas : - [ - { - MarketName : string, - High : decimal, - Low : decimal, - Volume : decimal, - Last : decimal, - BaseVolume : decimal, - TimeStamp : date, - Bid : decimal, - Ask : decimal, - OpenBuyOrders : int, - OpenSellOrders : int, - PrevDay : decimal, - Created : date - } - ] - } - */ + { + Nonce : int, + Deltas : + [ + { + MarketName : string, + High : decimal, + Low : decimal, + Volume : decimal, + Last : decimal, + BaseVolume : decimal, + TimeStamp : date, + Bid : decimal, + Ask : decimal, + OpenBuyOrders : int, + OpenSellOrders : int, + PrevDay : decimal, + Created : date + } + ] + } + */ #endregion sample json - var freshTickers = new Dictionary(StringComparer.OrdinalIgnoreCase); + var freshTickers = new Dictionary( + StringComparer.OrdinalIgnoreCase + ); JToken token = JToken.Parse(json); token = token["D"]; foreach (JToken ticker in token) @@ -132,13 +147,17 @@ async Task innerCallback(string json) { continue; } - var (baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync(marketName); + var (baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync( + marketName + ); decimal last = ticker["l"].ConvertInvariant(); decimal ask = ticker["A"].ConvertInvariant(); decimal bid = ticker["B"].ConvertInvariant(); decimal baseCurrencyVolume = ticker["V"].ConvertInvariant(); - decimal quoteCurrencyVolume = ticker["m"].ConvertInvariant();//NOTE: Bittrex uses the term BaseVolume when referring to QuoteCurrencyVolume - DateTime timestamp = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(ticker["T"].ConvertInvariant()); + decimal quoteCurrencyVolume = ticker["m"].ConvertInvariant(); //NOTE: Bittrex uses the term BaseVolume when referring to QuoteCurrencyVolume + DateTime timestamp = CryptoUtility.UnixTimeStampToDateTimeMilliseconds( + ticker["T"].ConvertInvariant() + ); var t = new ExchangeTicker { Exchange = Name, @@ -160,14 +179,16 @@ async Task innerCallback(string json) } callback(freshTickers); } - return await new BittrexWebSocketManager().SubscribeToSummaryDeltasAsync(innerCallback, marketSymbols); + return await new BittrexWebSocketManager().SubscribeToSummaryDeltasAsync( + innerCallback, + marketSymbols + ); } - protected override async Task OnGetDeltaOrderBookWebSocketAsync - ( - Action callback, - int maxCount = 20, - params string[] marketSymbols + protected override async Task OnGetDeltaOrderBookWebSocketAsync( + Action callback, + int maxCount = 20, + params string[] marketSymbols ) { if (marketSymbols == null || marketSymbols.Length == 0) @@ -179,41 +200,43 @@ Task innerCallback(string json) #region sample json /* - { - MarketName : string, - Nonce : int, - Buys: - [ - { - Type : int, - Rate : decimal, - Quantity : decimal - } - ], - Sells: - [ - { - Type : int, - Rate : decimal, - Quantity : decimal - } - ], - Fills: - [ - { - FillId : int, - OrderType : string, - Rate : decimal, - Quantity : decimal, - TimeStamp : date - } - ] - } - */ + { + MarketName : string, + Nonce : int, + Buys: + [ + { + Type : int, + Rate : decimal, + Quantity : decimal + } + ], + Sells: + [ + { + Type : int, + Rate : decimal, + Quantity : decimal + } + ], + Fills: + [ + { + FillId : int, + OrderType : string, + Rate : decimal, + Quantity : decimal, + TimeStamp : date + } + ] + } + */ #endregion sample json - var ordersUpdates = JsonConvert.DeserializeObject(json); + var ordersUpdates = JsonConvert.DeserializeObject( + json + ); var book = new ExchangeOrderBook(); foreach (BittrexStreamOrderBookUpdateEntry ask in ordersUpdates.Sells) { @@ -225,17 +248,24 @@ Task innerCallback(string json) var depth = new ExchangeOrderPrice { Price = bid.Rate, Amount = bid.Quantity }; book.Bids[depth.Price] = depth; } - book.MarketSymbol = ReverseMarketNameForWS(ordersUpdates.MarketName).ToUpperInvariant(); + book.MarketSymbol = ReverseMarketNameForWS(ordersUpdates.MarketName) + .ToUpperInvariant(); book.SequenceId = ordersUpdates.Nonce; book.LastUpdatedUtc = DateTime.UtcNow; callback(book); return Task.CompletedTask; } - return await new BittrexWebSocketManager().SubscribeToExchangeDeltasAsync(innerCallback, marketSymbols); + return await new BittrexWebSocketManager().SubscribeToExchangeDeltasAsync( + innerCallback, + marketSymbols + ); } - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + protected override async Task OnGetTradesWebSocketAsync( + Func, Task> callback, + params string[] marketSymbols + ) { if (marketSymbols == null || marketSymbols.Length == 0) { @@ -243,25 +273,34 @@ protected override async Task OnGetTradesWebSocketAsync(Func(json); + var ordersUpdates = JsonConvert.DeserializeObject( + json + ); foreach (var fill in ordersUpdates.Fills) { - await callback(new KeyValuePair(ReverseMarketNameForWS(ordersUpdates.MarketName), new ExchangeTrade() - { - Amount = fill.Quantity, - // Bittrex doesn't currently send out FillId on socket.bittrex.com, only beta.bittrex.com, but this will be ready when they start - // https://github.com/Bittrex/beta/issues/2, https://github.com/Bittrex/bittrex.github.io/issues/3 - // You can always change the URL on the top of the file to beta.bittrex.com to start getting FillIds now - Id = fill.FillId.ToStringInvariant(), - IsBuy = fill.OrderSide == OrderSide.Buy, - Price = fill.Rate, - Timestamp = fill.Timestamp - })); + await callback( + new KeyValuePair( + ReverseMarketNameForWS(ordersUpdates.MarketName), + new ExchangeTrade() + { + Amount = fill.Quantity, + // Bittrex doesn't currently send out FillId on socket.bittrex.com, only beta.bittrex.com, but this will be ready when they start + // https://github.com/Bittrex/beta/issues/2, https://github.com/Bittrex/bittrex.github.io/issues/3 + // You can always change the URL on the top of the file to beta.bittrex.com to start getting FillIds now + Id = fill.FillId.ToStringInvariant(), + IsBuy = fill.OrderSide == OrderSide.Buy, + Price = fill.Rate, + Timestamp = fill.Timestamp + } + ) + ); } } - return await new BittrexWebSocketManager().SubscribeToExchangeDeltasAsync(innerCallback, marketSymbols); + return await new BittrexWebSocketManager().SubscribeToExchangeDeltasAsync( + innerCallback, + marketSymbols + ); } - #endif protected override void OnDispose() diff --git a/src/ExchangeSharp/API/Exchanges/Bittrex/Models/BittrexModel.cs b/src/ExchangeSharp/API/Exchanges/Bittrex/Models/BittrexModel.cs index c52636c92..a86971594 100644 --- a/src/ExchangeSharp/API/Exchanges/Bittrex/Models/BittrexModel.cs +++ b/src/ExchangeSharp/API/Exchanges/Bittrex/Models/BittrexModel.cs @@ -18,252 +18,254 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp { - public partial class ExchangeBittrexAPI - { - /// Order book type - internal enum OrderBookType - { - /// Only show buy orders - Buy, + public partial class ExchangeBittrexAPI + { + /// Order book type + internal enum OrderBookType + { + /// Only show buy orders + Buy, - /// Only show sell orders - Sell, + /// Only show sell orders + Sell, - /// Show all orders - Both - } + /// Show all orders + Both + } - /// Whether the order is partially or fully filled - internal enum FillType - { - Fill, + /// Whether the order is partially or fully filled + internal enum FillType + { + Fill, - PartialFill - } + PartialFill + } - internal enum OrderSide - { - Buy, + internal enum OrderSide + { + Buy, - Sell - } + Sell + } - internal enum OrderType - { - Limit, + internal enum OrderType + { + Limit, - Market - } + Market + } - internal enum OrderSideExtended - { - LimitBuy, + internal enum OrderSideExtended + { + LimitBuy, - LimitSell - } + LimitSell + } - internal enum TickInterval - { - OneMinute, + internal enum TickInterval + { + OneMinute, - FiveMinutes, + FiveMinutes, - HalfHour, + HalfHour, - OneHour, + OneHour, - OneDay - } + OneDay + } - internal enum TimeInEffect - { - GoodTillCancelled, + internal enum TimeInEffect + { + GoodTillCancelled, - ImmediateOrCancel - } + ImmediateOrCancel + } - internal enum ConditionType - { - None, + internal enum ConditionType + { + None, - GreaterThan, + GreaterThan, - LessThan, + LessThan, - StopLossFixed, + StopLossFixed, - StopLossPercentage - } + StopLossPercentage + } - internal enum OrderUpdateType - { - Open, + internal enum OrderUpdateType + { + Open, - PartialFill, + PartialFill, - Fill, + Fill, - Cancel - } + Cancel + } - internal class BittrexStreamOrderBookUpdateEntry : BittrexStreamOrderBookEntry - { - /// how to handle data (used by stream) - [JsonProperty("TY")] - public OrderBookEntryType Type { get; set; } - } + internal class BittrexStreamOrderBookUpdateEntry : BittrexStreamOrderBookEntry + { + /// how to handle data (used by stream) + [JsonProperty("TY")] + public OrderBookEntryType Type { get; set; } + } - internal class BittrexStreamOrderBookEntry - { - /// Total quantity of order at this price - [JsonProperty("Q")] - public decimal Quantity { get; set; } + internal class BittrexStreamOrderBookEntry + { + /// Total quantity of order at this price + [JsonProperty("Q")] + public decimal Quantity { get; set; } - /// Price of the orders - [JsonProperty("R")] - public decimal Rate { get; set; } - } + /// Price of the orders + [JsonProperty("R")] + public decimal Rate { get; set; } + } - internal enum OrderBookEntryType - { - NewEntry = 0, + internal enum OrderBookEntryType + { + NewEntry = 0, - RemoveEntry = 1, + RemoveEntry = 1, - UpdateEntry = 2 - } + UpdateEntry = 2 + } - internal class BittrexStreamUpdateExchangeState - { - [JsonProperty("N")] - public long Nonce { get; set; } + internal class BittrexStreamUpdateExchangeState + { + [JsonProperty("N")] + public long Nonce { get; set; } - /// Name of the market - [JsonProperty("M")] - public string MarketName { get; set; } + /// Name of the market + [JsonProperty("M")] + public string MarketName { get; set; } - /// Buys in the order book - [JsonProperty("Z")] - public List Buys { get; set; } + /// Buys in the order book + [JsonProperty("Z")] + public List Buys { get; set; } - /// Sells in the order book - [JsonProperty("S")] - public List Sells { get; set; } + /// Sells in the order book + [JsonProperty("S")] + public List Sells { get; set; } - /// Market history - [JsonProperty("f")] - public List Fills { get; set; } - } + /// Market history + [JsonProperty("f")] + public List Fills { get; set; } + } - internal class BittrexStreamFill - { - /// Timestamp of the fill - [JsonProperty("T")] - [JsonConverter(typeof(TimestampConverter))] - public DateTime Timestamp { get; set; } + internal class BittrexStreamFill + { + /// Timestamp of the fill + [JsonProperty("T")] + [JsonConverter(typeof(TimestampConverter))] + public DateTime Timestamp { get; set; } - /// Quantity of the fill - [JsonProperty("Q")] - public decimal Quantity { get; set; } - - /// Rate of the fill - [JsonProperty("R")] - public decimal Rate { get; set; } - - /// The side of the order - [JsonConverter(typeof(OrderSideConverter))] - [JsonProperty("OT")] - public OrderSide OrderSide { get; set; } - - /// Rate of the fill - [JsonProperty("FI")] - public long FillId { get; set; } - } - - internal class BittrexStreamQueryExchangeState - { - [JsonProperty("N")] - public long Nonce { get; set; } - - /// Name of the market - [JsonProperty("M")] - public string MarketName { get; set; } - - /// Buys in the order book - [JsonProperty("Z")] - public List Buys { get; set; } - - /// Sells in the order book - [JsonProperty("S")] - public List Sells { get; set; } - - /// Market history - [JsonProperty("f")] - public List Fills { get; set; } - } - - internal class OrderSideConverter : BaseConverter - { - public OrderSideConverter() - : this(true) - { - } - - public OrderSideConverter(bool quotes) - : base(quotes) - { - } - - protected override Dictionary Mapping => new Dictionary { { OrderSide.Buy, "BUY" }, { OrderSide.Sell, "SELL" } }; - } - - internal class BittrexStreamMarketHistory - { - /// The order id - [JsonProperty("I")] - public long Id { get; set; } - - /// Timestamp of the order - [JsonConverter(typeof(TimestampConverter))] - [JsonProperty("T")] - public DateTime Timestamp { get; set; } - - /// Quantity of the order - [JsonProperty("Q")] - public decimal Quantity { get; set; } - - /// Price of the order - [JsonProperty("P")] - public decimal Price { get; set; } - - /// Total price of the order - [JsonProperty("t")] - public decimal Total { get; set; } - - /// Whether the order was fully filled - [JsonConverter(typeof(FillTypeConverter))] - [JsonProperty("F")] - public FillType FillType { get; set; } - - /// The side of the order - [JsonConverter(typeof(OrderSideConverter))] - [JsonProperty("OT")] - public OrderSide OrderSide { get; set; } - - public class FillTypeConverter : BaseConverter - { - public FillTypeConverter() - : this(true) - { - } - - public FillTypeConverter(bool quotes) - : base(quotes) - { - } - - protected override Dictionary Mapping => new Dictionary { { FillType.Fill, "FILL" }, { FillType.PartialFill, "PARTIAL_FILL" } }; - } - } - } + /// Quantity of the fill + [JsonProperty("Q")] + public decimal Quantity { get; set; } + + /// Rate of the fill + [JsonProperty("R")] + public decimal Rate { get; set; } + + /// The side of the order + [JsonConverter(typeof(OrderSideConverter))] + [JsonProperty("OT")] + public OrderSide OrderSide { get; set; } + + /// Rate of the fill + [JsonProperty("FI")] + public long FillId { get; set; } + } + + internal class BittrexStreamQueryExchangeState + { + [JsonProperty("N")] + public long Nonce { get; set; } + + /// Name of the market + [JsonProperty("M")] + public string MarketName { get; set; } + + /// Buys in the order book + [JsonProperty("Z")] + public List Buys { get; set; } + + /// Sells in the order book + [JsonProperty("S")] + public List Sells { get; set; } + + /// Market history + [JsonProperty("f")] + public List Fills { get; set; } + } + + internal class OrderSideConverter : BaseConverter + { + public OrderSideConverter() + : this(true) { } + + public OrderSideConverter(bool quotes) + : base(quotes) { } + + protected override Dictionary Mapping => + new Dictionary + { + { OrderSide.Buy, "BUY" }, + { OrderSide.Sell, "SELL" } + }; + } + + internal class BittrexStreamMarketHistory + { + /// The order id + [JsonProperty("I")] + public long Id { get; set; } + + /// Timestamp of the order + [JsonConverter(typeof(TimestampConverter))] + [JsonProperty("T")] + public DateTime Timestamp { get; set; } + + /// Quantity of the order + [JsonProperty("Q")] + public decimal Quantity { get; set; } + + /// Price of the order + [JsonProperty("P")] + public decimal Price { get; set; } + + /// Total price of the order + [JsonProperty("t")] + public decimal Total { get; set; } + + /// Whether the order was fully filled + [JsonConverter(typeof(FillTypeConverter))] + [JsonProperty("F")] + public FillType FillType { get; set; } + + /// The side of the order + [JsonConverter(typeof(OrderSideConverter))] + [JsonProperty("OT")] + public OrderSide OrderSide { get; set; } + + public class FillTypeConverter : BaseConverter + { + public FillTypeConverter() + : this(true) { } + + public FillTypeConverter(bool quotes) + : base(quotes) { } + + protected override Dictionary Mapping => + new Dictionary + { + { FillType.Fill, "FILL" }, + { FillType.PartialFill, "PARTIAL_FILL" } + }; + } + } + } } diff --git a/src/ExchangeSharp/API/Exchanges/Bleutrade/ExchangeBleutradeAPI.cs b/src/ExchangeSharp/API/Exchanges/Bleutrade/ExchangeBleutradeAPI.cs index 639d4ccff..f3c5d3b00 100644 --- a/src/ExchangeSharp/API/Exchanges/Bleutrade/ExchangeBleutradeAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Bleutrade/ExchangeBleutradeAPI.cs @@ -31,347 +31,533 @@ private ExchangeBleutradeAPI() { NonceStyle = NonceStyle.UnixMillisecondsString; MarketSymbolSeparator = "_"; - ExchangeGlobalCurrencyReplacements["BCC"] = "BCH"; + ExchangeGlobalCurrencyReplacements["BCC"] = "BCH"; } - #region ProcessRequest - - protected override Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) - { - if (CanMakeAuthenticatedRequest(payload)) - { - request.AddHeader("apisign", CryptoUtility.SHA512Sign(request.RequestUri.ToString(), PrivateApiKey.ToUnsecureString()).ToLowerInvariant()); - } - return base.ProcessRequestAsync(request, payload); - } - - protected override Uri ProcessRequestUrl(UriBuilder url, Dictionary payload, string method) - { - if (CanMakeAuthenticatedRequest(payload)) - { - // payload is ignored, except for the nonce which is added to the url query - var query = (url.Query ?? string.Empty).Trim('?', '&'); - url.Query = "apikey=" + PublicApiKey.ToUnsecureString() + "&nonce=" + payload["nonce"].ToStringInvariant() + (query.Length != 0 ? "&" + query : string.Empty); - } - return url.Uri; - } - - #endregion - - #region Public APIs - - protected override async Task> OnGetCurrenciesAsync() - { - var currencies = new Dictionary(StringComparer.OrdinalIgnoreCase); - //{ "success" : true,"message" : "", "result" : [{"Currency" : "BTC","CurrencyLong" : "Bitcoin","MinConfirmation" : 2,"TxFee" : 0.00080000,"IsActive" : true, "CoinType" : "BITCOIN","MaintenanceMode" : false}, ... - JToken result = await MakeJsonRequestAsync("/public/getcurrencies", null, null); - foreach (JToken token in result) - { - bool isMaintenanceMode = token["MaintenanceMode"].ConvertInvariant(); - var coin = new ExchangeCurrency - { - CoinType = token["CoinType"].ToStringInvariant(), - FullName = token["CurrencyLong"].ToStringInvariant(), - DepositEnabled = !isMaintenanceMode, - WithdrawalEnabled = !isMaintenanceMode, - MinConfirmations = token["MinConfirmation"].ConvertInvariant(), - Name = token["Currency"].ToStringUpperInvariant(), - Notes = token["Notice"].ToStringInvariant(), - TxFee = token["TxFee"].ConvertInvariant(), - }; - currencies[coin.Name] = coin; - } - return currencies; - } - - protected override async Task> OnGetMarketSymbolsAsync() - { - List symbols = new List(); - JToken result = await MakeJsonRequestAsync("/public/getmarkets", null, null); - foreach (var market in result) symbols.Add(market["MarketName"].ToStringInvariant()); - return symbols; - } - - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() - { - List markets = new List(); - // "result" : [{"MarketCurrency" : "DOGE","BaseCurrency" : "BTC","MarketCurrencyLong" : "Dogecoin","BaseCurrencyLong" : "Bitcoin", "MinTradeSize" : 0.10000000, "MarketName" : "DOGE_BTC", "IsActive" : true, }, ... - JToken result = await MakeJsonRequestAsync("/public/getmarkets", null, null); - foreach (JToken token in result) - { - markets.Add(new ExchangeMarket() - { - //NOTE: Bleutrade is another weird one that calls the QuoteCurrency the "BaseCurrency" and the BaseCurrency the "MarketCurrency". - QuoteCurrency = token["BaseCurrency"].ToStringInvariant(), - BaseCurrency = token["MarketCurrency"].ToStringInvariant(), - MarketSymbol = token["MarketName"].ToStringInvariant(), - IsActive = token["IsActive"].ToStringInvariant().Equals("true"), - MinTradeSize = token["MinTradeSize"].ConvertInvariant(), - }); - } - return markets; - } - - protected override async Task OnGetTickerAsync(string marketSymbol) - { - JToken result = await MakeJsonRequestAsync("/public/getmarketsummary?market=" + marketSymbol); - return this.ParseTicker(result, marketSymbol, "Ask", "Bid", "Last", "Volume", "BaseVolume", "Timestamp", TimestampType.Iso8601); - } - - protected override async Task>> OnGetTickersAsync() - { - List> tickers = new List>(); - // "result" : [{"MarketCurrency" : "Ethereum","BaseCurrency" : "Bitcoin","MarketName" : "ETH_BTC","PrevDay" : 0.00095000,"High" : 0.00105000,"Low" : 0.00086000, "Last" : 0.00101977, "Average" : 0.00103455, "Volume" : 2450.97496015, "BaseVolume" : 2.40781647, "TimeStamp" : "2014-07-29 11:19:30", "Bid" : 0.00100000, "Ask" : 0.00101977, "IsActive" : true }, ... ] - JToken result = await MakeJsonRequestAsync("/public/getmarketsummaries"); - foreach (JToken token in result) - { - var ticker = this.ParseTicker(token, token["MarketName"].ToStringInvariant(), "Ask", "Bid", "Last", "Volume", "BaseVolume", "Timestamp", TimestampType.Iso8601); - tickers.Add(new KeyValuePair(token["MarketName"].ToStringInvariant(), ticker)); - } - return tickers; - } - - protected override async Task> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) - { - List candles = new List(); - string periodString = PeriodSecondsToString(periodSeconds); - limit = limit ?? (limit > 2160 ? 2160 : limit); - endDate = endDate ?? CryptoUtility.UtcNow.AddMinutes(1.0); - startDate = startDate ?? endDate.Value.Subtract(TimeSpan.FromDays(1.0)); - - //market period(15m, 20m, 30m, 1h, 2h, 3h, 4h, 6h, 8h, 12h, 1d) count(default: 1000, max: 999999) lasthours(default: 24, max: 2160) - //"result":[{"TimeStamp":"2014-07-31 10:15:00","Open":"0.00000048","High":"0.00000050","Low":"0.00000048","Close":"0.00000049","Volume":"594804.73036048","BaseVolume":"0.11510368" }, ... - JToken result = await MakeJsonRequestAsync("/public/getcandles?market=" + marketSymbol + "&period=" + periodString + (limit == null ? string.Empty : "&lasthours=" + limit)); - foreach (JToken jsonCandle in result) - { - //NOTE: Bleutrade uses the term "BaseVolume" when referring to the QuoteCurrencyVolume - MarketCandle candle = this.ParseCandle(jsonCandle, marketSymbol, periodSeconds, "Open", "High", "Low", "Close", "Timestamp", TimestampType.Iso8601, "Volume", "BaseVolume"); - if (candle.Timestamp >= startDate && candle.Timestamp <= endDate) - { - candles.Add(candle); - } - } - return candles; - } - - - protected override async Task> OnGetRecentTradesAsync(string marketSymbol) - { - List trades = new List(); - //"result" : [{ "TimeStamp" : "2014-07-29 18:08:00","Quantity" : 654971.69417461,"Price" : 0.00000055,"Total" : 0.360234432,"OrderType" : "BUY"}, ... ] - JToken result = await MakeJsonRequestAsync("/public/getmarkethistory?market=" + marketSymbol); - foreach (JToken token in result) trades.Add(ParseTrade(token)); - return trades; - } - - protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null) - { - List trades = new List(); - // TODO: Not directly supported so the best we can do is get their Max 200 and check the timestamp if necessary - JToken result = await MakeJsonRequestAsync("/public/getmarkethistory?market=" + marketSymbol + "&count=200"); - foreach (JToken token in result) - { - ExchangeTrade trade = ParseTrade(token); - if (startDate == null || trade.Timestamp >= startDate) - { - trades.Add(trade); - } - } - if (trades.Count != 0) - { - callback(trades); - } - } - - protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) - { - //"result" : { "buy" : [{"Quantity" : 4.99400000,"Rate" : 3.00650900}, {"Quantity" : 50.00000000, "Rate" : 3.50000000 } ] ... - JToken token = await MakeJsonRequestAsync("/public/getorderbook?market=" + marketSymbol + "&type=ALL&depth=" + maxCount); - return ExchangeAPIExtensions.ParseOrderBookFromJTokenDictionaries(token, "sell", "buy", "Rate", "Quantity", maxCount: maxCount); - } - - #endregion - - #region Private APIs - - protected override async Task> OnGetAmountsAsync() - { - Dictionary amounts = new Dictionary(); - // "result" : [{"Currency" : "DOGE","Balance" : 0.00000000,"Available" : 0.00000000,"Pending" : 0.00000000,"CryptoAddress" : "DBSwFELQiVrwxFtyHpVHbgVrNJXwb3hoXL", "IsActive" : true}, ... - JToken result = await MakeJsonRequestAsync("/account/getbalances", null, await GetNoncePayloadAsync()); - foreach (JToken token in result) - { - decimal amount = result["Balance"].ConvertInvariant(); - if (amount > 0) amounts[token["Currency"].ToStringInvariant()] = amount; - } - return amounts; - } + #region ProcessRequest - protected override async Task> OnGetAmountsAvailableToTradeAsync() - { - Dictionary amounts = new Dictionary(); - // "result" : [{"Currency" : "DOGE","Balance" : 0.00000000,"Available" : 0.00000000,"Pending" : 0.00000000,"CryptoAddress" : "DBSwFELQiVrwxFtyHpVHbgVrNJXwb3hoXL", "IsActive" : true}, ... - JToken result = await MakeJsonRequestAsync("/account/getbalances", null, await GetNoncePayloadAsync()); - foreach (JToken token in result) - { - decimal amount = result["Available"].ConvertInvariant(); - if (amount > 0) amounts[token["Currency"].ToStringInvariant()] = amount; - } - return amounts; - } + protected override Task ProcessRequestAsync( + IHttpWebRequest request, + Dictionary payload + ) + { + if (CanMakeAuthenticatedRequest(payload)) + { + request.AddHeader( + "apisign", + CryptoUtility + .SHA512Sign(request.RequestUri.ToString(), PrivateApiKey.ToUnsecureString()) + .ToLowerInvariant() + ); + } + return base.ProcessRequestAsync(request, payload); + } - protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null) - { - // "result" : { "OrderId" : "65489","Exchange" : "LTC_BTC", "Type" : "BUY", "Quantity" : 20.00000000, "QuantityRemaining" : 5.00000000, "QuantityBaseTraded" : "0.16549400", "Price" : 0.01268311, "Status" : "OPEN", "Created" : "2014-08-03 13:55:20", "Comments" : "My optional comment, eg function id #123" } - JToken result = await MakeJsonRequestAsync("/account/getorder?orderid=" + orderId, null, await GetNoncePayloadAsync()); - return ParseOrder(result); - } + protected override Uri ProcessRequestUrl( + UriBuilder url, + Dictionary payload, + string method + ) + { + if (CanMakeAuthenticatedRequest(payload)) + { + // payload is ignored, except for the nonce which is added to the url query + var query = (url.Query ?? string.Empty).Trim('?', '&'); + url.Query = + "apikey=" + + PublicApiKey.ToUnsecureString() + + "&nonce=" + + payload["nonce"].ToStringInvariant() + + (query.Length != 0 ? "&" + query : string.Empty); + } + return url.Uri; + } - protected override async Task> OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) - { - List orders = new List(); - JToken result = await MakeJsonRequestAsync("/account/getorders?market=" + (string.IsNullOrEmpty(marketSymbol) ? "ALL" : marketSymbol) + "&orderstatus=OK&ordertype=ALL", null, await GetNoncePayloadAsync()); - foreach (JToken token in result) - { - ExchangeOrderResult order = ParseOrder(token); - if (afterDate != null) { if (order.OrderDate > afterDate) orders.Add(order); } - else orders.Add(order); - } - return orders; - } + #endregion - protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) - { - List orders = new List(); - JToken result = await MakeJsonRequestAsync("/market/getopenorders", null, await GetNoncePayloadAsync()); - foreach (JToken token in result) orders.Add(ParseOrder(token)); - return orders; - } + #region Public APIs - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + protected override async Task< + IReadOnlyDictionary + > OnGetCurrenciesAsync() + { + var currencies = new Dictionary( + StringComparer.OrdinalIgnoreCase + ); + //{ "success" : true,"message" : "", "result" : [{"Currency" : "BTC","CurrencyLong" : "Bitcoin","MinConfirmation" : 2,"TxFee" : 0.00080000,"IsActive" : true, "CoinType" : "BITCOIN","MaintenanceMode" : false}, ... + JToken result = await MakeJsonRequestAsync("/public/getcurrencies", null, null); + foreach (JToken token in result) + { + bool isMaintenanceMode = token["MaintenanceMode"].ConvertInvariant(); + var coin = new ExchangeCurrency { - ExchangeOrderResult result = new ExchangeOrderResult() { Result = ExchangeAPIOrderResult.Error }; - var payload = await GetNoncePayloadAsync(); - order.ExtraParameters.CopyTo(payload); - - // Only limit order is supported - no indication on how it is filled - JToken token = await MakeJsonRequestAsync((order.IsBuy ? "/market/buylimit?" : "market/selllimit?") + "market=" + order.MarketSymbol + - "&rate=" + order.Price.ToStringInvariant() + "&quantity=" + order.RoundAmount().ToStringInvariant(), null, payload); - if (token.HasValues) + CoinType = token["CoinType"].ToStringInvariant(), + FullName = token["CurrencyLong"].ToStringInvariant(), + DepositEnabled = !isMaintenanceMode, + WithdrawalEnabled = !isMaintenanceMode, + MinConfirmations = token["MinConfirmation"].ConvertInvariant(), + Name = token["Currency"].ToStringUpperInvariant(), + Notes = token["Notice"].ToStringInvariant(), + TxFee = token["TxFee"].ConvertInvariant(), + }; + currencies[coin.Name] = coin; + } + return currencies; + } + + protected override async Task> OnGetMarketSymbolsAsync() + { + List symbols = new List(); + JToken result = await MakeJsonRequestAsync("/public/getmarkets", null, null); + foreach (var market in result) + symbols.Add(market["MarketName"].ToStringInvariant()); + return symbols; + } + + protected internal override async Task< + IEnumerable + > OnGetMarketSymbolsMetadataAsync() + { + List markets = new List(); + // "result" : [{"MarketCurrency" : "DOGE","BaseCurrency" : "BTC","MarketCurrencyLong" : "Dogecoin","BaseCurrencyLong" : "Bitcoin", "MinTradeSize" : 0.10000000, "MarketName" : "DOGE_BTC", "IsActive" : true, }, ... + JToken result = await MakeJsonRequestAsync("/public/getmarkets", null, null); + foreach (JToken token in result) + { + markets.Add( + new ExchangeMarket() { - // Only the orderid is returned on success - result.OrderId = token["orderid"].ToStringInvariant(); - result.Result = ExchangeAPIOrderResult.Filled; + //NOTE: Bleutrade is another weird one that calls the QuoteCurrency the "BaseCurrency" and the BaseCurrency the "MarketCurrency". + QuoteCurrency = token["BaseCurrency"].ToStringInvariant(), + BaseCurrency = token["MarketCurrency"].ToStringInvariant(), + MarketSymbol = token["MarketName"].ToStringInvariant(), + IsActive = token["IsActive"].ToStringInvariant().Equals("true"), + MinTradeSize = token["MinTradeSize"].ConvertInvariant(), } - return result; - } + ); + } + return markets; + } - protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null) - { - await MakeJsonRequestAsync("/market/cancel?orderid=" + orderId, null, await GetNoncePayloadAsync()); - } + protected override async Task OnGetTickerAsync(string marketSymbol) + { + JToken result = await MakeJsonRequestAsync( + "/public/getmarketsummary?market=" + marketSymbol + ); + return this.ParseTicker( + result, + marketSymbol, + "Ask", + "Bid", + "Last", + "Volume", + "BaseVolume", + "Timestamp", + TimestampType.Iso8601 + ); + } - protected override async Task OnGetDepositAddressAsync(string currency, bool forceRegenerate = false) + protected override async Task< + IEnumerable> + > OnGetTickersAsync() + { + List> tickers = + new List>(); + // "result" : [{"MarketCurrency" : "Ethereum","BaseCurrency" : "Bitcoin","MarketName" : "ETH_BTC","PrevDay" : 0.00095000,"High" : 0.00105000,"Low" : 0.00086000, "Last" : 0.00101977, "Average" : 0.00103455, "Volume" : 2450.97496015, "BaseVolume" : 2.40781647, "TimeStamp" : "2014-07-29 11:19:30", "Bid" : 0.00100000, "Ask" : 0.00101977, "IsActive" : true }, ... ] + JToken result = await MakeJsonRequestAsync("/public/getmarketsummaries"); + foreach (JToken token in result) + { + var ticker = this.ParseTicker( + token, + token["MarketName"].ToStringInvariant(), + "Ask", + "Bid", + "Last", + "Volume", + "BaseVolume", + "Timestamp", + TimestampType.Iso8601 + ); + tickers.Add( + new KeyValuePair( + token["MarketName"].ToStringInvariant(), + ticker + ) + ); + } + return tickers; + } + + protected override async Task> OnGetCandlesAsync( + string marketSymbol, + int periodSeconds, + DateTime? startDate = null, + DateTime? endDate = null, + int? limit = null + ) + { + List candles = new List(); + string periodString = PeriodSecondsToString(periodSeconds); + limit = limit ?? (limit > 2160 ? 2160 : limit); + endDate = endDate ?? CryptoUtility.UtcNow.AddMinutes(1.0); + startDate = startDate ?? endDate.Value.Subtract(TimeSpan.FromDays(1.0)); + + //market period(15m, 20m, 30m, 1h, 2h, 3h, 4h, 6h, 8h, 12h, 1d) count(default: 1000, max: 999999) lasthours(default: 24, max: 2160) + //"result":[{"TimeStamp":"2014-07-31 10:15:00","Open":"0.00000048","High":"0.00000050","Low":"0.00000048","Close":"0.00000049","Volume":"594804.73036048","BaseVolume":"0.11510368" }, ... + JToken result = await MakeJsonRequestAsync( + "/public/getcandles?market=" + + marketSymbol + + "&period=" + + periodString + + (limit == null ? string.Empty : "&lasthours=" + limit) + ); + foreach (JToken jsonCandle in result) + { + //NOTE: Bleutrade uses the term "BaseVolume" when referring to the QuoteCurrencyVolume + MarketCandle candle = this.ParseCandle( + jsonCandle, + marketSymbol, + periodSeconds, + "Open", + "High", + "Low", + "Close", + "Timestamp", + TimestampType.Iso8601, + "Volume", + "BaseVolume" + ); + if (candle.Timestamp >= startDate && candle.Timestamp <= endDate) { - JToken token = await MakeJsonRequestAsync("/account/getdepositaddress?" + "currency=" + NormalizeMarketSymbol(currency), BaseUrl, await GetNoncePayloadAsync()); - if (token["Currency"].ToStringInvariant().Equals(currency) && token["Address"] != null) - { - // At this time, according to Bleutrade support, they don't support any currency requiring an Address Tag, but they will add this feature in the future - return new ExchangeDepositDetails() - { - Currency = token["Currency"].ToStringInvariant(), - Address = token["Address"].ToStringInvariant() - }; - } - return null; + candles.Add(candle); } + } + return candles; + } - protected override async Task> OnGetDepositHistoryAsync(string currency) - { - List transactions = new List(); + protected override async Task> OnGetRecentTradesAsync( + string marketSymbol + ) + { + List trades = new List(); + //"result" : [{ "TimeStamp" : "2014-07-29 18:08:00","Quantity" : 654971.69417461,"Price" : 0.00000055,"Total" : 0.360234432,"OrderType" : "BUY"}, ... ] + JToken result = await MakeJsonRequestAsync( + "/public/getmarkethistory?market=" + marketSymbol + ); + foreach (JToken token in result) + trades.Add(ParseTrade(token)); + return trades; + } - // "result" : [{"Id" : "44933431","TimeStamp" : "2015-05-13 07:15:23","Coin" : "LTC","Amount" : -0.10000000,"Label" : "Withdraw: 0.99000000 to address Anotheraddress; fee 0.01000000","TransactionId" : "c396228895f8976e3810286c1537bddd4a45bb37d214c0e2b29496a4dee9a09b" } - JToken result = await MakeJsonRequestAsync("/account/getdeposithistory", BaseUrl, await GetNoncePayloadAsync()); - foreach (JToken token in result) - { - transactions.Add(new ExchangeTransaction() - { - PaymentId = token["Id"].ToStringInvariant(), - BlockchainTxId = token["TransactionId"].ToStringInvariant(), - Timestamp = token["TimeStamp"].ToDateTimeInvariant(), - Currency = token["Coin"].ToStringInvariant(), - Amount = token["Amount"].ConvertInvariant(), - Notes = token["Label"].ToStringInvariant(), - TxFee = token["fee"].ConvertInvariant(), - Status = TransactionStatus.Unknown - }); - } - return transactions; + protected override async Task OnGetHistoricalTradesAsync( + Func, bool> callback, + string marketSymbol, + DateTime? startDate = null, + DateTime? endDate = null + ) + { + List trades = new List(); + // TODO: Not directly supported so the best we can do is get their Max 200 and check the timestamp if necessary + JToken result = await MakeJsonRequestAsync( + "/public/getmarkethistory?market=" + marketSymbol + "&count=200" + ); + foreach (JToken token in result) + { + ExchangeTrade trade = ParseTrade(token); + if (startDate == null || trade.Timestamp >= startDate) + { + trades.Add(trade); } + } + if (trades.Count != 0) + { + callback(trades); + } + } - protected override async Task OnWithdrawAsync(ExchangeWithdrawalRequest withdrawalRequest) - { - var payload = await GetNoncePayloadAsync(); - payload["currency"] = withdrawalRequest.Currency; - payload["quantity"] = withdrawalRequest.Amount; - payload["address"] = withdrawalRequest.Address; - if (!string.IsNullOrEmpty(withdrawalRequest.AddressTag)) payload["comments"] = withdrawalRequest.AddressTag; + protected override async Task OnGetOrderBookAsync( + string marketSymbol, + int maxCount = 100 + ) + { + //"result" : { "buy" : [{"Quantity" : 4.99400000,"Rate" : 3.00650900}, {"Quantity" : 50.00000000, "Rate" : 3.50000000 } ] ... + JToken token = await MakeJsonRequestAsync( + "/public/getorderbook?market=" + marketSymbol + "&type=ALL&depth=" + maxCount + ); + return ExchangeAPIExtensions.ParseOrderBookFromJTokenDictionaries( + token, + "sell", + "buy", + "Rate", + "Quantity", + maxCount: maxCount + ); + } - await MakeJsonRequestAsync("/account/withdraw", BaseUrl, payload); + #endregion - // Bleutrade doesn't return any info, just an empty string on success. The MakeJsonRequestAsync will throw an exception if there's an error - return new ExchangeWithdrawalResponse() { Success = true }; - } + #region Private APIs + protected override async Task> OnGetAmountsAsync() + { + Dictionary amounts = new Dictionary(); + // "result" : [{"Currency" : "DOGE","Balance" : 0.00000000,"Available" : 0.00000000,"Pending" : 0.00000000,"CryptoAddress" : "DBSwFELQiVrwxFtyHpVHbgVrNJXwb3hoXL", "IsActive" : true}, ... + JToken result = await MakeJsonRequestAsync( + "/account/getbalances", + null, + await GetNoncePayloadAsync() + ); + foreach (JToken token in result) + { + decimal amount = result["Balance"].ConvertInvariant(); + if (amount > 0) + amounts[token["Currency"].ToStringInvariant()] = amount; + } + return amounts; + } - #endregion + protected override async Task< + Dictionary + > OnGetAmountsAvailableToTradeAsync() + { + Dictionary amounts = new Dictionary(); + // "result" : [{"Currency" : "DOGE","Balance" : 0.00000000,"Available" : 0.00000000,"Pending" : 0.00000000,"CryptoAddress" : "DBSwFELQiVrwxFtyHpVHbgVrNJXwb3hoXL", "IsActive" : true}, ... + JToken result = await MakeJsonRequestAsync( + "/account/getbalances", + null, + await GetNoncePayloadAsync() + ); + foreach (JToken token in result) + { + decimal amount = result["Available"].ConvertInvariant(); + if (amount > 0) + amounts[token["Currency"].ToStringInvariant()] = amount; + } + return amounts; + } - #region Private Functions + protected override async Task OnGetOrderDetailsAsync( + string orderId, + string marketSymbol = null + ) + { + // "result" : { "OrderId" : "65489","Exchange" : "LTC_BTC", "Type" : "BUY", "Quantity" : 20.00000000, "QuantityRemaining" : 5.00000000, "QuantityBaseTraded" : "0.16549400", "Price" : 0.01268311, "Status" : "OPEN", "Created" : "2014-08-03 13:55:20", "Comments" : "My optional comment, eg function id #123" } + JToken result = await MakeJsonRequestAsync( + "/account/getorder?orderid=" + orderId, + null, + await GetNoncePayloadAsync() + ); + return ParseOrder(result); + } - private ExchangeTrade ParseTrade(JToken token) + protected override async Task< + IEnumerable + > OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) + { + List orders = new List(); + JToken result = await MakeJsonRequestAsync( + "/account/getorders?market=" + + (string.IsNullOrEmpty(marketSymbol) ? "ALL" : marketSymbol) + + "&orderstatus=OK&ordertype=ALL", + null, + await GetNoncePayloadAsync() + ); + foreach (JToken token in result) + { + ExchangeOrderResult order = ParseOrder(token); + if (afterDate != null) { - return token.ParseTrade("Quantity", "Price", "OrderType", "TimeStamp", TimestampType.Iso8601); + if (order.OrderDate > afterDate) + orders.Add(order); } + else + orders.Add(order); + } + return orders; + } + + protected override async Task> OnGetOpenOrderDetailsAsync( + string marketSymbol = null + ) + { + List orders = new List(); + JToken result = await MakeJsonRequestAsync( + "/market/getopenorders", + null, + await GetNoncePayloadAsync() + ); + foreach (JToken token in result) + orders.Add(ParseOrder(token)); + return orders; + } + + protected override async Task OnPlaceOrderAsync( + ExchangeOrderRequest order + ) + { + ExchangeOrderResult result = new ExchangeOrderResult() + { + Result = ExchangeAPIOrderResult.Error + }; + var payload = await GetNoncePayloadAsync(); + order.ExtraParameters.CopyTo(payload); + + // Only limit order is supported - no indication on how it is filled + JToken token = await MakeJsonRequestAsync( + (order.IsBuy ? "/market/buylimit?" : "market/selllimit?") + + "market=" + + order.MarketSymbol + + "&rate=" + + order.Price.ToStringInvariant() + + "&quantity=" + + order.RoundAmount().ToStringInvariant(), + null, + payload + ); + if (token.HasValues) + { + // Only the orderid is returned on success + result.OrderId = token["orderid"].ToStringInvariant(); + result.Result = ExchangeAPIOrderResult.Filled; + } + return result; + } + + protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null) + { + await MakeJsonRequestAsync( + "/market/cancel?orderid=" + orderId, + null, + await GetNoncePayloadAsync() + ); + } - private ExchangeOrderResult ParseOrder(JToken token) + protected override async Task OnGetDepositAddressAsync( + string currency, + bool forceRegenerate = false + ) + { + JToken token = await MakeJsonRequestAsync( + "/account/getdepositaddress?" + "currency=" + NormalizeMarketSymbol(currency), + BaseUrl, + await GetNoncePayloadAsync() + ); + if (token["Currency"].ToStringInvariant().Equals(currency) && token["Address"] != null) + { + // At this time, according to Bleutrade support, they don't support any currency requiring an Address Tag, but they will add this feature in the future + return new ExchangeDepositDetails() { - var order = new ExchangeOrderResult() - { - OrderId = token["OrderId"].ToStringInvariant(), - IsBuy = token["Type"].ToStringInvariant().Equals("BUY"), - MarketSymbol = token["Exchange"].ToStringInvariant(), - Amount = token["Quantity"].ConvertInvariant(), - OrderDate = token["Created"].ToDateTimeInvariant(), - AveragePrice = token["Price"].ConvertInvariant(), - AmountFilled = token["QuantityBaseTraded"].ConvertInvariant(), - Message = token["Comments"].ToStringInvariant(), - }; - - switch (token["status"].ToStringInvariant()) + Currency = token["Currency"].ToStringInvariant(), + Address = token["Address"].ToStringInvariant() + }; + } + return null; + } + + protected override async Task> OnGetDepositHistoryAsync( + string currency + ) + { + List transactions = new List(); + + // "result" : [{"Id" : "44933431","TimeStamp" : "2015-05-13 07:15:23","Coin" : "LTC","Amount" : -0.10000000,"Label" : "Withdraw: 0.99000000 to address Anotheraddress; fee 0.01000000","TransactionId" : "c396228895f8976e3810286c1537bddd4a45bb37d214c0e2b29496a4dee9a09b" } + JToken result = await MakeJsonRequestAsync( + "/account/getdeposithistory", + BaseUrl, + await GetNoncePayloadAsync() + ); + foreach (JToken token in result) + { + transactions.Add( + new ExchangeTransaction() { - case "OPEN": - order.Result = ExchangeAPIOrderResult.Pending; - break; - case "OK": - if (order.Amount == order.AmountFilled) order.Result = ExchangeAPIOrderResult.Filled; - else order.Result = ExchangeAPIOrderResult.FilledPartially; - break; - case "CANCELED": - order.Result = ExchangeAPIOrderResult.Canceled; - break; - default: - order.Result = ExchangeAPIOrderResult.Unknown; - break; + PaymentId = token["Id"].ToStringInvariant(), + BlockchainTxId = token["TransactionId"].ToStringInvariant(), + Timestamp = token["TimeStamp"].ToDateTimeInvariant(), + Currency = token["Coin"].ToStringInvariant(), + Amount = token["Amount"].ConvertInvariant(), + Notes = token["Label"].ToStringInvariant(), + TxFee = token["fee"].ConvertInvariant(), + Status = TransactionStatus.Unknown } - return order; - } + ); + } + return transactions; + } + + protected override async Task OnWithdrawAsync( + ExchangeWithdrawalRequest withdrawalRequest + ) + { + var payload = await GetNoncePayloadAsync(); + payload["currency"] = withdrawalRequest.Currency; + payload["quantity"] = withdrawalRequest.Amount; + payload["address"] = withdrawalRequest.Address; + if (!string.IsNullOrEmpty(withdrawalRequest.AddressTag)) + payload["comments"] = withdrawalRequest.AddressTag; + + await MakeJsonRequestAsync("/account/withdraw", BaseUrl, payload); + + // Bleutrade doesn't return any info, just an empty string on success. The MakeJsonRequestAsync will throw an exception if there's an error + return new ExchangeWithdrawalResponse() { Success = true }; + } + + #endregion + + #region Private Functions - #endregion + private ExchangeTrade ParseTrade(JToken token) + { + return token.ParseTrade( + "Quantity", + "Price", + "OrderType", + "TimeStamp", + TimestampType.Iso8601 + ); + } + private ExchangeOrderResult ParseOrder(JToken token) + { + var order = new ExchangeOrderResult() + { + OrderId = token["OrderId"].ToStringInvariant(), + IsBuy = token["Type"].ToStringInvariant().Equals("BUY"), + MarketSymbol = token["Exchange"].ToStringInvariant(), + Amount = token["Quantity"].ConvertInvariant(), + OrderDate = token["Created"].ToDateTimeInvariant(), + AveragePrice = token["Price"].ConvertInvariant(), + AmountFilled = token["QuantityBaseTraded"].ConvertInvariant(), + Message = token["Comments"].ToStringInvariant(), + }; + + switch (token["status"].ToStringInvariant()) + { + case "OPEN": + order.Result = ExchangeAPIOrderResult.Pending; + break; + case "OK": + if (order.Amount == order.AmountFilled) + order.Result = ExchangeAPIOrderResult.Filled; + else + order.Result = ExchangeAPIOrderResult.FilledPartially; + break; + case "CANCELED": + order.Result = ExchangeAPIOrderResult.Canceled; + break; + default: + order.Result = ExchangeAPIOrderResult.Unknown; + break; + } + return order; + } + + #endregion } - public partial class ExchangeName { public const string Bleutrade = "Bleutrade"; } + public partial class ExchangeName + { + public const string Bleutrade = "Bleutrade"; + } #endif - } diff --git a/src/ExchangeSharp/API/Exchanges/BtcTurk/ExchangeBtcTurkAPI.cs b/src/ExchangeSharp/API/Exchanges/BtcTurk/ExchangeBtcTurkAPI.cs index 9f9b23e65..b21348d90 100644 --- a/src/ExchangeSharp/API/Exchanges/BtcTurk/ExchangeBtcTurkAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/BtcTurk/ExchangeBtcTurkAPI.cs @@ -1,9 +1,9 @@ -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Newtonsoft.Json.Linq; namespace ExchangeSharp { @@ -22,7 +22,9 @@ public ExchangeBtcTurkAPI() // ExchangeGlobalCurrencyReplacements[] not implemented } - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() + protected internal override async Task< + IEnumerable + > OnGetMarketSymbolsMetadataAsync() { /*{ "data": { "timeZone": "UTC", @@ -70,24 +72,33 @@ protected internal override async Task> OnGetMarketS var markets = new List(); foreach (JToken instrument in instruments["symbols"]) { - markets.Add(new ExchangeMarket - { - MarketSymbol = instrument["name"].ToStringUpperInvariant(), - QuoteCurrency = instrument["denominator"].ToStringInvariant(), - BaseCurrency = instrument["numerator"].ToStringInvariant(), - }); + markets.Add( + new ExchangeMarket + { + MarketSymbol = instrument["name"].ToStringUpperInvariant(), + QuoteCurrency = instrument["denominator"].ToStringInvariant(), + BaseCurrency = instrument["numerator"].ToStringInvariant(), + } + ); } return markets; } - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + protected override async Task OnGetTradesWebSocketAsync( + Func, Task> callback, + params string[] marketSymbols + ) { if (marketSymbols == null || marketSymbols.Length == 0) { - marketSymbols = (await GetMarketSymbolsMetadataAsync()).Select(m => m.MarketSymbol).ToArray(); + marketSymbols = (await GetMarketSymbolsMetadataAsync()) + .Select(m => m.MarketSymbol) + .ToArray(); } - return await ConnectPublicWebSocketAsync("/market", async (_socket, msg) => - { /* {[ + return await ConnectPublicWebSocketAsync( + "/market", + async (_socket, msg) => + { /* {[ 991, { "type": 991, @@ -95,39 +106,38 @@ protected override async Task OnGetTradesWebSocketAsync(Func() == 991) - { - // no need to do anything with this - } - //else if (token["method"].ToStringInvariant() == "ERROR" || token["method"].ToStringInvariant() == "unknown") - //{ - // throw new APIException(token["code"].ToStringInvariant() + ": " + token["message"].ToStringInvariant()); - //} - else if (token[0].ToObject() == 451) - { - // channel 451 OrderInsert. Ignore. - } - else if (token[0].ToObject() == 100) - { /* + /* {[ + 451, + { + "type": 451, + "pairId": 0, + "symbol": "BTCTRY", + "id": 0, + "method": 0, + "userId": 0, + "orderType": 0, + "price": "0", + "amount": "0", + "numLeft": "0.00", + "denomLeft": "0", + "newOrderClientId": null + } + ]}] */ + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + if (token[0].ToObject() == 991) + { + // no need to do anything with this + } + //else if (token["method"].ToStringInvariant() == "ERROR" || token["method"].ToStringInvariant() == "unknown") + //{ + // throw new APIException(token["code"].ToStringInvariant() + ": " + token["message"].ToStringInvariant()); + //} + else if (token[0].ToObject() == 451) + { + // channel 451 OrderInsert. Ignore. + } + else if (token[0].ToObject() == 100) + { /* {[ 100, { @@ -137,10 +147,10 @@ protected override async Task OnGetTradesWebSocketAsync(Func() == 421) - { /* + // successfully joined + } + else if (token[0].ToObject() == 421) + { /* {[ 421, { @@ -166,20 +176,30 @@ protected override async Task OnGetTradesWebSocketAsync(Func(marketSymbol, trade)); - } - } - else if (token[0].ToObject() == 422) - { /* {[ + var data = token[1]; + var dataArray = data["items"].ToArray(); + for (int i = 0; i < dataArray.Length; i++) + { + var trade = dataArray[i].ParseTrade( + "A", + "P", + "S", + "D", + TimestampType.UnixMilliseconds, + "I", + "0" + ); + string marketSymbol = data["symbol"].ToStringInvariant(); + trade.Flags |= ExchangeTradeFlags.IsFromSnapshot; + if (i == dataArray.Length - 1) + trade.Flags |= ExchangeTradeFlags.IsLastFromSnapshot; + await callback( + new KeyValuePair(marketSymbol, trade) + ); + } + } + else if (token[0].ToObject() == 422) + { /* {[ 422, { "D": "1651204593830", @@ -193,14 +213,26 @@ protected override async Task OnGetTradesWebSocketAsync(Func(marketSymbol, trade)); - } - else Logger.Warn($"Unexpected channel {token[0].ToObject()}"); - }, async (_socket) => - { /* + var data = token[1]; + var trade = data.ParseTrade( + "A", + "P", + "S", + "D", + TimestampType.UnixMilliseconds, + "I", + "0" + ); + string marketSymbol = data["PS"].ToStringInvariant(); + await callback( + new KeyValuePair(marketSymbol, trade) + ); + } + else + Logger.Warn($"Unexpected channel {token[0].ToObject()}"); + }, + async (_socket) => + { /* [ 151, { @@ -211,22 +243,28 @@ protected override async Task OnGetTradesWebSocketAsync(Func(); - subscribeRequest.Add(151); - subscribeRequest.Add(new - { - type = 151, - channel = "trade", - @event = marketSymbol, - join = true, // If true, it means that you want to subscribe, if false, you can unsubscribe. - }); - await _socket.SendMessageAsync(subscribeRequest.ToArray()); - } - }); + foreach (var marketSymbol in marketSymbols) + { + var subscribeRequest = new List(); + subscribeRequest.Add(151); + subscribeRequest.Add( + new + { + type = 151, + channel = "trade", + @event = marketSymbol, + join = true, // If true, it means that you want to subscribe, if false, you can unsubscribe. + } + ); + await _socket.SendMessageAsync(subscribeRequest.ToArray()); + } + } + ); } + } + public partial class ExchangeName + { + public const string BtcTurk = "BtcTurk"; } - public partial class ExchangeName { public const string BtcTurk = "BtcTurk"; } } diff --git a/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs b/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs index 944e5edfb..4a64cd6b9 100644 --- a/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs @@ -30,6 +30,7 @@ public sealed partial class ExchangeBybitAPI : ExchangeAPI public override string BaseUrl { get; set; } = "https://api.bybit.com"; public override string BaseUrlWebSocket { get; set; } = "wss://stream.bybit.com/realtime"; + // public override string BaseUrl { get; set; } = "https://api-testnet.bybit.com/"; // public override string BaseUrlWebSocket { get; set; } = "wss://stream-testnet.bybit.com/realtime"; @@ -45,12 +46,16 @@ private ExchangeBybitAPI() RateLimit = new RateGate(500, TimeSpan.FromMinutes(1)); } - public override Task ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol) + public override Task ExchangeMarketSymbolToGlobalMarketSymbolAsync( + string marketSymbol + ) { throw new NotImplementedException(); } - public override Task GlobalMarketSymbolToExchangeMarketSymbolAsync(string marketSymbol) + public override Task GlobalMarketSymbolToExchangeMarketSymbolAsync( + string marketSymbol + ) { throw new NotImplementedException(); } @@ -67,7 +72,10 @@ public override Task GlobalMarketSymbolToExchangeMarketSymbolAsync(strin // NonceOffset = now - serverDate + TimeSpan.FromSeconds(1); // how much time to substract from Nonce when making a request // } - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + protected override async Task ProcessRequestAsync( + IHttpWebRequest request, + Dictionary payload + ) { if ((payload != null) && payload.ContainsKey("sign") && request.Method == "POST") { @@ -76,14 +84,22 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti } #nullable enable - //Not using MakeJsonRequest... so we can perform our own check on the ret_code - private async Task DoMakeJsonRequestAsync(string url, string? baseUrl = null, Dictionary? payload = null, string? requestMethod = null) + //Not using MakeJsonRequest... so we can perform our own check on the ret_code + private async Task DoMakeJsonRequestAsync( + string url, + string? baseUrl = null, + Dictionary? payload = null, + string? requestMethod = null + ) { await new SynchronizationContextRemover(); - string stringResult = (await MakeRequestAsync(url, baseUrl, payload, requestMethod)).Response; + string stringResult = ( + await MakeRequestAsync(url, baseUrl, payload, requestMethod) + ).Response; return JsonConvert.DeserializeObject(stringResult); } + #nullable disable private JToken CheckRetCode(JToken response, string[] allowedRetCodes) @@ -95,10 +111,10 @@ private JToken CheckRetCode(JToken response, string[] allowedRetCodes) } return result; } - + private JToken CheckRetCode(JToken response) { - return CheckRetCode(response, new string[] {"0"}); + return CheckRetCode(response, new string[] { "0" }); } private JToken GetResult(JToken response, out string retCode, out string retMessage) @@ -108,21 +124,33 @@ private JToken GetResult(JToken response, out string retCode, out string retMess return response["result"]; } - private async Task SendWebsocketAuth(IWebSocket socket) { + private async Task SendWebsocketAuth(IWebSocket socket) + { var payload = await GetNoncePayloadAsync(); var nonce = (payload["nonce"].ConvertInvariant() + 5000).ToStringInvariant(); - var signature = CryptoUtility.SHA256Sign($"GET/realtime{nonce}", CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); - await socket.SendMessageAsync(new { op = "auth", args = new [] {PublicApiKey.ToUnsecureString(), nonce, signature} }); + var signature = CryptoUtility.SHA256Sign( + $"GET/realtime{nonce}", + CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey) + ); + await socket.SendMessageAsync( + new + { + op = "auth", + args = new[] { PublicApiKey.ToUnsecureString(), nonce, signature } + } + ); } - private async Task> GetAuthenticatedPayload(Dictionary requestPayload = null) + private async Task> GetAuthenticatedPayload( + Dictionary requestPayload = null + ) { var payload = await GetNoncePayloadAsync(); var nonce = payload["nonce"].ConvertInvariant(); payload.Remove("nonce"); payload["api_key"] = PublicApiKey.ToUnsecureString(); payload["timestamp"] = nonce.ToStringInvariant(); - payload["recv_window"] = _recvWindow; + payload["recv_window"] = _recvWindow; if (requestPayload != null) { payload = payload.Concat(requestPayload).ToDictionary(p => p.Key, p => p.Value); @@ -131,11 +159,16 @@ private async Task> GetAuthenticatedPayload(Dictionar string form = CryptoUtility.GetFormForPayload(payload, false, true); form = form.Replace("=False", "=false"); form = form.Replace("=True", "=true"); - payload["sign"] = CryptoUtility.SHA256Sign(form, CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); + payload["sign"] = CryptoUtility.SHA256Sign( + form, + CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey) + ); return payload; } - private async Task GetAuthenticatedQueryString(Dictionary requestPayload = null) + private async Task GetAuthenticatedQueryString( + Dictionary requestPayload = null + ) { var payload = await GetAuthenticatedPayload(requestPayload); var sign = payload["sign"].ToStringInvariant(); @@ -145,204 +178,239 @@ private async Task GetAuthenticatedQueryString(Dictionary DoConnectWebSocketAsync(Func connected, Func callback, int symbolArrayIndex = 3) + private Task DoConnectWebSocketAsync( + Func connected, + Func callback, + int symbolArrayIndex = 3 + ) { Timer pingTimer = null; - return ConnectPublicWebSocketAsync(url: string.Empty, messageCallback: async (_socket, msg) => - { - var msgString = msg.ToStringFromUTF8(); - JToken token = JToken.Parse(msgString); - - if (token["ret_msg"]?.ToStringInvariant() == "pong") - { // received reply to our ping - return; - } - - if (token["topic"] != null) - { - var data = token["data"]; - await callback(_socket, data); - } - else - { - /* - subscription response: + return ConnectPublicWebSocketAsync( + url: string.Empty, + messageCallback: async (_socket, msg) => { - "success": true, // Whether subscription is successful - "ret_msg": "", // Successful subscription: "", otherwise it shows error message - "conn_id":"e0e10eee-4eff-4d21-881e-a0c55c25e2da",// current connection id - "request": { // Request to your subscription - "op": "subscribe", - "args": [ - "kline.BTCUSD.1m" - ] + var msgString = msg.ToStringFromUTF8(); + JToken token = JToken.Parse(msgString); + + if (token["ret_msg"]?.ToStringInvariant() == "pong") + { // received reply to our ping + return; } - } - */ - JToken response = token["request"]; - var op = response["op"]?.ToStringInvariant(); - if ((response != null) && ((op == "subscribe") || (op == "auth"))) - { - var responseMessage = token["ret_msg"]?.ToStringInvariant(); - if (responseMessage != "") + + if (token["topic"] != null) { - Logger.Info("Websocket unable to connect: " + msgString); - return; + var data = token["data"]; + await callback(_socket, data); } - else if (pingTimer == null) + else { /* - ping response: + subscription response: { - "success": true, // Whether ping is successful - "ret_msg": "pong", - "conn_id": "036e5d21-804c-4447-a92d-b65a44d00700",// current connection id - "request": { - "op": "ping", - "args": null - } + "success": true, // Whether subscription is successful + "ret_msg": "", // Successful subscription: "", otherwise it shows error message + "conn_id":"e0e10eee-4eff-4d21-881e-a0c55c25e2da",// current connection id + "request": { // Request to your subscription + "op": "subscribe", + "args": [ + "kline.BTCUSD.1m" + ] + } } */ - pingTimer = new Timer(callback: async s => await _socket.SendMessageAsync(new { op = "ping" }), - state: null, dueTime: 0, period: 15000); // send a ping every 15 seconds - return; + JToken response = token["request"]; + var op = response["op"]?.ToStringInvariant(); + if ((response != null) && ((op == "subscribe") || (op == "auth"))) + { + var responseMessage = token["ret_msg"]?.ToStringInvariant(); + if (responseMessage != "") + { + Logger.Info("Websocket unable to connect: " + msgString); + return; + } + else if (pingTimer == null) + { + /* + ping response: + { + "success": true, // Whether ping is successful + "ret_msg": "pong", + "conn_id": "036e5d21-804c-4447-a92d-b65a44d00700",// current connection id + "request": { + "op": "ping", + "args": null + } + } + */ + pingTimer = new Timer( + callback: async s => + await _socket.SendMessageAsync(new { op = "ping" }), + state: null, + dueTime: 0, + period: 15000 + ); // send a ping every 15 seconds + return; + } + } } + }, + connectCallback: async (_socket) => + { + await connected(_socket); + _socket.ConnectInterval = TimeSpan.FromHours(0); + }, + disconnectCallback: s => + { + pingTimer.Dispose(); + pingTimer = null; + return Task.CompletedTask; } - } - }, - connectCallback: async (_socket) => - { - await connected(_socket); - _socket.ConnectInterval = TimeSpan.FromHours(0); - }, - disconnectCallback: s => - { - pingTimer.Dispose(); - pingTimer = null; - return Task.CompletedTask; - }); + ); } - private async Task AddMarketSymbolsToChannel(IWebSocket socket, string argsPrefix, string[] marketSymbols) + private async Task AddMarketSymbolsToChannel( + IWebSocket socket, + string argsPrefix, + string[] marketSymbols + ) { string fullArgs = argsPrefix; if (marketSymbols == null || marketSymbols.Length == 0) { fullArgs += "*"; - } - else + } + else { foreach (var symbol in marketSymbols) { fullArgs += symbol + "|"; - } + } fullArgs = fullArgs.TrimEnd('|'); } - await socket.SendMessageAsync(new { op = "subscribe", args = new [] {fullArgs} }); + await socket.SendMessageAsync(new { op = "subscribe", args = new[] { fullArgs } }); } - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + protected override async Task OnGetTradesWebSocketAsync( + Func, Task> callback, + params string[] marketSymbols + ) { /* request: {"op":"subscribe","args":["trade.BTCUSD|XRPUSD"]} */ /* - response: - { - "topic": "trade.BTCUSD", - "data": [ + response: + { + "topic": "trade.BTCUSD", + "data": [ + { + "timestamp": "2020-01-12T16:59:59.000Z", + "trade_time_ms": 1582793344685, // trade time in millisecond + "symbol": "BTCUSD", + "side": "Sell", + "size": 328, + "price": 8098, + "tick_direction": "MinusTick", + "trade_id": "00c706e1-ba52-5bb0-98d0-bf694bdc69f7", + "cross_seq": 1052816407 + } + ] + } + */ + return await DoConnectWebSocketAsync( + async (_socket) => + { + await AddMarketSymbolsToChannel(_socket, "trade.", marketSymbols); + }, + async (_socket, token) => + { + foreach (var dataRow in token) { - "timestamp": "2020-01-12T16:59:59.000Z", - "trade_time_ms": 1582793344685, // trade time in millisecond - "symbol": "BTCUSD", - "side": "Sell", - "size": 328, - "price": 8098, - "tick_direction": "MinusTick", - "trade_id": "00c706e1-ba52-5bb0-98d0-bf694bdc69f7", - "cross_seq": 1052816407 + var trade = dataRow.ParseTradeBybit( + amountKey: "size", + priceKey: "price", + typeKey: "side", + timestampKey: "trade_time_ms", + timestampType: TimestampType.UnixMilliseconds, + idKey: "trade_id" + ); + await callback( + new KeyValuePair( + dataRow["symbol"].ToStringInvariant(), + trade + ) + ); } - ] - } - */ - return await DoConnectWebSocketAsync(async (_socket) => - { - await AddMarketSymbolsToChannel(_socket, "trade.", marketSymbols); - }, async (_socket, token) => - { - foreach (var dataRow in token) - { - var trade = dataRow.ParseTradeBybit( - amountKey: "size", - priceKey: "price", - typeKey: "side", - timestampKey: "trade_time_ms", - timestampType: TimestampType.UnixMilliseconds, - idKey: "trade_id"); - await callback(new KeyValuePair(dataRow["symbol"].ToStringInvariant(), trade)); - } - }); + } + ); } - protected override async Task OnGetPositionsWebSocketAsync(Action callback) + protected override async Task OnGetPositionsWebSocketAsync( + Action callback + ) { /* request: {"op": "subscribe", "args": ["position"]} */ /* - response: - { - "topic": "position", - "action": "update", - "data": [ + response: { - "user_id": 1, // user ID - "symbol": "BTCUSD", // the contract for this position - "size": 11, // the current position amount - "side": "Sell", // side - "position_value": "0.00159252", // positional value - "entry_price": "6907.291588174717", // entry price - "liq_price": "7100.234", // liquidation price - "bust_price": "7088.1234", // bankruptcy price - "leverage": "1", // leverage - "order_margin": "1", // order margin - "position_margin": "1", // position margin - "available_balance": "2", // available balance - "take_profit": "0", // take profit price - "tp_trigger_by": "LastPrice", // take profit trigger price, eg: LastPrice, IndexPrice. Conditional order only - "stop_loss": "0", // stop loss price - "sl_trigger_by": "", // stop loss trigger price, eg: LastPrice, IndexPrice. Conditional order only - "realised_pnl": "0.10", // realised PNL - "trailing_stop": "0", // trailing stop points - "trailing_active": "0", // trailing stop trigger price - "wallet_balance": "4.12", // wallet balance - "risk_id": 1, - "occ_closing_fee": "0.1", // position closing - "occ_funding_fee": "0.1", // funding fee - "auto_add_margin": 0, // auto margin replenishment switch - "cum_realised_pnl": "0.12", // Total realized profit and loss - "position_status": "Normal", // status of position (Normal: normal Liq: in the process of liquidation Adl: in the process of Auto-Deleveraging) - // Auto margin replenishment enabled (0: no 1: yes) - "position_seq": 14 // position version number + "topic": "position", + "action": "update", + "data": [ + { + "user_id": 1, // user ID + "symbol": "BTCUSD", // the contract for this position + "size": 11, // the current position amount + "side": "Sell", // side + "position_value": "0.00159252", // positional value + "entry_price": "6907.291588174717", // entry price + "liq_price": "7100.234", // liquidation price + "bust_price": "7088.1234", // bankruptcy price + "leverage": "1", // leverage + "order_margin": "1", // order margin + "position_margin": "1", // position margin + "available_balance": "2", // available balance + "take_profit": "0", // take profit price + "tp_trigger_by": "LastPrice", // take profit trigger price, eg: LastPrice, IndexPrice. Conditional order only + "stop_loss": "0", // stop loss price + "sl_trigger_by": "", // stop loss trigger price, eg: LastPrice, IndexPrice. Conditional order only + "realised_pnl": "0.10", // realised PNL + "trailing_stop": "0", // trailing stop points + "trailing_active": "0", // trailing stop trigger price + "wallet_balance": "4.12", // wallet balance + "risk_id": 1, + "occ_closing_fee": "0.1", // position closing + "occ_funding_fee": "0.1", // funding fee + "auto_add_margin": 0, // auto margin replenishment switch + "cum_realised_pnl": "0.12", // Total realized profit and loss + "position_status": "Normal", // status of position (Normal: normal Liq: in the process of liquidation Adl: in the process of Auto-Deleveraging) + // Auto margin replenishment enabled (0: no 1: yes) + "position_seq": 14 // position version number + } + ] } - ] - } */ - return await DoConnectWebSocketAsync(async (_socket) => - { - await SendWebsocketAuth(_socket); - await _socket.SendMessageAsync(new { op = "subscribe", args = new [] {"position"} }); - }, async (_socket, token) => - { - foreach (var dataRow in token) - { - callback(ParsePosition(dataRow)); - } - await Task.CompletedTask; - }); + return await DoConnectWebSocketAsync( + async (_socket) => + { + await SendWebsocketAuth(_socket); + await _socket.SendMessageAsync( + new { op = "subscribe", args = new[] { "position" } } + ); + }, + async (_socket, token) => + { + foreach (var dataRow in token) + { + callback(ParsePosition(dataRow)); + } + await Task.CompletedTask; + } + ); } protected override async Task> OnGetMarketSymbolsAsync() @@ -351,7 +419,9 @@ protected override async Task> OnGetMarketSymbolsAsync() return m.Select(x => x.MarketSymbol); } - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() + protected internal override async Task< + IEnumerable + > OnGetMarketSymbolsMetadataAsync() { /* { @@ -360,105 +430,107 @@ protected internal override async Task> OnGetMarketS "ext_code": "", "ext_info": "", "result": [ - { - "name": "BTCUSD", - "base_currency": "BTC", - "quote_currency": "USD", - "price_scale": 2, - "taker_fee": "0.00075", - "maker_fee": "-0.00025", - "leverage_filter": { - "min_leverage": 1, - "max_leverage": 100, - "leverage_step": "0.01" - }, - "price_filter": { - "min_price": "0.5", - "max_price": "999999.5", - "tick_size": "0.5" - }, - "lot_size_filter": { - "max_trading_qty": 1000000, - "min_trading_qty": 1, - "qty_step": 1 - } - }, - { - "name": "ETHUSD", - "base_currency": "ETH", - "quote_currency": "USD", - "price_scale": 2, - "taker_fee": "0.00075", - "maker_fee": "-0.00025", - "leverage_filter": { - "min_leverage": 1, - "max_leverage": 50, - "leverage_step": "0.01" - }, - "price_filter": { - "min_price": "0.05", - "max_price": "99999.95", - "tick_size": "0.05" - }, - "lot_size_filter": { - "max_trading_qty": 1000000, - "min_trading_qty": 1, - "qty_step": 1 - } - }, - { - "name": "EOSUSD", - "base_currency": "EOS", - "quote_currency": "USD", - "price_scale": 3, - "taker_fee": "0.00075", - "maker_fee": "-0.00025", - "leverage_filter": { - "min_leverage": 1, - "max_leverage": 50, - "leverage_step": "0.01" - }, - "price_filter": { - "min_price": "0.001", - "max_price": "1999.999", - "tick_size": "0.001" - }, - "lot_size_filter": { - "max_trading_qty": 1000000, - "min_trading_qty": 1, - "qty_step": 1 - } - }, - { - "name": "XRPUSD", - "base_currency": "XRP", - "quote_currency": "USD", - "price_scale": 4, - "taker_fee": "0.00075", - "maker_fee": "-0.00025", - "leverage_filter": { - "min_leverage": 1, - "max_leverage": 50, - "leverage_step": "0.01" - }, - "price_filter": { - "min_price": "0.0001", - "max_price": "199.9999", - "tick_size": "0.0001" - }, - "lot_size_filter": { - "max_trading_qty": 1000000, - "min_trading_qty": 1, - "qty_step": 1 - } - } + { + "name": "BTCUSD", + "base_currency": "BTC", + "quote_currency": "USD", + "price_scale": 2, + "taker_fee": "0.00075", + "maker_fee": "-0.00025", + "leverage_filter": { + "min_leverage": 1, + "max_leverage": 100, + "leverage_step": "0.01" + }, + "price_filter": { + "min_price": "0.5", + "max_price": "999999.5", + "tick_size": "0.5" + }, + "lot_size_filter": { + "max_trading_qty": 1000000, + "min_trading_qty": 1, + "qty_step": 1 + } + }, + { + "name": "ETHUSD", + "base_currency": "ETH", + "quote_currency": "USD", + "price_scale": 2, + "taker_fee": "0.00075", + "maker_fee": "-0.00025", + "leverage_filter": { + "min_leverage": 1, + "max_leverage": 50, + "leverage_step": "0.01" + }, + "price_filter": { + "min_price": "0.05", + "max_price": "99999.95", + "tick_size": "0.05" + }, + "lot_size_filter": { + "max_trading_qty": 1000000, + "min_trading_qty": 1, + "qty_step": 1 + } + }, + { + "name": "EOSUSD", + "base_currency": "EOS", + "quote_currency": "USD", + "price_scale": 3, + "taker_fee": "0.00075", + "maker_fee": "-0.00025", + "leverage_filter": { + "min_leverage": 1, + "max_leverage": 50, + "leverage_step": "0.01" + }, + "price_filter": { + "min_price": "0.001", + "max_price": "1999.999", + "tick_size": "0.001" + }, + "lot_size_filter": { + "max_trading_qty": 1000000, + "min_trading_qty": 1, + "qty_step": 1 + } + }, + { + "name": "XRPUSD", + "base_currency": "XRP", + "quote_currency": "USD", + "price_scale": 4, + "taker_fee": "0.00075", + "maker_fee": "-0.00025", + "leverage_filter": { + "min_leverage": 1, + "max_leverage": 50, + "leverage_step": "0.01" + }, + "price_filter": { + "min_price": "0.0001", + "max_price": "199.9999", + "tick_size": "0.0001" + }, + "lot_size_filter": { + "max_trading_qty": 1000000, + "min_trading_qty": 1, + "qty_step": 1 + } + } ], "time_now": "1581411225.414179" }} */ List markets = new List(); - JToken allSymbols = CheckRetCode(await DoMakeJsonRequestAsync("/v2/public/symbols")); + JToken allSymbols = CheckRetCode( + await DoMakeJsonRequestAsync("/v2/public/symbols") + ); foreach (JToken marketSymbolToken in allSymbols) { var market = new ExchangeMarket @@ -477,65 +549,72 @@ protected internal override async Task> OnGetMarketS market.PriceStepSize = priceFilter["tick_size"].ConvertInvariant(); JToken lotSizeFilter = marketSymbolToken["lot_size_filter"]; - market.MinTradeSize = lotSizeFilter["min_trading_qty"].ConvertInvariant(); - market.MaxTradeSize = lotSizeFilter["max_trading_qty"].ConvertInvariant(); + market.MinTradeSize = lotSizeFilter[ + "min_trading_qty" + ].ConvertInvariant(); + market.MaxTradeSize = lotSizeFilter[ + "max_trading_qty" + ].ConvertInvariant(); market.QuantityStepSize = lotSizeFilter["qty_step"].ConvertInvariant(); } - catch - { - - } + catch { } markets.Add(market); } return markets; } - private async Task> DoGetAmountsAsync(string field) { /* { - "ret_code": 0, - "ret_msg": "OK", - "ext_code": "", - "ext_info": "", - "result": { - "BTC": { - "equity": 1002, //equity = wallet_balance + unrealised_pnl - "available_balance": 999.99987471, //available_balance - //In Isolated Margin Mode: - // available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin) - //In Cross Margin Mode: - //if unrealised_pnl > 0: - //available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin); - //if unrealised_pnl < 0: - //available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin) + unrealised_pnl - "used_margin": 0.00012529, //used_margin = wallet_balance - available_balance - "order_margin": 0.00012529, //Used margin by order - "position_margin": 0, //position margin - "occ_closing_fee": 0, //position closing fee - "occ_funding_fee": 0, //funding fee - "wallet_balance": 1000, //wallet balance. When in Cross Margin mod, the number minus your unclosed loss is your real wallet balance. - "realised_pnl": 0, //daily realized profit and loss - "unrealised_pnl": 2, //unrealised profit and loss - //when side is sell: - // unrealised_pnl = size * (1.0 / mark_price - 1.0 / entry_price) - //when side is buy: - // unrealised_pnl = size * (1.0 / entry_price - 1.0 / mark_price) - "cum_realised_pnl": 0, //total relised profit and loss - "given_cash": 0, //given_cash - "service_cash": 0 //service_cash - } - }, - "time_now": "1578284274.816029", - "rate_limit_status": 98, - "rate_limit_reset_ms": 1580885703683, - "rate_limit": 100 + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": { + "BTC": { + "equity": 1002, //equity = wallet_balance + unrealised_pnl + "available_balance": 999.99987471, //available_balance + //In Isolated Margin Mode: + // available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin) + //In Cross Margin Mode: + //if unrealised_pnl > 0: + //available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin); + //if unrealised_pnl < 0: + //available_balance = wallet_balance - (position_margin + occ_closing_fee + occ_funding_fee + order_margin) + unrealised_pnl + "used_margin": 0.00012529, //used_margin = wallet_balance - available_balance + "order_margin": 0.00012529, //Used margin by order + "position_margin": 0, //position margin + "occ_closing_fee": 0, //position closing fee + "occ_funding_fee": 0, //funding fee + "wallet_balance": 1000, //wallet balance. When in Cross Margin mod, the number minus your unclosed loss is your real wallet balance. + "realised_pnl": 0, //daily realized profit and loss + "unrealised_pnl": 2, //unrealised profit and loss + //when side is sell: + // unrealised_pnl = size * (1.0 / mark_price - 1.0 / entry_price) + //when side is buy: + // unrealised_pnl = size * (1.0 / entry_price - 1.0 / mark_price) + "cum_realised_pnl": 0, //total relised profit and loss + "given_cash": 0, //given_cash + "service_cash": 0 //service_cash + } + }, + "time_now": "1578284274.816029", + "rate_limit_status": 98, + "rate_limit_reset_ms": 1580885703683, + "rate_limit": 100 } */ Dictionary amounts = new Dictionary(); var queryString = await GetAuthenticatedQueryString(); - JToken currencies = CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/wallet/balance?" + queryString, BaseUrl, null, "GET")); + JToken currencies = CheckRetCode( + await DoMakeJsonRequestAsync( + $"/v2/private/wallet/balance?" + queryString, + BaseUrl, + null, + "GET" + ) + ); foreach (JProperty currency in currencies.Children()) { var balance = currency.Value[field].ConvertInvariant(); @@ -556,38 +635,48 @@ protected override async Task> OnGetAmountsAsync() return await DoGetAmountsAsync("equity"); } - protected override async Task> OnGetAmountsAvailableToTradeAsync() + protected override async Task< + Dictionary + > OnGetAmountsAvailableToTradeAsync() { return await DoGetAmountsAsync("available_balance"); } - protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) + + protected override async Task OnGetOrderBookAsync( + string marketSymbol, + int maxCount = 100 + ) { /* { - "ret_code": 0, // return code - "ret_msg": "OK", // error message - "ext_code": "", // additional error code - "ext_info": "", // additional error info - "result": [ - { - "symbol": "BTCUSD", // symbol - "price": "9487", // price - "size": 336241, // size (in USD contracts) - "side": "Buy" // side - }, - { - "symbol": "BTCUSD", // symbol - "price": "9487.5", // price - "size": 522147, // size (in USD contracts) - "side": "Sell" // side - } - ], - "time_now": "1567108756.834357" // UTC timestamp + "ret_code": 0, // return code + "ret_msg": "OK", // error message + "ext_code": "", // additional error code + "ext_info": "", // additional error info + "result": [ + { + "symbol": "BTCUSD", // symbol + "price": "9487", // price + "size": 336241, // size (in USD contracts) + "side": "Buy" // side + }, + { + "symbol": "BTCUSD", // symbol + "price": "9487.5", // price + "size": 522147, // size (in USD contracts) + "side": "Sell" // side + } + ], + "time_now": "1567108756.834357" // UTC timestamp } */ - var tokens = CheckRetCode(await DoMakeJsonRequestAsync($"/v2/public/orderBook/L2?symbol={marketSymbol}")); + var tokens = CheckRetCode( + await DoMakeJsonRequestAsync( + $"/v2/public/orderBook/L2?symbol={marketSymbol}" + ) + ); var orderBook = new ExchangeOrderBook(); - foreach (var token in tokens) + foreach (var token in tokens) { var orderPrice = new ExchangeOrderPrice(); orderPrice.Price = token["price"].ConvertInvariant(); @@ -605,53 +694,60 @@ public async Task> GetCurrentPositionsAsync() { /* { - "ret_code": 0, - "ret_msg": "OK", - "ext_code": "", - "ext_info": "", - "result": { - "id": 27913, - "user_id": 1, - "risk_id": 1, - "symbol": "BTCUSD", - "side": "Buy", - "size": 5, - "position_value": "0.0006947", - "entry_price": "7197.35137469", - "is_isolated":true, - "auto_add_margin": 0, - "leverage": "1", //In Isolated Margin mode, the value is set by user. In Cross Margin mode, the value is the max leverage at current risk level - "effective_leverage": "1", // Effective Leverage. In Isolated Margin mode, its value equals `leverage`; In Cross Margin mode, The formula to calculate: - effective_leverage = position size / mark_price / (wallet_balance + unrealised_pnl) - "position_margin": "0.0006947", - "liq_price": "3608", - "bust_price": "3599", - "occ_closing_fee": "0.00000105", - "occ_funding_fee": "0", - "take_profit": "0", - "stop_loss": "0", - "trailing_stop": "0", - "position_status": "Normal", - "deleverage_indicator": 4, - "oc_calc_data": "{\"blq\":2,\"blv\":\"0.0002941\",\"slq\":0,\"bmp\":6800.408,\"smp\":0,\"fq\":-5,\"fc\":-0.00029477,\"bv2c\":1.00225,\"sv2c\":1.0007575}", - "order_margin": "0.00029477", - "wallet_balance": "0.03000227", - "realised_pnl": "-0.00000126", - "unrealised_pnl": 0, - "cum_realised_pnl": "-0.00001306", - "cross_seq": 444081383, - "position_seq": 287141589, - "created_at": "2019-10-19T17:04:55Z", - "updated_at": "2019-12-27T20:25:45.158767Z" - }, - "time_now": "1577480599.097287", - "rate_limit_status": 119, - "rate_limit_reset_ms": 1580885703683, - "rate_limit": 120 + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": { + "id": 27913, + "user_id": 1, + "risk_id": 1, + "symbol": "BTCUSD", + "side": "Buy", + "size": 5, + "position_value": "0.0006947", + "entry_price": "7197.35137469", + "is_isolated":true, + "auto_add_margin": 0, + "leverage": "1", //In Isolated Margin mode, the value is set by user. In Cross Margin mode, the value is the max leverage at current risk level + "effective_leverage": "1", // Effective Leverage. In Isolated Margin mode, its value equals `leverage`; In Cross Margin mode, The formula to calculate: + effective_leverage = position size / mark_price / (wallet_balance + unrealised_pnl) + "position_margin": "0.0006947", + "liq_price": "3608", + "bust_price": "3599", + "occ_closing_fee": "0.00000105", + "occ_funding_fee": "0", + "take_profit": "0", + "stop_loss": "0", + "trailing_stop": "0", + "position_status": "Normal", + "deleverage_indicator": 4, + "oc_calc_data": "{\"blq\":2,\"blv\":\"0.0002941\",\"slq\":0,\"bmp\":6800.408,\"smp\":0,\"fq\":-5,\"fc\":-0.00029477,\"bv2c\":1.00225,\"sv2c\":1.0007575}", + "order_margin": "0.00029477", + "wallet_balance": "0.03000227", + "realised_pnl": "-0.00000126", + "unrealised_pnl": 0, + "cum_realised_pnl": "-0.00001306", + "cross_seq": 444081383, + "position_seq": 287141589, + "created_at": "2019-10-19T17:04:55Z", + "updated_at": "2019-12-27T20:25:45.158767Z" + }, + "time_now": "1577480599.097287", + "rate_limit_status": 119, + "rate_limit_reset_ms": 1580885703683, + "rate_limit": 120 } */ var queryString = await GetAuthenticatedQueryString(); - JToken token = CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/position/list?" + queryString, BaseUrl, null, "GET")); + JToken token = CheckRetCode( + await DoMakeJsonRequestAsync( + $"/v2/private/position/list?" + queryString, + BaseUrl, + null, + "GET" + ) + ); List positions = new List(); foreach (var item in token) { @@ -664,27 +760,33 @@ public async Task GetCurrentFundingRateAsync(string marketSymbo { /* { - "ret_code": 0, - "ret_msg": "ok", - "ext_code": "", - "result": { - "symbol": "BTCUSD", - "funding_rate": "0.00010000", - "funding_rate_timestamp": 1577433600 - }, - "ext_info": null, - "time_now": "1577445586.446797", - "rate_limit_status": 119, - "rate_limit_reset_ms": 1577445586454, - "rate_limit": 120 + "ret_code": 0, + "ret_msg": "ok", + "ext_code": "", + "result": { + "symbol": "BTCUSD", + "funding_rate": "0.00010000", + "funding_rate_timestamp": 1577433600 + }, + "ext_info": null, + "time_now": "1577445586.446797", + "rate_limit_status": 119, + "rate_limit_reset_ms": 1577445586454, + "rate_limit": 120 } */ - JToken token = CheckRetCode(await DoMakeJsonRequestAsync($"/v2/public/funding/prev-funding-rate?symbol={marketSymbol}")); + JToken token = CheckRetCode( + await DoMakeJsonRequestAsync( + $"/v2/public/funding/prev-funding-rate?symbol={marketSymbol}" + ) + ); var funding = new ExchangeFunding(); funding.MarketSymbol = token["symbol"].ToStringInvariant(); funding.Rate = token["funding_rate"].ConvertInvariant(); // funding.TimeStamp = Convert.ToDateTime(TimeSpan.FromSeconds(token["funding_rate_timestamp"].ConvertInvariant())); - funding.TimeStamp = CryptoUtility.UnixTimeStampToDateTimeSeconds(token["funding_rate_timestamp"].ConvertInvariant()); + funding.TimeStamp = CryptoUtility.UnixTimeStampToDateTimeSeconds( + token["funding_rate_timestamp"].ConvertInvariant() + ); return funding; } @@ -693,24 +795,31 @@ public async Task GetPredictedFundingRateAsync(string marketSym { /* { - "ret_code": 0, - "ret_msg": "ok", - "ext_code": "", - "result": { - "predicted_funding_rate": 0.0001, - "predicted_funding_fee": 0 - }, - "ext_info": null, - "time_now": "1577447415.583259", - "rate_limit_status": 118, - "rate_limit_reset_ms": 1577447415590, - "rate_limit": 120 + "ret_code": 0, + "ret_msg": "ok", + "ext_code": "", + "result": { + "predicted_funding_rate": 0.0001, + "predicted_funding_fee": 0 + }, + "ext_info": null, + "time_now": "1577447415.583259", + "rate_limit_status": 118, + "rate_limit_reset_ms": 1577447415590, + "rate_limit": 120 } */ var extraParams = new Dictionary(); extraParams["symbol"] = marketSymbol; var queryString = await GetAuthenticatedQueryString(extraParams); - JToken token = CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/funding/predicted-funding?" + queryString, BaseUrl, null, "GET")); + JToken token = CheckRetCode( + await DoMakeJsonRequestAsync( + $"/v2/private/funding/predicted-funding?" + queryString, + BaseUrl, + null, + "GET" + ) + ); var funding = new ExchangeFunding(); funding.MarketSymbol = marketSymbol; funding.Rate = token["predicted_funding_rate"].ConvertInvariant(); @@ -718,11 +827,15 @@ public async Task GetPredictedFundingRateAsync(string marketSym return funding; } - private async Task> DoGetOrderDetailsAsync(string orderId, bool isClientOrderId = false, string marketSymbol = null) + private async Task> DoGetOrderDetailsAsync( + string orderId, + bool isClientOrderId = false, + string marketSymbol = null + ) { var extraParams = new Dictionary(); - if (orderId != null) + if (orderId != null) { if (isClientOrderId) extraParams["order_link_id"] = orderId; @@ -738,12 +851,21 @@ private async Task> DoGetOrderDetailsAsync(stri { throw new Exception("marketSymbol is required"); } - + var queryString = await GetAuthenticatedQueryString(extraParams); - JToken token = GetResult(await DoMakeJsonRequestAsync($"/v2/private/order?" + queryString, BaseUrl, null, "GET"), out var retCode, out var retMessage); + JToken token = GetResult( + await DoMakeJsonRequestAsync( + $"/v2/private/order?" + queryString, + BaseUrl, + null, + "GET" + ), + out var retCode, + out var retMessage + ); List orders = new List(); - if (orderId == null) + if (orderId == null) { foreach (JToken order in token) { @@ -759,15 +881,29 @@ private async Task> DoGetOrderDetailsAsync(stri } //Note, Bybit is not recommending the use of "/v2/private/order/list" now that "/v2/private/order" is capable of returning multiple results - protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) + protected override async Task> OnGetOpenOrderDetailsAsync( + string marketSymbol = null + ) { - var orders = await DoGetOrderDetailsAsync(null, isClientOrderId: false, marketSymbol: marketSymbol); + var orders = await DoGetOrderDetailsAsync( + null, + isClientOrderId: false, + marketSymbol: marketSymbol + ); return orders; } - protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnGetOrderDetailsAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { - var orders = await DoGetOrderDetailsAsync(orderId, isClientOrderId: isClientOrderId, marketSymbol: marketSymbol); + var orders = await DoGetOrderDetailsAsync( + orderId, + isClientOrderId: isClientOrderId, + marketSymbol: marketSymbol + ); if (orders.Count() > 0) { return orders.First(); @@ -778,7 +914,11 @@ protected override async Task OnGetOrderDetailsAsync(string } } - protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnCancelOrderAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { var extraParams = new Dictionary(); if (isClientOrderId) @@ -793,48 +933,85 @@ protected override async Task OnCancelOrderAsync(string orderId, string marketSy { throw new Exception("marketSymbol is required"); } - + var payload = await GetAuthenticatedPayload(extraParams); - CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/order/cancel", BaseUrl, payload, "POST")); - // new string[] {"0", "30032"}); - //30032: order has been finished or canceled + CheckRetCode( + await DoMakeJsonRequestAsync( + $"/v2/private/order/cancel", + BaseUrl, + payload, + "POST" + ) + ); + // new string[] {"0", "30032"}); + //30032: order has been finished or canceled } - + public async Task CancelAllOrdersAsync(string marketSymbol) { var extraParams = new Dictionary(); extraParams["symbol"] = marketSymbol; var payload = await GetAuthenticatedPayload(extraParams); - CheckRetCode(await DoMakeJsonRequestAsync($"/v2/private/order/cancelAll", BaseUrl, payload, "POST")); + CheckRetCode( + await DoMakeJsonRequestAsync( + $"/v2/private/order/cancelAll", + BaseUrl, + payload, + "POST" + ) + ); } - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + protected override async Task OnPlaceOrderAsync( + ExchangeOrderRequest order + ) { var payload = new Dictionary(); await AddOrderToPayload(order, payload); payload = await GetAuthenticatedPayload(payload); - JToken token = GetResult(await DoMakeJsonRequestAsync("/v2/private/order/create", BaseUrl, payload, "POST"), out var retCode, out var retMessage); + JToken token = GetResult( + await DoMakeJsonRequestAsync( + "/v2/private/order/create", + BaseUrl, + payload, + "POST" + ), + out var retCode, + out var retMessage + ); return ParseOrder(token, retCode, retMessage); } public async Task OnAmendOrderAsync(ExchangeOrderRequest order) { - if (order.IsPostOnly != null) throw new NotSupportedException("Post Only orders are not supported by this exchange or not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature."); + if (order.IsPostOnly != null) + throw new NotSupportedException( + "Post Only orders are not supported by this exchange or not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature." + ); var payload = new Dictionary(); payload["symbol"] = order.MarketSymbol; - if(order.OrderId != null) + if (order.OrderId != null) payload["order_id"] = order.OrderId; - else if(order.ClientOrderId != null) + else if (order.ClientOrderId != null) payload["order_link_id"] = order.ClientOrderId; - else + else throw new Exception("Need either OrderId or ClientOrderId"); - payload["p_r_qty"] = (long) await ClampOrderQuantity(order.MarketSymbol, order.Amount); - if(order.OrderType!=OrderType.Market) + payload["p_r_qty"] = (long)await ClampOrderQuantity(order.MarketSymbol, order.Amount); + if (order.OrderType != OrderType.Market) payload["p_r_price"] = order.Price; payload = await GetAuthenticatedPayload(payload); - JToken token = GetResult(await DoMakeJsonRequestAsync("/v2/private/order/replace", BaseUrl, payload, "POST"), out var retCode, out var retMessage); + JToken token = GetResult( + await DoMakeJsonRequestAsync( + "/v2/private/order/replace", + BaseUrl, + payload, + "POST" + ), + out var retCode, + out var retMessage + ); var result = new ExchangeOrderResult(); result.ResultCode = retCode; @@ -844,7 +1021,10 @@ public async Task OnAmendOrderAsync(ExchangeOrderRequest or return result; } - private async Task AddOrderToPayload(ExchangeOrderRequest order, Dictionary payload) + private async Task AddOrderToPayload( + ExchangeOrderRequest order, + Dictionary payload + ) { /* side true string Side @@ -865,10 +1045,10 @@ time_in_force true string Time in force payload["order_type"] = order.OrderType.ToStringInvariant(); payload["qty"] = await ClampOrderQuantity(order.MarketSymbol, order.Amount); - if(order.OrderType!=OrderType.Market) + if (order.OrderType != OrderType.Market) payload["price"] = order.Price; - if(order.ClientOrderId != null) + if (order.ClientOrderId != null) payload["order_link_id"] = order.ClientOrderId; if (order.ExtraParameters.TryGetValue("reduce_only", out var reduceOnly)) @@ -901,7 +1081,7 @@ private ExchangePosition ParsePosition(JToken token) "auto_add_margin": 0, "leverage": "1", //In Isolated Margin mode, the value is set by user. In Cross Margin mode, the value is the max leverage at current risk level "effective_leverage": "1", // Effective Leverage. In Isolated Margin mode, its value equals `leverage`; In Cross Margin mode, The formula to calculate: - effective_leverage = position size / mark_price / (wallet_balance + unrealised_pnl) + effective_leverage = position size / mark_price / (wallet_balance + unrealised_pnl) "position_margin": "0.0006947", "liq_price": "3608", "bust_price": "3599", @@ -930,14 +1110,21 @@ private ExchangePosition ParsePosition(JToken token) AveragePrice = token["entry_price"].ConvertInvariant(), LiquidationPrice = token["liq_price"].ConvertInvariant(), Leverage = token["effective_leverage"].ConvertInvariant(), - TimeStamp = CryptoUtility.ParseTimestamp(token["updated_at"], TimestampType.Iso8601UTC) + TimeStamp = CryptoUtility.ParseTimestamp( + token["updated_at"], + TimestampType.Iso8601UTC + ) }; if (token["side"].ToStringInvariant() == "Sell") result.Amount *= -1; return result; } - private ExchangeOrderResult ParseOrder(JToken token, string resultCode, string resultMessage) + private ExchangeOrderResult ParseOrder( + JToken token, + string resultCode, + string resultMessage + ) { /* Active Order: @@ -947,31 +1134,31 @@ private ExchangeOrderResult ParseOrder(JToken token, string resultCode, string r "ext_code": "", "ext_info": "", "result": { - "user_id": 106958, - "symbol": "BTCUSD", - "side": "Buy", - "order_type": "Limit", - "price": "11756.5", - "qty": 1, - "time_in_force": "PostOnly", - "order_status": "Filled", - "ext_fields": { - "o_req_num": -68948112492, - "xreq_type": "x_create" - }, - "last_exec_time": "1596304897.847944", - "last_exec_price": "11756.5", - "leaves_qty": 0, - "leaves_value": "0", - "cum_exec_qty": 1, - "cum_exec_value": "0.00008505", - "cum_exec_fee": "-0.00000002", - "reject_reason": "", - "cancel_type": "", - "order_link_id": "", - "created_at": "2020-08-01T18:00:26Z", - "updated_at": "2020-08-01T18:01:37Z", - "order_id": "e66b101a-ef3f-4647-83b5-28e0f38dcae0" + "user_id": 106958, + "symbol": "BTCUSD", + "side": "Buy", + "order_type": "Limit", + "price": "11756.5", + "qty": 1, + "time_in_force": "PostOnly", + "order_status": "Filled", + "ext_fields": { + "o_req_num": -68948112492, + "xreq_type": "x_create" + }, + "last_exec_time": "1596304897.847944", + "last_exec_price": "11756.5", + "leaves_qty": 0, + "leaves_value": "0", + "cum_exec_qty": 1, + "cum_exec_value": "0.00008505", + "cum_exec_fee": "-0.00000002", + "reject_reason": "", + "cancel_type": "", + "order_link_id": "", + "created_at": "2020-08-01T18:00:26Z", + "updated_at": "2020-08-01T18:01:37Z", + "order_id": "e66b101a-ef3f-4647-83b5-28e0f38dcae0" }, "time_now": "1597171013.867068", "rate_limit_status": 599, @@ -981,39 +1168,39 @@ private ExchangeOrderResult ParseOrder(JToken token, string resultCode, string r Active Order List: { - "ret_code": 0, - "ret_msg": "OK", - "ext_code": "", - "ext_info": "", - "result": { - "data": [ - { - "user_id": 160861, - "order_status": "Cancelled", - "symbol": "BTCUSD", - "side": "Buy", - "order_type": "Market", - "price": "9800", - "qty": "16737", - "time_in_force": "ImmediateOrCancel", - "order_link_id": "", - "order_id": "fead08d7-47c0-4d6a-b9e7-5c71d5df8ba1", - "created_at": "2020-07-24T08:22:30Z", - "updated_at": "2020-07-24T08:22:30Z", - "leaves_qty": "0", - "leaves_value": "0", - "cum_exec_qty": "0", - "cum_exec_value": "0", - "cum_exec_fee": "0", - "reject_reason": "EC_NoImmediateQtyToFill" - } - ], - "cursor": "w01XFyyZc8lhtCLl6NgAaYBRfsN9Qtpp1f2AUy3AS4+fFDzNSlVKa0od8DKCqgAn" - }, - "time_now": "1604653633.173848", - "rate_limit_status": 599, - "rate_limit_reset_ms": 1604653633171, - "rate_limit": 600 + "ret_code": 0, + "ret_msg": "OK", + "ext_code": "", + "ext_info": "", + "result": { + "data": [ + { + "user_id": 160861, + "order_status": "Cancelled", + "symbol": "BTCUSD", + "side": "Buy", + "order_type": "Market", + "price": "9800", + "qty": "16737", + "time_in_force": "ImmediateOrCancel", + "order_link_id": "", + "order_id": "fead08d7-47c0-4d6a-b9e7-5c71d5df8ba1", + "created_at": "2020-07-24T08:22:30Z", + "updated_at": "2020-07-24T08:22:30Z", + "leaves_qty": "0", + "leaves_value": "0", + "cum_exec_qty": "0", + "cum_exec_value": "0", + "cum_exec_fee": "0", + "reject_reason": "EC_NoImmediateQtyToFill" + } + ], + "cursor": "w01XFyyZc8lhtCLl6NgAaYBRfsN9Qtpp1f2AUy3AS4+fFDzNSlVKa0od8DKCqgAn" + }, + "time_now": "1604653633.173848", + "rate_limit_status": 599, + "rate_limit_reset_ms": 1604653633171, + "rate_limit": 600 } */ ExchangeOrderResult result = new ExchangeOrderResult(); @@ -1053,7 +1240,9 @@ private ExchangeOrderResult ParseOrder(JToken token, string resultCode, string r break; default: - throw new NotImplementedException($"Unexpected status type: {token["order_status"].ToStringInvariant()}"); + throw new NotImplementedException( + $"Unexpected status type: {token["order_status"].ToStringInvariant()}" + ); } } result.ResultCode = resultCode; @@ -1063,5 +1252,8 @@ private ExchangeOrderResult ParseOrder(JToken token, string resultCode, string r } } - public partial class ExchangeName { public const string Bybit = "Bybit"; } + public partial class ExchangeName + { + public const string Bybit = "Bybit"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitInverseAPI.cs b/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitInverseAPI.cs index 3250daaf8..403868cc2 100644 --- a/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitInverseAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitInverseAPI.cs @@ -9,13 +9,17 @@ public class ExchangeBybitInverseAPI : ExchangeBybitV5Base { protected override MarketCategory MarketCategory => MarketCategory.Inverse; public override string BaseUrlWebSocket => "wss://stream.bybit.com/v5/public/inverse"; - public ExchangeBybitInverseAPI() - { - } + + public ExchangeBybitInverseAPI() { } + public ExchangeBybitInverseAPI(bool isUnifiedAccount) { IsUnifiedAccount = isUnifiedAccount; } } - public partial class ExchangeName { public const string BybitInverse = "BybitInverse"; } + + public partial class ExchangeName + { + public const string BybitInverse = "BybitInverse"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitLinearAPI.cs b/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitLinearAPI.cs index 49458d09d..de6b2adce 100644 --- a/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitLinearAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitLinearAPI.cs @@ -1,7 +1,7 @@ -using ExchangeSharp.Bybit; using System; using System.Collections.Generic; using System.Text; +using ExchangeSharp.Bybit; namespace ExchangeSharp { @@ -9,13 +9,17 @@ public class ExchangeBybitLinearAPI : ExchangeBybitV5Base { protected override MarketCategory MarketCategory => MarketCategory.Linear; public override string BaseUrlWebSocket => "wss://stream.bybit.com/v5/public/linear"; - public ExchangeBybitLinearAPI() - { - } + + public ExchangeBybitLinearAPI() { } + public ExchangeBybitLinearAPI(bool isUnifiedAccount) { IsUnifiedAccount = isUnifiedAccount; } } - public partial class ExchangeName { public const string BybitLinear = "BybitLinear"; } + + public partial class ExchangeName + { + public const string BybitLinear = "BybitLinear"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitOptionAPI.cs b/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitOptionAPI.cs index fbeba34d0..103ef4be9 100644 --- a/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitOptionAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitOptionAPI.cs @@ -1,7 +1,7 @@ -using ExchangeSharp.Bybit; using System; using System.Collections.Generic; using System.Text; +using ExchangeSharp.Bybit; namespace ExchangeSharp { @@ -9,13 +9,17 @@ public class ExchangeBybitOptionAPI : ExchangeBybitV5Base { protected override MarketCategory MarketCategory => MarketCategory.Option; public override string BaseUrlWebSocket => "wss://stream.bybit.com/v5/public/option"; - public ExchangeBybitOptionAPI() - { - } + + public ExchangeBybitOptionAPI() { } + public ExchangeBybitOptionAPI(bool isUnifiedAccount) { IsUnifiedAccount = isUnifiedAccount; } } - public partial class ExchangeName { public const string BybitOption = "BybitOption"; } + + public partial class ExchangeName + { + public const string BybitOption = "BybitOption"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitSpotAPI.cs b/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitSpotAPI.cs index 2d2faf494..c0831b763 100644 --- a/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitSpotAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitSpotAPI.cs @@ -1,7 +1,7 @@ -using ExchangeSharp.Bybit; using System; using System.Collections.Generic; using System.Text; +using ExchangeSharp.Bybit; namespace ExchangeSharp { @@ -9,13 +9,17 @@ public class ExchangeBybitSpotAPI : ExchangeBybitV5Base { protected override MarketCategory MarketCategory => MarketCategory.Spot; public override string BaseUrlWebSocket => "wss://stream.bybit.com/v5/public/spot"; - public ExchangeBybitSpotAPI() - { - } + + public ExchangeBybitSpotAPI() { } + public ExchangeBybitSpotAPI(bool isUnified) { IsUnifiedAccount = isUnified; } } - public partial class ExchangeName { public const string BybitSpot = "BybitSpot"; } + + public partial class ExchangeName + { + public const string BybitSpot = "BybitSpot"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitV5Base.cs b/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitV5Base.cs index 9c4eb81a9..7e36c8b12 100644 --- a/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitV5Base.cs +++ b/src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitV5Base.cs @@ -1,11 +1,11 @@ -using Newtonsoft.Json.Linq; -using Newtonsoft.Json; using System; using System.Collections.Generic; +using System.Linq; using System.Text; -using ExchangeSharp.Bybit; using System.Threading.Tasks; -using System.Linq; +using ExchangeSharp.Bybit; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace ExchangeSharp { @@ -17,10 +17,11 @@ public abstract class ExchangeBybitV5Base : ExchangeAPI /// /// Can be one of: linear, inverse, option, spot /// - protected virtual MarketCategory MarketCategory => throw new NotImplementedException("MarketCategory"); + protected virtual MarketCategory MarketCategory => + throw new NotImplementedException("MarketCategory"); /// - /// Account status (is account Unified) needed in some private end-points (e.g. OnGetAmountsAvailableToTradeAsync or GetRecentOrderAsync). + /// Account status (is account Unified) needed in some private end-points (e.g. OnGetAmountsAvailableToTradeAsync or GetRecentOrderAsync). /// Better be set with constructor. Also it can be set explicitly or GetAccountInfo() can be used to get the account status. /// public virtual bool? IsUnifiedAccount { get; set; } @@ -41,18 +42,22 @@ public ExchangeBybitV5Base() protected override async Task OnGetNonceOffset() { /* - * https://bybit-exchange.github.io/docs/v5/intro#parameters-for-authenticated-endpoints - Please make sure that the timestamp parameter adheres to the following rule: - server_time - recv_window <= timestamp < server_time + 1000 - */ + * https://bybit-exchange.github.io/docs/v5/intro#parameters-for-authenticated-endpoints + Please make sure that the timestamp parameter adheres to the following rule: + server_time - recv_window <= timestamp < server_time + 1000 + */ try { JToken token = await MakeJsonRequestAsync(NonceEndPoint!); JToken value = token[NonceEndPointField]; - DateTime serverDate = value.ConvertInvariant().UnixTimeStampToDateTimeNanoseconds(); + DateTime serverDate = value + .ConvertInvariant() + .UnixTimeStampToDateTimeNanoseconds(); NonceOffset = (CryptoUtility.UtcNow - serverDate) + TimeSpan.FromSeconds(1); - Logger.Info($"Nonce offset set for {Name}: {NonceOffset.TotalMilliseconds} milisec"); + Logger.Info( + $"Nonce offset set for {Name}: {NonceOffset.TotalMilliseconds} milisec" + ); } catch { @@ -60,6 +65,7 @@ protected override async Task OnGetNonceOffset() Logger.Warn($"Failed to get nonce offset for {Name}"); } } + protected override async Task> GetNoncePayloadAsync() { return new Dictionary @@ -68,17 +74,31 @@ protected override async Task> GetNoncePayloadAsync() ["category"] = MarketCategory.ToStringLowerInvariant() }; } - protected override Uri ProcessRequestUrl(UriBuilder url, Dictionary payload, string method) + + protected override Uri ProcessRequestUrl( + UriBuilder url, + Dictionary payload, + string method + ) { if (payload != null && payload.Count > 1 && method == "GET") { - string query = CryptoUtility.GetFormForPayload(payload, includeNonce: false, orderByKey: true, formEncode: false); + string query = CryptoUtility.GetFormForPayload( + payload, + includeNonce: false, + orderByKey: true, + formEncode: false + ); query = query.Replace("=True", "=true").Replace("=False", "=false"); url.Query = query; } return url.Uri; } - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + + protected override async Task ProcessRequestAsync( + IHttpWebRequest request, + Dictionary payload + ) { if (CanMakeAuthenticatedRequest(payload)) { @@ -96,7 +116,10 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti { toSign += request.RequestUri.Query.Substring(1); } - string signature = CryptoUtility.SHA256Sign(toSign, PrivateApiKey.ToUnsecureString()); + string signature = CryptoUtility.SHA256Sign( + toSign, + PrivateApiKey.ToUnsecureString() + ); request.AddHeader("X-BAPI-SIGN", signature); request.AddHeader("X-BAPI-API-KEY", PublicApiKey.ToUnsecureString()); request.AddHeader("X-BAPI-TIMESTAMP", nonce); @@ -107,6 +130,7 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti } } } + protected override JToken CheckJsonResponse(JToken result) { int retCode = result["retCode"].ConvertInvariant(); @@ -120,10 +144,14 @@ protected override JToken CheckJsonResponse(JToken result) #region Public - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() + protected internal override async Task< + IEnumerable + > OnGetMarketSymbolsMetadataAsync() { List markets = new List(); - var responseToken = await MakeJsonRequestAsync($"/v5/market/instruments-info?category={MarketCategory.ToStringLowerInvariant()}"); + var responseToken = await MakeJsonRequestAsync( + $"/v5/market/instruments-info?category={MarketCategory.ToStringLowerInvariant()}" + ); foreach (var marketJson in responseToken["list"]) { @@ -139,12 +167,18 @@ protected internal override async Task> OnGetMarketS MinPrice = priceFilter["minPrice"].ConvertInvariant(), MaxPrice = priceFilter["maxPrice"].ConvertInvariant(), PriceStepSize = priceFilter["tickSize"].ConvertInvariant(), - IsActive = marketJson["status"] == null || marketJson["status"].ToStringLowerInvariant() == "trading" + IsActive = + marketJson["status"] == null + || marketJson["status"].ToStringLowerInvariant() == "trading" }; if (isInverse) { - market.MinTradeSizeInQuoteCurrency = sizeFilter["minOrderQty"].ConvertInvariant(); - market.MaxTradeSizeInQuoteCurrency = sizeFilter["maxOrderQty"].ConvertInvariant(); + market.MinTradeSizeInQuoteCurrency = sizeFilter[ + "minOrderQty" + ].ConvertInvariant(); + market.MaxTradeSizeInQuoteCurrency = sizeFilter[ + "maxOrderQty" + ].ConvertInvariant(); } else { @@ -155,7 +189,14 @@ protected internal override async Task> OnGetMarketS } return markets; } - protected override async Task> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + + protected override async Task> OnGetCandlesAsync( + string marketSymbol, + int periodSeconds, + DateTime? startDate = null, + DateTime? endDate = null, + int? limit = null + ) { string url = "/v5/market/kline"; int maxLimit = 200; @@ -171,26 +212,58 @@ protected override async Task> OnGetCandlesAsync(strin endDate ??= CryptoUtility.UtcNow; startDate = endDate - TimeSpan.FromMinutes(limit.Value * periodSeconds / 60); } - url += $"?category={MarketCategory.ToStringLowerInvariant()}&symbol={marketSymbol}&interval={periodString}&limit={limit.Value.ToStringInvariant()}&start={((long)startDate.Value.UnixTimestampFromDateTimeMilliseconds()).ToStringInvariant()}"; + url += + $"?category={MarketCategory.ToStringLowerInvariant()}&symbol={marketSymbol}&interval={periodString}&limit={limit.Value.ToStringInvariant()}&start={((long)startDate.Value.UnixTimestampFromDateTimeMilliseconds()).ToStringInvariant()}"; var responseJson = await MakeJsonRequestAsync(url); var baseVolKey = MarketCategory == MarketCategory.Inverse ? 6 : 5; var quoteVolKey = MarketCategory == MarketCategory.Inverse ? 5 : 6; foreach (var token in responseJson["list"]) { - candles.Add(this.ParseCandle(token, marketSymbol, periodSeconds, openKey: 1, highKey: 2, lowKey: 3, closeKey: 4, timestampKey: 0, timestampType: TimestampType.UnixMilliseconds, baseVolumeKey: baseVolKey, quoteVolumeKey: quoteVolKey)); + candles.Add( + this.ParseCandle( + token, + marketSymbol, + periodSeconds, + openKey: 1, + highKey: 2, + lowKey: 3, + closeKey: 4, + timestampKey: 0, + timestampType: TimestampType.UnixMilliseconds, + baseVolumeKey: baseVolKey, + quoteVolumeKey: quoteVolKey + ) + ); } return candles; } - protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) + + protected override async Task OnGetOrderBookAsync( + string marketSymbol, + int maxCount = 100 + ) { - var upperLimit = MarketCategory == MarketCategory.Linear || MarketCategory == MarketCategory.Inverse ? 200 : - MarketCategory == MarketCategory.Spot ? 50 : 25; + var upperLimit = + MarketCategory == MarketCategory.Linear || MarketCategory == MarketCategory.Inverse + ? 200 + : MarketCategory == MarketCategory.Spot + ? 50 + : 25; var limit = Math.Min(maxCount, upperLimit); - string url = $"/v5/market/orderbook?category={MarketCategory.ToStringLowerInvariant()}&symbol={marketSymbol}&limit={limit}"; + string url = + $"/v5/market/orderbook?category={MarketCategory.ToStringLowerInvariant()}&symbol={marketSymbol}&limit={limit}"; JToken token = await MakeJsonRequestAsync(url); - var book = ExchangeAPIExtensions.ParseOrderBookFromJTokenArrays(token, asks: "a", bids: "b", sequence: "u"); - book.LastUpdatedUtc = CryptoUtility.ParseTimestamp(token["ts"], TimestampType.UnixMilliseconds); + var book = ExchangeAPIExtensions.ParseOrderBookFromJTokenArrays( + token, + asks: "a", + bids: "b", + sequence: "u" + ); + book.LastUpdatedUtc = CryptoUtility.ParseTimestamp( + token["ts"], + TimestampType.UnixMilliseconds + ); book.ExchangeName = Name; book.MarketSymbol = token["s"].ToStringInvariant(); book.IsFromSnapshot = true; @@ -202,35 +275,62 @@ protected override async Task OnGetOrderBookAsync(string mark #region Private /// - /// Account status (is account Unified) needed in some private end-points (e.g. OnGetAmountsAvailableToTradeAsync or GetRecentOrderAsync). + /// Account status (is account Unified) needed in some private end-points (e.g. OnGetAmountsAvailableToTradeAsync or GetRecentOrderAsync). /// Better be set with constructor. If it's not set, this method will be used to get the account status. /// public async Task GetAccountUnifiedStatusAsync() { - JObject result = await MakeJsonRequestAsync("/v5/user/query-api", null, await GetNoncePayloadAsync()); - IsUnifiedAccount = result["unified"].ConvertInvariant() == 1 || result["uta"].ConvertInvariant() == 1; + JObject result = await MakeJsonRequestAsync( + "/v5/user/query-api", + null, + await GetNoncePayloadAsync() + ); + IsUnifiedAccount = + result["unified"].ConvertInvariant() == 1 + || result["uta"].ConvertInvariant() == 1; } + public async Task GetAPIKeyExpirationDateAsync() { - JObject result = await MakeJsonRequestAsync("/v5/user/query-api", null, await GetNoncePayloadAsync()); + JObject result = await MakeJsonRequestAsync( + "/v5/user/query-api", + null, + await GetNoncePayloadAsync() + ); return CryptoUtility.ParseTimestamp(result["expiredAt"], TimestampType.Iso8601UTC); } - protected override async Task> OnGetAmountsAvailableToTradeAsync() + + protected override async Task< + Dictionary + > OnGetAmountsAvailableToTradeAsync() { if (IsUnifiedAccount == null) { await GetAccountUnifiedStatusAsync(); } var payload = await GetNoncePayloadAsync(); - string accType = MarketCategory == MarketCategory.Inverse ? "CONTRACT" : - IsUnifiedAccount.Value ? "UNIFIED" : - MarketCategory == MarketCategory.Spot ? "SPOT" : "CONTRACT"; + string accType = + MarketCategory == MarketCategory.Inverse + ? "CONTRACT" + : IsUnifiedAccount.Value + ? "UNIFIED" + : MarketCategory == MarketCategory.Spot + ? "SPOT" + : "CONTRACT"; payload["accountType"] = accType; - JObject result = await MakeJsonRequestAsync("/v5/account/wallet-balance", null, payload); - Dictionary amounts = new Dictionary(StringComparer.OrdinalIgnoreCase); - - var accountBalances = result["list"].FirstOrDefault(i => i["accountType"].ToStringInvariant() == accType); + JObject result = await MakeJsonRequestAsync( + "/v5/account/wallet-balance", + null, + payload + ); + Dictionary amounts = new Dictionary( + StringComparer.OrdinalIgnoreCase + ); + + var accountBalances = result["list"].FirstOrDefault( + i => i["accountType"].ToStringInvariant() == accType + ); if (accountBalances == null) { return amounts; @@ -238,7 +338,10 @@ protected override async Task> OnGetAmountsAvailable if (IsUnifiedAccount.Value) { // All assets that can be used as collateral, converted to USD, will be here - amounts.Add("USD", accountBalances["totalAvailableBalance"].ConvertInvariant()); + amounts.Add( + "USD", + accountBalances["totalAvailableBalance"].ConvertInvariant() + ); } string balanceKey = accType == "SPOT" ? "free" : "availableToWithdraw"; foreach (var coin in accountBalances["coin"]) @@ -252,7 +355,10 @@ protected override async Task> OnGetAmountsAvailable } return amounts; } - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + + protected override async Task OnPlaceOrderAsync( + ExchangeOrderRequest order + ) { Dictionary payload = await GetNoncePayloadAsync(); payload.Add("symbol", order.MarketSymbol); @@ -265,7 +371,9 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd } else if (MarketCategory == MarketCategory.Option) { - throw new ArgumentNullException("OrderLinkId is required for market category 'Option'"); + throw new ArgumentNullException( + "OrderLinkId is required for market category 'Option'" + ); } if (order.Price > 0) { @@ -291,7 +399,12 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd } order.ExtraParameters.CopyTo(payload); - JToken token = await MakeJsonRequestAsync("/v5/order/create", null, payload, "POST"); + JToken token = await MakeJsonRequestAsync( + "/v5/order/create", + null, + payload, + "POST" + ); ExchangeOrderResult orderResult = new ExchangeOrderResult() { Amount = order.Amount, @@ -303,7 +416,12 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd }; return orderResult; } - protected override async Task OnCancelOrderAsync(string orderId, string? marketSymbol = null, bool isClientOrderId = false) + + protected override async Task OnCancelOrderAsync( + string orderId, + string? marketSymbol = null, + bool isClientOrderId = false + ) { Dictionary payload = await GetNoncePayloadAsync(); payload.Add("symbol", marketSymbol); @@ -321,8 +439,11 @@ protected override async Task OnCancelOrderAsync(string orderId, string? marketS } catch (APIException e) { - // Spot STOP orders need to be cancelled in a specific way - if (MarketCategory == MarketCategory.Spot && (e.Message.Contains("170145") || e.Message.Contains("170213"))) // 170145 - This order type does not support cancellation, 170213 - Order does not exist. + // Spot STOP orders need to be cancelled in a specific way + if ( + MarketCategory == MarketCategory.Spot + && (e.Message.Contains("170145") || e.Message.Contains("170213")) + ) // 170145 - This order type does not support cancellation, 170213 - Order does not exist. { payload = await GetNoncePayloadAsync(); payload.Add("symbol", marketSymbol); @@ -337,17 +458,30 @@ protected override async Task OnCancelOrderAsync(string orderId, string? marketS payload.Add("orderFilter", "tpslOrder"); await MakeJsonRequestAsync("/v5/order/cancel", null, payload, "POST"); } - else throw; + else + throw; } } - protected override async Task OnGetOrderDetailsAsync(string orderId, string? marketSymbol = null, bool isClientOrderId = false) + + protected override async Task OnGetOrderDetailsAsync( + string orderId, + string? marketSymbol = null, + bool isClientOrderId = false + ) { // WARNING: This method can't query spot STOP orders. // Need to add bool paramater to distinguish stop orders and then if order is stop add 'orderFilter' to payload. Like in GetRecentOrderAsync() Dictionary payload = await GetNoncePayloadAsync(); - if((MarketCategory == MarketCategory.Linear || MarketCategory == MarketCategory.Inverse) && string.IsNullOrEmpty(marketSymbol)) + if ( + ( + MarketCategory == MarketCategory.Linear + || MarketCategory == MarketCategory.Inverse + ) && string.IsNullOrEmpty(marketSymbol) + ) { - throw new ArgumentNullException("marketSymbol is null. For linear & inverse, either symbol or settleCoin is required"); + throw new ArgumentNullException( + "marketSymbol is null. For linear & inverse, either symbol or settleCoin is required" + ); } payload.Add("symbol", marketSymbol); if (isClientOrderId) @@ -362,13 +496,25 @@ protected override async Task OnGetOrderDetailsAsync(string JToken obj = await MakeJsonRequestAsync("/v5/order/history", null, payload); var item = obj?["list"]?.FirstOrDefault(); return item == null ? null : ParseOrder(item); - } - public async Task GetRecentOrderAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false, bool? isStop = null) + + public async Task GetRecentOrderAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false, + bool? isStop = null + ) { - if ((MarketCategory == MarketCategory.Linear || MarketCategory == MarketCategory.Inverse) && string.IsNullOrEmpty(marketSymbol)) + if ( + ( + MarketCategory == MarketCategory.Linear + || MarketCategory == MarketCategory.Inverse + ) && string.IsNullOrEmpty(marketSymbol) + ) { - throw new ArgumentNullException("marketSymbol is null. For linear & inverse, either symbol or settleCoin is required"); + throw new ArgumentNullException( + "marketSymbol is null. For linear & inverse, either symbol or settleCoin is required" + ); } if (IsUnifiedAccount == null) { @@ -376,8 +522,13 @@ public async Task GetRecentOrderAsync(string orderId, strin } Dictionary payload = await GetNoncePayloadAsync(); payload.Add("symbol", marketSymbol); - int openOnlyInt = MarketCategory == MarketCategory.Spot && !IsUnifiedAccount.Value ? 0 : - MarketCategory == MarketCategory.Inverse || (MarketCategory == MarketCategory.Linear && !IsUnifiedAccount.Value) ? 2 : 1; + int openOnlyInt = + MarketCategory == MarketCategory.Spot && !IsUnifiedAccount.Value + ? 0 + : MarketCategory == MarketCategory.Inverse + || (MarketCategory == MarketCategory.Linear && !IsUnifiedAccount.Value) + ? 2 + : 1; if (MarketCategory == MarketCategory.Spot && !IsUnifiedAccount.Value) { if (isStop == null) @@ -404,11 +555,14 @@ public async Task GetRecentOrderAsync(string orderId, strin var item = obj["list"]?.FirstOrDefault(); return item == null ? null : ParseOrder(item); } + /// /// Returns open, partially filled orders, also cancelled, rejected or totally filled orders by last 10 minutes for linear, inverse and spot(unified). /// Spot(not unified) returns only open orders /// - protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) + protected override async Task> OnGetOpenOrderDetailsAsync( + string marketSymbol = null + ) { Dictionary payload = await GetNoncePayloadAsync(); payload.Add("symbol", marketSymbol); @@ -428,77 +582,104 @@ protected override async Task> OnGetOpenOrderDe #region WebSockets Public - protected override async Task OnGetCandlesWebSocketAsync(Func callbackAsync, int periodSeconds, params string[] marketSymbols) + protected override async Task OnGetCandlesWebSocketAsync( + Func callbackAsync, + int periodSeconds, + params string[] marketSymbols + ) { - return await ConnectPublicWebSocketAsync(url: null, - messageCallback: async (_socket, msg) => - { - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - string topic = token["topic"]?.ToStringInvariant(); - if (string.IsNullOrWhiteSpace(topic)) + return await ConnectPublicWebSocketAsync( + url: null, + messageCallback: async (_socket, msg) => { - string op = token["op"]?.ToStringInvariant(); - if (op == "subscribe") + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + string topic = token["topic"]?.ToStringInvariant(); + if (string.IsNullOrWhiteSpace(topic)) { - if (token["success"].ConvertInvariant()) - { - Logger.Info($"Subscribed to candles websocket on {Name}"); - } - else + string op = token["op"]?.ToStringInvariant(); + if (op == "subscribe") { - Logger.Error($"Error subscribing to candles websocket on {Name}: {token["ret_msg"].ToStringInvariant()}"); + if (token["success"].ConvertInvariant()) + { + Logger.Info($"Subscribed to candles websocket on {Name}"); + } + else + { + Logger.Error( + $"Error subscribing to candles websocket on {Name}: {token["ret_msg"].ToStringInvariant()}" + ); + } } + return; } - return; - } - var strArray = topic.Split('.'); - string symbol = strArray[strArray.Length - 1]; - int periodSecondsInResponse = StringToPeriodSeconds(strArray[strArray.Length - 2]); + var strArray = topic.Split('.'); + string symbol = strArray[strArray.Length - 1]; + int periodSecondsInResponse = StringToPeriodSeconds( + strArray[strArray.Length - 2] + ); - foreach (var item in token["data"] as JArray) + foreach (var item in token["data"] as JArray) + { + MarketCandle candle = this.ParseCandle( + item, + symbol, + periodSecondsInResponse, + openKey: "open", + highKey: "high", + lowKey: "low", + closeKey: "close", + timestampKey: "start", + timestampType: TimestampType.UnixMilliseconds, + baseVolumeKey: MarketCategory == MarketCategory.Inverse + ? "turnover" + : "volume", + quoteVolumeKey: MarketCategory == MarketCategory.Inverse + ? "volume" + : "turnover" + ); + candle.IsClosed = item["confirm"].ConvertInvariant(); + await callbackAsync(candle); + } + }, + connectCallback: async (_socket) => { - MarketCandle candle = this.ParseCandle(item, symbol, periodSecondsInResponse, openKey: "open", highKey: "high", lowKey: "low", closeKey: "close", - timestampKey: "start", timestampType: TimestampType.UnixMilliseconds, - baseVolumeKey: MarketCategory == MarketCategory.Inverse ? "turnover" : "volume", - quoteVolumeKey: MarketCategory == MarketCategory.Inverse ? "volume" : "turnover"); - candle.IsClosed = item["confirm"].ConvertInvariant(); - await callbackAsync(candle); - } - }, - connectCallback: async (_socket) => - { - string interval = PeriodSecondsToString(periodSeconds); - var subscribeRequest = new + string interval = PeriodSecondsToString(periodSeconds); + var subscribeRequest = new + { + op = "subscribe", + args = marketSymbols.Select(s => $"kline.{interval}.{s}").ToArray() + }; + await _socket.SendMessageAsync(subscribeRequest); + }, + disconnectCallback: async (_socket) => { - op = "subscribe", - args = marketSymbols.Select(s => $"kline.{interval}.{s}").ToArray() - }; - await _socket.SendMessageAsync(subscribeRequest); - }, - disconnectCallback: async (_socket) => - { - Logger.Info($"Websocket for candles on {Name} disconnected"); - }); + Logger.Info($"Websocket for candles on {Name} disconnected"); + } + ); } - protected override async Task OnGetDeltaOrderBookWebSocketAsync(Action callback, int maxCount = 20, params string[] marketSymbols) + protected override async Task OnGetDeltaOrderBookWebSocketAsync( + Action callback, + int maxCount = 20, + params string[] marketSymbols + ) { /* - ABOUT DEPTH: - Linear & inverse: - Level 1 data, push frequency: 10ms - Level 50 data, push frequency: 20ms - Level 200 data, push frequency: 100ms - Level 500 data, push frequency: 100ms - - Spot: - Level 1 data, push frequency: 10ms - Level 50 data, push frequency: 20ms - - Option: - Level 25 data, push frequency: 20ms - Level 100 data, push frequency: 100ms - */ + ABOUT DEPTH: + Linear & inverse: + Level 1 data, push frequency: 10ms + Level 50 data, push frequency: 20ms + Level 200 data, push frequency: 100ms + Level 500 data, push frequency: 100ms + + Spot: + Level 1 data, push frequency: 10ms + Level 50 data, push frequency: 20ms + + Option: + Level 25 data, push frequency: 20ms + Level 100 data, push frequency: 100ms + */ int depth; if (MarketCategory == MarketCategory.Linear || MarketCategory == MarketCategory.Inverse) { @@ -512,48 +693,60 @@ protected override async Task OnGetDeltaOrderBookWebSocketAsync(Acti { depth = maxCount == 100 ? maxCount : 25; //Depth 25 by default } - return await ConnectPublicWebSocketAsync(url: null, - messageCallback: (_socket, msg) => - { - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - string topic = token["topic"]?.ToStringInvariant(); - if (string.IsNullOrWhiteSpace(topic)) + return await ConnectPublicWebSocketAsync( + url: null, + messageCallback: (_socket, msg) => { - string op = token["op"]?.ToStringInvariant(); - if (op == "subscribe") + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + string topic = token["topic"]?.ToStringInvariant(); + if (string.IsNullOrWhiteSpace(topic)) { - if (token["success"].ConvertInvariant()) - { - Logger.Info($"Subscribed to orderbook websocket on {Name}"); - } - else + string op = token["op"]?.ToStringInvariant(); + if (op == "subscribe") { - Logger.Error($"Error subscribing to orderbook websocket on {Name}: {token["ret_msg"].ToStringInvariant()}"); + if (token["success"].ConvertInvariant()) + { + Logger.Info($"Subscribed to orderbook websocket on {Name}"); + } + else + { + Logger.Error( + $"Error subscribing to orderbook websocket on {Name}: {token["ret_msg"].ToStringInvariant()}" + ); + } } + return Task.CompletedTask; } + var book = ExchangeAPIExtensions.ParseOrderBookFromJTokenArrays( + token["data"], + asks: "a", + bids: "b", + sequence: "u" + ); + book.LastUpdatedUtc = CryptoUtility.ParseTimestamp( + token["ts"], + TimestampType.UnixMilliseconds + ); + book.ExchangeName = Name; + book.MarketSymbol = token["data"]["s"].ToStringInvariant(); + book.IsFromSnapshot = token["type"].ToStringInvariant() == "snapshot"; + callback(book); return Task.CompletedTask; - } - var book = ExchangeAPIExtensions.ParseOrderBookFromJTokenArrays(token["data"], asks: "a", bids: "b", sequence: "u"); - book.LastUpdatedUtc = CryptoUtility.ParseTimestamp(token["ts"], TimestampType.UnixMilliseconds); - book.ExchangeName = Name; - book.MarketSymbol = token["data"]["s"].ToStringInvariant(); - book.IsFromSnapshot = token["type"].ToStringInvariant() == "snapshot"; - callback(book); - return Task.CompletedTask; - }, - connectCallback: async (_socket) => - { - var subscribeRequest = new + }, + connectCallback: async (_socket) => { - op = "subscribe", - args = marketSymbols.Select(s => $"orderbook.{depth}.{s}").ToArray() - }; - await _socket.SendMessageAsync(subscribeRequest); - }, - disconnectCallback: async (_socket) => - { - Logger.Info($"Websocket for orderbook on {Name} disconnected"); - }); + var subscribeRequest = new + { + op = "subscribe", + args = marketSymbols.Select(s => $"orderbook.{depth}.{s}").ToArray() + }; + await _socket.SendMessageAsync(subscribeRequest); + }, + disconnectCallback: async (_socket) => + { + Logger.Info($"Websocket for orderbook on {Name} disconnected"); + } + ); } #endregion Websockets Public @@ -562,78 +755,92 @@ protected override async Task OnGetDeltaOrderBookWebSocketAsync(Acti protected override async Task OnUserDataWebSocketAsync(Action callback) { - return await ConnectPrivateWebSocketAsync(url: null, - messageCallback: (_socket, msg) => - { - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - - var data = token["data"]; - if (data == null) + return await ConnectPrivateWebSocketAsync( + url: null, + messageCallback: (_socket, msg) => { - string op = token["op"]?.ToStringInvariant(); - if (op == "auth") + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + + var data = token["data"]; + if (data == null) { - if (token["success"].ConvertInvariant()) + string op = token["op"]?.ToStringInvariant(); + if (op == "auth") { - Logger.Info($"Authenticated to user data websocket on {Name}"); + if (token["success"].ConvertInvariant()) + { + Logger.Info($"Authenticated to user data websocket on {Name}"); + } + else + { + Logger.Error( + $"Error authenticating to user data websocket on {Name}: {token["ret_msg"].ToStringInvariant()}" + ); + } } - else + else if (op == "subscribe") { - Logger.Error($"Error authenticating to user data websocket on {Name}: {token["ret_msg"].ToStringInvariant()}"); + if (token["success"].ConvertInvariant()) + { + Logger.Info($"Subscribed to user data websocket on {Name}"); + } + else + { + Logger.Error( + $"Error subscribing to user data websocket on {Name}: {token["ret_msg"].ToStringInvariant()}" + ); + } } } - else if (op == "subscribe") + else { - if (token["success"].ConvertInvariant()) + var topic = token["topic"]?.ToStringInvariant(); + switch (topic) { - Logger.Info($"Subscribed to user data websocket on {Name}"); - } - else - { - Logger.Error($"Error subscribing to user data websocket on {Name}: {token["ret_msg"].ToStringInvariant()}"); + case "order": + foreach (var jObject in data) + { + callback(ParseOrder(jObject)); + } + break; + // Just orders for now, other topics can be added } } - } - else + return Task.CompletedTask; + }, + connectCallback: async (_socket) => { - var topic = token["topic"]?.ToStringInvariant(); - switch (topic) + long expires = + (long)DateTime.Now.UnixTimestampFromDateTimeMilliseconds() + 10000; //10 seconds for auth request + string stringToSign = $"GET/realtime{expires}"; + string signature = CryptoUtility.SHA256Sign( + stringToSign, + CryptoUtility.ToUnsecureString(PrivateApiKey) + ); + var authRequest = new { - case "order": - foreach (var jObject in data) - { - callback(ParseOrder(jObject)); - } - break; - // Just orders for now, other topics can be added - } - } - return Task.CompletedTask; - }, - connectCallback: async (_socket) => - { - long expires = (long)DateTime.Now.UnixTimestampFromDateTimeMilliseconds() + 10000; //10 seconds for auth request - string stringToSign = $"GET/realtime{expires}"; - string signature = CryptoUtility.SHA256Sign(stringToSign, CryptoUtility.ToUnsecureString(PrivateApiKey)); - var authRequest = new - { - op = "auth", - args = new string[] { CryptoUtility.ToUnsecureString(PublicApiKey), expires.ToStringInvariant(), signature } - }; - await _socket.SendMessageAsync(authRequest); - - var subscribeRequest = new + op = "auth", + args = new string[] + { + CryptoUtility.ToUnsecureString(PublicApiKey), + expires.ToStringInvariant(), + signature + } + }; + await _socket.SendMessageAsync(authRequest); + + var subscribeRequest = new + { + op = "subscribe", + args = new string[] { "order" } + }; + await _socket.SendMessageAsync(subscribeRequest); + }, + disconnectCallback: async (_socket) => { - op = "subscribe", - args = new string[] { "order" } - }; - await _socket.SendMessageAsync(subscribeRequest); - - }, - disconnectCallback: async (_socket) => - { - Logger.Info($"Socket for user data on {Name} disconnected"); - }); + Logger.Info($"Socket for user data on {Name} disconnected"); + } + ); } #endregion Websockets Private @@ -645,7 +852,8 @@ private ExchangeOrderResult ParseOrder(JToken jToken) string marketSymbol = jToken["symbol"].ToStringInvariant(); decimal executedValue = jToken["cumExecValue"].ConvertInvariant(); decimal amountFilled = jToken["cumExecQty"].ConvertInvariant(); - decimal averagePrice = (executedValue == 0 || amountFilled == 0) ? 0m : executedValue / amountFilled; + decimal averagePrice = + (executedValue == 0 || amountFilled == 0) ? 0m : executedValue / amountFilled; decimal triggerPrice = jToken["triggerPrice"]?.ConvertInvariant() ?? 0; decimal price = jToken["price"].ConvertInvariant(); ExchangeOrderResult order = new ExchangeOrderResult @@ -655,24 +863,44 @@ private ExchangeOrderResult ParseOrder(JToken jToken) AmountFilled = amountFilled, Price = price == 0 ? triggerPrice : price, Fees = jToken["cumExecFee"].ConvertInvariant(), - FeesCurrency = marketSymbol.EndsWith("USDT") && MarketCategory == MarketCategory.Linear ? "USDT" : null, + FeesCurrency = + marketSymbol.EndsWith("USDT") && MarketCategory == MarketCategory.Linear + ? "USDT" + : null, AveragePrice = averagePrice, IsBuy = jToken["side"].ToStringInvariant() == "Buy", - OrderDate = jToken["createdTime"] == null ? default : CryptoUtility.ParseTimestamp(jToken["createdTime"], TimestampType.UnixMilliseconds), + OrderDate = + jToken["createdTime"] == null + ? default + : CryptoUtility.ParseTimestamp( + jToken["createdTime"], + TimestampType.UnixMilliseconds + ), OrderId = jToken["orderId"]?.ToStringInvariant(), ClientOrderId = jToken["orderLinkId"]?.ToStringInvariant(), Result = StringToOrderStatus(jToken["orderStatus"].ToStringInvariant()) }; - if (order.Result == ExchangeAPIOrderResult.Filled || order.Result == ExchangeAPIOrderResult.Rejected || order.Result == ExchangeAPIOrderResult.Canceled) - { - order.CompletedDate = jToken["updatedTime"] == null ? (DateTime?)null : CryptoUtility.ParseTimestamp(jToken["updatedTime"], TimestampType.UnixMilliseconds); - if(order.Result == ExchangeAPIOrderResult.Filled) + if ( + order.Result == ExchangeAPIOrderResult.Filled + || order.Result == ExchangeAPIOrderResult.Rejected + || order.Result == ExchangeAPIOrderResult.Canceled + ) + { + order.CompletedDate = + jToken["updatedTime"] == null + ? (DateTime?)null + : CryptoUtility.ParseTimestamp( + jToken["updatedTime"], + TimestampType.UnixMilliseconds + ); + if (order.Result == ExchangeAPIOrderResult.Filled) { order.TradeDate = order.CompletedDate; } } return order; } + public override string PeriodSecondsToString(int seconds) { switch (seconds) @@ -699,6 +927,7 @@ public override string PeriodSecondsToString(int seconds) throw new ArgumentOutOfRangeException("seconds"); } } + private int StringToPeriodSeconds(string str) { switch (str) @@ -713,6 +942,7 @@ private int StringToPeriodSeconds(string str) return int.Parse(str) * 60; } } + protected ExchangeAPIOrderResult StringToOrderStatus(string orderStatus) { switch (orderStatus) diff --git a/src/ExchangeSharp/API/Exchanges/Bybit/Models/BybitTrade.cs b/src/ExchangeSharp/API/Exchanges/Bybit/Models/BybitTrade.cs index 11a1abe99..8bfa0d245 100644 --- a/src/ExchangeSharp/API/Exchanges/Bybit/Models/BybitTrade.cs +++ b/src/ExchangeSharp/API/Exchanges/Bybit/Models/BybitTrade.cs @@ -10,10 +10,10 @@ public class BybitTrade : ExchangeTrade /// Cross sequence (internal value) /// public long CrossSequence { get; set; } + public override string ToString() { return string.Format("{0},{1}", base.ToString(), CrossSequence); } - } } diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs index 29ef1ff45..bc9a7e6d1 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs @@ -12,15 +12,15 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp { - using ExchangeSharp.Coinbase; - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net; using System.Threading.Tasks; + using ExchangeSharp.Coinbase; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; public sealed partial class ExchangeCoinbaseAPI : ExchangeAPI { @@ -52,7 +52,6 @@ private ExchangeCoinbaseAPI() RateLimit = new RateGate(9, TimeSpan.FromSeconds(1)); // set to 9 to be safe } - private ExchangeOrderResult ParseFill(JToken result) { decimal amount = result["size"].ConvertInvariant(); @@ -87,9 +86,13 @@ private ExchangeOrderResult ParseOrder(JToken result) decimal amount = result["size"].ConvertInvariant(amountFilled); decimal price = result["price"].ConvertInvariant(); decimal stop_price = result["stop_price"].ConvertInvariant(); - decimal? averagePrice = (amountFilled <= 0m ? null : (decimal?)(executedValue / amountFilled)); + decimal? averagePrice = ( + amountFilled <= 0m ? null : (decimal?)(executedValue / amountFilled) + ); decimal fees = result["fill_fees"].ConvertInvariant(); - string marketSymbol = result["product_id"].ToStringInvariant(result["id"].ToStringInvariant()); + string marketSymbol = result["product_id"].ToStringInvariant( + result["id"].ToStringInvariant() + ); ExchangeOrderResult order = new ExchangeOrderResult { @@ -149,17 +152,24 @@ private ExchangeOrderResult ParseOrder(JToken result) order.Result = ExchangeAPIOrderResult.Canceled; break; default: - throw new NotImplementedException($"Unexpected status type: {result["status"].ToStringInvariant()}"); + throw new NotImplementedException( + $"Unexpected status type: {result["status"].ToStringInvariant()}" + ); } return order; } - protected override bool CanMakeAuthenticatedRequest(IReadOnlyDictionary payload) + protected override bool CanMakeAuthenticatedRequest( + IReadOnlyDictionary payload + ) { return base.CanMakeAuthenticatedRequest(payload) && Passphrase != null; } - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + protected override async Task ProcessRequestAsync( + IHttpWebRequest request, + Dictionary payload + ) { if (CanMakeAuthenticatedRequest(payload)) { @@ -168,14 +178,21 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti payload.Remove("nonce"); string form = CryptoUtility.GetJsonForPayload(payload); byte[] secret = CryptoUtility.ToBytesBase64Decode(PrivateApiKey); - string toHash = timestamp + request.Method.ToUpperInvariant() + request.RequestUri.PathAndQuery + form; + string toHash = + timestamp + + request.Method.ToUpperInvariant() + + request.RequestUri.PathAndQuery + + form; string signatureBase64String = CryptoUtility.SHA256SignBase64(toHash, secret); secret = null; toHash = null; request.AddHeader("CB-ACCESS-KEY", PublicApiKey.ToUnsecureString()); request.AddHeader("CB-ACCESS-SIGN", signatureBase64String); request.AddHeader("CB-ACCESS-TIMESTAMP", timestamp); - request.AddHeader("CB-ACCESS-PASSPHRASE", CryptoUtility.ToUnsecureString(Passphrase)); + request.AddHeader( + "CB-ACCESS-PASSPHRASE", + CryptoUtility.ToUnsecureString(Passphrase) + ); if (request.Method == "POST") { await CryptoUtility.WriteToRequestAsync(request, form); @@ -190,7 +207,9 @@ protected override void ProcessResponse(IHttpWebResponse response) cursorBefore = response.GetHeader("CB-BEFORE").FirstOrDefault(); } - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() + protected internal override async Task< + IEnumerable + > OnGetMarketSymbolsMetadataAsync() { var markets = new List(); JToken products = await MakeJsonRequestAsync("/products"); @@ -201,7 +220,11 @@ protected internal override async Task> OnGetMarketS MarketSymbol = product["id"].ToStringUpperInvariant(), QuoteCurrency = product["quote_currency"].ToStringUpperInvariant(), BaseCurrency = product["base_currency"].ToStringUpperInvariant(), - IsActive = string.Equals(product["status"].ToStringInvariant(), "online", StringComparison.OrdinalIgnoreCase), + IsActive = string.Equals( + product["status"].ToStringInvariant(), + "online", + StringComparison.OrdinalIgnoreCase + ), MinTradeSize = product["base_min_size"].ConvertInvariant(), MaxTradeSize = product["base_max_size"].ConvertInvariant(), PriceStepSize = product["quote_increment"].ConvertInvariant(), @@ -215,10 +238,14 @@ protected internal override async Task> OnGetMarketS protected override async Task> OnGetMarketSymbolsAsync() { - return (await GetMarketSymbolsMetadataAsync()).Where(market => market.IsActive ?? true).Select(market => market.MarketSymbol); + return (await GetMarketSymbolsMetadataAsync()) + .Where(market => market.IsActive ?? true) + .Select(market => market.MarketSymbol); } - protected override async Task> OnGetCurrenciesAsync() + protected override async Task< + IReadOnlyDictionary + > OnGetCurrenciesAsync() { var currencies = new Dictionary(); JToken products = await MakeJsonRequestAsync("/currencies"); @@ -240,16 +267,36 @@ protected override async Task> OnG protected override async Task OnGetTickerAsync(string marketSymbol) { - JToken ticker = await MakeJsonRequestAsync("/products/" + marketSymbol + "/ticker"); - return await this.ParseTickerAsync(ticker, marketSymbol, "ask", "bid", "price", "volume", null, "time", TimestampType.Iso8601UTC); + JToken ticker = await MakeJsonRequestAsync( + "/products/" + marketSymbol + "/ticker" + ); + return await this.ParseTickerAsync( + ticker, + marketSymbol, + "ask", + "bid", + "price", + "volume", + null, + "time", + TimestampType.Iso8601UTC + ); } - protected override async Task OnGetDepositAddressAsync(string symbol, bool forceRegenerate = false) + protected override async Task OnGetDepositAddressAsync( + string symbol, + bool forceRegenerate = false + ) { // Hack found here: https://github.com/coinbase/gdax-node/issues/91#issuecomment-352441654 + using Fiddler // Get coinbase accounts - JArray accounts = await this.MakeJsonRequestAsync("/coinbase-accounts", null, await GetNoncePayloadAsync(), "GET"); + JArray accounts = await this.MakeJsonRequestAsync( + "/coinbase-accounts", + null, + await GetNoncePayloadAsync(), + "GET" + ); foreach (JToken token in accounts) { @@ -257,345 +304,527 @@ protected override async Task OnGetDepositAddressAsync(s if (currency.Equals(symbol, StringComparison.InvariantCultureIgnoreCase)) { JToken accountWalletAddress = await this.MakeJsonRequestAsync( - $"/coinbase-accounts/{token["id"]}/addresses", - null, - await GetNoncePayloadAsync(), - "POST"); + $"/coinbase-accounts/{token["id"]}/addresses", + null, + await GetNoncePayloadAsync(), + "POST" + ); - return new ExchangeDepositDetails { Address = accountWalletAddress["address"].ToStringInvariant(), Currency = currency }; + return new ExchangeDepositDetails + { + Address = accountWalletAddress["address"].ToStringInvariant(), + Currency = currency + }; } - } throw new APIException($"Address not found for {symbol}"); } - protected override async Task>> OnGetTickersAsync() + protected override async Task< + IEnumerable> + > OnGetTickersAsync() { - Dictionary tickers = new Dictionary(StringComparer.OrdinalIgnoreCase); + Dictionary tickers = new Dictionary( + StringComparer.OrdinalIgnoreCase + ); System.Threading.ManualResetEvent evt = new System.Threading.ManualResetEvent(false); List symbols = (await GetMarketSymbolsAsync()).ToList(); // stupid Coinbase does not have a one shot API call for tickers outside of web sockets - using (var socket = await GetTickersWebSocketAsync((t) => - { - lock (tickers) - { - if (symbols.Count != 0) - { - foreach (var kv in t) - { - if (!tickers.ContainsKey(kv.Key)) + using ( + var socket = await GetTickersWebSocketAsync( + (t) => { - tickers[kv.Key] = kv.Value; - symbols.Remove(kv.Key); + lock (tickers) + { + if (symbols.Count != 0) + { + foreach (var kv in t) + { + if (!tickers.ContainsKey(kv.Key)) + { + tickers[kv.Key] = kv.Value; + symbols.Remove(kv.Key); + } + } + if (symbols.Count == 0) + { + evt.Set(); + } + } + } } - } - if (symbols.Count == 0) - { - evt.Set(); - } - } - } - } - )) + ) + ) { evt.WaitOne(10000); return tickers; } } - protected override Task OnGetDeltaOrderBookWebSocketAsync(Action callback, int maxCount = 20, params string[] marketSymbols) + protected override Task OnGetDeltaOrderBookWebSocketAsync( + Action callback, + int maxCount = 20, + params string[] marketSymbols + ) { - return ConnectPublicWebSocketAsync(string.Empty, (_socket, msg) => - { - string message = msg.ToStringFromUTF8(); - var book = new ExchangeOrderBook(); - - // string comparison on the json text for faster deserialization - // More likely to be an l2update so check for that first - if (message.Contains(@"""l2update""")) - { - // parse delta update - var delta = JsonConvert.DeserializeObject(message, SerializerSettings); - book.MarketSymbol = delta.ProductId; - book.SequenceId = delta.Time.Ticks; - foreach (string[] change in delta.Changes) + return ConnectPublicWebSocketAsync( + string.Empty, + (_socket, msg) => { - decimal price = change[1].ConvertInvariant(); - decimal amount = change[2].ConvertInvariant(); - if (change[0] == "buy") + string message = msg.ToStringFromUTF8(); + var book = new ExchangeOrderBook(); + + // string comparison on the json text for faster deserialization + // More likely to be an l2update so check for that first + if (message.Contains(@"""l2update""")) { - book.Bids[price] = new ExchangeOrderPrice { Amount = amount, Price = price }; + // parse delta update + var delta = JsonConvert.DeserializeObject( + message, + SerializerSettings + ); + book.MarketSymbol = delta.ProductId; + book.SequenceId = delta.Time.Ticks; + foreach (string[] change in delta.Changes) + { + decimal price = change[1].ConvertInvariant(); + decimal amount = change[2].ConvertInvariant(); + if (change[0] == "buy") + { + book.Bids[price] = new ExchangeOrderPrice + { + Amount = amount, + Price = price + }; + } + else + { + book.Asks[price] = new ExchangeOrderPrice + { + Amount = amount, + Price = price + }; + } + } + } + else if (message.Contains(@"""snapshot""")) + { + // parse snapshot + var snapshot = JsonConvert.DeserializeObject( + message, + SerializerSettings + ); + book.MarketSymbol = snapshot.ProductId; + foreach (decimal[] ask in snapshot.Asks) + { + decimal price = ask[0]; + decimal amount = ask[1]; + book.Asks[price] = new ExchangeOrderPrice + { + Amount = amount, + Price = price + }; + } + + foreach (decimal[] bid in snapshot.Bids) + { + decimal price = bid[0]; + decimal amount = bid[1]; + book.Bids[price] = new ExchangeOrderPrice + { + Amount = amount, + Price = price + }; + } } else { - book.Asks[price] = new ExchangeOrderPrice { Amount = amount, Price = price }; + // no other message type handled + return Task.CompletedTask; } - } - } - else if (message.Contains(@"""snapshot""")) - { - // parse snapshot - var snapshot = JsonConvert.DeserializeObject(message, SerializerSettings); - book.MarketSymbol = snapshot.ProductId; - foreach (decimal[] ask in snapshot.Asks) - { - decimal price = ask[0]; - decimal amount = ask[1]; - book.Asks[price] = new ExchangeOrderPrice { Amount = amount, Price = price }; - } - foreach (decimal[] bid in snapshot.Bids) + callback(book); + return Task.CompletedTask; + }, + async (_socket) => { - decimal price = bid[0]; - decimal amount = bid[1]; - book.Bids[price] = new ExchangeOrderPrice { Amount = amount, Price = price }; + // subscribe to order book channel for each symbol + if (marketSymbols == null || marketSymbols.Length == 0) + { + marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); + } + var chan = new Channel + { + Name = ChannelType.Level2, + ProductIds = marketSymbols.ToList() + }; + var channelAction = new ChannelAction + { + Type = ActionType.Subscribe, + Channels = new List { chan } + }; + await _socket.SendMessageAsync(channelAction); } - } - else - { - // no other message type handled - return Task.CompletedTask; - } - - callback(book); - return Task.CompletedTask; - }, async (_socket) => - { - // subscribe to order book channel for each symbol - if (marketSymbols == null || marketSymbols.Length == 0) - { - marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); - } - var chan = new Channel { Name = ChannelType.Level2, ProductIds = marketSymbols.ToList() }; - var channelAction = new ChannelAction { Type = ActionType.Subscribe, Channels = new List { chan } }; - await _socket.SendMessageAsync(channelAction); - }); + ); } - protected override async Task OnGetTickersWebSocketAsync(Action>> callback, params string[] marketSymbols) + protected override async Task OnGetTickersWebSocketAsync( + Action>> callback, + params string[] marketSymbols + ) { - return await ConnectPublicWebSocketAsync("/", async (_socket, msg) => - { - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - if (token["type"].ToStringInvariant() == "ticker") - { - ExchangeTicker ticker = await this.ParseTickerAsync(token, token["product_id"].ToStringInvariant(), "best_ask", "best_bid", "price", "volume_24h", null, "time", TimestampType.Iso8601UTC); - callback(new List>() { new KeyValuePair(token["product_id"].ToStringInvariant(), ticker) }); - } - }, async (_socket) => - { - marketSymbols = marketSymbols == null || marketSymbols.Length == 0 ? (await GetMarketSymbolsAsync()).ToArray() : marketSymbols; - var subscribeRequest = new - { - type = "subscribe", - product_ids = marketSymbols, - channels = new object[] + return await ConnectPublicWebSocketAsync( + "/", + async (_socket, msg) => { - new + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + if (token["type"].ToStringInvariant() == "ticker") { - name = "ticker", - product_ids = marketSymbols.ToArray() + ExchangeTicker ticker = await this.ParseTickerAsync( + token, + token["product_id"].ToStringInvariant(), + "best_ask", + "best_bid", + "price", + "volume_24h", + null, + "time", + TimestampType.Iso8601UTC + ); + callback( + new List>() + { + new KeyValuePair( + token["product_id"].ToStringInvariant(), + ticker + ) + } + ); } + }, + async (_socket) => + { + marketSymbols = + marketSymbols == null || marketSymbols.Length == 0 + ? (await GetMarketSymbolsAsync()).ToArray() + : marketSymbols; + var subscribeRequest = new + { + type = "subscribe", + product_ids = marketSymbols, + channels = new object[] + { + new { name = "ticker", product_ids = marketSymbols.ToArray() } + } + }; + await _socket.SendMessageAsync(subscribeRequest); } - }; - await _socket.SendMessageAsync(subscribeRequest); - }); + ); } - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + protected override async Task OnGetTradesWebSocketAsync( + Func, Task> callback, + params string[] marketSymbols + ) { if (marketSymbols == null || marketSymbols.Length == 0) { marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); } - return await ConnectPublicWebSocketAsync("/", async (_socket, msg) => - { - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - if (token["type"].ToStringInvariant() == "error") - { // {{ "type": "error", "message": "Failed to subscribe", "reason": "match is not a valid channel" }} - Logger.Info(token["message"].ToStringInvariant() + ": " + token["reason"].ToStringInvariant()); - return; - } - if (token["type"].ToStringInvariant() != "match") return; //the ticker channel provides the trade information as well - if (token["time"] == null) return; - ExchangeTrade trade = ParseTradeWebSocket(token); - string marketSymbol = token["product_id"].ToStringInvariant(); - await callback(new KeyValuePair(marketSymbol, trade)); - }, async (_socket) => - { - var subscribeRequest = new - { - type = "subscribe", - product_ids = marketSymbols, - channels = new object[] + return await ConnectPublicWebSocketAsync( + "/", + async (_socket, msg) => { - new - { - name = "matches", - product_ids = marketSymbols + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + if (token["type"].ToStringInvariant() == "error") + { // {{ "type": "error", "message": "Failed to subscribe", "reason": "match is not a valid channel" }} + Logger.Info( + token["message"].ToStringInvariant() + + ": " + + token["reason"].ToStringInvariant() + ); + return; } + if (token["type"].ToStringInvariant() != "match") + return; //the ticker channel provides the trade information as well + if (token["time"] == null) + return; + ExchangeTrade trade = ParseTradeWebSocket(token); + string marketSymbol = token["product_id"].ToStringInvariant(); + await callback(new KeyValuePair(marketSymbol, trade)); + }, + async (_socket) => + { + var subscribeRequest = new + { + type = "subscribe", + product_ids = marketSymbols, + channels = new object[] + { + new { name = "matches", product_ids = marketSymbols } + } + }; + await _socket.SendMessageAsync(subscribeRequest); } - }; - await _socket.SendMessageAsync(subscribeRequest); - }); + ); } private ExchangeTrade ParseTradeWebSocket(JToken token) { - return token.ParseTradeCoinbase("size", "price", "side", "time", TimestampType.Iso8601UTC, "trade_id"); + return token.ParseTradeCoinbase( + "size", + "price", + "side", + "time", + TimestampType.Iso8601UTC, + "trade_id" + ); } protected override async Task OnUserDataWebSocketAsync(Action callback) { - return await ConnectPublicWebSocketAsync("/", async (_socket, msg) => - { - var token = msg.ToStringFromUTF8(); - var response = JsonConvert.DeserializeObject(token, SerializerSettings); - switch (response.Type) - { - case ResponseType.Subscriptions: - var subscription = JsonConvert.DeserializeObject(token, SerializerSettings); - if (subscription.Channels == null || !subscription.Channels.Any()) - { - Trace.WriteLine($"{nameof(OnUserDataWebSocketAsync)}() no channels subscribed"); - } - else - { - Trace.WriteLine($"{nameof(OnUserDataWebSocketAsync)}() subscribed to " + - $"{string.Join(",", subscription.Channels.Select(c => c.ToString()))}"); - } - break; - case ResponseType.Ticker: - throw new NotImplementedException($"Not expecting type {response.Type} in {nameof(OnUserDataWebSocketAsync)}()"); - case ResponseType.Snapshot: - throw new NotImplementedException($"Not expecting type {response.Type} in {nameof(OnUserDataWebSocketAsync)}()"); - case ResponseType.L2Update: - throw new NotImplementedException($"Not expecting type {response.Type} in {nameof(OnUserDataWebSocketAsync)}()"); - case ResponseType.Heartbeat: - var heartbeat = JsonConvert.DeserializeObject(token, SerializerSettings); - Trace.WriteLine($"{nameof(OnUserDataWebSocketAsync)}() heartbeat received {heartbeat}"); - break; - case ResponseType.Received: - var received = JsonConvert.DeserializeObject(token, SerializerSettings); - callback(received.ExchangeOrderResult); - break; - case ResponseType.Open: - var open = JsonConvert.DeserializeObject(token, SerializerSettings); - callback(open.ExchangeOrderResult); - break; - case ResponseType.Done: - var done = JsonConvert.DeserializeObject(token, SerializerSettings); - callback(done.ExchangeOrderResult); - break; - case ResponseType.Match: - var match = JsonConvert.DeserializeObject(token, SerializerSettings); - callback(match.ExchangeOrderResult); - break; - case ResponseType.LastMatch: - //var lastMatch = JsonConvert.DeserializeObject(token); - throw new NotImplementedException($"Not expecting type {response.Type} in {nameof(OnUserDataWebSocketAsync)}()"); - case ResponseType.Error: - var error = JsonConvert.DeserializeObject(token, SerializerSettings); - throw new APIException($"{error.Reason}: {error.Message}"); - case ResponseType.Change: - var change = JsonConvert.DeserializeObject(token, SerializerSettings); - callback(change.ExchangeOrderResult); - break; - case ResponseType.Activate: - var activate = JsonConvert.DeserializeObject(token, SerializerSettings); - callback(activate.ExchangeOrderResult); - break; - case ResponseType.Status: - //var status = JsonConvert.DeserializeObject(token); - throw new NotImplementedException($"Not expecting type {response.Type} in {nameof(OnUserDataWebSocketAsync)}()"); - default: - throw new NotImplementedException($"Not expecting type {response.Type} in {nameof(OnUserDataWebSocketAsync)}()"); - } - }, async (_socket) => - { - var marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); - var nonce = await GetNoncePayloadAsync(); - string timestamp = nonce["nonce"].ToStringInvariant(); - byte[] secret = CryptoUtility.ToBytesBase64Decode(PrivateApiKey); - string toHash = timestamp + "GET" + "/users/self/verify"; - var subscribeRequest = new - { - type = "subscribe", - channels = new object[] + return await ConnectPublicWebSocketAsync( + "/", + async (_socket, msg) => { - new + var token = msg.ToStringFromUTF8(); + var response = JsonConvert.DeserializeObject( + token, + SerializerSettings + ); + switch (response.Type) { - name = "user", - product_ids = marketSymbols, + case ResponseType.Subscriptions: + var subscription = JsonConvert.DeserializeObject( + token, + SerializerSettings + ); + if (subscription.Channels == null || !subscription.Channels.Any()) + { + Trace.WriteLine( + $"{nameof(OnUserDataWebSocketAsync)}() no channels subscribed" + ); + } + else + { + Trace.WriteLine( + $"{nameof(OnUserDataWebSocketAsync)}() subscribed to " + + $"{string.Join(",", subscription.Channels.Select(c => c.ToString()))}" + ); + } + break; + case ResponseType.Ticker: + throw new NotImplementedException( + $"Not expecting type {response.Type} in {nameof(OnUserDataWebSocketAsync)}()" + ); + case ResponseType.Snapshot: + throw new NotImplementedException( + $"Not expecting type {response.Type} in {nameof(OnUserDataWebSocketAsync)}()" + ); + case ResponseType.L2Update: + throw new NotImplementedException( + $"Not expecting type {response.Type} in {nameof(OnUserDataWebSocketAsync)}()" + ); + case ResponseType.Heartbeat: + var heartbeat = JsonConvert.DeserializeObject( + token, + SerializerSettings + ); + Trace.WriteLine( + $"{nameof(OnUserDataWebSocketAsync)}() heartbeat received {heartbeat}" + ); + break; + case ResponseType.Received: + var received = JsonConvert.DeserializeObject( + token, + SerializerSettings + ); + callback(received.ExchangeOrderResult); + break; + case ResponseType.Open: + var open = JsonConvert.DeserializeObject( + token, + SerializerSettings + ); + callback(open.ExchangeOrderResult); + break; + case ResponseType.Done: + var done = JsonConvert.DeserializeObject( + token, + SerializerSettings + ); + callback(done.ExchangeOrderResult); + break; + case ResponseType.Match: + var match = JsonConvert.DeserializeObject( + token, + SerializerSettings + ); + callback(match.ExchangeOrderResult); + break; + case ResponseType.LastMatch: + //var lastMatch = JsonConvert.DeserializeObject(token); + throw new NotImplementedException( + $"Not expecting type {response.Type} in {nameof(OnUserDataWebSocketAsync)}()" + ); + case ResponseType.Error: + var error = JsonConvert.DeserializeObject( + token, + SerializerSettings + ); + throw new APIException($"{error.Reason}: {error.Message}"); + case ResponseType.Change: + var change = JsonConvert.DeserializeObject( + token, + SerializerSettings + ); + callback(change.ExchangeOrderResult); + break; + case ResponseType.Activate: + var activate = JsonConvert.DeserializeObject( + token, + SerializerSettings + ); + callback(activate.ExchangeOrderResult); + break; + case ResponseType.Status: + //var status = JsonConvert.DeserializeObject(token); + throw new NotImplementedException( + $"Not expecting type {response.Type} in {nameof(OnUserDataWebSocketAsync)}()" + ); + default: + throw new NotImplementedException( + $"Not expecting type {response.Type} in {nameof(OnUserDataWebSocketAsync)}()" + ); } }, - signature = CryptoUtility.SHA256SignBase64(toHash, secret), // signature base 64 string - key = PublicApiKey.ToUnsecureString(), - passphrase = CryptoUtility.ToUnsecureString(Passphrase), - timestamp = timestamp - }; - await _socket.SendMessageAsync(subscribeRequest); - }); + async (_socket) => + { + var marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); + var nonce = await GetNoncePayloadAsync(); + string timestamp = nonce["nonce"].ToStringInvariant(); + byte[] secret = CryptoUtility.ToBytesBase64Decode(PrivateApiKey); + string toHash = timestamp + "GET" + "/users/self/verify"; + var subscribeRequest = new + { + type = "subscribe", + channels = new object[] + { + new { name = "user", product_ids = marketSymbols, } + }, + signature = CryptoUtility.SHA256SignBase64(toHash, secret), // signature base 64 string + key = PublicApiKey.ToUnsecureString(), + passphrase = CryptoUtility.ToUnsecureString(Passphrase), + timestamp = timestamp + }; + await _socket.SendMessageAsync(subscribeRequest); + } + ); } - protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + protected override async Task OnGetHistoricalTradesAsync( + Func, bool> callback, + string marketSymbol, + DateTime? startDate = null, + DateTime? endDate = null, + int? limit = null + ) { /* - [{ - "time": "2014-11-07T22:19:28.578544Z", - "trade_id": 74, - "price": "10.00000000", - "size": "0.01000000", - "side": "buy" - }, { - "time": "2014-11-07T01:08:43.642366Z", - "trade_id": 73, - "price": "100.00000000", - "size": "0.01000000", - "side": "sell" - }] - */ + [{ + "time": "2014-11-07T22:19:28.578544Z", + "trade_id": 74, + "price": "10.00000000", + "size": "0.01000000", + "side": "buy" + }, { + "time": "2014-11-07T01:08:43.642366Z", + "trade_id": 73, + "price": "100.00000000", + "size": "0.01000000", + "side": "sell" + }] + */ ExchangeHistoricalTradeHelper state = new ExchangeHistoricalTradeHelper(this) { Callback = callback, EndDate = endDate, - ParseFunction = (JToken token) => token.ParseTrade("size", "price", "side", "time", TimestampType.Iso8601UTC, "trade_id"), + ParseFunction = (JToken token) => + token.ParseTrade( + "size", + "price", + "side", + "time", + TimestampType.Iso8601UTC, + "trade_id" + ), StartDate = startDate, MarketSymbol = marketSymbol, Url = "/products/[marketSymbol]/trades", UrlFunction = (ExchangeHistoricalTradeHelper _state) => { - return _state.Url + (string.IsNullOrWhiteSpace(cursorBefore) ? string.Empty : "?before=" + cursorBefore.ToStringInvariant()); + return _state.Url + + ( + string.IsNullOrWhiteSpace(cursorBefore) + ? string.Empty + : "?before=" + cursorBefore.ToStringInvariant() + ); } }; await state.ProcessHistoricalTrades(); } - protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null) + protected override async Task> OnGetRecentTradesAsync( + string marketSymbol, + int? limit = null + ) { //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; + string baseUrl = + "/products/" + + marketSymbol.ToUpperInvariant() + + "/trades" + + "?limit=" + + requestLimit; JToken trades = await MakeJsonRequestAsync(baseUrl); List tradeList = new List(); foreach (JToken trade in trades) { - tradeList.Add(trade.ParseTrade("size", "price", "side", "time", TimestampType.Iso8601UTC, "trade_id")); + tradeList.Add( + trade.ParseTrade( + "size", + "price", + "side", + "time", + TimestampType.Iso8601UTC, + "trade_id" + ) + ); } return tradeList; } - protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 50) + protected override async Task OnGetOrderBookAsync( + string marketSymbol, + int maxCount = 50 + ) { string url = "/products/" + marketSymbol.ToUpperInvariant() + "/book?level=2"; JToken token = await MakeJsonRequestAsync(url); return token.ParseOrderBookFromJTokenArrays(); } - protected override async Task> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + protected override async Task> OnGetCandlesAsync( + string marketSymbol, + int periodSeconds, + DateTime? startDate = null, + DateTime? endDate = null, + int? limit = null + ) { if (limit != null) { @@ -610,18 +839,35 @@ protected override async Task> OnGetCandlesAsync(strin { startDate = CryptoUtility.UtcNow.Subtract(TimeSpan.FromDays(1.0)); } - url += "&start=" + startDate.Value.ToString("s", System.Globalization.CultureInfo.InvariantCulture); + url += + "&start=" + + startDate.Value.ToString("s", System.Globalization.CultureInfo.InvariantCulture); if (endDate == null) { endDate = CryptoUtility.UtcNow; } - url += "&end=" + endDate.Value.ToString("s", System.Globalization.CultureInfo.InvariantCulture); + url += + "&end=" + + endDate.Value.ToString("s", System.Globalization.CultureInfo.InvariantCulture); // time, low, high, open, close, volume JToken token = await MakeJsonRequestAsync(url); foreach (JToken candle in token) { - candles.Add(this.ParseCandle(candle, marketSymbol, periodSeconds, 3, 2, 1, 4, 0, TimestampType.UnixSeconds, 5)); + candles.Add( + this.ParseCandle( + candle, + marketSymbol, + periodSeconds, + 3, + 2, + 1, + 4, + 0, + TimestampType.UnixSeconds, + 5 + ) + ); } // re-sort in ascending order candles.Sort((c1, c2) => c1.Timestamp.CompareTo(c2.Timestamp)); @@ -630,8 +876,15 @@ protected override async Task> OnGetCandlesAsync(strin protected override async Task> OnGetAmountsAsync() { - Dictionary amounts = new Dictionary(StringComparer.OrdinalIgnoreCase); - JArray array = await MakeJsonRequestAsync("/accounts", null, await GetNoncePayloadAsync(), "GET"); + Dictionary amounts = new Dictionary( + StringComparer.OrdinalIgnoreCase + ); + JArray array = await MakeJsonRequestAsync( + "/accounts", + null, + await GetNoncePayloadAsync(), + "GET" + ); foreach (JToken token in array) { decimal amount = token["balance"].ConvertInvariant(); @@ -643,10 +896,19 @@ protected override async Task> OnGetAmountsAsync() return amounts; } - protected override async Task> OnGetAmountsAvailableToTradeAsync() + protected override async Task< + Dictionary + > OnGetAmountsAvailableToTradeAsync() { - Dictionary amounts = new Dictionary(StringComparer.OrdinalIgnoreCase); - JArray array = await MakeJsonRequestAsync("/accounts", null, await GetNoncePayloadAsync(), "GET"); + Dictionary amounts = new Dictionary( + StringComparer.OrdinalIgnoreCase + ); + JArray array = await MakeJsonRequestAsync( + "/accounts", + null, + await GetNoncePayloadAsync(), + "GET" + ); foreach (JToken token in array) { decimal amount = token["available"].ConvertInvariant(); @@ -662,9 +924,16 @@ protected override async Task> OnGetFeesAsync() { var symbols = await OnGetMarketSymbolsAsync(); - Dictionary fees = new Dictionary(StringComparer.OrdinalIgnoreCase); + Dictionary fees = new Dictionary( + StringComparer.OrdinalIgnoreCase + ); - JObject token = await MakeJsonRequestAsync("/fees", null, await GetNoncePayloadAsync(), "GET"); + JObject token = await MakeJsonRequestAsync( + "/fees", + null, + await GetNoncePayloadAsync(), + "GET" + ); /* * We can chose between maker and taker fee, but currently ExchangeSharp only supports 1 fee rate per symbol. * Here, we choose taker fee, which are usually higher @@ -672,30 +941,37 @@ protected override async Task> OnGetFeesAsync() decimal makerRate = token["taker_fee_rate"].Value(); //percentage between 0 and 1 fees = symbols - .Select(symbol => new KeyValuePair(symbol, makerRate)) - .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + .Select(symbol => new KeyValuePair(symbol, makerRate)) + .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); return fees; } - protected override async Task OnWithdrawAsync(ExchangeWithdrawalRequest request) + protected override async Task OnWithdrawAsync( + ExchangeWithdrawalRequest request + ) { var nonce = await GenerateNonceAsync(); var payload = new Dictionary - { - { "nonce", nonce }, - { "amount", request.Amount }, - { "currency", request.Currency }, - { "crypto_address", request.Address }, - { "add_network_fee_to_total", !request.TakeFeeFromAmount }, - }; + { + { "nonce", nonce }, + { "amount", request.Amount }, + { "currency", request.Currency }, + { "crypto_address", request.Address }, + { "add_network_fee_to_total", !request.TakeFeeFromAmount }, + }; if (!string.IsNullOrEmpty(request.AddressTag)) { payload.Add("destination_tag", request.AddressTag); } - var result = await MakeJsonRequestAsync("/withdrawals/crypto", null, payload, "POST"); + var result = await MakeJsonRequestAsync( + "/withdrawals/crypto", + null, + payload, + "POST" + ); var feeParsed = decimal.TryParse(result.Fee, out var fee); return new ExchangeWithdrawalResponse @@ -705,30 +981,35 @@ protected override async Task OnWithdrawAsync(Exchan }; } - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + protected override async Task OnPlaceOrderAsync( + ExchangeOrderRequest order + ) { object nonce = await GenerateNonceAsync(); Dictionary payload = new Dictionary - { - { "nonce", nonce }, - { "type", order.OrderType.ToStringLowerInvariant() }, - { "side", (order.IsBuy ? "buy" : "sell") }, - { "product_id", order.MarketSymbol }, - { "size", order.RoundAmount().ToStringInvariant() } - }; + { + { "nonce", nonce }, + { "type", order.OrderType.ToStringLowerInvariant() }, + { "side", (order.IsBuy ? "buy" : "sell") }, + { "product_id", order.MarketSymbol }, + { "size", order.RoundAmount().ToStringInvariant() } + }; payload["time_in_force"] = "GTC"; // good til cancel switch (order.OrderType) { case OrderType.Limit: - if (order.IsPostOnly != null) payload["post_only"] = order.IsPostOnly; // [optional]** Post only flag, ** Invalid when time_in_force is IOC or FOK - if (order.Price == null) throw new ArgumentNullException(nameof(order.Price)); + if (order.IsPostOnly != null) + payload["post_only"] = order.IsPostOnly; // [optional]** Post only flag, ** Invalid when time_in_force is IOC or FOK + if (order.Price == null) + throw new ArgumentNullException(nameof(order.Price)); payload["price"] = order.Price.ToStringInvariant(); break; case OrderType.Stop: payload["stop"] = (order.IsBuy ? "entry" : "loss"); payload["stop_price"] = order.StopPrice.ToStringInvariant(); - if (order.Price == null) throw new ArgumentNullException(nameof(order.Price)); + if (order.Price == null) + throw new ArgumentNullException(nameof(order.Price)); payload["type"] = order.Price > 0m ? "limit" : "market"; break; @@ -744,20 +1025,48 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd return resultOrder; } - protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnGetOrderDetailsAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { // Orders may be queried using either the exchange assigned id or the client assigned client_oid. When using client_oid it must be preceded by the client: namespace. - JToken obj = await MakeJsonRequestAsync("/orders/" + (isClientOrderId ? "client:" : "") + orderId, - null, await GetNoncePayloadAsync(), "GET"); + JToken obj = await MakeJsonRequestAsync( + "/orders/" + (isClientOrderId ? "client:" : "") + orderId, + null, + await GetNoncePayloadAsync(), + "GET" + ); var order = ParseOrder(obj); - if (!order.MarketSymbol.Equals(marketSymbol, StringComparison.InvariantCultureIgnoreCase)) - throw new DataMisalignedException($"Order {orderId} found, but symbols {order.MarketSymbol} and {marketSymbol} don't match"); - else return order; + if ( + !order.MarketSymbol.Equals( + marketSymbol, + StringComparison.InvariantCultureIgnoreCase + ) + ) + throw new DataMisalignedException( + $"Order {orderId} found, but symbols {order.MarketSymbol} and {marketSymbol} don't match" + ); + else + return order; } - protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) + protected override async Task> OnGetOpenOrderDetailsAsync( + string marketSymbol = null + ) { List orders = new List(); - JArray array = await MakeJsonRequestAsync("orders?status=open&status=pending&status=active" + (string.IsNullOrWhiteSpace(marketSymbol) ? string.Empty : "&product_id=" + marketSymbol), null, await GetNoncePayloadAsync(), "GET"); + JArray array = await MakeJsonRequestAsync( + "orders?status=open&status=pending&status=active" + + ( + string.IsNullOrWhiteSpace(marketSymbol) + ? string.Empty + : "&product_id=" + marketSymbol + ), + null, + await GetNoncePayloadAsync(), + "GET" + ); foreach (JToken token in array) { orders.Add(ParseOrder(token)); @@ -766,10 +1075,22 @@ protected override async Task> OnGetOpenOrderDe return orders; } - protected override async Task> OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) + protected override async Task< + IEnumerable + > OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) { List orders = new List(); - JArray array = await MakeJsonRequestAsync("orders?status=done" + (string.IsNullOrWhiteSpace(marketSymbol) ? string.Empty : "&product_id=" + marketSymbol), null, await GetNoncePayloadAsync(), "GET"); + JArray array = await MakeJsonRequestAsync( + "orders?status=done" + + ( + string.IsNullOrWhiteSpace(marketSymbol) + ? string.Empty + : "&product_id=" + marketSymbol + ), + null, + await GetNoncePayloadAsync(), + "GET" + ); foreach (JToken token in array) { ExchangeOrderResult result = ParseOrder(token); @@ -782,11 +1103,18 @@ protected override async Task> OnGetCompletedOr return orders; } - public async Task> GetFillsAsync(string marketSymbol = null, DateTime? afterDate = null) + public async Task> GetFillsAsync( + string marketSymbol = null, + DateTime? afterDate = null + ) { List orders = new List(); marketSymbol = NormalizeMarketSymbol(marketSymbol); - var productId = (string.IsNullOrWhiteSpace(marketSymbol) ? string.Empty : "product_id=" + marketSymbol); + var productId = ( + string.IsNullOrWhiteSpace(marketSymbol) + ? string.Empty + : "product_id=" + marketSymbol + ); do { var after = cursorAfter == null ? string.Empty : $"after={cursorAfter}&"; @@ -796,10 +1124,19 @@ public async Task> GetFillsAsync(string marketS return orders; } - private async Task MakeFillRequest(DateTime? afterDate, string productId, List orders, string after) + private async Task MakeFillRequest( + DateTime? afterDate, + string productId, + List orders, + string after + ) { var interrogation = after != "" || productId != "" ? "?" : string.Empty; - JArray array = await MakeJsonRequestAsync($"fills{interrogation}{after}{productId}", null, await GetNoncePayloadAsync()); + JArray array = await MakeJsonRequestAsync( + $"fills{interrogation}{after}{productId}", + null, + await GetNoncePayloadAsync() + ); foreach (JToken token in array) { @@ -817,13 +1154,27 @@ private async Task MakeFillRequest(DateTime? afterDate, string productId, List("orders/" + (isClientOrderId ? "client:" : "") + orderId, null, await GetNoncePayloadAsync(), "DELETE"); + var jToken = await MakeJsonRequestAsync( + "orders/" + (isClientOrderId ? "client:" : "") + orderId, + null, + await GetNoncePayloadAsync(), + "DELETE" + ); if (jToken.ToStringInvariant() != orderId) - throw new APIException($"Cancelled {jToken.ToStringInvariant()} when trying to cancel {orderId}"); + throw new APIException( + $"Cancelled {jToken.ToStringInvariant()} when trying to cancel {orderId}" + ); } } - public partial class ExchangeName { public const string Coinbase = "Coinbase"; } + public partial class ExchangeName + { + public const string Coinbase = "Coinbase"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/Models/CoinbaseTrade.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/Models/CoinbaseTrade.cs index 6d3434596..a754d1d4a 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinbase/Models/CoinbaseTrade.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinbase/Models/CoinbaseTrade.cs @@ -22,6 +22,7 @@ public class CoinbaseTrade : ExchangeTrade { public Guid MakerOrderId { get; set; } public Guid TakerOrderId { get; set; } + public override string ToString() { return string.Format("{0},{1},{2}", base.ToString(), MakerOrderId, TakerOrderId); diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Request/Channel.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Request/Channel.cs index 7971cd858..519c59e67 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Request/Channel.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Request/Channel.cs @@ -12,19 +12,19 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp.Coinbase { - using System.Collections.Generic; + using System.Collections.Generic; - using Newtonsoft.Json; - using Newtonsoft.Json.Converters; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; - internal class Channel - { - [JsonConverter(typeof(StringEnumConverter))] - [JsonProperty("name")] - public ChannelType Name { get; set; } + internal class Channel + { + [JsonConverter(typeof(StringEnumConverter))] + [JsonProperty("name")] + public ChannelType Name { get; set; } - [JsonProperty("product_ids")] - public List ProductIds { get; set; } + [JsonProperty("product_ids")] + public List ProductIds { get; set; } public override string ToString() => $"{Name} channel w/ {ProductIds.Count} symbols"; } diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Request/ChannelAction.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Request/ChannelAction.cs index e6fd3870a..0f09f8035 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Request/ChannelAction.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Request/ChannelAction.cs @@ -12,18 +12,18 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp.Coinbase { - using System.Collections.Generic; + using System.Collections.Generic; - using Newtonsoft.Json; - using Newtonsoft.Json.Converters; + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; - internal class ChannelAction - { - [JsonConverter(typeof(StringEnumConverter))] - [JsonProperty("type")] - public ActionType Type { get; set; } + internal class ChannelAction + { + [JsonConverter(typeof(StringEnumConverter))] + [JsonProperty("type")] + public ActionType Type { get; set; } - [JsonProperty("channels")] - public List Channels { get; set; } - } -} \ No newline at end of file + [JsonProperty("channels")] + public List Channels { get; set; } + } +} diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Response/Level2.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Response/Level2.cs index 18172f9c3..23ef74caf 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Response/Level2.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Response/Level2.cs @@ -12,18 +12,18 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp.Coinbase { - using System; - using System.Collections.Generic; + using System; + using System.Collections.Generic; - using Newtonsoft.Json; + using Newtonsoft.Json; - internal class Level2 : BaseMessage - { - [JsonProperty("product_id")] - public string ProductId { get; set; } + internal class Level2 : BaseMessage + { + [JsonProperty("product_id")] + public string ProductId { get; set; } - public DateTime Time { get; set; } + public DateTime Time { get; set; } - public List Changes { get; set; } - } -} \ No newline at end of file + public List Changes { get; set; } + } +} diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Response/Messages.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Response/Messages.cs index 969b9c297..2e29e39f7 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Response/Messages.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Response/Messages.cs @@ -35,24 +35,25 @@ internal class Activate : BaseMessage public string ProductId { get; set; } public DateTimeOffset TimeStamp { get; set; } - public ExchangeOrderResult ExchangeOrderResult => new ExchangeOrderResult() - { - OrderId = OrderId.ToString(), - ClientOrderId = null, // not provided here - Result = ExchangeAPIOrderResult.PendingOpen, // order has just been activated (so it starts in PendingOpen) - Message = null, // + can use for something in the future if needed - Amount = Size, - AmountFilled = 0, // just activated, so none filled - Price = null, // not specified here (only StopPrice is) - AveragePrice = null, // not specified here (only StopPrice is) - OrderDate = TimeStamp.UtcDateTime, // order activated event - CompletedDate = null, // order is active - MarketSymbol = ProductId, - IsBuy = Side == OrderSide.Buy, - Fees = null, // only TakerFeeRate is specified - no fees have been charged yet - TradeId = null, // no trades have been made - UpdateSequence = null, // unfortunately, the Activate event doesn't provide a sequence number - }; + public ExchangeOrderResult ExchangeOrderResult => + new ExchangeOrderResult() + { + OrderId = OrderId.ToString(), + ClientOrderId = null, // not provided here + Result = ExchangeAPIOrderResult.PendingOpen, // order has just been activated (so it starts in PendingOpen) + Message = null, // + can use for something in the future if needed + Amount = Size, + AmountFilled = 0, // just activated, so none filled + Price = null, // not specified here (only StopPrice is) + AveragePrice = null, // not specified here (only StopPrice is) + OrderDate = TimeStamp.UtcDateTime, // order activated event + CompletedDate = null, // order is active + MarketSymbol = ProductId, + IsBuy = Side == OrderSide.Buy, + Fees = null, // only TakerFeeRate is specified - no fees have been charged yet + TradeId = null, // no trades have been made + UpdateSequence = null, // unfortunately, the Activate event doesn't provide a sequence number + }; } internal class Change : BaseMessage @@ -68,24 +69,25 @@ internal class Change : BaseMessage public long Sequence { get; set; } public DateTime Time { get; set; } - public ExchangeOrderResult ExchangeOrderResult => new ExchangeOrderResult() - { - OrderId = OrderId.ToString(), - ClientOrderId = null, // not provided here - Result = ExchangeAPIOrderResult.Unknown, // change messages are sent anytime an order changes in size; this includes resting orders (open) as well as received but not yet open - Message = null, // can use for something in the future if needed - Amount = NewSize, - AmountFilled = null, // not specified here - Price = Price, - AveragePrice = null, // not specified here - OrderDate = Time, // + unclear if the Time in the Change msg is the new OrderDate or whether that is unchanged - CompletedDate = null, // order is active - MarketSymbol = ProductId, - IsBuy = Side == OrderSide.Buy, - Fees = null, // only TakerFeeRate is specified - no fees have been charged yet - TradeId = null, // not a trade msg - UpdateSequence = Sequence, - }; + public ExchangeOrderResult ExchangeOrderResult => + new ExchangeOrderResult() + { + OrderId = OrderId.ToString(), + ClientOrderId = null, // not provided here + Result = ExchangeAPIOrderResult.Unknown, // change messages are sent anytime an order changes in size; this includes resting orders (open) as well as received but not yet open + Message = null, // can use for something in the future if needed + Amount = NewSize, + AmountFilled = null, // not specified here + Price = Price, + AveragePrice = null, // not specified here + OrderDate = Time, // + unclear if the Time in the Change msg is the new OrderDate or whether that is unchanged + CompletedDate = null, // order is active + MarketSymbol = ProductId, + IsBuy = Side == OrderSide.Buy, + Fees = null, // only TakerFeeRate is specified - no fees have been charged yet + TradeId = null, // not a trade msg + UpdateSequence = Sequence, + }; } internal class Done : BaseMessage @@ -99,26 +101,29 @@ internal class Done : BaseMessage public long Sequence { get; set; } public DateTimeOffset Time { get; set; } - public ExchangeOrderResult ExchangeOrderResult => new ExchangeOrderResult() - { - OrderId = OrderId.ToString(), - ClientOrderId = null, // not provided here - Result = Reason == DoneReasonType.Filled ? ExchangeAPIOrderResult.Filled - : ExchangeAPIOrderResult.Canceled, // no way to tell it it is FilledPartiallyAndCenceled here - Message = null, // can use for something in the future if needed - Amount = 0, // ideally, this would be null, but ExchangeOrderResult.Amount is not nullable - AmountFilled = RemainingSize, - IsAmountFilledReversed = true, // since only RemainingSize is provided, not Size or FilledSize - Price = Price, - AveragePrice = null, // not specified here - // OrderDate - not provided here. ideally would be null but ExchangeOrderResult.OrderDate - CompletedDate = Time.UtcDateTime, - MarketSymbol = ProductId, - IsBuy = Side == OrderSide.Buy, - Fees = null, // not specified here - TradeId = null, // not a trade msg - UpdateSequence = Sequence, - }; + public ExchangeOrderResult ExchangeOrderResult => + new ExchangeOrderResult() + { + OrderId = OrderId.ToString(), + ClientOrderId = null, // not provided here + Result = + Reason == DoneReasonType.Filled + ? ExchangeAPIOrderResult.Filled + : ExchangeAPIOrderResult.Canceled, // no way to tell it it is FilledPartiallyAndCenceled here + Message = null, // can use for something in the future if needed + Amount = 0, // ideally, this would be null, but ExchangeOrderResult.Amount is not nullable + AmountFilled = RemainingSize, + IsAmountFilledReversed = true, // since only RemainingSize is provided, not Size or FilledSize + Price = Price, + AveragePrice = null, // not specified here + // OrderDate - not provided here. ideally would be null but ExchangeOrderResult.OrderDate + CompletedDate = Time.UtcDateTime, + MarketSymbol = ProductId, + IsBuy = Side == OrderSide.Buy, + Fees = null, // not specified here + TradeId = null, // not a trade msg + UpdateSequence = Sequence, + }; } internal class Error : BaseMessage @@ -133,6 +138,7 @@ internal class Heartbeat : BaseMessage public string ProductId { get; set; } public long Sequence { get; set; } public DateTimeOffset Time { get; set; } + public override string ToString() { return $"Heartbeat: Last TID {LastTradeId}, Product Id {ProductId}, Sequence {Sequence}, Time {Time}"; @@ -172,26 +178,28 @@ internal class Match : BaseMessage public decimal? MakerFeeRate { get; set; } public decimal? TakerFeeRate { get; set; } - public ExchangeOrderResult ExchangeOrderResult => new ExchangeOrderResult() - { - OrderId = MakerProfileId != null ? MakerOrderId.ToString() : TakerOrderId.ToString(), - ClientOrderId = null, // not provided here - Result = ExchangeAPIOrderResult.FilledPartially, // could also be completely filled, but unable to determine that here - Message = null, // can use for something in the future if needed - Amount = 0, // ideally, this would be null, but ExchangeOrderResult.Amount is not nullable - AmountFilled = Size, - IsAmountFilledReversed = false, // the size here appears to be amount filled, no no need to reverse - Price = Price, - AveragePrice = Price, // not specified here - // OrderDate - not provided here. ideally would be null but ExchangeOrderResult.OrderDate is not nullable - CompletedDate = null, // order not necessarily fullly filled at this point - TradeDate = Time.UtcDateTime, - MarketSymbol = ProductId, - IsBuy = Side == OrderSide.Buy, - Fees = (MakerFeeRate ?? TakerFeeRate) * Price * Size, - TradeId = TradeId.ToString(), - UpdateSequence = Sequence, - }; + public ExchangeOrderResult ExchangeOrderResult => + new ExchangeOrderResult() + { + OrderId = + MakerProfileId != null ? MakerOrderId.ToString() : TakerOrderId.ToString(), + ClientOrderId = null, // not provided here + Result = ExchangeAPIOrderResult.FilledPartially, // could also be completely filled, but unable to determine that here + Message = null, // can use for something in the future if needed + Amount = 0, // ideally, this would be null, but ExchangeOrderResult.Amount is not nullable + AmountFilled = Size, + IsAmountFilledReversed = false, // the size here appears to be amount filled, no no need to reverse + Price = Price, + AveragePrice = Price, // not specified here + // OrderDate - not provided here. ideally would be null but ExchangeOrderResult.OrderDate is not nullable + CompletedDate = null, // order not necessarily fullly filled at this point + TradeDate = Time.UtcDateTime, + MarketSymbol = ProductId, + IsBuy = Side == OrderSide.Buy, + Fees = (MakerFeeRate ?? TakerFeeRate) * Price * Size, + TradeId = TradeId.ToString(), + UpdateSequence = Sequence, + }; } internal class Open : BaseMessage @@ -204,25 +212,26 @@ internal class Open : BaseMessage public long Sequence { get; set; } public DateTimeOffset Time { get; set; } - public ExchangeOrderResult ExchangeOrderResult => new ExchangeOrderResult() - { - OrderId = OrderId.ToString(), - ClientOrderId = null, // not provided here - Result = ExchangeAPIOrderResult.Open, // order is now Open - Message = null, // can use for something in the future if needed - Amount = 0, // ideally, this would be null, but ExchangeOrderResult.Amount is not nullable - AmountFilled = RemainingSize, - IsAmountFilledReversed = true, // since only RemainingSize is provided, not Size or FilledSize - Price = Price, - AveragePrice = null, // not specified here - OrderDate = Time.UtcDateTime, // order open event - CompletedDate = null, // order is active - MarketSymbol = ProductId, - IsBuy = Side == OrderSide.Buy, - Fees = null, // not specified here - TradeId = null, // not a trade msg - UpdateSequence = Sequence, - }; + public ExchangeOrderResult ExchangeOrderResult => + new ExchangeOrderResult() + { + OrderId = OrderId.ToString(), + ClientOrderId = null, // not provided here + Result = ExchangeAPIOrderResult.Open, // order is now Open + Message = null, // can use for something in the future if needed + Amount = 0, // ideally, this would be null, but ExchangeOrderResult.Amount is not nullable + AmountFilled = RemainingSize, + IsAmountFilledReversed = true, // since only RemainingSize is provided, not Size or FilledSize + Price = Price, + AveragePrice = null, // not specified here + OrderDate = Time.UtcDateTime, // order open event + CompletedDate = null, // order is active + MarketSymbol = ProductId, + IsBuy = Side == OrderSide.Buy, + Fees = null, // not specified here + TradeId = null, // not a trade msg + UpdateSequence = Sequence, + }; } internal class Received : BaseMessage @@ -237,25 +246,26 @@ internal class Received : BaseMessage public long Sequence { get; set; } public DateTimeOffset Time { get; set; } - public ExchangeOrderResult ExchangeOrderResult => new ExchangeOrderResult() - { - OrderId = OrderId.ToString(), - ClientOrderId = ClientOid.ToString(), - Result = ExchangeAPIOrderResult.PendingOpen, // order is now Pending - Message = null, // can use for something in the future if needed - Amount = Size, - AmountFilled = 0, // order received but not yet open, so none filled - IsAmountFilledReversed = false, - Price = Price, - AveragePrice = null, // not specified here - OrderDate = Time.UtcDateTime, // order received event - CompletedDate = null, // order is active - MarketSymbol = ProductId, - IsBuy = Side == OrderSide.Buy, - Fees = null, // not specified here - TradeId = null, // not a trade msg - UpdateSequence = Sequence, - }; + public ExchangeOrderResult ExchangeOrderResult => + new ExchangeOrderResult() + { + OrderId = OrderId.ToString(), + ClientOrderId = ClientOid.ToString(), + Result = ExchangeAPIOrderResult.PendingOpen, // order is now Pending + Message = null, // can use for something in the future if needed + Amount = Size, + AmountFilled = 0, // order received but not yet open, so none filled + IsAmountFilledReversed = false, + Price = Price, + AveragePrice = null, // not specified here + OrderDate = Time.UtcDateTime, // order received event + CompletedDate = null, // order is active + MarketSymbol = ProductId, + IsBuy = Side == OrderSide.Buy, + Fees = null, // not specified here + TradeId = null, // not a trade msg + UpdateSequence = Sequence, + }; } internal class Subscription : BaseMessage diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Response/Snapshot.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Response/Snapshot.cs index ab5dafa2f..c107d92eb 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Response/Snapshot.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Response/Snapshot.cs @@ -12,17 +12,17 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp.Coinbase { - using System.Collections.Generic; + using System.Collections.Generic; - using Newtonsoft.Json; + using Newtonsoft.Json; - internal class Snapshot : BaseMessage - { - [JsonProperty("product_id")] - public string ProductId { get; set; } + internal class Snapshot : BaseMessage + { + [JsonProperty("product_id")] + public string ProductId { get; set; } - public List Bids { get; set; } + public List Bids { get; set; } - public List Asks { get; set; } - } -} \ No newline at end of file + public List Asks { get; set; } + } +} diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Types/ActionType.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Types/ActionType.cs index ae1fefb30..7c96f9391 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Types/ActionType.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Types/ActionType.cs @@ -12,14 +12,14 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp.Coinbase { - using System.Runtime.Serialization; + using System.Runtime.Serialization; - internal enum ActionType - { - [EnumMember(Value = "subscribe")] - Subscribe, + internal enum ActionType + { + [EnumMember(Value = "subscribe")] + Subscribe, - [EnumMember(Value = "unsubscribe")] - Unsubscribe - } -} \ No newline at end of file + [EnumMember(Value = "unsubscribe")] + Unsubscribe + } +} diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Types/ChannelType.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Types/ChannelType.cs index 3218f8d07..9e5e63f2e 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Types/ChannelType.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Types/ChannelType.cs @@ -12,26 +12,26 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp.Coinbase { - using System.Runtime.Serialization; + using System.Runtime.Serialization; - internal enum ChannelType - { - [EnumMember(Value = "full")] - Full, + internal enum ChannelType + { + [EnumMember(Value = "full")] + Full, - [EnumMember(Value = "heartbeat")] - Heartbeat, + [EnumMember(Value = "heartbeat")] + Heartbeat, - [EnumMember(Value = "level2")] - Level2, + [EnumMember(Value = "level2")] + Level2, - [EnumMember(Value = "matches")] - Matches, + [EnumMember(Value = "matches")] + Matches, - [EnumMember(Value = "ticker")] - Ticker, + [EnumMember(Value = "ticker")] + Ticker, - [EnumMember(Value = "user")] - User - } -} \ No newline at end of file + [EnumMember(Value = "user")] + User + } +} diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Types/ResponseType.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Types/ResponseType.cs index 992910b33..261a9e76f 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Types/ResponseType.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Types/ResponseType.cs @@ -12,39 +12,39 @@ The above copyright notice and this permission notice shall be included in all c namespace ExchangeSharp.Coinbase { - using System.Runtime.Serialization; + using System.Runtime.Serialization; - internal enum ResponseType - { - Unknown = 0, + internal enum ResponseType + { + Unknown = 0, - Subscriptions, + Subscriptions, - Heartbeat, + Heartbeat, - Ticker, + Ticker, - Snapshot, + Snapshot, - L2Update, + L2Update, - Received, + Received, - Open, + Open, - Done, + Done, - Match, + Match, - [EnumMember(Value = "last_match")] - LastMatch, + [EnumMember(Value = "last_match")] + LastMatch, - Change, + Change, - Activate, + Activate, - Error, + Error, Status, - } + } } diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Types/Types.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Types/Types.cs index 694fd96ef..00ad6a89e 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Types/Types.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Types/Types.cs @@ -18,6 +18,7 @@ internal enum OrderSide { [EnumMember(Value = "buy")] Buy, + [EnumMember(Value = "sell")] Sell } @@ -26,8 +27,10 @@ internal enum StopType { [EnumMember(Value = "Unknown")] Unknown, + [EnumMember(Value = "loss")] Loss, + [EnumMember(Value = "entry")] Entry, } diff --git a/src/ExchangeSharp/API/Exchanges/Coincheck/ExchangeCoincheckAPI.cs b/src/ExchangeSharp/API/Exchanges/Coincheck/ExchangeCoincheckAPI.cs index 5bc2623a9..da3ee5439 100644 --- a/src/ExchangeSharp/API/Exchanges/Coincheck/ExchangeCoincheckAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Coincheck/ExchangeCoincheckAPI.cs @@ -1,9 +1,9 @@ -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Newtonsoft.Json.Linq; namespace ExchangeSharp { @@ -27,45 +27,59 @@ protected override async Task> OnGetMarketSymbolsAsync() return new[] { "btc_jpy", "etc_jpy", "fct_jpy", "mona_jpy", "plt_jpy", }; } - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + protected override async Task OnGetTradesWebSocketAsync( + Func, Task> callback, + params string[] marketSymbols + ) { if (marketSymbols == null || marketSymbols.Length == 0) { marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); } - return await ConnectPublicWebSocketAsync("", async (_socket, msg) => - { /*[ + return await ConnectPublicWebSocketAsync( + "", + async (_socket, msg) => + { /*[ 2357062, // 0 "ID", "[pair]", // 1 "Currency pair" "148638.0", // 2 "Order rate" "5.0", // 3 "Order amount" "sell" // 4 "Specify order_type." ]*/ - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - // no error msgs provided - if (token.Type == JTokenType.Array) - { - var trade = token.ParseTrade(3, 2, 4, null, TimestampType.None, 0); - string marketSymbol = token[1].ToStringInvariant(); - await callback(new KeyValuePair(marketSymbol, trade)); - } - else Logger.Warn($"Unexpected token type {token.Type}"); - }, async (_socket) => - { /*{ + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + // no error msgs provided + if (token.Type == JTokenType.Array) + { + var trade = token.ParseTrade(3, 2, 4, null, TimestampType.None, 0); + string marketSymbol = token[1].ToStringInvariant(); + await callback( + new KeyValuePair(marketSymbol, trade) + ); + } + else + Logger.Warn($"Unexpected token type {token.Type}"); + }, + async (_socket) => + { /*{ "type": "subscribe", "channel": "[pair]-trades" }*/ - foreach (var marketSymbol in marketSymbols) - { - var subscribeRequest = new - { - type = "subscribe", - channel = $"{marketSymbol}-trades", - }; - await _socket.SendMessageAsync(subscribeRequest); - } - }); + foreach (var marketSymbol in marketSymbols) + { + var subscribeRequest = new + { + type = "subscribe", + channel = $"{marketSymbol}-trades", + }; + await _socket.SendMessageAsync(subscribeRequest); + } + } + ); } } - public partial class ExchangeName { public const string Coincheck = "Coincheck"; } + + public partial class ExchangeName + { + public const string Coincheck = "Coincheck"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/Coinmate/ExchangeCoinmateAPI.cs b/src/ExchangeSharp/API/Exchanges/Coinmate/ExchangeCoinmateAPI.cs index 5189842d7..59bf1c606 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinmate/ExchangeCoinmateAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinmate/ExchangeCoinmateAPI.cs @@ -1,9 +1,9 @@ -using ExchangeSharp.API.Exchanges.Coinmate.Models; -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using ExchangeSharp.API.Exchanges.Coinmate.Models; +using Newtonsoft.Json.Linq; namespace ExchangeSharp { @@ -31,84 +31,144 @@ public string ClientId protected override async Task OnGetTickerAsync(string marketSymbol) { - var response = await MakeCoinmateRequest($"/ticker?currencyPair={marketSymbol}"); - return await this.ParseTickerAsync(response, marketSymbol, "ask", "bid", "last", "amount", null, "timestamp", TimestampType.UnixSeconds); + var response = await MakeCoinmateRequest( + $"/ticker?currencyPair={marketSymbol}" + ); + return await this.ParseTickerAsync( + response, + marketSymbol, + "ask", + "bid", + "last", + "amount", + null, + "timestamp", + TimestampType.UnixSeconds + ); } protected override async Task> OnGetMarketSymbolsAsync() { var response = await MakeCoinmateRequest("/products"); - return response.Select(x => $"{x.FromSymbol}{MarketSymbolSeparator}{x.ToSymbol}").ToArray(); + return response + .Select(x => $"{x.FromSymbol}{MarketSymbolSeparator}{x.ToSymbol}") + .ToArray(); } - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() + protected internal override async Task< + IEnumerable + > OnGetMarketSymbolsMetadataAsync() { var response = await MakeCoinmateRequest("/tradingPairs"); - return response.Select(x => new ExchangeMarket - { - IsActive = true, - BaseCurrency = x.FirstCurrency, - QuoteCurrency = x.SecondCurrency, - MarketSymbol = x.Name, - MinTradeSize = x.MinAmount, - PriceStepSize = 1 / (decimal)(Math.Pow(10, x.PriceDecimals)), - QuantityStepSize = 1 / (decimal)(Math.Pow(10, x.LotDecimals)) - }).ToArray(); + return response + .Select( + x => + new ExchangeMarket + { + IsActive = true, + BaseCurrency = x.FirstCurrency, + QuoteCurrency = x.SecondCurrency, + MarketSymbol = x.Name, + MinTradeSize = x.MinAmount, + PriceStepSize = 1 / (decimal)(Math.Pow(10, x.PriceDecimals)), + QuantityStepSize = 1 / (decimal)(Math.Pow(10, x.LotDecimals)) + } + ) + .ToArray(); } - protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) + protected override async Task OnGetOrderBookAsync( + string marketSymbol, + int maxCount = 100 + ) { - var book = await MakeCoinmateRequest("/orderBook?&groupByPriceLimit=False¤cyPair=" + marketSymbol); - var result = new ExchangeOrderBook - { - MarketSymbol = marketSymbol, - }; + var book = await MakeCoinmateRequest( + "/orderBook?&groupByPriceLimit=False¤cyPair=" + marketSymbol + ); + var result = new ExchangeOrderBook { MarketSymbol = marketSymbol, }; book.Asks - .GroupBy(x => x.Price) - .ToList() - .ForEach(x => result.Asks.Add(x.Key, new ExchangeOrderPrice { Amount = x.Sum(x => x.Amount), Price = x.Key })); + .GroupBy(x => x.Price) + .ToList() + .ForEach( + x => + result.Asks.Add( + x.Key, + new ExchangeOrderPrice { Amount = x.Sum(x => x.Amount), Price = x.Key } + ) + ); book.Bids - .GroupBy(x => x.Price) - .ToList() - .ForEach(x => result.Bids.Add(x.Key, new ExchangeOrderPrice { Amount = x.Sum(x => x.Amount), Price = x.Key })); + .GroupBy(x => x.Price) + .ToList() + .ForEach( + x => + result.Bids.Add( + x.Key, + new ExchangeOrderPrice { Amount = x.Sum(x => x.Amount), Price = x.Key } + ) + ); return result; } - protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null) + protected override async Task> OnGetRecentTradesAsync( + string marketSymbol, + int? limit = null + ) { - var txs = await MakeCoinmateRequest("/transactions?minutesIntoHistory=1440¤cyPair=" + marketSymbol); - return txs.Select(x => new ExchangeTrade - { - Amount = x.Amount, - Id = x.TransactionId, - IsBuy = x.TradeType == "BUY", - Price = x.Price, - Timestamp = CryptoUtility.ParseTimestamp(x.Timestamp, TimestampType.UnixMilliseconds) - }) - .Take(limit ?? int.MaxValue) - .ToArray(); + var txs = await MakeCoinmateRequest( + "/transactions?minutesIntoHistory=1440¤cyPair=" + marketSymbol + ); + return txs.Select( + x => + new ExchangeTrade + { + Amount = x.Amount, + Id = x.TransactionId, + IsBuy = x.TradeType == "BUY", + Price = x.Price, + Timestamp = CryptoUtility.ParseTimestamp( + x.Timestamp, + TimestampType.UnixMilliseconds + ) + } + ) + .Take(limit ?? int.MaxValue) + .ToArray(); } protected override async Task> OnGetAmountsAsync() { var payload = await GetNoncePayloadAsync(); - var balances = await MakeCoinmateRequest>("/balances", payload, "POST"); + var balances = await MakeCoinmateRequest>( + "/balances", + payload, + "POST" + ); return balances.ToDictionary(x => x.Key, x => x.Value.Balance); } - protected override async Task> OnGetAmountsAvailableToTradeAsync() + protected override async Task< + Dictionary + > OnGetAmountsAvailableToTradeAsync() { var payload = await GetNoncePayloadAsync(); - var balances = await MakeCoinmateRequest>("/balances", payload, "POST"); + var balances = await MakeCoinmateRequest>( + "/balances", + payload, + "POST" + ); return balances.ToDictionary(x => x.Key, x => x.Value.Available); } - protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnGetOrderDetailsAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { var payload = await GetNoncePayloadAsync(); @@ -126,7 +186,8 @@ protected override async Task OnGetOrderDetailsAsync(string o = await MakeCoinmateRequest("/orderById", payload, "POST"); } - if (o == null) return null; + if (o == null) + return null; return new ExchangeOrderResult { @@ -137,7 +198,10 @@ protected override async Task OnGetOrderDetailsAsync(string OrderId = o.Id.ToString(), Price = o.Price, IsBuy = o.Type == "BUY", - OrderDate = CryptoUtility.ParseTimestamp(o.Timestamp, TimestampType.UnixMilliseconds), + OrderDate = CryptoUtility.ParseTimestamp( + o.Timestamp, + TimestampType.UnixMilliseconds + ), ResultCode = o.Status, Result = o.Status switch { @@ -151,7 +215,11 @@ protected override async Task OnGetOrderDetailsAsync(string }; } - protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnCancelOrderAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { var payload = await GetNoncePayloadAsync(); payload["orderId"] = orderId; @@ -159,7 +227,9 @@ protected override async Task OnCancelOrderAsync(string orderId, string marketSy await MakeCoinmateRequest("/cancelOrder", payload, "POST"); } - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + protected override async Task OnPlaceOrderAsync( + ExchangeOrderRequest order + ) { var payload = await GetNoncePayloadAsync(); @@ -201,31 +271,51 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd } } - protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) + protected override async Task> OnGetOpenOrderDetailsAsync( + string marketSymbol = null + ) { var payload = await GetNoncePayloadAsync(); payload["currencyPair"] = marketSymbol; - var orders = await MakeCoinmateRequest("/openOrders", payload, "POST"); - - return orders.Select(x => new ExchangeOrderResult - { - Amount = x.Amount, - ClientOrderId = x.ClientOrderId?.ToString(), - IsBuy = x.Type == "BUY", - MarketSymbol = x.CurrencyPair, - OrderDate = CryptoUtility.ParseTimestamp(x.Timestamp, TimestampType.UnixMilliseconds), - OrderId = x.Id.ToString(), - Price = x.Price, - - }).ToArray(); + var orders = await MakeCoinmateRequest( + "/openOrders", + payload, + "POST" + ); + + return orders + .Select( + x => + new ExchangeOrderResult + { + Amount = x.Amount, + ClientOrderId = x.ClientOrderId?.ToString(), + IsBuy = x.Type == "BUY", + MarketSymbol = x.CurrencyPair, + OrderDate = CryptoUtility.ParseTimestamp( + x.Timestamp, + TimestampType.UnixMilliseconds + ), + OrderId = x.Id.ToString(), + Price = x.Price, + } + ) + .ToArray(); } - protected override async Task OnGetDepositAddressAsync(string currency, bool forceRegenerate = false) + protected override async Task OnGetDepositAddressAsync( + string currency, + bool forceRegenerate = false + ) { var payload = await GetNoncePayloadAsync(); var currencyName = GetCurrencyName(currency); - var addresses = await MakeCoinmateRequest($"/{currencyName}DepositAddresses", payload, "POST"); + var addresses = await MakeCoinmateRequest( + $"/{currencyName}DepositAddresses", + payload, + "POST" + ); return new ExchangeDepositDetails { @@ -234,7 +324,9 @@ protected override async Task OnGetDepositAddressAsync(s }; } - protected override async Task OnWithdrawAsync(ExchangeWithdrawalRequest withdrawalRequest) + protected override async Task OnWithdrawAsync( + ExchangeWithdrawalRequest withdrawalRequest + ) { var payload = await GetNoncePayloadAsync(); var currencyName = GetCurrencyName(withdrawalRequest.Currency); @@ -243,16 +335,19 @@ protected override async Task OnWithdrawAsync(Exchan payload["address"] = withdrawalRequest.Address; payload["amountType"] = withdrawalRequest.TakeFeeFromAmount ? "NET" : "GROSS"; - var id = await MakeCoinmateRequest($"/{currencyName}Withdrawal", payload, "POST"); + var id = await MakeCoinmateRequest( + $"/{currencyName}Withdrawal", + payload, + "POST" + ); - return new ExchangeWithdrawalResponse - { - Id = id?.ToString(), - Success = id != null - }; + return new ExchangeWithdrawalResponse { Id = id?.ToString(), Success = id != null }; } - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + protected override async Task ProcessRequestAsync( + IHttpWebRequest request, + Dictionary payload + ) { if (CanMakeAuthenticatedRequest(payload)) { @@ -263,7 +358,9 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti var apiKey = PublicApiKey.ToUnsecureString(); var messageToSign = payload["nonce"].ToStringInvariant() + ClientId + apiKey; - var signature = CryptoUtility.SHA256Sign(messageToSign, PrivateApiKey.ToUnsecureString()).ToUpperInvariant(); + var signature = CryptoUtility + .SHA256Sign(messageToSign, PrivateApiKey.ToUnsecureString()) + .ToUpperInvariant(); payload["signature"] = signature; payload["clientId"] = ClientId; payload["publicKey"] = apiKey; @@ -271,9 +368,18 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti } } - private async Task MakeCoinmateRequest(string url, Dictionary payload = null, string method = null) + private async Task MakeCoinmateRequest( + string url, + Dictionary payload = null, + string method = null + ) { - var response = await MakeJsonRequestAsync>(url, null, payload, method); + var response = await MakeJsonRequestAsync>( + url, + null, + payload, + method + ); if (response.Error) { @@ -298,6 +404,9 @@ private string GetCurrencyName(string currency) }; } - public partial class ExchangeName { public const string Coinmate = "Coinmate"; } + public partial class ExchangeName + { + public const string Coinmate = "Coinmate"; + } } } diff --git a/src/ExchangeSharp/API/Exchanges/Coinmate/Models/CoinmateOrderBook.cs b/src/ExchangeSharp/API/Exchanges/Coinmate/Models/CoinmateOrderBook.cs index b0c61eb7f..83cd0ce8a 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinmate/Models/CoinmateOrderBook.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinmate/Models/CoinmateOrderBook.cs @@ -4,7 +4,7 @@ public class CoinmateOrderBook { public AskBid[] Asks { get; set; } public AskBid[] Bids { get; set; } - + public class AskBid { public decimal Price { get; set; } diff --git a/src/ExchangeSharp/API/Exchanges/Coinmate/Models/CoinmateTransaction.cs b/src/ExchangeSharp/API/Exchanges/Coinmate/Models/CoinmateTransaction.cs index 90a19573f..a409574d4 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinmate/Models/CoinmateTransaction.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinmate/Models/CoinmateTransaction.cs @@ -7,6 +7,6 @@ public class CoinmateTransaction public decimal Price { get; set; } public decimal Amount { get; set; } public string CurrencyPair { get; set; } - public string TradeType { get; set; } + public string TradeType { get; set; } } } diff --git a/src/ExchangeSharp/API/Exchanges/CryptoCom/ExchangeCryptoComApi.cs b/src/ExchangeSharp/API/Exchanges/CryptoCom/ExchangeCryptoComApi.cs index 137554d73..8274d393d 100644 --- a/src/ExchangeSharp/API/Exchanges/CryptoCom/ExchangeCryptoComApi.cs +++ b/src/ExchangeSharp/API/Exchanges/CryptoCom/ExchangeCryptoComApi.cs @@ -1,9 +1,9 @@ -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Newtonsoft.Json.Linq; namespace ExchangeSharp { @@ -28,114 +28,155 @@ protected override async Task> OnGetMarketSymbolsAsync() var markets = new List(); foreach (JToken instrument in instruments["instruments"]) { - markets.Add(new ExchangeMarket - { - MarketSymbol = instrument["instrument_name"].ToStringUpperInvariant(), - QuoteCurrency = instrument["quote_currency"].ToStringInvariant(), - BaseCurrency = instrument["base_currency"].ToStringInvariant(), - }); + markets.Add( + new ExchangeMarket + { + MarketSymbol = instrument["instrument_name"].ToStringUpperInvariant(), + QuoteCurrency = instrument["quote_currency"].ToStringInvariant(), + BaseCurrency = instrument["base_currency"].ToStringInvariant(), + } + ); } return markets.Select(m => m.MarketSymbol); } - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + protected override async Task OnGetTradesWebSocketAsync( + Func, Task> callback, + params string[] marketSymbols + ) { if (marketSymbols == null || marketSymbols.Length == 0) { marketSymbols = new string[] { "" }; } - var ws = await ConnectPublicWebSocketAsync("/market", async (_socket, msg) => - { - /*{ - {{ - "code": 0, - "method": "subscribe", - "result": { - "instrument_name": "YFI_BTC", - "subscription": "trade.YFI_BTC", - "channel": "trade", - "data": [ - { - "dataTime": 1645139769555, - "d": 2258312914797956554, - "s": "BUY", - "p": 0.5541, - "q": 1E-06, - "t": 1645139769539, - "i": "YFI_BTC" - } - ] - } - }} - } */ - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - if (token["method"].ToStringInvariant() == "ERROR" || token["method"].ToStringInvariant() == "unknown") - { - throw new APIException(token["code"].ToStringInvariant() + ": " + token["message"].ToStringInvariant()); - } - else if (token["method"].ToStringInvariant() == "public/heartbeat") - { /* For websocket connections, the system will send a heartbeat message to the client every 30 seconds. - * The client must respond back with the public/respond-heartbeat method, using the same matching id, within 5 seconds, or the connection will break. */ - var hrResponse = new + var ws = await ConnectPublicWebSocketAsync( + "/market", + async (_socket, msg) => { - id = token["id"].ConvertInvariant(), - method = "public/respond-heartbeat", - }; - await _socket.SendMessageAsync(hrResponse); + /*{ + {{ + "code": 0, + "method": "subscribe", + "result": { + "instrument_name": "YFI_BTC", + "subscription": "trade.YFI_BTC", + "channel": "trade", + "data": [ + { + "dataTime": 1645139769555, + "d": 2258312914797956554, + "s": "BUY", + "p": 0.5541, + "q": 1E-06, + "t": 1645139769539, + "i": "YFI_BTC" + } + ] + } + }} + } */ + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + if ( + token["method"].ToStringInvariant() == "ERROR" + || token["method"].ToStringInvariant() == "unknown" + ) + { + throw new APIException( + token["code"].ToStringInvariant() + + ": " + + token["message"].ToStringInvariant() + ); + } + else if (token["method"].ToStringInvariant() == "public/heartbeat") + { /* For websocket connections, the system will send a heartbeat message to the client every 30 seconds. + * The client must respond back with the public/respond-heartbeat method, using the same matching id, within 5 seconds, or the connection will break. */ + var hrResponse = new + { + id = token["id"].ConvertInvariant(), + method = "public/respond-heartbeat", + }; + await _socket.SendMessageAsync(hrResponse); - if (token["message"].ToStringInvariant() == "server did not receive any client heartbeat, going to disconnect soon") - Logger.Warn(token["code"].ToStringInvariant() + ": " + token["message"].ToStringInvariant()); - } - else if (token["method"].ToStringInvariant() == "subscribe" && token["result"] != null) - { - var result = token["result"]; - var dataArray = result["data"].ToArray(); - for (int i = 0; i < dataArray.Length; i++) - { - JToken data = dataArray[i]; - var trade = data.ParseTrade("q", "p", "s", "t", TimestampType.UnixMilliseconds, "d"); - string marketSymbol = data["i"].ToStringInvariant(); - if (dataArray.Length == 100) // initial snapshot contains 100 trades + if ( + token["message"].ToStringInvariant() + == "server did not receive any client heartbeat, going to disconnect soon" + ) + Logger.Warn( + token["code"].ToStringInvariant() + + ": " + + token["message"].ToStringInvariant() + ); + } + else if ( + token["method"].ToStringInvariant() == "subscribe" + && token["result"] != null + ) { - trade.Flags |= ExchangeTradeFlags.IsFromSnapshot; - if (i == dataArray.Length - 1) - trade.Flags |= ExchangeTradeFlags.IsLastFromSnapshot; + var result = token["result"]; + var dataArray = result["data"].ToArray(); + for (int i = 0; i < dataArray.Length; i++) + { + JToken data = dataArray[i]; + var trade = data.ParseTrade( + "q", + "p", + "s", + "t", + TimestampType.UnixMilliseconds, + "d" + ); + string marketSymbol = data["i"].ToStringInvariant(); + if (dataArray.Length == 100) // initial snapshot contains 100 trades + { + trade.Flags |= ExchangeTradeFlags.IsFromSnapshot; + if (i == dataArray.Length - 1) + trade.Flags |= ExchangeTradeFlags.IsLastFromSnapshot; + } + await callback( + new KeyValuePair(marketSymbol, trade) + ); + } } - await callback(new KeyValuePair(marketSymbol, trade)); - } - } - }, async (_socket) => - { /* We recommend adding a 1-second sleep after establishing the websocket connection, and before requests are sent. + }, + async (_socket) => + { /* We recommend adding a 1-second sleep after establishing the websocket connection, and before requests are sent. * This will avoid occurrences of rate-limit (`TOO_MANY_REQUESTS`) errors, as the websocket rate limits are pro-rated based on the calendar-second that the websocket connection was opened. */ - await Task.Delay(1000); + await Task.Delay(1000); - /* - { - "id": 11, - "method": "subscribe", - "params": { - "channels": ["trade.ETH_CRO"] - }, - "nonce": 1587523073344 - } - */ - var subscribeRequest = new - { - // + consider using id field in the future to differentiate between requests - //id = new Random().Next(), - method = "subscribe", - @params = new - { - channels = marketSymbols.Select(s => string.IsNullOrWhiteSpace(s) ? "trade" : $"trade.{s}").ToArray(), - }, - nonce = await GenerateNonceAsync(), - }; - await _socket.SendMessageAsync(subscribeRequest); - }); + /* + { + "id": 11, + "method": "subscribe", + "params": { + "channels": ["trade.ETH_CRO"] + }, + "nonce": 1587523073344 + } + */ + var subscribeRequest = new + { + // + consider using id field in the future to differentiate between requests + //id = new Random().Next(), + method = "subscribe", + @params = new + { + channels = marketSymbols + .Select(s => string.IsNullOrWhiteSpace(s) ? "trade" : $"trade.{s}") + .ToArray(), + }, + nonce = await GenerateNonceAsync(), + }; + await _socket.SendMessageAsync(subscribeRequest); + } + ); ws.KeepAlive = new TimeSpan(0); // cryptocom throws bad request empty content msgs w/ keepalives return ws; } } - public partial class ExchangeName { public const string CryptoCom = "CryptoCom"; } + + public partial class ExchangeName + { + public const string CryptoCom = "CryptoCom"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/Digifinex/ExchangeDigifinexAPI.cs b/src/ExchangeSharp/API/Exchanges/Digifinex/ExchangeDigifinexAPI.cs index e6c541be4..96b331896 100644 --- a/src/ExchangeSharp/API/Exchanges/Digifinex/ExchangeDigifinexAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Digifinex/ExchangeDigifinexAPI.cs @@ -1,4 +1,3 @@ -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; @@ -6,6 +5,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Newtonsoft.Json.Linq; namespace ExchangeSharp { @@ -13,17 +13,18 @@ public partial class ExchangeDigifinexAPI : ExchangeAPI { private string[] Urls = { - "openapi.digifinex.com", - "openapi.digifinex.vip", // these other URLs don't work anymore - "openapi.digifinex.xyz", - }; + "openapi.digifinex.com", + "openapi.digifinex.vip", // these other URLs don't work anymore + "openapi.digifinex.xyz", + }; private string fastestUrl = null; private int failedUrlCount; private int successUrlCount; public override string BaseUrl { get; set; } = "https://openapi.digifinex.com/v3"; - public override string BaseUrlWebSocket { get; set; } = "wss://openapi.digifinex.com/ws/v1/"; + public override string BaseUrlWebSocket { get; set; } = + "wss://openapi.digifinex.com/ws/v1/"; private int websocketMessageId = 0; private string timeWindow; private TaskCompletionSource inited = new TaskCompletionSource(); @@ -40,7 +41,7 @@ private ExchangeDigifinexAPI() } private void GetFastestUrl() - { + { //var client = new HttpClient(); //foreach (var url in Urls) //{ @@ -76,13 +77,15 @@ protected override async Task OnGetNonceOffset() await inited.Task; var start = CryptoUtility.UtcNow; JToken token = await MakeJsonRequestAsync("/time"); - DateTime serverDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(token["server_time"].ConvertInvariant()); + DateTime serverDate = CryptoUtility.UnixTimeStampToDateTimeSeconds( + token["server_time"].ConvertInvariant() + ); var end = CryptoUtility.UtcNow; var now = start + TimeSpan.FromMilliseconds((end - start).TotalMilliseconds); var timeFaster = now - serverDate; timeWindow = "30"; // max latency of 30s NonceOffset = now - serverDate; // how much time to substract from Nonce when making a request - //Console.WriteLine($"NonceOffset {GetHashCode()}: {NonceOffset}"); + //Console.WriteLine($"NonceOffset {GetHashCode()}: {NonceOffset}"); } catch { @@ -90,7 +93,10 @@ protected override async Task OnGetNonceOffset() } } - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + protected override async Task ProcessRequestAsync( + IHttpWebRequest request, + Dictionary payload + ) { await inited.Task; var query = request.RequestUri.Query.TrimStart('?'); @@ -106,7 +112,10 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti query += '&'; query += body; } - string signature = CryptoUtility.SHA256Sign(query, CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey)); + string signature = CryptoUtility.SHA256Sign( + query, + CryptoUtility.ToUnsecureBytesUTF8(PrivateApiKey) + ); request.AddHeader("ACCESS-KEY", PublicApiKey.ToUnsecureString()); request.AddHeader("ACCESS-SIGN", signature); request.AddHeader("ACCESS-TIMESTAMP", nonce.ToStringInvariant()); @@ -160,7 +169,9 @@ private async Task ParseExchangeMarketAsync(JToken x) }; } - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() + protected internal override async Task< + IEnumerable + > OnGetMarketSymbolsMetadataAsync() { await inited.Task; JToken obj = await MakeJsonRequestAsync("markets"); @@ -198,7 +209,9 @@ private async Task ParseTickerAsync(JToken x) QuoteCurrency = quoteCurrency, QuoteCurrencyVolume = t["base_vol"].ConvertInvariant(), BaseCurrencyVolume = t["vol"].ConvertInvariant(), - Timestamp = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["date"].ConvertInvariant()), + Timestamp = CryptoUtility.UnixTimeStampToDateTimeSeconds( + x["date"].ConvertInvariant() + ), }, }; } @@ -209,31 +222,56 @@ protected override async Task OnGetTickerAsync(string marketSymb return await ParseTickerAsync(obj); } - protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) + protected override async Task OnGetOrderBookAsync( + string marketSymbol, + int maxCount = 100 + ) { - JToken obj = await MakeJsonRequestAsync($"/order_book?symbol={marketSymbol}&limit={maxCount}"); + JToken obj = await MakeJsonRequestAsync( + $"/order_book?symbol={marketSymbol}&limit={maxCount}" + ); var result = obj.ParseOrderBookFromJTokenArrays(sequence: "date"); - result.LastUpdatedUtc = CryptoUtility.UnixTimeStampToDateTimeSeconds(obj["date"].ConvertInvariant()); + result.LastUpdatedUtc = CryptoUtility.UnixTimeStampToDateTimeSeconds( + obj["date"].ConvertInvariant() + ); result.MarketSymbol = marketSymbol; return result; } - protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null) + protected override async Task> OnGetRecentTradesAsync( + string marketSymbol, + int? limit = null + ) { - JToken obj = await MakeJsonRequestAsync($"/trades?symbol={marketSymbol}&limit={limit ?? 500}"); // maximum limit = 500 - return obj["data"].Select(x => new ExchangeTrade - { - Id = x["id"].ToStringInvariant(), - Amount = x["amount"].ConvertInvariant(), - Price = x["price"].ConvertInvariant(), - IsBuy = x["type"].ToStringLowerInvariant() != "sell", - Timestamp = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["date"].ConvertInvariant()), - Flags = x["type"].ToStringLowerInvariant() == "sell" ? default : ExchangeTradeFlags.IsBuy, - }); + JToken obj = await MakeJsonRequestAsync( + $"/trades?symbol={marketSymbol}&limit={limit ?? 500}" + ); // maximum limit = 500 + return obj["data"].Select( + x => + new ExchangeTrade + { + Id = x["id"].ToStringInvariant(), + Amount = x["amount"].ConvertInvariant(), + Price = x["price"].ConvertInvariant(), + IsBuy = x["type"].ToStringLowerInvariant() != "sell", + Timestamp = CryptoUtility.UnixTimeStampToDateTimeSeconds( + x["date"].ConvertInvariant() + ), + Flags = + x["type"].ToStringLowerInvariant() == "sell" + ? default + : ExchangeTradeFlags.IsBuy, + } + ); } protected override async Task> OnGetCandlesAsync( - string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + string marketSymbol, + int periodSeconds, + DateTime? startDate = null, + DateTime? endDate = null, + int? limit = null + ) { string period; if (periodSeconds <= 60 * 720) @@ -243,11 +281,16 @@ protected override async Task> OnGetCandlesAsync( else if (periodSeconds == 7 * 24 * 60 * 60) period = "1W"; else - throw new ArgumentException($"Unsupported periodSeconds: {periodSeconds}", "periodSeconds"); + throw new ArgumentException( + $"Unsupported periodSeconds: {periodSeconds}", + "periodSeconds" + ); var url = $"/kline?symbol={marketSymbol}&period={period}"; if (startDate != null && endDate != null && limit != null) - throw new ArgumentException("Cannot specify `startDate`, `endDate` and `limit` all at the same time"); + throw new ArgumentException( + "Cannot specify `startDate`, `endDate` and `limit` all at the same time" + ); if (limit != null) { if (startDate != null) @@ -266,15 +309,20 @@ protected override async Task> OnGetCandlesAsync( url += $"&end_time={new DateTimeOffset(endDate.Value).ToUnixTimeSeconds()}"; JToken obj = await MakeJsonRequestAsync(url); - return obj["data"].Select(x => new MarketCandle - { - Timestamp = CryptoUtility.UnixTimeStampToDateTimeSeconds(x[0].ConvertInvariant()), - BaseCurrencyVolume = x[1].ConvertInvariant(), - ClosePrice = x[2].ConvertInvariant(), - HighPrice = x[3].ConvertInvariant(), - LowPrice = x[4].ConvertInvariant(), - OpenPrice = x[5].ConvertInvariant(), - }); + return obj["data"].Select( + x => + new MarketCandle + { + Timestamp = CryptoUtility.UnixTimeStampToDateTimeSeconds( + x[0].ConvertInvariant() + ), + BaseCurrencyVolume = x[1].ConvertInvariant(), + ClosePrice = x[2].ConvertInvariant(), + HighPrice = x[3].ConvertInvariant(), + LowPrice = x[4].ConvertInvariant(), + OpenPrice = x[5].ConvertInvariant(), + } + ); } #endregion Public APIs @@ -306,7 +354,9 @@ private ExchangeAPIOrderResult ParseOrderStatus(JToken token) } } - protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) + protected override async Task> OnGetOpenOrderDetailsAsync( + string marketSymbol = null + ) { Dictionary payload = await GetNoncePayloadAsync(); var url = "/spot/order/current"; @@ -316,23 +366,31 @@ protected override async Task> OnGetOpenOrderDe JToken token = await MakeJsonRequestAsync(url, payload: payload); var list = token["data"]; - return list.Select(x => new ExchangeOrderResult - { - MarketSymbol = x["symbol"].ToStringUpperInvariant(), - OrderId = x["order_id"].ToStringInvariant(), - OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["created_date"].ConvertInvariant()), - CompletedDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["finished_date"].ConvertInvariant()), - Price = x["price"].ConvertInvariant(), - AveragePrice = x["avg_price"].ConvertInvariant(), - Amount = x["amount"].ConvertInvariant(), - AmountFilled = x["executed_amount"].ConvertInvariant(), - IsBuy = x["type"].ToStringLowerInvariant() == "buy", - Result = ParseOrderStatus(x["status"]), - }); + return list.Select( + x => + new ExchangeOrderResult + { + MarketSymbol = x["symbol"].ToStringUpperInvariant(), + OrderId = x["order_id"].ToStringInvariant(), + OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds( + x["created_date"].ConvertInvariant() + ), + CompletedDate = CryptoUtility.UnixTimeStampToDateTimeSeconds( + x["finished_date"].ConvertInvariant() + ), + Price = x["price"].ConvertInvariant(), + AveragePrice = x["avg_price"].ConvertInvariant(), + Amount = x["amount"].ConvertInvariant(), + AmountFilled = x["executed_amount"].ConvertInvariant(), + IsBuy = x["type"].ToStringLowerInvariant() == "buy", + Result = ParseOrderStatus(x["status"]), + } + ); } - protected override async Task> OnGetCompletedOrderDetailsAsync( - string marketSymbol = null, DateTime? afterDate = null) + protected override async Task< + IEnumerable + > OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) { Dictionary payload = await GetNoncePayloadAsync(); var url = "/spot/mytrades?limit=500"; @@ -348,35 +406,54 @@ protected override async Task> OnGetCompletedOr JToken token = await MakeJsonRequestAsync(url, payload: payload); var list = token["list"]; - return list.Select(x => new ExchangeOrderResult - { - MarketSymbol = x["symbol"].ToStringUpperInvariant(), - OrderId = x["order_id"].ToStringInvariant(), - TradeId = x["id"].ToStringInvariant(), - Price = x["price"].ConvertInvariant(), - AmountFilled = x["amount"].ConvertInvariant(), - Fees = x["fee"].ConvertInvariant(), - FeesCurrency = x["fee_currency"].ToStringInvariant(), - // OrderDate - not provided here. ideally would be null but ExchangeOrderResult.OrderDate is not nullable - CompletedDate = null, // order not necessarily fully filled at this point - TradeDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["timestamp"].ConvertInvariant()), - IsBuy = x["side"].ToStringLowerInvariant() == "buy", - Result = ExchangeAPIOrderResult.Unknown, - }); + return list.Select( + x => + new ExchangeOrderResult + { + MarketSymbol = x["symbol"].ToStringUpperInvariant(), + OrderId = x["order_id"].ToStringInvariant(), + TradeId = x["id"].ToStringInvariant(), + Price = x["price"].ConvertInvariant(), + AmountFilled = x["amount"].ConvertInvariant(), + Fees = x["fee"].ConvertInvariant(), + FeesCurrency = x["fee_currency"].ToStringInvariant(), + // OrderDate - not provided here. ideally would be null but ExchangeOrderResult.OrderDate is not nullable + CompletedDate = null, // order not necessarily fully filled at this point + TradeDate = CryptoUtility.UnixTimeStampToDateTimeSeconds( + x["timestamp"].ConvertInvariant() + ), + IsBuy = x["side"].ToStringLowerInvariant() == "buy", + Result = ExchangeAPIOrderResult.Unknown, + } + ); } - protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnGetOrderDetailsAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { - if (isClientOrderId) throw new NotSupportedException("Querying by client order ID is not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature"); + if (isClientOrderId) + throw new NotSupportedException( + "Querying by client order ID is not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature" + ); Dictionary payload = await GetNoncePayloadAsync(); - JToken token = await MakeJsonRequestAsync($"/spot/order?order_id={orderId}", payload: payload); + JToken token = await MakeJsonRequestAsync( + $"/spot/order?order_id={orderId}", + payload: payload + ); var x = token["data"]; return new ExchangeOrderResult { MarketSymbol = x["symbol"].ToStringUpperInvariant(), OrderId = x["order_id"].ToStringInvariant(), - OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["created_date"].ConvertInvariant()), - CompletedDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(x["finished_date"].ConvertInvariant()), + OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds( + x["created_date"].ConvertInvariant() + ), + CompletedDate = CryptoUtility.UnixTimeStampToDateTimeSeconds( + x["finished_date"].ConvertInvariant() + ), Price = x["price"].ConvertInvariant(), AveragePrice = x["avg_price"].ConvertInvariant(), Amount = x["amount"].ConvertInvariant(), @@ -391,15 +468,25 @@ protected override async Task> OnGetAmountsAsync() Dictionary payload = await GetNoncePayloadAsync(); JToken token = await MakeJsonRequestAsync("/spot/assets", payload: payload); var list = token["list"]; - return list.Where(x => x["total"].ConvertInvariant() != 0m).ToDictionary(x => x["currency"].ToStringUpperInvariant(), x => x["total"].ConvertInvariant()); + return list.Where(x => x["total"].ConvertInvariant() != 0m) + .ToDictionary( + x => x["currency"].ToStringUpperInvariant(), + x => x["total"].ConvertInvariant() + ); } - protected override async Task> OnGetAmountsAvailableToTradeAsync() + protected override async Task< + Dictionary + > OnGetAmountsAvailableToTradeAsync() { Dictionary payload = await GetNoncePayloadAsync(); JToken token = await MakeJsonRequestAsync("/spot/assets", payload: payload); var list = token["list"]; - return list.Where(x => x["free"].ConvertInvariant() != 0m).ToDictionary(x => x["currency"].ToStringUpperInvariant(), x => x["free"].ConvertInvariant()); + return list.Where(x => x["free"].ConvertInvariant() != 0m) + .ToDictionary( + x => x["currency"].ToStringUpperInvariant(), + x => x["free"].ConvertInvariant() + ); } private string GetOrderType(ExchangeOrderRequest order) @@ -415,30 +502,51 @@ private string GetOrderType(ExchangeOrderRequest order) break; default: - throw new ArgumentException($"Unsupported order type `{order.OrderType}`", "OrderType"); + throw new ArgumentException( + $"Unsupported order type `{order.OrderType}`", + "OrderType" + ); } return result; } - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + protected override async Task OnPlaceOrderAsync( + ExchangeOrderRequest order + ) { Dictionary payload = await GetNoncePayloadAsync(); payload["symbol"] = order.MarketSymbol; payload["type"] = GetOrderType(order); payload["price"] = order.Price; payload["amount"] = order.Amount; - if (order.IsPostOnly != null) payload["post_only"] = order.IsPostOnly.Value ? "1" : "0"; // Default 0, enabled by 1, if enabled the order will be cancelled if it can be executed immediately, making sure there will be no market taking + if (order.IsPostOnly != null) + payload["post_only"] = order.IsPostOnly.Value ? "1" : "0"; // Default 0, enabled by 1, if enabled the order will be cancelled if it can be executed immediately, making sure there will be no market taking var market = order.IsMargin ? "margin" : "spot"; - JToken token = await MakeJsonRequestAsync($"/{market}/order/new", payload: payload, requestMethod: "POST"); + JToken token = await MakeJsonRequestAsync( + $"/{market}/order/new", + payload: payload, + requestMethod: "POST" + ); return new ExchangeOrderResult { OrderId = token["order_id"].ToStringInvariant() }; } - protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnCancelOrderAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { - if (isClientOrderId) throw new NotSupportedException("Cancelling by client order ID is not supported in ExchangeSharp. Please submit a PR if you are interested in this feature"); + if (isClientOrderId) + throw new NotSupportedException( + "Cancelling by client order ID is not supported in ExchangeSharp. Please submit a PR if you are interested in this feature" + ); Dictionary payload = await GetNoncePayloadAsync(); payload["order_id"] = orderId; - JToken token = await MakeJsonRequestAsync("/spot/order/cancel", payload: payload, requestMethod: "POST"); + JToken token = await MakeJsonRequestAsync( + "/spot/order/cancel", + payload: payload, + requestMethod: "POST" + ); //{ // "code": 0, // "success": [ @@ -455,7 +563,10 @@ protected override async Task OnCancelOrderAsync(string orderId, string marketSy #region WebSocket APIs - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + protected override async Task OnGetTradesWebSocketAsync( + Func, Task> callback, + params string[] marketSymbols + ) { await inited.Task; if (callback == null) @@ -467,77 +578,102 @@ protected override async Task OnGetTradesWebSocketAsync(Func - { - // { - // "method": "trades.update", - // "params": - // [ - // true, - // [ - // { - // "id": 7172173, - // "time": 1523339279.761838, - // "price": "398.59", - // "amount": "0.027", - // "type": "buy" - // } - // ], - // "ETH_USDT" - // ], - // "id": null - // } - JToken token = JToken.Parse(CryptoUtility.DecompressDeflate((new ArraySegment(msg, 2, msg.Length - 2)).ToArray()).ToStringFromUTF8()); - // doesn't send error msgs - just disconnects - if (token["method"].ToStringLowerInvariant() == "trades.update") - { - var args = token["params"]; - var clean = (bool)args[0]; - var trades = args[1]; - var symbol = args[2].ToStringUpperInvariant(); - - var x = trades as JArray; - for (int i = 0; i < x.Count; i++) + return await ConnectPublicWebSocketAsync( + string.Empty, + async (_socket, msg) => { - var trade = x[i]; - var isBuy = trade["type"].ToStringLowerInvariant() != "sell"; - var flags = default(ExchangeTradeFlags); - if (isBuy) + // { + // "method": "trades.update", + // "params": + // [ + // true, + // [ + // { + // "id": 7172173, + // "time": 1523339279.761838, + // "price": "398.59", + // "amount": "0.027", + // "type": "buy" + // } + // ], + // "ETH_USDT" + // ], + // "id": null + // } + JToken token = JToken.Parse( + CryptoUtility + .DecompressDeflate( + (new ArraySegment(msg, 2, msg.Length - 2)).ToArray() + ) + .ToStringFromUTF8() + ); + // doesn't send error msgs - just disconnects + if (token["method"].ToStringLowerInvariant() == "trades.update") { - flags |= ExchangeTradeFlags.IsBuy; - if (clean) + var args = token["params"]; + var clean = (bool)args[0]; + var trades = args[1]; + var symbol = args[2].ToStringUpperInvariant(); + + var x = trades as JArray; + for (int i = 0; i < x.Count; i++) { - flags |= ExchangeTradeFlags.IsFromSnapshot; - if (i == x.Count - 1) + var trade = x[i]; + var isBuy = trade["type"].ToStringLowerInvariant() != "sell"; + var flags = default(ExchangeTradeFlags); + if (isBuy) { - flags |= ExchangeTradeFlags.IsLastFromSnapshot; + flags |= ExchangeTradeFlags.IsBuy; + if (clean) + { + flags |= ExchangeTradeFlags.IsFromSnapshot; + if (i == x.Count - 1) + { + flags |= ExchangeTradeFlags.IsLastFromSnapshot; + } + } + await callback.Invoke( + new KeyValuePair( + symbol, + new ExchangeTrade + { + Id = trade["id"].ToStringInvariant(), + Timestamp = CryptoUtility + .UnixTimeStampToDateTimeSeconds(0) + .AddSeconds( + trade["time"].ConvertInvariant() + ), + Price = trade["price"].ConvertInvariant(), + Amount = trade["amount"].ConvertInvariant(), + IsBuy = isBuy, + Flags = flags, + } + ) + ); } } - await callback.Invoke(new KeyValuePair - ( - symbol, - new ExchangeTrade - { - Id = trade["id"].ToStringInvariant(), - Timestamp = CryptoUtility.UnixTimeStampToDateTimeSeconds(0).AddSeconds(trade["time"].ConvertInvariant()), - Price = trade["price"].ConvertInvariant(), - Amount = trade["amount"].ConvertInvariant(), - IsBuy = isBuy, - Flags = flags, - } - )); } + }, + async (_socket2) => + { + var id = Interlocked.Increment(ref websocketMessageId); + await _socket2.SendMessageAsync( + new + { + id, + method = "trades.subscribe", + @params = marketSymbols + } + ); } - } - }, - async (_socket2) => - { - var id = Interlocked.Increment(ref websocketMessageId); - await _socket2.SendMessageAsync(new { id, method = "trades.subscribe", @params = marketSymbols }); - }); + ); } - protected override async Task OnGetDeltaOrderBookWebSocketAsync(Action callback, int maxCount = 20, params string[] marketSymbols) + protected override async Task OnGetDeltaOrderBookWebSocketAsync( + Action callback, + int maxCount = 20, + params string[] marketSymbols + ) { if (callback == null) { @@ -545,66 +681,98 @@ protected override async Task OnGetDeltaOrderBookWebSocketAsync(Acti } await inited.Task; - return await ConnectPublicWebSocketAsync(string.Empty, (_socket, msg) => - { - //{ - // "method": "depth.update", - // "params": [ - // true, - // { - // "asks": [ - // [ - // "10249.68000000", - // "0.00200000" - // ], - // [ - // "10249.67000000", - // "0.00110000" - // ] - // ], - // "bids": [ - // [ - // "10249.61000000", - // "0.86570000" - // ], - // [ - // "10248.44000000", - // "1.00190000" - // ] - // ] - // }, - // "BTC_USDT" - // ], - // "id": null - //} - JToken token = JToken.Parse(CryptoUtility.DecompressDeflate((new ArraySegment(msg, 2, msg.Length - 2)).ToArray()).ToStringFromUTF8()); - if (token["method"].ToStringLowerInvariant() == "depth.update") - { - var args = token["params"]; - var data = args[1]; - var book = new ExchangeOrderBook { LastUpdatedUtc = CryptoUtility.UtcNow, MarketSymbol = args[2].ToStringUpperInvariant() }; - foreach (var x in data["asks"]) + return await ConnectPublicWebSocketAsync( + string.Empty, + (_socket, msg) => { - var price = x[0].ConvertInvariant(); - book.Asks[price] = new ExchangeOrderPrice { Price = price, Amount = x[1].ConvertInvariant() }; - } - foreach (var x in data["bids"]) + //{ + // "method": "depth.update", + // "params": [ + // true, + // { + // "asks": [ + // [ + // "10249.68000000", + // "0.00200000" + // ], + // [ + // "10249.67000000", + // "0.00110000" + // ] + // ], + // "bids": [ + // [ + // "10249.61000000", + // "0.86570000" + // ], + // [ + // "10248.44000000", + // "1.00190000" + // ] + // ] + // }, + // "BTC_USDT" + // ], + // "id": null + //} + JToken token = JToken.Parse( + CryptoUtility + .DecompressDeflate( + (new ArraySegment(msg, 2, msg.Length - 2)).ToArray() + ) + .ToStringFromUTF8() + ); + if (token["method"].ToStringLowerInvariant() == "depth.update") + { + var args = token["params"]; + var data = args[1]; + var book = new ExchangeOrderBook + { + LastUpdatedUtc = CryptoUtility.UtcNow, + MarketSymbol = args[2].ToStringUpperInvariant() + }; + foreach (var x in data["asks"]) + { + var price = x[0].ConvertInvariant(); + book.Asks[price] = new ExchangeOrderPrice + { + Price = price, + Amount = x[1].ConvertInvariant() + }; + } + foreach (var x in data["bids"]) + { + var price = x[0].ConvertInvariant(); + book.Bids[price] = new ExchangeOrderPrice + { + Price = price, + Amount = x[1].ConvertInvariant() + }; + } + callback(book); + } + return Task.CompletedTask; + }, + async (_socket) => { - var price = x[0].ConvertInvariant(); - book.Bids[price] = new ExchangeOrderPrice { Price = price, Amount = x[1].ConvertInvariant() }; + var id = Interlocked.Increment(ref websocketMessageId); + await _socket.SendMessageAsync( + new + { + id, + method = "depth.subscribe", + @params = marketSymbols + } + ); } - callback(book); - } - return Task.CompletedTask; - }, async (_socket) => - { - var id = Interlocked.Increment(ref websocketMessageId); - await _socket.SendMessageAsync(new { id, method = "depth.subscribe", @params = marketSymbols }); - }); + ); } #endregion WebSocket APIs } - public partial class ExchangeName { public const string Digifinex = "Digifinex"; } + public partial class ExchangeName + { + public const string Digifinex = "Digifinex"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/Dydx/ExchangeDydxApi.cs b/src/ExchangeSharp/API/Exchanges/Dydx/ExchangeDydxApi.cs index a441b2170..3be0acd1a 100644 --- a/src/ExchangeSharp/API/Exchanges/Dydx/ExchangeDydxApi.cs +++ b/src/ExchangeSharp/API/Exchanges/Dydx/ExchangeDydxApi.cs @@ -1,9 +1,9 @@ -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Newtonsoft.Json.Linq; namespace ExchangeSharp { @@ -25,146 +25,171 @@ public ExchangeDydxApi() protected override async Task> OnGetMarketSymbolsAsync() { /*{ - "markets": { - "LINK-USD": { - "market": "LINK-USD", - "status": "ONLINE", - "baseAsset": "LINK", - "quoteAsset": "USD", - "stepSize": "0.1", - "tickSize": "0.01", - "indexPrice": "12", - "oraclePrice": "101", - "priceChange24H": "0", - "nextFundingRate": "0.0000125000", - "nextFundingAt": "2021-03-01T18:00:00.000Z", - "minOrderSize": "1", - "type": "PERPETUAL", - "initialMarginFraction": "0.10", - "maintenanceMarginFraction": "0.05", - "baselinePositionSize": "1000", - "incrementalPositionSize": "1000", - "incrementalInitialMarginFraction": "0.2", - "volume24H": "0", - "trades24H": "0", - "openInterest": "0", - "maxPositionSize": "10000", - "assetResolution": "10000000", - "syntheticAssetId": "0x4c494e4b2d37000000000000000000", - }, - ... + "markets": { + "LINK-USD": { + "market": "LINK-USD", + "status": "ONLINE", + "baseAsset": "LINK", + "quoteAsset": "USD", + "stepSize": "0.1", + "tickSize": "0.01", + "indexPrice": "12", + "oraclePrice": "101", + "priceChange24H": "0", + "nextFundingRate": "0.0000125000", + "nextFundingAt": "2021-03-01T18:00:00.000Z", + "minOrderSize": "1", + "type": "PERPETUAL", + "initialMarginFraction": "0.10", + "maintenanceMarginFraction": "0.05", + "baselinePositionSize": "1000", + "incrementalPositionSize": "1000", + "incrementalInitialMarginFraction": "0.2", + "volume24H": "0", + "trades24H": "0", + "openInterest": "0", + "maxPositionSize": "10000", + "assetResolution": "10000000", + "syntheticAssetId": "0x4c494e4b2d37000000000000000000", + }, + ... }*/ var instruments = await MakeJsonRequestAsync("v3/markets"); var markets = new List(); foreach (JToken instrument in instruments["markets"]) { - markets.Add(new ExchangeMarket - { - MarketSymbol = instrument.ElementAt(0)["market"].ToStringInvariant(), - QuoteCurrency = instrument.ElementAt(0)["quoteAsset"].ToStringInvariant(), - BaseCurrency = instrument.ElementAt(0)["baseAsset"].ToStringInvariant(), - }); + markets.Add( + new ExchangeMarket + { + MarketSymbol = instrument.ElementAt(0)["market"].ToStringInvariant(), + QuoteCurrency = instrument.ElementAt(0)["quoteAsset"].ToStringInvariant(), + BaseCurrency = instrument.ElementAt(0)["baseAsset"].ToStringInvariant(), + } + ); } return markets.Select(m => m.MarketSymbol); } - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + protected override async Task OnGetTradesWebSocketAsync( + Func, Task> callback, + params string[] marketSymbols + ) { if (marketSymbols == null || marketSymbols.Length == 0) { marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); } - return await ConnectPublicWebSocketAsync("", async (_socket, msg) => - { - /*Example initial response: - { - "type": "subscribed", - "id": "BTC-USD", - "connection_id": "e2a6c717-6f77-4c1c-ac22-72ce2b7ed77d", - "channel": "v3_trades", - "message_id": 1, - "contents": { - "trades": [ - { - "side": "BUY", - "size": "100", - "price": "4000", - "createdAt": "2020-10-29T00:26:30.759Z" - }, - { - "side": "BUY", - "size": "100", - "price": "4000", - "createdAt": "2020-11-02T19:45:42.886Z" - }, - { - "side": "BUY", - "size": "100", - "price": "4000", - "createdAt": "2020-10-29T00:26:57.382Z" - } - ] - } - }*/ - /* Example subsequent response - { - "type": "channel_data", - "id": "BTC-USD", - "connection_id": "e2a6c717-6f77-4c1c-ac22-72ce2b7ed77d", - "channel": "v3_trades", - "message_id": 2, - "contents": { - "trades": [ + return await ConnectPublicWebSocketAsync( + "", + async (_socket, msg) => + { + /*Example initial response: { - "side": "BUY", - "size": "100", - "price": "4000", - "createdAt": "2020-11-29T00:26:30.759Z" - }, + "type": "subscribed", + "id": "BTC-USD", + "connection_id": "e2a6c717-6f77-4c1c-ac22-72ce2b7ed77d", + "channel": "v3_trades", + "message_id": 1, + "contents": { + "trades": [ + { + "side": "BUY", + "size": "100", + "price": "4000", + "createdAt": "2020-10-29T00:26:30.759Z" + }, + { + "side": "BUY", + "size": "100", + "price": "4000", + "createdAt": "2020-11-02T19:45:42.886Z" + }, + { + "side": "BUY", + "size": "100", + "price": "4000", + "createdAt": "2020-10-29T00:26:57.382Z" + } + ] + } + }*/ + /* Example subsequent response { - "side": "SELL", - "size": "100", - "price": "4000", - "createdAt": "2020-11-29T14:00:03.382Z" + "type": "channel_data", + "id": "BTC-USD", + "connection_id": "e2a6c717-6f77-4c1c-ac22-72ce2b7ed77d", + "channel": "v3_trades", + "message_id": 2, + "contents": { + "trades": [ + { + "side": "BUY", + "size": "100", + "price": "4000", + "createdAt": "2020-11-29T00:26:30.759Z" + }, + { + "side": "SELL", + "size": "100", + "price": "4000", + "createdAt": "2020-11-29T14:00:03.382Z" + } + ] + } + } */ + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + if (token["type"].ToStringInvariant() == "error") + { + throw new APIException(token["message"].ToStringInvariant()); } - ] - } - } */ - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - if (token["type"].ToStringInvariant() == "error") - { - throw new APIException(token["message"].ToStringInvariant()); - } - else if (token["channel"].ToStringInvariant() == "v3_trades") - { - var tradesArray = token["contents"]["trades"].ToArray(); - for (int i = 0; i < tradesArray.Length; i++) + else if (token["channel"].ToStringInvariant() == "v3_trades") + { + var tradesArray = token["contents"]["trades"].ToArray(); + for (int i = 0; i < tradesArray.Length; i++) + { + var trade = tradesArray[i].ParseTrade( + "size", + "price", + "side", + "createdAt", + TimestampType.Iso8601UTC, + null + ); + string marketSymbol = token["id"].ToStringInvariant(); + if ( + token["type"].ToStringInvariant() == "subscribed" + || token["message_id"].ToObject() == 1 + ) + { + trade.Flags |= ExchangeTradeFlags.IsFromSnapshot; + if (i == tradesArray.Length - 1) + trade.Flags |= ExchangeTradeFlags.IsLastFromSnapshot; + } + await callback( + new KeyValuePair(marketSymbol, trade) + ); + } + } + }, + async (_socket) => { - var trade = tradesArray[i].ParseTrade("size", "price", "side", "createdAt", TimestampType.Iso8601UTC, null); - string marketSymbol = token["id"].ToStringInvariant(); - if (token["type"].ToStringInvariant() == "subscribed" || token["message_id"].ToObject() == 1) + foreach (var marketSymbol in marketSymbols) { - trade.Flags |= ExchangeTradeFlags.IsFromSnapshot; - if (i == tradesArray.Length - 1) - trade.Flags |= ExchangeTradeFlags.IsLastFromSnapshot; + var subscribeRequest = new + { + type = "subscribe", + channel = "v3_trades", + id = marketSymbol, + }; + await _socket.SendMessageAsync(subscribeRequest); } - await callback(new KeyValuePair(marketSymbol, trade)); } - } - }, async (_socket) => - { - foreach (var marketSymbol in marketSymbols) - { - var subscribeRequest = new - { - type = "subscribe", - channel = "v3_trades", - id = marketSymbol, - }; - await _socket.SendMessageAsync(subscribeRequest); - } - }); + ); } } - public partial class ExchangeName { public const string Dydx = "Dydx"; } + + public partial class ExchangeName + { + public const string Dydx = "Dydx"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/FTX/ExchangeFTXAPI.cs b/src/ExchangeSharp/API/Exchanges/FTX/ExchangeFTXAPI.cs index 9ab8a7c4d..ceb243345 100644 --- a/src/ExchangeSharp/API/Exchanges/FTX/ExchangeFTXAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/FTX/ExchangeFTXAPI.cs @@ -10,6 +10,8 @@ public sealed class ExchangeFTXAPI : FTXGroupCommon public override string BaseUrlWebSocket { get; set; } = "wss://ftx.com/ws/"; } - public partial class ExchangeName { public const string FTX = "FTX"; } - + public partial class ExchangeName + { + public const string FTX = "FTX"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/FTX/ExchangeFTXUSAPI.cs b/src/ExchangeSharp/API/Exchanges/FTX/ExchangeFTXUSAPI.cs index 4f977b447..e099db697 100644 --- a/src/ExchangeSharp/API/Exchanges/FTX/ExchangeFTXUSAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/FTX/ExchangeFTXUSAPI.cs @@ -10,5 +10,8 @@ public sealed class ExchangeFTXUSAPI : FTXGroupCommon public override string BaseUrlWebSocket { get; set; } = "wss://ftx.us/ws/"; } - public partial class ExchangeName { public const string FTXUS = "FTXUS"; } + public partial class ExchangeName + { + public const string FTXUS = "FTXUS"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/FTX/FTXExtensions.cs b/src/ExchangeSharp/API/Exchanges/FTX/FTXExtensions.cs index 1bbaab4df..51c7a6a60 100644 --- a/src/ExchangeSharp/API/Exchanges/FTX/FTXExtensions.cs +++ b/src/ExchangeSharp/API/Exchanges/FTX/FTXExtensions.cs @@ -10,7 +10,10 @@ internal static class FTXExtensions /// /// FTX order status string. /// - internal static ExchangeAPIOrderResult ToExchangeAPIOrderResult(this string status, decimal remainingAmount) + internal static ExchangeAPIOrderResult ToExchangeAPIOrderResult( + this string status, + decimal remainingAmount + ) { return (status, remainingAmount) switch { diff --git a/src/ExchangeSharp/API/Exchanges/FTX/FTXGroupCommon.cs b/src/ExchangeSharp/API/Exchanges/FTX/FTXGroupCommon.cs index 320065ae7..9f7261e3f 100644 --- a/src/ExchangeSharp/API/Exchanges/FTX/FTXGroupCommon.cs +++ b/src/ExchangeSharp/API/Exchanges/FTX/FTXGroupCommon.cs @@ -1,9 +1,9 @@ -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; +using Newtonsoft.Json.Linq; namespace ExchangeSharp { @@ -23,7 +23,11 @@ public FTXGroupCommon() #region [ Implementation ] /// - protected async override Task OnCancelOrderAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + protected async override Task OnCancelOrderAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { var url = "/orders/"; if (isClientOrderId) @@ -31,7 +35,12 @@ protected async override Task OnCancelOrderAsync(string orderId, string marketSy url += "by_client_id/"; } - await MakeJsonRequestAsync($"{url}{orderId}", null, await GetNoncePayloadAsync(), "DELETE"); + await MakeJsonRequestAsync( + $"{url}{orderId}", + null, + await GetNoncePayloadAsync(), + "DELETE" + ); } /// @@ -39,7 +48,11 @@ protected async override Task> OnGetAmountsAsync() { var balances = new Dictionary(); - JToken result = await MakeJsonRequestAsync("/wallet/balances", null, await GetNoncePayloadAsync()); + JToken result = await MakeJsonRequestAsync( + "/wallet/balances", + null, + await GetNoncePayloadAsync() + ); foreach (JObject obj in result) { @@ -52,7 +65,9 @@ protected async override Task> OnGetAmountsAsync() } /// - protected async override Task> OnGetAmountsAvailableToTradeAsync() + protected async override Task< + Dictionary + > OnGetAmountsAvailableToTradeAsync() { // https://docs.ftx.com/#get-balances // NOTE there is also is "Get balances of all accounts"? @@ -65,22 +80,32 @@ protected async override Task> OnGetAmountsAvailable var balances = new Dictionary(); - JToken result = await MakeJsonRequestAsync($"/wallet/balances", null, await GetNoncePayloadAsync()); + JToken result = await MakeJsonRequestAsync( + $"/wallet/balances", + null, + await GetNoncePayloadAsync() + ); foreach (JToken token in result.Children()) { - balances.Add(token["coin"].ToStringInvariant(), - token["availableWithoutBorrow"].ConvertInvariant()); + balances.Add( + token["coin"].ToStringInvariant(), + token["availableWithoutBorrow"].ConvertInvariant() + ); } return balances; } - /// - protected async override Task> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + protected async override Task> OnGetCandlesAsync( + string marketSymbol, + int periodSeconds, + DateTime? startDate = null, + DateTime? endDate = null, + int? limit = null + ) { - //period options: 15, 60, 300, 900, 3600, 14400, 86400, or any multiple of 86400 up to 30*86400 var queryUrl = $"/markets/{marketSymbol}/candles?resolution={periodSeconds}"; @@ -97,11 +122,26 @@ protected async override Task> OnGetCandlesAsync(strin var candles = new List(); - var response = await MakeJsonRequestAsync(queryUrl, null, await GetNoncePayloadAsync()); + var response = await MakeJsonRequestAsync( + queryUrl, + null, + await GetNoncePayloadAsync() + ); foreach (JToken candle in response.Children()) { - var parsedCandle = this.ParseCandle(candle, marketSymbol, periodSeconds, "open", "high", "low", "close", "startTime", TimestampType.Iso8601UTC, "volume"); + var parsedCandle = this.ParseCandle( + candle, + marketSymbol, + periodSeconds, + "open", + "high", + "low", + "close", + "startTime", + TimestampType.Iso8601UTC, + "volume" + ); candles.Add(parsedCandle); } @@ -110,7 +150,9 @@ protected async override Task> OnGetCandlesAsync(strin } /// - protected async override Task> OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) + protected async override Task< + IEnumerable + > OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) { string query = "/orders/history"; @@ -131,7 +173,11 @@ protected async override Task> OnGetCompletedOr query += $"?{parameters}"; } - JToken response = await MakeJsonRequestAsync(query, null, await GetNoncePayloadAsync()); + JToken response = await MakeJsonRequestAsync( + query, + null, + await GetNoncePayloadAsync() + ); var orders = new List(); @@ -151,7 +197,13 @@ protected async override Task> OnGetCompletedOr } /// - protected async override Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + protected async override Task OnGetHistoricalTradesAsync( + Func, bool> callback, + string marketSymbol, + DateTime? startDate = null, + DateTime? endDate = null, + int? limit = null + ) { string baseUrl = $"/markets/{marketSymbol}/trades?"; @@ -173,7 +225,17 @@ protected async override Task OnGetHistoricalTradesAsync(Func - protected async override Task> OnGetMarketSymbolsAsync(bool isWebSocket = false) + protected async override Task> OnGetMarketSymbolsAsync( + bool isWebSocket = false + ) { JToken result = await MakeJsonRequestAsync("/markets"); //FTX contains futures which we are not interested in so we filter them out. - var names = result.Children().Select(x => x["name"].ToStringInvariant()).Where(x => Regex.Match(x, @"[\w\d]*\/[[\w\d]]*").Success).ToList(); + var names = result + .Children() + .Select(x => x["name"].ToStringInvariant()) + .Where(x => Regex.Match(x, @"[\w\d]*\/[[\w\d]]*").Success) + .ToList(); names.Sort(); @@ -199,7 +267,9 @@ protected async override Task> OnGetMarketSymbolsAsync(bool } /// - protected async internal override Task> OnGetMarketSymbolsMetadataAsync() + protected async internal override Task< + IEnumerable + > OnGetMarketSymbolsMetadataAsync() { //{ // "name": "BTC-0628", @@ -256,14 +326,20 @@ protected async internal override Task> OnGetMarketS } /// - protected async override Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) + protected async override Task> OnGetOpenOrderDetailsAsync( + string marketSymbol = null + ) { // https://docs.ftx.com/#get-open-orders var markets = new List(); - JToken result = await MakeJsonRequestAsync($"/orders?market={marketSymbol}", null, await GetNoncePayloadAsync()); + JToken result = await MakeJsonRequestAsync( + $"/orders?market={marketSymbol}", + null, + await GetNoncePayloadAsync() + ); foreach (JToken token in result.Children()) { @@ -281,17 +357,29 @@ protected async override Task> OnGetOpenOrderDe } /// - protected async override Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) + protected async override Task OnGetOrderBookAsync( + string marketSymbol, + int maxCount = 100 + ) { - JToken response = await MakeJsonRequestAsync($"/markets/{marketSymbol}/orderbook?depth={maxCount}"); + JToken response = await MakeJsonRequestAsync( + $"/markets/{marketSymbol}/orderbook?depth={maxCount}" + ); return response.ParseOrderBookFromJTokenArrays(); } /// - protected async override Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + protected async override Task OnGetOrderDetailsAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { // https://docs.ftx.com/#get-order-status and https://docs.ftx.com/#get-order-status-by-client-id - if (!string.IsNullOrEmpty(marketSymbol)) throw new NotSupportedException("Searching by marketSymbol is either not implemented by or supported by this exchange. Please submit a PR if you are interested in this feature"); + if (!string.IsNullOrEmpty(marketSymbol)) + throw new NotSupportedException( + "Searching by marketSymbol is either not implemented by or supported by this exchange. Please submit a PR if you are interested in this feature" + ); var url = "/orders/"; if (isClientOrderId) @@ -299,13 +387,19 @@ protected async override Task OnGetOrderDetailsAsync(string url += "by_client_id/"; } - JToken result = await MakeJsonRequestAsync($"{url}{orderId}", null, await GetNoncePayloadAsync()); + JToken result = await MakeJsonRequestAsync( + $"{url}{orderId}", + null, + await GetNoncePayloadAsync() + ); return ParseOrder(result); } /// - protected async override Task>> OnGetTickersAsync() + protected async override Task< + IEnumerable> + > OnGetTickersAsync() { JToken result = await MakeJsonRequestAsync("/markets"); @@ -320,7 +414,17 @@ protected async override Task>> continue; } - var ticker = await this.ParseTickerAsync(token, symbol, "ask", "bid", "last", null, null, "time", TimestampType.UnixSecondsDouble); + var ticker = await this.ParseTickerAsync( + token, + symbol, + "ask", + "bid", + "last", + null, + null, + "time", + TimestampType.UnixSecondsDouble + ); tickers.Add(symbol, ticker); } @@ -332,22 +436,39 @@ protected override async Task OnGetTickerAsync(string marketSymb { var result = await MakeJsonRequestAsync($"/markets/{marketSymbol}"); - return await this.ParseTickerAsync(result, marketSymbol, "ask", "bid", "last", null, null, "time", TimestampType.UnixSecondsDouble); + return await this.ParseTickerAsync( + result, + marketSymbol, + "ask", + "bid", + "last", + null, + null, + "time", + TimestampType.UnixSecondsDouble + ); } - protected override async Task OnWithdrawAsync(ExchangeWithdrawalRequest request) + protected override async Task OnWithdrawAsync( + ExchangeWithdrawalRequest request + ) { var parameters = new Dictionary - { - { "coin", request.Currency }, - { "size", request.Amount }, - { "address", request.Address }, - { "nonce", await GenerateNonceAsync() }, - { "password", request.Password }, - { "code", request.Code } - }; - - var result = await MakeJsonRequestAsync("/wallet/withdrawals", null, parameters, "POST"); + { + { "coin", request.Currency }, + { "size", request.Amount }, + { "address", request.Address }, + { "nonce", await GenerateNonceAsync() }, + { "password", request.Password }, + { "code", request.Code } + }; + + var result = await MakeJsonRequestAsync( + "/wallet/withdrawals", + null, + parameters, + "POST" + ); return new ExchangeWithdrawalResponse { @@ -357,85 +478,135 @@ protected override async Task OnWithdrawAsync(Exchan } /// - protected override async Task OnGetTickersWebSocketAsync(Action>> tickers, params string[] marketSymbols) + protected override async Task OnGetTickersWebSocketAsync( + Action>> tickers, + params string[] marketSymbols + ) { if (marketSymbols == null || marketSymbols.Length == 0) { marketSymbols = (await GetMarketSymbolsAsync(true)).ToArray(); } - return await ConnectPublicWebSocketAsync(null, messageCallback: async (_socket, msg) => - { - JToken parsedMsg = JToken.Parse(msg.ToStringFromUTF8()); - - if (parsedMsg["channel"].ToStringInvariant().Equals("ticker") && !parsedMsg["type"].ToStringInvariant().Equals("subscribed")) - { - JToken data = parsedMsg["data"]; - - var exchangeTicker = await this.ParseTickerAsync(data, parsedMsg["market"].ToStringInvariant(), "ask", "bid", "last", null, null, "time", TimestampType.UnixSecondsDouble); - - var kv = new KeyValuePair(exchangeTicker.MarketSymbol, exchangeTicker); - - tickers(new List> { kv }); - } - }, connectCallback: async (_socket) => - { - //{'op': 'subscribe', 'channel': 'trades', 'market': 'BTC-PERP'} - - for (int i = 0; i < marketSymbols.Length; i++) - { - await _socket.SendMessageAsync(new + return await ConnectPublicWebSocketAsync( + null, + messageCallback: async (_socket, msg) => { - op = "subscribe", - market = marketSymbols[i], - channel = "ticker" - }); - } - }); + JToken parsedMsg = JToken.Parse(msg.ToStringFromUTF8()); + + if ( + parsedMsg["channel"].ToStringInvariant().Equals("ticker") + && !parsedMsg["type"].ToStringInvariant().Equals("subscribed") + ) + { + JToken data = parsedMsg["data"]; + + var exchangeTicker = await this.ParseTickerAsync( + data, + parsedMsg["market"].ToStringInvariant(), + "ask", + "bid", + "last", + null, + null, + "time", + TimestampType.UnixSecondsDouble + ); + + var kv = new KeyValuePair( + exchangeTicker.MarketSymbol, + exchangeTicker + ); + + tickers(new List> { kv }); + } + }, + connectCallback: async (_socket) => + { + //{'op': 'subscribe', 'channel': 'trades', 'market': 'BTC-PERP'} + + for (int i = 0; i < marketSymbols.Length; i++) + { + await _socket.SendMessageAsync( + new + { + op = "subscribe", + market = marketSymbols[i], + channel = "ticker" + } + ); + } + } + ); } /// - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + protected override async Task OnGetTradesWebSocketAsync( + Func, Task> callback, + params string[] marketSymbols + ) { if (marketSymbols == null || marketSymbols.Length == 0) { marketSymbols = (await GetMarketSymbolsAsync(true)).ToArray(); } - return await ConnectPublicWebSocketAsync(null, messageCallback: async (_socket, msg) => - { - JToken parsedMsg = JToken.Parse(msg.ToStringFromUTF8()); - - if (parsedMsg["type"].ToStringInvariant() == "error") - { - throw new APIException(parsedMsg["msg"].ToStringInvariant()); - } - else if (parsedMsg["channel"].ToStringInvariant().Equals("trades") - && !parsedMsg["type"].ToStringInvariant().Equals("subscribed")) - { - foreach (var data in parsedMsg["data"]) + return await ConnectPublicWebSocketAsync( + null, + messageCallback: async (_socket, msg) => { - var exchangeTrade = data.ParseTradeFTX("size", "price", "side", "time", TimestampType.Iso8601Local, "id"); - - await callback(new KeyValuePair(parsedMsg["market"].ToStringInvariant(), exchangeTrade)); - } - } - }, connectCallback: async (_socket) => - { - //{'op': 'subscribe', 'channel': 'trades', 'market': 'BTC-PERP'} - - for (int i = 0; i < marketSymbols.Length; i++) - { - await _socket.SendMessageAsync(new + JToken parsedMsg = JToken.Parse(msg.ToStringFromUTF8()); + + if (parsedMsg["type"].ToStringInvariant() == "error") + { + throw new APIException(parsedMsg["msg"].ToStringInvariant()); + } + else if ( + parsedMsg["channel"].ToStringInvariant().Equals("trades") + && !parsedMsg["type"].ToStringInvariant().Equals("subscribed") + ) + { + foreach (var data in parsedMsg["data"]) + { + var exchangeTrade = data.ParseTradeFTX( + "size", + "price", + "side", + "time", + TimestampType.Iso8601Local, + "id" + ); + + await callback( + new KeyValuePair( + parsedMsg["market"].ToStringInvariant(), + exchangeTrade + ) + ); + } + } + }, + connectCallback: async (_socket) => { - op = "subscribe", - market = marketSymbols[i], - channel = "trades", - }); - } - }); + //{'op': 'subscribe', 'channel': 'trades', 'market': 'BTC-PERP'} + + for (int i = 0; i < marketSymbols.Length; i++) + { + await _socket.SendMessageAsync( + new + { + op = "subscribe", + market = marketSymbols[i], + channel = "trades", + } + ); + } + } + ); } /// - protected async override Task OnPlaceOrderAsync(ExchangeOrderRequest order) + protected async override Task OnPlaceOrderAsync( + ExchangeOrderRequest order + ) { //{ // "market": "XRP-PERP", @@ -450,17 +621,19 @@ protected async override Task OnPlaceOrderAsync(ExchangeOrd //} IEnumerable markets = await OnGetMarketSymbolsMetadataAsync(); - ExchangeMarket market = markets.Where(m => m.MarketSymbol == order.MarketSymbol).First(); + ExchangeMarket market = markets + .Where(m => m.MarketSymbol == order.MarketSymbol) + .First(); var payload = await GetNoncePayloadAsync(); var parameters = new Dictionary(StringComparer.OrdinalIgnoreCase) - { - {"market", market.MarketSymbol}, - {"side", order.IsBuy ? "buy" : "sell" }, - {"type", order.OrderType.ToStringLowerInvariant() }, - {"size", order.RoundAmount() } - }; + { + { "market", market.MarketSymbol }, + { "side", order.IsBuy ? "buy" : "sell" }, + { "type", order.OrderType.ToStringLowerInvariant() }, + { "size", order.RoundAmount() } + }; if (!string.IsNullOrEmpty(order.ClientOrderId)) { @@ -474,9 +647,12 @@ protected async override Task OnPlaceOrderAsync(ExchangeOrd if (order.OrderType != OrderType.Market) { - int precision = BitConverter.GetBytes(decimal.GetBits((decimal)market.PriceStepSize)[3])[2]; + int precision = BitConverter.GetBytes( + decimal.GetBits((decimal)market.PriceStepSize)[3] + )[2]; - if (order.Price == null) throw new ArgumentNullException(nameof(order.Price)); + if (order.Price == null) + throw new ArgumentNullException(nameof(order.Price)); parameters.Add("price", Math.Round(order.Price.Value, precision)); } @@ -499,7 +675,7 @@ protected async override Task OnPlaceOrderAsync(ExchangeOrd Price = CryptoUtility.ConvertInvariant(response["price"]), AmountFilled = CryptoUtility.ConvertInvariant(response["filledSize"]), AveragePrice = CryptoUtility.ConvertInvariant(response["avgFillPrice"]), - Amount = CryptoUtility.ConvertInvariant(response["size"]), + Amount = CryptoUtility.ConvertInvariant(response["size"]), MarketSymbol = response["market"].ToStringInvariant(), IsBuy = response["side"].ToStringInvariant() == "buy" }; @@ -508,7 +684,10 @@ protected async override Task OnPlaceOrderAsync(ExchangeOrd } /// - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + protected override async Task ProcessRequestAsync( + IHttpWebRequest request, + Dictionary payload + ) { if (CanMakeAuthenticatedRequest(payload)) { @@ -519,7 +698,8 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti string form = CryptoUtility.GetJsonForPayload(payload); //Create the signature payload - string toHash = $"{timestamp}{request.Method.ToUpperInvariant()}{request.RequestUri.PathAndQuery}"; + string toHash = + $"{timestamp}{request.Method.ToUpperInvariant()}{request.RequestUri.PathAndQuery}"; if (request.Method == "POST") { @@ -560,12 +740,16 @@ private ExchangeOrderResult ParseOrder(JToken token) Amount = token["size"].ConvertInvariant(), AmountFilled = token["filledSize"].ConvertInvariant(), ClientOrderId = token["clientId"].ToStringInvariant(), - Result = token["status"].ToStringInvariant().ToExchangeAPIOrderResult(token["size"].ConvertInvariant() - token["filledSize"].ConvertInvariant()), + Result = token["status"] + .ToStringInvariant() + .ToExchangeAPIOrderResult( + token["size"].ConvertInvariant() + - token["filledSize"].ConvertInvariant() + ), ResultCode = token["status"].ToStringInvariant() }; } #endregion - } } diff --git a/src/ExchangeSharp/API/Exchanges/GateIo/ExchangeGateIoAPI.cs b/src/ExchangeSharp/API/Exchanges/GateIo/ExchangeGateIoAPI.cs index 5b206bdd6..85de88ab7 100644 --- a/src/ExchangeSharp/API/Exchanges/GateIo/ExchangeGateIoAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/GateIo/ExchangeGateIoAPI.cs @@ -10,17 +10,20 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; +using Newtonsoft.Json.Linq; namespace ExchangeSharp { - public partial class ExchangeName { public const string GateIo = "GateIo"; } + public partial class ExchangeName + { + public const string GateIo = "GateIo"; + } public sealed class ExchangeGateIoAPI : ExchangeAPI { @@ -34,43 +37,61 @@ public ExchangeGateIoAPI() RequestContentType = "application/json"; } - protected override async Task>> OnGetTickersAsync() + protected override async Task< + IEnumerable> + > OnGetTickersAsync() { var json = await MakeJsonRequestAsync("/spot/tickers"); var tickers = json.Select(tickerToken => ParseTicker(tickerToken)) - .Select(ticker => new KeyValuePair(ticker.MarketSymbol, ticker)) - .ToList(); + .Select( + ticker => new KeyValuePair(ticker.MarketSymbol, ticker) + ) + .ToList(); return tickers; } - protected override async Task> OnGetRecentTradesAsync(string symbol, int? limit = null) + protected override async Task> OnGetRecentTradesAsync( + string symbol, + int? limit = null + ) { var trades = new List(); int maxRequestLimit = (limit == null || limit < 1 || limit > 100) ? 100 : (int)limit; - var json = await MakeJsonRequestAsync($"/spot/trades?currency_pair={symbol}&limit={maxRequestLimit}"); + var json = await MakeJsonRequestAsync( + $"/spot/trades?currency_pair={symbol}&limit={maxRequestLimit}" + ); foreach (JToken tradeToken in json) { /* - { - "id": "1232893232", - "create_time": "1548000000", - "create_time_ms": "1548000000123.456", - "order_id": "4128442423", - "side": "buy", - "role": "maker", - "amount": "0.15", - "price": "0.03", - "fee": "0.0005", - "fee_currency": "ETH", - "point_fee": "0", - "gt_fee": "0" - } + { + "id": "1232893232", + "create_time": "1548000000", + "create_time_ms": "1548000000123.456", + "order_id": "4128442423", + "side": "buy", + "role": "maker", + "amount": "0.15", + "price": "0.03", + "fee": "0.0005", + "fee_currency": "ETH", + "point_fee": "0", + "gt_fee": "0" + } */ - trades.Add(tradeToken.ParseTrade("amount", "price", "side", "create_time_ms", TimestampType.UnixMillisecondsDouble, "id")); + trades.Add( + tradeToken.ParseTrade( + "amount", + "price", + "side", + "create_time_ms", + TimestampType.UnixMillisecondsDouble, + "id" + ) + ); } return trades; } @@ -90,22 +111,24 @@ protected override async Task> OnGetMarketSymbolsAsync() return symbols; } - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() + protected internal override async Task< + IEnumerable + > OnGetMarketSymbolsMetadataAsync() { /* - { - "id": "ETH_USDT", - "base": "ETH", - "quote": "USDT", - "fee": "0.2", - "min_base_amount": "0.001", - "min_quote_amount": "1.0", - "amount_precision": 3, - "precision": 6, - "trade_status": "tradable", - "sell_start": 1516378650, - "buy_start": 1516378650 - } + { + "id": "ETH_USDT", + "base": "ETH", + "quote": "USDT", + "fee": "0.2", + "min_base_amount": "0.001", + "min_quote_amount": "1.0", + "amount_precision": 3, + "precision": 6, + "trade_status": "tradable", + "sell_start": 1516378650, + "buy_start": 1516378650 + } */ var markets = new List(); @@ -118,17 +141,25 @@ protected internal override async Task> OnGetMarketS var market = new ExchangeMarket { MarketSymbol = marketSymbolToken["id"].ToStringUpperInvariant(), - IsActive = marketSymbolToken["trade_status"].ToStringLowerInvariant() == "tradable", + IsActive = + marketSymbolToken["trade_status"].ToStringLowerInvariant() + == "tradable", QuoteCurrency = marketSymbolToken["quote"].ToStringUpperInvariant(), BaseCurrency = marketSymbolToken["base"].ToStringUpperInvariant(), }; int pricePrecision = marketSymbolToken["precision"].ConvertInvariant(); market.PriceStepSize = (decimal)Math.Pow(0.1, pricePrecision); - int quantityPrecision = marketSymbolToken["amount_precision"].ConvertInvariant(); + int quantityPrecision = marketSymbolToken[ + "amount_precision" + ].ConvertInvariant(); market.QuantityStepSize = (decimal)Math.Pow(0.1, quantityPrecision); - market.MinTradeSizeInQuoteCurrency = marketSymbolToken["min_quote_amount"].ConvertInvariant(); - market.MinTradeSize = marketSymbolToken["min_base_amount"].ConvertInvariant(); + market.MinTradeSizeInQuoteCurrency = marketSymbolToken[ + "min_quote_amount" + ].ConvertInvariant(); + market.MinTradeSize = marketSymbolToken[ + "min_base_amount" + ].ConvertInvariant(); markets.Add(market); } @@ -145,81 +176,95 @@ protected override async Task OnGetTickerAsync(string symbol) private ExchangeTicker ParseTicker(JToken tickerToken) { - bool IsEmptyString(JToken token) => token.Type == JTokenType.String && token.ToObject() == string.Empty; + bool IsEmptyString(JToken token) => + token.Type == JTokenType.String && token.ToObject() == string.Empty; /* - { - "currency_pair": "BTC3L_USDT", - "last": "2.46140352", - "lowest_ask": "2.477", - "highest_bid": "2.4606821", - "change_percentage": "-8.91", - "base_volume": "656614.0845820589", - "quote_volume": "1602221.66468375534639404191", - "high_24h": "2.7431", - "low_24h": "1.9863", - "etf_net_value": "2.46316141", - "etf_pre_net_value": "2.43201848", - "etf_pre_timestamp": 1611244800, - "etf_leverage": "2.2803019447281203" - } + { + "currency_pair": "BTC3L_USDT", + "last": "2.46140352", + "lowest_ask": "2.477", + "highest_bid": "2.4606821", + "change_percentage": "-8.91", + "base_volume": "656614.0845820589", + "quote_volume": "1602221.66468375534639404191", + "high_24h": "2.7431", + "low_24h": "1.9863", + "etf_net_value": "2.46316141", + "etf_pre_net_value": "2.43201848", + "etf_pre_timestamp": 1611244800, + "etf_leverage": "2.2803019447281203" + } */ return new ExchangeTicker { Exchange = Name, MarketSymbol = tickerToken["currency_pair"].ToStringInvariant(), - Bid = IsEmptyString(tickerToken["lowest_ask"]) ? default : tickerToken["lowest_ask"].ConvertInvariant(), - Ask = IsEmptyString(tickerToken["highest_bid"]) ? default : tickerToken["highest_bid"].ConvertInvariant(), + Bid = IsEmptyString(tickerToken["lowest_ask"]) + ? default + : tickerToken["lowest_ask"].ConvertInvariant(), + Ask = IsEmptyString(tickerToken["highest_bid"]) + ? default + : tickerToken["highest_bid"].ConvertInvariant(), Last = tickerToken["last"].ConvertInvariant(), }; } - protected override async Task OnGetOrderBookAsync(string symbol, int maxCount = 100) + protected override async Task OnGetOrderBookAsync( + string symbol, + int maxCount = 100 + ) { - var json = await MakeJsonRequestAsync($"/spot/order_book?currency_pair={symbol}"); + var json = await MakeJsonRequestAsync( + $"/spot/order_book?currency_pair={symbol}" + ); /* - { - "id": 123456, - "current": 1623898993123, - "update": 1623898993121, - "asks": [ - [ - "1.52", - "1.151" - ], - [ - "1.53", - "1.218" - ] - ], - "bids": [ - [ - "1.17", - "201.863" + { + "id": 123456, + "current": 1623898993123, + "update": 1623898993121, + "asks": [ + [ + "1.52", + "1.151" + ], + [ + "1.53", + "1.218" + ] ], - [ - "1.16", - "725.464" + "bids": [ + [ + "1.17", + "201.863" + ], + [ + "1.16", + "725.464" + ] ] - ] - } + } */ var orderBook = json.ParseOrderBookFromJTokenArrays(sequence: "current"); - orderBook.LastUpdatedUtc = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(json["current"].ConvertInvariant()); + orderBook.LastUpdatedUtc = CryptoUtility.UnixTimeStampToDateTimeMilliseconds( + json["current"].ConvertInvariant() + ); return orderBook; } protected override async Task> OnGetCandlesAsync( - string symbol, - int periodSeconds, - DateTime? startDate = null, - DateTime? endDate = null, - int? limit = null) + string symbol, + int periodSeconds, + DateTime? startDate = null, + DateTime? endDate = null, + int? limit = null + ) { - string url = $"/spot/candlesticks?currency_pair={symbol}&interval={PeriodSecondsToString(periodSeconds)}"; + string url = + $"/spot/candlesticks?currency_pair={symbol}&interval={PeriodSecondsToString(periodSeconds)}"; if (limit != null) { @@ -244,24 +289,33 @@ protected override async Task> OnGetCandlesAsync( endDate = startDate.Value.AddSeconds(periodSeconds * (limit ?? 999)); } } - url += $"&from={((long)startDate.Value.UnixTimestampFromDateTimeSeconds()).ToStringInvariant()}"; - url += $"&to={((long)endDate.Value.UnixTimestampFromDateTimeSeconds()).ToStringInvariant()}"; + url += + $"&from={((long)startDate.Value.UnixTimestampFromDateTimeSeconds()).ToStringInvariant()}"; + url += + $"&to={((long)endDate.Value.UnixTimestampFromDateTimeSeconds()).ToStringInvariant()}"; } var json = await MakeJsonRequestAsync(url); - var candles = json.Select(candleToken => new MarketCandle - { - Timestamp = CryptoUtility.ParseTimestamp(candleToken[0], TimestampType.UnixSeconds), - BaseCurrencyVolume = candleToken[1].ConvertInvariant(), - ClosePrice = candleToken[2].ConvertInvariant(), - ExchangeName = Name, - HighPrice = candleToken[3].ConvertInvariant(), - LowPrice = candleToken[4].ConvertInvariant(), - Name = symbol, - OpenPrice = candleToken[5].ConvertInvariant(), - PeriodSeconds = periodSeconds, - }).ToList(); + var candles = json.Select( + candleToken => + new MarketCandle + { + Timestamp = CryptoUtility.ParseTimestamp( + candleToken[0], + TimestampType.UnixSeconds + ), + BaseCurrencyVolume = candleToken[1].ConvertInvariant(), + ClosePrice = candleToken[2].ConvertInvariant(), + ExchangeName = Name, + HighPrice = candleToken[3].ConvertInvariant(), + LowPrice = candleToken[4].ConvertInvariant(), + Name = symbol, + OpenPrice = candleToken[5].ConvertInvariant(), + PeriodSeconds = periodSeconds, + } + ) + .ToList(); return candles; } @@ -269,20 +323,32 @@ protected override async Task> OnGetCandlesAsync( protected override async Task> OnGetAmountsAsync() { var payload = await GetNoncePayloadAsync(); - var responseToken = await MakeJsonRequestAsync("/spot/accounts", payload: payload); - return responseToken.Select(x => ParseBalance(x)) - .ToDictionary(x => x.currency, x => x.available + x.locked); + var responseToken = await MakeJsonRequestAsync( + "/spot/accounts", + payload: payload + ); + return responseToken + .Select(x => ParseBalance(x)) + .ToDictionary(x => x.currency, x => x.available + x.locked); } - protected override async Task> OnGetAmountsAvailableToTradeAsync() + protected override async Task< + Dictionary + > OnGetAmountsAvailableToTradeAsync() { var payload = await GetNoncePayloadAsync(); - var responseToken = await MakeJsonRequestAsync("/spot/accounts", payload: payload); - return responseToken.Select(x => ParseBalance(x)) - .ToDictionary(x => x.currency, x => x.available); + var responseToken = await MakeJsonRequestAsync( + "/spot/accounts", + payload: payload + ); + return responseToken + .Select(x => ParseBalance(x)) + .ToDictionary(x => x.currency, x => x.available); } - private (string currency, decimal available, decimal locked) ParseBalance(JToken balanceToken) + private (string currency, decimal available, decimal locked) ParseBalance( + JToken balanceToken + ) { var currency = balanceToken["currency"].ToStringInvariant(); var available = balanceToken["available"].ConvertInvariant(); @@ -291,7 +357,9 @@ protected override async Task> OnGetAmountsAvailable return (currency, available, locked); } - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + protected override async Task OnPlaceOrderAsync( + ExchangeOrderRequest order + ) { if (order.OrderType != OrderType.Limit) throw new NotSupportedException("Gate.io API supports only limit orders"); @@ -299,33 +367,51 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd var payload = await GetNoncePayloadAsync(); AddOrderToPayload(order, payload); - JToken responseToken = await MakeJsonRequestAsync("/spot/orders", payload: payload, requestMethod: "POST"); + JToken responseToken = await MakeJsonRequestAsync( + "/spot/orders", + payload: payload, + requestMethod: "POST" + ); return ParseOrder(responseToken); } - protected override async Task OnPlaceOrdersAsync(params ExchangeOrderRequest[] orders) + protected override async Task OnPlaceOrdersAsync( + params ExchangeOrderRequest[] orders + ) { - var orderRequests = orders.Select((order, i) => - { - var subPayload = new Dictionary(); - - if (string.IsNullOrEmpty(order.ClientOrderId)) - { - order.ClientOrderId = $"{CryptoUtility.UnixTimestampFromDateTimeMilliseconds(DateTime.Now)}-{i.ToStringInvariant()}" ; - } - AddOrderToPayload(order, subPayload); - return subPayload; - }).ToList(); + var orderRequests = orders + .Select( + (order, i) => + { + var subPayload = new Dictionary(); + + if (string.IsNullOrEmpty(order.ClientOrderId)) + { + order.ClientOrderId = + $"{CryptoUtility.UnixTimestampFromDateTimeMilliseconds(DateTime.Now)}-{i.ToStringInvariant()}"; + } + AddOrderToPayload(order, subPayload); + return subPayload; + } + ) + .ToList(); var payload = await GetNoncePayloadAsync(); payload[CryptoUtility.PayloadKeyArray] = orderRequests; - var responseToken = await MakeJsonRequestAsync("/spot/batch_orders", payload: payload, requestMethod: "POST"); + var responseToken = await MakeJsonRequestAsync( + "/spot/batch_orders", + payload: payload, + requestMethod: "POST" + ); return responseToken.Select(x => ParseOrder(x)).ToArray(); } - private void AddOrderToPayload(ExchangeOrderRequest order, Dictionary payload) + private void AddOrderToPayload( + ExchangeOrderRequest order, + Dictionary payload + ) { if (!string.IsNullOrEmpty(order.ClientOrderId)) { @@ -337,14 +423,18 @@ private void AddOrderToPayload(ExchangeOrderRequest order, Dictionary(); decimal amountFilled = amount - order["left"].ConvertInvariant(); - decimal? fillPrice = amountFilled == 0 ? null : (decimal?)(order["filled_total"].ConvertInvariant() / amountFilled); + decimal? fillPrice = + amountFilled == 0 + ? null + : (decimal?)(order["filled_total"].ConvertInvariant() / amountFilled); decimal price = order["price"].ConvertInvariant(); var result = new ExchangeOrderResult { @@ -354,17 +444,25 @@ private ExchangeOrderResult ParseOrder(JToken order) AveragePrice = fillPrice, Message = string.Empty, OrderId = order["id"].ToStringInvariant(), - OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(order["create_time_ms"].ConvertInvariant()), + OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds( + order["create_time_ms"].ConvertInvariant() + ), MarketSymbol = order["currency_pair"].ToStringInvariant(), IsBuy = order["side"].ToStringInvariant() == "buy", ClientOrderId = order["text"].ToStringInvariant(), }; - result.Result = ParseExchangeAPIOrderResult(order["status"].ToStringInvariant(), amountFilled); + result.Result = ParseExchangeAPIOrderResult( + order["status"].ToStringInvariant(), + amountFilled + ); return result; } - private static ExchangeAPIOrderResult ParseExchangeAPIOrderResult(string status, decimal amountFilled) + private static ExchangeAPIOrderResult ParseExchangeAPIOrderResult( + string status, + decimal amountFilled + ) { switch (status) { @@ -373,39 +471,62 @@ private static ExchangeAPIOrderResult ParseExchangeAPIOrderResult(string status, case "closed": return ExchangeAPIOrderResult.Filled; case "cancelled": - return amountFilled > 0 ? ExchangeAPIOrderResult.FilledPartiallyAndCancelled : ExchangeAPIOrderResult.Canceled; + return amountFilled > 0 + ? ExchangeAPIOrderResult.FilledPartiallyAndCancelled + : ExchangeAPIOrderResult.Canceled; default: throw new NotImplementedException($"Unexpected status type: {status}"); } } - protected override async Task OnGetOrderDetailsAsync(string orderId, string symbol = null, bool isClientOrderId = false) + protected override async Task OnGetOrderDetailsAsync( + string orderId, + string symbol = null, + bool isClientOrderId = false + ) { if (string.IsNullOrWhiteSpace(symbol)) { - throw new ArgumentNullException("MarketSymbol is required for querying order details with Gate.io API"); + throw new ArgumentNullException( + "MarketSymbol is required for querying order details with Gate.io API" + ); } - if (isClientOrderId) throw new NotSupportedException("Querying by client order ID is not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature"); + if (isClientOrderId) + throw new NotSupportedException( + "Querying by client order ID is not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature" + ); var payload = await GetNoncePayloadAsync(); - var responseToken = await MakeJsonRequestAsync($"/spot/orders/{orderId}?currency_pair={symbol}", payload: payload); + var responseToken = await MakeJsonRequestAsync( + $"/spot/orders/{orderId}?currency_pair={symbol}", + payload: payload + ); return ParseOrder(responseToken); } - protected override async Task> OnGetOpenOrderDetailsAsync(string symbol = null) + protected override async Task> OnGetOpenOrderDetailsAsync( + string symbol = null + ) { if (string.IsNullOrWhiteSpace(symbol)) { - throw new ArgumentNullException("MarketSymbol is required for querying open orders with Gate.io API"); + throw new ArgumentNullException( + "MarketSymbol is required for querying open orders with Gate.io API" + ); } var payload = await GetNoncePayloadAsync(); - var responseToken = await MakeJsonRequestAsync($"/spot/orders?currency_pair={symbol}&status=open", payload: payload); + var responseToken = await MakeJsonRequestAsync( + $"/spot/orders?currency_pair={symbol}&status=open", + payload: payload + ); return responseToken.Select(x => ParseOrder(x)).ToArray(); } - protected override async Task> OnGetCompletedOrderDetailsAsync(string symbol = null, DateTime? afterDate = null) + protected override async Task< + IEnumerable + > OnGetCompletedOrderDetailsAsync(string symbol = null, DateTime? afterDate = null) { var payload = await GetNoncePayloadAsync(); var url = $"/spot/orders?status=finished"; @@ -415,27 +536,50 @@ protected override async Task> OnGetCompletedOr } if (afterDate.HasValue) { - url += $"&from={(long)CryptoUtility.UnixTimestampFromDateTimeMilliseconds(afterDate.Value)}"; - url += $"&to={(long)CryptoUtility.UnixTimestampFromDateTimeMilliseconds(DateTime.Now)}"; + url += + $"&from={(long)CryptoUtility.UnixTimestampFromDateTimeMilliseconds(afterDate.Value)}"; + url += + $"&to={(long)CryptoUtility.UnixTimestampFromDateTimeMilliseconds(DateTime.Now)}"; } var responseToken = await MakeJsonRequestAsync(url, payload: payload); return responseToken.Select(x => ParseOrder(x)).ToArray(); } - protected override async Task OnCancelOrderAsync(string orderId, string symbol = null, bool isClientOrderId = false) + protected override async Task OnCancelOrderAsync( + string orderId, + string symbol = null, + bool isClientOrderId = false + ) { if (string.IsNullOrWhiteSpace(symbol)) { - throw new ArgumentNullException("MarketSymbol is required for cancelling order with Gate.io API"); + throw new ArgumentNullException( + "MarketSymbol is required for cancelling order with Gate.io API" + ); } - if (isClientOrderId) throw new NotSupportedException("Cancelling by client order ID is not supported in ExchangeSharp. Please submit a PR if you are interested in this feature"); + if (isClientOrderId) + throw new NotSupportedException( + "Cancelling by client order ID is not supported in ExchangeSharp. Please submit a PR if you are interested in this feature" + ); Dictionary payload = await GetNoncePayloadAsync(); - await MakeJsonRequestAsync($"/spot/orders/{orderId}?currency_pair={symbol}", BaseUrl, payload, "DELETE"); + await MakeJsonRequestAsync( + $"/spot/orders/{orderId}?currency_pair={symbol}", + BaseUrl, + payload, + "DELETE" + ); } - string unixTimeInSeconds => ((long)CryptoUtility.UnixTimestampFromDateTimeSeconds(DateTime.Now)).ToStringInvariant(); - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary? payload) + string unixTimeInSeconds => + ( + (long)CryptoUtility.UnixTimestampFromDateTimeSeconds(DateTime.Now) + ).ToStringInvariant(); + + protected override async Task ProcessRequestAsync( + IHttpWebRequest request, + Dictionary? payload + ) { if (CanMakeAuthenticatedRequest(payload)) { @@ -452,13 +596,21 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti using (SHA512 sha512Hash = SHA512.Create()) { var hashBytes = sha512Hash.ComputeHash(sourceBytes); - var bodyHash = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); - var queryString = string.IsNullOrEmpty(request.RequestUri.Query) ? "" : request.RequestUri.Query.Substring(1); - var signatureString = $"{request.Method}\n{request.RequestUri.AbsolutePath}\n{queryString}\n{bodyHash}\n{unixTimeInSeconds}"; + var bodyHash = BitConverter + .ToString(hashBytes) + .Replace("-", "") + .ToLowerInvariant(); + var queryString = string.IsNullOrEmpty(request.RequestUri.Query) + ? "" + : request.RequestUri.Query.Substring(1); + var signatureString = + $"{request.Method}\n{request.RequestUri.AbsolutePath}\n{queryString}\n{bodyHash}\n{unixTimeInSeconds}"; using (HMACSHA512 hmac = new HMACSHA512(Encoding.UTF8.GetBytes(privateApiKey))) { - var signature = CryptoUtility.SHA512Sign(signatureString, privateApiKey).ToLowerInvariant(); + var signature = CryptoUtility + .SHA512Sign(signatureString, privateApiKey) + .ToLowerInvariant(); request.AddHeader("SIGN", signature); } } @@ -471,51 +623,74 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti } } - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + protected override async Task OnGetTradesWebSocketAsync( + Func, Task> callback, + params string[] marketSymbols + ) { if (marketSymbols == null || marketSymbols.Length == 0) { marketSymbols = (await GetMarketSymbolsAsync(true)).ToArray(); } - return await ConnectPublicWebSocketAsync(null, messageCallback: async (_socket, msg) => - { - JToken parsedMsg = JToken.Parse(msg.ToStringFromUTF8()); - - if (parsedMsg["channel"].ToStringInvariant().Equals("spot.trades")) - { - if (parsedMsg["error"] != null) - throw new APIException($"Exchange returned error: {parsedMsg["error"].ToStringInvariant()}"); - else if (parsedMsg["result"]["status"].ToStringInvariant().Equals("success")) + return await ConnectPublicWebSocketAsync( + null, + messageCallback: async (_socket, msg) => { - // successfully subscribed to trade stream - } - else - { - var exchangeTrade = parsedMsg["result"].ParseTrade("amount", "price", "side", "create_time_ms", TimestampType.UnixMillisecondsDouble, "id"); - - await callback(new KeyValuePair(parsedMsg["result"]["currency_pair"].ToStringInvariant(), exchangeTrade)); - } - } - }, connectCallback: async (_socket) => - {/*{ "time": int(time.time()), + JToken parsedMsg = JToken.Parse(msg.ToStringFromUTF8()); + + if (parsedMsg["channel"].ToStringInvariant().Equals("spot.trades")) + { + if (parsedMsg["error"] != null) + throw new APIException( + $"Exchange returned error: {parsedMsg["error"].ToStringInvariant()}" + ); + else if ( + parsedMsg["result"]["status"].ToStringInvariant().Equals("success") + ) + { + // successfully subscribed to trade stream + } + else + { + var exchangeTrade = parsedMsg["result"].ParseTrade( + "amount", + "price", + "side", + "create_time_ms", + TimestampType.UnixMillisecondsDouble, + "id" + ); + + await callback( + new KeyValuePair( + parsedMsg["result"]["currency_pair"].ToStringInvariant(), + exchangeTrade + ) + ); + } + } + }, + connectCallback: async (_socket) => + { /*{ "time": int(time.time()), "channel": "spot.trades", "event": "subscribe", # "unsubscribe" for unsubscription "payload": ["BTC_USDT"] }*/ - - // this doesn't work for some reason - //await _socket.SendMessageAsync(new - //{ - // time = unixTimeInSeconds, - // channel = "spot.trades", - // @event = "subscribe", - // payload = marketSymbols, - //}); - var quotedSymbols = marketSymbols.Select(s => $"\"{s}\""); - var combinedString = string.Join(",", quotedSymbols); - await _socket.SendMessageAsync( - $"{{ \"time\": {unixTimeInSeconds},\"channel\": \"spot.trades\",\"event\": \"subscribe\",\"payload\": [{combinedString}] }}"); - }); + // this doesn't work for some reason + //await _socket.SendMessageAsync(new + //{ + // time = unixTimeInSeconds, + // channel = "spot.trades", + // @event = "subscribe", + // payload = marketSymbols, + //}); + var quotedSymbols = marketSymbols.Select(s => $"\"{s}\""); + var combinedString = string.Join(",", quotedSymbols); + await _socket.SendMessageAsync( + $"{{ \"time\": {unixTimeInSeconds},\"channel\": \"spot.trades\",\"event\": \"subscribe\",\"payload\": [{combinedString}] }}" + ); + } + ); } } } diff --git a/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs b/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs index e5a9088b9..cb220fc01 100644 --- a/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs @@ -27,7 +27,8 @@ namespace ExchangeSharp public sealed partial class ExchangeGeminiAPI : ExchangeAPI { public override string BaseUrl { get; set; } = "https://api.gemini.com/v1"; - public override string BaseUrlWebSocket { get; set; } = "wss://api.gemini.com/v2/marketdata"; + public override string BaseUrlWebSocket { get; set; } = + "wss://api.gemini.com/v2/marketdata"; private ExchangeGeminiAPI() { @@ -43,12 +44,20 @@ private async Task ParseVolumeAsync(JToken token, string symbol) JProperty[] props = token.Children().ToArray(); if (props.Length == 3) { - var (baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync(symbol); + var (baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync( + symbol + ); vol.QuoteCurrency = quoteCurrency.ToUpperInvariant(); - vol.QuoteCurrencyVolume = token[quoteCurrency.ToUpperInvariant()].ConvertInvariant(); + vol.QuoteCurrencyVolume = token[ + quoteCurrency.ToUpperInvariant() + ].ConvertInvariant(); vol.BaseCurrency = baseCurrency.ToUpperInvariant(); - vol.BaseCurrencyVolume = token[baseCurrency.ToUpperInvariant()].ConvertInvariant(); - vol.Timestamp = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(props[2].Value.ConvertInvariant()); + vol.BaseCurrencyVolume = token[ + baseCurrency.ToUpperInvariant() + ].ConvertInvariant(); + vol.Timestamp = CryptoUtility.UnixTimeStampToDateTimeMilliseconds( + props[2].Value.ConvertInvariant() + ); } return vol; @@ -66,21 +75,37 @@ private ExchangeOrderResult ParseOrder(JToken result) AveragePrice = result["avg_execution_price"].ConvertInvariant(), Message = string.Empty, OrderId = result["id"].ToStringInvariant(), - Result = (amountFilled == amount ? ExchangeAPIOrderResult.Filled : (amountFilled == 0 ? ExchangeAPIOrderResult.Open : ExchangeAPIOrderResult.FilledPartially)), - OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(result["timestampms"].ConvertInvariant()), + Result = ( + amountFilled == amount + ? ExchangeAPIOrderResult.Filled + : ( + amountFilled == 0 + ? ExchangeAPIOrderResult.Open + : ExchangeAPIOrderResult.FilledPartially + ) + ), + OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds( + result["timestampms"].ConvertInvariant() + ), MarketSymbol = result["symbol"].ToStringInvariant(), IsBuy = result["side"].ToStringInvariant() == "buy" }; } - protected override Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + protected override Task ProcessRequestAsync( + IHttpWebRequest request, + Dictionary payload + ) { if (CanMakeAuthenticatedRequest(payload)) { payload.Add("request", request.RequestUri.AbsolutePath); string json = JsonConvert.SerializeObject(payload); string json64 = System.Convert.ToBase64String(json.ToBytesUTF8()); - string hexSha384 = CryptoUtility.SHA384Sign(json64, CryptoUtility.ToUnsecureString(PrivateApiKey)); + string hexSha384 = CryptoUtility.SHA384Sign( + json64, + CryptoUtility.ToUnsecureString(PrivateApiKey) + ); request.AddHeader("X-GEMINI-PAYLOAD", json64); request.AddHeader("X-GEMINI-SIGNATURE", hexSha384); request.AddHeader("X-GEMINI-APIKEY", CryptoUtility.ToUnsecureString(PublicApiKey)); @@ -96,29 +121,41 @@ protected override async Task> OnGetMarketSymbolsAsync() return await MakeJsonRequestAsync("/symbols"); } - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() + protected internal override async Task< + IEnumerable + > OnGetMarketSymbolsMetadataAsync() { List markets = new List(); try { - string html = (await RequestMaker.MakeRequestAsync("/rest-api", "https://docs.gemini.com")).Response; - int startPos = html.IndexOf("

Symbols and minimums

"); + string html = ( + await RequestMaker.MakeRequestAsync("/rest-api", "https://docs.gemini.com") + ).Response; + int startPos = html.IndexOf( + "

Symbols and minimums

" + ); if (startPos < 0) { - throw new ApplicationException("Gemini html for symbol metadata is missing expected h1 tag and id"); + throw new ApplicationException( + "Gemini html for symbol metadata is missing expected h1 tag and id" + ); } startPos = html.IndexOf("", startPos); if (startPos < 0) { - throw new ApplicationException("Gemini html for symbol metadata is missing start tbody tag"); + throw new ApplicationException( + "Gemini html for symbol metadata is missing start tbody tag" + ); } int endPos = html.IndexOf("", startPos); if (endPos < 0) { - throw new ApplicationException("Gemini html for symbol metadata is missing ending tbody tag"); + throw new ApplicationException( + "Gemini html for symbol metadata is missing ending tbody tag" + ); } string table = html.Substring(startPos, endPos - startPos + "".Length); @@ -127,7 +164,9 @@ protected internal override async Task> OnGetMarketS doc.LoadXml(xml); if (doc.ChildNodes.Count < 2) { - throw new ApplicationException("Gemini html for symbol metadata does not have the expected number of nodes"); + throw new ApplicationException( + "Gemini html for symbol metadata does not have the expected number of nodes" + ); } XmlNode root = doc.ChildNodes.Item(1); @@ -147,7 +186,9 @@ protected internal override async Task> OnGetMarketS if (tr.ChildNodes.Count != 4) { - throw new ApplicationException("Gemini html for symbol metadata does not have 4 rows per entry anymore"); + throw new ApplicationException( + "Gemini html for symbol metadata does not have 4 rows per entry anymore" + ); } ExchangeMarket market = new ExchangeMarket { IsActive = true }; @@ -159,21 +200,33 @@ protected internal override async Task> OnGetMarketS int minOrderSizePos = minOrderSizeNode.InnerText.IndexOf(' '); if (minOrderSizePos < 0) { - throw new ArgumentException("Min order size text does not have a space after the number"); + throw new ArgumentException( + "Min order size text does not have a space after the number" + ); } - decimal minOrderSize = minOrderSizeNode.InnerText.Substring(0, minOrderSizePos).ConvertInvariant(); + decimal minOrderSize = minOrderSizeNode.InnerText + .Substring(0, minOrderSizePos) + .ConvertInvariant(); int tickSizePos = tickSizeNode.InnerText.IndexOf(' '); if (tickSizePos < 0) { - throw new ArgumentException("Tick size text does not have a space after the number"); + throw new ArgumentException( + "Tick size text does not have a space after the number" + ); } - decimal tickSize = tickSizeNode.InnerText.Substring(0, tickSizePos).ConvertInvariant(); + decimal tickSize = tickSizeNode.InnerText + .Substring(0, tickSizePos) + .ConvertInvariant(); int incrementSizePos = incrementNode.InnerText.IndexOf(' '); if (incrementSizePos < 0) { - throw new ArgumentException("Increment size text does not have a space after the number"); + throw new ArgumentException( + "Increment size text does not have a space after the number" + ); } - decimal incrementSize = incrementNode.InnerText.Substring(0, incrementSizePos).ConvertInvariant(); + decimal incrementSize = incrementNode.InnerText + .Substring(0, incrementSizePos) + .ConvertInvariant(); market.MarketSymbol = symbol; market.AltMarketSymbol = symbol.ToUpper(); market.BaseCurrency = symbol.Substring(0, symbol.Length - 3); @@ -188,7 +241,10 @@ protected internal override async Task> OnGetMarketS catch (Exception ex) { markets.Clear(); - Logger.Error(ex, "Failed to parse gemini symbol metadata web page, falling back to per symbol query..."); + Logger.Error( + ex, + "Failed to parse gemini symbol metadata web page, falling back to per symbol query..." + ); } // slow way, fetch each symbol one by one, gemini api epic fail @@ -198,25 +254,39 @@ protected internal override async Task> OnGetMarketS List tasks = new List(); foreach (string symbol in symbols) { - tasks.Add(Task.Run(async () => - { - JToken token = await MakeJsonRequestAsync("/symbols/details/" + HttpUtility.UrlEncode(symbol)); - - // {"symbol":"BTCUSD","base_currency":"BTC","quote_currency":"USD","tick_size":1E-8,"quote_increment":0.01,"min_order_size":"0.00001","status":"open"} - lock (markets) - { - markets.Add(new ExchangeMarket + tasks.Add( + Task.Run(async () => { - BaseCurrency = token["base_currency"].ToStringInvariant(), - IsActive = token["status"].ToStringInvariant().Equals("open", StringComparison.OrdinalIgnoreCase), - MarketSymbol = token["symbol"].ToStringInvariant(), - MinTradeSize = token["min_order_size"].ConvertInvariant(), - QuantityStepSize = token["tick_size"].ConvertInvariant(), - QuoteCurrency = token["quote_currency"].ToStringInvariant(), - PriceStepSize = token["quote_increment"].ConvertInvariant() - }); - } - })); + JToken token = await MakeJsonRequestAsync( + "/symbols/details/" + HttpUtility.UrlEncode(symbol) + ); + + // {"symbol":"BTCUSD","base_currency":"BTC","quote_currency":"USD","tick_size":1E-8,"quote_increment":0.01,"min_order_size":"0.00001","status":"open"} + lock (markets) + { + markets.Add( + new ExchangeMarket + { + BaseCurrency = token["base_currency"].ToStringInvariant(), + IsActive = token["status"] + .ToStringInvariant() + .Equals("open", StringComparison.OrdinalIgnoreCase), + MarketSymbol = token["symbol"].ToStringInvariant(), + MinTradeSize = token[ + "min_order_size" + ].ConvertInvariant(), + QuantityStepSize = token[ + "tick_size" + ].ConvertInvariant(), + QuoteCurrency = token["quote_currency"].ToStringInvariant(), + PriceStepSize = token[ + "quote_increment" + ].ConvertInvariant() + } + ); + } + }) + ); } await Task.WhenAll(tasks); @@ -245,23 +315,45 @@ protected override async Task OnGetTickerAsync(string marketSymb return t; } - protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) + protected override async Task OnGetOrderBookAsync( + string marketSymbol, + int maxCount = 100 + ) { - JToken obj = await MakeJsonRequestAsync("/book/" + marketSymbol + "?limit_bids=" + maxCount + "&limit_asks=" + maxCount); + JToken obj = await MakeJsonRequestAsync( + "/book/" + marketSymbol + "?limit_bids=" + maxCount + "&limit_asks=" + maxCount + ); return obj.ParseOrderBookFromJTokenDictionaries(); } - protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = 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) { Callback = callback, DirectionIsBackwards = false, EndDate = endDate, - ParseFunction = (JToken token) => token.ParseTrade("amount", "price", "type", "timestampms", TimestampType.UnixMilliseconds, idKey: "tid"), + ParseFunction = (JToken token) => + token.ParseTrade( + "amount", + "price", + "type", + "timestampms", + TimestampType.UnixMilliseconds, + idKey: "tid" + ), StartDate = startDate, MarketSymbol = marketSymbol, - TimestampFunction = (DateTime dt) => ((long)CryptoUtility.UnixTimestampFromDateTimeMilliseconds(dt)).ToStringInvariant(), + TimestampFunction = (DateTime dt) => + ( + (long)CryptoUtility.UnixTimestampFromDateTimeMilliseconds(dt) + ).ToStringInvariant(), Url = "/trades/[marketSymbol]?limit_trades=100×tamp={0}" }; await state.ProcessHistoricalTrades(); @@ -269,10 +361,21 @@ protected override async Task OnGetHistoricalTradesAsync(Func> OnGetAmountsAsync() { - Dictionary lookup = new Dictionary(StringComparer.OrdinalIgnoreCase); - JArray obj = await MakeJsonRequestAsync("/balances", null, await GetNoncePayloadAsync()); - var q = from JToken token in obj - select new { Currency = token["currency"].ToStringInvariant(), Available = token["amount"].ConvertInvariant() }; + Dictionary lookup = new Dictionary( + StringComparer.OrdinalIgnoreCase + ); + JArray obj = await MakeJsonRequestAsync( + "/balances", + null, + await GetNoncePayloadAsync() + ); + var q = + from JToken token in obj + select new + { + Currency = token["currency"].ToStringInvariant(), + Available = token["amount"].ConvertInvariant() + }; foreach (var kv in q) { if (kv.Available > 0m) @@ -283,12 +386,25 @@ protected override async Task> OnGetAmountsAsync() return lookup; } - protected override async Task> OnGetAmountsAvailableToTradeAsync() + protected override async Task< + Dictionary + > OnGetAmountsAvailableToTradeAsync() { - Dictionary lookup = new Dictionary(StringComparer.OrdinalIgnoreCase); - JArray obj = await MakeJsonRequestAsync("/balances", null, await GetNoncePayloadAsync()); - var q = from JToken token in obj - select new { Currency = token["currency"].ToStringInvariant(), Available = token["available"].ConvertInvariant() }; + Dictionary lookup = new Dictionary( + StringComparer.OrdinalIgnoreCase + ); + JArray obj = await MakeJsonRequestAsync( + "/balances", + null, + await GetNoncePayloadAsync() + ); + var q = + from JToken token in obj + select new + { + Currency = token["currency"].ToStringInvariant(), + Available = token["available"].ConvertInvariant() + }; foreach (var kv in q) { if (kv.Available > 0m) @@ -299,7 +415,9 @@ protected override async Task> OnGetAmountsAvailable return lookup; } - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) + protected override async Task OnPlaceOrderAsync( + ExchangeOrderRequest order + ) { if (order.OrderType == OrderType.Market) { @@ -308,21 +426,34 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd object nonce = await GenerateNonceAsync(); Dictionary payload = new Dictionary - { { "nonce", nonce }, - { "client_order_id", "ExchangeSharp_" + CryptoUtility.UtcNow.ToString("s", System.Globalization.CultureInfo.InvariantCulture) }, - { "symbol", order.MarketSymbol }, - { "amount", order.RoundAmount().ToStringInvariant() }, - { "price", order.Price.ToStringInvariant() }, - { "side", (order.IsBuy ? "buy" : "sell") }, - { "type", "exchange limit" } - }; - if (order.IsPostOnly == true) payload["options"] = "[maker-or-cancel]"; // This order will only add liquidity to the order book. If any part of the order could be filled immediately, the whole order will instead be canceled before any execution occurs. If that happens, the response back from the API will indicate that the order has already been canceled("is_cancelled": true in JSON). Note: some other exchanges call this option "post-only". + { + { "nonce", nonce }, + { + "client_order_id", + "ExchangeSharp_" + + CryptoUtility.UtcNow.ToString( + "s", + System.Globalization.CultureInfo.InvariantCulture + ) + }, + { "symbol", order.MarketSymbol }, + { "amount", order.RoundAmount().ToStringInvariant() }, + { "price", order.Price.ToStringInvariant() }, + { "side", (order.IsBuy ? "buy" : "sell") }, + { "type", "exchange limit" } + }; + if (order.IsPostOnly == true) + payload["options"] = "[maker-or-cancel]"; // This order will only add liquidity to the order book. If any part of the order could be filled immediately, the whole order will instead be canceled before any execution occurs. If that happens, the response back from the API will indicate that the order has already been canceled("is_cancelled": true in JSON). Note: some other exchanges call this option "post-only". order.ExtraParameters.CopyTo(payload); JToken obj = await MakeJsonRequestAsync("/order/new", null, payload); return ParseOrder(obj); } - protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnGetOrderDetailsAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { if (string.IsNullOrWhiteSpace(orderId)) { @@ -330,17 +461,30 @@ protected override async Task OnGetOrderDetailsAsync(string } object nonce = await GenerateNonceAsync(); - var payload = new Dictionary { { "nonce", nonce }, - { isClientOrderId ? "client_order_id" : "order_id", orderId } }; // client_order_id cannot be used in combination with order_id - JToken result = await MakeJsonRequestAsync("/order/status", null, payload: payload); + var payload = new Dictionary + { + { "nonce", nonce }, + { isClientOrderId ? "client_order_id" : "order_id", orderId } + }; // client_order_id cannot be used in combination with order_id + JToken result = await MakeJsonRequestAsync( + "/order/status", + null, + payload: payload + ); return ParseOrder(result); } - protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) + protected override async Task> OnGetOpenOrderDetailsAsync( + string marketSymbol = null + ) { List orders = new List(); object nonce = await GenerateNonceAsync(); - JToken result = await MakeJsonRequestAsync("/orders", null, new Dictionary { { "nonce", nonce } }); + JToken result = await MakeJsonRequestAsync( + "/orders", + null, + new Dictionary { { "nonce", nonce } } + ); if (result is JArray array) { foreach (JToken token in array) @@ -355,45 +499,77 @@ protected override async Task> OnGetOpenOrderDe return orders; } - protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnCancelOrderAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { - if (isClientOrderId) throw new NotSupportedException("Cancelling by client order ID is not supported in ExchangeSharp. Please submit a PR if you are interested in this feature"); + if (isClientOrderId) + throw new NotSupportedException( + "Cancelling by client order ID is not supported in ExchangeSharp. Please submit a PR if you are interested in this feature" + ); object nonce = await GenerateNonceAsync(); - await MakeJsonRequestAsync("/order/cancel", null, new Dictionary { { "nonce", nonce }, { "order_id", orderId } }); + await MakeJsonRequestAsync( + "/order/cancel", + null, + new Dictionary { { "nonce", nonce }, { "order_id", orderId } } + ); } - protected override async Task OnGetTickersWebSocketAsync(Action>> tickerCallback, params string[] marketSymbols) + protected override async Task OnGetTickersWebSocketAsync( + Action>> tickerCallback, + params string[] marketSymbols + ) { if (marketSymbols == null || marketSymbols.Length == 0) { marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); } - ConcurrentDictionary volumeDict = new ConcurrentDictionary(); - ConcurrentDictionary tickerDict = new ConcurrentDictionary(); - static ExchangeTicker GetTicker(ConcurrentDictionary tickerDict, ExchangeGeminiAPI api, string marketSymbol) + ConcurrentDictionary volumeDict = + new ConcurrentDictionary(); + ConcurrentDictionary tickerDict = + new ConcurrentDictionary(); + static ExchangeTicker GetTicker( + ConcurrentDictionary tickerDict, + ExchangeGeminiAPI api, + string marketSymbol + ) { - return tickerDict.GetOrAdd(marketSymbol, (_marketSymbol) => - { - (string baseCurrency, string quoteCurrency) = api.ExchangeMarketSymbolToCurrenciesAsync(_marketSymbol).Sync(); - return new ExchangeTicker - { - Exchange = api.Name, - MarketSymbol = _marketSymbol, - Volume = new ExchangeVolume + return tickerDict.GetOrAdd( + marketSymbol, + (_marketSymbol) => { - BaseCurrency = baseCurrency, - QuoteCurrency = quoteCurrency + (string baseCurrency, string quoteCurrency) = + api.ExchangeMarketSymbolToCurrenciesAsync(_marketSymbol).Sync(); + return new ExchangeTicker + { + Exchange = api.Name, + MarketSymbol = _marketSymbol, + Volume = new ExchangeVolume + { + BaseCurrency = baseCurrency, + QuoteCurrency = quoteCurrency + } + }; } - }; - }); + ); } - static void PublishTicker(ExchangeTicker ticker, string marketSymbol, ConcurrentDictionary _volumeDict, - Action>> callback) + static void PublishTicker( + ExchangeTicker ticker, + string marketSymbol, + ConcurrentDictionary _volumeDict, + Action>> callback + ) { // if we are fully populated... - if (ticker.Bid > 0m && ticker.Ask > 0m && ticker.Bid <= ticker.Ask - && _volumeDict.TryGetValue(marketSymbol, out decimal tickerVolume)) + if ( + ticker.Bid > 0m + && ticker.Ask > 0m + && ticker.Bid <= ticker.Ask + && _volumeDict.TryGetValue(marketSymbol, out decimal tickerVolume) + ) { ticker.Volume.BaseCurrencyVolume = tickerVolume; ticker.Volume.QuoteCurrencyVolume = tickerVolume * ticker.Last; @@ -402,95 +578,138 @@ static void PublishTicker(ExchangeTicker ticker, string marketSymbol, Concurrent } } - return await ConnectPublicWebSocketAsync(null, messageCallback: (_socket, msg) => - { - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - if (token["result"].ToStringInvariant() == "error") - { - // {{ "result": "error", "reason": "InvalidJson"}} - Logger.Info(token["reason"].ToStringInvariant()); - return Task.CompletedTask; - } - string type = token["type"].ToStringInvariant(); - switch (type) - { - case "candles_1d_updates": + return await ConnectPublicWebSocketAsync( + null, + messageCallback: (_socket, msg) => + { + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + if (token["result"].ToStringInvariant() == "error") { - JToken changesToken = token["changes"]; - if (changesToken != null) - { - string marketSymbol = token["symbol"].ToStringInvariant(); - if (changesToken.FirstOrDefault() is JArray candleArray) - { - decimal volume = candleArray[5].ConvertInvariant(); - volumeDict[marketSymbol] = volume; - ExchangeTicker ticker = GetTicker(tickerDict, this, marketSymbol); - PublishTicker(ticker, marketSymbol, volumeDict, tickerCallback); - } - } + // {{ "result": "error", "reason": "InvalidJson"}} + Logger.Info(token["reason"].ToStringInvariant()); + return Task.CompletedTask; } - break; - - case "l2_updates": + string type = token["type"].ToStringInvariant(); + switch (type) { - // fetch the last bid/ask/last prices - if (token["trades"] is JArray tradesToken) - { - string marketSymbol = token["symbol"].ToStringInvariant(); - ExchangeTicker ticker = GetTicker(tickerDict, this, marketSymbol); - JToken lastSell = tradesToken.FirstOrDefault(t => t["side"].ToStringInvariant().Equals("sell", StringComparison.OrdinalIgnoreCase)); - if (lastSell != null) + case "candles_1d_updates": + { - decimal lastTradePrice = lastSell["price"].ConvertInvariant(); - ticker.Bid = ticker.Last = lastTradePrice; + JToken changesToken = token["changes"]; + if (changesToken != null) + { + string marketSymbol = token["symbol"].ToStringInvariant(); + if (changesToken.FirstOrDefault() is JArray candleArray) + { + decimal volume = candleArray[5].ConvertInvariant(); + volumeDict[marketSymbol] = volume; + ExchangeTicker ticker = GetTicker( + tickerDict, + this, + marketSymbol + ); + PublishTicker( + ticker, + marketSymbol, + volumeDict, + tickerCallback + ); + } + } } - JToken lastBuy = tradesToken.FirstOrDefault(t => t["side"].ToStringInvariant().Equals("buy", StringComparison.OrdinalIgnoreCase)); - if (lastBuy != null) + break; + + case "l2_updates": + { - decimal lastTradePrice = lastBuy["price"].ConvertInvariant(); - ticker.Ask = ticker.Last = lastTradePrice; + // fetch the last bid/ask/last prices + if (token["trades"] is JArray tradesToken) + { + string marketSymbol = token["symbol"].ToStringInvariant(); + ExchangeTicker ticker = GetTicker( + tickerDict, + this, + marketSymbol + ); + JToken lastSell = tradesToken.FirstOrDefault( + t => + t["side"] + .ToStringInvariant() + .Equals("sell", StringComparison.OrdinalIgnoreCase) + ); + if (lastSell != null) + { + decimal lastTradePrice = lastSell[ + "price" + ].ConvertInvariant(); + ticker.Bid = ticker.Last = lastTradePrice; + } + JToken lastBuy = tradesToken.FirstOrDefault( + t => + t["side"] + .ToStringInvariant() + .Equals("buy", StringComparison.OrdinalIgnoreCase) + ); + if (lastBuy != null) + { + decimal lastTradePrice = lastBuy[ + "price" + ].ConvertInvariant(); + ticker.Ask = ticker.Last = lastTradePrice; + } + + PublishTicker(ticker, marketSymbol, volumeDict, tickerCallback); + } } + break; - PublishTicker(ticker, marketSymbol, volumeDict, tickerCallback); - } - } - break; + case "trade": - case "trade": - { - //{ "type":"trade","symbol":"ETHUSD","event_id":35899433249,"timestamp":1619191314701,"price":"2261.65","quantity":"0.010343","side":"buy"} + { + //{ "type":"trade","symbol":"ETHUSD","event_id":35899433249,"timestamp":1619191314701,"price":"2261.65","quantity":"0.010343","side":"buy"} - // fetch the active ticker metadata for this symbol - string marketSymbol = token["symbol"].ToStringInvariant(); - ExchangeTicker ticker = GetTicker(tickerDict, this, marketSymbol); - string side = token["side"].ToStringInvariant(); - decimal price = token["price"].ConvertInvariant(); - if (side == "sell") - { - ticker.Bid = ticker.Last = price; - } - else - { - ticker.Ask = ticker.Last = price; - } - PublishTicker(ticker, marketSymbol, volumeDict, tickerCallback); + // fetch the active ticker metadata for this symbol + string marketSymbol = token["symbol"].ToStringInvariant(); + ExchangeTicker ticker = GetTicker(tickerDict, this, marketSymbol); + string side = token["side"].ToStringInvariant(); + decimal price = token["price"].ConvertInvariant(); + if (side == "sell") + { + ticker.Bid = ticker.Last = price; + } + else + { + ticker.Ask = ticker.Last = price; + } + PublishTicker(ticker, marketSymbol, volumeDict, tickerCallback); + } + break; } - break; - } - return Task.CompletedTask; - }, connectCallback: async (_socket) => - { - volumeDict.Clear(); - tickerDict.Clear(); - await _socket.SendMessageAsync(new - { - type = "subscribe", - subscriptions = new[] { new { name = "candles_1d", symbols = marketSymbols }, new { name = "l2", symbols = marketSymbols } } - }); - }); + return Task.CompletedTask; + }, + connectCallback: async (_socket) => + { + volumeDict.Clear(); + tickerDict.Clear(); + await _socket.SendMessageAsync( + new + { + type = "subscribe", + subscriptions = new[] + { + new { name = "candles_1d", symbols = marketSymbols }, + new { name = "l2", symbols = marketSymbols } + } + } + ); + } + ); } - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + protected override async Task OnGetTradesWebSocketAsync( + Func, Task> callback, + params string[] marketSymbols + ) { //{ // "type": "l2_updates", @@ -565,54 +784,57 @@ protected override async Task OnGetTradesWebSocketAsync(Func - { - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - if (token["result"].ToStringInvariant() == "error") - { - // {{ "result": "error", "reason": "InvalidJson"}} - Logger.Info(token["reason"].ToStringInvariant()); - } - else if (token["type"].ToStringInvariant() == "l2_updates") - { - string marketSymbol = token["symbol"].ToStringInvariant(); - var tradesToken = token["trades"]; - if (tradesToken != null) - foreach (var tradeToken in tradesToken) - { - var trade = ParseWebSocketTrade(tradeToken); - trade.Flags |= ExchangeTradeFlags.IsFromSnapshot; - await callback(new KeyValuePair(marketSymbol, trade)); - } - } - else if (token["type"].ToStringInvariant() == "trade") - { - string marketSymbol = token["symbol"].ToStringInvariant(); - var trade = ParseWebSocketTrade(token); - await callback(new KeyValuePair(marketSymbol, trade)); - } - }, connectCallback: async (_socket) => - { - //{ "type": "subscribe","subscriptions":[{ "name":"l2","symbols":["BTCUSD","ETHUSD","ETHBTC"]}]} - await _socket.SendMessageAsync(new - { - type = "subscribe", - subscriptions = new[] - { - new - { - name = "l2", - symbols = marketSymbols - } - } - }); - }); + return await ConnectPublicWebSocketAsync( + BaseUrlWebSocket, + messageCallback: async (_socket, msg) => + { + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + if (token["result"].ToStringInvariant() == "error") + { + // {{ "result": "error", "reason": "InvalidJson"}} + Logger.Info(token["reason"].ToStringInvariant()); + } + else if (token["type"].ToStringInvariant() == "l2_updates") + { + string marketSymbol = token["symbol"].ToStringInvariant(); + var tradesToken = token["trades"]; + if (tradesToken != null) + foreach (var tradeToken in tradesToken) + { + var trade = ParseWebSocketTrade(tradeToken); + trade.Flags |= ExchangeTradeFlags.IsFromSnapshot; + await callback( + new KeyValuePair(marketSymbol, trade) + ); + } + } + else if (token["type"].ToStringInvariant() == "trade") + { + string marketSymbol = token["symbol"].ToStringInvariant(); + var trade = ParseWebSocketTrade(token); + await callback( + new KeyValuePair(marketSymbol, trade) + ); + } + }, + connectCallback: async (_socket) => + { + //{ "type": "subscribe","subscriptions":[{ "name":"l2","symbols":["BTCUSD","ETHUSD","ETHBTC"]}]} + await _socket.SendMessageAsync( + new + { + type = "subscribe", + subscriptions = new[] { new { name = "l2", symbols = marketSymbols } } + } + ); + } + ); } protected override async Task OnGetDeltaOrderBookWebSocketAsync( - Action callback, - int maxCount = 20, - params string[] marketSymbols + Action callback, + int maxCount = 20, + params string[] marketSymbols ) { if (marketSymbols == null || marketSymbols.Length == 0) @@ -620,62 +842,78 @@ params string[] marketSymbols marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); } - return await ConnectPublicWebSocketAsync(string.Empty, (_socket, msg) => - { - string message = msg.ToStringFromUTF8(); - var book = new ExchangeOrderBook(); - - if (message.Contains("l2_updates")) - { - // parse delta update - var delta = JsonConvert.DeserializeObject(message) as JObject; + return await ConnectPublicWebSocketAsync( + string.Empty, + (_socket, msg) => + { + string message = msg.ToStringFromUTF8(); + var book = new ExchangeOrderBook(); - var symbol = delta["symbol"].ToString(); - book.MarketSymbol = symbol; + if (message.Contains("l2_updates")) + { + // parse delta update + var delta = JsonConvert.DeserializeObject(message) as JObject; - // Gemini doesn't have a send timestamp in their response so use received timestamp. - book.LastUpdatedUtc = DateTime.UtcNow; + var symbol = delta["symbol"].ToString(); + book.MarketSymbol = symbol; - // Gemini doesn't have a sequence id in their response so use timestamp ticks. - book.SequenceId = DateTime.Now.Ticks; + // Gemini doesn't have a send timestamp in their response so use received timestamp. + book.LastUpdatedUtc = DateTime.UtcNow; - foreach (JArray change in delta["changes"]) - { - if (change.Count == 3) - { - bool sell = change[0].ToStringInvariant() == "sell"; - decimal price = change[1].ConvertInvariant(); - decimal amount = change[2].ConvertInvariant(); + // Gemini doesn't have a sequence id in their response so use timestamp ticks. + book.SequenceId = DateTime.Now.Ticks; - SortedDictionary dict = (sell ? book.Asks : book.Bids); + foreach (JArray change in delta["changes"]) + { + if (change.Count == 3) + { + bool sell = change[0].ToStringInvariant() == "sell"; + decimal price = change[1].ConvertInvariant(); + decimal amount = change[2].ConvertInvariant(); + + SortedDictionary dict = ( + sell ? book.Asks : book.Bids + ); + + dict[price] = new ExchangeOrderPrice + { + Amount = amount, + Price = price + }; + } + } - dict[price] = new ExchangeOrderPrice { Amount = amount, Price = price }; + callback(book); } - } - callback(book); - } - - return Task.CompletedTask; - }, connectCallback: async (_socket) => - { - await _socket.SendMessageAsync(new - { - type = "subscribe", - subscriptions = new[] { new { - name = "l2", - symbols = marketSymbols - } } - }); - }); + return Task.CompletedTask; + }, + connectCallback: async (_socket) => + { + await _socket.SendMessageAsync( + new + { + type = "subscribe", + subscriptions = new[] { new { name = "l2", symbols = marketSymbols } } + } + ); + } + ); } - private static ExchangeTrade ParseWebSocketTrade(JToken token) => token.ParseTrade( - amountKey: "quantity", priceKey: "price", - typeKey: "side", timestampKey: "timestamp", - TimestampType.UnixMilliseconds, idKey: "event_id" - ); + private static ExchangeTrade ParseWebSocketTrade(JToken token) => + token.ParseTrade( + amountKey: "quantity", + priceKey: "price", + typeKey: "side", + timestampKey: "timestamp", + TimestampType.UnixMilliseconds, + idKey: "event_id" + ); } - public partial class ExchangeName { public const string Gemini = "Gemini"; } + public partial class ExchangeName + { + public const string Gemini = "Gemini"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/Hitbtc/ExchangeHitbtcAPI.cs b/src/ExchangeSharp/API/Exchanges/Hitbtc/ExchangeHitbtcAPI.cs index 660165a32..110fb9bee 100644 --- a/src/ExchangeSharp/API/Exchanges/Hitbtc/ExchangeHitbtcAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Hitbtc/ExchangeHitbtcAPI.cs @@ -12,21 +12,20 @@ The above copyright notice and this permission notice shall be included in all c using System; using System.Collections.Generic; -using System.Net; using System.Linq; +using System.Net; using System.Security.Cryptography; using System.Text; -using System.Web; using System.Threading.Tasks; - +using System.Web; using Newtonsoft.Json.Linq; namespace ExchangeSharp { - public sealed partial class ExchangeHitBTCAPI : ExchangeAPI - { - public override string BaseUrl { get; set; } = "https://api.hitbtc.com/api/2"; - public override string BaseUrlWebSocket { get; set; } = "wss://api.hitbtc.com/api/2/ws"; + public sealed partial class ExchangeHitBTCAPI : ExchangeAPI + { + public override string BaseUrl { get; set; } = "https://api.hitbtc.com/api/2"; + public override string BaseUrlWebSocket { get; set; } = "wss://api.hitbtc.com/api/2/ws"; private ExchangeHitBTCAPI() { @@ -37,426 +36,598 @@ private ExchangeHitBTCAPI() } public override string PeriodSecondsToString(int seconds) - { - switch (seconds) - { - case 60: return "M1"; - case 180: return "M3"; - case 300: return "M5"; - case 900: return "M15"; - case 1800: return "M30"; - case 3600: return "H1"; - case 14400: return "H4"; - case 86400: return "D1"; - case 604800: return "D7"; - case 2419200: return "1M"; // 28 days - case 2592000: return "1M"; // 30 days - case 2678000: return "1M"; // 31 days - case 4233600: return "1M"; // 49 days - default: throw new ArgumentException( - $"{nameof(seconds)} must be 60, 180, 300, 900, 1800, 3600, 14400, 86400, 604800, 2419200, 2592000, 2678000, 4233600"); - } - } - - protected override Uri ProcessRequestUrl(UriBuilder url, Dictionary payload, string method) - { - if (method != "PUT" && method != "POST" && payload != null && payload.Count != 0) - { - url.AppendPayloadToQuery(payload); - } - return url.Uri; - } - - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) - { - // only authenticated requests write json, everything uses GET and url params - if (CanMakeAuthenticatedRequest(payload)) - { - request.AddHeader("Authorization", CryptoUtility.BasicAuthenticationString(PublicApiKey.ToUnsecureString(), PrivateApiKey.ToUnsecureString())); - if (request.Method == "POST") - { - await CryptoUtility.WritePayloadJsonToRequestAsync(request, payload); - } - } - } - - #region Public APIs - - protected override async Task> OnGetCurrenciesAsync() - { - Dictionary currencies = new Dictionary(); - //[{"id": "BTC", "fullName": "Bitcoin", "crypto": true, "payinEnabled": true, "payinPaymentId": false, "payinConfirmations": 2, "payoutEnabled": true, "payoutIsPaymentId": false, "transferEnabled": true, "delisted": false, "payoutFee": "0.00958" }, ... - JToken obj = await MakeJsonRequestAsync("/public/currency"); - foreach (JToken token in obj) - { - bool enabled = token["delisted"].ToStringInvariant().Equals("false"); - currencies.Add(token["id"].ToStringInvariant(), new ExchangeCurrency() - { - Name = token["id"].ToStringInvariant(), - FullName = token["fullName"].ToStringInvariant(), - TxFee = token["payoutFee"].ConvertInvariant(), - MinConfirmations = token["payinConfirmations"].ConvertInvariant(), - DepositEnabled = enabled, - WithdrawalEnabled = enabled - }); - } - return currencies; - } - - protected override async Task> OnGetMarketSymbolsAsync() - { - List symbols = new List(); - // [ {"id": "ETHBTC","baseCurrency": "ETH","quoteCurrency": "BTC", "quantityIncrement": "0.001", "tickSize": "0.000001", "takeLiquidityRate": "0.001", "provideLiquidityRate": "-0.0001", "feeCurrency": "BTC" } ... ] - JToken obj = await MakeJsonRequestAsync("/public/symbol"); - foreach (JToken token in obj) symbols.Add(token["id"].ToStringInvariant()); - return symbols; - } - - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() - { - List markets = new List(); - // [ {"id": "ETHBTC","baseCurrency": "ETH","quoteCurrency": "BTC", "quantityIncrement": "0.001", "tickSize": "0.000001", "takeLiquidityRate": "0.001", "provideLiquidityRate": "-0.0001", "feeCurrency": "BTC" } ... ] - JToken obj = await MakeJsonRequestAsync("/public/symbol"); - foreach (JToken token in obj) - { - markets.Add(new ExchangeMarket() - { - MarketSymbol = token["id"].ToStringInvariant(), - BaseCurrency = token["baseCurrency"].ToStringInvariant(), - QuoteCurrency = token["quoteCurrency"].ToStringInvariant(), - QuantityStepSize = token["quantityIncrement"].ConvertInvariant(), - PriceStepSize = token["tickSize"].ConvertInvariant(), - IsActive = true - }); - } - return markets; - } - - - protected override async Task OnGetTickerAsync(string marketSymbol) - { - JToken obj = await MakeJsonRequestAsync("/public/ticker/" + marketSymbol); - return await ParseTickerAsync(obj, marketSymbol); - } - - protected override async Task>> OnGetTickersAsync() - { - List> tickers = new List>(); - JToken obj = await MakeJsonRequestAsync("/public/ticker"); - foreach (JToken token in obj) - { - string marketSymbol = NormalizeMarketSymbol(token["symbol"].ToStringInvariant()); - tickers.Add(new KeyValuePair(marketSymbol, await ParseTickerAsync(token, marketSymbol))); - } - return tickers; - } - - protected override async Task> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) - { - // [ {"timestamp": "2017-10-20T20:00:00.000Z","open": "0.050459","close": "0.050087","min": "0.050000","max": "0.050511","volume": "1326.628", "volumeQuote": "66.555987736"}, ... ] - List candles = new List(); - string periodString = PeriodSecondsToString(periodSeconds); - limit = limit ?? 100; - JToken obj = await MakeJsonRequestAsync("/public/candles/" + marketSymbol + "?period=" + periodString + "&limit=" + limit); - foreach (JToken token in obj) - { - candles.Add(this.ParseCandle(token, marketSymbol, periodSeconds, "open", "max", "min", "close", "timestamp", TimestampType.Iso8601UTC, "volume", "volumeQuote")); - } - return candles; - } - - protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null) - { - List trades = new List(); + { + switch (seconds) + { + case 60: + return "M1"; + case 180: + return "M3"; + case 300: + return "M5"; + case 900: + return "M15"; + case 1800: + return "M30"; + case 3600: + return "H1"; + case 14400: + return "H4"; + case 86400: + return "D1"; + case 604800: + return "D7"; + case 2419200: + return "1M"; // 28 days + case 2592000: + return "1M"; // 30 days + case 2678000: + return "1M"; // 31 days + case 4233600: + return "1M"; // 49 days + default: + throw new ArgumentException( + $"{nameof(seconds)} must be 60, 180, 300, 900, 1800, 3600, 14400, 86400, 604800, 2419200, 2592000, 2678000, 4233600" + ); + } + } + + protected override Uri ProcessRequestUrl( + UriBuilder url, + Dictionary payload, + string method + ) + { + if (method != "PUT" && method != "POST" && payload != null && payload.Count != 0) + { + url.AppendPayloadToQuery(payload); + } + return url.Uri; + } + + protected override async Task ProcessRequestAsync( + IHttpWebRequest request, + Dictionary payload + ) + { + // only authenticated requests write json, everything uses GET and url params + if (CanMakeAuthenticatedRequest(payload)) + { + request.AddHeader( + "Authorization", + CryptoUtility.BasicAuthenticationString( + PublicApiKey.ToUnsecureString(), + PrivateApiKey.ToUnsecureString() + ) + ); + if (request.Method == "POST") + { + await CryptoUtility.WritePayloadJsonToRequestAsync(request, payload); + } + } + } + + #region Public APIs + + protected override async Task< + IReadOnlyDictionary + > OnGetCurrenciesAsync() + { + Dictionary currencies = + new Dictionary(); + //[{"id": "BTC", "fullName": "Bitcoin", "crypto": true, "payinEnabled": true, "payinPaymentId": false, "payinConfirmations": 2, "payoutEnabled": true, "payoutIsPaymentId": false, "transferEnabled": true, "delisted": false, "payoutFee": "0.00958" }, ... + JToken obj = await MakeJsonRequestAsync("/public/currency"); + foreach (JToken token in obj) + { + bool enabled = token["delisted"].ToStringInvariant().Equals("false"); + currencies.Add( + token["id"].ToStringInvariant(), + new ExchangeCurrency() + { + Name = token["id"].ToStringInvariant(), + FullName = token["fullName"].ToStringInvariant(), + TxFee = token["payoutFee"].ConvertInvariant(), + MinConfirmations = token["payinConfirmations"].ConvertInvariant(), + DepositEnabled = enabled, + WithdrawalEnabled = enabled + } + ); + } + return currencies; + } + + protected override async Task> OnGetMarketSymbolsAsync() + { + List symbols = new List(); + // [ {"id": "ETHBTC","baseCurrency": "ETH","quoteCurrency": "BTC", "quantityIncrement": "0.001", "tickSize": "0.000001", "takeLiquidityRate": "0.001", "provideLiquidityRate": "-0.0001", "feeCurrency": "BTC" } ... ] + JToken obj = await MakeJsonRequestAsync("/public/symbol"); + foreach (JToken token in obj) + symbols.Add(token["id"].ToStringInvariant()); + return symbols; + } + + protected internal override async Task< + IEnumerable + > OnGetMarketSymbolsMetadataAsync() + { + List markets = new List(); + // [ {"id": "ETHBTC","baseCurrency": "ETH","quoteCurrency": "BTC", "quantityIncrement": "0.001", "tickSize": "0.000001", "takeLiquidityRate": "0.001", "provideLiquidityRate": "-0.0001", "feeCurrency": "BTC" } ... ] + JToken obj = await MakeJsonRequestAsync("/public/symbol"); + foreach (JToken token in obj) + { + markets.Add( + new ExchangeMarket() + { + MarketSymbol = token["id"].ToStringInvariant(), + BaseCurrency = token["baseCurrency"].ToStringInvariant(), + QuoteCurrency = token["quoteCurrency"].ToStringInvariant(), + QuantityStepSize = token["quantityIncrement"].ConvertInvariant(), + PriceStepSize = token["tickSize"].ConvertInvariant(), + IsActive = true + } + ); + } + return markets; + } + + protected override async Task OnGetTickerAsync(string marketSymbol) + { + JToken obj = await MakeJsonRequestAsync("/public/ticker/" + marketSymbol); + return await ParseTickerAsync(obj, marketSymbol); + } + + protected override async Task< + IEnumerable> + > OnGetTickersAsync() + { + List> tickers = + new List>(); + JToken obj = await MakeJsonRequestAsync("/public/ticker"); + foreach (JToken token in obj) + { + string marketSymbol = NormalizeMarketSymbol(token["symbol"].ToStringInvariant()); + tickers.Add( + new KeyValuePair( + marketSymbol, + await ParseTickerAsync(token, marketSymbol) + ) + ); + } + return tickers; + } + + protected override async Task> OnGetCandlesAsync( + string marketSymbol, + int periodSeconds, + DateTime? startDate = null, + DateTime? endDate = null, + int? limit = null + ) + { + // [ {"timestamp": "2017-10-20T20:00:00.000Z","open": "0.050459","close": "0.050087","min": "0.050000","max": "0.050511","volume": "1326.628", "volumeQuote": "66.555987736"}, ... ] + List candles = new List(); + string periodString = PeriodSecondsToString(periodSeconds); + limit = limit ?? 100; + JToken obj = await MakeJsonRequestAsync( + "/public/candles/" + marketSymbol + "?period=" + periodString + "&limit=" + limit + ); + foreach (JToken token in obj) + { + candles.Add( + this.ParseCandle( + token, + marketSymbol, + periodSeconds, + "open", + "max", + "min", + "close", + "timestamp", + TimestampType.Iso8601UTC, + "volume", + "volumeQuote" + ) + ); + } + return candles; + } + + protected override async Task> OnGetRecentTradesAsync( + string marketSymbol, + int? limit = null + ) + { + List trades = new List(); // 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) { + 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; - } - - protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) - { - JToken token = await MakeJsonRequestAsync("/public/orderbook/" + marketSymbol + "?limit=" + maxCount.ToStringInvariant()); - return token.ParseOrderBookFromJTokenDictionaries(asks: "ask", bids: "bid", amount: "size"); - } - - protected override async Task OnGetHistoricalTradesAsync(Func, bool> callback, string marketSymbol, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) - { - List trades = new List(); + return trades; + } + + protected override async Task OnGetOrderBookAsync( + string marketSymbol, + int maxCount = 100 + ) + { + JToken token = await MakeJsonRequestAsync( + "/public/orderbook/" + marketSymbol + "?limit=" + maxCount.ToStringInvariant() + ); + return token.ParseOrderBookFromJTokenDictionaries( + asks: "ask", + bids: "bid", + amount: "size" + ); + } + + 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 // 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"); + //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) - { - ExchangeTrade trade = ParseExchangeTrade(token); - if (startDate == null || trade.Timestamp >= startDate) - { - trades.Add(trade); - } - } - if (trades.Count != 0) - { + { + foreach (JToken token in obj) + { + ExchangeTrade trade = ParseExchangeTrade(token); + if (startDate == null || trade.Timestamp >= startDate) + { + trades.Add(trade); + } + } + if (trades.Count != 0) + { callback(trades); //no need to OrderBy or OrderByDescending, handled by sort=DESC or sort=ASC - //callback(trades.OrderBy(t => t.Timestamp)); + //callback(trades.OrderBy(t => t.Timestamp)); + } + } + } + + #endregion + + #region Private APIs + + protected override async Task> OnGetAmountsAsync() + { + Dictionary amounts = new Dictionary(); + // [ {"currency": "BTC","available": "0.0504600","reserved": "0.0000000"}, ... ] + JToken obj = await MakeJsonRequestAsync( + "/trading/balance", + null, + await GetNoncePayloadAsync() + ); + foreach (JToken token in obj) + { + decimal amount = + token["available"].ConvertInvariant() + + token["reserved"].ConvertInvariant(); + if (amount > 0m) + amounts[token["currency"].ToStringInvariant()] = amount; + } + return amounts; + } + + protected override async Task< + Dictionary + > OnGetAmountsAvailableToTradeAsync() + { + Dictionary amounts = new Dictionary(); + // [ {"currency": "BTC","available": "0.0504600","reserved": "0.0000000"}, ... ] + JToken obj = await MakeJsonRequestAsync( + "/trading/balance", + null, + await GetNoncePayloadAsync() + ); + foreach (JToken token in obj) + { + decimal amount = token["available"].ConvertInvariant(); + if (amount > 0m) + amounts[token["currency"].ToStringInvariant()] = amount; + } + return amounts; + } + + /// + /// HitBtc differentiates between active orders and historical trades. They also have three diffrent OrderIds (the id, the orderId, and the ClientOrderId). + /// When placing an order, we return the ClientOrderId, which is only in force for the duration of the order. Completed orders are given a different id. + /// Therefore, this call returns an open order only. Do not use it to return an historical trade order. + /// To retrieve an historical trade order by id, use the GetCompletedOrderDetails with the symbol parameter empty, then filter for desired Id. + /// + /// + /// + protected override async Task OnGetOrderDetailsAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) + { + if (isClientOrderId) + throw new NotSupportedException( + "Querying by client order ID is not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature" + ); + JToken obj = await MakeJsonRequestAsync( + "/history/order/" + orderId + "/trades", + null, + await GetNoncePayloadAsync() + ); + if (obj != null && obj.HasValues) + return ParseCompletedOrder(obj); + return null; + } + + protected override async Task< + IEnumerable + > OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) + { + List orders = new List(); + var payload = await GetNoncePayloadAsync(); + if (!string.IsNullOrEmpty(marketSymbol)) + { + payload["symbol"] = marketSymbol; + } + if (afterDate != null) + { + payload["from"] = afterDate; + } + JToken obj = await MakeJsonRequestAsync("/history/trades", null, payload); + if (obj != null && obj.HasValues) + { + foreach (JToken token in obj) + { + orders.Add(ParseCompletedOrder(token)); } - } - } - - #endregion - - #region Private APIs - - protected override async Task> OnGetAmountsAsync() - { - Dictionary amounts = new Dictionary(); - // [ {"currency": "BTC","available": "0.0504600","reserved": "0.0000000"}, ... ] - JToken obj = await MakeJsonRequestAsync("/trading/balance", null, await GetNoncePayloadAsync()); - foreach (JToken token in obj) - { - decimal amount = token["available"].ConvertInvariant() + token["reserved"].ConvertInvariant(); - if (amount > 0m) amounts[token["currency"].ToStringInvariant()] = amount; - } - return amounts; - } - - protected override async Task> OnGetAmountsAvailableToTradeAsync() - { - Dictionary amounts = new Dictionary(); - // [ {"currency": "BTC","available": "0.0504600","reserved": "0.0000000"}, ... ] - JToken obj = await MakeJsonRequestAsync("/trading/balance", null, await GetNoncePayloadAsync()); - foreach (JToken token in obj) - { - decimal amount = token["available"].ConvertInvariant(); - if (amount > 0m) amounts[token["currency"].ToStringInvariant()] = amount; - } - return amounts; - } - - /// - /// HitBtc differentiates between active orders and historical trades. They also have three diffrent OrderIds (the id, the orderId, and the ClientOrderId). - /// When placing an order, we return the ClientOrderId, which is only in force for the duration of the order. Completed orders are given a different id. - /// Therefore, this call returns an open order only. Do not use it to return an historical trade order. - /// To retrieve an historical trade order by id, use the GetCompletedOrderDetails with the symbol parameter empty, then filter for desired Id. - /// - /// - /// - protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + } + return orders; + } + + protected override async Task> OnGetOpenOrderDetailsAsync( + string marketSymbol = null + ) { - if (isClientOrderId) throw new NotSupportedException("Querying by client order ID is not implemented in ExchangeSharp. Please submit a PR if you are interested in this feature"); - JToken obj = await MakeJsonRequestAsync("/history/order/" + orderId + "/trades", null, await GetNoncePayloadAsync()); - if (obj != null && obj.HasValues) return ParseCompletedOrder(obj); - return null; - } - - protected override async Task> OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) - { - List orders = new List(); - var payload = await GetNoncePayloadAsync(); - if (!string.IsNullOrEmpty(marketSymbol)) - { - payload["symbol"] = marketSymbol; - } - if (afterDate != null) - { - payload["from"] = afterDate; - } - JToken obj = await MakeJsonRequestAsync("/history/trades", null, payload); - if (obj != null && obj.HasValues) - { - foreach (JToken token in obj) - { - orders.Add(ParseCompletedOrder(token)); - } - } - return orders; - } - - protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) - { - List orders = new List(); - var payload = await GetNoncePayloadAsync(); - if (!string.IsNullOrEmpty(marketSymbol)) - { - payload["symbol"] = marketSymbol; - } - JToken obj = await MakeJsonRequestAsync("/order", null, payload); - if (obj != null && obj.HasValues) - { - foreach (JToken token in obj) - { - orders.Add(ParseOpenOrder(token)); - } - } - return orders; - } - - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) - { - var payload = await GetNoncePayloadAsync(); - //payload["clientOrderId"] = "neuMedia" + payload["nonce"]; Currently letting hitbtc assign this, but may not be unique for more than 24 hours - payload["quantity"] = order.Amount; - payload["symbol"] = order.MarketSymbol; - payload["side"] = order.IsBuy ? "buy" : "sell"; - payload["type"] = order.OrderType == OrderType.Limit ? "limit" : "market"; - if (order.OrderType == OrderType.Limit) - { - payload["price"] = order.Price; - payload["timeInForce"] = "GTC"; - } - if (order.IsPostOnly != null) payload["post_only"] = order.IsPostOnly; // Optional. If your post-only order causes a match with a pre-existing order as a taker, then the order will be cancelled. + List orders = new List(); + var payload = await GetNoncePayloadAsync(); + if (!string.IsNullOrEmpty(marketSymbol)) + { + payload["symbol"] = marketSymbol; + } + JToken obj = await MakeJsonRequestAsync("/order", null, payload); + if (obj != null && obj.HasValues) + { + foreach (JToken token in obj) + { + orders.Add(ParseOpenOrder(token)); + } + } + return orders; + } + + protected override async Task OnPlaceOrderAsync( + ExchangeOrderRequest order + ) + { + var payload = await GetNoncePayloadAsync(); + //payload["clientOrderId"] = "neuMedia" + payload["nonce"]; Currently letting hitbtc assign this, but may not be unique for more than 24 hours + payload["quantity"] = order.Amount; + payload["symbol"] = order.MarketSymbol; + payload["side"] = order.IsBuy ? "buy" : "sell"; + payload["type"] = order.OrderType == OrderType.Limit ? "limit" : "market"; + if (order.OrderType == OrderType.Limit) + { + payload["price"] = order.Price; + payload["timeInForce"] = "GTC"; + } + if (order.IsPostOnly != null) + payload["post_only"] = order.IsPostOnly; // Optional. If your post-only order causes a match with a pre-existing order as a taker, then the order will be cancelled. order.ExtraParameters.CopyTo(payload); - // { "id": 0,"clientOrderId": "d8574207d9e3b16a4a5511753eeef175","symbol": "ETHBTC","side": "sell","status": "new","type": "limit","timeInForce": "GTC","quantity": "0.063","price": "0.046016","cumQuantity": "0.000","createdAt": "2017-05-15T17:01:05.092Z","updatedAt": "2017-05-15T17:01:05.092Z" } - JToken token = await MakeJsonRequestAsync("/order", null, payload, "POST"); - ExchangeOrderResult result = new ExchangeOrderResult - { - OrderId = token["id"].ToStringInvariant(), - MarketSymbol = token["symbol"].ToStringInvariant(), - OrderDate = token["createdAt"].ToDateTimeInvariant(), - Amount = token["quantity"].ConvertInvariant(), - Price = token["price"].ConvertInvariant(), - AmountFilled = token["cumQuantity"].ConvertInvariant(), - Message = token["clientOrderId"].ToStringInvariant() - }; - if (result.AmountFilled >= result.Amount) - { - result.Result = ExchangeAPIOrderResult.Filled; - } - else if (result.AmountFilled > 0m) - { - result.Result = ExchangeAPIOrderResult.FilledPartially; - } - else - { - result.Result = ExchangeAPIOrderResult.Open; - } - - ParseAveragePriceAndFeesFromFills(result, token["tradesReport"]); - - return result; - } - - protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + // { "id": 0,"clientOrderId": "d8574207d9e3b16a4a5511753eeef175","symbol": "ETHBTC","side": "sell","status": "new","type": "limit","timeInForce": "GTC","quantity": "0.063","price": "0.046016","cumQuantity": "0.000","createdAt": "2017-05-15T17:01:05.092Z","updatedAt": "2017-05-15T17:01:05.092Z" } + JToken token = await MakeJsonRequestAsync("/order", null, payload, "POST"); + ExchangeOrderResult result = new ExchangeOrderResult + { + OrderId = token["id"].ToStringInvariant(), + MarketSymbol = token["symbol"].ToStringInvariant(), + OrderDate = token["createdAt"].ToDateTimeInvariant(), + Amount = token["quantity"].ConvertInvariant(), + Price = token["price"].ConvertInvariant(), + AmountFilled = token["cumQuantity"].ConvertInvariant(), + Message = token["clientOrderId"].ToStringInvariant() + }; + if (result.AmountFilled >= result.Amount) + { + result.Result = ExchangeAPIOrderResult.Filled; + } + else if (result.AmountFilled > 0m) + { + result.Result = ExchangeAPIOrderResult.FilledPartially; + } + else + { + result.Result = ExchangeAPIOrderResult.Open; + } + + ParseAveragePriceAndFeesFromFills(result, token["tradesReport"]); + + return result; + } + + protected override async Task OnCancelOrderAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { - if (isClientOrderId) throw new NotSupportedException("Cancelling by client order ID is not supported in ExchangeSharp. Please submit a PR if you are interested in this feature"); + if (isClientOrderId) + throw new NotSupportedException( + "Cancelling by client order ID is not supported in ExchangeSharp. Please submit a PR if you are interested in this feature" + ); // this call returns info about the success of the cancel. Sure would be nice have a return type on this method. - JToken token = await MakeJsonRequestAsync("/order/" + orderId, null, await GetNoncePayloadAsync(), "DELETE"); - } - - private void ParseAveragePriceAndFeesFromFills(ExchangeOrderResult result, JToken fillsToken) - { - decimal totalCost = 0; - decimal totalQuantity = 0; - - if (fillsToken is JArray) - { - foreach (var fill in fillsToken) - { - result.Fees += fill["fee"].ConvertInvariant(); - - decimal price = fill["price"].ConvertInvariant(); - decimal quantity = fill["quantity"].ConvertInvariant(); - totalCost += price * quantity; - totalQuantity += quantity; - } - } - - result.AveragePrice = (totalQuantity == 0 ? null : (decimal?)(totalCost / totalQuantity)); - } - - protected override async Task OnGetDepositAddressAsync(string currency, bool forceRegenerate = false) - { - ExchangeDepositDetails deposit = new ExchangeDepositDetails() { Currency = currency }; - JToken token = await MakeJsonRequestAsync("/payment/address/" + currency, null, await GetNoncePayloadAsync()); - if (token != null) - { - deposit.Address = token["address"].ToStringInvariant(); - if (deposit.Address.StartsWith("bitcoincash:")) - { - deposit.Address = deposit.Address.Replace("bitcoincash:", string.Empty); // don't know why they do this for bitcoincash - } - deposit.AddressTag = token["wallet"].ToStringInvariant(); - } - return deposit; - } - - - /// - /// This returns both Deposit and Withdawl history for the Bank and Trading Accounts. Currently returning everything and not filtering. - /// There is no support for retrieving by Symbol, so we'll filter that after reteiving all symbols - /// - /// - /// - protected override async Task> OnGetDepositHistoryAsync(string? currency) - { - List transactions = new List(); - // [ {"id": "6a2fb54d-7466-490c-b3a6-95d8c882f7f7","index": 20400458,"currency": "ETH","amount": "38.616700000000000000000000","fee": "0.000880000000000000000000", "address": "0xfaEF4bE10dDF50B68c220c9ab19381e20B8EEB2B", "hash": "eece4c17994798939cea9f6a72ee12faa55a7ce44860cfb95c7ed71c89522fe8","status": "pending","type": "payout", "createdAt": "2017-05-18T18:05:36.957Z", "updatedAt": "2017-05-18T19:21:05.370Z" }, ... ] - JToken result = await MakeJsonRequestAsync("/account/transactions", null, await GetNoncePayloadAsync()); - if (result != null && result.HasValues) - { - foreach (JToken token in result) - { - if (string.IsNullOrWhiteSpace(currency) || token["currency"].ToStringInvariant().Equals(currency)) - { - ExchangeTransaction transaction = new ExchangeTransaction - { - PaymentId = token["id"].ToStringInvariant(), - Currency = token["currency"].ToStringInvariant(), - Address = token["address"].ToStringInvariant(), // Address Tag isn't returned - BlockchainTxId = token["hash"].ToStringInvariant(), // not sure about this - Amount = token["amount"].ConvertInvariant(), - Notes = token["type"].ToStringInvariant(), // since no notes are returned, we'll use this to show the transaction type - TxFee = token["fee"].ConvertInvariant(), - Timestamp = token["createdAt"].ToDateTimeInvariant() - }; - - string status = token["status"].ToStringInvariant(); - if (status.Equals("pending")) transaction.Status = TransactionStatus.Processing; - else if (status.Equals("success")) transaction.Status = TransactionStatus.Complete; - else if (status.Equals("failed")) transaction.Status = TransactionStatus.Failure; - else transaction.Status = TransactionStatus.Unknown; - - transactions.Add(transaction); - } - } - } - return transactions; - } - - protected override async Task OnWithdrawAsync(ExchangeWithdrawalRequest withdrawalRequest) - { - ExchangeWithdrawalResponse withdraw = new ExchangeWithdrawalResponse() { Success = false }; - var payload = await GetNoncePayloadAsync(); - payload["amount"] = withdrawalRequest.Amount; - payload["currency_code"] = withdrawalRequest.Currency; - payload["address"] = withdrawalRequest.Address; - if (!string.IsNullOrEmpty(withdrawalRequest.AddressTag)) payload["paymentId"] = withdrawalRequest.AddressTag; - //{ "id": "d2ce578f-647d-4fa0-b1aa-4a27e5ee597b"} that's all folks! - JToken token = await MakeJsonRequestAsync("/payment/payout", null, payload, "POST"); - if (token != null && token["id"] != null) - { - withdraw.Success = true; - withdraw.Id = token["id"].ToStringInvariant(); - } - return withdraw; - } + JToken token = await MakeJsonRequestAsync( + "/order/" + orderId, + null, + await GetNoncePayloadAsync(), + "DELETE" + ); + } + + private void ParseAveragePriceAndFeesFromFills( + ExchangeOrderResult result, + JToken fillsToken + ) + { + decimal totalCost = 0; + decimal totalQuantity = 0; + + if (fillsToken is JArray) + { + foreach (var fill in fillsToken) + { + result.Fees += fill["fee"].ConvertInvariant(); + + decimal price = fill["price"].ConvertInvariant(); + decimal quantity = fill["quantity"].ConvertInvariant(); + totalCost += price * quantity; + totalQuantity += quantity; + } + } + + result.AveragePrice = ( + totalQuantity == 0 ? null : (decimal?)(totalCost / totalQuantity) + ); + } + + protected override async Task OnGetDepositAddressAsync( + string currency, + bool forceRegenerate = false + ) + { + ExchangeDepositDetails deposit = new ExchangeDepositDetails() { Currency = currency }; + JToken token = await MakeJsonRequestAsync( + "/payment/address/" + currency, + null, + await GetNoncePayloadAsync() + ); + if (token != null) + { + deposit.Address = token["address"].ToStringInvariant(); + if (deposit.Address.StartsWith("bitcoincash:")) + { + deposit.Address = deposit.Address.Replace("bitcoincash:", string.Empty); // don't know why they do this for bitcoincash + } + deposit.AddressTag = token["wallet"].ToStringInvariant(); + } + return deposit; + } + + /// + /// This returns both Deposit and Withdawl history for the Bank and Trading Accounts. Currently returning everything and not filtering. + /// There is no support for retrieving by Symbol, so we'll filter that after reteiving all symbols + /// + /// + /// + protected override async Task> OnGetDepositHistoryAsync( + string? currency + ) + { + List transactions = new List(); + // [ {"id": "6a2fb54d-7466-490c-b3a6-95d8c882f7f7","index": 20400458,"currency": "ETH","amount": "38.616700000000000000000000","fee": "0.000880000000000000000000", "address": "0xfaEF4bE10dDF50B68c220c9ab19381e20B8EEB2B", "hash": "eece4c17994798939cea9f6a72ee12faa55a7ce44860cfb95c7ed71c89522fe8","status": "pending","type": "payout", "createdAt": "2017-05-18T18:05:36.957Z", "updatedAt": "2017-05-18T19:21:05.370Z" }, ... ] + JToken result = await MakeJsonRequestAsync( + "/account/transactions", + null, + await GetNoncePayloadAsync() + ); + if (result != null && result.HasValues) + { + foreach (JToken token in result) + { + if ( + string.IsNullOrWhiteSpace(currency) + || token["currency"].ToStringInvariant().Equals(currency) + ) + { + ExchangeTransaction transaction = new ExchangeTransaction + { + PaymentId = token["id"].ToStringInvariant(), + Currency = token["currency"].ToStringInvariant(), + Address = token["address"].ToStringInvariant(), // Address Tag isn't returned + BlockchainTxId = token["hash"].ToStringInvariant(), // not sure about this + Amount = token["amount"].ConvertInvariant(), + Notes = token["type"].ToStringInvariant(), // since no notes are returned, we'll use this to show the transaction type + TxFee = token["fee"].ConvertInvariant(), + Timestamp = token["createdAt"].ToDateTimeInvariant() + }; + + string status = token["status"].ToStringInvariant(); + if (status.Equals("pending")) + transaction.Status = TransactionStatus.Processing; + else if (status.Equals("success")) + transaction.Status = TransactionStatus.Complete; + else if (status.Equals("failed")) + transaction.Status = TransactionStatus.Failure; + else + transaction.Status = TransactionStatus.Unknown; + + transactions.Add(transaction); + } + } + } + return transactions; + } + + protected override async Task OnWithdrawAsync( + ExchangeWithdrawalRequest withdrawalRequest + ) + { + ExchangeWithdrawalResponse withdraw = new ExchangeWithdrawalResponse() + { + Success = false + }; + var payload = await GetNoncePayloadAsync(); + payload["amount"] = withdrawalRequest.Amount; + payload["currency_code"] = withdrawalRequest.Currency; + payload["address"] = withdrawalRequest.Address; + if (!string.IsNullOrEmpty(withdrawalRequest.AddressTag)) + payload["paymentId"] = withdrawalRequest.AddressTag; + //{ "id": "d2ce578f-647d-4fa0-b1aa-4a27e5ee597b"} that's all folks! + JToken token = await MakeJsonRequestAsync( + "/payment/payout", + null, + payload, + "POST" + ); + if (token != null && token["id"] != null) + { + withdraw.Success = true; + withdraw.Id = token["id"].ToStringInvariant(); + } + return withdraw; + } #endregion @@ -464,17 +635,22 @@ protected override async Task OnWithdrawAsync(Exchan // working on it. Hitbtc has extensive support for sockets, including trading - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) + protected override async Task OnGetTradesWebSocketAsync( + Func, Task> callback, + params string[] marketSymbols + ) { if (marketSymbols == null || marketSymbols.Length == 0) { marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); } - return await ConnectPublicWebSocketAsync(null, messageCallback: async (_socket, msg) => - { - JToken token = JToken.Parse(msg.ToStringFromUTF8()); - if (token["error"] != null) - { /* { + return await ConnectPublicWebSocketAsync( + null, + messageCallback: async (_socket, msg) => + { + JToken token = JToken.Parse(msg.ToStringFromUTF8()); + if (token["error"] != null) + { /* { "jsonrpc": "2.0", "error": { "code": 2001, @@ -483,12 +659,16 @@ protected override async Task OnGetTradesWebSocketAsync(Func OnGetTradesWebSocketAsync(Func(marketSymbol, trade)); - } - } - else if (token["method"].ToStringInvariant() == "updateTrades") - { /* { + token = token["params"]; + string marketSymbol = token["symbol"].ToStringInvariant(); + foreach (var tradesToken in token["data"]) + { + var trade = parseTrade(tradesToken); + trade.Flags |= ExchangeTradeFlags.IsFromSnapshot; + await callback( + new KeyValuePair(marketSymbol, trade) + ); + } + } + else if (token["method"].ToStringInvariant() == "updateTrades") + { /* { "jsonrpc": "2.0", "method": "updateTrades", "params": { @@ -544,16 +726,19 @@ protected override async Task OnGetTradesWebSocketAsync(Func(marketSymbol, trade)); - } - } - }, connectCallback: async (_socket) => - { /* { + token = token["params"]; + string marketSymbol = token["symbol"].ToStringInvariant(); + foreach (var tradesToken in token["data"]) + { + var trade = parseTrade(tradesToken); + await callback( + new KeyValuePair(marketSymbol, trade) + ); + } + } + }, + connectCallback: async (_socket) => + { /* { "method": "subscribeTrades", "params": { "symbol": "ETHBTC", @@ -561,22 +746,28 @@ protected override async Task OnGetTradesWebSocketAsync(Func token.ParseTrade(amountKey: "quantity", - priceKey: "price", typeKey: "side", timestampKey: "timestamp", - timestampType: TimestampType.Iso8601UTC, idKey: "id"); + foreach (var marketSymbol in marketSymbols) + { + await _socket.SendMessageAsync( + new + { + method = "subscribeTrades", + @params = new { symbol = marketSymbol, limit = 10, }, + id = CryptoUtility.UtcNow.Ticks // just need a unique number for client ID + } + ); + } + } + ); + ExchangeTrade parseTrade(JToken token) => + token.ParseTrade( + amountKey: "quantity", + priceKey: "price", + typeKey: "side", + timestampKey: "timestamp", + timestampType: TimestampType.Iso8601UTC, + idKey: "id" + ); } #endregion @@ -591,12 +782,16 @@ ExchangeTrade parseTrade(JToken token) => token.ParseTrade(amountKey: "quantity" public async Task> GetBankAmountsAsync() { Dictionary amounts = new Dictionary(); - JToken obj = await MakeJsonRequestAsync("/account/balance", null, await GetNoncePayloadAsync()); + JToken obj = await MakeJsonRequestAsync( + "/account/balance", + null, + await GetNoncePayloadAsync() + ); foreach (JToken token in obj) - { decimal amount = token["available"].ConvertInvariant(); - if (amount > 0m) amounts[token["currency"].ToStringInvariant()] = amount; + if (amount > 0m) + amounts[token["currency"].ToStringInvariant()] = amount; } return amounts; } @@ -607,8 +802,15 @@ public async Task AccountTransfer(string Symbol, decimal Amount, bool ToBa payload["type"] = ToBank ? "exchangeToBank" : "bankToExchange"; payload["currency"] = Symbol; payload["amount"] = Amount; - JToken obj = await MakeJsonRequestAsync("/account/transfer", null, payload, "POST"); - return (obj != null && obj.HasValues && !String.IsNullOrEmpty(obj["id"].ToStringInvariant())); + JToken obj = await MakeJsonRequestAsync( + "/account/transfer", + null, + payload, + "POST" + ); + return ( + obj != null && obj.HasValues && !String.IsNullOrEmpty(obj["id"].ToStringInvariant()) + ); } #endregion @@ -616,64 +818,100 @@ public async Task AccountTransfer(string Symbol, decimal Amount, bool ToBa #region Private Functions private async Task ParseTickerAsync(JToken token, string symbol) - { - // [ {"ask": "0.050043","bid": "0.050042","last": "0.050042","open": "0.047800","low": "0.047052","high": "0.051679","volume": "36456.720","volumeQuote": "1782.625000","timestamp": "2017-05-12T14:57:19.999Z","symbol": "ETHBTC"} ] - return await this.ParseTickerAsync(token, symbol, "ask", "bid", "last", "volume", "volumeQuote", "timestamp", TimestampType.Iso8601UTC); - } - - private ExchangeTrade ParseExchangeTrade(JToken token) - { - // [ { "id": 9533117, "price": "0.046001", "quantity": "0.220", "side": "sell", "timestamp": "2017-04-14T12:18:40.426Z" }, ... ] - return token.ParseTrade("quantity", "price", "side", "timestamp", TimestampType.Iso8601UTC, "id"); - } - - private ExchangeOrderResult ParseCompletedOrder(JToken token) - { - //[ { "id": 9535486, "clientOrderId": "f8dbaab336d44d5ba3ff578098a68454", "orderId": 816088377, "symbol": "ETHBTC", "side": "sell", "quantity": "0.061", "price": "0.045487", "fee": "0.000002775", "timestamp": "2017-05-17T12:32:57.848Z" }, - return new ExchangeOrderResult() - { - OrderId = token["orderId"].ToStringInvariant(), - MarketSymbol = token["symbol"].ToStringInvariant(), - IsBuy = token["side"].ToStringInvariant().Equals("buy"), - Amount = token["quantity"].ConvertInvariant(), - AmountFilled = token["quantity"].ConvertInvariant(), // these are closed, so I guess the filled quantity matches the order quantiity - Price = token["price"].ConvertInvariant(), - Fees = token["fee"].ConvertInvariant(), - OrderDate = token["timestamp"].ToDateTimeInvariant(), - Result = ExchangeAPIOrderResult.Filled - }; - } - - private ExchangeOrderResult ParseOpenOrder(JToken token) - { - // [ { "id": 840450210, "clientOrderId": "c1837634ef81472a9cd13c81e7b91401", "symbol": "ETHBTC", "side": "buy", "status": "partiallyFilled", "type": "limit", "timeInForce": "GTC", "quantity": "0.020", "price": "0.046001", "cumQuantity": "0.005", "createdAt": "2017-05-12T17:17:57.437Z", "updatedAt": "2017-05-12T17:18:08.610Z" }] - ExchangeOrderResult result = new ExchangeOrderResult() - { - OrderId = token["clientOrderId"].ToStringInvariant(), // here we're using ClientOrderId in order to get order details by open orders - MarketSymbol = token["symbol"].ToStringInvariant(), - IsBuy = token["side"].ToStringInvariant().Equals("buy"), - Amount = token["quantity"].ConvertInvariant(), - AmountFilled = token["cumQuantity"].ConvertInvariant(), - Price = token["price"].ConvertInvariant(), - OrderDate = token["createdAt"].ToDateTimeInvariant(), - Message = string.Format("OrderType: {0}, TimeInForce: {1}", token["type"].ToStringInvariant(), token["timeInForce"].ToStringInvariant()) // A bit arbitrary, but this will show the ordertype and timeinforce - }; - // new, suspended, partiallyFilled, filled, canceled, expired - string status = token["status"].ToStringInvariant(); - switch (status) + { + // [ {"ask": "0.050043","bid": "0.050042","last": "0.050042","open": "0.047800","low": "0.047052","high": "0.051679","volume": "36456.720","volumeQuote": "1782.625000","timestamp": "2017-05-12T14:57:19.999Z","symbol": "ETHBTC"} ] + return await this.ParseTickerAsync( + token, + symbol, + "ask", + "bid", + "last", + "volume", + "volumeQuote", + "timestamp", + TimestampType.Iso8601UTC + ); + } + + private ExchangeTrade ParseExchangeTrade(JToken token) + { + // [ { "id": 9533117, "price": "0.046001", "quantity": "0.220", "side": "sell", "timestamp": "2017-04-14T12:18:40.426Z" }, ... ] + return token.ParseTrade( + "quantity", + "price", + "side", + "timestamp", + TimestampType.Iso8601UTC, + "id" + ); + } + + private ExchangeOrderResult ParseCompletedOrder(JToken token) + { + //[ { "id": 9535486, "clientOrderId": "f8dbaab336d44d5ba3ff578098a68454", "orderId": 816088377, "symbol": "ETHBTC", "side": "sell", "quantity": "0.061", "price": "0.045487", "fee": "0.000002775", "timestamp": "2017-05-17T12:32:57.848Z" }, + return new ExchangeOrderResult() + { + OrderId = token["orderId"].ToStringInvariant(), + MarketSymbol = token["symbol"].ToStringInvariant(), + IsBuy = token["side"].ToStringInvariant().Equals("buy"), + Amount = token["quantity"].ConvertInvariant(), + AmountFilled = token["quantity"].ConvertInvariant(), // these are closed, so I guess the filled quantity matches the order quantiity + Price = token["price"].ConvertInvariant(), + Fees = token["fee"].ConvertInvariant(), + OrderDate = token["timestamp"].ToDateTimeInvariant(), + Result = ExchangeAPIOrderResult.Filled + }; + } + + private ExchangeOrderResult ParseOpenOrder(JToken token) + { + // [ { "id": 840450210, "clientOrderId": "c1837634ef81472a9cd13c81e7b91401", "symbol": "ETHBTC", "side": "buy", "status": "partiallyFilled", "type": "limit", "timeInForce": "GTC", "quantity": "0.020", "price": "0.046001", "cumQuantity": "0.005", "createdAt": "2017-05-12T17:17:57.437Z", "updatedAt": "2017-05-12T17:18:08.610Z" }] + ExchangeOrderResult result = new ExchangeOrderResult() + { + OrderId = token["clientOrderId"].ToStringInvariant(), // here we're using ClientOrderId in order to get order details by open orders + MarketSymbol = token["symbol"].ToStringInvariant(), + IsBuy = token["side"].ToStringInvariant().Equals("buy"), + Amount = token["quantity"].ConvertInvariant(), + AmountFilled = token["cumQuantity"].ConvertInvariant(), + Price = token["price"].ConvertInvariant(), + OrderDate = token["createdAt"].ToDateTimeInvariant(), + Message = string.Format( + "OrderType: {0}, TimeInForce: {1}", + token["type"].ToStringInvariant(), + token["timeInForce"].ToStringInvariant() + ) // A bit arbitrary, but this will show the ordertype and timeinforce + }; + // new, suspended, partiallyFilled, filled, canceled, expired + string status = token["status"].ToStringInvariant(); + switch (status) { // Possible values: new, suspended, partiallyFilled, filled, canceled, expired - case "filled": result.Result = ExchangeAPIOrderResult.Filled; break; - case "partiallyFilled": result.Result = ExchangeAPIOrderResult.FilledPartially; break; - case "canceled": result.Result = ExchangeAPIOrderResult.Canceled; break; - case "expired": result.Result = ExchangeAPIOrderResult.Expired; break; - case "new": result.Result = ExchangeAPIOrderResult.Open; break; - default: result.Result = ExchangeAPIOrderResult.Rejected; break; - } - return result; - } - - #endregion - } - - public partial class ExchangeName { public const string HitBTC = "HitBTC"; } + case "filled": + result.Result = ExchangeAPIOrderResult.Filled; + break; + case "partiallyFilled": + result.Result = ExchangeAPIOrderResult.FilledPartially; + break; + case "canceled": + result.Result = ExchangeAPIOrderResult.Canceled; + break; + case "expired": + result.Result = ExchangeAPIOrderResult.Expired; + break; + case "new": + result.Result = ExchangeAPIOrderResult.Open; + break; + default: + result.Result = ExchangeAPIOrderResult.Rejected; + break; + } + return result; + } + + #endregion + } + + public partial class ExchangeName + { + public const string HitBTC = "HitBTC"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/Huobi/ExchangeHuobiAPI.cs b/src/ExchangeSharp/API/Exchanges/Huobi/ExchangeHuobiAPI.cs index dd02e55b3..1387be28b 100644 --- a/src/ExchangeSharp/API/Exchanges/Huobi/ExchangeHuobiAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Huobi/ExchangeHuobiAPI.cs @@ -10,899 +10,1131 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; +using System.Linq; using System.Net; +using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; -using System.Linq; -using System.Security.Cryptography; +using Newtonsoft.Json.Linq; namespace ExchangeSharp { - public sealed partial class ExchangeHuobiAPI : ExchangeAPI - { - public override string BaseUrl { get; set; } = "https://api.huobipro.com"; + public sealed partial class ExchangeHuobiAPI : ExchangeAPI + { + public override string BaseUrl { get; set; } = "https://api.huobipro.com"; public string BaseUrlV1 { get; set; } = "https://api.huobipro.com/v1"; public string BaseUrlV2 { get; set; } = "https://api.huobipro.com/v2"; public override string BaseUrlWebSocket { get; set; } = "wss://api.huobipro.com/ws"; - public string PrivateUrlV1 { get; set; } = "https://api.huobipro.com/v1"; + public string PrivateUrlV1 { get; set; } = "https://api.huobipro.com/v1"; - public bool IsMargin { get; set; } - public string SubType { get; set; } + public bool IsMargin { get; set; } + public string SubType { get; set; } - private long webSocketId = 0; + private long webSocketId = 0; private ExchangeHuobiAPI() - { - RequestContentType = "application/x-www-form-urlencoded"; - NonceStyle = NonceStyle.UnixMilliseconds; - MarketSymbolSeparator = string.Empty; - MarketSymbolIsUppercase = false; - WebSocketOrderBookType = WebSocketOrderBookType.FullBookAlways; - } - - public override Task ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol) - { - if (marketSymbol.Length < 6) - { - throw new ArgumentException("Invalid market symbol " + marketSymbol); - } - else if (marketSymbol.Length == 6) - { - return ExchangeMarketSymbolToGlobalMarketSymbolWithSeparatorAsync(marketSymbol.Substring(0, 3) + GlobalMarketSymbolSeparator + marketSymbol.Substring(3, 3), GlobalMarketSymbolSeparator); - } - return ExchangeMarketSymbolToGlobalMarketSymbolWithSeparatorAsync(marketSymbol.Substring(3) + GlobalMarketSymbolSeparator + marketSymbol.Substring(0, 3), GlobalMarketSymbolSeparator); - } - - public override string PeriodSecondsToString(int seconds) - { - return CryptoUtility.SecondsToPeriodStringLong(seconds); - } - - #region ProcessRequest - - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) - { - if (CanMakeAuthenticatedRequest(payload)) - { - if (request.Method == "POST") - { - request.AddHeader("content-type", "application/json"); - payload.Remove("nonce"); - var msg = CryptoUtility.GetJsonForPayload(payload); - await CryptoUtility.WriteToRequestAsync(request, msg); - } - } - } - - protected override Uri ProcessRequestUrl(UriBuilder url, Dictionary payload, string method) - { - if (CanMakeAuthenticatedRequest(payload)) - { - // must sort case sensitive - var dict = new SortedDictionary(StringComparer.Ordinal) - { - ["Timestamp"] = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(payload["nonce"].ConvertInvariant()).ToString("s"), - ["AccessKeyId"] = PublicApiKey.ToUnsecureString(), - ["SignatureMethod"] = "HmacSHA256", - ["SignatureVersion"] = "2" - }; - - if (method == "GET") - { - foreach (var kv in payload) - { - dict.Add(kv.Key, kv.Value); - } - } - - string msg = CryptoUtility.GetFormForPayload(dict, false, false, false); - string toSign = $"{method}\n{url.Host}\n{url.Path}\n{msg}"; - - // calculate signature - var sign = CryptoUtility.SHA256SignBase64(toSign, PrivateApiKey.ToUnsecureBytesUTF8()).UrlEncode(); - - // append signature to end of message - msg += $"&Signature={sign}"; - - url.Query = msg; - } - return url.Uri; - } - - #endregion - - #region Public APIs - - protected override async Task> OnGetMarketSymbolsAsync() - { - var m = await GetMarketSymbolsMetadataAsync(); - return m.Select(x => x.MarketSymbol); - } - - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() - { - /*{ - "status":"ok", - "data":[ + { + RequestContentType = "application/x-www-form-urlencoded"; + NonceStyle = NonceStyle.UnixMilliseconds; + MarketSymbolSeparator = string.Empty; + MarketSymbolIsUppercase = false; + WebSocketOrderBookType = WebSocketOrderBookType.FullBookAlways; + } + + public override Task ExchangeMarketSymbolToGlobalMarketSymbolAsync( + string marketSymbol + ) + { + if (marketSymbol.Length < 6) + { + throw new ArgumentException("Invalid market symbol " + marketSymbol); + } + else if (marketSymbol.Length == 6) + { + return ExchangeMarketSymbolToGlobalMarketSymbolWithSeparatorAsync( + marketSymbol.Substring(0, 3) + + GlobalMarketSymbolSeparator + + marketSymbol.Substring(3, 3), + GlobalMarketSymbolSeparator + ); + } + return ExchangeMarketSymbolToGlobalMarketSymbolWithSeparatorAsync( + marketSymbol.Substring(3) + + GlobalMarketSymbolSeparator + + marketSymbol.Substring(0, 3), + GlobalMarketSymbolSeparator + ); + } + + public override string PeriodSecondsToString(int seconds) + { + return CryptoUtility.SecondsToPeriodStringLong(seconds); + } + + #region ProcessRequest + + protected override async Task ProcessRequestAsync( + IHttpWebRequest request, + Dictionary payload + ) + { + if (CanMakeAuthenticatedRequest(payload)) + { + if (request.Method == "POST") + { + request.AddHeader("content-type", "application/json"); + payload.Remove("nonce"); + var msg = CryptoUtility.GetJsonForPayload(payload); + await CryptoUtility.WriteToRequestAsync(request, msg); + } + } + } + + protected override Uri ProcessRequestUrl( + UriBuilder url, + Dictionary payload, + string method + ) + { + if (CanMakeAuthenticatedRequest(payload)) + { + // must sort case sensitive + var dict = new SortedDictionary(StringComparer.Ordinal) + { + ["Timestamp"] = CryptoUtility + .UnixTimeStampToDateTimeMilliseconds( + payload["nonce"].ConvertInvariant() + ) + .ToString("s"), + ["AccessKeyId"] = PublicApiKey.ToUnsecureString(), + ["SignatureMethod"] = "HmacSHA256", + ["SignatureVersion"] = "2" + }; + + if (method == "GET") + { + foreach (var kv in payload) { - "tags": "", - "state": "online", - "wr": "1.5", - "sc": "ethusdt", - "p": [ + dict.Add(kv.Key, kv.Value); + } + } + + string msg = CryptoUtility.GetFormForPayload(dict, false, false, false); + string toSign = $"{method}\n{url.Host}\n{url.Path}\n{msg}"; + + // calculate signature + var sign = CryptoUtility + .SHA256SignBase64(toSign, PrivateApiKey.ToUnsecureBytesUTF8()) + .UrlEncode(); + + // append signature to end of message + msg += $"&Signature={sign}"; + + url.Query = msg; + } + return url.Uri; + } + + #endregion + + #region Public APIs + + protected override async Task> OnGetMarketSymbolsAsync() + { + var m = await GetMarketSymbolsMetadataAsync(); + return m.Select(x => x.MarketSymbol); + } + + protected internal override async Task< + IEnumerable + > OnGetMarketSymbolsMetadataAsync() + { + /*{ + "status":"ok", + "data":[ { - "id": 9, - "name": "Grayscale", - "weight": 91 + "tags": "", + "state": "online", + "wr": "1.5", + "sc": "ethusdt", + "p": [ + { + "id": 9, + "name": "Grayscale", + "weight": 91 + } + ], + "bcdn": "ETH", + "qcdn": "USDT", + "elr": null, + "tpp": 2, + "tap": 4, + "fp": 8, + "smlr": null, + "flr": null, + "whe": false, + "cd": false, + "te": true, + "sp": "main", + "d": null, + "bc": "eth", + "qc": "usdt", + "toa": 1514779200000, + "ttp": 8, + "w": 999400000, + "lr": 5, + "dn": "ETH/USDT" } - ], - "bcdn": "ETH", - "qcdn": "USDT", - "elr": null, - "tpp": 2, - "tap": 4, - "fp": 8, - "smlr": null, - "flr": null, - "whe": false, - "cd": false, - "te": true, - "sp": "main", - "d": null, - "bc": "eth", - "qc": "usdt", - "toa": 1514779200000, - "ttp": 8, - "w": 999400000, - "lr": 5, - "dn": "ETH/USDT" - } - ], - "ts":"1641870869718", - "full":1 + ], + "ts":"1641870869718", + "full":1 }*/ List markets = new List(); - JToken allMarketSymbols = await MakeJsonRequestAsync("/settings/common/symbols", BaseUrlV2, null); - foreach (var marketSymbol in allMarketSymbols) - { - var baseCurrency = marketSymbol["bc"].ToStringLowerInvariant(); + JToken allMarketSymbols = await MakeJsonRequestAsync( + "/settings/common/symbols", + BaseUrlV2, + null + ); + foreach (var marketSymbol in allMarketSymbols) + { + var baseCurrency = marketSymbol["bc"].ToStringLowerInvariant(); var quoteCurrency = marketSymbol["qc"].ToStringLowerInvariant(); var symbolCode = marketSymbol["sc"].ToStringLowerInvariant(); var pricePrecision = marketSymbol["tpp"].ConvertInvariant(); - var priceStepSize = Math.Pow(10, -pricePrecision).ConvertInvariant(); - var amountPrecision = marketSymbol["tap"].ConvertInvariant(); - var quantityStepSize = Math.Pow(10, -amountPrecision).ConvertInvariant(); + var priceStepSize = Math.Pow(10, -pricePrecision).ConvertInvariant(); + var amountPrecision = marketSymbol["tap"].ConvertInvariant(); + var quantityStepSize = Math.Pow(10, -amountPrecision).ConvertInvariant(); var state = marketSymbol["state"].ToStringLowerInvariant(); - var market = new ExchangeMarket - { - BaseCurrency = baseCurrency, - QuoteCurrency = quoteCurrency, - MarketSymbol = symbolCode, - IsActive = state == "online", - PriceStepSize = priceStepSize, - QuantityStepSize = quantityStepSize, - MinPrice = priceStepSize, - MinTradeSize = quantityStepSize, - }; - markets.Add(market); - } - return markets; - } - - protected override async Task OnGetTickerAsync(string marketSymbol) - { - /* - {{ - "status": "ok", - "ch": "market.naseth.detail.merged", - "ts": 1525136582460, - "tick": { - "amount": 1614089.3164448638, - "open": 0.014552, - "close": 0.013308, - "high": 0.015145, - "id": 6442118070, - "count": 74643, - "low": 0.013297, - "version": 6442118070, - "ask": [ - 0.013324, - 0.0016 - ], - "vol": 22839.223396720725, - "bid": [ - 0.013297, - 3192.2322 - ] - } - }} - */ - JToken ticker = await MakeJsonRequestAsync("/market/detail/merged?symbol=" + marketSymbol); - return await this.ParseTickerAsync(ticker["tick"], marketSymbol, "ask", "bid", "close", "amount", "vol", "ts", TimestampType.UnixMillisecondsDouble, idKey: "id"); - } - - protected async override Task>> OnGetTickersAsync() - { - List> tickers = new List>(); - string symbol; - JToken obj = await MakeJsonRequestAsync("/market/tickers", BaseUrl, null); - Dictionary markets = await this.GetExchangeMarketDictionaryFromCacheAsync(); - foreach (JToken child in obj) - { - symbol = child["symbol"].ToStringInvariant(); - if (markets.ContainsKey(symbol)) - { - tickers.Add(new KeyValuePair(symbol, await this.ParseTickerAsync(child, symbol, null, null, "close", "amount", "vol"))); - } - } - - return tickers; - } - - protected override async Task OnGetTradesWebSocketAsync(Func, Task> callback, params string[] marketSymbols) - { - return await ConnectPublicWebSocketAsync(string.Empty, async (_socket, msg) => - { - /* + var market = new ExchangeMarket + { + BaseCurrency = baseCurrency, + QuoteCurrency = quoteCurrency, + MarketSymbol = symbolCode, + IsActive = state == "online", + PriceStepSize = priceStepSize, + QuantityStepSize = quantityStepSize, + MinPrice = priceStepSize, + MinTradeSize = quantityStepSize, + }; + markets.Add(market); + } + return markets; + } + + protected override async Task OnGetTickerAsync(string marketSymbol) + { + /* + {{ + "status": "ok", + "ch": "market.naseth.detail.merged", + "ts": 1525136582460, + "tick": { + "amount": 1614089.3164448638, + "open": 0.014552, + "close": 0.013308, + "high": 0.015145, + "id": 6442118070, + "count": 74643, + "low": 0.013297, + "version": 6442118070, + "ask": [ + 0.013324, + 0.0016 + ], + "vol": 22839.223396720725, + "bid": [ + 0.013297, + 3192.2322 + ] + } + }} + */ + JToken ticker = await MakeJsonRequestAsync( + "/market/detail/merged?symbol=" + marketSymbol + ); + return await this.ParseTickerAsync( + ticker["tick"], + marketSymbol, + "ask", + "bid", + "close", + "amount", + "vol", + "ts", + TimestampType.UnixMillisecondsDouble, + idKey: "id" + ); + } + + protected override async Task< + IEnumerable> + > OnGetTickersAsync() + { + List> tickers = + new List>(); + string symbol; + JToken obj = await MakeJsonRequestAsync("/market/tickers", BaseUrl, null); + Dictionary markets = + await this.GetExchangeMarketDictionaryFromCacheAsync(); + foreach (JToken child in obj) + { + symbol = child["symbol"].ToStringInvariant(); + if (markets.ContainsKey(symbol)) + { + tickers.Add( + new KeyValuePair( + symbol, + await this.ParseTickerAsync( + child, + symbol, + null, + null, + "close", + "amount", + "vol" + ) + ) + ); + } + } + + return tickers; + } + + protected override async Task OnGetTradesWebSocketAsync( + Func, Task> callback, + params string[] marketSymbols + ) + { + return await ConnectPublicWebSocketAsync( + string.Empty, + async (_socket, msg) => + { + /* {"id":"id1","status":"ok","subbed":"market.btcusdt.trade.detail","ts":1527574853489} { - "ch":"market.btcusdt.trade.detail", - "ts":1630994963175, - "tick":{ - "id":137005445109, - "ts":1630994963173, - "data":[ - { - "id":137005445109359286410323766, - "ts":1630994963173, - "tradeId":102523573486, - "amount":0.006754, - "price":52648.62, - "direction":"buy" - } - ] - } +"ch":"market.btcusdt.trade.detail", +"ts":1630994963175, +"tick":{ + "id":137005445109, + "ts":1630994963173, + "data":[ + { + "id":137005445109359286410323766, + "ts":1630994963173, + "tradeId":102523573486, + "amount":0.006754, + "price":52648.62, + "direction":"buy" + } + ] +} } - */ - var str = msg.ToStringFromUTF8Gzip(); - JToken token = JToken.Parse(str); - - if (token["status"] != null) - { - if (token["status"].ToStringLowerInvariant() == "error") - Logger.Error($"Error in {this.GetType()}.{nameof(OnGetTradesWebSocketAsync)}: {token.ToStringInvariant()}"); - return; - } - else if (token["ping"] != null) - { - await _socket.SendMessageAsync(str.Replace("ping", "pong")); - return; - } - - var ch = token["ch"].ToStringInvariant(); - var sArray = ch.Split('.'); - var marketSymbol = sArray[1]; - - var tick = token["tick"]; - - var data = tick["data"]; - var trades = ParseTradesWebSocket(data); - foreach (var trade in trades) - { - await callback(new KeyValuePair(marketSymbol, trade)); - } - }, async (_socket) => - { - if (marketSymbols == null || marketSymbols.Length == 0) - { - marketSymbols = (await GetMarketSymbolsMetadataAsync()).Where(s => s.IsActive.Value).Select(s => s.MarketSymbol).ToArray(); - } - foreach (string marketSymbol in marketSymbols) - { - long id = System.Threading.Interlocked.Increment(ref webSocketId); - string channel = $"market.{marketSymbol}.trade.detail"; - await _socket.SendMessageAsync(new { sub = channel, id = "id" + id.ToStringInvariant() }); - } - }); - } - - protected override async Task OnGetDeltaOrderBookWebSocketAsync(Action callback, int maxCount = 20, params string[] marketSymbols) - { - return await ConnectPublicWebSocketAsync(string.Empty, async (_socket, msg) => - { - /* + */ + var str = msg.ToStringFromUTF8Gzip(); + JToken token = JToken.Parse(str); + + if (token["status"] != null) + { + if (token["status"].ToStringLowerInvariant() == "error") + Logger.Error( + $"Error in {this.GetType()}.{nameof(OnGetTradesWebSocketAsync)}: {token.ToStringInvariant()}" + ); + return; + } + else if (token["ping"] != null) + { + await _socket.SendMessageAsync(str.Replace("ping", "pong")); + return; + } + + var ch = token["ch"].ToStringInvariant(); + var sArray = ch.Split('.'); + var marketSymbol = sArray[1]; + + var tick = token["tick"]; + + var data = tick["data"]; + var trades = ParseTradesWebSocket(data); + foreach (var trade in trades) + { + await callback( + new KeyValuePair(marketSymbol, trade) + ); + } + }, + async (_socket) => + { + if (marketSymbols == null || marketSymbols.Length == 0) + { + marketSymbols = (await GetMarketSymbolsMetadataAsync()) + .Where(s => s.IsActive.Value) + .Select(s => s.MarketSymbol) + .ToArray(); + } + foreach (string marketSymbol in marketSymbols) + { + long id = System.Threading.Interlocked.Increment(ref webSocketId); + string channel = $"market.{marketSymbol}.trade.detail"; + await _socket.SendMessageAsync( + new { sub = channel, id = "id" + id.ToStringInvariant() } + ); + } + } + ); + } + + protected override async Task OnGetDeltaOrderBookWebSocketAsync( + Action callback, + int maxCount = 20, + params string[] marketSymbols + ) + { + return await ConnectPublicWebSocketAsync( + string.Empty, + async (_socket, msg) => + { + /* {{ - "id": "id1", - "status": "ok", - "subbed": "market.btcusdt.depth.step0", - "ts": 1526749164133 +"id": "id1", +"status": "ok", +"subbed": "market.btcusdt.depth.step0", +"ts": 1526749164133 }} {{ - "ch": "market.btcusdt.depth.step0", - "ts": 1526749254037, - "tick": { - "bids": [ - [ - 8268.3, - 0.101 - ], - [ - 8268.29, - 0.8248 - ], - - ], - "asks": [ - [ - 8275.07, - 0.1961 - ], - - [ - 8337.1, - 0.5803 - ] - ], - "ts": 1526749254016, - "version": 7664175145 - } +"ch": "market.btcusdt.depth.step0", +"ts": 1526749254037, +"tick": { +"bids": [ + [ + 8268.3, + 0.101 + ], + [ + 8268.29, + 0.8248 + ], + +], +"asks": [ + [ + 8275.07, + 0.1961 + ], + + [ + 8337.1, + 0.5803 + ] +], +"ts": 1526749254016, +"version": 7664175145 +} }} - */ - var str = msg.ToStringFromUTF8Gzip(); - JToken token = JToken.Parse(str); - - if (token["status"] != null) - { - return; - } - else if (token["ping"] != null) - { - await _socket.SendMessageAsync(str.Replace("ping", "pong")); - return; - } - var ch = token["ch"].ToStringInvariant(); - var sArray = ch.Split('.'); - var marketSymbol = sArray[1].ToStringInvariant(); - ExchangeOrderBook book = token["tick"].ParseOrderBookFromJTokenArrays(); - book.MarketSymbol = marketSymbol; - callback(book); - }, async (_socket) => - { - if (marketSymbols == null || marketSymbols.Length == 0) - { - marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); - } - foreach (string symbol in marketSymbols) - { - long id = System.Threading.Interlocked.Increment(ref webSocketId); - var normalizedSymbol = NormalizeMarketSymbol(symbol); - string channel = $"market.{normalizedSymbol}.depth.step0"; - await _socket.SendMessageAsync(new { sub = channel, id = "id" + id.ToStringInvariant() }); - } - }); - } - - protected override async Task> OnGetCurrenciesAsync() - { - var currencies = new Dictionary(StringComparer.OrdinalIgnoreCase); - JToken array = await MakeJsonRequestAsync("/v1/hadax/common/currencys"); - - foreach (JToken token in array) - { - bool enabled = true; - var coin = new ExchangeCurrency - { - BaseAddress = null, - CoinType = null, - FullName = null, - DepositEnabled = enabled, - WithdrawalEnabled = enabled, - MinConfirmations = 0, - Name = token.ToStringInvariant(), - Notes = null, - TxFee = 0, - }; - - currencies[coin.Name] = coin; - } - - return currencies; - } - - protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) - { - /* - { - "status": "ok", - "ch": "market.btcusdt.depth.step0", - "ts": 1489472598812, - "tick": { - "id": 1489464585407, - "ts": 1489464585407, - "bids": [ - [7964, 0.0678], // [price, amount] - [7963, 0.9162], - [7961, 0.1], - [7960, 12.8898], - [7958, 1.2], - [7955, 2.1009], - [7954, 0.4708], - [7953, 0.0564], - [7951, 2.8031], - [7950, 13.7785], - [7949, 0.125], - [7948, 4], - [7942, 0.4337], - [7940, 6.1612], - [7936, 0.02], - [7935, 1.3575], - [7933, 2.002], - [7932, 1.3449], - [7930, 10.2974], - [7929, 3.2226] - ], - "asks": [ - [7979, 0.0736], - [7980, 1.0292], - [7981, 5.5652], - [7986, 0.2416], - [7990, 1.9970], - [7995, 0.88], - */ - JToken obj = await MakeJsonRequestAsync("/market/depth?symbol=" + marketSymbol + "&type=step0", BaseUrl, null); - return obj["tick"].ParseOrderBookFromJTokenArrays(sequence: "ts"); - } - - protected override async Task> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) - { - /* - { - "status": "ok", - "ch": "market.btcusdt.kline.1day", - "ts": 1499223904680, - “data”: [ - { - "id": 1499184000, - "amount": 37593.0266, - "count": 0, - "open": 1935.2000, - "close": 1879.0000, - "low": 1856.0000, - "high": 1940.0000, - "vol": 71031537.97866500 - }, - */ - - List candles = new List(); - string url = "/market/history/kline?symbol=" + marketSymbol; - if (limit != null) - { - // default is 150, max: 2000 - url += "&size=" + (limit.Value.ToStringInvariant()); - } - string periodString = PeriodSecondsToString(periodSeconds); - url += "&period=" + periodString; - JToken allCandles = await MakeJsonRequestAsync(url, BaseUrl, null); - foreach (var token in allCandles) - { - candles.Add(this.ParseCandle(token, marketSymbol, periodSeconds, "open", "high", "low", "close", "id", TimestampType.UnixSeconds, null, "vol")); - } - - candles.Reverse(); - return candles; - } - - #endregion - - #region Private APIs - - private async Task> OnGetAccountsAsync() - { - /* - {[ - { - "id": 3274515, - "type": "spot", - "subtype": "", - "state": "working" - }, - { - "id": 4267855, - "type": "margin", - "subtype": "btcusdt", - "state": "working" - }, - { - "id": 3544747, - "type": "margin", - "subtype": "ethusdt", - "state": "working" - }, - { - "id": 3274640, - "type": "otc", - "subtype": "", - "state": "working" - } + */ + var str = msg.ToStringFromUTF8Gzip(); + JToken token = JToken.Parse(str); + + if (token["status"] != null) + { + return; + } + else if (token["ping"] != null) + { + await _socket.SendMessageAsync(str.Replace("ping", "pong")); + return; + } + var ch = token["ch"].ToStringInvariant(); + var sArray = ch.Split('.'); + var marketSymbol = sArray[1].ToStringInvariant(); + ExchangeOrderBook book = token["tick"].ParseOrderBookFromJTokenArrays(); + book.MarketSymbol = marketSymbol; + callback(book); + }, + async (_socket) => + { + if (marketSymbols == null || marketSymbols.Length == 0) + { + marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); + } + foreach (string symbol in marketSymbols) + { + long id = System.Threading.Interlocked.Increment(ref webSocketId); + var normalizedSymbol = NormalizeMarketSymbol(symbol); + string channel = $"market.{normalizedSymbol}.depth.step0"; + await _socket.SendMessageAsync( + new { sub = channel, id = "id" + id.ToStringInvariant() } + ); + } + } + ); + } + + protected override async Task< + IReadOnlyDictionary + > OnGetCurrenciesAsync() + { + var currencies = new Dictionary( + StringComparer.OrdinalIgnoreCase + ); + JToken array = await MakeJsonRequestAsync("/v1/hadax/common/currencys"); + + foreach (JToken token in array) + { + bool enabled = true; + var coin = new ExchangeCurrency + { + BaseAddress = null, + CoinType = null, + FullName = null, + DepositEnabled = enabled, + WithdrawalEnabled = enabled, + MinConfirmations = 0, + Name = token.ToStringInvariant(), + Notes = null, + TxFee = 0, + }; + + currencies[coin.Name] = coin; + } + + return currencies; + } + + protected override async Task OnGetOrderBookAsync( + string marketSymbol, + int maxCount = 100 + ) + { + /* + { +"status": "ok", +"ch": "market.btcusdt.depth.step0", +"ts": 1489472598812, +"tick": { +"id": 1489464585407, +"ts": 1489464585407, +"bids": [ +[7964, 0.0678], // [price, amount] +[7963, 0.9162], +[7961, 0.1], +[7960, 12.8898], +[7958, 1.2], +[7955, 2.1009], +[7954, 0.4708], +[7953, 0.0564], +[7951, 2.8031], +[7950, 13.7785], +[7949, 0.125], +[7948, 4], +[7942, 0.4337], +[7940, 6.1612], +[7936, 0.02], +[7935, 1.3575], +[7933, 2.002], +[7932, 1.3449], +[7930, 10.2974], +[7929, 3.2226] +], +"asks": [ +[7979, 0.0736], +[7980, 1.0292], +[7981, 5.5652], +[7986, 0.2416], +[7990, 1.9970], +[7995, 0.88], + */ + JToken obj = await MakeJsonRequestAsync( + "/market/depth?symbol=" + marketSymbol + "&type=step0", + BaseUrl, + null + ); + return obj["tick"].ParseOrderBookFromJTokenArrays(sequence: "ts"); + } + + protected override async Task> OnGetCandlesAsync( + string marketSymbol, + int periodSeconds, + DateTime? startDate = null, + DateTime? endDate = null, + int? limit = null + ) + { + /* + { + "status": "ok", + "ch": "market.btcusdt.kline.1day", + "ts": 1499223904680, + “data”: [ + { + "id": 1499184000, + "amount": 37593.0266, + "count": 0, + "open": 1935.2000, + "close": 1879.0000, + "low": 1856.0000, + "high": 1940.0000, + "vol": 71031537.97866500 + }, + */ + + List candles = new List(); + string url = "/market/history/kline?symbol=" + marketSymbol; + if (limit != null) + { + // default is 150, max: 2000 + url += "&size=" + (limit.Value.ToStringInvariant()); + } + string periodString = PeriodSecondsToString(periodSeconds); + url += "&period=" + periodString; + JToken allCandles = await MakeJsonRequestAsync(url, BaseUrl, null); + foreach (var token in allCandles) + { + candles.Add( + this.ParseCandle( + token, + marketSymbol, + periodSeconds, + "open", + "high", + "low", + "close", + "id", + TimestampType.UnixSeconds, + null, + "vol" + ) + ); + } + + candles.Reverse(); + return candles; + } + + #endregion + + #region Private APIs + + private async Task> OnGetAccountsAsync() + { + /* + {[ +{ +"id": 3274515, +"type": "spot", +"subtype": "", +"state": "working" +}, +{ +"id": 4267855, +"type": "margin", +"subtype": "btcusdt", +"state": "working" +}, +{ +"id": 3544747, +"type": "margin", +"subtype": "ethusdt", +"state": "working" +}, +{ +"id": 3274640, +"type": "otc", +"subtype": "", +"state": "working" +} ]} - */ - Dictionary accounts = new Dictionary(); - var payload = await GetNoncePayloadAsync(); - JToken data = await MakeJsonRequestAsync("/account/accounts", PrivateUrlV1, payload); - foreach (var acc in data) - { - string key = acc["type"].ToStringInvariant() + "_" + acc["subtype"].ToStringInvariant(); - accounts.Add(key, acc["id"].ToStringInvariant()); - } - return accounts; - } - - protected override async Task> OnGetAmountsAsync() - { - /* - - "status": "ok", - "data": { - "id": 3274515, - "type": "spot", - "state": "working", - "list": [ - { - "currency": "usdt", - "type": "trade", - "balance": "0.000045000000000000" - }, - { - "currency": "eth", - "type": "frozen", - "balance": "0.000000000000000000" - }, - { - "currency": "eth", - "type": "trade", - "balance": "0.044362165000000000" - }, - { - "currency": "eos", - "type": "trade", - "balance": "16.467000000000000000" - }, - */ - var account_id = await GetAccountID(); - Dictionary amounts = new Dictionary(); - var payload = await GetNoncePayloadAsync(); - JToken token = await MakeJsonRequestAsync($"/account/accounts/{account_id}/balance", PrivateUrlV1, payload); - var list = token["list"]; - foreach (var item in list) - { - var balance = item["balance"].ConvertInvariant(); - if (balance == 0m) - continue; - - var currency = item["currency"].ToStringInvariant(); - - if (amounts.ContainsKey(currency)) - { - amounts[currency] += balance; - } - else - { - amounts[currency] = balance; - } - } - return amounts; - } - - protected override async Task> OnGetAmountsAvailableToTradeAsync() - { - var account_id = await GetAccountID(); - - Dictionary amounts = new Dictionary(); - var payload = await GetNoncePayloadAsync(); - JToken token = await MakeJsonRequestAsync($"/account/accounts/{account_id}/balance", PrivateUrlV1, payload); - var list = token["list"]; - foreach (var item in list) - { - var balance = item["balance"].ConvertInvariant(); - if (balance == 0m) - continue; - var type = item["type"].ToStringInvariant(); - if (type != "trade") - continue; - - var currency = item["currency"].ToStringInvariant(); - - if (amounts.ContainsKey(currency)) - { - amounts[currency] += balance; - } - else - { - amounts[currency] = balance; - } - } - return amounts; - } - - protected override async Task OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) - { - /* - {{ - "status": "ok", - "data": { - "id": 3908501445, - "symbol": "naseth", - "account-id": 3274515, - "amount": "0.050000000000000000", - "price": "0.000001000000000000", - "created-at": 1525100546601, - "type": "buy-limit", - "field-amount": "0.0", - "field-cash-amount": "0.0", - "field-fees": "0.0", - "finished-at": 1525100816771, - "source": "api", - "state": "canceled", - "canceled-at": 1525100816399 - } - }} - */ - var payload = await GetNoncePayloadAsync(); +*/ + Dictionary accounts = new Dictionary(); + var payload = await GetNoncePayloadAsync(); + JToken data = await MakeJsonRequestAsync( + "/account/accounts", + PrivateUrlV1, + payload + ); + foreach (var acc in data) + { + string key = + acc["type"].ToStringInvariant() + "_" + acc["subtype"].ToStringInvariant(); + accounts.Add(key, acc["id"].ToStringInvariant()); + } + return accounts; + } + + protected override async Task> OnGetAmountsAsync() + { + /* + +"status": "ok", +"data": { +"id": 3274515, +"type": "spot", +"state": "working", +"list": [ +{ + "currency": "usdt", + "type": "trade", + "balance": "0.000045000000000000" +}, +{ + "currency": "eth", + "type": "frozen", + "balance": "0.000000000000000000" +}, +{ + "currency": "eth", + "type": "trade", + "balance": "0.044362165000000000" +}, +{ + "currency": "eos", + "type": "trade", + "balance": "16.467000000000000000" +}, + */ + var account_id = await GetAccountID(); + Dictionary amounts = new Dictionary(); + var payload = await GetNoncePayloadAsync(); + JToken token = await MakeJsonRequestAsync( + $"/account/accounts/{account_id}/balance", + PrivateUrlV1, + payload + ); + var list = token["list"]; + foreach (var item in list) + { + var balance = item["balance"].ConvertInvariant(); + if (balance == 0m) + continue; + + var currency = item["currency"].ToStringInvariant(); + + if (amounts.ContainsKey(currency)) + { + amounts[currency] += balance; + } + else + { + amounts[currency] = balance; + } + } + return amounts; + } + + protected override async Task< + Dictionary + > OnGetAmountsAvailableToTradeAsync() + { + var account_id = await GetAccountID(); + + Dictionary amounts = new Dictionary(); + var payload = await GetNoncePayloadAsync(); + JToken token = await MakeJsonRequestAsync( + $"/account/accounts/{account_id}/balance", + PrivateUrlV1, + payload + ); + var list = token["list"]; + foreach (var item in list) + { + var balance = item["balance"].ConvertInvariant(); + if (balance == 0m) + continue; + var type = item["type"].ToStringInvariant(); + if (type != "trade") + continue; + + var currency = item["currency"].ToStringInvariant(); + + if (amounts.ContainsKey(currency)) + { + amounts[currency] += balance; + } + else + { + amounts[currency] = balance; + } + } + return amounts; + } + + protected override async Task OnGetOrderDetailsAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) + { + /* + {{ + "status": "ok", + "data": { + "id": 3908501445, + "symbol": "naseth", + "account-id": 3274515, + "amount": "0.050000000000000000", + "price": "0.000001000000000000", + "created-at": 1525100546601, + "type": "buy-limit", + "field-amount": "0.0", + "field-cash-amount": "0.0", + "field-fees": "0.0", + "finished-at": 1525100816771, + "source": "api", + "state": "canceled", + "canceled-at": 1525100816399 + } + }} + */ + var payload = await GetNoncePayloadAsync(); JToken data; if (isClientOrderId) { payload.Add("clientOrderId", orderId); - data = await MakeJsonRequestAsync($"/order/orders/getClientOrder", PrivateUrlV1, payload); + data = await MakeJsonRequestAsync( + $"/order/orders/getClientOrder", + PrivateUrlV1, + payload + ); } - else data = await MakeJsonRequestAsync($"/order/orders/{orderId}", PrivateUrlV1, payload); + else + data = await MakeJsonRequestAsync( + $"/order/orders/{orderId}", + PrivateUrlV1, + payload + ); return ParseOrder(data); - } + } - protected override async Task> OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) - { - if (marketSymbol == null) { throw new APIException("symbol cannot be null"); } + protected override async Task< + IEnumerable + > OnGetCompletedOrderDetailsAsync(string marketSymbol = null, DateTime? afterDate = null) + { + if (marketSymbol == null) + { + throw new APIException("symbol cannot be null"); + } - List orders = new List(); - var payload = await GetNoncePayloadAsync(); - payload.Add("symbol", marketSymbol); - payload.Add("states", "partial-canceled,filled,canceled"); - if (afterDate != null) + List orders = new List(); + var payload = await GetNoncePayloadAsync(); + payload.Add("symbol", marketSymbol); + payload.Add("states", "partial-canceled,filled,canceled"); + if (afterDate != null) { // This endpoint returns the detail of one order by specified client order id (within 8 hours). - // The order created via API will no longer be queryable after being cancelled for more than 2 hours. - // It is suggested to cancel orders via GET /v1/order/orders/{order-id}, which is faster and more stable. + // The order created via API will no longer be queryable after being cancelled for more than 2 hours. + // It is suggested to cancel orders via GET /v1/order/orders/{order-id}, which is faster and more stable. payload.Add("start-date", afterDate.Value.ToString("yyyy-MM-dd")); - } - JToken data = await MakeJsonRequestAsync("/order/orders", PrivateUrlV1, payload); - foreach (var prop in data) - { - orders.Add(ParseOrder(prop)); - } - return orders; - } - - protected override async Task> OnGetOpenOrderDetailsAsync(string marketSymbol = null) - { - if (marketSymbol == null) { throw new APIException("symbol cannot be null"); } - - List orders = new List(); - var payload = await GetNoncePayloadAsync(); - payload.Add("symbol", marketSymbol); - payload.Add("states", "pre-submitted,submitting,submitted,partial-filled"); - JToken data = await MakeJsonRequestAsync("/order/orders", PrivateUrlV1, payload); - foreach (var prop in data) - { - orders.Add(ParseOrder(prop)); - } - return orders; - } - - protected override async Task OnPlaceOrderAsync(ExchangeOrderRequest order) - { - var account_id = await GetAccountID(order.IsMargin, order.MarketSymbol); - - var payload = await GetNoncePayloadAsync(); - payload.Add("account-id", account_id); - payload.Add("symbol", order.MarketSymbol); - payload.Add("type", order.IsBuy ? "buy" : "sell"); - payload.Add("source", order.IsMargin ? "margin-api" : "api"); - - decimal outputQuantity = await ClampOrderQuantity(order.MarketSymbol, order.Amount); - decimal outputPrice = await ClampOrderPrice(order.MarketSymbol, order.Price.Value); - - payload["amount"] = outputQuantity.ToStringInvariant(); - - if (order.OrderType == OrderType.Market) - { - payload["type"] += "-market"; - } - else - { - payload["type"] += "-limit"; - if (order.Price == null) throw new ArgumentNullException(nameof(order.Price)); + } + JToken data = await MakeJsonRequestAsync( + "/order/orders", + PrivateUrlV1, + payload + ); + foreach (var prop in data) + { + orders.Add(ParseOrder(prop)); + } + return orders; + } + + protected override async Task> OnGetOpenOrderDetailsAsync( + string marketSymbol = null + ) + { + if (marketSymbol == null) + { + throw new APIException("symbol cannot be null"); + } + + List orders = new List(); + var payload = await GetNoncePayloadAsync(); + payload.Add("symbol", marketSymbol); + payload.Add("states", "pre-submitted,submitting,submitted,partial-filled"); + JToken data = await MakeJsonRequestAsync( + "/order/orders", + PrivateUrlV1, + payload + ); + foreach (var prop in data) + { + orders.Add(ParseOrder(prop)); + } + return orders; + } + + protected override async Task OnPlaceOrderAsync( + ExchangeOrderRequest order + ) + { + var account_id = await GetAccountID(order.IsMargin, order.MarketSymbol); + + var payload = await GetNoncePayloadAsync(); + payload.Add("account-id", account_id); + payload.Add("symbol", order.MarketSymbol); + payload.Add("type", order.IsBuy ? "buy" : "sell"); + payload.Add("source", order.IsMargin ? "margin-api" : "api"); + + decimal outputQuantity = await ClampOrderQuantity(order.MarketSymbol, order.Amount); + decimal outputPrice = await ClampOrderPrice(order.MarketSymbol, order.Price.Value); + + payload["amount"] = outputQuantity.ToStringInvariant(); + + if (order.OrderType == OrderType.Market) + { + payload["type"] += "-market"; + } + else + { + payload["type"] += "-limit"; + if (order.Price == null) + throw new ArgumentNullException(nameof(order.Price)); payload["price"] = outputPrice.ToStringInvariant(); - } + } - if (order.IsPostOnly == true) payload["timeInForce"] += "boc"; // timeInForce enum values: gtc - good till cancel,boc - book or cancel (also called as post only, or book only), ioc - immediate or cancel, fok - fill or kill + if (order.IsPostOnly == true) + payload["timeInForce"] += "boc"; // timeInForce enum values: gtc - good till cancel,boc - book or cancel (also called as post only, or book only), ioc - immediate or cancel, fok - fill or kill order.ExtraParameters.CopyTo(payload); - JToken obj = await MakeJsonRequestAsync("/order/orders/place", PrivateUrlV1, payload, "POST"); - order.Amount = outputQuantity; - order.Price = outputPrice; - return ParsePlaceOrder(obj, order); - } + JToken obj = await MakeJsonRequestAsync( + "/order/orders/place", + PrivateUrlV1, + payload, + "POST" + ); + order.Amount = outputQuantity; + order.Price = outputPrice; + return ParsePlaceOrder(obj, order); + } - protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false) + protected override async Task OnCancelOrderAsync( + string orderId, + string marketSymbol = null, + bool isClientOrderId = false + ) { - var payload = await GetNoncePayloadAsync(); + var payload = await GetNoncePayloadAsync(); JToken data; if (isClientOrderId) { payload.Add("clientOrderId", orderId); - data = await MakeJsonRequestAsync($"/order/orders/submitCancelClientOrder", PrivateUrlV1, payload, "POST"); + data = await MakeJsonRequestAsync( + $"/order/orders/submitCancelClientOrder", + PrivateUrlV1, + payload, + "POST" + ); } - else data = await MakeJsonRequestAsync($"/order/orders/{orderId}/submitcancel", PrivateUrlV1, payload, "POST"); - } - - protected override async Task> OnGetDepositHistoryAsync(string currency) - { - var payload = await GetNoncePayloadAsync(); - currency = currency.ToLowerInvariant(); - payload["currency"] = currency; - payload["type"] = "deposit"; - payload["from"] = 5; - payload["size"] = 12; - - var deposits = await MakeJsonRequestAsync($"/query/deposit-withdraw", PrivateUrlV1, payload); - var result = deposits - .Where(d => d["type"].ToStringInvariant() == "deposit") - .Select(d => new ExchangeTransaction - { - Address = d["address"].ToStringInvariant(), - AddressTag = d["address-tag"].ToStringInvariant(), - Amount = d["amount"].ConvertInvariant(), - BlockchainTxId = d["tx-hash"].ToStringInvariant(), - Currency = d["currency"].ToStringInvariant(), - PaymentId = d["id"].ConvertInvariant().ToString(), - Status = ToDepositStatus(d["state"].ToStringInvariant()), - Timestamp = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(d["created-at"].ConvertInvariant()), - TxFee = d["fee"].ConvertInvariant() - }); - - return result; - } - private TransactionStatus ToDepositStatus(string status) - { - switch (status) - { - case "confirming": - return TransactionStatus.AwaitingApproval; - case "safe": - case "confirmed": - return TransactionStatus.Complete; - case "orphan": - return TransactionStatus.Failure; - case "unknown": - return TransactionStatus.Unknown; - default: - throw new InvalidOperationException($"Unknown status: {status}"); - } - } - - protected override Task OnGetDepositAddressAsync(string currency, bool forceRegenerate = false) - { - throw new NotImplementedException("Huobi does not provide a deposit API"); - - /* - var payload = await GetNoncePayloadAsync(); - payload.Add("need_new", forceRegenerate ? 1 : 0); - payload.Add("method", "GetDepositAddress"); - payload.Add("coinName", symbol); - payload["method"] = "POST"; - // "return":{"address": 1UHAnAWvxDB9XXETsi7z483zRRBmcUZxb3,"processed_amount": 1.00000000,"server_time": 1437146228 } - JToken token = await MakeJsonRequestAsync("/", PrivateUrlV1, payload, "POST"); - return new ExchangeDepositDetails - { - Address = token["address"].ToStringInvariant(), - Symbol = symbol - }; - */ - } - - protected override async Task OnWithdrawAsync(ExchangeWithdrawalRequest withdrawalRequest) - { - var payload = await GetNoncePayloadAsync(); - - payload["address"] = withdrawalRequest.Address; - payload["amount"] = withdrawalRequest.Amount; - payload["currency"] = withdrawalRequest.Currency; - if (withdrawalRequest.AddressTag != null) - payload["attr-tag"] = withdrawalRequest.AddressTag; - - JToken result = await MakeJsonRequestAsync("/dw/withdraw/api/create", PrivateUrlV1, payload, "POST"); - - return new ExchangeWithdrawalResponse - { - Id = result.Root["data"].ToStringInvariant(), - Message = result.Root["status"].ToStringInvariant() - }; - } - - protected override async Task> OnGetMarginAmountsAvailableToTradeAsync(bool includeZeroBalances) - { - Dictionary marginAmounts = new Dictionary(); - - JToken resultAccounts = await MakeJsonRequestAsync("/account/accounts", PrivateUrlV1, await GetNoncePayloadAsync()); - - // Take only first account? - JToken resultBalances = await MakeJsonRequestAsync($"/account/accounts/{resultAccounts.First["id"].ConvertInvariant()}/balance", PrivateUrlV1, await GetNoncePayloadAsync()); - - foreach (var balance in resultBalances["list"]) - { - if (balance["type"].ToStringInvariant() == "trade") // not frozen - marginAmounts.Add(balance["currency"].ToStringInvariant(), balance["balance"].ConvertInvariant()); - } - - return marginAmounts; - } - - #endregion - - #region Private Functions - - protected override JToken CheckJsonResponse(JToken result) - { - if (result == null || (result["status"] != null && result["status"].ToStringInvariant() != "ok")) - { - throw new APIException((result["err-msg"] != null ? result["err-msg"].ToStringInvariant() : "Unknown Error")); - } - return result["data"] ?? result; - } - - private ExchangeOrderResult ParsePlaceOrder(JToken token, ExchangeOrderRequest order) - { - /* - { - "status": "ok", - "data": "59378" - } - */ - ExchangeOrderResult result = new ExchangeOrderResult - { - Amount = order.Amount, - Price = order.Price, - IsBuy = order.IsBuy, - OrderId = token.ToStringInvariant(), - MarketSymbol = order.MarketSymbol - }; - result.AveragePrice = result.Price; - result.Result = ExchangeAPIOrderResult.Open; - - return result; - } - - private ExchangeAPIOrderResult ParseState(string state) - { - switch (state) - { + else + data = await MakeJsonRequestAsync( + $"/order/orders/{orderId}/submitcancel", + PrivateUrlV1, + payload, + "POST" + ); + } + + protected override async Task> OnGetDepositHistoryAsync( + string currency + ) + { + var payload = await GetNoncePayloadAsync(); + currency = currency.ToLowerInvariant(); + payload["currency"] = currency; + payload["type"] = "deposit"; + payload["from"] = 5; + payload["size"] = 12; + + var deposits = await MakeJsonRequestAsync( + $"/query/deposit-withdraw", + PrivateUrlV1, + payload + ); + var result = deposits + .Where(d => d["type"].ToStringInvariant() == "deposit") + .Select( + d => + new ExchangeTransaction + { + Address = d["address"].ToStringInvariant(), + AddressTag = d["address-tag"].ToStringInvariant(), + Amount = d["amount"].ConvertInvariant(), + BlockchainTxId = d["tx-hash"].ToStringInvariant(), + Currency = d["currency"].ToStringInvariant(), + PaymentId = d["id"].ConvertInvariant().ToString(), + Status = ToDepositStatus(d["state"].ToStringInvariant()), + Timestamp = CryptoUtility.UnixTimeStampToDateTimeMilliseconds( + d["created-at"].ConvertInvariant() + ), + TxFee = d["fee"].ConvertInvariant() + } + ); + + return result; + } + + private TransactionStatus ToDepositStatus(string status) + { + switch (status) + { + case "confirming": + return TransactionStatus.AwaitingApproval; + case "safe": + case "confirmed": + return TransactionStatus.Complete; + case "orphan": + return TransactionStatus.Failure; + case "unknown": + return TransactionStatus.Unknown; + default: + throw new InvalidOperationException($"Unknown status: {status}"); + } + } + + protected override Task OnGetDepositAddressAsync( + string currency, + bool forceRegenerate = false + ) + { + throw new NotImplementedException("Huobi does not provide a deposit API"); + + /* + var payload = await GetNoncePayloadAsync(); + payload.Add("need_new", forceRegenerate ? 1 : 0); + payload.Add("method", "GetDepositAddress"); + payload.Add("coinName", symbol); + payload["method"] = "POST"; + // "return":{"address": 1UHAnAWvxDB9XXETsi7z483zRRBmcUZxb3,"processed_amount": 1.00000000,"server_time": 1437146228 } + JToken token = await MakeJsonRequestAsync("/", PrivateUrlV1, payload, "POST"); + return new ExchangeDepositDetails + { + Address = token["address"].ToStringInvariant(), + Symbol = symbol + }; + */ + } + + protected override async Task OnWithdrawAsync( + ExchangeWithdrawalRequest withdrawalRequest + ) + { + var payload = await GetNoncePayloadAsync(); + + payload["address"] = withdrawalRequest.Address; + payload["amount"] = withdrawalRequest.Amount; + payload["currency"] = withdrawalRequest.Currency; + if (withdrawalRequest.AddressTag != null) + payload["attr-tag"] = withdrawalRequest.AddressTag; + + JToken result = await MakeJsonRequestAsync( + "/dw/withdraw/api/create", + PrivateUrlV1, + payload, + "POST" + ); + + return new ExchangeWithdrawalResponse + { + Id = result.Root["data"].ToStringInvariant(), + Message = result.Root["status"].ToStringInvariant() + }; + } + + protected override async Task< + Dictionary + > OnGetMarginAmountsAvailableToTradeAsync(bool includeZeroBalances) + { + Dictionary marginAmounts = new Dictionary(); + + JToken resultAccounts = await MakeJsonRequestAsync( + "/account/accounts", + PrivateUrlV1, + await GetNoncePayloadAsync() + ); + + // Take only first account? + JToken resultBalances = await MakeJsonRequestAsync( + $"/account/accounts/{resultAccounts.First["id"].ConvertInvariant()}/balance", + PrivateUrlV1, + await GetNoncePayloadAsync() + ); + + foreach (var balance in resultBalances["list"]) + { + if (balance["type"].ToStringInvariant() == "trade") // not frozen + marginAmounts.Add( + balance["currency"].ToStringInvariant(), + balance["balance"].ConvertInvariant() + ); + } + + return marginAmounts; + } + + #endregion + + #region Private Functions + + protected override JToken CheckJsonResponse(JToken result) + { + if ( + result == null + || (result["status"] != null && result["status"].ToStringInvariant() != "ok") + ) + { + throw new APIException( + ( + result["err-msg"] != null + ? result["err-msg"].ToStringInvariant() + : "Unknown Error" + ) + ); + } + return result["data"] ?? result; + } + + private ExchangeOrderResult ParsePlaceOrder(JToken token, ExchangeOrderRequest order) + { + /* + { + "status": "ok", + "data": "59378" + } + */ + ExchangeOrderResult result = new ExchangeOrderResult + { + Amount = order.Amount, + Price = order.Price, + IsBuy = order.IsBuy, + OrderId = token.ToStringInvariant(), + MarketSymbol = order.MarketSymbol + }; + result.AveragePrice = result.Price; + result.Result = ExchangeAPIOrderResult.Open; + + return result; + } + + private ExchangeAPIOrderResult ParseState(string state) + { + switch (state) + { case "created": case "pre-submitted": case "submitting": return ExchangeAPIOrderResult.PendingOpen; case "submitted": - return ExchangeAPIOrderResult.Open; - case "partial-filled": - return ExchangeAPIOrderResult.FilledPartially; - case "filled": - return ExchangeAPIOrderResult.Filled; - case "partial-canceled": + return ExchangeAPIOrderResult.Open; + case "partial-filled": + return ExchangeAPIOrderResult.FilledPartially; + case "filled": + return ExchangeAPIOrderResult.Filled; + case "partial-canceled": return ExchangeAPIOrderResult.FilledPartiallyAndCancelled; case "canceling": return ExchangeAPIOrderResult.PendingCancel; @@ -913,53 +1145,67 @@ private ExchangeAPIOrderResult ParseState(string state) } } - private ExchangeOrderResult ParseOrder(JToken token) - { - ExchangeOrderResult result = new ExchangeOrderResult() - { - OrderId = token["id"].ToStringInvariant(), - MarketSymbol = token["symbol"].ToStringInvariant(), - Amount = token["amount"].ConvertInvariant(), - AmountFilled = token["field-amount"].ConvertInvariant(), - Price = token["price"].ConvertInvariant(), - OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds(token["created-at"].ConvertInvariant()), - IsBuy = token["type"].ToStringInvariant().StartsWith("buy"), - Result = ParseState(token["state"].ToStringInvariant()), - }; - - if (result.Price == 0 && result.AmountFilled != 0m) - { - var amountCash = token["field-cash-amount"].ConvertInvariant(); - result.Price = amountCash / result.AmountFilled; - } - - return result; - } - - private IEnumerable ParseTradesWebSocket(JToken token) - { - var trades = new List(); - foreach (var t in token) - { - trades.Add(t.ParseTrade("amount", "price", "direction", "ts", TimestampType.UnixMilliseconds, "tradeId")); - } - - return trades; - } - - private async Task GetAccountID(bool isMargin = false, string subtype = "") - { - var accounts = await OnGetAccountsAsync(); - var key = "spot_"; - if (isMargin) - { - key = "margin_" + subtype; - } - var account_id = accounts[key]; - return account_id; - } - #endregion - } - - public partial class ExchangeName { public const string Huobi = "Huobi"; } + private ExchangeOrderResult ParseOrder(JToken token) + { + ExchangeOrderResult result = new ExchangeOrderResult() + { + OrderId = token["id"].ToStringInvariant(), + MarketSymbol = token["symbol"].ToStringInvariant(), + Amount = token["amount"].ConvertInvariant(), + AmountFilled = token["field-amount"].ConvertInvariant(), + Price = token["price"].ConvertInvariant(), + OrderDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds( + token["created-at"].ConvertInvariant() + ), + IsBuy = token["type"].ToStringInvariant().StartsWith("buy"), + Result = ParseState(token["state"].ToStringInvariant()), + }; + + if (result.Price == 0 && result.AmountFilled != 0m) + { + var amountCash = token["field-cash-amount"].ConvertInvariant(); + result.Price = amountCash / result.AmountFilled; + } + + return result; + } + + private IEnumerable ParseTradesWebSocket(JToken token) + { + var trades = new List(); + foreach (var t in token) + { + trades.Add( + t.ParseTrade( + "amount", + "price", + "direction", + "ts", + TimestampType.UnixMilliseconds, + "tradeId" + ) + ); + } + + return trades; + } + + private async Task GetAccountID(bool isMargin = false, string subtype = "") + { + var accounts = await OnGetAccountsAsync(); + var key = "spot_"; + if (isMargin) + { + key = "margin_" + subtype; + } + var account_id = accounts[key]; + return account_id; + } + #endregion + } + + public partial class ExchangeName + { + public const string Huobi = "Huobi"; + } } diff --git a/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs b/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs index ffbf223b5..09d0d9272 100644 --- a/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Kraken/ExchangeKrakenAPI.cs @@ -41,11 +41,16 @@ private ExchangeKrakenAPI() WebSocketOrderBookType = WebSocketOrderBookType.FullBookFirstThenDeltas; } - private IReadOnlyDictionary exchangeCurrencyToNormalizedCurrency = new Dictionary(); - private IReadOnlyDictionary normalizedCurrencyToExchangeCurrency = new Dictionary(); - private IReadOnlyDictionary exchangeSymbolToNormalizedSymbol = new Dictionary(); - private IReadOnlyDictionary normalizedSymbolToExchangeSymbol = new Dictionary(); - private IReadOnlyDictionary exchangeCurrenciesToMarketSymbol = new Dictionary(); + private IReadOnlyDictionary exchangeCurrencyToNormalizedCurrency = + new Dictionary(); + private IReadOnlyDictionary normalizedCurrencyToExchangeCurrency = + new Dictionary(); + private IReadOnlyDictionary exchangeSymbolToNormalizedSymbol = + new Dictionary(); + private IReadOnlyDictionary normalizedSymbolToExchangeSymbol = + new Dictionary(); + private IReadOnlyDictionary exchangeCurrenciesToMarketSymbol = + new Dictionary(); /// /// Populate dictionaries to deal with Kraken weirdness in currency and market names, will use cache if it exists @@ -53,93 +58,142 @@ private ExchangeKrakenAPI() /// Task private async Task PopulateLookupTables() { - await Cache.GetOrCreate(nameof(PopulateLookupTables), async () => - { - IReadOnlyDictionary currencies = await GetCurrenciesAsync(); - ExchangeMarket[] markets = (await GetMarketSymbolsMetadataAsync())?.ToArray(); - if (markets == null || markets.Length == 0) - { - return new CachedItem(); - } - - Dictionary exchangeCurrencyToNormalizedCurrencyNew = new Dictionary(StringComparer.OrdinalIgnoreCase); - Dictionary normalizedCurrencyToExchangeCurrencyNew = new Dictionary(StringComparer.OrdinalIgnoreCase); - Dictionary exchangeSymbolToNormalizedSymbolNew = new Dictionary(StringComparer.OrdinalIgnoreCase); - Dictionary normalizedSymbolToExchangeSymbolNew = new Dictionary(StringComparer.OrdinalIgnoreCase); - Dictionary exchangeCurrenciesToMarketSymbolNew = new Dictionary(StringComparer.OrdinalIgnoreCase); - - foreach (KeyValuePair kv in currencies) - { - string altName = kv.Value.AltName; - switch (altName.ToLowerInvariant()) + await Cache.GetOrCreate( + nameof(PopulateLookupTables), + async () => { - // wtf kraken... - case "xbt": - altName = "BTC"; - break; - - case "xdg": - altName = "DOGE"; - break; - } - exchangeCurrencyToNormalizedCurrencyNew[kv.Value.Name] = altName; - normalizedCurrencyToExchangeCurrencyNew[altName] = kv.Value.Name; - } + IReadOnlyDictionary currencies = + await GetCurrenciesAsync(); + ExchangeMarket[] markets = (await GetMarketSymbolsMetadataAsync())?.ToArray(); + if (markets == null || markets.Length == 0) + { + return new CachedItem(); + } - foreach (ExchangeMarket market in markets.Where(m => !m.MarketSymbol.Contains(".d"))) - { - string baseSymbol = market.BaseCurrency; - string quoteSymbol = market.QuoteCurrency; - string baseNorm = exchangeCurrencyToNormalizedCurrencyNew[market.BaseCurrency]; - string quoteNorm = exchangeCurrencyToNormalizedCurrencyNew[market.QuoteCurrency]; - string marketSymbolNorm = baseNorm + quoteNorm; - string marketSymbol = market.MarketSymbol; - exchangeSymbolToNormalizedSymbolNew[marketSymbol] = marketSymbolNorm; - normalizedSymbolToExchangeSymbolNew[marketSymbolNorm] = marketSymbol; - exchangeCurrenciesToMarketSymbolNew[baseSymbol + quoteSymbol] = marketSymbol; - exchangeCurrenciesToMarketSymbolNew[quoteSymbol + baseSymbol] = marketSymbol; - } + Dictionary exchangeCurrencyToNormalizedCurrencyNew = + new Dictionary(StringComparer.OrdinalIgnoreCase); + Dictionary normalizedCurrencyToExchangeCurrencyNew = + new Dictionary(StringComparer.OrdinalIgnoreCase); + Dictionary exchangeSymbolToNormalizedSymbolNew = new Dictionary< + string, + string + >(StringComparer.OrdinalIgnoreCase); + Dictionary normalizedSymbolToExchangeSymbolNew = new Dictionary< + string, + string + >(StringComparer.OrdinalIgnoreCase); + Dictionary exchangeCurrenciesToMarketSymbolNew = new Dictionary< + string, + string + >(StringComparer.OrdinalIgnoreCase); + + foreach (KeyValuePair kv in currencies) + { + string altName = kv.Value.AltName; + switch (altName.ToLowerInvariant()) + { + // wtf kraken... + case "xbt": + altName = "BTC"; + break; + + case "xdg": + altName = "DOGE"; + break; + } + exchangeCurrencyToNormalizedCurrencyNew[kv.Value.Name] = altName; + normalizedCurrencyToExchangeCurrencyNew[altName] = kv.Value.Name; + } - exchangeCurrencyToNormalizedCurrency = ExchangeGlobalCurrencyReplacements = exchangeCurrencyToNormalizedCurrencyNew; - normalizedCurrencyToExchangeCurrency = normalizedCurrencyToExchangeCurrencyNew; - exchangeSymbolToNormalizedSymbol = exchangeSymbolToNormalizedSymbolNew; - normalizedSymbolToExchangeSymbol = normalizedSymbolToExchangeSymbolNew; - exchangeCurrenciesToMarketSymbol = exchangeCurrenciesToMarketSymbolNew; + foreach ( + ExchangeMarket market in markets.Where(m => !m.MarketSymbol.Contains(".d")) + ) + { + string baseSymbol = market.BaseCurrency; + string quoteSymbol = market.QuoteCurrency; + string baseNorm = exchangeCurrencyToNormalizedCurrencyNew[ + market.BaseCurrency + ]; + string quoteNorm = exchangeCurrencyToNormalizedCurrencyNew[ + market.QuoteCurrency + ]; + string marketSymbolNorm = baseNorm + quoteNorm; + string marketSymbol = market.MarketSymbol; + exchangeSymbolToNormalizedSymbolNew[marketSymbol] = marketSymbolNorm; + normalizedSymbolToExchangeSymbolNew[marketSymbolNorm] = marketSymbol; + exchangeCurrenciesToMarketSymbolNew[baseSymbol + quoteSymbol] = + marketSymbol; + exchangeCurrenciesToMarketSymbolNew[quoteSymbol + baseSymbol] = + marketSymbol; + } + + exchangeCurrencyToNormalizedCurrency = ExchangeGlobalCurrencyReplacements = + exchangeCurrencyToNormalizedCurrencyNew; + normalizedCurrencyToExchangeCurrency = normalizedCurrencyToExchangeCurrencyNew; + exchangeSymbolToNormalizedSymbol = exchangeSymbolToNormalizedSymbolNew; + normalizedSymbolToExchangeSymbol = normalizedSymbolToExchangeSymbolNew; + exchangeCurrenciesToMarketSymbol = exchangeCurrenciesToMarketSymbolNew; - return new CachedItem(new object(), CryptoUtility.UtcNow.AddHours(4.0)); - }); + return new CachedItem(new object(), CryptoUtility.UtcNow.AddHours(4.0)); + } + ); } - public override async Task<(string baseCurrency, string quoteCurrency)> ExchangeMarketSymbolToCurrenciesAsync(string marketSymbol) + public override async Task<( + string baseCurrency, + string quoteCurrency + )> ExchangeMarketSymbolToCurrenciesAsync(string marketSymbol) { ExchangeMarket market = await GetExchangeMarketFromCacheAsync(marketSymbol); if (market == null) { - market = await GetExchangeMarketFromCacheAsync(marketSymbol.Replace("/", string.Empty)); + market = await GetExchangeMarketFromCacheAsync( + marketSymbol.Replace("/", string.Empty) + ); if (market == null) { - throw new ArgumentException("Unable to get currencies for market symbol " + marketSymbol); + throw new ArgumentException( + "Unable to get currencies for market symbol " + marketSymbol + ); } } return (market.BaseCurrency, market.QuoteCurrency); } - public override async Task ExchangeMarketSymbolToGlobalMarketSymbolAsync(string marketSymbol) + public override async Task ExchangeMarketSymbolToGlobalMarketSymbolAsync( + string marketSymbol + ) { await PopulateLookupTables(); - var (baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync(marketSymbol); - if (!exchangeCurrencyToNormalizedCurrency.TryGetValue(baseCurrency, out string baseCurrencyNormalized)) + var (baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync( + marketSymbol + ); + if ( + !exchangeCurrencyToNormalizedCurrency.TryGetValue( + baseCurrency, + out string baseCurrencyNormalized + ) + ) { baseCurrencyNormalized = baseCurrency; } - if (!exchangeCurrencyToNormalizedCurrency.TryGetValue(quoteCurrency, out string quoteCurrencyNormalized)) + if ( + !exchangeCurrencyToNormalizedCurrency.TryGetValue( + quoteCurrency, + out string quoteCurrencyNormalized + ) + ) { quoteCurrencyNormalized = quoteCurrency; } - return baseCurrencyNormalized + GlobalMarketSymbolSeparatorString + quoteCurrencyNormalized; + return baseCurrencyNormalized + + GlobalMarketSymbolSeparatorString + + quoteCurrencyNormalized; } - public override async Task GlobalMarketSymbolToExchangeMarketSymbolAsync(string marketSymbol) + public override async Task GlobalMarketSymbolToExchangeMarketSymbolAsync( + string marketSymbol + ) { await PopulateLookupTables(); string[] pieces = marketSymbol.Split('-'); @@ -147,17 +201,34 @@ public override async Task GlobalMarketSymbolToExchangeMarketSymbolAsync { throw new ArgumentException("Market symbol must be at least two pieces"); } - if (!normalizedCurrencyToExchangeCurrency.TryGetValue(pieces[0], out string baseCurrencyExchange)) + if ( + !normalizedCurrencyToExchangeCurrency.TryGetValue( + pieces[0], + out string baseCurrencyExchange + ) + ) { baseCurrencyExchange = pieces[0]; } - if (!normalizedCurrencyToExchangeCurrency.TryGetValue(pieces[1], out string quoteCurrencyExchange)) + if ( + !normalizedCurrencyToExchangeCurrency.TryGetValue( + pieces[1], + out string quoteCurrencyExchange + ) + ) { quoteCurrencyExchange = pieces[1]; } - if (!exchangeCurrenciesToMarketSymbol.TryGetValue(baseCurrencyExchange + quoteCurrencyExchange, out string exchangeMarketSymbol)) + if ( + !exchangeCurrenciesToMarketSymbol.TryGetValue( + baseCurrencyExchange + quoteCurrencyExchange, + out string exchangeMarketSymbol + ) + ) { - throw new ArgumentException("Unable to find exchange market for global market symbol " + marketSymbol); + throw new ArgumentException( + "Unable to find exchange market for global market symbol " + marketSymbol + ); } return exchangeMarketSymbol; } @@ -176,7 +247,9 @@ private ExchangeOrderResult ParseOrder(string orderId, JToken order) ExchangeOrderResult orderResult = new ExchangeOrderResult { OrderId = orderId }; orderResult.Message = (orderResult.Message ?? order["reason"].ToStringInvariant()); - orderResult.OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(order["opentm"].ConvertInvariant()); + orderResult.OrderDate = CryptoUtility.UnixTimeStampToDateTimeSeconds( + order["opentm"].ConvertInvariant() + ); orderResult.MarketSymbol = order["descr"]["pair"].ToStringInvariant(); orderResult.IsBuy = (order["descr"]["type"].ToStringInvariant() == "buy"); orderResult.Amount = order["vol"].ConvertInvariant(); @@ -253,40 +326,69 @@ private async Task ParseHistoryOrder(string orderId, JToken orderResult.AmountFilled = order["vol"].ConvertInvariant(); // orderResult.OrderDate - not provided here. ideally would be null but ExchangeOrderResult.OrderDate is not nullable orderResult.CompletedDate = null; // order not necessarily fully filled at this point - orderResult.TradeDate = CryptoUtility.UnixTimeStampToDateTimeSeconds(order["time"].ConvertInvariant()); + orderResult.TradeDate = CryptoUtility.UnixTimeStampToDateTimeSeconds( + order["time"].ConvertInvariant() + ); - string[] pairs = (await ExchangeMarketSymbolToGlobalMarketSymbolAsync(order["pair"].ToStringInvariant())).Split('-'); + string[] pairs = ( + await ExchangeMarketSymbolToGlobalMarketSymbolAsync( + order["pair"].ToStringInvariant() + ) + ).Split('-'); orderResult.FeesCurrency = pairs[1]; return orderResult; } - internal ExchangeOrderResult ExtendResultsWithOrderDescr(ExchangeOrderResult result, string orderStr) + internal ExchangeOrderResult ExtendResultsWithOrderDescr( + ExchangeOrderResult result, + string orderStr + ) { //"buy 0.00000001 XBTUSD @ limit 1000000" //"buy 58.00000000 ADAUSDT @ market" string[] orderStrParts = orderStr.Split(' '); - result.IsBuy = string.Equals(orderStrParts[0], "buy", StringComparison.InvariantCultureIgnoreCase); + result.IsBuy = string.Equals( + orderStrParts[0], + "buy", + StringComparison.InvariantCultureIgnoreCase + ); result.Amount = orderStrParts[1].ConvertInvariant(); result.MarketSymbol = orderStrParts[2]; - var isMarket = string.Equals(orderStrParts[4], "market", StringComparison.InvariantCultureIgnoreCase); - if (!isMarket) { + var isMarket = string.Equals( + orderStrParts[4], + "market", + StringComparison.InvariantCultureIgnoreCase + ); + if (!isMarket) + { result.Price = orderStrParts[5].ConvertInvariant(); } return result; } - private async Task> QueryOrdersAsync(string symbol, string path) + private async Task> QueryOrdersAsync( + string symbol, + string path + ) { await PopulateLookupTables(); List orders = new List(); - JToken result = await MakeJsonRequestAsync(path, null, await GetNoncePayloadAsync()); + JToken result = await MakeJsonRequestAsync( + path, + null, + await GetNoncePayloadAsync() + ); result = result["open"]; if (exchangeSymbolToNormalizedSymbol.TryGetValue(symbol, out string normalizedSymbol)) { foreach (JProperty order in result) { - if (normalizedSymbol == null || order.Value["descr"]["pair"].ToStringInvariant() == normalizedSymbol.ToUpperInvariant()) + if ( + normalizedSymbol == null + || order.Value["descr"]["pair"].ToStringInvariant() + == normalizedSymbol.ToUpperInvariant() + ) { orders.Add(ParseOrder(order.Name, order.Value)); } @@ -296,17 +398,28 @@ private async Task> QueryOrdersAsync(string sym return orders; } - private async Task> QueryClosedOrdersAsync(string symbol, string path) + private async Task> QueryClosedOrdersAsync( + string symbol, + string path + ) { await PopulateLookupTables(); List orders = new List(); - JToken result = await MakeJsonRequestAsync(path, null, await GetNoncePayloadAsync()); + JToken result = await MakeJsonRequestAsync( + path, + null, + await GetNoncePayloadAsync() + ); result = result["closed"]; if (exchangeSymbolToNormalizedSymbol.TryGetValue(symbol, out string normalizedSymbol)) { foreach (JProperty order in result) { - if (normalizedSymbol == null || order.Value["descr"]["pair"].ToStringInvariant() == normalizedSymbol.ToUpperInvariant()) + if ( + normalizedSymbol == null + || order.Value["descr"]["pair"].ToStringInvariant() + == normalizedSymbol.ToUpperInvariant() + ) { orders.Add(ParseOrder(order.Name, order.Value)); } @@ -323,17 +436,27 @@ private async Task> QueryClosedOrdersAsync(stri return orders; } - private async Task> QueryHistoryOrdersAsync(string symbol, string path) + private async Task> QueryHistoryOrdersAsync( + string symbol, + string path + ) { await PopulateLookupTables(); List orders = new List(); - JToken result = await MakeJsonRequestAsync(path, null, await GetNoncePayloadAsync()); + JToken result = await MakeJsonRequestAsync( + path, + null, + await GetNoncePayloadAsync() + ); result = result["trades"]; if (exchangeSymbolToNormalizedSymbol.TryGetValue(symbol, out string normalizedSymbol)) { foreach (JProperty order in result) { - if (normalizedSymbol == null || order.Value["pair"].ToStringInvariant() == symbol.ToUpperInvariant()) + if ( + normalizedSymbol == null + || order.Value["pair"].ToStringInvariant() == symbol.ToUpperInvariant() + ) { orders.Add(await ParseHistoryOrder(order.Name, order.Value)); } @@ -350,9 +473,17 @@ private async Task> QueryHistoryOrdersAsync(str return orders; } - protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary payload) + protected override async Task ProcessRequestAsync( + IHttpWebRequest request, + Dictionary payload + ) { - if (payload == null || PrivateApiKey == null || PublicApiKey == null || !payload.ContainsKey("nonce")) + if ( + payload == null + || PrivateApiKey == null + || PublicApiKey == null + || !payload.ContainsKey("nonce") + ) { await CryptoUtility.WritePayloadFormToRequestAsync(request, payload); } @@ -362,7 +493,10 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti payload.Remove("nonce"); string form = CryptoUtility.GetFormForPayload(payload); // nonce must be first on Kraken - form = "nonce=" + nonce + (string.IsNullOrWhiteSpace(form) ? string.Empty : "&" + form); + form = + "nonce=" + + nonce + + (string.IsNullOrWhiteSpace(form) ? string.Empty : "&" + form); using (SHA256 sha256 = SHA256Managed.Create()) { string hashString = nonce + form; @@ -371,8 +505,13 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti byte[] sigBytes = new byte[sha256Bytes.Length + pathBytes.Length]; pathBytes.CopyTo(sigBytes, 0); sha256Bytes.CopyTo(sigBytes, pathBytes.Length); - byte[] privateKey = System.Convert.FromBase64String(CryptoUtility.ToUnsecureString(PrivateApiKey)); - using (System.Security.Cryptography.HMACSHA512 hmac = new System.Security.Cryptography.HMACSHA512(privateKey)) + byte[] privateKey = System.Convert.FromBase64String( + CryptoUtility.ToUnsecureString(PrivateApiKey) + ); + using ( + System.Security.Cryptography.HMACSHA512 hmac = + new System.Security.Cryptography.HMACSHA512(privateKey) + ) { string sign = System.Convert.ToBase64String(hmac.ComputeHash(sigBytes)); request.AddHeader("API-Sign", sign); @@ -383,12 +522,19 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti } } - protected override async Task> OnGetCurrenciesAsync() + protected override async Task< + IReadOnlyDictionary + > OnGetCurrenciesAsync() { // https://api.kraken.com/0/public/Assets - Dictionary allCoins = new Dictionary(StringComparer.OrdinalIgnoreCase); + Dictionary allCoins = new Dictionary< + string, + ExchangeCurrency + >(StringComparer.OrdinalIgnoreCase); - var currencies = new Dictionary(StringComparer.OrdinalIgnoreCase); + var currencies = new Dictionary( + StringComparer.OrdinalIgnoreCase + ); JToken array = await MakeJsonRequestAsync("/0/public/Assets"); foreach (JProperty token in array) { @@ -406,18 +552,29 @@ protected override async Task> OnG return currencies; } - protected override async Task> OnGetMarketSymbolsAsync(bool isWebSocket = false) + protected override async Task> OnGetMarketSymbolsAsync( + bool isWebSocket = false + ) { JToken result = await MakeJsonRequestAsync("/0/public/AssetPairs"); - var names = result.Children().Where(p => !p.Name.Contains(".d")).Select(p => p.Name).ToList(); + var names = result + .Children() + .Where(p => !p.Name.Contains(".d")) + .Select(p => p.Name) + .ToList(); if (isWebSocket) { - names = result.Children() - .Where(p => p.Value["wsname"] != null && !string.IsNullOrEmpty(p.Value["wsname"].ToStringInvariant())) - .Select(s => s.Value["wsname"].ToStringInvariant()) - .ToList(); + names = result + .Children() + .Where( + p => + p.Value["wsname"] != null + && !string.IsNullOrEmpty(p.Value["wsname"].ToStringInvariant()) + ) + .Select(s => s.Value["wsname"].ToStringInvariant()) + .ToList(); } names.Sort(); @@ -425,7 +582,9 @@ protected override async Task> OnGetMarketSymbolsAsync(bool return names; } - protected internal override async Task> OnGetMarketSymbolsMetadataAsync() + protected internal override async Task< + IEnumerable + > OnGetMarketSymbolsMetadataAsync() { var markets = new List(); JToken allPairs = await MakeJsonRequestAsync("/0/public/AssetPairs"); @@ -435,7 +594,8 @@ protected internal override async Task> OnGetMarketS { JToken pair = prop.Value; JToken child = prop.Children().FirstOrDefault(); - var quantityStepSize = Math.Pow(0.1, pair["lot_decimals"].ConvertInvariant()).ConvertInvariant(); + var quantityStepSize = Math.Pow(0.1, pair["lot_decimals"].ConvertInvariant()) + .ConvertInvariant(); var market = new ExchangeMarket { IsActive = true, @@ -443,11 +603,14 @@ protected internal override async Task> OnGetMarketS AltMarketSymbol = child["altname"].ToStringInvariant(), AltMarketSymbol2 = child["wsname"].ToStringInvariant(), MinTradeSize = quantityStepSize, - MarginEnabled = pair["leverage_buy"].Children().Any() || pair["leverage_sell"].Children().Any(), + MarginEnabled = + pair["leverage_buy"].Children().Any() + || pair["leverage_sell"].Children().Any(), BaseCurrency = pair["base"].ToStringInvariant(), QuoteCurrency = pair["quote"].ToStringInvariant(), QuantityStepSize = quantityStepSize, - PriceStepSize = Math.Pow(0.1, pair["pair_decimals"].ConvertInvariant()).ConvertInvariant() + PriceStepSize = Math.Pow(0.1, pair["pair_decimals"].ConvertInvariant()) + .ConvertInvariant() }; markets.Add(market); } @@ -455,11 +618,18 @@ protected internal override async Task> OnGetMarketS return markets; } - protected override async Task OnWithdrawAsync(ExchangeWithdrawalRequest withdrawalRequest) + protected override async Task OnWithdrawAsync( + ExchangeWithdrawalRequest withdrawalRequest + ) { - if (!string.IsNullOrEmpty(withdrawalRequest.Address) || string.IsNullOrEmpty(withdrawalRequest.AddressTag)) + if ( + !string.IsNullOrEmpty(withdrawalRequest.Address) + || string.IsNullOrEmpty(withdrawalRequest.AddressTag) + ) { - throw new APIException($"Kraken only supports withdrawals to addresses setup beforehand and identified by a 'key'. Set this key in the {nameof(ExchangeWithdrawalRequest.AddressTag)} property."); + throw new APIException( + $"Kraken only supports withdrawals to addresses setup beforehand and identified by a 'key'. Set this key in the {nameof(ExchangeWithdrawalRequest.AddressTag)} property." + ); } var request = new Dictionary @@ -470,7 +640,12 @@ protected override async Task OnWithdrawAsync(Exchan ["key"] = withdrawalRequest.AddressTag }; - var result = await MakeJsonRequestAsync("/0/private/Withdraw", null, request, "POST"); + var result = await MakeJsonRequestAsync( + "/0/private/Withdraw", + null, + request, + "POST" + ); return new ExchangeWithdrawalResponse { @@ -479,13 +654,20 @@ protected override async Task OnWithdrawAsync(Exchan }; } - - protected override async Task>> OnGetTickersAsync() + protected override async Task< + IEnumerable> + > OnGetTickersAsync() { var marketSymbols = (await GetMarketSymbolsAsync()).ToArray(); - var normalizedPairsList = marketSymbols.Select(symbol => NormalizeMarketSymbol(symbol)).ToList(); + var normalizedPairsList = marketSymbols + .Select(symbol => NormalizeMarketSymbol(symbol)) + .ToList(); var csvPairsList = string.Join(",", normalizedPairsList); - JToken apiTickers = await MakeJsonRequestAsync("/0/public/Ticker", null, new Dictionary { { "pair", csvPairsList } }); + JToken apiTickers = await MakeJsonRequestAsync( + "/0/public/Ticker", + null, + new Dictionary { { "pair", csvPairsList } } + ); var tickers = new List>(); var unfoundSymbols = new List(); @@ -499,7 +681,9 @@ protected override async Task>> { // Some pairs like USDTZUSD are not found, but they (hopefully) can be found using Metadata. var symbols = (await GetMarketSymbolsMetadataAsync()).ToList(); - ExchangeMarket symbol = symbols.FirstOrDefault(a => a.AltMarketSymbol.Equals(marketSymbol)); + ExchangeMarket symbol = symbols.FirstOrDefault( + a => a.AltMarketSymbol.Equals(marketSymbol) + ); if (symbol == null) { unfoundSymbols.Add(marketSymbol); @@ -512,7 +696,12 @@ protected override async Task>> try { - tickers.Add(new KeyValuePair(marketSymbol, await ConvertToExchangeTickerAsync(marketSymbol, ticker))); + tickers.Add( + new KeyValuePair( + marketSymbol, + await ConvertToExchangeTickerAsync(marketSymbol, ticker) + ) + ); } catch (Exception e) { @@ -521,19 +710,28 @@ protected override async Task>> } if (unfoundSymbols.Count > 0) { - Logger.Warn($"Of {marketSymbols.Count()} symbols, tickers could not be found for {unfoundSymbols.Count}: [{String.Join(", ", unfoundSymbols)}]"); + Logger.Warn( + $"Of {marketSymbols.Count()} symbols, tickers could not be found for {unfoundSymbols.Count}: [{String.Join(", ", unfoundSymbols)}]" + ); } return tickers; } protected override async Task OnGetTickerAsync(string marketSymbol) { - JToken apiTickers = await MakeJsonRequestAsync("/0/public/Ticker", null, new Dictionary { { "pair", NormalizeMarketSymbol(marketSymbol) } }); + JToken apiTickers = await MakeJsonRequestAsync( + "/0/public/Ticker", + null, + new Dictionary { { "pair", NormalizeMarketSymbol(marketSymbol) } } + ); JToken ticker = apiTickers[marketSymbol]; return await ConvertToExchangeTickerAsync(marketSymbol, ticker); } - private async Task ConvertToExchangeTickerAsync(string symbol, JToken ticker) + private async Task ConvertToExchangeTickerAsync( + string symbol, + JToken ticker + ) { decimal last = ticker["c"][0].ConvertInvariant(); var (baseCurrency, quoteCurrency) = await ExchangeMarketSymbolToCurrenciesAsync(symbol); @@ -549,7 +747,9 @@ private async Task ConvertToExchangeTickerAsync(string symbol, J { QuoteCurrencyVolume = ticker["v"][1].ConvertInvariant(), QuoteCurrency = quoteCurrency, - BaseCurrencyVolume = ticker["v"][1].ConvertInvariant() * ticker["p"][1].ConvertInvariant(), + BaseCurrencyVolume = + ticker["v"][1].ConvertInvariant() + * ticker["p"][1].ConvertInvariant(), BaseCurrency = baseCurrency, Timestamp = CryptoUtility.UtcNow } @@ -561,13 +761,21 @@ protected override Task OnInitializeAsync() return PopulateLookupTables(); } - protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 100) + protected override async Task OnGetOrderBookAsync( + string marketSymbol, + int maxCount = 100 + ) { - JToken obj = await MakeJsonRequestAsync("/0/public/Depth?pair=" + marketSymbol + "&count=" + maxCount); + JToken obj = await MakeJsonRequestAsync( + "/0/public/Depth?pair=" + marketSymbol + "&count=" + maxCount + ); return obj[marketSymbol].ParseOrderBookFromJTokenArrays(); } - protected override async Task> OnGetRecentTradesAsync(string marketSymbol, int? limit = null) + protected override async Task> OnGetRecentTradesAsync( + string marketSymbol, + int? limit = null + ) { List trades = new List(); @@ -587,14 +795,22 @@ protected override async Task> OnGetRecentTradesAsync { foreach (JToken trade in outerArray.Children()) { - trades.Add(trade.ParseTrade(1, 0, 3, 2, TimestampType.UnixSecondsDouble, null, "b")); + 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) + 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; @@ -604,7 +820,12 @@ protected override async Task OnGetHistoricalTradesAsync(Func() / 1000000.0d); + startDate = CryptoUtility.UnixTimeStampToDateTimeMilliseconds( + result["last"].ConvertInvariant() / 1000000.0d + ); } foreach (JToken trade in outerArray.Children()) { - trades.Add(trade.ParseTrade(1, 0, 3, 2, TimestampType.UnixSecondsDouble, null, "b")); + trades.Add( + trade.ParseTrade(1, 0, 3, 2, TimestampType.UnixSecondsDouble, null, "b") + ); } trades.Sort((t1, t2) => t1.Timestamp.CompareTo(t2.Timestamp)); if (!callback(trades)) @@ -637,7 +862,13 @@ protected override async Task OnGetHistoricalTradesAsync(Func> OnGetCandlesAsync(string marketSymbol, int periodSeconds, DateTime? startDate = null, DateTime? endDate = null, int? limit = null) + protected override async Task> OnGetCandlesAsync( + string marketSymbol, + int periodSeconds, + DateTime? startDate = null, + DateTime? endDate = null, + int? limit = null + ) { if (limit != null) { @@ -649,14 +880,35 @@ protected override async Task> OnGetCandlesAsync(strin // array of array entries(