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:
164
TradingBot/Services/TradeHistoryService.cs
Normal file
164
TradingBot/Services/TradeHistoryService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user