Files
Predictalytics/src/Predictalytics.Application/Services/RateLimiterService.cs
T
Richard afb251acfc Initial commit: Predictalytics solution
Clean Architecture .NET 8 solution (Domain/Application/Infrastructure/Api/Worker/WinFormsHost)
for analyzing Polymarket traders for copytrading/strategy-replication candidates.

Includes EF Core InitialBaseline migration and DB secrets removed from source/config
in preparation for version control.
2026-07-01 19:53:29 +02:00

73 lines
2.7 KiB
C#

using Predictalytics.Application.Interfaces;
using Predictalytics.Domain.Enums;
using System.Collections.Concurrent;
namespace Predictalytics.Application.Services;
/// <summary>
/// Token-bucket rate limiter with per-platform configuration.
/// </summary>
public class RateLimiterService : IRateLimiter
{
private readonly ConcurrentDictionary<PlatformType, SemaphoreSlim> _semaphores = new();
private readonly ConcurrentDictionary<PlatformType, DateTime> _lastRequest = new();
private readonly ConcurrentDictionary<PlatformType, DateTime> _blockedUntil = new();
// Minimum delay between requests per platform (milliseconds)
private static readonly Dictionary<PlatformType, int> PlatformDelays = new()
{
{ PlatformType.Polymarket, 200 },
{ PlatformType.Limitless, 500 },
{ PlatformType.Azuro, 1000 },
{ PlatformType.Myriad, 1000 },
{ PlatformType.PredictFun, 1000 },
{ PlatformType.Kalshi, 500 },
{ PlatformType.Stake, 1000 }
};
public async Task WaitAsync(PlatformType platform, CancellationToken ct = default)
{
var sem = _semaphores.GetOrAdd(platform, _ => new SemaphoreSlim(1, 1));
await sem.WaitAsync(ct);
try
{
// 1. Check if we are currently blocked due to a 429
if (_blockedUntil.TryGetValue(platform, out var blockedUntil))
{
var waitTime = blockedUntil - DateTime.UtcNow;
if (waitTime > TimeSpan.Zero)
{
await Task.Delay(waitTime, ct);
}
}
// 2. Normal token bucket delay
if (_lastRequest.TryGetValue(platform, out var last))
{
var delayMs = PlatformDelays.GetValueOrDefault(platform, 1000);
var elapsed = (DateTime.UtcNow - last).TotalMilliseconds;
if (elapsed < delayMs)
await Task.Delay((int)(delayMs - elapsed), ct);
}
_lastRequest[platform] = DateTime.UtcNow;
}
finally { sem.Release(); }
}
public bool CanMakeRequest(PlatformType platform)
{
if (_blockedUntil.TryGetValue(platform, out var blockedUntil) && blockedUntil > DateTime.UtcNow)
return false;
if (!_lastRequest.TryGetValue(platform, out var last)) return true;
var delayMs = PlatformDelays.GetValueOrDefault(platform, 1000);
return (DateTime.UtcNow - last).TotalMilliseconds >= delayMs;
}
public void ReportRateLimitExceeded(PlatformType platform, TimeSpan? retryAfter = null)
{
var penalty = retryAfter ?? TimeSpan.FromSeconds(30);
_blockedUntil[platform] = DateTime.UtcNow.Add(penalty);
}
}