380 lines
16 KiB
C#
380 lines
16 KiB
C#
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
|
|
{
|
|
/// <summary>Voce del log attività mostrata nella dashboard.</summary>
|
|
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<BalanceRowViewModel> BalanceRows { get; } = new ObservableCollection<BalanceRowViewModel>();
|
|
public ObservableCollection<PositionViewModel> Positions { get; } = new ObservableCollection<PositionViewModel>();
|
|
public ObservableCollection<OrderViewModel> Orders { get; } = new ObservableCollection<OrderViewModel>();
|
|
public ObservableCollection<ActivityLogEntry> ActivityLog { get; } = new ObservableCollection<ActivityLogEntry>();
|
|
|
|
/// <summary>
|
|
/// Statistiche e rate-limiter per le chiamate API Alpaca.
|
|
/// Disponibile per il binding in dashboard (null se il servizio non è ancora inizializzato).
|
|
/// </summary>
|
|
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}");
|
|
}
|
|
|
|
/// <summary>Aggiunge una voce al log attività (thread-safe via Dispatcher, limiti parametrizzati).</summary>
|
|
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;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|