210 lines
9.5 KiB
C#
210 lines
9.5 KiB
C#
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 (0–100).</summary>
|
||
public double MarketDataUsagePercent => MarketDataRpm * 100.0 / MarketDataRpmLimit;
|
||
|
||
/// <summary>Percentuale utilizzo finestra 60s per Trading (0–100).</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));
|
||
}
|
||
}
|