using TradingBot.Models; namespace TradingBot.Services; public class TradingBotService { private readonly IMarketDataService _marketDataService; private readonly ITradingStrategy _strategy; private readonly TradeHistoryService _historyService; private readonly LoggingService _loggingService; private readonly IndicatorsService _indicatorsService; private readonly TradingStrategiesService _strategiesService; private readonly Dictionary _assetConfigs = new(); private readonly Dictionary _assetStats = new(); private readonly List _trades = new(); private readonly Dictionary> _priceHistory = new(); private readonly Dictionary _indicators = new(); private readonly Dictionary _activePositions = new(); private Timer? _timer; private Timer? _persistenceTimer; public BotStatus Status { get; private set; } = new(); public IReadOnlyList Trades => _trades.AsReadOnly(); public IReadOnlyDictionary AssetConfigurations => _assetConfigs; public IReadOnlyDictionary AssetStatistics => _assetStats; public IReadOnlyDictionary ActivePositions => _activePositions; public event Action? OnStatusChanged; public event Action? OnSignalGenerated; public event Action? OnTradeExecuted; public event Action? OnIndicatorsUpdated; public event Action? OnPriceUpdated; public event Action? OnStatisticsUpdated; public TradingBotService( IMarketDataService marketDataService, ITradingStrategy strategy, TradeHistoryService historyService, LoggingService loggingService, IndicatorsService indicatorsService, TradingStrategiesService strategiesService) { _marketDataService = marketDataService; _strategy = strategy; _historyService = historyService; _loggingService = loggingService; _indicatorsService = indicatorsService; _strategiesService = strategiesService; Status.CurrentStrategy = strategy.Name; // Subscribe to simulated market updates if available if (_marketDataService is SimulatedMarketDataService simService) { simService.OnPriceUpdated += HandleSimulatedPriceUpdate; } InitializeDefaultAssets(); // Load persisted data _ = LoadPersistedDataAsync(); _loggingService.LogInfo("System", "TradingBot Service initialized"); } private async Task LoadPersistedDataAsync() { try { // Load trade history var trades = await _historyService.LoadTradeHistoryAsync(); _trades.AddRange(trades); // Load active positions var positions = await _historyService.LoadActivePositionsAsync(); foreach (var kvp in positions) { _activePositions[kvp.Key] = kvp.Value; } // Restore asset configurations from active positions RestoreAssetConfigurationsFromTrades(); OnStatusChanged?.Invoke(); } catch (Exception ex) { Console.WriteLine($"Error loading persisted data: {ex.Message}"); } } private void RestoreAssetConfigurationsFromTrades() { foreach (var position in _activePositions.Values) { if (_assetConfigs.TryGetValue(position.Symbol, out var config)) { if (position.Type == TradeType.Buy) { config.CurrentHoldings += position.Amount; config.AverageEntryPrice = position.Price; } } } } private void InitializeDefaultAssets() { // Get available symbols from SimulatedMarketDataService var availableSymbols = _marketDataService is SimulatedMarketDataService simService ? simService.GetAvailableSymbols() : new List { "BTC", "ETH", "SOL", "ADA", "MATIC" }; var assetNames = _marketDataService is SimulatedMarketDataService simService2 ? simService2.GetAssetNames() : new Dictionary { { "BTC", "Bitcoin" }, { "ETH", "Ethereum" }, { "SOL", "Solana" }, { "ADA", "Cardano" }, { "MATIC", "Polygon" } }; foreach (var symbol in availableSymbols) { _assetConfigs[symbol] = new AssetConfiguration { Symbol = symbol, Name = assetNames.TryGetValue(symbol, out var name) ? name : symbol, IsEnabled = true, InitialBalance = 1000m, CurrentBalance = 1000m }; _assetStats[symbol] = new AssetStatistics { Symbol = symbol, Name = assetNames.TryGetValue(symbol, out var name2) ? name2 : symbol }; } } public void UpdateAssetConfiguration(string symbol, AssetConfiguration config) { _assetConfigs[symbol] = config; OnStatusChanged?.Invoke(); } public void ToggleAsset(string symbol, bool enabled) { if (_assetConfigs.TryGetValue(symbol, out var config)) { config.IsEnabled = enabled; OnStatusChanged?.Invoke(); } } public void AddAsset(string symbol, string name) { if (!_assetConfigs.ContainsKey(symbol)) { _assetConfigs[symbol] = new AssetConfiguration { Symbol = symbol, Name = name, IsEnabled = false, InitialBalance = 1000m, CurrentBalance = 1000m }; _assetStats[symbol] = new AssetStatistics { Symbol = symbol, Name = name }; OnStatusChanged?.Invoke(); } } public void Start() { if (Status.IsRunning) return; Status.IsRunning = true; Status.StartedAt = DateTime.UtcNow; _loggingService.LogInfo("Bot", "Trading Bot started", $"Strategy: {_strategy.Name}"); // Reset daily trade counts foreach (var config in _assetConfigs.Values) { if (config.DailyTradeCountReset.Date < DateTime.UtcNow.Date) { config.DailyTradeCount = 0; config.DailyTradeCountReset = DateTime.UtcNow.Date; } } // Start update timer (every 3 seconds for simulation) _timer = new Timer(async _ => await UpdateAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(3)); // Start persistence timer (save every 30 seconds) _persistenceTimer = new Timer( async _ => await SaveDataAsync(), null, TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(30)); OnStatusChanged?.Invoke(); } public async void Stop() { if (!Status.IsRunning) return; Status.IsRunning = false; _timer?.Dispose(); _timer = null; _persistenceTimer?.Dispose(); _persistenceTimer = null; _loggingService.LogInfo("Bot", "Trading Bot stopped", $"Total trades: {_trades.Count}"); // Save data on stop await SaveDataAsync(); OnStatusChanged?.Invoke(); } private async Task SaveDataAsync() { try { await _historyService.SaveTradeHistoryAsync(_trades); await _historyService.SaveActivePositionsAsync(_activePositions); } catch (Exception ex) { Console.WriteLine($"Error saving data: {ex.Message}"); } } private void HandleSimulatedPriceUpdate() { if (Status.IsRunning) { _ = UpdateAsync(); } } private async Task UpdateAsync() { try { var enabledSymbols = _assetConfigs.Values .Where(c => c != null && c.IsEnabled) .Select(c => c.Symbol) .Where(s => !string.IsNullOrWhiteSpace(s)) .ToList(); if (enabledSymbols.Count == 0) return; var prices = await _marketDataService.GetMarketPricesAsync(enabledSymbols); if (prices == null) return; foreach (var price in prices) { if (price != null) { await ProcessAssetUpdate(price); } } UpdateGlobalStatistics(); } catch (Exception ex) { Console.WriteLine($"Error in UpdateAsync: {ex.Message}"); Console.WriteLine($"Stack trace: {ex.StackTrace}"); } } private async Task ProcessAssetUpdate(MarketPrice price) { if (price == null || price.Price <= 0) return; if (!_assetConfigs.TryGetValue(price.Symbol, out var config) || !config.IsEnabled) return; // Update price history if (!_priceHistory.ContainsKey(price.Symbol)) { _priceHistory[price.Symbol] = new List(); } _priceHistory[price.Symbol].Add(price); if (_priceHistory[price.Symbol].Count > 200) { _priceHistory[price.Symbol].RemoveAt(0); } // Update statistics current price if (_assetStats.TryGetValue(price.Symbol, out var stats)) { stats.CurrentPrice = price.Price; } OnPriceUpdated?.Invoke(price.Symbol, price); // Calculate indicators if enough data if (_priceHistory[price.Symbol].Count >= 26) { UpdateIndicators(price.Symbol); // Generate trading signal var signal = await _strategy.AnalyzeAsync(price.Symbol, _priceHistory[price.Symbol]); if (signal != null) { OnSignalGenerated?.Invoke(signal); // Execute trades based on strategy and configuration await EvaluateAndExecuteTrade(price.Symbol, signal, price, config); } } } private async Task EvaluateAndExecuteTrade(string symbol, TradingSignal signal, MarketPrice price, AssetConfiguration config) { if (!_indicators.TryGetValue(symbol, out var indicators)) return; // Check daily trade limit if (config.DailyTradeCount >= config.MaxDailyTrades) return; // Check if enough time has passed since last trade (min 10 seconds) if (config.LastTradeTime.HasValue && (DateTime.UtcNow - config.LastTradeTime.Value).TotalSeconds < 10) return; // Buy logic if (signal.Type == SignalType.Buy && indicators.RSI < 40 && indicators.Histogram > 0 && config.CurrentBalance >= config.MinTradeAmount) { var tradeAmount = Math.Min( Math.Min(config.CurrentBalance * 0.3m, config.MaxTradeAmount), config.MaxPositionSize - (config.CurrentHoldings * price.Price) ); if (tradeAmount >= config.MinTradeAmount) { await ExecuteBuyAsync(symbol, price.Price, tradeAmount, config); } } // Sell logic else if (signal.Type == SignalType.Sell && indicators.RSI > 60 && indicators.Histogram < 0 && config.CurrentHoldings > 0) { var profitPercentage = config.AverageEntryPrice > 0 ? ((price.Price - config.AverageEntryPrice) / config.AverageEntryPrice) * 100 : 0; // Sell if profit target reached or stop loss triggered if (profitPercentage >= config.TakeProfitPercentage || profitPercentage <= -config.StopLossPercentage) { await ExecuteSellAsync(symbol, price.Price, config.CurrentHoldings, config); } } } private async Task ExecuteBuyAsync(string symbol, decimal price, decimal amountUSD, AssetConfiguration config) { var amount = amountUSD / price; // Update config var previousHoldings = config.CurrentHoldings; config.CurrentHoldings += amount; config.CurrentBalance -= amountUSD; config.AverageEntryPrice = previousHoldings > 0 ? ((config.AverageEntryPrice * previousHoldings) + (price * amount)) / config.CurrentHoldings : price; config.LastTradeTime = DateTime.UtcNow; config.DailyTradeCount++; var trade = new Trade { Symbol = symbol, Type = TradeType.Buy, Price = price, Amount = amount, Timestamp = DateTime.UtcNow, Strategy = _strategy.Name, IsBot = true }; _trades.Add(trade); _activePositions[symbol] = trade; UpdateAssetStatistics(symbol, trade); Status.TradesExecuted++; _loggingService.LogTrade( symbol, $"BUY {amount:F6} {symbol} @ ${price:N2}", $"Value: ${amountUSD:N2} | Balance: ${config.CurrentBalance:N2}"); OnTradeExecuted?.Invoke(trade); OnStatusChanged?.Invoke(); // Save immediately after trade await SaveDataAsync(); } private async Task ExecuteSellAsync(string symbol, decimal price, decimal amount, AssetConfiguration config) { var amountUSD = amount * price; var profit = (price - config.AverageEntryPrice) * amount; // Update config config.CurrentHoldings = 0; config.CurrentBalance += amountUSD; config.LastTradeTime = DateTime.UtcNow; config.DailyTradeCount++; var trade = new Trade { Symbol = symbol, Type = TradeType.Sell, Price = price, Amount = amount, Timestamp = DateTime.UtcNow, Strategy = _strategy.Name, IsBot = true }; _trades.Add(trade); _activePositions.Remove(symbol); UpdateAssetStatistics(symbol, trade, profit); Status.TradesExecuted++; _loggingService.LogTrade( symbol, $"SELL {amount:F6} {symbol} @ ${price:N2}", $"Value: ${amountUSD:N2} | Profit: ${profit:N2} | Balance: ${config.CurrentBalance:N2}"); OnTradeExecuted?.Invoke(trade); OnStatusChanged?.Invoke(); // Save immediately after trade await SaveDataAsync(); } private void UpdateIndicators(string symbol) { if (!_priceHistory.TryGetValue(symbol, out var history) || history == null || history.Count < 26) return; var prices = history .Where(p => p != null && p.Price > 0) .Select(p => p.Price) .ToList(); if (prices.Count < 26) return; var rsi = TechnicalAnalysis.CalculateRSI(prices); var (macd, signal, histogram) = TechnicalAnalysis.CalculateMACD(prices); var indicators = new TechnicalIndicators { RSI = rsi, MACD = macd, Signal = signal, Histogram = histogram, EMA12 = TechnicalAnalysis.CalculateEMA(prices, 12), EMA26 = TechnicalAnalysis.CalculateEMA(prices, 26) }; _indicators[symbol] = indicators; OnIndicatorsUpdated?.Invoke(symbol, indicators); // Update IndicatorsService statuses UpdateIndicatorStatuses(symbol, indicators, prices); } private void UpdateIndicatorStatuses(string symbol, TechnicalIndicators indicators, List prices) { // Update RSI status var rsiConfig = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "rsi"); if (rsiConfig?.IsEnabled == true) { var rsiStatus = new IndicatorStatus { IndicatorId = "rsi", Symbol = symbol, CurrentValue = indicators.RSI, Condition = indicators.RSI > (rsiConfig.OverboughtThreshold ?? 70) ? MarketCondition.Overbought : indicators.RSI < (rsiConfig.OversoldThreshold ?? 30) ? MarketCondition.Oversold : MarketCondition.Neutral, Recommendation = indicators.RSI > (rsiConfig.OverboughtThreshold ?? 70) ? "Possibile vendita" : indicators.RSI < (rsiConfig.OversoldThreshold ?? 30) ? "Possibile acquisto" : "Attendi conferma" }; _indicatorsService.UpdateIndicatorStatus("rsi", symbol, rsiStatus); // Generate signal if crossing threshold if (indicators.RSI < 30) { _indicatorsService.GenerateSignal(new IndicatorSignal { IndicatorId = "rsi", IndicatorName = "RSI", Symbol = symbol, Type = SignalType.Buy, Strength = indicators.RSI < 20 ? SignalStrength.VeryStrong : SignalStrength.Strong, Message = $"RSI in zona ipervenduto: {indicators.RSI:F2}", Value = indicators.RSI }); } else if (indicators.RSI > 70) { _indicatorsService.GenerateSignal(new IndicatorSignal { IndicatorId = "rsi", IndicatorName = "RSI", Symbol = symbol, Type = SignalType.Sell, Strength = indicators.RSI > 80 ? SignalStrength.VeryStrong : SignalStrength.Strong, Message = $"RSI in zona ipercomprato: {indicators.RSI:F2}", Value = indicators.RSI }); } } // Update MACD status var macdConfig = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "macd"); if (macdConfig?.IsEnabled == true) { var macdStatus = new IndicatorStatus { IndicatorId = "macd", Symbol = symbol, CurrentValue = indicators.MACD, Condition = indicators.Histogram > 0 ? MarketCondition.Bullish : MarketCondition.Bearish, Recommendation = indicators.Histogram > 0 ? "Trend rialzista" : "Trend ribassista" }; _indicatorsService.UpdateIndicatorStatus("macd", symbol, macdStatus); // Generate signal on crossover if (Math.Abs(indicators.Histogram) < 0.5m) // Near crossover { _indicatorsService.GenerateSignal(new IndicatorSignal { IndicatorId = "macd", IndicatorName = "MACD", Symbol = symbol, Type = indicators.Histogram > 0 ? SignalType.Buy : SignalType.Sell, Strength = SignalStrength.Moderate, Message = $"MACD {(indicators.Histogram > 0 ? "bullish" : "bearish")} crossover", Value = indicators.MACD }); } } // Update SMA statuses var currentPrice = prices.Last(); var sma20Config = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "sma_20"); if (sma20Config?.IsEnabled == true && prices.Count >= 20) { var sma20 = prices.TakeLast(20).Average(); var sma20Status = new IndicatorStatus { IndicatorId = "sma_20", Symbol = symbol, CurrentValue = sma20, Condition = currentPrice > sma20 ? MarketCondition.Bullish : MarketCondition.Bearish, Recommendation = currentPrice > sma20 ? "Prezzo sopra media" : "Prezzo sotto media" }; _indicatorsService.UpdateIndicatorStatus("sma_20", symbol, sma20Status); } var sma50Config = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "sma_50"); if (sma50Config?.IsEnabled == true && prices.Count >= 50) { var sma50 = prices.TakeLast(50).Average(); var sma50Status = new IndicatorStatus { IndicatorId = "sma_50", Symbol = symbol, CurrentValue = sma50, Condition = currentPrice > sma50 ? MarketCondition.Bullish : MarketCondition.Bearish, Recommendation = currentPrice > sma50 ? "Trend rialzista medio termine" : "Trend ribassista medio termine" }; _indicatorsService.UpdateIndicatorStatus("sma_50", symbol, sma50Status); } // Update EMA status var ema12Config = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "ema_12"); if (ema12Config?.IsEnabled == true) { var ema12Status = new IndicatorStatus { IndicatorId = "ema_12", Symbol = symbol, CurrentValue = indicators.EMA12, Condition = currentPrice > indicators.EMA12 ? MarketCondition.Bullish : MarketCondition.Bearish, Recommendation = currentPrice > indicators.EMA12 ? "Trend positivo" : "Trend negativo" }; _indicatorsService.UpdateIndicatorStatus("ema_12", symbol, ema12Status); } } private void UpdateAssetStatistics(string symbol, Trade trade, decimal? realizedProfit = null) { if (!_assetStats.TryGetValue(symbol, out var stats)) return; stats.TotalTrades++; stats.RecentTrades.Insert(0, trade); if (stats.RecentTrades.Count > 50) stats.RecentTrades.RemoveAt(stats.RecentTrades.Count - 1); if (!stats.FirstTradeTime.HasValue) stats.FirstTradeTime = trade.Timestamp; stats.LastTradeTime = trade.Timestamp; if (realizedProfit.HasValue) { if (realizedProfit.Value > 0) { stats.WinningTrades++; stats.TotalProfit += realizedProfit.Value; stats.ConsecutiveWins++; stats.ConsecutiveLosses = 0; stats.MaxConsecutiveWins = Math.Max(stats.MaxConsecutiveWins, stats.ConsecutiveWins); if (realizedProfit.Value > stats.LargestWin) stats.LargestWin = realizedProfit.Value; } else if (realizedProfit.Value < 0) { stats.LosingTrades++; stats.TotalLoss += Math.Abs(realizedProfit.Value); stats.ConsecutiveLosses++; stats.ConsecutiveWins = 0; stats.MaxConsecutiveLosses = Math.Max(stats.MaxConsecutiveLosses, stats.ConsecutiveLosses); if (Math.Abs(realizedProfit.Value) > stats.LargestLoss) stats.LargestLoss = Math.Abs(realizedProfit.Value); } } if (_assetConfigs.TryGetValue(symbol, out var config)) { stats.TotalProfit = config.TotalProfit; stats.ProfitPercentage = config.ProfitPercentage; stats.CurrentPosition = config.CurrentHoldings; stats.AverageEntryPrice = config.AverageEntryPrice; } OnStatisticsUpdated?.Invoke(); } private void UpdateGlobalStatistics() { decimal totalProfit = 0; int totalTrades = 0; foreach (var config in _assetConfigs.Values.Where(c => c.IsEnabled)) { totalProfit += config.TotalProfit; } totalTrades = _trades.Count; Status.TotalProfit = totalProfit; Status.TradesExecuted = totalTrades; } public PortfolioStatistics GetPortfolioStatistics() { var portfolio = new PortfolioStatistics { TotalAssets = _assetConfigs.Count, ActiveAssets = _assetConfigs.Values.Count(c => c.IsEnabled), TotalTrades = _trades.Count, AssetStatistics = _assetStats.Values.ToList(), StartDate = Status.StartedAt }; portfolio.TotalBalance = _assetConfigs.Values.Sum(c => c.CurrentBalance + (c.CurrentHoldings * (_assetStats.TryGetValue(c.Symbol, out var s) ? s.CurrentPrice : 0))); portfolio.InitialBalance = _assetConfigs.Values.Sum(c => c.InitialBalance); if (_assetStats.Values.Any()) { var winningTrades = _assetStats.Values.Sum(s => s.WinningTrades); var totalTrades = _assetStats.Values.Sum(s => s.TotalTrades); portfolio.WinRate = totalTrades > 0 ? (decimal)winningTrades / totalTrades * 100 : 0; var bestAsset = _assetStats.Values.OrderByDescending(s => s.NetProfit).FirstOrDefault(); if (bestAsset != null) { portfolio.BestPerformingAssetSymbol = bestAsset.Symbol; portfolio.BestPerformingAssetProfit = bestAsset.NetProfit; } var worstAsset = _assetStats.Values.OrderBy(s => s.NetProfit).FirstOrDefault(); if (worstAsset != null) { portfolio.WorstPerformingAssetSymbol = worstAsset.Symbol; portfolio.WorstPerformingAssetProfit = worstAsset.NetProfit; } } return portfolio; } public List? GetPriceHistory(string symbol) { return _priceHistory.TryGetValue(symbol, out var history) ? history : null; } public TechnicalIndicators? GetIndicators(string symbol) { return _indicators.TryGetValue(symbol, out var indicators) ? indicators : null; } public MarketPrice? GetLatestPrice(string symbol) { if (string.IsNullOrWhiteSpace(symbol)) return null; var history = GetPriceHistory(symbol); return history?.LastOrDefault(); } public async Task ClearAllDataAsync() { _trades.Clear(); _activePositions.Clear(); _historyService.ClearAll(); foreach (var config in _assetConfigs.Values) { config.CurrentBalance = config.InitialBalance; config.CurrentHoldings = 0; config.AverageEntryPrice = 0; config.DailyTradeCount = 0; } OnStatusChanged?.Invoke(); await Task.CompletedTask; } /// /// Manually close a position /// public async Task ClosePositionManuallyAsync(string symbol) { if (!_activePositions.TryGetValue(symbol, out var position)) { throw new InvalidOperationException($"No active position found for {symbol}"); } if (!_assetConfigs.TryGetValue(symbol, out var config)) { throw new InvalidOperationException($"Asset configuration not found for {symbol}"); } // Get current market price var latestPrice = GetLatestPrice(symbol); if (latestPrice == null || latestPrice.Price <= 0) { throw new InvalidOperationException($"Cannot get current price for {symbol}"); } // Execute sell await ExecuteSellAsync(symbol, latestPrice.Price, config.CurrentHoldings, config); } }