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