286 lines
11 KiB
C#
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
|
|
}
|
|
}
|