Files
Encelado/DesktopBot/ViewModels/PriceChartViewModel.cs
T
2026-06-09 18:29:41 +02:00

286 lines
11 KiB
C#

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
{
/// <summary>
/// ViewModel per la visualizzazione del grafico dei prezzi in tempo reale.
/// Supporta streaming via WebSocket per crypto e azioni, con buffer storico.
/// </summary>
public class PriceChartViewModel : BaseViewModel
{
private readonly ITradingService _tradingService;
private CancellationTokenSource _streamCts;
private Task _streamTask;
// ── Dati del grafico ─────────────────────────────────────────────────
/// <summary>Collezione di punti prezzo per il grafico (timestamp, prezzo)</summary>
public ObservableCollection<PricePoint> PriceData { get; }
= new ObservableCollection<PricePoint>();
/// <summary>Limite massimo dei punti dati nel grafico (parametrizzato, default 3000)</summary>
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
}
}