Persistenza dati e logging avanzato con UI e Unraid

- Aggiunto TradeHistoryService per persistenza trade/posizioni attive su disco (JSON, auto-save/restore)
- Logging centralizzato (LoggingService) con livelli, categorie, simbolo e buffer circolare (500 log)
- Nuova pagina Logs: monitoraggio real-time, filtri avanzati, cancellazione log, colorazione livelli
- Sezione "Dati Persistenti" in Settings: conteggio trade, dimensione dati, reset con conferma modale
- Background service per salvataggio sicuro su shutdown/stop container
- Aggiornata sidebar, stili modali/bottoni danger, .gitignore e documentazione (README, CHANGELOG, UNRAID_INSTALL, checklist)
- Versione 1.3.0
This commit is contained in:
2025-12-22 11:24:17 +01:00
parent d7ae3e5d44
commit 92c8e57a8c
15 changed files with 1697 additions and 36 deletions
+132 -13
View File
@@ -6,17 +6,22 @@ public class TradingBotService
{
private readonly IMarketDataService _marketDataService;
private readonly ITradingStrategy _strategy;
private readonly TradeHistoryService _historyService;
private readonly LoggingService _loggingService;
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;
@@ -25,10 +30,16 @@ public class TradingBotService
public event Action<string, MarketPrice>? OnPriceUpdated;
public event Action? OnStatisticsUpdated;
public TradingBotService(IMarketDataService marketDataService, ITradingStrategy strategy)
public TradingBotService(
IMarketDataService marketDataService,
ITradingStrategy strategy,
TradeHistoryService historyService,
LoggingService loggingService)
{
_marketDataService = marketDataService;
_strategy = strategy;
_historyService = historyService;
_loggingService = loggingService;
Status.CurrentStrategy = strategy.Name;
// Subscribe to simulated market updates if available
@@ -38,6 +49,52 @@ public class TradingBotService
}
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()
@@ -64,7 +121,7 @@ public class TradingBotService
{
Symbol = symbol,
Name = assetNames.TryGetValue(symbol, out var name) ? name : symbol,
IsEnabled = true, // Enable ALL assets by default for full simulation
IsEnabled = true,
InitialBalance = 1000m,
CurrentBalance = 1000m
};
@@ -122,6 +179,8 @@ public class TradingBotService
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)
{
@@ -135,10 +194,17 @@ public class TradingBotService
// 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 void Stop()
public async void Stop()
{
if (!Status.IsRunning) return;
@@ -146,9 +212,30 @@ public class TradingBotService
_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)
@@ -192,7 +279,6 @@ public class TradingBotService
private async Task ProcessAssetUpdate(MarketPrice price)
{
// Add null check for price
if (price == null || price.Price <= 0)
return;
@@ -228,7 +314,6 @@ public class TradingBotService
// Generate trading signal
var signal = await _strategy.AnalyzeAsync(price.Symbol, _priceHistory[price.Symbol]);
// Add null check for signal
if (signal != null)
{
OnSignalGenerated?.Invoke(signal);
@@ -266,7 +351,7 @@ public class TradingBotService
if (tradeAmount >= config.MinTradeAmount)
{
ExecuteBuy(symbol, price.Price, tradeAmount, config);
await ExecuteBuyAsync(symbol, price.Price, tradeAmount, config);
}
}
// Sell logic
@@ -283,14 +368,12 @@ public class TradingBotService
if (profitPercentage >= config.TakeProfitPercentage ||
profitPercentage <= -config.StopLossPercentage)
{
ExecuteSell(symbol, price.Price, config.CurrentHoldings, config);
await ExecuteSellAsync(symbol, price.Price, config.CurrentHoldings, config);
}
}
await Task.CompletedTask;
}
private void ExecuteBuy(string symbol, decimal price, decimal amountUSD, AssetConfiguration config)
private async Task ExecuteBuyAsync(string symbol, decimal price, decimal amountUSD, AssetConfiguration config)
{
var amount = amountUSD / price;
@@ -316,14 +399,24 @@ public class TradingBotService
};
_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 void ExecuteSell(string symbol, decimal price, decimal amount, AssetConfiguration config)
private async Task ExecuteSellAsync(string symbol, decimal price, decimal amount, AssetConfiguration config)
{
var amountUSD = amount * price;
var profit = (price - config.AverageEntryPrice) * amount;
@@ -346,11 +439,21 @@ public class TradingBotService
};
_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)
@@ -358,13 +461,11 @@ public class TradingBotService
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;
@@ -512,4 +613,22 @@ public class TradingBotService
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;
}
}