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

@@ -88,8 +88,11 @@ $RECYCLE.BIN/
# Mac files # Mac files
.DS_Store .DS_Store
# Application data # Application data and persistence
**/data/ **/data/
trade-history.json
active-positions.json
settings.json
*.db *.db
*.db-shm *.db-shm
*.db-wal *.db-wal

View File

@@ -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 ## [1.1.0] - 2024-12-17
### Added ### Added
@@ -51,8 +105,11 @@ Formato basato su [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), segu
- **Removed**: Features rimosse - **Removed**: Features rimosse
- **Fixed**: Bug fixes - **Fixed**: Bug fixes
- **Security**: Security 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.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 [1.0.0]: https://gitea.encke-hake.ts.net/Alby96/Encelado/releases/tag/v1.0.0

View File

@@ -81,6 +81,14 @@
} }
</NavLink> </NavLink>
<NavLink class="menu-item" href="/logs" title="Logs">
<span class="item-icon bi bi-terminal"></span>
@if (!sidebarCollapsed)
{
<span class="item-text">Logs</span>
}
</NavLink>
<NavLink class="menu-item" href="/settings" title="Impostazioni"> <NavLink class="menu-item" href="/settings" title="Impostazioni">
<span class="item-icon bi bi-gear"></span> <span class="item-icon bi bi-gear"></span>
@if (!sidebarCollapsed) @if (!sidebarCollapsed)

View File

