mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 08:53:01 +00:00
Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ec5984fad | |||
| 8260c2661d | |||
| 591c1dd405 | |||
| 0164cdfcc4 | |||
| 23a6cfff87 | |||
| fdcdb90a5f | |||
| 0b7107401f | |||
| 06add65354 | |||
| 773d288497 | |||
| fd4e8da938 | |||
| 271743b669 | |||
| f4797caf37 | |||
| 62c9769c72 | |||
| 92d7bc1e2e | |||
| 99e4f96f63 | |||
| 94d8afe149 | |||
| 90ad59c63a | |||
| c2273edfaa | |||
| 236283f4dd | |||
| b66f12ff75 | |||
| 0403384beb | |||
| 7d7bc35869 | |||
| 48797038be | |||
| d21792d04c | |||
| 8414e9d94f | |||
| ab0243445d | |||
| f2cf70b02f | |||
| 9ff417bba8 | |||
| 6b43d08a4d | |||
| 39bf7fe9b9 | |||
| b5893c3b60 | |||
| 15657ba683 | |||
| 1aed9f0c67 | |||
| 17f1560310 | |||
| 41de0a3150 | |||
| 3e410be611 | |||
| be75449e4a | |||
| b1b05c8f6b | |||
| a0e588c3de | |||
| 9e86a08327 | |||
| ed007b5272 | |||
| bdd5526244 | |||
| ce35e30688 | |||
| b40f72b1b0 | |||
| 31a6cf285b | |||
| 1842f4fda0 | |||
| 7a58902ab6 | |||
| 3cb91296ca | |||
| 130ed40580 | |||
| 94cb2caf0b | |||
| 917d060827 | |||
| c58bc2be07 | |||
| ff3356e2b4 | |||
| 79434c7be5 | |||
| 168dabc11f | |||
| 71ee263683 | |||
| 7239b9c289 | |||
| 84d36544e4 | |||
| a71f57ae7f | |||
| 6e5bcd5e9a | |||
| 4131e563c3 | |||
| 613766dbca | |||
| 23b07d709e | |||
| bbbdac2fd3 | |||
| c614b7869c | |||
| 1f31e4a9d7 | |||
| 6cb6cd6b11 | |||
| 17ffec329f | |||
| 7a3927ef49 | |||
| c1b0437c93 |
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
@@ -6,10 +6,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0"></PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="NUnit" Version="4.1.0"></PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"></PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"></PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="NUnit" Version="4.2.2"></PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0"></PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
@@ -70,5 +71,20 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result = ExchangeHelpers.Normalize(input);
|
||||
Assert.That(expected == result.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("123", "BKR", 32, true, "BKRJK123")]
|
||||
[TestCase("123", "BKR", 32, false, "123")]
|
||||
[TestCase("123123123123123123123123123123", "BKR", 32, true, "123123123123123123123123123123")] // 30
|
||||
[TestCase("12312312312312312312312312312", "BKR", 32, true, "12312312312312312312312312312")] // 27
|
||||
[TestCase("123123123123123123123123123", "BKR", 32, true, "BKRJK123123123123123123123123123")] // 25
|
||||
[TestCase(null, "BKR", 32, true, null)]
|
||||
public void ApplyBrokerIdTests(string clientOrderId, string brokerId, int maxLength, bool allowValueAdjustement, string expected)
|
||||
{
|
||||
var result = LibraryHelpers.ApplyBrokerId(clientOrderId, brokerId, maxLength, allowValueAdjustement);
|
||||
|
||||
if (expected != null)
|
||||
Assert.That(result, Is.EqualTo(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
Assert.That(authProvider1.GetSecret() == "222");
|
||||
Assert.That(authProvider2.GetKey() == "123");
|
||||
Assert.That(authProvider2.GetSecret() == "456");
|
||||
|
||||
// Cleanup static values
|
||||
TestClientOptions.Default.ApiCredentials = null;
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = null;
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -121,6 +125,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
Assert.That(authProvider2.GetKey() == "123");
|
||||
Assert.That(authProvider2.GetSecret() == "456");
|
||||
Assert.That(client.Api2.BaseAddress == "https://localhost:123");
|
||||
|
||||
// Cleanup static values
|
||||
TestClientOptions.Default.ApiCredentials = null;
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +142,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
Environment = new TestEnvironment("test", "https://test.com")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public TestClientOptions()
|
||||
{
|
||||
Default?.Set(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The default receive window for requests
|
||||
/// </summary>
|
||||
@@ -143,12 +159,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
|
||||
|
||||
internal TestClientOptions Copy()
|
||||
internal TestClientOptions Set(TestClientOptions targetOptions)
|
||||
{
|
||||
var options = Copy<TestClientOptions>();
|
||||
options.Api1Options = Api1Options.Copy<RestApiOptions>();
|
||||
options.Api2Options = Api2Options.Copy<RestApiOptions>();
|
||||
return options;
|
||||
targetOptions = base.Set<TestClientOptions>(targetOptions);
|
||||
targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options);
|
||||
targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options);
|
||||
return targetOptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool expectLimited)
|
||||
{
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Sliding));
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ using NUnit.Framework;
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using NUnit.Framework.Legacy;
|
||||
using CryptoExchange.Net.Converters;
|
||||
using CryptoExchange.Net.Testing.Comparers;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
@@ -242,6 +244,44 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": " + value + "}");
|
||||
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected));
|
||||
}
|
||||
|
||||
[Test()]
|
||||
public void TestArrayConverter()
|
||||
{
|
||||
var data = new Test()
|
||||
{
|
||||
Prop1 = 2,
|
||||
Prop2 = null,
|
||||
Prop3 = "123",
|
||||
Prop3Again = "123",
|
||||
Prop4 = null,
|
||||
Prop5 = new Test2
|
||||
{
|
||||
Prop21 = 3,
|
||||
Prop22 = "456"
|
||||
},
|
||||
Prop6 = new Test3
|
||||
{
|
||||
Prop31 = 4,
|
||||
Prop32 = "789"
|
||||
},
|
||||
Prop7 = TestEnum.Two
|
||||
};
|
||||
|
||||
var serialized = JsonSerializer.Serialize(data);
|
||||
var deserialized = JsonSerializer.Deserialize<Test>(serialized);
|
||||
|
||||
Assert.That(deserialized.Prop1, Is.EqualTo(2));
|
||||
Assert.That(deserialized.Prop2, Is.Null);
|
||||
Assert.That(deserialized.Prop3, Is.EqualTo("123"));
|
||||
Assert.That(deserialized.Prop3Again, Is.EqualTo("123"));
|
||||
Assert.That(deserialized.Prop4, Is.Null);
|
||||
Assert.That(deserialized.Prop5.Prop21, Is.EqualTo(3));
|
||||
Assert.That(deserialized.Prop5.Prop22, Is.EqualTo("456"));
|
||||
Assert.That(deserialized.Prop6.Prop31, Is.EqualTo(4));
|
||||
Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789"));
|
||||
Assert.That(deserialized.Prop7, Is.EqualTo(TestEnum.Two));
|
||||
}
|
||||
}
|
||||
|
||||
public class STJDecimalObject
|
||||
@@ -281,4 +321,42 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[JsonConverter(typeof(BoolConverter))]
|
||||
public bool Value { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ArrayConverter))]
|
||||
record Test
|
||||
{
|
||||
[ArrayProperty(0)]
|
||||
public int Prop1 { get; set; }
|
||||
[ArrayProperty(1)]
|
||||
public int? Prop2 { get; set; }
|
||||
[ArrayProperty(2)]
|
||||
public string Prop3 { get; set; }
|
||||
[ArrayProperty(2)]
|
||||
public string Prop3Again { get; set; }
|
||||
[ArrayProperty(3)]
|
||||
public string Prop4 { get; set; }
|
||||
[ArrayProperty(4)]
|
||||
public Test2 Prop5 { get; set; }
|
||||
[ArrayProperty(5)]
|
||||
public Test3 Prop6 { get; set; }
|
||||
[ArrayProperty(6), JsonConverter(typeof(EnumConverter))]
|
||||
public TestEnum? Prop7 { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ArrayConverter))]
|
||||
record Test2
|
||||
{
|
||||
[ArrayProperty(0)]
|
||||
public int Prop21 { get; set; }
|
||||
[ArrayProperty(1)]
|
||||
public string Prop22 { get; set; }
|
||||
}
|
||||
|
||||
record Test3
|
||||
{
|
||||
[JsonPropertyName("prop31")]
|
||||
public int Prop31 { get; set; }
|
||||
[JsonPropertyName("prop32")]
|
||||
public string Prop32 { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
public TestBaseClient(): base(null, "Test")
|
||||
{
|
||||
var options = TestClientOptions.Default.Copy();
|
||||
var options = new TestClientOptions();
|
||||
Initialize(options);
|
||||
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
@@ -24,22 +25,17 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
public TestRestApi1Client Api1 { get; }
|
||||
public TestRestApi2Client Api2 { get; }
|
||||
|
||||
public TestRestClient(Action<TestClientOptions> optionsFunc) : this(optionsFunc, null)
|
||||
public TestRestClient(Action<TestClientOptions> optionsDelegate = null)
|
||||
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
|
||||
{
|
||||
}
|
||||
|
||||
public TestRestClient(ILoggerFactory loggerFactory = null, HttpClient httpClient = null) : this((x) => { }, httpClient, loggerFactory)
|
||||
public TestRestClient(HttpClient httpClient, ILoggerFactory loggerFactory, IOptions<TestClientOptions> options) : base(loggerFactory, "Test")
|
||||
{
|
||||
}
|
||||
Initialize(options.Value);
|
||||
|
||||
public TestRestClient(Action<TestClientOptions> optionsFunc, HttpClient httpClient = null, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
||||
{
|
||||
var options = TestClientOptions.Default.Copy();
|
||||
optionsFunc(options);
|
||||
Initialize(options);
|
||||
|
||||
Api1 = new TestRestApi1Client(options);
|
||||
Api2 = new TestRestApi2Client(options);
|
||||
Api1 = new TestRestApi1Client(options.Value);
|
||||
Api2 = new TestRestApi2Client(options.Value);
|
||||
}
|
||||
|
||||
public void SetResponse(string responseData, out IRequest requestObj)
|
||||
|
||||
@@ -15,6 +15,7 @@ using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using CryptoExchange.Net.Testing.Implementations;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
@@ -22,25 +23,20 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public TestSubSocketClient SubClient { get; }
|
||||
|
||||
public TestSocketClient(ILoggerFactory loggerFactory = null) : this((x) => { }, loggerFactory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of KucoinSocketClient
|
||||
/// </summary>
|
||||
/// <param name="optionsFunc">Configure the options to use for this client</param>
|
||||
public TestSocketClient(Action<TestSocketOptions> optionsFunc) : this(optionsFunc, null)
|
||||
public TestSocketClient(Action<TestSocketOptions> optionsDelegate = null)
|
||||
: this(Options.Create(ApplyOptionsDelegate(optionsDelegate)), null)
|
||||
{
|
||||
}
|
||||
|
||||
public TestSocketClient(Action<TestSocketOptions> optionsFunc, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
||||
public TestSocketClient(IOptions<TestSocketOptions> options, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
||||
{
|
||||
var options = TestSocketOptions.Default.Copy<TestSocketOptions>();
|
||||
optionsFunc(options);
|
||||
Initialize(options);
|
||||
Initialize(options.Value);
|
||||
|
||||
SubClient = AddApiClient(new TestSubSocketClient(options, options.SubOptions));
|
||||
SubClient = AddApiClient(new TestSubSocketClient(options.Value, options.Value.SubOptions));
|
||||
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
|
||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
|
||||
}
|
||||
@@ -70,7 +66,22 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
Environment = new TestEnvironment("Live", "https://test.test")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public TestSocketOptions()
|
||||
{
|
||||
Default?.Set(this);
|
||||
}
|
||||
|
||||
public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions();
|
||||
|
||||
internal TestSocketOptions Set(TestSocketOptions targetOptions)
|
||||
{
|
||||
targetOptions = base.Set<TestSocketOptions>(targetOptions);
|
||||
targetOptions.SubOptions = SubOptions.Set(targetOptions.SubOptions);
|
||||
return targetOptions;
|
||||
}
|
||||
}
|
||||
|
||||
public class TestSubSocketClient : SocketApiClient
|
||||
|
||||
@@ -11,7 +11,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorClient", "Examples\Bl
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{5734C2A9-F12C-4754-A8B9-640C24DC4E02}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleClient", "Examples\ConsoleClient\ConsoleClient.csproj", "{23480C58-23BF-4EBF-A173-B7F51A043A99}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleClient", "Examples\ConsoleClient\ConsoleClient.csproj", "{23480C58-23BF-4EBF-A173-B7F51A043A99}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -35,6 +37,10 @@ Global
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -42,6 +48,7 @@ Global
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
|
||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {0D1B9CE9-E0B7-4B8B-88BF-6EA2CC8CA3D7}
|
||||
|
||||
@@ -11,41 +11,32 @@ namespace CryptoExchange.Net.Authentication
|
||||
public class ApiCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// The api key to authenticate requests
|
||||
/// The api key / label to authenticate requests
|
||||
/// </summary>
|
||||
public string Key { get; }
|
||||
public string Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The api secret to authenticate requests
|
||||
/// The api secret or private key to authenticate requests
|
||||
/// </summary>
|
||||
public string Secret { get; }
|
||||
public string Secret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of the credentials
|
||||
/// </summary>
|
||||
public ApiCredentialsType CredentialType { get; }
|
||||
public ApiCredentialsType CredentialType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing an api key and secret for authentication
|
||||
/// </summary>
|
||||
/// <param name="key">The api key used for identification</param>
|
||||
/// <param name="secret">The api secret used for signing</param>
|
||||
public ApiCredentials(string key, string secret) : this(key, secret, ApiCredentialsType.Hmac)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing an api key and secret for authentication
|
||||
/// </summary>
|
||||
/// <param name="key">The api key used for identification</param>
|
||||
/// <param name="secret">The api secret used for signing</param>
|
||||
/// <param name="credentialsType">The type of credentials</param>
|
||||
public ApiCredentials(string key, string secret, ApiCredentialsType credentialsType)
|
||||
/// <param name="key">The api key / label used for identification</param>
|
||||
/// <param name="secret">The api secret or private key used for signing</param>
|
||||
/// <param name="credentialType">The type of credentials</param>
|
||||
public ApiCredentials(string key, string secret, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
|
||||
throw new ArgumentException("Key and secret can't be null/empty");
|
||||
|
||||
CredentialType = credentialsType;
|
||||
CredentialType = credentialType;
|
||||
Key = key;
|
||||
Secret = secret;
|
||||
}
|
||||
@@ -65,7 +56,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <param name="inputStream">The stream containing the json data</param>
|
||||
/// <param name="identifierKey">A key to identify the credentials for the API. For example, when set to `binanceKey` the json data should contain a value for the property `binanceKey`. Defaults to 'apiKey'.</param>
|
||||
/// <param name="identifierSecret">A key to identify the credentials for the API. For example, when set to `binanceSecret` the json data should contain a value for the property `binanceSecret`. Defaults to 'apiSecret'.</param>
|
||||
public ApiCredentials(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
|
||||
public static ApiCredentials FromStream(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
|
||||
{
|
||||
var accessor = new SystemTextJsonStreamMessageAccessor();
|
||||
if (!accessor.Read(inputStream, false).Result)
|
||||
@@ -75,11 +66,9 @@ namespace CryptoExchange.Net.Authentication
|
||||
var secret = accessor.GetValue<string>(MessagePath.Get().Property(identifierSecret ?? "apiSecret"));
|
||||
if (key == null || secret == null)
|
||||
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
|
||||
|
||||
Key = key;
|
||||
Secret = secret;
|
||||
|
||||
|
||||
inputStream.Seek(0, SeekOrigin.Begin);
|
||||
return new ApiCredentials(key, secret);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <summary>
|
||||
/// Get the API key of the current credentials
|
||||
/// </summary>
|
||||
public string ApiKey => _credentials.Key;
|
||||
public string ApiKey => _credentials.Key!;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@@ -39,7 +39,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <param name="credentials"></param>
|
||||
protected AuthenticationProvider(ApiCredentials credentials)
|
||||
{
|
||||
if (credentials.Secret == null)
|
||||
if (credentials.Key == null || credentials.Secret == null)
|
||||
throw new ArgumentException("ApiKey/Secret needed");
|
||||
|
||||
_credentials = credentials;
|
||||
|
||||
@@ -38,6 +38,11 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
public bool OutputOriginalData { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
|
||||
/// </summary>
|
||||
public bool Authenticated => ApiOptions.ApiCredentials != null || ClientOptions.ApiCredentials != null;
|
||||
|
||||
/// <summary>
|
||||
/// Api options
|
||||
/// </summary>
|
||||
@@ -83,6 +88,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <inheritdoc />
|
||||
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||
{
|
||||
ApiOptions.ApiCredentials = credentials;
|
||||
if (credentials != null)
|
||||
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
|
||||
}
|
||||
|
||||
@@ -12,6 +12,28 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
public abstract class BaseClient : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Version of the CryptoExchange.Net base library
|
||||
/// </summary>
|
||||
public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version;
|
||||
|
||||
/// <summary>
|
||||
/// Version of the client implementation
|
||||
/// </summary>
|
||||
public Version ExchangeLibVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
lock(_versionLock)
|
||||
{
|
||||
if (_exchangeVersion == null)
|
||||
_exchangeVersion = GetType().Assembly.GetName().Version;
|
||||
|
||||
return _exchangeVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The name of the API the client is for
|
||||
/// </summary>
|
||||
@@ -27,6 +49,9 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
protected internal ILogger _logger;
|
||||
|
||||
private object _versionLock = new object();
|
||||
private Version _exchangeVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Provided client options
|
||||
/// </summary>
|
||||
@@ -57,7 +82,7 @@ namespace CryptoExchange.Net.Clients
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
|
||||
ClientOptions = options;
|
||||
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {Exchange}.Net: v{GetType().Assembly.GetName().Version}");
|
||||
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -84,6 +109,16 @@ namespace CryptoExchange.Net.Clients
|
||||
return apiClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply the options delegate to a new options instance
|
||||
/// </summary>
|
||||
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T: new()
|
||||
{
|
||||
var opts = new T();
|
||||
del?.Invoke(opts);
|
||||
return opts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
|
||||
@@ -72,6 +72,11 @@ namespace CryptoExchange.Net.Clients
|
||||
/// </summary>
|
||||
protected List<DedicatedConnectionConfig> DedicatedConnectionConfigs { get; set; } = new List<DedicatedConnectionConfig>();
|
||||
|
||||
/// <summary>
|
||||
/// Whether to allow multiple subscriptions with the same topic on the same connection
|
||||
/// </summary>
|
||||
protected bool AllowTopicsOnTheSameConnection { get; set; } = true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public double IncomingKbps
|
||||
{
|
||||
@@ -211,7 +216,7 @@ namespace CryptoExchange.Net.Clients
|
||||
while (true)
|
||||
{
|
||||
// Get a new or existing socket connection
|
||||
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false).ConfigureAwait(false);
|
||||
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, subscription.Topic).ConfigureAwait(false);
|
||||
if (!socketResult)
|
||||
return socketResult.As<UpdateSubscription>(null);
|
||||
|
||||
@@ -403,7 +408,7 @@ namespace CryptoExchange.Net.Clients
|
||||
return new CallResult(new NoApiCredentialsError());
|
||||
|
||||
_logger.AttemptingToAuthenticate(socket.SocketId);
|
||||
var authRequest = GetAuthenticationRequest(socket);
|
||||
var authRequest = await GetAuthenticationRequestAsync(socket).ConfigureAwait(false);
|
||||
if (authRequest != null)
|
||||
{
|
||||
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
|
||||
@@ -428,7 +433,7 @@ namespace CryptoExchange.Net.Clients
|
||||
/// Should return the request which can be used to authenticate a socket connection
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected internal virtual Query? GetAuthenticationRequest(SocketConnection connection) => throw new NotImplementedException();
|
||||
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a system subscription. Used for example to reply to ping requests
|
||||
@@ -478,23 +483,28 @@ namespace CryptoExchange.Net.Clients
|
||||
/// <param name="address">The address the socket is for</param>
|
||||
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
|
||||
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection)
|
||||
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, string? topic = null)
|
||||
{
|
||||
var socketQuery = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
|
||||
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
||||
&& s.Value.ApiClient.GetType() == GetType()
|
||||
&& (s.Value.Authenticated == authenticated || !authenticated)
|
||||
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic))
|
||||
&& s.Value.Connected);
|
||||
|
||||
SocketConnection connection;
|
||||
if (!dedicatedRequestConnection)
|
||||
{
|
||||
connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
|
||||
connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection).FirstOrDefault().Value;
|
||||
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault().Value;
|
||||
if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
|
||||
// Mark dedicated request connection as authenticated if the request is authenticated
|
||||
connection.DedicatedRequestConnection.Authenticated = authenticated;
|
||||
}
|
||||
|
||||
if (connection != null)
|
||||
@@ -519,7 +529,14 @@ namespace CryptoExchange.Net.Clients
|
||||
var socketConnection = new SocketConnection(_logger, this, socket, address);
|
||||
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
||||
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
|
||||
socketConnection.DedicatedRequestConnection = dedicatedRequestConnection;
|
||||
if (dedicatedRequestConnection)
|
||||
{
|
||||
socketConnection.DedicatedRequestConnection = new DedicatedConnectionState
|
||||
{
|
||||
IsDedicatedRequestConnection = dedicatedRequestConnection,
|
||||
Authenticated = authenticated
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var ptg in PeriodicTaskRegistrations)
|
||||
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback);
|
||||
@@ -652,7 +669,7 @@ namespace CryptoExchange.Net.Clients
|
||||
var tasks = new List<Task>();
|
||||
{
|
||||
var socketList = socketConnections.Values;
|
||||
foreach (var connection in socketList.Where(s => !s.DedicatedRequestConnection))
|
||||
foreach (var connection in socketList.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection))
|
||||
tasks.Add(connection.CloseAsync());
|
||||
}
|
||||
|
||||
|
||||
@@ -38,12 +38,67 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
private class ArrayConverterInner<T> : JsonConverter<T>
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, List<ArrayPropertyInfo>> _typeAttributesCache = new ConcurrentDictionary<Type, List<ArrayPropertyInfo>>();
|
||||
|
||||
private static readonly ConcurrentDictionary<Type, JsonSerializerOptions> _converterOptionsCache = new ConcurrentDictionary<Type, JsonSerializerOptions>();
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||
{
|
||||
// TODO
|
||||
throw new NotImplementedException();
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteStartArray();
|
||||
|
||||
var valueType = value.GetType();
|
||||
if (!_typeAttributesCache.TryGetValue(valueType, out var typeAttributes))
|
||||
typeAttributes = CacheTypeAttributes(valueType);
|
||||
|
||||
var ordered = typeAttributes.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index);
|
||||
var last = -1;
|
||||
foreach (var prop in ordered)
|
||||
{
|
||||
if (prop.ArrayProperty.Index == last)
|
||||
continue;
|
||||
|
||||
while (prop.ArrayProperty.Index != last + 1)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
last += 1;
|
||||
}
|
||||
|
||||
last = prop.ArrayProperty.Index;
|
||||
|
||||
var objValue = prop.PropertyInfo.GetValue(value);
|
||||
if (objValue == null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
continue;
|
||||
}
|
||||
|
||||
JsonSerializerOptions? typeOptions = null;
|
||||
if (prop.JsonConverterType != null)
|
||||
{
|
||||
var converter = (JsonConverter)Activator.CreateInstance(prop.JsonConverterType);
|
||||
typeOptions = new JsonSerializerOptions();
|
||||
typeOptions.Converters.Clear();
|
||||
typeOptions.Converters.Add(converter);
|
||||
}
|
||||
|
||||
if (prop.JsonConverterType == null && IsSimple(prop.PropertyInfo.PropertyType))
|
||||
{
|
||||
if (prop.PropertyInfo.PropertyType == typeof(string))
|
||||
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
|
||||
else
|
||||
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
|
||||
}
|
||||
else
|
||||
{
|
||||
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
|
||||
}
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -53,7 +108,20 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
return default;
|
||||
|
||||
var result = Activator.CreateInstance(typeToConvert);
|
||||
return (T)ParseObject(ref reader, result, typeToConvert);
|
||||
return (T)ParseObject(ref reader, result, typeToConvert, options);
|
||||
}
|
||||
|
||||
private static bool IsSimple(Type type)
|
||||
{
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
{
|
||||
// nullable type, check if the nested type is simple.
|
||||
return IsSimple(type.GetGenericArguments()[0]);
|
||||
}
|
||||
return type.IsPrimitive
|
||||
|| type.IsEnum
|
||||
|| type == typeof(string)
|
||||
|| type == typeof(decimal);
|
||||
}
|
||||
|
||||
private static List<ArrayPropertyInfo> CacheTypeAttributes(Type type)
|
||||
@@ -71,7 +139,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
ArrayProperty = att,
|
||||
PropertyInfo = property,
|
||||
DefaultDeserialization = property.GetCustomAttribute<JsonConversionAttribute>() != null,
|
||||
JsonConverterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType,
|
||||
JsonConverterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? property.PropertyType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType,
|
||||
TargetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType
|
||||
});
|
||||
}
|
||||
@@ -80,7 +148,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType)
|
||||
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartArray)
|
||||
throw new Exception("Not an array");
|
||||
@@ -94,42 +162,58 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (reader.TokenType == JsonTokenType.EndArray)
|
||||
break;
|
||||
|
||||
var attribute = attributes.SingleOrDefault(a => a.ArrayProperty.Index == index);
|
||||
if (attribute == null)
|
||||
var indexAttributes = attributes.Where(a => a.ArrayProperty.Index == index);
|
||||
if (!indexAttributes.Any())
|
||||
{
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var targetType = attribute.TargetType;
|
||||
object? value = null;
|
||||
if (attribute.JsonConverterType != null)
|
||||
foreach (var attribute in indexAttributes)
|
||||
{
|
||||
// Has JsonConverter attribute
|
||||
var options = new JsonSerializerOptions();
|
||||
options.Converters.Add((JsonConverter)Activator.CreateInstance(attribute.JsonConverterType));
|
||||
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType, options);
|
||||
}
|
||||
else if (attribute.DefaultDeserialization)
|
||||
{
|
||||
// Use default deserialization
|
||||
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType);
|
||||
}
|
||||
else
|
||||
{
|
||||
value = reader.TokenType switch
|
||||
var targetType = attribute.TargetType;
|
||||
object? value = null;
|
||||
if (attribute.JsonConverterType != null)
|
||||
{
|
||||
JsonTokenType.Null => null,
|
||||
JsonTokenType.False => false,
|
||||
JsonTokenType.True => true,
|
||||
JsonTokenType.String => reader.GetString(),
|
||||
JsonTokenType.Number => reader.GetDecimal(),
|
||||
_ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"),
|
||||
};
|
||||
if (!_converterOptionsCache.TryGetValue(attribute.JsonConverterType, out var newOptions))
|
||||
{
|
||||
var converter = (JsonConverter)Activator.CreateInstance(attribute.JsonConverterType);
|
||||
newOptions = new JsonSerializerOptions
|
||||
{
|
||||
NumberHandling = SerializerOptions.WithConverters.NumberHandling,
|
||||
PropertyNameCaseInsensitive = SerializerOptions.WithConverters.PropertyNameCaseInsensitive,
|
||||
Converters = { converter },
|
||||
};
|
||||
_converterOptionsCache.TryAdd(attribute.JsonConverterType, newOptions);
|
||||
}
|
||||
|
||||
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType, newOptions);
|
||||
}
|
||||
else if (attribute.DefaultDeserialization)
|
||||
{
|
||||
// Use default deserialization
|
||||
value = JsonDocument.ParseValue(ref reader).Deserialize(targetType, options);
|
||||
}
|
||||
else
|
||||
{
|
||||
value = reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.Null => null,
|
||||
JsonTokenType.False => false,
|
||||
JsonTokenType.True => true,
|
||||
JsonTokenType.String => reader.GetString(),
|
||||
JsonTokenType.Number => reader.GetDecimal(),
|
||||
JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options),
|
||||
_ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"),
|
||||
};
|
||||
}
|
||||
|
||||
if (targetType.IsAssignableFrom(value?.GetType()))
|
||||
attribute.PropertyInfo.SetValue(result, value == null ? null : value);
|
||||
else
|
||||
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for comma seperated enum values
|
||||
/// </summary>
|
||||
public class CommaSplitEnumConverter<T> : JsonConverter<IEnumerable<T>> where T : Enum
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return (reader.GetString()?.Split(',').Select(x => EnumConverter.ParseString<T>(x)).ToArray() ?? new T[0])!;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, IEnumerable<T> value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
var stringValue = reader.GetString();
|
||||
if (string.IsNullOrWhiteSpace(stringValue)
|
||||
|| stringValue == "-1"
|
||||
|| stringValue == "0001-01-01T00:00:00Z"
|
||||
|| double.TryParse(stringValue, out var doubleVal) && doubleVal == 0)
|
||||
{
|
||||
return default;
|
||||
|
||||
@@ -19,9 +19,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
var value = reader.GetString();
|
||||
if (string.IsNullOrEmpty(value) || string.Equals("null", value))
|
||||
if (string.IsNullOrEmpty(value) || string.Equals("null", value, StringComparison.OrdinalIgnoreCase))
|
||||
return null;
|
||||
|
||||
if (string.Equals("Infinity", value, StringComparison.Ordinal))
|
||||
// Infinity returned by the server, default to max value
|
||||
return decimal.MaxValue;
|
||||
|
||||
try
|
||||
{
|
||||
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Attribute for allowing specifying a JsonConverter with constructor parameters
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class JsonConverterCtorAttribute : JsonConverterAttribute
|
||||
{
|
||||
private readonly object[] _parameters;
|
||||
private readonly Type _type;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public JsonConverterCtorAttribute(Type type, params object[] parameters)
|
||||
{
|
||||
_type = type;
|
||||
_parameters = parameters;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override JsonConverter CreateConverter(Type typeToConvert)
|
||||
{
|
||||
return (JsonConverter)Activator.CreateInstance(_type, _parameters);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,7 +23,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
return reader.GetDecimal().ToString();
|
||||
}
|
||||
|
||||
return reader.GetString();
|
||||
try
|
||||
{
|
||||
return reader.GetString();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Replace a value on a string property
|
||||
/// </summary>
|
||||
public class ReplaceConverter : JsonConverter<string>
|
||||
{
|
||||
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ReplaceConverter(params string[] replaceSets)
|
||||
{
|
||||
_replacementSets = replaceSets.Select(x =>
|
||||
{
|
||||
var split = x.Split(new string[] { "->" }, StringSplitOptions.None);
|
||||
if (split.Length != 2)
|
||||
throw new ArgumentException("Invalid replacement config");
|
||||
return (split[0], split[1]);
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var value = reader.GetString();
|
||||
foreach (var set in _replacementSets)
|
||||
value = value?.Replace(set.ValueToReplace, set.ValueToReplaceWith);
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value);
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var info = $"Deserialize unknown Exception: {ex.Message}";
|
||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -121,7 +126,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
return default;
|
||||
|
||||
if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
try
|
||||
{
|
||||
return value.Value.Deserialize<T>(_serializerOptions);
|
||||
}
|
||||
catch { }
|
||||
return default;
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(string))
|
||||
{
|
||||
@@ -133,7 +145,20 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<T?>? GetValues<T>(MessagePath path) => throw new NotImplementedException();
|
||||
public List<T?>? GetValues<T>(MessagePath path)
|
||||
{
|
||||
if (!IsJson)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
var value = GetPathNode(path);
|
||||
if (value == null)
|
||||
return default;
|
||||
|
||||
if (value.Value.ValueKind != JsonValueKind.Array)
|
||||
return default;
|
||||
|
||||
return value.Value.Deserialize<List<T>>()!;
|
||||
}
|
||||
|
||||
private JsonElement? GetPathNode(MessagePath path)
|
||||
{
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||
<PackageVersion>8.0.0</PackageVersion>
|
||||
<AssemblyVersion>8.0.0</AssemblyVersion>
|
||||
<FileVersion>8.0.0</FileVersion>
|
||||
<PackageVersion>8.4.4</PackageVersion>
|
||||
<AssemblyVersion>8.4.4</AssemblyVersion>
|
||||
<FileVersion>8.4.4</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
@@ -20,7 +20,7 @@
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>10.0</LangVersion>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
@@ -48,16 +48,17 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -189,6 +189,16 @@ namespace CryptoExchange.Net
|
||||
throw new ArgumentException($"No values provided for parameter {argumentName}", argumentName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format a string to RFC3339/ISO8601 string
|
||||
/// </summary>
|
||||
/// <param name="dateTime"></param>
|
||||
/// <returns></returns>
|
||||
public static string ToRfc3339String(this DateTime dateTime)
|
||||
{
|
||||
return dateTime.ToString("yyyy-MM-dd'T'HH:mm:ss.fffzzz", DateTimeFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format an exception and inner exception to a readable string
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
@@ -23,5 +24,12 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <param name="options">Options for the order book</param>
|
||||
/// <returns></returns>
|
||||
public ISymbolOrderBook Create(string baseAsset, string quoteAsset, Action<TOptions>? options = null);
|
||||
/// <summary>
|
||||
/// Create a new order book by base and quote asset names
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol</param>
|
||||
/// <param name="options">Options for the order book</param>
|
||||
/// <returns></returns>
|
||||
public ISymbolOrderBook Create(SharedSymbol symbol, Action<TOptions>? options = null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
Task UnsubscribeAsync(UpdateSubscription subscription);
|
||||
|
||||
/// <summary>
|
||||
/// Prepare connections which can subsequently be used for sending websocket requests.
|
||||
/// Prepare connections which can subsequently be used for sending websocket requests. Note that this is not required. If not prepared it will be initialized at the first websocket request.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<CallResult> PrepareConnectionsAsync();
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// Helpers for client libraries
|
||||
/// </summary>
|
||||
public static class LibraryHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Client order id seperator
|
||||
/// </summary>
|
||||
public const string ClientOrderIdSeperator = "JK";
|
||||
|
||||
/// <summary>
|
||||
/// Apply broker id to a client order id
|
||||
/// </summary>
|
||||
/// <param name="clientOrderId"></param>
|
||||
/// <param name="brokerId"></param>
|
||||
/// <param name="maxLength"></param>
|
||||
/// <param name="allowValueAdjustement"></param>
|
||||
/// <returns></returns>
|
||||
public static string ApplyBrokerId(string? clientOrderId, string brokerId, int maxLength, bool allowValueAdjustement)
|
||||
{
|
||||
var reservedLength = brokerId.Length + ClientOrderIdSeperator.Length;
|
||||
|
||||
if ((clientOrderId?.Length + reservedLength) > maxLength)
|
||||
return clientOrderId!;
|
||||
|
||||
if (!string.IsNullOrEmpty(clientOrderId))
|
||||
{
|
||||
if (allowValueAdjustement)
|
||||
clientOrderId = brokerId + ClientOrderIdSeperator + clientOrderId;
|
||||
|
||||
return clientOrderId!;
|
||||
}
|
||||
else
|
||||
{
|
||||
clientOrderId = ExchangeHelpers.AppendRandomString(brokerId + ClientOrderIdSeperator, maxLength);
|
||||
}
|
||||
|
||||
return clientOrderId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
|
||||
public static class TrackerLoggingExtensions
|
||||
{
|
||||
private static readonly Action<ILogger, string, SyncStatus, SyncStatus, Exception?> _klineTrackerStatusChanged;
|
||||
private static readonly Action<ILogger, string, Exception?> _klineTrackerStarting;
|
||||
private static readonly Action<ILogger, string, string, Exception?> _klineTrackerStartFailed;
|
||||
private static readonly Action<ILogger, string, Exception?> _klineTrackerStarted;
|
||||
private static readonly Action<ILogger, string, Exception?> _klineTrackerStopping;
|
||||
private static readonly Action<ILogger, string, Exception?> _klineTrackerStopped;
|
||||
private static readonly Action<ILogger, string, DateTime, Exception?> _klineTrackerInitialDataSet;
|
||||
private static readonly Action<ILogger, string, DateTime, Exception?> _klineTrackerKlineUpdated;
|
||||
private static readonly Action<ILogger, string, DateTime, Exception?> _klineTrackerKlineAdded;
|
||||
private static readonly Action<ILogger, string, Exception?> _klineTrackerConnectionLost;
|
||||
private static readonly Action<ILogger, string, Exception?> _klineTrackerConnectionClosed;
|
||||
private static readonly Action<ILogger, string, Exception?> _klineTrackerConnectionRestored;
|
||||
|
||||
private static readonly Action<ILogger, string, SyncStatus, SyncStatus, Exception?> _tradeTrackerStatusChanged;
|
||||
private static readonly Action<ILogger, string, Exception?> _tradeTrackerStarting;
|
||||
private static readonly Action<ILogger, string, string, Exception?> _tradeTrackerStartFailed;
|
||||
private static readonly Action<ILogger, string, Exception?> _tradeTrackerStarted;
|
||||
private static readonly Action<ILogger, string, Exception?> _tradeTrackerStopping;
|
||||
private static readonly Action<ILogger, string, Exception?> _tradeTrackerStopped;
|
||||
private static readonly Action<ILogger, string, int, long, Exception?> _tradeTrackerInitialDataSet;
|
||||
private static readonly Action<ILogger, string, long, Exception?> _tradeTrackerPreSnapshotSkip;
|
||||
private static readonly Action<ILogger, string, long, Exception?> _tradeTrackerPreSnapshotApplied;
|
||||
private static readonly Action<ILogger, string, long, Exception?> _tradeTrackerTradeAdded;
|
||||
private static readonly Action<ILogger, string, Exception?> _tradeTrackerConnectionLost;
|
||||
private static readonly Action<ILogger, string, Exception?> _tradeTrackerConnectionClosed;
|
||||
private static readonly Action<ILogger, string, Exception?> _tradeTrackerConnectionRestored;
|
||||
|
||||
static TrackerLoggingExtensions()
|
||||
{
|
||||
_klineTrackerStatusChanged = LoggerMessage.Define<string, SyncStatus, SyncStatus>(
|
||||
LogLevel.Debug,
|
||||
new EventId(6001, "KlineTrackerStatusChanged"),
|
||||
"Kline tracker for {Symbol} status changed: {OldStatus} => {NewStatus}");
|
||||
|
||||
_klineTrackerStarting = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
new EventId(6002, "KlineTrackerStarting"),
|
||||
"Kline tracker for {Symbol} starting");
|
||||
|
||||
_klineTrackerStartFailed = LoggerMessage.Define<string, string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(6003, "KlineTrackerStartFailed"),
|
||||
"Kline tracker for {Symbol} failed to start: {Error}");
|
||||
|
||||
_klineTrackerStarted = LoggerMessage.Define<string>(
|
||||
LogLevel.Information,
|
||||
new EventId(6004, "KlineTrackerStarted"),
|
||||
"Kline tracker for {Symbol} started");
|
||||
|
||||
_klineTrackerStopping = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
new EventId(6005, "KlineTrackerStopping"),
|
||||
"Kline tracker for {Symbol} stopping");
|
||||
|
||||
_klineTrackerStopped = LoggerMessage.Define<string>(
|
||||
LogLevel.Information,
|
||||
new EventId(6006, "KlineTrackerStopped"),
|
||||
"Kline tracker for {Symbol} stopped");
|
||||
|
||||
_klineTrackerInitialDataSet = LoggerMessage.Define<string, DateTime>(
|
||||
LogLevel.Debug,
|
||||
new EventId(6007, "KlineTrackerInitialDataSet"),
|
||||
"Kline tracker for {Symbol} initial data set, last timestamp: {LastTime}");
|
||||
|
||||
_klineTrackerKlineUpdated = LoggerMessage.Define<string, DateTime>(
|
||||
LogLevel.Trace,
|
||||
new EventId(6008, "KlineTrackerKlineUpdated"),
|
||||
"Kline tracker for {Symbol} kline updated for open time: {LastTime}");
|
||||
|
||||
_klineTrackerKlineAdded = LoggerMessage.Define<string, DateTime>(
|
||||
LogLevel.Trace,
|
||||
new EventId(6009, "KlineTrackerKlineAdded"),
|
||||
"Kline tracker for {Symbol} new kline for open time: {LastTime}");
|
||||
|
||||
_klineTrackerConnectionLost = LoggerMessage.Define<string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(6010, "KlineTrackerConnectionLost"),
|
||||
"Kline tracker for {Symbol} connection lost");
|
||||
|
||||
_klineTrackerConnectionClosed = LoggerMessage.Define<string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(6011, "KlineTrackerConnectionClosed"),
|
||||
"Kline tracker for {Symbol} disconnected");
|
||||
|
||||
_klineTrackerConnectionRestored = LoggerMessage.Define<string>(
|
||||
LogLevel.Information,
|
||||
new EventId(6012, "KlineTrackerConnectionRestored"),
|
||||
"Kline tracker for {Symbol} successfully resynchronized");
|
||||
|
||||
|
||||
_tradeTrackerStatusChanged = LoggerMessage.Define<string, SyncStatus, SyncStatus>(
|
||||
LogLevel.Debug,
|
||||
new EventId(6013, "KlineTrackerStatusChanged"),
|
||||
"Trade tracker for {Symbol} status changed: {OldStatus} => {NewStatus}");
|
||||
|
||||
_tradeTrackerStarting = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
new EventId(6014, "KlineTrackerStarting"),
|
||||
"Trade tracker for {Symbol} starting");
|
||||
|
||||
_tradeTrackerStartFailed = LoggerMessage.Define<string, string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(6015, "KlineTrackerStartFailed"),
|
||||
"Trade tracker for {Symbol} failed to start: {Error}");
|
||||
|
||||
_tradeTrackerStarted = LoggerMessage.Define<string>(
|
||||
LogLevel.Information,
|
||||
new EventId(6016, "KlineTrackerStarted"),
|
||||
"Trade tracker for {Symbol} started");
|
||||
|
||||
_tradeTrackerStopping = LoggerMessage.Define<string>(
|
||||
LogLevel.Debug,
|
||||
new EventId(6017, "KlineTrackerStopping"),
|
||||
"Trade tracker for {Symbol} stopping");
|
||||
|
||||
_tradeTrackerStopped = LoggerMessage.Define<string>(
|
||||
LogLevel.Information,
|
||||
new EventId(6018, "KlineTrackerStopped"),
|
||||
"Trade tracker for {Symbol} stopped");
|
||||
|
||||
_tradeTrackerInitialDataSet = LoggerMessage.Define<string, int, long>(
|
||||
LogLevel.Debug,
|
||||
new EventId(6019, "TradeTrackerInitialDataSet"),
|
||||
"Trade tracker for {Symbol} snapshot set, Count: {Count}, Last id: {LastId}");
|
||||
|
||||
_tradeTrackerPreSnapshotSkip = LoggerMessage.Define<string, long>(
|
||||
LogLevel.Trace,
|
||||
new EventId(6020, "TradeTrackerPreSnapshotSkip"),
|
||||
"Trade tracker for {Symbol} skipping {Id}, already in snapshot");
|
||||
|
||||
_tradeTrackerPreSnapshotApplied = LoggerMessage.Define<string, long>(
|
||||
LogLevel.Trace,
|
||||
new EventId(6021, "TradeTrackerPreSnapshotApplied"),
|
||||
"Trade tracker for {Symbol} adding {Id} from pre-snapshot");
|
||||
|
||||
_tradeTrackerTradeAdded = LoggerMessage.Define<string, long>(
|
||||
LogLevel.Trace,
|
||||
new EventId(6022, "TradeTrackerTradeAdded"),
|
||||
"Trade tracker for {Symbol} adding trade {Id}");
|
||||
|
||||
_tradeTrackerConnectionLost = LoggerMessage.Define<string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(6023, "TradeTrackerConnectionLost"),
|
||||
"Trade tracker for {Symbol} connection lost");
|
||||
|
||||
_tradeTrackerConnectionClosed = LoggerMessage.Define<string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(6024, "TradeTrackerConnectionClosed"),
|
||||
"Trade tracker for {Symbol} disconnected");
|
||||
|
||||
_tradeTrackerConnectionRestored = LoggerMessage.Define<string>(
|
||||
LogLevel.Information,
|
||||
new EventId(6025, "TradeTrackerConnectionRestored"),
|
||||
"Trade tracker for {Symbol} successfully resynchronized");
|
||||
}
|
||||
|
||||
public static void KlineTrackerStatusChanged(this ILogger logger, string symbol, SyncStatus oldStatus, SyncStatus newStatus)
|
||||
{
|
||||
_klineTrackerStatusChanged(logger, symbol, oldStatus, newStatus, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerStarting(this ILogger logger, string symbol)
|
||||
{
|
||||
_klineTrackerStarting(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerStartFailed(this ILogger logger, string symbol, string error)
|
||||
{
|
||||
_klineTrackerStartFailed(logger, symbol, error, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerStarted(this ILogger logger, string symbol)
|
||||
{
|
||||
_klineTrackerStarted(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerStopping(this ILogger logger, string symbol)
|
||||
{
|
||||
_klineTrackerStopping(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerStopped(this ILogger logger, string symbol)
|
||||
{
|
||||
_klineTrackerStopped(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerInitialDataSet(this ILogger logger, string symbol, DateTime lastTime)
|
||||
{
|
||||
_klineTrackerInitialDataSet(logger, symbol, lastTime, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerKlineUpdated(this ILogger logger, string symbol, DateTime lastTime)
|
||||
{
|
||||
_klineTrackerKlineUpdated(logger, symbol, lastTime, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerKlineAdded(this ILogger logger, string symbol, DateTime lastTime)
|
||||
{
|
||||
_klineTrackerKlineAdded(logger, symbol, lastTime, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerConnectionLost(this ILogger logger, string symbol)
|
||||
{
|
||||
_klineTrackerConnectionLost(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerConnectionClosed(this ILogger logger, string symbol)
|
||||
{
|
||||
_klineTrackerConnectionClosed(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void KlineTrackerConnectionRestored(this ILogger logger, string symbol)
|
||||
{
|
||||
_klineTrackerConnectionRestored(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerStatusChanged(this ILogger logger, string symbol, SyncStatus oldStatus, SyncStatus newStatus)
|
||||
{
|
||||
_tradeTrackerStatusChanged(logger, symbol, oldStatus, newStatus, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerStarting(this ILogger logger, string symbol)
|
||||
{
|
||||
_tradeTrackerStarting(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerStartFailed(this ILogger logger, string symbol, string error)
|
||||
{
|
||||
_tradeTrackerStartFailed(logger, symbol, error, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerStarted(this ILogger logger, string symbol)
|
||||
{
|
||||
_tradeTrackerStarted(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerStopping(this ILogger logger, string symbol)
|
||||
{
|
||||
_tradeTrackerStopping(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerStopped(this ILogger logger, string symbol)
|
||||
{
|
||||
_tradeTrackerStopped(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerInitialDataSet(this ILogger logger, string symbol, int count, long lastId)
|
||||
{
|
||||
_tradeTrackerInitialDataSet(logger, symbol, count, lastId, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerPreSnapshotSkip(this ILogger logger, string symbol, long lastId)
|
||||
{
|
||||
_tradeTrackerPreSnapshotSkip(logger, symbol, lastId, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerPreSnapshotApplied(this ILogger logger, string symbol, long lastId)
|
||||
{
|
||||
_tradeTrackerPreSnapshotApplied(logger, symbol, lastId, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerTradeAdded(this ILogger logger, string symbol, long lastId)
|
||||
{
|
||||
_tradeTrackerTradeAdded(logger, symbol, lastId, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerConnectionLost(this ILogger logger, string symbol)
|
||||
{
|
||||
_tradeTrackerConnectionLost(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerConnectionClosed(this ILogger logger, string symbol)
|
||||
{
|
||||
_tradeTrackerConnectionClosed(logger, symbol, null);
|
||||
}
|
||||
|
||||
public static void TradeTrackerConnectionRestored(this ILogger logger, string symbol)
|
||||
{
|
||||
_tradeTrackerConnectionRestored(logger, symbol, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,30 +8,21 @@
|
||||
/// <summary>
|
||||
/// The host address of the proxy
|
||||
/// </summary>
|
||||
public string Host { get; }
|
||||
public string Host { get; set; }
|
||||
/// <summary>
|
||||
/// The port of the proxy
|
||||
/// </summary>
|
||||
public int Port { get; }
|
||||
public int Port { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The login of the proxy
|
||||
/// </summary>
|
||||
public string? Login { get; }
|
||||
public string? Login { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The password of the proxy
|
||||
/// </summary>
|
||||
public string? Password { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create new settings for a proxy
|
||||
/// </summary>
|
||||
/// <param name="host">The proxy hostname/ip</param>
|
||||
/// <param name="port">The proxy port</param>
|
||||
public ApiProxy(string host, int port): this(host, port, null, null)
|
||||
{
|
||||
}
|
||||
public string? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create new settings for a proxy
|
||||
@@ -40,7 +31,7 @@
|
||||
/// <param name="port">The proxy port</param>
|
||||
/// <param name="login">The proxy login</param>
|
||||
/// <param name="password">The proxy password</param>
|
||||
public ApiProxy(string host, int port, string? login, string? password)
|
||||
public ApiProxy(string host, int port, string? login = null, string? password = null)
|
||||
{
|
||||
Host = host;
|
||||
Port = port;
|
||||
|
||||
@@ -68,6 +68,33 @@
|
||||
Json
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tracker sync status
|
||||
/// </summary>
|
||||
public enum SyncStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// Not connected
|
||||
/// </summary>
|
||||
Disconnected,
|
||||
/// <summary>
|
||||
/// Syncing, data connection is being made
|
||||
/// </summary>
|
||||
Syncing,
|
||||
/// <summary>
|
||||
/// The connection is active, but the full data backlog is not yet reached. For example, a tracker set to retain 10 minutes of data only has 8 minutes of data at this moment.
|
||||
/// </summary>
|
||||
PartiallySynced,
|
||||
/// <summary>
|
||||
/// Synced
|
||||
/// </summary>
|
||||
Synced,
|
||||
/// <summary>
|
||||
/// Disposed
|
||||
/// </summary>
|
||||
Diposed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Status of the order book
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace CryptoExchange.Net.Objects.Options
|
||||
{
|
||||
/// <summary>
|
||||
/// Library options
|
||||
/// </summary>
|
||||
/// <typeparam name="TRestOptions"></typeparam>
|
||||
/// <typeparam name="TSocketOptions"></typeparam>
|
||||
/// <typeparam name="TApiCredentials"></typeparam>
|
||||
/// <typeparam name="TEnvironment"></typeparam>
|
||||
public class LibraryOptions<TRestOptions, TSocketOptions, TApiCredentials, TEnvironment>
|
||||
where TRestOptions: RestExchangeOptions, new()
|
||||
where TSocketOptions: SocketExchangeOptions, new()
|
||||
where TApiCredentials: ApiCredentials
|
||||
where TEnvironment: TradeEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// Rest client options
|
||||
/// </summary>
|
||||
public TRestOptions Rest { get; set; } = new TRestOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Socket client options
|
||||
/// </summary>
|
||||
public TSocketOptions Socket { get; set; } = new TSocketOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Trade environment. Contains info about URL's to use to connect to the API.
|
||||
/// </summary>
|
||||
public TEnvironment? Environment { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The api credentials used for signing requests.
|
||||
/// </summary>
|
||||
public TApiCredentials? ApiCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The DI service lifetime for the socket client
|
||||
/// </summary>
|
||||
public ServiceLifetime? SocketClientLifeTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Copy values from these options to the target options
|
||||
/// </summary>
|
||||
public T Set<T>(T targetOptions) where T: LibraryOptions<TRestOptions, TSocketOptions, TApiCredentials, TEnvironment>
|
||||
{
|
||||
targetOptions.ApiCredentials = ApiCredentials;
|
||||
targetOptions.Environment = Environment;
|
||||
targetOptions.SocketClientLifeTime = SocketClientLifeTime;
|
||||
targetOptions.Rest = Rest.Set(targetOptions.Rest);
|
||||
targetOptions.Socket = Socket.Set(targetOptions.Socket);
|
||||
|
||||
return targetOptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,19 +19,15 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
public TimeSpan? TimestampRecalculationInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// Set the values of this options on the target options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public virtual T Copy<T>() where T : RestApiOptions, new()
|
||||
public T Set<T>(T item) where T : RestApiOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
AutoTimestamp = AutoTimestamp,
|
||||
TimestampRecalculationInterval = TimestampRecalculationInterval
|
||||
};
|
||||
item.ApiCredentials = ApiCredentials?.Copy();
|
||||
item.OutputOriginalData = OutputOriginalData;
|
||||
item.AutoTimestamp = AutoTimestamp;
|
||||
item.TimestampRecalculationInterval = TimestampRecalculationInterval;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,25 +29,21 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// Set the values of this options on the target options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : RestExchangeOptions, new()
|
||||
public T Set<T>(T item) where T : RestExchangeOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
AutoTimestamp = AutoTimestamp,
|
||||
TimestampRecalculationInterval = TimestampRecalculationInterval,
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
Proxy = Proxy,
|
||||
RequestTimeout = RequestTimeout,
|
||||
RateLimiterEnabled = RateLimiterEnabled,
|
||||
RateLimitingBehaviour = RateLimitingBehaviour,
|
||||
CachingEnabled = CachingEnabled,
|
||||
CachingMaxAge = CachingMaxAge,
|
||||
};
|
||||
item.OutputOriginalData = OutputOriginalData;
|
||||
item.AutoTimestamp = AutoTimestamp;
|
||||
item.TimestampRecalculationInterval = TimestampRecalculationInterval;
|
||||
item.ApiCredentials = ApiCredentials?.Copy();
|
||||
item.Proxy = Proxy;
|
||||
item.RequestTimeout = RequestTimeout;
|
||||
item.RateLimiterEnabled = RateLimiterEnabled;
|
||||
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
||||
item.CachingEnabled = CachingEnabled;
|
||||
item.CachingMaxAge = CachingMaxAge;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,15 +62,13 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// Set the values of this options on the target options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public new T Copy<T>() where T : RestExchangeOptions<TEnvironment>, new()
|
||||
public new T Set<T>(T target) where T : RestExchangeOptions<TEnvironment>, new()
|
||||
{
|
||||
var result = base.Copy<T>();
|
||||
result.Environment = Environment;
|
||||
return result;
|
||||
base.Set(target);
|
||||
target.Environment = Environment;
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,19 +20,15 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
public int? MaxSocketConnections { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// Set the values of this options on the target options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : SocketApiOptions, new()
|
||||
public T Set<T>(T item) where T : SocketApiOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
SocketNoDataTimeout = SocketNoDataTimeout,
|
||||
MaxSocketConnections = MaxSocketConnections,
|
||||
};
|
||||
item.ApiCredentials = ApiCredentials?.Copy();
|
||||
item.OutputOriginalData = OutputOriginalData;
|
||||
item.SocketNoDataTimeout = SocketNoDataTimeout;
|
||||
item.MaxSocketConnections = MaxSocketConnections;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,24 +57,22 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T Copy<T>() where T : SocketExchangeOptions, new()
|
||||
public T Set<T>(T item) where T : SocketExchangeOptions, new()
|
||||
{
|
||||
return new T
|
||||
{
|
||||
ApiCredentials = ApiCredentials?.Copy(),
|
||||
OutputOriginalData = OutputOriginalData,
|
||||
ReconnectPolicy = ReconnectPolicy,
|
||||
DelayAfterConnect = DelayAfterConnect,
|
||||
MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket,
|
||||
ReconnectInterval = ReconnectInterval,
|
||||
SocketNoDataTimeout = SocketNoDataTimeout,
|
||||
SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget,
|
||||
MaxSocketConnections = MaxSocketConnections,
|
||||
Proxy = Proxy,
|
||||
RequestTimeout = RequestTimeout,
|
||||
RateLimitingBehaviour = RateLimitingBehaviour,
|
||||
RateLimiterEnabled = RateLimiterEnabled,
|
||||
};
|
||||
item.ApiCredentials = ApiCredentials?.Copy();
|
||||
item.OutputOriginalData = OutputOriginalData;
|
||||
item.ReconnectPolicy = ReconnectPolicy;
|
||||
item.DelayAfterConnect = DelayAfterConnect;
|
||||
item.MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket;
|
||||
item.ReconnectInterval = ReconnectInterval;
|
||||
item.SocketNoDataTimeout = SocketNoDataTimeout;
|
||||
item.SocketSubscriptionsCombineTarget = SocketSubscriptionsCombineTarget;
|
||||
item.MaxSocketConnections = MaxSocketConnections;
|
||||
item.Proxy = Proxy;
|
||||
item.RequestTimeout = RequestTimeout;
|
||||
item.RateLimitingBehaviour = RateLimitingBehaviour;
|
||||
item.RateLimiterEnabled = RateLimiterEnabled;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,15 +91,13 @@ namespace CryptoExchange.Net.Objects.Options
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
|
||||
/// <summary>
|
||||
/// Create a copy of this options
|
||||
/// Set the values of this options on the target options
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public new T Copy<T>() where T : SocketExchangeOptions<TEnvironment>, new()
|
||||
public new T Set<T>(T target) where T : SocketExchangeOptions<TEnvironment>, new()
|
||||
{
|
||||
var result = base.Copy<T>();
|
||||
result.Environment = Environment;
|
||||
return result;
|
||||
base.Set(target);
|
||||
target.Environment = Environment;
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,12 +58,16 @@ namespace CryptoExchange.Net.Objects
|
||||
/// </summary>
|
||||
public IRateLimitGuard? LimitGuard { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether this request should never be cached
|
||||
/// </summary>
|
||||
public bool PreventCaching { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Connection id
|
||||
/// </summary>
|
||||
public int? ConnectionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
|
||||
@@ -24,14 +24,14 @@
|
||||
/// <summary>
|
||||
/// Name of the environment
|
||||
/// </summary>
|
||||
public string EnvironmentName { get; init; }
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
protected TradeEnvironment(string name)
|
||||
{
|
||||
EnvironmentName = name;
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.OrderBook
|
||||
@@ -8,14 +9,14 @@ namespace CryptoExchange.Net.OrderBook
|
||||
public class OrderBookFactory<TOptions> : IOrderBookFactory<TOptions> where TOptions: OrderBookOptions
|
||||
{
|
||||
private readonly Func<string, Action<TOptions>?, ISymbolOrderBook> _symbolCtor;
|
||||
private readonly Func<string, string, Action<TOptions>?, ISymbolOrderBook> _assetsCtor;
|
||||
private readonly Func<SharedSymbol, Action<TOptions>?, ISymbolOrderBook> _assetsCtor;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="symbolCtor"></param>
|
||||
/// <param name="assetsCtor"></param>
|
||||
public OrderBookFactory(Func<string, Action<TOptions>?, ISymbolOrderBook> symbolCtor, Func<string, string, Action<TOptions>?, ISymbolOrderBook> assetsCtor)
|
||||
public OrderBookFactory(Func<string, Action<TOptions>?, ISymbolOrderBook> symbolCtor, Func<SharedSymbol, Action<TOptions>?, ISymbolOrderBook> assetsCtor)
|
||||
{
|
||||
_symbolCtor = symbolCtor;
|
||||
_assetsCtor = assetsCtor;
|
||||
@@ -25,6 +26,9 @@ namespace CryptoExchange.Net.OrderBook
|
||||
public ISymbolOrderBook Create(string symbol, Action<TOptions>? options = null) => _symbolCtor(symbol, options);
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISymbolOrderBook Create(string baseAsset, string quoteAsset, Action<TOptions>? options = null) => _assetsCtor(baseAsset, quoteAsset, options);
|
||||
public ISymbolOrderBook Create(string baseAsset, string quoteAsset, Action<TOptions>? options = null) => _assetsCtor(new SharedSymbol(TradingMode.Spot, baseAsset, quoteAsset), options);
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISymbolOrderBook Create(SharedSymbol symbol, Action<TOptions>? options = null) => _assetsCtor(symbol, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, string?, string> PerEndpoint { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method);
|
||||
/// <summary>
|
||||
/// Apply guard per connection
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, string?, string> PerConnection { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.ConnectionId.ToString());
|
||||
/// <summary>
|
||||
/// Apply guard per API key
|
||||
/// </summary>
|
||||
public static Func<RequestDefinition, string, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => key!);
|
||||
@@ -106,7 +110,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
|
||||
var delay = tracker.GetWaitTime(requestWeight);
|
||||
if (delay == default)
|
||||
return LimitCheck.NotNeeded;
|
||||
return LimitCheck.NotNeeded(Limit, TimeSpan, tracker.Current);
|
||||
|
||||
return LimitCheck.Needed(delay, Limit, TimeSpan, tracker.Current);
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
||||
|
||||
var delay = tracker.GetWaitTime(requestWeight);
|
||||
if (delay == default)
|
||||
return LimitCheck.NotNeeded;
|
||||
return LimitCheck.NotNeeded(_limit, _period, tracker.Current);
|
||||
|
||||
return LimitCheck.Needed(delay, _limit, _period, tracker.Current);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,11 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
||||
/// </summary>
|
||||
event Action<RateLimitEvent> RateLimitTriggered;
|
||||
|
||||
/// <summary>
|
||||
/// Event when the rate limit is updated. Note that it's only updated when a request is send, so there are no specific updates when the current usage is decaying.
|
||||
/// </summary>
|
||||
event Action<RateLimitUpdateEvent>? RateLimitUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Add a rate limit guard
|
||||
/// </summary>
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
/// <summary>
|
||||
/// No wait needed
|
||||
/// </summary>
|
||||
public static LimitCheck NotNeeded { get; } = new LimitCheck(true, default, default, default, default);
|
||||
public static LimitCheck NotNeeded(int limit, TimeSpan period, int current) => new(true, default, limit, period, current);
|
||||
|
||||
/// <summary>
|
||||
/// Wait needed
|
||||
|
||||
@@ -4,10 +4,14 @@ using System;
|
||||
namespace CryptoExchange.Net.RateLimiting
|
||||
{
|
||||
/// <summary>
|
||||
/// Rate limit event
|
||||
/// Rate limit triggered event
|
||||
/// </summary>
|
||||
public record RateLimitEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the item the limit was checked for
|
||||
/// </summary>
|
||||
public int ItemId { get; set; }
|
||||
/// <summary>
|
||||
/// Name of the API limit that is reached
|
||||
/// </summary>
|
||||
@@ -52,18 +56,9 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="apiLimit"></param>
|
||||
/// <param name="limitDescription"></param>
|
||||
/// <param name="definition"></param>
|
||||
/// <param name="host"></param>
|
||||
/// <param name="current"></param>
|
||||
/// <param name="requestWeight"></param>
|
||||
/// <param name="limit"></param>
|
||||
/// <param name="timePeriod"></param>
|
||||
/// <param name="delayTime"></param>
|
||||
/// <param name="behaviour"></param>
|
||||
public RateLimitEvent(string apiLimit, string limitDescription, RequestDefinition definition, string host, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour)
|
||||
public RateLimitEvent(int itemId, string apiLimit, string limitDescription, RequestDefinition definition, string host, int current, int requestWeight, int? limit, TimeSpan? timePeriod, TimeSpan? delayTime, RateLimitingBehaviour behaviour)
|
||||
{
|
||||
ItemId = itemId;
|
||||
ApiLimit = apiLimit;
|
||||
LimitDescription = limitDescription;
|
||||
RequestDefinition = definition;
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<RateLimitEvent>? RateLimitTriggered;
|
||||
/// <inheritdoc />
|
||||
public event Action<RateLimitUpdateEvent>? RateLimitUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@@ -105,7 +107,7 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
else
|
||||
logger.RateLimitRequestFailed(itemId, definition.Path, guard.Name, guard.Description);
|
||||
|
||||
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
||||
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
||||
return new CallResult(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}"));
|
||||
}
|
||||
|
||||
@@ -120,7 +122,7 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
else
|
||||
logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description);
|
||||
|
||||
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
||||
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
||||
await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false);
|
||||
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||
return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
|
||||
@@ -133,6 +135,8 @@ namespace CryptoExchange.Net.RateLimiting
|
||||
var result = guard.ApplyWeight(type, definition, host, apiKey, requestWeight);
|
||||
if (result.IsApplied)
|
||||
{
|
||||
RateLimitUpdated?.Invoke(new RateLimitUpdateEvent(itemId, _name, guard.Description, result.Current, result.Limit, result.Period));
|
||||
|
||||
if (type == RateLimitItemType.Connection)
|
||||
logger.RateLimitAppliedConnection(itemId, guard.Name, guard.Description, result.Current);
|
||||
else
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.RateLimiting
|
||||
{
|
||||
/// <summary>
|
||||
/// Rate limit update event
|
||||
/// </summary>
|
||||
public record RateLimitUpdateEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the item the limit was checked for
|
||||
/// </summary>
|
||||
public int ItemId { get; set; }
|
||||
/// <summary>
|
||||
/// Name of the API limit that is reached
|
||||
/// </summary>
|
||||
public string ApiLimit { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// Description of the limit that is reached
|
||||
/// </summary>
|
||||
public string LimitDescription { get; set; } = string.Empty;
|
||||
/// <summary>
|
||||
/// The current counter value
|
||||
/// </summary>
|
||||
public int Current { get; set; }
|
||||
/// <summary>
|
||||
/// The limit per time period
|
||||
/// </summary>
|
||||
public int? Limit { get; set; }
|
||||
/// <summary>
|
||||
/// The time period the limit is for
|
||||
/// </summary>
|
||||
public TimeSpan? TimePeriod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public RateLimitUpdateEvent(int itemId, string apiLimit, string limitDescription, int current, int? limit, TimeSpan? timePeriod)
|
||||
{
|
||||
ItemId = itemId;
|
||||
ApiLimit = apiLimit;
|
||||
LimitDescription = limitDescription;
|
||||
Current = current;
|
||||
Limit = limit;
|
||||
TimePeriod = timePeriod;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.Requests
|
||||
/// </summary>
|
||||
public class RequestFactory : IRequestFactory
|
||||
{
|
||||
private HttpClient? _httpClient;
|
||||
private HttpClient? _httpClient;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? client = null)
|
||||
@@ -19,7 +19,12 @@ namespace CryptoExchange.Net.Requests
|
||||
if (client == null)
|
||||
{
|
||||
var handler = new HttpClientHandler();
|
||||
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||
try
|
||||
{
|
||||
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||
}
|
||||
catch (PlatformNotSupportedException) { }
|
||||
|
||||
if (proxy != null)
|
||||
{
|
||||
handler.Proxy = new WebProxy
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
/// <summary>
|
||||
/// Leverage is configured for the symbol
|
||||
/// </summary>
|
||||
PerSymbol
|
||||
PerSymbol,
|
||||
/// <summary>
|
||||
/// Leverage is configured for the entire account
|
||||
/// </summary>
|
||||
PerAccount
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,11 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
TradingMode[] SupportedTradingModes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
|
||||
/// </summary>
|
||||
bool Authenticated { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Format a base and quote asset to an exchange accepted symbol
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Client for requesting user trading fees
|
||||
/// </summary>
|
||||
public interface IFeeRestClient : ISharedClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Fee request options
|
||||
/// </summary>
|
||||
EndpointOptions<GetFeeRequest> GetFeeOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get trading fees for a symbol
|
||||
/// </summary>
|
||||
/// <param name="request">Request info</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
Task<ExchangeWebResult<SharedFee>> GetFeesAsync(GetFeeRequest request, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetClosedOrdersOptions(SharedPaginationSupport paginationType, bool timeFilterSupported) : base(paginationType, true)
|
||||
public GetClosedOrdersOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
|
||||
{
|
||||
TimeFilterSupported = timeFilterSupported;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetDepositsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported) : base(paginationType, true)
|
||||
public GetDepositsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
|
||||
{
|
||||
TimeFilterSupported = timeFilterSupported;
|
||||
}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetFundingRateHistoryOptions(SharedPaginationSupport paginationType, bool needsAuthentication) : base(paginationType, needsAuthentication)
|
||||
public GetFundingRateHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,10 +20,6 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
public int? MaxTotalDataPoints { get; set; }
|
||||
/// <summary>
|
||||
/// Max number of data points which can be requested in a single request
|
||||
/// </summary>
|
||||
public int? MaxRequestDataPoints { get; set; }
|
||||
/// <summary>
|
||||
/// The max age of the data that can be requested
|
||||
/// </summary>
|
||||
public TimeSpan? MaxAge { get; set; }
|
||||
@@ -31,14 +27,13 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetKlinesOptions(SharedPaginationSupport paginationType, bool needsAuthentication) : base(paginationType, needsAuthentication)
|
||||
public GetKlinesOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
|
||||
{
|
||||
SupportIntervals = new[]
|
||||
{
|
||||
SharedKlineInterval.FiveMinutes,
|
||||
SharedKlineInterval.FifteenMinutes,
|
||||
SharedKlineInterval.OneHour,
|
||||
SharedKlineInterval.FifteenMinutes,
|
||||
SharedKlineInterval.OneDay,
|
||||
SharedKlineInterval.OneWeek,
|
||||
SharedKlineInterval.OneMonth
|
||||
@@ -48,7 +43,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetKlinesOptions(SharedPaginationSupport paginationType, bool needsAuthentication, params SharedKlineInterval[] intervals) : base(paginationType, needsAuthentication)
|
||||
public GetKlinesOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication, params SharedKlineInterval[] intervals) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
|
||||
{
|
||||
SupportIntervals = intervals;
|
||||
}
|
||||
@@ -69,8 +64,8 @@ namespace CryptoExchange.Net.SharedApis
|
||||
if (MaxAge.HasValue && request.StartTime < DateTime.UtcNow.Add(-MaxAge.Value))
|
||||
return new ArgumentError($"Only the most recent {MaxAge} klines are available");
|
||||
|
||||
if (MaxRequestDataPoints.HasValue && request.Limit > MaxRequestDataPoints.Value)
|
||||
return new ArgumentError($"Only {MaxRequestDataPoints} klines can be retrieved per request");
|
||||
if (request.Limit > MaxLimit)
|
||||
return new ArgumentError($"Only {MaxLimit} klines can be retrieved per request");
|
||||
|
||||
if (MaxTotalDataPoints.HasValue)
|
||||
{
|
||||
@@ -96,8 +91,6 @@ namespace CryptoExchange.Net.SharedApis
|
||||
sb.AppendLine($"Max age of data: {MaxAge}");
|
||||
if (MaxTotalDataPoints != null)
|
||||
sb.AppendLine($"Max total data points available: {MaxTotalDataPoints}");
|
||||
if (MaxRequestDataPoints != null)
|
||||
sb.AppendLine($"Max data points per request: {MaxRequestDataPoints}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetPositionHistoryOptions(SharedPaginationSupport paginationType) : base(paginationType, true)
|
||||
public GetPositionHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetTradeHistoryOptions(SharedPaginationSupport paginationType, bool needsAuthentication) : base(paginationType, needsAuthentication)
|
||||
public GetTradeHistoryOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit, bool needsAuthentication) : base(paginationType, timeFilterSupported, maxLimit, needsAuthentication)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public GetWithdrawalsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported) : base(paginationType, true)
|
||||
public GetWithdrawalsOptions(SharedPaginationSupport paginationType, bool timeFilterSupported, int maxLimit) : base(paginationType, timeFilterSupported, maxLimit, true)
|
||||
{
|
||||
TimeFilterSupported = timeFilterSupported;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
@@ -13,12 +14,24 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// </summary>
|
||||
public SharedPaginationSupport PaginationSupport { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether filtering based on start/end time is supported
|
||||
/// </summary>
|
||||
public bool TimePeriodFilterSupport { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Max amount of results that can be requested
|
||||
/// </summary>
|
||||
public int MaxLimit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public PaginatedEndpointOptions(SharedPaginationSupport paginationType, bool needsAuthentication) : base(needsAuthentication)
|
||||
public PaginatedEndpointOptions(SharedPaginationSupport paginationType, bool timePeriodSupport, int maxLimit, bool needsAuthentication) : base(needsAuthentication)
|
||||
{
|
||||
PaginationSupport = paginationType;
|
||||
TimePeriodFilterSupport = timePeriodSupport;
|
||||
MaxLimit = maxLimit;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -26,6 +39,8 @@ namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
var sb = new StringBuilder(base.ToString(exchange));
|
||||
sb.AppendLine($"Pagination type: {PaginationSupport}");
|
||||
sb.AppendLine($"Time period filter support: {TimePeriodFilterSupport}");
|
||||
sb.AppendLine($"Max limit: {MaxLimit}");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Request to retrieve trading fees
|
||||
/// </summary>
|
||||
public record GetFeeRequest : SharedSymbolRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol to retrieve fees for</param>
|
||||
/// <param name="exchangeParameters">Exchange specific parameters</param>
|
||||
public GetFeeRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.SharedApis
|
||||
{
|
||||
/// <summary>
|
||||
/// Trading fee info
|
||||
/// </summary>
|
||||
public record SharedFee
|
||||
{
|
||||
/// <summary>
|
||||
/// Taker fee percentage
|
||||
/// </summary>
|
||||
public decimal TakerFee { get; set; }
|
||||
/// <summary>
|
||||
/// Maker fee percentage
|
||||
/// </summary>
|
||||
public decimal MakerFee { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SharedFee(decimal makerFee, decimal takerFee)
|
||||
{
|
||||
MakerFee = makerFee;
|
||||
TakerFee = takerFee;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,15 +14,15 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// Last trade price
|
||||
/// </summary>
|
||||
public decimal LastPrice { get; set; }
|
||||
public decimal? LastPrice { get; set; }
|
||||
/// <summary>
|
||||
/// High price in the last 24h
|
||||
/// </summary>
|
||||
public decimal HighPrice { get; set; }
|
||||
public decimal? HighPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Low price in the last 24h
|
||||
/// </summary>
|
||||
public decimal LowPrice { get; set; }
|
||||
public decimal? LowPrice { get; set; }
|
||||
/// <summary>
|
||||
/// The volume in the last 24h
|
||||
/// </summary>
|
||||
@@ -51,7 +51,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SharedFuturesTicker(string symbol, decimal lastPrice, decimal highPrice, decimal lowPrice, decimal volume, decimal? changePercentage)
|
||||
public SharedFuturesTicker(string symbol, decimal? lastPrice, decimal? highPrice, decimal? lowPrice, decimal volume, decimal? changePercentage)
|
||||
{
|
||||
Symbol = symbol;
|
||||
LastPrice = lastPrice;
|
||||
|
||||
@@ -19,6 +19,10 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// Trade time
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
/// <summary>
|
||||
/// Trade side. Buy means that the taker took an ask order of the order book, sell means the taker took a bid order of the order book.
|
||||
/// </summary>
|
||||
public SharedOrderSide? Side { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// Side of the trade
|
||||
/// </summary>
|
||||
public SharedOrderSide Side { get; set; }
|
||||
public SharedOrderSide? Side { get; set; }
|
||||
/// <summary>
|
||||
/// Fee paid for the trade
|
||||
/// </summary>
|
||||
@@ -51,7 +51,7 @@ namespace CryptoExchange.Net.SharedApis
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SharedUserTrade(string symbol, string orderId, string id, SharedOrderSide side, decimal quantity, decimal price, DateTime timestamp)
|
||||
public SharedUserTrade(string symbol, string orderId, string id, SharedOrderSide? side, decimal quantity, decimal price, DateTime timestamp)
|
||||
{
|
||||
Symbol = symbol;
|
||||
OrderId = orderId;
|
||||
|
||||
@@ -209,7 +209,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
{
|
||||
if (Parameters.RateLimiter != null)
|
||||
{
|
||||
var definition = new RequestDefinition(Id.ToString(), HttpMethod.Get);
|
||||
var definition = new RequestDefinition(Uri.AbsolutePath, HttpMethod.Get) { ConnectionId = Id };
|
||||
var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, Id, RateLimitItemType.Connection, definition, _baseAddress, null, 1, Parameters.RateLimitingBehaviour, _ctsSource.Token).ConfigureAwait(false);
|
||||
if (!limitResult)
|
||||
return new CallResult(new ClientRateLimitError("Connection limit reached"));
|
||||
@@ -475,7 +475,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
private async Task SendLoopAsync()
|
||||
{
|
||||
var requestDefinition = new RequestDefinition(Id.ToString(), HttpMethod.Get);
|
||||
var requestDefinition = new RequestDefinition(Uri.AbsolutePath, HttpMethod.Get) { ConnectionId = Id };
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
|
||||
@@ -14,4 +14,19 @@
|
||||
/// </summary>
|
||||
public bool Authenticated { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dedicated connection state
|
||||
/// </summary>
|
||||
public class DedicatedConnectionState
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the connection is a dedicated request connection
|
||||
/// </summary>
|
||||
public bool IsDedicatedRequestConnection { get; set; }
|
||||
/// <summary>
|
||||
/// Whether the dedication request connection should be authenticated
|
||||
/// </summary>
|
||||
public bool Authenticated { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +177,10 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <inheritdoc />
|
||||
public override async Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message)
|
||||
{
|
||||
var typedMessage = message.As((TServerResponse)message.Data);
|
||||
if (!ValidateMessage(typedMessage))
|
||||
return new CallResult(null);
|
||||
|
||||
CurrentResponses++;
|
||||
if (CurrentResponses == RequiredResponses)
|
||||
{
|
||||
@@ -186,7 +190,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
if (Result?.Success != false)
|
||||
// If an error result is already set don't override that
|
||||
Result = HandleMessage(connection, message.As((TServerResponse)message.Data));
|
||||
Result = HandleMessage(connection, typedMessage);
|
||||
|
||||
if (CurrentResponses == RequiredResponses)
|
||||
{
|
||||
@@ -198,6 +202,13 @@ namespace CryptoExchange.Net.Sockets
|
||||
return Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate if a message is actually processable by this query
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <returns></returns>
|
||||
public virtual bool ValidateMessage(DataEvent<TServerResponse> message) => true;
|
||||
|
||||
/// <summary>
|
||||
/// Handle the query response
|
||||
/// </summary>
|
||||
|
||||
@@ -186,9 +186,21 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether this connection should be kept alive even when there is no subscription
|
||||
/// Info on whether this connection is a dedicated request connection
|
||||
/// </summary>
|
||||
public bool DedicatedRequestConnection { get; internal set; }
|
||||
public DedicatedConnectionState DedicatedRequestConnection { get; internal set; } = new DedicatedConnectionState();
|
||||
|
||||
/// <summary>
|
||||
/// Current subscription topics on this connection
|
||||
/// </summary>
|
||||
public IEnumerable<string> Topics
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_listenersLock)
|
||||
return _listeners.OfType<Subscription>().Select(x => x.Topic).Where(t => t != null).ToList()!;
|
||||
}
|
||||
}
|
||||
|
||||
private bool _pausedActivity;
|
||||
private readonly object _listenersLock;
|
||||
@@ -268,7 +280,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
lock (_listenersLock)
|
||||
{
|
||||
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription))
|
||||
subscription.Confirmed = false;
|
||||
subscription.Reset();
|
||||
|
||||
foreach (var query in _listeners.OfType<Query>().ToList())
|
||||
{
|
||||
@@ -293,7 +305,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
lock (_listenersLock)
|
||||
{
|
||||
foreach (var subscription in _listeners.OfType<Subscription>().Where(l => l.UserSubscription))
|
||||
subscription.Confirmed = false;
|
||||
subscription.Reset();
|
||||
|
||||
foreach (var query in _listeners.OfType<Query>().ToList())
|
||||
{
|
||||
@@ -506,8 +518,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
}
|
||||
|
||||
if (processor is Subscription subscriptionProcessor && !subscriptionProcessor.Confirmed)
|
||||
{
|
||||
// If this message is for this listener then it is automatically confirmed, even if the subscription is not (yet) confirmed
|
||||
subscriptionProcessor.Confirmed = true;
|
||||
// This doesn't trigger a waiting subscribe query, should probably also somehow set the wait event for that
|
||||
}
|
||||
|
||||
// 6. Deserialize the message
|
||||
object? deserialized = null;
|
||||
@@ -600,6 +615,10 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
public async Task CloseAsync(Subscription subscription)
|
||||
{
|
||||
// If we are resubscribing this subscription at this moment we'll want to wait for a bit until it is finished to avoid concurrency issues
|
||||
while (subscription.IsResubscribing)
|
||||
await Task.Delay(50).ConfigureAwait(false);
|
||||
|
||||
subscription.Closed = true;
|
||||
|
||||
if (Status == SocketStatus.Closing || Status == SocketStatus.Closed || Status == SocketStatus.Disposed)
|
||||
@@ -615,7 +634,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
bool shouldCloseConnection;
|
||||
lock (_listenersLock)
|
||||
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Closed) && !DedicatedRequestConnection;
|
||||
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Closed) && !DedicatedRequestConnection.IsDedicatedRequestConnection;
|
||||
|
||||
if (!anyDuplicateSubscription)
|
||||
{
|
||||
@@ -838,7 +857,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
if (!_socket.IsOpen)
|
||||
return new CallResult(new WebError("Socket not connected"));
|
||||
|
||||
if (!DedicatedRequestConnection)
|
||||
if (!DedicatedRequestConnection.IsDedicatedRequestConnection)
|
||||
{
|
||||
bool anySubscriptions;
|
||||
lock (_listenersLock)
|
||||
@@ -856,7 +875,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
lock (_listenersLock)
|
||||
{
|
||||
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated)
|
||||
|| (DedicatedRequestConnection && ApiClient.AuthenticationProvider != null);
|
||||
|| (DedicatedRequestConnection.IsDedicatedRequestConnection && DedicatedRequestConnection.Authenticated);
|
||||
}
|
||||
|
||||
if (anyAuthenticated)
|
||||
@@ -883,7 +902,7 @@ namespace CryptoExchange.Net.Sockets
|
||||
|
||||
List<Subscription> subList;
|
||||
lock (_listenersLock)
|
||||
subList = _listeners.OfType<Subscription>().Skip(batch * batchSize).Take(batchSize).ToList();
|
||||
subList = _listeners.OfType<Subscription>().Where(x => !x.Closed).Skip(batch * batchSize).Take(batchSize).ToList();
|
||||
|
||||
if (subList.Count == 0)
|
||||
break;
|
||||
@@ -892,20 +911,30 @@ namespace CryptoExchange.Net.Sockets
|
||||
foreach (var subscription in subList)
|
||||
{
|
||||
subscription.ConnectionInvocations = 0;
|
||||
if (subscription.Closed)
|
||||
// Can be closed during resubscribing
|
||||
continue;
|
||||
|
||||
subscription.IsResubscribing = true;
|
||||
var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
_logger.FailedRequestRevitalization(SocketId, result.Error?.ToString());
|
||||
subscription.IsResubscribing = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
var subQuery = subscription.GetSubQuery(this);
|
||||
if (subQuery == null)
|
||||
{
|
||||
subscription.IsResubscribing = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
var waitEvent = new AsyncResetEvent(false);
|
||||
taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) =>
|
||||
{
|
||||
subscription.IsResubscribing = false;
|
||||
subscription.HandleSubQueryResponse(subQuery.Response!);
|
||||
waitEvent.Set();
|
||||
if (r.Result.Success)
|
||||
|
||||
@@ -44,6 +44,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// </summary>
|
||||
public bool Closed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is the subscription currently resubscribing
|
||||
/// </summary>
|
||||
public bool IsResubscribing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Logger
|
||||
/// </summary>
|
||||
@@ -76,6 +81,11 @@ namespace CryptoExchange.Net.Sockets
|
||||
/// <returns></returns>
|
||||
public abstract Type? GetMessageType(IMessageAccessor message);
|
||||
|
||||
/// <summary>
|
||||
/// Subscription topic
|
||||
/// </summary>
|
||||
public string? Topic { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
@@ -130,6 +140,20 @@ namespace CryptoExchange.Net.Sockets
|
||||
return Task.FromResult(DoHandleMessage(connection, message));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset the subscription
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
Confirmed = false;
|
||||
DoHandleReset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connection has been reset, do any logic for resetting the subscription
|
||||
/// </summary>
|
||||
public virtual void DoHandleReset() { }
|
||||
|
||||
/// <summary>
|
||||
/// Handle the update message
|
||||
/// </summary>
|
||||
|
||||
@@ -188,7 +188,7 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
{
|
||||
if (propertyValue == default && propValue.Type != JTokenType.Null && !string.IsNullOrEmpty(propValue.ToString()))
|
||||
{
|
||||
if (propertyType == typeof(DateTime?) && (propValue.ToString() == "" || propValue.ToString() == "0" || propValue.ToString() == "-1"))
|
||||
if (propertyType == typeof(DateTime?) && (propValue.ToString() == "" || propValue.ToString() == "0" || propValue.ToString() == "-1" || propValue.ToString() == "01/01/0001 00:00:00"))
|
||||
return;
|
||||
|
||||
// Property value not correct
|
||||
@@ -224,6 +224,9 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
else if (propertyValue.GetType().GetInterfaces().Contains(typeof(IEnumerable))
|
||||
&& propertyValue.GetType() != typeof(string))
|
||||
{
|
||||
if (propValue.Type != JTokenType.Array)
|
||||
return;
|
||||
|
||||
var jObjs = (JArray)propValue;
|
||||
var list = (IEnumerable)propertyValue;
|
||||
var enumerator = list.GetEnumerator();
|
||||
@@ -372,7 +375,8 @@ namespace CryptoExchange.Net.Testing.Comparers
|
||||
}
|
||||
else if (objectValue is DateTime time)
|
||||
{
|
||||
if (time != DateTimeConverter.ParseFromString(jsonValue.Value<string>()!))
|
||||
var jsonStr = jsonValue.Value<string>()!;
|
||||
if (!string.IsNullOrEmpty(jsonStr) && time != DateTimeConverter.ParseFromString(jsonStr))
|
||||
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<string>()} vs {time}");
|
||||
}
|
||||
else if (objectValue is bool bl)
|
||||
|
||||
@@ -173,17 +173,20 @@ namespace CryptoExchange.Net.Testing
|
||||
|
||||
foreach (var clientInterface in clientInterfaces)
|
||||
{
|
||||
var implementation = assembly.GetTypes().Single(t => clientInterface.IsAssignableFrom(t) && t != clientInterface);
|
||||
int methods = 0;
|
||||
foreach (var method in implementation.GetMethods().Where(m => implementationTypes.IsAssignableFrom(m.ReturnType)))
|
||||
var implementations = assembly.GetTypes().Where(t => clientInterface.IsAssignableFrom(t) && t != clientInterface);
|
||||
foreach (var implementation in implementations)
|
||||
{
|
||||
var interfaceMethod = clientInterface.GetMethod(method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray());
|
||||
if (interfaceMethod == null)
|
||||
throw new Exception($"Missing interface for method {method.Name} in {implementation.Name} implementing interface {clientInterface.Name}");
|
||||
methods++;
|
||||
}
|
||||
int methods = 0;
|
||||
foreach (var method in implementation.GetMethods().Where(m => implementationTypes.IsAssignableFrom(m.ReturnType)))
|
||||
{
|
||||
var interfaceMethod = clientInterface.GetMethod(method.Name, method.GetParameters().Select(p => p.ParameterType).ToArray());
|
||||
if (interfaceMethod == null)
|
||||
throw new Exception($"Missing interface for method {method.Name} in {implementation.Name} implementing interface {clientInterface.Name}");
|
||||
methods++;
|
||||
}
|
||||
|
||||
Debug.WriteLine($"{clientInterface.Name} {methods} methods validated");
|
||||
Debug.WriteLine($"{clientInterface.Name} {methods} methods validated");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Compare value
|
||||
/// </summary>
|
||||
public record CompareValue
|
||||
{
|
||||
/// <summary>
|
||||
/// The value difference
|
||||
/// </summary>
|
||||
public decimal? Difference { get; set; }
|
||||
/// <summary>
|
||||
/// The value difference percentage
|
||||
/// </summary>
|
||||
public decimal? PercentageDifference { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public CompareValue(decimal? value1, decimal? value2)
|
||||
{
|
||||
if (value1 == null || value2 == null)
|
||||
return;
|
||||
|
||||
Difference = value2 - value1;
|
||||
PercentageDifference = value1.Value == 0 ? null : Math.Round(value2.Value / value1.Value * 100 - 100, 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.Klines
|
||||
{
|
||||
/// <summary>
|
||||
/// A tracker for kline data of a symbol
|
||||
/// </summary>
|
||||
public interface IKlineTracker
|
||||
{
|
||||
/// <summary>
|
||||
/// The total number of klines
|
||||
/// </summary>
|
||||
int Count { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Exchange name
|
||||
/// </summary>
|
||||
string Exchange { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol name
|
||||
/// </summary>
|
||||
string SymbolName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol
|
||||
/// </summary>
|
||||
SharedSymbol Symbol { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The max number of klines tracked
|
||||
/// </summary>
|
||||
int? Limit { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The max age of the data tracked
|
||||
/// </summary>
|
||||
TimeSpan? Period { get; }
|
||||
|
||||
/// <summary>
|
||||
/// From which timestamp the trades are registered
|
||||
/// </summary>
|
||||
DateTime? SyncedFrom { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sync status
|
||||
/// </summary>
|
||||
SyncStatus Status { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the last kline
|
||||
/// </summary>
|
||||
SharedKline? Last { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Event for when a new kline is added
|
||||
/// </summary>
|
||||
event Func<SharedKline, Task>? OnAdded;
|
||||
/// <summary>
|
||||
/// Event for when a kline is removed because it's no longer within the period/limit window
|
||||
/// </summary>
|
||||
event Func<SharedKline, Task>? OnRemoved;
|
||||
/// <summary>
|
||||
/// Event for when a kline is updated
|
||||
/// </summary>
|
||||
event Func<SharedKline, Task> OnUpdated;
|
||||
/// <summary>
|
||||
/// Event for when the sync status changes
|
||||
/// </summary>
|
||||
event Func<SyncStatus, SyncStatus, Task>? OnStatusChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Start synchronization
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<CallResult> StartAsync(bool startWithSnapshot = true);
|
||||
|
||||
/// <summary>
|
||||
/// Stop synchronization
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task StopAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Get the data tracked
|
||||
/// </summary>
|
||||
/// <param name="fromTimestamp">Start timestamp to get the data from, defaults to tracked data start time</param>
|
||||
/// <param name="toTimestamp">End timestamp to get the data until, defaults to current time</param>
|
||||
/// <returns></returns>
|
||||
IEnumerable<SharedKline> GetData(DateTime? fromTimestamp = null, DateTime? toTimestamp = null);
|
||||
|
||||
/// <summary>
|
||||
/// Get statitistics on the klines
|
||||
/// </summary>
|
||||
/// <param name="fromTimestamp">Start timestamp to get the data from, defaults to tracked data start time</param>
|
||||
/// <param name="toTimestamp">End timestamp to get the data until, defaults to current time</param>
|
||||
/// <returns></returns>
|
||||
KlinesStats GetStats(DateTime? fromTimestamp = null, DateTime? toTimestamp = null);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,481 @@
|
||||
using CryptoExchange.Net.Logging.Extensions;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.Klines
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class KlineTracker : IKlineTracker
|
||||
{
|
||||
private readonly IKlineSocketClient _socketClient;
|
||||
private readonly IKlineRestClient _restClient;
|
||||
private SyncStatus _status;
|
||||
private bool _startWithSnapshot;
|
||||
|
||||
/// <summary>
|
||||
/// The internal data structure
|
||||
/// </summary>
|
||||
protected readonly SortedDictionary<DateTime, SharedKline> _data = new SortedDictionary<DateTime, SharedKline>();
|
||||
/// <summary>
|
||||
/// The pre-snapshot queue buffering updates received before the snapshot is set and which will be applied after the snapshot was set
|
||||
/// </summary>
|
||||
protected readonly List<SharedKline> _preSnapshotQueue = new List<SharedKline>();
|
||||
/// <summary>
|
||||
/// Lock for accessing _data
|
||||
/// </summary>
|
||||
protected readonly object _lock = new object();
|
||||
/// <summary>
|
||||
/// The last time the window was applied
|
||||
/// </summary>
|
||||
protected DateTime _lastWindowApplied = DateTime.MinValue;
|
||||
/// <summary>
|
||||
/// Whether or not the data has changed since last window was applied
|
||||
/// </summary>
|
||||
protected bool _changed = false;
|
||||
/// <summary>
|
||||
/// The kline interval
|
||||
/// </summary>
|
||||
protected readonly SharedKlineInterval _interval;
|
||||
/// <summary>
|
||||
/// Whether the snapshot has been set
|
||||
/// </summary>
|
||||
protected bool _snapshotSet;
|
||||
/// <summary>
|
||||
/// Logger
|
||||
/// </summary>
|
||||
protected readonly ILogger _logger;
|
||||
/// <summary>
|
||||
/// Update subscription
|
||||
/// </summary>
|
||||
protected UpdateSubscription? _updateSubscription;
|
||||
|
||||
/// <summary>
|
||||
/// The timestamp of the first item
|
||||
/// </summary>
|
||||
protected DateTime? _firstTimestamp;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SyncStatus Status
|
||||
{
|
||||
get => _status;
|
||||
set
|
||||
{
|
||||
if (value == _status)
|
||||
return;
|
||||
|
||||
var old = _status;
|
||||
_status = value;
|
||||
_logger.KlineTrackerStatusChanged(SymbolName, old, value);
|
||||
OnStatusChanged?.Invoke(old, _status);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Exchange { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string SymbolName { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public SharedSymbol Symbol { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int? Limit { get; }
|
||||
/// <inheritdoc/>
|
||||
public TimeSpan? Period { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime? SyncedFrom
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Period == null)
|
||||
return _firstTimestamp;
|
||||
|
||||
var max = DateTime.UtcNow - Period.Value;
|
||||
if (_firstTimestamp > max)
|
||||
return _firstTimestamp;
|
||||
|
||||
return max;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
ApplyWindow(true);
|
||||
return _data.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public SharedKline? Last
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
ApplyWindow(true);
|
||||
return _data.LastOrDefault().Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Func<SharedKline, Task>? OnAdded;
|
||||
/// <inheritdoc />
|
||||
public event Func<SharedKline, Task>? OnUpdated;
|
||||
/// <inheritdoc />
|
||||
public event Func<SharedKline, Task>? OnRemoved;
|
||||
/// <inheritdoc />
|
||||
public event Func<SyncStatus, SyncStatus, Task>? OnStatusChanged;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public KlineTracker(
|
||||
ILogger? logger,
|
||||
IKlineRestClient restClient,
|
||||
IKlineSocketClient socketClient,
|
||||
SharedSymbol symbol,
|
||||
SharedKlineInterval interval,
|
||||
int? limit = null,
|
||||
TimeSpan? period = null)
|
||||
{
|
||||
_logger = logger ?? new NullLogger<KlineTracker>();
|
||||
Symbol = symbol;
|
||||
SymbolName = socketClient.FormatSymbol(symbol.BaseAsset, symbol.QuoteAsset, symbol.TradingMode, symbol.DeliverTime);
|
||||
Exchange = restClient.Exchange;
|
||||
Limit = limit;
|
||||
Period = period;
|
||||
_interval = interval;
|
||||
_socketClient = socketClient;
|
||||
_restClient = restClient;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CallResult> StartAsync(bool startWithSnapshot = true)
|
||||
{
|
||||
if (Status != SyncStatus.Disconnected)
|
||||
throw new InvalidOperationException($"Can't start syncing unless state is {SyncStatus.Disconnected}. Current state: {Status}");
|
||||
|
||||
_startWithSnapshot = startWithSnapshot;
|
||||
Status = SyncStatus.Syncing;
|
||||
_logger.KlineTrackerStarting(SymbolName);
|
||||
|
||||
var startResult = await DoStartAsync().ConfigureAwait(false);
|
||||
if (!startResult)
|
||||
{
|
||||
_logger.KlineTrackerStartFailed(SymbolName, startResult.Error!.ToString());
|
||||
Status = SyncStatus.Disconnected;
|
||||
return new CallResult(startResult.Error!);
|
||||
}
|
||||
|
||||
_updateSubscription = startResult.Data;
|
||||
_updateSubscription.ConnectionLost += HandleConnectionLost;
|
||||
_updateSubscription.ConnectionClosed += HandleConnectionClosed;
|
||||
_updateSubscription.ConnectionRestored += HandleConnectionRestored;
|
||||
Status = SyncStatus.Synced;
|
||||
_logger.KlineTrackerStarted(SymbolName);
|
||||
return new CallResult(null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StopAsync()
|
||||
{
|
||||
_logger.KlineTrackerStopping(SymbolName);
|
||||
Status = SyncStatus.Disconnected;
|
||||
await DoStopAsync().ConfigureAwait(false);
|
||||
_data.Clear();
|
||||
_preSnapshotQueue.Clear();
|
||||
_logger.KlineTrackerStopped(SymbolName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The start procedure needed for kline syncing, generally subscribing to an update stream and requesting the snapshot
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<UpdateSubscription>> DoStartAsync()
|
||||
{
|
||||
var subResult = await _socketClient.SubscribeToKlineUpdatesAsync(new SubscribeKlineRequest(Symbol, _interval),
|
||||
update =>
|
||||
{
|
||||
AddOrUpdate(update.Data);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (!subResult)
|
||||
{
|
||||
Status = SyncStatus.Disconnected;
|
||||
return subResult;
|
||||
}
|
||||
|
||||
if (!_startWithSnapshot)
|
||||
return subResult;
|
||||
|
||||
var startTime = Period == null ? (DateTime?)null : DateTime.UtcNow.Add(-Period.Value);
|
||||
if (_restClient.GetKlinesOptions.MaxAge != null && DateTime.UtcNow.Add(-_restClient.GetKlinesOptions.MaxAge.Value) > startTime)
|
||||
startTime = DateTime.UtcNow.Add(-_restClient.GetKlinesOptions.MaxAge.Value);
|
||||
|
||||
var limit = Math.Min(_restClient.GetKlinesOptions.MaxLimit, Limit ?? 100);
|
||||
|
||||
var request = new GetKlinesRequest(Symbol, _interval, startTime, DateTime.UtcNow, limit: limit);
|
||||
var data = new List<SharedKline>();
|
||||
await foreach (var result in ExchangeHelpers.ExecutePages(_restClient.GetKlinesAsync, request).ConfigureAwait(false))
|
||||
{
|
||||
if (!result)
|
||||
{
|
||||
_ = subResult.Data.CloseAsync();
|
||||
Status = SyncStatus.Disconnected;
|
||||
return subResult.AsError<UpdateSubscription>(result.Error!);
|
||||
}
|
||||
|
||||
if (Limit != null && data.Count > Limit)
|
||||
break;
|
||||
|
||||
data.AddRange(result.Data);
|
||||
}
|
||||
|
||||
SetInitialData(data);
|
||||
return subResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The stop procedure needed, generally stopping the update stream
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual Task DoStopAsync() => _updateSubscription?.CloseAsync() ?? Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public KlinesStats GetStats(DateTime? fromTimestamp = null, DateTime? toTimestamp = null)
|
||||
{
|
||||
var compareTime = SyncedFrom?.AddSeconds(-2);
|
||||
var stats = GetStats(GetData(fromTimestamp, toTimestamp));
|
||||
stats.Complete = (fromTimestamp == null || fromTimestamp >= compareTime) && (toTimestamp == null || toTimestamp >= compareTime);
|
||||
return stats;
|
||||
}
|
||||
|
||||
private KlinesStats GetStats(IEnumerable<SharedKline> klines)
|
||||
{
|
||||
if (!klines.Any())
|
||||
return new KlinesStats();
|
||||
|
||||
return new KlinesStats
|
||||
{
|
||||
KlineCount = klines.Count(),
|
||||
FirstOpenTime = klines.First().OpenTime,
|
||||
LastOpenTime = klines.Last().OpenTime,
|
||||
HighPrice = klines.Select(d => d.LowPrice).Max(),
|
||||
LowPrice = klines.Select(d => d.HighPrice).Min(),
|
||||
Volume = klines.Select(d => d.Volume).Sum(),
|
||||
AverageVolume = Math.Round(klines.OrderByDescending(d => d.OpenTime).Skip(1).Select(d => d.Volume).DefaultIfEmpty().Average(), 8)
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<SharedKline> GetData(DateTime? since = null, DateTime? until = null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
ApplyWindow(true);
|
||||
|
||||
IEnumerable<SharedKline> result = _data.Values;
|
||||
if (since != null)
|
||||
result = result.Where(d => d.OpenTime >= since);
|
||||
if (until != null)
|
||||
result = result.Where(d => d.OpenTime <= until);
|
||||
|
||||
return result.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the initial kline data snapshot
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
protected void SetInitialData(IEnumerable<SharedKline> data)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_data.Clear();
|
||||
|
||||
IEnumerable<SharedKline> items = data.OrderByDescending(d => d.OpenTime);
|
||||
if (Limit != null)
|
||||
items = items.Take(Limit.Value);
|
||||
if (Period != null)
|
||||
items = items.Where(e => e.OpenTime >= DateTime.UtcNow.Add(-Period.Value));
|
||||
|
||||
foreach (var item in items.OrderBy(d => d.OpenTime))
|
||||
_data.Add(item.OpenTime, item);
|
||||
|
||||
_snapshotSet = true;
|
||||
|
||||
foreach (var item in _preSnapshotQueue)
|
||||
{
|
||||
if (_data.ContainsKey(item.OpenTime))
|
||||
continue;
|
||||
|
||||
_data.Add(item.OpenTime, item);
|
||||
}
|
||||
|
||||
_firstTimestamp = _data.Min(v => v.Key);
|
||||
ApplyWindow(false);
|
||||
_logger.KlineTrackerInitialDataSet(SymbolName, _data.Last().Key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add or update a kline
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
protected void AddOrUpdate(SharedKline item) => AddOrUpdate(new[] { item });
|
||||
|
||||
/// <summary>
|
||||
/// Add or update klines
|
||||
/// </summary>
|
||||
/// <param name="items"></param>
|
||||
protected void AddOrUpdate(IEnumerable<SharedKline> items)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_restClient != null && _startWithSnapshot && !_snapshotSet)
|
||||
{
|
||||
_preSnapshotQueue.AddRange(items);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (_data.TryGetValue(item.OpenTime, out var existing))
|
||||
{
|
||||
_data.Remove(item.OpenTime);
|
||||
_data.Add(item.OpenTime, item);
|
||||
OnUpdated?.Invoke(item);
|
||||
_logger.KlineTrackerKlineUpdated(SymbolName, _data.Last().Key);
|
||||
}
|
||||
else
|
||||
{
|
||||
_data.Add(item.OpenTime, item);
|
||||
OnAdded?.Invoke(item);
|
||||
_logger.KlineTrackerKlineAdded(SymbolName, _data.Last().Key);
|
||||
}
|
||||
}
|
||||
|
||||
_firstTimestamp = _data.Min(x => x.Key);
|
||||
_changed = true;
|
||||
|
||||
SetSyncStatus();
|
||||
ApplyWindow(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyWindow(bool broadcastEvents)
|
||||
{
|
||||
if (!_changed && (DateTime.UtcNow - _lastWindowApplied) < TimeSpan.FromSeconds(1))
|
||||
return;
|
||||
|
||||
if (Period != null)
|
||||
{
|
||||
var compareDate = DateTime.UtcNow.Add(-Period.Value);
|
||||
for (var i = 0; i < _data.Count; i++)
|
||||
{
|
||||
var item = _data.ElementAt(0);
|
||||
if (item.Key >= compareDate)
|
||||
break;
|
||||
|
||||
_data.Remove(item.Key);
|
||||
if (broadcastEvents)
|
||||
OnRemoved?.Invoke(item.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (Limit != null && _data.Count > Limit.Value)
|
||||
{
|
||||
var toRemove = Math.Max(0, _data.Count - Limit.Value);
|
||||
for (var i = 0; i < toRemove; i++)
|
||||
{
|
||||
var item = _data.ElementAt(0);
|
||||
_data.Remove(item.Key);
|
||||
if (broadcastEvents)
|
||||
OnRemoved?.Invoke(item.Value);
|
||||
}
|
||||
}
|
||||
|
||||
_lastWindowApplied = DateTime.UtcNow;
|
||||
_changed = false;
|
||||
}
|
||||
|
||||
private void HandleConnectionLost()
|
||||
{
|
||||
_logger.KlineTrackerConnectionLost(SymbolName);
|
||||
if (Status != SyncStatus.Disconnected)
|
||||
{
|
||||
Status = SyncStatus.Syncing;
|
||||
_snapshotSet = false;
|
||||
_firstTimestamp = null;
|
||||
_preSnapshotQueue.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleConnectionClosed()
|
||||
{
|
||||
_logger.KlineTrackerConnectionClosed(SymbolName);
|
||||
Status = SyncStatus.Disconnected;
|
||||
_ = StopAsync();
|
||||
}
|
||||
|
||||
private async void HandleConnectionRestored(TimeSpan _)
|
||||
{
|
||||
Status = SyncStatus.Syncing;
|
||||
var success = false;
|
||||
while (!success)
|
||||
{
|
||||
if (Status != SyncStatus.Syncing)
|
||||
return;
|
||||
|
||||
var resyncResult = await DoStartAsync().ConfigureAwait(false);
|
||||
success = resyncResult;
|
||||
}
|
||||
|
||||
_logger.KlineTrackerConnectionRestored(SymbolName);
|
||||
SetSyncStatus();
|
||||
}
|
||||
|
||||
private void SetSyncStatus()
|
||||
{
|
||||
if (Status == SyncStatus.Synced)
|
||||
return;
|
||||
|
||||
if (Period != null)
|
||||
{
|
||||
if (_firstTimestamp <= DateTime.UtcNow - Period.Value)
|
||||
Status = SyncStatus.Synced;
|
||||
else
|
||||
Status = SyncStatus.PartiallySynced;
|
||||
}
|
||||
|
||||
if (Limit != null)
|
||||
{
|
||||
if (_data.Count == Limit.Value)
|
||||
Status = SyncStatus.Synced;
|
||||
else
|
||||
Status = SyncStatus.PartiallySynced;
|
||||
}
|
||||
|
||||
if (Period == null && Limit == null)
|
||||
Status = SyncStatus.Synced;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.Klines
|
||||
{
|
||||
/// <summary>
|
||||
/// Klines statistics comparison
|
||||
/// </summary>
|
||||
public record KlinesCompare
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of trades
|
||||
/// </summary>
|
||||
public CompareValue? LowPriceDif { get; set; }
|
||||
/// <summary>
|
||||
/// Number of trades
|
||||
/// </summary>
|
||||
public CompareValue? HighPriceDif { get; set; }
|
||||
/// <summary>
|
||||
/// Number of trades
|
||||
/// </summary>
|
||||
public CompareValue? VolumeDif { get; set; }
|
||||
/// <summary>
|
||||
/// Number of trades
|
||||
/// </summary>
|
||||
public CompareValue? AverageVolumeDif { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.Klines
|
||||
{
|
||||
/// <summary>
|
||||
/// Klines statistics
|
||||
/// </summary>
|
||||
public record KlinesStats
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of klines
|
||||
/// </summary>
|
||||
public int KlineCount { get; set; }
|
||||
/// <summary>
|
||||
/// The kline open time of the first entry
|
||||
/// </summary>
|
||||
public DateTime? FirstOpenTime { get; set; }
|
||||
/// <summary>
|
||||
/// The kline open time of the last entry
|
||||
/// </summary>
|
||||
public DateTime? LastOpenTime { get; set; }
|
||||
/// <summary>
|
||||
/// Lowest trade price
|
||||
/// </summary>
|
||||
public decimal? LowPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Highest trade price
|
||||
/// </summary>
|
||||
public decimal? HighPrice { get; set; }
|
||||
/// <summary>
|
||||
/// Trade volume
|
||||
/// </summary>
|
||||
public decimal Volume { get; set; }
|
||||
/// <summary>
|
||||
/// Average volume per kline
|
||||
/// </summary>
|
||||
public decimal? AverageVolume { get; set; }
|
||||
/// <summary>
|
||||
/// Whether the data is complete
|
||||
/// </summary>
|
||||
public bool Complete { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Compare 2 stat snapshots to eachother
|
||||
/// </summary>
|
||||
public KlinesCompare CompareTo(KlinesStats otherStats)
|
||||
{
|
||||
return new KlinesCompare
|
||||
{
|
||||
LowPriceDif = new CompareValue(LowPrice, otherStats.LowPrice),
|
||||
HighPriceDif = new CompareValue(HighPrice, otherStats.HighPrice),
|
||||
VolumeDif = new CompareValue(Volume, otherStats.Volume),
|
||||
AverageVolumeDif = new CompareValue(AverageVolume, otherStats.AverageVolume),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.Trades
|
||||
{
|
||||
/// <summary>
|
||||
/// A tracker for trades on a symbol
|
||||
/// </summary>
|
||||
public interface ITradeTracker
|
||||
{
|
||||
/// <summary>
|
||||
/// The total number of trades
|
||||
/// </summary>
|
||||
int Count { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Exchange name
|
||||
/// </summary>
|
||||
string Exchange { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol name
|
||||
/// </summary>
|
||||
string SymbolName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Symbol
|
||||
/// </summary>
|
||||
SharedSymbol Symbol { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The max number of trades tracked
|
||||
/// </summary>
|
||||
int? Limit { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The max age of the data tracked
|
||||
/// </summary>
|
||||
TimeSpan? Period { get; }
|
||||
|
||||
/// <summary>
|
||||
/// From which timestamp the trades are registered
|
||||
/// </summary>
|
||||
DateTime? SyncedFrom { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The current synchronization status
|
||||
/// </summary>
|
||||
SyncStatus Status { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the last trade
|
||||
/// </summary>
|
||||
SharedTrade? Last { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Event for when a new trade is added
|
||||
/// </summary>
|
||||
event Func<SharedTrade, Task>? OnAdded;
|
||||
/// <summary>
|
||||
/// Event for when a trade is removed because it's no longer within the period/limit window
|
||||
/// </summary>
|
||||
event Func<SharedTrade, Task>? OnRemoved;
|
||||
/// <summary>
|
||||
/// Event for when the sync status changes
|
||||
/// </summary>
|
||||
event Func<SyncStatus, SyncStatus, Task>? OnStatusChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Start synchronization
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<CallResult> StartAsync(bool startWithSnapshot = true);
|
||||
|
||||
/// <summary>
|
||||
/// Stop synchronization
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task StopAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Get the data tracked
|
||||
/// </summary>
|
||||
/// <param name="fromTimestamp">Start timestamp to get the data from, defaults to tracked data start time</param>
|
||||
/// <param name="toTimestamp">End timestamp to get the data until, defaults to current time</param>
|
||||
/// <returns></returns>
|
||||
IEnumerable<SharedTrade> GetData(DateTime? fromTimestamp = null, DateTime? toTimestamp = null);
|
||||
|
||||
/// <summary>
|
||||
/// Get statitistics on the trades
|
||||
/// </summary>
|
||||
/// <param name="fromTimestamp">Start timestamp to get the data from, defaults to tracked data start time</param>
|
||||
/// <param name="toTimestamp">End timestamp to get the data until, defaults to current time</param>
|
||||
/// <returns></returns>
|
||||
TradesStats GetStats(DateTime? fromTimestamp = null, DateTime? toTimestamp = null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
using CryptoExchange.Net.Logging.Extensions;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.Trades
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class TradeTracker : ITradeTracker
|
||||
{
|
||||
private readonly ITradeSocketClient _socketClient;
|
||||
private readonly IRecentTradeRestClient? _recentRestClient;
|
||||
private readonly ITradeHistoryRestClient? _historyRestClient;
|
||||
private SyncStatus _status;
|
||||
private long _snapshotId;
|
||||
private bool _startWithSnapshot;
|
||||
|
||||
/// <summary>
|
||||
/// The internal data structure
|
||||
/// </summary>
|
||||
protected readonly List<SharedTrade> _data = new List<SharedTrade>();
|
||||
/// <summary>
|
||||
/// The pre-snapshot queue buffering updates received before the snapshot is set and which will be applied after the snapshot was set
|
||||
/// </summary>
|
||||
protected readonly List<SharedTrade> _preSnapshotQueue = new List<SharedTrade>();
|
||||
|
||||
/// <summary>
|
||||
/// The last time the window was applied
|
||||
/// </summary>
|
||||
protected DateTime _lastWindowApplied = DateTime.MinValue;
|
||||
/// <summary>
|
||||
/// Whether or not the data has changed since last window was applied
|
||||
/// </summary>
|
||||
protected bool _changed = false;
|
||||
/// <summary>
|
||||
/// Lock for accessing _data
|
||||
/// </summary>
|
||||
protected readonly object _lock = new object();
|
||||
/// <summary>
|
||||
/// Whether the snapshot has been set
|
||||
/// </summary>
|
||||
protected bool _snapshotSet;
|
||||
/// <summary>
|
||||
/// Logger
|
||||
/// </summary>
|
||||
protected readonly ILogger _logger;
|
||||
/// <summary>
|
||||
/// Update subscription
|
||||
/// </summary>
|
||||
protected UpdateSubscription? _updateSubscription;
|
||||
|
||||
/// <summary>
|
||||
/// The timestamp of the first item
|
||||
/// </summary>
|
||||
protected DateTime? _firstTimestamp;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Exchange { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string SymbolName { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public SharedSymbol Symbol { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int? Limit { get; }
|
||||
/// <inheritdoc/>
|
||||
public TimeSpan? Period { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SyncStatus Status
|
||||
{
|
||||
get => _status;
|
||||
set
|
||||
{
|
||||
if (value == _status)
|
||||
return;
|
||||
|
||||
var old = _status;
|
||||
_status = value;
|
||||
_logger.TradeTrackerStatusChanged(SymbolName, old, value);
|
||||
OnStatusChanged?.Invoke(old, _status);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
ApplyWindow(true);
|
||||
return _data.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public DateTime? SyncedFrom
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Period == null)
|
||||
return _firstTimestamp;
|
||||
|
||||
var max = DateTime.UtcNow - Period.Value;
|
||||
if (_firstTimestamp > max)
|
||||
return _firstTimestamp;
|
||||
|
||||
return max;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public SharedTrade? Last
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
ApplyWindow(true);
|
||||
return _data.LastOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Func<SharedTrade, Task>? OnAdded;
|
||||
/// <inheritdoc />
|
||||
public event Func<SharedTrade, Task>? OnRemoved;
|
||||
/// <inheritdoc />
|
||||
public event Func<SyncStatus, SyncStatus, Task>? OnStatusChanged;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public TradeTracker(
|
||||
ILogger? logger,
|
||||
IRecentTradeRestClient? recentRestClient,
|
||||
ITradeHistoryRestClient? historyRestClient,
|
||||
ITradeSocketClient socketClient,
|
||||
SharedSymbol symbol,
|
||||
int? limit = null,
|
||||
TimeSpan? period = null)
|
||||
{
|
||||
_logger = logger ?? new NullLogger<TradeTracker>();
|
||||
_recentRestClient = recentRestClient;
|
||||
_historyRestClient = historyRestClient;
|
||||
_socketClient = socketClient;
|
||||
Exchange = socketClient.Exchange;
|
||||
Symbol = symbol;
|
||||
SymbolName = socketClient.FormatSymbol(symbol.BaseAsset, symbol.QuoteAsset, symbol.TradingMode, symbol.DeliverTime);
|
||||
Limit = limit;
|
||||
Period = period;
|
||||
}
|
||||
|
||||
private TradesStats GetStats(IEnumerable<SharedTrade> trades)
|
||||
{
|
||||
if (!trades.Any())
|
||||
return new TradesStats();
|
||||
|
||||
return new TradesStats
|
||||
{
|
||||
TradeCount = trades.Count(),
|
||||
FirstTradeTime = trades.First().Timestamp,
|
||||
LastTradeTime = trades.Last().Timestamp,
|
||||
AveragePrice = Math.Round(trades.Select(d => d.Price).DefaultIfEmpty().Average(), 8),
|
||||
VolumeWeightedAveragePrice = trades.Any() ? Math.Round(trades.Select(d => d.Price * d.Quantity).DefaultIfEmpty().Sum() / trades.Select(d => d.Quantity).DefaultIfEmpty().Sum(), 8) : null,
|
||||
Volume = Math.Round(trades.Sum(d => d.Quantity), 8),
|
||||
QuoteVolume = Math.Round(trades.Sum(d => d.Quantity * d.Price), 8),
|
||||
BuySellRatio = Math.Round(trades.Where(x => x.Side == SharedOrderSide.Buy).Sum(x => x.Quantity) / trades.Sum(x => x.Quantity), 8)
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public TradesStats GetStats(DateTime? fromTimestamp = null, DateTime? toTimestamp = null)
|
||||
{
|
||||
var compareTime = SyncedFrom?.AddSeconds(-2);
|
||||
var stats = GetStats(GetData(fromTimestamp, toTimestamp));
|
||||
stats.Complete = (fromTimestamp == null || fromTimestamp >= compareTime) && (toTimestamp == null || toTimestamp >= compareTime);
|
||||
return stats;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CallResult> StartAsync(bool startWithSnapshot = true)
|
||||
{
|
||||
if (Status != SyncStatus.Disconnected)
|
||||
throw new InvalidOperationException($"Can't start syncing unless state is {SyncStatus.Disconnected}. Current state: {Status}");
|
||||
|
||||
_startWithSnapshot = startWithSnapshot;
|
||||
Status = SyncStatus.Syncing;
|
||||
_logger.TradeTrackerStarting(SymbolName);
|
||||
var subResult = await DoStartAsync().ConfigureAwait(false);
|
||||
if (!subResult)
|
||||
{
|
||||
_logger.TradeTrackerStartFailed(SymbolName, subResult.Error!.ToString());
|
||||
Status = SyncStatus.Disconnected;
|
||||
return subResult;
|
||||
}
|
||||
|
||||
_updateSubscription = subResult.Data;
|
||||
_updateSubscription.ConnectionLost += HandleConnectionLost;
|
||||
_updateSubscription.ConnectionClosed += HandleConnectionClosed;
|
||||
_updateSubscription.ConnectionRestored += HandleConnectionRestored;
|
||||
SetSyncStatus();
|
||||
_logger.TradeTrackerStarted(SymbolName);
|
||||
return new CallResult(null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task StopAsync()
|
||||
{
|
||||
_logger.TradeTrackerStopping(SymbolName);
|
||||
Status = SyncStatus.Disconnected;
|
||||
await DoStopAsync().ConfigureAwait(false);
|
||||
_data.Clear();
|
||||
_preSnapshotQueue.Clear();
|
||||
_logger.TradeTrackerStopped(SymbolName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The start procedure needed for trade syncing, generally subscribing to an update stream and requesting the snapshot
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<CallResult<UpdateSubscription>> DoStartAsync()
|
||||
{
|
||||
var subResult = await _socketClient.SubscribeToTradeUpdatesAsync(new SubscribeTradeRequest(Symbol),
|
||||
update =>
|
||||
{
|
||||
AddData(update.Data);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (!subResult)
|
||||
{
|
||||
Status = SyncStatus.Disconnected;
|
||||
return subResult;
|
||||
}
|
||||
|
||||
if (!_startWithSnapshot)
|
||||
return subResult;
|
||||
|
||||
if (_historyRestClient != null)
|
||||
{
|
||||
var startTime = Period == null ? DateTime.UtcNow.AddMinutes(-5) : DateTime.UtcNow.Add(-Period.Value);
|
||||
var request = new GetTradeHistoryRequest(Symbol, startTime, DateTime.UtcNow);
|
||||
var data = new List<SharedTrade>();
|
||||
await foreach(var result in ExchangeHelpers.ExecutePages(_historyRestClient.GetTradeHistoryAsync, request).ConfigureAwait(false))
|
||||
{
|
||||
if (!result)
|
||||
{
|
||||
_ = subResult.Data.CloseAsync();
|
||||
Status = SyncStatus.Disconnected;
|
||||
return subResult.AsError<UpdateSubscription>(result.Error!);
|
||||
}
|
||||
|
||||
if (Limit != null && data.Count > Limit)
|
||||
break;
|
||||
|
||||
data.AddRange(result.Data);
|
||||
}
|
||||
|
||||
SetInitialData(data);
|
||||
}
|
||||
else if (_recentRestClient != null)
|
||||
{
|
||||
int? limit = null;
|
||||
if (Limit.HasValue)
|
||||
limit = Math.Min(_recentRestClient.GetRecentTradesOptions.MaxLimit, Limit.Value);
|
||||
|
||||
var snapshot = await _recentRestClient.GetRecentTradesAsync(new GetRecentTradesRequest(Symbol, limit)).ConfigureAwait(false);
|
||||
if (!snapshot)
|
||||
{
|
||||
_ = subResult.Data.CloseAsync();
|
||||
Status = SyncStatus.Disconnected;
|
||||
return subResult.AsError<UpdateSubscription>(snapshot.Error!);
|
||||
}
|
||||
|
||||
SetInitialData(snapshot.Data);
|
||||
}
|
||||
|
||||
return subResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The stop procedure needed, generally stopping the update stream
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual Task DoStopAsync() => _updateSubscription?.CloseAsync() ?? Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<SharedTrade> GetData(DateTime? since = null, DateTime? until = null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
ApplyWindow(true);
|
||||
|
||||
IEnumerable<SharedTrade> result = _data;
|
||||
if (since != null)
|
||||
result = result.Where(d => d.Timestamp >= since);
|
||||
if (until != null)
|
||||
result = result.Where(d => d.Timestamp <= until);
|
||||
|
||||
return result.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the initial trade data snapshot
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
protected void SetInitialData(IEnumerable<SharedTrade> data)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_data.Clear();
|
||||
|
||||
IEnumerable<SharedTrade> items = data.OrderByDescending(d => d.Timestamp);
|
||||
if (Limit != null)
|
||||
items = items.Take(Limit.Value);
|
||||
if (Period != null)
|
||||
items = items.Where(e => e.Timestamp >= DateTime.UtcNow.Add(-Period.Value));
|
||||
|
||||
_snapshotId = data.Max(d => d.Timestamp.Ticks);
|
||||
foreach (var item in items.OrderBy(d => d.Timestamp))
|
||||
_data.Add(item);
|
||||
|
||||
_snapshotSet = true;
|
||||
_changed = true;
|
||||
|
||||
_logger.TradeTrackerInitialDataSet(SymbolName, _data.Count, _snapshotId);
|
||||
|
||||
foreach (var item in _preSnapshotQueue)
|
||||
{
|
||||
if (_snapshotId >= item.Timestamp.Ticks)
|
||||
{
|
||||
_logger.TradeTrackerPreSnapshotSkip(SymbolName, item.Timestamp.Ticks);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.TradeTrackerPreSnapshotApplied(SymbolName, item.Timestamp.Ticks);
|
||||
_data.Add(item);
|
||||
}
|
||||
|
||||
_firstTimestamp = _data.Min(v => v.Timestamp);
|
||||
|
||||
ApplyWindow(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a trade
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
protected void AddData(SharedTrade item) => AddData(new[] { item });
|
||||
|
||||
/// <summary>
|
||||
/// Add a list of trades
|
||||
/// </summary>
|
||||
/// <param name="items"></param>
|
||||
protected void AddData(IEnumerable<SharedTrade> items)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if ((_recentRestClient != null || _historyRestClient != null) && _startWithSnapshot && !_snapshotSet)
|
||||
{
|
||||
_preSnapshotQueue.AddRange(items);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
_logger.TradeTrackerTradeAdded(SymbolName, item.Timestamp.Ticks);
|
||||
_data.Add(item);
|
||||
OnAdded?.Invoke(item);
|
||||
}
|
||||
|
||||
_firstTimestamp = _data.Min(x => x.Timestamp);
|
||||
_changed = true;
|
||||
SetSyncStatus();
|
||||
ApplyWindow(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyWindow(bool broadcastEvents)
|
||||
{
|
||||
if (!_changed && (DateTime.UtcNow - _lastWindowApplied) < TimeSpan.FromSeconds(1))
|
||||
return;
|
||||
|
||||
if (Period != null)
|
||||
{
|
||||
var compareDate = DateTime.UtcNow.Add(-Period.Value);
|
||||
for(var i = 0; i < _data.Count; i++)
|
||||
{
|
||||
var item = _data[0];
|
||||
if (item.Timestamp >= compareDate)
|
||||
break;
|
||||
|
||||
_data.Remove(item);
|
||||
if (broadcastEvents)
|
||||
OnRemoved?.Invoke(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (Limit != null && _data.Count > Limit.Value)
|
||||
{
|
||||
var toRemove = _data.Count - Limit.Value;
|
||||
for (var i = 0; i < toRemove; i++)
|
||||
{
|
||||
var item = _data[0];
|
||||
_data.Remove(item);
|
||||
if (broadcastEvents)
|
||||
OnRemoved?.Invoke(item);
|
||||
}
|
||||
}
|
||||
|
||||
_lastWindowApplied = DateTime.UtcNow;
|
||||
_changed = false;
|
||||
|
||||
if (Status == SyncStatus.PartiallySynced)
|
||||
// Need to check if sync status should be changed even if there may not be any new data
|
||||
SetSyncStatus();
|
||||
}
|
||||
|
||||
|
||||
private void HandleConnectionLost()
|
||||
{
|
||||
_logger.TradeTrackerConnectionLost(SymbolName);
|
||||
if (Status != SyncStatus.Disconnected)
|
||||
{
|
||||
Status = SyncStatus.Syncing;
|
||||
_snapshotSet = false;
|
||||
_firstTimestamp = null;
|
||||
_preSnapshotQueue.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleConnectionClosed()
|
||||
{
|
||||
_logger.TradeTrackerConnectionClosed(SymbolName);
|
||||
Status = SyncStatus.Disconnected;
|
||||
_ = StopAsync();
|
||||
}
|
||||
|
||||
private async void HandleConnectionRestored(TimeSpan _)
|
||||
{
|
||||
Status = SyncStatus.Syncing;
|
||||
var success = false;
|
||||
while (!success)
|
||||
{
|
||||
if (Status != SyncStatus.Syncing)
|
||||
return;
|
||||
|
||||
var resyncResult = await DoStartAsync().ConfigureAwait(false);
|
||||
success = resyncResult;
|
||||
}
|
||||
|
||||
_logger.TradeTrackerConnectionRestored(SymbolName);
|
||||
SetSyncStatus();
|
||||
}
|
||||
|
||||
private void SetSyncStatus()
|
||||
{
|
||||
if (Status == SyncStatus.Synced)
|
||||
return;
|
||||
|
||||
if (Period != null)
|
||||
{
|
||||
if (_firstTimestamp <= DateTime.UtcNow - Period.Value)
|
||||
Status = SyncStatus.Synced;
|
||||
else
|
||||
Status = SyncStatus.PartiallySynced;
|
||||
}
|
||||
|
||||
if (Limit != null)
|
||||
{
|
||||
if (_data.Count == Limit.Value)
|
||||
Status = SyncStatus.Synced;
|
||||
else
|
||||
Status = SyncStatus.PartiallySynced;
|
||||
}
|
||||
|
||||
if (Period == null && Limit == null)
|
||||
Status = SyncStatus.Synced;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.Trades
|
||||
{
|
||||
/// <summary>
|
||||
/// Trades statistics comparison
|
||||
/// </summary>
|
||||
public record TradesCompare
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of trades
|
||||
/// </summary>
|
||||
public CompareValue TradeCountDif { get; set; } = new CompareValue(null, null);
|
||||
/// <summary>
|
||||
/// Average trade price
|
||||
/// </summary>
|
||||
public CompareValue? AveragePriceDif { get; set; }
|
||||
/// <summary>
|
||||
/// Volume weighted average trade price
|
||||
/// </summary>
|
||||
public CompareValue? VolumeWeightedAveragePriceDif { get; set; }
|
||||
/// <summary>
|
||||
/// Volume of the trades
|
||||
/// </summary>
|
||||
public CompareValue VolumeDif { get; set; } = new CompareValue(null, null);
|
||||
/// <summary>
|
||||
/// Volume of the trades in quote asset
|
||||
/// </summary>
|
||||
public CompareValue QuoteVolumeDif { get; set; } = new CompareValue(null, null);
|
||||
/// <summary>
|
||||
/// The volume weighted Buy/Sell ratio. A 0.7 ratio means 70% of the trade volume was a buy.
|
||||
/// </summary>
|
||||
public CompareValue? BuySellRatioDif { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Trackers.Trades
|
||||
{
|
||||
/// <summary>
|
||||
/// Trades statistics
|
||||
/// </summary>
|
||||
public record TradesStats
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of trades
|
||||
/// </summary>
|
||||
public int TradeCount { get; set; }
|
||||
/// <summary>
|
||||
/// Timestamp of the last trade
|
||||
/// </summary>
|
||||
public DateTime? FirstTradeTime { get; set; }
|
||||
/// <summary>
|
||||
/// Timestamp of the first trade
|
||||
/// </summary>
|
||||
public DateTime? LastTradeTime { get; set; }
|
||||
/// <summary>
|
||||
/// Average trade price
|
||||
/// </summary>
|
||||
public decimal? AveragePrice { get; set; }
|
||||
/// <summary>
|
||||
/// Volume weighted average trade price
|
||||
/// </summary>
|
||||
public decimal? VolumeWeightedAveragePrice { get; set; }
|
||||
/// <summary>
|
||||
/// Volume of the trades
|
||||
/// </summary>
|
||||
public decimal Volume { get; set; }
|
||||
/// <summary>
|
||||
/// Volume of the trades in quote asset
|
||||
/// </summary>
|
||||
public decimal QuoteVolume { get; set; }
|
||||
/// <summary>
|
||||
/// The volume weighted Buy/Sell ratio. A 0.7 ratio means 70% of the trade volume was a buy.
|
||||
/// </summary>
|
||||
public decimal? BuySellRatio { get; set; }
|
||||
/// <summary>
|
||||
/// Whether the data is complete
|
||||
/// </summary>
|
||||
public bool Complete { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Compare 2 stat snapshots to eachother
|
||||
/// </summary>
|
||||
public TradesCompare CompareTo(TradesStats otherStats)
|
||||
{
|
||||
return new TradesCompare
|
||||
{
|
||||
TradeCountDif = new CompareValue(TradeCount, otherStats.TradeCount),
|
||||
AveragePriceDif = new CompareValue(AveragePrice, otherStats.AveragePrice),
|
||||
VolumeWeightedAveragePriceDif = new CompareValue(VolumeWeightedAveragePrice, otherStats.VolumeWeightedAveragePrice),
|
||||
VolumeDif = new CompareValue(Volume, otherStats.Volume),
|
||||
QuoteVolumeDif = new CompareValue(QuoteVolume, otherStats.QuoteVolume),
|
||||
BuySellRatioDif = new CompareValue(BuySellRatio, otherStats.BuySellRatio),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,17 +5,23 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="9.7.1" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="7.2.2" />
|
||||
<PackageReference Include="Bybit.Net" Version="3.7.1" />
|
||||
<PackageReference Include="CoinEx.Net" Version="6.2.1" />
|
||||
<PackageReference Include="Huobi.Net" Version="5.2.1" />
|
||||
<PackageReference Include="JK.BingX.Net" Version="1.0.0" />
|
||||
<PackageReference Include="JK.Bitget.Net" Version="1.3.1" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="1.7.1" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="4.4.3" />
|
||||
<PackageReference Include="Kucoin.Net" Version="5.3.2" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="6.0.0" />
|
||||
<PackageReference Include="Binance.Net" Version="10.9.0" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="7.10.0" />
|
||||
<PackageReference Include="BitMart.Net" Version="1.7.0" />
|
||||
<PackageReference Include="Bybit.Net" Version="3.16.0" />
|
||||
<PackageReference Include="CoinEx.Net" Version="7.9.0" />
|
||||
<PackageReference Include="CryptoCom.Net" Version="1.2.0" />
|
||||
<PackageReference Include="GateIo.Net" Version="1.12.0" />
|
||||
<PackageReference Include="JK.BingX.Net" Version="1.14.0" />
|
||||
<PackageReference Include="JK.Bitget.Net" Version="1.13.0" />
|
||||
<PackageReference Include="JK.Mexc.Net" Version="1.11.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="2.8.0" />
|
||||
<PackageReference Include="JKorf.Coinbase.Net" Version="1.4.0" />
|
||||
<PackageReference Include="JKorf.HTX.Net" Version="6.4.0" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="5.2.0" />
|
||||
<PackageReference Include="Kucoin.Net" Version="5.18.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
<PackageReference Include="WhiteBit.Net" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -2,13 +2,19 @@
|
||||
@inject IBinanceRestClient binanceClient
|
||||
@inject IBingXRestClient bingXClient
|
||||
@inject IBitfinexRestClient bitfinexClient
|
||||
@inject IBitMartRestClient bitmartClient
|
||||
@inject IBitgetRestClient bitgetClient
|
||||
@inject IBybitRestClient bybitClient
|
||||
@inject ICoinbaseRestClient coinbaseClient
|
||||
@inject ICoinExRestClient coinexClient
|
||||
@inject IHuobiRestClient huobiClient
|
||||
@inject ICryptoComRestClient cryptocomClient
|
||||
@inject IGateIoRestClient gateioClient
|
||||
@inject IHTXRestClient huobiClient
|
||||
@inject IKrakenRestClient krakenClient
|
||||
@inject IKucoinRestClient kucoinClient
|
||||
@inject IMexcRestClient mexcClient
|
||||
@inject IOKXRestClient okxClient
|
||||
@inject IWhiteBitRestClient whitebitClient
|
||||
|
||||
<h3>BTC-USD prices:</h3>
|
||||
@foreach(var price in _prices.OrderBy(p => p.Key))
|
||||
@@ -25,18 +31,24 @@
|
||||
var bingXTask = bingXClient.SpotApi.ExchangeData.GetTickersAsync("BTC-USDT");
|
||||
var bitfinexTask = bitfinexClient.SpotApi.ExchangeData.GetTickerAsync("tBTCUSD");
|
||||
var bitgetTask = bitgetClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT_SPBL");
|
||||
var bitmartTask = bitmartClient.SpotApi.ExchangeData.GetTickerAsync("BTC_USDT");
|
||||
var bybitTask = bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT");
|
||||
var coinbaseTask = coinbaseClient.AdvancedTradeApi.ExchangeData.GetSymbolAsync("BTC-USDT");
|
||||
var coinexTask = coinexClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||
var huobiTask = huobiClient.SpotApi.ExchangeData.GetTickerAsync("btcusdt");
|
||||
var cryptocomTask = cryptocomClient.ExchangeApi.ExchangeData.GetTickersAsync("BTC_USDT");
|
||||
var gateioTask = gateioClient.SpotApi.ExchangeData.GetTickersAsync("BTC_USDT");
|
||||
var htxTask = huobiClient.SpotApi.ExchangeData.GetTickerAsync("btcusdt");
|
||||
var krakenTask = krakenClient.SpotApi.ExchangeData.GetTickerAsync("XBTUSD");
|
||||
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
|
||||
var mexcTask = mexcClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||
var okxTask = okxClient.UnifiedApi.ExchangeData.GetTickerAsync("BTCUSDT");
|
||||
var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync();
|
||||
|
||||
await Task.WhenAll(binanceTask, bingXTask, bitfinexTask, bybitTask, coinexTask, huobiTask, krakenTask, kucoinTask);
|
||||
await Task.WhenAll(binanceTask, bingXTask, bitfinexTask, bitgetTask, bitmartTask, bybitTask, coinexTask, gateioTask, htxTask, krakenTask, kucoinTask, mexcTask, okxTask);
|
||||
|
||||
if (binanceTask.Result.Success)
|
||||
_prices.Add("Binance", binanceTask.Result.Data.LastPrice);
|
||||
|
||||
|
||||
if (bingXTask.Result.Success)
|
||||
_prices.Add("BingX", bingXTask.Result.Data.First().LastPrice);
|
||||
|
||||
@@ -46,14 +58,26 @@
|
||||
if (bitgetTask.Result.Success)
|
||||
_prices.Add("Bitget", bitgetTask.Result.Data.ClosePrice);
|
||||
|
||||
if (bitmartTask.Result.Success)
|
||||
_prices.Add("BitMart", bitgetTask.Result.Data.ClosePrice);
|
||||
|
||||
if (bybitTask.Result.Success)
|
||||
_prices.Add("Bybit", bybitTask.Result.Data.List.First().LastPrice);
|
||||
|
||||
if (coinbaseTask.Result.Success)
|
||||
_prices.Add("Coinbase", coinbaseTask.Result.Data.LastPrice ?? 0);
|
||||
|
||||
if (coinexTask.Result.Success)
|
||||
_prices.Add("CoinEx", coinexTask.Result.Data.Ticker.LastPrice);
|
||||
|
||||
if (huobiTask.Result.Success)
|
||||
_prices.Add("Huobi", huobiTask.Result.Data.ClosePrice ?? 0);
|
||||
if (cryptocomTask.Result.Success)
|
||||
_prices.Add("CryptoCom", cryptocomTask.Result.Data.First().LastPrice ?? 0);
|
||||
|
||||
if (gateioTask.Result.Success)
|
||||
_prices.Add("GateIo", gateioTask.Result.Data.First().LastPrice);
|
||||
|
||||
if (htxTask.Result.Success)
|
||||
_prices.Add("HTX", htxTask.Result.Data.ClosePrice ?? 0);
|
||||
|
||||
if (krakenTask.Result.Success)
|
||||
_prices.Add("Kraken", krakenTask.Result.Data.First().Value.LastTrade.Price);
|
||||
@@ -61,8 +85,17 @@
|
||||
if (kucoinTask.Result.Success)
|
||||
_prices.Add("Kucoin", kucoinTask.Result.Data.LastPrice ?? 0);
|
||||
|
||||
if (mexcTask.Result.Success)
|
||||
_prices.Add("Mexc", mexcTask.Result.Data.LastPrice);
|
||||
|
||||
if (okxTask.Result.Success)
|
||||
_prices.Add("OKX", okxTask.Result.Data.LastPrice ?? 0);
|
||||
|
||||
if (whitebitTask.Result.Success){
|
||||
// WhiteBit API doesn't offer an endpoint to filter for a specific ticker, so we have to filter client side
|
||||
var tickers = whitebitTask.Result.Data;
|
||||
_prices.Add("WhiteBit", tickers.Single(x => x.Symbol == "BTC_USDT").LastPrice);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,12 +3,18 @@
|
||||
@inject IBingXSocketClient bingXSocketClient
|
||||
@inject IBitfinexSocketClient bitfinexSocketClient
|
||||
@inject IBitgetSocketClient bitgetSocketClient
|
||||
@inject IBitMartSocketClient bitmartSocketClient
|
||||
@inject IBybitSocketClient bybitSocketClient
|
||||
@inject ICoinbaseSocketClient coinbaseSocketClient
|
||||
@inject ICoinExSocketClient coinExSocketClient
|
||||
@inject IHuobiSocketClient huobiSocketClient
|
||||
@inject ICryptoComSocketClient cryptocomSocketClient
|
||||
@inject IGateIoSocketClient gateioSocketClient
|
||||
@inject IHTXSocketClient htxSocketClient
|
||||
@inject IKrakenSocketClient krakenSocketClient
|
||||
@inject IKucoinSocketClient kucoinSocketClient
|
||||
@inject IMexcSocketClient mexcSocketClient
|
||||
@inject IOKXSocketClient okxSocketClient
|
||||
@inject IWhiteBitSocketClient whitebitSocketClient
|
||||
@using System.Collections.Concurrent
|
||||
@using CryptoExchange.Net.Objects
|
||||
@using CryptoExchange.Net.Objects.Sockets;
|
||||
@@ -33,12 +39,18 @@
|
||||
bingXSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("BingX", data.Data.LastPrice)),
|
||||
bitfinexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
|
||||
bitgetSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bitget", data.Data.LastPrice)),
|
||||
bitmartSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("BitMart", data.Data.LastPrice)),
|
||||
bybitSocketClient.V5SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
|
||||
coinExSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("CoinEx", data.Data.LastPrice)),
|
||||
huobiSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ethbtc", data => UpdateData("Huobi", data.Data.ClosePrice ?? 0)),
|
||||
krakenSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH/XBT", data => UpdateData("Kraken", data.Data.LastTrade.Price)),
|
||||
coinbaseSocketClient.AdvancedTradeApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Coinbase", data.Data.LastPrice)),
|
||||
cryptocomSocketClient.ExchangeApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("CryptoCom", data.Data.LastPrice ?? 0)),
|
||||
gateioSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("GateIo", data.Data.LastPrice)),
|
||||
htxSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ethbtc", data => UpdateData("HTX", data.Data.ClosePrice ?? 0)),
|
||||
krakenSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH/XBT", data => UpdateData("Kraken", data.Data.LastPrice)),
|
||||
kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
|
||||
mexcSocketClient.SpotApi.SubscribeToMiniTickerUpdatesAsync("ETHBTC", data => UpdateData("Mexc", data.Data.LastPrice)),
|
||||
okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.Data.LastPrice ?? 0)),
|
||||
whitebitSocketClient.V4Api.SubscribeToTickerUpdatesAsync("ETH_BTC", data => UpdateData("WhiteBit", data.Data.Ticker.LastPrice)),
|
||||
};
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
|
||||
@@ -5,24 +5,36 @@
|
||||
@using BingX.Net.Interfaces
|
||||
@using Bitfinex.Net.Interfaces
|
||||
@using Bitget.Net.Interfaces;
|
||||
@using BitMart.Net.Interfaces;
|
||||
@using Bybit.Net.Interfaces
|
||||
@using CoinEx.Net.Interfaces
|
||||
@using Coinbase.Net.Interfaces
|
||||
@using CryptoExchange.Net.Interfaces
|
||||
@using Huobi.Net.Interfaces
|
||||
@using CryptoCom.Net.Interfaces
|
||||
@using GateIo.Net.Interfaces
|
||||
@using HTX.Net.Interfaces
|
||||
@using Kraken.Net.Interfaces
|
||||
@using Kucoin.Net.Clients
|
||||
@using Kucoin.Net.Interfaces
|
||||
@using Mexc.Net.Interfaces
|
||||
@using OKX.Net.Interfaces;
|
||||
@using WhiteBit.Net.Interfaces
|
||||
@inject IBinanceOrderBookFactory binanceFactory
|
||||
@inject IBingXOrderBookFactory bingXFactory
|
||||
@inject IBitfinexOrderBookFactory bitfinexFactory
|
||||
@inject IBitgetOrderBookFactory bitgetFactory
|
||||
@inject IBitMartOrderBookFactory bitmartFactory
|
||||
@inject IBybitOrderBookFactory bybitFactory
|
||||
@inject ICoinbaseOrderBookFactory coinbaseFactory
|
||||
@inject ICoinExOrderBookFactory coinExFactory
|
||||
@inject IHuobiOrderBookFactory huobiFactory
|
||||
@inject ICryptoComOrderBookFactory cryptocomFactory
|
||||
@inject IGateIoOrderBookFactory gateioFactory
|
||||
@inject IHTXOrderBookFactory htxFactory
|
||||
@inject IKrakenOrderBookFactory krakenFactory
|
||||
@inject IKucoinOrderBookFactory kucoinFactory
|
||||
@inject IMexcOrderBookFactory mexcFactory
|
||||
@inject IOKXOrderBookFactory okxFactory
|
||||
@inject IWhiteBitOrderBookFactory whitebitFactory
|
||||
@implements IDisposable
|
||||
|
||||
<h3>ETH-BTC books, live updates:</h3>
|
||||
@@ -60,12 +72,18 @@
|
||||
{ "BingX", bingXFactory.CreateSpot("ETH-BTC") },
|
||||
{ "Bitfinex", bitfinexFactory.Create("tETHBTC") },
|
||||
{ "Bitget", bitgetFactory.CreateSpot("ETHBTC") },
|
||||
{ "BitMart", bitmartFactory.CreateSpot("ETH_BTC", null) },
|
||||
{ "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) },
|
||||
{ "Coinbase", coinbaseFactory.Create("ETH-BTC", null) },
|
||||
{ "CoinEx", coinExFactory.CreateSpot("ETHBTC") },
|
||||
{ "Huobi", huobiFactory.CreateSpot("ethbtc") },
|
||||
{ "Kraken", krakenFactory.CreateSpot("ETH/XBT") },
|
||||
{ "CryptoCom", cryptocomFactory.Create("ETH_BTC") },
|
||||
{ "GateIo", gateioFactory.CreateSpot("ETH_BTC") },
|
||||
{ "HTX", htxFactory.CreateSpot("ethbtc") },
|
||||
{ "Kraken", krakenFactory.CreateSpot("ETH/BTC") },
|
||||
{ "Kucoin", kucoinFactory.CreateSpot("ETH-BTC") },
|
||||
{ "Mexc", mexcFactory.CreateSpot("ETHBTC") },
|
||||
{ "OKX", okxFactory.Create("ETH-BTC") },
|
||||
{ "WhiteBit", whitebitFactory.CreateV4("ETH_BTC") },
|
||||
};
|
||||
|
||||
await Task.WhenAll(_books.Select(b => b.Value.StartAsync()));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@page "/SpotClient"
|
||||
@inject ICryptoRestClient restClient
|
||||
@using CryptoExchange.Net.SharedApis
|
||||
@inject IEnumerable<ISpotTickerRestClient> restClients
|
||||
|
||||
<h3>ETH-BTC prices:</h3>
|
||||
@foreach(var price in _prices.OrderBy(p => p.Key))
|
||||
@@ -12,13 +13,13 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var clients = restClient.GetSpotClients();
|
||||
var tasks = clients.Select(c => (c.ExchangeName, c.GetTickerAsync(c.GetSymbolName("ETH", "BTC"))));
|
||||
await Task.WhenAll(tasks.Select(t => t.Item2));
|
||||
foreach(var task in tasks)
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "BTC");
|
||||
var tasks = restClients.Select(x => x.GetSpotTickerAsync(new GetTickerRequest(symbol)));
|
||||
await Task.WhenAll(tasks);
|
||||
foreach (var ticker in tasks.Select(x => x.Result))
|
||||
{
|
||||
if(task.Item2.Result.Success)
|
||||
_prices.Add(task.Item1, task.Item2.Result.Data.HighPrice);
|
||||
if (ticker.Success)
|
||||
_prices.Add(ticker.Exchange, ticker.Data.LastPrice);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
@page "/Trackers"
|
||||
@using System.Collections.Concurrent
|
||||
@using System.Timers
|
||||
@using Binance.Net.Interfaces
|
||||
@using BingX.Net.Interfaces
|
||||
@using Bitfinex.Net.Interfaces
|
||||
@using Bitget.Net.Interfaces;
|
||||
@using BitMart.Net.Interfaces;
|
||||
@using Bybit.Net.Interfaces
|
||||
@using CoinEx.Net.Interfaces
|
||||
@using Coinbase.Net.Interfaces
|
||||
@using CryptoExchange.Net.Interfaces
|
||||
@using CryptoCom.Net.Interfaces
|
||||
@using CryptoExchange.Net.SharedApis
|
||||
@using CryptoExchange.Net.Trackers.Trades
|
||||
@using GateIo.Net.Interfaces
|
||||
@using HTX.Net.Interfaces
|
||||
@using Kraken.Net.Interfaces
|
||||
@using Kucoin.Net.Clients
|
||||
@using Kucoin.Net.Interfaces
|
||||
@using Mexc.Net.Interfaces
|
||||
@using OKX.Net.Interfaces;
|
||||
@using WhiteBit.Net.Interfaces
|
||||
@inject IBinanceTrackerFactory binanceFactory
|
||||
@inject IBingXTrackerFactory bingXFactory
|
||||
@inject IBitfinexTrackerFactory bitfinexFactory
|
||||
@inject IBitgetTrackerFactory bitgetFactory
|
||||
@inject IBitMartTrackerFactory bitmartFactory
|
||||
@inject IBybitTrackerFactory bybitFactory
|
||||
@inject ICoinbaseTrackerFactory coinbaseFactory
|
||||
@inject ICoinExTrackerFactory coinExFactory
|
||||
@inject ICryptoComTrackerFactory cryptocomFactory
|
||||
@inject IGateIoTrackerFactory gateioFactory
|
||||
@inject IHTXTrackerFactory htxFactory
|
||||
@inject IKrakenTrackerFactory krakenFactory
|
||||
@inject IKucoinTrackerFactory kucoinFactory
|
||||
@inject IMexcTrackerFactory mexcFactory
|
||||
@inject IOKXTrackerFactory okxFactory
|
||||
@inject IWhiteBitTrackerFactory whitebitFactory
|
||||
@implements IDisposable
|
||||
|
||||
<h3>ETH-BTC trade Trackers, live updates:</h3>
|
||||
<div style="display:flex; flex-wrap: wrap;">
|
||||
@foreach (var tracker in _trackers.OrderBy(p => p.Exchange))
|
||||
{
|
||||
<div style="margin-bottom: 20px; flex: 1; min-width: 700px;">
|
||||
<h4>@tracker.Exchange</h4>
|
||||
@foreach(var line in GetInfo(tracker))
|
||||
{
|
||||
<div>@line</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code{
|
||||
private List<ITradeTracker> _trackers = new List<ITradeTracker>();
|
||||
private Timer _timer;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||
|
||||
_trackers = new List<ITradeTracker>
|
||||
{
|
||||
{ binanceFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bingXFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitfinexFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitgetFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bitmartFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ bybitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ coinbaseFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ coinExFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ cryptocomFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ gateioFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ htxFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ krakenFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ kucoinFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ mexcFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ okxFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
{ whitebitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
|
||||
};
|
||||
|
||||
await Task.WhenAll(_trackers.Select(b => b.StartAsync()));
|
||||
|
||||
// Use a manual update timer so the page isn't refreshed too often
|
||||
_timer = new Timer(500);
|
||||
_timer.Start();
|
||||
_timer.Elapsed += (o, e) => InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private string[] GetInfo(ITradeTracker tracker)
|
||||
{
|
||||
var secondLastMinute = tracker.GetStats(DateTime.UtcNow.AddMinutes(-2), DateTime.UtcNow.AddMinutes(-1));
|
||||
var lastMinute = tracker.GetStats(DateTime.UtcNow.AddMinutes(-1));
|
||||
var compare = lastMinute.CompareTo(secondLastMinute);
|
||||
|
||||
return [
|
||||
$"{tracker.SymbolName} | {tracker.Status} - Synced from {tracker.SyncedFrom}",
|
||||
$"Total trades: {tracker.Count}",
|
||||
$"Trades last minute: {lastMinute.TradeCount}, minute before: {secondLastMinute.TradeCount}",
|
||||
$"Average weighted price: {lastMinute.VolumeWeightedAveragePrice}, minute before: {secondLastMinute.VolumeWeightedAveragePrice}, dif: {compare.VolumeWeightedAveragePriceDif.PercentageDifference}%"
|
||||
];
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer.Stop();
|
||||
_timer.Dispose();
|
||||
foreach (var tracker in _trackers.Where(b => b.Status != CryptoExchange.Net.Objects.SyncStatus.Disconnected))
|
||||
// It's not necessary to wait for this
|
||||
_ = tracker.StopAsync();
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="SpotClient">
|
||||
Get data ISpotClient
|
||||
Get data SharedClient
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
@@ -27,6 +27,11 @@
|
||||
Order books
|
||||
</NavLink>
|
||||
</li>
|
||||
<li class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="Trackers">
|
||||
Trackers
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -39,12 +39,18 @@ namespace BlazorClient
|
||||
services.AddBingX();
|
||||
services.AddBitfinex();
|
||||
services.AddBitget();
|
||||
services.AddBitMart();
|
||||
services.AddBybit();
|
||||
services.AddCoinbase();
|
||||
services.AddCoinEx();
|
||||
services.AddHuobi();
|
||||
services.AddCryptoCom();
|
||||
services.AddGateIo();
|
||||
services.AddHTX();
|
||||
services.AddKraken();
|
||||
services.AddKucoin();
|
||||
services.AddMexc();
|
||||
services.AddOKX();
|
||||
services.AddWhiteBit();
|
||||
}
|
||||
|
||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||
|
||||
@@ -12,10 +12,16 @@
|
||||
@using BingX.Net.Interfaces.Clients;
|
||||
@using Bitfinex.Net.Interfaces.Clients;
|
||||
@using Bitget.Net.Interfaces.Clients;
|
||||
@using BitMart.Net.Interfaces.Clients;
|
||||
@using Bybit.Net.Interfaces.Clients;
|
||||
@using Coinbase.Net.Interfaces.Clients;
|
||||
@using CoinEx.Net.Interfaces.Clients;
|
||||
@using Huobi.Net.Interfaces.Clients;
|
||||
@using CryptoCom.Net.Interfaces.Clients;
|
||||
@using GateIo.Net.Interfaces.Clients;
|
||||
@using HTX.Net.Interfaces.Clients;
|
||||
@using Kraken.Net.Interfaces.Clients;
|
||||
@using Kucoin.Net.Interfaces.Clients;
|
||||
@using Mexc.Net.Interfaces.Clients;
|
||||
@using OKX.Net.Interfaces.Clients;
|
||||
@using WhiteBit.Net.Interfaces.Clients
|
||||
@using CryptoExchange.Net.Interfaces;
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
@@ -6,16 +6,20 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="9.5.0" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="7.1.0" />
|
||||
<PackageReference Include="Bittrex.Net" Version="8.0.3" />
|
||||
<PackageReference Include="Bybit.Net" Version="3.4.0" />
|
||||
<PackageReference Include="CoinEx.Net" Version="6.1.0" />
|
||||
<PackageReference Include="Huobi.Net" Version="5.1.0" />
|
||||
<PackageReference Include="JK.Bitget.Net" Version="1.1.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="1.6.0" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="4.3.0" />
|
||||
<PackageReference Include="Kucoin.Net" Version="5.2.0" />
|
||||
<PackageReference Include="Binance.Net" Version="10.9.0" />
|
||||
<PackageReference Include="Bitfinex.Net" Version="7.10.0" />
|
||||
<PackageReference Include="BitMart.Net" Version="1.7.0" />
|
||||
<PackageReference Include="Bybit.Net" Version="3.16.0" />
|
||||
<PackageReference Include="CoinEx.Net" Version="7.9.0" />
|
||||
<PackageReference Include="CryptoCom.Net" Version="1.2.0" />
|
||||
<PackageReference Include="GateIo.Net" Version="1.12.0" />
|
||||
<PackageReference Include="JK.Bitget.Net" Version="1.13.0" />
|
||||
<PackageReference Include="JK.Mexc.Net" Version="1.11.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="2.8.0" />
|
||||
<PackageReference Include="JKorf.Coinbase.Net" Version="1.4.0" />
|
||||
<PackageReference Include="JKorf.HTX.Net" Version="6.4.0" />
|
||||
<PackageReference Include="KrakenExchange.Net" Version="5.2.0" />
|
||||
<PackageReference Include="Kucoin.Net" Version="5.18.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace ConsoleClient.Exchanges
|
||||
{
|
||||
using var client = new BybitRestClient();
|
||||
var result = await client.V5Api.Account.GetBalancesAsync(Bybit.Net.Enums.AccountType.Spot);
|
||||
return result.Data.List.First().Assets.ToDictionary(d => d.Asset, d => d.WalletBalance);
|
||||
return result.Data.List.First().Assets.ToDictionary(d => d.Asset, d => d.WalletBalance ?? 0);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<OpenOrder>> GetOpenOrders()
|
||||
|
||||
@@ -4,12 +4,10 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Binance.Net.Clients;
|
||||
using Binance.Net.Objects;
|
||||
using Bybit.Net.Clients;
|
||||
using ConsoleClient.Exchanges;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
|
||||
namespace ConsoleClient
|
||||
{
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using Binance.Net.Clients;
|
||||
using BitMart.Net.Clients;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using OKX.Net.Clients;
|
||||
|
||||
var symbol = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
|
||||
var binanceSpotRestClient = new BinanceRestClient().SpotApi.SharedClient;
|
||||
var okxSpotRestClient = new OKXRestClient().UnifiedApi.SharedClient;
|
||||
var bitmartSpotRestClient = new BitMartRestClient().SpotApi.SharedClient;
|
||||
|
||||
var binanceSpotSocketClient = new BinanceSocketClient().SpotApi.SharedClient;
|
||||
var okxSpotSocketClient = new OKXSocketClient().UnifiedApi.SharedClient;
|
||||
var bitmartSpotSocketClient = new BitMartSocketClient().SpotApi.SharedClient;
|
||||
|
||||
await GetLastTradePriceAsync(binanceSpotRestClient, symbol);
|
||||
await GetLastTradePriceAsync(okxSpotRestClient, symbol);
|
||||
await GetLastTradePriceAsync(bitmartSpotRestClient, symbol);
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Press enter to start websocket");
|
||||
Console.ReadLine();
|
||||
|
||||
await SubscribeTickerUpdatesAsync(binanceSpotSocketClient, symbol);
|
||||
await SubscribeTickerUpdatesAsync(okxSpotSocketClient, symbol);
|
||||
await SubscribeTickerUpdatesAsync(bitmartSpotSocketClient, symbol);
|
||||
|
||||
Console.ReadLine();
|
||||
|
||||
async Task GetLastTradePriceAsync(ISpotTickerRestClient client, SharedSymbol symbol)
|
||||
{
|
||||
var result = await client.GetSpotTickerAsync(new GetTickerRequest(symbol));
|
||||
if (!result.Success)
|
||||
{
|
||||
Console.WriteLine($"Failed to get ticker: {result.Error}");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"{client.Exchange} {result.Data.Symbol}: {result.Data.LastPrice}");
|
||||
}
|
||||
|
||||
async Task SubscribeTickerUpdatesAsync(ITickerSocketClient client, SharedSymbol symbol)
|
||||
{
|
||||
var result = await client.SubscribeToTickerUpdatesAsync(new SubscribeTickerRequest(symbol), update =>
|
||||
{
|
||||
Console.WriteLine($"{client.Exchange} {update.Data.Symbol} {update.Data.LastPrice}");
|
||||
});
|
||||
|
||||
if (!result.Success)
|
||||
Console.WriteLine($"Failed to subscribe ticker: {result.Error}");
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Binance.Net" Version="10.9.0" />
|
||||
<PackageReference Include="BitMart.Net" Version="1.7.0" />
|
||||
<PackageReference Include="JK.OKX.Net" Version="2.8.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
// Options section, select this section during DI registration using Configuration.GetSection("ExchangeApiOptions")
|
||||
"ExchangeApiOptions": {
|
||||
// API credentials for both REST and Websocket client
|
||||
"ApiCredentials": {
|
||||
"Key": "APIKEY",
|
||||
"Secret": "SECRET",
|
||||
"PassPhrase": "Phrase" // Optional passphrase for exchanges which need it
|
||||
},
|
||||
// Set the environment by name
|
||||
"Environment": {
|
||||
"name": "live"
|
||||
},
|
||||
// REST client options
|
||||
"Rest": {
|
||||
"RequestTimeout": "00:00:20",
|
||||
"CachingEnabled": true,
|
||||
"OutputOriginalData": true,
|
||||
"Proxy": {
|
||||
"Host": "https://127.0.0.1",
|
||||
"Port": 8080,
|
||||
"Login": "User",
|
||||
"Password": "Pass"
|
||||
}
|
||||
},
|
||||
// Socket client options
|
||||
"Socket": {
|
||||
"RequestTimeout": "00:00:05",
|
||||
"SocketSubscriptionsCombineTarget": 15
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,14 +18,18 @@ The following API's are directly supported. Note that there are 3rd party implem
|
||||
|Bitget|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[](https://www.nuget.org/packages/JK.Bitget.Net)|
|
||||
|BitMart|[JKorf/BitMart.Net](https://github.com/JKorf/BitMart.Net)|[](https://www.nuget.org/packages/BitMart.Net)|
|
||||
|Bybit|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[](https://www.nuget.org/packages/Bybit.Net)|
|
||||
|Coinbase|[JKorf/Coinbase.Net](https://github.com/JKorf/Coinbase.Net)|[](https://www.nuget.org/packages/JKorf.Coinbase.Net)|
|
||||
|CoinEx|[JKorf/CoinEx.Net](https://github.com/JKorf/CoinEx.Net)|[](https://www.nuget.org/packages/CoinEx.Net)|
|
||||
|CoinGecko|[JKorf/CoinGecko.Net](https://github.com/JKorf/CoinGecko.Net)|[](https://www.nuget.org/packages/CoinGecko.Net)|
|
||||
|Crypto.com|[JKorf/CryptoCom.Net](https://github.com/JKorf/CryptoCom.Net)|[](https://www.nuget.org/packages/CryptoCom.Net)|
|
||||
|Gate.io|[JKorf/GateIo.Net](https://github.com/JKorf/GateIo.Net)|[](https://www.nuget.org/packages/GateIo.Net)|
|
||||
|HTX|[JKorf/HTX.Net](https://github.com/JKorf/HTX.Net)|[](https://www.nuget.org/packages/JKorf.HTX.Net)|
|
||||
|Kraken|[JKorf/Kraken.Net](https://github.com/JKorf/Kraken.Net)|[](https://www.nuget.org/packages/KrakenExchange.Net)|
|
||||
|Kucoin|[JKorf/Kucoin.Net](https://github.com/JKorf/Kucoin.Net)|[](https://www.nuget.org/packages/Kucoin.Net)|
|
||||
|Mexc|[JKorf/Mexc.Net](https://github.com/JKorf/Mexc.Net)|[](https://www.nuget.org/packages/JK.Mexc.Net)|
|
||||
|OKX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[](https://www.nuget.org/packages/JK.OKX.Net)|
|
||||
|WhiteBit|[JKorf/WhiteBit.Net](https://github.com/JKorf/WhiteBit.Net)|[](https://www.nuget.org/packages/WhiteBit.Net)|
|
||||
|XT|[JKorf/XT.Net](https://github.com/JKorf/XT.Net)|[](https://www.nuget.org/packages/XT.Net)|
|
||||
|
||||
Any of these can be installed independently or install [CryptoClients.Net](https://github.com/jkorf/CryptoClients.Net) which includes all exchange API's.
|
||||
|
||||
@@ -34,7 +38,22 @@ Any of these can be installed independently or install [CryptoClients.Net](https
|
||||
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.
|
||||
|
||||
## Support the project
|
||||
I develop and maintain this package on my own for free in my spare time, any support is greatly appreciated.
|
||||
Any support is greatly appreciated.
|
||||
|
||||
## Referral
|
||||
When creating an account on new exchanges please consider using a referral link from below to support development
|
||||
|
||||
|Exchange|Link|
|
||||
|--|--|
|
||||
|Bybit|[https://partner.bybit.com/b/jkorf](https://partner.bybit.com/b/jkorf)|
|
||||
|Coinbase|[https://advanced.coinbase.com/join/T6H54H8](https://advanced.coinbase.com/join/T6H54H8)|
|
||||
|CoinEx|[https://www.coinex.com/register?refer_code=hd6gn](https://www.coinex.com/register?refer_code=hd6gn)|
|
||||
|Crypto.com|[https://crypto.com/exch/26ge92xbkn](https://crypto.com/exch/26ge92xbkn)|
|
||||
|HTX|[https://www.htx.com/invite/en-us/1f?invite_code=fxp9](https://www.htx.com/invite/en-us/1f?invite_code=fxp9)|
|
||||
|Kucoin|[https://www.kucoin.com/r/rf/QBS4FPED](https://www.kucoin.com/r/rf/QBS4FPED)|
|
||||
|OKX|[https://okx.com/join/48046699](https://okx.com/join/48046699)|
|
||||
|WhiteBit|[https://whitebit.com/referral/a8e59b59-186c-4662-824c-3095248e0edf](https://whitebit.com/referral/a8e59b59-186c-4662-824c-3095248e0edf)|
|
||||
|XT|[https://www.xt.com/en/accounts/register?ref=1HRM5J](https://www.xt.com/en/accounts/register?ref=1HRM5J)|
|
||||
|
||||
### Donate
|
||||
Make a one time donation in a crypto currency of your choice. If you prefer to donate a currency not listed here please contact me.
|
||||
@@ -47,8 +66,72 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
|
||||
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
|
||||
|
||||
## Release notes
|
||||
* Version 8.4.4 - 08 Dec 2024
|
||||
* Changed JsonConverterCtorAttribute to use constructor type parameter instead of generic type parameter to support .net framework
|
||||
|
||||
* Version 8.4.3 - 03 Dec 2024
|
||||
* Fixed KlineTracker update handling
|
||||
|
||||
* Version 8.4.2 - 02 Dec 2024
|
||||
* Removed special characters in ClientOrderIdSeperator to adhere to field content rules
|
||||
|
||||
* Version 8.4.1 - 02 Dec 2024
|
||||
* Added JsonConverterCtorAttribute to allow specifying a custom JsonConverter with constructor parameters on properties
|
||||
* Added ReplaceConverter System.Text.Json converter
|
||||
* Added LibraryHelpers class for internal helper methods
|
||||
|
||||
* Version 8.4.0 - 28 Nov 2024
|
||||
* Added GetFeesAsync Shared REST client support
|
||||
* Added LibraryOptions base class
|
||||
* Added CommaSplitEnumConverter System.Text.Json converter
|
||||
* Added TimePeriodFilterSupport and MaxLimit properties to PaginatedEndpointOptions
|
||||
* Updated package dependency versions
|
||||
|
||||
* Version 8.3.0 - 19 Nov 2024
|
||||
* Added support for IOptions injection, allowing options to be read from IConfiguration
|
||||
* Added handling of Infinity values in decimal converter
|
||||
* Added rate limit update event
|
||||
* Small refactor on client options internals
|
||||
* Fixed concurrency issue when unsubscribing websocket subscription during reconnection
|
||||
|
||||
* Version 8.2.0 - 06 Nov 2024
|
||||
* Added support for not allowing duplicate subscription topics on the same websocket connection
|
||||
* Added PerAccount SharedLeverageSettingMode enum value, changed Side on SharedUserTrade to nullable
|
||||
* Added support for object deserialization in SystemTextJsonMessageAccessor.GetValue<T>
|
||||
* Changed SocketApiClient GetAuthenticationRequest to GetAuthenticationRequestAsync to allow for requesting token
|
||||
|
||||
* Version 8.1.1 - 01 Nov 2024
|
||||
* Fixed socket connections trying to authenticated connection when it's marked as dedicated request connection even when no authentication is needed
|
||||
* Fixed System.Text.Json ArrayConverter not passing serializer options to nested deserialization
|
||||
* Fixed System.Text.Json ArrayConverter creating new serializer options each time a JsonConverter attribute is encountered
|
||||
|
||||
* Version 8.1.0 - 28 Oct 2024
|
||||
* Added KlineTracker and TradeTracker implementation
|
||||
* Added Side to SharedTrade model
|
||||
* Added overload for Create method in OrderBookFactory using SharedSymbol
|
||||
* Added ValidateMessage method to websocket Query object to filter messages even though it is matched to the query based on the ListenIdentifier
|
||||
* Added DoHandleReset method for websocket subscriptions
|
||||
* Added ConnectionId to RequestDefinition to correctly handle connection and path rate limiting configuration
|
||||
* Added System.Text.Json ArrayConverter Write implementation
|
||||
* Updated SharedFuturesTicker LastPrice, HighPrice and LowPrice properties to be nullable
|
||||
* Updated SetApiCredentials method to also updated the credentials on the client specific options to prevent unknown client credentials in some situations
|
||||
|
||||
* Version 8.0.3 - 14 Oct 2024
|
||||
* Added support for duplicate array indexes in System.Text.Json ArrayConverter
|
||||
* Added fallback for unparsable value in System.Text.Json NumberStringConverter
|
||||
* Added Authenticated property on base client and shared client
|
||||
* Added GetValues System.Text.Json implementation in message accessor
|
||||
|
||||
* Version 8.0.2 - 09 Oct 2024
|
||||
* Updated dependency versions, including System.Text.Json from 8.0.4 to 8.0.5 containing a vulnerability fix
|
||||
|
||||
* Version 8.0.1 - 07 Oct 2024
|
||||
* Added cached library version properties on base client
|
||||
* Added support for derserializing 0001-01-01 as datetime null value
|
||||
* Added ToRfc3339String extension method for DateTime type
|
||||
|
||||
* Version 8.0.0 - 27 Sep 2024
|
||||
* Added new cross exchange interfaces implementation
|
||||
* Added new cross exchange interfaces implementation
|
||||
* Supports REST, WebSocket, Spot and Futures API's
|
||||
* Added various client interfaces for specific functionality
|
||||
* Added SharedSymbol type, taking care of symbol formatting for different exchanges
|
||||
|
||||
+1801
-128
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user