1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-10-04 02:11:11 +00:00

Shared API's V2 (#289)

Added V2 Shared API's infrastructure
Added request coalescing
Added rate limiting admission logic
Fixed some rate limiting issues
This commit is contained in:
Jan Korf
2026-09-23 09:15:39 +02:00
committed by GitHub
parent cecfbcba48
commit ae7504c26c
482 changed files with 12870 additions and 2150 deletions
+93 -24
View File
@@ -3,6 +3,7 @@ using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Filters; using CryptoExchange.Net.RateLimiting.Filters;
using CryptoExchange.Net.RateLimiting.Guards; using CryptoExchange.Net.RateLimiting.Guards;
using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.RateLimiting.Trackers;
using CryptoExchange.Net.UnitTests.Implementations; using CryptoExchange.Net.UnitTests.Implementations;
using NUnit.Framework; using NUnit.Framework;
using System; using System;
@@ -33,12 +34,12 @@ namespace CryptoExchange.Net.UnitTests
for (var i = 0; i < requests + 1; i++) for (var i = 0; i < requests + 1; i++)
{ {
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
Assert.That(i == requests ? triggered : !triggered); Assert.That(i == requests ? triggered : !triggered);
} }
triggered = false; triggered = false;
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10); await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
Assert.That(!triggered); Assert.That(!triggered);
} }
@@ -58,7 +59,7 @@ namespace CryptoExchange.Net.UnitTests
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++) for (var i = 0; i < 2; i++)
{ {
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
bool expected = i == 1 ? expectLimiting ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null; bool expected = i == 1 ? expectLimiting ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
Assert.That(expected); Assert.That(expected);
} }
@@ -79,9 +80,9 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent? evnt = null; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
Assert.That(evnt == null); Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "123", 1, RateLimitingBehaviour.Wait, null, default); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
Assert.That(expectLimiting ? evnt != null : evnt == null); Assert.That(expectLimiting ? evnt != null : evnt == null);
} }
@@ -100,12 +101,12 @@ namespace CryptoExchange.Net.UnitTests
for (var i = 0; i < requests + 1; i++) for (var i = 0; i < requests + 1; i++)
{ {
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
Assert.That(i == requests ? triggered : !triggered); Assert.That(i == requests ? triggered : !triggered);
} }
triggered = false; triggered = false;
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10); await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
Assert.That(!triggered); Assert.That(!triggered);
} }
@@ -123,7 +124,7 @@ namespace CryptoExchange.Net.UnitTests
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++) for (var i = 0; i < 2; i++)
{ {
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null; bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
Assert.That(expected); Assert.That(expected);
} }
@@ -143,7 +144,7 @@ namespace CryptoExchange.Net.UnitTests
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++) for (var i = 0; i < 2; i++)
{ {
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null; bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
Assert.That(expected); Assert.That(expected);
} }
@@ -166,9 +167,9 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent? evnt = null; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, key1, 1, RateLimitingBehaviour.Wait, null, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, key1, 1, RateLimitingBehaviour.Wait, null, 1, default);
Assert.That(evnt == null); Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, key2, 1, RateLimitingBehaviour.Wait, null, default); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, key2, 1, RateLimitingBehaviour.Wait, null, 1, default);
Assert.That(expectLimited ? evnt != null : evnt == null); Assert.That(expectLimited ? evnt != null : evnt == null);
} }
@@ -185,9 +186,9 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent? evnt = null; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
Assert.That(evnt == null); Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, null, 1, RateLimitingBehaviour.Wait, null, default); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, null, 1, RateLimitingBehaviour.Wait, null, 1, default);
Assert.That(expectLimited ? evnt != null : evnt == null); Assert.That(expectLimited ? evnt != null : evnt == null);
} }
@@ -205,9 +206,9 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent? evnt = null; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
Assert.That(evnt == null); Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "123", 1, RateLimitingBehaviour.Wait, null, default); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
Assert.That(expectLimited ? evnt != null : evnt == null); Assert.That(expectLimited ? evnt != null : evnt == null);
} }
@@ -222,9 +223,9 @@ namespace CryptoExchange.Net.UnitTests
RateLimitEvent? evnt = null; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition(host1, "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition(host1, "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
Assert.That(evnt == null); Assert.That(evnt == null);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition(host2, "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, default); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition(host2, "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, 1, default);
Assert.That(expectLimited ? evnt != null : evnt == null); Assert.That(expectLimited ? evnt != null : evnt == null);
} }
@@ -238,8 +239,8 @@ namespace CryptoExchange.Net.UnitTests
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2)); var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("https://test.com", "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, ct.Token); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("https://test.com", "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, 1, ct.Token);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("https://test.com", "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, ct.Token); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("https://test.com", "1", HttpMethod.Get), "123", 1, RateLimitingBehaviour.Wait, null, 1, ct.Token);
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>()); Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
} }
@@ -257,9 +258,9 @@ namespace CryptoExchange.Net.UnitTests
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2)); var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
// act // act
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, null, 1, RateLimitingBehaviour.Fail, null, ct.Token); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, null, 1, RateLimitingBehaviour.Fail, null, 1, ct.Token);
await rateLimiter.ResetAsync(RateLimitItemType.Request, definition, null, null, null, default); await rateLimiter.ResetAsync(RateLimitItemType.Request, definition, null, null, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, null, 1, RateLimitingBehaviour.Fail, null, ct.Token); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, null, 1, RateLimitingBehaviour.Fail, null, 1, ct.Token);
// assert // assert
Assert.That(evnt, Is.Null); Assert.That(evnt, Is.Null);
@@ -279,10 +280,10 @@ namespace CryptoExchange.Net.UnitTests
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
// act // act
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition1, null, 1, RateLimitingBehaviour.Fail, null, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition1, null, 1, RateLimitingBehaviour.Fail, null, 1, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, null, 1, RateLimitingBehaviour.Fail, null, default); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, null, 1, RateLimitingBehaviour.Fail, null, 1, default);
await rateLimiter.ResetAsync(RateLimitItemType.Request, definition1, null, null, null, default); await rateLimiter.ResetAsync(RateLimitItemType.Request, definition1, null, null, null, default);
var result3 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, null, 1, RateLimitingBehaviour.Fail, null, default); var result3 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, null, 1, RateLimitingBehaviour.Fail, null, 1, default);
// assert // assert
Assert.That(evnt, Is.Not.Null); Assert.That(evnt, Is.Not.Null);
@@ -321,5 +322,73 @@ namespace CryptoExchange.Net.UnitTests
// assert // assert
Assert.That(evnt != null, Is.EqualTo(expectLimited)); Assert.That(evnt != null, Is.EqualTo(expectLimited));
} }
[TestCase(RateLimitWindowType.Fixed)]
[TestCase(RateLimitWindowType.FixedAfterFirst)]
[TestCase(RateLimitWindowType.Sliding)]
[TestCase(RateLimitWindowType.Decay)]
public async Task LowerThreshold_ReservesCapacityForFullThreshold(RateLimitWindowType windowType)
{
var gate = new RateLimitGate("Test");
gate.AddGuard(new RateLimitGuard(
RateLimitGuard.PerHost,
new LimitItemTypeFilter(RateLimitItemType.Request),
10,
TimeSpan.FromHours(1),
windowType,
decayPerTimeSpan: 1));
var definition = new RequestDefinition(
"https://test.com", "/ticker", HttpMethod.Get);
var logger = new TraceLogger();
for (var i = 0; i < 8; i++)
{
var result = await gate.ProcessAsync(
logger, i, RateLimitItemType.Request, definition, null, 1,
RateLimitingBehaviour.Fail, null, 0.8, default);
Assert.That(result.Success, Is.True);
}
var marketData = await gate.ProcessAsync(
logger, 9, RateLimitItemType.Request, definition, null, 1,
RateLimitingBehaviour.Fail, null, 0.8, default);
Assert.That(marketData.Error, Is.TypeOf<ClientRateLimitError>());
var order = await gate.ProcessAsync(
logger, 10, RateLimitItemType.Request, definition, null, 2,
RateLimitingBehaviour.Fail, null, 1.0, default);
Assert.That(order.Success, Is.True);
var overHardLimit = await gate.ProcessAsync(
logger, 11, RateLimitItemType.Request, definition, null, 1,
RateLimitingBehaviour.Fail, null, 1.0, default);
Assert.That(overHardLimit.Error, Is.TypeOf<ClientRateLimitError>());
}
[TestCase(0.1, 10)]
[TestCase(1, 50)]
[TestCase(5, 250)]
[TestCase(60, 250)]
public void RateLimitSafetyMargin_DefaultIsProportionalAndCapped(double periodSeconds, int expectedMarginMilliseconds)
{
var margin = WindowTrackerHelpers.GetDefaultSafetyMargin(TimeSpan.FromSeconds(periodSeconds));
Assert.That(margin, Is.EqualTo(TimeSpan.FromMilliseconds(expectedMarginMilliseconds)));
}
[Test]
public void RateLimitGuard_ExplicitSafetyMarginIsUsed()
{
var guard = new RateLimitGuard(
RateLimitGuard.PerHost,
new LimitItemTypeFilter(RateLimitItemType.Request),
1,
TimeSpan.FromSeconds(1),
RateLimitWindowType.Sliding,
safetyMargin: TimeSpan.Zero);
Assert.That(guard.SafetyMargin, Is.EqualTo(TimeSpan.Zero));
}
} }
} }
@@ -0,0 +1,281 @@
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging.Abstractions;
using NUnit.Framework;
using System;
using System.Collections;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture]
public class RequestCoalescerTests
{
private static readonly TimeSpan _testTimeout = TimeSpan.FromSeconds(5);
[Test]
public async Task ExecuteAsync_WithIdenticalRequests_ShouldExecuteRequestOnce()
{
var coalescer = CreateCoalescer();
var key = CreateKey();
var requestCompletion = CreateCompletionSource<string>();
var requestStarted = CreateCompletionSource<bool>();
var requestCount = 0;
async Task<string> ExecuteRequest(CancellationToken cancellationToken)
{
Interlocked.Increment(ref requestCount);
requestStarted.TrySetResult(true);
return await requestCompletion.Task;
}
var first = coalescer.ExecuteAsync(key, ExecuteRequest, () => "canceled", CancellationToken.None);
await requestStarted.Task.WaitAsync(_testTimeout);
var second = coalescer.ExecuteAsync(key, ExecuteRequest, () => "canceled", CancellationToken.None);
Assert.That(requestCount, Is.EqualTo(1));
requestCompletion.SetResult("result");
Assert.That(await first.WaitAsync(_testTimeout), Is.EqualTo("result"));
Assert.That(await second.WaitAsync(_testTimeout), Is.EqualTo("result"));
Assert.That(requestCount, Is.EqualTo(1));
}
[Test]
public async Task ExecuteAsync_WithDifferentKeys_ShouldExecuteEachRequest()
{
var coalescer = CreateCoalescer();
var firstCompletion = CreateCompletionSource<string>();
var secondCompletion = CreateCompletionSource<string>();
var firstStarted = CreateCompletionSource<bool>();
var secondStarted = CreateCompletionSource<bool>();
var requestCount = 0;
var first = coalescer.ExecuteAsync(
CreateKey(parameters: "symbol=ETH"),
async cancellationToken =>
{
Interlocked.Increment(ref requestCount);
firstStarted.TrySetResult(true);
return await firstCompletion.Task;
},
() => "canceled",
CancellationToken.None);
var second = coalescer.ExecuteAsync(
CreateKey(parameters: "symbol=BTC"),
async cancellationToken =>
{
Interlocked.Increment(ref requestCount);
secondStarted.TrySetResult(true);
return await secondCompletion.Task;
},
() => "canceled",
CancellationToken.None);
await Task.WhenAll(firstStarted.Task, secondStarted.Task).WaitAsync(_testTimeout);
Assert.That(requestCount, Is.EqualTo(2));
firstCompletion.SetResult("first");
secondCompletion.SetResult("second");
Assert.That(await first.WaitAsync(_testTimeout), Is.EqualTo("first"));
Assert.That(await second.WaitAsync(_testTimeout), Is.EqualTo("second"));
}
[Test]
public async Task ExecuteAsync_WhenOneListenerCancels_ShouldKeepSharedRequestRunning()
{
var coalescer = CreateCoalescer();
var requestCompletion = CreateCompletionSource<string>();
var requestStarted = CreateCompletionSource<CancellationToken>();
var requestCount = 0;
using var firstCancellationSource = new CancellationTokenSource();
async Task<string> ExecuteRequest(CancellationToken cancellationToken)
{
Interlocked.Increment(ref requestCount);
requestStarted.TrySetResult(cancellationToken);
return await requestCompletion.Task;
}
var first = coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "canceled", firstCancellationSource.Token);
var requestCancellationToken = await requestStarted.Task.WaitAsync(_testTimeout);
var second = coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "canceled", CancellationToken.None);
firstCancellationSource.Cancel();
Assert.That(await first.WaitAsync(_testTimeout), Is.EqualTo("canceled"));
Assert.That(requestCancellationToken.IsCancellationRequested, Is.False);
Assert.That(requestCount, Is.EqualTo(1));
requestCompletion.SetResult("result");
Assert.That(await second.WaitAsync(_testTimeout), Is.EqualTo("result"));
}
[Test]
public async Task ExecuteAsync_WhenAllListenersCancel_ShouldCancelSharedRequest()
{
var coalescer = CreateCoalescer();
var requestStarted = CreateCompletionSource<bool>();
var requestCanceled = CreateCompletionSource<bool>();
using var firstCancellationSource = new CancellationTokenSource();
using var secondCancellationSource = new CancellationTokenSource();
async Task<string> ExecuteRequest(CancellationToken cancellationToken)
{
using (cancellationToken.Register(() => requestCanceled.TrySetResult(true)))
{
requestStarted.TrySetResult(true);
await requestCanceled.Task;
return "request canceled";
}
}
var first = coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "listener canceled", firstCancellationSource.Token);
await requestStarted.Task.WaitAsync(_testTimeout);
var second = coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "listener canceled", secondCancellationSource.Token);
firstCancellationSource.Cancel();
Assert.That(await first.WaitAsync(_testTimeout), Is.EqualTo("listener canceled"));
Assert.That(requestCanceled.Task.IsCompleted, Is.False);
secondCancellationSource.Cancel();
Assert.That(await second.WaitAsync(_testTimeout), Is.EqualTo("listener canceled"));
Assert.That(await requestCanceled.Task.WaitAsync(_testTimeout), Is.True);
}
[Test]
public async Task ExecuteAsync_WithAlreadyCanceledListener_ShouldNotStartRequest()
{
var coalescer = CreateCoalescer();
var requestCount = 0;
using var cancellationSource = new CancellationTokenSource();
cancellationSource.Cancel();
var result = await coalescer.ExecuteAsync(
CreateKey(),
cancellationToken =>
{
Interlocked.Increment(ref requestCount);
return Task.FromResult("result");
},
() => "canceled",
cancellationSource.Token);
Assert.That(result, Is.EqualTo("canceled"));
Assert.That(requestCount, Is.Zero);
}
[Test]
public async Task ExecuteAsync_AfterRequestCompletes_ShouldStartNewRequest()
{
var coalescer = CreateCoalescer();
var requestCount = 0;
Task<int> ExecuteRequest(CancellationToken cancellationToken)
=> Task.FromResult(Interlocked.Increment(ref requestCount));
var first = await coalescer.ExecuteAsync(CreateKey(typeof(int)), ExecuteRequest, () => -1, CancellationToken.None);
var second = await coalescer.ExecuteAsync(CreateKey(typeof(int)), ExecuteRequest, () => -1, CancellationToken.None);
Assert.That(first, Is.EqualTo(1));
Assert.That(second, Is.EqualTo(2));
Assert.That(requestCount, Is.EqualTo(2));
}
[Test]
public void ExecuteAsync_AfterRequestFails_ShouldStartNewRequest()
{
var coalescer = CreateCoalescer();
var requestCount = 0;
Task<string> ExecuteRequest(CancellationToken cancellationToken)
{
if (Interlocked.Increment(ref requestCount) == 1)
return Task.FromException<string>(new InvalidOperationException("Request failed"));
return Task.FromResult("result");
}
Assert.ThrowsAsync<InvalidOperationException>(async () =>
await coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "canceled", CancellationToken.None));
Assert.That(
coalescer.ExecuteAsync(CreateKey(), ExecuteRequest, () => "canceled", CancellationToken.None).GetAwaiter().GetResult(),
Is.EqualTo("result"));
Assert.That(requestCount, Is.EqualTo(2));
}
[Test]
public async Task ExecuteAsync_WhenAbandonedRequestCompletes_ShouldNotRemoveReplacementRequest()
{
var coalescer = CreateCoalescer();
var key = CreateKey();
var oldRequestCompletion = CreateCompletionSource<string>();
var oldRequestStarted = CreateCompletionSource<bool>();
var replacementCompletion = CreateCompletionSource<string>();
var replacementStarted = CreateCompletionSource<bool>();
var requestCount = 0;
using var cancellationSource = new CancellationTokenSource();
async Task<string> ExecuteOldRequest(CancellationToken cancellationToken)
{
Interlocked.Increment(ref requestCount);
oldRequestStarted.TrySetResult(true);
return await oldRequestCompletion.Task;
}
async Task<string> ExecuteReplacementRequest(CancellationToken cancellationToken)
{
Interlocked.Increment(ref requestCount);
replacementStarted.TrySetResult(true);
return await replacementCompletion.Task;
}
var abandonedListener = coalescer.ExecuteAsync(key, ExecuteOldRequest, () => "canceled", cancellationSource.Token);
await oldRequestStarted.Task.WaitAsync(_testTimeout);
var oldPhysicalRequest = GetPhysicalRequest(coalescer, key);
cancellationSource.Cancel();
Assert.That(await abandonedListener.WaitAsync(_testTimeout), Is.EqualTo("canceled"));
var replacementListener = coalescer.ExecuteAsync(key, ExecuteReplacementRequest, () => "canceled", CancellationToken.None);
await replacementStarted.Task.WaitAsync(_testTimeout);
oldRequestCompletion.SetResult("old result");
await oldPhysicalRequest.WaitAsync(_testTimeout);
var joinedReplacementListener = coalescer.ExecuteAsync(key, ExecuteReplacementRequest, () => "canceled", CancellationToken.None);
Assert.That(requestCount, Is.EqualTo(2));
replacementCompletion.SetResult("replacement result");
Assert.That(await replacementListener.WaitAsync(_testTimeout), Is.EqualTo("replacement result"));
Assert.That(await joinedReplacementListener.WaitAsync(_testTimeout), Is.EqualTo("replacement result"));
Assert.That(requestCount, Is.EqualTo(2));
}
private static RequestCoalescer CreateCoalescer()
=> new RequestCoalescer(NullLogger.Instance);
private static RequestCoalescingKey CreateKey(Type? responseType = null, string? parameters = null)
=> new RequestCoalescingKey("GET", "https://localhost/test", parameters, responseType ?? typeof(string));
private static TaskCompletionSource<T> CreateCompletionSource<T>()
=> new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
private static Task GetPhysicalRequest(RequestCoalescer coalescer, RequestCoalescingKey key)
{
var requestsField = typeof(RequestCoalescer).GetField("_requests", BindingFlags.Instance | BindingFlags.NonPublic)!;
var requests = (IDictionary)requestsField.GetValue(coalescer)!;
var entry = requests[key]!;
var requestProperty = entry.GetType().GetProperty("Request")!;
return ((Lazy<Task<object>>)requestProperty.GetValue(entry)!).Value;
}
}
}
@@ -0,0 +1,355 @@
using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture]
internal class SharedApiClientTests
{
[Test]
public void GetCapability_ShouldUseCapabilityTradingModes()
{
var sharedApi = new TestSharedApi(
[TradingMode.Spot, TradingMode.PerpetualLinear],
[TradingMode.Spot]);
var client = new TestSharedApiClient(sharedApi);
Assert.Multiple(() =>
{
Assert.That(client.GetCapability<ITestCapability>(TradingMode.Spot)?.Capability, Is.SameAs(sharedApi));
Assert.That(client.GetCapability<ITestCapability>(TradingMode.PerpetualLinear), Is.Null);
Assert.That(client.GetCapabilities<ITestCapability>(TradingMode.Spot), Has.Count.EqualTo(1));
Assert.That(client.GetCapabilities<ITestCapability>(TradingMode.PerpetualLinear), Is.Empty);
});
}
[Test]
public void CapabilityTradingModes_ShouldDefaultToSharedApiTradingModes()
{
var sharedApi = new TestSharedApi(
[TradingMode.Spot, TradingMode.PerpetualLinear],
null);
var client = new TestSharedApiClient(sharedApi);
Assert.Multiple(() =>
{
Assert.That(client.GetCapability<ITestCapability>(TradingMode.Spot)?.Capability, Is.SameAs(sharedApi));
Assert.That(client.GetCapability<ITestCapability>(TradingMode.PerpetualLinear)?.Capability, Is.SameAs(sharedApi));
});
}
[Test]
public void GetCapabilities_WithReference_ShouldReturnMatchingCapabilities()
{
var sharedApi = new TestSharedApi(
[TradingMode.Spot],
[TradingMode.Spot]);
var client = new TestSharedApiClient(sharedApi);
var result = client.GetCapabilities(
new SharedCapabilityReference<ITestCapability>(),
TradingMode.Spot);
Assert.That(result, Has.Count.EqualTo(1));
Assert.That(result[0].Capability, Is.SameAs(sharedApi));
Assert.That(result[0].Options, Is.SameAs(sharedApi.CapabilityOptions));
}
[Test]
public void Discover_ShouldReturnAllSharedApiInformation()
{
var restApi = new TestRestSharedApi();
var socketApi = new TestSocketSharedApi();
var client = new TestDiSharedApiClient(
restApi,
socketApi);
var result = client.Discover();
Assert.Multiple(() =>
{
Assert.That(result.PreferredTransport, Is.EqualTo(SharedTransport.Socket));
Assert.That(result.SharedApis, Has.Length.EqualTo(2));
Assert.That(result.SharedApis[0].Transport, Is.EqualTo(SharedTransport.Rest));
Assert.That(result.SharedApis[1].Transport, Is.EqualTo(SharedTransport.Socket));
Assert.That(result.SharedApis[0].SupportedTradingModes, Is.EqualTo(new[] { TradingMode.Spot }));
Assert.That(result.SharedApis[0].Capabilities, Has.Length.EqualTo(1));
Assert.That(result.SharedApis[0].Authenticated, Is.False);
});
}
[Test]
public void RegisterSharedApiClient_ShouldResolvePreferredTransport()
{
var restApi = new TestRestSharedApi();
var socketApi = new TestSocketSharedApi();
var services = new ServiceCollection();
services.AddSingleton(restApi);
services.AddSingleton(socketApi);
services.RegisterSharedApiClient<
ITestSharedApiClient,
TestDiSharedApiClient>(sharedApis => sharedApis
.Add(client => client.Rest)
.Add(client => client.Socket));
using var provider = services.BuildServiceProvider();
Assert.Multiple(() =>
{
Assert.That(
provider.GetRequiredService<ITestSharedApiClient>(),
Is.Not.Null);
Assert.That(
provider.GetRequiredService<ISharedApiClientBase>(),
Is.InstanceOf<TestDiSharedApiClient>());
Assert.That(
provider.GetRequiredService<ITestCapability>(),
Is.SameAs(socketApi));
Assert.That(
provider.GetServices<ITestCapability>().ToArray(),
Is.EqualTo(new[] { socketApi }));
Assert.That(
provider.GetRequiredService<ITestRestCapability>(),
Is.SameAs(restApi));
Assert.That(
provider.GetRequiredService<ITestSocketCapability>(),
Is.SameAs(socketApi));
});
}
[Test]
public void CapabilityTradingModes_ShouldIntersectApiApplicableAndOverrideModes()
{
var sharedApi = new TestSharedApi(
[TradingMode.Spot, TradingMode.PerpetualLinear, TradingMode.DeliveryLinear],
[TradingMode.Spot, TradingMode.PerpetualLinear],
[TradingMode.PerpetualLinear, TradingMode.DeliveryLinear]);
Assert.That(
sharedApi.CapabilityOptions.SupportedTradingModes,
Is.EqualTo(new[] { TradingMode.PerpetualLinear }));
}
[Test]
public void CapabilityOptions_ShouldApplyIntrinsicTradingModes()
{
var apiModes = new[]
{
TradingMode.Spot,
TradingMode.PerpetualLinear,
TradingMode.DeliveryLinear,
TradingMode.PerpetualInverse,
TradingMode.DeliveryInverse
};
var spotOptions = new PlaceSpotOrderOptions("TestExchange");
var futuresOptions = new GetPositionsOptions("TestExchange", false);
var perpetualOptions = new GetFundingRateHistoryOptions(
"TestExchange",
true,
true,
true,
100,
false);
var unrestrictedOptions = new GetTickerOptions("TestExchange");
_ = new TestOptionsHost(apiModes, spotOptions);
_ = new TestOptionsHost(apiModes, futuresOptions);
_ = new TestOptionsHost(apiModes, perpetualOptions);
_ = new TestOptionsHost(apiModes, unrestrictedOptions);
Assert.Multiple(() =>
{
Assert.That(spotOptions.SupportedTradingModes, Is.EqualTo(new[] { TradingMode.Spot }));
Assert.That(futuresOptions.SupportedTradingModes, Is.EqualTo(new[]
{
TradingMode.PerpetualLinear,
TradingMode.DeliveryLinear,
TradingMode.PerpetualInverse,
TradingMode.DeliveryInverse
}));
Assert.That(perpetualOptions.SupportedTradingModes, Is.EqualTo(new[]
{
TradingMode.PerpetualLinear,
TradingMode.PerpetualInverse
}));
Assert.That(unrestrictedOptions.SupportedTradingModes, Is.EqualTo(apiModes));
});
}
private interface ITestCapability : ISharedApiCapability
{
}
private sealed class TestApiClient : IBaseApiClient
{
public string Exchange => "TestExchange";
public string BaseAddress => "https://test.invalid";
public string FormatSymbol(
string baseAsset,
string quoteAsset,
TradingMode tradingMode,
DateTime? deliverDate = null)
=> $"{baseAsset}{quoteAsset}";
public Task<TResult> WithRateLimitAdmissionAsync<TResult>(
RateLimitAdmission admission,
Func<Task<TResult>> operation)
=> operation();
}
private interface ITestRestCapability : ITestCapability, ISharedRest
{
}
private interface ITestSocketCapability : ITestCapability, ISharedSocket
{
}
private interface ITestRestSharedApi : ITestRestCapability
{
}
private interface ITestSocketSharedApi : ITestSocketCapability
{
}
private interface ITestSharedApiClient : ISharedApiClientBase
{
ITestRestSharedApi Rest { get; }
ITestSocketSharedApi Socket { get; }
}
private sealed class TestCapabilityOptions : CapabilityOptions<SharedRequest, ITestCapability>
{
public override string Description => "Test capability";
public TestCapabilityOptions(TradingMode[]? applicableTradingModes)
: base("TestExchange", false, "TestOperation", [], applicableTradingModes)
{
}
}
private sealed class TestSharedApi : SharedApiBase, ITestCapability
{
public TestCapabilityOptions CapabilityOptions { get; }
public TestSharedApi(TradingMode[] apiTradingModes, TradingMode[]? capabilityTradingModes)
: this(apiTradingModes, capabilityTradingModes, null)
{
}
public TestSharedApi(
TradingMode[] apiTradingModes,
TradingMode[]? capabilityTradingModes,
TradingMode[]? applicableTradingModes)
: base(
SharedTransport.Rest,
new TestApiClient(),
apiTradingModes,
() => false,
(baseAsset, quoteAsset, tradingMode, deliverDate) => $"{baseAsset}{quoteAsset}")
{
var options = new TestCapabilityOptions(applicableTradingModes)
{
SupportedTradingModeOverrides = capabilityTradingModes
};
SetCapabilities(options);
CapabilityOptions = options;
}
public override SharedClientInfo Discover() => new();
}
private sealed class TestOptionsHost : SharedApiBase
{
public TestOptionsHost(TradingMode[] apiTradingModes, CapabilityOptions capabilityOptions)
: base(
SharedTransport.Rest,
new TestApiClient(),
apiTradingModes,
() => false,
(baseAsset, quoteAsset, tradingMode, deliverDate) => $"{baseAsset}{quoteAsset}")
{
SetCapabilities(capabilityOptions);
}
public override SharedClientInfo Discover() => new();
}
private abstract class TestTransportSharedApi : SharedApiBase
{
protected TestTransportSharedApi(SharedTransport transport)
: base(
transport,
new TestApiClient(),
[TradingMode.Spot],
() => false,
(baseAsset, quoteAsset, tradingMode, deliverDate) => $"{baseAsset}{quoteAsset}")
{
SetCapabilities(new TestCapabilityOptions([TradingMode.Spot]));
}
public override SharedClientInfo Discover() => new()
{
Exchange = this.Exchange,
TypeName = GetType().Name,
SupportedTradingModes = this.SupportedTradingModes,
Transport = this.Transport,
Authenticated = this.Authenticated,
Capabilities = ((ISharedApi)this).Capabilities.ToArray()
};
}
private sealed class TestRestSharedApi : TestTransportSharedApi, ITestRestSharedApi
{
public TestRestSharedApi() : base(SharedTransport.Rest)
{
}
}
private sealed class TestSocketSharedApi : TestTransportSharedApi, ITestSocketSharedApi
{
public TestSocketSharedApi() : base(SharedTransport.Socket)
{
}
}
private sealed class TestDiSharedApiClient : SharedApiClientBase, ITestSharedApiClient
{
public ITestRestSharedApi Rest { get; }
public ITestSocketSharedApi Socket { get; }
public TestDiSharedApiClient(
TestRestSharedApi rest,
TestSocketSharedApi socket)
: base(SharedTransport.Socket, rest, socket)
{
Rest = rest;
Socket = socket;
}
}
private sealed class TestSharedApiClient : SharedApiClientBase
{
public TestSharedApiClient(params ISharedApiCapability[] sharedApis)
: base(SharedTransport.Rest, sharedApis)
{
}
}
}
}
@@ -0,0 +1,89 @@
using CryptoExchange.Net.SharedApis;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture]
internal class SharedCapabilitiesTests
{
[Test]
public void AllSharedApiCapabilities_ShouldBeListedInSharedCapabilities()
{
var markerTypes = new[]
{
typeof(ISharedApiCapability),
typeof(ISharedRest),
typeof(ISharedSocket),
typeof(ISharedSubscription)
};
var expectedCapabilities = typeof(ISharedApiCapability).Assembly
.GetTypes()
.Where(x => x.IsInterface
&& typeof(ISharedApiCapability).IsAssignableFrom(x)
&& !markerTypes.Contains(x))
.OrderBy(x => x.FullName)
.ToArray();
var listedCapabilities = GetTypeAndNestedTypes(typeof(SharedCapabilities))
.SelectMany(x => x.GetProperties(
BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly))
.SelectMany(x => x.PropertyType.GetGenericArguments())
.Where(x => x.IsInterface
&& typeof(ISharedApiCapability).IsAssignableFrom(x))
.ToArray();
var missingCapabilities = expectedCapabilities
.Except(listedCapabilities)
.Select(x => x.Name)
.ToArray();
var unknownCapabilities = listedCapabilities
.Except(expectedCapabilities)
.Select(x => x.Name)
.ToArray();
var duplicateCapabilities = listedCapabilities
.GroupBy(x => x)
.Where(x => x.Count() > 1)
.Select(x => x.Key.Name)
.ToArray();
Assert.Multiple(() =>
{
Assert.That(
missingCapabilities,
Is.Empty,
$"Capabilities missing from SharedCapabilities: " +
string.Join(", ", missingCapabilities));
Assert.That(
unknownCapabilities,
Is.Empty,
$"Unknown capabilities listed in SharedCapabilities: " +
string.Join(", ", unknownCapabilities));
Assert.That(
duplicateCapabilities,
Is.Empty,
$"Capabilities listed multiple times in SharedCapabilities: " +
string.Join(", ", duplicateCapabilities));
});
}
private static IEnumerable<Type> GetTypeAndNestedTypes(Type type)
{
yield return type;
foreach (var nestedType in type.GetNestedTypes(BindingFlags.Public))
{
foreach (var result in GetTypeAndNestedTypes(nestedType))
yield return result;
}
}
}
}
@@ -0,0 +1,180 @@
using NUnit.Framework;
using CryptoExchange.Net.SharedApis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace CryptoExchange.Net.UnitTests
{
[TestFixture]
internal class SharedParametersTests
{
[Test]
public void AllSharedAPIOptions_ShouldListAllDefaultParametersInRequestParameterRules()
{
var failures = new List<string>();
var optionsTypes = typeof(CapabilityOptions).Assembly
.GetTypes()
.Where(x => x.IsClass
&& !x.IsAbstract
&& typeof(CapabilityOptions).IsAssignableFrom(x))
.OrderBy(x => x.FullName)
.ToArray();
foreach (var optionsType in optionsTypes)
{
var genericOptionsType = GetGenericOptionsType(optionsType);
if (genericOptionsType == null)
{
if (optionsType == typeof(ClosePositionOptions))
continue;
failures.Add($"{optionsType.Name}: unable to determine request type");
continue;
}
var requestType = genericOptionsType.GetGenericArguments()[0];
var defaultRulesField = GetDefaultParameterRulesField(optionsType);
if (defaultRulesField == null)
{
failures.Add($"{optionsType.Name}: no _defaultParameterRules field found");
continue;
}
var rules = (RequestParameterDescription[]?)defaultRulesField.GetValue(null);
if (rules == null)
{
failures.Add($"{optionsType.Name}: _defaultParameterRules is null");
continue;
}
var constructorParameterNames = requestType
.GetConstructors(BindingFlags.Instance | BindingFlags.Public)
.SelectMany(x => x.GetParameters())
.Where(x => !string.Equals(
x.Name,
"exchangeParameters",
StringComparison.OrdinalIgnoreCase))
.Select(x => x.Name!)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var parameterProperties = requestType
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.DeclaringType == requestType
|| constructorParameterNames.Contains(x.Name))
.Where(x => x.Name != nameof(SharedRequest.ExchangeParameters))
.Select(x => x.Name)
.OrderBy(x => x)
.ToArray();
var ruleNames = rules
.Select(x => x.Name)
.ToHashSet(StringComparer.Ordinal);
foreach (var parameterProperty in parameterProperties)
{
if (!ruleNames.Contains(parameterProperty))
{
failures.Add(
$"{optionsType.Name}: request property " +
$"{requestType.Name}.{parameterProperty} has no default request parameter rule");
}
}
}
Assert.That(
failures,
Is.Empty,
$"Missing default request parameter rules:{Environment.NewLine}" +
string.Join(Environment.NewLine, failures));
}
[Test]
public void RequiredExchangeParameter_ShouldAcceptNameOrAnyAlias()
{
const string exchange = "TestExchange";
var options = new GetTickerOptions(exchange)
{
ExchangeParameterRules =
[
ExchangeParameterRule.Required(
"Parameter",
"Test parameter",
1,
"Alias1",
"Alias2")
]
};
var missingResult = options.ValidateRequest(null, null, [TradingMode.Spot]);
var nameResult = options.ValidateRequest(
new ExchangeParameters(new ExchangeParameter(exchange, "Parameter", 1)),
null,
[TradingMode.Spot]);
var firstAliasResult = options.ValidateRequest(
new ExchangeParameters(new ExchangeParameter(exchange, "Alias1", 1)),
null,
[TradingMode.Spot]);
var secondAliasResult = options.ValidateRequest(
new ExchangeParameters(new ExchangeParameter(exchange, "Alias2", 1)),
null,
[TradingMode.Spot]);
Assert.Multiple(() =>
{
Assert.That(missingResult, Is.Not.Null);
Assert.That(nameResult, Is.Null);
Assert.That(firstAliasResult, Is.Null);
Assert.That(secondAliasResult, Is.Null);
});
}
[Test]
public void RequiredExchangeParameterWithoutAliases_ShouldBeRequired()
{
const string exchange = "TestExchange";
var options = new GetTickerOptions(exchange)
{
ExchangeParameterRules =
[
ExchangeParameterRule.Required(
"Parameter",
"Test parameter",
1)
]
};
var result = options.ValidateRequest(null, null, [TradingMode.Spot]);
Assert.That(result, Is.Not.Null);
}
private static Type? GetGenericOptionsType(Type optionsType)
{
for (var type = optionsType; type != null; type = type.BaseType)
{
if (type.IsGenericType
&& type.GetGenericTypeDefinition() == typeof(CapabilityOptions<,>))
return type;
}
return null;
}
private static FieldInfo? GetDefaultParameterRulesField(Type optionsType)
{
for (var type = optionsType; type != null; type = type.BaseType)
{
var field = type.GetField(
"_defaultParameterRules",
BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
if (field != null)
return field;
}
return null;
}
}
}
+32 -1
View File
@@ -1,10 +1,13 @@
using System;
using CryptoExchange.Net.Interfaces.Clients; using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Objects.Errors; using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Clients namespace CryptoExchange.Net.Clients
{ {
@@ -78,6 +81,11 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]); protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
/// <summary>
/// Current rate limit admission override
/// </summary>
protected readonly AsyncLocal<RateLimitAdmission?> AdmissionOverride = new();
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
@@ -122,6 +130,29 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message); public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
/// <inheritdoc />
public async Task<TResult> WithRateLimitAdmissionAsync<TResult>(
RateLimitAdmission admission,
Func<Task<TResult>> operation)
{
if (admission == null)
throw new ArgumentNullException(nameof(admission));
if (operation == null)
throw new ArgumentNullException(nameof(operation));
var previous = AdmissionOverride.Value;
AdmissionOverride.Value = admission;
try
{
return await operation().ConfigureAwait(false);
}
finally
{
AdmissionOverride.Value = previous;
}
}
/// <summary> /// <summary>
/// Dispose /// Dispose
/// </summary> /// </summary>
+100 -2
View File
@@ -98,6 +98,10 @@ namespace CryptoExchange.Net.Clients
/// Configured environment name /// Configured environment name
/// </summary> /// </summary>
public abstract string EnvironmentName { get; } public abstract string EnvironmentName { get; }
/// <summary>
/// Request coalescer
/// </summary>
protected RequestCoalescer RequestCoalescer { get; }
/// <summary> /// <summary>
/// ctor /// ctor
@@ -123,6 +127,7 @@ namespace CryptoExchange.Net.Clients
{ {
TimeOffsetManager.RegisterRestApi(ClientName); TimeOffsetManager.RegisterRestApi(ClientName);
RequestCoalescer = new RequestCoalescer(_logger);
RequestFactory.Configure(options, httpClient); RequestFactory.Configure(options, httpClient);
} }
@@ -177,8 +182,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param> /// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param> /// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param> /// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <returns></returns> protected virtual Task<HttpResult<T>> SendAsync<T>(
protected virtual async Task<HttpResult<T>> SendAsync<T>(
RequestDefinition definition, RequestDefinition definition,
Parameters? uriParameters, Parameters? uriParameters,
Parameters? bodyParameters, Parameters? bodyParameters,
@@ -187,6 +191,61 @@ namespace CryptoExchange.Net.Clients
int? weight = null, int? weight = null,
int? weightSingleLimiter = null, int? weightSingleLimiter = null,
string? rateLimitKeySuffix = null) string? rateLimitKeySuffix = null)
{
if (!ShouldCoalesce(definition, additionalHeaders))
{
return SendCoreAsync<T>(
definition,
uriParameters,
bodyParameters,
cancellationToken,
additionalHeaders,
weight,
weightSingleLimiter,
rateLimitKeySuffix);
}
var key = GetRequestCoalescingKey<T>(
definition,
uriParameters);
return RequestCoalescer.ExecuteAsync(
key,
requestCancellationToken => SendCoreAsync<T>(
definition,
uriParameters,
bodyParameters,
requestCancellationToken,
additionalHeaders,
weight,
weightSingleLimiter,
rateLimitKeySuffix),
() => HttpResult.Fail<T>(
Exchange,
new CancellationRequestedError()),
cancellationToken);
}
/// <summary>
/// Send a request to the base address based on the request definition
/// </summary>
/// <typeparam name="T">Response type</typeparam>
/// <param name="definition">Request definition</param>
/// <param name="uriParameters">Request query parameters</param>
/// <param name="bodyParameters">Request body parameters</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
protected virtual async Task<HttpResult<T>> SendCoreAsync<T>(RequestDefinition definition,
Parameters? uriParameters,
Parameters? bodyParameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null,
int? weightSingleLimiter = null,
string? rateLimitKeySuffix = null)
{ {
var requestId = ExchangeHelpers.NextId(); var requestId = ExchangeHelpers.NextId();
if (definition.Authenticated && GetAuthenticationProvider() == null) if (definition.Authenticated && GetAuthenticationProvider() == null)
@@ -291,6 +350,9 @@ namespace CryptoExchange.Net.Clients
if (ClientOptions.RateLimiterEnabled) if (ClientOptions.RateLimiterEnabled)
{ {
var admissionOverride = AdmissionOverride.Value ?? ClientOptions.RateLimitAdmission?.Invoke(definition, requestWeight);
var rateRatio = admissionOverride?.MaxUtilizationRatio ?? 1.0;
var limitResult = await definition.RateLimitGate.ProcessAsync( var limitResult = await definition.RateLimitGate.ProcessAsync(
_logger, _logger,
requestId, requestId,
@@ -300,6 +362,7 @@ namespace CryptoExchange.Net.Clients
requestWeight, requestWeight,
ClientOptions.RateLimitingBehaviour, ClientOptions.RateLimitingBehaviour,
rateLimitKeySuffix + ClientOptions.RateLimitGroup, rateLimitKeySuffix + ClientOptions.RateLimitGroup,
rateRatio,
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
if (!limitResult.Success) if (!limitResult.Success)
return limitResult.Error!; return limitResult.Error!;
@@ -315,6 +378,9 @@ namespace CryptoExchange.Net.Clients
if (ClientOptions.RateLimiterEnabled) if (ClientOptions.RateLimiterEnabled)
{ {
var singleRequestWeight = weightSingleLimiter ?? 1; var singleRequestWeight = weightSingleLimiter ?? 1;
var admissionOverride = AdmissionOverride.Value ?? ClientOptions.RateLimitAdmission?.Invoke(definition, singleRequestWeight);
var rateRatio = admissionOverride?.MaxUtilizationRatio ?? 1.0;
var limitResult = await definition.RateLimitGate.ProcessSingleAsync( var limitResult = await definition.RateLimitGate.ProcessSingleAsync(
_logger, _logger,
requestId, requestId,
@@ -325,6 +391,7 @@ namespace CryptoExchange.Net.Clients
singleRequestWeight, singleRequestWeight,
ClientOptions.RateLimitingBehaviour, ClientOptions.RateLimitingBehaviour,
rateLimitKeySuffix, rateLimitKeySuffix,
rateRatio,
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
if (!limitResult.Success) if (!limitResult.Success)
return limitResult.Error!; return limitResult.Error!;
@@ -688,7 +755,10 @@ namespace CryptoExchange.Net.Clients
stringData = stringSerializer.Serialize(parameters.BodyValue); stringData = stringSerializer.Serialize(parameters.BodyValue);
} }
else else
{
stringData = stringSerializer.Serialize(parameters); stringData = stringSerializer.Serialize(parameters);
}
request.SetContent(stringData, RequestBodyContentEncoding, contentType); request.SetContent(stringData, RequestBodyContentEncoding, contentType);
} }
else if (contentType == Constants.FormContentHeader) else if (contentType == Constants.FormContentHeader)
@@ -808,6 +878,34 @@ namespace CryptoExchange.Net.Clients
return definition.FullUrl + definition + parameters?.ToFormData(); return definition.FullUrl + definition + parameters?.ToFormData();
} }
/// <summary>
/// Whether a request can be coalesced or not
/// </summary>
protected virtual bool ShouldCoalesce(
RequestDefinition definition,
Dictionary<string, string>? additionalHeaders)
{
return ClientOptions.RequestCoalescingEnabled // Enabled in client options
&& !definition.PreventRequestCoalescing // Enabled in request definition
&& definition.Method == HttpMethod.Get // Is a GET request
&& !definition.Authenticated // Not an authenticated request
&& (additionalHeaders == null || additionalHeaders.Count == 0); // No additional headers that might change the response
}
/// <summary>
/// Get a unique key for coalescing requests based on the request definition, parameters, and the type of the expected response.
/// </summary>
protected virtual RequestCoalescingKey GetRequestCoalescingKey<T>(
RequestDefinition definition,
Parameters? uriParameters)
{
return new RequestCoalescingKey(
definition.Method.Method,
definition.FullUrl,
uriParameters?.ToFormData(),
typeof(T));
}
/// <inheritdoc /> /// <inheritdoc />
public virtual void SetOptions(UpdateOptions options) public virtual void SetOptions(UpdateOptions options)
{ {
@@ -858,6 +858,8 @@ namespace CryptoExchange.Net.Clients
ReconnectInterval = ClientOptions.ReconnectInterval, ReconnectInterval = ClientOptions.ReconnectInterval,
RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null, RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null,
RateLimitingBehavior = ClientOptions.RateLimitingBehaviour, RateLimitingBehavior = ClientOptions.RateLimitingBehaviour,
RateLimitAdmissionCallbackOptions = (def, weight) => ClientOptions.RateLimitAdmission?.Invoke(def, weight),
RateLimitAdmissionCallbackRequest = () => AdmissionOverride.Value,
Proxy = ClientOptions.Proxy, Proxy = ClientOptions.Proxy,
Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout, Timeout = ApiOptions.SocketNoDataTimeout ?? ClientOptions.SocketNoDataTimeout,
ReceiveBufferSize = ClientOptions.ReceiveBufferSize, ReceiveBufferSize = ClientOptions.ReceiveBufferSize,
+64 -8
View File
@@ -1,15 +1,20 @@
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Threading.Tasks;
using System.Web; using System.Web;
namespace CryptoExchange.Net namespace CryptoExchange.Net
@@ -145,25 +150,42 @@ namespace CryptoExchange.Net
/// <returns></returns> /// <returns></returns>
public static string ToFormData(this IDictionary<string, object> parameters) public static string ToFormData(this IDictionary<string, object> parameters)
{ {
var formData = HttpUtility.ParseQueryString(string.Empty); var result = new StringBuilder();
var first = true;
void Append(string key, object value)
{
if (!first)
result.Append('&');
first = false;
if (!string.IsNullOrEmpty(key))
{
result.Append(HttpUtility.UrlEncode(key, Encoding.UTF8));
result.Append('=');
}
var text = string.Format(CultureInfo.InvariantCulture, "{0}", value);
result.Append(HttpUtility.UrlEncode(text, Encoding.UTF8));
}
foreach (var kvp in parameters) foreach (var kvp in parameters)
{ {
if (kvp.Value is null) if (kvp.Value is null)
continue; continue;
if (kvp.Value.GetType().IsArray) if (kvp.Value is Array array)
{ {
var array = (Array)kvp.Value;
foreach (var value in array) foreach (var value in array)
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", value)); Append(kvp.Key, value);
} }
else else
{ {
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value)); Append(kvp.Key, kvp.Value);
} }
} }
return formData.ToString()!; return result.ToString();
} }
/// <summary> /// <summary>
@@ -395,7 +417,6 @@ namespace CryptoExchange.Net
services.AddTransient(x => (IBookTickerRestClient)client(x)!); services.AddTransient(x => (IBookTickerRestClient)client(x)!);
if (typeof(ITransferRestClient).IsAssignableFrom(typeof(T))) if (typeof(ITransferRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ITransferRestClient)client(x)!); services.AddTransient(x => (ITransferRestClient)client(x)!);
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T))) if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotOrderRestClient)client(x)!); services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
if (typeof(ISpotSymbolRestClient).IsAssignableFrom(typeof(T))) if (typeof(ISpotSymbolRestClient).IsAssignableFrom(typeof(T)))
@@ -493,6 +514,41 @@ namespace CryptoExchange.Net
return bytes; return bytes;
} }
/// <summary>
/// Execute an operation with a specific rate limit admission rule
/// </summary>
/// <param name="client">Client</param>
/// <param name="admission">Rate limit admission rule</param>
/// <param name="operation">Operation to execute</param>
public static Task<TResult> WithRateLimitAdmissionAsync<TClient, TResult>(
this TClient client,
RateLimitAdmission admission,
Func<TClient, Task<TResult>> operation)
where TClient : IRateLimitAdmissionClient
{
return client.WithRateLimitAdmissionAsync(
admission,
() => operation(client));
}
/// <summary>
/// Return the task results in the form of an IAsyncEnumerable, returning the first completed task first
/// </summary>
/// <typeparam name="T">Type of task result</typeparam>
/// <param name="tasks">Task list</param>
public static async IAsyncEnumerable<T> ParallelEnumerateAsync<T>(this IEnumerable<Task<T>> tasks)
{
var remaining = new List<Task<T>>(tasks);
while (remaining.Count != 0)
{
var task = await Task.WhenAny(remaining).ConfigureAwait(false);
remaining.Remove(task);
yield return await task.ConfigureAwait(false);
}
}
} }
} }
@@ -1,6 +1,4 @@
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using System; using System;
namespace CryptoExchange.Net.Interfaces.Clients namespace CryptoExchange.Net.Interfaces.Clients
@@ -8,7 +6,7 @@ namespace CryptoExchange.Net.Interfaces.Clients
/// <summary> /// <summary>
/// Base api client /// Base api client
/// </summary> /// </summary>
public interface IBaseApiClient public interface IBaseApiClient : IRateLimitAdmissionClient
{ {
/// <summary> /// <summary>
/// Exchange name /// Exchange name
@@ -0,0 +1,22 @@
using CryptoExchange.Net.RateLimiting;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// A client supporting rate limit admission rules
/// </summary>
public interface IRateLimitAdmissionClient
{
/// <summary>
/// Execute an operation with a specific rate limit admission rule
/// </summary>
Task<TResult> WithRateLimitAdmissionAsync<TResult>(
RateLimitAdmission admission,
Func<Task<TResult>> operation);
}
}
@@ -22,6 +22,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, string, Exception?> _restApiCacheHit; private static readonly Action<ILogger, string, Exception?> _restApiCacheHit;
private static readonly Action<ILogger, string, Exception?> _restApiCacheNotHit; private static readonly Action<ILogger, string, Exception?> _restApiCacheNotHit;
private static readonly Action<ILogger, int?, Exception?> _restApiCancellationRequested; private static readonly Action<ILogger, int?, Exception?> _restApiCancellationRequested;
private static readonly Action<ILogger, string, string, Exception?> _restApiRequestsJoined;
static RestApiClientLoggingExtensions() static RestApiClientLoggingExtensions()
{ {
@@ -90,6 +91,10 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(4012, "RestApiCancellationRequested"), new EventId(4012, "RestApiCancellationRequested"),
"[Req {RequestId}] request cancelled by user"); "[Req {RequestId}] request cancelled by user");
_restApiRequestsJoined = LoggerMessage.Define<string, string>(
LogLevel.Debug,
new EventId(4013, "RestApiRequestsJoined"),
"Request for {Method} {Path} joined with in flight request");
} }
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error, string? originalData, Exception? exception) public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error, string? originalData, Exception? exception)
@@ -151,9 +156,15 @@ namespace CryptoExchange.Net.Logging.Extensions
{ {
_restApiCacheNotHit(logger, key, null); _restApiCacheNotHit(logger, key, null);
} }
public static void RestApiCancellationRequested(this ILogger logger, int? requestId) public static void RestApiCancellationRequested(this ILogger logger, int? requestId)
{ {
_restApiCancellationRequested(logger, requestId, null); _restApiCancellationRequested(logger, requestId, null);
} }
public static void RestApiRequestsJoined(this ILogger logger, string method, string path)
{
_restApiRequestsJoined(logger, method, path, null);
}
} }
} }
@@ -1,4 +1,5 @@
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.RateLimiting;
using System; using System;
namespace CryptoExchange.Net.Objects.Options namespace CryptoExchange.Net.Objects.Options
@@ -41,6 +42,12 @@ namespace CryptoExchange.Net.Objects.Options
/// </summary> /// </summary>
public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait; public RateLimitingBehaviour RateLimitingBehaviour { get; set; } = RateLimitingBehaviour.Wait;
/// <summary>
/// A delegate receiving the request definition and the request weight that can be used to determine whether a request should be admitted or rejected based
/// on the request definition and the current rate limit usage ratio. This allows for custom rate limiting logic to be implemented.
/// </summary>
public Func<RequestDefinition, int, RateLimitAdmission>? RateLimitAdmission { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() public override string ToString()
{ {
@@ -34,6 +34,11 @@ namespace CryptoExchange.Net.Objects.Options
= new Version(1, 1); = new Version(1, 1);
#endif #endif
/// <summary>
/// Whether concurrent identical REST requests may share the same in-flight request when allowed by the request definition.
/// </summary>
public bool RequestCoalescingEnabled { get; set; } = true;
/// <summary> /// <summary>
/// Http client keep alive interval for keeping connections open. Only applied when using dotnet8.0 or higher and dependency injection /// Http client keep alive interval for keeping connections open. Only applied when using dotnet8.0 or higher and dependency injection
/// </summary> /// </summary>
@@ -70,9 +75,11 @@ namespace CryptoExchange.Net.Objects.Options
item.RateLimiterEnabled = RateLimiterEnabled; item.RateLimiterEnabled = RateLimiterEnabled;
item.RateLimitingBehaviour = RateLimitingBehaviour; item.RateLimitingBehaviour = RateLimitingBehaviour;
item.RateLimitGroup = RateLimitGroup; item.RateLimitGroup = RateLimitGroup;
item.RateLimitAdmission = RateLimitAdmission;
item.CachingEnabled = CachingEnabled; item.CachingEnabled = CachingEnabled;
item.CachingMaxAge = CachingMaxAge; item.CachingMaxAge = CachingMaxAge;
item.HttpVersion = HttpVersion; item.HttpVersion = HttpVersion;
item.RequestCoalescingEnabled = RequestCoalescingEnabled;
item.HttpKeepAliveInterval = HttpKeepAliveInterval; item.HttpKeepAliveInterval = HttpKeepAliveInterval;
#if NET5_0_OR_GREATER #if NET5_0_OR_GREATER
item.HttpMaxConnectionsPerServer = HttpMaxConnectionsPerServer; item.HttpMaxConnectionsPerServer = HttpMaxConnectionsPerServer;
@@ -105,6 +105,7 @@ namespace CryptoExchange.Net.Objects.Options
item.RateLimitingBehaviour = RateLimitingBehaviour; item.RateLimitingBehaviour = RateLimitingBehaviour;
item.RateLimiterEnabled = RateLimiterEnabled; item.RateLimiterEnabled = RateLimiterEnabled;
item.RateLimitGroup = RateLimitGroup; item.RateLimitGroup = RateLimitGroup;
item.RateLimitAdmission = RateLimitAdmission;
item.ReceiveBufferSize = ReceiveBufferSize; item.ReceiveBufferSize = ReceiveBufferSize;
return item; return item;
} }
@@ -0,0 +1,277 @@
using CryptoExchange.Net.Logging.Extensions;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Request coalescer, used to detect multiple identical requests and wait for and return only a single actual request result
/// </summary>
public class RequestCoalescer
{
private sealed class RequestEntry
{
public RequestCoalescingKey Key { get; }
public CancellationTokenSource CancellationSource { get; } = new();
public Lazy<Task<object>> Request { get; set; } = null!;
public int ListenerCount { get; set; }
public bool Completed { get; set; }
public bool CancellationInProgress { get; set; }
public bool Disposed { get; set; }
public RequestEntry(RequestCoalescingKey key)
{
Key = key;
}
}
private readonly object _sync = new();
private readonly ILogger _logger;
private readonly Dictionary<RequestCoalescingKey, RequestEntry> _requests = new();
/// <summary>
/// ctor
/// </summary>
public RequestCoalescer(ILogger logger)
{
_logger = logger;
}
/// <summary>
/// Execute a request, coalescing identical requests into a single request
/// </summary>
public Task<TResult> ExecuteAsync<TResult>(
RequestCoalescingKey key,
Func<CancellationToken, Task<TResult>> requestFactory,
Func<TResult> cancellationResultFactory,
CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
return Task.FromResult(cancellationResultFactory());
RequestEntry entry;
bool joined;
lock (_sync)
{
joined = _requests.TryGetValue(key, out entry!);
if (!joined)
{
entry = CreateEntry(key, requestFactory);
_requests.Add(key, entry);
}
entry.ListenerCount++;
}
if (joined)
_logger.RestApiRequestsJoined(key.Method, key.Url);
return WaitAsync(
entry,
cancellationResultFactory,
cancellationToken);
}
private RequestEntry CreateEntry<TResult>(
RequestCoalescingKey key,
Func<CancellationToken, Task<TResult>> requestFactory)
{
var entry = new RequestEntry(key);
entry.Request = new Lazy<Task<object>>(
() => ExecuteCoreAsync(
entry,
async requestCancellationToken =>
(await requestFactory(requestCancellationToken).ConfigureAwait(false))!),
LazyThreadSafetyMode.ExecutionAndPublication);
return entry;
}
private async Task<object> ExecuteCoreAsync(
RequestEntry entry,
Func<CancellationToken, Task<object>> requestFactory)
{
try
{
return await requestFactory(entry.CancellationSource.Token).ConfigureAwait(false);
}
finally
{
CompleteRequest(entry);
}
}
private async Task<TResult> WaitAsync<TResult>(
RequestEntry entry,
Func<TResult> cancellationResultFactory,
CancellationToken cancellationToken)
{
try
{
var requestTask = entry.Request.Value;
if (!cancellationToken.CanBeCanceled)
return (TResult)await requestTask.ConfigureAwait(false);
var cancellationCompletion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
using (cancellationToken.Register(
state => ((TaskCompletionSource<bool>)state!).TrySetResult(true),
cancellationCompletion))
{
var completedTask = await Task.WhenAny(requestTask, cancellationCompletion.Task).ConfigureAwait(false);
if (completedTask != requestTask)
return cancellationResultFactory();
}
return (TResult)await requestTask.ConfigureAwait(false);
}
finally
{
ReleaseListener(entry);
}
}
private void ReleaseListener(RequestEntry entry)
{
var cancelRequest = false;
var disposeCancellationSource = false;
lock (_sync)
{
entry.ListenerCount--;
if (entry.ListenerCount == 0)
{
if (!entry.Completed)
{
// Remove the entry before canceling it. A caller arriving
// after this point must create a new physical request
// instead of joining one that is being canceled.
RemoveEntry(entry);
entry.CancellationInProgress = true;
cancelRequest = true;
}
else if (!entry.Disposed)
{
entry.Disposed = true;
disposeCancellationSource = true;
}
}
}
if (cancelRequest)
{
try
{
entry.CancellationSource.Cancel();
}
catch (Exception exception)
{
// Cancellation callbacks are external to the coalescer and
// must not cause an exception to escape the library.
_logger.LogWarning(
exception,
"Error canceling in-flight request {Method} {Url}",
entry.Key.Method,
entry.Key.Url);
}
finally
{
CompleteCancellation(entry);
}
}
else if (disposeCancellationSource)
{
entry.CancellationSource.Dispose();
}
}
private void CompleteCancellation(RequestEntry entry)
{
var disposeCancellationSource = false;
lock (_sync)
{
entry.CancellationInProgress = false;
if (entry.Completed
&& entry.ListenerCount == 0
&& !entry.Disposed)
{
entry.Disposed = true;
disposeCancellationSource = true;
}
}
if (disposeCancellationSource)
entry.CancellationSource.Dispose();
}
private void CompleteRequest(RequestEntry entry)
{
var disposeCancellationSource = false;
lock (_sync)
{
entry.Completed = true;
// Only remove this specific entry. If all listeners canceled,
// a replacement request may already have been created for the
// same key.
RemoveEntry(entry);
if (entry.ListenerCount == 0
&& !entry.CancellationInProgress
&& !entry.Disposed)
{
entry.Disposed = true;
disposeCancellationSource = true;
}
}
if (disposeCancellationSource)
entry.CancellationSource.Dispose();
}
/// <summary>
/// Remove an entry when it is still the current entry for its key.
/// Must only be called while holding <see cref="_sync"/>.
/// </summary>
private void RemoveEntry(RequestEntry entry)
{
if (_requests.TryGetValue(entry.Key, out var current)
&& ReferenceEquals(current, entry))
{
_requests.Remove(entry.Key);
}
}
}
/// <summary>
/// Key used to identify identical requests for coalescing
/// </summary>
/// <param name="Method">Request method</param>
/// <param name="Url">Request URL</param>
/// <param name="Parameters">Request parameter string</param>
/// <param name="ResponseType">Response type</param>
public sealed record RequestCoalescingKey(
string Method,
string Url,
string? Parameters,
Type ResponseType);
}
@@ -101,6 +101,11 @@ namespace CryptoExchange.Net.Objects
} }
} }
/// <summary>
/// Whether concurrent identical requests should be prevented from sharing the same request/response
/// </summary>
public bool PreventRequestCoalescing { get; set; }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
@@ -53,6 +53,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param> /// <param name="tryParseOnNonSuccess">Try parse the response even when status is not success</param>
/// <param name="forcePathEndWithSlash">Force trailing `/`</param> /// <param name="forcePathEndWithSlash">Force trailing `/`</param>
/// <param name="identifier">Optional request identifier override</param> /// <param name="identifier">Optional request identifier override</param>
/// <param name="preventRequestCoalescing">Whether concurrent identical requests should be prevented from sharing the same request/response</param>
/// <returns></returns> /// <returns></returns>
public RequestDefinition GetOrCreate( public RequestDefinition GetOrCreate(
HttpMethod method, HttpMethod method,
@@ -68,7 +69,8 @@ namespace CryptoExchange.Net.Objects
bool? preventCaching = null, bool? preventCaching = null,
bool? tryParseOnNonSuccess = null, bool? tryParseOnNonSuccess = null,
bool? forcePathEndWithSlash = null, bool? forcePathEndWithSlash = null,
string? identifier = null) string? identifier = null,
bool? preventRequestCoalescing = null)
{ {
var identifierToUse = identifier ?? $"{path}{method.Method}{baseAddress}"; var identifierToUse = identifier ?? $"{path}{method.Method}{baseAddress}";
if (!_definitions.TryGetValue(identifierToUse, out var def)) if (!_definitions.TryGetValue(identifierToUse, out var def))
@@ -85,6 +87,7 @@ namespace CryptoExchange.Net.Objects
PreventCaching = preventCaching ?? false, PreventCaching = preventCaching ?? false,
TryParseOnNonSuccess = tryParseOnNonSuccess ?? false, TryParseOnNonSuccess = tryParseOnNonSuccess ?? false,
ForcePathEndWithSlash = forcePathEndWithSlash ?? false, ForcePathEndWithSlash = forcePathEndWithSlash ?? false,
PreventRequestCoalescing = preventRequestCoalescing ?? false,
}; };
_definitions.TryAdd(identifierToUse, def); _definitions.TryAdd(identifierToUse, def);
} }
@@ -97,7 +97,7 @@ public record CallResult<T> : CallResult, ICallResult<T>
/// Call result for an exchange /// Call result for an exchange
/// </summary> /// </summary>
/// <typeparam name="T">Data type</typeparam> /// <typeparam name="T">Data type</typeparam>
public record ExchangeCallResult<T> : CallResult<T> public record ExchangeCallResult<T> : CallResult<T>, IExchangeCallResult<T>
{ {
/// <summary> /// <summary>
/// Exchange name /// Exchange name
@@ -41,3 +41,22 @@ public interface ICallResult<T> : ICallResult
/// </summary> /// </summary>
T? Data { get; } T? Data { get; }
} }
/// <summary>
/// Exchange call result
/// </summary>
public interface IExchangeCallResult : ICallResult
{
/// <summary>
/// Exchange name
/// </summary>
public string Exchange { get; }
}
/// <summary>
/// Exchange call result
/// </summary>
/// <typeparam name="T">Result data type</typeparam>
public interface IExchangeCallResult<T> : IExchangeCallResult, ICallResult<T>
{
}
@@ -10,12 +10,8 @@ namespace CryptoExchange.Net.Objects
/// <summary> /// <summary>
/// HTTP call result /// HTTP call result
/// </summary> /// </summary>
public interface IHttpResult : ICallResult public interface IHttpResult : ICallResult, IExchangeCallResult
{ {
/// <summary>
/// Exchange name
/// </summary>
string Exchange { get; init; }
/// <summary> /// <summary>
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options /// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
/// </summary> /// </summary>
@@ -79,7 +75,7 @@ namespace CryptoExchange.Net.Objects
/// HTTP call result /// HTTP call result
/// </summary> /// </summary>
/// <typeparam name="T">Result data type</typeparam> /// <typeparam name="T">Result data type</typeparam>
public interface IHttpResult<T> : IHttpResult, ICallResult<T> public interface IHttpResult<T> : IHttpResult, IExchangeCallResult<T>
{ {
} }
} }
@@ -10,13 +10,8 @@ namespace CryptoExchange.Net.Objects
/// <summary> /// <summary>
/// WebSocket call result /// WebSocket call result
/// </summary> /// </summary>
public interface IWebSocketResult : ICallResult public interface IWebSocketResult : ICallResult, IExchangeCallResult
{ {
/// <summary>
/// Exchange name
/// </summary>
string Exchange { get; init; }
/// <summary> /// <summary>
/// The request id /// The request id
/// </summary> /// </summary>
@@ -42,7 +37,7 @@ namespace CryptoExchange.Net.Objects
/// WebSocket call result /// WebSocket call result
/// </summary> /// </summary>
/// <typeparam name="T">Data result type</typeparam> /// <typeparam name="T">Data result type</typeparam>
public interface IWebSocketResult<T> : IWebSocketResult, ICallResult<T> public interface IWebSocketResult<T> : IWebSocketResult, IExchangeCallResult<T>
{ {
} }
@@ -165,7 +165,10 @@ namespace CryptoExchange.Net.Objects.Sockets
{ {
StreamId = StreamId, StreamId = StreamId,
UpdateType = UpdateType, UpdateType = UpdateType,
Symbol = Symbol Symbol = Symbol,
DataTime = DataTime,
DataTimeLocal = DataTimeLocal,
SequenceNumber = SequenceNumber
}; };
} }
@@ -1,4 +1,5 @@
using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
@@ -64,6 +65,17 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary> /// </summary>
public RateLimitingBehaviour RateLimitingBehavior { get; set; } public RateLimitingBehaviour RateLimitingBehavior { get; set; }
/// <summary>
/// A delegate receiving the request definition and the request weight that can be used to determine whether a request should be admitted or rejected based
/// on the request definition and the current rate limit usage ratio. This allows for custom rate limiting logic to be implemented.
/// </summary>
public Func<RequestDefinition, int, RateLimitAdmission?>? RateLimitAdmissionCallbackOptions { get; set; }
/// <summary>
/// A delegate for retrieving the current rate limit admission decision for a specific request. This allows for custom rate limiting logic to be implemented.
/// </summary>
public Func<RateLimitAdmission?>? RateLimitAdmissionCallbackRequest { get; set; }
/// <summary> /// <summary>
/// Encoding for sending/receiving data /// Encoding for sending/receiving data
/// </summary> /// </summary>
@@ -36,6 +36,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
private readonly Dictionary<string, IWindowTracker> _trackers; private readonly Dictionary<string, IWindowTracker> _trackers;
private readonly RateLimitWindowType _windowType; private readonly RateLimitWindowType _windowType;
private readonly double? _decayRate; private readonly double? _decayRate;
private readonly TimeSpan _safetyMargin;
private readonly int? _connectionWeight; private readonly int? _connectionWeight;
private readonly Func<RequestDefinition, string?, string> _keySelector; private readonly Func<RequestDefinition, string?, string> _keySelector;
private readonly SemaphoreSlim? _sharedGuardSemaphore; private readonly SemaphoreSlim? _sharedGuardSemaphore;
@@ -54,6 +55,10 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// The time period for the limit /// The time period for the limit
/// </summary> /// </summary>
public TimeSpan TimeSpan { get; } public TimeSpan TimeSpan { get; }
/// <summary>
/// Additional time to wait after a rate limit window expires to account for latency and timing differences
/// </summary>
public TimeSpan SafetyMargin => _safetyMargin;
/// <summary> /// <summary>
/// Whether this guard is shared between multiple gates /// Whether this guard is shared between multiple gates
@@ -68,11 +73,12 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <param name="limit">Limit per period</param> /// <param name="limit">Limit per period</param>
/// <param name="timeSpan">Timespan for the period</param> /// <param name="timeSpan">Timespan for the period</param>
/// <param name="windowType">Type of rate limit window</param> /// <param name="windowType">Type of rate limit window</param>
/// <param name="safetyMargin">Additional time to wait after a window expires, or null to use the default margin</param>
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param> /// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
/// <param name="connectionWeight">The weight of a new connection</param> /// <param name="connectionWeight">The weight of a new connection</param>
/// <param name="shared">Whether this guard is shared between multiple gates</param> /// <param name="shared">Whether this guard is shared between multiple gates</param>
public RateLimitGuard(Func<RequestDefinition, string?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false) public RateLimitGuard(Func<RequestDefinition, string?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false, TimeSpan? safetyMargin = null)
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight, shared) : this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight, shared, safetyMargin)
{ {
} }
@@ -84,10 +90,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <param name="limit">Limit per period</param> /// <param name="limit">Limit per period</param>
/// <param name="timeSpan">Timespan for the period</param> /// <param name="timeSpan">Timespan for the period</param>
/// <param name="windowType">Type of rate limit window</param> /// <param name="windowType">Type of rate limit window</param>
/// <param name="safetyMargin">Additional time to wait after a window expires, or null to use the default margin</param>
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param> /// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
/// <param name="connectionWeight">The weight of a new connection</param> /// <param name="connectionWeight">The weight of a new connection</param>
/// <param name="shared">Whether this guard is shared between multiple gates</param> /// <param name="shared">Whether this guard is shared between multiple gates</param>
public RateLimitGuard(Func<RequestDefinition, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false) public RateLimitGuard(Func<RequestDefinition, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null, bool shared = false, TimeSpan? safetyMargin = null)
{ {
_filters = filters; _filters = filters;
_trackers = new Dictionary<string, IWindowTracker>(); _trackers = new Dictionary<string, IWindowTracker>();
@@ -97,6 +104,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
SharedGuard = shared; SharedGuard = shared;
_keySelector = keySelector; _keySelector = keySelector;
_decayRate = decayPerTimeSpan; _decayRate = decayPerTimeSpan;
_safetyMargin = safetyMargin ?? WindowTrackerHelpers.GetDefaultSafetyMargin(timeSpan); ;
_connectionWeight = connectionWeight; _connectionWeight = connectionWeight;
if (SharedGuard) if (SharedGuard)
@@ -104,7 +112,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
} }
/// <inheritdoc /> /// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix) public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix, double allowedRateRatio)
{ {
foreach (var filter in _filters) foreach (var filter in _filters)
{ {
@@ -127,7 +135,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
_trackers.Add(key, tracker); _trackers.Add(key, tracker);
} }
var delay = tracker.GetWaitTime(requestWeight); var delay = tracker.GetWaitTime(requestWeight, allowedRateRatio);
if (delay == default) if (delay == default)
return LimitCheck.NotNeeded(Limit, TimeSpan, tracker.Current); return LimitCheck.NotNeeded(Limit, TimeSpan, tracker.Current);
@@ -205,9 +213,9 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <returns></returns> /// <returns></returns>
protected IWindowTracker CreateTracker() protected IWindowTracker CreateTracker()
{ {
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(Limit, TimeSpan) return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(Limit, TimeSpan, _safetyMargin)
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(Limit, TimeSpan) : _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(Limit, TimeSpan, _safetyMargin)
: _windowType == RateLimitWindowType.FixedAfterFirst ? new FixedAfterStartWindowTracker(Limit, TimeSpan) : : _windowType == RateLimitWindowType.FixedAfterFirst ? new FixedAfterStartWindowTracker(Limit, TimeSpan, _safetyMargin) :
new DecayWindowTracker(Limit, TimeSpan, _decayRate ?? throw new InvalidOperationException("Decay rate not provided")); new DecayWindowTracker(Limit, TimeSpan, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
} }
} }
@@ -42,7 +42,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
} }
/// <inheritdoc /> /// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix) public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix, double allowedRateRatio)
{ {
if (type != Type) if (type != Type)
return LimitCheck.NotApplicable; return LimitCheck.NotApplicable;
@@ -24,6 +24,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
private readonly Dictionary<string, IWindowTracker> _trackers; private readonly Dictionary<string, IWindowTracker> _trackers;
private readonly RateLimitWindowType _windowType; private readonly RateLimitWindowType _windowType;
private readonly double? _decayRate; private readonly double? _decayRate;
private readonly TimeSpan _safetyMargin;
private readonly int _limit; private readonly int _limit;
private readonly TimeSpan _period; private readonly TimeSpan _period;
private readonly Func<RequestDefinition, string?, string> _keySelector; private readonly Func<RequestDefinition, string?, string> _keySelector;
@@ -32,7 +33,12 @@ namespace CryptoExchange.Net.RateLimiting.Guards
public string Name => "EndpointLimitGuard"; public string Name => "EndpointLimitGuard";
/// <inheritdoc /> /// <inheritdoc />
public string Description => $"Limit requests to endpoint"; public string Description => _windowType == RateLimitWindowType.Decay ? $"Endpoint limit of {_limit} with a decay rate of {_decayRate}" : $"Limit of {_limit} per {_period}";
/// <summary>
/// Additional time to wait after a rate limit window expires to account for latency and timing differences
/// </summary>
public TimeSpan SafetyMargin => _safetyMargin;
/// <summary> /// <summary>
/// ctor /// ctor
@@ -41,19 +47,21 @@ namespace CryptoExchange.Net.RateLimiting.Guards
int limit, int limit,
TimeSpan period, TimeSpan period,
RateLimitWindowType windowType, RateLimitWindowType windowType,
double? decayRate = null, TimeSpan? safetyMargin = null,
Func<RequestDefinition, string?, string>? keySelector = null) Func<RequestDefinition, string?, string>? keySelector = null,
double? decayRate = null)
{ {
_limit = limit; _limit = limit;
_period = period; _period = period;
_windowType = windowType; _windowType = windowType;
_decayRate = decayRate; _decayRate = decayRate;
_safetyMargin = safetyMargin ?? WindowTrackerHelpers.GetDefaultSafetyMargin(period);
_keySelector = keySelector ?? Default; _keySelector = keySelector ?? Default;
_trackers = new Dictionary<string, IWindowTracker>(); _trackers = new Dictionary<string, IWindowTracker>();
} }
/// <inheritdoc /> /// <inheritdoc />
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix) public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix, double allowedRateRatio)
{ {
var key = _keySelector(definition, apiKey) + keySuffix; var key = _keySelector(definition, apiKey) + keySuffix;
if (!_trackers.TryGetValue(key, out var tracker)) if (!_trackers.TryGetValue(key, out var tracker))
@@ -62,7 +70,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
_trackers.Add(key, tracker); _trackers.Add(key, tracker);
} }
var delay = tracker.GetWaitTime(requestWeight); var delay = tracker.GetWaitTime(requestWeight, allowedRateRatio);
if (delay == default) if (delay == default)
return LimitCheck.NotNeeded(_limit, _period, tracker.Current); return LimitCheck.NotNeeded(_limit, _period, tracker.Current);
@@ -84,8 +92,8 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// <returns></returns> /// <returns></returns>
protected IWindowTracker CreateTracker() protected IWindowTracker CreateTracker()
{ {
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(_limit, _period) return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(_limit, _period, _safetyMargin)
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(_limit, _period) : : _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(_limit, _period, _safetyMargin) :
new DecayWindowTracker(_limit, _period, _decayRate ?? throw new InvalidOperationException("Decay rate not provided")); new DecayWindowTracker(_limit, _period, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
} }
@@ -53,9 +53,20 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="requestWeight">Request weight</param> /// <param name="requestWeight">Request weight</param>
/// <param name="behaviour">Behaviour when rate limit is hit</param> /// <param name="behaviour">Behaviour when rate limit is hit</param>
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param> /// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <param name="ct">Cancelation token</param> /// <param name="allowedRateRatio">The allowed rate ratio</param>
/// <param name="ct">Cancellation token</param>
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns> /// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
ValueTask<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct); ValueTask<CallResult> ProcessAsync(
ILogger logger,
int itemId,
RateLimitItemType type,
RequestDefinition definition,
string? apiKey,
int requestWeight,
RateLimitingBehaviour behaviour,
string? keySuffix,
double allowedRateRatio,
CancellationToken ct);
/// <summary> /// <summary>
/// Enforces the rate limit as defined in the request definition. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail /// Enforces the rate limit as defined in the request definition. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail
@@ -69,9 +80,21 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="behaviour">Behaviour when rate limit is hit</param> /// <param name="behaviour">Behaviour when rate limit is hit</param>
/// <param name="requestWeight">The weight to apply to the limit guard</param> /// <param name="requestWeight">The weight to apply to the limit guard</param>
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param> /// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <param name="allowedRateRatio">The allowed rate ratio</param>
/// <param name="ct">Cancelation token</param> /// <param name="ct">Cancelation token</param>
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns> /// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
ValueTask<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct); ValueTask<CallResult> ProcessSingleAsync(
ILogger logger,
int itemId,
IRateLimitGuard guard,
RateLimitItemType type,
RequestDefinition definition,
string? apiKey,
int requestWeight,
RateLimitingBehaviour behaviour,
string? keySuffix,
double allowedRateRatio,
CancellationToken ct);
/// <summary> /// <summary>
/// Reset the limit for the specified parameters /// Reset the limit for the specified parameters
@@ -25,8 +25,9 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="apiKey">The API key</param> /// <param name="apiKey">The API key</param>
/// <param name="requestWeight">The request weight</param> /// <param name="requestWeight">The request weight</param>
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param> /// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <param name="allowedRateRatio">The allowed rate ratio</param>
/// <returns></returns> /// <returns></returns>
LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix); LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, string? keySuffix, double allowedRateRatio);
/// <summary> /// <summary>
/// Apply the request to this guard with the specified weight /// Apply the request to this guard with the specified weight
@@ -22,9 +22,7 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <summary> /// <summary>
/// Get the time to wait to fit the weight /// Get the time to wait to fit the weight
/// </summary> /// </summary>
/// <param name="weight"></param> TimeSpan GetWaitTime(int weight, double allowedRateRatio);
/// <returns></returns>
TimeSpan GetWaitTime(int weight);
/// <summary> /// <summary>
/// Register the weight in this window /// Register the weight in this window
/// </summary> /// </summary>
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.RateLimiting
{
/// <summary>
/// Rate limit admission decision result
/// </summary>
public record RateLimitAdmission
{
/// <summary>
/// Value ratio between 0 and 1
/// </summary>
public double MaxUtilizationRatio { get; }
private RateLimitAdmission(double maxUtilizationValue) {
if (maxUtilizationValue <= 0 || maxUtilizationValue > 1)
throw new ArgumentOutOfRangeException(nameof(maxUtilizationValue), "Max utilization value must be bigger than 0 and less than or equal to 1");
MaxUtilizationRatio = maxUtilizationValue;
}
/// <summary>
/// Only allow the request when below a certain ratio of the rate limit, for example 0.5 means it can use a max of 50% of the rate limit,
/// 1 means it's allowed to use the full rate limit.
/// </summary>
/// <param name="value">0.5 means a max use 50% of the rate limit, 1 means the request is allowed to use the full rate limit</param>
public static RateLimitAdmission WithMaxUtilizationRatio(double value)
=> new RateLimitAdmission(value);
}
}
@@ -37,14 +37,24 @@ namespace CryptoExchange.Net.RateLimiting
} }
/// <inheritdoc /> /// <inheritdoc />
public async ValueTask<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct) public async ValueTask<CallResult> ProcessAsync(
ILogger logger,
int itemId,
RateLimitItemType type,
RequestDefinition definition,
string? apiKey,
int requestWeight,
RateLimitingBehaviour rateLimitingBehaviour,
string? keySuffix,
double allowedRateRatio,
CancellationToken ct)
{ {
await _semaphore.WaitAsync(ct).ConfigureAwait(false); await _semaphore.WaitAsync(ct).ConfigureAwait(false);
bool release = true; bool release = true;
_waitingCount++; _waitingCount++;
try try
{ {
return await CheckGuardsAsync(_guards, logger, itemId, type, definition, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false); return await CheckGuardsAsync(_guards, logger, itemId, type, definition, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, allowedRateRatio, ct).ConfigureAwait(false);
} }
catch (TaskCanceledException tce) catch (TaskCanceledException tce)
{ {
@@ -71,6 +81,7 @@ namespace CryptoExchange.Net.RateLimiting
int requestWeight, int requestWeight,
RateLimitingBehaviour rateLimitingBehaviour, RateLimitingBehaviour rateLimitingBehaviour,
string? keySuffix, string? keySuffix,
double allowedRateRatio,
CancellationToken ct) CancellationToken ct)
{ {
await _semaphore.WaitAsync(ct).ConfigureAwait(false); await _semaphore.WaitAsync(ct).ConfigureAwait(false);
@@ -78,7 +89,7 @@ namespace CryptoExchange.Net.RateLimiting
_waitingCount++; _waitingCount++;
try try
{ {
return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false); return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, allowedRateRatio, ct).ConfigureAwait(false);
} }
catch (TaskCanceledException tce) catch (TaskCanceledException tce)
{ {
@@ -94,12 +105,23 @@ namespace CryptoExchange.Net.RateLimiting
} }
} }
private async ValueTask<CallResult> CheckGuardsAsync(IEnumerable<IRateLimitGuard> guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, string? keySuffix, CancellationToken ct) private async ValueTask<CallResult> CheckGuardsAsync(
IEnumerable<IRateLimitGuard> guards,
ILogger logger,
int itemId,
RateLimitItemType type,
RequestDefinition definition,
string? apiKey,
int requestWeight,
RateLimitingBehaviour rateLimitingBehaviour,
string? keySuffix,
double allowedRateRatio,
CancellationToken ct)
{ {
foreach (var guard in guards) foreach (var guard in guards)
{ {
// Check if a wait is needed for this guard // Check if a wait is needed for this guard
var result = guard.Check(type, definition, apiKey, requestWeight, keySuffix); var result = guard.Check(type, definition, apiKey, requestWeight, keySuffix, allowedRateRatio);
if (result.Delay != TimeSpan.Zero && rateLimitingBehaviour == RateLimitingBehaviour.Fail) if (result.Delay != TimeSpan.Zero && rateLimitingBehaviour == RateLimitingBehaviour.Fail)
{ {
// Delay is needed and limit behaviour is to fail the request // Delay is needed and limit behaviour is to fail the request
@@ -109,7 +131,10 @@ namespace CryptoExchange.Net.RateLimiting
logger.RateLimitRequestFailed(itemId, definition.Path, guard.Name, guard.Description); logger.RateLimitRequestFailed(itemId, definition.Path, guard.Name, guard.Description);
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour)); RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
return CallResult.Fail(new ClientRateLimitError($"Rate limit check failed on guard {guard.Name}; {guard.Description}")); var errorMsg = $"Rate limit check failed on guard {guard.Name}; {guard.Description}";
if (allowedRateRatio < 1)
errorMsg += $" with allowed ratio {allowedRateRatio}";
return CallResult.Fail(new ClientRateLimitError(errorMsg));
} }
if (result.Delay != TimeSpan.Zero) if (result.Delay != TimeSpan.Zero)
@@ -117,7 +142,14 @@ namespace CryptoExchange.Net.RateLimiting
// Delay is needed and limit behaviour is to wait for the request to be under the limit // Delay is needed and limit behaviour is to wait for the request to be under the limit
_semaphore.Release(); _semaphore.Release();
var description = result.Limit == null ? guard.Description : $"{guard.Description}, Request weight: {requestWeight}, Current: {result.Current}, Limit: {result.Limit}, requests now being limited: {_waitingCount}"; var description =
result.Limit == null
? guard.Description
: $"{guard.Description}, Request weight: {requestWeight}," +
$" Current: {result.Current}" +
$", Limit: {result.Limit}" +
(allowedRateRatio < 1 ? $", Allowed ratio: {allowedRateRatio}" : "") +
$", requests now being limited: {_waitingCount}";
if (type == RateLimitItemType.Connection) if (type == RateLimitItemType.Connection)
logger.RateLimitDelayingConnection(itemId, result.Delay, guard.Name, description); logger.RateLimitDelayingConnection(itemId, result.Delay, guard.Name, description);
else else
@@ -126,7 +158,7 @@ namespace CryptoExchange.Net.RateLimiting
RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour)); RateLimitTriggered?.Invoke(new RateLimitEvent(itemId, _name, guard.Description, definition, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false); await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false);
await _semaphore.WaitAsync(ct).ConfigureAwait(false); await _semaphore.WaitAsync(ct).ConfigureAwait(false);
return await CheckGuardsAsync(guards, logger, itemId, type, definition, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false); return await CheckGuardsAsync(guards, logger, itemId, type, definition, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, allowedRateRatio, ct).ConfigureAwait(false);
} }
} }
@@ -18,9 +18,18 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
private int _currentWeight = 0; private int _currentWeight = 0;
private DateTime _lastDecrease = DateTime.UtcNow; private DateTime _lastDecrease = DateTime.UtcNow;
private double _decayProgress;
public DecayWindowTracker(int limit, TimeSpan period, double decayRate) public DecayWindowTracker(
int limit,
TimeSpan period,
double decayRate)
{ {
if (period <= TimeSpan.Zero)
throw new ArgumentOutOfRangeException(nameof(period));
if (decayRate <= 0)
throw new ArgumentOutOfRangeException(nameof(decayRate));
Limit = limit; Limit = limit;
TimePeriod = period; TimePeriod = period;
DecreaseRate = decayRate; DecreaseRate = decayRate;
@@ -31,73 +40,118 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
{ {
if (amount == null) if (amount == null)
{ {
_lastDecrease = DateTime.UtcNow; ResetState();
_currentWeight = 0; return;
}
else
{
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
} }
_currentWeight = Math.Max(0, _currentWeight - amount.Value);
if (_currentWeight == 0)
ResetState();
} }
/// <inheritdoc /> /// <inheritdoc />
public TimeSpan GetWaitTime(int weight) public TimeSpan GetWaitTime(int weight, double allowedRateRatio)
{ {
// Decrease the counter based on the last update time and decay rate var now = DateTime.UtcNow;
DecreaseCounter(DateTime.UtcNow); DecreaseCounter(now);
if (Current + weight > Limit) if ((Current + weight) / (double)Limit <= allowedRateRatio)
return TimeSpan.Zero;
if (Current == 0)
{ {
// The weight would cause the rate limit to be passed if (allowedRateRatio < 1)
if (Current == 0)
{ {
throw new Exception("Request limit reached without any prior request. " + throw new Exception(
$"This request can never execute with the current rate limiter. Request weight: {weight}, RateLimit: {Limit}"); "Request limit reached max utilization. " +
"This request can never execute with the current rate limiter configuration. " +
$"Request weight: {weight}, RateLimit: {Limit}, " +
$"Request ratio: {(Current + weight) / (double)Limit}, " +
$"AllowedRateRatio: {allowedRateRatio}");
} }
// Determine the time to wait before this weight can be applied without going over the rate limit throw new Exception(
return DetermineWaitTime(weight); "Request limit reached without any prior request. " +
"This request can never execute with the current rate limiter. " +
$"Request weight: {weight}, RateLimit: {Limit}");
} }
// Weight can fit without going over limit return DetermineWaitTime(
return TimeSpan.Zero; weight,
allowedRateRatio);
} }
/// <inheritdoc />
public void ApplyWeight(int weight) public void ApplyWeight(int weight)
{ {
if (_currentWeight == 0) if (_currentWeight == 0)
{
_lastDecrease = DateTime.UtcNow; _lastDecrease = DateTime.UtcNow;
_decayProgress = 0;
}
_currentWeight += weight; _currentWeight += weight;
} }
/// <summary> private void DecreaseCounter(DateTime now)
/// Decrease the counter based on time passed since last update and the decay rate
/// </summary>
/// <param name="time"></param>
protected void DecreaseCounter(DateTime time)
{ {
var dif = (time - _lastDecrease).TotalMilliseconds / TimePeriod.TotalMilliseconds * DecreaseRate; if (_currentWeight == 0)
var decrease = (int)Math.Floor(dif);
if (decrease >= 1)
{ {
_currentWeight = Math.Max(0, _currentWeight - (int)Math.Floor(dif)); _lastDecrease = now;
_lastDecrease = time; _decayProgress = 0;
return;
}
var elapsed = now - _lastDecrease;
if (elapsed <= TimeSpan.Zero)
return;
var elapsedDecay = elapsed.Ticks / (double)TimePeriod.Ticks * DecreaseRate;
var totalDecay = _decayProgress + elapsedDecay;
var completedDecay = (int)Math.Floor(totalDecay);
_lastDecrease = now;
if (completedDecay == 0)
{
_decayProgress = totalDecay;
return;
}
_currentWeight = Math.Max(0, _currentWeight - completedDecay);
if (_currentWeight == 0)
{
// Decay cannot accumulate as credit while the counter is empty.
_decayProgress = 0;
}
else
{
_decayProgress = totalDecay - completedDecay;
} }
} }
/// <summary> private TimeSpan DetermineWaitTime(
/// Determine the time to wait before the weight would fit int requestWeight,
/// </summary> double allowedRateRatio)
/// <param name="requestWeight"></param>
/// <returns></returns>
private TimeSpan DetermineWaitTime(int requestWeight)
{ {
var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0); var weightToRemove = Current + requestWeight - Limit * allowedRateRatio;
var result = TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds);
if (result < TimeSpan.Zero) // The counter is integer-valued, so enough whole weight units
return TimeSpan.Zero; // must decay before the request can be admitted.
return result; var requiredDecay = Math.Ceiling(weightToRemove);
var remainingDecay = Math.Max(0, requiredDecay - _decayProgress);
var waitTicks = Math.Ceiling(remainingDecay / DecreaseRate * TimePeriod.Ticks);
return waitTicks <= 0
? TimeSpan.Zero
: TimeSpan.FromTicks((long)waitTicks);
}
private void ResetState()
{
_currentWeight = 0;
_decayProgress = 0;
_lastDecrease = DateTime.UtcNow;
} }
} }
} }
@@ -17,15 +17,18 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
private int _currentWeight = 0; private int _currentWeight = 0;
private DateTime? _nextReset; private DateTime? _nextReset;
/// <summary> private readonly TimeSpan _safetyMargin;
/// Additional wait time to apply to account for time offset between server and client
/// </summary>
private static TimeSpan _fixedWindowBuffer = TimeSpan.FromMilliseconds(1000);
public FixedAfterStartWindowTracker(int limit, TimeSpan period) public FixedAfterStartWindowTracker(int limit, TimeSpan period)
: this(limit, period, WindowTrackerHelpers.GetDefaultSafetyMargin(period))
{
}
public FixedAfterStartWindowTracker(int limit, TimeSpan period, TimeSpan safetyMargin)
{ {
Limit = limit; Limit = limit;
TimePeriod = period; TimePeriod = period;
_safetyMargin = safetyMargin;
_entries = new Queue<LimitEntry>(); _entries = new Queue<LimitEntry>();
} }
@@ -53,23 +56,32 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
} }
} }
public TimeSpan GetWaitTime(int weight) public TimeSpan GetWaitTime(int weight, double allowedRateRatio)
{ {
// Remove requests no longer in time period from the history // Remove requests no longer in time period from the history
var checkTime = DateTime.UtcNow; var checkTime = DateTime.UtcNow;
if (_nextReset != null && checkTime > _nextReset) if (_nextReset != null && checkTime >= _nextReset + _safetyMargin)
RemoveBefore(_nextReset.Value); RemoveBefore(_nextReset.Value);
if (Current == 0) if (Current == 0)
_nextReset = null; _nextReset = null;
if (Current + weight > Limit) if ((Current + weight) / (double)Limit > allowedRateRatio)
{ {
// The weight would cause the rate limit to be passed // The weight would cause the rate limit to be passed
if (Current == 0) if (Current == 0)
{ {
throw new Exception("Request limit reached without any prior request. " + if (allowedRateRatio < 1)
$"This request can never execute with the current rate limiter. Request weight: {weight}, RateLimit: {Limit}"); {
throw new Exception("Request limit reached max utilization. " +
$"This request can never execute with the current rate limiter configuration. Request weight: {weight}, RateLimit: {Limit}, " +
$"Request ratio: {(Current + weight) / (double)Limit}, AllowedRateRatio: {allowedRateRatio}");
}
else
{
throw new Exception("Request limit reached without any prior request. " +
$"This request can never execute with the current rate limiter. Request weight: {weight}, RateLimit: {Limit}");
}
} }
// Determine the time to wait before this weight can be applied without going over the rate limit // Determine the time to wait before this weight can be applied without going over the rate limit
@@ -121,7 +133,7 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
private TimeSpan DetermineWaitTime() private TimeSpan DetermineWaitTime()
{ {
var checkTime = DateTime.UtcNow; var checkTime = DateTime.UtcNow;
var result = (_nextReset!.Value - checkTime) + _fixedWindowBuffer; var result = _nextReset!.Value + _safetyMargin - checkTime;
if (result < TimeSpan.Zero) if (result < TimeSpan.Zero)
return TimeSpan.Zero; return TimeSpan.Zero;
return result; return result;
@@ -16,15 +16,18 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
private readonly Queue<LimitEntry> _entries; private readonly Queue<LimitEntry> _entries;
private int _currentWeight = 0; private int _currentWeight = 0;
/// <summary> private readonly TimeSpan _safetyMargin;
/// Additional wait time to apply to account for time offset between server and client
/// </summary>
private static readonly TimeSpan _fixedWindowBuffer = TimeSpan.FromMilliseconds(1000);
public FixedWindowTracker(int limit, TimeSpan period) public FixedWindowTracker(int limit, TimeSpan period)
: this(limit, period, WindowTrackerHelpers.GetDefaultSafetyMargin(period))
{
}
public FixedWindowTracker(int limit, TimeSpan period, TimeSpan safetyMargin)
{ {
Limit = limit; Limit = limit;
TimePeriod = period; TimePeriod = period;
_safetyMargin = safetyMargin;
_entries = new Queue<LimitEntry>(); _entries = new Queue<LimitEntry>();
} }
@@ -52,19 +55,32 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
} }
/// <inheritdoc /> /// <inheritdoc />
public TimeSpan GetWaitTime(int weight) public TimeSpan GetWaitTime(int weight, double allowedRateRatio)
{ {
// Remove requests no longer in time period from the history // Remove requests no longer in time period from the history
var checkTime = DateTime.UtcNow; var checkTime = DateTime.UtcNow;
RemoveBefore(checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks))); var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks));
var resetBoundary = checkTime >= startCurrentWindow + _safetyMargin
? startCurrentWindow
: startCurrentWindow - TimePeriod;
RemoveBefore(resetBoundary);
if (Current + weight > Limit) if ((Current + weight) / (double)Limit > allowedRateRatio)
{ {
// The weight would cause the rate limit to be passed // The weight would cause the rate limit to be passed
if (Current == 0) if (Current == 0)
{ {
throw new Exception("Request limit reached without any prior request. " + if (allowedRateRatio < 1)
$"This request can never execute with the current rate limiter. Request weight: {weight}, RateLimit: {Limit}"); {
throw new Exception("Request limit reached max utilization. " +
$"This request can never execute with the current rate limiter configuration. Request weight: {weight}, RateLimit: {Limit}, " +
$"Request ratio: {(Current + weight) / (double)Limit}, AllowedRateRatio: {allowedRateRatio}");
}
else
{
throw new Exception("Request limit reached without any prior request. " +
$"This request can never execute with the current rate limiter. Request weight: {weight}, RateLimit: {Limit}");
}
} }
// Determine the time to wait before this weight can be applied without going over the rate limit // Determine the time to wait before this weight can be applied without going over the rate limit
@@ -115,8 +131,9 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
{ {
var checkTime = DateTime.UtcNow; var checkTime = DateTime.UtcNow;
var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks)); var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks));
var wait = startCurrentWindow.Add(TimePeriod) - checkTime; var result = checkTime < startCurrentWindow + _safetyMargin
var result = wait.Add(_fixedWindowBuffer); ? startCurrentWindow + _safetyMargin - checkTime
: startCurrentWindow + TimePeriod + _safetyMargin - checkTime;
if (result < TimeSpan.Zero) if (result < TimeSpan.Zero)
return TimeSpan.Zero; return TimeSpan.Zero;
return result; return result;
@@ -16,15 +16,18 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
private readonly List<LimitEntry> _entries; private readonly List<LimitEntry> _entries;
private int _currentWeight = 0; private int _currentWeight = 0;
/// <summary> private readonly TimeSpan _safetyMargin;
/// Additional wait time to apply to account for fluctuating request times
/// </summary>
private static readonly TimeSpan _slidingWindowBuffer = TimeSpan.FromMilliseconds(1000);
public SlidingWindowTracker(int limit, TimeSpan period) public SlidingWindowTracker(int limit, TimeSpan period)
: this(limit, period, WindowTrackerHelpers.GetDefaultSafetyMargin(period))
{
}
public SlidingWindowTracker(int limit, TimeSpan period, TimeSpan safetyMargin)
{ {
Limit = limit; Limit = limit;
TimePeriod = period; TimePeriod = period;
_safetyMargin = safetyMargin;
_entries = new List<LimitEntry>(); _entries = new List<LimitEntry>();
} }
@@ -53,22 +56,31 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
} }
/// <inheritdoc /> /// <inheritdoc />
public TimeSpan GetWaitTime(int weight) public TimeSpan GetWaitTime(int weight, double allowedRateRatio)
{ {
// Remove requests no longer in time period from the history // Remove requests no longer in time period from the history
RemoveBefore(DateTime.UtcNow - TimePeriod); RemoveBefore(DateTime.UtcNow - TimePeriod - _safetyMargin);
if (Current + weight > Limit) if ((Current + weight) / (double)Limit > allowedRateRatio)
{ {
// The weight would cause the rate limit to be passed // The weight would cause the rate limit to be passed
if (Current == 0) if (Current == 0)
{ {
throw new Exception("Request limit reached without any prior request. " + if (allowedRateRatio < 1)
$"This request can never execute with the current rate limiter. Request weight: {weight}, RateLimit: {Limit}"); {
throw new Exception("Request limit reached max utilization. " +
$"This request can never execute with the current rate limiter configuration. Request weight: {weight}, RateLimit: {Limit}, " +
$"Request ratio: {(Current + weight) / (double)Limit}, AllowedRateRatio: {allowedRateRatio}");
}
else
{
throw new Exception("Request limit reached without any prior request. " +
$"This request can never execute with the current rate limiter. Request weight: {weight}, RateLimit: {Limit}");
}
} }
// Determine the time to wait before this weight can be applied without going over the rate limit // Determine the time to wait before this weight can be applied without going over the rate limit
return DetermineWaitTime(weight); return DetermineWaitTime(weight, allowedRateRatio);
} }
// Weight can fit without going over limit // Weight can fit without going over limit
@@ -90,7 +102,7 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
{ {
for (var i = 0; i < _entries.Count; i++) for (var i = 0; i < _entries.Count; i++)
{ {
if (_entries[i].Timestamp < time) if (_entries[i].Timestamp <= time)
{ {
var entry = _entries[i]; var entry = _entries[i];
_entries.Remove(entry); _entries.Remove(entry);
@@ -108,9 +120,9 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
/// Determine the time to wait before the weight would fit /// Determine the time to wait before the weight would fit
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
private TimeSpan DetermineWaitTime(int requestWeight) private TimeSpan DetermineWaitTime(int requestWeight, double allowedRateRatio)
{ {
var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0); var weightToRemove = Math.Max(Current + requestWeight - Limit * allowedRateRatio, 0);
var removedWeight = 0; var removedWeight = 0;
for (var i = 0; i < _entries.Count; i++) for (var i = 0; i < _entries.Count; i++)
{ {
@@ -118,7 +130,7 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
removedWeight += entry.Weight; removedWeight += entry.Weight;
if (removedWeight >= weightToRemove) if (removedWeight >= weightToRemove)
{ {
var result = entry.Timestamp + TimePeriod + _slidingWindowBuffer - DateTime.UtcNow; var result = entry.Timestamp + TimePeriod + _safetyMargin - DateTime.UtcNow;
if (result < TimeSpan.Zero) if (result < TimeSpan.Zero)
return TimeSpan.Zero; return TimeSpan.Zero;
return result; return result;
@@ -0,0 +1,20 @@
using System;
namespace CryptoExchange.Net.RateLimiting.Trackers
{
internal static class WindowTrackerHelpers
{
private static readonly TimeSpan _minimumSafetyMargin = TimeSpan.FromMilliseconds(10);
private static readonly TimeSpan _maximumSafetyMargin = TimeSpan.FromMilliseconds(250);
public static TimeSpan GetDefaultSafetyMargin(TimeSpan period)
{
var margin = TimeSpan.FromTicks(period.Ticks / 20);
if (margin < _minimumSafetyMargin)
return _minimumSafetyMargin;
if (margin > _maximumSafetyMargin)
return _maximumSafetyMargin;
return margin;
}
}
}
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Catalog of Shared API capability references.
/// </summary>
public static partial class SharedCapabilities
{
/// <summary>
/// Asset capabilities.
/// </summary>
public static class Assets
{
/// <summary>
/// Get asset capability.
/// </summary>
public static SharedRestCapability<IGetAsset, IGetAssetRest> GetAsset { get; } = new();
/// <summary>
/// Get all assets capability.
/// </summary>
public static SharedRestCapability<IGetAllAssets, IGetAllAssetsRest> GetAllAssets { get; } = new();
}
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Balance capabilities.
/// </summary>
public static class Balances
{
/// <summary>
/// Get balances capability.
/// </summary>
public static SharedRestCapability<IGetBalances, IGetBalancesRest> GetBalances { get; } = new();
/// <summary>
/// Subscribe to balance updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeBalancesSocket> SubscribeBalances { get; } = new();
}
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Deposit capabilities.
/// </summary>
public static class Deposits
{
/// <summary>
/// Get deposit addresses capability.
/// </summary>
public static SharedRestCapability<IGetDepositAddresses, IGetDepositAddressesRest> GetDepositAddresses { get; } = new();
/// <summary>
/// Get deposit history capability.
/// </summary>
public static SharedRestCapability<IGetDepositHistory, IGetDepositHistoryRest> GetDepositHistory { get; } = new();
}
}
}
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Fee capabilities.
/// </summary>
public static class Fees
{
/// <summary>
/// Get fees capability.
/// </summary>
public static SharedRestCapability<IGetFees, IGetFeesRest> GetFees { get; } = new();
}
}
}
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Funding capabilities.
/// </summary>
public static class Funding
{
/// <summary>
/// Get current funding information capability.
/// </summary>
public static SharedRestCapability<IGetFundingInfo, IGetFundingInfoRest> GetFundingInfo { get; } = new();
/// <summary>
/// Get funding rate history capability.
/// </summary>
public static SharedRestCapability<IGetFundingRateHistory, IGetFundingRateHistoryRest> GetFundingRateHistory { get; } = new();
/// <summary>
/// Get user funding history capability.
/// </summary>
public static SharedRestCapability<IGetUserFundingHistory, IGetUserFundingHistoryRest> GetUserFundingHistory { get; } = new();
}
}
}
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Index price capabilities.
/// </summary>
public static class IndexPrices
{
/// <summary>
/// Get an index price capability.
/// </summary>
public static SharedRestCapability<IGetIndexPrice, IGetIndexPriceRest> GetIndexPrice { get; } = new();
/// <summary>
/// Get all index prices capability.
/// </summary>
public static SharedRestCapability<IGetAllIndexPrices, IGetAllIndexPricesRest> GetAllIndexPrices { get; } = new();
/// <summary>
/// Subscribe to index price updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeIndexPriceSocket> SubscribeIndexPrice { get; } = new();
}
}
}
@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Kline capabilities.
/// </summary>
public static class Klines
{
/// <summary>
/// Get klines capability.
/// </summary>
public static SharedRestCapability<IGetKlines, IGetKlinesRest> GetKlines { get; } = new();
/// <summary>
/// Get mark price klines capability.
/// </summary>
public static SharedRestCapability<IGetMarkPriceKlines, IGetMarkPriceKlinesRest> GetMarkPriceKlines { get; } = new();
/// <summary>
/// Get index price klines capability.
/// </summary>
public static SharedRestCapability<IGetIndexPriceKlines, IGetIndexPriceKlinesRest> GetIndexPriceKlines { get; } = new();
/// <summary>
/// Subscribe to kline updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeKlinesSocket> SubscribeKlines { get; } = new();
}
}
}
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Ledger capabilities.
/// </summary>
public static class Ledger
{
/// <summary>
/// Get ledger entries capability.
/// </summary>
public static SharedRestCapability<IGetLedger, IGetLedgerRest> GetLedger { get; } = new();
}
}
}
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Leverage capabilities.
/// </summary>
public static class Leverage
{
/// <summary>
/// Get leverage capability.
/// </summary>
public static SharedRestCapability<IGetLeverage, IGetLeverageRest> GetLeverage { get; } = new();
/// <summary>
/// Set leverage capability.
/// </summary>
public static SharedRestCapability<ISetLeverage, ISetLeverageRest> SetLeverage { get; } = new();
/// <summary>
/// Get leverage tiers capability.
/// </summary>
public static SharedRestCapability<IGetLeverageTiers, IGetLeverageTiersRest> GetLeverageTiers { get; } = new();
}
}
}
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Mark price capabilities.
/// </summary>
public static class MarkPrices
{
/// <summary>
/// Get a mark price capability.
/// </summary>
public static SharedRestCapability<IGetMarkPrice, IGetMarkPriceRest> GetMarkPrice { get; } = new();
/// <summary>
/// Get all mark prices capability.
/// </summary>
public static SharedRestCapability<IGetAllMarkPrices, IGetAllMarkPricesRest> GetAllMarkPrices { get; } = new();
/// <summary>
/// Subscribe to mark price updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeMarkPriceSocket> SubscribeMarkPrice { get; } = new();
}
}
}
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Open interest capabilities.
/// </summary>
public static class OpenInterest
{
/// <summary>
/// Get open interest capability.
/// </summary>
public static SharedRestCapability<IGetOpenInterest, IGetOpenInterestRest> GetOpenInterest { get; } = new();
}
}
}
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Order book capabilities.
/// </summary>
public static class OrderBooks
{
/// <summary>
/// Get an order book capability.
/// </summary>
public static SharedRestCapability<IGetOrderBook, IGetOrderBookRest> GetOrderBook { get; } = new();
/// <summary>
/// Get a book ticker capability.
/// </summary>
public static SharedRestCapability<IGetBookTicker, IGetBookTickerRest> GetBookTicker { get; } = new();
/// <summary>
/// Subscribe to order book updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeOrderBookSocket> SubscribeOrderBook { get; } = new();
/// <summary>
/// Subscribe to incremental order book updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeIncrementalOrderBookSocket> SubscribeIncrementalOrderBook { get; } = new();
}
}
}
@@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Order capabilities.
/// </summary>
public static class Orders
{
/// <summary>
/// Spot order capabilities.
/// </summary>
public static class Spot
{
/// <summary>
/// Place a spot order capability.
/// </summary>
public static SharedRestSocketCapability<IPlaceSpotOrder, IPlaceSpotOrderRest, IPlaceSpotOrderSocket> PlaceOrder { get; } = new();
/// <summary>
/// Place multiple spot orders capability.
/// </summary>
public static SharedRestSocketCapability<IPlaceMultipleSpotOrders, IPlaceMultipleSpotOrdersRest, IPlaceMultipleSpotOrdersSocket> PlaceMultipleOrders { get; } = new();
/// <summary>
/// Edit a spot order capability.
/// </summary>
public static SharedRestSocketCapability<IEditSpotOrder, IEditSpotOrderRest, IEditSpotOrderSocket> EditOrder { get; } = new();
/// <summary>
/// Edit a spot order by client order id capability.
/// </summary>
public static SharedRestSocketCapability<IEditSpotOrderByClientOrderId, IEditSpotOrderByClientOrderIdRest, IEditSpotOrderByClientOrderIdSocket> EditOrderByClientOrderId { get; } = new();
/// <summary>
/// Cancel a spot order capability.
/// </summary>
public static SharedRestSocketCapability<ICancelSpotOrder, ICancelSpotOrderRest, ICancelSpotOrderSocket> CancelOrder { get; } = new();
/// <summary>
/// Cancel a spot order by client order id capability.
/// </summary>
public static SharedRestSocketCapability<ICancelSpotOrderByClientOrderId, ICancelSpotOrderByClientOrderIdRest, ICancelSpotOrderByClientOrderIdSocket> CancelOrderByClientOrderId { get; } = new();
/// <summary>
/// Cancel all spot orders capability.
/// </summary>
public static SharedRestSocketCapability<ICancelAllSpotOrders, ICancelAllSpotOrdersRest, ICancelAllSpotOrdersSocket> CancelAllOrders { get; } = new();
/// <summary>
/// Cancel all spot orders for a symbol capability.
/// </summary>
public static SharedRestSocketCapability<ICancelAllSpotSymbolOrders, ICancelAllSpotSymbolOrdersRest, ICancelAllSpotSymbolOrdersSocket> CancelAllSymbolOrders { get; } = new();
/// <summary>
/// Get a spot order capability.
/// </summary>
public static SharedRestCapability<IGetSpotOrder, IGetSpotOrderRest> GetOrder { get; } = new();
/// <summary>
/// Get a spot order by client order id capability.
/// </summary>
public static SharedRestCapability<IGetSpotOrderByClientOrderId, IGetSpotOrderByClientOrderIdRest> GetOrderByClientOrderId { get; } = new();
/// <summary>
/// Get open spot orders capability.
/// </summary>
public static SharedRestCapability<IGetOpenSpotOrders, IGetOpenSpotOrdersRest> GetOpenOrders { get; } = new();
/// <summary>
/// Get closed spot orders capability.
/// </summary>
public static SharedRestCapability<IGetClosedSpotOrders, IGetClosedSpotOrdersRest> GetClosedOrders { get; } = new();
/// <summary>
/// Subscribe to spot order updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeSpotOrdersSocket> SubscribeOrders { get; } = new();
}
/// <summary>
/// Futures order capabilities.
/// </summary>
public static class Futures
{
/// <summary>
/// Place a futures order capability.
/// </summary>
public static SharedRestSocketCapability<IPlaceFuturesOrder, IPlaceFuturesOrderRest, IPlaceFuturesOrderSocket> PlaceOrder { get; } = new();
/// <summary>
/// Place multiple futures orders capability.
/// </summary>
public static SharedRestSocketCapability<IPlaceMultipleFuturesOrders, IPlaceMultipleFuturesOrdersRest, IPlaceMultipleFuturesOrdersSocket> PlaceMultipleOrders { get; } = new();
/// <summary>
/// Edit a futures order capability.
/// </summary>
public static SharedRestSocketCapability<IEditFuturesOrder, IEditFuturesOrderRest, IEditFuturesOrderSocket> EditOrder { get; } = new();
/// <summary>
/// Edit a futures order by client order id capability.
/// </summary>
public static SharedRestSocketCapability<IEditFuturesOrderByClientOrderId, IEditFuturesOrderByClientOrderIdRest, IEditFuturesOrderByClientOrderIdSocket> EditOrderByClientOrderId { get; } = new();
/// <summary>
/// Cancel a futures order capability.
/// </summary>
public static SharedRestSocketCapability<ICancelFuturesOrder, ICancelFuturesOrderRest, ICancelFuturesOrderSocket> CancelOrder { get; } = new();
/// <summary>
/// Cancel a futures order by client order id capability.
/// </summary>
public static SharedRestSocketCapability<ICancelFuturesOrderByClientOrderId, ICancelFuturesOrderByClientOrderIdRest, ICancelFuturesOrderByClientOrderIdSocket> CancelOrderByClientOrderId { get; } = new();
/// <summary>
/// Cancel all futures orders capability.
/// </summary>
public static SharedRestSocketCapability<ICancelAllFuturesOrders, ICancelAllFuturesOrdersRest, ICancelAllFuturesOrdersSocket> CancelAllOrders { get; } = new();
/// <summary>
/// Cancel all futures orders for a symbol capability.
/// </summary>
public static SharedRestSocketCapability<ICancelAllFuturesSymbolOrders, ICancelAllFuturesSymbolOrdersRest, ICancelAllFuturesSymbolOrdersSocket> CancelAllSymbolOrders { get; } = new();
/// <summary>
/// Get a futures order capability.
/// </summary>
public static SharedRestCapability<IGetFuturesOrder, IGetFuturesOrderRest> GetOrder { get; } = new();
/// <summary>
/// Get a futures order by client order id capability.
/// </summary>
public static SharedRestCapability<IGetFuturesOrderByClientOrderId, IGetFuturesOrderByClientOrderIdRest> GetOrderByClientOrderId { get; } = new();
/// <summary>
/// Get open futures orders capability.
/// </summary>
public static SharedRestCapability<IGetOpenFuturesOrders, IGetOpenFuturesOrdersRest> GetOpenOrders { get; } = new();
/// <summary>
/// Get closed futures orders capability.
/// </summary>
public static SharedRestCapability<IGetClosedFuturesOrders, IGetClosedFuturesOrdersRest> GetClosedOrders { get; } = new();
/// <summary>
/// Subscribe to futures order updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeFuturesOrdersSocket> SubscribeOrders { get; } = new();
}
}
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Position mode capabilities.
/// </summary>
public static class PositionMode
{
/// <summary>
/// Get position mode capability.
/// </summary>
public static SharedRestCapability<IGetPositionMode, IGetPositionModeRest> GetPositionMode { get; } = new();
/// <summary>
/// Set position mode capability.
/// </summary>
public static SharedRestCapability<ISetPositionMode, ISetPositionModeRest> SetPositionMode { get; } = new();
}
}
}
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Position capabilities.
/// </summary>
public static class Positions
{
/// <summary>
/// Get positions capability.
/// </summary>
public static SharedRestCapability<IGetPositions, IGetPositionsRest> GetPositions { get; } = new();
/// <summary>
/// Get position history capability.
/// </summary>
public static SharedRestCapability<IGetPositionHistory, IGetPositionHistoryRest> GetPositionHistory { get; } = new();
/// <summary>
/// Close a full position capability.
/// </summary>
public static SharedRestCapability<ICloseFullPosition, ICloseFullPositionRest> CloseFullPosition { get; } = new();
/// <summary>
/// Subscribe to position updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribePositionsSocket> SubscribePositions { get; } = new();
}
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Symbol capabilities.
/// </summary>
public static class Symbols
{
/// <summary>
/// Get spot symbols capability.
/// </summary>
public static SharedRestCapability<IGetSpotSymbols, IGetSpotSymbolsRest> GetSpotSymbols { get; } = new();
/// <summary>
/// Get futures symbols capability.
/// </summary>
public static SharedRestCapability<IGetFuturesSymbols, IGetFuturesSymbolsRest> GetFuturesSymbols { get; } = new();
}
}
}
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Ticker capabilities.
/// </summary>
public static class Tickers
{
/// <summary>
/// Get a ticker capability.
/// </summary>
public static SharedRestCapability<IGetTicker, IGetTickerRest> GetTicker { get; } = new();
/// <summary>
/// Get all tickers capability.
/// </summary>
public static SharedRestCapability<IGetAllTickers, IGetAllTickersRest> GetAllTickers { get; } = new();
/// <summary>
/// Subscribe to ticker updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeTickerSocket> SubscribeTicker { get; } = new();
/// <summary>
/// Subscribe to all ticker updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeAllTickersSocket> SubscribeAllTickers { get; } = new();
/// <summary>
/// Subscribe to book ticker updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeBookTickerSocket> SubscribeBookTicker { get; } = new();
}
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Take profit and stop loss capabilities.
/// </summary>
public static class TpSl
{
/// <summary>
/// Set futures take profit or stop loss capability.
/// </summary>
public static SharedRestCapability<ISetFuturesTpSl, ISetFuturesTpSlRest> SetFuturesTpSl { get; } = new();
/// <summary>
/// Cancel futures take profit or stop loss capability.
/// </summary>
public static SharedRestCapability<ICancelFuturesTpSl, ICancelFuturesTpSlRest> CancelFuturesTpSl { get; } = new();
}
}
}
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Public trade capabilities.
/// </summary>
public static class Trades
{
/// <summary>
/// Get recent trades capability.
/// </summary>
public static SharedRestCapability<IGetRecentTrades, IGetRecentTradesRest> GetRecentTrades { get; } = new();
/// <summary>
/// Get trade history capability.
/// </summary>
public static SharedRestCapability<IGetTradeHistory, IGetTradeHistoryRest> GetTradeHistory { get; } = new();
/// <summary>
/// Subscribe to trade updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeTradesSocket> SubscribeTrades { get; } = new();
}
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Transfer capabilities.
/// </summary>
public static class Transfers
{
/// <summary>
/// Transfer assets capability.
/// </summary>
public static SharedRestCapability<ITransfer, ITransferRest> Transfer { get; } = new();
/// <summary>
/// Get transfer history capability.
/// </summary>
public static SharedRestCapability<IGetTransferHistory, IGetTransferHistoryRest> GetTransferHistory { get; } = new();
}
}
}
@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Trigger order capabilities.
/// </summary>
public static class TriggerOrders
{
/// <summary>
/// Spot trigger order capabilities.
/// </summary>
public static class Spot
{
/// <summary>
/// Place a spot trigger order capability.
/// </summary>
public static SharedRestCapability<IPlaceSpotTriggerOrder, IPlaceSpotTriggerOrderRest> PlaceOrder { get; } = new();
/// <summary>
/// Get a spot trigger order capability.
/// </summary>
public static SharedRestCapability<IGetSpotTriggerOrder, IGetSpotTriggerOrderRest> GetOrder { get; } = new();
/// <summary>
/// Cancel a spot trigger order capability.
/// </summary>
public static SharedRestCapability<ICancelSpotTriggerOrder, ICancelSpotTriggerOrderRest> CancelOrder { get; } = new();
}
/// <summary>
/// Futures trigger order capabilities.
/// </summary>
public static class Futures
{
/// <summary>
/// Place a futures trigger order capability.
/// </summary>
public static SharedRestCapability<IPlaceFuturesTriggerOrder, IPlaceFuturesTriggerOrderRest> PlaceOrder { get; } = new();
/// <summary>
/// Get a futures trigger order capability.
/// </summary>
public static SharedRestCapability<IGetFuturesTriggerOrder, IGetFuturesTriggerOrderRest> GetOrder { get; } = new();
/// <summary>
/// Cancel a futures trigger order capability.
/// </summary>
public static SharedRestCapability<ICancelFuturesTriggerOrder, ICancelFuturesTriggerOrderRest> CancelOrder { get; } = new();
}
}
}
}
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// User trade capabilities.
/// </summary>
public static class UserTrades
{
/// <summary>
/// Get trades for a spot order capability.
/// </summary>
public static SharedRestCapability<IGetSpotOrderTrades, IGetSpotOrderTradesRest> GetSpotOrderTrades { get; } = new();
/// <summary>
/// Get spot user trade history capability.
/// </summary>
public static SharedRestCapability<IGetSpotUserTradeHistory, IGetSpotUserTradeHistoryRest> GetSpotTradeHistory { get; } = new();
/// <summary>
/// Get trades for a futures order capability.
/// </summary>
public static SharedRestCapability<IGetFuturesOrderTrades, IGetFuturesOrderTradesRest> GetFuturesOrderTrades { get; } = new();
/// <summary>
/// Get futures user trade history capability.
/// </summary>
public static SharedRestCapability<IGetFuturesUserTradeHistory, IGetFuturesUserTradeHistoryRest> GetFuturesTradeHistory { get; } = new();
/// <summary>
/// Subscribe to user trade updates capability.
/// </summary>
public static SharedCapabilityReference<ISubscribeUserTradesSocket> SubscribeUserTrades { get; } = new();
}
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
public static partial class SharedCapabilities
{
/// <summary>
/// Withdrawal capabilities.
/// </summary>
public static class Withdrawals
{
/// <summary>
/// Withdraw assets capability.
/// </summary>
public static SharedRestCapability<IWithdraw, IWithdrawRest> Withdraw { get; } = new();
/// <summary>
/// Get withdrawal history capability.
/// </summary>
public static SharedRestCapability<IGetWithdrawalHistory, IGetWithdrawalHistoryRest> GetWithdrawalHistory { get; } = new();
}
}
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Strongly typed reference to a Shared API capability.
/// </summary>
public class SharedCapabilityReference<T>
where T : ISharedApiCapability
{
/// <summary>
/// Capability interface type.
/// </summary>
public Type CapabilityType => typeof(T);
internal SharedCapabilityReference()
{
}
/// <inheritdoc />
public override string ToString() => typeof(T).Name;
}
}
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Capability lookup result
/// </summary>
/// <typeparam name="T">Capability type</typeparam>
public class SharedCapabilityResolution<T>
where T : ISharedApiCapability
{
/// <summary>
/// The exchange name
/// </summary>
public string Exchange => Capability.Exchange;
/// <summary>
/// The transport type
/// </summary>
public SharedTransport Transport => Capability.Transport;
/// <summary>
/// The capability
/// </summary>
public T Capability { get; }
/// <summary>
/// The capability options
/// </summary>
public CapabilityOptions Options { get; }
internal SharedCapabilityResolution(
T capability,
CapabilityOptions options)
{
Capability = capability;
Options = options;
}
}
}
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// References for a capability available through REST.
/// </summary>
public class SharedRestCapability<TCapability, TRest>
: SharedCapabilityReference<TCapability>
where TCapability : ISharedApiCapability
where TRest : TCapability, ISharedRest
{
/// <summary>
/// Transport-agnostic capability.
/// </summary>
public SharedCapabilityReference<TCapability> Any { get; } = new();
/// <summary>
/// REST capability.
/// </summary>
public SharedCapabilityReference<TRest> Rest { get; } = new();
internal SharedRestCapability()
{
}
}
}
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// References for a capability available through REST and WebSocket.
/// </summary>
public class SharedRestSocketCapability<TCapability, TRest, TSocket>
: SharedCapabilityReference<TCapability>
where TCapability : ISharedApiCapability
where TRest : TCapability, ISharedRest
where TSocket : TCapability, ISharedSocket
{
/// <summary>
/// REST capability.
/// </summary>
public SharedCapabilityReference<TRest> Rest { get; } = new();
/// <summary>
/// WebSocket capability.
/// </summary>
public SharedCapabilityReference<TSocket> Socket { get; } = new();
internal SharedRestSocketCapability()
{
}
}
}
@@ -0,0 +1,42 @@
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Ledger entry type
/// </summary>
public enum SharedLedgerEntryType
{
/// <summary>
/// Trade execution
/// </summary>
Trade,
/// <summary>
/// Withdrawal
/// </summary>
Withdrawal,
/// <summary>
/// Deposit
/// </summary>
Deposit,
/// <summary>
/// Transfer
/// </summary>
Transfer,
/// <summary>
/// Fee payment
/// </summary>
Fee,
/// <summary>
/// Funding fee payment
/// </summary>
FundingFee,
/// <summary>
/// Rebate
/// </summary>
Rebate,
/// <summary>
/// Other or unknown
/// </summary>
Unknown
}
}
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// The type of updates the incremental order book subscription produces
/// </summary>
public enum SharedOrderBookSubscriptionType
{
/// <summary>
/// Subscription produces an initial snapshot update, followed by incremental change updates
/// </summary>
SnapshotThenIncremental,
/// <summary>
/// Subscription produces only incremental change updates
/// </summary>
OnlyIncremental
}
}
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// The type of transport used for the shared API
/// </summary>
public enum SharedTransport
{
/// <summary>
/// REST transport
/// </summary>
Rest,
/// <summary>
/// WebSocket transport
/// </summary>
Socket
}
}
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// A shared/common client interface
/// </summary>
public interface ISharedClient : ISharedApi
{
}
}
@@ -7,7 +7,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Options for canceling a futures order /// Options for canceling a futures order
/// </summary> /// </summary>
public class CancelFuturesOrderSocketOptions : EndpointOptions<CancelOrderRequest, IFuturesOrderManagementSocketClient> public class CancelFuturesOrderSocketOptions : CancelFuturesOrderOptions
{ {
/// <inheritdoc /> /// <inheritdoc />
public override string Description => "Cancel a futures order over a socket connection"; public override string Description => "Cancel a futures order over a socket connection";
@@ -15,7 +15,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
public CancelFuturesOrderSocketOptions(string exchange, bool authenticated) : base(exchange, authenticated, nameof(IFuturesOrderManagementSocketClient.CancelFuturesOrderAsync)) public CancelFuturesOrderSocketOptions(string exchange, bool authenticated) : base(exchange, authenticated)
{ {
} }
} }
@@ -0,0 +1,23 @@
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Options for canceling a spot order
/// </summary>
public class CancelSpotOrderSocketOptions : CancelSpotOrderOptions
{
/// <inheritdoc />
public override string Description => "Cancel a spot order over a socket connection";
/// <summary>
/// ctor
/// </summary>
public CancelSpotOrderSocketOptions(string exchange, bool authenticated) : base(exchange, authenticated)
{
}
}
}
@@ -0,0 +1,82 @@
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Options for closing position
/// </summary>
#pragma warning disable CS0618 // Type or member is obsolete
public class ClosePositionOptions : EndpointOptions
#pragma warning restore CS0618 // Type or member is obsolete
{
private static PropertyInfo[] _requestProperties = typeof(ClosePositionRequest).GetProperties();
/// <inheritdoc />
public override string Description => "Close an open futures position";
/// <inheritdoc />
public override Type CapabilityType => typeof(IFuturesOrderRestClient);
private static readonly RequestParameterDescription[] _defaultParameterRules = new[]
{
RequestParameterRule<ClosePositionRequest>.Required(x => x.Symbol, "The symbol of the position to close", new SharedSymbol(TradingMode.PerpetualLinear, "ETH", "USDT")),
RequestParameterRule<ClosePositionRequest>.Required(x => x.PositionMode, "The current position mode of the account", SharedPositionMode.OneWay),
RequestParameterRule<ClosePositionRequest>.Optional(x => x.PositionSide, "The side of the position to close", SharedPositionSide.Long),
RequestParameterRule<ClosePositionRequest>.Optional(x => x.MarginMode, "The margin mode of the position", SharedMarginMode.Cross),
RequestParameterRule<ClosePositionRequest>.Optional(x => x.Quantity, "The quantity of the position to close", 1m),
};
/// <summary>
/// ctor
/// </summary>
public ClosePositionOptions(string exchange, bool authenticated) : base(exchange, nameof(IFuturesOrderRestClient.ClosePositionAsync), true, _defaultParameterRules, SharedTradingModeSets.Futures)
{
}
/// <summary>
/// Validate request
/// </summary>
public Error? ValidateRequest(ClosePositionRequest request, IFuturesOrderRestClient client)
{
if (NeedsAuthentication && !client.Authenticated)
return new NoApiCredentialsError();
foreach (var param in RequestParameterRules)
{
var property = _requestProperties.Single(x => x.Name == param.Name);
var value = property.GetValue(request);
if (param.Support == RequestParameterSupport.Required)
{
if (value == null)
{
return ArgumentError.Invalid(
param.Name,
$"Request parameter `{param.Name}` for exchange `{Exchange}` is required and should be provided. Example: {param.ExampleValue}");
}
}
}
if (request is SharedSymbolRequest symbolsRequest)
{
if (symbolsRequest.Symbols != null)
{
if (!SupportsMultipleSymbols)
return ArgumentError.Invalid(nameof(SharedSymbolRequest.Symbols), $"Only a single symbol parameter is allowed, multiple symbols are not supported");
if (symbolsRequest.Symbols.Length > MaxSymbolCount)
return ArgumentError.Invalid(nameof(SharedSymbolRequest.Symbols), $"Max number of symbols is {MaxSymbolCount} but {symbolsRequest.Symbols.Length} were passed");
}
}
return ValidateRequest(request.ExchangeParameters, request.TradingMode, client.SupportedTradingModes);
}
}
}
@@ -0,0 +1,23 @@
using CryptoExchange.Net.Objects;
using System;
using System.Linq;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Options for placing a new futures order
/// </summary>
public class PlaceFuturesOrderSocketOptions : PlaceFuturesOrderOptions
{
/// <inheritdoc />
public override string Description => "Place a new futures order over a socket connection";
/// <summary>
/// ctor
/// </summary>
public PlaceFuturesOrderSocketOptions(string exchange, bool supportsTpSl) : base(exchange, supportsTpSl)
{
}
}
}
@@ -0,0 +1,23 @@
using CryptoExchange.Net.Objects;
using System;
using System.Linq;
namespace CryptoExchange.Net.SharedApis
{
/// <summary>
/// Options for placing a new spot order
/// </summary>
public class PlaceSpotOrderSocketOptions : PlaceSpotOrderOptions
{
/// <inheritdoc />
public override string Description => "Place a new spot order over a socket connection";
/// <summary>
/// ctor
/// </summary>
public PlaceSpotOrderSocketOptions(string exchange) : base(exchange)
{
}
}
}
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Funding rate request options.<br /> /// Funding rate request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetFundingRateHistoryOptions GetFundingRateHistoryOptions { get; } GetFundingRateHistoryOptions GetFundingRateHistoryOptions { get; }
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Futures get order by client order id request options.<br /> /// Futures get order by client order id request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetFuturesOrderByClientOrderIdOptions GetFuturesOrderByClientOrderIdOptions { get; } GetFuturesOrderByClientOrderIdOptions GetFuturesOrderByClientOrderIdOptions { get; }
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Futures cancel order by client order id request options.<br /> /// Futures cancel order by client order id request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
CancelFuturesOrderByClientOrderIdOptions CancelFuturesOrderByClientOrderIdOptions { get; } CancelFuturesOrderByClientOrderIdOptions CancelFuturesOrderByClientOrderIdOptions { get; }
@@ -38,7 +38,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Futures place order request options.<br /> /// Futures place order request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
PlaceFuturesOrderOptions PlaceFuturesOrderOptions { get; } PlaceFuturesOrderOptions PlaceFuturesOrderOptions { get; }
@@ -52,7 +52,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Futures get order request options.<br /> /// Futures get order request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetFuturesOrderOptions GetFuturesOrderOptions { get; } GetFuturesOrderOptions GetFuturesOrderOptions { get; }
@@ -65,7 +65,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Futures get open orders request options.<br /> /// Futures get open orders request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetOpenFuturesOrdersOptions GetOpenFuturesOrdersOptions { get; } GetOpenFuturesOrdersOptions GetOpenFuturesOrdersOptions { get; }
@@ -78,7 +78,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Spot get closed orders request options.<br /> /// Spot get closed orders request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetFuturesClosedOrdersOptions GetClosedFuturesOrdersOptions { get; } GetFuturesClosedOrdersOptions GetClosedFuturesOrdersOptions { get; }
@@ -93,7 +93,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Futures get order trades request options.<br /> /// Futures get order trades request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetFuturesOrderTradesOptions GetFuturesOrderTradesOptions { get; } GetFuturesOrderTradesOptions GetFuturesOrderTradesOptions { get; }
@@ -106,10 +106,10 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Futures user trades request options.<br /> /// Futures user trades request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetFuturesUserTradesOptions GetFuturesUserTradesOptions { get; } GetFuturesUserTradeHistoryOptions GetFuturesUserTradesOptions { get; }
/// <summary> /// <summary>
/// Get futures user trade records, see <see cref="GetFuturesUserTradesOptions"/> for request options and exchange specific required/optional parameters. <br /> /// Get futures user trade records, see <see cref="GetFuturesUserTradesOptions"/> for request options and exchange specific required/optional parameters. <br />
/// The result is paginated, if there are more results to be retrieved, the `NextPageRequest` property of the result will contain the pagination request to be used for the next request to continue pagination. /// The result is paginated, if there are more results to be retrieved, the `NextPageRequest` property of the result will contain the pagination request to be used for the next request to continue pagination.
@@ -121,7 +121,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Futures cancel order request options.<br /> /// Futures cancel order request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
CancelFuturesOrderOptions CancelFuturesOrderOptions { get; } CancelFuturesOrderOptions CancelFuturesOrderOptions { get; }
@@ -134,7 +134,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Positions request options.<br /> /// Positions request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetPositionsOptions GetPositionsOptions { get; } GetPositionsOptions GetPositionsOptions { get; }
@@ -147,7 +147,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Close position order request options.<br /> /// Close position order request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
ClosePositionOptions ClosePositionOptions { get; } ClosePositionOptions ClosePositionOptions { get; }
@@ -16,7 +16,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Futures symbol request options.<br /> /// Futures symbol request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetFuturesSymbolsOptions GetFuturesSymbolsOptions { get; } GetFuturesSymbolsOptions GetFuturesSymbolsOptions { get; }
@@ -11,10 +11,10 @@ namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Futures get ticker request options.<br /> /// Futures get ticker request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetFuturesTickerOptions GetFuturesTickerOptions { get; } GetTickerOptions GetFuturesTickerOptions { get; }
/// <summary> /// <summary>
/// Get ticker info for a specific futures symbol, see <see cref="GetFuturesTickerOptions"/> for request options and exchange specific required/optional parameters. <br /> /// Get ticker info for a specific futures symbol, see <see cref="GetFuturesTickerOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary> /// </summary>
@@ -24,10 +24,10 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Futures get tickers request options.<br /> /// Futures get tickers request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetFuturesTickersOptions GetFuturesTickersOptions { get; } GetAllTickersOptions GetFuturesTickersOptions { get; }
/// <summary> /// <summary>
/// Get ticker info for all futures symbols, see <see cref="GetFuturesTickersOptions"/> for request options and exchange specific required/optional parameters. <br /> /// Get ticker info for all futures symbols, see <see cref="GetFuturesTickersOptions"/> for request options and exchange specific required/optional parameters. <br />
/// </summary> /// </summary>
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Set take profit and/or stop loss options.<br /> /// Set take profit and/or stop loss options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
SetFuturesTpSlOptions SetFuturesTpSlOptions { get; } SetFuturesTpSlOptions SetFuturesTpSlOptions { get; }
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Cancel a take profit and/or stop loss options.<br /> /// Cancel a take profit and/or stop loss options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
CancelFuturesTpSlOptions CancelFuturesTpSlOptions { get; } CancelFuturesTpSlOptions CancelFuturesTpSlOptions { get; }
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Place spot trigger order options.<br /> /// Place spot trigger order options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
PlaceFuturesTriggerOrderOptions PlaceFuturesTriggerOrderOptions { get; } PlaceFuturesTriggerOrderOptions PlaceFuturesTriggerOrderOptions { get; }
@@ -26,7 +26,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Get trigger order request options.<br /> /// Get trigger order request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetFuturesTriggerOrderOptions GetFuturesTriggerOrderOptions { get; } GetFuturesTriggerOrderOptions GetFuturesTriggerOrderOptions { get; }
@@ -39,7 +39,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Cancel trigger order request options.<br /> /// Cancel trigger order request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
CancelFuturesTriggerOrderOptions CancelFuturesTriggerOrderOptions { get; } CancelFuturesTriggerOrderOptions CancelFuturesTriggerOrderOptions { get; }
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Index price klines request options.<br /> /// Index price klines request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetIndexPriceKlinesOptions GetIndexPriceKlinesOptions { get; } GetIndexPriceKlinesOptions GetIndexPriceKlinesOptions { get; }
@@ -16,7 +16,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Leverage request options.<br /> /// Leverage request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetLeverageOptions GetLeverageOptions { get; } GetLeverageOptions GetLeverageOptions { get; }
@@ -29,7 +29,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Leverage set request options.<br /> /// Leverage set request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
SetLeverageOptions SetLeverageOptions { get; } SetLeverageOptions SetLeverageOptions { get; }
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Mark price klines request options.<br /> /// Mark price klines request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetMarkPriceKlinesOptions GetMarkPriceKlinesOptions { get; } GetMarkPriceKlinesOptions GetMarkPriceKlinesOptions { get; }
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Open interest request options.<br /> /// Open interest request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetOpenInterestOptions GetOpenInterestOptions { get; } GetOpenInterestOptions GetOpenInterestOptions { get; }
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Position history request options.<br /> /// Position history request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetPositionHistoryOptions GetPositionHistoryOptions { get; } GetPositionHistoryOptions GetPositionHistoryOptions { get; }
@@ -16,7 +16,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Position mode request options.<br /> /// Position mode request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetPositionModeOptions GetPositionModeOptions { get; } GetPositionModeOptions GetPositionModeOptions { get; }
@@ -29,7 +29,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Position mode set request options.<br /> /// Position mode set request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
SetPositionModeOptions SetPositionModeOptions { get; } SetPositionModeOptions SetPositionModeOptions { get; }
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Asset request options.<br /> /// Asset request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetAssetOptions GetAssetOptions { get; } GetAssetOptions GetAssetOptions { get; }
@@ -25,10 +25,10 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Assets request options.<br /> /// Assets request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetAssetsOptions GetAssetsOptions { get; } GetAllAssetsOptions GetAssetsOptions { get; }
/// <summary> /// <summary>
/// Get info on all assets the exchange supports, see <see cref="GetAssetsOptions"/> for request options and exchange specific required/optional parameters. <br /> /// Get info on all assets the exchange supports, see <see cref="GetAssetsOptions"/> for request options and exchange specific required/optional parameters. <br />
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Balances request options.<br /> /// Balances request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetBalancesOptions GetBalancesOptions { get; } GetBalancesOptions GetBalancesOptions { get; }
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Book ticker request options.<br /> /// Book ticker request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetBookTickerOptions GetBookTickerOptions { get; } GetBookTickerOptions GetBookTickerOptions { get; }
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Deposit addresses request options.<br /> /// Deposit addresses request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetDepositAddressesOptions GetDepositAddressesOptions { get; } GetDepositAddressesOptions GetDepositAddressesOptions { get; }
@@ -26,10 +26,10 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Deposits request options.<br /> /// Deposits request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetDepositsOptions GetDepositsOptions { get; } GetDepositHistoryOptions GetDepositsOptions { get; }
/// <summary> /// <summary>
/// Get deposit records, see <see cref="GetDepositsOptions"/> for request options and exchange specific required/optional parameters. <br /> /// Get deposit records, see <see cref="GetDepositsOptions"/> for request options and exchange specific required/optional parameters. <br />
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Fee request options.<br /> /// Fee request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetFeeOptions GetFeeOptions { get; } GetFeeOptions GetFeeOptions { get; }
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Kline request options.<br /> /// Kline request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetKlinesOptions GetKlinesOptions { get; } GetKlinesOptions GetKlinesOptions { get; }
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Order book request options.<br /> /// Order book request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetOrderBookOptions GetOrderBookOptions { get; } GetOrderBookOptions GetOrderBookOptions { get; }
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Recent trades request options.<br /> /// Recent trades request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetRecentTradesOptions GetRecentTradesOptions { get; } GetRecentTradesOptions GetRecentTradesOptions { get; }
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Trade history request options.<br /> /// Trade history request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetTradeHistoryOptions GetTradeHistoryOptions { get; } GetTradeHistoryOptions GetTradeHistoryOptions { get; }
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Transfer request options.<br /> /// Transfer request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
TransferOptions TransferOptions { get; } TransferOptions TransferOptions { get; }
@@ -1,17 +1,20 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace CryptoExchange.Net.SharedApis namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Client for requesting to withdraw funds from the exchange /// Client for withdrawing funds
/// </summary> /// </summary>
public interface IWithdrawRestClient : ISharedClient public interface IWithdrawRestClient : ISharedClient
{ {
/// <summary> /// <summary>
/// Withdraw request options.<br /> /// Withdraw request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
WithdrawOptions WithdrawOptions { get; } WithdrawOptions WithdrawOptions { get; }
@@ -11,10 +11,10 @@ namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Withdrawal records request options.<br /> /// Withdrawal records request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetWithdrawalsOptions GetWithdrawalsOptions { get; } GetWithdrawalHistoryOptions GetWithdrawalsOptions { get; }
/// <summary> /// <summary>
/// Get withdrawal records, see <see cref="GetWithdrawalsOptions"/> for request options and exchange specific required/optional parameters. <br /> /// Get withdrawal records, see <see cref="GetWithdrawalsOptions"/> for request options and exchange specific required/optional parameters. <br />
@@ -11,7 +11,7 @@ namespace CryptoExchange.Net.SharedApis
{ {
/// <summary> /// <summary>
/// Spot get order by client order id request options.<br /> /// Spot get order by client order id request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
GetSpotOrderByClientOrderIdOptions GetSpotOrderByClientOrderIdOptions { get; } GetSpotOrderByClientOrderIdOptions GetSpotOrderByClientOrderIdOptions { get; }
@@ -25,7 +25,7 @@ namespace CryptoExchange.Net.SharedApis
/// <summary> /// <summary>
/// Spot cancel order by client order id request options.<br /> /// Spot cancel order by client order id request options.<br />
/// Use <see cref="EndpointOptions.RequiredExchangeParameters"/> and <see cref="EndpointOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br /> /// Use <see cref="CapabilityOptions.RequiredExchangeParameters"/> and <see cref="CapabilityOptions.OptionalExchangeParameters"/> to check for required and optional parameters for the request. <br />
/// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object. /// Exchange specific parameters can be added to the request via the `ExchangeParameters` property of the request object.
/// </summary> /// </summary>
CancelSpotOrderByClientOrderIdOptions CancelSpotOrderByClientOrderIdOptions { get; } CancelSpotOrderByClientOrderIdOptions CancelSpotOrderByClientOrderIdOptions { get; }

Some files were not shown because too many files have changed in this diff Show More