using TradingBot.Models; namespace TradingBot.Services; public class TradingBotService { private readonly IMarketDataService _marketDataService; private readonly ITradingStrategy _strategy; 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 Timer? _timer; public BotStatus Status { get; private set; } = new(); public IReadOnlyList Trades => _trades.AsReadOnly(); public IReadOnlyDictionary AssetConfigurations => _assetConfigs; public IReadOnlyDictionary AssetStatistics => _assetStats; 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) { _marketDataService = marketDataService; _strategy = strategy; Status.CurrentStrategy = strategy.Name; // Subscribe to simulated market updates if available if (_marketDataService is SimulatedMarketDataService simService) { simService.OnPriceUpdated += HandleSimulatedPriceUpdate; } InitializeDefaultAssets(); } 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, // Enable ALL assets by default for full simulation 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; // 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)); OnStatusChanged?.Invoke(); } public void Stop() { if (!Status.IsRunning) return; Status.IsRunning = false; _timer?.Dispose(); _timer = null; OnStatusChanged?.Invoke(); } 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) { // Add null check for 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]); // Add null check for signal 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) { ExecuteBuy(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) { ExecuteSell(symbol, price.Price, config.CurrentHoldings, config); } } await Task.CompletedTask; } private void ExecuteBuy(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); UpdateAssetStatistics(symbol, trade); Status.TradesExecuted++; OnTradeExecuted?.Invoke(trade); OnStatusChanged?.Invoke(); } private void ExecuteSell(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); UpdateAssetStatistics(symbol, trade, profit); Status.TradesExecuted++; OnTradeExecuted?.Invoke(trade); OnStatusChanged?.Invoke(); } private void UpdateIndicators(string symbol) { if (!_priceHistory.TryGetValue(symbol, out var history) || history == null || history.Count < 26) return; // Filter out null prices and extract valid price values var prices = history .Where(p => p != null && p.Price > 0) .Select(p => p.Price) .ToList(); // Ensure we still have enough data after filtering 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); } 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(); } }