- Sidebar portfolio con metriche dettagliate (Totale, Investito, Disponibile, P&L, ROI) e aggiornamento real-time - Sistema multi-strategia: 8 strategie assegnabili per asset, voting decisionale, pagina Trading Control - Nuova pagina Posizioni: gestione, chiusura manuale, P&L non realizzato, notifiche - Sistema indicatori tecnici: 7+ indicatori configurabili, segnali real-time, raccomandazioni, storico segnali - Refactoring TradingBotService per capitale, P&L, ROI, eventi - Nuovi modelli e servizi per strategie/indicatori, persistenza configurazioni - UI/UX: navigazione aggiornata, widget, modali, responsive - Aggiornamento README e CHANGELOG con tutte le novità
797 lines
28 KiB
C#
797 lines
28 KiB
C#
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<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 readonly Dictionary<string, Trade> _activePositions = new();
|
|
private Timer? _timer;
|
|
private Timer? _persistenceTimer;
|
|
|
|
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 IReadOnlyDictionary<string, Trade> ActivePositions => _activePositions;
|
|
|
|
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,
|
|
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<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,
|
|
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<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]);
|
|
|
|
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<decimal> 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<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();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Manually close a position
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|