using System; using System.Collections.ObjectModel; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows.Input; using Alpaca.Markets; using DesktopBot.Services; namespace DesktopBot.ViewModels { /// /// ViewModel per la visualizzazione del grafico dei prezzi in tempo reale. /// Supporta streaming via WebSocket per crypto e azioni, con buffer storico. /// public class PriceChartViewModel : BaseViewModel { private readonly ITradingService _tradingService; private CancellationTokenSource _streamCts; private Task _streamTask; // ── Dati del grafico ───────────────────────────────────────────────── /// Collezione di punti prezzo per il grafico (timestamp, prezzo) public ObservableCollection PriceData { get; } = new ObservableCollection(); /// Limite massimo dei punti dati nel grafico (parametrizzato, default 3000) public int MaxDataPoints { get; set; } = 3000; // ── Simbolo monitorato ─────────────────────────────────────────────── private string _symbol = "BTCUSD"; public string Symbol { get => _symbol; set { if (SetProperty(ref _symbol, value)) { _ = RestartStreamAsync(); } } } // ── Prezzo corrente ────────────────────────────────────────────────── private decimal _currentPrice; public decimal CurrentPrice { get => _currentPrice; private set => SetProperty(ref _currentPrice, value); } private decimal _priceChange; public decimal PriceChange { get => _priceChange; private set => SetProperty(ref _priceChange, value); } private decimal _priceChangePercent; public decimal PriceChangePercent { get => _priceChangePercent; private set => SetProperty(ref _priceChangePercent, value); } public string PriceChangeColor => PriceChange >= 0 ? "#00E676" : "#FF5252"; public string PriceChangeIcon => PriceChange >= 0 ? "▲" : "▼"; public System.Windows.Media.Brush PriceChangeBrush => new System.Windows.Media.SolidColorBrush( (System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(PriceChangeColor)); // ── Stato streaming ────────────────────────────────────────────────── private bool _isStreaming; public bool IsStreaming { get => _isStreaming; private set => SetProperty(ref _isStreaming, value); } private string _statusMessage = "Pronto"; public string StatusMessage { get => _statusMessage; set => SetProperty(ref _statusMessage, value); } // ── Timeframe ──────────────────────────────────────────────────────── private ChartTimeFrame _selectedTimeFrame = ChartTimeFrame.OneMinute; public ChartTimeFrame SelectedTimeFrame { get => _selectedTimeFrame; set { if (SetProperty(ref _selectedTimeFrame, value)) { _ = LoadHistoricalDataAsync(); } } } // ── Comandi ────────────────────────────────────────────────────────── public ICommand StartStreamCommand { get; } public ICommand StopStreamCommand { get; } // ── Costruttore ────────────────────────────────────────────────────── public PriceChartViewModel(ITradingService tradingService) { _tradingService = tradingService ?? throw new ArgumentNullException(nameof(tradingService)); StartStreamCommand = new RelayCommand(_ => _ = StartStreamingAsync(), _ => !IsStreaming); StopStreamCommand = new RelayCommand(_ => StopStreaming(), _ => IsStreaming); } // ── Caricamento dati storici ───────────────────────────────────────── private async Task LoadHistoricalDataAsync() { try { StatusMessage = $"Caricamento storico {Symbol}..."; var timeFrame = SelectedTimeFrame == ChartTimeFrame.OneMinute ? BarTimeFrame.Minute : BarTimeFrame.Hour; int barCount = SelectedTimeFrame == ChartTimeFrame.OneMinute ? 200 : 100; var bars = await _tradingService.GetHistoricalBarsAsync(Symbol, timeFrame, barCount); if (bars == null || bars.Count == 0) { StatusMessage = "Nessun dato storico disponibile"; return; } PriceData.Clear(); decimal? firstPrice = null; foreach (var bar in bars.OrderBy(b => b.TimeUtc)) { if (!firstPrice.HasValue) firstPrice = bar.Close; PriceData.Add(new PricePoint { Timestamp = bar.TimeUtc.ToLocalTime(), Price = bar.Close }); } if (PriceData.Count > 0) { var last = PriceData.Last(); CurrentPrice = last.Price; if (firstPrice.HasValue && firstPrice.Value > 0) { PriceChange = CurrentPrice - firstPrice.Value; PriceChangePercent = (PriceChange / firstPrice.Value) * 100m; } } StatusMessage = $"Caricati {bars.Count} punti storici"; OnPropertyChanged(nameof(PriceChangeColor)); OnPropertyChanged(nameof(PriceChangeBrush)); OnPropertyChanged(nameof(PriceChangeIcon)); } catch (Exception ex) { StatusMessage = $"Errore caricamento: {ex.Message}"; } } // ── Streaming real-time ────────────────────────────────────────────── public async Task StartStreamingAsync() { if (IsStreaming) return; await LoadHistoricalDataAsync(); IsStreaming = true; _streamCts = new CancellationTokenSource(); StatusMessage = $"Streaming {Symbol} attivo..."; // TODO: Implementare WebSocket streaming con Alpaca // Per ora: polling ogni 5 secondi come fallback _streamTask = Task.Run(() => PollingLoopAsync(_streamCts.Token)); } private async Task PollingLoopAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { try { await Task.Delay(TimeSpan.FromSeconds(5), ct); if (ct.IsCancellationRequested) break; decimal price = await _tradingService.GetLatestPriceAsync(Symbol); UpdatePrice(price); } catch (OperationCanceledException) { break; } catch (Exception ex) { StatusMessage = $"Errore stream: {ex.Message}"; await Task.Delay(TimeSpan.FromSeconds(10), ct); } } } private void UpdatePrice(decimal price) { var dispatcher = System.Windows.Application.Current?.Dispatcher; if (dispatcher != null && !dispatcher.CheckAccess()) { dispatcher.Invoke(() => UpdatePriceInternal(price)); } else { UpdatePriceInternal(price); } } private void UpdatePriceInternal(decimal price) { var firstPrice = PriceData.Count > 0 ? PriceData.First().Price : price; PriceChange = price - CurrentPrice; CurrentPrice = price; if (firstPrice > 0) { PriceChangePercent = ((price - firstPrice) / firstPrice) * 100m; } PriceData.Add(new PricePoint { Timestamp = DateTime.Now, Price = price }); // Limita dimensione buffer while (PriceData.Count > MaxDataPoints) PriceData.RemoveAt(0); OnPropertyChanged(nameof(PriceChangeColor)); OnPropertyChanged(nameof(PriceChangeBrush)); OnPropertyChanged(nameof(PriceChangeIcon)); } public void StopStreaming() { _streamCts?.Cancel(); IsStreaming = false; StatusMessage = "Stream fermato"; } private async Task RestartStreamAsync() { if (IsStreaming) { StopStreaming(); await Task.Delay(500); await StartStreamingAsync(); } } // ── Cleanup ────────────────────────────────────────────────────────── public void Dispose() { StopStreaming(); _streamCts?.Dispose(); } } // ── Modello dati punto prezzo ──────────────────────────────────────────── public class PricePoint { public DateTime Timestamp { get; set; } public decimal Price { get; set; } } // ── Enum timeframe grafico ─────────────────────────────────────────────── public enum ChartTimeFrame { OneMinute, FiveMinutes, OneHour } }