Files
Encelado/TradingBot/Services/TradingBotService.cs
Alberto Balbo c93ccd5e4a Migliora robustezza Dockerfile e controlli TradingBotService
- Installa wget e aggiorna healthcheck in Dockerfile (usa wget invece di curl, UID 1001 per utente non-root)
- Aggiunti controlli di nullità e validità su simboli, prezzi e segnali in TradingBotService
- Migliorata gestione delle eccezioni con stampa dello stack trace
- Filtrati dati non validi prima del calcolo degli indicatori
- Aumentata la sicurezza e la resilienza contro dati corrotti o incompleti
2025-12-15 10:37:31 +01:00

516 lines
17 KiB
C#

using TradingBot.Models;
namespace TradingBot.Services;
public class TradingBotService
{
private readonly IMarketDataService _marketDataService;
private readonly ITradingStrategy _strategy;
private readonly Dictionary<string, AssetConfiguration> _assetConfigs = new();
private readonly Dictionary<string, AssetStatistics> _assetStats = new();
private readonly List<Trade> _trades = new();
private readonly Dictionary<string, List<MarketPrice>> _priceHistory = new();
private readonly Dictionary<string, TechnicalIndicators> _indicators = new();
private Timer? _timer;
public BotStatus Status { get; private set; } = new();
public IReadOnlyList<Trade> Trades => _trades.AsReadOnly();
public IReadOnlyDictionary<string, AssetConfiguration> AssetConfigurations => _assetConfigs;
public IReadOnlyDictionary<string, AssetStatistics> AssetStatistics => _assetStats;
public event Action? OnStatusChanged;
public event Action<TradingSignal>? OnSignalGenerated;
public event Action<Trade>? OnTradeExecuted;
public event Action<string, TechnicalIndicators>? OnIndicatorsUpdated;
public event Action<string, MarketPrice>? 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<string> { "BTC", "ETH", "SOL", "ADA", "MATIC" };
var assetNames = _marketDataService is SimulatedMarketDataService simService2
? simService2.GetAssetNames()
: new Dictionary<string, string>
{
{ "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<MarketPrice>();
}
_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<MarketPrice>? 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();
}
}