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:
5
TradingBot/.gitignore
vendored
5
TradingBot/.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -81,6 +81,14 @@
|
||||
}
|
||||
</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">
|
||||
<span class="item-icon bi bi-gear"></span>
|
||||
@if (!sidebarCollapsed)
|
||||
|
||||
398
TradingBot/Components/Pages/Logs.razor
Normal file
398
TradingBot/Components/Pages/Logs.razor
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
</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">
|
||||
<h2>Avanzate</h2>
|
||||
<div class="settings-group">
|
||||
@@ -116,16 +151,57 @@
|
||||
Impostazioni salvate con successo!
|
||||
</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>
|
||||
|
||||
@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<T>(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;
|
||||
}
|
||||
}
|
||||
|
||||
27
TradingBot/Models/LogEntry.cs
Normal file
27
TradingBot/Models/LogEntry.cs
Normal 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
|
||||
}
|
||||
@@ -20,9 +20,14 @@ builder.Services.AddRazorComponents()
|
||||
// Trading Bot Services - Using Simulated Market Data
|
||||
builder.Services.AddSingleton<IMarketDataService, SimulatedMarketDataService>();
|
||||
builder.Services.AddSingleton<ITradingStrategy, SimpleMovingAverageStrategy>();
|
||||
builder.Services.AddSingleton<TradeHistoryService>();
|
||||
builder.Services.AddSingleton<LoggingService>();
|
||||
builder.Services.AddSingleton<TradingBotService>();
|
||||
builder.Services.AddSingleton<SettingsService>();
|
||||
|
||||
// Register background service for graceful shutdown
|
||||
builder.Services.AddHostedService<TradingBotBackgroundService>();
|
||||
|
||||
// Add health checks for Docker
|
||||
builder.Services.AddHealthChecks();
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
[](https://dotnet.microsoft.com/)
|
||||
[](https://blazor.net/)
|
||||
[](https://www.docker.com/)
|
||||
[](https://gitea.encke-hake.ts.net/Alby96/Encelado/-/packages)
|
||||
[](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
|
||||
|
||||
122
TradingBot/Services/LoggingService.cs
Normal file
122
TradingBot/Services/LoggingService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
164
TradingBot/Services/TradeHistoryService.cs
Normal file
164
TradingBot/Services/TradeHistoryService.cs
Normal file
@@ -0,0 +1,164 @@
|
||||
using System.Text.Json;
|
||||
using TradingBot.Models;
|
||||
|
||||
namespace TradingBot.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for persisting trade history and active positions to disk
|
||||
/// </summary>
|
||||
public class TradeHistoryService
|
||||
{
|
||||
private readonly string _dataDirectory;
|
||||
private readonly string _tradesFilePath;
|
||||
private readonly string _activePositionsFilePath;
|
||||
private readonly ILogger<TradeHistoryService> _logger;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
public TradeHistoryService(ILogger<TradeHistoryService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_dataDirectory = Path.Combine(Directory.GetCurrentDirectory(), "data");
|
||||
_tradesFilePath = Path.Combine(_dataDirectory, "trade-history.json");
|
||||
_activePositionsFilePath = Path.Combine(_dataDirectory, "active-positions.json");
|
||||
|
||||
_jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
EnsureDataDirectoryExists();
|
||||
}
|
||||
|
||||
private void EnsureDataDirectoryExists()
|
||||
{
|
||||
if (!Directory.Exists(_dataDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(_dataDirectory);
|
||||
_logger.LogInformation("Created data directory: {Directory}", _dataDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save complete trade history to disk
|
||||
/// </summary>
|
||||
public async Task SaveTradeHistoryAsync(List<Trade> trades)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(trades, _jsonOptions);
|
||||
await File.WriteAllTextAsync(_tradesFilePath, json);
|
||||
_logger.LogInformation("Saved {Count} trades to history", trades.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to save trade history");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load trade history from disk
|
||||
/// </summary>
|
||||
public async Task<List<Trade>> LoadTradeHistoryAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_tradesFilePath))
|
||||
{
|
||||
_logger.LogInformation("No trade history file found, starting fresh");
|
||||
return new List<Trade>();
|
||||
}
|
||||
|
||||
var json = await File.ReadAllTextAsync(_tradesFilePath);
|
||||
var trades = JsonSerializer.Deserialize<List<Trade>>(json, _jsonOptions);
|
||||
|
||||
_logger.LogInformation("Loaded {Count} trades from history", trades?.Count ?? 0);
|
||||
return trades ?? new List<Trade>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to load trade history, starting fresh");
|
||||
return new List<Trade>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save active positions (open trades) to disk
|
||||
/// </summary>
|
||||
public async Task SaveActivePositionsAsync(Dictionary<string, Trade> activePositions)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(activePositions, _jsonOptions);
|
||||
await File.WriteAllTextAsync(_activePositionsFilePath, json);
|
||||
_logger.LogInformation("Saved {Count} active positions", activePositions.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to save active positions");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load active positions from disk
|
||||
/// </summary>
|
||||
public async Task<Dictionary<string, Trade>> LoadActivePositionsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_activePositionsFilePath))
|
||||
{
|
||||
_logger.LogInformation("No active positions file found");
|
||||
return new Dictionary<string, Trade>();
|
||||
}
|
||||
|
||||
var json = await File.ReadAllTextAsync(_activePositionsFilePath);
|
||||
var positions = JsonSerializer.Deserialize<Dictionary<string, Trade>>(json, _jsonOptions);
|
||||
|
||||
_logger.LogInformation("Loaded {Count} active positions", positions?.Count ?? 0);
|
||||
return positions ?? new Dictionary<string, Trade>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to load active positions");
|
||||
return new Dictionary<string, Trade>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear all persisted data
|
||||
/// </summary>
|
||||
public void ClearAll()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(_tradesFilePath))
|
||||
File.Delete(_tradesFilePath);
|
||||
|
||||
if (File.Exists(_activePositionsFilePath))
|
||||
File.Delete(_activePositionsFilePath);
|
||||
|
||||
_logger.LogInformation("Cleared all persisted trade data");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to clear persisted data");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get total file size of persisted data
|
||||
/// </summary>
|
||||
public long GetDataSize()
|
||||
{
|
||||
long size = 0;
|
||||
|
||||
if (File.Exists(_tradesFilePath))
|
||||
size += new FileInfo(_tradesFilePath).Length;
|
||||
|
||||
if (File.Exists(_activePositionsFilePath))
|
||||
size += new FileInfo(_activePositionsFilePath).Length;
|
||||
|
||||
return size;
|
||||
}
|
||||
}
|
||||
58
TradingBot/Services/TradingBotBackgroundService.cs
Normal file
58
TradingBot/Services/TradingBotBackgroundService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -6,17 +6,22 @@ public class TradingBotService
|
||||
{
|
||||
private readonly IMarketDataService _marketDataService;
|
||||
private readonly ITradingStrategy _strategy;
|
||||
private readonly TradeHistoryService _historyService;
|
||||
private readonly LoggingService _loggingService;
|
||||
private readonly Dictionary<string, AssetConfiguration> _assetConfigs = new();
|
||||
private readonly Dictionary<string, AssetStatistics> _assetStats = new();
|
||||
private readonly List<Trade> _trades = new();
|
||||
private readonly Dictionary<string, List<MarketPrice>> _priceHistory = new();
|
||||
private readonly Dictionary<string, TechnicalIndicators> _indicators = new();
|
||||
private readonly Dictionary<string, Trade> _activePositions = new();
|
||||
private Timer? _timer;
|
||||
private Timer? _persistenceTimer;
|
||||
|
||||
public BotStatus Status { get; private set; } = new();
|
||||
public IReadOnlyList<Trade> Trades => _trades.AsReadOnly();
|
||||
public IReadOnlyDictionary<string, AssetConfiguration> AssetConfigurations => _assetConfigs;
|
||||
public IReadOnlyDictionary<string, AssetStatistics> AssetStatistics => _assetStats;
|
||||
public IReadOnlyDictionary<string, Trade> ActivePositions => _activePositions;
|
||||
|
||||
public event Action? OnStatusChanged;
|
||||
public event Action<TradingSignal>? OnSignalGenerated;
|
||||
@@ -25,10 +30,16 @@ public class TradingBotService
|
||||
public event Action<string, MarketPrice>? OnPriceUpdated;
|
||||
public event Action? OnStatisticsUpdated;
|
||||
|
||||
public TradingBotService(IMarketDataService marketDataService, ITradingStrategy strategy)
|
||||
public TradingBotService(
|
||||
IMarketDataService marketDataService,
|
||||
ITradingStrategy strategy,
|
||||
TradeHistoryService historyService,
|
||||
LoggingService loggingService)
|
||||
{
|
||||
_marketDataService = marketDataService;
|
||||
_strategy = strategy;
|
||||
_historyService = historyService;
|
||||
_loggingService = loggingService;
|
||||
Status.CurrentStrategy = strategy.Name;
|
||||
|
||||
// Subscribe to simulated market updates if available
|
||||
@@ -38,6 +49,52 @@ public class TradingBotService
|
||||
}
|
||||
|
||||
InitializeDefaultAssets();
|
||||
|
||||
// Load persisted data
|
||||
_ = LoadPersistedDataAsync();
|
||||
|
||||
_loggingService.LogInfo("System", "TradingBot Service initialized");
|
||||
}
|
||||
|
||||
private async Task LoadPersistedDataAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Load trade history
|
||||
var trades = await _historyService.LoadTradeHistoryAsync();
|
||||
_trades.AddRange(trades);
|
||||
|
||||
// Load active positions
|
||||
var positions = await _historyService.LoadActivePositionsAsync();
|
||||
foreach (var kvp in positions)
|
||||
{
|
||||
_activePositions[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
// Restore asset configurations from active positions
|
||||
RestoreAssetConfigurationsFromTrades();
|
||||
|
||||
OnStatusChanged?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error loading persisted data: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void RestoreAssetConfigurationsFromTrades()
|
||||
{
|
||||
foreach (var position in _activePositions.Values)
|
||||
{
|
||||
if (_assetConfigs.TryGetValue(position.Symbol, out var config))
|
||||
{
|
||||
if (position.Type == TradeType.Buy)
|
||||
{
|
||||
config.CurrentHoldings += position.Amount;
|
||||
config.AverageEntryPrice = position.Price;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeDefaultAssets()
|
||||
@@ -64,7 +121,7 @@ public class TradingBotService
|
||||
{
|
||||
Symbol = symbol,
|
||||
Name = assetNames.TryGetValue(symbol, out var name) ? name : symbol,
|
||||
IsEnabled = true, // Enable ALL assets by default for full simulation
|
||||
IsEnabled = true,
|
||||
InitialBalance = 1000m,
|
||||
CurrentBalance = 1000m
|
||||
};
|
||||
@@ -122,6 +179,8 @@ public class TradingBotService
|
||||
Status.IsRunning = true;
|
||||
Status.StartedAt = DateTime.UtcNow;
|
||||
|
||||
_loggingService.LogInfo("Bot", "Trading Bot started", $"Strategy: {_strategy.Name}");
|
||||
|
||||
// Reset daily trade counts
|
||||
foreach (var config in _assetConfigs.Values)
|
||||
{
|
||||
@@ -135,10 +194,17 @@ public class TradingBotService
|
||||
// Start update timer (every 3 seconds for simulation)
|
||||
_timer = new Timer(async _ => await UpdateAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(3));
|
||||
|
||||
// Start persistence timer (save every 30 seconds)
|
||||
_persistenceTimer = new Timer(
|
||||
async _ => await SaveDataAsync(),
|
||||
null,
|
||||
TimeSpan.FromSeconds(30),
|
||||
TimeSpan.FromSeconds(30));
|
||||
|
||||
OnStatusChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
public async void Stop()
|
||||
{
|
||||
if (!Status.IsRunning) return;
|
||||
|
||||
@@ -146,9 +212,30 @@ public class TradingBotService
|
||||
_timer?.Dispose();
|
||||
_timer = null;
|
||||
|
||||
_persistenceTimer?.Dispose();
|
||||
_persistenceTimer = null;
|
||||
|
||||
_loggingService.LogInfo("Bot", "Trading Bot stopped", $"Total trades: {_trades.Count}");
|
||||
|
||||
// Save data on stop
|
||||
await SaveDataAsync();
|
||||
|
||||
OnStatusChanged?.Invoke();
|
||||
}
|
||||
|
||||
private async Task SaveDataAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _historyService.SaveTradeHistoryAsync(_trades);
|
||||
await _historyService.SaveActivePositionsAsync(_activePositions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error saving data: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleSimulatedPriceUpdate()
|
||||
{
|
||||
if (Status.IsRunning)
|
||||
@@ -192,7 +279,6 @@ public class TradingBotService
|
||||
|
||||
private async Task ProcessAssetUpdate(MarketPrice price)
|
||||
{
|
||||
// Add null check for price
|
||||
if (price == null || price.Price <= 0)
|
||||
return;
|
||||
|
||||
@@ -228,7 +314,6 @@ public class TradingBotService
|
||||
// Generate trading signal
|
||||
var signal = await _strategy.AnalyzeAsync(price.Symbol, _priceHistory[price.Symbol]);
|
||||
|
||||
// Add null check for signal
|
||||
if (signal != null)
|
||||
{
|
||||
OnSignalGenerated?.Invoke(signal);
|
||||
@@ -266,7 +351,7 @@ public class TradingBotService
|
||||
|
||||
if (tradeAmount >= config.MinTradeAmount)
|
||||
{
|
||||
ExecuteBuy(symbol, price.Price, tradeAmount, config);
|
||||
await ExecuteBuyAsync(symbol, price.Price, tradeAmount, config);
|
||||
}
|
||||
}
|
||||
// Sell logic
|
||||
@@ -283,14 +368,12 @@ public class TradingBotService
|
||||
if (profitPercentage >= config.TakeProfitPercentage ||
|
||||
profitPercentage <= -config.StopLossPercentage)
|
||||
{
|
||||
ExecuteSell(symbol, price.Price, config.CurrentHoldings, config);
|
||||
await ExecuteSellAsync(symbol, price.Price, config.CurrentHoldings, config);
|
||||
}
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void ExecuteBuy(string symbol, decimal price, decimal amountUSD, AssetConfiguration config)
|
||||
private async Task ExecuteBuyAsync(string symbol, decimal price, decimal amountUSD, AssetConfiguration config)
|
||||
{
|
||||
var amount = amountUSD / price;
|
||||
|
||||
@@ -316,14 +399,24 @@ public class TradingBotService
|
||||
};
|
||||
|
||||
_trades.Add(trade);
|
||||
_activePositions[symbol] = trade;
|
||||
UpdateAssetStatistics(symbol, trade);
|
||||
|
||||
Status.TradesExecuted++;
|
||||
|
||||
_loggingService.LogTrade(
|
||||
symbol,
|
||||
$"BUY {amount:F6} {symbol} @ ${price:N2}",
|
||||
$"Value: ${amountUSD:N2} | Balance: ${config.CurrentBalance:N2}");
|
||||
|
||||
OnTradeExecuted?.Invoke(trade);
|
||||
OnStatusChanged?.Invoke();
|
||||
|
||||
// Save immediately after trade
|
||||
await SaveDataAsync();
|
||||
}
|
||||
|
||||
private void ExecuteSell(string symbol, decimal price, decimal amount, AssetConfiguration config)
|
||||
private async Task ExecuteSellAsync(string symbol, decimal price, decimal amount, AssetConfiguration config)
|
||||
{
|
||||
var amountUSD = amount * price;
|
||||
var profit = (price - config.AverageEntryPrice) * amount;
|
||||
@@ -346,11 +439,21 @@ public class TradingBotService
|
||||
};
|
||||
|
||||
_trades.Add(trade);
|
||||
_activePositions.Remove(symbol);
|
||||
UpdateAssetStatistics(symbol, trade, profit);
|
||||
|
||||
Status.TradesExecuted++;
|
||||
|
||||
_loggingService.LogTrade(
|
||||
symbol,
|
||||
$"SELL {amount:F6} {symbol} @ ${price:N2}",
|
||||
$"Value: ${amountUSD:N2} | Profit: ${profit:N2} | Balance: ${config.CurrentBalance:N2}");
|
||||
|
||||
OnTradeExecuted?.Invoke(trade);
|
||||
OnStatusChanged?.Invoke();
|
||||
|
||||
// Save immediately after trade
|
||||
await SaveDataAsync();
|
||||
}
|
||||
|
||||
private void UpdateIndicators(string symbol)
|
||||
@@ -358,13 +461,11 @@ public class TradingBotService
|
||||
if (!_priceHistory.TryGetValue(symbol, out var history) || history == null || history.Count < 26)
|
||||
return;
|
||||
|
||||
// Filter out null prices and extract valid price values
|
||||
var prices = history
|
||||
.Where(p => p != null && p.Price > 0)
|
||||
.Select(p => p.Price)
|
||||
.ToList();
|
||||
|
||||
// Ensure we still have enough data after filtering
|
||||
if (prices.Count < 26)
|
||||
return;
|
||||
|
||||
@@ -512,4 +613,22 @@ public class TradingBotService
|
||||
var history = GetPriceHistory(symbol);
|
||||
return history?.LastOrDefault();
|
||||
}
|
||||
|
||||
public async Task ClearAllDataAsync()
|
||||
{
|
||||
_trades.Clear();
|
||||
_activePositions.Clear();
|
||||
_historyService.ClearAll();
|
||||
|
||||
foreach (var config in _assetConfigs.Values)
|
||||
{
|
||||
config.CurrentBalance = config.InitialBalance;
|
||||
config.CurrentHoldings = 0;
|
||||
config.AverageEntryPrice = 0;
|
||||
config.DailyTradeCount = 0;
|
||||
}
|
||||
|
||||
OnStatusChanged?.Invoke();
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
288
TradingBot/deployment/DEPLOYMENT_CHECKLIST.md
Normal file
288
TradingBot/deployment/DEPLOYMENT_CHECKLIST.md
Normal 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
|
||||
@@ -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!**
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user