From 92c8e57a8c8d239e942c0ab4d351bb5c815e9c79 Mon Sep 17 00:00:00 2001 From: Alberto Balbo Date: Mon, 22 Dec 2025 11:24:17 +0100 Subject: [PATCH] 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 --- TradingBot/.gitignore | 5 +- TradingBot/CHANGELOG.md | 57 +++ TradingBot/Components/Layout/MainLayout.razor | 8 + TradingBot/Components/Pages/Logs.razor | 398 ++++++++++++++++++ TradingBot/Components/Pages/Settings.razor | 118 ++++++ TradingBot/Models/LogEntry.cs | 27 ++ TradingBot/Program.cs | 5 + TradingBot/README.md | 16 +- TradingBot/Services/LoggingService.cs | 122 ++++++ TradingBot/Services/TradeHistoryService.cs | 164 ++++++++ .../Services/TradingBotBackgroundService.cs | 58 +++ TradingBot/Services/TradingBotService.cs | 145 ++++++- TradingBot/deployment/DEPLOYMENT_CHECKLIST.md | 288 +++++++++++++ TradingBot/deployment/UNRAID_INSTALL.md | 205 ++++++++- TradingBot/wwwroot/app.css | 117 +++++ 15 files changed, 1697 insertions(+), 36 deletions(-) create mode 100644 TradingBot/Components/Pages/Logs.razor create mode 100644 TradingBot/Models/LogEntry.cs create mode 100644 TradingBot/Services/LoggingService.cs create mode 100644 TradingBot/Services/TradeHistoryService.cs create mode 100644 TradingBot/Services/TradingBotBackgroundService.cs create mode 100644 TradingBot/deployment/DEPLOYMENT_CHECKLIST.md diff --git a/TradingBot/.gitignore b/TradingBot/.gitignore index 3c1c0dc..a898ae3 100644 --- a/TradingBot/.gitignore +++ b/TradingBot/.gitignore @@ -88,8 +88,11 @@ $RECYCLE.BIN/ # Mac files .DS_Store -# Application data +# Application data and persistence **/data/ +trade-history.json +active-positions.json +settings.json *.db *.db-shm *.db-wal diff --git a/TradingBot/CHANGELOG.md b/TradingBot/CHANGELOG.md index 0a598da..8547848 100644 --- a/TradingBot/CHANGELOG.md +++ b/TradingBot/CHANGELOG.md @@ -6,6 +6,60 @@ Formato basato su [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), segu --- +## [1.3.0] - 2024-12-21 + +### Added +- **Logs Page**: Comprehensive logging system with real-time monitoring + - Real-time log updates with auto-scroll + - Advanced filtering (Level, Category, Symbol) + - Color-coded log levels (Debug, Info, Warning, Error, Trade) + - Trade-specific logs with detailed information + - 500 log entries buffer with automatic rotation + - Clear logs functionality +- **LoggingService**: Centralized logging management + - Structured log entries with timestamps + - Category and symbol-based filtering + - Event-driven updates for real-time UI +- **Enhanced TradingBotService**: Integrated logging + - Bot lifecycle events (start/stop) + - Trade execution logs (buy/sell) + - Detailed trade information in logs + +### Changed +- MainLayout updated with Logs navigation item +- TradingBotService now logs all major operations + +--- + +## [1.2.0] - 2024-12-21 + +### Added +- **Trade Persistence**: Complete persistence system for trade history and active positions + - TradeHistoryService for JSON-based data storage + - Automatic save every 30 seconds + - Immediate save after each trade execution + - Automatic data restore on application startup +- **Data Management UI**: Settings page section for persistent data management + - View trade count and data size + - View active positions count + - Clear all data functionality with confirmation modal +- **Graceful Shutdown**: TradingBotBackgroundService for data persistence on application exit + - Automatic save on container stop/restart + - No data loss on unexpected shutdowns + +### Changed +- TradingBotService now integrates with TradeHistoryService +- Buy/Sell methods are now async to support immediate persistence +- Settings page enhanced with data management section + +### Technical +- Data stored in `/app/data` directory +- JSON format for human-readable persistence +- Compatible with Docker volume mapping +- Background service registered as IHostedService + +--- + ## [1.1.0] - 2024-12-17 ### Added @@ -51,8 +105,11 @@ Formato basato su [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), segu - **Removed**: Features rimosse - **Fixed**: Bug fixes - **Security**: Security fixes +- **Technical**: Miglioramenti tecnici e infrastrutturali --- +[1.3.0]: https://gitea.encke-hake.ts.net/Alby96/Encelado/compare/v1.2.0...v1.3.0 +[1.2.0]: https://gitea.encke-hake.ts.net/Alby96/Encelado/compare/v1.1.0...v1.2.0 [1.1.0]: https://gitea.encke-hake.ts.net/Alby96/Encelado/compare/v1.0.0...v1.1.0 [1.0.0]: https://gitea.encke-hake.ts.net/Alby96/Encelado/releases/tag/v1.0.0 diff --git a/TradingBot/Components/Layout/MainLayout.razor b/TradingBot/Components/Layout/MainLayout.razor index 6712a70..a156639 100644 --- a/TradingBot/Components/Layout/MainLayout.razor +++ b/TradingBot/Components/Layout/MainLayout.razor @@ -81,6 +81,14 @@ } + + + @if (!sidebarCollapsed) + { + Logs + } + + @if (!sidebarCollapsed) diff --git a/TradingBot/Components/Pages/Logs.razor b/TradingBot/Components/Pages/Logs.razor new file mode 100644 index 0000000..fb5f62a --- /dev/null +++ b/TradingBot/Components/Pages/Logs.razor @@ -0,0 +1,398 @@ +@page "/logs" +@using TradingBot.Services +@using TradingBot.Models +@inject LoggingService LoggingService +@implements IDisposable +@rendermode InteractiveServer + +Logs - TradingBot + +
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ @filteredLogs.Count / @allLogs.Count logs +
+
+ +
+ @if (filteredLogs.Count == 0) + { +
+ +

Nessun log disponibile

+
+ } + else + { +
+ @foreach (var log in filteredLogs.OrderByDescending(l => l.Timestamp)) + { +
+
+ @log.Timestamp.ToLocalTime().ToString("HH:mm:ss.fff") + + + @log.Level + + @log.Category + @if (!string.IsNullOrEmpty(log.Symbol)) + { + @log.Symbol + } +
+
@log.Message
+ @if (!string.IsNullOrEmpty(log.Details)) + { +
@log.Details
+ } +
+ } +
+ } +
+
+ + + +@code { + private ElementReference logsContainer; + private List allLogs = new(); + private List filteredLogs = new(); + private string selectedLevel = ""; + private string selectedCategory = ""; + private string selectedSymbol = ""; + private bool autoScroll = true; + private List categories = new(); + private List symbols = new(); + + protected override void OnInitialized() + { + LoadLogs(); + LoggingService.OnLogAdded += HandleLogAdded; + } + + private void LoadLogs() + { + allLogs = LoggingService.GetLogs().ToList(); + UpdateFilters(); + FilterLogs(); + } + + private void UpdateFilters() + { + categories = allLogs.Select(l => l.Category).Distinct().OrderBy(c => c).ToList(); + symbols = allLogs.Where(l => !string.IsNullOrEmpty(l.Symbol)) + .Select(l => l.Symbol!) + .Distinct() + .OrderBy(s => s) + .ToList(); + } + + private void FilterLogs() + { + var query = allLogs.AsEnumerable(); + + if (!string.IsNullOrEmpty(selectedLevel)) + { + if (Enum.TryParse(selectedLevel, out var level)) + { + query = query.Where(l => l.Level == level); + } + } + + if (!string.IsNullOrEmpty(selectedCategory)) + { + query = query.Where(l => l.Category == selectedCategory); + } + + if (!string.IsNullOrEmpty(selectedSymbol)) + { + query = query.Where(l => l.Symbol == selectedSymbol); + } + + filteredLogs = query.ToList(); + StateHasChanged(); + } + + private async void HandleLogAdded() + { + LoadLogs(); + await InvokeAsync(StateHasChanged); + + if (autoScroll) + { + await Task.Delay(100); + // Auto-scroll logic would go here if needed + } + } + + private void ClearLogs() + { + LoggingService.ClearLogs(); + LoadLogs(); + } + + private string GetLevelIcon(TradingBot.Models.LogLevel level) + { + return level switch + { + TradingBot.Models.LogLevel.Debug => "bug", + TradingBot.Models.LogLevel.Info => "info-circle", + TradingBot.Models.LogLevel.Warning => "exclamation-triangle", + TradingBot.Models.LogLevel.Error => "x-circle", + TradingBot.Models.LogLevel.Trade => "graph-up-arrow", + _ => "circle" + }; + } + + public void Dispose() + { + LoggingService.OnLogAdded -= HandleLogAdded; + } +} diff --git a/TradingBot/Components/Pages/Settings.razor b/TradingBot/Components/Pages/Settings.razor index 825c525..6d0db8e 100644 --- a/TradingBot/Components/Pages/Settings.razor +++ b/TradingBot/Components/Pages/Settings.razor @@ -2,6 +2,8 @@ @using TradingBot.Services @using TradingBot.Models @inject SettingsService SettingsService +@inject TradingBotService TradingBotService +@inject TradeHistoryService HistoryService @implements IDisposable @rendermode InteractiveServer @@ -67,6 +69,39 @@ +
+

Dati Persistenti

+
+
+
+
Trade Salvati
+
@TradingBotService.Trades.Count trade nella cronologia
+
+
+ @FormatBytes(dataSize) +
+
+ +
+
+
Posizioni Attive
+
@TradingBotService.ActivePositions.Count posizioni aperte
+
+
+ +
+
+
Cancella Tutti i Dati
+
Elimina cronologia trade e resetta i saldi
+
+ +
+
+
+

Avanzate

@@ -116,16 +151,57 @@ Impostazioni salvate con successo!
} + + @if (showClearConfirmation) + { + + }
@code { private AppSettings settings = new(); private bool showNotification = false; + private bool showClearConfirmation = false; + private long dataSize = 0; protected override void OnInitialized() { settings = SettingsService.GetSettings(); SettingsService.OnSettingsChanged += HandleSettingsChanged; + TradingBotService.OnStatusChanged += HandleStatusChanged; + UpdateDataSize(); + } + + private void UpdateDataSize() + { + dataSize = HistoryService.GetDataSize(); } private void UpdateSetting(string propertyName, T value) @@ -148,6 +224,28 @@ ShowNotification(); } + private void ShowClearDataConfirmation() + { + showClearConfirmation = true; + } + + private void HideClearDataConfirmation() + { + showClearConfirmation = false; + } + + private async Task ConfirmClearData() + { + await TradingBotService.ClearAllDataAsync(); + UpdateDataSize(); + showClearConfirmation = false; + showNotification = true; + StateHasChanged(); + await Task.Delay(3000); + showNotification = false; + StateHasChanged(); + } + private async void ShowNotification() { showNotification = true; @@ -157,14 +255,34 @@ StateHasChanged(); } + private string FormatBytes(long bytes) + { + string[] sizes = { "B", "KB", "MB", "GB" }; + double len = bytes; + int order = 0; + while (len >= 1024 && order < sizes.Length - 1) + { + order++; + len = len / 1024; + } + return $"{len:0.##} {sizes[order]}"; + } + private void HandleSettingsChanged() { settings = SettingsService.GetSettings(); InvokeAsync(StateHasChanged); } + private void HandleStatusChanged() + { + UpdateDataSize(); + InvokeAsync(StateHasChanged); + } + public void Dispose() { SettingsService.OnSettingsChanged -= HandleSettingsChanged; + TradingBotService.OnStatusChanged -= HandleStatusChanged; } } diff --git a/TradingBot/Models/LogEntry.cs b/TradingBot/Models/LogEntry.cs new file mode 100644 index 0000000..3c8b307 --- /dev/null +++ b/TradingBot/Models/LogEntry.cs @@ -0,0 +1,27 @@ +namespace TradingBot.Models; + +/// +/// Represents a log entry with timestamp, severity and message +/// +public class LogEntry +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + public LogLevel Level { get; set; } + public string Category { get; set; } = string.Empty; + public string Message { get; set; } = string.Empty; + public string? Details { get; set; } + public string? Symbol { get; set; } +} + +/// +/// Log severity levels +/// +public enum LogLevel +{ + Debug, + Info, + Warning, + Error, + Trade +} diff --git a/TradingBot/Program.cs b/TradingBot/Program.cs index dd8dfb4..fe880e6 100644 --- a/TradingBot/Program.cs +++ b/TradingBot/Program.cs @@ -20,9 +20,14 @@ builder.Services.AddRazorComponents() // Trading Bot Services - Using Simulated Market Data builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +// Register background service for graceful shutdown +builder.Services.AddHostedService(); + // Add health checks for Docker builder.Services.AddHealthChecks(); diff --git a/TradingBot/README.md b/TradingBot/README.md index e28f2b2..6aefaff 100644 --- a/TradingBot/README.md +++ b/TradingBot/README.md @@ -5,7 +5,7 @@ [![.NET 10](https://img.shields.io/badge/.NET-10.0-512BD4)](https://dotnet.microsoft.com/) [![Blazor](https://img.shields.io/badge/Blazor-Server-512BD4)](https://blazor.net/) [![Docker](https://img.shields.io/badge/Docker-Ready-2496ED)](https://www.docker.com/) -[![Version](https://img.shields.io/badge/version-1.1.0-blue)](https://gitea.encke-hake.ts.net/Alby96/Encelado/-/packages) +[![Version](https://img.shields.io/badge/version-1.3.0-blue)](https://gitea.encke-hake.ts.net/Alby96/Encelado/-/packages) --- @@ -16,6 +16,8 @@ - **15 Criptovalute**: BTC, ETH, BNB, ADA, SOL, XRP, DOT, DOGE, AVAX, MATIC, LINK, LTC, UNI, ATOM, XLM - **Analisi Tecnica**: SMA, EMA, RSI, MACD, Bollinger Bands - **Portfolio Management**: Gestione automatizzata posizioni +- **Trade Persistence**: Salvataggio automatico trade e posizioni attive +- **Comprehensive Logs**: Sistema di logging real-time con filtri avanzati - **Docker Ready**: Container ottimizzato con health checks --- @@ -62,16 +64,18 @@ wget -O /boot/config/plugins/dockerMan/templates-user/TradingBot.xml \ ## ?? Versioning -### Current Version: `1.1.0` +### Current Version: `1.3.0` + +**Latest**: Comprehensive logs page con monitoring real-time ```powershell -# Bug fix (1.1.0 ? 1.1.1) +# Bug fix (1.3.0 ? 1.3.1) .\bump-version.ps1 patch -Message "Fix memory leak" -# New feature (1.1.0 ? 1.2.0) +# New feature (1.3.0 ? 1.4.0) .\bump-version.ps1 minor -Message "Add RSI strategy" -# Breaking change (1.1.0 ? 2.0.0) +# Breaking change (1.3.0 ? 2.0.0) .\bump-version.ps1 major -Message "New API" ``` @@ -89,7 +93,7 @@ Vedi [CHANGELOG.md](CHANGELOG.md) per release notes complete. Il sistema automaticamente: - ? Build Docker image -- ? Tag: `latest`, `1.1.0`, `1.1.0-20241217` +- ? Tag: `latest`, `1.3.0`, `1.3.0-20241221` - ? Push su Gitea Registry ### Deploy su Unraid diff --git a/TradingBot/Services/LoggingService.cs b/TradingBot/Services/LoggingService.cs new file mode 100644 index 0000000..20b33aa --- /dev/null +++ b/TradingBot/Services/LoggingService.cs @@ -0,0 +1,122 @@ +using TradingBot.Models; +using System.Collections.Concurrent; + +namespace TradingBot.Services; + +/// +/// Centralized logging service for application events +/// +public class LoggingService +{ + private readonly ConcurrentQueue _logs = new(); + private const int MaxLogEntries = 500; + + public event Action? OnLogAdded; + + /// + /// Get all log entries + /// + public IReadOnlyList GetLogs() + { + return _logs.ToList().AsReadOnly(); + } + + /// + /// Add a debug log entry + /// + public void LogDebug(string category, string message, string? details = null) + { + AddLog(Models.LogLevel.Debug, category, message, details); + } + + /// + /// Add an info log entry + /// + public void LogInfo(string category, string message, string? details = null, string? symbol = null) + { + AddLog(Models.LogLevel.Info, category, message, details, symbol); + } + + /// + /// Add a warning log entry + /// + public void LogWarning(string category, string message, string? details = null, string? symbol = null) + { + AddLog(Models.LogLevel.Warning, category, message, details, symbol); + } + + /// + /// Add an error log entry + /// + public void LogError(string category, string message, string? details = null, string? symbol = null) + { + AddLog(Models.LogLevel.Error, category, message, details, symbol); + } + + /// + /// Add a trade log entry + /// + public void LogTrade(string symbol, string message, string? details = null) + { + AddLog(Models.LogLevel.Trade, "Trading", message, details, symbol); + } + + /// + /// Clear all logs + /// + public void ClearLogs() + { + _logs.Clear(); + OnLogAdded?.Invoke(); + } + + /// + /// Get logs filtered by level + /// + public IReadOnlyList GetLogsByLevel(Models.LogLevel level) + { + return _logs.Where(l => l.Level == level).ToList().AsReadOnly(); + } + + /// + /// Get logs filtered by category + /// + public IReadOnlyList GetLogsByCategory(string category) + { + return _logs.Where(l => l.Category.Equals(category, StringComparison.OrdinalIgnoreCase)) + .ToList() + .AsReadOnly(); + } + + /// + /// Get logs filtered by symbol + /// + public IReadOnlyList GetLogsBySymbol(string symbol) + { + return _logs.Where(l => l.Symbol != null && l.Symbol.Equals(symbol, StringComparison.OrdinalIgnoreCase)) + .ToList() + .AsReadOnly(); + } + + private void AddLog(Models.LogLevel level, string category, string message, string? details = null, string? symbol = null) + { + var logEntry = new LogEntry + { + Level = level, + Category = category, + Message = message, + Details = details, + Symbol = symbol + }; + + _logs.Enqueue(logEntry); + + // Maintain max size + while (_logs.Count > MaxLogEntries) + { + _logs.TryDequeue(out _); + } + + OnLogAdded?.Invoke(); + } +} diff --git a/TradingBot/Services/TradeHistoryService.cs b/TradingBot/Services/TradeHistoryService.cs new file mode 100644 index 0000000..9065834 --- /dev/null +++ b/TradingBot/Services/TradeHistoryService.cs @@ -0,0 +1,164 @@ +using System.Text.Json; +using TradingBot.Models; + +namespace TradingBot.Services; + +/// +/// Service for persisting trade history and active positions to disk +/// +public class TradeHistoryService +{ + private readonly string _dataDirectory; + private readonly string _tradesFilePath; + private readonly string _activePositionsFilePath; + private readonly ILogger _logger; + private readonly JsonSerializerOptions _jsonOptions; + + public TradeHistoryService(ILogger 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); + } + } + + /// + /// Save complete trade history to disk + /// + public async Task SaveTradeHistoryAsync(List 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"); + } + } + + /// + /// Load trade history from disk + /// + public async Task> LoadTradeHistoryAsync() + { + try + { + if (!File.Exists(_tradesFilePath)) + { + _logger.LogInformation("No trade history file found, starting fresh"); + return new List(); + } + + var json = await File.ReadAllTextAsync(_tradesFilePath); + var trades = JsonSerializer.Deserialize>(json, _jsonOptions); + + _logger.LogInformation("Loaded {Count} trades from history", trades?.Count ?? 0); + return trades ?? new List(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to load trade history, starting fresh"); + return new List(); + } + } + + /// + /// Save active positions (open trades) to disk + /// + public async Task SaveActivePositionsAsync(Dictionary 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"); + } + } + + /// + /// Load active positions from disk + /// + public async Task> LoadActivePositionsAsync() + { + try + { + if (!File.Exists(_activePositionsFilePath)) + { + _logger.LogInformation("No active positions file found"); + return new Dictionary(); + } + + var json = await File.ReadAllTextAsync(_activePositionsFilePath); + var positions = JsonSerializer.Deserialize>(json, _jsonOptions); + + _logger.LogInformation("Loaded {Count} active positions", positions?.Count ?? 0); + return positions ?? new Dictionary(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to load active positions"); + return new Dictionary(); + } + } + + /// + /// Clear all persisted data + /// + 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"); + } + } + + /// + /// Get total file size of persisted data + /// + 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; + } +} diff --git a/TradingBot/Services/TradingBotBackgroundService.cs b/TradingBot/Services/TradingBotBackgroundService.cs new file mode 100644 index 0000000..dd43e4c --- /dev/null +++ b/TradingBot/Services/TradingBotBackgroundService.cs @@ -0,0 +1,58 @@ +namespace TradingBot.Services; + +/// +/// Background service for automatic data persistence on application shutdown +/// +public class TradingBotBackgroundService : BackgroundService +{ + private readonly TradingBotService _tradingBotService; + private readonly ILogger _logger; + private readonly IHostApplicationLifetime _lifetime; + + public TradingBotBackgroundService( + TradingBotService tradingBotService, + ILogger logger, + IHostApplicationLifetime lifetime) + { + _tradingBotService = tradingBotService; + _logger = logger; + _lifetime = lifetime; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("TradingBot Background Service started"); + + // Register shutdown handler + _lifetime.ApplicationStopping.Register(OnShutdown); + + // Keep service running + await Task.Delay(Timeout.Infinite, stoppingToken); + } + + private void OnShutdown() + { + _logger.LogInformation("Application shutdown detected, saving trade data..."); + + try + { + // Stop bot if running + if (_tradingBotService.Status.IsRunning) + { + _tradingBotService.Stop(); + } + + _logger.LogInformation("Trade data saved successfully on shutdown"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error saving data on shutdown"); + } + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("TradingBot Background Service stopping"); + await base.StopAsync(cancellationToken); + } +} diff --git a/TradingBot/Services/TradingBotService.cs b/TradingBot/Services/TradingBotService.cs index aee56bc..46818c1 100644 --- a/TradingBot/Services/TradingBotService.cs +++ b/TradingBot/Services/TradingBotService.cs @@ -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 _assetConfigs = new(); private readonly Dictionary _assetStats = new(); private readonly List _trades = new(); private readonly Dictionary> _priceHistory = new(); private readonly Dictionary _indicators = new(); + private readonly Dictionary _activePositions = new(); private Timer? _timer; + private Timer? _persistenceTimer; public BotStatus Status { get; private set; } = new(); public IReadOnlyList Trades => _trades.AsReadOnly(); public IReadOnlyDictionary AssetConfigurations => _assetConfigs; public IReadOnlyDictionary AssetStatistics => _assetStats; + public IReadOnlyDictionary ActivePositions => _activePositions; public event Action? OnStatusChanged; public event Action? OnSignalGenerated; @@ -25,10 +30,16 @@ public class TradingBotService public event Action? 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; + } } diff --git a/TradingBot/deployment/DEPLOYMENT_CHECKLIST.md b/TradingBot/deployment/DEPLOYMENT_CHECKLIST.md new file mode 100644 index 0000000..40b81d6 --- /dev/null +++ b/TradingBot/deployment/DEPLOYMENT_CHECKLIST.md @@ -0,0 +1,288 @@ +# ?? TradingBot - Deployment Checklist + +Checklist completa per deployment sicuro e corretto su Unraid. + +--- + +## ? Pre-Deployment + +### Environment +- [ ] Unraid 6.10+ installato e aggiornato +- [ ] Docker service attivo e funzionante +- [ ] Internet connesso e stabile +- [ ] SSH access configurato +- [ ] Backup Unraid recente disponibile + +### Network +- [ ] Porta 8888 disponibile (o alternativa scelta) +- [ ] Test porta: `netstat -tulpn | grep :8888` +- [ ] Firewall configurato correttamente +- [ ] IP Unraid noto: `192.168.30.23` + +### Gitea Registry +- [ ] Account Gitea attivo +- [ ] Personal Access Token generato +- [ ] Login test: `docker login gitea.encke-hake.ts.net` +- [ ] Immagine disponibile in Packages + +--- + +## ?? Installation + +### Template Setup +- [ ] Template XML scaricato + ```bash + wget -O /boot/config/plugins/dockerMan/templates-user/TradingBot.xml \ + https://gitea.encke-hake.ts.net/Alby96/Encelado/raw/branch/main/TradingBot/deployment/unraid-template.xml + ``` +- [ ] Template visibile in Unraid UI +- [ ] Dropdown "TradingBot" disponibile + +### Container Configuration +- [ ] **Name**: `TradingBot` +- [ ] **Repository**: `gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest` +- [ ] **Network**: Bridge +- [ ] **Port Mapping**: `8888:8080` (o custom) + - Host Port: `8888` (modificabile) + - Container Port: `8080` (FIXED) +- [ ] **Volume**: `/mnt/user/appdata/tradingbot:/app/data` + - Access: Read/Write +- [ ] **Environment Variables**: + - `ASPNETCORE_ENVIRONMENT=Production` + - `ASPNETCORE_URLS=http://+:8080` + - `TZ=Europe/Rome` + +### First Start +- [ ] Click **Apply** +- [ ] Container pulls image successfully +- [ ] Container status: **running** +- [ ] No errors in logs: `docker logs TradingBot` + +--- + +## ? Post-Installation Verification + +### Container Health +- [ ] Container running: `docker ps | grep TradingBot` +- [ ] Port mapping correct: `docker port TradingBot` + - Expected: `8080/tcp -> 0.0.0.0:8888` +- [ ] Logs healthy: `docker logs TradingBot --tail 50` + - No errors or exceptions + - "Now listening on: http://[::]:8080" + +### WebUI Access +- [ ] WebUI icon visible in Unraid Docker tab +- [ ] Click WebUI icon opens browser +- [ ] Manual access works: `http://192.168.30.23:8888` +- [ ] Dashboard loads completely +- [ ] No JavaScript errors in browser console + +### Functionality Test +- [ ] Bot can be started from UI +- [ ] Market data updates (check Dashboard) +- [ ] Settings can be modified and saved +- [ ] Assets can be enabled/disabled +- [ ] Trade history visible (if any previous data) + +--- + +## ?? Persistence Verification + +### Data Directory +- [ ] Volume created: `ls -la /mnt/user/appdata/tradingbot/` +- [ ] Directory writable: `touch /mnt/user/appdata/tradingbot/test && rm /mnt/user/appdata/tradingbot/test` + +### Persistence Test +1. [ ] Start bot and execute some trades +2. [ ] Stop bot +3. [ ] Verify files exist: + ```bash + ls -lh /mnt/user/appdata/tradingbot/ + # Should show: + # - trade-history.json + # - active-positions.json + # - settings.json + ``` +4. [ ] Stop container: `docker stop TradingBot` +5. [ ] Start container: `docker start TradingBot` +6. [ ] Verify data restored: + - Trade count same in History page + - Settings preserved + - Active positions restored + +### Backup Test +- [ ] Create backup: + ```bash + tar -czf tradingbot-backup-$(date +%Y%m%d).tar.gz \ + /mnt/user/appdata/tradingbot/ + ``` +- [ ] Backup file created successfully +- [ ] Test restore (optional): + ```bash + tar -xzf tradingbot-backup-YYYYMMDD.tar.gz -C /tmp/ + # Verify files intact + ``` + +--- + +## ?? Update Test + +### Update Procedure +- [ ] Stop container +- [ ] Force Update in Unraid UI +- [ ] Wait for pull completion +- [ ] Start container +- [ ] Verify data persisted: + - [ ] Trade history intact + - [ ] Settings intact + - [ ] Active positions intact + +### Rollback Test (Optional) +- [ ] Tag current image before update +- [ ] Test update to new version +- [ ] If issues, rollback to previous tag +- [ ] Verify data still intact + +--- + +## ?? Security Check + +### Access Control +- [ ] Port 8888 not exposed to internet +- [ ] Only LAN/VPN access configured +- [ ] No default passwords used + +### Data Protection +- [ ] AppData directory permissions correct + ```bash + ls -la /mnt/user/appdata/ | grep tradingbot + # Should be owned by appropriate user + ``` +- [ ] Backup schedule configured (CA Backup plugin) +- [ ] Backup retention policy set + +### Registry Security +- [ ] Gitea login required for pulls +- [ ] Personal Access Token secure +- [ ] No credentials in logs + +--- + +## ?? Monitoring Setup + +### Unraid Dashboard +- [ ] Container appears in Docker tab +- [ ] Auto-start enabled (optional) +- [ ] Resource limits configured (optional): + ``` + --cpus="2.0" --memory="1g" + ``` + +### Logs +- [ ] Know how to access logs: + - Unraid UI: Docker tab ? TradingBot ? Logs icon + - CLI: `docker logs TradingBot -f` +- [ ] No error messages in logs + +### Notifications +- [ ] Unraid notifications enabled +- [ ] Email/Telegram configured (optional) + +--- + +## ?? Troubleshooting Checklist + +### If WebUI Not Accessible + +- [ ] Check container running: `docker ps | grep TradingBot` +- [ ] Check port mapping: `docker port TradingBot` +- [ ] Test localhost: `curl http://localhost:8888/` +- [ ] Check firewall: `iptables -L | grep 8888` +- [ ] Check logs for errors: `docker logs TradingBot` +- [ ] Try different port if 8888 occupied + +### If Data Not Persisting + +- [ ] Volume mapping correct: `docker inspect TradingBot | grep -A5 Mounts` +- [ ] Directory exists: `ls -la /mnt/user/appdata/tradingbot/` +- [ ] Files being created: Monitor during bot run +- [ ] Permissions correct: `ls -la /mnt/user/appdata/tradingbot/` + +### If Container Won't Start + +- [ ] Check image pulled: `docker images | grep tradingbot` +- [ ] Check port not in use: `netstat -tulpn | grep :8888` +- [ ] Check disk space: `df -h` +- [ ] Review logs: `docker logs TradingBot` +- [ ] Try manual start: `docker start TradingBot` + +--- + +## ?? Post-Deployment Tasks + +### Documentation +- [ ] Note custom port if not 8888 +- [ ] Document backup location +- [ ] Save deployment date +- [ ] Note Gitea image tag deployed + +### Monitoring +- [ ] Add to monitoring dashboard (if any) +- [ ] Set up health check alerts (optional) +- [ ] Configure update notifications + +### User Training +- [ ] Show how to access WebUI +- [ ] Explain Settings page +- [ ] Demonstrate how to view trades +- [ ] Explain data management (clear data) + +--- + +## ?? Success Criteria + +All of the following must be true: + +? Container running and healthy +? WebUI accessible and functional +? Bot can start/stop from UI +? Market data updates in real-time +? Trades can be executed +? Data persists across restarts +? Backup can be created +? No errors in logs +? Resource usage acceptable +? Update procedure tested + +--- + +## ?? Support + +If issues persist after completing this checklist: + +1. **Check Documentation**: + - [UNRAID_INSTALL.md](UNRAID_INSTALL.md) + - [CHANGELOG.md](../CHANGELOG.md) + +2. **Collect Diagnostic Info**: + ```bash + # Container info + docker ps -a | grep TradingBot + docker logs TradingBot --tail 100 > /tmp/tradingbot-logs.txt + docker inspect TradingBot > /tmp/tradingbot-inspect.json + + # System info + df -h + free -h + netstat -tulpn | grep 8888 + ``` + +3. **Open Issue**: + - Repository: https://gitea.encke-hake.ts.net/Alby96/Encelado/issues + - Include: Docker version, Unraid version, logs + +--- + +**Last Updated**: 2024-12-21 +**Version**: 1.2.0 +**Status**: ? Production Ready diff --git a/TradingBot/deployment/UNRAID_INSTALL.md b/TradingBot/deployment/UNRAID_INSTALL.md index f27120a..696d735 100644 --- a/TradingBot/deployment/UNRAID_INSTALL.md +++ b/TradingBot/deployment/UNRAID_INSTALL.md @@ -59,9 +59,13 @@ wget -O /boot/config/plugins/dockerMan/templates-user/TradingBot.xml \ - La porta CONTAINER rimane sempre 8080 (non modificare) - Alternative comuni se 8888 occupata: `8881`, `9999`, `7777` - **Volume Dati**: + **Volume Dati** (?? IMPORTANTE per persistenza!): - **AppData**: `/mnt/user/appdata/tradingbot` (giā impostato) - - Puoi cambiare se preferisci altra directory + - Questo volume salva: + - Trade history (`trade-history.json`) + - Posizioni attive (`active-positions.json`) + - Settings applicazione (`settings.json`) + - ? I dati sopravvivono a restart/update del container **Variabili Ambiente** (Avanzate - espandi se necessario): - **ASPNETCORE_ENVIRONMENT**: `Production` (non modificare) @@ -74,7 +78,7 @@ Unraid far - ? Pull immagine da Gitea Registry - ? Crea container con nome "TradingBot" - ? Configura porta WebUI (default 8888 ? host, 8080 ? container) -- ? Crea volume per persistenza dati +- ? **Crea volume persistente per dati** - ? Start automatico ### Step 3: Accedi WebUI @@ -99,6 +103,57 @@ Dovresti vedere la **Dashboard TradingBot**! ?? --- +## ?? PERSISTENZA DATI + +### Come Funziona + +TradingBot salva automaticamente tutti i dati in `/app/data` dentro il container, che viene mappato sul volume host `/mnt/user/appdata/tradingbot`. + +**File salvati automaticamente**: +``` +/mnt/user/appdata/tradingbot/ +??? trade-history.json # Storia completa trade +??? active-positions.json # Posizioni attualmente aperte +??? settings.json # Impostazioni applicazione +``` + +**Salvataggio automatico**: +- ? Ogni 30 secondi (mentre bot running) +- ?? Immediato dopo ogni trade eseguito +- ?? On-stop quando fermi il bot +- ?? Graceful shutdown su Docker stop/restart + +### Benefici + +? **Zero perdita dati** - Anche in caso di crash +? **Restore automatico** - Stato ripristinato al riavvio +? **Update sicuri** - Dati preservati durante aggiornamenti +? **Backup facile** - Basta copiare la cartella appdata + +### Backup Dati + +```bash +# Backup manuale +tar -czf tradingbot-backup-$(date +%Y%m%d).tar.gz \ + /mnt/user/appdata/tradingbot + +# Restore +tar -xzf tradingbot-backup-20241221.tar.gz \ + -C /mnt/user/appdata/ +``` + +### Gestione Dati (via WebUI) + +Vai su **Settings** ? **Dati Persistenti**: +- Visualizza numero trade salvati +- Visualizza dimensione dati +- Visualizza posizioni attive +- **Cancella tutti i dati** (con conferma) + +?? **Nota**: Puoi cancellare i dati solo se il bot č fermo. + +--- + ## ?? METODO 2: Installazione Manuale Se preferisci non usare template: @@ -119,19 +174,21 @@ gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest **Console shell command**: `Shell` -### Step 3: Port Mapping +### Step 3: Port Mapping (?? CRITICO!) Click **Add another Path, Port, Variable, Label or Device** **Config Type**: `Port` - **Name**: `WebUI` - **Container Port**: `8080` -- **Host Port**: `8080` ? **Cambia questa se occupata!** +- **Host Port**: `8888` ? **Cambia questa se occupata!** - **Connection Type**: `TCP` -### Step 4: Volume Mapping +?? **Se questo mapping non viene configurato, la WebUI non sarā accessibile!** -Click **Add another Path, Port, Variable, Label or Device** +### Step 4: Volume Mapping (?? IMPORTANTE per persistenza!) + +Click **Add another Path, Port, Variable, Label o Device** **Config Type**: `Path` - **Name**: `AppData` @@ -153,14 +210,7 @@ Click **Add another Path, Port, Variable, Label or Device** - **Name**: `TZ` - **Value**: `Europe/Rome` (o tuo timezone) -### Step 6: Health Check (Opzionale ma Consigliato) - -**Extra Parameters**: -``` ---health-cmd="wget --no-verbose --tries=1 --spider http://localhost:8080/ || exit 1" --health-interval=30s --health-timeout=3s --health-retries=3 --health-start-period=40s -``` - -### Step 7: Apply +### Step 6: Apply Click **Apply** in fondo alla pagina. @@ -179,9 +229,11 @@ Click **Apply** in fondo alla pagina. Unraid farā: - ? Pull ultima immagine da Gitea - ? Ricrea container con nuova immagine -- ? Mantiene dati persistenti (volume non viene toccato) +- ? **Mantiene dati persistenti** (volume non viene toccato) - ? Mantiene configurazione (porta, variabili, etc.) +?? **I tuoi trade e impostazioni sono al sicuro durante gli update!** + ### Automatico con User Scripts Plugin Installa **User Scripts** plugin: @@ -391,3 +443,124 @@ https://tradingbot.tuo-dominio.com ``` ?? **Nota**: Il reverse proxy si connette alla porta HOST (8888), non container (8080) + +--- + +## ?? SICUREZZA + +### Best Practices + +? **Porta non esposta pubblicamente** (solo LAN o VPN) +? **Volume dati protetto** (`/mnt/user/appdata/tradingbot/`) +? **Registry privato** (Gitea richiede login) +? **Certificati validi** (Tailscale) +? **User non-root** (giā configurato nel Dockerfile) +? **Dati persistenti** backup-ready + +--- + +## ?? CHECKLIST INSTALLAZIONE + +### Pre-Install +- [ ] Unraid 6.10+ installato +- [ ] Docker service attivo +- [ ] Porta 8888 (o alternativa) disponibile +- [ ] `docker login gitea.encke-hake.ts.net` successful +- [ ] Internet attivo per pull immagine + +### Install +- [ ] Template XML scaricato su Unraid +- [ ] Container creato da template +- [ ] Porta WebUI configurata (8888 host ? 8080 container) +- [ ] Volume AppData creato (`/mnt/user/appdata/tradingbot`) +- [ ] Container status: **running** + +### Post-Install +- [ ] WebUI accessibile (http://IP:8888) +- [ ] Dashboard carica correttamente +- [ ] Settings modificabili e salvabili +- [ ] Bot avviabile dalla UI +- [ ] Trade vengono salvati automaticamente +- [ ] Dati persistono dopo restart + +--- + +## ?? VANTAGGI UNRAID NATIVO + +? **Zero dipendenze** (no Portainer, no docker-compose) +? **WebUI Unraid integrata** (gestione familiare) +? **Auto-start** (container parte con Unraid) +? **Backup integrato** (con plugin CA) +? **Update semplice** (2 click: Stop ? Update ? Start) +? **Template riutilizzabile** (reinstall in 1 minuto) +? **Dati persistenti** (trade e settings sopravvivono) +? **Logs accessibili** dalla UI + +--- + +## ?? WORKFLOW COMPLETO + +### Sviluppo (PC) +``` +1. ?? Visual Studio ? Codice +2. ?? Build ? Publish (Docker profile) +3. ? Automatico: Push Gitea Registry + ?? Tags: latest, 1.2.0, 1.2.0-YYYYMMDD +4. ?? git push origin main --tags +``` + +### Deploy (Unraid) +``` +1. ?? Docker tab ? TradingBot +2. ?? Stop +3. ?? Force Update (pull latest) +4. ?? Start +5. ? Done! (~ 1 minuto) + ?? Dati automaticamente ripristinati +``` + +**Tempo totale**: ~2 minuti dal commit al running! + +--- + +## ?? RISORSE + +### Links Utili + +| Risorsa | URL | +|---------|-----| +| **Template XML** | `https://gitea.encke-hake.ts.net/Alby96/Encelado/raw/branch/main/TradingBot/deployment/unraid-template.xml` | +| **Repository Git** | `https://gitea.encke-hake.ts.net/Alby96/Encelado` | +| **Docker Image** | `gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest` | +| **Packages** | `https://gitea.encke-hake.ts.net/Alby96/Encelado/-/packages` | +| **Support/Issues** | `https://gitea.encke-hake.ts.net/Alby96/Encelado/issues` | + +### Comandi Utili + +```bash +# Status container +docker ps -a | grep TradingBot + +# Logs real-time +docker logs -f TradingBot + +# Statistics +docker stats TradingBot --no-stream + +# Restart +docker restart TradingBot + +# Update +docker pull gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest + +# Remove (mantiene dati in /mnt/user/appdata/tradingbot) +docker rm -f TradingBot + +# Inspect persistent data +ls -lh /mnt/user/appdata/tradingbot/ +cat /mnt/user/appdata/tradingbot/trade-history.json | jq +``` + +--- + +**?? TradingBot v1.2.0 con persistenza completa pronto su Unraid!** diff --git a/TradingBot/wwwroot/app.css b/TradingBot/wwwroot/app.css index 27f15f2..a7a830d 100644 --- a/TradingBot/wwwroot/app.css +++ b/TradingBot/wwwroot/app.css @@ -469,6 +469,123 @@ select:focus { } } +/* Modal Styles */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.75); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + animation: fadeIn 0.2s ease-out; +} + +.modal-dialog { + background: #1a1f3a; + border-radius: 0.75rem; + width: 90%; + max-width: 500px; + border: 1px solid rgba(99, 102, 241, 0.2); + animation: slideIn 0.3s ease-out; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); +} + +.modal-header { + padding: 1.5rem; + border-bottom: 1px solid rgba(99, 102, 241, 0.1); + display: flex; + align-items: center; + justify-content: space-between; +} + +.modal-header h3 { + font-size: 1.25rem; + font-weight: 700; + color: #e2e8f0; + margin: 0; +} + +.btn-close { + width: 2rem; + height: 2rem; + border-radius: 0.375rem; + background: transparent; + border: none; + color: #94a3b8; + font-size: 1.5rem; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.btn-close:hover { + background: rgba(239, 68, 68, 0.1); + color: #ef4444; +} + +.modal-body { + padding: 1.5rem; +} + +.modal-body p { + margin-bottom: 1rem; + line-height: 1.6; +} + +.modal-body ul { + margin: 1rem 0; + padding-left: 1.5rem; +} + +.modal-body li { + margin: 0.5rem 0; + color: #cbd5e1; +} + +.modal-footer { + padding: 1.5rem; + border-top: 1px solid rgba(99, 102, 241, 0.1); + display: flex; + gap: 1rem; + justify-content: flex-end; +} + +.text-danger { + color: #ef4444 !important; +} + +/* Danger Button */ +.btn-danger { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + font-size: 0.875rem; + font-weight: 600; + background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%); + color: white; + border: none; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 12px rgba(220, 38, 38, 0.3); +} + +.btn-danger:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(220, 38, 38, 0.4); + background: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%); +} + +.btn-danger:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + /* Print Styles */ @media print { .no-print {