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.
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user