Sviluppo TradingBot
This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user