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;
});
}
}
}
}