@@ -0,0 +1,398 @@
@page "/logs"
@using TradingBot.Services
@using TradingBot.Models
@inject LoggingService LoggingService
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Logs - TradingBot</PageTitle>
<div class="logs-page">
<div class="page-header">
<div>
<h1>Logs Operazioni</h1>
<p class="subtitle">Cronologia eventi e operazioni del bot</p>
</div>
<div class="header-actions">
<button class="btn-secondary" @onclick="ClearLogs">
<span class="bi bi-trash"></span>
Cancella Logs
</button>
</div>
</div>
<div class="logs-filters">
<div class="filter-group">
<label>Livello:</label>
<select @bind="selectedLevel" @bind:after="FilterLogs">
<option value="">Tutti</option>
<option value="Debug">Debug</option>
<option value="Info">Info</option>
<option value="Warning">Warning</option>
<option value="Error">Error</option>
<option value="Trade">Trade</option>
</select>
</div>
<div class="filter-group">
<label>Categoria:</label>
<select @bind="selectedCategory" @bind:after="FilterLogs">
<option value="">Tutte</option>
@foreach (var category in categories)
{
<option value="@category">@category</option>
}
</select>
</div>
<div class="filter-group">
<label>Symbol:</label>
<select @bind="selectedSymbol" @bind:after="FilterLogs">
<option value="">Tutti</option>
@foreach (var symbol in symbols)
{
<option value="@symbol">@symbol</option>
}
</select>
</div>
<div class="filter-group">
<label>
<input type="checkbox" @bind="autoScroll" />
Auto-scroll
</label>
</div>
<div class="logs-count">
@filteredLogs.Count / @allLogs.Count logs
</div>
</div>
<div class="logs-container" @ref="logsContainer">
@if (filteredLogs.Count == 0)
{
<div class="empty-state">
<span class="bi bi-inbox"></span>
<p>Nessun log disponibile</p>
</div>
}
else
{
<div class="logs-list">
@foreach (var log in filteredLogs.OrderByDescending(l => l.Timestamp))
{
<div class="log-entry log-@log.Level.ToString().ToLower()">
<div class="log-header">
<span class="log-timestamp">@log.Timestamp.ToLocalTime().ToString("HH:mm:ss.fff")</span>
<span class="log-level">
<span class="bi bi-@GetLevelIcon(log.Level)"></span>
@log.Level
</span>
<span class="log-category">@log.Category</span>
@if (!string.IsNullOrEmpty(log.Symbol))
{
<span class="log-symbol">@log.Symbol</span>
}
</div>
<div class="log-message">@log.Message</div>
@if (!string.IsNullOrEmpty(log.Details))
{
<div class="log-details">@log.Details</div>
}
</div>
}
</div>
}
</div>
</div>
<style>
.logs-page {
height: 100%;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.logs-filters {
display: flex;
gap: 1rem;
align-items: center;
padding: 1rem;
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
flex-wrap: wrap;
}
.filter-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.filter-group label {
font-size: 0.875rem;
color: #94a3b8;
font-weight: 600;
}
.filter-group select {
padding: 0.5rem 1rem;
border-radius: 0.375rem;
background: #0f1629;
border: 1px solid #334155;
color: #e2e8f0;
font-size: 0.875rem;
}
.logs-count {
margin-left: auto;
padding: 0.5rem 1rem;
background: rgba(99, 102, 241, 0.1);
border-radius: 0.375rem;
color: #6366f1;
font-weight: 600;
font-size: 0.875rem;
}
.logs-container {
flex: 1;
overflow-y: auto;
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
padding: 1rem;
}
.logs-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.log-entry {
padding: 1rem;
border-radius: 0.5rem;
border-left: 3px solid;
background: #0f1629;
}
.log-entry.log-debug {
border-left-color: #64748b;
}
.log-entry.log-info {
border-left-color: #3b82f6;
}
.log-entry.log-warning {
border-left-color: #f59e0b;
}
.log-entry.log-error {
border-left-color: #ef4444;
}
.log-entry.log-trade {
border-left-color: #10b981;
background: rgba(16, 185, 129, 0.05);
}
.log-header {
display: flex;
gap: 1rem;
align-items: center;
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
.log-timestamp {
color: #64748b;
font-family: 'Courier New', monospace;
}
.log-level {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.75rem;
border-radius: 0.25rem;
font-weight: 700;
text-transform: uppercase;
font-size: 0.75rem;
}
.log-entry.log-debug .log-level {
background: rgba(100, 116, 139, 0.2);
color: #94a3b8;
}
.log-entry.log-info .log-level {
background: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.log-entry.log-warning .log-level {
background: rgba(245, 158, 11, 0.2);
color: #f59e0b;
}
.log-entry.log-error .log-level {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.log-entry.log-trade .log-level {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
}
.log-category {
padding: 0.25rem 0.75rem;
background: rgba(99, 102, 241, 0.1);
border-radius: 0.25rem;
color: #6366f1;
font-weight: 600;
}
.log-symbol {
padding: 0.25rem 0.75rem;
background: rgba(139, 92, 246, 0.1);
border-radius: 0.25rem;
color: #8b5cf6;
font-weight: 700;
}
.log-message {
color: #e2e8f0;
line-height: 1.6;
}
.log-details {
margin-top: 0.5rem;
padding: 0.75rem;
background: rgba(0, 0, 0, 0.3);
border-radius: 0.375rem;
color: #94a3b8;
font-size: 0.875rem;
font-family: 'Courier New', monospace;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem 2rem;
color: #64748b;
}
.empty-state .bi {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-state p {
font-size: 1.125rem;
margin: 0;
}
</style>
@code {
private ElementReference logsContainer;
private List<LogEntry> allLogs = new();
private List<LogEntry> filteredLogs = new();
private string selectedLevel = "";
private string selectedCategory = "";
private string selectedSymbol = "";
private bool autoScroll = true;
private List<string> categories = new();
private List<string> 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<TradingBot.Models.LogLevel>(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;
}
}

View File

@@ -2,6 +2,8 @@
@using TradingBot.Services @using TradingBot.Services
@using TradingBot.Models @using TradingBot.Models
@inject SettingsService SettingsService @inject SettingsService SettingsService
@inject TradingBotService TradingBotService
@inject TradeHistoryService HistoryService
@implements IDisposable @implements IDisposable
@rendermode InteractiveServer @rendermode InteractiveServer
@@ -67,6 +69,39 @@
</div> </div>
</div> </div>
<div class="settings-section">
<h2>Dati Persistenti</h2>
<div class="settings-group">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Trade Salvati</div>
<div class="setting-description">@TradingBotService.Trades.Count trade nella cronologia</div>
</div>
<div class="setting-value">
@FormatBytes(dataSize)
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Posizioni Attive</div>
<div class="setting-description">@TradingBotService.ActivePositions.Count posizioni aperte</div>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Cancella Tutti i Dati</div>
<div class="setting-description text-danger">Elimina cronologia trade e resetta i saldi</div>
</div>
<button class="btn-danger" @onclick="ShowClearDataConfirmation" disabled="@TradingBotService.Status.IsRunning">
<span class="bi bi-trash"></span>
Cancella Dati
</button>
</div>
</div>
</div>
<div class="settings-section"> <div class="settings-section">
<h2>Avanzate</h2> <h2>Avanzate</h2>
<div class="settings-group"> <div class="settings-group">
@@ -116,16 +151,57 @@
Impostazioni salvate con successo! Impostazioni salvate con successo!
</div> </div>
} }
@if (showClearConfirmation)
{
<div class="modal-overlay" @onclick="HideClearDataConfirmation">
<div class="modal-dialog" @onclick:stopPropagation="true">
<div class="modal-header">
<h3>Conferma Cancellazione</h3>
<button class="btn-close" @onclick="HideClearDataConfirmation">×</button>
</div>
<div class="modal-body">
<p class="text-danger">
<strong>Attenzione!</strong> Questa azione eliminerà:
</p>
<ul>
<li>Tutta la cronologia dei trade (@TradingBotService.Trades.Count trade)</li>
<li>Tutte le posizioni attive (@TradingBotService.ActivePositions.Count posizioni)</li>
<li>I saldi verranno resettati ai valori iniziali</li>
</ul>
<p class="text-danger">
<strong>Questa operazione è irreversibile!</strong>
</p>
</div>
<div class="modal-footer">
<button class="btn-secondary" @onclick="HideClearDataConfirmation">Annulla</button>
<button class="btn-danger" @onclick="ConfirmClearData">
<span class="bi bi-trash"></span>
Conferma Cancellazione
</button>
</div>
</div>
</div>
}
</div> </div>
@code { @code {
private AppSettings settings = new(); private AppSettings settings = new();
private bool showNotification = false; private bool showNotification = false;
private bool showClearConfirmation = false;
private long dataSize = 0;
protected override void OnInitialized() protected override void OnInitialized()
{ {
settings = SettingsService.GetSettings(); settings = SettingsService.GetSettings();
SettingsService.OnSettingsChanged += HandleSettingsChanged; SettingsService.OnSettingsChanged += HandleSettingsChanged;
TradingBotService.OnStatusChanged += HandleStatusChanged;
UpdateDataSize();
}
private void UpdateDataSize()
{
dataSize = HistoryService.GetDataSize();
} }
private void UpdateSetting<T>(string propertyName, T value) private void UpdateSetting<T>(string propertyName, T value)
@@ -148,6 +224,28 @@
ShowNotification(); 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() private async void ShowNotification()
{ {
showNotification = true; showNotification = true;
@@ -157,14 +255,34 @@
StateHasChanged(); 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() private void HandleSettingsChanged()
{ {
settings = SettingsService.GetSettings(); settings = SettingsService.GetSettings();
InvokeAsync(StateHasChanged); InvokeAsync(StateHasChanged);
} }
private void HandleStatusChanged()
{
UpdateDataSize();
InvokeAsync(StateHasChanged);
}
public void Dispose() public void Dispose()
{ {
SettingsService.OnSettingsChanged -= HandleSettingsChanged; SettingsService.OnSettingsChanged -= HandleSettingsChanged;
TradingBotService.OnStatusChanged -= HandleStatusChanged;
} }
} }

View File

@@ -0,0 +1,27 @@
namespace TradingBot.Models;
/// <summary>
/// Represents a log entry with timestamp, severity and message
/// </summary>
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; }
}
/// <summary>
/// Log severity levels
/// </summary>
public enum LogLevel
{
Debug,
Info,
Warning,
Error,
Trade
}

View File

@@ -20,9 +20,14 @@ builder.Services.AddRazorComponents()
// Trading Bot Services - Using Simulated Market Data // Trading Bot Services - Using Simulated Market Data
builder.Services.AddSingleton<IMarketDataService, SimulatedMarketDataService>(); builder.Services.AddSingleton<IMarketDataService, SimulatedMarketDataService>();
builder.Services.AddSingleton<ITradingStrategy, SimpleMovingAverageStrategy>(); builder.Services.AddSingleton<ITradingStrategy, SimpleMovingAverageStrategy>();
builder.Services.AddSingleton<TradeHistoryService>();
builder.Services.AddSingleton<LoggingService>();
builder.Services.AddSingleton<TradingBotService>(); builder.Services.AddSingleton<TradingBotService>();
builder.Services.AddSingleton<SettingsService>(); builder.Services.AddSingleton<SettingsService>();
// Register background service for graceful shutdown
builder.Services.AddHostedService<TradingBotBackgroundService>();
// Add health checks for Docker // Add health checks for Docker
builder.Services.AddHealthChecks(); builder.Services.AddHealthChecks();

View File

@@ -5,7 +5,7 @@
[![.NET 10](https://img.shields.io/badge/.NET-10.0-512BD4)](https://dotnet.microsoft.com/) [![.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/) [![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/) [![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 - **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 - **Analisi Tecnica**: SMA, EMA, RSI, MACD, Bollinger Bands
- **Portfolio Management**: Gestione automatizzata posizioni - **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 - **Docker Ready**: Container ottimizzato con health checks
--- ---
@@ -62,16 +64,18 @@ wget -O /boot/config/plugins/dockerMan/templates-user/TradingBot.xml \
## ?? Versioning ## ?? Versioning
### Current Version: `1.1.0` ### Current Version: `1.3.0`
**Latest**: Comprehensive logs page con monitoring real-time
```powershell ```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" .\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" .\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" .\bump-version.ps1 major -Message "New API"
``` ```
@@ -89,7 +93,7 @@ Vedi [CHANGELOG.md](CHANGELOG.md) per release notes complete.
Il sistema automaticamente: Il sistema automaticamente:
- ? Build Docker image - ? 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 - ? Push su Gitea Registry
### Deploy su Unraid ### Deploy su Unraid

View File

@@ -0,0 +1,122 @@
using TradingBot.Models;
using System.Collections.Concurrent;
namespace TradingBot.Services;
/// <summary>
/// Centralized logging service for application events
/// </summary>
public class LoggingService
{
private readonly ConcurrentQueue<LogEntry> _logs = new();
private const int MaxLogEntries = 500;
public event Action? OnLogAdded;
/// <summary>
/// Get all log entries
/// </summary>
public IReadOnlyList<LogEntry> GetLogs()
{
return _logs.ToList().AsReadOnly();
}
/// <summary>
/// Add a debug log entry
/// </summary>
public void LogDebug(string category, string message, string? details = null)
{
AddLog(Models.LogLevel.Debug, category, message, details);
}
/// <summary>
/// Add an info log entry
/// </summary>
public void LogInfo(string category, string message, string? details = null, string? symbol = null)
{
AddLog(Models.LogLevel.Info, category, message, details, symbol);
}
/// <summary>
/// Add a warning log entry
/// </summary>
public void LogWarning(string category, string message, string? details = null, string? symbol = null)
{
AddLog(Models.LogLevel.Warning, category, message, details, symbol);
}
/// <summary>
/// Add an error log entry
/// </summary>
public void LogError(string category, string message, string? details = null, string? symbol = null)
{
AddLog(Models.LogLevel.Error, category, message, details, symbol);
}
/// <summary>
/// Add a trade log entry
/// </summary>
public void LogTrade(string symbol, string message, string? details = null)
{
AddLog(Models.LogLevel.Trade, "Trading", message, details, symbol);
}
/// <summary>
/// Clear all logs
/// </summary>
public void ClearLogs()
{
_logs.Clear();
OnLogAdded?.Invoke();
}
/// <summary>
/// Get logs filtered by level
/// </summary>
public IReadOnlyList<LogEntry> GetLogsByLevel(Models.LogLevel level)
{
return _logs.Where(l => l.Level == level).ToList().AsReadOnly();
}
/// <summary>
/// Get logs filtered by category
/// </summary>
public IReadOnlyList<LogEntry> GetLogsByCategory(string category)
{
return _logs.Where(l => l.Category.Equals(category, StringComparison.OrdinalIgnoreCase))
.ToList()
.AsReadOnly();
}
/// <summary>
/// Get logs filtered by symbol
/// </summary>
public IReadOnlyList<LogEntry> 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();
}
}

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;
}
}

View File

@@ -0,0 +1,58 @@
namespace TradingBot.Services;
/// <summary>
/// Background service for automatic data persistence on application shutdown
/// </summary>
public class TradingBotBackgroundService : BackgroundService
{
private readonly TradingBotService _tradingBotService;
private readonly ILogger<TradingBotBackgroundService> _logger;
private readonly IHostApplicationLifetime _lifetime;
public TradingBotBackgroundService(
TradingBotService tradingBotService,
ILogger<TradingBotBackgroundService> 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);
}
}

View File

@@ -6,17 +6,22 @@ public class TradingBotService
{ {
private readonly IMarketDataService _marketDataService; private readonly IMarketDataService _marketDataService;
private readonly ITradingStrategy _strategy; private readonly ITradingStrategy _strategy;
private readonly TradeHistoryService _historyService;
private readonly LoggingService _loggingService;
private readonly Dictionary<string, AssetConfiguration> _assetConfigs = new(); private readonly Dictionary<string, AssetConfiguration> _assetConfigs = new();
private readonly Dictionary<string, AssetStatistics> _assetStats = new(); private readonly Dictionary<string, AssetStatistics> _assetStats = new();
private readonly List<Trade> _trades = new(); private readonly List<Trade> _trades = new();
private readonly Dictionary<string, List<MarketPrice>> _priceHistory = new(); private readonly Dictionary<string, List<MarketPrice>> _priceHistory = new();
private readonly Dictionary<string, TechnicalIndicators> _indicators = new(); private readonly Dictionary<string, TechnicalIndicators> _indicators = new();
private readonly Dictionary<string, Trade> _activePositions = new();
private Timer? _timer; private Timer? _timer;
private Timer? _persistenceTimer;
public BotStatus Status { get; private set; } = new(); public BotStatus Status { get; private set; } = new();
public IReadOnlyList<Trade> Trades => _trades.AsReadOnly(); public IReadOnlyList<Trade> Trades => _trades.AsReadOnly();
public IReadOnlyDictionary<string, AssetConfiguration> AssetConfigurations => _assetConfigs; public IReadOnlyDictionary<string, AssetConfiguration> AssetConfigurations => _assetConfigs;
public IReadOnlyDictionary<string, AssetStatistics> AssetStatistics => _assetStats; public IReadOnlyDictionary<string, AssetStatistics> AssetStatistics => _assetStats;
public IReadOnlyDictionary<string, Trade> ActivePositions => _activePositions;
public event Action? OnStatusChanged; public event Action? OnStatusChanged;
public event Action<TradingSignal>? OnSignalGenerated; public event Action<TradingSignal>? OnSignalGenerated;
@@ -25,10 +30,16 @@ public class TradingBotService
public event Action<string, MarketPrice>? OnPriceUpdated; public event Action<string, MarketPrice>? OnPriceUpdated;
public event Action? OnStatisticsUpdated; public event Action? OnStatisticsUpdated;
public TradingBotService(IMarketDataService marketDataService, ITradingStrategy strategy) public TradingBotService(
IMarketDataService marketDataService,
ITradingStrategy strategy,
TradeHistoryService historyService,
LoggingService loggingService)
{ {
_marketDataService = marketDataService; _marketDataService = marketDataService;
_strategy = strategy; _strategy = strategy;
_historyService = historyService;
_loggingService = loggingService;
Status.CurrentStrategy = strategy.Name; Status.CurrentStrategy = strategy.Name;
// Subscribe to simulated market updates if available // Subscribe to simulated market updates if available
@@ -38,6 +49,52 @@ public class TradingBotService
} }
InitializeDefaultAssets(); 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() private void InitializeDefaultAssets()
@@ -64,7 +121,7 @@ public class TradingBotService
{ {
Symbol = symbol, Symbol = symbol,
Name = assetNames.TryGetValue(symbol, out var name) ? name : symbol, Name = assetNames.TryGetValue(symbol, out var name) ? name : symbol,
IsEnabled = true, // Enable ALL assets by default for full simulation IsEnabled = true,
InitialBalance = 1000m, InitialBalance = 1000m,
CurrentBalance = 1000m CurrentBalance = 1000m
}; };
@@ -122,6 +179,8 @@ public class TradingBotService
Status.IsRunning = true; Status.IsRunning = true;
Status.StartedAt = DateTime.UtcNow; Status.StartedAt = DateTime.UtcNow;
_loggingService.LogInfo("Bot", "Trading Bot started", $"Strategy: {_strategy.Name}");
// Reset daily trade counts // Reset daily trade counts
foreach (var config in _assetConfigs.Values) foreach (var config in _assetConfigs.Values)
{ {
@@ -135,10 +194,17 @@ public class TradingBotService
// Start update timer (every 3 seconds for simulation) // Start update timer (every 3 seconds for simulation)
_timer = new Timer(async _ => await UpdateAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(3)); _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(); OnStatusChanged?.Invoke();
} }
public void Stop() public async void Stop()
{ {
if (!Status.IsRunning) return; if (!Status.IsRunning) return;
@@ -146,9 +212,30 @@ public class TradingBotService
_timer?.Dispose(); _timer?.Dispose();
_timer = null; _timer = null;
_persistenceTimer?.Dispose();
_persistenceTimer = null;
_loggingService.LogInfo("Bot", "Trading Bot stopped", $"Total trades: {_trades.Count}");
// Save data on stop
await SaveDataAsync();
OnStatusChanged?.Invoke(); 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() private void HandleSimulatedPriceUpdate()
{ {
if (Status.IsRunning) if (Status.IsRunning)
@@ -192,7 +279,6 @@ public class TradingBotService
private async Task ProcessAssetUpdate(MarketPrice price) private async Task ProcessAssetUpdate(MarketPrice price)
{ {
// Add null check for price
if (price == null || price.Price <= 0) if (price == null || price.Price <= 0)
return; return;
@@ -228,7 +314,6 @@ public class TradingBotService
// Generate trading signal // Generate trading signal
var signal = await _strategy.AnalyzeAsync(price.Symbol, _priceHistory[price.Symbol]); var signal = await _strategy.AnalyzeAsync(price.Symbol, _priceHistory[price.Symbol]);
// Add null check for signal
if (signal != null) if (signal != null)
{ {
OnSignalGenerated?.Invoke(signal); OnSignalGenerated?.Invoke(signal);
@@ -266,7 +351,7 @@ public class TradingBotService
if (tradeAmount >= config.MinTradeAmount) if (tradeAmount >= config.MinTradeAmount)
{ {
ExecuteBuy(symbol, price.Price, tradeAmount, config); await ExecuteBuyAsync(symbol, price.Price, tradeAmount, config);
} }
} }
// Sell logic // Sell logic
@@ -283,14 +368,12 @@ public class TradingBotService
if (profitPercentage >= config.TakeProfitPercentage || if (profitPercentage >= config.TakeProfitPercentage ||
profitPercentage <= -config.StopLossPercentage) 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; var amount = amountUSD / price;
@@ -316,14 +399,24 @@ public class TradingBotService
}; };
_trades.Add(trade); _trades.Add(trade);
_activePositions[symbol] = trade;
UpdateAssetStatistics(symbol, trade); UpdateAssetStatistics(symbol, trade);
Status.TradesExecuted++; Status.TradesExecuted++;
_loggingService.LogTrade(
symbol,
$"BUY {amount:F6} {symbol} @ ${price:N2}",
$"Value: ${amountUSD:N2} | Balance: ${config.CurrentBalance:N2}");
OnTradeExecuted?.Invoke(trade); OnTradeExecuted?.Invoke(trade);
OnStatusChanged?.Invoke(); 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 amountUSD = amount * price;
var profit = (price - config.AverageEntryPrice) * amount; var profit = (price - config.AverageEntryPrice) * amount;
@@ -346,11 +439,21 @@ public class TradingBotService
}; };
_trades.Add(trade); _trades.Add(trade);
_activePositions.Remove(symbol);
UpdateAssetStatistics(symbol, trade, profit); UpdateAssetStatistics(symbol, trade, profit);
Status.TradesExecuted++; Status.TradesExecuted++;
_loggingService.LogTrade(
symbol,
$"SELL {amount:F6} {symbol} @ ${price:N2}",
$"Value: ${amountUSD:N2} | Profit: ${profit:N2} | Balance: ${config.CurrentBalance:N2}");
OnTradeExecuted?.Invoke(trade); OnTradeExecuted?.Invoke(trade);
OnStatusChanged?.Invoke(); OnStatusChanged?.Invoke();
// Save immediately after trade
await SaveDataAsync();
} }
private void UpdateIndicators(string symbol) private void UpdateIndicators(string symbol)
@@ -358,13 +461,11 @@ public class TradingBotService
if (!_priceHistory.TryGetValue(symbol, out var history) || history == null || history.Count < 26) if (!_priceHistory.TryGetValue(symbol, out var history) || history == null || history.Count < 26)
return; return;
// Filter out null prices and extract valid price values
var prices = history var prices = history
.Where(p => p != null && p.Price > 0) .Where(p => p != null && p.Price > 0)
.Select(p => p.Price) .Select(p => p.Price)
.ToList(); .ToList();
// Ensure we still have enough data after filtering
if (prices.Count < 26) if (prices.Count < 26)
return; return;
@@ -512,4 +613,22 @@ public class TradingBotService
var history = GetPriceHistory(symbol); var history = GetPriceHistory(symbol);
return history?.LastOrDefault(); 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;
}
} }

View File

@@ -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

View File

@@ -59,9 +59,13 @@ wget -O /boot/config/plugins/dockerMan/templates-user/TradingBot.xml \
- La porta CONTAINER rimane sempre 8080 (non modificare) - La porta CONTAINER rimane sempre 8080 (non modificare)
- Alternative comuni se 8888 occupata: `8881`, `9999`, `7777` - Alternative comuni se 8888 occupata: `8881`, `9999`, `7777`
**Volume Dati**: **Volume Dati** (?? IMPORTANTE per persistenza!):
- **AppData**: `/mnt/user/appdata/tradingbot` (già impostato) - **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): **Variabili Ambiente** (Avanzate - espandi se necessario):
- **ASPNETCORE_ENVIRONMENT**: `Production` (non modificare) - **ASPNETCORE_ENVIRONMENT**: `Production` (non modificare)
@@ -74,7 +78,7 @@ Unraid far
- ? Pull immagine da Gitea Registry - ? Pull immagine da Gitea Registry
- ? Crea container con nome "TradingBot" - ? Crea container con nome "TradingBot"
- ? Configura porta WebUI (default 8888 ? host, 8080 ? container) - ? Configura porta WebUI (default 8888 ? host, 8080 ? container)
- ? Crea volume per persistenza dati - ? **Crea volume persistente per dati**
- ? Start automatico - ? Start automatico
### Step 3: Accedi WebUI ### 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 ## ?? METODO 2: Installazione Manuale
Se preferisci non usare template: Se preferisci non usare template:
@@ -119,19 +174,21 @@ gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest
**Console shell command**: `Shell` **Console shell command**: `Shell`
### Step 3: Port Mapping ### Step 3: Port Mapping (?? CRITICO!)
Click **Add another Path, Port, Variable, Label or Device** Click **Add another Path, Port, Variable, Label or Device**
**Config Type**: `Port` **Config Type**: `Port`
- **Name**: `WebUI` - **Name**: `WebUI`
- **Container Port**: `8080` - **Container Port**: `8080`
- **Host Port**: `8080` ? **Cambia questa se occupata!** - **Host Port**: `8888` ? **Cambia questa se occupata!**
- **Connection Type**: `TCP` - **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` **Config Type**: `Path`
- **Name**: `AppData` - **Name**: `AppData`
@@ -153,14 +210,7 @@ Click **Add another Path, Port, Variable, Label or Device**
- **Name**: `TZ` - **Name**: `TZ`
- **Value**: `Europe/Rome` (o tuo timezone) - **Value**: `Europe/Rome` (o tuo timezone)
### Step 6: Health Check (Opzionale ma Consigliato) ### Step 6: Apply
**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
Click **Apply** in fondo alla pagina. Click **Apply** in fondo alla pagina.
@@ -179,9 +229,11 @@ Click **Apply** in fondo alla pagina.
Unraid farà: Unraid farà:
- ? Pull ultima immagine da Gitea - ? Pull ultima immagine da Gitea
- ? Ricrea container con nuova immagine - ? Ricrea container con nuova immagine
- ? Mantiene dati persistenti (volume non viene toccato) - ? **Mantiene dati persistenti** (volume non viene toccato)
- ? Mantiene configurazione (porta, variabili, etc.) - ? Mantiene configurazione (porta, variabili, etc.)
?? **I tuoi trade e impostazioni sono al sicuro durante gli update!**
### Automatico con User Scripts Plugin ### Automatico con User Scripts Plugin
Installa **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) ?? **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!**

View File

@@ -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 */ /* Print Styles */
@media print { @media print {
.no-print { .no-print {