Sviluppo TradingBot

This commit is contained in:
2026-06-09 18:29:41 +02:00
parent 61f1e59964
commit e3c0bd51b2
133 changed files with 24903 additions and 1 deletions
+260
View File
@@ -0,0 +1,260 @@
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using Alpaca.Markets;
using DesktopBot.Services;
namespace DesktopBot.ViewModels
{
// ─── BalanceViewModel ────────────────────────────────────────────────────────
public class BalanceViewModel : BaseViewModel
{
private readonly ITradingService _tradingService;
private bool _isLoading;
private string _errorMessage;
private bool _hasError;
public bool IsLoading
{
get => _isLoading;
set => SetProperty(ref _isLoading, 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 ICommand RefreshCommand { get; }
public BalanceViewModel(ITradingService tradingService)
{
_tradingService = tradingService;
RefreshCommand = new RelayCommand(async _ => await LoadAsync());
}
public async Task LoadAsync()
{
IsLoading = true;
HasError = false;
try
{
var account = await _tradingService.GetAccountAsync();
Application.Current.Dispatcher.Invoke(() =>
{
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", account.LastEquity, account.Equity ?? 0m, bold: true);
AddRow("Long Market Value", account.LongMarketValue, account.LongMarketValue);
AddRow("Short Market Value", account.ShortMarketValue, account.ShortMarketValue);
});
}
catch (Exception ex)
{
Application.Current.Dispatcher.Invoke(() =>
{
HasError = true;
ErrorMessage = "Errore: " + ex.Message;
});
}
finally
{
Application.Current.Dispatcher.Invoke(() => IsLoading = false);
}
}
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
});
}
// ─── PositionsViewModel ──────────────────────────────────────────────────────
public class PositionsViewModel : BaseViewModel
{
private readonly ITradingService _tradingService;
private bool _isLoading;
private string _errorMessage;
private bool _hasError;
public bool IsLoading
{
get => _isLoading;
set => SetProperty(ref _isLoading, value);
}
public string ErrorMessage
{
get => _errorMessage;
set => SetProperty(ref _errorMessage, value);
}
public bool HasError
{
get => _hasError;
set => SetProperty(ref _hasError, value);
}
public ObservableCollection<PositionViewModel> Positions { get; }
= new ObservableCollection<PositionViewModel>();
public ICommand RefreshCommand { get; }
public ICommand CloseAllCommand { get; }
public ICommand ClosePositionCommand { get; }
public PositionsViewModel(ITradingService tradingService)
{
_tradingService = tradingService;
RefreshCommand = new RelayCommand(async _ => await LoadAsync());
CloseAllCommand = new RelayCommand(async _ =>
{
await _tradingService.CloseAllPositionsAsync();
await LoadAsync();
});
ClosePositionCommand = new RelayCommand(async p =>
{
if (p is string symbol)
{
await _tradingService.ClosePositionAsync(symbol);
await LoadAsync();
}
});
}
public async Task LoadAsync()
{
IsLoading = true;
HasError = false;
try
{
var positions = await _tradingService.GetAllPositionsAsync();
Application.Current.Dispatcher.Invoke(() =>
{
Positions.Clear();
foreach (var p in positions)
Positions.Add(new PositionViewModel(p));
});
}
catch (Exception ex)
{
Application.Current.Dispatcher.Invoke(() =>
{
HasError = true;
ErrorMessage = "Errore: " + ex.Message;
});
}
finally
{
Application.Current.Dispatcher.Invoke(() => IsLoading = false);
}
}
}
// ─── OrdersViewModel ────────────────────────────────────────────────────────
public class OrdersViewModel : BaseViewModel
{
private readonly ITradingService _tradingService;
private bool _isLoading;
private string _errorMessage;
private bool _hasError;
public bool IsLoading
{
get => _isLoading;
set => SetProperty(ref _isLoading, value);
}
public string ErrorMessage
{
get => _errorMessage;
set => SetProperty(ref _errorMessage, value);
}
public bool HasError
{
get => _hasError;
set => SetProperty(ref _hasError, value);
}
public ObservableCollection<OrderViewModel> Orders { get; }
= new ObservableCollection<OrderViewModel>();
public ICommand RefreshCommand { get; }
public ICommand CancelOrderCommand { get; }
public OrdersViewModel(ITradingService tradingService)
{
_tradingService = tradingService;
RefreshCommand = new RelayCommand(async _ => await LoadAsync());
CancelOrderCommand = new RelayCommand(async p =>
{
if (p is Guid id)
{
await _tradingService.CancelOrderAsync(id);
await LoadAsync();
}
});
}
public async Task LoadAsync()
{
IsLoading = true;
HasError = false;
try
{
var orders = await _tradingService.GetOrdersAsync(OrderStatusFilter.All, 100);
Application.Current.Dispatcher.Invoke(() =>
{
Orders.Clear();
foreach (var o in orders)
Orders.Add(new OrderViewModel(o));
});
}
catch (Exception ex)
{
Application.Current.Dispatcher.Invoke(() =>
{
HasError = true;
ErrorMessage = "Errore: " + ex.Message;
});
}
finally
{
Application.Current.Dispatcher.Invoke(() => IsLoading = false);
}
}
}
}
+82
View File
@@ -0,0 +1,82 @@
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using DesktopBot.Models;
namespace DesktopBot.ViewModels
{
/// <summary>
/// Base ViewModel con implementazione INotifyPropertyChanged
/// </summary>
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
if (Equals(field, value))
return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
}
/// <summary>
/// Implementazione semplice di ICommand
/// </summary>
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter);
public void Execute(object parameter) => _execute(parameter);
}
/// <summary>
/// Command senza parametri
/// </summary>
public class RelayCommand<T> : ICommand
{
private readonly Action<T> _execute;
private readonly Func<T, bool> _canExecute;
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public RelayCommand(Action<T> execute, Func<T, bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute == null || _canExecute((T)parameter);
public void Execute(object parameter) => _execute((T)parameter);
}
}
@@ -0,0 +1,85 @@
using System;
using System.Windows.Input;
using DesktopBot.Engine;
using DesktopBot.Models;
using DesktopBot.Services;
namespace DesktopBot.ViewModels
{
/// <summary>
/// ViewModel per la configurazione del bot
/// </summary>
public class BotConfigViewModel : BaseViewModel
{
private readonly ITradingService _tradingService;
private readonly AutomatedBotEngine _botEngine;
private BotConfiguration _config = new BotConfiguration();
private bool _isRunning;
private string _statusMessage;
public BotConfiguration Config
{
get => _config;
set => SetProperty(ref _config, value);
}
public bool IsRunning
{
get => _isRunning;
set => SetProperty(ref _isRunning, value);
}
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
public ICommand StartBotCommand { get; }
public ICommand StopBotCommand { get; }
public BotConfigViewModel(ITradingService tradingService, AutomatedBotEngine botEngine)
{
_tradingService = tradingService;
_botEngine = botEngine;
StartBotCommand = new RelayCommand(
async _ => await StartBotAsync(),
_ => !IsRunning
);
StopBotCommand = new RelayCommand(
_ => StopBot(),
_ => IsRunning
);
}
public void LoadConfiguration()
{
Config = new BotConfiguration();
}
private async System.Threading.Tasks.Task StartBotAsync()
{
try
{
IsRunning = true;
StatusMessage = $"Bot avviato - {Config.Symbol} - Strategia: {Config.Strategy}";
await _botEngine.StartAsync(Config);
}
catch (Exception ex)
{
StatusMessage = $"Errore: {ex.Message}";
IsRunning = false;
}
}
private void StopBot()
{
_botEngine.Stop();
IsRunning = false;
StatusMessage = "Bot arrestato";
}
}
}
@@ -0,0 +1,548 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using DesktopBot.Engine;
using DesktopBot.Models;
using DesktopBot.Services;
namespace DesktopBot.ViewModels
{
public class BotInstanceViewModel : BaseViewModel
{
private readonly ITradingService _tradingService;
private AutomatedBotEngine _engine;
private bool _isRunning;
private bool _isWaitingMarket;
private string _statusMessage = "Inattivo";
private string _lastSignal = "---";
private decimal _lastPrice;
public ObservableCollection<BotLogEntry> BotLog { get; } = new ObservableCollection<BotLogEntry>();
public ObservableCollection<BotTradeRecord> OpenPositions { get; } = new ObservableCollection<BotTradeRecord>();
public ObservableCollection<BotTradeRecord> TradeHistory { get; } = new ObservableCollection<BotTradeRecord>();
// CancellationToken per il polling periodico delle posizioni
private CancellationTokenSource _positionPollCts;
private void AppendLog(string message, Models.LogLevel level = Models.LogLevel.Info)
{
var entry = new BotLogEntry(level, message);
var dispatcher = System.Windows.Application.Current?.Dispatcher;
if (dispatcher != null && !dispatcher.CheckAccess())
dispatcher.Invoke(() => { BotLog.Add(entry); TrimLog(); });
else
{ BotLog.Add(entry); TrimLog(); }
}
private void TrimLog()
{
int maxEntries = Model?.Config?.LoggingConfig?.MaxBotLogEntries ?? 5000;
while (BotLog.Count > maxEntries) BotLog.RemoveAt(0);
}
public BotInstance Model { get; }
public string BotId => Model.BotId;
public string BadgeColor => Model.BadgeColor;
public string Name
{
get => Model.Name;
set { Model.Name = value; OnPropertyChanged(); OnPropertyChanged(nameof(DisplayTitle)); }
}
public string Symbol
{
get => Model.Symbol;
set
{
// Impedisce la modifica del simbolo se il bot è bloccato
if (Model.IsAssetLocked)
{
AppendLog($"⚠ Tentativo di modifica del simbolo ignorato: il bot è bloccato su {Model.Symbol}", Models.LogLevel.Warning);
return;
}
Model.Symbol = value;
Model.Config.Symbol = value;
OnPropertyChanged();
OnPropertyChanged(nameof(DisplayTitle));
RefreshMarketStatus();
}
}
public string AssetName => Model.AssetName;
public string AssetClass
{
get => Model.AssetClass;
set
{
// Impedisce la modifica della classe se il bot è bloccato
if (Model.IsAssetLocked)
{
AppendLog($"⚠ Tentativo di modifica della classe asset ignorato: il bot è bloccato", Models.LogLevel.Warning);
return;
}
Model.AssetClass = value;
OnPropertyChanged();
RefreshMarketStatus();
RefreshStrategyProfiles();
}
}
public bool IsEnabled
{
get => Model.IsEnabled;
set { Model.IsEnabled = value; OnPropertyChanged(); }
}
public string Notes
{
get => Model.Notes;
set { Model.Notes = value; OnPropertyChanged(); }
}
public BotConfiguration Config => Model.Config;
/// <summary>
/// Indica se il bot è bloccato a un asset specifico (immutabile).
/// </summary>
public bool IsAssetLocked => Model.IsAssetLocked;
/// <summary>
/// Timestamp del blocco dell'asset.
/// </summary>
public string LockedAtLabel => Model.LockedAt?.ToString("dd/MM/yyyy HH:mm") ?? "---";
/// <summary>
/// Indica se la configurazione della strategia è bloccata.
/// </summary>
public bool IsConfigLocked => Model.Config.IsLocked;
/// <summary>
/// Aggiorna le proprietà relative al lock status.
/// Chiamato dopo l'associazione dell'asset.
/// </summary>
public void RefreshLockStatus()
{
OnPropertyChanged(nameof(IsAssetLocked));
OnPropertyChanged(nameof(IsConfigLocked));
OnPropertyChanged(nameof(LockedAtLabel));
}
public bool IsRunning
{
get => _isRunning;
private set
{
SetProperty(ref _isRunning, value);
OnPropertyChanged(nameof(StatusColor));
OnPropertyChanged(nameof(RunButtonLabel));
OnPropertyChanged(nameof(StatusBadge));
}
}
public bool IsWaitingMarket
{
get => _isWaitingMarket;
private set
{
SetProperty(ref _isWaitingMarket, value);
OnPropertyChanged(nameof(StatusColor));
OnPropertyChanged(nameof(StatusBadge));
}
}
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
public string LastSignal
{
get => _lastSignal;
private set => SetProperty(ref _lastSignal, value);
}
public decimal LastPrice
{
get => _lastPrice;
private set => SetProperty(ref _lastPrice, value);
}
public bool IsMarketOpen => MarketHoursService.IsMarketOpen(Model.AssetClass);
public string MarketStatusLabel => MarketHoursService.GetMarketStatusLabel(Model.AssetClass);
public string MarketStatusColor => MarketHoursService.GetMarketStatusColor(Model.AssetClass);
private void RefreshMarketStatus()
{
OnPropertyChanged(nameof(IsMarketOpen));
OnPropertyChanged(nameof(MarketStatusLabel));
OnPropertyChanged(nameof(MarketStatusColor));
}
public string StatusColor
{
get
{
if (!IsRunning) return "#555566";
if (IsWaitingMarket) return "#FFC107";
return "#00E676";
}
}
public string StatusBadge
{
get
{
if (!IsRunning) return "FERMO";
if (IsWaitingMarket) return "IN ATTESA";
return "ATTIVO";
}
}
public bool IsSelected
{
get => _isSelected;
set => SetProperty(ref _isSelected, value);
}
private bool _isSelected;
public string RunButtonLabel => IsRunning ? "STOP" : "START";
public string DisplayTitle => $"{Name} [{Symbol}]";
public string CreatedAtLabel => Model.CreatedAt.ToString("dd/MM/yyyy HH:mm");
public ObservableCollection<StrategyProfileViewModel> StrategyProfiles { get; }
= new ObservableCollection<StrategyProfileViewModel>();
public bool IsUsingRecommendedStrategy => Model.Config.IsOptimizedForAsset;
public string RecommendedStrategyLabel
{
get
{
if (!Model.Config.RecommendedStrategy.HasValue) return string.Empty;
var profiles = StrategyAdvisor.GetAvailableProfiles(Model.AssetClass);
foreach (var p in profiles)
if (p.Strategy == Model.Config.RecommendedStrategy.Value)
return $"{p.Icon} {p.DisplayName}";
return Model.Config.RecommendedStrategy.Value.ToString();
}
}
private void RefreshStrategyProfiles()
{
var profiles = StrategyAdvisor.GetAvailableProfiles(Model.AssetClass);
StrategyProfiles.Clear();
foreach (var p in profiles)
StrategyProfiles.Add(new StrategyProfileViewModel(p, this));
OnPropertyChanged(nameof(RecommendedStrategyLabel));
OnPropertyChanged(nameof(IsUsingRecommendedStrategy));
RefreshStrategySelection();
}
public void RefreshStrategySelection()
{
foreach (var sp in StrategyProfiles)
sp.RefreshIsActive();
OnPropertyChanged(nameof(IsUsingRecommendedStrategy));
OnPropertyChanged(nameof(StrategyDescription));
OnPropertyChanged(nameof(RecommendedStrategyLabel));
}
internal void ApplyStrategyProfile(StrategyProfile profile)
{
// Impedisce il cambio di strategia se il bot è bloccato
if (Model.Config.IsLocked)
{
AppendLog($"⚠ Tentativo di cambio strategia ignorato: la configurazione è bloccata su {Model.Config.Strategy}", Models.LogLevel.Warning);
StatusMessage = $"❌ Impossibile cambiare strategia: bot bloccato su {Model.Config.Strategy}";
return;
}
profile.ApplyParameters(Model.Config);
Model.Config.Strategy = profile.Strategy;
if (profile.IsRecommended)
Model.Config.RecommendedStrategy = profile.Strategy;
RefreshStrategySelection();
OnPropertyChanged(nameof(Config));
}
public string StrategyDescription
{
get
{
var c = Model.Config;
switch (c.Strategy)
{
case TradingStrategy.EMA_CROSSOVER:
return $"EMA CROSSOVER -- EMA veloce ({c.FastEmaPeriod} p.) vs EMA lenta ({c.SlowEmaPeriod} p.).\n" +
$"ACQUISTO al crossover rialzista -- VENDITA al ribassista.\n" +
$"SL: {c.StopLossPercentage * 100:N1}% x TP: {c.TakeProfitPercentage * 100:N1}% x Qty: {c.Quantity} x Ogni {c.CheckIntervalSeconds}s";
case TradingStrategy.RSI:
return $"RSI ({c.RsiPeriod} p.) -- oscillatore di momentum 0-100.\n" +
$"ACQUISTO: RSI < {c.RsiOversoldThreshold} x VENDITA: RSI > {c.RsiOverboughtThreshold}.\n" +
$"SL: {c.StopLossPercentage * 100:N1}% x TP: {c.TakeProfitPercentage * 100:N1}% x Qty: {c.Quantity} x Ogni {c.CheckIntervalSeconds}s";
case TradingStrategy.MACD:
return $"MACD -- EMA({c.MacdFastPeriod}) - EMA({c.MacdSlowPeriod}) con Signal EMA({c.MacdSignalPeriod}).\n" +
$"ACQUISTO al crossover rialzista MACD > Signal.\n" +
$"SL: {c.StopLossPercentage * 100:N1}% x TP: {c.TakeProfitPercentage * 100:N1}% x Qty: {c.Quantity} x Ogni {c.CheckIntervalSeconds}s";
case TradingStrategy.VOLATILITY_BREAKOUT:
return $"VOLATILITY BREAKOUT -- Keltner EMA({c.KeltnerPeriod}) +/- {c.KeltnerMultiplier}xATR.\n" +
$"Breakout: close > banda AND RVOL >= {c.RvolMinThreshold}x AND CVD slope > 0.\n" +
$"SL = min barra - {c.AtrStopMultiplier}xATR x TP simmetrico x Qty: {c.Quantity} x Ogni {c.CheckIntervalSeconds}s";
case TradingStrategy.KALMAN_MEAN_REVERSION:
return $"KALMAN MEAN REVERSION -- fair value dinamico via filtro di Kalman.\n" +
$"ACQUISTO: Z-Score < -{c.KalmanEntryZScore:F1} x USCITA: |Z| < {c.KalmanExitZScore:F2}.\n" +
$"delta={c.KalmanDelta:G3} x SL: {c.StopLossPercentage * 100:N1}% x TP: {c.TakeProfitPercentage * 100:N1}% x Qty: {c.Quantity} x Ogni {c.CheckIntervalSeconds}s";
default:
return "Strategia non riconosciuta.";
}
}
}
public void RefreshStrategyDescription() => OnPropertyChanged(nameof(StrategyDescription));
public ICommand ToggleRunCommand { get; }
public ICommand RemoveCommand { get; }
public ICommand CopyLogCommand { get; }
public event EventHandler RemoveRequested;
public BotInstanceViewModel(BotInstance model, ITradingService tradingService)
{
Model = model ?? throw new ArgumentNullException(nameof(model));
_tradingService = tradingService ?? throw new ArgumentNullException(nameof(tradingService));
_engine = new AutomatedBotEngine(_tradingService);
_engine.LogGenerated += (s, log) =>
{
StatusMessage = $"[{log.Level}] {log.Message}";
AppendLog(log.Message, log.Level);
};
_engine.SignalGenerated += (s, signal) =>
{
var reason = signal.Reason ?? signal.Type.ToString();
LastSignal = reason;
AppendLog($"SEGNALE: {reason}", Models.LogLevel.Success);
// Dopo ogni segnale rilevante aggiorna le posizioni chiedendo conferma ad Alpaca
if (signal.Type == SignalType.Buy || signal.Type == SignalType.Sell)
_ = RefreshOpenPositionsAsync(signal);
};
_engine.EquityUpdated += (s, eq) =>
{
LastPrice = eq;
AppendLog($"Equity: {eq:N2}", Models.LogLevel.Info);
};
_engine.WaitingForMarketChanged += (s, waiting) =>
{
IsWaitingMarket = waiting;
AppendLog(waiting
? $"Mercato chiuso per {Model.Symbol} -- in attesa apertura"
: $"Mercato aperto -- ripresa operativita su {Model.Symbol}",
Models.LogLevel.Info);
};
ToggleRunCommand = new RelayCommand(_ => _ = ToggleRunAsync());
RemoveCommand = new RelayCommand(
_ => RemoveRequested?.Invoke(this, EventArgs.Empty),
_ => !Model.IsAssetLocked); // il bot fisso BTC/USD non può essere rimosso
CopyLogCommand = new RelayCommand(_ =>
{
if (BotLog.Count == 0) return;
var sb = new StringBuilder();
foreach (var entry in BotLog)
sb.AppendLine($"[{entry.Timestamp:dd-MM-yyyy HH:mm:ss}] [{entry.Level}] {entry.Message}");
Clipboard.SetText(sb.ToString());
});
RefreshStrategyProfiles();
}
private async Task ToggleRunAsync()
{
if (IsRunning) StopBot();
else await StartBotAsync();
}
private async Task StartBotAsync()
{
if (string.IsNullOrWhiteSpace(Model.Symbol))
{
StatusMessage = "Nessun asset associato. Seleziona un simbolo prima di avviare.";
AppendLog("Avvio fallito: nessun asset associato.", Models.LogLevel.Error);
return;
}
try
{
IsRunning = true;
_engine.AssetClass = Model.AssetClass ?? "us_equity";
StatusMessage = $"Avvio... {Model.Symbol}";
AppendLog($"Avvio bot su {Model.Symbol} -- strategia: {Model.Config.Strategy}", Models.LogLevel.Info);
// Prima di avviare: leggi le posizioni correnti da Alpaca
await RefreshOpenPositionsAsync();
// Avvia polling periodico posizioni (ogni 30 secondi)
_positionPollCts = new CancellationTokenSource();
_ = StartPositionPollingAsync(_positionPollCts.Token);
await _engine.StartAsync(Model.Config);
}
catch (Exception ex)
{
StatusMessage = $"Errore: {ex.Message}";
AppendLog($"Errore durante l avvio: {ex.Message}", Models.LogLevel.Error);
IsRunning = false;
}
}
private void StopBot()
{
_engine.Stop();
_positionPollCts?.Cancel();
_positionPollCts = null;
IsRunning = false;
IsWaitingMarket = false;
StatusMessage = "Fermato manualmente";
AppendLog("Bot fermato manualmente.", Models.LogLevel.Warning);
// Refresh finale per mostrare lo stato reale dopo lo stop
_ = RefreshOpenPositionsAsync();
}
/// <summary>
/// Interroga Alpaca per le posizioni aperte sul simbolo del bot e aggiorna OpenPositions.
/// Se viene fornito un segnale di chiusura (Sell), registra il trade nello storico.
/// Nessuno stato viene mantenuto in locale: la sorgente di verità è sempre Alpaca.
/// </summary>
private async Task RefreshOpenPositionsAsync(TradingSignal closedSignal = null)
{
try
{
var position = await _tradingService.GetPositionAsync(Model.Symbol);
var d = System.Windows.Application.Current?.Dispatcher;
void RunOnUi(Action a) { if (d != null && !d.CheckAccess()) d.Invoke(a); else a(); }
RunOnUi(() =>
{
if (position != null && Math.Abs(position.Quantity) > 0)
{
// Posizione aperta confermata da Alpaca
var existing = OpenPositions.FirstOrDefault(p => p.Symbol == Model.Symbol);
if (existing == null)
{
// Nuova posizione: aggiungila
var record = new BotTradeRecord
{
Symbol = position.Symbol,
Side = position.Quantity > 0 ? "BUY" : "SELL",
EntryPrice = position.AverageEntryPrice,
Quantity = Math.Abs(position.Quantity),
OpenedAt = DateTime.Now
};
OpenPositions.Insert(0, record);
}
else
{
// Aggiorna quantità e prezzo medio (possono cambiare)
existing.Quantity = Math.Abs(position.Quantity);
existing.EntryPrice = position.AverageEntryPrice;
}
}
else
{
// Nessuna posizione aperta su Alpaca: rimuovila dalla lista
// Se c'era una posizione aperta la registriamo come chiusa nello storico
var wasOpen = OpenPositions.FirstOrDefault(p => p.Symbol == Model.Symbol);
if (wasOpen != null)
{
OpenPositions.Remove(wasOpen);
wasOpen.ClosedAt = DateTime.Now;
wasOpen.ExitPrice = closedSignal?.Price > 0 ? closedSignal.Price : wasOpen.EntryPrice;
wasOpen.PnL = wasOpen.Quantity > 0
? (wasOpen.ExitPrice - wasOpen.EntryPrice) * wasOpen.Quantity
: wasOpen.ExitPrice - wasOpen.EntryPrice;
TradeHistory.Insert(0, wasOpen);
int maxTradeEntries = Model?.Config?.LoggingConfig?.MaxTradeHistoryEntries ?? 2000;
if (TradeHistory.Count > maxTradeEntries) TradeHistory.RemoveAt(TradeHistory.Count - 1);
}
}
});
}
catch (Exception ex)
{
AppendLog($"[Warn] Impossibile aggiornare posizioni da Alpaca: {ex.Message}", Models.LogLevel.Warning);
}
}
/// <summary>
/// Polling periodico: aggiorna le posizioni ogni 30 secondi durante l'esecuzione del bot.
/// </summary>
private async Task StartPositionPollingAsync(CancellationToken ct)
{
try
{
while (!ct.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(30), ct).ConfigureAwait(false);
if (!ct.IsCancellationRequested)
await RefreshOpenPositionsAsync().ConfigureAwait(false);
}
}
catch (TaskCanceledException) { /* atteso alla cancellazione */ }
}
public void ForceStop()
{
if (IsRunning) StopBot();
}
public void StartBot()
{
if (!IsRunning) _ = StartBotAsync();
}
}
public class StrategyProfileViewModel : BaseViewModel
{
private readonly StrategyProfile _profile;
private readonly BotInstanceViewModel _owner;
public string Icon => _profile.Icon;
public string DisplayName => _profile.DisplayName;
public string Description => _profile.Description;
public string AccentColor => _profile.AccentColor;
public bool IsRecommended => _profile.IsRecommended;
public TradingStrategy Strategy => _profile.Strategy;
private bool _isActive;
public bool IsActive
{
get => _isActive;
private set => SetProperty(ref _isActive, value);
}
public ICommand SelectCommand { get; }
public StrategyProfileViewModel(StrategyProfile profile, BotInstanceViewModel owner)
{
_profile = profile;
_owner = owner;
SelectCommand = new RelayCommand(_ => _owner.ApplyStrategyProfile(_profile));
RefreshIsActive();
}
public void RefreshIsActive()
=> IsActive = _owner.Config.Strategy == _profile.Strategy;
}
}
@@ -0,0 +1,173 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Alpaca.Markets;
using DesktopBot.Engine;
using DesktopBot.Models;
using DesktopBot.Services;
namespace DesktopBot.ViewModels
{
/// <summary>
/// ViewModel del pannello Bot Manager.
/// Contiene un unico bot BTC/USD fisso, precaricato all'avvio.
/// L'utente può soltanto avviarlo e fermarlo.
/// </summary>
public class BotsManagerViewModel : BaseViewModel
{
private readonly ITradingService _tradingService;
// ── Collezione (sempre 1 elemento: il bot BTC/USD fisso) ─────────────
public ObservableCollection<BotInstanceViewModel> Bots { get; }
= new ObservableCollection<BotInstanceViewModel>();
/// <summary>Shortcut diretto al bot fisso BTC/USD.</summary>
public BotInstanceViewModel BtcBot => Bots.FirstOrDefault();
// ── Grafico prezzi BTC/USD ─────────────────────────────────────────
public PriceChartViewModel ChartVM { get; }
// ── Stato UI ─────────────────────────────────────────────────────────
private string _statusMessage;
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
// ── Comandi ──────────────────────────────────────────────────────────
public ICommand StartBotCommand { get; }
public ICommand StopBotCommand { get; }
public ICommand SaveAllCommand { get; }
// ── Ctor ─────────────────────────────────────────────────────────────
public BotsManagerViewModel(ITradingService tradingService)
{
_tradingService = tradingService ?? throw new ArgumentNullException(nameof(tradingService));
ChartVM = new PriceChartViewModel(tradingService);
StartBotCommand = new RelayCommand(_ => BtcBot?.StartBot(), _ => BtcBot != null && !BtcBot.IsRunning);
StopBotCommand = new RelayCommand(_ => BtcBot?.ForceStop(), _ => BtcBot != null && BtcBot.IsRunning);
SaveAllCommand = new RelayCommand(_ => SaveAll());
EnsureBtcBotExists();
// Avvia streaming grafico in background
_ = ChartVM.StartStreamingAsync();
}
// ── Preload bot BTC/USD fisso ─────────────────────────────────────────
private void EnsureBtcBotExists()
{
// Tenta di caricare da disco
var saved = BotInstanceStore.Load();
var btcModel = saved.FirstOrDefault(m =>
string.Equals(m.Symbol, "BTCUSD", StringComparison.OrdinalIgnoreCase));
if (btcModel == null)
{
// Prima esecuzione: crea il bot BTC/USD con parametri ottimali
btcModel = CreateBtcUsdBot();
BotInstanceStore.Save(new[] { btcModel });
}
var vm = new BotInstanceViewModel(btcModel, _tradingService);
// Il bot fisso non può essere rimosso — ignoriamo RemoveRequested
Bots.Add(vm);
StatusMessage = "Bot BTC/USD caricato — pronto all'avvio.";
}
private static BotInstance CreateBtcUsdBot()
{
var config = new BotConfiguration
{
Symbol = "BTCUSD",
Quantity = 1,
CheckIntervalSeconds = 60,
AnalysisTimeFrame = BarTimeFrame.Minute,
HistoricalBarCount = 200,
// EMA ottimizzate per 1min BTC
FastEmaPeriod = 5,
SlowEmaPeriod = 20,
// RSI
RsiPeriod = 14,
RsiOversoldThreshold = 30,
RsiOverboughtThreshold = 70,
// MACD
MacdFastPeriod = 8,
MacdSlowPeriod = 21,
MacdSignalPeriod = 5,
// Keltner / RVOL
KeltnerPeriod = 20,
KeltnerMultiplier = 2.0m,
RvolMinThreshold = 2.0m,
AtrStopMultiplier = 1.5m,
// Kalman
KalmanDelta = 5e-6,
KalmanObservationVariance = 1.0,
KalmanEntryZScore = 1.8,
KalmanExitZScore = 0.3,
// Risk
StopLossPercentage = 0.02m,
TakeProfitPercentage = 0.04m,
MaxPositionSizePercent = 0.10m,
// Algoritmo BTC/USD avanzato (dispatch automatico nell'engine)
Strategy = TradingStrategy.VOLATILITY_BREAKOUT,
RecommendedStrategy = TradingStrategy.VOLATILITY_BREAKOUT,
IsLocked = true,
LockedAt = DateTime.Now
};
var model = new BotInstance
{
Name = "BTC/USD — Algoritmo Avanzato",
BadgeColor = "#F7931A", // arancione Bitcoin
Config = config,
Notes = "Bot predefinito BTC/USD. Algoritmo multi-segnale: Regime Detector + Kalman + ATR Sizing.",
CreatedAt = DateTime.Now,
IsEnabled = true
};
// Blocca asset e config (bot fisso)
model.Symbol = "BTCUSD";
model.AssetName = "Bitcoin / US Dollar";
model.AssetClass = "crypto";
model.LockToAsset();
return model;
}
// ── Persistenza ──────────────────────────────────────────────────────
public void SaveAll()
{
BotInstanceStore.Save(Bots.Select(vm => vm.Model));
StatusMessage = $"Salvato — {DateTime.Now:HH:mm:ss}";
}
// ── Start / Stop esposto ai comandi ───────────────────────────────────
private async Task StartAllAsync()
{
if (BtcBot != null && !BtcBot.IsRunning)
BtcBot.StartBot();
await Task.CompletedTask;
}
private void StopAll()
{
if (BtcBot?.IsRunning == true)
BtcBot.ForceStop();
StatusMessage = "Bot fermato.";
}
}
}
+379
View File
@@ -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;
});
}
}
}
}
+58
View File
@@ -0,0 +1,58 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Input;
using DesktopBot.Engine;
using DesktopBot.Models;
namespace DesktopBot.ViewModels
{
/// <summary>
/// ViewModel per il Live Log
/// </summary>
public class LiveLogViewModel : BaseViewModel
{
private ObservableCollection<BotLogEntry> _logs = new ObservableCollection<BotLogEntry>();
public ObservableCollection<BotLogEntry> Logs
{
get => _logs;
set => SetProperty(ref _logs, value);
}
public ICommand ClearLogsCommand { get; }
public ICommand CopyLogsCommand { get; }
public LiveLogViewModel(AutomatedBotEngine botEngine)
{
ClearLogsCommand = new RelayCommand(_ => Logs.Clear());
CopyLogsCommand = new RelayCommand(_ =>
{
if (Logs.Count == 0) return;
var sb = new StringBuilder();
// I log sono inseriti in testa (più recente prima): li copiamo in ordine cronologico
foreach (var entry in Logs.Reverse())
sb.AppendLine(string.Format("[{0:HH:mm:ss dd/MM}] [{1}] {2}",
entry.Timestamp, entry.Level, entry.Message));
Clipboard.SetText(sb.ToString());
});
AddLog(new BotLogEntry(LogLevel.Info, "Sistema avviato - in attesa di configurazione"));
}
/// <summary>
/// Aggiunge un log entry in cima alla lista, sul UI thread (limiti parametrizzati)
/// </summary>
public void AddLog(BotLogEntry logEntry)
{
Application.Current?.Dispatcher.Invoke(() =>
{
Logs.Insert(0, logEntry);
// Limite massimo per il live log (default 10000)
int maxLiveLogEntries = 10000;
while (Logs.Count > maxLiveLogEntries)
Logs.RemoveAt(Logs.Count - 1);
});
}
}
}
@@ -0,0 +1,67 @@
using System.Windows.Input;
using DesktopBot.Models;
namespace DesktopBot.ViewModels
{
/// <summary>
/// ViewModel per la configurazione dei limiti di logging.
/// </summary>
public class LoggingSettingsViewModel : BaseViewModel
{
private LoggingConfiguration _config;
public LoggingSettingsViewModel(LoggingConfiguration config)
{
_config = config?.Clone() ?? new LoggingConfiguration();
}
/// <summary>Numero massimo di elementi nel log del bot.</summary>
public int MaxBotLogEntries
{
get => _config.MaxBotLogEntries;
set { _config.MaxBotLogEntries = value; OnPropertyChanged(); }
}
/// <summary>Numero massimo di elementi nello storico trade.</summary>
public int MaxTradeHistoryEntries
{
get => _config.MaxTradeHistoryEntries;
set { _config.MaxTradeHistoryEntries = value; OnPropertyChanged(); }
}
/// <summary>Numero massimo di elementi nel log attività della dashboard.</summary>
public int MaxActivityLogEntries
{
get => _config.MaxActivityLogEntries;
set { _config.MaxActivityLogEntries = value; OnPropertyChanged(); }
}
/// <summary>Numero massimo di elementi nel log live globale.</summary>
public int MaxLiveLogEntries
{
get => _config.MaxLiveLogEntries;
set { _config.MaxLiveLogEntries = value; OnPropertyChanged(); }
}
/// <summary>Numero massimo di punti dati nel grafico dei prezzi.</summary>
public int MaxPriceDataPoints
{
get => _config.MaxPriceDataPoints;
set { _config.MaxPriceDataPoints = value; OnPropertyChanged(); }
}
/// <summary>Restituisce la configurazione modificata.</summary>
public LoggingConfiguration GetUpdatedConfig() => _config.Clone();
/// <summary>Ripristina i valori di default.</summary>
public void ResetToDefaults()
{
var defaults = new LoggingConfiguration();
MaxBotLogEntries = defaults.MaxBotLogEntries;
MaxTradeHistoryEntries = defaults.MaxTradeHistoryEntries;
MaxActivityLogEntries = defaults.MaxActivityLogEntries;
MaxLiveLogEntries = defaults.MaxLiveLogEntries;
MaxPriceDataPoints = defaults.MaxPriceDataPoints;
}
}
}
+184
View File
@@ -0,0 +1,184 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using DesktopBot.Engine;
using DesktopBot.Models;
using DesktopBot.Services;
namespace DesktopBot.ViewModels
{
public class MainViewModel : BaseViewModel
{
private readonly ITradingService _tradingService;
private readonly AutomatedBotEngine _botEngine;
private readonly AlpacaPingService _pingService;
public PingViewModel PingVM { get; } = new PingViewModel();
// ── Auto-refresh periodico ─────────────────────────────────────────
/// <summary>Intervallo (secondi) per l'aggiornamento frequente: posizioni e ordini.</summary>
private const int FastRefreshSeconds = 30;
/// <summary>Intervallo (secondi) per l'aggiornamento lento: balance, wallet, dashboard.</summary>
private const int SlowRefreshSeconds = 60;
private CancellationTokenSource _autoRefreshCts;
private int _refreshCycle; // contatore cicli per sfalsare il refresh lento
private BaseViewModel _currentViewModel;
private string _selectedTab;
public DashboardViewModel DashboardVM { get; }
public BotsManagerViewModel BotsVM { get; }
public LiveLogViewModel LiveLogVM { get; }
public WalletViewModel WalletVM { get; }
public SettingsViewModel SettingsVM { get; }
public BalanceViewModel BalanceVM { get; }
public PositionsViewModel PositionsVM { get; }
public OrdersViewModel OrdersVM { get; }
public BaseViewModel CurrentViewModel
{
get => _currentViewModel;
set => SetProperty(ref _currentViewModel, value);
}
public string SelectedTab
{
get => _selectedTab;
set
{
if (SetProperty(ref _selectedTab, value))
NavigateToTab(value);
}
}
public ICommand NavigateCommand { get; }
public MainViewModel()
{
_tradingService = new AlpacaTradingService();
_botEngine = new AutomatedBotEngine(_tradingService);
_pingService = new AlpacaPingService();
_pingService.PingCompleted += (_, r) => PingVM.Update(r);
DashboardVM = new DashboardViewModel(_tradingService, _botEngine);
BotsVM = new BotsManagerViewModel(_tradingService);
LiveLogVM = new LiveLogViewModel(_botEngine);
WalletVM = new WalletViewModel(_tradingService);
SettingsVM = new SettingsViewModel(_tradingService, OnCredentialsSaved);
BalanceVM = new BalanceViewModel(_tradingService);
PositionsVM = new PositionsViewModel(_tradingService);
OrdersVM = new OrdersViewModel(_tradingService);
NavigateCommand = new RelayCommand(param => SelectedTab = param?.ToString());
SubscribeToBotEvents();
var saved = CredentialService.LoadCredentials();
if (saved != null)
{
_tradingService.Initialize(saved.ApiKey, saved.ApiSecret, saved.IsPaper);
_pingService.SetEnvironment(saved.IsPaper);
_pingService.Start(10);
CurrentViewModel = DashboardVM;
SelectedTab = "Dashboard";
_ = LoadInitialDataAsync();
}
else
{
CurrentViewModel = SettingsVM;
SelectedTab = "Settings";
}
}
private void NavigateToTab(string tabName)
{
CurrentViewModel = tabName switch
{
"Dashboard" => (BaseViewModel)DashboardVM,
"Bot" => BotsVM,
"LiveLog" => LiveLogVM,
"Wallet" => WalletVM,
"Settings" => SettingsVM,
"Balance" => BalanceVM,
"Positions" => PositionsVM,
"Orders" => OrdersVM,
_ => DashboardVM
};
}
private void SubscribeToBotEvents()
{
_botEngine.LogGenerated += (sender, log) => LiveLogVM.AddLog(log);
_botEngine.EquityUpdated += (sender, equity) => DashboardVM.UpdateEquity(equity);
}
private async Task LoadInitialDataAsync()
{
await Task.WhenAll(
DashboardVM.LoadAsync(),
WalletVM.LoadAsync(),
BalanceVM.LoadAsync(),
PositionsVM.LoadAsync(),
OrdersVM.LoadAsync()
);
StartAutoRefresh();
}
// ── Auto-refresh periodico ─────────────────────────────────────────
private void StartAutoRefresh()
{
_autoRefreshCts?.Cancel();
_autoRefreshCts = new CancellationTokenSource();
_refreshCycle = 0;
_ = AutoRefreshLoopAsync(_autoRefreshCts.Token);
}
private async Task AutoRefreshLoopAsync(CancellationToken ct)
{
try
{
while (!ct.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(FastRefreshSeconds), ct);
if (ct.IsCancellationRequested) break;
_refreshCycle++;
// Refresh frequente: posizioni aperte e ordini recenti
var fastTasks = new System.Collections.Generic.List<Task>
{
SafeRefresh(PositionsVM.LoadAsync),
SafeRefresh(OrdersVM.LoadAsync)
};
// Refresh lento (ogni 2 cicli = 60s): balance, wallet, dashboard
if (_refreshCycle % 2 == 0)
{
fastTasks.Add(SafeRefresh(BalanceVM.LoadAsync));
fastTasks.Add(SafeRefresh(WalletVM.LoadAsync));
fastTasks.Add(SafeRefresh(DashboardVM.LoadAsync));
}
await Task.WhenAll(fastTasks);
}
}
catch (OperationCanceledException) { /* atteso */ }
}
private static async Task SafeRefresh(Func<Task> loader)
{
try { await loader(); }
catch { /* non bloccare il ciclo per errori singoli di rete */ }
}
private void OnCredentialsSaved(string apiKey, string apiSecret, bool isPaper)
{
SelectedTab = "Dashboard";
_ = LoadInitialDataAsync();
}
public void InitializeWithCredentials(string apiKey, string apiSecret, bool isPaper)
{
_tradingService.Initialize(apiKey, apiSecret, isPaper);
}
}
}
+84
View File
@@ -0,0 +1,84 @@
using System.Windows.Media;
using DesktopBot.Services;
namespace DesktopBot.ViewModels
{
/// <summary>
/// ViewModel per l'indicatore di ping verso Alpaca.
/// Esposto in MainViewModel e mostrato nella sidebar.
/// </summary>
public class PingViewModel : BaseViewModel
{
private int _latencyMs = -1;
private bool _isOnline;
private string _statusText = "---";
private Brush _statusColor;
private static readonly Brush BrushGood = new SolidColorBrush(Color.FromRgb(0x00, 0xC8, 0x5A)); // verde
private static readonly Brush BrushFair = new SolidColorBrush(Color.FromRgb(0xFF, 0xBF, 0x00)); // giallo
private static readonly Brush BrushPoor = new SolidColorBrush(Color.FromRgb(0xFF, 0x60, 0x00)); // arancione
private static readonly Brush BrushOffline = new SolidColorBrush(Color.FromRgb(0xFF, 0x33, 0x33)); // rosso
private static readonly Brush BrushIdle = new SolidColorBrush(Color.FromRgb(0x66, 0x66, 0x80)); // grigio
public int LatencyMs
{
get => _latencyMs;
private set => SetProperty(ref _latencyMs, value);
}
public bool IsOnline
{
get => _isOnline;
private set => SetProperty(ref _isOnline, value);
}
public string StatusText
{
get => _statusText;
private set => SetProperty(ref _statusText, value);
}
public Brush StatusColor
{
get => _statusColor ??= BrushIdle;
private set => SetProperty(ref _statusColor, value);
}
public void Update(PingResult result)
{
var dispatcher = System.Windows.Application.Current?.Dispatcher;
if (dispatcher != null && !dispatcher.CheckAccess())
{
dispatcher.Invoke(() => Apply(result));
return;
}
Apply(result);
}
private void Apply(PingResult result)
{
LatencyMs = result.LatencyMs;
IsOnline = result.IsSuccess;
switch (result.Status)
{
case PingStatus.Good:
StatusText = $"{result.LatencyMs} ms";
StatusColor = BrushGood;
break;
case PingStatus.Fair:
StatusText = $"{result.LatencyMs} ms";
StatusColor = BrushFair;
break;
case PingStatus.Poor:
StatusText = $"{result.LatencyMs} ms";
StatusColor = BrushPoor;
break;
default:
StatusText = "OFFLINE";
StatusColor = BrushOffline;
break;
}
}
}
}
@@ -0,0 +1,285 @@
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
}
}
+223
View File
@@ -0,0 +1,223 @@
using System;
using System.Reflection;
using System.Windows.Input;
using DesktopBot.Models;
using DesktopBot.Services;
namespace DesktopBot.ViewModels
{
/// <summary>
/// ViewModel per la schermata di configurazione credenziali Alpaca e logging
/// </summary>
public class SettingsViewModel : BaseViewModel
{
private readonly ITradingService _tradingService;
private readonly Action<string, string, bool> _onCredentialsSaved;
private readonly Action<LoggingConfiguration> _onLoggingConfigSaved;
private LoggingSettingsViewModel _loggingSettings;
private string _apiKey;
private string _apiSecret;
private bool _isPaper = true;
private string _statusMessage;
private bool _isConnecting;
private bool _isConnected;
public string ApiKey
{
get => _apiKey;
set => SetProperty(ref _apiKey, value);
}
/// <summary>
/// Il secret non è bindabile direttamente (PasswordBox non supporta binding).
/// Viene impostato dal code-behind tramite questa proprietà.
/// </summary>
public string ApiSecret
{
get => _apiSecret;
set => SetProperty(ref _apiSecret, value);
}
public bool IsPaper
{
get => _isPaper;
set => SetProperty(ref _isPaper, value);
}
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
public bool IsConnecting
{
get => _isConnecting;
set => SetProperty(ref _isConnecting, value);
}
public bool IsConnected
{
get => _isConnected;
set => SetProperty(ref _isConnected, value);
}
/// <summary>ViewModel per la configurazione dei log</summary>
public LoggingSettingsViewModel LoggingSettings
{
get => _loggingSettings;
set => SetProperty(ref _loggingSettings, value);
}
public ICommand SaveCommand { get; }
public ICommand TestConnectionCommand { get; }
public ICommand DeleteCredentialsCommand { get; }
public ICommand SaveLoggingCommand { get; }
public ICommand ResetLoggingCommand { get; }
// ── Informazioni applicazione ──────────────────────────────────────────
public string AppVersion { get; } = Assembly.GetExecutingAssembly()
.GetName().Version?.ToString() ?? "1.0.0.0";
public string BuildDate { get; } = System.IO.File.GetLastWriteTime(
Assembly.GetExecutingAssembly().Location)
.ToString("dd/MM/yyyy HH:mm");
public string Author { get; } = "Alberto Balbo";
public string Framework { get; } = ".NET Framework 4.8.1";
public string Description { get; } = "Trading Bot automatico per Alpaca Markets API v2. " +
"Supporta paper trading e live trading con strategia EMA Crossover + RSI.";
public SettingsViewModel(ITradingService tradingService,
Action<string, string, bool> onCredentialsSaved,
Action<LoggingConfiguration> onLoggingConfigSaved = null)
{
_tradingService = tradingService;
_onCredentialsSaved = onCredentialsSaved;
_onLoggingConfigSaved = onLoggingConfigSaved;
SaveCommand = new RelayCommand(
_ => SaveCredentials(),
_ => !string.IsNullOrWhiteSpace(ApiKey) && !string.IsNullOrWhiteSpace(ApiSecret)
);
TestConnectionCommand = new RelayCommand(
async _ => await TestConnectionAsync(),
_ => !string.IsNullOrWhiteSpace(ApiKey) && !string.IsNullOrWhiteSpace(ApiSecret) && !IsConnecting
);
DeleteCredentialsCommand = new RelayCommand(_ => DeleteCredentials());
SaveLoggingCommand = new RelayCommand(_ => SaveLoggingConfiguration());
ResetLoggingCommand = new RelayCommand(_ => ResetLoggingConfiguration());
// Carica credenziali esistenti (mostra solo la key, non il secret)
var saved = CredentialService.LoadCredentials();
if (saved != null)
{
ApiKey = saved.ApiKey;
ApiSecret = saved.ApiSecret;
IsPaper = saved.IsPaper;
StatusMessage = "✓ Credenziali caricate da configurazione salvata";
IsConnected = true;
}
else
{
StatusMessage = "Inserisci le credenziali API di Alpaca";
}
// Inizializza logging settings con configurazione di default
LoggingSettings = new LoggingSettingsViewModel(new LoggingConfiguration());
}
/// <summary>
/// Imposta la configurazione di logging iniziale dal bot
/// </summary>
public void SetLoggingConfiguration(LoggingConfiguration config)
{
LoggingSettings = new LoggingSettingsViewModel(config);
}
/// <summary>
/// Salva la configurazione di logging
/// </summary>
private void SaveLoggingConfiguration()
{
var updatedConfig = LoggingSettings.GetUpdatedConfig();
_onLoggingConfigSaved?.Invoke(updatedConfig);
StatusMessage = "✓ Configurazione log salvata";
}
/// <summary>
/// Ripristina i limiti di logging ai valori di default
/// </summary>
private void ResetLoggingConfiguration()
{
LoggingSettings.ResetToDefaults();
StatusMessage = "✓ Limiti log ripristinati ai valori di default";
}
/// <summary>
/// Salva le credenziali cifrate e notifica il ViewModel principale
/// </summary>
private void SaveCredentials()
{
if (string.IsNullOrWhiteSpace(ApiKey) || string.IsNullOrWhiteSpace(ApiSecret))
{
StatusMessage = "⚠ API Key e Secret sono obbligatori";
return;
}
try
{
CredentialService.SaveCredentials(ApiKey, ApiSecret, IsPaper);
_tradingService.Initialize(ApiKey, ApiSecret, IsPaper);
_onCredentialsSaved?.Invoke(ApiKey, ApiSecret, IsPaper);
StatusMessage = "✓ Credenziali salvate e connessione configurata";
IsConnected = true;
}
catch (Exception ex)
{
StatusMessage = $"✗ Errore nel salvataggio: {ex.Message}";
}
}
/// <summary>
/// Testa la connessione alle API Alpaca con le credenziali inserite
/// </summary>
private async System.Threading.Tasks.Task TestConnectionAsync()
{
IsConnecting = true;
IsConnected = false;
StatusMessage = "⟳ Test connessione in corso...";
try
{
_tradingService.Initialize(ApiKey, ApiSecret, IsPaper);
var equity = await _tradingService.GetAvailableEquityAsync();
StatusMessage = $"✓ Connessione riuscita! Equity disponibile: ${equity:N2}";
IsConnected = true;
}
catch (Exception ex)
{
StatusMessage = $"✗ Connessione fallita: {ex.Message}";
IsConnected = false;
}
finally
{
IsConnecting = false;
}
}
/// <summary>
/// Elimina le credenziali salvate
/// </summary>
private void DeleteCredentials()
{
CredentialService.DeleteCredentials();
ApiKey = string.Empty;
ApiSecret = string.Empty;
IsConnected = false;
StatusMessage = "Credenziali eliminate. Inserisci nuove credenziali.";
}
}
}
+146
View File
@@ -0,0 +1,146 @@
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Input;
using DesktopBot.Services;
namespace DesktopBot.ViewModels
{
/// <summary>
/// ViewModel per il Wallet e le posizioni aperte
/// </summary>
public class WalletViewModel : BaseViewModel
{
private readonly ITradingService _tradingService;
private decimal _equity;
private bool _isLoading;
private string _errorMessage;
private bool _hasError;
private ObservableCollection<PositionViewModel> _positions = new ObservableCollection<PositionViewModel>();
public decimal Equity
{
get => _equity;
set => SetProperty(ref _equity, value);
}
public bool IsLoading
{
get => _isLoading;
set => SetProperty(ref _isLoading, value);
}
public string ErrorMessage
{
get => _errorMessage;
set => SetProperty(ref _errorMessage, value);
}
public bool HasError
{
get => _hasError;
set => SetProperty(ref _hasError, value);
}
public ObservableCollection<PositionViewModel> Positions
{
get => _positions;
set => SetProperty(ref _positions, value);
}
public ICommand RefreshCommand { get; }
public ICommand CloseAllCommand { get; }
public WalletViewModel(ITradingService tradingService)
{
_tradingService = tradingService;
RefreshCommand = new RelayCommand(async _ => await RefreshAsync());
CloseAllCommand = new RelayCommand(async _ => await CloseAllPositionsAsync());
}
private async System.Threading.Tasks.Task RefreshAsync()
{
await LoadAsync();
}
/// <summary>
/// Carica equity e posizioni aperte da Alpaca
/// </summary>
public async System.Threading.Tasks.Task LoadAsync()
{
IsLoading = true;
HasError = false;
try
{
var accountTask = _tradingService.GetAccountAsync();
var positionsTask = _tradingService.GetAllPositionsAsync();
await System.Threading.Tasks.Task.WhenAll(accountTask, positionsTask);
var account = accountTask.Result;
var positions = positionsTask.Result;
Application.Current?.Dispatcher.Invoke(() =>
{
Equity = account.Equity ?? 0m;
Positions.Clear();
foreach (var pos in positions)
Positions.Add(new PositionViewModel(pos));
});
}
catch (Exception ex)
{
Application.Current?.Dispatcher.Invoke(() =>
{
HasError = true;
ErrorMessage = "Errore: " + ex.Message;
});
}
finally
{
Application.Current?.Dispatcher.Invoke(() => IsLoading = false);
}
}
private async System.Threading.Tasks.Task CloseAllPositionsAsync()
{
try
{
await _tradingService.CloseAllPositionsAsync();
await RefreshAsync();
}
catch { /* Gestito silenziosamente */ }
}
}
/// <summary>
/// ViewModel per una singola posizione aperta
/// </summary>
public class PositionViewModel : BaseViewModel
{
public string Symbol { get; }
public decimal Quantity { get; }
public string Side { get; }
public decimal EntryPrice { get; }
public decimal CurrentPrice { get; }
public decimal MarketValue { get; }
public decimal UnrealizedPnL { get; }
public decimal UnrealizedPnLPercent { get; }
public bool IsProfit { get; }
public PositionViewModel(Alpaca.Markets.IPosition position)
{
Symbol = position.Symbol;
Quantity = Math.Abs(position.Quantity);
Side = position.Quantity >= 0 ? "LONG" : "SHORT";
EntryPrice = position.AverageEntryPrice;
CurrentPrice = position.AssetCurrentPrice ?? 0m;
MarketValue = position.MarketValue ?? 0m;
UnrealizedPnL = position.UnrealizedProfitLoss ?? 0m;
var cost = position.CostBasis != 0 ? position.CostBasis : 1m;
UnrealizedPnLPercent = (cost != 0) ? (UnrealizedPnL / Math.Abs(cost)) * 100m : 0m;
IsProfit = UnrealizedPnL >= 0;
}
}
}