From dcb2fa35af83ca630bdff505f7520802a2c99d17 Mon Sep 17 00:00:00 2001 From: Jkorf Date: Thu, 12 Aug 2021 13:46:39 +0200 Subject: [PATCH] Squashed commit of the following: commit 1ca93ce870468c7effcc69a05b1675f855598f3e Author: Jkorf Date: Thu Aug 12 13:44:03 2021 +0200 Version 4.0.0 release commit f8cefe72cae8b845aa995d16415f160228f5dfc4 Author: Jkorf Date: Mon Aug 9 11:46:50 2021 +0200 Fix for processing a duplicate message in symbol order book commit f3573e414f80ea622caadb38f9e020f2eab7c834 Author: Jkorf Date: Mon Aug 9 11:36:50 2021 +0200 Updated version commit 170a3b5c83cd500f0adb5cf74eaaa9c1a8674a10 Merge: 73a0e2c 0d43f6e Author: Jkorf Date: Mon Aug 9 11:33:59 2021 +0200 Merge branch 'feature/replace-websocket4net' of https://github.com/JKorf/CryptoExchange.Net into feature/replace-websocket4net commit 73a0e2cb62f8d51b4b8fe1b29fbf16f07ebf13e0 Author: Jkorf Date: Mon Aug 9 11:33:45 2021 +0200 Fixed processing issue in SymbolOrderBook commit 0d43f6e14e9b61af9bb3250fd72559a5fbf5300a Author: Jan Korf Date: Sat Jul 31 14:48:23 2021 +0200 Updated version commit 037d765fc73740d2aa2159bce6d57f01d989bdc2 Author: Jan Korf Date: Sat Jul 31 11:14:42 2021 +0200 Socket fixes commit 13cf3e27b9e91b252368a5dc799363111eed9148 Author: Jkorf Date: Mon Jul 26 09:55:51 2021 +0200 Updated version commit 333c6c4207cafbfd09b37743fb36008f87939666 Author: Jkorf Date: Mon Jul 26 09:50:35 2021 +0200 Update CryptoExchangeWebSocketClient.cs commit ad2b6284b042cfa846e55f8b5f53b2397039dff7 Merge: 64bdcdf 2591cc6 Author: Jan Korf Date: Sun Jul 25 22:44:49 2021 +0200 Merge branch 'feature/replace-websocket4net' of https://github.com/jkorf/CryptoExchange.Net into feature/replace-websocket4net commit 64bdcdf087d56b8470baedac1ac86d263ad72490 Author: Jan Korf Date: Sun Jul 25 22:44:03 2021 +0200 Fix for deadlock commit 2591cc6782e281595c436ccb4b74a9c7c7f5dc57 Author: Jkorf Date: Thu Jul 15 15:10:08 2021 +0200 Cleanup commit b3b6235b6edf0ea1a6437239919666d08c274129 Author: Jkorf Date: Fri Jul 9 16:14:33 2021 +0200 Updated version commit 12975fd13f7884382fea608d774b1e0ac384cb70 Author: Jkorf Date: Fri Jul 9 16:12:04 2021 +0200 Async postfix for order book methods commit d49d59094e18c83de994a1b591f906f7efe916c5 Author: Jkorf Date: Fri Jul 9 15:47:38 2021 +0200 Renamed async methods with Async post fixes commit cbe103930ab90378507780cb150596abcb15eb3f Author: Jkorf Date: Thu Jul 8 09:29:34 2021 +0200 Added Book property on SymbolOrderBook commit ecc101aed11bab73c49fadc93d3ca4607094d92a Merge: 181f942 a172461 Author: Jan Korf Date: Wed Jul 7 23:38:58 2021 +0200 Merge branch 'feature/replace-websocket4net' of https://github.com/jkorf/CryptoExchange.Net into feature/replace-websocket4net commit 181f942e2137ff5dfede5365012e24dfb2f07cf9 Author: Jan Korf Date: Wed Jul 7 23:38:49 2021 +0200 Added CalculateAverageFillPrice to order book commit a17246153df463aa881e9808ffb87fa25f144d90 Author: Jkorf Date: Wed Jul 7 13:11:39 2021 +0200 Updated version commit c60f138ae846a991662e7544d45845decd25cc10 Author: Jkorf Date: Wed Jul 7 13:05:44 2021 +0200 Fix not logging responses with LogLevel.Debug commit 91f168fd25150b3048346bcc6acc549ee3685e50 Author: Jkorf Date: Mon Jul 5 10:39:48 2021 +0200 Added ExchangeHelpers to ReadMe commit ff6c02e574d3f8a270e2960fdda0222c53457868 Author: Jkorf Date: Mon Jul 5 10:34:25 2021 +0200 Added some helper methods, some code comments commit 55900a3db938f032436303c2399f41b7a3a5fef1 Merge: 9067a8a 7ab6bb0 Author: Jkorf Date: Mon Jul 5 09:24:43 2021 +0200 Merge branch 'master' into feature/replace-websocket4net commit 9067a8a82c5d8c1e1f6ba0f2600539efd7f1a122 Author: Jkorf Date: Fri Jul 2 16:03:38 2021 +0200 Update README.md commit 0f202d69c7e3db71f66dcfef5673b97f56451a40 Author: Jkorf Date: Fri Jun 25 16:37:06 2021 +0200 Code comments, added console logger commit 52160c6dc1bbaca3cc498c49ec0dd71310d5102b Author: Jkorf Date: Thu Jun 24 16:47:04 2021 +0200 Added/cleaned up code comments commit 95771c5c4ae3a74963fea5a01e05559596b1b39d Author: Jkorf Date: Tue Jun 22 14:22:15 2021 +0200 Additional info socket connection error commit 6a90e2316d54d42c0f5b7772d60d4861ff340c5a Author: Jkorf Date: Thu Jun 17 16:10:43 2021 +0200 Updated version commit b138c1050372b5d5fbdc3b2a33d3c09b677a669a Author: Jkorf Date: Thu Jun 17 16:08:19 2021 +0200 Fix not receiving OriginalData in output commit 462f44cf45e1bc7ee333e37ad0a778185d84fa58 Author: Jan Korf Date: Wed Jun 16 22:35:03 2021 +0200 Fix invalidoperation in order book commit aa7414321b1297487a5133fa0a7e5ede059a6939 Author: Jan Korf Date: Sun Jun 13 20:56:01 2021 +0200 logging no data task commit 22aa5928f5cb11ac5877bc4c606d64550aad3f0c Author: Jan Korf Date: Tue Jun 8 19:13:18 2021 +0200 Updated version commit 1ceb22994a5cdf779de8890e39101956728ab80c Author: Jan Korf Date: Tue Jun 8 19:11:20 2021 +0200 Fixed exception on .net framework when creating socket commit 971cab739dd543de0122c9bc12819f57d4df15e3 Author: Jkorf Date: Mon Jun 7 14:41:27 2021 +0200 Updated unit test packages commit 216213d5ad0e14c6cd7b8e22131b8b07338bacfc Author: Jkorf Date: Mon Jun 7 10:46:57 2021 +0200 Updated version commit ce2c7ddc182599c5ca9ef0eb64e14906c8b734e1 Merge: b219705 55aa77e Author: Jan Korf Date: Mon Jun 7 09:29:57 2021 +0200 Merge pull request #96 from ridicoulous/master iexchange client improvements commit b21970552602eb853a061034a5c223a381e7465b Author: Jkorf Date: Tue Jun 1 14:55:02 2021 +0200 Updated version commit dd2cf84c163920e545ec09e1b0d29ab295f2fbe2 Author: Jkorf Date: Tue Jun 1 14:52:55 2021 +0200 Added tests for LogLevel null commit 975003284f53094750ec3d4a608d814a6d70f745 Author: Jkorf Date: Tue Jun 1 14:42:59 2021 +0200 Made LogLevel nullable in options, moved log formatting to the ILogger implementation commit 1c1de5651e31e9288b4fd125b0545efbfa9098f7 Merge: 34bf986 e7838ab Author: Jkorf Date: Mon May 31 10:16:52 2021 +0200 Merge branch 'feature/replace-websocket4net' of https://github.com/JKorf/CryptoExchange.Net into feature/replace-websocket4net commit 34bf9867dcd2c2b195079094560351d0db2dac0f Author: Jkorf Date: Mon May 31 10:16:49 2021 +0200 fixed some axync issues commit e7838ab9e8310b4ae02c172f0a764aad8ca8fd4e Author: Jan Korf Date: Wed May 26 13:46:40 2021 +0200 Added Discord link to ReadMe commit 502939f57cf8deac1809c4bd2c63af4acc62bd43 Author: Jan Korf Date: Wed May 26 11:12:59 2021 +0200 Updated version commit 92b745ca291259eb246572c8029709b87ddbf2d3 Author: Jan Korf Date: Tue May 25 21:52:47 2021 +0200 Refactored logging to use ILogger commit b4904b5e4a239ffb29f00b13a314fea33d45d9cb Author: Jan Korf Date: Tue May 25 14:07:01 2021 +0200 Added optional json output, added DataEvent for socket updates commit 55aa77e92647ccdb00abdb35a7ac84f1317b9742 Author: Artem Kurianov Date: Fri May 14 13:11:05 2021 +0000 added order time commit efe70445ed13228425278e6f57fb17d4156c12f4 Author: Artem Kurianov Date: Fri May 14 13:08:58 2021 +0000 iexchange client improvements commit 8f0943f0f0469b42f40334257f0f3ca5e39e3bd9 Author: Jkorf Date: Thu May 6 10:24:33 2021 +0200 Updated version commit 17d1e5f71bd1986eea0e8273da070f3615d3e378 Author: Jkorf Date: Thu May 6 10:01:24 2021 +0200 Added missing configureawaits commit ed748aa474e0d52a78bc15b9b0aa0a9b4d8dd35e Author: Jan Korf Date: Fri Apr 30 23:00:07 2021 +0200 Updated version commit 1ae545563497889c75ed6183377e759da5beed61 Author: Jan Korf Date: Fri Apr 30 21:00:40 2021 +0200 Updated socket closing commit 8243ad60dc64f96ed262ec54cbd6c55553c056be Author: Jkorf Date: Fri Apr 30 15:48:36 2021 +0200 Updated version commit 4299f238f3debc2b1b062eb7cde195a3d64dbdfc Author: Jan Korf Date: Fri Apr 30 15:44:32 2021 +0200 Fix for closing socket without timeout task commit 71f49cfe5142cb70fb651936e74b8eef5b7350d4 Author: Jkorf Date: Fri Apr 30 14:59:10 2021 +0200 Updated version commit 5d669068ec0a7241daef383c6867c2e22ec21ed3 Author: Jkorf Date: Fri Apr 30 14:56:15 2021 +0200 Renaming, cleanup handler -> subscription socket (SocketConnection) -> socketConnection commit 0fa13e286060cd3b96b808406434092c0e89e89d Author: Jkorf Date: Thu Apr 29 16:20:41 2021 +0200 Initial work replacing Websocket4Net with ClientWebSocket --- .gitignore | 1 + .../BaseClientTests.cs | 67 +- .../CryptoExchange.Net.UnitTests.csproj | 8 +- .../ExchangeHelpersTests.cs | 73 ++ .../RestClientTests.cs | 4 +- .../SocketClientTests.cs | 52 +- .../SymbolOrderBookTests.cs | 55 +- .../TestImplementations/TestBaseClient.cs | 7 +- .../TestImplementations/TestRestClient.cs | 12 +- .../TestImplementations/TestSocket.cs | 14 +- .../TestImplementations/TestSocketClient.cs | 6 +- .../TestImplementations/TestStringLogger.cs | 25 + .../Authentication/ApiCredentials.cs | 7 +- .../Authentication/AuthenticationProvider.cs | 36 +- CryptoExchange.Net/BaseClient.cs | 148 ++- .../Converters/ArrayConverter.cs | 3 +- CryptoExchange.Net/CryptoExchange.Net.csproj | 20 +- CryptoExchange.Net/CryptoExchange.Net.xml | 981 +++++++++++------- CryptoExchange.Net/ExchangeHelpers.cs | 120 +++ .../ExchangeInterfaces/ICommonBalance.cs | 6 +- .../ExchangeInterfaces/ICommonTrade.cs | 6 +- .../ExchangeInterfaces/IExchangeClient.cs | 27 + .../ExchangeInterfaces/IKline.cs | 2 - .../ExchangeInterfaces/IOrder.cs | 8 +- .../ExchangeInterfaces/IOrderBook.cs | 4 +- .../ExchangeInterfaces/IPlacedOrder.cs | 6 +- .../ExchangeInterfaces/IRecentTrade.cs | 2 - .../ExchangeInterfaces/ISymbol.cs | 6 +- .../ExchangeInterfaces/ITicker.cs | 6 +- CryptoExchange.Net/ExtensionMethods.cs | 37 +- CryptoExchange.Net/Interfaces/IRequest.cs | 2 +- CryptoExchange.Net/Interfaces/IResponse.cs | 2 +- CryptoExchange.Net/Interfaces/IRestClient.cs | 2 +- .../Interfaces/ISocketClient.cs | 4 +- .../Interfaces/ISymbolOrderBook.cs | 22 +- CryptoExchange.Net/Interfaces/IWebsocket.cs | 22 +- CryptoExchange.Net/Logging/ConsoleLogger.cs | 24 + CryptoExchange.Net/Logging/DebugLogger.cs | 25 + CryptoExchange.Net/Logging/DebugTextWriter.cs | 21 - CryptoExchange.Net/Logging/Log.cs | 61 +- .../Logging/ThreadSafeFileWriter.cs | 56 - CryptoExchange.Net/Objects/CallResult.cs | 60 +- CryptoExchange.Net/Objects/Enums.cs | 17 +- CryptoExchange.Net/Objects/Error.cs | 14 +- CryptoExchange.Net/Objects/Options.cs | 37 +- .../OrderBook/ProcessBufferEntry.cs | 5 +- .../OrderBook/ProcessQueueItem.cs | 9 +- .../OrderBook/SymbolOrderBook.cs | 139 ++- CryptoExchange.Net/Requests/Request.cs | 3 +- CryptoExchange.Net/Requests/Response.cs | 2 +- CryptoExchange.Net/RestClient.cs | 100 +- CryptoExchange.Net/SocketClient.cs | 270 ++--- CryptoExchange.Net/Sockets/BaseSocket.cs | 422 -------- .../Sockets/CryptoExchangeWebSocketClient.cs | 561 ++++++++++ CryptoExchange.Net/Sockets/DataEvent.cs | 72 ++ CryptoExchange.Net/Sockets/MessageEvent.cs | 43 + .../Sockets/SocketConnection.cs | 157 +-- .../Sockets/SocketSubscription.cs | 9 +- .../Sockets/UpdateSubscription.cs | 24 +- .../Sockets/WebsocketFactory.cs | 6 +- README.md | 411 +++++--- 61 files changed, 2696 insertions(+), 1655 deletions(-) create mode 100644 CryptoExchange.Net.UnitTests/ExchangeHelpersTests.cs create mode 100644 CryptoExchange.Net.UnitTests/TestImplementations/TestStringLogger.cs create mode 100644 CryptoExchange.Net/ExchangeHelpers.cs create mode 100644 CryptoExchange.Net/Logging/ConsoleLogger.cs create mode 100644 CryptoExchange.Net/Logging/DebugLogger.cs delete mode 100644 CryptoExchange.Net/Logging/DebugTextWriter.cs delete mode 100644 CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs delete mode 100644 CryptoExchange.Net/Sockets/BaseSocket.cs create mode 100644 CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs create mode 100644 CryptoExchange.Net/Sockets/DataEvent.cs create mode 100644 CryptoExchange.Net/Sockets/MessageEvent.cs diff --git a/.gitignore b/.gitignore index 940794e..86a11ab 100644 --- a/.gitignore +++ b/.gitignore @@ -286,3 +286,4 @@ __pycache__/ *.btm.cs *.odx.cs *.xsd.cs +CryptoExchange.Net/CryptoExchange.Net.xml diff --git a/CryptoExchange.Net.UnitTests/BaseClientTests.cs b/CryptoExchange.Net.UnitTests/BaseClientTests.cs index c109479..39b9668 100644 --- a/CryptoExchange.Net.UnitTests/BaseClientTests.cs +++ b/CryptoExchange.Net.UnitTests/BaseClientTests.cs @@ -1,11 +1,10 @@ using CryptoExchange.Net.Authentication; -using CryptoExchange.Net.Logging; using CryptoExchange.Net.Objects; +using CryptoExchange.Net.UnitTests.TestImplementations; +using Microsoft.Extensions.Logging; using NUnit.Framework; using System; using System.Collections.Generic; -using System.IO; -using System.Text; namespace CryptoExchange.Net.UnitTests { @@ -30,54 +29,58 @@ namespace CryptoExchange.Net.UnitTests public void SettingLogOutput_Should_RedirectLogOutput() { // arrange - var stringBuilder = new StringBuilder(); + var logger = new TestStringLogger(); var client = new TestBaseClient(new RestClientOptions("") { - LogWriters = new List { new StringWriter(stringBuilder) } + LogWriters = new List { logger } }); // act - client.Log(LogVerbosity.Info, "Test"); + client.Log(LogLevel.Information, "Test"); // assert - Assert.IsFalse(string.IsNullOrEmpty(stringBuilder.ToString())); + Assert.IsFalse(string.IsNullOrEmpty(logger.GetLogs())); } - [TestCase(LogVerbosity.None, LogVerbosity.Error, false)] - [TestCase(LogVerbosity.None, LogVerbosity.Warning, false)] - [TestCase(LogVerbosity.None, LogVerbosity.Info, false)] - [TestCase(LogVerbosity.None, LogVerbosity.Debug, false)] - [TestCase(LogVerbosity.Error, LogVerbosity.Error, true)] - [TestCase(LogVerbosity.Error, LogVerbosity.Warning, false)] - [TestCase(LogVerbosity.Error, LogVerbosity.Info, false)] - [TestCase(LogVerbosity.Error, LogVerbosity.Debug, false)] - [TestCase(LogVerbosity.Warning, LogVerbosity.Error, true)] - [TestCase(LogVerbosity.Warning, LogVerbosity.Warning, true)] - [TestCase(LogVerbosity.Warning, LogVerbosity.Info, false)] - [TestCase(LogVerbosity.Warning, LogVerbosity.Debug, false)] - [TestCase(LogVerbosity.Info, LogVerbosity.Error, true)] - [TestCase(LogVerbosity.Info, LogVerbosity.Warning, true)] - [TestCase(LogVerbosity.Info, LogVerbosity.Info, true)] - [TestCase(LogVerbosity.Info, LogVerbosity.Debug, false)] - [TestCase(LogVerbosity.Debug, LogVerbosity.Error, true)] - [TestCase(LogVerbosity.Debug, LogVerbosity.Warning, true)] - [TestCase(LogVerbosity.Debug, LogVerbosity.Info, true)] - [TestCase(LogVerbosity.Debug, LogVerbosity.Debug, true)] - public void SettingLogVerbosity_Should_RestrictLogging(LogVerbosity verbosity, LogVerbosity testVerbosity, bool expected) + [TestCase(LogLevel.None, LogLevel.Error, false)] + [TestCase(LogLevel.None, LogLevel.Warning, false)] + [TestCase(LogLevel.None, LogLevel.Information, false)] + [TestCase(LogLevel.None, LogLevel.Debug, false)] + [TestCase(LogLevel.Error, LogLevel.Error, true)] + [TestCase(LogLevel.Error, LogLevel.Warning, false)] + [TestCase(LogLevel.Error, LogLevel.Information, false)] + [TestCase(LogLevel.Error, LogLevel.Debug, false)] + [TestCase(LogLevel.Warning, LogLevel.Error, true)] + [TestCase(LogLevel.Warning, LogLevel.Warning, true)] + [TestCase(LogLevel.Warning, LogLevel.Information, false)] + [TestCase(LogLevel.Warning, LogLevel.Debug, false)] + [TestCase(LogLevel.Information, LogLevel.Error, true)] + [TestCase(LogLevel.Information, LogLevel.Warning, true)] + [TestCase(LogLevel.Information, LogLevel.Information, true)] + [TestCase(LogLevel.Information, LogLevel.Debug, false)] + [TestCase(LogLevel.Debug, LogLevel.Error, true)] + [TestCase(LogLevel.Debug, LogLevel.Warning, true)] + [TestCase(LogLevel.Debug, LogLevel.Information, true)] + [TestCase(LogLevel.Debug, LogLevel.Debug, true)] + [TestCase(null, LogLevel.Error, true)] + [TestCase(null, LogLevel.Warning, true)] + [TestCase(null, LogLevel.Information, true)] + [TestCase(null, LogLevel.Debug, true)] + public void SettingLogLevel_Should_RestrictLogging(LogLevel? verbosity, LogLevel testVerbosity, bool expected) { // arrange - var stringBuilder = new StringBuilder(); + var logger = new TestStringLogger(); var client = new TestBaseClient(new RestClientOptions("") { - LogWriters = new List { new StringWriter(stringBuilder) }, - LogVerbosity = verbosity + LogWriters = new List { logger }, + LogLevel = verbosity }); // act client.Log(testVerbosity, "Test"); // assert - Assert.AreEqual(!string.IsNullOrEmpty(stringBuilder.ToString()), expected); + Assert.AreEqual(!string.IsNullOrEmpty(logger.GetLogs()), expected); } [TestCase] diff --git a/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj b/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj index bdcecb7..dbd0c92 100644 --- a/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj +++ b/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj @@ -6,10 +6,10 @@ - - - - + + + + diff --git a/CryptoExchange.Net.UnitTests/ExchangeHelpersTests.cs b/CryptoExchange.Net.UnitTests/ExchangeHelpersTests.cs new file mode 100644 index 0000000..3514f89 --- /dev/null +++ b/CryptoExchange.Net.UnitTests/ExchangeHelpersTests.cs @@ -0,0 +1,73 @@ +using CryptoExchange.Net.Objects; +using NUnit.Framework; +using System.Globalization; + +namespace CryptoExchange.Net.UnitTests +{ + [TestFixture()] + public class ExchangeHelpersTests + { + [TestCase(0.1, 1, 0.4, 0.4)] + [TestCase(0.1, 1, 0.1, 0.1)] + [TestCase(0.1, 1, 1, 1)] + [TestCase(0.1, 1, 0.99, 0.99)] + [TestCase(0.1, 1, 0.09, 0.1)] + [TestCase(0.1, 1, 1.01, 1)] + public void ClampValueTests(decimal min, decimal max, decimal input, decimal expected) + { + var result = ExchangeHelpers.ClampValue(min, max, input); + Assert.AreEqual(expected, result); + } + + [TestCase(0.1, 1, 0.1, RoundingType.Down, 0.4, 0.4)] + [TestCase(0.1, 1, 0.1, RoundingType.Down, 0.1, 0.1)] + [TestCase(0.1, 1, 0.1, RoundingType.Down, 1, 1)] + [TestCase(0.1, 1, 0.1, RoundingType.Down, 0.99, 0.9)] + [TestCase(0.1, 1, 0.1, RoundingType.Down, 0.09, 0.1)] + [TestCase(0.1, 1, 0.1, RoundingType.Down, 1.01, 1)] + [TestCase(0.1, 1, 0.01, RoundingType.Down, 0.532, 0.53)] + [TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.532, 0.532)] + [TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.532, 0.532)] + [TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.5516592, 0.5516)] + [TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.5516592, 0.5517)] + public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected) + { + var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input); + Assert.AreEqual(expected, result); + } + + [TestCase(0.1, 1, 2, RoundingType.Closest, 0.4, 0.4)] + [TestCase(0.1, 1, 2, RoundingType.Closest, 0.1, 0.1)] + [TestCase(0.1, 1, 2, RoundingType.Closest, 1, 1)] + [TestCase(0.1, 1, 2, RoundingType.Down, 0.555, 0.55)] + [TestCase(0.1, 1, 2, RoundingType.Closest, 0.555, 0.56)] + [TestCase(0, 100, 5, RoundingType.Closest, 23.125987, 23.126)] + [TestCase(0, 100, 5, RoundingType.Down, 23.125987, 23.125)] + [TestCase(0, 100, 8, RoundingType.Down, 0.145647985948, 0.14564798)] + [TestCase(0, 100, 8, RoundingType.Closest, 0.145647985948, 0.14564799)] + public void AdjustValuePrecisionTests(decimal min, decimal max, int? precision, RoundingType roundingType, decimal input, decimal expected) + { + var result = ExchangeHelpers.AdjustValuePrecision(min, max, precision, roundingType, input); + Assert.AreEqual(expected, result); + } + + [TestCase(5, 0.1563158, 0.15631)] + [TestCase(5, 12.1789258, 12.17892)] + [TestCase(2, 12.1789258, 12.17)] + [TestCase(8, 156146.1247, 156146.1247)] + [TestCase(8, 50, 50)] + public void RoundDownTests(int decimalPlaces, decimal input, decimal expected) + { + var result = ExchangeHelpers.RoundDown(input, decimalPlaces); + Assert.AreEqual(expected, result); + } + + [TestCase(0.1234560000, "0.123456")] + [TestCase(794.1230130600, "794.12301306")] + public void NormalizeTests(decimal input, string expected) + { + var result = ExchangeHelpers.Normalize(input); + Assert.AreEqual(expected, result.ToString(CultureInfo.InvariantCulture)); + } + } +} diff --git a/CryptoExchange.Net.UnitTests/RestClientTests.cs b/CryptoExchange.Net.UnitTests/RestClientTests.cs index 7a13a62..286db2b 100644 --- a/CryptoExchange.Net.UnitTests/RestClientTests.cs +++ b/CryptoExchange.Net.UnitTests/RestClientTests.cs @@ -1,5 +1,4 @@ using CryptoExchange.Net.Authentication; -using CryptoExchange.Net.Logging; using CryptoExchange.Net.Objects; using CryptoExchange.Net.UnitTests.TestImplementations; using Newtonsoft.Json; @@ -10,6 +9,7 @@ using System.Diagnostics; using System.Linq; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.RateLimiter; +using Microsoft.Extensions.Logging; namespace CryptoExchange.Net.UnitTests { @@ -175,7 +175,7 @@ namespace CryptoExchange.Net.UnitTests { RateLimiters = new List { new RateLimiterAPIKey(1, TimeSpan.FromSeconds(1)) }, RateLimitingBehaviour = RateLimitingBehaviour.Wait, - LogVerbosity = LogVerbosity.Debug, + LogLevel = LogLevel.Debug, ApiCredentials = new ApiCredentials("TestKey", "TestSecret") }); client.SetResponse("{\"property\": 123}"); diff --git a/CryptoExchange.Net.UnitTests/SocketClientTests.cs b/CryptoExchange.Net.UnitTests/SocketClientTests.cs index f684473..e4a0e6f 100644 --- a/CryptoExchange.Net.UnitTests/SocketClientTests.cs +++ b/CryptoExchange.Net.UnitTests/SocketClientTests.cs @@ -1,9 +1,9 @@ using System; using System.Threading; -using CryptoExchange.Net.Logging; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Sockets; using CryptoExchange.Net.UnitTests.TestImplementations; +using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; using NUnit.Framework; @@ -49,7 +49,7 @@ namespace CryptoExchange.Net.UnitTests public void SocketMessages_Should_BeProcessedInDataHandlers() { // arrange - var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); + var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug }); var socket = client.CreateSocket(); socket.ShouldReconnect = true; socket.CanConnect = true; @@ -57,9 +57,9 @@ namespace CryptoExchange.Net.UnitTests var sub = new SocketConnection(client, socket); var rstEvent = new ManualResetEvent(false); JToken result = null; - sub.AddHandler(SocketSubscription.CreateForIdentifier("TestHandler", true, (connection, data) => + sub.AddSubscription(SocketSubscription.CreateForIdentifier("TestHandler", true, (messageEvent) => { - result = data; + result = messageEvent.JsonData; rstEvent.Set(); })); client.ConnectSocketSub(sub); @@ -71,13 +71,41 @@ namespace CryptoExchange.Net.UnitTests // assert Assert.IsTrue((int)result["property"] == 123); } - + + [TestCase(false)] + [TestCase(true)] + public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled) + { + // arrange + var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug, OutputOriginalData = enabled }); + var socket = client.CreateSocket(); + socket.ShouldReconnect = true; + socket.CanConnect = true; + socket.DisconnectTime = DateTime.UtcNow; + var sub = new SocketConnection(client, socket); + var rstEvent = new ManualResetEvent(false); + string original = null; + sub.AddSubscription(SocketSubscription.CreateForIdentifier("TestHandler", true, (messageEvent) => + { + original = messageEvent.OriginalData; + rstEvent.Set(); + })); + client.ConnectSocketSub(sub); + + // act + socket.InvokeMessage("{\"property\": 123}"); + rstEvent.WaitOne(1000); + + // assert + Assert.IsTrue(original == (enabled ? "{\"property\": 123}" : null)); + } + [TestCase] public void DisconnectedSocket_Should_Reconnect() { // arrange bool reconnected = false; - var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); + var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug }); var socket = client.CreateSocket(); socket.ShouldReconnect = true; socket.CanConnect = true; @@ -104,15 +132,15 @@ namespace CryptoExchange.Net.UnitTests public void UnsubscribingStream_Should_CloseTheSocket() { // arrange - var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); + var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug }); var socket = client.CreateSocket(); socket.CanConnect = true; var sub = new SocketConnection(client, socket); client.ConnectSocketSub(sub); - var ups = new UpdateSubscription(sub, SocketSubscription.CreateForIdentifier("Test", true, (d, a) => {})); + var ups = new UpdateSubscription(sub, SocketSubscription.CreateForIdentifier("Test", true, (e) => {})); // act - client.Unsubscribe(ups).Wait(); + client.UnsubscribeAsync(ups).Wait(); // assert Assert.IsTrue(socket.Connected == false); @@ -122,7 +150,7 @@ namespace CryptoExchange.Net.UnitTests public void UnsubscribingAll_Should_CloseAllSockets() { // arrange - var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); + var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug }); var socket1 = client.CreateSocket(); var socket2 = client.CreateSocket(); socket1.CanConnect = true; @@ -133,7 +161,7 @@ namespace CryptoExchange.Net.UnitTests client.ConnectSocketSub(sub2); // act - client.UnsubscribeAll().Wait(); + client.UnsubscribeAllAsync().Wait(); // assert Assert.IsTrue(socket1.Connected == false); @@ -144,7 +172,7 @@ namespace CryptoExchange.Net.UnitTests public void FailingToConnectSocket_Should_ReturnError() { // arrange - var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogVerbosity = LogVerbosity.Debug }); + var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug }); var socket = client.CreateSocket(); socket.CanConnect = false; var sub = new SocketConnection(client, socket); diff --git a/CryptoExchange.Net.UnitTests/SymbolOrderBookTests.cs b/CryptoExchange.Net.UnitTests/SymbolOrderBookTests.cs index beefa97..f3692a9 100644 --- a/CryptoExchange.Net.UnitTests/SymbolOrderBookTests.cs +++ b/CryptoExchange.Net.UnitTests/SymbolOrderBookTests.cs @@ -1,12 +1,10 @@ using System; -using System.Threading; +using System.Collections.Generic; using System.Threading.Tasks; -using CryptoExchange.Net.Logging; +using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using CryptoExchange.Net.OrderBook; using CryptoExchange.Net.Sockets; -using CryptoExchange.Net.UnitTests.TestImplementations; -using Newtonsoft.Json.Linq; using NUnit.Framework; namespace CryptoExchange.Net.UnitTests @@ -24,15 +22,32 @@ namespace CryptoExchange.Net.UnitTests public override void Dispose() {} - protected override Task> DoResync() + protected override Task> DoResyncAsync() { throw new NotImplementedException(); } - protected override Task> DoStart() + protected override Task> DoStartAsync() { throw new NotImplementedException(); } + + public void SetData(IEnumerable bids, IEnumerable asks) + { + Status = OrderBookStatus.Synced; + base.bids.Clear(); + foreach (var bid in bids) + base.bids.Add(bid.Price, bid); + base.asks.Clear(); + foreach (var ask in asks) + base.asks.Add(ask.Price, ask); + } + } + + public class BookEntry : ISymbolOrderBookEntry + { + public decimal Quantity { get; set; } + public decimal Price { get; set; } } [TestCase] @@ -65,5 +80,33 @@ namespace CryptoExchange.Net.UnitTests Assert.AreEqual(0m, symbolOrderBook.BestOffers.Ask.Price); Assert.AreEqual(0m, symbolOrderBook.BestOffers.Ask.Quantity); } + + [TestCase] + public void CalculateAverageFillPrice() + { + var orderbook = new TestableSymbolOrderBook(); + orderbook.SetData(new List + { + new BookEntry{ Price = 1, Quantity = 1 }, + new BookEntry{ Price = 1.1m, Quantity = 1 }, + }, + new List() + { + new BookEntry{ Price = 1.2m, Quantity = 1 }, + new BookEntry{ Price = 1.3m, Quantity = 1 }, + }); + + var resultBids = orderbook.CalculateAverageFillPrice(2, OrderBookEntryType.Bid); + var resultAsks = orderbook.CalculateAverageFillPrice(2, OrderBookEntryType.Ask); + var resultBids2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Bid); + var resultAsks2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Ask); + + Assert.True(resultBids.Success); + Assert.True(resultAsks.Success); + Assert.AreEqual(1.05m, resultBids.Data); + Assert.AreEqual(1.25m, resultAsks.Data); + Assert.AreEqual(1.06666667m, resultBids2.Data); + Assert.AreEqual(1.23333333m, resultAsks2.Data); + } } } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs index df87c64..93fec79 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs @@ -1,9 +1,8 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Net.Http; using CryptoExchange.Net.Authentication; -using CryptoExchange.Net.Logging; using CryptoExchange.Net.Objects; +using Microsoft.Extensions.Logging; namespace CryptoExchange.Net.UnitTests { @@ -17,7 +16,7 @@ namespace CryptoExchange.Net.UnitTests { } - public void Log(LogVerbosity verbosity, string data) + public void Log(LogLevel verbosity, string data) { log.Write(verbosity, data); } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs index f38aa0c..075f291 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs @@ -40,11 +40,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations var response = new Mock(); response.Setup(c => c.IsSuccessStatusCode).Returns(true); - response.Setup(c => c.GetResponseStream()).Returns(Task.FromResult((Stream)responseStream)); + response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream)); var request = new Mock(); request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com")); - request.Setup(c => c.GetResponse(It.IsAny())).Returns(Task.FromResult(response.Object)); + request.Setup(c => c.GetResponseAsync(It.IsAny())).Returns(Task.FromResult(response.Object)); var factory = Mock.Get(RequestFactory); factory.Setup(c => c.Create(It.IsAny(), It.IsAny(), It.IsAny())) @@ -57,7 +57,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations typeof(HttpRequestException).GetField("_message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, message); var request = new Mock(); - request.Setup(c => c.GetResponse(It.IsAny())).Throws(we); + request.Setup(c => c.GetResponseAsync(It.IsAny())).Throws(we); var factory = Mock.Get(RequestFactory); factory.Setup(c => c.Create(It.IsAny(), It.IsAny(), It.IsAny())) @@ -73,11 +73,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations var response = new Mock(); response.Setup(c => c.IsSuccessStatusCode).Returns(false); - response.Setup(c => c.GetResponseStream()).Returns(Task.FromResult((Stream)responseStream)); + response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream)); var request = new Mock(); request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com")); - request.Setup(c => c.GetResponse(It.IsAny())).Returns(Task.FromResult(response.Object)); + request.Setup(c => c.GetResponseAsync(It.IsAny())).Returns(Task.FromResult(response.Object)); var factory = Mock.Get(RequestFactory); factory.Setup(c => c.Create(It.IsAny(), It.IsAny(), It.IsAny())) @@ -86,7 +86,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations public async Task> Request(CancellationToken ct = default) where T:class { - return await SendRequest(new Uri("http://www.test.com"), HttpMethod.Get, ct); + return await SendRequestAsync(new Uri("http://www.test.com"), HttpMethod.Get, ct); } } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs index ef757e9..ed46836 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs @@ -1,8 +1,9 @@ using System; using System.Security.Authentication; +using System.Text; using System.Threading.Tasks; using CryptoExchange.Net.Interfaces; -using WebSocket4Net; +using CryptoExchange.Net.Objects; namespace CryptoExchange.Net.UnitTests.TestImplementations { @@ -23,12 +24,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations public Func DataInterpreterBytes { get; set; } public DateTime? DisconnectTime { get; set; } public string Url { get; } - public WebSocketState SocketState { get; } public bool IsClosed => !Connected; public bool IsOpen => Connected; public bool PingConnection { get; set; } public TimeSpan PingInterval { get; set; } public SslProtocols SSLProtocols { get; set; } + public Encoding Encoding { get; set; } public int ConnectCalls { get; private set; } public bool Reconnecting { get; set; } @@ -46,7 +47,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations } } - public Task Connect() + public Task ConnectAsync() { Connected = CanConnect; ConnectCalls++; @@ -65,7 +66,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations { } - public Task Close() + public Task CloseAsync() { Connected = false; DisconnectTime = DateTime.UtcNow; @@ -97,5 +98,10 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations { OnMessage?.Invoke(data); } + + public void SetProxy(ApiProxy proxy) + { + throw new NotImplementedException(); + } } } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs index 886d948..de62926 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs @@ -29,7 +29,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations public CallResult ConnectSocketSub(SocketConnection sub) { - return ConnectSocket(sub).Result; + return ConnectSocketAsync(sub).Result; } protected internal override bool HandleQueryResponse(SocketConnection s, object request, JToken data, out CallResult callResult) @@ -53,12 +53,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations return true; } - protected internal override Task> AuthenticateSocket(SocketConnection s) + protected internal override Task> AuthenticateSocketAsync(SocketConnection s) { throw new NotImplementedException(); } - protected internal override Task Unsubscribe(SocketConnection connection, SocketSubscription s) + protected internal override Task UnsubscribeAsync(SocketConnection connection, SocketSubscription s) { throw new NotImplementedException(); } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestStringLogger.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestStringLogger.cs new file mode 100644 index 0000000..c852728 --- /dev/null +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestStringLogger.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Text; + +namespace CryptoExchange.Net.UnitTests.TestImplementations +{ + public class TestStringLogger : ILogger + { + StringBuilder _builder = new StringBuilder(); + + public IDisposable BeginScope(TState state) => null; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + _builder.AppendLine(formatter(state, exception)); + } + + public string GetLogs() + { + return _builder.ToString(); + } + } +} diff --git a/CryptoExchange.Net/Authentication/ApiCredentials.cs b/CryptoExchange.Net/Authentication/ApiCredentials.cs index e567e9f..9f7242f 100644 --- a/CryptoExchange.Net/Authentication/ApiCredentials.cs +++ b/CryptoExchange.Net/Authentication/ApiCredentials.cs @@ -36,18 +36,21 @@ namespace CryptoExchange.Net.Authentication } /// - /// Create Api credentials providing a api key and secret for authentication + /// Create Api credentials providing an api key and secret for authentication /// /// The api key used for identification /// The api secret used for signing public ApiCredentials(SecureString key, SecureString secret) { + if (key == null || secret == null) + throw new ArgumentException("Key and secret can't be null/empty"); + Key = key; Secret = secret; } /// - /// Create Api credentials providing a api key and secret for authentication + /// Create Api credentials providing an api key and secret for authentication /// /// The api key used for identification /// The api secret used for signing diff --git a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs index 21857b1..c3524a9 100644 --- a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs +++ b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs @@ -24,15 +24,15 @@ namespace CryptoExchange.Net.Authentication } /// - /// Add authentication to the parameter list + /// Add authentication to the parameter list based on the provided credentials /// - /// - /// - /// - /// - /// - /// - /// + /// The uri the request is for + /// The HTTP method of the request + /// The provided parameters for the request + /// Wether or not the request needs to be signed. If not typically the parameters list can just be returned + /// Where post parameters are placed, in the URI or in the request body + /// How array parameters are serialized + /// Should return the original parameter list including any authentication parameters needed public virtual Dictionary AddAuthenticationToParameters(string uri, HttpMethod method, Dictionary parameters, bool signed, PostParameters postParameterPosition, ArrayParametersSerialization arraySerialization) { @@ -40,15 +40,15 @@ namespace CryptoExchange.Net.Authentication } /// - /// Add authentication to the header dictionary + /// Add authentication to the header dictionary based on the provided credentials /// - /// - /// - /// - /// - /// - /// - /// + /// The uri the request is for + /// The HTTP method of the request + /// The provided parameters for the request + /// Wether or not the request needs to be signed. If not typically the parameters list can just be returned + /// Where post parameters are placed, in the URI or in the request body + /// How array parameters are serialized + /// Should return a dictionary containing any header key/value pairs needed for authenticating the request public virtual Dictionary AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary parameters, bool signed, PostParameters postParameterPosition, ArrayParametersSerialization arraySerialization) { @@ -80,9 +80,9 @@ namespace CryptoExchange.Net.Authentication /// /// /// - protected string ByteToString(byte[] buff) + protected static string ByteToString(byte[] buff) { - var result = ""; + var result = string.Empty; foreach (var t in buff) result += t.ToString("X2"); /* hex format */ return result; diff --git a/CryptoExchange.Net/BaseClient.cs b/CryptoExchange.Net/BaseClient.cs index e4c60de..79ca452 100644 --- a/CryptoExchange.Net/BaseClient.cs +++ b/CryptoExchange.Net/BaseClient.cs @@ -2,6 +2,7 @@ using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Logging; using CryptoExchange.Net.Objects; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; @@ -16,7 +17,7 @@ using System.Threading.Tasks; namespace CryptoExchange.Net { /// - /// The base for all clients + /// The base for all clients, websocket client and rest client /// public abstract class BaseClient : IDisposable { @@ -25,9 +26,9 @@ namespace CryptoExchange.Net /// public string BaseAddress { get; } /// - /// The name of the client + /// The name of the exchange the client is for /// - public string ClientName { get; } + public string ExchangeName { get; } /// /// The log object /// @@ -37,16 +38,19 @@ namespace CryptoExchange.Net /// protected ApiProxy? apiProxy; /// - /// The auth provider + /// The authentication provider /// protected internal AuthenticationProvider? authProvider; /// /// Should check objects for missing properties based on the model and the received JSON /// public bool ShouldCheckObjects { get; set; } - /// - /// The last used id + /// If true, the CallResult and DataEvent objects should also contain the originally received json data in the OriginalDaa property + /// + public bool OutputOriginalData { get; private set; } + /// + /// The last used id, use NextId() to get the next id and up this /// protected static int lastId; /// @@ -54,6 +58,9 @@ namespace CryptoExchange.Net /// protected static object idLock = new object(); + /// + /// A default serializer + /// private static readonly JsonSerializer defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings { DateTimeZoneHandling = DateTimeZoneHandling.Utc, @@ -61,43 +68,44 @@ namespace CryptoExchange.Net }); /// - /// Last is used + /// Last id used /// public static int LastId => lastId; /// /// ctor /// - /// - /// - /// - protected BaseClient(string clientName, ClientOptions options, AuthenticationProvider? authenticationProvider) + /// The name of the exchange this client is for + /// The options for this client + /// The authentication provider for this client (can be null if no credentials are provided) + protected BaseClient(string exchangeName, ClientOptions options, AuthenticationProvider? authenticationProvider) { - log = new Log(clientName); + log = new Log(exchangeName); authProvider = authenticationProvider; log.UpdateWriters(options.LogWriters); - log.Level = options.LogVerbosity; + log.Level = options.LogLevel; - ClientName = clientName; + ExchangeName = exchangeName; + OutputOriginalData = options.OutputOriginalData; BaseAddress = options.BaseAddress; apiProxy = options.Proxy; - log.Write(LogVerbosity.Debug, $"Client configuration: {options}"); + log.Write(LogLevel.Debug, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {ExchangeName}.Net: v{GetType().Assembly.GetName().Version}"); ShouldCheckObjects = options.ShouldCheckObjects; } /// - /// Set the authentication provider + /// Set the authentication provider, can be used when manually setting the API credentials /// /// protected void SetAuthenticationProvider(AuthenticationProvider authenticationProvider) { - log.Write(LogVerbosity.Debug, "Setting api credentials"); + log.Write(LogLevel.Debug, "Setting api credentials"); authProvider = authenticationProvider; } /// - /// Tries to parse the json data and returns a token + /// Tries to parse the json data and returns a JToken, validating the input not being empty and being valid json /// /// The data to parse /// @@ -106,7 +114,7 @@ namespace CryptoExchange.Net if (string.IsNullOrEmpty(data)) { var info = "Empty data object received"; - log.Write(LogVerbosity.Error, info); + log.Write(LogLevel.Error, info); return new CallResult(null, new DeserializeError(info, data)); } @@ -126,7 +134,7 @@ namespace CryptoExchange.Net } catch (Exception ex) { - var exceptionInfo = GetExceptionInfo(ex); + var exceptionInfo = ex.ToLogString(); var info = $"Deserialize Unknown Exception: {exceptionInfo}"; return new CallResult(null, new DeserializeError(info, data)); } @@ -139,14 +147,14 @@ namespace CryptoExchange.Net /// The data to deserialize /// Whether or not the parsing should be checked for missing properties (will output data to the logging if log verbosity is Debug) /// A specific serializer to use - /// Id of the request + /// Id of the request the data is returned from (used for grouping logging by request) /// protected CallResult Deserialize(string data, bool? checkObject = null, JsonSerializer? serializer = null, int? requestId = null) { var tokenResult = ValidateJson(data); if (!tokenResult) { - log.Write(LogVerbosity.Error, tokenResult.Error!.Message); + log.Write(LogLevel.Error, tokenResult.Error!.Message); return new CallResult(default, tokenResult.Error); } @@ -160,7 +168,7 @@ namespace CryptoExchange.Net /// The data to deserialize /// Whether or not the parsing should be checked for missing properties (will output data to the logging if log verbosity is Debug) /// A specific serializer to use - /// A request identifier + /// Id of the request the data is returned from (used for grouping logging by request) /// protected CallResult Deserialize(JToken obj, bool? checkObject = null, JsonSerializer? serializer = null, int? requestId = null) { @@ -169,8 +177,10 @@ namespace CryptoExchange.Net try { - if ((checkObject ?? ShouldCheckObjects)&& log.Level == LogVerbosity.Debug) + if ((checkObject ?? ShouldCheckObjects)&& log.Level <= LogLevel.Debug) { + // This checks the input JToken object against the class it is being serialized into and outputs any missing fields + // in either the input or the class try { if (obj is JObject o) @@ -185,7 +195,7 @@ namespace CryptoExchange.Net } catch (Exception e) { - log.Write(LogVerbosity.Debug, $"{(requestId != null ? $"[{ requestId}] " : "")}Failed to check response data: " + (e.InnerException?.Message ?? e.Message)); + log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{ requestId}] " : "")}Failed to check response data: " + (e.InnerException?.Message ?? e.Message)); } } @@ -194,20 +204,20 @@ namespace CryptoExchange.Net catch (JsonReaderException jre) { var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}"; - log.Write(LogVerbosity.Error, info); + log.Write(LogLevel.Error, info); return new CallResult(default, new DeserializeError(info, obj)); } catch (JsonSerializationException jse) { var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}"; - log.Write(LogVerbosity.Error, info); + log.Write(LogLevel.Error, info); return new CallResult(default, new DeserializeError(info, obj)); } catch (Exception ex) { - var exceptionInfo = GetExceptionInfo(ex); + var exceptionInfo = ex.ToLogString(); var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}"; - log.Write(LogVerbosity.Error, info); + log.Write(LogLevel.Error, info); return new CallResult(default, new DeserializeError(info, obj)); } } @@ -218,24 +228,33 @@ namespace CryptoExchange.Net /// The type to deserialize into /// The stream to deserialize /// A specific serializer to use - /// Id of the request - /// Milliseconds response time + /// Id of the request the data is returned from (used for grouping logging by request) + /// Milliseconds response time for the request this stream is a response for /// - protected async Task> Deserialize(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null) + protected async Task> DeserializeAsync(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null) { if (serializer == null) serializer = defaultSerializer; try { + // Let the reader keep the stream open so we're able to seek if needed. The calling method will close the stream. using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true); - if (log.Level == LogVerbosity.Debug) + + // If we have to output the original json data or output the data into the logging we'll have to read to full response + // in order to log/return the json data + if (OutputOriginalData || log.Level <= LogLevel.Debug) { var data = await reader.ReadToEndAsync().ConfigureAwait(false); - log.Write(LogVerbosity.Debug, $"{(requestId != null ? $"[{requestId}] ": "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms: {data}"); - return Deserialize(data, null, serializer, requestId); + log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] ": "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms: {data}"); + var result = Deserialize(data, null, serializer, requestId); + if(OutputOriginalData) + result.OriginalData = data; + return result; } + // If we don't have to keep track of the original json data we can use the JsonTextReader to deserialize the stream directly + // into the desired object, which has increased performance over first reading the string value into memory and deserializing from that using var jsonReader = new JsonTextReader(reader); return new CallResult(serializer.Deserialize(jsonReader), null); } @@ -244,12 +263,13 @@ namespace CryptoExchange.Net string data; if (stream.CanSeek) { + // If we can seek the stream rewind it so we can retrieve the original data that was sent stream.Seek(0, SeekOrigin.Begin); - data = await ReadStream(stream).ConfigureAwait(false); + data = await ReadStreamAsync(stream).ConfigureAwait(false); } else - data = "[Data only available in Debug LogVerbosity]"; - log.Write(LogVerbosity.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}"); + data = "[Data only available in Debug LogLevel]"; + log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}"); return new CallResult(default, new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data)); } catch (JsonSerializationException jse) @@ -258,12 +278,12 @@ namespace CryptoExchange.Net if (stream.CanSeek) { stream.Seek(0, SeekOrigin.Begin); - data = await ReadStream(stream).ConfigureAwait(false); + data = await ReadStreamAsync(stream).ConfigureAwait(false); } else - data = "[Data only available in Debug LogVerbosity]"; + data = "[Data only available in Debug LogLevel]"; - log.Write(LogVerbosity.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}"); + log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}"); return new CallResult(default, new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data)); } catch (Exception ex) @@ -271,18 +291,18 @@ namespace CryptoExchange.Net string data; if (stream.CanSeek) { stream.Seek(0, SeekOrigin.Begin); - data = await ReadStream(stream).ConfigureAwait(false); + data = await ReadStreamAsync(stream).ConfigureAwait(false); } else - data = "[Data only available in Debug LogVerbosity]"; + data = "[Data only available in Debug LogLevel]"; - var exceptionInfo = GetExceptionInfo(ex); - log.Write(LogVerbosity.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}"); + var exceptionInfo = ex.ToLogString(); + log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}"); return new CallResult(default, new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data)); } } - private async Task ReadStream(Stream stream) + private async Task ReadStreamAsync(Stream stream) { using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true); return await reader.ReadToEndAsync().ConfigureAwait(false); @@ -302,7 +322,7 @@ namespace CryptoExchange.Net if (!obj.HasValues && type != typeof(object)) { - log.Write(LogVerbosity.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Expected `{type.Name}`, but received object was empty"); + log.Write(LogLevel.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Expected `{type.Name}`, but received object was empty"); return; } @@ -329,7 +349,7 @@ namespace CryptoExchange.Net { if (!(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))) { - log.Write(LogVerbosity.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Local object doesn't have property `{token.Key}` expected in type `{type.Name}`"); + log.Write(LogLevel.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Local object doesn't have property `{token.Key}` expected in type `{type.Name}`"); isDif = true; } continue; @@ -359,11 +379,11 @@ namespace CryptoExchange.Net continue; isDif = true; - log.Write(LogVerbosity.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Local object has property `{prop}` but was not found in received object of type `{type.Name}`"); + log.Write(LogLevel.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Local object has property `{prop}` but was not found in received object of type `{type.Name}`"); } if (isDif) - log.Write(LogVerbosity.Debug, $"{(requestId != null ? $"[{ requestId}] " : "")}Returned data: " + obj); + log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{ requestId}] " : "")}Returned data: " + obj); } private static PropertyInfo? GetProperty(string name, IEnumerable props) @@ -399,7 +419,7 @@ namespace CryptoExchange.Net } /// - /// Generate a unique id + /// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique across different client instances /// /// protected int NextId() @@ -431,37 +451,13 @@ namespace CryptoExchange.Net return path; } - - /// - /// Get's all exception messages from a nested exception - /// - /// - /// - public static string GetExceptionInfo(Exception ex) - { - string result = ""; - var padding = 0; - while (true) - { - result += ex.Message.PadLeft(ex.Message.Length + padding) + Environment.NewLine; - - if (ex.InnerException == null) - break; - - ex = ex.InnerException; - padding += 2; - } - - return result; - } - /// /// Dispose /// public virtual void Dispose() { authProvider?.Credentials?.Dispose(); - log.Write(LogVerbosity.Debug, "Disposing exchange client"); + log.Write(LogLevel.Debug, "Disposing exchange client"); } } } diff --git a/CryptoExchange.Net/Converters/ArrayConverter.cs b/CryptoExchange.Net/Converters/ArrayConverter.cs index 40307ff..f46cfb3 100644 --- a/CryptoExchange.Net/Converters/ArrayConverter.cs +++ b/CryptoExchange.Net/Converters/ArrayConverter.cs @@ -11,7 +11,8 @@ using Newtonsoft.Json.Linq; namespace CryptoExchange.Net.Converters { /// - /// Converter for arrays to properties + /// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties + /// with [ArrayProperty(x)] where x is the index of the property in the array /// public class ArrayConverter : JsonConverter { diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index ac3304b..e6c3101 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -1,4 +1,4 @@ - + netstandard2.0;netstandard2.1 @@ -6,16 +6,16 @@ CryptoExchange.Net JKorf A base package for implementing cryptocurrency exchange API's - 3.9.0 - 3.9.0 - 3.9.0 + 4.0.0 + 4.0.0 + 4.0.0 false git https://github.com/JKorf/CryptoExchange.Net.git https://github.com/JKorf/CryptoExchange.Net en true - 3.9.0 - Added optional JsonSerializer parameter to SendRequest to use during deserialization, Fix for unhandled message warning when unsubscribing a socket subscription + 4.0.0 enable 8.0 MIT @@ -37,7 +37,15 @@ CryptoExchange.Net.xml + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + \ No newline at end of file diff --git a/CryptoExchange.Net/CryptoExchange.Net.xml b/CryptoExchange.Net/CryptoExchange.Net.xml index 0bb5baf..f9a1a0f 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.xml +++ b/CryptoExchange.Net/CryptoExchange.Net.xml @@ -42,14 +42,14 @@ - Create Api credentials providing a api key and secret for authentication + Create Api credentials providing an api key and secret for authentication The api key used for identification The api secret used for signing - Create Api credentials providing a api key and secret for authentication + Create Api credentials providing an api key and secret for authentication The api key used for identification The api secret used for signing @@ -99,27 +99,27 @@ - Add authentication to the parameter list + Add authentication to the parameter list based on the provided credentials - - - - - - - + The uri the request is for + The HTTP method of the request + The provided parameters for the request + Wether or not the request needs to be signed. If not typically the parameters list can just be returned + Where post parameters are placed, in the URI or in the request body + How array parameters are serialized + Should return the original parameter list including any authentication parameters needed - Add authentication to the header dictionary + Add authentication to the header dictionary based on the provided credentials - - - - - - - + The uri the request is for + The HTTP method of the request + The provided parameters for the request + Wether or not the request needs to be signed. If not typically the parameters list can just be returned + Where post parameters are placed, in the URI or in the request body + How array parameters are serialized + Should return a dictionary containing any header key/value pairs needed for authenticating the request @@ -201,7 +201,7 @@ - The base for all clients + The base for all clients, websocket client and rest client @@ -209,9 +209,9 @@ The address of the client - + - The name of the client + The name of the exchange the client is for @@ -226,7 +226,7 @@ - The auth provider + The authentication provider @@ -234,9 +234,14 @@ Should check objects for missing properties based on the model and the received JSON + + + If true, the CallResult and DataEvent objects should also contain the originally received json data in the OriginalDaa property + + - The last used id + The last used id, use NextId() to get the next id and up this @@ -244,28 +249,33 @@ Lock for id generating + + + A default serializer + + - Last is used + Last id used ctor - - - + The name of the exchange this client is for + The options for this client + The authentication provider for this client (can be null if no credentials are provided) - Set the authentication provider + Set the authentication provider, can be used when manually setting the API credentials - Tries to parse the json data and returns a token + Tries to parse the json data and returns a JToken, validating the input not being empty and being valid json The data to parse @@ -278,7 +288,7 @@ The data to deserialize Whether or not the parsing should be checked for missing properties (will output data to the logging if log verbosity is Debug) A specific serializer to use - Id of the request + Id of the request the data is returned from (used for grouping logging by request) @@ -289,23 +299,23 @@ The data to deserialize Whether or not the parsing should be checked for missing properties (will output data to the logging if log verbosity is Debug) A specific serializer to use - A request identifier + Id of the request the data is returned from (used for grouping logging by request) - + Deserialize a stream into an object The type to deserialize into The stream to deserialize A specific serializer to use - Id of the request - Milliseconds response time + Id of the request the data is returned from (used for grouping logging by request) + Milliseconds response time for the request this stream is a response for - Generate a unique id + Generate a new unique id. The id is staticly stored so it is guarenteed to be unique across different client instances @@ -317,13 +327,6 @@ The values to fill - - - Get's all exception messages from a nested exception - - - - Dispose @@ -331,7 +334,8 @@ - Converter for arrays to properties + Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties + with [ArrayProperty(x)] where x is the index of the property in the array @@ -462,6 +466,66 @@ + + + General helpers functions + + + + + Clamp a value between a min and max + + + + + + + + + Adjust a value to be between the min and max parameters and rounded to the closest step. + + The min value + The max value + The step size the value should be floored to. For example, value 2.548 with a step size of 0.01 will output 2.54 + How to round + The input value + + + + + Adjust a value to be between the min and max parameters and rounded to the closest precision. + + The min value + The max value + The precision the value should be rounded to. For example, value 2.554215 with a precision of 5 will output 2.5542 + How to round + The input value + + + + + Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12 + + The value to round + The total amount of digits (NOT decimal places) to round to + How to round + + + + + Rounds a value down to + + + + + + + + Strips any trailing zero's of a decimal value, useful when converting the value to string. + + + + Common balance @@ -512,11 +576,26 @@ The asset fee was paid in + + + Trade time + + Shared interface for exchange wrappers based on the CryptoExchange.Net package + + + Should be triggered on order placing + + + + + Should be triggered on order cancelling + + Get the symbol name based on a base and quote asset @@ -661,6 +740,26 @@ Sell order + + + Common order status + + + + + placed and not fully filled order + + + + + cancelled order + + + + + filled order + + Common kline @@ -736,6 +835,11 @@ Type of the order + + + order time + + Common order book @@ -987,6 +1091,13 @@ The value of the object Name of the parameter + + + Format an exception and inner exception to a readable string + + + + Rate limiter interface @@ -1052,7 +1163,7 @@ - + Get the response @@ -1101,7 +1212,7 @@ The response headers - + Get the response stream @@ -1142,7 +1253,7 @@ The base address of the API - + Client name @@ -1209,14 +1320,14 @@ - + Unsubscribe from a stream The subscription to unsubscribe - + Unsubscribe all subscriptions @@ -1272,6 +1383,11 @@ The number of bids in the book + + + Get a snapshot of the book at this moment + + The list of asks @@ -1297,30 +1413,27 @@ BestBid/BesAsk returned as a pair - - - Start connecting and synchronizing the order book - - - Start connecting and synchronizing the order book - - - Stop syncing the order book - - - Stop syncing the order book + + + Get the average price that a market order would fill at at the current order book state. This is no guarentee that an order of that quantity would actually be filled + at that price since between this calculation and the order placement the book can have changed. + + The quantity in base asset to fill + The type + Average fill price + Interface for order book entries @@ -1381,6 +1494,11 @@ Origin + + + Encoding to use + + Reconnecting @@ -1401,11 +1519,6 @@ Socket url - - - State - - Is closed @@ -1426,7 +1539,7 @@ Timeout - + Connect the socket @@ -1443,18 +1556,17 @@ Reset socket - + Close the connecting - + Set proxy - - + @@ -1479,15 +1591,32 @@ - + + + Log to console + + + + + + + + + + + + Default log writer, writes to debug - + - + + + + @@ -1495,9 +1624,14 @@ Log implementation + + + List of ILogger implementations to forward the message to + + - The verbosity of the logging + The verbosity of the logging, anything more verbose will not be forwarded to the writers @@ -1509,72 +1643,20 @@ ctor + The name of the client the logging is used in - + Set the writers - + Write a log entry - - - - - - The log verbosity - - - - - Debug logging - - - - - Info logging - - - - - Warning logging - - - - - Error logging - - - - - None, used for disabling logging - - - - - File writer - - - - - - - - ctor - - - - - - - - - Dispose - - + The verbosity of the message + The message to log @@ -1646,7 +1728,7 @@ - An error if the call didn't succeed + An error if the call didn't succeed, will always be filled if Success = false @@ -1681,7 +1763,12 @@ - The data returned by the call + The data returned by the call, only available when Success = true + + + + + The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options @@ -1731,17 +1818,17 @@ ctor - - - + Status code + Response headers + Error Create an error result - - - + Status code + Response headers + Error @@ -1776,18 +1863,22 @@ - + - Create new based on existing + ctor - + + + + + - + - Create from a call result + Copy the WebCallResult to a new data type - - + The new type + The data of the new type @@ -1831,7 +1922,7 @@ - Where the post parameters should be added + Where the parameters for a post request should be added @@ -1919,6 +2010,21 @@ Create an []=value array + + + How to round + + + + + Round down (flooring) + + + + + Round to closest value + + Base class for errors @@ -1965,7 +2071,7 @@ - No api credentials provided while trying to access private endpoint + No api credentials provided while trying to access a private endpoint @@ -2069,14 +2175,25 @@ ctor + + + Invalid operation requested + + + + + ctor + + + Base options - + - The log verbosity + The minimum log level to output. Setting it to null will send all messages to the registered ILoggers. @@ -2084,6 +2201,11 @@ The log writers + + + If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property + + @@ -2105,11 +2227,12 @@ Whether or not a level should be removed from the book when it's pushed out of scope of the limit. For example with a book of limit 10, - when a new bid is added which makes the total amount of bids 11, should the last bid entry be removed + when a new bid level is added which makes the total amount of bids 11, should the last bid entry be removed + ctor The name of the order book implementation Whether each update should have a consecutive id number. Used to identify and reconnect when numbers are skipped. @@ -2148,7 +2271,7 @@ ctor - + The base address to use @@ -2175,7 +2298,7 @@ - Http client to use. If a HttpClient is provided in this property the RequestTimeout and Proxy options will be ignored and should be set on the provided HttpClient instance + Http client to use. If a HttpClient is provided in this property the RequestTimeout and Proxy options will be ignored in requests and should be set on the provided HttpClient instance @@ -2218,25 +2341,26 @@ - The time to wait for a socket response + The time to wait for a socket response before giving a timeout - The time after which the connection is assumed to be dropped + The time after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected. The amount of subscriptions that should be made on a single socket connection. Not all exchanges support multiple subscriptions on a single socket. - Setting this to a higher number increases subscription speed, but having more subscriptions on a single connection will also increase the amount of traffic on that single connection. + Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a + single connection will also increase the amount of traffic on that single connection, potentially leading to issues. ctor - + The base address to use @@ -2368,6 +2492,11 @@ The list of bids + + + Get a snapshot of the book at this moment + + The best bid currently in the order book @@ -2390,23 +2519,20 @@ - - - Start connecting and synchronizing the order book - - - Start connecting and synchronizing the order book - + - Stop syncing the order book + Get the average price that a market order would fill at at the current order book state. This is no guarentee that an order of that quantity would actually be filled + at that price since between this calculation and the order placement the book can have changed. - + The quantity in base asset to fill + The type + Average fill price @@ -2414,7 +2540,7 @@ - + Start the order book @@ -2425,7 +2551,7 @@ Reset the order book - + Resync the order book @@ -2489,7 +2615,7 @@ Type of entry The entry - + Wait until the order book has been set @@ -2645,7 +2771,7 @@ - + @@ -2679,7 +2805,7 @@ The actual response - + @@ -2697,7 +2823,7 @@ - Where to place post parameters + Where to place post parameters by default @@ -2712,22 +2838,22 @@ - How to serialize array parameters + How to serialize array parameters when making requests - What request body should be when no data is send + What request body should be set when no data is send (only used in combination with postParametersPosition.InBody) - Timeout for requests + Timeout for requests. This setting is ignored when injecting a HttpClient in the options, requests timeouts should be set on the client then. - Rate limiting behaviour + What should happen when running into a rate limit @@ -2737,16 +2863,16 @@ - Total requests made + Total requests made by this client ctor - - - + The name of the exchange this client is for + The options for this client + The authentication provider for this client (can be null if no credentials are provided) @@ -2771,36 +2897,37 @@ The roundtrip time of the ping request - + - Execute a request + Execute a request to the uri and deserialize the response into the provided type parameter - The expected result type + The type to deserialize into The uri to send the request to The method of the request Cancellation token The parameters of the request Whether or not the request should be authenticated Whether or not the resulting object should be checked for missing properties in the mapping (only outputs if log verbosity is Debug) - Where the post parameters should be placed - How array parameters should be serialized + Where the post parameters should be placed, overwrites the value set in the client + How array parameters should be serialized, overwrites the value set in the client Credits used for the request The JsonSerializer to use for deserialization - + - Executes the request and returns the string result + Executes the request and returns the result deserialized into the type parameter class The request object to execute The JsonSerializer to use for deserialization Cancellation token - + Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error. - This can be used together with ManualParseError to check if it is an error before deserializing to an object + When setting manualParseError to true this method will be called for each response to be able to check if the response is an error or not. + If the response is an error this method should return the parsed error, else it should return null Received data Null if not an error, Error otherwise @@ -2820,11 +2947,11 @@ - Writes the parameters of the request to the request object, either in the query string or the request body + Writes the parameters of the request to the request object body - - - + The request to set the parameters on + The parameters to set + The content type of the data @@ -2850,6 +2977,7 @@ + Semaphore used while creating sockets @@ -2874,32 +3002,32 @@ - Handler for byte data + Delegate used for processing byte data received from socket connections before it is processed by handlers - Handler for string data + Delegate used for processing string data received from socket connections before it is processed by handlers - Generic handlers + Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example. - Periodic task + The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similair. Not necesarry. - Periodic task event + Wait event for the periodicTask - Is disposing + If client is disposing @@ -2915,63 +3043,63 @@ - Create a socket client + ctor - Client name - Client options - Authentication provider + The name of the exchange this client is for + The options for this client + The authentication provider for this client (can be null if no credentials are provided) - Set a function to interpret the data, used when the data is received as bytes instead of a string + Set a delegate to be used for processing data received from socket connections before it is processed by handlers Handler for byte data Handler for string data - + - Subscribe + Connect to an url and listen for data on the BaseAddress - The expected return data - The request to send - The identifier to use - If the subscription should be authenticated + The type of the expected data + The optional request object to send, will be serialized to json + The identifier to use, necessary if no request object is sent + If the subscription is to an authenticated endpoint The handler of update data - + - Subscribe using a specif URL + Connect to an url and listen for data The type of the expected data The URL to connect to - The request to send - The identifier to use - If the subscription should be authenticated + The optional request object to send, will be serialized to json + The identifier to use, necessary if no request object is sent + If the subscription is to an authenticated endpoint The handler of update data - + Sends the subscribe request and waits for a response to that request - The connection to send the request on - The request to send + The connection to send the request on + The request to send, will be serialized to json The subscription the request is for - + - Query for data + Send a query on a socket connection to the BaseAddress and wait for the response Expected result type - The request to send - Whether the socket should be authenticated + The request to send, will be serialized to json + If the query is to an authenticated endpoint - + - Query for data + Send a query on a socket connection and wait for the response The expected result type The url for the request @@ -2979,7 +3107,7 @@ Whether the socket should be authenticated - + Sends the query request and waits for the result @@ -2988,9 +3116,9 @@ The request to send - + - Checks if a socket needs to be connected and does so if needed + Checks if a socket needs to be connected and does so if needed. Also authenticates on the socket if needed The connection to check Whether the socket should authenticated @@ -2998,55 +3126,65 @@ - Needs to check if a received message was an answer to a query request (preferable by id) and set the callResult out to whatever the response is + The socketConnection received data (the data JToken parameter). The implementation of this method should check if the received data is a response to the query that was send (the request parameter). + For example; A query is sent in a request message with an Id parameter with value 10. The socket receives data and calls this method to see if the data it received is an + anwser to any query that was done. The implementation of this method should check if the response.Id == request.Id to see if they match (assuming the api has some sort of Id tracking on messages, + if not some other method has be implemented to match the messages). + If the messages match, the callResult out parameter should be set with the deserialized data in the from of (T) and return true. - The type of response - The socket connection + The type of response that is expected on the query + The socket connection The request that a response is awaited for - The message + The message received from the server The interpretation (null if message wasn't a response to the request) True if the message was a response to the query - Needs to check if a received message was an answer to a subscription request (preferable by id) and set the callResult out to whatever the response is + The socketConnection received data (the data JToken parameter). The implementation of this method should check if the received data is a response to the subscription request that was send (the request parameter). + For example; A subscribe request message is send with an Id parameter with value 10. The socket receives data and calls this method to see if the data it received is an + anwser to any subscription request that was done. The implementation of this method should check if the response.Id == request.Id to see if they match (assuming the api has some sort of Id tracking on messages, + if not some other method has be implemented to match the messages). + If the messages match, the callResult out parameter should be set with the deserialized data in the from of (T) and return true. - The socket connection - - The request that a response is awaited for - The message + The socket connection + A subscription that waiting for a subscription response + The request that the subscription sent + The message received from the server The interpretation (null if message wasn't a response to the request) True if the message was a response to the subscription request - Needs to check if a received message matches a handler. Typically if an update message matches the request + Needs to check if a received message matches a handler by request. After subscribing data message will come in. These data messages need to be matched to a specific connection + to pass the correct data to the correct handler. The implementation of this method should check if the message received matches the subscribe request that was sent. The received data The subscription request - + True if the message is for the subscription which sent the request - Needs to check if a received message matches a handler. Typically if an received message matches a ping request or a other information pushed from the the server + Needs to check if a received message matches a handler by identifier. Generally used by GenericHandlers. For example; a generic handler is registered which handles ping messages + from the server. This method should check if the message received is a ping message and the identifer is the identifier of the GenericHandler The received data The string identifier of the handler - + True if the message is for the handler which has the identifier - + Needs to authenticate the socket so authenticated queries/subscriptions can be made on this socket connection - + The socket connection that should be authenticated - + Needs to unsubscribe a subscription, typically by sending an unsubscribe request. If multiple subscriptions per socket is not allowed this can just return since the socket will be closed anyway The connection on which to unsubscribe - The subscription to unsubscribe + The subscription to unsubscribe @@ -3056,9 +3194,9 @@ - + - Add a handler for a subscription + Add a subscription to a connection The type of data the subscription expects The request of the subscription @@ -3068,14 +3206,14 @@ The handler of the data received - + Adds a generic message handler. Used for example to reply to ping requests The name of the request handler. Needs to be unique The action to execute when receiving a message for this handler (checked by ) - + Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one. @@ -3089,7 +3227,7 @@ The token that wasn't processed - + Connect a socket @@ -3105,19 +3243,19 @@ - Periodically sends an object to a socket + Periodically sends data over a socket connection How often Method returning the object to send - + - Unsubscribe from a stream + Unsubscribe an update subscription The subscription to unsubscribe - + Unsubscribe all subscriptions @@ -3128,192 +3266,301 @@ Dispose the client - + - Socket implementation + A wrapper around the ClientWebSocket - - - Socket - - - + Log - + - Error handlers + Handlers for when an error happens on the socket - + - Open handlers + Handlers for when the socket connection is opened - + - Close handlers + Handlers for when the connection is closed - + - Message handlers + Handlers for when a message is received - + - Id + The id of this socket - + + + + - If is reconnecting + Whether this socket is currently reconnecting - + - Origin + The timestamp this socket has been active for the last time - + - Url + Delegate used for processing byte data received from socket connections before it is processed by handlers - + - Is closed + Delegate used for processing string data received from socket connections before it is processed by handlers - + - Is open + Url this socket connects to - + - Protocols + If the connection is closed - + - Interpreter for bytes + If the connection is open - + - Interpreter for strings + Ssl protocols supported. NOT USED BY THIS IMPLEMENTATION - + - Last action time + Encoding used for decoding the received bytes into a string - + - Timeout + The timespan no data is received on the socket. If no data is received within this time an error is generated - + - Socket state + Socket closed event - + + + Socket message received event + + + + + Socket error event + + + + + Socket opened event + + + ctor - - + The log object to use + The url the socket should connect to - + ctor - - - - + The log object to use + The url the socket should connect to + Cookies to sent in the socket connection request + Headers to sent in the socket connection request - + - On close + Set a proxy to use. Should be set before connecting + + + + + + Connect the websocket + + True if successfull + + + + Send data over the websocket + + Data to send + + + + Close the websocket + + + + + + Internal close method, will wait for each task to complete to gracefully close + + + + + + + + Dispose the socket - + - On message + Reset the socket so a new connection can be attempted after it has been connected before - + - On error + Create the socket object - + - On open + Loop for sending data + - + - Handle + Loop for receiving and reassembling data + + + + + + Handles the message + + + + + + + + + Checks if there is no data received for a period longer than the specified timeout + + + + + + Helper to invoke handlers - + - Handle + Helper to invoke handlers - + - Checks if timed out + Get the next identifier - + - Close socket + An update received from a socket update subscription + The type of the data + + + + The timestamp the data was received + + + + + The topic of the update, what symbol/asset etc.. + + + + + The original data that was received, only available when OutputOriginalData is set to true in the client options + + + + + The received data deserialized into an object + + + + + Create a new DataEvent with data in the from of type K based on the current DataEvent. Topic, OriginalData and Timestamp will be copied over + + The type of the new data + The new data - + - Reset socket - - - - - Send data - - - - - - Connect socket + Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and Timestamp will be copied over + The type of the new data + The new data + The new topic - + - Set a proxy + Message received event - - - + - Dispose + The connection the message was received on + + + The json object of the data + + + + + The originally received string data + + + + + The timestamp of when the data was received + + + + + + + + + + + Socket connecting @@ -3349,9 +3596,9 @@ Unhandled message event - + - The amount of handlers + The amount of subscriptions on this connection @@ -3366,12 +3613,12 @@ - The socket + The underlying socket - If should reconnect upon closing + If the socket should be reconnected upon closing @@ -3391,17 +3638,23 @@ The socket client The socket - + - Add handler + Process a message received by the socket - + - + - Send data + Add subscription to this connection - The data type + + + + + Send data and wait for an answer + + The data type expected in response The object to send The timeout for response The response handler @@ -3409,7 +3662,7 @@ - Send data to the websocket + Send data over the websocket connection The type of the object to send The object to send @@ -3417,7 +3670,7 @@ - Send string data to the websocket + Send string data over the websocket connection The data to send @@ -3426,15 +3679,15 @@ Handler for a socket closing. Reconnects the socket if needed, or removes it from the active socket list if not - + Close the connection - + - Close the subscription + Close a subscription on this connection. If all subscriptions on this connection are closed the connection gets closed as well Subscription to close @@ -3474,7 +3727,7 @@ If the subscription has been confirmed - + Create SocketSubscription for a request @@ -3483,7 +3736,7 @@ - + Create SocketSubscription for an identifier @@ -3500,7 +3753,7 @@ - Subscription + Subscription to a data stream @@ -3510,22 +3763,24 @@ - Event when the connection is restored. Timespan parameter indicates the time the socket has been offline for before reconnecting + Event when the connection is restored. Timespan parameter indicates the time the socket has been offline for before reconnecting. + Note that when the executing code is suspended and resumed at a later period (for example laptop going to sleep) the disconnect time will be incorrect as the diconnect + will only be detected after resuming. This will lead to an incorrect disconnected timespan. - Event when the connection to the server is paused. No operations can be performed while paused + Event when the connection to the server is paused based on a server indication. No operations can be performed while paused - Event when the connection to the server is unpaused + Event when the connection to the server is unpaused after being paused - Event when an exception happened + Event when an exception happens during the handling of the data @@ -3537,16 +3792,16 @@ ctor - - + The socket connection the subscription is on + The subscription - + Close the subscription - + Close the socket to cause a reconnect @@ -3554,7 +3809,7 @@ - Factory implementation + Default weboscket factory implementation diff --git a/CryptoExchange.Net/ExchangeHelpers.cs b/CryptoExchange.Net/ExchangeHelpers.cs new file mode 100644 index 0000000..421de11 --- /dev/null +++ b/CryptoExchange.Net/ExchangeHelpers.cs @@ -0,0 +1,120 @@ +using CryptoExchange.Net.Objects; +using System; + +namespace CryptoExchange.Net +{ + /// + /// General helpers functions + /// + public static class ExchangeHelpers + { + /// + /// Clamp a value between a min and max + /// + /// + /// + /// + /// + public static decimal ClampValue(decimal min, decimal max, decimal value) + { + value = Math.Min(max, value); + value = Math.Max(min, value); + return value; + } + + /// + /// Adjust a value to be between the min and max parameters and rounded to the closest step. + /// + /// The min value + /// The max value + /// The step size the value should be floored to. For example, value 2.548 with a step size of 0.01 will output 2.54 + /// How to round + /// The input value + /// + public static decimal AdjustValueStep(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal value) + { + if(step == 0) + throw new ArgumentException($"0 not allowed for parameter {nameof(step)}, pass in null to ignore the step size", nameof(step)); + + value = Math.Min(max, value); + value = Math.Max(min, value); + if (step == null) + return value; + + var offset = value % step.Value; + if(roundingType == RoundingType.Down) + value -= offset; + else + { + if (offset < step / 2) + value -= offset; + else value += (step.Value - offset); + } + + value = RoundDown(value, 8); + + return value.Normalize(); + } + + /// + /// Adjust a value to be between the min and max parameters and rounded to the closest precision. + /// + /// The min value + /// The max value + /// The precision the value should be rounded to. For example, value 2.554215 with a precision of 5 will output 2.5542 + /// How to round + /// The input value + /// + public static decimal AdjustValuePrecision(decimal min, decimal max, int? precision, RoundingType roundingType, decimal value) + { + value = Math.Min(max, value); + value = Math.Max(min, value); + if (precision == null) + return value; + + return RoundToSignificantDigits(value, precision.Value, roundingType); + } + + /// + /// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12 + /// + /// The value to round + /// The total amount of digits (NOT decimal places) to round to + /// How to round + /// + public static decimal RoundToSignificantDigits(decimal value, int digits, RoundingType roundingType) + { + var val = (double)value; + if (value == 0) + return 0; + + double scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(val))) + 1); + if(roundingType == RoundingType.Closest) + return (decimal)(scale * Math.Round(val / scale, digits)); + else + return (decimal)(scale * (double)RoundDown((decimal)(val / scale), digits)); + } + + /// + /// Rounds a value down to + /// + /// + /// + /// + public static decimal RoundDown(decimal i, double decimalPlaces) + { + var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces)); + return Math.Floor(i * power) / power; + } + + /// + /// Strips any trailing zero's of a decimal value, useful when converting the value to string. + /// + /// + /// + public static decimal Normalize(this decimal value) + { + return value / 1.000000000000000000000000000000000m; + } + } +} diff --git a/CryptoExchange.Net/ExchangeInterfaces/ICommonBalance.cs b/CryptoExchange.Net/ExchangeInterfaces/ICommonBalance.cs index abf6ab4..22b6802 100644 --- a/CryptoExchange.Net/ExchangeInterfaces/ICommonBalance.cs +++ b/CryptoExchange.Net/ExchangeInterfaces/ICommonBalance.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace CryptoExchange.Net.ExchangeInterfaces +namespace CryptoExchange.Net.ExchangeInterfaces { /// /// Common balance diff --git a/CryptoExchange.Net/ExchangeInterfaces/ICommonTrade.cs b/CryptoExchange.Net/ExchangeInterfaces/ICommonTrade.cs index b1a424f..3ac0d49 100644 --- a/CryptoExchange.Net/ExchangeInterfaces/ICommonTrade.cs +++ b/CryptoExchange.Net/ExchangeInterfaces/ICommonTrade.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace CryptoExchange.Net.ExchangeInterfaces { @@ -29,5 +27,9 @@ namespace CryptoExchange.Net.ExchangeInterfaces /// The asset fee was paid in /// public string? CommonFeeAsset { get; } + /// + /// Trade time + /// + DateTime CommonTradeTime { get; } } } diff --git a/CryptoExchange.Net/ExchangeInterfaces/IExchangeClient.cs b/CryptoExchange.Net/ExchangeInterfaces/IExchangeClient.cs index 041f4ea..395676d 100644 --- a/CryptoExchange.Net/ExchangeInterfaces/IExchangeClient.cs +++ b/CryptoExchange.Net/ExchangeInterfaces/IExchangeClient.cs @@ -10,6 +10,15 @@ namespace CryptoExchange.Net.ExchangeInterfaces /// public interface IExchangeClient { + /// + /// Should be triggered on order placing + /// + event Action OnOrderPlaced; + /// + /// Should be triggered on order cancelling + /// + event Action OnOrderCanceled; + /// /// Get the symbol name based on a base and quote asset /// @@ -146,5 +155,23 @@ namespace CryptoExchange.Net.ExchangeInterfaces /// Sell } + /// + /// Common order status + /// + public enum OrderStatus + { + /// + /// placed and not fully filled order + /// + Active, + /// + /// cancelled order + /// + Canceled, + /// + /// filled order + /// + Filled + } } } diff --git a/CryptoExchange.Net/ExchangeInterfaces/IKline.cs b/CryptoExchange.Net/ExchangeInterfaces/IKline.cs index f5aff71..c8548bf 100644 --- a/CryptoExchange.Net/ExchangeInterfaces/IKline.cs +++ b/CryptoExchange.Net/ExchangeInterfaces/IKline.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace CryptoExchange.Net.ExchangeInterfaces { diff --git a/CryptoExchange.Net/ExchangeInterfaces/IOrder.cs b/CryptoExchange.Net/ExchangeInterfaces/IOrder.cs index cd93a5f..ed526a3 100644 --- a/CryptoExchange.Net/ExchangeInterfaces/IOrder.cs +++ b/CryptoExchange.Net/ExchangeInterfaces/IOrder.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace CryptoExchange.Net.ExchangeInterfaces { @@ -24,7 +22,7 @@ namespace CryptoExchange.Net.ExchangeInterfaces /// /// Status of the order /// - public string CommonStatus { get; } + public IExchangeClient.OrderStatus CommonStatus { get; } /// /// Whether the order is active /// @@ -37,5 +35,9 @@ namespace CryptoExchange.Net.ExchangeInterfaces /// Type of the order /// public IExchangeClient.OrderType CommonType { get; } + /// + /// order time + /// + DateTime CommonOrderTime { get; } } } diff --git a/CryptoExchange.Net/ExchangeInterfaces/IOrderBook.cs b/CryptoExchange.Net/ExchangeInterfaces/IOrderBook.cs index d614110..9ebfd6e 100644 --- a/CryptoExchange.Net/ExchangeInterfaces/IOrderBook.cs +++ b/CryptoExchange.Net/ExchangeInterfaces/IOrderBook.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System.Collections.Generic; using CryptoExchange.Net.Interfaces; namespace CryptoExchange.Net.ExchangeInterfaces diff --git a/CryptoExchange.Net/ExchangeInterfaces/IPlacedOrder.cs b/CryptoExchange.Net/ExchangeInterfaces/IPlacedOrder.cs index 0e66649..b576f96 100644 --- a/CryptoExchange.Net/ExchangeInterfaces/IPlacedOrder.cs +++ b/CryptoExchange.Net/ExchangeInterfaces/IPlacedOrder.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace CryptoExchange.Net.ExchangeInterfaces +namespace CryptoExchange.Net.ExchangeInterfaces { /// /// Common order id diff --git a/CryptoExchange.Net/ExchangeInterfaces/IRecentTrade.cs b/CryptoExchange.Net/ExchangeInterfaces/IRecentTrade.cs index f9b59e4..e1a4a27 100644 --- a/CryptoExchange.Net/ExchangeInterfaces/IRecentTrade.cs +++ b/CryptoExchange.Net/ExchangeInterfaces/IRecentTrade.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; namespace CryptoExchange.Net.ExchangeInterfaces { diff --git a/CryptoExchange.Net/ExchangeInterfaces/ISymbol.cs b/CryptoExchange.Net/ExchangeInterfaces/ISymbol.cs index 2ff59a5..c937112 100644 --- a/CryptoExchange.Net/ExchangeInterfaces/ISymbol.cs +++ b/CryptoExchange.Net/ExchangeInterfaces/ISymbol.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace CryptoExchange.Net.ExchangeInterfaces +namespace CryptoExchange.Net.ExchangeInterfaces { /// /// Common symbol diff --git a/CryptoExchange.Net/ExchangeInterfaces/ITicker.cs b/CryptoExchange.Net/ExchangeInterfaces/ITicker.cs index 53979b5..c72f05d 100644 --- a/CryptoExchange.Net/ExchangeInterfaces/ITicker.cs +++ b/CryptoExchange.Net/ExchangeInterfaces/ITicker.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace CryptoExchange.Net.ExchangeInterfaces +namespace CryptoExchange.Net.ExchangeInterfaces { /// /// Common ticker diff --git a/CryptoExchange.Net/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs index 299f9f6..424d70c 100644 --- a/CryptoExchange.Net/ExtensionMethods.cs +++ b/CryptoExchange.Net/ExtensionMethods.cs @@ -2,14 +2,14 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Net; using System.Runtime.InteropServices; using System.Security; +using System.Text; using System.Threading; using System.Threading.Tasks; -using System.Web; using CryptoExchange.Net.Logging; using CryptoExchange.Net.Objects; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -125,7 +125,7 @@ namespace CryptoExchange.Net /// public static string CreateParamString(this Dictionary parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType) { - var uriString = ""; + var uriString = string.Empty; var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList(); foreach (var arrayEntry in arraysParameters) { @@ -252,14 +252,14 @@ namespace CryptoExchange.Net catch (JsonReaderException jre) { var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Data: {stringData}"; - log?.Write(LogVerbosity.Error, info); + log?.Write(LogLevel.Error, info); if (log == null) Debug.WriteLine(info); return null; } catch (JsonSerializationException jse) { var info = $"Deserialize JsonSerializationException: {jse.Message}. Data: {stringData}"; - log?.Write(LogVerbosity.Error, info); + log?.Write(LogLevel.Error, info); if (log == null) Debug.WriteLine(info); return null; } @@ -335,6 +335,33 @@ namespace CryptoExchange.Net if (value == null || !value.Any()) throw new ArgumentException($"No values provided for parameter {argumentName}", argumentName); } + + /// + /// Format an exception and inner exception to a readable string + /// + /// + /// + public static string ToLogString(this Exception exception) + { + var message = new StringBuilder(); + var indent = 0; + while (exception != null) + { + for (var i = 0; i < indent; i++) + message.Append(' '); + message.Append(exception.GetType().Name); + message.Append(" - "); + message.AppendLine(exception.Message); + for (var i = 0; i < indent; i++) + message.Append(' '); + message.AppendLine(exception.StackTrace); + + indent += 2; + exception = exception.InnerException; + } + + return message.ToString(); + } } } diff --git a/CryptoExchange.Net/Interfaces/IRequest.cs b/CryptoExchange.Net/Interfaces/IRequest.cs index 1544436..1645d1e 100644 --- a/CryptoExchange.Net/Interfaces/IRequest.cs +++ b/CryptoExchange.Net/Interfaces/IRequest.cs @@ -53,6 +53,6 @@ namespace CryptoExchange.Net.Interfaces /// /// /// - Task GetResponse(CancellationToken cancellationToken); + Task GetResponseAsync(CancellationToken cancellationToken); } } diff --git a/CryptoExchange.Net/Interfaces/IResponse.cs b/CryptoExchange.Net/Interfaces/IResponse.cs index 9ebb888..3d3b54b 100644 --- a/CryptoExchange.Net/Interfaces/IResponse.cs +++ b/CryptoExchange.Net/Interfaces/IResponse.cs @@ -29,7 +29,7 @@ namespace CryptoExchange.Net.Interfaces /// Get the response stream /// /// - Task GetResponseStream(); + Task GetResponseStreamAsync(); /// /// Close the response diff --git a/CryptoExchange.Net/Interfaces/IRestClient.cs b/CryptoExchange.Net/Interfaces/IRestClient.cs index f46f430..65c8c30 100644 --- a/CryptoExchange.Net/Interfaces/IRestClient.cs +++ b/CryptoExchange.Net/Interfaces/IRestClient.cs @@ -40,7 +40,7 @@ namespace CryptoExchange.Net.Interfaces /// /// Client name /// - string ClientName { get; } + string ExchangeName { get; } /// /// Adds a rate limiter to the client. There are 2 choices, the and the . diff --git a/CryptoExchange.Net/Interfaces/ISocketClient.cs b/CryptoExchange.Net/Interfaces/ISocketClient.cs index 7cbacc9..b1828d2 100644 --- a/CryptoExchange.Net/Interfaces/ISocketClient.cs +++ b/CryptoExchange.Net/Interfaces/ISocketClient.cs @@ -49,12 +49,12 @@ namespace CryptoExchange.Net.Interfaces /// /// The subscription to unsubscribe /// - Task Unsubscribe(UpdateSubscription subscription); + Task UnsubscribeAsync(UpdateSubscription subscription); /// /// Unsubscribe all subscriptions /// /// - Task UnsubscribeAll(); + Task UnsubscribeAllAsync(); } } \ No newline at end of file diff --git a/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs index 7fbb088..9a94d9f 100644 --- a/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs +++ b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs @@ -50,6 +50,11 @@ namespace CryptoExchange.Net.Interfaces /// int BidCount { get; } + /// + /// Get a snapshot of the book at this moment + /// + (IEnumerable bids, IEnumerable asks) Book { get; } + /// /// The list of asks /// @@ -75,12 +80,6 @@ namespace CryptoExchange.Net.Interfaces /// (ISymbolOrderBookEntry Bid, ISymbolOrderBookEntry Ask) BestOffers { get; } - /// - /// Start connecting and synchronizing the order book - /// - /// - CallResult Start(); - /// /// Start connecting and synchronizing the order book /// @@ -91,12 +90,15 @@ namespace CryptoExchange.Net.Interfaces /// Stop syncing the order book /// /// - void Stop(); + Task StopAsync(); /// - /// Stop syncing the order book + /// Get the average price that a market order would fill at at the current order book state. This is no guarentee that an order of that quantity would actually be filled + /// at that price since between this calculation and the order placement the book can have changed. /// - /// - Task StopAsync(); + /// The quantity in base asset to fill + /// The type + /// Average fill price + CallResult CalculateAverageFillPrice(decimal quantity, OrderBookEntryType type); } } diff --git a/CryptoExchange.Net/Interfaces/IWebsocket.cs b/CryptoExchange.Net/Interfaces/IWebsocket.cs index ede336e..170a584 100644 --- a/CryptoExchange.Net/Interfaces/IWebsocket.cs +++ b/CryptoExchange.Net/Interfaces/IWebsocket.cs @@ -1,7 +1,8 @@ -using System; +using CryptoExchange.Net.Objects; +using System; using System.Security.Authentication; +using System.Text; using System.Threading.Tasks; -using WebSocket4Net; namespace CryptoExchange.Net.Interfaces { @@ -36,6 +37,10 @@ namespace CryptoExchange.Net.Interfaces /// string? Origin { get; set; } /// + /// Encoding to use + /// + Encoding? Encoding { get; set; } + /// /// Reconnecting /// bool Reconnecting { get; set; } @@ -52,10 +57,6 @@ namespace CryptoExchange.Net.Interfaces /// string Url { get; } /// - /// State - /// - WebSocketState SocketState { get; } - /// /// Is closed /// bool IsClosed { get; } @@ -75,7 +76,7 @@ namespace CryptoExchange.Net.Interfaces /// Connect the socket /// /// - Task Connect(); + Task ConnectAsync(); /// /// Send data /// @@ -89,12 +90,11 @@ namespace CryptoExchange.Net.Interfaces /// Close the connecting /// /// - Task Close(); + Task CloseAsync(); /// /// Set proxy /// - /// - /// - void SetProxy(string host, int port); + /// + void SetProxy(ApiProxy proxy); } } diff --git a/CryptoExchange.Net/Logging/ConsoleLogger.cs b/CryptoExchange.Net/Logging/ConsoleLogger.cs new file mode 100644 index 0000000..baabb7d --- /dev/null +++ b/CryptoExchange.Net/Logging/ConsoleLogger.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Logging; +using System; + +namespace CryptoExchange.Net.Logging +{ + /// + /// Log to console + /// + public class ConsoleLogger : ILogger + { + /// + public IDisposable BeginScope(TState state) => null!; + + /// + public bool IsEnabled(LogLevel logLevel) => true; + + /// + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {formatter(state, exception)}"; + Console.WriteLine(logMessage); + } + } +} diff --git a/CryptoExchange.Net/Logging/DebugLogger.cs b/CryptoExchange.Net/Logging/DebugLogger.cs new file mode 100644 index 0000000..88b8f7d --- /dev/null +++ b/CryptoExchange.Net/Logging/DebugLogger.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Diagnostics; + +namespace CryptoExchange.Net.Logging +{ + /// + /// Default log writer, writes to debug + /// + public class DebugLogger: ILogger + { + /// + public IDisposable BeginScope(TState state) => null!; + + /// + public bool IsEnabled(LogLevel logLevel) => true; + + /// + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {formatter(state, exception)}"; + Trace.WriteLine(logMessage); + } + } +} diff --git a/CryptoExchange.Net/Logging/DebugTextWriter.cs b/CryptoExchange.Net/Logging/DebugTextWriter.cs deleted file mode 100644 index 3e0bc14..0000000 --- a/CryptoExchange.Net/Logging/DebugTextWriter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Diagnostics; -using System.IO; -using System.Text; - -namespace CryptoExchange.Net.Logging -{ - /// - /// Default log writer, writes to debug - /// - public class DebugTextWriter: TextWriter - { - /// - public override Encoding Encoding => Encoding.ASCII; - - /// - public override void WriteLine(string value) - { - Debug.WriteLine(value); - } - } -} diff --git a/CryptoExchange.Net/Logging/Log.cs b/CryptoExchange.Net/Logging/Log.cs index f37458a..a85117c 100644 --- a/CryptoExchange.Net/Logging/Log.cs +++ b/CryptoExchange.Net/Logging/Log.cs @@ -1,7 +1,7 @@ -using System; +using Microsoft.Extensions.Logging; +using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; namespace CryptoExchange.Net.Logging @@ -11,11 +11,15 @@ namespace CryptoExchange.Net.Logging /// public class Log { - private List writers; /// - /// The verbosity of the logging + /// List of ILogger implementations to forward the message to /// - public LogVerbosity Level { get; set; } = LogVerbosity.Info; + private List writers; + + /// + /// The verbosity of the logging, anything more verbose will not be forwarded to the writers + /// + public LogLevel? Level { get; set; } = LogLevel.Information; /// /// Client name @@ -25,17 +29,18 @@ namespace CryptoExchange.Net.Logging /// /// ctor /// + /// The name of the client the logging is used in public Log(string clientName) { ClientName = clientName; - writers = new List(); + writers = new List(); } /// /// Set the writers /// /// - public void UpdateWriters(List textWriters) + public void UpdateWriters(List textWriters) { writers = textWriters; } @@ -43,52 +48,26 @@ namespace CryptoExchange.Net.Logging /// /// Write a log entry /// - /// - /// - public void Write(LogVerbosity logType, string message) + /// The verbosity of the message + /// The message to log + public void Write(LogLevel logLevel, string message) { - if ((int)logType < (int)Level) + if (Level != null && (int)logLevel < (int)Level) return; - var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {ClientName.PadRight(10)} | {logType} | {message}"; + var logMessage = $"{ClientName,-10} | {message}"; foreach (var writer in writers.ToList()) { try { - writer.WriteLine(logMessage); + writer.Log(logLevel, logMessage); } catch (Exception e) { - Debug.WriteLine($"Failed to write log to writer {writer.GetType()}: " + (e.InnerException?.Message ?? e.Message)); + // Can't write to the logging so where else to output.. + Debug.WriteLine($"Failed to write log to writer {writer.GetType()}: " + e.ToLogString()); } } } } - - /// - /// The log verbosity - /// - public enum LogVerbosity - { - /// - /// Debug logging - /// - Debug, - /// - /// Info logging - /// - Info, - /// - /// Warning logging - /// - Warning, - /// - /// Error logging - /// - Error, - /// - /// None, used for disabling logging - /// - None - } } diff --git a/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs b/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs deleted file mode 100644 index a9556dc..0000000 --- a/CryptoExchange.Net/Logging/ThreadSafeFileWriter.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace CryptoExchange.Net.Logging -{ - /// - /// File writer - /// - public class ThreadSafeFileWriter: TextWriter - { - private static readonly object openedFilesLock = new object(); - private static readonly List openedFiles = new List(); - - private readonly StreamWriter logWriter; - private readonly object writeLock; - - /// - public override Encoding Encoding => Encoding.ASCII; - - /// - /// ctor - /// - /// - public ThreadSafeFileWriter(string path) - { - logWriter = new StreamWriter(File.Open(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)) {AutoFlush = true}; - writeLock = new object(); - - lock(openedFilesLock) - { - if (openedFiles.Contains(path)) - throw new System.Exception("Can't have multiple ThreadSafeFileWriters for the same file, reuse a single instance"); - - openedFiles.Add(path); - } - } - - /// - public override void WriteLine(string logMessage) - { - lock(writeLock) - logWriter.WriteLine(logMessage); - } - - /// - /// Dispose - /// - /// - protected override void Dispose(bool disposing) - { - lock (writeLock) - logWriter.Close(); - } - } -} diff --git a/CryptoExchange.Net/Objects/CallResult.cs b/CryptoExchange.Net/Objects/CallResult.cs index d19e8f9..4ce9b75 100644 --- a/CryptoExchange.Net/Objects/CallResult.cs +++ b/CryptoExchange.Net/Objects/CallResult.cs @@ -10,9 +10,10 @@ namespace CryptoExchange.Net.Objects public class CallResult { /// - /// An error if the call didn't succeed + /// An error if the call didn't succeed, will always be filled if Success = false /// public Error? Error { get; internal set; } + /// /// Whether the call was successful /// @@ -54,20 +55,27 @@ namespace CryptoExchange.Net.Objects public class CallResult: CallResult { /// - /// The data returned by the call + /// The data returned by the call, only available when Success = true /// public T Data { get; internal set; } + /// + /// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options + /// + public string? OriginalData { get; set; } + /// /// ctor /// /// /// +#pragma warning disable 8618 public CallResult([AllowNull]T data, Error? error): base(error) +#pragma warning restore 8618 { #pragma warning disable 8601 Data = data; -#pragma warning disable 8601 +#pragma warning restore 8601 } /// @@ -132,9 +140,9 @@ namespace CryptoExchange.Net.Objects /// /// ctor /// - /// - /// - /// + /// Status code + /// Response headers + /// Error public WebCallResult( HttpStatusCode? code, IEnumerable>>? responseHeaders, Error? error) : base(error) @@ -146,9 +154,9 @@ namespace CryptoExchange.Net.Objects /// /// Create an error result /// - /// - /// - /// + /// Status code + /// Response headers + /// Error /// public static WebCallResult CreateErrorResult(HttpStatusCode? code, IEnumerable>>? responseHeaders, Error error) { @@ -191,31 +199,43 @@ namespace CryptoExchange.Net.Objects /// public WebCallResult( HttpStatusCode? code, - IEnumerable>>? responseHeaders, [AllowNull] T data, Error? error): base(data, error) + IEnumerable>>? responseHeaders, + [AllowNull] T data, + Error? error): base(data, error) { ResponseStatusCode = code; ResponseHeaders = responseHeaders; } /// - /// Create new based on existing + /// ctor /// - /// - public WebCallResult(WebCallResult callResult): base(callResult.Data, callResult.Error) + /// + /// + /// + /// + /// + public WebCallResult( + HttpStatusCode? code, + IEnumerable>>? responseHeaders, + string? originalData, + [AllowNull] T data, + Error? error) : base(data, error) { - ResponseHeaders = callResult.ResponseHeaders; - ResponseStatusCode = callResult.ResponseStatusCode; + OriginalData = originalData; + ResponseStatusCode = code; + ResponseHeaders = responseHeaders; } /// - /// Create from a call result + /// Copy the WebCallResult to a new data type /// - /// - /// + /// The new type + /// The data of the new type /// - public static WebCallResult CreateFrom(WebCallResult source) where Y : T + public WebCallResult As([AllowNull] K data) { - return new WebCallResult(source.ResponseStatusCode, source.ResponseHeaders, (T)source.Data, source.Error); + return new WebCallResult(ResponseStatusCode, ResponseHeaders, OriginalData, data, Error); } /// diff --git a/CryptoExchange.Net/Objects/Enums.cs b/CryptoExchange.Net/Objects/Enums.cs index 785b8ff..d31b48d 100644 --- a/CryptoExchange.Net/Objects/Enums.cs +++ b/CryptoExchange.Net/Objects/Enums.cs @@ -16,7 +16,7 @@ } /// - /// Where the post parameters should be added + /// Where the parameters for a post request should be added /// public enum PostParameters { @@ -101,4 +101,19 @@ /// Array } + + /// + /// How to round + /// + public enum RoundingType + { + /// + /// Round down (flooring) + /// + Down, + /// + /// Round to closest value + /// + Closest + } } diff --git a/CryptoExchange.Net/Objects/Error.cs b/CryptoExchange.Net/Objects/Error.cs index 614ee2c..8fa7e7a 100644 --- a/CryptoExchange.Net/Objects/Error.cs +++ b/CryptoExchange.Net/Objects/Error.cs @@ -55,7 +55,7 @@ } /// - /// No api credentials provided while trying to access private endpoint + /// No api credentials provided while trying to access a private endpoint /// public class NoApiCredentialsError : Error { @@ -169,4 +169,16 @@ /// public CancellationRequestedError() : base(null, "Cancellation requested", null) { } } + + /// + /// Invalid operation requested + /// + public class InvalidOperationError: Error + { + /// + /// ctor + /// + /// + public InvalidOperationError(string message) : base(null, message, null) { } + } } diff --git a/CryptoExchange.Net/Objects/Options.cs b/CryptoExchange.Net/Objects/Options.cs index 3df0038..3c93b7c 100644 --- a/CryptoExchange.Net/Objects/Options.cs +++ b/CryptoExchange.Net/Objects/Options.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; -using System.IO; using System.Net.Http; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Logging; +using Microsoft.Extensions.Logging; namespace CryptoExchange.Net.Objects { @@ -14,19 +14,24 @@ namespace CryptoExchange.Net.Objects public class BaseOptions { /// - /// The log verbosity + /// The minimum log level to output. Setting it to null will send all messages to the registered ILoggers. /// - public LogVerbosity LogVerbosity { get; set; } = LogVerbosity.Info; + public LogLevel? LogLevel { get; set; } = Microsoft.Extensions.Logging.LogLevel.Information; /// /// The log writers /// - public List LogWriters { get; set; } = new List { new DebugTextWriter() }; + public List LogWriters { get; set; } = new List { new DebugLogger() }; + + /// + /// If true, the CallResult and DataEvent objects will also include the originally received json data in the OriginalData property + /// + public bool OutputOriginalData { get; set; } = false; /// public override string ToString() { - return $"LogVerbosity: {LogVerbosity}, Writers: {LogWriters.Count}"; + return $"LogLevel: {LogLevel}, Writers: {LogWriters.Count}, OutputOriginalData: {OutputOriginalData}"; } } @@ -47,11 +52,12 @@ namespace CryptoExchange.Net.Objects /// /// Whether or not a level should be removed from the book when it's pushed out of scope of the limit. For example with a book of limit 10, - /// when a new bid is added which makes the total amount of bids 11, should the last bid entry be removed + /// when a new bid level is added which makes the total amount of bids 11, should the last bid entry be removed /// public bool StrictLevels { get; } /// + /// ctor /// /// The name of the order book implementation /// Whether each update should have a consecutive id number. Used to identify and reconnect when numbers are skipped. @@ -111,7 +117,7 @@ namespace CryptoExchange.Net.Objects /// /// ctor /// - /// + /// The base address to use #pragma warning disable 8618 public ClientOptions(string baseAddress) #pragma warning restore 8618 @@ -122,7 +128,7 @@ namespace CryptoExchange.Net.Objects /// public override string ToString() { - return $"{base.ToString()}, Credentials: {(ApiCredentials == null ? "-": "Set")}, BaseAddress: {BaseAddress}, Proxy: {(Proxy == null? "-": Proxy.Host)}"; + return $"{base.ToString()}, Credentials: {(ApiCredentials == null ? "-" : "Set")}, BaseAddress: {BaseAddress}, Proxy: {(Proxy == null ? "-" : Proxy.Host)}"; } } @@ -147,7 +153,7 @@ namespace CryptoExchange.Net.Objects public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30); /// - /// Http client to use. If a HttpClient is provided in this property the RequestTimeout and Proxy options will be ignored and should be set on the provided HttpClient instance + /// Http client to use. If a HttpClient is provided in this property the RequestTimeout and Proxy options will be ignored in requests and should be set on the provided HttpClient instance /// public HttpClient? HttpClient { get; set; } @@ -177,7 +183,7 @@ namespace CryptoExchange.Net.Objects var copy = new T { BaseAddress = BaseAddress, - LogVerbosity = LogVerbosity, + LogLevel = LogLevel, Proxy = Proxy, LogWriters = LogWriters, RateLimiters = RateLimiters, @@ -215,24 +221,25 @@ namespace CryptoExchange.Net.Objects public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5); /// - /// The time to wait for a socket response + /// The time to wait for a socket response before giving a timeout /// public TimeSpan SocketResponseTimeout { get; set; } = TimeSpan.FromSeconds(10); /// - /// The time after which the connection is assumed to be dropped + /// The time after which the connection is assumed to be dropped. This can only be used for socket connections where a steady flow of data is expected. /// public TimeSpan SocketNoDataTimeout { get; set; } /// /// The amount of subscriptions that should be made on a single socket connection. Not all exchanges support multiple subscriptions on a single socket. - /// Setting this to a higher number increases subscription speed, but having more subscriptions on a single connection will also increase the amount of traffic on that single connection. + /// Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a + /// single connection will also increase the amount of traffic on that single connection, potentially leading to issues. /// public int? SocketSubscriptionsCombineTarget { get; set; } /// /// ctor /// - /// + /// The base address to use public SocketClientOptions(string baseAddress) : base(baseAddress) { } @@ -247,7 +254,7 @@ namespace CryptoExchange.Net.Objects var copy = new T { BaseAddress = BaseAddress, - LogVerbosity = LogVerbosity, + LogLevel = LogLevel, Proxy = Proxy, LogWriters = LogWriters, AutoReconnect = AutoReconnect, diff --git a/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs b/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs index 1e7c919..1bf9a60 100644 --- a/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs +++ b/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs @@ -1,4 +1,5 @@ using CryptoExchange.Net.Interfaces; +using System; using System.Collections.Generic; namespace CryptoExchange.Net.OrderBook @@ -19,10 +20,10 @@ namespace CryptoExchange.Net.OrderBook /// /// List of asks /// - public IEnumerable Asks { get; set; } = new List(); + public IEnumerable Asks { get; set; } = Array.Empty(); /// /// List of bids /// - public IEnumerable Bids { get; set; } = new List(); + public IEnumerable Bids { get; set; } = Array.Empty(); } } diff --git a/CryptoExchange.Net/OrderBook/ProcessQueueItem.cs b/CryptoExchange.Net/OrderBook/ProcessQueueItem.cs index 14832a0..2ddc46e 100644 --- a/CryptoExchange.Net/OrderBook/ProcessQueueItem.cs +++ b/CryptoExchange.Net/OrderBook/ProcessQueueItem.cs @@ -1,4 +1,5 @@ using CryptoExchange.Net.Interfaces; +using System; using System.Collections.Generic; namespace CryptoExchange.Net.OrderBook @@ -7,16 +8,16 @@ namespace CryptoExchange.Net.OrderBook { public long StartUpdateId { get; set; } public long EndUpdateId { get; set; } - public IEnumerable Bids { get; set; } = new List(); - public IEnumerable Asks { get; set; } = new List(); + public IEnumerable Bids { get; set; } = Array.Empty(); + public IEnumerable Asks { get; set; } = Array.Empty(); } internal class InitialOrderBookItem { public long StartUpdateId { get; set; } public long EndUpdateId { get; set; } - public IEnumerable Bids { get; set; } = new List(); - public IEnumerable Asks { get; set; } = new List(); + public IEnumerable Bids { get; set; } = Array.Empty(); + public IEnumerable Asks { get; set; } = Array.Empty(); } internal class ChecksumItem diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index 1f856c8..5d1f894 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -2,7 +2,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -10,6 +9,7 @@ using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Logging; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Sockets; +using Microsoft.Extensions.Logging; namespace CryptoExchange.Net.OrderBook { @@ -74,7 +74,7 @@ namespace CryptoExchange.Net.OrderBook var old = status; status = value; - log.Write(LogVerbosity.Info, $"{Id} order book {Symbol} status changed: {old} => {value}"); + log.Write(LogLevel.Information, $"{Id} order book {Symbol} status changed: {old} => {value}"); OnStatusChange?.Invoke(old, status); } } @@ -140,6 +140,18 @@ namespace CryptoExchange.Net.OrderBook } } + /// + /// Get a snapshot of the book at this moment + /// + public (IEnumerable bids, IEnumerable asks) Book + { + get + { + lock (bookLock) + return (Bids, Asks); + } + } + private class EmptySymbolOrderBookEntry : ISymbolOrderBookEntry { public decimal Quantity { get { return 0m; } set {; } } @@ -208,51 +220,82 @@ namespace CryptoExchange.Net.OrderBook asks = new SortedList(); bids = new SortedList(new DescComparer()); - log = new Log(options.OrderBookName) { Level = options.LogVerbosity }; - var writers = options.LogWriters ?? new List { new DebugTextWriter() }; + log = new Log(options.OrderBookName) { Level = options.LogLevel }; + var writers = options.LogWriters ?? new List { new DebugLogger() }; log.UpdateWriters(writers.ToList()); } - /// - /// Start connecting and synchronizing the order book - /// - /// - public CallResult Start() => StartAsync().Result; - /// /// Start connecting and synchronizing the order book /// /// public async Task> StartAsync() { - log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} starting"); + log.Write(LogLevel.Debug, $"{Id} order book {Symbol} starting"); Status = OrderBookStatus.Connecting; _processTask = Task.Run(ProcessQueue); - var startResult = await DoStart().ConfigureAwait(false); + var startResult = await DoStartAsync().ConfigureAwait(false); if (!startResult) return new CallResult(false, startResult.Error); subscription = startResult.Data; subscription.ConnectionLost += Reset; - subscription.ConnectionRestored += time => Resync(); + subscription.ConnectionRestored += async time => await ResyncAsync().ConfigureAwait(false); Status = OrderBookStatus.Synced; return new CallResult(true, null); } + /// + /// Get the average price that a market order would fill at at the current order book state. This is no guarentee that an order of that quantity would actually be filled + /// at that price since between this calculation and the order placement the book can have changed. + /// + /// The quantity in base asset to fill + /// The type + /// Average fill price + public CallResult CalculateAverageFillPrice(decimal quantity, OrderBookEntryType type) + { + if (Status != OrderBookStatus.Synced) + return new CallResult(0, new InvalidOperationError($"{nameof(CalculateAverageFillPrice)} is not available when book is not in Synced state")); + + var totalCost = 0m; + var totalAmount = 0m; + var amountLeft = quantity; + lock (bookLock) + { + var list = type == OrderBookEntryType.Ask ? asks : bids; + + var step = 0; + while (amountLeft > 0) + { + if (step == list.Count) + return new CallResult(0, new InvalidOperationError($"Quantity is larger than order in the order book")); + + var element = list.ElementAt(step); + var stepAmount = Math.Min(element.Value.Quantity, amountLeft); + totalCost += stepAmount * element.Value.Price; + totalAmount += stepAmount; + amountLeft -= stepAmount; + step++; + } + } + + return new CallResult(Math.Round(totalCost / totalAmount, 8), null); + } + private void Reset() { - log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} connection lost"); + log.Write(LogLevel.Warning, $"{Id} order book {Symbol} connection lost"); Status = OrderBookStatus.Reconnecting; _queueEvent.Set(); // Clear queue - while(_processQueue.TryDequeue(out _)) + while (_processQueue.TryDequeue(out _)) { } processBuffer.Clear(); bookSet = false; DoReset(); } - private void Resync() + private async Task ResyncAsync() { Status = OrderBookStatus.Syncing; var success = false; @@ -261,39 +304,35 @@ namespace CryptoExchange.Net.OrderBook if (Status != OrderBookStatus.Syncing) return; - var resyncResult = DoResync().Result; + var resyncResult = await DoResyncAsync().ConfigureAwait(false); success = resyncResult; } - log.Write(LogVerbosity.Info, $"{Id} order book {Symbol} successfully resynchronized"); + log.Write(LogLevel.Information, $"{Id} order book {Symbol} successfully resynchronized"); Status = OrderBookStatus.Synced; } - /// - /// Stop syncing the order book - /// - /// - public void Stop() => StopAsync().Wait(); - /// /// Stop syncing the order book /// /// public async Task StopAsync() { - log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} stopping"); + log.Write(LogLevel.Debug, $"{Id} order book {Symbol} stopping"); Status = OrderBookStatus.Disconnected; _queueEvent.Set(); - _processTask?.Wait(); + if(_processTask != null) + await _processTask.ConfigureAwait(false); + if(subscription != null) - await subscription.Close().ConfigureAwait(false); + await subscription.CloseAsync().ConfigureAwait(false); } /// /// Start the order book /// /// - protected abstract Task> DoStart(); + protected abstract Task> DoStartAsync(); /// /// Reset the order book @@ -304,7 +343,7 @@ namespace CryptoExchange.Net.OrderBook /// Resync the order book /// /// - protected abstract Task> DoResync(); + protected abstract Task> DoResyncAsync(); /// /// Validate a checksum with the current order book @@ -341,6 +380,7 @@ namespace CryptoExchange.Net.OrderBook if (Status == OrderBookStatus.Connecting || Status == OrderBookStatus.Disconnected) return; + bookSet = true; asks.Clear(); foreach (var ask in item.Asks) asks.Add(ask.Price, ask); @@ -354,7 +394,7 @@ namespace CryptoExchange.Net.OrderBook BidCount = bids.Count; LastOrderBookUpdate = DateTime.UtcNow; - log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{item.EndUpdateId}"); + log.Write(LogLevel.Debug, $"{Id} order book {Symbol} data set: {BidCount} bids, {AskCount} asks. #{item.EndUpdateId}"); CheckProcessBuffer(); OnOrderBookUpdate?.Invoke((item.Bids, item.Asks)); OnBestOffersChanged?.Invoke((BestBid, BestAsk)); @@ -377,7 +417,7 @@ namespace CryptoExchange.Net.OrderBook FirstUpdateId = item.StartUpdateId, LastUpdateId = item.EndUpdateId, }); - log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update buffered #{item.StartUpdateId}-#{item.EndUpdateId} [{item.Asks.Count()} asks, {item.Bids.Count()} bids]"); + log.Write(LogLevel.Debug, $"{Id} order book {Symbol} update buffered #{item.StartUpdateId}-#{item.EndUpdateId} [{item.Asks.Count()} asks, {item.Bids.Count()} bids]"); } else { @@ -385,12 +425,15 @@ namespace CryptoExchange.Net.OrderBook var (prevBestBid, prevBestAsk) = BestOffers; ProcessRangeUpdates(item.StartUpdateId, item.EndUpdateId, item.Bids, item.Asks); + if (!asks.Any() || !bids.Any()) + return; + if (asks.First().Key < bids.First().Key) { - log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} detected out of sync order book. Resyncing"); - _ = subscription?.Reconnect(); + log.Write(LogLevel.Warning, $"{Id} order book {Symbol} detected out of sync order book. Resyncing"); + _ = subscription?.ReconnectAsync(); return; - } + } OnOrderBookUpdate?.Invoke((item.Bids, item.Asks)); CheckBestOffersChanged(prevBestBid, prevBestAsk); @@ -417,8 +460,8 @@ namespace CryptoExchange.Net.OrderBook if(!checksumResult) { - log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} out of sync. Resyncing"); - _ = subscription?.Reconnect(); + log.Write(LogLevel.Warning, $"{Id} order book {Symbol} out of sync. Resyncing"); + _ = subscription?.ReconnectAsync(); return; } } @@ -432,8 +475,6 @@ namespace CryptoExchange.Net.OrderBook /// List of bids protected void SetInitialOrderBook(long orderBookSequenceNumber, IEnumerable bidList, IEnumerable askList) { - bookSet = true; - _processQueue.Enqueue(new InitialOrderBookItem { StartUpdateId = orderBookSequenceNumber, EndUpdateId = orderBookSequenceNumber, Asks = askList, Bids = bidList }); _queueEvent.Set(); } @@ -489,9 +530,9 @@ namespace CryptoExchange.Net.OrderBook private void ProcessRangeUpdates(long firstUpdateId, long lastUpdateId, IEnumerable bids, IEnumerable asks) { - if (lastUpdateId < LastSequenceNumber) + if (lastUpdateId <= LastSequenceNumber) { - log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update skipped #{firstUpdateId}-{lastUpdateId}"); + log.Write(LogLevel.Debug, $"{Id} order book {Symbol} update skipped #{firstUpdateId}-{lastUpdateId}"); return; } @@ -503,13 +544,13 @@ namespace CryptoExchange.Net.OrderBook if (Levels.HasValue && strictLevels) { - while (this.bids.Count() > Levels.Value) + while (this.bids.Count > Levels.Value) { BidCount--; this.bids.Remove(this.bids.Last().Key); } - while (this.asks.Count() > Levels.Value) + while (this.asks.Count > Levels.Value) { AskCount--; this.asks.Remove(this.asks.Last().Key); @@ -517,7 +558,7 @@ namespace CryptoExchange.Net.OrderBook } LastSequenceNumber = lastUpdateId; - log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update processed #{firstUpdateId}-{lastUpdateId}"); + log.Write(LogLevel.Debug, $"{Id} order book {Symbol} update processed #{firstUpdateId}-{lastUpdateId}"); } /// @@ -527,7 +568,7 @@ namespace CryptoExchange.Net.OrderBook { var pbList = processBuffer.ToList(); if(pbList.Count > 0) - log.Write(LogVerbosity.Debug, "Processing buffered updates"); + log.Write(LogLevel.Debug, "Processing buffered updates"); foreach (var bufferEntry in pbList) { @@ -549,15 +590,15 @@ namespace CryptoExchange.Net.OrderBook if (sequence <= LastSequenceNumber) { - log.Write(LogVerbosity.Debug, $"{Id} order book {Symbol} update skipped #{sequence}"); + log.Write(LogLevel.Debug, $"{Id} order book {Symbol} update skipped #{sequence}"); return false; } if (sequencesAreConsecutive && sequence > LastSequenceNumber + 1) { // Out of sync - log.Write(LogVerbosity.Warning, $"{Id} order book {Symbol} out of sync (expected { LastSequenceNumber + 1}, was {sequence}), reconnecting"); - subscription?.Reconnect(); + log.Write(LogLevel.Warning, $"{Id} order book {Symbol} out of sync (expected { LastSequenceNumber + 1}, was {sequence}), reconnecting"); + subscription?.ReconnectAsync(); return false; } @@ -594,7 +635,7 @@ namespace CryptoExchange.Net.OrderBook /// /// Max wait time /// - protected async Task> WaitForSetOrderBook(int timeout) + protected async Task> WaitForSetOrderBookAsync(int timeout) { var startWait = DateTime.UtcNow; while (!bookSet && Status == OrderBookStatus.Syncing) @@ -636,7 +677,7 @@ namespace CryptoExchange.Net.OrderBook /// public string ToString(int numberOfEntries) { - var result = ""; + var result = string.Empty; result += $"Asks ({AskCount}): {Environment.NewLine}"; foreach (var entry in Asks.Take(numberOfEntries).Reverse()) result += $" {entry.Price.ToString(CultureInfo.InvariantCulture).PadLeft(8)} | {entry.Quantity.ToString(CultureInfo.InvariantCulture).PadRight(8)}{Environment.NewLine}"; diff --git a/CryptoExchange.Net/Requests/Request.cs b/CryptoExchange.Net/Requests/Request.cs index b0b669a..9cb7073 100644 --- a/CryptoExchange.Net/Requests/Request.cs +++ b/CryptoExchange.Net/Requests/Request.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Net.Http; using System.Net.Http.Headers; using System.Text; @@ -71,7 +70,7 @@ namespace CryptoExchange.Net.Requests } /// - public async Task GetResponse(CancellationToken cancellationToken) + public async Task GetResponseAsync(CancellationToken cancellationToken) { return new Response(await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false)); } diff --git a/CryptoExchange.Net/Requests/Response.cs b/CryptoExchange.Net/Requests/Response.cs index 3df463d..93d2113 100644 --- a/CryptoExchange.Net/Requests/Response.cs +++ b/CryptoExchange.Net/Requests/Response.cs @@ -33,7 +33,7 @@ namespace CryptoExchange.Net.Requests } /// - public async Task GetResponseStream() + public async Task GetResponseStreamAsync() { return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); } diff --git a/CryptoExchange.Net/RestClient.cs b/CryptoExchange.Net/RestClient.cs index 5d44df4..0e8e6fd 100644 --- a/CryptoExchange.Net/RestClient.cs +++ b/CryptoExchange.Net/RestClient.cs @@ -7,16 +7,15 @@ using System.Linq; using System.Net.Http; using System.Net.NetworkInformation; using System.Net.Sockets; -using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using System.Web; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Interfaces; -using CryptoExchange.Net.Logging; using CryptoExchange.Net.Objects; using CryptoExchange.Net.RateLimiter; using CryptoExchange.Net.Requests; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -33,9 +32,10 @@ namespace CryptoExchange.Net public IRequestFactory RequestFactory { get; set; } = new RequestFactory(); /// - /// Where to place post parameters + /// Where to place post parameters by default /// protected PostParameters postParametersPosition = PostParameters.InBody; + /// /// Request body content type /// @@ -47,21 +47,21 @@ namespace CryptoExchange.Net protected bool manualParseError = false; /// - /// How to serialize array parameters + /// How to serialize array parameters when making requests /// protected ArrayParametersSerialization arraySerialization = ArrayParametersSerialization.Array; /// - /// What request body should be when no data is send + /// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody) /// protected string requestBodyEmptyContent = "{}"; /// - /// Timeout for requests + /// Timeout for requests. This setting is ignored when injecting a HttpClient in the options, requests timeouts should be set on the client then. /// public TimeSpan RequestTimeout { get; } /// - /// Rate limiting behaviour + /// What should happen when running into a rate limit /// public RateLimitingBehaviour RateLimitBehaviour { get; } /// @@ -69,17 +69,17 @@ namespace CryptoExchange.Net /// public IEnumerable RateLimiters { get; private set; } /// - /// Total requests made + /// Total requests made by this client /// public int TotalRequestsMade { get; private set; } /// /// ctor /// - /// - /// - /// - protected RestClient(string clientName, RestClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider) : base(clientName, exchangeOptions, authenticationProvider) + /// The name of the exchange this client is for + /// The options for this client + /// The authentication provider for this client (can be null if no credentials are provided) + protected RestClient(string exchangeName, RestClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider) : base(exchangeName, exchangeOptions, authenticationProvider) { if (exchangeOptions == null) throw new ArgumentNullException(nameof(exchangeOptions)); @@ -158,31 +158,31 @@ namespace CryptoExchange.Net } /// - /// Execute a request + /// Execute a request to the uri and deserialize the response into the provided type parameter /// - /// The expected result type + /// The type to deserialize into /// The uri to send the request to /// The method of the request /// Cancellation token /// The parameters of the request /// Whether or not the request should be authenticated /// Whether or not the resulting object should be checked for missing properties in the mapping (only outputs if log verbosity is Debug) - /// Where the post parameters should be placed - /// How array parameters should be serialized + /// Where the post parameters should be placed, overwrites the value set in the client + /// How array parameters should be serialized, overwrites the value set in the client /// Credits used for the request /// The JsonSerializer to use for deserialization /// [return: NotNull] - protected virtual async Task> SendRequest(Uri uri, HttpMethod method, CancellationToken cancellationToken, + protected virtual async Task> SendRequestAsync(Uri uri, HttpMethod method, CancellationToken cancellationToken, Dictionary? parameters = null, bool signed = false, bool checkResult = true, PostParameters? postPosition = null, ArrayParametersSerialization? arraySerialization = null, int credits = 1, JsonSerializer? deserializer = null) where T : class { var requestId = NextId(); - log.Write(LogVerbosity.Debug, $"[{requestId}] Creating request for " + uri); + log.Write(LogLevel.Debug, $"[{requestId}] Creating request for " + uri); if (signed && authProvider == null) { - log.Write(LogVerbosity.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided"); + log.Write(LogLevel.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided"); return new WebCallResult(null, null, null, new NoApiCredentialsError()); } @@ -192,74 +192,82 @@ namespace CryptoExchange.Net var limitResult = limiter.LimitRequest(this, uri.AbsolutePath, RateLimitBehaviour, credits); if (!limitResult.Success) { - log.Write(LogVerbosity.Debug, $"[{requestId}] Request {uri.AbsolutePath} failed because of rate limit"); + log.Write(LogLevel.Information, $"[{requestId}] Request {uri.AbsolutePath} failed because of rate limit"); return new WebCallResult(null, null, null, limitResult.Error); } if (limitResult.Data > 0) - log.Write(LogVerbosity.Debug, $"[{requestId}] Request {uri.AbsolutePath} was limited by {limitResult.Data}ms by {limiter.GetType().Name}"); + log.Write(LogLevel.Information, $"[{requestId}] Request {uri.AbsolutePath} was limited by {limitResult.Data}ms by {limiter.GetType().Name}"); } string? paramString = null; if (method == HttpMethod.Post) paramString = " with request body " + request.Content; - log.Write(LogVerbosity.Debug, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(apiProxy == null ? "" : $" via proxy {apiProxy.Host}")}"); - return await GetResponse(request, deserializer, cancellationToken).ConfigureAwait(false); + log.Write(LogLevel.Debug, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(apiProxy == null ? "" : $" via proxy {apiProxy.Host}")}"); + return await GetResponseAsync(request, deserializer, cancellationToken).ConfigureAwait(false); } /// - /// Executes the request and returns the string result + /// Executes the request and returns the result deserialized into the type parameter class /// /// The request object to execute /// The JsonSerializer to use for deserialization /// Cancellation token /// - protected virtual async Task> GetResponse(IRequest request, JsonSerializer? deserializer, CancellationToken cancellationToken) + protected virtual async Task> GetResponseAsync(IRequest request, JsonSerializer? deserializer, CancellationToken cancellationToken) { try { TotalRequestsMade++; var sw = Stopwatch.StartNew(); - var response = await request.GetResponse(cancellationToken).ConfigureAwait(false); + var response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false); sw.Stop(); var statusCode = response.StatusCode; var headers = response.ResponseHeaders; - var responseStream = await response.GetResponseStream().ConfigureAwait(false); + var responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false); if (response.IsSuccessStatusCode) { + // If we have to manually parse error responses (can't rely on HttpStatusCode) we'll need to read the full + // response before being able to deserialize it into the resulting type since we don't know if it an error response or data if (manualParseError) { using var reader = new StreamReader(responseStream); var data = await reader.ReadToEndAsync().ConfigureAwait(false); responseStream.Close(); response.Close(); - log.Write(LogVerbosity.Debug, $"[{request.RequestId}] Response received in {sw.ElapsedMilliseconds}ms: {data}"); + log.Write(LogLevel.Debug, $"[{request.RequestId}] Response received in {sw.ElapsedMilliseconds}ms: {data}"); + // Validate if it is valid json. Sometimes other data will be returned, 502 error html pages for example var parseResult = ValidateJson(data); if (!parseResult.Success) return WebCallResult.CreateErrorResult(response.StatusCode, response.ResponseHeaders, parseResult.Error!); - var error = await TryParseError(parseResult.Data); + + // Let the library implementation see if it is an error response, and if so parse the error + var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false); if (error != null) return WebCallResult.CreateErrorResult(response.StatusCode, response.ResponseHeaders, error); + // Not an error, so continue deserializing var deserializeResult = Deserialize(parseResult.Data, null, deserializer, request.RequestId); - return new WebCallResult(response.StatusCode, response.ResponseHeaders, deserializeResult.Data, deserializeResult.Error); + return new WebCallResult(response.StatusCode, response.ResponseHeaders, OutputOriginalData ? data: null, deserializeResult.Data, deserializeResult.Error); } else { - var desResult = await Deserialize(responseStream, deserializer, request.RequestId, sw.ElapsedMilliseconds).ConfigureAwait(false); + // Success status code, and we don't have to check for errors. Continue deserializing directly from the stream + var desResult = await DeserializeAsync(responseStream, deserializer, request.RequestId, sw.ElapsedMilliseconds).ConfigureAwait(false); responseStream.Close(); response.Close(); - return new WebCallResult(statusCode, headers, desResult.Data, desResult.Error); + return new WebCallResult(statusCode, headers, OutputOriginalData ? desResult.OriginalData : null, desResult.Data, desResult.Error); } } else { + // Http status code indicates error using var reader = new StreamReader(responseStream); var data = await reader.ReadToEndAsync().ConfigureAwait(false); - log.Write(LogVerbosity.Debug, $"[{request.RequestId}] Error received: {data}"); + log.Write(LogLevel.Debug, $"[{request.RequestId}] Error received: {data}"); responseStream.Close(); response.Close(); var parseResult = ValidateJson(data); @@ -271,22 +279,23 @@ namespace CryptoExchange.Net } catch (HttpRequestException requestException) { - var exceptionInfo = GetExceptionInfo(requestException); - log.Write(LogVerbosity.Warning, $"[{request.RequestId}] Request exception: " + exceptionInfo); + // Request exception, can't reach server for instance + var exceptionInfo = requestException.ToLogString(); + log.Write(LogLevel.Warning, $"[{request.RequestId}] Request exception: " + exceptionInfo); return new WebCallResult(null, null, default, new WebError(exceptionInfo)); } catch (TaskCanceledException canceledException) { if (canceledException.CancellationToken == cancellationToken) { - // Cancellation token cancelled - log.Write(LogVerbosity.Warning, $"[{request.RequestId}] Request cancel requested"); + // Cancellation token cancelled by caller + log.Write(LogLevel.Warning, $"[{request.RequestId}] Request cancel requested"); return new WebCallResult(null, null, default, new CancellationRequestedError()); } else { // Request timed out - log.Write(LogVerbosity.Warning, $"[{request.RequestId}] Request timed out"); + log.Write(LogLevel.Warning, $"[{request.RequestId}] Request timed out"); return new WebCallResult(null, null, default, new WebError($"[{request.RequestId}] Request timed out")); } } @@ -294,11 +303,12 @@ namespace CryptoExchange.Net /// /// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error. - /// This can be used together with ManualParseError to check if it is an error before deserializing to an object + /// When setting manualParseError to true this method will be called for each response to be able to check if the response is an error or not. + /// If the response is an error this method should return the parsed error, else it should return null /// /// Received data /// Null if not an error, Error otherwise - protected virtual Task TryParseError(JToken data) + protected virtual Task TryParseErrorAsync(JToken data) { return Task.FromResult(null); } @@ -349,20 +359,22 @@ namespace CryptoExchange.Net } /// - /// Writes the parameters of the request to the request object, either in the query string or the request body + /// Writes the parameters of the request to the request object body /// - /// - /// - /// + /// The request to set the parameters on + /// The parameters to set + /// The content type of the data protected virtual void WriteParamBody(IRequest request, Dictionary parameters, string contentType) { if (requestBodyFormat == RequestBodyFormat.Json) { + // Write the parameters as json in the body var stringData = JsonConvert.SerializeObject(parameters.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value)); request.SetContent(stringData, contentType); } else if (requestBodyFormat == RequestBodyFormat.FormData) { + // Write the parameters as form data in the body var formData = HttpUtility.ParseQueryString(string.Empty); foreach (var kvp in parameters.OrderBy(p => p.Key)) { diff --git a/CryptoExchange.Net/SocketClient.cs b/CryptoExchange.Net/SocketClient.cs index 9c8c12d..ec0aaf7 100644 --- a/CryptoExchange.Net/SocketClient.cs +++ b/CryptoExchange.Net/SocketClient.cs @@ -7,9 +7,9 @@ using System.Threading; using System.Threading.Tasks; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Interfaces; -using CryptoExchange.Net.Logging; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Sockets; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -31,6 +31,7 @@ namespace CryptoExchange.Net /// protected internal ConcurrentDictionary sockets = new ConcurrentDictionary(); /// + /// Semaphore used while creating sockets /// protected internal readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1); @@ -48,29 +49,28 @@ namespace CryptoExchange.Net public int MaxSocketConnections { get; protected set; } = 9999; /// public int SocketCombineTarget { get; protected set; } - /// - /// Handler for byte data + /// Delegate used for processing byte data received from socket connections before it is processed by handlers /// protected Func? dataInterpreterBytes; /// - /// Handler for string data + /// Delegate used for processing string data received from socket connections before it is processed by handlers /// protected Func? dataInterpreterString; /// - /// Generic handlers + /// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example. /// - protected Dictionary> genericHandlers = new Dictionary>(); + protected Dictionary> genericHandlers = new Dictionary>(); /// - /// Periodic task + /// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similair. Not necesarry. /// protected Task? periodicTask; /// - /// Periodic task event + /// Wait event for the periodicTask /// protected AutoResetEvent? periodicEvent; /// - /// Is disposing + /// If client is disposing /// protected bool disposing; @@ -87,12 +87,12 @@ namespace CryptoExchange.Net #endregion /// - /// Create a socket client + /// ctor /// - /// Client name - /// Client options - /// Authentication provider - protected SocketClient(string clientName, SocketClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider): base(clientName, exchangeOptions, authenticationProvider) + /// The name of the exchange this client is for + /// The options for this client + /// The authentication provider for this client (can be null if no credentials are provided) + protected SocketClient(string exchangeName, SocketClientOptions exchangeOptions, AuthenticationProvider? authenticationProvider): base(exchangeName, exchangeOptions, authenticationProvider) { if (exchangeOptions == null) throw new ArgumentNullException(nameof(exchangeOptions)); @@ -105,7 +105,7 @@ namespace CryptoExchange.Net } /// - /// Set a function to interpret the data, used when the data is received as bytes instead of a string + /// Set a delegate to be used for processing data received from socket connections before it is processed by handlers /// /// Handler for byte data /// Handler for string data @@ -116,98 +116,103 @@ namespace CryptoExchange.Net } /// - /// Subscribe + /// Connect to an url and listen for data on the BaseAddress /// - /// The expected return data - /// The request to send - /// The identifier to use - /// If the subscription should be authenticated + /// The type of the expected data + /// The optional request object to send, will be serialized to json + /// The identifier to use, necessary if no request object is sent + /// If the subscription is to an authenticated endpoint /// The handler of update data /// - protected virtual Task> Subscribe(object? request, string? identifier, bool authenticated, Action dataHandler) + protected virtual Task> SubscribeAsync(object? request, string? identifier, bool authenticated, Action> dataHandler) { - return Subscribe(BaseAddress, request, identifier, authenticated, dataHandler); + return SubscribeAsync(BaseAddress, request, identifier, authenticated, dataHandler); } /// - /// Subscribe using a specif URL + /// Connect to an url and listen for data /// /// The type of the expected data /// The URL to connect to - /// The request to send - /// The identifier to use - /// If the subscription should be authenticated + /// The optional request object to send, will be serialized to json + /// The identifier to use, necessary if no request object is sent + /// If the subscription is to an authenticated endpoint /// The handler of update data /// - protected virtual async Task> Subscribe(string url, object? request, string? identifier, bool authenticated, Action dataHandler) + protected virtual async Task> SubscribeAsync(string url, object? request, string? identifier, bool authenticated, Action> dataHandler) { - SocketConnection socket; - SocketSubscription handler; + SocketConnection socketConnection; + SocketSubscription subscription; var released = false; + // Wait for a semaphore here, so we only connect 1 socket at a time. + // This is necessary for being able to see if connections can be combined await semaphoreSlim.WaitAsync().ConfigureAwait(false); try { - socket = GetWebsocket(url, authenticated); - handler = AddHandler(request, identifier, true, socket, dataHandler); + // Get a new or existing socket connection + socketConnection = GetSocketConnection(url, authenticated); + + // Add a subscription on the socket connection + subscription = AddSubscription(request, identifier, true, socketConnection, dataHandler); if (SocketCombineTarget == 1) { - // Can release early when only a single sub per connection + // Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway semaphoreSlim.Release(); released = true; } - var needsconnecting = !socket.Connected; + var needsConnecting = !socketConnection.Connected; - var connectResult = await ConnectIfNeeded(socket, authenticated).ConfigureAwait(false); + var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false); if (!connectResult) return new CallResult(null, connectResult.Error); - if (needsconnecting) - log.Write(LogVerbosity.Debug, $"Socket {socket.Socket.Id} connected to {url} {(request == null ? "": "with request " + JsonConvert.SerializeObject(request))}"); + if (needsConnecting) + log.Write(LogLevel.Debug, $"Socket {socketConnection.Socket.Id} connected to {url} {(request == null ? "": "with request " + JsonConvert.SerializeObject(request))}"); } finally { - //When the task is ready, release the semaphore. It is vital to ALWAYS release the semaphore when we are ready, or else we will end up with a Semaphore that is forever locked. - //This is why it is important to do the Release within a try...finally clause; program execution may crash or take a different path, this way you are guaranteed execution if(!released) semaphoreSlim.Release(); } - if (socket.PausedActivity) + if (socketConnection.PausedActivity) { - log.Write(LogVerbosity.Info, "Socket has been paused, can't subscribe at this moment"); + log.Write(LogLevel.Information, "Socket has been paused, can't subscribe at this moment"); return new CallResult(default, new ServerError("Socket is paused")); } if (request != null) { - var subResult = await SubscribeAndWait(socket, request, handler).ConfigureAwait(false); + // Send the request and wait for answer + var subResult = await SubscribeAndWaitAsync(socketConnection, request, subscription).ConfigureAwait(false); if (!subResult) { - await socket.Close(handler).ConfigureAwait(false); + await socketConnection.CloseAsync(subscription).ConfigureAwait(false); return new CallResult(null, subResult.Error); } } else { - handler.Confirmed = true; + // No request to be sent, so just mark the subscription as comfirmed + subscription.Confirmed = true; } - - socket.ShouldReconnect = true; - return new CallResult(new UpdateSubscription(socket, handler), null); + + socketConnection.ShouldReconnect = true; + return new CallResult(new UpdateSubscription(socketConnection, subscription), null); } /// /// Sends the subscribe request and waits for a response to that request /// - /// The connection to send the request on - /// The request to send + /// The connection to send the request on + /// The request to send, will be serialized to json /// The subscription the request is for /// - protected internal virtual async Task> SubscribeAndWait(SocketConnection socket, object request, SocketSubscription subscription) + protected internal virtual async Task> SubscribeAndWaitAsync(SocketConnection socketConnection, object request, SocketSubscription subscription) { CallResult? callResult = null; - await socket.SendAndWait(request, ResponseTimeout, data => HandleSubscriptionResponse(socket, subscription, request, data, out callResult)).ConfigureAwait(false); + await socketConnection.SendAndWaitAsync(request, ResponseTimeout, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false); if (callResult?.Success == true) subscription.Confirmed = true; @@ -216,33 +221,33 @@ namespace CryptoExchange.Net } /// - /// Query for data + /// Send a query on a socket connection to the BaseAddress and wait for the response /// /// Expected result type - /// The request to send - /// Whether the socket should be authenticated + /// The request to send, will be serialized to json + /// If the query is to an authenticated endpoint /// - protected virtual Task> Query(object request, bool authenticated) + protected virtual Task> QueryAsync(object request, bool authenticated) { - return Query(BaseAddress, request, authenticated); + return QueryAsync(BaseAddress, request, authenticated); } /// - /// Query for data + /// Send a query on a socket connection and wait for the response /// /// The expected result type /// The url for the request /// The request to send /// Whether the socket should be authenticated /// - protected virtual async Task> Query(string url, object request, bool authenticated) + protected virtual async Task> QueryAsync(string url, object request, bool authenticated) { - SocketConnection socket; + SocketConnection socketConnection; var released = false; await semaphoreSlim.WaitAsync().ConfigureAwait(false); try { - socket = GetWebsocket(url, authenticated); + socketConnection = GetSocketConnection(url, authenticated); if (SocketCombineTarget == 1) { // Can release early when only a single sub per connection @@ -250,7 +255,7 @@ namespace CryptoExchange.Net released = true; } - var connectResult = await ConnectIfNeeded(socket, authenticated).ConfigureAwait(false); + var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false); if (!connectResult) return new CallResult(default, connectResult.Error); } @@ -262,13 +267,13 @@ namespace CryptoExchange.Net semaphoreSlim.Release(); } - if (socket.PausedActivity) + if (socketConnection.PausedActivity) { - log.Write(LogVerbosity.Info, "Socket has been paused, can't send query at this moment"); + log.Write(LogLevel.Information, "Socket has been paused, can't send query at this moment"); return new CallResult(default, new ServerError("Socket is paused")); } - return await QueryAndWait(socket, request).ConfigureAwait(false); + return await QueryAndWaitAsync(socketConnection, request).ConfigureAwait(false); } /// @@ -278,10 +283,10 @@ namespace CryptoExchange.Net /// The connection to send and wait on /// The request to send /// - protected virtual async Task> QueryAndWait(SocketConnection socket, object request) + protected virtual async Task> QueryAndWaitAsync(SocketConnection socket, object request) { var dataResult = new CallResult(default, new ServerError("No response on query received")); - await socket.SendAndWait(request, ResponseTimeout, data => + await socket.SendAndWaitAsync(request, ResponseTimeout, data => { if (!HandleQueryResponse(socket, request, data, out var callResult)) return false; @@ -294,27 +299,27 @@ namespace CryptoExchange.Net } /// - /// Checks if a socket needs to be connected and does so if needed + /// Checks if a socket needs to be connected and does so if needed. Also authenticates on the socket if needed /// /// The connection to check /// Whether the socket should authenticated /// - protected virtual async Task> ConnectIfNeeded(SocketConnection socket, bool authenticated) + protected virtual async Task> ConnectIfNeededAsync(SocketConnection socket, bool authenticated) { if (socket.Connected) return new CallResult(true, null); - var connectResult = await ConnectSocket(socket).ConfigureAwait(false); + var connectResult = await ConnectSocketAsync(socket).ConfigureAwait(false); if (!connectResult) return new CallResult(false, connectResult.Error); if (!authenticated || socket.Authenticated) return new CallResult(true, null); - var result = await AuthenticateSocket(socket).ConfigureAwait(false); + var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false); if (!result) { - log.Write(LogVerbosity.Warning, "Socket authentication failed"); + log.Write(LogLevel.Warning, "Socket authentication failed"); result.Error!.Message = "Authentication failed: " + result.Error.Message; return new CallResult(false, result.Error); } @@ -322,54 +327,64 @@ namespace CryptoExchange.Net socket.Authenticated = true; return new CallResult(true, null); } - + /// - /// Needs to check if a received message was an answer to a query request (preferable by id) and set the callResult out to whatever the response is + /// The socketConnection received data (the data JToken parameter). The implementation of this method should check if the received data is a response to the query that was send (the request parameter). + /// For example; A query is sent in a request message with an Id parameter with value 10. The socket receives data and calls this method to see if the data it received is an + /// anwser to any query that was done. The implementation of this method should check if the response.Id == request.Id to see if they match (assuming the api has some sort of Id tracking on messages, + /// if not some other method has be implemented to match the messages). + /// If the messages match, the callResult out parameter should be set with the deserialized data in the from of (T) and return true. /// - /// The type of response - /// The socket connection + /// The type of response that is expected on the query + /// The socket connection /// The request that a response is awaited for - /// The message + /// The message received from the server /// The interpretation (null if message wasn't a response to the request) /// True if the message was a response to the query - protected internal abstract bool HandleQueryResponse(SocketConnection s, object request, JToken data, [NotNullWhen(true)]out CallResult? callResult); + protected internal abstract bool HandleQueryResponse(SocketConnection socketConnection, object request, JToken data, [NotNullWhen(true)]out CallResult? callResult); /// - /// Needs to check if a received message was an answer to a subscription request (preferable by id) and set the callResult out to whatever the response is + /// The socketConnection received data (the data JToken parameter). The implementation of this method should check if the received data is a response to the subscription request that was send (the request parameter). + /// For example; A subscribe request message is send with an Id parameter with value 10. The socket receives data and calls this method to see if the data it received is an + /// anwser to any subscription request that was done. The implementation of this method should check if the response.Id == request.Id to see if they match (assuming the api has some sort of Id tracking on messages, + /// if not some other method has be implemented to match the messages). + /// If the messages match, the callResult out parameter should be set with the deserialized data in the from of (T) and return true. /// - /// The socket connection - /// - /// The request that a response is awaited for - /// The message + /// The socket connection + /// A subscription that waiting for a subscription response + /// The request that the subscription sent + /// The message received from the server /// The interpretation (null if message wasn't a response to the request) /// True if the message was a response to the subscription request - protected internal abstract bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message, out CallResult? callResult); + protected internal abstract bool HandleSubscriptionResponse(SocketConnection socketConnection, SocketSubscription subscription, object request, JToken data, out CallResult? callResult); /// - /// Needs to check if a received message matches a handler. Typically if an update message matches the request + /// Needs to check if a received message matches a handler by request. After subscribing data message will come in. These data messages need to be matched to a specific connection + /// to pass the correct data to the correct handler. The implementation of this method should check if the message received matches the subscribe request that was sent. /// /// The received data /// The subscription request - /// + /// True if the message is for the subscription which sent the request protected internal abstract bool MessageMatchesHandler(JToken message, object request); /// - /// Needs to check if a received message matches a handler. Typically if an received message matches a ping request or a other information pushed from the the server + /// Needs to check if a received message matches a handler by identifier. Generally used by GenericHandlers. For example; a generic handler is registered which handles ping messages + /// from the server. This method should check if the message received is a ping message and the identifer is the identifier of the GenericHandler /// /// The received data /// The string identifier of the handler - /// + /// True if the message is for the handler which has the identifier protected internal abstract bool MessageMatchesHandler(JToken message, string identifier); /// /// Needs to authenticate the socket so authenticated queries/subscriptions can be made on this socket connection /// - /// + /// The socket connection that should be authenticated /// - protected internal abstract Task> AuthenticateSocket(SocketConnection s); + protected internal abstract Task> AuthenticateSocketAsync(SocketConnection socketConnection); /// /// Needs to unsubscribe a subscription, typically by sending an unsubscribe request. If multiple subscriptions per socket is not allowed this can just return since the socket will be closed anyway /// /// The connection on which to unsubscribe - /// The subscription to unsubscribe + /// The subscription to unsubscribe /// - protected internal abstract Task Unsubscribe(SocketConnection connection, SocketSubscription s); + protected internal abstract Task UnsubscribeAsync(SocketConnection connection, SocketSubscription subscriptionToUnsub); /// /// Optional handler to interpolate data before sending it to the handlers @@ -382,7 +397,7 @@ namespace CryptoExchange.Net } /// - /// Add a handler for a subscription + /// Add a subscription to a connection /// /// The type of data the subscription expects /// The request of the subscription @@ -391,31 +406,32 @@ namespace CryptoExchange.Net /// The socket connection the handler is on /// The handler of the data received /// - protected virtual SocketSubscription AddHandler(object? request, string? identifier, bool userSubscription, SocketConnection connection, Action dataHandler) + protected virtual SocketSubscription AddSubscription(object? request, string? identifier, bool userSubscription, SocketConnection connection, Action> dataHandler) { - void InternalHandler(SocketConnection socketWrapper, JToken data) + void InternalHandler(MessageEvent messageEvent) { if (typeof(T) == typeof(string)) { - dataHandler((T) Convert.ChangeType(data.ToString(), typeof(T))); + var stringData = (T)Convert.ChangeType(messageEvent.JsonData.ToString(), typeof(T)); + dataHandler(new DataEvent(stringData, null, OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp)); return; } - var desResult = Deserialize(data, false); + var desResult = Deserialize(messageEvent.JsonData, false); if (!desResult) { - log.Write(LogVerbosity.Warning, $"Failed to deserialize data into type {typeof(T)}: {desResult.Error}"); + log.Write(LogLevel.Warning, $"Failed to deserialize data into type {typeof(T)}: {desResult.Error}"); return; } - dataHandler(desResult.Data); + dataHandler(new DataEvent(desResult.Data, null, OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp)); } - var handler = request == null + var subscription = request == null ? SocketSubscription.CreateForIdentifier(identifier!, userSubscription, InternalHandler) : SocketSubscription.CreateForRequest(request, userSubscription, InternalHandler); - connection.AddHandler(handler); - return handler; + connection.AddSubscription(subscription); + return subscription; } /// @@ -423,12 +439,12 @@ namespace CryptoExchange.Net /// /// The name of the request handler. Needs to be unique /// The action to execute when receiving a message for this handler (checked by ) - protected void AddGenericHandler(string identifier, Action action) + protected void AddGenericHandler(string identifier, Action action) { genericHandlers.Add(identifier, action); - var handler = SocketSubscription.CreateForIdentifier(identifier, false, action); + var subscription = SocketSubscription.CreateForIdentifier(identifier, false, action); foreach (var connection in sockets.Values) - connection.AddHandler(handler); + connection.AddSubscription(subscription); } /// @@ -437,14 +453,14 @@ namespace CryptoExchange.Net /// The address the socket is for /// Whether the socket should be authenticated /// - protected virtual SocketConnection GetWebsocket(string address, bool authenticated) + protected virtual SocketConnection GetSocketConnection(string address, bool authenticated) { var socketResult = sockets.Where(s => s.Value.Socket.Url.TrimEnd('/') == address.TrimEnd('/') - && (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.HandlerCount).FirstOrDefault(); + && (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.SubscriptionCount).FirstOrDefault(); var result = socketResult.Equals(default(KeyValuePair)) ? null : socketResult.Value; if (result != null) { - if (result.HandlerCount < SocketCombineTarget || (sockets.Count >= MaxSocketConnections && sockets.All(s => s.Value.HandlerCount >= SocketCombineTarget))) + if (result.SubscriptionCount < SocketCombineTarget || (sockets.Count >= MaxSocketConnections && sockets.All(s => s.Value.SubscriptionCount >= SocketCombineTarget))) { // Use existing socket if it has less than target connections OR it has the least connections and we can't make new return result; @@ -453,15 +469,15 @@ namespace CryptoExchange.Net // Create new socket var socket = CreateSocket(address); - var socketWrapper = new SocketConnection(this, socket); - socketWrapper.UnhandledMessage += HandleUnhandledMessage; + var socketConnection = new SocketConnection(this, socket); + socketConnection.UnhandledMessage += HandleUnhandledMessage; foreach (var kvp in genericHandlers) { var handler = SocketSubscription.CreateForIdentifier(kvp.Key, false, kvp.Value); - socketWrapper.AddHandler(handler); + socketConnection.AddSubscription(handler); } - return socketWrapper; + return socketConnection; } /// @@ -477,9 +493,9 @@ namespace CryptoExchange.Net /// /// The socket to connect /// - protected virtual async Task> ConnectSocket(SocketConnection socketConnection) + protected virtual async Task> ConnectSocketAsync(SocketConnection socketConnection) { - if (await socketConnection.Socket.Connect().ConfigureAwait(false)) + if (await socketConnection.Socket.ConnectAsync().ConfigureAwait(false)) { sockets.TryAdd(socketConnection.Socket.Id, socketConnection); return new CallResult(true, null); @@ -497,23 +513,23 @@ namespace CryptoExchange.Net protected virtual IWebsocket CreateSocket(string address) { var socket = SocketFactory.CreateWebsocket(log, address); - log.Write(LogVerbosity.Debug, "Created new socket for " + address); + log.Write(LogLevel.Debug, "Created new socket for " + address); if (apiProxy != null) - socket.SetProxy(apiProxy.Host, apiProxy.Port); + socket.SetProxy(apiProxy); socket.Timeout = SocketNoDataTimeout; socket.DataInterpreterBytes = dataInterpreterBytes; socket.DataInterpreterString = dataInterpreterString; socket.OnError += e => { - log.Write(LogVerbosity.Info, $"Socket {socket.Id} error: " + e); + log.Write(LogLevel.Warning, $"Socket {socket.Id} error: " + e); }; return socket; } /// - /// Periodically sends an object to a socket + /// Periodically sends data over a socket connection /// /// How often /// Method returning the object to send @@ -532,7 +548,7 @@ namespace CryptoExchange.Net break; if (sockets.Any()) - log.Write(LogVerbosity.Debug, "Sending periodic"); + log.Write(LogLevel.Debug, "Sending periodic"); foreach (var socket in sockets.Values) { @@ -549,7 +565,7 @@ namespace CryptoExchange.Net } catch (Exception ex) { - log.Write(LogVerbosity.Warning, "Periodic send failed: " + ex); + log.Write(LogLevel.Warning, "Periodic send failed: " + ex); } } } @@ -558,26 +574,26 @@ namespace CryptoExchange.Net /// - /// Unsubscribe from a stream + /// Unsubscribe an update subscription /// /// The subscription to unsubscribe /// - public virtual async Task Unsubscribe(UpdateSubscription subscription) + public virtual async Task UnsubscribeAsync(UpdateSubscription subscription) { if (subscription == null) throw new ArgumentNullException(nameof(subscription)); - log.Write(LogVerbosity.Info, "Closing subscription"); - await subscription.Close().ConfigureAwait(false); + log.Write(LogLevel.Information, "Closing subscription"); + await subscription.CloseAsync().ConfigureAwait(false); } /// /// Unsubscribe all subscriptions /// /// - public virtual async Task UnsubscribeAll() + public virtual async Task UnsubscribeAllAsync() { - log.Write(LogVerbosity.Debug, $"Closing all {sockets.Sum(s => s.Value.HandlerCount)} subscriptions"); + log.Write(LogLevel.Debug, $"Closing all {sockets.Sum(s => s.Value.SubscriptionCount)} subscriptions"); await Task.Run(async () => { @@ -585,7 +601,7 @@ namespace CryptoExchange.Net { var socketList = sockets.Values; foreach (var sub in socketList) - tasks.Add(sub.Close()); + tasks.Add(sub.CloseAsync()); } await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false); @@ -600,8 +616,8 @@ namespace CryptoExchange.Net disposing = true; periodicEvent?.Set(); periodicEvent?.Dispose(); - log.Write(LogVerbosity.Debug, "Disposing socket client, closing all subscriptions"); - Task.Run(UnsubscribeAll).Wait(); + log.Write(LogLevel.Debug, "Disposing socket client, closing all subscriptions"); + Task.Run(UnsubscribeAllAsync).ConfigureAwait(false).GetAwaiter().GetResult(); semaphoreSlim?.Dispose(); base.Dispose(); } diff --git a/CryptoExchange.Net/Sockets/BaseSocket.cs b/CryptoExchange.Net/Sockets/BaseSocket.cs deleted file mode 100644 index a69be1f..0000000 --- a/CryptoExchange.Net/Sockets/BaseSocket.cs +++ /dev/null @@ -1,422 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Security.Authentication; -using System.Threading; -using System.Threading.Tasks; -using CryptoExchange.Net.Interfaces; -using CryptoExchange.Net.Logging; -using SuperSocket.ClientEngine; -using SuperSocket.ClientEngine.Proxy; -using WebSocket4Net; - -namespace CryptoExchange.Net.Sockets -{ - /// - /// Socket implementation - /// - public class BaseSocket: IWebsocket - { - internal static int lastStreamId; - private static readonly object streamIdLock = new object(); - - /// - /// Socket - /// - protected WebSocket? socket; - /// - /// Log - /// - protected Log log; - private readonly object socketLock = new object(); - - /// - /// Error handlers - /// - protected readonly List> errorHandlers = new List>(); - /// - /// Open handlers - /// - protected readonly List openHandlers = new List(); - /// - /// Close handlers - /// - protected readonly List closeHandlers = new List(); - /// - /// Message handlers - /// - protected readonly List> messageHandlers = new List>(); - - private readonly IDictionary cookies; - private readonly IDictionary headers; - private HttpConnectProxy? proxy; - - /// - /// Id - /// - public int Id { get; } - /// - /// If is reconnecting - /// - public bool Reconnecting { get; set; } - /// - /// Origin - /// - public string? Origin { get; set; } - - /// - /// Url - /// - public string Url { get; } - /// - /// Is closed - /// - public bool IsClosed => socket?.State == null || socket.State == WebSocketState.Closed; - /// - /// Is open - /// - public bool IsOpen => socket?.State == WebSocketState.Open; - /// - /// Protocols - /// - public SslProtocols SSLProtocols { get; set; } = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls; - /// - /// Interpreter for bytes - /// - public Func? DataInterpreterBytes { get; set; } - /// - /// Interpreter for strings - /// - public Func? DataInterpreterString { get; set; } - - /// - /// Last action time - /// - public DateTime LastActionTime { get; private set; } - /// - /// Timeout - /// - public TimeSpan Timeout { get; set; } - private Task? timeoutTask; - - /// - /// Socket state - /// - public WebSocketState SocketState => socket?.State ?? WebSocketState.None; - - /// - /// ctor - /// - /// - /// - public BaseSocket(Log log, string url):this(log, url, new Dictionary(), new Dictionary()) - { - } - - /// - /// ctor - /// - /// - /// - /// - /// - public BaseSocket(Log log, string url, IDictionary cookies, IDictionary headers) - { - Id = NextStreamId(); - this.log = log; - Url = url; - this.cookies = cookies; - this.headers = headers; - } - - private void HandleByteData(byte[] data) - { - if (DataInterpreterBytes == null) - throw new Exception("Byte interpreter not set while receiving byte data"); - - try - { - var message = DataInterpreterBytes(data); - Handle(messageHandlers, message); - } - catch (Exception ex) - { - log.Write(LogVerbosity.Error, $"{Id} Something went wrong while processing a byte message from the socket: {ex}"); - } - } - - private void HandleStringData(string data) - { - try - { - if (DataInterpreterString != null) - data = DataInterpreterString(data); - Handle(messageHandlers, data); - } - catch (Exception ex) - { - log.Write(LogVerbosity.Error, $"{Id} Something went wrong while processing a string message from the socket: {ex}"); - } - } - - /// - /// On close - /// - public event Action OnClose - { - add => closeHandlers.Add(value); - remove => closeHandlers.Remove(value); - } - /// - /// On message - /// - public event Action OnMessage - { - add => messageHandlers.Add(value); - remove => messageHandlers.Remove(value); - } - /// - /// On error - /// - public event Action OnError - { - add => errorHandlers.Add(value); - remove => errorHandlers.Remove(value); - } - /// - /// On open - /// - public event Action OnOpen - { - add => openHandlers.Add(value); - remove => openHandlers.Remove(value); - } - - /// - /// Handle - /// - /// - protected void Handle(List handlers) - { - LastActionTime = DateTime.UtcNow; - foreach (var handle in new List(handlers)) - handle?.Invoke(); - } - - /// - /// Handle - /// - /// - /// - /// - protected void Handle(List> handlers, T data) - { - LastActionTime = DateTime.UtcNow; - foreach (var handle in new List>(handlers)) - handle?.Invoke(data); - } - - /// - /// Checks if timed out - /// - /// - protected async Task CheckTimeout() - { - while (true) - { - lock (socketLock) - { - if (socket == null || socket.State != WebSocketState.Open) - return; - - if (DateTime.UtcNow - LastActionTime > Timeout) - { - log.Write(LogVerbosity.Warning, $"No data received for {Timeout}, reconnecting socket"); - _ = Close().ConfigureAwait(false); - return; - } - } - - await Task.Delay(500).ConfigureAwait(false); - } - } - - /// - /// Close socket - /// - /// - public virtual async Task Close() - { - await Task.Run(() => - { - lock (socketLock) - { - if (socket == null || IsClosed) - { - log?.Write(LogVerbosity.Debug, $"Socket {Id} was already closed/disposed"); - return; - } - - var waitLock = new object(); - log?.Write(LogVerbosity.Debug, $"Socket {Id} closing"); - ManualResetEvent? evnt = new ManualResetEvent(false); - var handler = new EventHandler((o, a) => - { - lock(waitLock) - evnt?.Set(); - }); - socket.Closed += handler; - socket.Close(); - evnt.WaitOne(2000); - lock (waitLock) - { - socket.Closed -= handler; - evnt.Dispose(); - evnt = null; - } - log?.Write(LogVerbosity.Debug, $"Socket {Id} closed"); - } - }).ConfigureAwait(false); - } - - /// - /// Reset socket - /// - public virtual void Reset() - { - lock (socketLock) - { - log.Write(LogVerbosity.Debug, $"Socket {Id} resetting"); - socket?.Dispose(); - socket = null; - } - } - - /// - /// Send data - /// - /// - public virtual void Send(string data) - { - socket?.Send(data); - } - - /// - /// Connect socket - /// - /// - public virtual Task Connect() - { - if (socket == null) - { - socket = new WebSocket(Url, cookies: cookies.ToList(), customHeaderItems: headers.ToList(), origin: Origin ?? "") - { - EnableAutoSendPing = true, - AutoSendPingInterval = 10 - }; - - if (proxy != null) - socket.Proxy = proxy; - - socket.Security.EnabledSslProtocols = SSLProtocols; - socket.Opened += (o, s) => Handle(openHandlers); - socket.Closed += (o, s) => Handle(closeHandlers); - socket.Error += (o, s) => Handle(errorHandlers, s.Exception); - socket.MessageReceived += (o, s) => HandleStringData(s.Message); - socket.DataReceived += (o, s) => HandleByteData(s.Data); - } - - return Task.Run(() => - { - bool connected; - lock (socketLock) - { - log?.Write(LogVerbosity.Debug, $"Socket {Id} connecting"); - var waitLock = new object(); - ManualResetEvent? evnt = new ManualResetEvent(false); - var handler = new EventHandler((o, a) => - { - lock (waitLock) - evnt?.Set(); - }); - var errorHandler = new EventHandler((o, a) => - { - lock(waitLock) - evnt?.Set(); - }); - socket.Opened += handler; - socket.Closed += handler; - socket.Error += errorHandler; - socket.Open(); - evnt.WaitOne(TimeSpan.FromSeconds(15)); - lock (waitLock) - { - socket.Opened -= handler; - socket.Closed -= handler; - socket.Error -= errorHandler; - evnt.Dispose(); - evnt = null; - } - connected = socket.State == WebSocketState.Open; - if (connected) - { - log?.Write(LogVerbosity.Debug, $"Socket {Id} connected"); - if ((timeoutTask == null || timeoutTask.IsCompleted) && Timeout != default) - timeoutTask = Task.Run(CheckTimeout); - } - else - { - log?.Write(LogVerbosity.Debug, $"Socket {Id} connection failed, state: " + socket.State); - } - } - - if (socket.State == WebSocketState.Connecting) - socket.Close(); - - return connected; - }); - } - - /// - /// Set a proxy - /// - /// - /// - public virtual void SetProxy(string host, int port) - { - proxy = IPAddress.TryParse(host, out var address) - ? new HttpConnectProxy(new IPEndPoint(address, port)) - : new HttpConnectProxy(new DnsEndPoint(host, port)); - } - - /// - /// Dispose - /// - public void Dispose() - { - lock (socketLock) - { - if (socket != null) - log?.Write(LogVerbosity.Debug, $"Socket {Id} disposing websocket"); - - socket?.Dispose(); - socket = null; - - errorHandlers.Clear(); - openHandlers.Clear(); - closeHandlers.Clear(); - messageHandlers.Clear(); - } - } - - private static int NextStreamId() - { - lock (streamIdLock) - { - lastStreamId++; - return lastStreamId; - } - } - } -} diff --git a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs new file mode 100644 index 0000000..f57abae --- /dev/null +++ b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs @@ -0,0 +1,561 @@ +using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Logging; +using CryptoExchange.Net.Objects; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.WebSockets; +using System.Security.Authentication; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.Sockets +{ + /// + /// A wrapper around the ClientWebSocket + /// + public class CryptoExchangeWebSocketClient : IWebsocket + { + internal static int lastStreamId; + private static readonly object streamIdLock = new object(); + + private ClientWebSocket _socket; + private Task? _sendTask; + private Task? _receiveTask; + private Task? _timeoutTask; + private readonly AutoResetEvent _sendEvent; + private readonly ConcurrentQueue _sendBuffer; + private readonly IDictionary cookies; + private readonly IDictionary headers; + private CancellationTokenSource _ctsSource; + private bool _closing; + + /// + /// Log + /// + protected Log log; + + /// + /// Handlers for when an error happens on the socket + /// + protected readonly List> errorHandlers = new List>(); + /// + /// Handlers for when the socket connection is opened + /// + protected readonly List openHandlers = new List(); + /// + /// Handlers for when the connection is closed + /// + protected readonly List closeHandlers = new List(); + /// + /// Handlers for when a message is received + /// + protected readonly List> messageHandlers = new List>(); + + /// + /// The id of this socket + /// + public int Id { get; } + + /// + public string? Origin { get; set; } + /// + /// Whether this socket is currently reconnecting + /// + public bool Reconnecting { get; set; } + /// + /// The timestamp this socket has been active for the last time + /// + public DateTime LastActionTime { get; private set; } + + /// + /// Delegate used for processing byte data received from socket connections before it is processed by handlers + /// + public Func? DataInterpreterBytes { get; set; } + /// + /// Delegate used for processing string data received from socket connections before it is processed by handlers + /// + public Func? DataInterpreterString { get; set; } + /// + /// Url this socket connects to + /// + public string Url { get; } + /// + /// If the connection is closed + /// + public bool IsClosed => _socket.State == WebSocketState.Closed; + + /// + /// If the connection is open + /// + public bool IsOpen => _socket.State == WebSocketState.Open; + + /// + /// Ssl protocols supported. NOT USED BY THIS IMPLEMENTATION + /// + public SslProtocols SSLProtocols { get; set; } + + private Encoding _encoding = Encoding.UTF8; + /// + /// Encoding used for decoding the received bytes into a string + /// + public Encoding? Encoding + { + get => _encoding; + set + { + if(value != null) + _encoding = value; + } + } + + /// + /// The timespan no data is received on the socket. If no data is received within this time an error is generated + /// + public TimeSpan Timeout { get; set; } + + /// + /// Socket closed event + /// + public event Action OnClose + { + add => closeHandlers.Add(value); + remove => closeHandlers.Remove(value); + } + /// + /// Socket message received event + /// + public event Action OnMessage + { + add => messageHandlers.Add(value); + remove => messageHandlers.Remove(value); + } + /// + /// Socket error event + /// + public event Action OnError + { + add => errorHandlers.Add(value); + remove => errorHandlers.Remove(value); + } + /// + /// Socket opened event + /// + public event Action OnOpen + { + add => openHandlers.Add(value); + remove => openHandlers.Remove(value); + } + + /// + /// ctor + /// + /// The log object to use + /// The url the socket should connect to + public CryptoExchangeWebSocketClient(Log log, string url) : this(log, url, new Dictionary(), new Dictionary()) + { + } + + /// + /// ctor + /// + /// The log object to use + /// The url the socket should connect to + /// Cookies to sent in the socket connection request + /// Headers to sent in the socket connection request + public CryptoExchangeWebSocketClient(Log log, string url, IDictionary cookies, IDictionary headers) + { + Id = NextStreamId(); + this.log = log; + Url = url; + this.cookies = cookies; + this.headers = headers; + + _sendEvent = new AutoResetEvent(false); + _sendBuffer = new ConcurrentQueue(); + _ctsSource = new CancellationTokenSource(); + + _socket = CreateSocket(); + } + + /// + /// Set a proxy to use. Should be set before connecting + /// + /// + public virtual void SetProxy(ApiProxy proxy) + { + _socket.Options.Proxy = new WebProxy(proxy.Host, proxy.Port); + if (proxy.Login != null) + _socket.Options.Proxy.Credentials = new NetworkCredential(proxy.Login, proxy.Password); + } + + /// + /// Connect the websocket + /// + /// True if successfull + public virtual async Task ConnectAsync() + { + log.Write(LogLevel.Debug, $"Socket {Id} connecting"); + try + { + using CancellationTokenSource tcs = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + await _socket.ConnectAsync(new Uri(Url), default).ConfigureAwait(false); + + Handle(openHandlers); + } + catch (Exception e) + { + log.Write(LogLevel.Debug, $"Socket {Id} connection failed: " + e.ToLogString()); + return false; + } + + log.Write(LogLevel.Debug, $"Socket {Id} connected"); + _sendTask = Task.Run(async () => await SendLoopAsync().ConfigureAwait(false)); + _receiveTask = Task.Run(ReceiveLoopAsync); + if (Timeout != default) + _timeoutTask = Task.Run(CheckTimeoutAsync); + return true; + } + + /// + /// Send data over the websocket + /// + /// Data to send + public virtual void Send(string data) + { + if (_closing) + throw new InvalidOperationException("Can't send data when socket is not connected"); + + var bytes = _encoding.GetBytes(data); + _sendBuffer.Enqueue(bytes); + _sendEvent.Set(); + } + + /// + /// Close the websocket + /// + /// + public virtual async Task CloseAsync() + { + log.Write(LogLevel.Debug, $"Socket {Id} closing"); + await CloseInternalAsync(true, true).ConfigureAwait(false); + } + + /// + /// Internal close method, will wait for each task to complete to gracefully close + /// + /// + /// + /// + private async Task CloseInternalAsync(bool waitSend, bool waitReceive) + { + if (_closing) + return; + + _closing = true; + var tasksToAwait = new List(); + if (_socket.State == WebSocketState.Open) + tasksToAwait.Add(_socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default)); + + _ctsSource.Cancel(); + _sendEvent.Set(); + if (waitSend) + tasksToAwait.Add(_sendTask!); + if (waitReceive) + tasksToAwait.Add(_receiveTask!); + if (_timeoutTask != null) + tasksToAwait.Add(_timeoutTask); + + await Task.WhenAll(tasksToAwait).ConfigureAwait(false); + log.Write(LogLevel.Debug, $"Socket {Id} closed"); + Handle(closeHandlers); + } + + /// + /// Dispose the socket + /// + public void Dispose() + { + log.Write(LogLevel.Debug, $"Socket {Id} disposing"); + _socket.Dispose(); + _ctsSource.Dispose(); + + errorHandlers.Clear(); + openHandlers.Clear(); + closeHandlers.Clear(); + messageHandlers.Clear(); + } + + /// + /// Reset the socket so a new connection can be attempted after it has been connected before + /// + public void Reset() + { + log.Write(LogLevel.Debug, $"Socket {Id} resetting"); + _ctsSource = new CancellationTokenSource(); + _closing = false; + _socket = CreateSocket(); + } + + /// + /// Create the socket object + /// + private ClientWebSocket CreateSocket() + { + var cookieContainer = new CookieContainer(); + foreach (var cookie in cookies) + cookieContainer.Add(new Cookie(cookie.Key, cookie.Value)); + + var socket = new ClientWebSocket(); + socket.Options.Cookies = cookieContainer; + foreach (var header in headers) + socket.Options.SetRequestHeader(header.Key, header.Value); + socket.Options.KeepAliveInterval = TimeSpan.FromSeconds(10); + socket.Options.SetBuffer(65536, 65536); // Setting it to anything bigger than 65536 throws an exception in .net framework + return socket; + } + + /// + /// Loop for sending data + /// + /// + private async Task SendLoopAsync() + { + while (true) + { + _sendEvent.WaitOne(); + + if (_closing) + break; + + if (!_sendBuffer.TryDequeue(out var data)) + continue; + + try + { + await _socket.SendAsync(new ArraySegment(data, 0, data.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false); + } + catch (TaskCanceledException) + { + // cancelled + break; + } + catch (WebSocketException wse) + { + // Connection closed unexpectedly + Handle(errorHandlers, wse); + await CloseInternalAsync(false, true).ConfigureAwait(false); + break; + } + } + } + + /// + /// Loop for receiving and reassembling data + /// + /// + private async Task ReceiveLoopAsync() + { + var buffer = new ArraySegment(new byte[4096]); + var received = 0; + while (true) + { + if (_closing) + break; + + MemoryStream? memoryStream = null; + WebSocketReceiveResult? receiveResult = null; + bool multiPartMessage = false; + while (true) + { + try + { + receiveResult = await _socket.ReceiveAsync(buffer, _ctsSource.Token).ConfigureAwait(false); + received += receiveResult.Count; + } + catch (TaskCanceledException) + { + // Cancelled + break; + } + catch (WebSocketException wse) + { + // Connection closed unexpectedly + Handle(errorHandlers, wse); + await CloseInternalAsync(true, false).ConfigureAwait(false); + break; + } + + if (receiveResult.MessageType == WebSocketMessageType.Close) + { + // Connection closed unexpectedly + log.Write(LogLevel.Debug, $"Socket {Id} received `Close` message"); + await CloseInternalAsync(true, false).ConfigureAwait(false); + break; + } + + if (!receiveResult.EndOfMessage) + { + // We received data, but it is not complete, write it to a memory stream for reassembling + multiPartMessage = true; + if (memoryStream == null) + memoryStream = new MemoryStream(); + await memoryStream.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false); + } + else + { + if (!multiPartMessage) + // Received a complete message and it's not multi part + HandleMessage(buffer.Array, buffer.Offset, receiveResult.Count, receiveResult.MessageType); + else + // Received the end of a multipart message, write to memory stream for reassembling + await memoryStream!.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false); + break; + } + } + + if (receiveResult?.MessageType == WebSocketMessageType.Close) + { + // Received close message + break; + } + + if (receiveResult == null || _closing) + { + // Error during receiving or cancellation requested, stop. + break; + } + + if (multiPartMessage) + { + // Reassemble complete message from memory stream + HandleMessage(memoryStream!.ToArray(), 0, (int)memoryStream.Length, receiveResult.MessageType); + memoryStream.Dispose(); + } + } + } + + /// + /// Handles the message + /// + /// + /// + /// + /// + private void HandleMessage(byte[] data, int offset, int count, WebSocketMessageType messageType) + { + string strData; + if (messageType == WebSocketMessageType.Binary) + { + if (DataInterpreterBytes == null) + throw new Exception("Byte interpreter not set while receiving byte data"); + + try + { + strData = DataInterpreterBytes(data); + } + catch(Exception e) + { + log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during byte data interpretation: " + e.ToLogString()); + return; + } + } + else + strData = _encoding.GetString(data, offset, count); + + if (DataInterpreterString != null) + { + try + { + strData = DataInterpreterString(strData); + } + catch(Exception e) + { + log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during string data interpretation: " + e.ToLogString()); + return; + } + } + + try + { + Handle(messageHandlers, strData); + } + catch(Exception e) + { + log.Write(LogLevel.Error, $"Socket {Id} unhandled exception during message processing: " + e.ToLogString()); + return; + } + } + + /// + /// Checks if there is no data received for a period longer than the specified timeout + /// + /// + protected async Task CheckTimeoutAsync() + { + log.Write(LogLevel.Debug, $"Socket {Id} Starting task checking for no data received for {Timeout}"); + while (true) + { + if (_closing) + return; + + if (DateTime.UtcNow - LastActionTime > Timeout) + { + log.Write(LogLevel.Warning, $"Socket {Id} No data received for {Timeout}, reconnecting socket"); + _ = CloseAsync().ConfigureAwait(false); + return; + } + try + { + await Task.Delay(500, _ctsSource.Token).ConfigureAwait(false); + } + catch(TaskCanceledException) + { + // cancelled + return; + } + } + } + + /// + /// Helper to invoke handlers + /// + /// + protected void Handle(List handlers) + { + LastActionTime = DateTime.UtcNow; + foreach (var handle in new List(handlers)) + handle?.Invoke(); + } + + /// + /// Helper to invoke handlers + /// + /// + /// + /// + protected void Handle(List> handlers, T data) + { + LastActionTime = DateTime.UtcNow; + foreach (var handle in new List>(handlers)) + handle?.Invoke(data); + } + + /// + /// Get the next identifier + /// + /// + private static int NextStreamId() + { + lock (streamIdLock) + { + lastStreamId++; + return lastStreamId; + } + } + } +} diff --git a/CryptoExchange.Net/Sockets/DataEvent.cs b/CryptoExchange.Net/Sockets/DataEvent.cs new file mode 100644 index 0000000..f85cded --- /dev/null +++ b/CryptoExchange.Net/Sockets/DataEvent.cs @@ -0,0 +1,72 @@ +using System; + +namespace CryptoExchange.Net.Sockets +{ + /// + /// An update received from a socket update subscription + /// + /// The type of the data + public class DataEvent + { + /// + /// The timestamp the data was received + /// + public DateTime Timestamp { get; set; } + /// + /// The topic of the update, what symbol/asset etc.. + /// + public string? Topic { get; set; } + /// + /// The original data that was received, only available when OutputOriginalData is set to true in the client options + /// + public string? OriginalData { get; set; } + /// + /// The received data deserialized into an object + /// + public T Data { get; set; } + + internal DataEvent(T data, DateTime timestamp) + { + Data = data; + Timestamp = timestamp; + } + + internal DataEvent(T data, string? topic, DateTime timestamp) + { + Data = data; + Topic = topic; + Timestamp = timestamp; + } + + internal DataEvent(T data, string? topic, string? originalData, DateTime timestamp) + { + Data = data; + Topic = topic; + OriginalData = originalData; + Timestamp = timestamp; + } + + /// + /// Create a new DataEvent with data in the from of type K based on the current DataEvent. Topic, OriginalData and Timestamp will be copied over + /// + /// The type of the new data + /// The new data + /// + public DataEvent As(K data) + { + return new DataEvent(data, Topic, OriginalData, Timestamp); + } + + /// + /// Create a new DataEvent with data in the from of type K based on the current DataEvent. OriginalData and Timestamp will be copied over + /// + /// The type of the new data + /// The new data + /// The new topic + /// + public DataEvent As(K data, string? topic) + { + return new DataEvent(data, topic, OriginalData, Timestamp); + } + } +} diff --git a/CryptoExchange.Net/Sockets/MessageEvent.cs b/CryptoExchange.Net/Sockets/MessageEvent.cs new file mode 100644 index 0000000..02a3792 --- /dev/null +++ b/CryptoExchange.Net/Sockets/MessageEvent.cs @@ -0,0 +1,43 @@ +using Newtonsoft.Json.Linq; +using System; + +namespace CryptoExchange.Net.Sockets +{ + /// + /// Message received event + /// + public class MessageEvent + { + /// + /// The connection the message was received on + /// + public SocketConnection Connection { get; set; } + /// + /// The json object of the data + /// + public JToken JsonData { get; set; } + /// + /// The originally received string data + /// + public string? OriginalData { get; set; } + /// + /// The timestamp of when the data was received + /// + public DateTime ReceivedTimestamp { get; set; } + + /// + /// + /// + /// + /// + /// + /// + public MessageEvent(SocketConnection connection, JToken jsonData, string? originalData, DateTime timestamp) + { + Connection = connection; + JsonData = jsonData; + OriginalData = originalData; + ReceivedTimestamp = timestamp; + } + } +} diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs index 83721bb..d50303e 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/SocketConnection.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using CryptoExchange.Net.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Microsoft.Extensions.Logging; namespace CryptoExchange.Net.Sockets { @@ -42,12 +43,12 @@ namespace CryptoExchange.Net.Sockets public event Action? UnhandledMessage; /// - /// The amount of handlers + /// The amount of subscriptions on this connection /// - public int HandlerCount + public int SubscriptionCount { - get { lock (handlersLock) - return handlers.Count(h => h.UserSubscription); } + get { lock (subscriptionLock) + return subscriptions.Count(h => h.UserSubscription); } } /// @@ -60,11 +61,11 @@ namespace CryptoExchange.Net.Sockets public bool Connected { get; private set; } /// - /// The socket + /// The underlying socket /// public IWebsocket Socket { get; set; } /// - /// If should reconnect upon closing + /// If the socket should be reconnected upon closing /// public bool ShouldReconnect { get; set; } @@ -84,7 +85,7 @@ namespace CryptoExchange.Net.Sockets if (pausedActivity != value) { pausedActivity = value; - log.Write(LogVerbosity.Debug, "Paused activity: " + value); + log.Write(LogLevel.Debug, "Paused activity: " + value); if(pausedActivity) ActivityPaused?.Invoke(); else ActivityUnpaused?.Invoke(); } @@ -92,8 +93,8 @@ namespace CryptoExchange.Net.Sockets } private bool pausedActivity; - private readonly List handlers; - private readonly object handlersLock = new object(); + private readonly List subscriptions; + private readonly object subscriptionLock = new object(); private bool lostTriggered; private readonly Log log; @@ -113,7 +114,7 @@ namespace CryptoExchange.Net.Sockets pendingRequests = new List(); - handlers = new List(); + subscriptions = new List(); Socket = socket; Socket.Timeout = client.SocketNoDataTimeout; @@ -137,9 +138,14 @@ namespace CryptoExchange.Net.Sockets }; } + /// + /// Process a message received by the socket + /// + /// private void ProcessMessage(string data) { - log.Write(LogVerbosity.Debug, $"Socket {Socket.Id} received data: " + data); + var timestamp = DateTime.UtcNow; + log.Write(LogLevel.Trace, $"Socket {Socket.Id} received data: " + data); if (string.IsNullOrEmpty(data)) return; var tokenData = data.ToJToken(log); @@ -151,12 +157,14 @@ namespace CryptoExchange.Net.Sockets return; } + var messageEvent = new MessageEvent(this, tokenData, socketClient.OutputOriginalData ? data: null, timestamp); var handledResponse = false; PendingRequest[] requests; lock(pendingRequests) { requests = pendingRequests.ToArray(); } + // Check if this message is an answer on any pending requests foreach (var pendingRequest in requests) { if (pendingRequest.Check(tokenData)) @@ -176,51 +184,54 @@ namespace CryptoExchange.Net.Sockets } } - if (!HandleData(tokenData) && !handledResponse) + // Message was not a request response, check data handlers + if (!HandleData(messageEvent) && !handledResponse) { if (!socketClient.UnhandledMessageExpected) - log.Write(LogVerbosity.Warning, "Message not handled: " + tokenData); + log.Write(LogLevel.Warning, $"Socket {Socket.Id} Message not handled: " + tokenData); UnhandledMessage?.Invoke(tokenData); } } /// - /// Add handler + /// Add subscription to this connection /// - /// - public void AddHandler(SocketSubscription handler) + /// + public void AddSubscription(SocketSubscription subscription) { - lock(handlersLock) - handlers.Add(handler); + lock(subscriptionLock) + subscriptions.Add(subscription); } - private bool HandleData(JToken tokenData) + private bool HandleData(MessageEvent messageEvent) { SocketSubscription? currentSubscription = null; try { var handled = false; var sw = Stopwatch.StartNew(); - lock (handlersLock) + + // Loop the subscriptions to check if any of them signal us that the message is for them + lock (subscriptionLock) { - foreach (var handler in handlers.ToList()) + foreach (var subscription in subscriptions.ToList()) { - currentSubscription = handler; - if (handler.Request == null) + currentSubscription = subscription; + if (subscription.Request == null) { - if (socketClient.MessageMatchesHandler(tokenData, handler.Identifier!)) + if (socketClient.MessageMatchesHandler(messageEvent.JsonData, subscription.Identifier!)) { handled = true; - handler.MessageHandler(this, tokenData); + subscription.MessageHandler(messageEvent); } } else { - if (socketClient.MessageMatchesHandler(tokenData, handler.Request)) + if (socketClient.MessageMatchesHandler(messageEvent.JsonData, subscription.Request)) { handled = true; - tokenData = socketClient.ProcessTokenData(tokenData); - handler.MessageHandler(this, tokenData); + messageEvent.JsonData = socketClient.ProcessTokenData(messageEvent.JsonData); + subscription.MessageHandler(messageEvent); } } } @@ -228,29 +239,29 @@ namespace CryptoExchange.Net.Sockets sw.Stop(); if (sw.ElapsedMilliseconds > 500) - log.Write(LogVerbosity.Warning, $"Socket {Socket.Id} message processing slow ({sw.ElapsedMilliseconds}ms), consider offloading data handling to another thread. " + + log.Write(LogLevel.Warning, $"Socket {Socket.Id} message processing slow ({sw.ElapsedMilliseconds}ms), consider offloading data handling to another thread. " + "Data from this socket may arrive late or not at all if message processing is continuously slow."); else - log.Write(LogVerbosity.Debug, $"Socket {Socket.Id} message processed in {sw.ElapsedMilliseconds}ms"); + log.Write(LogLevel.Trace, $"Socket {Socket.Id} message processed in {sw.ElapsedMilliseconds}ms"); return handled; } catch (Exception ex) { - log.Write(LogVerbosity.Error, $"Socket {Socket.Id} Exception during message processing\r\nException: {ex}\r\nData: {tokenData}"); + log.Write(LogLevel.Error, $"Socket {Socket.Id} Exception during message processing\r\nException: {ex.ToLogString()}\r\nData: {messageEvent.JsonData}"); currentSubscription?.InvokeExceptionHandler(ex); return false; } } /// - /// Send data + /// Send data and wait for an answer /// - /// The data type + /// The data type expected in response /// The object to send /// The timeout for response /// The response handler /// - public virtual Task SendAndWait(T obj, TimeSpan timeout, Func handler) + public virtual Task SendAndWaitAsync(T obj, TimeSpan timeout, Func handler) { var pending = new PendingRequest(handler, timeout); lock (pendingRequests) @@ -262,7 +273,7 @@ namespace CryptoExchange.Net.Sockets } /// - /// Send data to the websocket + /// Send data over the websocket connection /// /// The type of the object to send /// The object to send @@ -276,12 +287,12 @@ namespace CryptoExchange.Net.Sockets } /// - /// Send string data to the websocket + /// Send string data over the websocket connection /// /// The data to send public virtual void Send(string data) { - log.Write(LogVerbosity.Debug, $"Socket {Socket.Id} sending data: {data}"); + log.Write(LogLevel.Debug, $"Socket {Socket.Id} sending data: {data}"); Socket.Send(data); } @@ -297,11 +308,12 @@ namespace CryptoExchange.Net.Sockets Socket.Reconnecting = true; - log.Write(LogVerbosity.Info, $"Socket {Socket.Id} Connection lost, will try to reconnect after {socketClient.ReconnectInterval}"); + log.Write(LogLevel.Information, $"Socket {Socket.Id} Connection lost, will try to reconnect after {socketClient.ReconnectInterval}"); Task.Run(async () => { while (ShouldReconnect) { + // Wait a bit before attempting reconnect await Task.Delay(socketClient.ReconnectInterval).ConfigureAwait(false); if (!ShouldReconnect) { @@ -311,20 +323,21 @@ namespace CryptoExchange.Net.Sockets } Socket.Reset(); - if (!await Socket.Connect().ConfigureAwait(false)) + if (!await Socket.ConnectAsync().ConfigureAwait(false)) { - log.Write(LogVerbosity.Debug, $"Socket {Socket.Id} failed to reconnect"); + log.Write(LogLevel.Debug, $"Socket {Socket.Id} failed to reconnect"); continue; } + // Successfully reconnected var time = DisconnectTime; DisconnectTime = null; - log.Write(LogVerbosity.Info, $"Socket {Socket.Id} reconnected after {DateTime.UtcNow - time}"); + log.Write(LogLevel.Information, $"Socket {Socket.Id} reconnected after {DateTime.UtcNow - time}"); - var reconnectResult = await ProcessReconnect().ConfigureAwait(false); + var reconnectResult = await ProcessReconnectAsync().ConfigureAwait(false); if (!reconnectResult) - await Socket.Close().ConfigureAwait(false); + await Socket.CloseAsync().ConfigureAwait(false); else { if (lostTriggered) @@ -342,11 +355,11 @@ namespace CryptoExchange.Net.Sockets } else { - log.Write(LogVerbosity.Info, $"Socket {Socket.Id} closed"); + // No reconnecting needed + log.Write(LogLevel.Information, $"Socket {Socket.Id} closed"); if (socketClient.sockets.ContainsKey(Socket.Id)) socketClient.sockets.TryRemove(Socket.Id, out _); - Socket.Dispose(); Closed?.Invoke(); } } @@ -356,29 +369,32 @@ namespace CryptoExchange.Net.Sockets await Task.Run(() => ConnectionRestored?.Invoke(disconnectTime.HasValue ? DateTime.UtcNow - disconnectTime.Value : TimeSpan.FromSeconds(0))).ConfigureAwait(false); } - private async Task ProcessReconnect() + private async Task ProcessReconnectAsync() { if (Authenticated) { - var authResult = await socketClient.AuthenticateSocket(this).ConfigureAwait(false); + // If we reconnected a authenticated connection we need to re-authenticate + var authResult = await socketClient.AuthenticateSocketAsync(this).ConfigureAwait(false); if (!authResult) { - log.Write(LogVerbosity.Info, "Authentication failed on reconnected socket. Disconnecting and reconnecting."); + log.Write(LogLevel.Information, $"Socket {Socket.Id} authentication failed on reconnected socket. Disconnecting and reconnecting."); return false; } - log.Write(LogVerbosity.Debug, "Authentication succeeded on reconnected socket."); + log.Write(LogLevel.Debug, $"Socket {Socket.Id} authentication succeeded on reconnected socket."); } - List handlerList; - lock (handlersLock) - handlerList = handlers.Where(h => h.Request != null).ToList(); + // Get a list of all subscriptions on the socket + List subscriptionList; + lock (subscriptionLock) + subscriptionList = subscriptions.Where(h => h.Request != null).ToList(); var success = true; var taskList = new List(); - foreach (var handler in handlerList) + // Foreach subscription which is subscribed by a subscription request we will need to resend that request to resubscribe + foreach (var subscription in subscriptionList) { - var task = socketClient.SubscribeAndWait(this, handler.Request!, handler).ContinueWith(t => + var task = socketClient.SubscribeAndWaitAsync(this, subscription.Request!, subscription).ContinueWith(t => { if (!t.Result) success = false; @@ -386,14 +402,14 @@ namespace CryptoExchange.Net.Sockets taskList.Add(task); } - Task.WaitAll(taskList.ToArray()); + await Task.WhenAll(taskList).ConfigureAwait(false); if (!success) { - log.Write(LogVerbosity.Debug, "Resubscribing all subscriptions failed on reconnected socket. Disconnecting and reconnecting."); + log.Write(LogLevel.Debug, $"Socket {Socket.Id} resubscribing all subscriptions failed on reconnected socket. Disconnecting and reconnecting."); return false; } - log.Write(LogVerbosity.Debug, "All subscription successfully resubscribed on reconnected socket."); + log.Write(LogLevel.Debug, $"Socket {Socket.Id} all subscription successfully resubscribed on reconnected socket."); return true; } @@ -401,36 +417,39 @@ namespace CryptoExchange.Net.Sockets /// Close the connection /// /// - public async Task Close() + public async Task CloseAsync() { Connected = false; ShouldReconnect = false; if (socketClient.sockets.ContainsKey(Socket.Id)) socketClient.sockets.TryRemove(Socket.Id, out _); - await Socket.Close().ConfigureAwait(false); + await Socket.CloseAsync().ConfigureAwait(false); Socket.Dispose(); } /// - /// Close the subscription + /// Close a subscription on this connection. If all subscriptions on this connection are closed the connection gets closed as well /// /// Subscription to close /// - public async Task Close(SocketSubscription subscription) + public async Task CloseAsync(SocketSubscription subscription) { + if (!Socket.IsOpen) + return; + if (subscription.Confirmed) - await socketClient.Unsubscribe(this, subscription).ConfigureAwait(false); + await socketClient.UnsubscribeAsync(this, subscription).ConfigureAwait(false); - var shouldCloseWrapper = false; - lock (handlersLock) - shouldCloseWrapper = handlers.Count(r => r.UserSubscription && subscription != r) == 0; + var shouldCloseConnection = false; + lock (subscriptionLock) + shouldCloseConnection = !subscriptions.Any(r => r.UserSubscription && subscription != r); - if (shouldCloseWrapper) - await Close().ConfigureAwait(false); + if (shouldCloseConnection) + await CloseAsync().ConfigureAwait(false); - lock (handlersLock) - handlers.Remove(subscription); + lock (subscriptionLock) + subscriptions.Remove(subscription); } } diff --git a/CryptoExchange.Net/Sockets/SocketSubscription.cs b/CryptoExchange.Net/Sockets/SocketSubscription.cs index 492ab9d..9481bc4 100644 --- a/CryptoExchange.Net/Sockets/SocketSubscription.cs +++ b/CryptoExchange.Net/Sockets/SocketSubscription.cs @@ -1,5 +1,4 @@ using System; -using Newtonsoft.Json.Linq; namespace CryptoExchange.Net.Sockets { @@ -16,7 +15,7 @@ namespace CryptoExchange.Net.Sockets /// /// Message handlers for this subscription. Should return true if the message is handled and should not be distributed to the other handlers /// - public Action MessageHandler { get; set; } + public Action MessageHandler { get; set; } /// /// Request object @@ -36,7 +35,7 @@ namespace CryptoExchange.Net.Sockets /// public bool Confirmed { get; set; } - private SocketSubscription(object? request, string? identifier, bool userSubscription, Action dataHandler) + private SocketSubscription(object? request, string? identifier, bool userSubscription, Action dataHandler) { UserSubscription = userSubscription; MessageHandler = dataHandler; @@ -52,7 +51,7 @@ namespace CryptoExchange.Net.Sockets /// /// public static SocketSubscription CreateForRequest(object request, bool userSubscription, - Action dataHandler) + Action dataHandler) { return new SocketSubscription(request, null, userSubscription, dataHandler); } @@ -65,7 +64,7 @@ namespace CryptoExchange.Net.Sockets /// /// public static SocketSubscription CreateForIdentifier(string identifier, bool userSubscription, - Action dataHandler) + Action dataHandler) { return new SocketSubscription(null, identifier, userSubscription, dataHandler); } diff --git a/CryptoExchange.Net/Sockets/UpdateSubscription.cs b/CryptoExchange.Net/Sockets/UpdateSubscription.cs index 98f37b4..c6e7b7a 100644 --- a/CryptoExchange.Net/Sockets/UpdateSubscription.cs +++ b/CryptoExchange.Net/Sockets/UpdateSubscription.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace CryptoExchange.Net.Sockets { /// - /// Subscription + /// Subscription to a data stream /// public class UpdateSubscription { @@ -21,7 +21,9 @@ namespace CryptoExchange.Net.Sockets } /// - /// Event when the connection is restored. Timespan parameter indicates the time the socket has been offline for before reconnecting + /// Event when the connection is restored. Timespan parameter indicates the time the socket has been offline for before reconnecting. + /// Note that when the executing code is suspended and resumed at a later period (for example laptop going to sleep) the disconnect time will be incorrect as the diconnect + /// will only be detected after resuming. This will lead to an incorrect disconnected timespan. /// public event Action ConnectionRestored { @@ -30,7 +32,7 @@ namespace CryptoExchange.Net.Sockets } /// - /// Event when the connection to the server is paused. No operations can be performed while paused + /// Event when the connection to the server is paused based on a server indication. No operations can be performed while paused /// public event Action ActivityPaused { @@ -39,7 +41,7 @@ namespace CryptoExchange.Net.Sockets } /// - /// Event when the connection to the server is unpaused + /// Event when the connection to the server is unpaused after being paused /// public event Action ActivityUnpaused { @@ -48,7 +50,7 @@ namespace CryptoExchange.Net.Sockets } /// - /// Event when an exception happened + /// Event when an exception happens during the handling of the data /// public event Action Exception { @@ -64,8 +66,8 @@ namespace CryptoExchange.Net.Sockets /// /// ctor /// - /// - /// + /// The socket connection the subscription is on + /// The subscription public UpdateSubscription(SocketConnection connection, SocketSubscription subscription) { this.connection = connection; @@ -76,18 +78,18 @@ namespace CryptoExchange.Net.Sockets /// Close the subscription /// /// - public async Task Close() + public Task CloseAsync() { - await connection.Close(subscription).ConfigureAwait(false); + return connection.CloseAsync(subscription); } /// /// Close the socket to cause a reconnect /// /// - internal Task Reconnect() + internal Task ReconnectAsync() { - return connection.Socket.Close(); + return connection.Socket.CloseAsync(); } } } diff --git a/CryptoExchange.Net/Sockets/WebsocketFactory.cs b/CryptoExchange.Net/Sockets/WebsocketFactory.cs index bfe17df..33ca49e 100644 --- a/CryptoExchange.Net/Sockets/WebsocketFactory.cs +++ b/CryptoExchange.Net/Sockets/WebsocketFactory.cs @@ -5,20 +5,20 @@ using CryptoExchange.Net.Logging; namespace CryptoExchange.Net.Sockets { /// - /// Factory implementation + /// Default weboscket factory implementation /// public class WebsocketFactory : IWebsocketFactory { /// public IWebsocket CreateWebsocket(Log log, string url) { - return new BaseSocket(log, url); + return new CryptoExchangeWebSocketClient(log, url); } /// public IWebsocket CreateWebsocket(Log log, string url, IDictionary cookies, IDictionary headers) { - return new BaseSocket(log, url, cookies, headers); + return new CryptoExchangeWebSocketClient(log, url, cookies, headers); } } } diff --git a/README.md b/README.md index dad13a9..caad951 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,10 @@ -# CryptoExchange.Net +# CryptoExchange.Net +![Build status](https://travis-ci.org/JKorf/CryptoExchange.Net.svg?branch=master) ![Nuget version](https://img.shields.io/nuget/v/CryptoExchange.Net.svg) ![Nuget downloads](https://img.shields.io/nuget/dt/CryptoExchange.Net.svg) -![Build status](https://travis-ci.org/JKorf/CryptoExchange.Net.svg?branch=master) +CryptoExchange.Net is a base package which can be used to easily implement crypto currency exchange API's in C#. This library offers base classes for creating rest and websocket clients, and includes additional features like an automatically synchronizing order book implementation, error handling and automatic reconnects on websocket connections. -A base library for easy implementation of cryptocurrency API's. Include: -* REST API calls and error handling -* Websocket subscriptions, error handling and automatic reconnecting -* Order book implementations automatically synchronizing and updating -* Automatic rate limiting - -**If you think something is broken, something is missing or have any questions, please open an [Issue](https://github.com/JKorf/CryptoExchange.Net/issues)** - ---- ## Implementations +By me: -
@@ -22,10 +15,6 @@ A base library for easy implementation of cryptocurrency API's. Include:
Bitfinex
-
-Binance -

