Files
Encelado/DesktopBot/Services/ApiCallCounterService.cs
T
2026-06-09 18:29:41 +02:00

210 lines
9.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace DesktopBot.Services
{
/// <summary>
/// Categoria di chiamata API verso Alpaca.
/// </summary>
public enum ApiCategory
{
/// <summary>Trading API: ordini, posizioni, conto (paper-api / api.alpaca.markets)</summary>
Trading,
/// <summary>Market Data API: barre storiche, prezzi real-time (data.alpaca.markets)</summary>
MarketData
}
/// <summary>
/// Monitora e limita la frequenza delle chiamate verso le API Alpaca.
///
/// Limiti piano Basic (gratuito) da documentazione ufficiale Alpaca:
/// - Market Data API (Historical): 200 req/min
/// - Trading API: nessun limite documentato pubblicamente,
/// usa 100 req/min come valore conservativo
///
/// Fonte: https://docs.alpaca.markets/docs/about-market-data-api#subscription-plans
/// </summary>
public class ApiCallCounterService : INotifyPropertyChanged
{
// ── Limiti per piano Basic ──────────────────────────────────────────────
public const int MarketDataRpmLimit = 200; // req/min Basic plan (gratuito)
public const int TradingRpmLimit = 100; // req/min valore conservativo
// ── Finestra sliding di 60 secondi ─────────────────────────────────────
private static readonly TimeSpan _window = TimeSpan.FromSeconds(60);
private readonly ConcurrentQueue<DateTime> _marketDataTimestamps = new ConcurrentQueue<DateTime>();
private readonly ConcurrentQueue<DateTime> _tradingTimestamps = new ConcurrentQueue<DateTime>();
// ── Totali cumulativi ───────────────────────────────────────────────────
private long _totalMarketData;
private long _totalTrading;
private long _throttledCalls;
private readonly object _uiLock = new object();
// ── Proprietà notificabili per la UI ───────────────────────────────────
private int _marketDataRpm;
/// <summary>Chiamate Market Data API negli ultimi 60 secondi.</summary>
public int MarketDataRpm
{
get => _marketDataRpm;
private set { if (_marketDataRpm != value) { _marketDataRpm = value; OnPropertyChanged(); OnPropertyChanged(nameof(MarketDataUsagePercent)); } }
}
private int _tradingRpm;
/// <summary>Chiamate Trading API negli ultimi 60 secondi.</summary>
public int TradingRpm
{
get => _tradingRpm;
private set { if (_tradingRpm != value) { _tradingRpm = value; OnPropertyChanged(); OnPropertyChanged(nameof(TradingUsagePercent)); } }
}
/// <summary>Totale cumulativo chiamate Market Data dalla creazione.</summary>
public long TotalMarketDataCalls => Interlocked.Read(ref _totalMarketData);
/// <summary>Totale cumulativo chiamate Trading dalla creazione.</summary>
public long TotalTradingCalls => Interlocked.Read(ref _totalTrading);
/// <summary>Totale chiamate rallentate per rate-limit.</summary>
public long ThrottledCalls => Interlocked.Read(ref _throttledCalls);
/// <summary>Percentuale utilizzo finestra 60s per Market Data (0100).</summary>
public double MarketDataUsagePercent => MarketDataRpm * 100.0 / MarketDataRpmLimit;
/// <summary>Percentuale utilizzo finestra 60s per Trading (0100).</summary>
public double TradingUsagePercent => TradingRpm * 100.0 / TradingRpmLimit;
/// <summary>True se Market Data è sopra il 90% del limite.</summary>
public bool IsMarketDataNearLimit => MarketDataRpm >= MarketDataRpmLimit * 0.9;
/// <summary>True se Trading è sopra il 90% del limite.</summary>
public bool IsTradingNearLimit => TradingRpm >= TradingRpmLimit * 0.9;
// ── Statistiche per categoria ───────────────────────────────────────────
private readonly ConcurrentDictionary<string, long> _callsByEndpoint =
new ConcurrentDictionary<string, long>(StringComparer.OrdinalIgnoreCase);
// ── Costruttore ─────────────────────────────────────────────────────────
public ApiCallCounterService()
{
// Timer per aggiornare la UI ogni secondo
var timer = new Timer(_ => RefreshRpmCounters(), null,
TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
}
// ── API pubblica ────────────────────────────────────────────────────────
/// <summary>
/// Attende se necessario rispettando il rate limit, poi registra la chiamata.
/// Deve essere chiamato PRIMA di ogni richiesta HTTP verso Alpaca.
/// </summary>
/// <param name="category">Categoria della chiamata.</param>
/// <param name="endpoint">Nome endpoint per statistiche (es. "GetAccount").</param>
/// <param name="cancellationToken">Token di cancellazione.</param>
public async Task ThrottleAsync(ApiCategory category, string endpoint = "",
CancellationToken cancellationToken = default)
{
var queue = category == ApiCategory.MarketData
? _marketDataTimestamps : _tradingTimestamps;
var limit = category == ApiCategory.MarketData
? MarketDataRpmLimit : TradingRpmLimit;
// Attendi finché la finestra non è libera
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
Purge(queue);
if (queue.Count < limit)
break;
Interlocked.Increment(ref _throttledCalls);
// Attende il tempo residuo prima che il più vecchio timestamp esca dalla finestra
DateTime oldest;
if (queue.TryPeek(out oldest))
{
var wait = _window - (DateTime.UtcNow - oldest);
if (wait > TimeSpan.Zero)
await Task.Delay(wait, cancellationToken).ConfigureAwait(false);
}
}
// Registra la chiamata
queue.Enqueue(DateTime.UtcNow);
if (category == ApiCategory.MarketData)
Interlocked.Increment(ref _totalMarketData);
else
Interlocked.Increment(ref _totalTrading);
if (!string.IsNullOrEmpty(endpoint))
_callsByEndpoint.AddOrUpdate(endpoint, 1, (_, v) => v + 1);
RefreshRpmCounters();
}
/// <summary>
/// Restituisce le statistiche per endpoint (nome → conteggio totale).
/// </summary>
public IReadOnlyDictionary<string, long> GetEndpointStats()
=> new Dictionary<string, long>(_callsByEndpoint);
/// <summary>
/// Azzera i contatori cumulativi (non il rate-limiter sliding).
/// </summary>
public void Reset()
{
Interlocked.Exchange(ref _totalMarketData, 0);
Interlocked.Exchange(ref _totalTrading, 0);
Interlocked.Exchange(ref _throttledCalls, 0);
_callsByEndpoint.Clear();
RefreshRpmCounters();
OnPropertyChanged(nameof(TotalMarketDataCalls));
OnPropertyChanged(nameof(TotalTradingCalls));
OnPropertyChanged(nameof(ThrottledCalls));
}
// ── Privati ─────────────────────────────────────────────────────────────
private void Purge(ConcurrentQueue<DateTime> queue)
{
var cutoff = DateTime.UtcNow - _window;
DateTime ts;
while (queue.TryPeek(out ts) && ts < cutoff)
queue.TryDequeue(out _);
}
private void RefreshRpmCounters()
{
Purge(_marketDataTimestamps);
Purge(_tradingTimestamps);
lock (_uiLock)
{
MarketDataRpm = _marketDataTimestamps.Count;
TradingRpm = _tradingTimestamps.Count;
}
OnPropertyChanged(nameof(TotalMarketDataCalls));
OnPropertyChanged(nameof(TotalTradingCalls));
OnPropertyChanged(nameof(ThrottledCalls));
OnPropertyChanged(nameof(IsMarketDataNearLimit));
OnPropertyChanged(nameof(IsTradingNearLimit));
}
// ── INotifyPropertyChanged ──────────────────────────────────────────────
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}