using System; using System.Collections.ObjectModel; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; using Alpaca.Markets; using DesktopBot.Engine; using DesktopBot.Services; namespace DesktopBot.ViewModels { /// Voce del log attività mostrata nella dashboard. public class ActivityLogEntry { public DateTime Timestamp { get; set; } public string Level { get; set; } public string Message { get; set; } public string LevelColor { get; set; } public string LevelBg { get; set; } public static ActivityLogEntry Create(string level, string message) { string color, bg; switch (level.ToUpperInvariant()) { case "SUCCESS": color = "#00E676"; bg = "#1A00E676"; break; case "ERROR": color = "#FF1744"; bg = "#1AFF1744"; break; case "WARNING": color = "#FFC107"; bg = "#1AFFC107"; break; case "BUY": color = "#00E676"; bg = "#1A003A00"; break; case "SELL": color = "#FF1744"; bg = "#1A3A0000"; break; default: color = "#9E9EAF"; bg = "#1A3A3A4A"; break; } return new ActivityLogEntry { Timestamp = DateTime.Now, Level = level.ToUpperInvariant(), Message = message, LevelColor = color, LevelBg = bg }; } } public class BalanceRowViewModel : BaseViewModel { public string Label { get; set; } public string LastClose { get; set; } public string Current { get; set; } public bool IsBold { get; set; } public bool IsSection { get; set; } } public class OrderViewModel : BaseViewModel { public Guid OrderId { get; } public string Symbol { get; } public string OrderType { get; } public string Side { get; } public decimal Qty { get; } public decimal FilledQty { get; } public string AvgFillPrice { get; } public string Status { get; } public string SubmittedAt { get; } public string FilledAt { get; } public string LimitPrice { get; } public string StopPrice { get; } public bool IsOpen { get; } public string SideBadgeColor { get; } public OrderViewModel(IOrder order) { OrderId = order.OrderId; Symbol = order.Symbol; OrderType = order.OrderType.ToString(); Side = order.OrderSide.ToString().ToUpperInvariant(); Qty = order.Quantity ?? 0m; FilledQty = order.FilledQuantity; AvgFillPrice = order.AverageFillPrice.HasValue ? "$" + order.AverageFillPrice.Value.ToString("N2") : "-"; Status = order.OrderStatus.ToString(); SubmittedAt = order.SubmittedAtUtc.HasValue ? order.SubmittedAtUtc.Value.ToLocalTime().ToString("dd/MM HH:mm:ss") : "-"; FilledAt = order.FilledAtUtc.HasValue ? order.FilledAtUtc.Value.ToLocalTime().ToString("dd/MM HH:mm:ss") : "-"; LimitPrice = order.LimitPrice.HasValue ? "$" + order.LimitPrice.Value.ToString("N2") : "-"; StopPrice = order.StopPrice.HasValue ? "$" + order.StopPrice.Value.ToString("N2") : "-"; IsOpen = order.OrderStatus == OrderStatus.New || order.OrderStatus == OrderStatus.PartiallyFilled || order.OrderStatus == OrderStatus.PendingNew || order.OrderStatus == OrderStatus.Accepted; SideBadgeColor = order.OrderSide == OrderSide.Buy ? "#1A3A1A" : "#3A1A1A"; } } public class DashboardViewModel : BaseViewModel { private readonly ITradingService _tradingService; private readonly AutomatedBotEngine _botEngine; private decimal _equity; private decimal _buyingPower; private decimal _dailyPnL; private decimal _dailyPnLPercent; private int _openPositionsCount; private int _openOrdersCount; private string _botStatus; private bool _isBotRunning; private bool _isLoading; private string _accountStatus; private bool _isPaperTrading; private string _accountNumber; private string _currency; private string _errorMessage; private bool _hasError; private int _selectedTabIndex; public int SelectedTabIndex { get => _selectedTabIndex; set => SetProperty(ref _selectedTabIndex, value); } public decimal Equity { get => _equity; set => SetProperty(ref _equity, value); } public decimal BuyingPower { get => _buyingPower; set => SetProperty(ref _buyingPower, value); } public decimal DailyPnL { get => _dailyPnL; set { if (SetProperty(ref _dailyPnL, value)) OnPropertyChanged(nameof(IsPnLPositive)); } } public decimal DailyPnLPercent { get => _dailyPnLPercent; set => SetProperty(ref _dailyPnLPercent, value); } public bool IsPnLPositive => DailyPnL >= 0; public int OpenPositionsCount { get => _openPositionsCount; set => SetProperty(ref _openPositionsCount, value); } public int OpenOrdersCount { get => _openOrdersCount; set => SetProperty(ref _openOrdersCount, value); } public string BotStatus { get => _botStatus; set => SetProperty(ref _botStatus, value); } public bool IsBotRunning { get => _isBotRunning; set => SetProperty(ref _isBotRunning, value); } public bool IsLoading { get => _isLoading; set => SetProperty(ref _isLoading, value); } public string AccountStatus { get => _accountStatus; set => SetProperty(ref _accountStatus, value); } public bool IsPaperTrading { get => _isPaperTrading; set => SetProperty(ref _isPaperTrading, value); } public string AccountNumber { get => _accountNumber; set => SetProperty(ref _accountNumber, value); } public string Currency { get => _currency; set => SetProperty(ref _currency, value); } public string ErrorMessage { get => _errorMessage; set => SetProperty(ref _errorMessage, value); } public bool HasError { get => _hasError; set => SetProperty(ref _hasError, value); } public ObservableCollection BalanceRows { get; } = new ObservableCollection(); public ObservableCollection Positions { get; } = new ObservableCollection(); public ObservableCollection Orders { get; } = new ObservableCollection(); public ObservableCollection ActivityLog { get; } = new ObservableCollection(); /// /// Statistiche e rate-limiter per le chiamate API Alpaca. /// Disponibile per il binding in dashboard (null se il servizio non è ancora inizializzato). /// public ApiCallCounterService ApiCounter => (_tradingService as AlpacaTradingService)?.ApiCounter; public System.Windows.Input.ICommand RefreshCommand { get; } public System.Windows.Input.ICommand CancelOrderCommand { get; } public System.Windows.Input.ICommand CloseAllCommand { get; } public System.Windows.Input.ICommand CopyActivityLogCommand { get; } public DashboardViewModel(ITradingService tradingService, AutomatedBotEngine botEngine) { _tradingService = tradingService; _botEngine = botEngine; BotStatus = "In attesa"; AccountStatus = "--"; RefreshCommand = new RelayCommand(async _ => await LoadAsync()); CancelOrderCommand = new RelayCommand(async p => { if (p is Guid id) await CancelOrderAsync(id); }); CloseAllCommand = new RelayCommand(async _ => { await _tradingService.CloseAllPositionsAsync(); await LoadAsync(); }); CopyActivityLogCommand = new RelayCommand(_ => { if (ActivityLog.Count == 0) return; var sb = new StringBuilder(); foreach (var entry in ActivityLog.Reverse()) sb.AppendLine(string.Format("[{0:HH:mm:ss dd/MM/yy}] [{1}] {2}", entry.Timestamp, entry.Level, entry.Message)); Clipboard.SetText(sb.ToString()); }); } public async Task LoadAsync() { IsLoading = true; HasError = false; try { var accountTask = _tradingService.GetAccountAsync(); var positionsTask = _tradingService.GetAllPositionsAsync(); var ordersTask = _tradingService.GetOrdersAsync(OrderStatusFilter.All, 100); await Task.WhenAll(accountTask, positionsTask, ordersTask); var account = accountTask.Result; var positions = positionsTask.Result; var orders = ordersTask.Result; var equity = account.Equity ?? 0m; var lastEquity = account.LastEquity; var pnl = equity - lastEquity; var pnlPct = lastEquity > 0 ? (pnl / lastEquity) * 100m : 0m; Application.Current.Dispatcher.Invoke(() => { Equity = equity; BuyingPower = account.BuyingPower ?? 0m; DailyPnL = pnl; DailyPnLPercent = pnlPct; OpenPositionsCount = positions.Count; AccountStatus = account.IsTradingBlocked ? "BLOCCATO" : "ATTIVO"; AccountNumber = account.AccountNumber; Currency = account.Currency; BalanceRows.Clear(); AddSection("Buying Power"); AddRow("RegT Buying Power", account.RegulationBuyingPower, account.RegulationBuyingPower); AddRow("Day Trading Buying Power", account.DayTradingBuyingPower, account.DayTradingBuyingPower); AddRow("Effective Buying Power", account.BuyingPower, account.BuyingPower); AddRow("Non-Marginable Buying Power", account.NonMarginableBuyingPower, account.NonMarginableBuyingPower); AddSection("Margin"); AddRow("Initial Margin", account.InitialMargin, account.InitialMargin); AddRow("Maintenance Margin", account.MaintenanceMargin, account.MaintenanceMargin); AddSection("Cash"); AddRow("Cash", account.TradableCash, account.TradableCash); AddSection("Positions"); AddRow("Equity", lastEquity, equity, bold: true); AddRow("Long Market Value", account.LongMarketValue, account.LongMarketValue); AddRow("Short Market Value", account.ShortMarketValue, account.ShortMarketValue); Positions.Clear(); foreach (var p in positions) Positions.Add(new PositionViewModel(p)); Orders.Clear(); int openCount = 0; foreach (var o in orders) { var ovm = new OrderViewModel(o); Orders.Add(ovm); if (ovm.IsOpen) openCount++; } OpenOrdersCount = openCount; AddActivityLog("SUCCESS", $"Dati aggiornati — Equity: ${equity:N2} | Posizioni: {positions.Count} | Ordini aperti: {openCount}"); }); } catch (Exception ex) { Application.Current.Dispatcher.Invoke(() => { HasError = true; ErrorMessage = "Errore: " + ex.Message; AddActivityLog("ERROR", ex.Message); }); } finally { Application.Current.Dispatcher.Invoke(() => IsLoading = false); } } public void UpdateEquity(decimal equity) { Equity = equity; AddActivityLog("INFO", $"Equity aggiornata: ${equity:N2}"); } /// Aggiunge una voce al log attività (thread-safe via Dispatcher, limiti parametrizzati). public void AddActivityLog(string level, string message) { Application.Current.Dispatcher.Invoke(() => { ActivityLog.Insert(0, ActivityLogEntry.Create(level, message)); // Recupera il limite dalla configurazione del bot, fallback a 5000 int maxActivityEntries = _botEngine?.Model?.Config?.LoggingConfig?.MaxActivityLogEntries ?? 5000; while (ActivityLog.Count > maxActivityEntries) ActivityLog.RemoveAt(ActivityLog.Count - 1); }); } private void AddSection(string title) => BalanceRows.Add(new BalanceRowViewModel { Label = title, IsSection = true }); private void AddRow(string label, decimal? last, decimal? curr, bool bold = false) => BalanceRows.Add(new BalanceRowViewModel { Label = label, LastClose = last.HasValue ? "$" + last.Value.ToString("N2") : "-", Current = curr.HasValue ? "$" + curr.Value.ToString("N2") : "-", IsBold = bold }); private void AddRow(string label, decimal last, decimal curr, bool bold = false) => BalanceRows.Add(new BalanceRowViewModel { Label = label, LastClose = "$" + last.ToString("N2"), Current = "$" + curr.ToString("N2"), IsBold = bold }); private async Task CancelOrderAsync(Guid orderId) { try { await _tradingService.CancelOrderAsync(orderId); await LoadAsync(); } catch (Exception ex) { Application.Current.Dispatcher.Invoke(() => { HasError = true; ErrorMessage = "Errore cancellazione: " + ex.Message; }); } } } }