CoinEx @@ -44,8 +33,7 @@ A base library for easy implementation of cryptocurrency API's. Include:
- -Implementations from third parties +By third parties: - @@ -80,7 +68,7 @@ Implementations from third parties
BtcTurk - @@ -95,105 +83,80 @@ Implementations from third parties
@@ -56,7 +44,7 @@ Implementations from third parties
Liquid
+
Bitmex
+
Thodex
-Planned implementations (no timeline or specific order): -* Bitstamp -* CoinFalcon -* Binance DEX +## Discord +A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries. ## Donations -Donations are greatly appreciated and a motivation to keep improving. +I develop and maintain this package on my own for free in my spare time. Donations are greatly appreciated. If you prefer to donate any other currency please contact me. **Btc**: 12KwZk3r2Y3JZ2uMULcjqqBvXmpDwjhhQS **Eth**: 0x069176ca1a4b1d6e0b7901a6bc0dbf3bb0bf5cc2 **Nano**: xrb_1ocs3hbp561ef76eoctjwg85w5ugr8wgimkj8mfhoyqbx4s1pbc74zggw7gs -## Discord -A Discord server is available [here](https://discord.gg/MSpeEtSY8t). For discussion and/or questions around the CryptoExchange.Net and implementation libraries, feel free to join. +## Implementation usage +### Clients +The CryptoExchange.Net library offers 2 base clients which should be implemented in each implementation library. The `RestClient` and the `SocketClient`. -## Usage -Most API methods are available in two flavors, sync and async, see the example using the `BinanceClient`: +**RestClient** + +The `RestClient`, as the name suggests, handles requests to the exchange REST API. Typically the `RestClient` implementation name is [ExchangeName]Client. So for the Binance exchange this would be called the `BinanceClient`, and for Bittrex the client type name is `BittrexClient`. + +The `RestClient` implementations can be used in either a `using` or with a static instance: ````C# -public void NonAsyncMethod() +using (var binanceClient = new BinanceClient()) { - using(var client = new BinanceClient()) - { - var result = client.Spot.Market.Get24HPrices(); - } + var exchangeInfoResult = binanceClient.Spot.System.GetExchangeInfoAsync(); } - -public async Task AsyncMethod() -{ - using(var client = new BinanceClient()) - { - var result2 = await client.Spot.Market.Get24HPricesAsync(); - } -} -```` - -## Response handling -All API requests will respond with a (Web)CallResult object. This object contains whether the call was successful, the data returned from the call and an error if the call wasn't successful. As such, one should always check the Success flag when processing a response. -For example: -```C# -using(var client = new BinanceClient()) -{ - var result = client.Spot.System.GetServerTime(); - if (result.Success) - Console.WriteLine($"Server time: {result.Data}"); - else - Console.WriteLine($"Error: {result.Error}"); -} -``` - -## Options & Authentication -The default behavior of the clients can be changed by providing options to the constructor, or using the `SetDefaultOptions` before creating a new client. Api credentials can be provided in the options. -Credentials can be provided 2 ways: -* Providing key and secret: +```` +or ````C# - BinanceClient.SetDefaultOptions(new BinanceClientOptions - { - ApiCredentials = new ApiCredentials("apiKey", "apiSecret") - }); +var client = new BinanceClient(); +var exchangeInfoResult = client.Spot.System.GetExchangeInfoAsync(); ```` -* Providing a (file)stream containing the key/secret +If you're opting for the `using` syntax, a `HttpClient` should be provided in the client options to prevent each client creating it's own `HttpClient` instance. + +Calls made on the `RestClient` will return a `WebCallResult` object which will contain the following properties: +|Property|Description|Available when +|---|---|---| +|`Success`|Whether or not the call was successfully executed.|Always +|`Data`|The data the server sent us as response.| When `Success` is `true` +|`Error`|Details on the error that happened during the call.| When `Success` is `false` +|`OriginalData`|The originally received Json data which was received before being deserialized into an object.| When `OutputOriginalData` is enabled in the client options +|`ResponseStatusCode`|The Http status code received as answer on the request.|Always +|`ResponseHeaders`|The headers received in the response.|Always + +The `RestClient` implementation should implement the `IExchangeClient` interface, which offers some basic methods for interacting with an exchange without having to know the implementation. + +**SocketClient** + +The `SocketClient` can be used to connect to websocket streams offered by the API, and receive callbacks whenever new data is received. `SocketClient` implementations are typically named [ExchangeName]SocketClient, so in case of Binance this would be `BinanceSocketClient`. + +To use the `SocketClient` to connect to a stream simply call the `SubscribeXXX` method for the data you're interested in and pass in a delegate for handling updates. For example, to subscribe to the Binance ticker stream call `SubscribeToAllSymbolTickerUpdatesAsync` method: ````C# -using (var stream = File.OpenRead("/path/to/credential-file")) -{ - BinanceClient.SetDefaultOptions(new BinanceClientOptions - { - ApiCredentials = new ApiCredentials(stream) - }); -} +var socketClient = new BinanceSocketClient(); +var subscribeResult = socketClient.Spot.SubscribeToAllSymbolTickerUpdatesAsync(data => { + // Handle updates received here +}); ```` -Note that when using a file it can provide credentials for multiple exchanges by providing the identifierKey and identifierSecret parameters: -```` -// File content: -{ - "binanceKey": "actualBinanceApiKey", - "binanceSecret": "actualBinanceApiSecret", - "bittrexKey": "actualBittrexApiKey", - "bittrexSecret": "actualBittrexApiSecret", -} +or +````C# +var socketClient = new BinanceSocketClient(); +var subscribeResult = socketClient.Spot.SubscribeToAllSymbolTickerUpdatesAsync(HandleData); -// Loading: -using (var stream = File.OpenRead("/path/to/credential-file")) +private void HandleData(DataEvent> data) { - BinanceClient.SetDefaultOptions(new BinanceClientOptions - { - ApiCredentials = new ApiCredentials(stream, "binanceKey", "binanceSecret") - }); - BittrexClient.SetDefaultOptions(new BittrexClientOptions - { - ApiCredentials = new ApiCredentials(stream, "bittrexKey", "bittrexSecret") - }); + // Handle updates received here } ```` -## Websockets -Most implementations have a websocket client. The client will manage the websocket connection for you, all you have to do is call a subscribe method. The client will automatically handle reconnecting when losing a connection. +Make sure to check the result of the subscribe call to ensure it was successful. The Subscribe methods will return a `CallResult` object with the following properties: +|Property|Description|Available when +|---|---|---| +|`Success`|Whether or not the call was successfully executed.|Always +|`Data`|The `UpdateSubscription` object for this stream. This can be used to for listening to connection changed events and unsubscribing | When `Success` is `true` +|`Error`|Details on the error that happened during the subscription. | When `Success` is `false` -When using a subscribe method it will return an `UpdateSubscription` object. This object has 3 events: ConnectionLost/ConnectionRestored and Exception. Use the connection lost/restored to be notified when the socket has lost it's connection and when it was reconnected. The Exception event is thrown when there was an exception within the data handling callback. - -To unsubscribe use the client.Unsubscribe method and pass the UpdateSubscription received when subscribing: +To unsubscribe from a stream `Unsubscribe()` method can be used, with the `UpdateSubscription` received in the `SubscribeXXX` call as parameter, or use the `UnsubscribeAll()` method to close all subscriptions: ````C# // Subscribe var client = new BinanceSocketClient(); @@ -202,15 +165,59 @@ var subResult = client.Spot.SubscribeToOrderBookUpdates("BTCUSDT", data => {}); // Unsubscribe client.Unsubscribe(subResult.Data); ```` -To unsubscribe all subscriptions the `client.UnsubscribeAll()` method can be used. +The `SocketClient` handles connection management to the server internally and will close a connection if there are no more subscriptions on that connection. -## Order books -The library implementations provide a `SymbolOrderBook` implementation. This implementation can be used to keep an updated order book without having to think about synchronizing it. This example is from the Binance.Net library, -but the implementation is similar for each library: +*[WARNING] Do not use `using` statements in combination with constructing a `SocketClient`. Doing so will dispose the `SocketClient` instance when the subscription is done, which will result in the connection getting closed. Instead assign the socket client to a variable outside of the method scope.* + +### Client options +Options for a client can be provided in the constructor or using the static SetDefaultOptions method. +````C# +var client = new BinanceClient(new BinanceClientOptions() +{ +}); +```` +or +````C# +BinanceClient.SetDefaultOptions(new BinanceClientOptions() +{ +}); +```` +When providing options in the constructor the options will only apply for that specific client. When using the `SetDefaultOptions` method the options will be applied to any client created after that call which doesn't have any options provided to it via the constructor. Providing options in the constructor means any options set using the `SetDefaultOptions` method will be reset to default unless overwritten in the provided options. + +**Options for all clients** + +| Property | Description | Default | +| ----------- | ----------- | ---------| +| `LogWriters`| A list of `ILogger`s to handle log messages. | `new List { new DebugLogger() }` | +| `LogLevel`| The minimum log level before passing messages to the `LogWriters`. Messages with a more verbose level than the one specified here will be ignored. Setting this to `null` will pass all messages to the `LogWriters`.| `LogLevel.Information` +|`OutputOriginalData`|If set to `true` the originally received Json data will be output as well as the deserialized object. For `RestClient` calls the data will be in the `WebCallResult.OriginalData` property, for `SocketClient` subscriptions the data will be available in the `DataEvent.OriginalData` property when receiving an update. | `false` +|`BaseAddress`|The base address to the API. All calls to the API will use this base address as basis for the endpoints. This allows for swapping to test API's or swapping to a different cluster for example.| Depends on implementation +|`ApiCredentials`| The API credentials to use for accessing protected endpoints. Typically a key/secret combination.| `null` +|`Proxy`|The proxy to use for connecting to the API.| `null` + +**Options for RestClients** +| Property | Description | Default | +| ----------- | ----------- | ---------| +| `RateLimiters`| A list of `IRateLimiter`s to use.| `new List()` | +| `RateLimitingBehaviour`| What should happen when a rate limit is reached.| `RateLimitingBehaviour.Wait` | +| `RequestTimeout`| The time out to use for requests.| `TimeSpan.FromSeconds(30)` | +| `HttpClient`| The `HttpClient` instance to use for making requests. When creating multiple `RestClient` instances a single `HttpClient` should be provided to prevent each client instance from creating its own. *[WARNING] When providing the `HttpClient` instance in the options both the `RequestTimeout` and `Proxy` client options will be ignored and should be set on the provided `HttpClient` instance.*| `null` | + +**Options for SocketClients** +| Property | Description | Default | +| ----------- | ----------- | ---------| +|`AutoReconnect`|Whether or not the socket should automatically reconnect when disconnected.|`true` +|`ReconnectInterval`|The time to wait between connection tries when reconnecting.|`TimeSpan.FromSeconds(5)` +|`SocketResponseTimeout`|The time in which a response is expected on a request before giving a timeout.|`TimeSpan.FromSeconds(10)` +|`SocketNoDataTimeout`|If no data is received after this timespan then assume the connection is dropped. This is mainly used for API's which have some sort of ping/keepalive system. For example; the Bitfinex API will sent a heartbeat message every 15 seconds, so the `SocketNoDataTimeout` could be set to 20 seconds. On API's without such a mechanism this might not work because there just might not be any update while still being fully connected. | `default(TimeSpan)` (no timeout) +|`SocketSubscriptionsCombineTarget`|The amount of subscriptions that should be made on a single socket connection. Not all exchanges support multiple subscriptions on a single socket. Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a single connection will also increase the amount of traffic on that single connection, potentially leading to issues.| Depends on implementation + +### Order book +The library implementations provide a `SymbolOrderBook` implementation. This implementation can be used to keep an updated order book without having to think about synchronization. This example is from the Binance.Net library, but the implementation is similar for each library: ````C# var orderBook = new BinanceSymbolOrderBook("BTCUSDT", new BinanceOrderBookOptions(20)); orderBook.OnStatusChange += (oldStatus, newStatus) => Console.WriteLine($"Book state changed from {oldStatus} to {newStatus}"); -orderBook.OnOrderBookUpdate += (changedBids, changedAsks) => Console.WriteLine("Book updated"); +orderBook.OnOrderBookUpdate += ((changedBids, changedAsks)) => Console.WriteLine("Book updated"); var startResult = await orderBook.StartAsync(); if(!startResult.Success) { @@ -218,85 +225,171 @@ if(!startResult.Success) return; } -var status = orderBook.Status; // The current status. Note that the order book is only current when the status is Synced +var status = orderBook.Status; // The current status. Note that the order book is only up to date when the status is Synced var askCount = orderBook.AskCount; // The current number of asks in the book var bidCount = orderBook.BidCount; // The current number of bids in the book var asks = orderBook.Asks; // All asks var bids = orderBook.Bids; // All bids var bestBid = orderBook.BestBid; // The best bid available in the book var bestAsk = orderBook.BestAsk; // The best ask available in the book - ```` -The order book will automatically reconnect when the connection is lost and resync if it detects the sequence is off. Make sure to check the Status property to see it the book is currently in sync. +The order book will automatically reconnect when the connection is lost and resync if it detects that it is out of sync. Make sure to check the Status property to see it the book is currently in sync. To stop synchronizing an order book use the `Stop` method. -## Shared IExchangeClient interface -Most implementations have an implementation of the `IExchangeClient` interface. It is a shared interface, which makes it easier to re-use the same code for multiple exchanges. It offers basic functionality only, for exchange specific calls use the actual client interface, for example `IBinanceClient` or `IBitfinexClient`. +## Helper methods +The static `ExchangeHelpers` class offers some helper methods for adjusting value like rounding and adjusting values to fit a certain step size or precision. -The IExchangeClient interface supports the following methods: +## Creating an implementation +Implementations should implement at least the following: +**[Exchange]Client based on the RestClient base class** +Containing calls to the different endpoints, internally using the `SendRequest` method of the `RestClient`. -`string GetSymbolName(string baseAsset, string quoteAsset);` - Based on a base and quote asset return the symbol name for the exchange. +**[Exchange]SocketClient based on the SocketClient base class** +Containing methods to subscribe to different streams using the `Subscribe` method of the `SocketClient`. +Implement exchange specific handling of requests/messages by overriding these methods: +`HandleQueryResponse`: Check if the data received from the websocket matches the sent query. +`HandleSubscriptionResponse`: Check if the data received from the websocket matches the subscription request. +`MessageMatchesHandler`: Check if the data received from the websocket matches the handler/subscription. +`AuthenticateSocket`: Authenticate the connection to be able to subscribe to protected streams. +`Unsubscribe`: Unsubscribe from a stream, typically by sending an Unsubscribe message. -`GetSymbolsAsync();` - Get a list of supported symbols. +**[Exchange]SymbolOrderBook based on the SymbolOrderBook base class** +An implementation of an automatically synchronized order book. Implement exchange specific behavior by implementing these methods: +`DoStart`: Start the order book and sync process +`DoResync`: Resync the order book after a reconnection +`DoReset`: Reset the state of the orderbook, called when the connection is lost. +`DoChecksum`: [Optional] Validate the order book with a checksum. -`GetTickersAsync();` - Get a list of tickers (High/Low/volume data for the last 24 hours) for all symbols. +**[Exchange]AuthenticationProvider** +An implementation of the AuthenticationProvider base class. Should contain the logic for authenticating requests from the RestClient on protected endpoints. Override these methods as needed: +`AddAuthenticationToParameters`: Will be called before `AddAuthenticationToHeaders`, allows the implementation to add specific parameters to the request which are needed for protected endpoints. +`AddAuthenticationToHeaders`: Will be called after `AddAuthenticationToParameters`, allows the implementation to add specific headers to the request message. +If you have any issues or questions regarding implementing an exchange using CryptoExchange.Net hop into the Discord or open an issue. -`GetTickerAsync(string symbol);` - Get a specific ticker. - -`GetKlinesAsync(string symbol, TimeSpan timespan, DateTime? startTime = null, DateTime? endTime = null, int? limit = null);` - Get candlestick data for a symbol. - -`GetOrderBookAsync(string symbol);` - Get the order book for a symbol. - -`GetRecentTradesAsync(string symbol);` - Get a list of the most recent trades for a symbol. - -`PlaceOrderAsync(string symbol, OrderSide side, OrderType type, decimal quantity, decimal? price = null, string? accountId = null);` - \[Authenticated\] Place an order. - -`GetOrderAsync(string orderId, string? symbol = null);` - \[Authenticated\] Get details on an order. - -`GetTradesAsync(string orderId, string? symbol = null);` - \[Authenticated\] Get executed trades for an order. - -`GetOpenOrdersAsync(string? symbol = null);` - \[Authenticated\] Get a list of open orders. - -`GetClosedOrdersAsync(string? symbol = null);` - \[Authenticated\] Get a list of closed orders. - -`CancelOrderAsync(string orderId, string? symbol = null);` - \[Authenticated\] Cancel an order. - -`GetBalancesAsync(string? accountId = null);` - \[Authenticated\] Get a list of balances. - - -Example usage: +## FAQ +**I sometimes get NullReferenceException, what's wrong?** +You probably don't check the result status of a call and just assume the data is always there. `NullReferenceExecption`s will happen when you have code like this `var symbol = client.GetTickersAync().Result.Data.Symbol` because the `Data` property is null when the call fails. Instead check if the call is successful like this: ````C# -static async Task Main(string[] args) +var tickerResult = await client.GetTickersAync(); +if(!tickerResult.Success) { - var clients = new List() - { - { new BinanceClient(new BinanceClientOptions(){ LogVerbosity = LogVerbosity.Debug, ApiCredentials = new ApiCredentials("BinanceKey", "BinanceSecret") }) }, - { new BitfinexClient(new BitfinexClientOptions(){ LogVerbosity = LogVerbosity.Debug, ApiCredentials = new ApiCredentials("BitfinexKey", "BitfinexSecret") }) }, - { new BittrexClientV3(new BittrexClientOptions(){ LogVerbosity = LogVerbosity.Debug, ApiCredentials = new ApiCredentials("BittrexKey", "BittrexSecret") }) }, - }; + // Handle error +} +else +{ + // Handle data, it is now safe to access the data + var symbol = tickerResult.Data.Symbol; +} +```` +**The socket client stops sending updates after a little while** +You probably didn't keep a reference to the socket client and it got disposed. +Instead of subscribing like this: +````C# +private void SomeMethod() +{ + var socketClient = new BinanceSocketClient(); + socketClient.Spot.SubscribeToOrderBookUpdates("BTCUSDT", data => { + // Handle data + }); +} +```` +Subscribe like this: +````C# +private BinanceSocketClient _socketClient; - await Task.WhenAll(clients.Select(GetExchangeData)); - Console.WriteLine("Done"); - Console.ReadLine(); +// .. rest of the class + +private void SomeMethod() +{ + if(_socketClient == null) + _socketClient = new BinanceSocketClient(); + + _socketClient.Spot.SubscribeToOrderBookUpdates("BTCUSDT", data => { + // Handle data + }); } -static async Task GetExchangeData(IExchangeClient client) -{ - var symbols = await client.GetSymbolsAsync(); - Console.WriteLine($"{((RestClient)client).ClientName}: {symbols.Data?.Count()} symbols returned, first = {symbols.Data?.First().CommonName}"); - - var balances = await client.GetBalancesAsync(); - var btcBalance = balances.Data?.Where(b => b.CommonAsset.ToUpperInvariant() == "BTC").FirstOrDefault(); - Console.WriteLine($"{((RestClient)client).ClientName}: {balances.Data?.Count()} balance returned, BTC balance = {btcBalance?.CommonTotal}"); - - var symbolName = client.GetSymbolName("BTC", "USDT"); - var klines = await client.GetKlinesAsync(symbolName, TimeSpan.FromHours(1)); - Console.WriteLine($"{((RestClient)client).ClientName}: {klines.Data?.Count()} klines returned, first klines @ {klines.Data?.First().CommonClose}"); -} ```` ## Release notes +* Version 4.0.0 - 12 Aug 2020 + * Release version, summed up changes from previous beta releases: + * Removed `Websocket4Net` dependency in favor of a `ClientWebSocket` native implementation for websocket connections + * Socket events now always come wrapped in a `DataEvent<>` object which contain the timestamp of the data, and optionally the originally received json string + * Implemented usage of the `Microsoft.Extensions.Logging.Abstractions` `ILogger` interface instead of a custom implementation + * Added some properties to the `IExchangeClient` interface + * `ICommonOrder.CommonOrderTime` + * `ICommonOrder.CommonOrderStatus` enum + * `ICommonTrade.CommonTradeTime` + * Added `OnOrderPlaced` and `OnOrderCanceled` events on the `IExchangeClient` interface + * Added `ExchangeHelpers` static class for various helper methods + * Removed non-async methods due to too much overhead in development/maintainance + * If you were previously using non-async methods you can add `.Result` to the end of the async call to get the same result + * Added `Book` property to `SymbolOrderBook` for a book snapshot + * Added `CalculateAverageFillPrice` to `SymbolOrderBook` to calculate the average fill price for an order with the current order book state + * Various fixes + +* Version 4.0.0-beta15 - 12 Aug 2021 + * Conditional version Logging.Abstractions + +* Version 4.0.0-beta14 - 09 Aug 2021 + * Fix for bug in processing order in SymbolOrderBook + +* Version 4.0.0-beta13 - 31 Jul 2021 + * Fix for socket connection + +* Version 4.0.0-beta12 - 26 Jul 2021 + * Fix for socket connection + +* Version 4.0.0-beta11 - 09 Jul 2021 + * Added CalculateAverageFillPrice to SymbolOrderBook + * Added Book property to SymbolOrderBook + * Added Async postfix to async methods + +* Version 4.0.0-beta10 - 07 Jul 2021 + * Updated BaseConverter to be case sensitive + * Added ExchangeHelpers class containing some helper methods + * Fixed responses not being logged on Trace log level + * Added some code docs + +* Version 4.0.0-beta9 - 17 Jun 2021 + * Small fixes + +* Version 4.0.0-beta8 - 08 Jun 2021 + * Fixed exception socket buffer size in .net framework + +* Version 4.0.0-beta7 - 07 Jun 2021 + * Added CommonOrderTime to IOrder + * Added OrderStatus enum for IOrder + * Added OnOrderPlaced and OnOrderCanceled events on IExchangeClient + * Added CommonTradeTime to ICommonTrade + +* Version 4.0.0-beta6 - 01 jun 2021 + * Some logging adjustments + * Fixed some async issues + +* Version 4.0.0-beta5 - 26 May 2021 + * Added DataEvent wrapper for socket updates + * Added optional original json output + * Changed logging implementation to use ILogger + +* Version 4.0.0-beta4 - 06 mei 2021 + * Added analyzers + * Fixed some warnings + +* Version 4.0.0-beta3 - 30 Apr 2021 + * Updated socket closing + +* Version 4.0.0-beta2 - 30 apr 2021 + * Fix for closing socket without timeout task + +* Version 4.0.0-beta1 - 30 apr 2021 + * Removed Websocket4Net dependency + * Added custom ClientWebSocket implementation + * Renamed handler -> subscription internally + * Renamed socket -> socketConenction when type is socketConnection + * Version 3.9.0 - 28 apr 2021 * Added optional JsonSerializer parameter to SendRequest to use during deserialization * Fix for unhandled message warning when unsubscribing a socket subscription @@ -432,4 +525,4 @@ static async Task GetExchangeData(IExchangeClient client) * Version 2.1.2 - 14 may 2019 * Added order book base class for easy implementation - * Added additional constructor to ApiCredentials to be able to read from file + * Added additional constructor to ApiCredentials to be able to read from file \ No newline at end of file