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

View File

@@ -0,0 +1,164 @@
using System.Text.Json;
using TradingBot.Models;
namespace TradingBot.Services;
/// <summary>
/// Service for persisting trade history and active positions to disk
/// </summary>
public class TradeHistoryService
{
private readonly string _dataDirectory;
private readonly string _tradesFilePath;
private readonly string _activePositionsFilePath;
private readonly ILogger<TradeHistoryService> _logger;
private readonly JsonSerializerOptions _jsonOptions;
public TradeHistoryService(ILogger<TradeHistoryService> logger)
{
_logger = logger;
_dataDirectory = Path.Combine(Directory.GetCurrentDirectory(), "data");
_tradesFilePath = Path.Combine(_dataDirectory, "trade-history.json");
_activePositionsFilePath = Path.Combine(_dataDirectory, "active-positions.json");
_jsonOptions = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
EnsureDataDirectoryExists();
}
private void EnsureDataDirectoryExists()
{
if (!Directory.Exists(_dataDirectory))
{
Directory.CreateDirectory(_dataDirectory);
_logger.LogInformation("Created data directory: {Directory}", _dataDirectory);
}
}
/// <summary>
/// Save complete trade history to disk
/// </summary>
public async Task SaveTradeHistoryAsync(List<Trade> trades)
{
try
{
var json = JsonSerializer.Serialize(trades, _jsonOptions);
await File.WriteAllTextAsync(_tradesFilePath, json);
_logger.LogInformation("Saved {Count} trades to history", trades.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to save trade history");
}
}
/// <summary>
/// Load trade history from disk
/// </summary>
public async Task<List<Trade>> LoadTradeHistoryAsync()
{
try
{
if (!File.Exists(_tradesFilePath))
{
_logger.LogInformation("No trade history file found, starting fresh");
return new List<Trade>();
}
var json = await File.ReadAllTextAsync(_tradesFilePath);
var trades = JsonSerializer.Deserialize<List<Trade>>(json, _jsonOptions);
_logger.LogInformation("Loaded {Count} trades from history", trades?.Count ?? 0);
return trades ?? new List<Trade>();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load trade history, starting fresh");
return new List<Trade>();
}
}
/// <summary>
/// Save active positions (open trades) to disk
/// </summary>
public async Task SaveActivePositionsAsync(Dictionary<string, Trade> activePositions)
{
try
{
var json = JsonSerializer.Serialize(activePositions, _jsonOptions);
await File.WriteAllTextAsync(_activePositionsFilePath, json);
_logger.LogInformation("Saved {Count} active positions", activePositions.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to save active positions");
}
}
/// <summary>
/// Load active positions from disk
/// </summary>
public async Task<Dictionary<string, Trade>> LoadActivePositionsAsync()
{
try
{
if (!File.Exists(_activePositionsFilePath))
{
_logger.LogInformation("No active positions file found");
return new Dictionary<string, Trade>();
}
var json = await File.ReadAllTextAsync(_activePositionsFilePath);
var positions = JsonSerializer.Deserialize<Dictionary<string, Trade>>(json, _jsonOptions);
_logger.LogInformation("Loaded {Count} active positions", positions?.Count ?? 0);
return positions ?? new Dictionary<string, Trade>();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load active positions");
return new Dictionary<string, Trade>();
}
}
/// <summary>
/// Clear all persisted data
/// </summary>
public void ClearAll()
{
try
{
if (File.Exists(_tradesFilePath))
File.Delete(_tradesFilePath);
if (File.Exists(_activePositionsFilePath))
File.Delete(_activePositionsFilePath);
_logger.LogInformation("Cleared all persisted trade data");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to clear persisted data");
}
}
/// <summary>
/// Get total file size of persisted data
/// </summary>
public long GetDataSize()
{
long size = 0;
if (File.Exists(_tradesFilePath))
size += new FileInfo(_tradesFilePath).Length;
if (File.Exists(_activePositionsFilePath))
size += new FileInfo(_activePositionsFilePath).Length;
return size;
}
}