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.
73 lines
2.7 KiB
C#
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);
|
|
}
|
|
}
|