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));
}
}