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 { /// /// Categoria di chiamata API verso Alpaca. /// public enum ApiCategory { /// Trading API: ordini, posizioni, conto (paper-api / api.alpaca.markets) Trading, /// Market Data API: barre storiche, prezzi real-time (data.alpaca.markets) MarketData } /// /// 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 /// 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 _marketDataTimestamps = new ConcurrentQueue(); private readonly ConcurrentQueue _tradingTimestamps = new ConcurrentQueue(); // ── 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; /// Chiamate Market Data API negli ultimi 60 secondi. public int MarketDataRpm { get => _marketDataRpm; private set { if (_marketDataRpm != value) { _marketDataRpm = value; OnPropertyChanged(); OnPropertyChanged(nameof(MarketDataUsagePercent)); } } } private int _tradingRpm; /// Chiamate Trading API negli ultimi 60 secondi. public int TradingRpm { get => _tradingRpm; private set { if (_tradingRpm != value) { _tradingRpm = value; OnPropertyChanged(); OnPropertyChanged(nameof(TradingUsagePercent)); } } } /// Totale cumulativo chiamate Market Data dalla creazione. public long TotalMarketDataCalls => Interlocked.Read(ref _totalMarketData); /// Totale cumulativo chiamate Trading dalla creazione. public long TotalTradingCalls => Interlocked.Read(ref _totalTrading); /// Totale chiamate rallentate per rate-limit. public long ThrottledCalls => Interlocked.Read(ref _throttledCalls); /// Percentuale utilizzo finestra 60s per Market Data (0–100). public double MarketDataUsagePercent => MarketDataRpm * 100.0 / MarketDataRpmLimit; /// Percentuale utilizzo finestra 60s per Trading (0–100). public double TradingUsagePercent => TradingRpm * 100.0 / TradingRpmLimit; /// True se Market Data è sopra il 90% del limite. public bool IsMarketDataNearLimit => MarketDataRpm >= MarketDataRpmLimit * 0.9; /// True se Trading è sopra il 90% del limite. public bool IsTradingNearLimit => TradingRpm >= TradingRpmLimit * 0.9; // ── Statistiche per categoria ─────────────────────────────────────────── private readonly ConcurrentDictionary _callsByEndpoint = new ConcurrentDictionary(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 ──────────────────────────────────────────────────────── /// /// Attende se necessario rispettando il rate limit, poi registra la chiamata. /// Deve essere chiamato PRIMA di ogni richiesta HTTP verso Alpaca. /// /// Categoria della chiamata. /// Nome endpoint per statistiche (es. "GetAccount"). /// Token di cancellazione. 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(); } /// /// Restituisce le statistiche per endpoint (nome → conteggio totale). /// public IReadOnlyDictionary GetEndpointStats() => new Dictionary(_callsByEndpoint); /// /// Azzera i contatori cumulativi (non il rate-limiter sliding). /// 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 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)); } }