Sviluppo TradingBot

This commit is contained in:
2026-06-09 18:29:41 +02:00
parent 61f1e59964
commit e3c0bd51b2
133 changed files with 24903 additions and 1 deletions
@@ -0,0 +1,209 @@
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));
}
}