Compare commits

...

18 Commits

Author SHA1 Message Date
Alby96 61f1e59964 Rimuovi cartella docs\deployment\ da TradingBot.csproj
Eliminata la voce relativa alla cartella docs\deployment\ dal file di progetto per semplificare la struttura e rimuovere riferimenti non più necessari alla documentazione di deployment.
2026-05-26 17:49:25 +02:00
Alby96 64f3511695 Nuove: multi-strategy, indicatori avanzati, posizioni
- Sidebar portfolio con metriche dettagliate (Totale, Investito, Disponibile, P&L, ROI) e aggiornamento real-time
- Sistema multi-strategia: 8 strategie assegnabili per asset, voting decisionale, pagina Trading Control
- Nuova pagina Posizioni: gestione, chiusura manuale, P&L non realizzato, notifiche
- Sistema indicatori tecnici: 7+ indicatori configurabili, segnali real-time, raccomandazioni, storico segnali
- Refactoring TradingBotService per capitale, P&L, ROI, eventi
- Nuovi modelli e servizi per strategie/indicatori, persistenza configurazioni
- UI/UX: navigazione aggiornata, widget, modali, responsive
- Aggiornamento README e CHANGELOG con tutte le novità
2026-01-06 17:49:07 +01:00
Alby96 c229c50f1d chore: Bump version to 1.5.2 - Add detailed capital metrics in sidebar: Total, Invested, Available, P&L, ROI 2025-12-23 10:51:06 +01:00
Alby96 0809c9af87 chore: Bump version to 1.5.1 - Add Positions management page with manual close functionality 2025-12-22 21:04:16 +01:00
Alby96 ae5f8f9249 chore: Bump version to 1.5.0 - Multi-strategy trading system with 8 famous strategies 2025-12-22 18:16:22 +01:00
Alby96 f21adf3313 chore: Bump version to 1.4.0 - Add comprehensive indicators system with configuration 2025-12-22 15:54:55 +01:00
Alby96 92c8e57a8c 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
2025-12-22 11:24:17 +01:00
Alby96 d7ae3e5d44 chore: Bump version to 1.3.0 - Add comprehensive logs page with real-time monitoring 2025-12-22 00:43:45 +01:00
Alby96 54cfe05687 chore: Bump version to 1.2.0 - Add trade history persistence and data management 2025-12-21 18:48:41 +01:00
Alby96 0e64afa1f2 Refactor documentazione, versioning e deployment
- Riorganizzato README.md con badge versione, changelog, guida rapida e istruzioni semplificate per Docker/Unraid
- Creato CHANGELOG.md secondo standard Keep a Changelog/SemVer
- Aggiunto script bump-version.ps1 per gestione automatica versioni e tagging Git
- Aggiornate guide deployment: PUBLISHING_GUIDE.md, UNRAID_INSTALL.md e README.md in /deployment
- Modificato unraid-template.xml: porta WebUI configurabile (default 8888), volumi e variabili ambiente semplificati
- Aggiornata PROJECT_STRUCTURE.md con nuova struttura e best practices
- Migliorata chiarezza, professionalità e automazione del workflow di rilascio
2025-12-21 18:31:00 +01:00
Alby96 121324dfc7 chore: Bump version to 1.1.0 - Add automated deployment system, versioning, and comprehensive documentation 2025-12-17 23:34:36 +01:00
Alby96 cc34d2b57f Riorganizzazione deployment, doc e publish automatico
- Spostata tutta la configurazione di deployment in /deployment (docker-compose, unraid-template, guide)
- Aggiunte e aggiornate guide dettagliate: publishing su Gitea, installazione Unraid, struttura progetto
- Migliorato target MSBuild: publish automatico su Gitea Registry da Visual Studio, log dettagliati, condizioni più robuste
- Aggiornato e ampliato .gitignore per escludere build, dati e file locali
- Rimossi file obsoleti dalla root (ora tutto in /deployment)
- Struttura più chiara, zero script esterni, documentazione completa e workflow di deploy semplificato
2025-12-17 23:15:46 +01:00
Alby96 5532ad2473 Supporto Unraid/Docker nativo, healthcheck e template
- Configurazione Kestrel ottimizzata per ambienti Docker/Unraid: porta 8080 in produzione, HTTPS redirect solo in sviluppo
- Endpoint /health sempre attivo per healthcheck automatici
- Aggiunti file docker-compose.yml e unraid-template.xml per deploy e gestione nativa su Unraid (senza Portainer)
- Nuova guida UNRAID_NATIVE_INSTALL.md per installazione, update e troubleshooting su Unraid
- Logging e appsettings separati per Development/Production
- launchSettings.json aggiornato e semplificato
- Rimosso package Azure Containers Tools dal csproj; aggiunto target MSBuild per push automatico su Gitea Registry dopo publish
- Algoritmo SMA più robusto: filtra dati nulli/invalidi e gestisce casi di dati insufficienti
- Pronto per deploy professionale, aggiornamento e gestione semplificata in ambienti containerizzati
2025-12-17 14:34:52 +01:00
Alby96 8ee8dc7e71 Refactoring Docker: integrazione con Visual Studio
Rimosse configurazioni e script manuali per Docker, build e documentazione. Riscritto il Dockerfile per supportare il flusso di lavoro Visual Studio/.NET 10 con multi-stage build semplificato. Aggiunte impostazioni di pubblicazione Docker in TradingBot.csproj e nuovo profilo "Docker" in launchSettings.json. Eliminati file di configurazione e script non più necessari; aggiunto Dockerfile.original come riferimento legacy. Ottimizzato il progetto per la pubblicazione tramite strumenti Microsoft.
2025-12-15 15:46:26 +01:00
Alby96 d933c7e812 Semplifica configurazione Docker e gestione porta UI
Riorganizza .env.example lasciando solo EXTERNAL_PORT e spostando tutte le altre impostazioni applicative nella UI web. Il mapping della porta in docker-compose.yml ora usa la variabile EXTERNAL_PORT per una personalizzazione più semplice. Rimosse variabili e opzioni avanzate non essenziali dal compose. Aggiornata la documentazione per riflettere la nuova gestione centralizzata delle impostazioni tramite interfaccia web.
2025-12-15 11:38:36 +01:00
Alby96 f69d5dd567 Configura Kestrel e accesso browser per Docker/Unraid
- Kestrel ora ascolta su 0.0.0.0:8080 per compatibilità Docker
- HTTPS redirect attivo solo in sviluppo, disabilitato in prod
- Aggiunta sezione "Kestrel" in appsettings.json e nuovo appsettings.Production.json con limiti di sicurezza
- Healthcheck Docker ora usa wget su /health (porta 8080)
- Aggiunta documentazione dettagliata in BROWSER_ACCESS_CONFIG.md
- Migliorata accessibilità browser, supporto reverse proxy e SignalR
2025-12-15 11:32:26 +01:00
Alby96 c93ccd5e4a Migliora robustezza Dockerfile e controlli TradingBotService
- Installa wget e aggiorna healthcheck in Dockerfile (usa wget invece di curl, UID 1001 per utente non-root)
- Aggiunti controlli di nullità e validità su simboli, prezzi e segnali in TradingBotService
- Migliorata gestione delle eccezioni con stampa dello stack trace
- Filtrati dati non validi prima del calcolo degli indicatori
- Aumentata la sicurezza e la resilienza contro dati corrotti o incompleti
2025-12-15 10:37:31 +01:00
Alby96 e414123cd0 Aggiorna e riorganizza la documentazione del progetto
- Sostituito README.md con versione avanzata e strutturata (indice, badge, quick start, deployment, troubleshooting, roadmap, credits)
- Aggiunto .gitignore completo per .NET, Docker, VSCode, log, dati locali e secrets
- Creato .env.example con tutte le variabili d’ambiente documentate per Docker/Unraid
- Aggiunti script organize-docs.ps1/.sh per strutturare e spostare la documentazione in docs/
- Aggiornate e migliorate tutte le guide tecniche (Docker, Unraid, Git workflow, troubleshooting, verifica finale)
- Documentazione ora pronta per ambienti di produzione, collaborazione e manutenzione
2025-12-13 00:24:58 +01:00
46 changed files with 7931 additions and 2499 deletions
+837 -2
View File
@@ -1,3 +1,838 @@
# Encelado
# ?? Encelado - TradingBot
Semplice e piccolo robot di trading automatizzato
**Sistema automatizzato di trading su criptovalute con interfaccia web moderna**
[![.NET](https://img.shields.io/badge/.NET-10.0-512BD4?logo=dotnet)](https://dotnet.microsoft.com/)
[![Blazor](https://img.shields.io/badge/Blazor-Server-512BD4?logo=blazor)](https://blazor.net/)
[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED?logo=docker)](https://www.docker.com/)
[![License](https://img.shields.io/badge/License-Private-red)](LICENSE)
> ?? **Robot di trading automatizzato** con strategie personalizzabili, analisi tecnica in tempo reale e interfaccia web intuitiva.
---
## ?? Indice
- [Panoramica](#-panoramica)
- [Caratteristiche](#-caratteristiche-principali)
- [Come Funziona](#-come-funziona)
- [Quick Start](#-quick-start)
- [Installazione](#-installazione)
- [Deployment](#-deployment)
- [Configurazione](#-configurazione)
- [Utilizzo](#-utilizzo)
- [Architettura](#-architettura)
- [Manutenzione](#-manutenzione)
- [Troubleshooting](#-troubleshooting)
- [Documentazione](#-documentazione-completa)
- [Roadmap](#-roadmap)
- [Licenza](#-licenza)
---
## ?? Panoramica
**Encelado TradingBot** è un'applicazione **Blazor Server** che implementa un sistema di trading automatizzato su criptovalute. Il bot analizza il mercato in tempo reale, applica strategie di trading configurabili e gestisce automaticamente l'esecuzione delle operazioni.
### ?? Demo
```
http://localhost:8080 # Locale
http://[UNRAID-IP]:8080 # Produzione
```
### ? Highlights
- ?? **Trading Automatico** - 15 asset supportati con strategie personalizzabili
- ?? **Dashboard Real-time** - Aggiornamenti live ogni 3 secondi
- ?? **Analisi Tecnica** - RSI, MACD, EMA integrate
- ?? **UI Moderna** - Dark theme, responsive, glassmorphism
- ?? **Docker Ready** - Deploy semplificato con containerizzazione
- ?? **Persistenza Dati** - Settings e configurazioni salvate
- ?? **Sicuro** - Modalità simulazione per testing
---
## ?? Caratteristiche Principali
### ?? Dashboard Completo
- **Portfolio Overview**: Valore totale, profitto, asset attivi
- **Top Performers**: Asset più redditizi
- **Attività Recente**: Ultime 8 operazioni
- **Aggiornamenti Real-time**: Via SignalR
### ?? Gestione Strategie
- **6 Strategie Predefinite**: RSI+MACD, SMA, Scalping, Trend Following, Mean Reversion, Conservative
- **Parametri Configurabili**: Stop Loss, Take Profit, condizioni entry/exit
- **Backtesting Ready**: Template per test storici
### ?? Asset Management
- **15 Criptovalute**: BTC, ETH, BNB, SOL, ADA, XRP, DOT, AVAX, MATIC, LINK, UNI, ATOM, LTC, ALGO, VET
- **Grid/List View**: Visualizzazioni multiple
- **Assegnazione Strategie**: Per singolo asset
- **Toggle On/Off**: Controllo granulare
### ?? Analisi Tecnica
- **Indicatori**: RSI (14), MACD (12,26,9), EMA (12,26)
- **Grafici SVG**: Rendering performante client-side
- **Time Series**: Storico prezzi e variazioni
### ?? Analisi Mercato
- **Grafici Interattivi**: Visualizzazione prezzi
- **Selector Asset**: Cambio asset dinamico
- **States Colorati**: Overbought/Oversold/Neutral
### ?? Statistiche Dettagliate
- **Performance Portfolio**: Metriche aggregate
- **Breakdown per Asset**: ROI, win rate, trades
- **Best/Worst Performers**: Identificazione automatica
- **Drilldown**: Analisi approfondita singolo asset
### ?? Impostazioni
- **Persistenza Automatica**: Salvataggio su file JSON
- **Configurazioni**: Intervallo aggiornamento, log level, auto-start
- **Notifiche**: Feedback visivo
---
## ?? Come Funziona
### ?? Architettura del Sistema
```
???????????????????????????????????????????????????????????
? BLAZOR SERVER UI ?
? (Dashboard, Strategie, Trading, Market, Stats) ?
???????????????????????????????????????????????????????????
? SignalR Real-time
?
???????????????????????????????????????????????????????????
? TRADING BOT SERVICE (Core) ?
? - Gestione Asset ?
? - Esecuzione Strategie ?
? - Risk Management ?
? - Event System ?
???????????????????????????????????????????????????????????
? ? ?
? ? ?
??????????? ???????????????? ????????????????????
? Market ? ? Technical ? ? Settings ?
? Data ? ? Analysis ? ? Service ?
? Service ? ? Service ? ? ?
??????????? ???????????????? ????????????????????
?
?
???????????????????????????????????????????????????????????
? SIMULATED / REAL MARKET DATA ?
? (CoinGecko API / Simulazione) ?
???????????????????????????????????????????????????????????
```
### ?? Ciclo di Trading
1. **?? Acquisizione Dati**
- Market Data Service recupera prezzi ogni 3 secondi
- Aggiorna cache interna per tutti gli asset
2. **?? Analisi Tecnica**
- Calcolo indicatori (RSI, MACD, EMA)
- Valutazione trend e momentum
- Identificazione pattern
3. **?? Valutazione Strategia**
- Verifica condizioni BUY/SELL
- Controllo risk management
- Validazione budget disponibile
4. **? Esecuzione Trade**
- Calcolo dimensione posizione
- Esecuzione ordine (simulato/reale)
- Aggiornamento portfolio
5. **?? Logging & Notifica**
- Salvataggio operazione
- Aggiornamento statistiche
- Notifica UI via SignalR
### ?? Strategia di Default: RSI + MACD
```csharp
BUY quando:
- RSI < 40 (asset ipervenduto)
- MACD Histogram > 0 (momentum positivo)
- Budget disponibile >= MinTradeAmount
SELL quando:
- RSI > 60 (asset ipercomprato)
- MACD Histogram < 0 (momentum negativo)
- Holdings > 0
OPPURE
- Profitto >= Take Profit (10%)
- Perdita >= Stop Loss (5%)
```
### ?? Gestione Rischio
```
? Max Daily Trades: 50 per asset
? Max Position Size: $5000 per asset
? Min Trade Amount: $10
? Trade Size: 30% del balance disponibile (max)
? Min Interval: 10 secondi tra trades
? Stop Loss: 5% configurabile
? Take Profit: 10% configurabile
```
---
## ?? Quick Start
### Locale (Windows)
```powershell
# Clone repository
git clone https://192.168.30.23/Alby96/Encelado
cd Encelado\TradingBot
# Restore e Build
dotnet restore
dotnet build
# Run
dotnet run
# Accesso
# http://localhost:5001
```
### Docker (Cross-platform)
```bash
# Build
docker-compose build
# Run
docker-compose up -d
# Accesso
# http://localhost:8080
```
### Unraid (Production)
Vedi [Deployment su Unraid](#deployment-su-unraid)
---
## ?? Installazione
### Prerequisiti
**Per Sviluppo Locale**:
- ? [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- ? Visual Studio 2022+ o VS Code
- ? Git
**Per Docker**:
- ? [Docker Desktop](https://www.docker.com/products/docker-desktop) (Windows/Mac)
- ? Docker Engine (Linux)
- ? Docker Compose
**Per Unraid**:
- ? Unraid 6.10+
- ? Docker installato
- ? Portainer (opzionale ma consigliato)
### Installazione Passo-Passo
#### 1. Clone Repository
```bash
git clone https://192.168.30.23/Alby96/Encelado
cd Encelado/TradingBot
```
#### 2. Restore Dipendenze
```bash
dotnet restore
```
#### 3. Configurazione (Opzionale)
Crea `appsettings.Development.json`:
```json
{
"Logging": {
"LogLevel": {
"Default": "Information"
}
},
"TradingBot": {
"SimulationMode": true,
"UpdateIntervalSeconds": 3
}
}
```
#### 4. Build
```bash
dotnet build -c Release
```
#### 5. Run
```bash
dotnet run
```
#### 6. Verifica
Apri browser su: `https://localhost:5001` o `http://localhost:5000`
---
## ?? Deployment
### Deployment Locale
```bash
# Development
dotnet run
# Production build
dotnet publish -c Release -o ./publish
cd publish
dotnet TradingBot.dll
```
### Deployment Docker
#### Build Immagine
```bash
# Windows
.\build-docker.bat
# Linux/Mac
chmod +x build-docker.sh
./build-docker.sh
```
#### Run Container
```bash
docker-compose up -d
```
#### Verifica
```bash
# Status
docker ps | grep tradingbot
# Logs
docker logs tradingbot -f
# Health
curl http://localhost:8080/health
```
### Deployment su Unraid
#### Metodo 1: Portainer Stack (Consigliato)
1. **Portainer** ? Stacks ? Add stack
2. **Name**: `tradingbot`
3. **Git Repository**: `https://192.168.30.23/Alby96/Encelado`
4. **Compose path**: `TradingBot/docker-compose.yml`
5. **Deploy**
**Setup Webhook Auto-deploy**:
- Portainer ? Webhooks ? Create
- Gitea ? Settings ? Webhooks ? Add
- Ogni push ? Auto-deploy!
#### Metodo 2: SSH Manuale
```bash
# SSH su Unraid
ssh root@[UNRAID-IP]
# Clone
cd /mnt/user/appdata
git clone https://192.168.30.23/Alby96/Encelado.git tradingbot
cd tradingbot/TradingBot
# Deploy
docker-compose up -d
```
**Guida Completa**: [docs/deployment/UNRAID_DEPLOYMENT.md](TradingBot/docs/deployment/UNRAID_DEPLOYMENT.md)
---
## ?? Configurazione
### File Configurazione
**Locale**: `%LocalAppData%/TradingBot/appsettings.json`
**Docker**: Volume `tradingbot-data`
### Parametri Principali
```json
{
"SimulationMode": true, // true = simulazione, false = reale
"AutoStartBot": true, // Avvio automatico
"UpdateIntervalSeconds": 3, // Intervallo aggiornamento (2-10)
"DesktopNotifications": false, // Notifiche desktop
"ConfirmManualTrades": false, // Conferma trades manuali
"LogLevel": "Info", // Error, Warning, Info, Debug
"SidebarCollapsed": false // Stato sidebar UI
}
```
### Environment Variables (Docker)
```bash
# .env file
TZ=Europe/Rome
ASPNETCORE_ENVIRONMENT=Production
TRADINGBOT__SimulationMode=true
TRADINGBOT__AutoStartBot=true
TRADINGBOT__UpdateIntervalSeconds=3
```
### Configurazione Asset
Modifica in UI: **Asset** ? Seleziona asset ? **Configura**
O nel codice: `Services/TradingBotService.cs` ? `InitializeAssets()`
---
## ?? Utilizzo
### Navigazione UI
#### ?? Dashboard
- Overview portfolio e performance
- Top 6 asset attivi
- Ultimi 8 trades
#### ?? Strategie
- Visualizza strategie disponibili
- Strategia attiva: RSI + MACD Cross
- Template predefiniti
#### ?? Asset
- **Grid View**: Card dettagliate
- **List View**: Tabella compatta
- **Assegna Strategie**: Dropdown per asset
- **Toggle**: Attiva/disattiva trading
#### ?? Trading
- Grid tutti asset
- Toggle on/off
- Storico operazioni con filtri
#### ?? Analisi Mercato
- Grafici interattivi
- Indicatori tecnici live
- Selector asset
#### ?? Statistiche
- Metriche aggregate
- Performance per asset
- Best/Worst performers
- Drilldown dettagliato
#### ?? Impostazioni
- Configurazioni globali
- Salvataggio automatico
- Reset a defaults
### Operazioni Comuni
#### Avviare/Fermare Bot
```
Top Bar ? Button "Avvia" / "Stop"
```
#### Cambiare Strategia per Asset
```
Asset ? Grid/List ? Dropdown "Strategia Assegnata" ? Seleziona
```
#### Attivare/Disattivare Asset
```
Asset ? Toggle switch per ogni asset
```
#### Monitorare Performance
```
Dashboard ? Vedi summary
Statistics ? Dettagli completi
```
---
## ??? Architettura
### Stack Tecnologico
```
Frontend: Blazor Server (.NET 10)
SignalR (Real-time)
CSS Custom (Dark theme)
SVG Charts
Backend: ASP.NET Core
Services (Singleton)
Event-driven architecture
Data: In-memory (runtime)
JSON files (settings)
Deploy: Docker
Docker Compose
Unraid compatible
```
### Componenti Principali
```
TradingBot/
??? Components/
? ??? Layout/
? ? ??? MainLayout.razor # Sidebar + Layout
? ??? Pages/
? ? ??? Dashboard.razor # Homepage
? ? ??? Strategies.razor # Strategie
? ? ??? Assets.razor # Asset management
? ? ??? Trading.razor # Trading view
? ? ??? Market.razor # Analisi mercato
? ? ??? Statistics.razor # Statistiche
? ? ??? Settings.razor # Impostazioni
? ??? Shared/
? ??? AdvancedChart.razor # Grafico SVG
? ??? AssetSettings.razor # Config asset
?
??? Services/
? ??? TradingBotService.cs # Core trading logic
? ??? SimulatedMarketDataService.cs # Dati simulati
? ??? SettingsService.cs # Persistenza settings
? ??? ITradingStrategy.cs # Strategy interface
? ??? SimpleMovingAverageStrategy.cs
? ??? TechnicalAnalysis.cs # Calcolo indicatori
?
??? Models/
? ??? AssetConfiguration.cs # Config asset
? ??? AssetStatistics.cs # Statistiche
? ??? MarketPrice.cs # Dati mercato
? ??? Trade.cs # Operazione
? ??? TechnicalIndicators.cs # RSI, MACD, EMA
? ??? AppSettings.cs # Settings app
?
??? wwwroot/
??? app.css # Stili globali
```
### Pattern & Practices
- ? **Singleton Services**: Per state management
- ? **Event-driven**: OnStatusChanged, OnPriceUpdated, OnTradeExecuted
- ? **Dependency Injection**: ASP.NET Core DI
- ? **Component Isolation**: Scoped CSS
- ? **Real-time Updates**: SignalR
- ? **Responsive Design**: Mobile-first
---
## ?? Manutenzione
### Backup
#### Backup Automatico (Unraid)
Script: `/root/scripts/backup-tradingbot.sh`
```bash
#!/bin/bash
BACKUP_DIR="/mnt/user/backups/tradingbot"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p $BACKUP_DIR
docker run --rm \
-v tradingbot_tradingbot-data:/data \
-v $BACKUP_DIR:/backup \
alpine tar czf /backup/tradingbot-data-$DATE.tar.gz -C /data .
# Mantieni 7 giorni
find $BACKUP_DIR -name "*.tar.gz" -mtime +7 -delete
```
Crontab:
```
0 3 * * * /root/scripts/backup-tradingbot.sh
```
#### Backup Manuale
```bash
# Locale
cp -r %LocalAppData%/TradingBot backup/
# Docker
docker run --rm -v tradingbot_tradingbot-data:/data -v $(pwd):/backup alpine tar czf /backup/backup.tar.gz -C /data .
```
### Aggiornamenti
#### Via Git (Locale)
```bash
cd Encelado/TradingBot
git pull origin main
dotnet build
dotnet run
```
#### Via Docker
```bash
git pull origin main
docker-compose build
docker-compose up -d
```
#### Via Unraid (SSH)
```bash
cd /mnt/user/appdata/tradingbot/TradingBot
git pull
docker-compose up -d --build
```
#### Via Webhook (Automatico)
Ogni push su Gitea ? Auto-deploy se webhook configurato!
### Monitoring
#### Health Check
```bash
curl http://localhost:8080/health
```
Output atteso: `Healthy`
#### Logs
```bash
# Docker
docker logs tradingbot -f
# Locale
# Vedi console di Visual Studio
```
#### Resource Usage
```bash
docker stats tradingbot
```
### Pulizia
```bash
# Stop container
docker-compose down
# Remove volumes (ATTENZIONE: dati persi!)
docker-compose down -v
# Clean images
docker system prune -a
```
---
## ?? Troubleshooting
### Problemi Comuni
#### Bot non si avvia
**Sintomo**: Container esce immediatamente
**Soluzione**:
```bash
docker logs tradingbot
# Verifica errori
# Controlla porta 8080 libera
```
#### Cache Browser
**Sintomo**: UI non si aggiorna
**Soluzione**:
```
Ctrl + Shift + R (hard refresh)
O vedi: docs/troubleshooting/BROWSER_CACHE_GUIDE.md
```
#### Sidebar non collassa
**Sintomo**: Toggle non funziona
**Soluzione**:
```
1. Hard refresh: Ctrl + Shift + R
2. Vedi: docs/troubleshooting/SIDEBAR_TOGGLE_DEBUG.md
3. Check console: F12 ? Console
```
#### Porta in uso
**Sintomo**: `Address already in use`
**Soluzione**:
```yaml
# docker-compose.yml
ports:
- "8081:8080" # Cambia porta esterna
```
#### Out of Memory
**Sintomo**: Container crashato
**Soluzione**:
```yaml
# docker-compose.yml
deploy:
resources:
limits:
memory: 2G # Aumenta da 1G
```
### Log Debug
```bash
# Aumenta log level
# Settings ? Log Level ? Debug
# O via environment
TRADINGBOT__LogLevel=Debug
```
### Support
1. ?? Leggi [docs/troubleshooting/COMMON_ISSUES.md](TradingBot/docs/troubleshooting/COMMON_ISSUES.md)
2. ?? Cerca issue simili su Gitea
3. ?? Apri issue con:
- Descrizione problema
- Logs rilevanti
- Environment (OS, Docker version, etc.)
- Steps to reproduce
---
## ?? Documentazione Completa
Tutta la documentazione è organizzata in `TradingBot/docs/`:
- ?? **[docs/README.md](TradingBot/docs/README.md)** - Indice completo
- ?? **[docs/installation/](TradingBot/docs/installation/)** - Guide installazione
- ??? **[docs/architecture/](TradingBot/docs/architecture/)** - Architettura sistema
- ?? **[docs/deployment/](TradingBot/docs/deployment/)** - Guide deployment
- ?? **[docs/configuration/](TradingBot/docs/configuration/)** - Configurazione
- ?? **[docs/trading/](TradingBot/docs/trading/)** - Strategie e indicatori
- ?? **[docs/development/](TradingBot/docs/development/)** - Workflow sviluppo
- ?? **[docs/troubleshooting/](TradingBot/docs/troubleshooting/)** - Risoluzione problemi
- ?? **[docs/verification/](TradingBot/docs/verification/)** - Testing e QA
- ?? **[docs/api/](TradingBot/docs/api/)** - API Reference
---
## ??? Roadmap
### v1.0 (Current) ?
- [x] Core trading engine
- [x] 15 asset supportati
- [x] Strategie base
- [x] UI completa
- [x] Docker deployment
- [x] Unraid support
### v1.1 (Planned)
- [ ] Dati reali (CoinGecko API integration completa)
- [ ] Multi-strategy per asset
- [ ] Alert system con notifiche
- [ ] Export/Import configurazioni
- [ ] Paper trading mode
### v1.2 (Future)
- [ ] Backtesting su dati storici
- [ ] Machine Learning per ottimizzazione
- [ ] Mobile app (MAUI)
- [ ] Multi-user support
- [ ] API REST pubblica
### v2.0 (Vision)
- [ ] Exchange integration (Binance, Coinbase)
- [ ] Real money trading
- [ ] Advanced risk management
- [ ] Portfolio rebalancing
- [ ] Tax reporting
---
## ?? Contributing
Progetto privato. Contributi benvenuti previo contatto con il maintainer.
Vedi: [docs/development/CONTRIBUTING.md](TradingBot/docs/development/CONTRIBUTING.md)
---
## ?? Licenza
Progetto privato - Tutti i diritti riservati
© 2024 Alberto - Encelado Project
**DISCLAIMER**: Questa è un'applicazione di simulazione a scopo educativo. Non utilizzare con denaro reale senza test approfonditi e comprensione completa dei rischi del trading.
---
## ?? Contatti
- **Maintainer**: Alberto (Alby96)
- **Repository**: https://192.168.30.23/Alby96/Encelado
- **Gitea**: https://192.168.30.23
---
## ?? Ringraziamenti
Grazie a:
- Microsoft per .NET e Blazor
- CoinGecko per API dati mercato
- Community open source per librerie e tools
---
**Ultima modifica**: 2024-12-12
**Versione**: 1.0.0
**Status**: ? Production Ready
+105
View File
@@ -0,0 +1,105 @@
# Visual Studio
.vs/
*.user
*.suo
*.userosscache
*.sln.docstates
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Docker
obj/Docker/
# NuGet
*.nupkg
*.snupkg
**/packages/*
!**/packages/build/
# Files generated by publishing
[Pp]ublish/
PublishOutput/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# ReSharper
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JetBrains Rider
.idea/
*.sln.iml
# Visual Studio cache files
*.[Cc]ache
!?*.[Cc]ache/
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# Mono Auto Generated Files
mono_crash.*
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Mac files
.DS_Store
# Application data and persistence
**/data/
trade-history.json
active-positions.json
settings.json
*.db
*.db-shm
*.db-wal
# Logs
*.log
# Temporary files
*.tmp
*.temp
-230
View File
@@ -1,230 +0,0 @@
# ?? ISTRUZIONI PER FORZARE IL REFRESH DEL BROWSER
## ?? Il Problema
L'applicazione è stata completamente aggiornata con una nuova sidebar verticale moderna, ma il browser potrebbe mostrare ancora la versione vecchia a causa della **cache**.
## ? Build Status
- ? **Compilazione riuscita**
- ? **0 errori**
- ? **0 warning**
- ? **Tutti i CSS aggiornati**
- ? **Bootstrap Icons caricato**
---
## ?? METODO 1: Hard Refresh (CONSIGLIATO)
### Windows - Chrome/Edge
```
1. Apri DevTools: F12
2. Click DESTRO sul pulsante Refresh (?)
3. Seleziona "Svuota cache e ricaricamento forzato"
```
**OPPURE**
```
Premi: Ctrl + Shift + R
```
### Windows - Firefox
```
Premi: Ctrl + Shift + R
```
### Windows - Tutti i Browser
```
Premi: Ctrl + F5
```
---
## ?? METODO 2: Cancella Cache Manualmente
### Chrome/Edge
```
1. Premi Ctrl + Shift + Delete
2. Seleziona "Immagini e file memorizzati nella cache"
3. Intervallo: "Tutto"
4. Click "Cancella dati"
5. Ricarica la pagina (F5)
```
### Firefox
```
1. Premi Ctrl + Shift + Delete
2. Seleziona "Cache"
3. Click "Cancella adesso"
4. Ricarica la pagina (F5)
```
---
## ?? METODO 3: Modalità Incognito (TEST VELOCE)
### Chrome/Edge
```
Premi: Ctrl + Shift + N
```
### Firefox
```
Premi: Ctrl + Shift + P
```
Poi naviga su `https://localhost:[PORT]` nella finestra incognito.
---
## ??? METODO 4: Disabilita Cache (Durante Sviluppo)
### Per Tutti i Browser
```
1. Apri DevTools: F12
2. Vai su tab "Network"
3. Spunta "Disable cache"
4. MANTIENI DevTools APERTO
5. Ricarica (F5)
```
Questo è perfetto durante lo sviluppo!
---
## ?? METODO 5: Restart Server + Clean Build
Se proprio non funziona, fai un clean restart:
```powershell
# Stop server
Ctrl + C
# Clean
dotnet clean
# Remove bin/obj
Remove-Item -Recurse -Force bin,obj
# Restore
dotnet restore
# Rebuild
dotnet build
# Run
dotnet run
```
Poi fai Hard Refresh nel browser.
---
## ?? COSA DOVRESTI VEDERE
Dopo il refresh corretto, dovresti vedere:
```
??????????????????????????????????????
? Sidebar Verticale Sinistra ?
? ?
? [??] TradingBot [?] ? ? Brand + Toggle
? ? ATTIVO ?
? ?????????????????????????????????? ?
? ?? Dashboard ? ? Menu Items
? ?? Strategie ? Verticali
? ?? Asset ?
? ?? Trading ?
? ?? Analisi Mercato ?
? ?? Statistiche ?
? ?? Impostazioni ?
? ?????????????????????????????????? ?
? Portfolio $15,000 ? ? Summary
? Profitto $0.00 ?
??????????????????????????????????????
```
**NON** dovresti vedere più i link testuali sotto il logo!
---
## ?? TROUBLESHOOTING
### Problema: "Vedo ancora i link sotto il logo"
**Soluzione**: Cache non pulita correttamente
```
1. Chiudi TUTTE le tab del browser
2. Chiudi il browser completamente
3. Riapri e vai direttamente a localhost
4. Premi Ctrl + Shift + R
```
### Problema: "Le icone non si vedono"
**Soluzione**: Bootstrap Icons non caricato
```
1. Apri DevTools (F12)
2. Tab Console
3. Cerca errori di caricamento CSS
4. Se vedi errori, il server potrebbe non essere avviato correttamente
```
### Problema: "Tutto bianco/rotto"
**Soluzione**: CSS non caricato
```
1. DevTools ? Network tab
2. Ricarica (F5)
3. Verifica che app.css e MainLayout.razor.css siano caricati (200 OK)
4. Se vedi 404, restart del server
```
---
## ? CHECKLIST FINALE
Prima di contattare per supporto, verifica:
- [ ] Ho fatto Hard Refresh (Ctrl + Shift + R)?
- [ ] Ho provato in modalità Incognito?
- [ ] Ho pulito la cache manualmente?
- [ ] Il server è in esecuzione correttamente?
- [ ] Ho fatto `dotnet clean` e `dotnet build`?
- [ ] Ho verificato la Console (F12) per errori?
- [ ] Ho provato con un browser diverso?
---
## ?? FUNZIONA?
Se dopo questi passaggi vedi la sidebar moderna verticale:
- ? Tutto è corretto!
- ? Puoi iniziare a usare l'applicazione
- ? Il problema era solo la cache
Se NON funziona ancora:
- ?? Apri DevTools (F12)
- ?? Fai uno screenshot della Console
- ?? Condividi gli errori che vedi
---
## ?? NOTE TECNICHE
### File CSS Modificati
1. `wwwroot/app.css` - Stili globali con priorità
2. `Components/Layout/MainLayout.razor.css` - Stili scoped con ::deep
### Modifiche Applicate
- ? Bootstrap Icons CDN aggiunto
- ? Namespace globali in _Imports.razor
- ? CSS con !important per override
- ? ::deep selectors per scoped CSS
- ? Layout completamente riscritto
### Port di Default
L'applicazione di solito gira su:
- `https://localhost:5001` (HTTPS)
- `http://localhost:5000` (HTTP)
Verifica nel terminal quale porta sta usando!
---
**Buon trading! ??**
+259
View File
@@ -0,0 +1,259 @@
# Changelog
Tutte le modifiche significative a TradingBot sono documentate qui.
Formato basato su [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), segue [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [1.5.2] - 2024-12-22
### Added
- **Detailed Capital Metrics in Sidebar**: Portfolio Summary espansa con metriche complete
- Capitale Totale: Somma di disponibile + investito
- Capitale Investito: Valore posizioni aperte correnti
- Capitale Disponibile: Cash disponibile per trading
- P&L Corrente: Profitto/perdita non realizzato sulle posizioni aperte
- ROI: Return on Investment percentuale sul capitale iniziale
- **Real-time Updates**: Metriche aggiornate automaticamente
- Su ogni cambio di prezzo
- Su ogni trade eseguito
- Su ogni cambio di status
### Changed
- Portfolio Summary sidebar completamente ridisegnata
- Color-coding per metriche (verde=profit, rosso=loss, arancio=investito, blu=disponibile)
- Calcolo ROI basato su capitale iniziale + P&L realizzato e non realizzato
### Technical
- Event-driven updates su OnPriceUpdated, OnTradeExecuted, OnStatusChanged
- Separazione chiara tra capitale investito e disponibile
- P&L calculation: (Current Value - Entry Value) per posizioni aperte
---
## [1.5.1] - 2024-12-22
### Added
- **Positions Management Page**: Pagina dedicata gestione posizioni aperte
- Visualizzazione completa posizioni attive
- Real-time P&L unrealized calculation
- Manual close position functionality
- Confirmation modal con dettagli completi
- Header statistics (Active positions, Total value, Total P&L)
- Empty state quando nessuna posizione aperta
- Success notifications
- **ClosePositionManuallyAsync**: Metodo pubblico TradingBotService
- Close manual positions via API
- Safety checks (bot running, position exists)
- Automatic logging
### Changed
- MainLayout navigation menu aggiornato con link Positions
- Version display aggiornata a v1.5.1
### Technical
- Event-driven updates per real-time P&L
- Position cards con holding time formattato
- Color-coded P&L (green=profit, red=loss)
- Modal confirmation per sicurezza
- No manual opening - solo chiusura
---
## [1.5.0] - 2024-12-22
### Added
- **Multi-Strategy Trading System**: Sistema completo di gestione strategie multiple per asset
- 8 strategie di trading famose implementate
- Assignment multiplo strategie per asset
- Sistema di voting per decisioni aggregate
- Trading Control page dedicata
- **Trading Strategies**: 8 strategie professionali preimpostate
- RSI Strategy (Oscillator - Medium risk)
- MACD Strategy (Momentum - Medium risk)
- Bollinger Bands (Volatility - Low risk)
- Mean Reversion (Contrarian - High risk)
- Momentum (Trend Following - Medium risk)
- EMA Crossover / Golden Cross (Trend - Low risk)
- Scalping (Short-term - Very High risk)
- Breakout (Volatility - High risk)
- **TradingStrategiesService**: Gestione centralizzata strategie
- Strategy registry con metadata
- Asset-strategy mapping
- Decision aggregation con voting
- Persistence delle configurazioni
- **Trading Control Page**: Interfaccia gestione strategie
- Grid asset con stato real-time
- Strategy selector modal per category
- Visualizzazione decisioni aggregate
- Risk e timeframe indicators
- **Version Display**: Versione applicazione visibile in sidebar footer
- Version number (v1.5.0)
- Build date
### Changed
- TradingBotService integrato con TradingStrategiesService
- MainLayout aggiornato con link Trading Control
- Navigation menu riorganizzato
### Technical
- 8 strategy implementations (ITradingStrategy interface)
- Voting algorithm: 60% consensus threshold
- Strategy metadata: Category, Risk Level, Timeframe
- Persistence: `strategy-mappings.json`
- Confidence-based decision making
---
## [1.4.0] - 2024-12-22
### Added
- **Indicators System**: Sistema completo di indicatori tecnici configurabili
- 7 indicatori predefiniti: RSI, MACD, SMA (20/50), EMA 12, Bollinger Bands, Stochastic
- Configurazione parametri per ogni indicatore
- Abilitazione/disabilitazione selettiva indicatori
- Soglie personalizzabili (ipercomprato/ipervenduto)
- **Indicators Page**: Interfaccia dedicata gestione indicatori
- Configurazione real-time di tutti i parametri
- Status live per ogni asset attivo
- Visualizzazione condizioni mercato
- Raccomandazioni basate su indicatori
- **Indicator Signals**: Sistema di segnali trading
- Segnali BUY/SELL/HOLD con strength rating
- Storia ultimi 100 segnali generati
- Filtro segnali per symbol
- Notifiche real-time nuovi segnali
- **Trading Recommendations**: Raccomandazioni aggregate
- Analisi multi-indicatore per ogni asset
- Livello di confidenza basato su consenso
- Lista indicatori di supporto
- Azioni consigliate (BUY/SELL/HOLD)
- **Indicator Models**: Nuovi modelli dati
- IndicatorConfig - Configurazione indicatore
- IndicatorSignal - Segnale trading
- IndicatorStatus - Status per asset
- TradingRecommendation - Raccomandazione trading
### Changed
- MainLayout aggiornato con link Indicators
- IndicatorsService registrato come singleton
- Configurazione indicatori persistita in `indicators-config.json`
### Technical
- Persistenza configurazione indicatori in `/app/data`
- Event-driven updates per UI real-time
- Supporto 8 tipi di indicatori (RSI, MACD, SMA, EMA, BB, Stochastic, Volume, ATR)
- Market conditions: Overbought, Oversold, Bullish, Bearish, Neutral, Ranging, Trending
---
## [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
- **Automated Deployment**: MSBuild post-build per push automatico su Gitea Registry
- **Multiple Docker Tags**: latest, version, version-date per ogni release
- **Versioning System**: Script PowerShell `bump-version.ps1` per gestione versioni
- **Unraid Support**: Template XML per installazione 1-click
- **Documentation**: Guide complete per deployment e versioning
### Changed
- Riorganizzata struttura progetto (`/deployment`, `/docs`)
- Default WebUI port cambiato da 8080 a 8888
- Health check timing aumentato a 40s per startup Blazor
### Fixed
- WebUI icon non visibile in Unraid Docker tab
- Port mapping non configurabile in template
- Template URL path corretto
---
## [1.0.0] - 2024-12-15
### Added
- **Initial Release** di TradingBot
- Blazor Server UI con dashboard real-time
- Simple Moving Average (SMA) trading strategy
- 15 criptovalute supportate
- Simulazione market data per testing
- Trade history e statistics
- Settings persistenti via JSON
- Indicatori tecnici: SMA, EMA, RSI, MACD, Bollinger Bands
- Docker support con multi-stage build
- Health checks integrati
---
## Version Legend
- **Added**: Nuove features
- **Changed**: Modifiche a funzionalità esistenti
- **Deprecated**: Features da rimuovere
- **Removed**: Features rimosse
- **Fixed**: Bug fixes
- **Security**: Security fixes
- **Technical**: Miglioramenti tecnici e infrastrutturali
---
[1.5.0]: https://gitea.encke-hake.ts.net/Alby96/Encelado/compare/v1.4.0...v1.5.0
[1.4.0]: https://gitea.encke-hake.ts.net/Alby96/Encelado/compare/v1.3.0...v1.4.0
[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
+133 -25
View File
@@ -41,6 +41,22 @@
}
</NavLink>
<NavLink class="menu-item" href="/trading-control" title="Trading Control">
<span class="item-icon bi bi-sliders"></span>
@if (!sidebarCollapsed)
{
<span class="item-text">Trading Control</span>
}
</NavLink>
<NavLink class="menu-item" href="/positions" title="Posizioni">
<span class="item-icon bi bi-wallet2"></span>
@if (!sidebarCollapsed)
{
<span class="item-text">Posizioni</span>
}
</NavLink>
<NavLink class="menu-item" href="/strategies" title="Strategie">
<span class="item-icon bi bi-diagram-3"></span>
@if (!sidebarCollapsed)
@@ -49,6 +65,14 @@
}
</NavLink>
<NavLink class="menu-item" href="/indicators" title="Indicatori">
<span class="item-icon bi bi-graph-down-arrow"></span>
@if (!sidebarCollapsed)
{
<span class="item-text">Indicatori</span>
}
</NavLink>
<NavLink class="menu-item" href="/assets" title="Asset">
<span class="item-icon bi bi-coin"></span>
@if (!sidebarCollapsed)
@@ -81,6 +105,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)
@@ -95,16 +127,51 @@
{
<div class="sidebar-summary">
<div class="summary-card">
<div class="summary-row">
<span class="summary-title">Portfolio</span>
<span class="summary-amount">$@portfolioValue.ToString("N0")</span>
<div class="summary-header">
<span class="summary-section-title">Portfolio</span>
</div>
<div class="summary-row">
<span class="summary-title">Profitto</span>
<span class="summary-amount @(totalProfit >= 0 ? "profit" : "loss")">
$@totalProfit.ToString("N2")
<span class="summary-title">Capitale Totale</span>
<span class="summary-amount">$@totalCapital.ToString("N2")</span>
</div>
<div class="summary-row">
<span class="summary-title">Investito</span>
<span class="summary-amount invested">$@investedCapital.ToString("N2")</span>
</div>
<div class="summary-row">
<span class="summary-title">Disponibile</span>
<span class="summary-amount available">$@availableCapital.ToString("N2")</span>
</div>
<div class="summary-divider"></div>
<div class="summary-row highlight">
<span class="summary-title">P&L Corrente</span>
<span class="summary-amount @(currentPL >= 0 ? "profit" : "loss")">
@(currentPL >= 0 ? "+" : "")$@currentPL.ToString("N2")
</span>
</div>
<div class="summary-row">
<span class="summary-title">ROI</span>
<span class="summary-amount @(roiPercentage >= 0 ? "profit" : "loss")">
@(roiPercentage >= 0 ? "+" : "")@roiPercentage.ToString("F2")%
</span>
</div>
</div>
</div>
<!-- Version Footer -->
<div class="sidebar-footer">
<div class="version-info">
<span class="version-label">TradingBot</span>
<span class="version-number">v1.5.2</span>
</div>
<div class="build-info">
<span class="build-date">Build: @DateTime.Now.ToString("yyyy-MM-dd")</span>
</div>
</div>
}
@@ -140,6 +207,11 @@
private bool isRunning => BotService.Status.IsRunning;
private decimal portfolioValue = 0;
private decimal totalProfit = 0;
private decimal totalCapital = 0;
private decimal investedCapital = 0;
private decimal availableCapital = 0;
private decimal currentPL = 0;
private decimal roiPercentage = 0;
protected override void OnInitialized()
{
@@ -147,6 +219,7 @@
sidebarCollapsed = settings.SidebarCollapsed;
BotService.OnStatusChanged += HandleUpdate;
BotService.OnTradeExecuted += HandleTradeExecuted;
BotService.OnPriceUpdated += HandlePriceUpdate;
SettingsService.OnSettingsChanged += HandleSettingsChanged;
@@ -176,34 +249,69 @@
private void UpdateStats()
{
portfolioValue = BotService.AssetConfigurations.Values.Sum(c =>
c.CurrentBalance + (c.CurrentHoldings * (BotService.GetLatestPrice(c.Symbol)?.Price ?? 0)));
totalCapital = 0;
investedCapital = 0;
availableCapital = 0;
currentPL = 0;
totalProfit = 0;
totalProfit = BotService.AssetConfigurations.Values.Sum(c => c.TotalProfit);
foreach (var config in BotService.AssetConfigurations.Values)
{
if (!config.IsEnabled) continue;
// Capitale disponibile (cash)
availableCapital += config.CurrentBalance;
// Capitale investito in posizioni aperte
var currentPrice = BotService.GetLatestPrice(config.Symbol)?.Price ?? 0;
var positionValue = config.CurrentHoldings * currentPrice;
investedCapital += positionValue;
// P&L non realizzato sulle posizioni aperte
if (config.CurrentHoldings > 0 && config.AverageEntryPrice > 0)
{
var entryValue = config.CurrentHoldings * config.AverageEntryPrice;
currentPL += (positionValue - entryValue);
}
// Totale profitti/perdite realizzati
totalProfit += config.TotalProfit;
}
// Capitale totale = Disponibile + Investito
totalCapital = availableCapital + investedCapital;
// Portfolio value include anche i profitti realizzati
portfolioValue = totalCapital + totalProfit;
// ROI basato sul capitale iniziale
var initialCapital = BotService.AssetConfigurations.Values
.Where(c => c.IsEnabled)
.Sum(c => c.InitialBalance);
if (initialCapital > 0)
{
var totalGain = currentPL + totalProfit;
roiPercentage = (totalGain / initialCapital) * 100;
}
else
{
roiPercentage = 0;
}
}
private void HandleUpdate()
{
UpdateStats();
InvokeAsync(StateHasChanged);
}
private void HandleUpdate() => InvokeAsync(() => { UpdateStats(); StateHasChanged(); });
private void HandlePriceUpdate(string symbol, MarketPrice price)
{
UpdateStats();
InvokeAsync(StateHasChanged);
}
private void HandleTradeExecuted(Trade trade) => InvokeAsync(() => { UpdateStats(); StateHasChanged(); });
private void HandleSettingsChanged()
{
var settings = SettingsService.GetSettings();
sidebarCollapsed = settings.SidebarCollapsed;
InvokeAsync(StateHasChanged);
}
private void HandlePriceUpdate(string symbol, MarketPrice price) => InvokeAsync(() => { UpdateStats(); StateHasChanged(); });
private void HandleSettingsChanged() => InvokeAsync(StateHasChanged);
public void Dispose()
{
BotService.OnStatusChanged -= HandleUpdate;
BotService.OnTradeExecuted -= HandleTradeExecuted;
BotService.OnPriceUpdated -= HandlePriceUpdate;
SettingsService.OnSettingsChanged -= HandleSettingsChanged;
}
@@ -256,12 +256,35 @@
gap: 0.875rem !important;
}
::deep .summary-header {
margin-bottom: 0.5rem !important;
}
::deep .summary-section-title {
font-size: 0.875rem !important;
color: #6366f1 !important;
font-weight: 700 !important;
text-transform: uppercase !important;
letter-spacing: 0.05em !important;
}
::deep .summary-row {
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
}
::deep .summary-row.highlight {
padding-top: 0.5rem !important;
margin-top: 0.5rem !important;
}
::deep .summary-divider {
height: 1px !important;
background: rgba(99, 102, 241, 0.2) !important;
margin: 0.25rem 0 !important;
}
::deep .summary-title {
font-size: 0.75rem !important;
color: #64748b !important;
@@ -285,6 +308,14 @@
color: #ef4444 !important;
}
::deep .summary-amount.invested {
color: #f59e0b !important;
}
::deep .summary-amount.available {
color: #3b82f6 !important;
}
/* ==============================================
MAIN CONTENT AREA
============================================== */
@@ -466,3 +497,46 @@
padding: 1.5rem 1rem !important;
}
}
/* ==============================================
VERSION FOOTER
============================================== */
::deep .sidebar-footer {
padding: 1rem 1.5rem !important;
border-top: 1px solid rgba(99, 102, 241, 0.1) !important;
margin-top: auto !important;
}
::deep .version-info {
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
margin-bottom: 0.5rem !important;
}
::deep .version-label {
font-size: 0.75rem !important;
color: #64748b !important;
font-weight: 600 !important;
text-transform: uppercase !important;
letter-spacing: 0.05em !important;
}
::deep .version-number {
font-size: 0.875rem !important;
font-weight: 700 !important;
color: #6366f1 !important;
font-family: 'Courier New', monospace !important;
}
::deep .build-info {
display: flex !important;
justify-content: flex-start !important;
}
::deep .build-date {
font-size: 0.625rem !important;
color: #475569 !important;
font-family: 'Courier New', monospace !important;
}
@@ -0,0 +1,467 @@
@page "/indicators"
@using TradingBot.Services
@using TradingBot.Models
@inject IndicatorsService IndicatorsService
@inject TradingBotService BotService
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Indicatori - TradingBot</PageTitle>
<div class="indicators-page">
<div class="page-header">
<div>
<h1>Indicatori Tecnici</h1>
<p class="subtitle">Configura gli indicatori per analisi avanzata del mercato</p>
</div>
</div>
<!-- Indicators Grid -->
<div class="indicators-grid">
@foreach (var indicator in indicators.Values.OrderBy(i => !i.IsEnabled).ThenBy(i => i.Name))
{
<div class="indicator-card @(indicator.IsEnabled ? "enabled" : "disabled")">
<div class="indicator-header">
<div class="indicator-title">
<h3>@indicator.Name</h3>
<span class="indicator-type">@indicator.Type</span>
</div>
<label class="toggle-switch">
<input type="checkbox" checked="@indicator.IsEnabled"
@onchange="@(e => ToggleIndicator(indicator.Id, (bool)e.Value!))" />
<span class="toggle-slider"></span>
</label>
</div>
<p class="indicator-description">@indicator.Description</p>
@if (indicator.IsEnabled)
{
<div class="indicator-config">
@switch (indicator.Type)
{
case IndicatorType.RSI:
case IndicatorType.Stochastic:
<div class="config-row">
<label>Periodo:</label>
<input type="number" @bind="indicator.Period" min="5" max="50" />
</div>
<div class="config-row">
<label>Ipercomprato:</label>
<input type="number" @bind="indicator.OverboughtThreshold" min="60" max="90" step="5" />
</div>
<div class="config-row">
<label>Ipervenduto:</label>
<input type="number" @bind="indicator.OversoldThreshold" min="10" max="40" step="5" />
</div>
break;
case IndicatorType.MACD:
<div class="config-row">
<label>Fast Period:</label>
<input type="number" @bind="indicator.FastPeriod" min="8" max="20" />
</div>
<div class="config-row">
<label>Slow Period:</label>
<input type="number" @bind="indicator.SlowPeriod" min="20" max="35" />
</div>
<div class="config-row">
<label>Signal Period:</label>
<input type="number" @bind="indicator.SignalPeriod" min="5" max="15" />
</div>
break;
case IndicatorType.SMA:
case IndicatorType.EMA:
case IndicatorType.BollingerBands:
<div class="config-row">
<label>Periodo:</label>
<input type="number" @bind="indicator.Period" min="5" max="200" />
</div>
break;
}
<button class="btn-secondary btn-sm" @onclick="() => SaveIndicator(indicator)">
<span class="bi bi-check-lg"></span>
Salva
</button>
</div>
<!-- Current Status for Active Assets -->
<div class="indicator-status-section">
<h4>Status Asset Attivi</h4>
@foreach (var symbol in BotService.AssetConfigurations.Values.Where(c => c.IsEnabled).Select(c => c.Symbol))
{
var status = IndicatorsService.GetIndicatorStatus(indicator.Id, symbol);
if (status != null)
{
<div class="status-row">
<span class="status-symbol">@symbol</span>
<span class="status-value">@status.CurrentValue.ToString("F2")</span>
<span class="status-condition condition-@status.Condition.ToString().ToLower()">
@status.Condition
</span>
<span class="status-recommendation">@status.Recommendation</span>
</div>
}
}
</div>
}
</div>
}
</div>
<!-- Recent Signals -->
<div class="signals-section">
<div class="section-header">
<h2>Segnali Recenti</h2>
<span class="signals-count">@recentSignals.Count segnali</span>
</div>
@if (recentSignals.Count == 0)
{
<div class="empty-state">
<span class="bi bi-broadcast"></span>
<p>Nessun segnale generato</p>
</div>
}
else
{
<div class="signals-list">
@foreach (var signal in recentSignals.Take(20))
{
<div class="signal-card signal-@signal.Type.ToString().ToLower()">
<div class="signal-header">
<span class="signal-time">@signal.Timestamp.ToLocalTime().ToString("HH:mm:ss")</span>
<span class="signal-indicator">@signal.IndicatorName</span>
<span class="signal-symbol">@signal.Symbol</span>
<span class="signal-type type-@signal.Type.ToString().ToLower()">
@signal.Type
</span>
<span class="signal-strength strength-@signal.Strength.ToString().ToLower()">
@signal.Strength
</span>
</div>
<div class="signal-message">@signal.Message</div>
@if (signal.Value.HasValue)
{
<div class="signal-value">Valore: @signal.Value.Value.ToString("F2")</div>
}
</div>
}
</div>
}
</div>
</div>
<style>
.indicators-page {
display: flex;
flex-direction: column;
gap: 2rem;
}
.indicators-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
gap: 1.5rem;
}
.indicator-card {
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
padding: 1.5rem;
transition: all 0.3s ease;
}
.indicator-card.disabled {
opacity: 0.6;
border-color: rgba(100, 116, 139, 0.2);
}
.indicator-card.enabled {
border-color: rgba(99, 102, 241, 0.4);
}
.indicator-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.indicator-title h3 {
font-size: 1.25rem;
font-weight: 700;
color: #e2e8f0;
margin: 0 0 0.25rem 0;
}
.indicator-type {
display: inline-block;
padding: 0.25rem 0.75rem;
background: rgba(99, 102, 241, 0.2);
border-radius: 0.25rem;
color: #6366f1;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
}
.indicator-description {
color: #94a3b8;
font-size: 0.875rem;
line-height: 1.5;
margin-bottom: 1rem;
}
.indicator-config {
background: rgba(0, 0, 0, 0.2);
border-radius: 0.5rem;
padding: 1rem;
margin-top: 1rem;
}
.config-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.75rem;
}
.config-row:last-of-type {
margin-bottom: 1rem;
}
.config-row label {
font-size: 0.875rem;
color: #cbd5e1;
font-weight: 600;
}
.config-row input[type="number"] {
width: 80px;
padding: 0.5rem;
border-radius: 0.375rem;
background: #0f1629;
border: 1px solid #334155;
color: #e2e8f0;
font-size: 0.875rem;
}
.indicator-status-section {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid rgba(99, 102, 241, 0.1);
}
.indicator-status-section h4 {
font-size: 0.875rem;
color: #94a3b8;
margin-bottom: 0.75rem;
text-transform: uppercase;
font-weight: 600;
}
.status-row {
display: grid;
grid-template-columns: 60px 80px 100px 1fr;
gap: 0.5rem;
align-items: center;
padding: 0.5rem;
background: rgba(0, 0, 0, 0.2);
border-radius: 0.375rem;
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
.status-symbol {
font-weight: 700;
color: #8b5cf6;
}
.status-value {
font-family: 'Courier New', monospace;
color: #e2e8f0;
}
.status-condition {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-weight: 700;
font-size: 0.75rem;
text-align: center;
}
.condition-overbought { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
.condition-oversold { background: rgba(16, 185, 129, 0.2); color: #10b981; }
.condition-bullish { background: rgba(16, 185, 129, 0.2); color: #10b981; }
.condition-bearish { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
.condition-neutral { background: rgba(100, 116, 139, 0.2); color: #94a3b8; }
.status-recommendation {
color: #cbd5e1;
font-size: 0.75rem;
}
.signals-section {
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
padding: 1.5rem;
}
.signals-count {
color: #6366f1;
font-weight: 600;
}
.signals-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-top: 1rem;
}
.signal-card {
background: #0f1629;
border-radius: 0.5rem;
border-left: 3px solid;
padding: 1rem;
}
.signal-card.signal-buy {
border-left-color: #10b981;
}
.signal-card.signal-sell {
border-left-color: #ef4444;
}
.signal-card.signal-hold {
border-left-color: #f59e0b;
}
.signal-header {
display: flex;
gap: 0.75rem;
align-items: center;
margin-bottom: 0.5rem;
flex-wrap: wrap;
}
.signal-time {
color: #64748b;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
}
.signal-indicator {
padding: 0.25rem 0.75rem;
background: rgba(99, 102, 241, 0.2);
border-radius: 0.25rem;
color: #6366f1;
font-weight: 700;
font-size: 0.75rem;
}
.signal-symbol {
padding: 0.25rem 0.75rem;
background: rgba(139, 92, 246, 0.2);
border-radius: 0.25rem;
color: #8b5cf6;
font-weight: 700;
font-size: 0.75rem;
}
.signal-type {
padding: 0.25rem 0.75rem;
border-radius: 0.25rem;
font-weight: 700;
font-size: 0.75rem;
text-transform: uppercase;
}
.type-buy { background: rgba(16, 185, 129, 0.2); color: #10b981; }
.type-sell { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
.type-hold { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
.signal-strength {
padding: 0.25rem 0.75rem;
background: rgba(59, 130, 246, 0.2);
border-radius: 0.25rem;
color: #3b82f6;
font-weight: 600;
font-size: 0.75rem;
}
.signal-message {
color: #e2e8f0;
margin-bottom: 0.25rem;
}
.signal-value {
color: #94a3b8;
font-size: 0.875rem;
font-family: 'Courier New', monospace;
}
.empty-state {
text-align: center;
padding: 3rem;
color: #64748b;
}
.empty-state .bi {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
</style>
@code {
private Dictionary<string, IndicatorConfig> indicators = new();
private List<IndicatorSignal> recentSignals = new();
protected override void OnInitialized()
{
LoadIndicators();
IndicatorsService.OnIndicatorsChanged += HandleIndicatorsChanged;
IndicatorsService.OnSignalGenerated += HandleSignalGenerated;
}
private void LoadIndicators()
{
indicators = IndicatorsService.GetIndicators().ToDictionary(k => k.Key, v => v.Value);
recentSignals = IndicatorsService.GetRecentSignals().ToList();
}
private void ToggleIndicator(string id, bool enabled)
{
IndicatorsService.ToggleIndicator(id, enabled);
}
private void SaveIndicator(IndicatorConfig indicator)
{
IndicatorsService.UpdateIndicator(indicator.Id, indicator);
}
private void HandleIndicatorsChanged()
{
LoadIndicators();
InvokeAsync(StateHasChanged);
}
private void HandleSignalGenerated(IndicatorSignal signal)
{
recentSignals = IndicatorsService.GetRecentSignals().ToList();
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
IndicatorsService.OnIndicatorsChanged -= HandleIndicatorsChanged;
IndicatorsService.OnSignalGenerated -= HandleSignalGenerated;
}
}
+398
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;
}
}
+716
View File
@@ -0,0 +1,716 @@
@page "/positions"
@using TradingBot.Services
@using TradingBot.Models
@inject TradingBotService BotService
@inject LoggingService LoggingService
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Posizioni Aperte - TradingBot</PageTitle>
<div class="positions-page">
<div class="page-header">
<div>
<h1>Posizioni Aperte</h1>
<p class="subtitle">Gestisci le tue posizioni attive - Solo chiusura manuale disponibile</p>
</div>
<div class="header-stats">
<div class="stat-card">
<span class="stat-label">Posizioni Attive</span>
<span class="stat-value">@activePositions.Count</span>
</div>
<div class="stat-card">
<span class="stat-label">Valore Totale</span>
<span class="stat-value">$@totalValue.ToString("N2")</span>
</div>
<div class="stat-card">
<span class="stat-label">P&L Non Realizzato</span>
<span class="stat-value @(totalUnrealizedPL >= 0 ? "profit" : "loss")">
@(totalUnrealizedPL >= 0 ? "+" : "")$@totalUnrealizedPL.ToString("N2")
</span>
</div>
</div>
</div>
@if (activePositions.Count == 0)
{
<div class="empty-state">
<div class="empty-icon">
<span class="bi bi-inbox"></span>
</div>
<h3>Nessuna Posizione Aperta</h3>
<p>Non hai posizioni attive al momento. Le posizioni appaiono qui automaticamente quando il bot esegue un acquisto.</p>
<div class="empty-actions">
<a href="/trading-control" class="btn-primary">
<span class="bi bi-sliders"></span>
Configura Trading
</a>
<a href="/dashboard" class="btn-secondary">
<span class="bi bi-speedometer2"></span>
Dashboard
</a>
</div>
</div>
}
else
{
<div class="positions-grid">
@foreach (var position in activePositions.OrderBy(p => p.Symbol))
{
var currentPrice = GetCurrentPrice(position.Symbol);
var unrealizedPL = CalculateUnrealizedPL(position, currentPrice);
var plPercentage = CalculatePLPercentage(position, currentPrice);
var currentValue = position.Amount * currentPrice;
var holdingTime = DateTime.UtcNow - position.Timestamp;
<div class="position-card">
<div class="position-header">
<div class="position-asset">
<h3>@position.Symbol</h3>
<span class="position-date">
Aperta @position.Timestamp.ToLocalTime().ToString("dd/MM/yyyy HH:mm")
</span>
</div>
<div class="position-pl @(unrealizedPL >= 0 ? "profit" : "loss")">
<span class="pl-value">
@(unrealizedPL >= 0 ? "+" : "")$@unrealizedPL.ToString("N2")
</span>
<span class="pl-percentage">
(@(plPercentage >= 0 ? "+" : "")@plPercentage.ToString("F2")%)
</span>
</div>
</div>
<div class="position-details">
<div class="detail-row">
<span class="detail-label">Quantità</span>
<span class="detail-value">@position.Amount.ToString("F8") @position.Symbol</span>
</div>
<div class="detail-row">
<span class="detail-label">Prezzo Entrata</span>
<span class="detail-value">$@position.Price.ToString("N2")</span>
</div>
<div class="detail-row">
<span class="detail-label">Prezzo Corrente</span>
<span class="detail-value">$@currentPrice.ToString("N2")</span>
</div>
<div class="detail-row">
<span class="detail-label">Valore Iniziale</span>
<span class="detail-value">$@(position.Amount * position.Price).ToString("N2")</span>
</div>
<div class="detail-row">
<span class="detail-label">Valore Corrente</span>
<span class="detail-value">$@currentValue.ToString("N2")</span>
</div>
<div class="detail-row">
<span class="detail-label">Tempo Holding</span>
<span class="detail-value">@FormatHoldingTime(holdingTime)</span>
</div>
@if (!string.IsNullOrEmpty(position.Strategy))
{
<div class="detail-row">
<span class="detail-label">Strategia</span>
<span class="detail-value strategy-badge">@position.Strategy</span>
</div>
}
</div>
<div class="position-actions">
<button class="btn-danger" @onclick="() => ShowCloseConfirmation(position)"
disabled="@(!BotService.Status.IsRunning)">
<span class="bi bi-x-circle"></span>
Chiudi Posizione
</button>
@if (!BotService.Status.IsRunning)
{
<span class="action-note">
<span class="bi bi-info-circle"></span>
Avvia il bot per chiudere posizioni
</span>
}
</div>
</div>
}
</div>
}
<!-- Close Confirmation Modal -->
@if (showCloseModal && positionToClose != null)
{
var currentPrice = GetCurrentPrice(positionToClose.Symbol);
var unrealizedPL = CalculateUnrealizedPL(positionToClose, currentPrice);
var plPercentage = CalculatePLPercentage(positionToClose, currentPrice);
<div class="modal-overlay" @onclick="HideCloseConfirmation">
<div class="modal-dialog" @onclick:stopPropagation="true">
<div class="modal-header">
<h3>Conferma Chiusura Posizione</h3>
<button class="btn-close" @onclick="HideCloseConfirmation">×</button>
</div>
<div class="modal-body">
<div class="confirmation-details">
<div class="confirm-asset">
<h4>@positionToClose.Symbol</h4>
<span class="confirm-amount">@positionToClose.Amount.ToString("F8") @positionToClose.Symbol</span>
</div>
<div class="confirm-prices">
<div class="price-item">
<span class="price-label">Prezzo Entrata</span>
<span class="price-value">$@positionToClose.Price.ToString("N2")</span>
</div>
<span class="bi bi-arrow-right"></span>
<div class="price-item">
<span class="price-label">Prezzo Chiusura</span>
<span class="price-value">$@currentPrice.ToString("N2")</span>
</div>
</div>
<div class="confirm-pl @(unrealizedPL >= 0 ? "profit" : "loss")">
<div class="pl-label">Profitto/Perdita Stimato</div>
<div class="pl-amount">
@(unrealizedPL >= 0 ? "+" : "")$@unrealizedPL.ToString("N2")
</div>
<div class="pl-percent">
(@(plPercentage >= 0 ? "+" : "")@plPercentage.ToString("F2")%)
</div>
</div>
<div class="confirm-warning">
<span class="bi bi-exclamation-triangle"></span>
<p>
<strong>Attenzione:</strong> Questa azione chiuderà immediatamente la posizione al prezzo di mercato corrente.
L'operazione non può essere annullata.
</p>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" @onclick="HideCloseConfirmation">Annulla</button>
<button class="btn-danger" @onclick="ConfirmClosePosition">
<span class="bi bi-x-circle"></span>
Conferma Chiusura
</button>
</div>
</div>
</div>
}
<!-- Success Notification -->
@if (showSuccessNotification)
{
<div class="notification success">
<span class="bi bi-check-circle-fill"></span>
Posizione chiusa con successo!
</div>
}
</div>
<style>
.positions-page {
display: flex;
flex-direction: column;
gap: 2rem;
}
.header-stats {
display: flex;
gap: 1.5rem;
}
.stat-card {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem 1.5rem;
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
}
.stat-label {
font-size: 0.875rem;
color: #94a3b8;
font-weight: 600;
text-transform: uppercase;
}
.stat-value {
font-size: 1.75rem;
font-weight: 700;
color: #e2e8f0;
font-family: 'Courier New', monospace;
}
.stat-value.profit {
color: #10b981;
}
.stat-value.loss {
color: #ef4444;
}
.empty-state {
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
padding: 4rem 2rem;
text-align: center;
}
.empty-icon {
font-size: 4rem;
color: #64748b;
margin-bottom: 1.5rem;
}
.empty-state h3 {
color: #e2e8f0;
margin-bottom: 0.75rem;
}
.empty-state p {
color: #94a3b8;
margin-bottom: 2rem;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.empty-actions {
display: flex;
gap: 1rem;
justify-content: center;
}
.positions-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
gap: 1.5rem;
}
.position-card {
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
transition: all 0.3s ease;
}
.position-card:hover {
border-color: rgba(99, 102, 241, 0.4);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.position-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding-bottom: 1rem;
border-bottom: 1px solid rgba(99, 102, 241, 0.1);
}
.position-asset h3 {
font-size: 1.5rem;
font-weight: 700;
color: #e2e8f0;
margin: 0 0 0.25rem 0;
}
.position-date {
font-size: 0.75rem;
color: #64748b;
}
.position-pl {
text-align: right;
}
.pl-value {
display: block;
font-size: 1.25rem;
font-weight: 700;
font-family: 'Courier New', monospace;
}
.position-pl.profit .pl-value {
color: #10b981;
}
.position-pl.loss .pl-value {
color: #ef4444;
}
.pl-percentage {
font-size: 0.875rem;
opacity: 0.8;
}
.position-details {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.detail-label {
font-size: 0.875rem;
color: #94a3b8;
font-weight: 600;
}
.detail-value {
font-size: 0.875rem;
color: #e2e8f0;
font-family: 'Courier New', monospace;
font-weight: 600;
}
.strategy-badge {
padding: 0.25rem 0.75rem;
background: rgba(99, 102, 241, 0.2);
border-radius: 0.25rem;
color: #6366f1;
font-family: inherit;
}
.position-actions {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding-top: 1rem;
border-top: 1px solid rgba(99, 102, 241, 0.1);
}
.action-note {
font-size: 0.75rem;
color: #f59e0b;
display: flex;
align-items: center;
gap: 0.5rem;
}
/* Modal Styles */
.confirmation-details {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.confirm-asset {
text-align: center;
padding: 1rem;
background: rgba(99, 102, 241, 0.1);
border-radius: 0.5rem;
}
.confirm-asset h4 {
font-size: 1.5rem;
color: #e2e8f0;
margin: 0 0 0.5rem 0;
}
.confirm-amount {
color: #94a3b8;
font-family: 'Courier New', monospace;
}
.confirm-prices {
display: flex;
justify-content: space-around;
align-items: center;
padding: 1rem;
background: rgba(0, 0, 0, 0.2);
border-radius: 0.5rem;
}
.price-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
text-align: center;
}
.price-label {
font-size: 0.75rem;
color: #64748b;
text-transform: uppercase;
font-weight: 600;
}
.price-value {
font-size: 1.25rem;
color: #e2e8f0;
font-weight: 700;
font-family: 'Courier New', monospace;
}
.confirm-prices .bi {
font-size: 1.5rem;
color: #6366f1;
}
.confirm-pl {
text-align: center;
padding: 1.5rem;
border-radius: 0.5rem;
}
.confirm-pl.profit {
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.3);
}
.confirm-pl.loss {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
}
.pl-label {
font-size: 0.875rem;
color: #94a3b8;
margin-bottom: 0.5rem;
text-transform: uppercase;
font-weight: 600;
}
.pl-amount {
font-size: 2rem;
font-weight: 700;
font-family: 'Courier New', monospace;
margin-bottom: 0.25rem;
}
.confirm-pl.profit .pl-amount {
color: #10b981;
}
.confirm-pl.loss .pl-amount {
color: #ef4444;
}
.pl-percent {
font-size: 1rem;
opacity: 0.8;
}
.confirm-warning {
display: flex;
gap: 1rem;
padding: 1rem;
background: rgba(245, 158, 11, 0.1);
border: 1px solid rgba(245, 158, 11, 0.3);
border-radius: 0.5rem;
align-items: flex-start;
}
.confirm-warning .bi {
font-size: 1.25rem;
color: #f59e0b;
flex-shrink: 0;
}
.confirm-warning p {
margin: 0;
font-size: 0.875rem;
color: #cbd5e1;
line-height: 1.5;
}
.confirm-warning strong {
color: #f59e0b;
}
.notification {
position: fixed;
bottom: 2rem;
right: 2rem;
padding: 1rem 1.5rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 600;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
z-index: 9999;
animation: slideInRight 0.3s ease;
}
.notification.success {
background: rgba(16, 185, 129, 0.2);
border: 1px solid #10b981;
color: #10b981;
}
@@keyframes slideInRight {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@@media (max-width: 768px) {
.positions-grid {
grid-template-columns: 1fr;
}
.header-stats {
flex-direction: column;
}
.confirm-prices {
flex-direction: column;
gap: 1rem;
}
.confirm-prices .bi {
transform: rotate(90deg);
}
}
</style>
@code {
private List<Trade> activePositions = new();
private decimal totalValue = 0;
private decimal totalUnrealizedPL = 0;
private bool showCloseModal = false;
private Trade? positionToClose = null;
private bool showSuccessNotification = false;
protected override void OnInitialized()
{
LoadPositions();
BotService.OnTradeExecuted += HandleTradeExecuted;
BotService.OnPriceUpdated += HandlePriceUpdated;
BotService.OnStatusChanged += HandleStatusChanged;
}
private void LoadPositions()
{
activePositions = BotService.ActivePositions.Values.ToList();
CalculateTotals();
}
private void CalculateTotals()
{
totalValue = 0;
totalUnrealizedPL = 0;
foreach (var position in activePositions)
{
var currentPrice = GetCurrentPrice(position.Symbol);
var positionValue = position.Amount * currentPrice;
var pl = CalculateUnrealizedPL(position, currentPrice);
totalValue += positionValue;
totalUnrealizedPL += pl;
}
}
private decimal GetCurrentPrice(string symbol)
{
var latestPrice = BotService.GetLatestPrice(symbol);
return latestPrice?.Price ?? 0;
}
private decimal CalculateUnrealizedPL(Trade position, decimal currentPrice)
{
return (currentPrice - position.Price) * position.Amount;
}
private decimal CalculatePLPercentage(Trade position, decimal currentPrice)
{
if (position.Price == 0) return 0;
return ((currentPrice - position.Price) / position.Price) * 100;
}
private string FormatHoldingTime(TimeSpan time)
{
if (time.TotalDays >= 1)
return $"{(int)time.TotalDays}g {time.Hours}h";
else if (time.TotalHours >= 1)
return $"{(int)time.TotalHours}h {time.Minutes}m";
else
return $"{(int)time.TotalMinutes}m {time.Seconds}s";
}
private void ShowCloseConfirmation(Trade position)
{
positionToClose = position;
showCloseModal = true;
}
private void HideCloseConfirmation()
{
showCloseModal = false;
positionToClose = null;
}
private async Task ConfirmClosePosition()
{
if (positionToClose == null) return;
try
{
// Close position using TradingBotService public method
await BotService.ClosePositionManuallyAsync(positionToClose.Symbol);
LoggingService.LogInfo(
"Positions",
$"Posizione chiusa manualmente: {positionToClose.Symbol}",
$"Quantità: {positionToClose.Amount:F8}");
showSuccessNotification = true;
HideCloseConfirmation();
LoadPositions();
// Hide notification after 3 seconds
await Task.Delay(3000);
showSuccessNotification = false;
StateHasChanged();
}
catch (Exception ex)
{
LoggingService.LogError("Positions", $"Errore chiusura posizione: {ex.Message}");
}
}
private void HandleTradeExecuted(Trade trade)
{
LoadPositions();
InvokeAsync(StateHasChanged);
}
private void HandlePriceUpdated(string symbol, MarketPrice price)
{
if (activePositions.Any(p => p.Symbol == symbol))
{
CalculateTotals();
InvokeAsync(StateHasChanged);
}
}
private void HandleStatusChanged()
{
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
BotService.OnTradeExecuted -= HandleTradeExecuted;
BotService.OnPriceUpdated -= HandlePriceUpdated;
BotService.OnStatusChanged -= HandleStatusChanged;
}
}
+118
View File
@@ -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;
}
}
@@ -0,0 +1,555 @@
@page "/trading-control"
@using TradingBot.Services
@using TradingBot.Models
@inject TradingStrategiesService StrategiesService
@inject TradingBotService BotService
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Trading Control - TradingBot</PageTitle>
<div class="trading-control-page">
<div class="page-header">
<div>
<h1>Trading Control</h1>
<p class="subtitle">Gestisci le strategie di trading per ogni asset</p>
</div>
<div class="header-stats">
<div class="stat-item">
<span class="stat-label">Asset Attivi</span>
<span class="stat-value">@activeAssets</span>
</div>
<div class="stat-item">
<span class="stat-label">Strategie in Uso</span>
<span class="stat-value">@totalStrategies</span>
</div>
</div>
</div>
<!-- Assets Grid -->
<div class="assets-control-grid">
@foreach (var asset in BotService.AssetConfigurations.Values.OrderBy(a => a.Symbol))
{
var mapping = StrategiesService.GetAssetMapping(asset.Symbol);
var engineStatus = StrategiesService.GetEngineStatus(asset.Symbol);
var isActive = mapping?.IsActive ?? false;
<div class="asset-control-card @(isActive ? "active" : "")">
<div class="asset-header">
<div class="asset-info">
<h3>@asset.Symbol</h3>
<span class="asset-name">@asset.Name</span>
</div>
<div class="asset-controls">
<label class="toggle-switch">
<input type="checkbox"
checked="@isActive"
@onchange="(e) => ToggleAsset(asset.Symbol, (bool)e.Value!)"
disabled="@((mapping?.StrategyIds.Count ?? 0) == 0)" />
<span class="toggle-slider"></span>
</label>
</div>
</div>
@if (mapping != null && mapping.StrategyIds.Count > 0)
{
<div class="assigned-strategies">
<h4>Strategie Assegnate (@mapping.StrategyIds.Count)</h4>
<div class="strategies-list">
@foreach (var strategyId in mapping.StrategyIds)
{
var strategyInfo = availableStrategies[strategyId];
<div class="strategy-badge">
<span class="strategy-name">@strategyInfo.Name</span>
<span class="strategy-category">@strategyInfo.Category</span>
<button class="btn-remove" @onclick="() => RemoveStrategy(asset.Symbol, strategyId)">
<span class="bi bi-x"></span>
</button>
</div>
}
</div>
</div>
@if (engineStatus?.LastDecision != null && isActive)
{
<div class="last-decision">
<h4>Ultima Decisione</h4>
<div class="decision-info">
<span class="decision-type type-@engineStatus.LastDecision.Decision.ToString().ToLower()">
@engineStatus.LastDecision.Decision
</span>
<span class="decision-confidence">
Confidenza: @engineStatus.LastDecision.Confidence.ToString("F0")%
</span>
</div>
<div class="decision-reason">@engineStatus.LastDecision.Reason</div>
<div class="decision-votes">
<span class="vote buy">Buy: @engineStatus.LastDecision.BuyVotes</span>
<span class="vote sell">Sell: @engineStatus.LastDecision.SellVotes</span>
<span class="vote hold">Hold: @engineStatus.LastDecision.HoldVotes</span>
</div>
</div>
}
}
<div class="asset-actions">
<button class="btn-primary" @onclick="() => OpenStrategySelector(asset.Symbol, asset.Name)">
<span class="bi bi-plus-lg"></span>
@((mapping?.StrategyIds.Count ?? 0) > 0 ? "Gestisci Strategie" : "Assegna Strategie")
</button>
</div>
</div>
}
</div>
<!-- Strategy Selector Modal -->
@if (showStrategySelector)
{
<div class="modal-overlay" @onclick="CloseStrategySelector">
<div class="modal-dialog large" @onclick:stopPropagation="true">
<div class="modal-header">
<h3>Gestisci Strategie - @selectedAssetSymbol</h3>
<button class="btn-close" @onclick="CloseStrategySelector">×</button>
</div>
<div class="modal-body">
<div class="strategies-selector">
@foreach (var category in availableStrategies.Values.Select(s => s.Category).Distinct().OrderBy(c => c))
{
<div class="category-section">
<h4 class="category-title">@category</h4>
<div class="category-strategies">
@foreach (var strategy in availableStrategies.Values.Where(s => s.Category == category))
{
var isSelected = selectedStrategies.Contains(strategy.Id);
<div class="strategy-option @(isSelected ? "selected" : "")"
@onclick="() => ToggleStrategy(strategy.Id)">
<div class="strategy-option-header">
<div class="strategy-checkbox">
<input type="checkbox" checked="@isSelected" />
</div>
<div class="strategy-details">
<h5>@strategy.Name</h5>
<p>@strategy.Description</p>
</div>
</div>
<div class="strategy-meta">
<span class="risk-badge risk-@strategy.RiskLevel.ToString().ToLower()">
@strategy.RiskLevel Risk
</span>
<span class="timeframe-badge">
@strategy.RecommendedTimeFrame
</span>
</div>
</div>
}
</div>
</div>
}
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" @onclick="CloseStrategySelector">Annulla</button>
<button class="btn-primary" @onclick="SaveStrategies">
<span class="bi bi-check-lg"></span>
Salva (@selectedStrategies.Count strategie)
</button>
</div>
</div>
</div>
}
</div>
<style>
.trading-control-page {
display: flex;
flex-direction: column;
gap: 2rem;
}
.header-stats {
display: flex;
gap: 2rem;
}
.stat-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.stat-label {
font-size: 0.875rem;
color: #94a3b8;
font-weight: 600;
}
.stat-value {
font-size: 1.5rem;
font-weight: 700;
color: #6366f1;
}
.assets-control-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 1.5rem;
}
.asset-control-card {
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
padding: 1.5rem;
transition: all 0.3s ease;
}
.asset-control-card.active {
border-color: rgba(16, 185, 129, 0.5);
background: linear-gradient(135deg, #1a1f3a 0%, rgba(16, 185, 129, 0.05) 100%);
}
.asset-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1.5rem;
}
.asset-info h3 {
font-size: 1.5rem;
font-weight: 700;
color: #e2e8f0;
margin: 0 0 0.25rem 0;
}
.asset-name {
color: #94a3b8;
font-size: 0.875rem;
}
.assigned-strategies {
margin-bottom: 1.5rem;
}
.assigned-strategies h4 {
font-size: 0.875rem;
color: #94a3b8;
margin-bottom: 0.75rem;
text-transform: uppercase;
font-weight: 600;
}
.strategies-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.strategy-badge {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem;
background: rgba(0, 0, 0, 0.3);
border-radius: 0.5rem;
border: 1px solid rgba(99, 102, 241, 0.2);
}
.strategy-name {
flex: 1;
font-weight: 600;
color: #e2e8f0;
}
.strategy-category {
padding: 0.25rem 0.75rem;
background: rgba(99, 102, 241, 0.2);
border-radius: 0.25rem;
color: #6366f1;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
}
.btn-remove {
background: none;
border: none;
color: #ef4444;
cursor: pointer;
padding: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
transition: background 0.2s;
}
.btn-remove:hover {
background: rgba(239, 68, 68, 0.2);
}
.last-decision {
background: rgba(0, 0, 0, 0.3);
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
}
.last-decision h4 {
font-size: 0.875rem;
color: #94a3b8;
margin-bottom: 0.75rem;
text-transform: uppercase;
font-weight: 600;
}
.decision-info {
display: flex;
gap: 1rem;
align-items: center;
margin-bottom: 0.75rem;
}
.decision-type {
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 700;
text-transform: uppercase;
font-size: 0.875rem;
}
.type-buy { background: rgba(16, 185, 129, 0.2); color: #10b981; }
.type-sell { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
.type-hold { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
.decision-confidence {
color: #94a3b8;
font-size: 0.875rem;
}
.decision-reason {
color: #cbd5e1;
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.decision-votes {
display: flex;
gap: 1rem;
}
.vote {
padding: 0.25rem 0.75rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 700;
}
.vote.buy { background: rgba(16, 185, 129, 0.1); color: #10b981; }
.vote.sell { background: rgba(239, 68, 68, 0.1); color: #ef4444; }
.vote.hold { background: rgba(245, 158, 11, 0.1); color: #f59e0b; }
.asset-actions {
display: flex;
gap: 0.75rem;
}
/* Modal Styles */
.modal-dialog.large {
max-width: 900px;
max-height: 80vh;
}
.modal-body {
max-height: 60vh;
overflow-y: auto;
}
.strategies-selector {
display: flex;
flex-direction: column;
gap: 2rem;
}
.category-section {
border-bottom: 1px solid rgba(99, 102, 241, 0.1);
padding-bottom: 1.5rem;
}
.category-section:last-child {
border-bottom: none;
}
.category-title {
font-size: 1rem;
font-weight: 700;
color: #6366f1;
margin-bottom: 1rem;
text-transform: uppercase;
}
.category-strategies {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.strategy-option {
background: rgba(0, 0, 0, 0.3);
border: 2px solid rgba(99, 102, 241, 0.2);
border-radius: 0.5rem;
padding: 1rem;
cursor: pointer;
transition: all 0.2s ease;
}
.strategy-option:hover {
border-color: rgba(99, 102, 241, 0.5);
background: rgba(99, 102, 241, 0.05);
}
.strategy-option.selected {
border-color: #6366f1;
background: rgba(99, 102, 241, 0.1);
}
.strategy-option-header {
display: flex;
gap: 1rem;
margin-bottom: 0.75rem;
}
.strategy-checkbox input {
width: 20px;
height: 20px;
cursor: pointer;
}
.strategy-details h5 {
font-size: 1rem;
font-weight: 700;
color: #e2e8f0;
margin: 0 0 0.25rem 0;
}
.strategy-details p {
font-size: 0.875rem;
color: #94a3b8;
margin: 0;
line-height: 1.5;
}
.strategy-meta {
display: flex;
gap: 0.75rem;
}
.risk-badge, .timeframe-badge {
padding: 0.25rem 0.75rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
}
.risk-low { background: rgba(16, 185, 129, 0.2); color: #10b981; }
.risk-medium { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
.risk-high { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
.risk-veryhigh { background: rgba(220, 38, 38, 0.3); color: #dc2626; }
.timeframe-badge {
background: rgba(139, 92, 246, 0.2);
color: #8b5cf6;
}
</style>
@code {
private Dictionary<string, StrategyInfo> availableStrategies = new();
private bool showStrategySelector = false;
private string selectedAssetSymbol = "";
private string selectedAssetName = "";
private List<string> selectedStrategies = new();
private int activeAssets = 0;
private int totalStrategies = 0;
protected override void OnInitialized()
{
LoadData();
StrategiesService.OnMappingsChanged += HandleMappingsChanged;
}
private void LoadData()
{
availableStrategies = StrategiesService.GetAvailableStrategies().ToDictionary(k => k.Key, v => v.Value);
var mappings = StrategiesService.GetAllMappings();
activeAssets = mappings.Values.Count(m => m.IsActive);
totalStrategies = mappings.Values.Sum(m => m.StrategyIds.Count);
}
private void ToggleAsset(string symbol, bool active)
{
if (active)
{
StrategiesService.ActivateAsset(symbol);
}
else
{
StrategiesService.DeactivateAsset(symbol);
}
LoadData();
}
private void OpenStrategySelector(string symbol, string name)
{
selectedAssetSymbol = symbol;
selectedAssetName = name;
var mapping = StrategiesService.GetAssetMapping(symbol);
selectedStrategies = mapping?.StrategyIds.ToList() ?? new List<string>();
showStrategySelector = true;
}
private void CloseStrategySelector()
{
showStrategySelector = false;
selectedStrategies.Clear();
}
private void ToggleStrategy(string strategyId)
{
if (selectedStrategies.Contains(strategyId))
{
selectedStrategies.Remove(strategyId);
}
else
{
selectedStrategies.Add(strategyId);
}
}
private void SaveStrategies()
{
StrategiesService.AssignStrategiesToAsset(selectedAssetSymbol, selectedAssetName, selectedStrategies);
CloseStrategySelector();
LoadData();
}
private void RemoveStrategy(string symbol, string strategyId)
{
StrategiesService.RemoveStrategyFromAsset(symbol, strategyId);
LoadData();
}
private void HandleMappingsChanged()
{
LoadData();
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
StrategiesService.OnMappingsChanged -= HandleMappingsChanged;
}
}
@@ -0,0 +1,220 @@
@using TradingBot.Services
@using TradingBot.Models
@inject IndicatorsService IndicatorsService
@inject TradingBotService BotService
@implements IDisposable
<div class="indicators-widget">
<div class="widget-header">
<h3>Indicatori Attivi</h3>
<a href="/indicators" class="btn-link">
Configura <span class="bi bi-arrow-right"></span>
</a>
</div>
<div class="indicators-grid">
@foreach (var indicator in enabledIndicators.Take(6))
{
<div class="indicator-mini-card">
<div class="indicator-mini-header">
<span class="indicator-mini-name">@indicator.Name</span>
<span class="indicator-mini-type">@indicator.Type</span>
</div>
@if (topAssets.Any())
{
var symbol = topAssets.First();
var status = IndicatorsService.GetIndicatorStatus(indicator.Id, symbol);
if (status != null)
{
<div class="indicator-mini-value">
<span class="value-number">@status.CurrentValue.ToString("F2")</span>
<span class="value-condition condition-@status.Condition.ToString().ToLower()">
@status.Condition
</span>
</div>
<div class="indicator-mini-recommendation">
@status.Recommendation
</div>
}
else
{
<div class="indicator-mini-loading">Calcolo...</div>
}
}
else
{
<div class="indicator-mini-empty">Nessun asset attivo</div>
}
</div>
}
</div>
@if (enabledIndicators.Count() > 6)
{
<div class="indicators-more">
<a href="/indicators" class="btn-secondary btn-sm">
Vedi tutti (@enabledIndicators.Count()) <span class="bi bi-arrow-right"></span>
</a>
</div>
}
</div>
<style>
.indicators-widget {
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
padding: 1.5rem;
}
.widget-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.widget-header h3 {
font-size: 1.125rem;
font-weight: 700;
color: #e2e8f0;
margin: 0;
}
.btn-link {
display: inline-flex;
align-items: center;
gap: 0.25rem;
color: #6366f1;
font-size: 0.875rem;
font-weight: 600;
text-decoration: none;
transition: color 0.2s;
}
.btn-link:hover {
color: #8b5cf6;
}
.indicators-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 1rem;
}
.indicator-mini-card {
background: #0f1629;
border-radius: 0.5rem;
padding: 1rem;
border: 1px solid rgba(99, 102, 241, 0.1);
}
.indicator-mini-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.indicator-mini-name {
font-weight: 700;
color: #e2e8f0;
font-size: 0.875rem;
}
.indicator-mini-type {
padding: 0.125rem 0.5rem;
background: rgba(99, 102, 241, 0.2);
border-radius: 0.25rem;
color: #6366f1;
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
}
.indicator-mini-value {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.value-number {
font-size: 1.25rem;
font-weight: 700;
color: #e2e8f0;
font-family: 'Courier New', monospace;
}
.value-condition {
padding: 0.125rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
}
.condition-overbought { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
.condition-oversold { background: rgba(16, 185, 129, 0.2); color: #10b981; }
.condition-bullish { background: rgba(16, 185, 129, 0.2); color: #10b981; }
.condition-bearish { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
.condition-neutral { background: rgba(100, 116, 139, 0.2); color: #94a3b8; }
.condition-ranging { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
.condition-trending { background: rgba(59, 130, 246, 0.2); color: #3b82f6; }
.indicator-mini-recommendation {
color: #94a3b8;
font-size: 0.75rem;
line-height: 1.4;
}
.indicator-mini-loading,
.indicator-mini-empty {
color: #64748b;
font-size: 0.875rem;
text-align: center;
padding: 1rem 0;
}
.indicators-more {
margin-top: 1rem;
text-align: center;
}
</style>
@code {
private List<IndicatorConfig> enabledIndicators = new();
private List<string> topAssets = new();
protected override void OnInitialized()
{
LoadData();
IndicatorsService.OnIndicatorsChanged += HandleUpdate;
BotService.OnStatusChanged += HandleUpdate;
}
private void LoadData()
{
enabledIndicators = IndicatorsService.GetEnabledIndicators().ToList();
topAssets = BotService.AssetConfigurations.Values
.Where(c => c.IsEnabled)
.OrderByDescending(c => c.CurrentBalance + (c.CurrentHoldings * (BotService.GetLatestPrice(c.Symbol)?.Price ?? 0)))
.Select(c => c.Symbol)
.Take(1)
.ToList();
}
private void HandleUpdate()
{
LoadData();
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
IndicatorsService.OnIndicatorsChanged -= HandleUpdate;
BotService.OnStatusChanged -= HandleUpdate;
}
}
-141
View File
@@ -1,141 +0,0 @@
# ?? QUICK START - Docker Deployment
## Per Sviluppo Locale
### Windows
```powershell
# Build
.\build-docker.bat
# Run
docker-compose up -d
# Logs
docker-compose logs -f
# Stop
docker-compose down
```
### Linux/Mac
```sh
# Build
chmod +x build-docker.sh
./build-docker.sh
# Run
docker-compose up -d
# Logs
docker-compose logs -f
# Stop
docker-compose down
```
### Accesso
```
http://localhost:8080
```
---
## Per Unraid (via Portainer)
### 1. Setup Git Repository
```sh
git add .
git commit -m "Docker ready"
git push origin main
```
### 2. Deploy su Portainer
1. Stacks ? Add stack
2. Name: `tradingbot`
3. Git Repository: `https://192.168.30.23/Alby96/Encelado`
4. Compose path: `TradingBot/docker-compose.yml`
5. Deploy
### 3. Accesso
```
http://[UNRAID-IP]:8080
```
---
## Per Unraid (via SSH)
```sh
# SSH
ssh root@[UNRAID-IP]
# Clone
cd /mnt/user/appdata
git clone https://192.168.30.23/Alby96/Encelado.git tradingbot
cd tradingbot/TradingBot
# Deploy
docker-compose up -d
# Check
docker ps | grep tradingbot
```
---
## Comandi Utili
```sh
# Status
docker ps
# Logs
docker logs tradingbot -f
# Restart
docker restart tradingbot
# Update
git pull && docker-compose up -d --build
# Remove
docker-compose down -v
```
---
## Environment Variables (opzionali)
Crea file `.env`:
```env
TZ=Europe/Rome
ASPNETCORE_ENVIRONMENT=Production
TRADINGBOT_AUTOSTART=true
```
---
## Troubleshooting
### Container non parte
```sh
docker logs tradingbot
```
### Porta già usata
```sh
# Cambia porta in docker-compose.yml
ports:
- "8081:8080"
```
### Rebuild da zero
```sh
docker-compose down -v
docker-compose build --no-cache
docker-compose up -d
```
---
Documentazione completa: [UNRAID_DEPLOYMENT.md](UNRAID_DEPLOYMENT.md)
+24 -48
View File
@@ -1,54 +1,30 @@
# Dockerfile per TradingBot - Multi-stage build ottimizzato
# Stage 1: Build
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
# Vedere https://aka.ms/customizecontainer per informazioni su come personalizzare il contenitore di debug e su come Visual Studio usa questo Dockerfile per compilare le immagini per un debug più rapido.
# Copy csproj e restore dipendenze (layer caching)
COPY ["TradingBot.csproj", "./"]
RUN dotnet restore "TradingBot.csproj"
# Copy tutto il codice sorgente
COPY . .
# Build in Release mode
RUN dotnet build "TradingBot.csproj" -c Release -o /app/build
# Stage 2: Publish
FROM build AS publish
RUN dotnet publish "TradingBot.csproj" -c Release -o /app/publish /p:UseAppHost=false
# Stage 3: Runtime
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
# Questa fase viene usata durante l'esecuzione da Visual Studio in modalità rapida (impostazione predefinita per la configurazione di debug)
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
USER $APP_UID
WORKDIR /app
# Crea utente non-root per sicurezza
RUN useradd -m -u 1000 tradingbot && \
chown -R tradingbot:tradingbot /app
# Esponi porta
EXPOSE 8080
EXPOSE 8081
# Copy published app
# Questa fase viene usata per compilare il progetto di servizio
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["TradingBot.csproj", "."]
RUN dotnet restore "./TradingBot.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "./TradingBot.csproj" -c $BUILD_CONFIGURATION -o /app/build
# Questa fase viene usata per pubblicare il progetto di servizio da copiare nella fase finale
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./TradingBot.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
# Questa fase viene usata nell'ambiente di produzione o durante l'esecuzione da Visual Studio in modalità normale (impostazione predefinita quando non si usa la configurazione di debug)
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
# Crea directory per persistenza dati
RUN mkdir -p /app/data && \
chown -R tradingbot:tradingbot /app/data
# Volume per dati persistenti
VOLUME ["/app/data"]
# Switch a utente non-root
USER tradingbot
# Environment variables
ENV ASPNETCORE_URLS=http://+:8080
ENV ASPNETCORE_ENVIRONMENT=Production
ENV DOTNET_RUNNING_IN_CONTAINER=true
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# Entry point
ENTRYPOINT ["dotnet", "TradingBot.dll"]
+59
View File
@@ -0,0 +1,59 @@
# Dockerfile per TradingBot - Multi-stage build ottimizzato
# Stage 1: Build
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
# Copy csproj e restore dipendenze (layer caching)
COPY ["TradingBot.csproj", "./"]
RUN dotnet restore "TradingBot.csproj"
# Copy tutto il codice sorgente
COPY . .
# Build in Release mode
RUN dotnet build "TradingBot.csproj" -c Release -o /app/build
# Stage 2: Publish
FROM build AS publish
RUN dotnet publish "TradingBot.csproj" -c Release -o /app/publish /p:UseAppHost=false
# Stage 3: Runtime
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
# Installa wget per health check (curl non disponibile nell'immagine base)
RUN apt-get update && apt-get install -y wget && rm -rf /var/lib/apt/lists/*
# Crea utente non-root per sicurezza
# Usa UID 1001 invece di 1000 (1000 spesso già in uso nell'immagine base)
RUN groupadd -r -g 1001 tradingbot && \
useradd -r -u 1001 -g tradingbot -m -s /bin/bash tradingbot && \
chown -R tradingbot:tradingbot /app
# Esponi porta
EXPOSE 8080
# Copy published app
COPY --from=publish /app/publish .
# Crea directory per persistenza dati
RUN mkdir -p /app/data && \
chown -R tradingbot:tradingbot /app/data
# Volume per dati persistenti
VOLUME ["/app/data"]
# Switch a utente non-root
USER tradingbot
# Environment variables
ENV ASPNETCORE_URLS=http://+:8080
ENV ASPNETCORE_ENVIRONMENT=Production
ENV DOTNET_RUNNING_IN_CONTAINER=true
# Health check con wget invece di curl
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
# Entry point
ENTRYPOINT ["dotnet", "TradingBot.dll"]
-386
View File
@@ -1,386 +0,0 @@
# ? VERIFICA FINALE - TradingBot Application
## ?? CHECKLIST COMPLETA
### ?? Layout & Design
- [x] Sidebar verticale moderna implementata
- [x] Brand section con logo gradient
- [x] Status badge animato (? ATTIVO)
- [x] Menu items con icone Bootstrap
- [x] Active state highlighting
- [x] Collapsible sidebar (280px ? 80px)
- [x] Portfolio summary nel footer
- [x] Top bar con bot control
- [x] Responsive design (mobile-ready)
### ?? File Modificati
- [x] `Components/Layout/MainLayout.razor` - Layout completo riscritto
- [x] `Components/Layout/MainLayout.razor.css` - CSS moderno con ::deep
- [x] `wwwroot/app.css` - Stili globali prioritari
- [x] `Components/App.razor` - Bootstrap Icons CDN
- [x] `Components/_Imports.razor` - Namespace globali
### ?? Pagine Verificate
- [x] Dashboard - Razor syntax corretta
- [x] Strategies - Asset count dinamico (15/15)
- [x] Assets - Nuova pagina completa
- [x] Trading - Funzionante
- [x] Market - Query parameters supportati
- [x] Statistics - Dettagli completi
- [x] Settings - Persistenza attiva
### ?? Services
- [x] TradingBotService - 15 asset abilitati
- [x] SettingsService - Persistenza JSON
- [x] SimulatedMarketDataService - Tutti asset disponibili
- [x] SimpleMovingAverageStrategy - RSI + MACD
### ?? Features Implementate
#### Sidebar
```
? Logo gradient 3.5rem
? Brand text con accent
? Status indicator animato
? 7 menu items verticali
? Icone 1.375rem
? Hover effects
? Active state con border
? Portfolio summary live
? Toggle collapse button
```
#### Navigation
```
? Dashboard (/)
? Strategie (/strategies)
? Asset (/assets) - NUOVA
? Trading (/trading)
? Analisi Mercato (/market)
? Statistiche (/statistics)
? Impostazioni (/settings)
```
#### Assets Page
```
? Grid view / List view
? 15 asset visibili
? Strategy assignment dropdown
? Toggle on/off per asset
? Filtri: Tutti/Attivi/Inattivi
? Real-time metrics
? Navigate to chart
```
### ?? Design System
#### Colors
```css
Primary: #6366f1 (Indigo)
Secondary: #8b5cf6 (Purple)
Success: #10b981 (Green)
Danger: #ef4444 (Red)
Warning: #f59e0b (Amber)
Background: #0a0e27 (Dark Blue)
Sidebar: #1a1f3a ? #0f1629 (Gradient)
```
#### Typography
```
Headers: System Font Stack
Monospace: Courier New (numeri)
Weights: 600 (semi-bold), 700 (bold)
Sizes: 0.75rem - 1.75rem
```
#### Spacing
```
Unit: 0.25rem (4px)
Padding: 1rem - 2rem
Gaps: 0.5rem - 1.5rem
Radius: 0.5rem - 1rem
```
### ?? Real-time Updates
- [x] Prezzi aggiornati ogni 3 secondi
- [x] Portfolio stats live
- [x] Trade notifications
- [x] Indicators recalculated
- [x] SignalR connection active
### ?? Persistenza
- [x] Settings salvati in JSON
- [x] Sidebar state ricordato
- [x] Auto-start bot configurabile
- [x] File path: %LocalAppData%/TradingBot/appsettings.json
### ?? Simulazione
- [x] 15 asset simultanei
- [x] Dati di mercato realistici
- [x] Variazioni % simulate
- [x] Trading automatico attivo
- [x] Risk management implementato
### ??? Architettura
#### Frontend
```
Blazor Server (.NET 10)
??? SignalR per real-time
??? Scoped CSS per component isolation
??? Global CSS per layout
??? Bootstrap Icons via CDN
```
#### Backend
```
Services
??? TradingBotService (singleton)
??? SimulatedMarketDataService (singleton)
??? SettingsService (singleton)
??? SimpleMovingAverageStrategy (singleton)
```
#### Models
```
Core
??? AssetConfiguration
??? AssetStatistics
??? MarketPrice
??? Trade
??? TechnicalIndicators
??? PortfolioStatistics
??? AppSettings
```
### ?? Build Status
```
Compilazione: ? RIUSCITA
Errori: ? 0
Warning: ? 0
Target: ? .NET 10
```
### ?? Documentazione
- [x] README.md aggiornato
- [x] BROWSER_CACHE_GUIDE.md creato
- [x] FINAL_VERIFICATION.md (questo file)
- [x] Inline code comments
### ?? Sicurezza
- [x] Input validation
- [x] Readonly settings per sim mode
- [x] Safe decimal calculations
- [x] Error boundaries
### ? Accessibilità
- [x] Semantic HTML
- [x] ARIA labels via title attributes
- [x] Keyboard navigation support
- [x] Focus states visible
### ?? Responsive
```
Desktop: > 1024px ? Full layout
Tablet: 768-1024px ? Sidebar 260px
Mobile: < 768px ? Offscreen sidebar
Small: < 480px ? Compact padding
```
### ? Performance
- [x] CSS transitions GPU-accelerated
- [x] Component rendering optimized
- [x] Minimal re-renders (StateHasChanged strategico)
- [x] Lazy evaluation dove possibile
### ?? Testing Checklist
#### Manual Testing
- [ ] Avvia applicazione
- [ ] Verifica sidebar appare correttamente
- [ ] Click su ogni menu item
- [ ] Verifica navigazione funziona
- [ ] Toggle sidebar collapse/expand
- [ ] Verifica portfolio stats si aggiornano
- [ ] Click "Stop" bot
- [ ] Click "Avvia" bot
- [ ] Vai su Assets page
- [ ] Cambia view (Grid ? List)
- [ ] Assegna strategia ad un asset
- [ ] Toggle asset on/off
- [ ] Vai su Settings
- [ ] Cambia impostazioni
- [ ] Verifica salvataggio automatico
- [ ] Resize finestra (responsive test)
- [ ] Test su mobile (DevTools)
#### Browser Compatibility
- [ ] Chrome (latest)
- [ ] Edge (latest)
- [ ] Firefox (latest)
- [ ] Safari (se disponibile)
#### Cache Testing
- [ ] Hard refresh (Ctrl+Shift+R)
- [ ] Incognito mode
- [ ] After server restart
- [ ] After clean build
### ?? Metrics
#### Code Stats
```
Razor Files: ~15 pages
CSS Files: ~15 scoped + 1 global
C# Services: ~8 services
Models: ~12 models
Total Lines: ~5000+ LOC
```
#### Features Count
```
Pages: 7 main pages
Components: ~5 shared components
Services: 8 business services
Asset Support: 15 cryptocurrencies
Strategies: 6 templates
Indicators: 3 technical (RSI, MACD, EMA)
```
### ?? Success Criteria
#### Visual
? Sidebar verticale moderna visibile
? Icone Bootstrap caricate
? Gradients applicati
? Animazioni fluide
? Colors coerenti
? Typography corretta
#### Functional
? Navigazione funzionante
? Bot start/stop
? Real-time updates
? Settings persistono
? Assets management
? Strategy assignment
#### Technical
? Build successful
? 0 compilation errors
? CSS correttamente applicato
? Services registered
? SignalR connected
### ?? Deployment Ready
#### Pre-deployment
- [x] Build in Release mode
- [x] Verify all assets
- [x] Test all routes
- [x] Check console for errors
- [x] Validate responsive design
#### Production Checklist
- [ ] Remove debug code
- [ ] Optimize images
- [ ] Minify CSS/JS
- [ ] Enable HTTPS
- [ ] Configure CORS
- [ ] Set production URLs
- [ ] Configure logging
- [ ] Setup monitoring
### ?? Support
#### Se Qualcosa Non Funziona
1. **Verifica Build**
```sh
dotnet build
```
2. **Pulisci Cache**
```sh
dotnet clean
Remove-Item bin,obj -Recurse -Force
dotnet restore
dotnet build
```
3. **Hard Refresh Browser**
```
Ctrl + Shift + R
```
4. **Check Console**
```
F12 ? Console tab
Cerca errori rossi
```
5. **Verifica Network**
```
F12 ? Network tab
Reload ? Verifica CSS caricati (200 OK)
```
### ?? Screenshots Attesi
#### Desktop - Expanded
```
[Logo 3.5rem] TradingBot [?]
? ATTIVO
????????????????????????????????
?? Dashboard
?? Strategie
?? Asset
?? Trading
?? Analisi Mercato
?? Statistiche
?? Impostazioni
????????????????????????????????
Portfolio $15,000
Profitto $0.00
```
#### Desktop - Collapsed
```
[Logo]
[?]
??
??
??
??
??
??
??
```
#### Mobile
```
[?] TradingBot [Stop]
Main Content Here...
```
### ? CONCLUSIONE
L'applicazione è:
- ? **Completamente funzionale**
- ? **Build successful**
- ? **Design moderno implementato**
- ? **Tutti i 15 asset attivi**
- ? **Persistenza settings funzionante**
- ? **Responsive su tutti i device**
- ? **Real-time updates attivi**
- ? **Documentazione completa**
**?? PRONTO PER L'USO!**
---
**Data verifica**: 2025-12-12
**Versione**: 1.0.0
**Status**: ? PRODUCTION READY
-410
View File
@@ -1,410 +0,0 @@
# ?? WORKFLOW: Sviluppo ? Gitea ? Unraid
## Flusso di Lavoro Completo
```
PC Sviluppo ? Git Commit ? Gitea Push ? Unraid Pull ? Docker Deploy
```
---
## ?? STEP BY STEP
### 1. Sviluppo Locale (PC)
```sh
# Lavora sul codice
code .
# Test locale
dotnet run
# Oppure
docker-compose up
# Verifica funzionamento
http://localhost:8080
```
### 2. Commit e Push su Gitea
```sh
# Status modifiche
git status
# Stage files
git add .
# Commit
git commit -m "Feature: Descrizione modifiche"
# Push su Gitea
git push origin main
```
### 3. Deploy su Unraid
#### Opzione A: Automatico (Portainer Webhook)
**Setup iniziale** (una volta):
```
1. Portainer ? Stacks ? tradingbot ? Webhooks
2. Create webhook
3. Copy URL
4. Gitea ? Settings ? Webhooks ? Add Webhook
5. Paste URL
6. Trigger: Push events
7. Save
```
**Uso**: Ogni push su Gitea ? Auto-deploy su Unraid!
#### Opzione B: Manuale (SSH)
```sh
# SSH su Unraid
ssh root@[UNRAID-IP]
# Vai nella directory
cd /mnt/user/appdata/tradingbot/TradingBot
# Pull modifiche
git pull origin main
# Rebuild e restart
docker-compose down
docker-compose build
docker-compose up -d
```
#### Opzione C: Script Automatico
Crea `/root/scripts/deploy-tradingbot.sh`:
```sh
#!/bin/bash
cd /mnt/user/appdata/tradingbot/TradingBot
echo "?? Pulling latest changes..."
git pull origin main
if [ $? -eq 0 ]; then
echo "?? Rebuilding container..."
docker-compose down
docker-compose build
docker-compose up -d
echo "? Deployment completed!"
else
echo "? Git pull failed!"
exit 1
fi
```
Usa:
```sh
chmod +x /root/scripts/deploy-tradingbot.sh
/root/scripts/deploy-tradingbot.sh
```
---
## ?? WORKFLOW GIORNALIERO
### Mattina - Modifiche
```sh
# PC
git pull origin main # Sync
code . # Sviluppa
dotnet run # Test
```
### Pomeriggio - Deploy
```sh
# PC
git add .
git commit -m "Daily improvements"
git push origin main
# Unraid (se non auto-deploy)
ssh root@unraid
/root/scripts/deploy-tradingbot.sh
```
### Sera - Monitoring
```sh
# Check logs
docker logs tradingbot -f
# Check stats
docker stats tradingbot
# Backup (opzionale)
/root/scripts/backup-tradingbot.sh
```
---
## ?? BRANCHING STRATEGY
### Main Branch (Production)
```sh
# Solo codice stabile e testato
git checkout main
git pull origin main
```
### Development Branch
```sh
# Crea branch per nuove feature
git checkout -b feature/nome-feature
# Sviluppa e testa
# ...
# Merge in main quando pronto
git checkout main
git merge feature/nome-feature
git push origin main
```
### Hotfix
```sh
# Per fix urgenti
git checkout -b hotfix/descrizione
# Fix
git checkout main
git merge hotfix/descrizione
git push origin main
```
---
## ?? RELEASE VERSIONING
### Tagging
```sh
# Tag versione
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
# Build con tag
docker build -t tradingbot:v1.0.0 .
docker tag tradingbot:v1.0.0 tradingbot:latest
```
### Rollback
```sh
# Lista tags
git tag -l
# Checkout versione precedente
git checkout v0.9.0
# Deploy versione specifica
docker-compose down
docker-compose build
docker-compose up -d
```
---
## ?? BEST PRACTICES
### 1. Non Committare Secrets
```sh
# .gitignore già configurato per:
appsettings.Development.json
*.env
*.key
```
### 2. Test Prima di Push
```sh
# Sempre test locale prima
dotnet build
dotnet test # Se hai tests
docker-compose up # Test container
```
### 3. Commit Messages Descrittivi
```sh
# ? Buoni
git commit -m "Fix: Sidebar toggle button not working"
git commit -m "Feature: Add Docker support"
git commit -m "Docs: Update deployment guide"
# ? Cattivi
git commit -m "fix"
git commit -m "update"
git commit -m "changes"
```
### 4. Pull Prima di Push
```sh
# Sempre sync prima
git pull origin main
# Risolvi conflitti se presenti
git push origin main
```
---
## ?? TROUBLESHOOTING
### Conflitto Git
```sh
# Pull con conflitti
git pull origin main
# Risolvi manualmente i file in conflitto
# Cerca <<<<<<< HEAD
# Dopo risolto
git add .
git commit -m "Resolve merge conflicts"
git push origin main
```
### Push Rifiutato
```sh
# Se remote è avanti
git pull --rebase origin main
git push origin main
```
### Reset Completo (ATTENZIONE!)
```sh
# Solo in caso di emergenza
git fetch origin
git reset --hard origin/main
```
---
## ?? MONITORING WORKFLOW
### Check Health
```sh
# Local
curl http://localhost:8080/health
# Unraid
curl http://[UNRAID-IP]:8080/health
```
### View Logs
```sh
# Real-time
docker logs -f tradingbot
# Last 100 lines
docker logs --tail 100 tradingbot
# Since timestamp
docker logs --since 2024-12-12T10:00:00 tradingbot
```
### Resource Usage
```sh
# Stats
docker stats tradingbot
# Processes
docker top tradingbot
```
---
## ?? CHECKLIST COMPLETO
### Pre-Development
- [ ] Git repository synced (`git pull`)
- [ ] Branch corretto (`git branch`)
- [ ] Dependencies updated (`dotnet restore`)
### Development
- [ ] Codice scritto e testato
- [ ] Build successful (`dotnet build`)
- [ ] Test locale OK (`dotnet run`)
- [ ] Docker test OK (`docker-compose up`)
### Pre-Commit
- [ ] Codice formattato
- [ ] No secrets committati
- [ ] .gitignore aggiornato
- [ ] README aggiornato se necessario
### Commit & Push
- [ ] `git status` verificato
- [ ] Commit message descrittivo
- [ ] Push successful
- [ ] Verifica su Gitea web UI
### Deployment
- [ ] Pull su Unraid OK
- [ ] Docker build successful
- [ ] Container running
- [ ] Health check passing
- [ ] WebUI accessibile
### Post-Deployment
- [ ] Logs verificati
- [ ] Nessun errore critico
- [ ] Funzionalità testate
- [ ] Performance OK
---
## ?? MAINTENANCE
### Giornaliero
- Check logs per errori
- Verifica health endpoint
- Monitor resource usage
### Settimanale
- Git pull updates
- Review commits
- Check disk space
### Mensile
- Full backup
- Review performance metrics
- Update dependencies
- Security audit
---
## ?? COMANDI RAPIDI
```sh
# Development
git status
git add .
git commit -m "message"
git push origin main
# Local Test
dotnet run
docker-compose up -d
# Unraid Deploy
ssh root@unraid "/root/scripts/deploy-tradingbot.sh"
# Check Status
docker ps | grep tradingbot
docker logs tradingbot --tail 50
# Restart
docker restart tradingbot
# Update
git pull && docker-compose up -d --build
```
---
**?? Workflow pronto! Sviluppo ? Gitea ? Unraid automatizzato!**
+94
View File
@@ -0,0 +1,94 @@
namespace TradingBot.Models;
/// <summary>
/// Configuration for a trading indicator
/// </summary>
public class IndicatorConfig
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public IndicatorType Type { get; set; }
public bool IsEnabled { get; set; } = true;
// Thresholds for signals
public decimal? OverboughtThreshold { get; set; }
public decimal? OversoldThreshold { get; set; }
public decimal? BuyThreshold { get; set; }
public decimal? SellThreshold { get; set; }
// Indicator-specific parameters
public int Period { get; set; } = 14;
public int FastPeriod { get; set; } = 12;
public int SlowPeriod { get; set; } = 26;
public int SignalPeriod { get; set; } = 9;
}
/// <summary>
/// Real-time indicator signal
/// </summary>
public class IndicatorSignal
{
public string IndicatorId { get; set; } = string.Empty;
public string IndicatorName { get; set; } = string.Empty;
public string Symbol { get; set; } = string.Empty;
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public SignalStrength Strength { get; set; }
public SignalType Type { get; set; }
public string Message { get; set; } = string.Empty;
public decimal? Value { get; set; }
}
/// <summary>
/// Indicator status for a specific asset
/// </summary>
public class IndicatorStatus
{
public string IndicatorId { get; set; } = string.Empty;
public string Symbol { get; set; } = string.Empty;
public decimal CurrentValue { get; set; }
public decimal? PreviousValue { get; set; }
public MarketCondition Condition { get; set; }
public string Recommendation { get; set; } = string.Empty;
public DateTime LastUpdate { get; set; } = DateTime.UtcNow;
}
/// <summary>
/// Types of trading indicators
/// </summary>
public enum IndicatorType
{
RSI, // Relative Strength Index
MACD, // Moving Average Convergence Divergence
SMA, // Simple Moving Average
EMA, // Exponential Moving Average
BollingerBands, // Bollinger Bands
Stochastic, // Stochastic Oscillator
Volume, // Volume indicators
ATR // Average True Range (volatility)
}
/// <summary>
/// Signal strength levels
/// </summary>
public enum SignalStrength
{
Weak,
Moderate,
Strong,
VeryStrong
}
/// <summary>
/// Market condition based on indicators
/// </summary>
public enum MarketCondition
{
Neutral,
Overbought,
Oversold,
Bullish,
Bearish,
Ranging,
Trending
}
+27
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
}
+137
View File
@@ -0,0 +1,137 @@
namespace TradingBot.Models;
/// <summary>
/// Represents the mapping between an asset and its assigned trading strategies
/// </summary>
public class AssetStrategyMapping
{
public string Symbol { get; set; } = string.Empty;
public string AssetName { get; set; } = string.Empty;
public List<string> StrategyIds { get; set; } = new();
public bool IsActive { get; set; }
public DateTime ActivatedAt { get; set; }
public DateTime? DeactivatedAt { get; set; }
/// <summary>
/// Strategy-specific parameters override
/// Key: StrategyId, Value: Dictionary of parameter names and values
/// </summary>
public Dictionary<string, Dictionary<string, object>> StrategyParameters { get; set; } = new();
}
/// <summary>
/// Represents a trading strategy instance
/// </summary>
public class StrategyInfo
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty; // Trend, Oscillator, Volatility, etc.
public StrategyRisk RiskLevel { get; set; }
public TimeFrame RecommendedTimeFrame { get; set; }
public List<string> RequiredIndicators { get; set; } = new();
public Dictionary<string, ParameterInfo> Parameters { get; set; } = new();
}
/// <summary>
/// Parameter information for strategy configuration
/// </summary>
public class ParameterInfo
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public ParameterType Type { get; set; }
public object DefaultValue { get; set; } = 0;
public object? MinValue { get; set; }
public object? MaxValue { get; set; }
}
/// <summary>
/// Trading engine status for an asset
/// </summary>
public class TradingEngineStatus
{
public string Symbol { get; set; } = string.Empty;
public bool IsRunning { get; set; }
public int ActiveStrategies { get; set; }
public DateTime? LastSignalTime { get; set; }
public List<StrategySignal> RecentSignals { get; set; } = new();
public TradingDecision? LastDecision { get; set; }
}
/// <summary>
/// Signal from a specific strategy
/// </summary>
public class StrategySignal
{
public string StrategyId { get; set; } = string.Empty;
public string StrategyName { get; set; } = string.Empty;
public TradingSignal Signal { get; set; } = new();
public DateTime GeneratedAt { get; set; } = DateTime.UtcNow;
}
/// <summary>
/// Aggregated trading decision from multiple strategies
/// </summary>
public class TradingDecision
{
public string Symbol { get; set; } = string.Empty;
public SignalType Decision { get; set; }
public decimal Confidence { get; set; }
public string Reason { get; set; } = string.Empty;
public int BuyVotes { get; set; }
public int SellVotes { get; set; }
public int HoldVotes { get; set; }
public List<string> SupportingStrategies { get; set; } = new();
public List<string> OpposingStrategies { get; set; } = new();
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
}
/// <summary>
/// Strategy performance metrics
/// </summary>
public class StrategyPerformance
{
public string StrategyId { get; set; } = string.Empty;
public string Symbol { get; set; } = string.Empty;
public int TotalSignals { get; set; }
public int CorrectSignals { get; set; }
public decimal Accuracy { get; set; }
public decimal TotalProfit { get; set; }
public decimal AverageConfidence { get; set; }
public DateTime FirstSignalTime { get; set; }
public DateTime LastSignalTime { get; set; }
}
/// <summary>
/// Risk level for strategies
/// </summary>
public enum StrategyRisk
{
Low,
Medium,
High,
VeryHigh
}
/// <summary>
/// Recommended time frame for strategy
/// </summary>
public enum TimeFrame
{
ShortTerm, // Minutes to hours
MediumTerm, // Hours to days
LongTerm // Days to weeks
}
/// <summary>
/// Parameter data type
/// </summary>
public enum ParameterType
{
Integer,
Decimal,
Boolean,
String
}
+1
View File
@@ -5,6 +5,7 @@ public class TradingSignal
public string Symbol { get; set; } = string.Empty;
public SignalType Type { get; set; }
public decimal Price { get; set; }
public decimal Confidence { get; set; } // 0-100 confidence level
public string Reason { get; set; } = string.Empty;
public DateTime Timestamp { get; set; }
}
+23 -3
View File
@@ -3,6 +3,16 @@ using TradingBot.Services;
var builder = WebApplication.CreateBuilder(args);
// Configure Kestrel - Solo in Development usa porte da launchSettings
// In Production/Docker usa porta 8080 su tutte le interfacce
if (builder.Environment.IsProduction() || builder.Environment.EnvironmentName == "Docker")
{
builder.WebHost.ConfigureKestrel(serverOptions =>
{
serverOptions.ListenAnyIP(8080);
});
}
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
@@ -10,9 +20,16 @@ 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<IndicatorsService>();
builder.Services.AddSingleton<TradingStrategiesService>();
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();
@@ -22,16 +39,19 @@ var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
// HTTPS redirect solo in Development
if (app.Environment.IsDevelopment())
{
app.UseHttpsRedirection();
}
app.UseStaticFiles();
app.UseAntiforgery();
// Health check endpoint for Docker
// Health check endpoint
app.MapHealthChecks("/health");
app.MapRazorComponents<App>()
+13 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"TradingBot": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
@@ -10,7 +10,7 @@
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"TradingBot (HTTPS)": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
@@ -18,6 +18,17 @@
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Docker": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
"environmentVariables": {
"ASPNETCORE_URLS": "http://+:8080",
"ASPNETCORE_ENVIRONMENT": "Production"
},
"publishAllPorts": true,
"useSSL": false
}
}
}
+158 -340
View File
@@ -1,345 +1,163 @@
# ?? TradingBot - Automated Crypto Trading Simulator
# ?? TradingBot
Un'applicazione Blazor Server avanzata per simulare e testare strategie di trading automatizzato su criptovalute.
**Automated Crypto Trading Bot** con interfaccia Blazor Server per trading algoritmico simulato.
## ?? Caratteristiche Principali
### ?? Dashboard
- **Panoramica Portfolio**: Visualizzazione completa del valore totale e performance
- **Metriche Chiave**: Profitto totale, operazioni eseguite, asset attivi
- **Asset Attivi**: Grid dei top 6 asset con performance in tempo reale
- **Attività Recente**: Storico delle ultime 8 operazioni
### ?? Strategie
- **Gestione Strategie**: Crea, modifica ed elimina strategie di trading
- **Template Predefiniti**:
- Scalping Veloce
- Trend Following
- Mean Reversion
- Conservative
- **Strategia Attiva**: RSI + MACD Cross (personalizzabile)
- **Parametri Configurabili**: Stop Loss, Take Profit, condizioni BUY/SELL
### ?? Asset (NUOVO!)
- **Vista Completa Asset**: Tutti i 15 asset disponibili
- **Due Modalità di Visualizzazione**:
- **Grid View**: Card dettagliate con metriche
- **List View**: Tabella compatta per overview rapido
- **Assegnazione Strategie**: Dropdown per ogni asset
- **Toggle On/Off**: Attiva/disattiva trading per asset
- **Filtri**: Tutti / Solo Attivi / Solo Inattivi
- **Metriche Real-time**: Prezzo, variazione 24h, holdings, profitto
- **Azioni Rapide**: Configura e Visualizza Grafico
### ?? Trading
- **15 Asset Simulati**: BTC, ETH, BNB, SOL, ADA, XRP, DOT, AVAX, MATIC, LINK, UNI, ATOM, LTC, ALGO, VET
- **Gestione Asset**: Toggle on/off per ogni asset
- **Monitoraggio Real-time**: Prezzi, holdings, profitti aggiornati ogni 3 secondi
- **Tabella Operazioni**: Storico completo con filtri e ricerca
### ?? Analisi Mercato
- **Grafici Interattivi**: Visualizzazione prezzi con SVG rendering
- **Indicatori Tecnici**:
- RSI (14) con stati Overbought/Oversold/Neutral
- MACD con signal e histogram
- EMA (12, 26)
- **Selector Asset**: Cambia asset per analisi dettagliate
### ?? Statistiche
- **Overview Portfolio**: Metriche aggregate di tutti gli asset
- **Breakdown per Asset**: Tabella dettagliata con ROI, win rate, trades
- **Best/Worst Performers**: Identificazione automatica
- **Analisi Dettagliata**: Drilldown su singolo asset con:
- Performance trading completa
- Analisi profitti/perdite
- Operazioni recenti
### ?? Impostazioni
- **Persistenza Automatica**: Tutte le modifiche salvate su file
- **Configurazioni**:
- Modalità simulazione
- Notifiche desktop
- Auto-start bot
- Conferma operazioni manuali
- Intervallo aggiornamento (2-10 secondi)
- Log level
- **Notifiche Visive**: Feedback immediato sui salvataggi
## ??? Architettura
### Frontend
- **Blazor Server (.NET 10)**: Rendering server-side con SignalR
- **Sidebar Collapsible**: Navigazione verticale espandibile/minimizzabile
- **Responsive Design**: Ottimizzato per desktop, tablet e mobile
- **Dark Theme**: Design moderno con palette Indigo/Purple
### Backend Services
- **TradingBotService**: Core logic per trading automatizzato
- **SimulatedMarketDataService**: Generazione dati di mercato realistici
- **SettingsService**: Persistenza configurazioni su file JSON
- **SimpleMovingAverageStrategy**: Strategia di trading con RSI e MACD
### Models
- **AssetConfiguration**: Configurazione per singolo asset
- **AssetStatistics**: Metriche e performance tracking
- **MarketPrice**: Dati di mercato in tempo reale
- **TechnicalIndicators**: RSI, MACD, EMA
- **AppSettings**: Configurazioni globali applicazione
## ?? Quick Start
### Prerequisiti
- .NET 10 SDK
- Visual Studio 2022+ o VS Code
### Installazione Locale
```bash
# Clone repository
git clone https://192.168.30.23/Alby96/Encelado
cd TradingBot
# Restore packages
dotnet restore
# Run application
dotnet run
```
### ?? Deployment Docker
#### Development
```sh
# Build
docker-compose build
# Run
docker-compose up -d
# Access
http://localhost:8080
```
#### Production (Unraid)
Vedi documentazione completa:
- ?? [UNRAID_DEPLOYMENT.md](UNRAID_DEPLOYMENT.md) - Guida completa Unraid + Gitea
- ?? [DOCKER_QUICKSTART.md](DOCKER_QUICKSTART.md) - Quick start rapido
### Uso
1. L'applicazione si avvia automaticamente in modalità simulazione
2. Tutti i 15 asset sono attivi di default
3. Il bot inizia il trading automaticamente (configurabile in Impostazioni)
4. Usa la sidebar per navigare tra le sezioni
## ?? Struttura Progetto
```
TradingBot/
??? Components/
? ??? Layout/
? ? ??? MainLayout.razor # Layout principale con sidebar
? ? ??? MainLayout.razor.css
? ??? Pages/
? ? ??? Dashboard.razor # Homepage overview
? ? ??? Strategies.razor # Gestione strategie
? ? ??? Trading.razor # Trading view
? ? ??? Market.razor # Analisi mercato
? ? ??? Statistics.razor # Statistiche dettagliate
? ? ??? Settings.razor # Configurazioni
? ? ??? Assets.razor # Gestione asset (NUOVA PAGINA)
? ??? Shared/
? ??? AdvancedChart.razor # Componente grafico SVG
? ??? AssetSettings.razor # Config singolo asset
??? Models/
? ??? AssetConfiguration.cs
? ??? AssetStatistics.cs
? ??? AppSettings.cs
? ??? MarketPrice.cs
? ??? TechnicalIndicators.cs
? ??? ...
??? Services/
? ??? TradingBotService.cs # Core trading logic
? ??? SimulatedMarketDataService.cs # Simulazione mercato
? ??? SettingsService.cs # Persistenza settings
? ??? ITradingStrategy.cs # Interface strategia
? ??? SimpleMovingAverageStrategy.cs
? ??? TechnicalAnalysis.cs # Calcolo indicatori
??? wwwroot/
? ??? app.css # Stili globali
??? Program.cs # Entry point + DI
```
## ?? **STRUTTURA FINALE APPLICAZIONE**
### **7 Sezioni Principali**
1. **?? Dashboard** (`/`)
- Overview portfolio completo
- 4 summary cards con metriche
- Top 6 asset attivi
- Ultimi 8 trades
2. **?? Strategie** (`/strategies`)
- Gestione strategie di trading
- Strategia attiva: RSI + MACD Cross
- Template predefiniti
- Performance tracking
3. **?? Asset** (`/assets`) **? NUOVA PAGINA!**
- Vista completa tutti i 15 asset
- Grid view / List view
- Assegnazione strategia per asset
- Toggle attivazione
- Filtri e ricerca
- Metriche real-time
4. **?? Trading** (`/trading`
- Tutti 15 asset in grid
- Toggle on/off per ogni asset
- Metriche real-time
- Tabella operazioni complete
5. **?? Analisi Mercato** (`/market`)
- Grafici interattivi SVG
- Indicatori tecnici (RSI, MACD, EMA)
- Selector asset
- Dati aggiornati ogni 3 secondi
6. **?? Statistiche** (`/statistics`)
- Overview portfolio dettagliato
- Breakdown per asset
- Best/Worst performers
- Analisi P&L completa
- Drilldown su singolo asset
7. **?? Impostazioni** (`/settings`)
- Tutte le configurazioni globali
- Salvataggio automatico
- Notifiche di conferma
- Reset a defaults
## ?? Design System
### Colori
- **Primary**: `#6366f1` (Indigo)
- **Secondary**: `#8b5cf6` (Purple)
- **Success**: `#10b981` (Green)
- **Danger**: `#ef4444` (Red)
- **Warning**: `#f59e0b` (Amber)
- **Background**: `#0a0e27` (Dark Blue)
### Typography
- **Headers**: System Font Stack
- **Monospace**: Courier New (per valori numerici)
## ?? Configurazione
Le impostazioni vengono salvate automaticamente in:
```
%LocalAppData%/TradingBot/appsettings.json
```
### Esempio appsettings.json
```json
{
"SimulationMode": true,
"DesktopNotifications": false,
"AutoStartBot": true,
"ConfirmManualTrades": false,
"UpdateIntervalSeconds": 3,
"LogLevel": "Info",
"SidebarCollapsed": false
}
```
## ?? Indicatori Tecnici Implementati
### RSI (Relative Strength Index)
- **Periodo**: 14
- **Overbought**: > 70
- **Oversold**: < 30
- **Neutro**: 30-70
### MACD (Moving Average Convergence Divergence)
- **Fast EMA**: 12 periodi
- **Slow EMA**: 26 periodi
- **Signal**: 9 periodi
- **Histogram**: MACD - Signal
### EMA (Exponential Moving Average)
- **EMA 12**: Media breve termine
- **EMA 26**: Media lungo termine
## ?? Strategia di Trading
### Condizioni BUY
- RSI < 40 (asset ipervenduto)
- MACD Histogram > 0 (momentum positivo)
- Budget disponibile >= MinTradeAmount
### Condizioni SELL
- RSI > 60 (asset ipercomprato)
- MACD Histogram < 0 (momentum negativo)
- Holdings > 0
- **Oppure**:
- Profitto >= Take Profit (10%)
- Perdita >= Stop Loss (5%)
### Risk Management
- **Max Daily Trades**: 50 per asset
- **Max Position Size**: $5000 per asset
- **Min Trade Amount**: $10
- **Trade Size**: 30% del balance disponibile (max)
- **Min Interval**: 10 secondi tra trades
## ?? Aggiornamenti Real-time
- **Prezzi**: Ogni 3 secondi (configurabile)
- **Indicatori**: Calcolati ad ogni aggiornamento prezzo
- **Stats Portfolio**: Aggiornate ad ogni trade
- **UI**: SignalR per aggiornamenti istantanei
## ?? Responsive Breakpoints
- **Desktop**: > 1024px (full features)
- **Tablet**: 768px - 1024px (layout adattato)
- **Mobile**: < 768px (sidebar collapsible automatico)
## ?? Debug & Logging
I log vengono stampati nella console del browser e nel terminal di Visual Studio.
Livelli disponibili:
- **Error**: Solo errori critici
- **Warning**: Warning e errori
- **Info**: Informazioni generali (default)
- **Debug**: Dettagli completi per debugging
## ?? Note Importanti
1. **Modalità Simulazione**: Sempre attiva, dati non reali
2. **Dati Persistenti**: Solo impostazioni, non trades storici
3. **Reset Dati**: Riavvio applicazione = reset portfolio
4. **Performance**: Ottimizzata per 15 asset simultanei
## ?? Future Enhancements
- [ ] Backtesting su dati storici
- [ ] Multi-strategy support
- [ ] Export/import configurazioni
- [ ] Alert system con notifiche
- [ ] Paper trading con dati reali
- [ ] Machine learning per ottimizzazione strategie
## ????? Sviluppatore
**Alberto** - Encelado Project
## ?? Licenza
Progetto privato - Tutti i diritti riservati
[![.NET 10](https://img.shields.io/badge/.NET-10.0-512BD4)](https://dotnet.microsoft.com/)
[![Blazor](https://img.shields.io/badge/Blazor-Server-512BD4)](https://blazor.net/)
[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED)](https://www.docker.com/)
[![Version](https://img.shields.io/badge/version-1.5.2-blue)](https://gitea.encke-hake.ts.net/Alby96/Encelado/-/packages)
---
**Note**: Questa è un'applicazione di simulazione a scopo educativo. Non utilizzare con denaro reale senza test approfonditi e comprensione completa dei rischi del trading.
## ? Caratteristiche
- **Multi-Strategy Trading**: 8 strategie professionali assegnabili a ogni asset
- **Positions Management**: Visualizza e chiudi manualmente posizioni aperte
- **Detailed Portfolio Metrics**: Capitale totale, investito, disponibile, P&L, ROI
- **Trading Control**: Gestione visuale strategie con voting system
- **Dashboard Blazor**: Real-time updates ogni 3 secondi
- **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, Stochastic
- **Indicators System**: 7+ indicatori tecnici configurabili con segnali real-time
- **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
---
## ?? Trading Strategies
### **8 Strategie Professionali**
1. **RSI Strategy** - Oscillator (Medium Risk)
2. **MACD Strategy** - Momentum (Medium Risk)
3. **Bollinger Bands** - Volatility (Low Risk)
4. **Mean Reversion** - Contrarian (High Risk)
5. **Momentum** - Trend Following (Medium Risk)
6. **EMA Crossover** - Golden/Death Cross (Low Risk)
7. **Scalping** - Short-term (Very High Risk)
8. **Breakout** - Volatility Breakout (High Risk)
### **Multi-Strategy System**
- Assegna multiple strategie per asset
- Sistema di voting per decisioni aggregate
- Confidence-based trading
- Parametri configurabili per strategia
---
## ?? Quick Start
### Locale (Development)
```bash
git clone https://gitea.encke-hake.ts.net/Alby96/Encelado
cd Encelado/TradingBot
dotnet run
```
Accedi a: `http://localhost:5243`
### Docker
```bash
docker pull gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest
docker run -d -p 8888:8080 \
-v tradingbot-data:/app/data \
gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest
```
Accedi a: `http://localhost:8888`
### Unraid
Guida completa: [deployment/UNRAID_INSTALL.md](deployment/UNRAID_INSTALL.md)
```bash
# 1. Login Gitea Registry
docker login gitea.encke-hake.ts.net
# 2. Download template
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
# 3. Install via Unraid Docker UI
```
---
## ?? Versioning
### Current Version: `1.5.2`
**Latest**: Metriche dettagliate capitale in sidebar (Totale, Investito, Disponibile, P&L, ROI)
```powershell
# Bug fix (1.5.2 ? 1.5.3)
.\bump-version.ps1 patch -Message "Fix calculation bug"
# New feature (1.5.2 ? 1.6.0)
.\bump-version.ps1 minor -Message "Add new strategy"
# Breaking change (1.5.2 ? 2.0.0)
.\bump-version.ps1 major -Message "New API"
```
Vedi [CHANGELOG.md](CHANGELOG.md) per release notes complete.
---
## ?? Publishing
### Da Visual Studio
1. **Build** ? **Configuration Manager** ? **Release**
2. **Build** ? **Publish TradingBot** ? Profilo **Docker**
3. Click **Publish**
Il sistema automaticamente:
- ? Build Docker image
- ? Tag: `latest`, `1.5.1`, `1.5.1-20241222`
- ? Push su Gitea Registry
### Deploy su Unraid
```bash
# Docker tab ? TradingBot ? Stop ? Force Update ? Start
```
---
## ?? Documentazione
- **[UNRAID_INSTALL.md](deployment/UNRAID_INSTALL.md)** - Installazione completa su Unraid
- **[CHANGELOG.md](CHANGELOG.md)** - Release notes e versioni
- **[Dockerfile](Dockerfile)** - Docker multi-stage build
- **[docker-compose.yml](deployment/docker-compose.yml)** - Deploy con Compose
---
## ??? Tecnologie
- **.NET 10** | **Blazor Server** | **C# 14**
- **Bootstrap 5.3** | **Docker** | **Gitea Registry**
---
## ?? License
MIT License - Copyright © 2024 Alby96
---
## ?? Autore
**Alberto** (Alby96)
- Gitea: [@Alby96](https://gitea.encke-hake.ts.net/Alby96)
- Repository: [Encelado/TradingBot](https://gitea.encke-hake.ts.net/Alby96/Encelado)
---
**?? Happy Trading!**
-271
View File
@@ -1,271 +0,0 @@
# ?? DEBUG - Sidebar Collapse Toggle
## Problema Riportato
Il pulsante per ridurre la sidebar a sole icone non funziona.
## Modifiche Applicate
### 1. **MainLayout.razor** - Migliorato Toggle
```csharp
private void ToggleSidebar()
{
sidebarCollapsed = !sidebarCollapsed;
SettingsService.UpdateSetting(nameof(AppSettings.SidebarCollapsed), sidebarCollapsed);
StateHasChanged(); // ? AGGIUNTO: Force immediate UI update
Console.WriteLine($"Sidebar toggled: collapsed={sidebarCollapsed}"); // ? AGGIUNTO: Debug log
}
```
**Cosa fa**:
- ? Forza il re-render immediato con `StateHasChanged()`
- ? Log nella console per debug
- ? Salva lo stato nelle impostazioni
### 2. **MainLayout.razor.css** - CSS Collapsed State
```css
::deep .trading-bot-layout.collapsed .sidebar-brand {
padding: 1.5rem 0.75rem !important;
justify-content: center !important;
}
::deep .trading-bot-layout.collapsed .brand-logo {
width: 3rem !important;
height: 3rem !important;
}
```
**Cosa fa**:
- ? Riduce padding quando collapsed
- ? Centra il logo
- ? Riduce dimensione logo
## Come Testare
### 1. **Riavvia l'Applicazione**
```sh
# Stop server
Ctrl + C
# Clean build
dotnet clean
dotnet build
# Run
dotnet run
```
### 2. **Forza Cache Refresh**
```
Ctrl + Shift + R (o Ctrl + F5)
```
### 3. **Test del Button**
1. Apri l'applicazione
2. Click sul pulsante `[?]` in alto a destra nella sidebar
3. Verifica che:
- La sidebar si riduca a ~80px
- Rimangano solo le icone
- Il logo si ridimensioni
- L'area contenuto si espanda
### 4. **Verifica Console**
Apri DevTools (F12) ? Console
Dovresti vedere:
```
Sidebar toggled: collapsed=true (quando minimizzi)
Sidebar toggled: collapsed=false (quando espandi)
```
## Comportamento Atteso
### Expanded (280px)
```
????????????????????????????
? [??] TradingBot [?] ? ? Button qui
? ? ATTIVO ?
????????????????????????????
? ?? Dashboard ?
? ?? Strategie ?
? ?? Asset ?
? ... ?
????????????????????????????
```
### Collapsed (80px)
```
???????
? [??]? ? Logo centrato
? ?
???????
? ?? ? ? Solo icone
? ?? ? centrate
? ?? ?
? ... ?
???????
```
## Debug Checklist
Se il button ancora non funziona:
- [ ] Build riuscito senza errori?
- [ ] Cache browser pulita (Ctrl+Shift+R)?
- [ ] Console mostra i log "Sidebar toggled"?
- [ ] Ispeziona elemento: classe "collapsed" viene applicata al container?
- [ ] CSS caricato correttamente (verifica in Network tab)?
## Verifica con DevTools
### 1. Ispeziona il Container
```
F12 ? Elements tab
Cerca: <div class="trading-bot-layout ...">
```
**Quando Expanded**:
```html
<div class="trading-bot-layout expanded">
```
**Quando Collapsed**:
```html
<div class="trading-bot-layout collapsed">
```
### 2. Verifica CSS Applicato
```
Click su .modern-sidebar
Guarda tab "Computed" ? width
```
**Expanded**: `width: 280px`
**Collapsed**: `width: 80px`
### 3. Verifica Button Click
```
Console tab
Click sul button [?]
```
**Output atteso**:
```
Sidebar toggled: collapsed=true
```
## Possibili Cause se Non Funziona
### 1. CSS Non Caricato
**Sintomo**: Button visibile ma sidebar non cambia dimensione
**Soluzione**:
```sh
dotnet clean
dotnet build
Ctrl + Shift + R nel browser
```
### 2. JavaScript/SignalR Bloccato
**Sintomo**: Click non produce effetto, nessun log
**Soluzione**:
```
F12 ? Console ? Cerca errori
Riavvia server Blazor
```
### 3. Settings Service Non Salva
**Sintomo**: Toggle funziona ma non persiste al reload
**Soluzione**:
Verifica file:
```
%LocalAppData%/TradingBot/appsettings.json
```
Cerca proprietà:
```json
{
"SidebarCollapsed": true/false
}
```
### 4. Binding Non Aggiornato
**Sintomo**: Classe non cambia nel DOM
**Soluzione**:
Aggiungi nel code block:
```csharp
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
Console.WriteLine($"Initial collapsed state: {sidebarCollapsed}");
}
}
```
## File Modificati
1. ? `Components/Layout/MainLayout.razor`
- Aggiunto `StateHasChanged()`
- Aggiunto debug log
2. ? `Components/Layout/MainLayout.razor.css`
- CSS specifico per collapsed state
- Riduzione dimensioni logo
## Test Manuale Step-by-Step
1. ? Avvia app: `dotnet run`
2. ? Apri browser: `https://localhost:[PORT]`
3. ? Hard refresh: `Ctrl + Shift + R`
4. ? Apri DevTools: `F12`
5. ? Vai su Console tab
6. ? Click sul button `[?]`
7. ? Verifica log: "Sidebar toggled: collapsed=true"
8. ? Verifica visuale: Sidebar si riduce
9. ? Click di nuovo: "Sidebar toggled: collapsed=false"
10. ? Verifica visuale: Sidebar si espande
## Expected Log Output
```
// Al caricamento
Initial collapsed state: false
// Click 1 (Minimize)
Sidebar toggled: collapsed=true
// Click 2 (Expand)
Sidebar toggled: collapsed=false
// Click 3 (Minimize)
Sidebar toggled: collapsed=true
```
## CSS Transitions
Con le modifiche applicate, le transizioni dovrebbero essere smooth:
```css
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
```
**Durata**: 300ms
**Easing**: Smooth cubic-bezier
## Support
Se dopo questi passaggi il button ancora non funziona:
1. ?? Screenshot della sidebar
2. ?? Log della Console (F12)
3. ?? Ispeziona elemento HTML del container
4. ?? Contenuto di appsettings.json
---
**Status**: ? Fix Applicato
**Build**: ? Successful
**Test**: ? Pending User Verification
+346
View File
@@ -0,0 +1,346 @@
using TradingBot.Models;
using System.Text.Json;
namespace TradingBot.Services;
/// <summary>
/// Service for managing trading indicators configuration and signals
/// </summary>
public class IndicatorsService
{
private readonly Dictionary<string, IndicatorConfig> _indicators = new();
private readonly Dictionary<string, Dictionary<string, IndicatorStatus>> _indicatorStatus = new();
private readonly List<IndicatorSignal> _recentSignals = new();
private readonly string _configPath;
private const int MaxSignals = 100;
public event Action? OnIndicatorsChanged;
public event Action<IndicatorSignal>? OnSignalGenerated;
public IndicatorsService()
{
_configPath = Path.Combine(Directory.GetCurrentDirectory(), "data", "indicators-config.json");
InitializeDefaultIndicators();
LoadConfiguration();
}
private void InitializeDefaultIndicators()
{
_indicators["rsi"] = new IndicatorConfig
{
Id = "rsi",
Name = "RSI",
Description = "Relative Strength Index - Misura la forza del trend",
Type = IndicatorType.RSI,
IsEnabled = true,
Period = 14,
OverboughtThreshold = 70,
OversoldThreshold = 30
};
_indicators["macd"] = new IndicatorConfig
{
Id = "macd",
Name = "MACD",
Description = "Moving Average Convergence Divergence - Identifica cambi di trend",
Type = IndicatorType.MACD,
IsEnabled = true,
FastPeriod = 12,
SlowPeriod = 26,
SignalPeriod = 9
};
_indicators["sma_20"] = new IndicatorConfig
{
Id = "sma_20",
Name = "SMA 20",
Description = "Simple Moving Average 20 periodi - Trend a breve termine",
Type = IndicatorType.SMA,
IsEnabled = true,
Period = 20
};
_indicators["sma_50"] = new IndicatorConfig
{
Id = "sma_50",
Name = "SMA 50",
Description = "Simple Moving Average 50 periodi - Trend a medio termine",
Type = IndicatorType.SMA,
IsEnabled = true,
Period = 50
};
_indicators["ema_12"] = new IndicatorConfig
{
Id = "ema_12",
Name = "EMA 12",
Description = "Exponential Moving Average 12 periodi - Reattivo ai cambiamenti",
Type = IndicatorType.EMA,
IsEnabled = true,
Period = 12
};
_indicators["bollinger"] = new IndicatorConfig
{
Id = "bollinger",
Name = "Bollinger Bands",
Description = "Bande di Bollinger - Misura volatilità e livelli estremi",
Type = IndicatorType.BollingerBands,
IsEnabled = true,
Period = 20
};
_indicators["stochastic"] = new IndicatorConfig
{
Id = "stochastic",
Name = "Stochastic",
Description = "Oscillatore Stocastico - Identifica momenti di inversione",
Type = IndicatorType.Stochastic,
IsEnabled = false,
Period = 14,
OverboughtThreshold = 80,
OversoldThreshold = 20
};
}
/// <summary>
/// Get all indicator configurations
/// </summary>
public IReadOnlyDictionary<string, IndicatorConfig> GetIndicators()
{
return _indicators;
}
/// <summary>
/// Get enabled indicators only
/// </summary>
public IEnumerable<IndicatorConfig> GetEnabledIndicators()
{
return _indicators.Values.Where(i => i.IsEnabled);
}
/// <summary>
/// Update indicator configuration
/// </summary>
public void UpdateIndicator(string id, IndicatorConfig config)
{
_indicators[id] = config;
SaveConfiguration();
OnIndicatorsChanged?.Invoke();
}
/// <summary>
/// Toggle indicator on/off
/// </summary>
public void ToggleIndicator(string id, bool enabled)
{
if (_indicators.TryGetValue(id, out var indicator))
{
indicator.IsEnabled = enabled;
SaveConfiguration();
OnIndicatorsChanged?.Invoke();
}
}
/// <summary>
/// Update indicator status for a symbol
/// </summary>
public void UpdateIndicatorStatus(string indicatorId, string symbol, IndicatorStatus status)
{
if (!_indicatorStatus.ContainsKey(symbol))
{
_indicatorStatus[symbol] = new Dictionary<string, IndicatorStatus>();
}
_indicatorStatus[symbol][indicatorId] = status;
}
/// <summary>
/// Get indicator status for a symbol
/// </summary>
public IndicatorStatus? GetIndicatorStatus(string indicatorId, string symbol)
{
if (_indicatorStatus.TryGetValue(symbol, out var symbolIndicators))
{
symbolIndicators.TryGetValue(indicatorId, out var status);
return status;
}
return null;
}
/// <summary>
/// Get all indicator statuses for a symbol
/// </summary>
public IEnumerable<IndicatorStatus> GetSymbolIndicators(string symbol)
{
if (_indicatorStatus.TryGetValue(symbol, out var symbolIndicators))
{
return symbolIndicators.Values;
}
return Enumerable.Empty<IndicatorStatus>();
}
/// <summary>
/// Generate and record a signal
/// </summary>
public void GenerateSignal(IndicatorSignal signal)
{
_recentSignals.Insert(0, signal);
// Maintain max size
while (_recentSignals.Count > MaxSignals)
{
_recentSignals.RemoveAt(_recentSignals.Count - 1);
}
OnSignalGenerated?.Invoke(signal);
}
/// <summary>
/// Get recent signals
/// </summary>
public IReadOnlyList<IndicatorSignal> GetRecentSignals(int count = 20)
{
return _recentSignals.Take(count).ToList().AsReadOnly();
}
/// <summary>
/// Get signals for a specific symbol
/// </summary>
public IReadOnlyList<IndicatorSignal> GetSymbolSignals(string symbol, int count = 20)
{
return _recentSignals
.Where(s => s.Symbol.Equals(symbol, StringComparison.OrdinalIgnoreCase))
.Take(count)
.ToList()
.AsReadOnly();
}
/// <summary>
/// Analyze indicators and generate trading recommendation
/// </summary>
public TradingRecommendation AnalyzeIndicators(string symbol)
{
var recommendation = new TradingRecommendation
{
Symbol = symbol,
Timestamp = DateTime.UtcNow
};
var symbolIndicators = GetSymbolIndicators(symbol).ToList();
if (!symbolIndicators.Any())
{
recommendation.Action = "HOLD";
recommendation.Confidence = 0;
recommendation.Reason = "Indicatori non disponibili";
return recommendation;
}
int buySignals = 0;
int sellSignals = 0;
int totalEnabled = GetEnabledIndicators().Count();
foreach (var status in symbolIndicators)
{
if (!_indicators.TryGetValue(status.IndicatorId, out var config) || !config.IsEnabled)
continue;
switch (status.Condition)
{
case MarketCondition.Oversold:
case MarketCondition.Bullish:
buySignals++;
recommendation.SupportingIndicators.Add($"{config.Name}: {status.Recommendation}");
break;
case MarketCondition.Overbought:
case MarketCondition.Bearish:
sellSignals++;
recommendation.SupportingIndicators.Add($"{config.Name}: {status.Recommendation}");
break;
}
}
// Determine action based on signals
if (buySignals > sellSignals && buySignals >= totalEnabled * 0.6m)
{
recommendation.Action = "BUY";
recommendation.Confidence = (decimal)buySignals / totalEnabled * 100;
recommendation.Reason = $"{buySignals}/{totalEnabled} indicatori suggeriscono acquisto";
}
else if (sellSignals > buySignals && sellSignals >= totalEnabled * 0.6m)
{
recommendation.Action = "SELL";
recommendation.Confidence = (decimal)sellSignals / totalEnabled * 100;
recommendation.Reason = $"{sellSignals}/{totalEnabled} indicatori suggeriscono vendita";
}
else
{
recommendation.Action = "HOLD";
recommendation.Confidence = 50;
recommendation.Reason = "Segnali contrastanti - attendere conferma";
}
return recommendation;
}
private void SaveConfiguration()
{
try
{
var directory = Path.GetDirectoryName(_configPath);
if (directory != null && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var json = JsonSerializer.Serialize(_indicators, new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(_configPath, json);
}
catch (Exception ex)
{
Console.WriteLine($"Error saving indicators configuration: {ex.Message}");
}
}
private void LoadConfiguration()
{
try
{
if (File.Exists(_configPath))
{
var json = File.ReadAllText(_configPath);
var loaded = JsonSerializer.Deserialize<Dictionary<string, IndicatorConfig>>(json);
if (loaded != null)
{
foreach (var kvp in loaded)
{
_indicators[kvp.Key] = kvp.Value;
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error loading indicators configuration: {ex.Message}");
}
}
}
/// <summary>
/// Trading recommendation based on multiple indicators
/// </summary>
public class TradingRecommendation
{
public string Symbol { get; set; } = string.Empty;
public DateTime Timestamp { get; set; }
public string Action { get; set; } = "HOLD"; // BUY, SELL, HOLD
public decimal Confidence { get; set; }
public string Reason { get; set; } = string.Empty;
public List<string> SupportingIndicators { get; set; } = new();
}
+122
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();
}
}
@@ -11,19 +11,38 @@ public class SimpleMovingAverageStrategy : ITradingStrategy
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> historicalPrices)
{
if (historicalPrices.Count < _longPeriod)
// Filtra null e valori invalidi prima di usare la lista
if (historicalPrices == null || historicalPrices.Count < _longPeriod)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Price = historicalPrices.LastOrDefault()?.Price ?? 0,
Price = historicalPrices?.LastOrDefault()?.Price ?? 0,
Reason = "Dati insufficienti per l'analisi",
Timestamp = DateTime.UtcNow
});
}
var recentPrices = historicalPrices.OrderByDescending(p => p.Timestamp).Take(_longPeriod).ToList();
// Filtra oggetti null e ordina
var recentPrices = historicalPrices
.Where(p => p != null && p.Price > 0)
.OrderByDescending(p => p.Timestamp)
.Take(_longPeriod)
.ToList();
// Verifica ancora la count dopo il filtro
if (recentPrices.Count < _longPeriod)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Price = recentPrices.LastOrDefault()?.Price ?? 0,
Reason = "Dati insufficienti per l'analisi dopo il filtro",
Timestamp = DateTime.UtcNow
});
}
var shortSMA = recentPrices.Take(_shortPeriod).Average(p => p.Price);
var longSMA = recentPrices.Average(p => p.Price);
+18
View File
@@ -70,4 +70,22 @@ public static class TechnicalAnalysis
return (macdLine, signalLine, histogram);
}
public static (decimal upper, decimal middle, decimal lower) CalculateBollingerBands(List<decimal> prices, int period = 20, decimal standardDeviations = 2)
{
if (prices.Count < period) return (0, 0, 0);
var recentPrices = prices.TakeLast(period).ToList();
var sma = recentPrices.Average();
// Calculate standard deviation
var squaredDifferences = recentPrices.Select(p => (double)Math.Pow((double)(p - sma), 2));
var variance = squaredDifferences.Average();
var stdDev = (decimal)Math.Sqrt(variance);
var upper = sma + (standardDeviations * stdDev);
var lower = sma - (standardDeviations * stdDev);
return (upper, sma, lower);
}
}
+164
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;
}
}
@@ -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);
}
}
+325 -17
View File
@@ -6,17 +6,24 @@ public class TradingBotService
{
private readonly IMarketDataService _marketDataService;
private readonly ITradingStrategy _strategy;
private readonly TradeHistoryService _historyService;
private readonly LoggingService _loggingService;
private readonly IndicatorsService _indicatorsService;
private readonly TradingStrategiesService _strategiesService;
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 +32,20 @@ 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,
IndicatorsService indicatorsService,
TradingStrategiesService strategiesService)
{
_marketDataService = marketDataService;
_strategy = strategy;
_historyService = historyService;
_loggingService = loggingService;
_indicatorsService = indicatorsService;
_strategiesService = strategiesService;
Status.CurrentStrategy = strategy.Name;
// Subscribe to simulated market updates if available
@@ -38,6 +55,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 +127,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 +185,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 +200,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 +218,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)
@@ -162,17 +255,23 @@ public class TradingBotService
try
{
var enabledSymbols = _assetConfigs.Values
.Where(c => c.IsEnabled)
.Where(c => c != null && c.IsEnabled)
.Select(c => c.Symbol)
.Where(s => !string.IsNullOrWhiteSpace(s))
.ToList();
if (enabledSymbols.Count == 0) return;
var prices = await _marketDataService.GetMarketPricesAsync(enabledSymbols);
if (prices == null) return;
foreach (var price in prices)
{
await ProcessAssetUpdate(price);
if (price != null)
{
await ProcessAssetUpdate(price);
}
}
UpdateGlobalStatistics();
@@ -180,11 +279,15 @@ public class TradingBotService
catch (Exception ex)
{
Console.WriteLine($"Error in UpdateAsync: {ex.Message}");
Console.WriteLine($"Stack trace: {ex.StackTrace}");
}
}
private async Task ProcessAssetUpdate(MarketPrice price)
{
if (price == null || price.Price <= 0)
return;
if (!_assetConfigs.TryGetValue(price.Symbol, out var config) || !config.IsEnabled)
return;
@@ -216,10 +319,14 @@ public class TradingBotService
// Generate trading signal
var signal = await _strategy.AnalyzeAsync(price.Symbol, _priceHistory[price.Symbol]);
OnSignalGenerated?.Invoke(signal);
// Execute trades based on strategy and configuration
await EvaluateAndExecuteTrade(price.Symbol, signal, price, config);
if (signal != null)
{
OnSignalGenerated?.Invoke(signal);
// Execute trades based on strategy and configuration
await EvaluateAndExecuteTrade(price.Symbol, signal, price, config);
}
}
}
@@ -250,7 +357,7 @@ public class TradingBotService
if (tradeAmount >= config.MinTradeAmount)
{
ExecuteBuy(symbol, price.Price, tradeAmount, config);
await ExecuteBuyAsync(symbol, price.Price, tradeAmount, config);
}
}
// Sell logic
@@ -267,14 +374,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;
@@ -300,14 +405,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;
@@ -330,19 +445,35 @@ 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)
{
var history = _priceHistory[symbol];
if (history.Count < 26) return;
if (!_priceHistory.TryGetValue(symbol, out var history) || history == null || history.Count < 26)
return;
var prices = history.Select(p => p.Price).ToList();
var prices = history
.Where(p => p != null && p.Price > 0)
.Select(p => p.Price)
.ToList();
if (prices.Count < 26)
return;
var rsi = TechnicalAnalysis.CalculateRSI(prices);
var (macd, signal, histogram) = TechnicalAnalysis.CalculateMACD(prices);
@@ -359,6 +490,136 @@ public class TradingBotService
_indicators[symbol] = indicators;
OnIndicatorsUpdated?.Invoke(symbol, indicators);
// Update IndicatorsService statuses
UpdateIndicatorStatuses(symbol, indicators, prices);
}
private void UpdateIndicatorStatuses(string symbol, TechnicalIndicators indicators, List<decimal> prices)
{
// Update RSI status
var rsiConfig = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "rsi");
if (rsiConfig?.IsEnabled == true)
{
var rsiStatus = new IndicatorStatus
{
IndicatorId = "rsi",
Symbol = symbol,
CurrentValue = indicators.RSI,
Condition = indicators.RSI > (rsiConfig.OverboughtThreshold ?? 70) ? MarketCondition.Overbought :
indicators.RSI < (rsiConfig.OversoldThreshold ?? 30) ? MarketCondition.Oversold :
MarketCondition.Neutral,
Recommendation = indicators.RSI > (rsiConfig.OverboughtThreshold ?? 70) ? "Possibile vendita" :
indicators.RSI < (rsiConfig.OversoldThreshold ?? 30) ? "Possibile acquisto" :
"Attendi conferma"
};
_indicatorsService.UpdateIndicatorStatus("rsi", symbol, rsiStatus);
// Generate signal if crossing threshold
if (indicators.RSI < 30)
{
_indicatorsService.GenerateSignal(new IndicatorSignal
{
IndicatorId = "rsi",
IndicatorName = "RSI",
Symbol = symbol,
Type = SignalType.Buy,
Strength = indicators.RSI < 20 ? SignalStrength.VeryStrong : SignalStrength.Strong,
Message = $"RSI in zona ipervenduto: {indicators.RSI:F2}",
Value = indicators.RSI
});
}
else if (indicators.RSI > 70)
{
_indicatorsService.GenerateSignal(new IndicatorSignal
{
IndicatorId = "rsi",
IndicatorName = "RSI",
Symbol = symbol,
Type = SignalType.Sell,
Strength = indicators.RSI > 80 ? SignalStrength.VeryStrong : SignalStrength.Strong,
Message = $"RSI in zona ipercomprato: {indicators.RSI:F2}",
Value = indicators.RSI
});
}
}
// Update MACD status
var macdConfig = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "macd");
if (macdConfig?.IsEnabled == true)
{
var macdStatus = new IndicatorStatus
{
IndicatorId = "macd",
Symbol = symbol,
CurrentValue = indicators.MACD,
Condition = indicators.Histogram > 0 ? MarketCondition.Bullish : MarketCondition.Bearish,
Recommendation = indicators.Histogram > 0 ? "Trend rialzista" : "Trend ribassista"
};
_indicatorsService.UpdateIndicatorStatus("macd", symbol, macdStatus);
// Generate signal on crossover
if (Math.Abs(indicators.Histogram) < 0.5m) // Near crossover
{
_indicatorsService.GenerateSignal(new IndicatorSignal
{
IndicatorId = "macd",
IndicatorName = "MACD",
Symbol = symbol,
Type = indicators.Histogram > 0 ? SignalType.Buy : SignalType.Sell,
Strength = SignalStrength.Moderate,
Message = $"MACD {(indicators.Histogram > 0 ? "bullish" : "bearish")} crossover",
Value = indicators.MACD
});
}
}
// Update SMA statuses
var currentPrice = prices.Last();
var sma20Config = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "sma_20");
if (sma20Config?.IsEnabled == true && prices.Count >= 20)
{
var sma20 = prices.TakeLast(20).Average();
var sma20Status = new IndicatorStatus
{
IndicatorId = "sma_20",
Symbol = symbol,
CurrentValue = sma20,
Condition = currentPrice > sma20 ? MarketCondition.Bullish : MarketCondition.Bearish,
Recommendation = currentPrice > sma20 ? "Prezzo sopra media" : "Prezzo sotto media"
};
_indicatorsService.UpdateIndicatorStatus("sma_20", symbol, sma20Status);
}
var sma50Config = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "sma_50");
if (sma50Config?.IsEnabled == true && prices.Count >= 50)
{
var sma50 = prices.TakeLast(50).Average();
var sma50Status = new IndicatorStatus
{
IndicatorId = "sma_50",
Symbol = symbol,
CurrentValue = sma50,
Condition = currentPrice > sma50 ? MarketCondition.Bullish : MarketCondition.Bearish,
Recommendation = currentPrice > sma50 ? "Trend rialzista medio termine" : "Trend ribassista medio termine"
};
_indicatorsService.UpdateIndicatorStatus("sma_50", symbol, sma50Status);
}
// Update EMA status
var ema12Config = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "ema_12");
if (ema12Config?.IsEnabled == true)
{
var ema12Status = new IndicatorStatus
{
IndicatorId = "ema_12",
Symbol = symbol,
CurrentValue = indicators.EMA12,
Condition = currentPrice > indicators.EMA12 ? MarketCondition.Bullish : MarketCondition.Bearish,
Recommendation = currentPrice > indicators.EMA12 ? "Trend positivo" : "Trend negativo"
};
_indicatorsService.UpdateIndicatorStatus("ema_12", symbol, ema12Status);
}
}
private void UpdateAssetStatistics(string symbol, Trade trade, decimal? realizedProfit = null)
@@ -482,7 +743,54 @@ public class TradingBotService
public MarketPrice? GetLatestPrice(string symbol)
{
if (string.IsNullOrWhiteSpace(symbol))
return null;
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;
}
/// <summary>
/// Manually close a position
/// </summary>
public async Task ClosePositionManuallyAsync(string symbol)
{
if (!_activePositions.TryGetValue(symbol, out var position))
{
throw new InvalidOperationException($"No active position found for {symbol}");
}
if (!_assetConfigs.TryGetValue(symbol, out var config))
{
throw new InvalidOperationException($"Asset configuration not found for {symbol}");
}
// Get current market price
var latestPrice = GetLatestPrice(symbol);
if (latestPrice == null || latestPrice.Price <= 0)
{
throw new InvalidOperationException($"Cannot get current price for {symbol}");
}
// Execute sell
await ExecuteSellAsync(symbol, latestPrice.Price, config.CurrentHoldings, config);
}
}
+564
View File
@@ -0,0 +1,564 @@
using TradingBot.Models;
namespace TradingBot.Services;
/// <summary>
/// RSI-based trading strategy
/// Buy when RSI < oversold threshold, Sell when RSI > overbought threshold
/// </summary>
public class RSIStrategy : ITradingStrategy
{
public string Name => "RSI Strategy";
public string Description => "Strategia basata su Relative Strength Index. Compra in zona ipervenduto, vende in zona ipercomprato.";
private readonly decimal _oversoldThreshold;
private readonly decimal _overboughtThreshold;
private readonly int _period;
public RSIStrategy(decimal oversoldThreshold = 30, decimal overboughtThreshold = 70, int period = 14)
{
_oversoldThreshold = oversoldThreshold;
_overboughtThreshold = overboughtThreshold;
_period = period;
}
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
{
if (priceHistory == null || priceHistory.Count < _period + 1)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 0,
Reason = "Dati insufficienti per RSI"
});
}
var prices = priceHistory.Select(p => p.Price).ToList();
var rsi = TechnicalAnalysis.CalculateRSI(prices, _period);
if (rsi < _oversoldThreshold)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Buy,
Confidence = (decimal)(((_oversoldThreshold - rsi) / _oversoldThreshold) * 100),
Reason = $"RSI in zona ipervenduto: {rsi:F2}"
});
}
else if (rsi > _overboughtThreshold)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Sell,
Confidence = (decimal)(((rsi - _overboughtThreshold) / (100 - _overboughtThreshold)) * 100),
Reason = $"RSI in zona ipercomprato: {rsi:F2}"
});
}
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 50,
Reason = $"RSI neutro: {rsi:F2}"
});
}
}
/// <summary>
/// MACD-based trading strategy
/// Buy on bullish crossover, Sell on bearish crossover
/// </summary>
public class MACDStrategy : ITradingStrategy
{
public string Name => "MACD Strategy";
public string Description => "Strategia basata su MACD crossover. Compra su incrocio rialzista, vende su incrocio ribassista.";
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
{
if (priceHistory == null || priceHistory.Count < 26)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 0,
Reason = "Dati insufficienti per MACD"
});
}
var prices = priceHistory.Select(p => p.Price).ToList();
var (macd, signal, histogram) = TechnicalAnalysis.CalculateMACD(prices);
if (histogram > 0 && Math.Abs(histogram) > 0.1m)
{
var confidence = Math.Min((decimal)(Math.Abs((double)histogram) * 10), 100);
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Buy,
Confidence = confidence,
Reason = $"MACD crossover rialzista, histogram: {histogram:F2}"
});
}
else if (histogram < 0 && Math.Abs(histogram) > 0.1m)
{
var confidence = Math.Min((decimal)(Math.Abs((double)histogram) * 10), 100);
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Sell,
Confidence = confidence,
Reason = $"MACD crossover ribassista, histogram: {histogram:F2}"
});
}
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 30,
Reason = "MACD vicino a equilibrio"
});
}
}
/// <summary>
/// Bollinger Bands strategy
/// Buy when price touches lower band, Sell when price touches upper band
/// </summary>
public class BollingerBandsStrategy : ITradingStrategy
{
public string Name => "Bollinger Bands";
public string Description => "Compra quando il prezzo tocca la banda inferiore, vende alla banda superiore.";
private readonly int _period;
private readonly decimal _standardDeviations;
public BollingerBandsStrategy(int period = 20, decimal standardDeviations = 2)
{
_period = period;
_standardDeviations = standardDeviations;
}
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
{
if (priceHistory == null || priceHistory.Count < _period)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 0,
Reason = "Dati insufficienti per Bollinger Bands"
});
}
var prices = priceHistory.Select(p => p.Price).ToList();
var (upper, middle, lower) = TechnicalAnalysis.CalculateBollingerBands(prices, _period, _standardDeviations);
var currentPrice = prices.Last();
var distanceToLower = ((currentPrice - lower) / lower) * 100;
var distanceToUpper = ((upper - currentPrice) / upper) * 100;
if (distanceToLower < 2) // Within 2% of lower band
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Buy,
Confidence = 80,
Reason = $"Prezzo vicino banda inferiore: ${currentPrice:F2} vs ${lower:F2}"
});
}
else if (distanceToUpper < 2) // Within 2% of upper band
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Sell,
Confidence = 80,
Reason = $"Prezzo vicino banda superiore: ${currentPrice:F2} vs ${upper:F2}"
});
}
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 40,
Reason = "Prezzo tra le bande"
});
}
}
/// <summary>
/// Mean Reversion strategy
/// Assumes price will return to average
/// </summary>
public class MeanReversionStrategy : ITradingStrategy
{
public string Name => "Mean Reversion";
public string Description => "Sfrutta il ritorno del prezzo verso la media. Compra sotto media, vende sopra media.";
private readonly int _period;
private readonly decimal _deviationThreshold;
public MeanReversionStrategy(int period = 20, decimal deviationThreshold = 5)
{
_period = period;
_deviationThreshold = deviationThreshold;
}
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
{
if (priceHistory == null || priceHistory.Count < _period)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 0,
Reason = "Dati insufficienti"
});
}
var prices = priceHistory.Select(p => p.Price).TakeLast(_period).ToList();
var mean = prices.Average();
var currentPrice = prices.Last();
var deviation = ((currentPrice - mean) / mean) * 100;
if (deviation < -_deviationThreshold)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Buy,
Confidence = Math.Min((decimal)Math.Abs((double)deviation) * 10, 100),
Reason = $"Prezzo {deviation:F2}% sotto media, probabile rimbalzo"
});
}
else if (deviation > _deviationThreshold)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Sell,
Confidence = Math.Min((decimal)Math.Abs((double)deviation) * 10, 100),
Reason = $"Prezzo {deviation:F2}% sopra media, probabile correzione"
});
}
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 50,
Reason = "Prezzo vicino alla media"
});
}
}
/// <summary>
/// Momentum strategy
/// Follows strong trends
/// </summary>
public class MomentumStrategy : ITradingStrategy
{
public string Name => "Momentum";
public string Description => "Segue i trend forti. Compra su momentum positivo, vende su momentum negativo.";
private readonly int _period;
public MomentumStrategy(int period = 10)
{
_period = period;
}
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
{
if (priceHistory == null || priceHistory.Count < _period + 5)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 0,
Reason = "Dati insufficienti"
});
}
var prices = priceHistory.Select(p => p.Price).ToList();
var currentPrice = prices.Last();
var pastPrice = prices[^_period];
var momentum = ((currentPrice - pastPrice) / pastPrice) * 100;
// Calculate rate of change
var recentPrices = prices.TakeLast(5).ToList();
var priceChanges = new List<decimal>();
for (int i = 1; i < recentPrices.Count; i++)
{
priceChanges.Add(((recentPrices[i] - recentPrices[i - 1]) / recentPrices[i - 1]) * 100);
}
var avgChange = priceChanges.Average();
if (momentum > 3 && avgChange > 0)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Buy,
Confidence = Math.Min((decimal)Math.Abs((double)momentum) * 15, 100),
Reason = $"Forte momentum positivo: {momentum:F2}%"
});
}
else if (momentum < -3 && avgChange < 0)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Sell,
Confidence = Math.Min((decimal)Math.Abs((double)momentum) * 15, 100),
Reason = $"Forte momentum negativo: {momentum:F2}%"
});
}
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 30,
Reason = "Momentum debole o neutro"
});
}
}
/// <summary>
/// EMA Crossover strategy (Golden Cross / Death Cross)
/// Buy when fast EMA crosses above slow EMA, Sell on opposite
/// </summary>
public class EMACrossoverStrategy : ITradingStrategy
{
public string Name => "EMA Crossover";
public string Description => "Golden Cross/Death Cross. Compra quando EMA veloce supera EMA lenta.";
private readonly int _fastPeriod;
private readonly int _slowPeriod;
public EMACrossoverStrategy(int fastPeriod = 12, int slowPeriod = 26)
{
_fastPeriod = fastPeriod;
_slowPeriod = slowPeriod;
}
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
{
if (priceHistory == null || priceHistory.Count < _slowPeriod + 5)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 0,
Reason = "Dati insufficienti"
});
}
var prices = priceHistory.Select(p => p.Price).ToList();
var fastEMA = TechnicalAnalysis.CalculateEMA(prices, _fastPeriod);
var slowEMA = TechnicalAnalysis.CalculateEMA(prices, _slowPeriod);
// Calculate previous EMAs to detect crossover
var prevPrices = prices.Take(prices.Count - 1).ToList();
var prevFastEMA = TechnicalAnalysis.CalculateEMA(prevPrices, _fastPeriod);
var prevSlowEMA = TechnicalAnalysis.CalculateEMA(prevPrices, _slowPeriod);
var currentDiff = fastEMA - slowEMA;
var prevDiff = prevFastEMA - prevSlowEMA;
// Golden Cross (bullish)
if (currentDiff > 0 && prevDiff <= 0)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Buy,
Confidence = 85,
Reason = $"Golden Cross! EMA{_fastPeriod} crossed above EMA{_slowPeriod}"
});
}
// Death Cross (bearish)
else if (currentDiff < 0 && prevDiff >= 0)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Sell,
Confidence = 85,
Reason = $"Death Cross! EMA{_fastPeriod} crossed below EMA{_slowPeriod}"
});
}
// Trend continuation
else if (currentDiff > 0)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 60,
Reason = "EMA fast sopra slow - trend rialzista confermato"
});
}
else
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 40,
Reason = "EMA fast sotto slow - trend ribassista confermato"
});
}
}
}
/// <summary>
/// Scalping strategy for short-term gains
/// </summary>
public class ScalpingStrategy : ITradingStrategy
{
public string Name => "Scalping";
public string Description => "Strategia per guadagni rapidi a breve termine. Alta frequenza, piccoli profitti.";
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
{
if (priceHistory == null || priceHistory.Count < 10)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 0,
Reason = "Dati insufficienti"
});
}
var recentPrices = priceHistory.Select(p => p.Price).TakeLast(10).ToList();
var currentPrice = recentPrices.Last();
var shortMA = recentPrices.TakeLast(3).Average();
var mediumMA = recentPrices.TakeLast(7).Average();
// Calculate short-term volatility
var priceChanges = new List<decimal>();
for (int i = 1; i < recentPrices.Count; i++)
{
priceChanges.Add(Math.Abs(recentPrices[i] - recentPrices[i - 1]));
}
var avgVolatility = priceChanges.Average();
var recentChange = Math.Abs(currentPrice - recentPrices[^2]);
// Quick reversal detection
if (currentPrice < shortMA && shortMA < mediumMA && recentChange > avgVolatility * 1.5m)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Buy,
Confidence = 70,
Reason = "Possibile rimbalzo rapido"
});
}
else if (currentPrice > shortMA && shortMA > mediumMA && recentChange > avgVolatility * 1.5m)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Sell,
Confidence = 70,
Reason = "Possibile correzione rapida"
});
}
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 30,
Reason = "Attesa opportunità scalping"
});
}
}
/// <summary>
/// Breakout strategy
/// Trades on price breaking resistance/support levels
/// </summary>
public class BreakoutStrategy : ITradingStrategy
{
public string Name => "Breakout";
public string Description => "Compra su rottura resistenza, vende su rottura supporto. Cattura breakout significativi.";
private readonly int _lookbackPeriod;
public BreakoutStrategy(int lookbackPeriod = 20)
{
_lookbackPeriod = lookbackPeriod;
}
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
{
if (priceHistory == null || priceHistory.Count < _lookbackPeriod)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 0,
Reason = "Dati insufficienti"
});
}
var prices = priceHistory.Select(p => p.Price).ToList();
var recentPrices = prices.TakeLast(_lookbackPeriod).ToList();
var currentPrice = prices.Last();
var resistance = recentPrices.Max();
var support = recentPrices.Min();
var range = resistance - support;
// Breakout above resistance
if (currentPrice > resistance * 1.01m) // 1% above previous high
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Buy,
Confidence = 80,
Reason = $"Breakout sopra resistenza: ${resistance:F2}"
});
}
// Breakdown below support
else if (currentPrice < support * 0.99m) // 1% below previous low
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Sell,
Confidence = 80,
Reason = $"Breakdown sotto supporto: ${support:F2}"
});
}
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 40,
Reason = $"Prezzo in range ${support:F2} - ${resistance:F2}"
});
}
}
@@ -0,0 +1,486 @@
using TradingBot.Models;
using System.Text.Json;
namespace TradingBot.Services;
/// <summary>
/// Service for managing trading strategies and their assignments to assets
/// </summary>
public class TradingStrategiesService
{
private readonly Dictionary<string, StrategyInfo> _availableStrategies = new();
private readonly Dictionary<string, ITradingStrategy> _strategyInstances = new();
private readonly Dictionary<string, AssetStrategyMapping> _assetMappings = new();
private readonly Dictionary<string, TradingEngineStatus> _engineStatuses = new();
private readonly string _configPath;
public event Action? OnMappingsChanged;
public event Action<string, TradingDecision>? OnDecisionMade;
public TradingStrategiesService()
{
_configPath = Path.Combine(Directory.GetCurrentDirectory(), "data", "strategy-mappings.json");
InitializeStrategies();
LoadMappings();
}
private void InitializeStrategies()
{
// RSI Strategy
var rsiStrategy = new RSIStrategy();
_strategyInstances["rsi"] = rsiStrategy;
_availableStrategies["rsi"] = new StrategyInfo
{
Id = "rsi",
Name = "RSI Strategy",
Description = "Relative Strength Index - Compra in ipervenduto, vende in ipercomprato",
Category = "Oscillator",
RiskLevel = StrategyRisk.Medium,
RecommendedTimeFrame = TimeFrame.ShortTerm,
RequiredIndicators = new List<string> { "RSI" },
Parameters = new Dictionary<string, ParameterInfo>
{
["oversoldThreshold"] = new() { Name = "Oversold", Description = "Soglia ipervenduto", Type = ParameterType.Decimal, DefaultValue = 30m, MinValue = 10m, MaxValue = 40m },
["overboughtThreshold"] = new() { Name = "Overbought", Description = "Soglia ipercomprato", Type = ParameterType.Decimal, DefaultValue = 70m, MinValue = 60m, MaxValue = 90m },
["period"] = new() { Name = "Period", Description = "Periodo di calcolo", Type = ParameterType.Integer, DefaultValue = 14, MinValue = 5, MaxValue = 30 }
}
};
// MACD Strategy
var macdStrategy = new MACDStrategy();
_strategyInstances["macd"] = macdStrategy;
_availableStrategies["macd"] = new StrategyInfo
{
Id = "macd",
Name = "MACD Strategy",
Description = "Moving Average Convergence Divergence - Crossover rialzista/ribassista",
Category = "Momentum",
RiskLevel = StrategyRisk.Medium,
RecommendedTimeFrame = TimeFrame.MediumTerm,
RequiredIndicators = new List<string> { "MACD", "Signal", "Histogram" }
};
// Bollinger Bands Strategy
var bollingerStrategy = new BollingerBandsStrategy();
_strategyInstances["bollinger"] = bollingerStrategy;
_availableStrategies["bollinger"] = new StrategyInfo
{
Id = "bollinger",
Name = "Bollinger Bands",
Description = "Compra vicino banda inferiore, vende vicino banda superiore",
Category = "Volatility",
RiskLevel = StrategyRisk.Low,
RecommendedTimeFrame = TimeFrame.MediumTerm,
RequiredIndicators = new List<string> { "Bollinger Bands" },
Parameters = new Dictionary<string, ParameterInfo>
{
["period"] = new() { Name = "Period", Description = "Periodo SMA", Type = ParameterType.Integer, DefaultValue = 20, MinValue = 10, MaxValue = 50 },
["standardDeviations"] = new() { Name = "Std Dev", Description = "Deviazioni standard", Type = ParameterType.Decimal, DefaultValue = 2m, MinValue = 1m, MaxValue = 3m }
}
};
// Mean Reversion Strategy
var meanReversionStrategy = new MeanReversionStrategy();
_strategyInstances["mean_reversion"] = meanReversionStrategy;
_availableStrategies["mean_reversion"] = new StrategyInfo
{
Id = "mean_reversion",
Name = "Mean Reversion",
Description = "Sfrutta il ritorno del prezzo verso la media",
Category = "Contrarian",
RiskLevel = StrategyRisk.High,
RecommendedTimeFrame = TimeFrame.ShortTerm,
RequiredIndicators = new List<string> { "SMA" },
Parameters = new Dictionary<string, ParameterInfo>
{
["period"] = new() { Name = "Period", Description = "Periodo media", Type = ParameterType.Integer, DefaultValue = 20, MinValue = 10, MaxValue = 50 },
["deviationThreshold"] = new() { Name = "Deviation %", Description = "Soglia deviazione", Type = ParameterType.Decimal, DefaultValue = 5m, MinValue = 2m, MaxValue = 10m }
}
};
// Momentum Strategy
var momentumStrategy = new MomentumStrategy();
_strategyInstances["momentum"] = momentumStrategy;
_availableStrategies["momentum"] = new StrategyInfo
{
Id = "momentum",
Name = "Momentum",
Description = "Segue i trend forti basati su momentum",
Category = "Trend",
RiskLevel = StrategyRisk.Medium,
RecommendedTimeFrame = TimeFrame.MediumTerm,
RequiredIndicators = new List<string> { "Price Change" },
Parameters = new Dictionary<string, ParameterInfo>
{
["period"] = new() { Name = "Period", Description = "Periodo momentum", Type = ParameterType.Integer, DefaultValue = 10, MinValue = 5, MaxValue = 20 }
}
};
// EMA Crossover Strategy
var emaCrossoverStrategy = new EMACrossoverStrategy();
_strategyInstances["ema_crossover"] = emaCrossoverStrategy;
_availableStrategies["ema_crossover"] = new StrategyInfo
{
Id = "ema_crossover",
Name = "EMA Crossover",
Description = "Golden Cross/Death Cross con EMA",
Category = "Trend",
RiskLevel = StrategyRisk.Low,
RecommendedTimeFrame = TimeFrame.LongTerm,
RequiredIndicators = new List<string> { "EMA12", "EMA26" },
Parameters = new Dictionary<string, ParameterInfo>
{
["fastPeriod"] = new() { Name = "Fast EMA", Description = "Periodo EMA veloce", Type = ParameterType.Integer, DefaultValue = 12, MinValue = 8, MaxValue = 20 },
["slowPeriod"] = new() { Name = "Slow EMA", Description = "Periodo EMA lenta", Type = ParameterType.Integer, DefaultValue = 26, MinValue = 20, MaxValue = 50 }
}
};
// Scalping Strategy
var scalpingStrategy = new ScalpingStrategy();
_strategyInstances["scalping"] = scalpingStrategy;
_availableStrategies["scalping"] = new StrategyInfo
{
Id = "scalping",
Name = "Scalping",
Description = "Guadagni rapidi a breve termine",
Category = "Short-term",
RiskLevel = StrategyRisk.VeryHigh,
RecommendedTimeFrame = TimeFrame.ShortTerm,
RequiredIndicators = new List<string> { "Short MA", "Volatility" }
};
// Breakout Strategy
var breakoutStrategy = new BreakoutStrategy();
_strategyInstances["breakout"] = breakoutStrategy;
_availableStrategies["breakout"] = new StrategyInfo
{
Id = "breakout",
Name = "Breakout",
Description = "Cattura rotture di resistenza/supporto",
Category = "Volatility",
RiskLevel = StrategyRisk.High,
RecommendedTimeFrame = TimeFrame.MediumTerm,
RequiredIndicators = new List<string> { "Resistance", "Support" },
Parameters = new Dictionary<string, ParameterInfo>
{
["lookbackPeriod"] = new() { Name = "Lookback", Description = "Periodo lookback", Type = ParameterType.Integer, DefaultValue = 20, MinValue = 10, MaxValue = 50 }
}
};
}
/// <summary>
/// Get all available strategies
/// </summary>
public IReadOnlyDictionary<string, StrategyInfo> GetAvailableStrategies()
{
return _availableStrategies;
}
/// <summary>
/// Get strategies by category
/// </summary>
public IEnumerable<StrategyInfo> GetStrategiesByCategory(string category)
{
return _availableStrategies.Values.Where(s => s.Category == category);
}
/// <summary>
/// Get asset mapping
/// </summary>
public AssetStrategyMapping? GetAssetMapping(string symbol)
{
_assetMappings.TryGetValue(symbol, out var mapping);
return mapping;
}
/// <summary>
/// Get all asset mappings
/// </summary>
public IReadOnlyDictionary<string, AssetStrategyMapping> GetAllMappings()
{
return _assetMappings;
}
/// <summary>
/// Assign strategies to an asset
/// </summary>
public void AssignStrategiesToAsset(string symbol, string assetName, List<string> strategyIds)
{
var mapping = new AssetStrategyMapping
{
Symbol = symbol,
AssetName = assetName,
StrategyIds = strategyIds,
IsActive = false,
ActivatedAt = DateTime.UtcNow
};
_assetMappings[symbol] = mapping;
// Initialize engine status
if (!_engineStatuses.ContainsKey(symbol))
{
_engineStatuses[symbol] = new TradingEngineStatus
{
Symbol = symbol,
IsRunning = false,
ActiveStrategies = 0
};
}
SaveMappings();
OnMappingsChanged?.Invoke();
}
/// <summary>
/// Remove strategy from asset
/// </summary>
public void RemoveStrategyFromAsset(string symbol, string strategyId)
{
if (_assetMappings.TryGetValue(symbol, out var mapping))
{
mapping.StrategyIds.Remove(strategyId);
if (mapping.StrategyIds.Count == 0)
{
mapping.IsActive = false;
}
SaveMappings();
OnMappingsChanged?.Invoke();
}
}
/// <summary>
/// Activate trading for an asset
/// </summary>
public void ActivateAsset(string symbol)
{
if (_assetMappings.TryGetValue(symbol, out var mapping) && mapping.StrategyIds.Count > 0)
{
mapping.IsActive = true;
mapping.ActivatedAt = DateTime.UtcNow;
mapping.DeactivatedAt = null;
if (_engineStatuses.TryGetValue(symbol, out var status))
{
status.IsRunning = true;
status.ActiveStrategies = mapping.StrategyIds.Count;
}
SaveMappings();
OnMappingsChanged?.Invoke();
}
}
/// <summary>
/// Deactivate trading for an asset
/// </summary>
public void DeactivateAsset(string symbol)
{
if (_assetMappings.TryGetValue(symbol, out var mapping))
{
mapping.IsActive = false;
mapping.DeactivatedAt = DateTime.UtcNow;
if (_engineStatuses.TryGetValue(symbol, out var status))
{
status.IsRunning = false;
}
SaveMappings();
OnMappingsChanged?.Invoke();
}
}
/// <summary>
/// Analyze market with assigned strategies
/// </summary>
public async Task<TradingDecision> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
{
if (!_assetMappings.TryGetValue(symbol, out var mapping) || !mapping.IsActive)
{
return new TradingDecision
{
Symbol = symbol,
Decision = SignalType.Hold,
Confidence = 0,
Reason = "Trading non attivo per questo asset"
};
}
var signals = new List<StrategySignal>();
int buyVotes = 0, sellVotes = 0, holdVotes = 0;
decimal totalConfidence = 0;
// Execute all assigned strategies
foreach (var strategyId in mapping.StrategyIds)
{
if (_strategyInstances.TryGetValue(strategyId, out var strategy))
{
var signal = await strategy.AnalyzeAsync(symbol, priceHistory);
var strategySignal = new StrategySignal
{
StrategyId = strategyId,
StrategyName = _availableStrategies[strategyId].Name,
Signal = signal,
GeneratedAt = DateTime.UtcNow
};
signals.Add(strategySignal);
switch (signal.Type)
{
case SignalType.Buy:
buyVotes++;
break;
case SignalType.Sell:
sellVotes++;
break;
case SignalType.Hold:
holdVotes++;
break;
}
totalConfidence += signal.Confidence;
}
}
// Update engine status
if (_engineStatuses.TryGetValue(symbol, out var status))
{
status.RecentSignals = signals;
status.LastSignalTime = DateTime.UtcNow;
}
// Aggregate decision
var decision = MakeDecision(symbol, signals, buyVotes, sellVotes, holdVotes, totalConfidence);
if (status != null)
{
status.LastDecision = decision;
}
OnDecisionMade?.Invoke(symbol, decision);
return decision;
}
private TradingDecision MakeDecision(string symbol, List<StrategySignal> signals, int buyVotes, int sellVotes, int holdVotes, decimal totalConfidence)
{
var totalVotes = buyVotes + sellVotes + holdVotes;
if (totalVotes == 0)
{
return new TradingDecision
{
Symbol = symbol,
Decision = SignalType.Hold,
Confidence = 0,
Reason = "Nessuna strategia attiva"
};
}
var avgConfidence = totalConfidence / totalVotes;
SignalType finalDecision;
string reason;
List<string> supporting = new();
List<string> opposing = new();
// Decision logic: majority voting with confidence threshold
if (buyVotes > sellVotes && buyVotes >= totalVotes * 0.6m)
{
finalDecision = SignalType.Buy;
reason = $"{buyVotes}/{totalVotes} strategie suggeriscono acquisto";
supporting = signals.Where(s => s.Signal.Type == SignalType.Buy).Select(s => s.StrategyName).ToList();
opposing = signals.Where(s => s.Signal.Type != SignalType.Buy).Select(s => s.StrategyName).ToList();
}
else if (sellVotes > buyVotes && sellVotes >= totalVotes * 0.6m)
{
finalDecision = SignalType.Sell;
reason = $"{sellVotes}/{totalVotes} strategie suggeriscono vendita";
supporting = signals.Where(s => s.Signal.Type == SignalType.Sell).Select(s => s.StrategyName).ToList();
opposing = signals.Where(s => s.Signal.Type != SignalType.Sell).Select(s => s.StrategyName).ToList();
}
else
{
finalDecision = SignalType.Hold;
reason = "Segnali contrastanti - attendi conferma";
supporting = signals.Where(s => s.Signal.Type == SignalType.Hold).Select(s => s.StrategyName).ToList();
}
return new TradingDecision
{
Symbol = symbol,
Decision = finalDecision,
Confidence = avgConfidence,
Reason = reason,
BuyVotes = buyVotes,
SellVotes = sellVotes,
HoldVotes = holdVotes,
SupportingStrategies = supporting,
OpposingStrategies = opposing
};
}
/// <summary>
/// Get trading engine status for asset
/// </summary>
public TradingEngineStatus? GetEngineStatus(string symbol)
{
_engineStatuses.TryGetValue(symbol, out var status);
return status;
}
private void SaveMappings()
{
try
{
var directory = Path.GetDirectoryName(_configPath);
if (directory != null && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var json = JsonSerializer.Serialize(_assetMappings, new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(_configPath, json);
}
catch (Exception ex)
{
Console.WriteLine($"Error saving strategy mappings: {ex.Message}");
}
}
private void LoadMappings()
{
try
{
if (File.Exists(_configPath))
{
var json = File.ReadAllText(_configPath);
var loaded = JsonSerializer.Deserialize<Dictionary<string, AssetStrategyMapping>>(json);
if (loaded != null)
{
foreach (var kvp in loaded)
{
_assetMappings[kvp.Key] = kvp.Value;
// Initialize engine status
_engineStatuses[kvp.Key] = new TradingEngineStatus
{
Symbol = kvp.Key,
IsRunning = kvp.Value.IsActive,
ActiveStrategies = kvp.Value.StrategyIds.Count
};
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error loading strategy mappings: {ex.Message}");
}
}
}
+129 -2
View File
@@ -1,10 +1,137 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
</PropertyGroup>
<!-- Docker Publishing -->
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>.</DockerfileContext>
<!--
Versioning - Semantic Versioning (https://semver.org/)
Format: MAJOR.MINOR.PATCH
- MAJOR: Breaking changes (incompatible API changes)
- MINOR: New features (backward-compatible)
- PATCH: Bug fixes (backward-compatible)
Esempio workflow:
- 1.0.0 -> 1.0.1 (bug fix)
- 1.0.1 -> 1.1.0 (new feature)
- 1.1.0 -> 2.0.0 (breaking change)
-->
<Version>1.5.2</Version>
<AssemblyVersion>1.5.2.0</AssemblyVersion>
<FileVersion>1.5.2.0</FileVersion>
<!-- Assembly Information -->
<Product>TradingBot</Product>
<Description>Automated Crypto Trading Bot with Blazor UI</Description>
<Copyright>Copyright © 2024 Alby96</Copyright>
<Company>Alby96</Company>
<Authors>Alby96</Authors>
<!-- Build Metadata -->
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<Deterministic>true</Deterministic>
<InformationalVersion>$(Version)+$(GITHUB_SHA)</InformationalVersion>
<!-- Gitea Registry -->
<ContainerRegistry>gitea.encke-hake.ts.net</ContainerRegistry>
<ContainerRepository>alby96/encelado/tradingbot</ContainerRepository>
</PropertyGroup>
<!-- Folders for organization -->
<ItemGroup>
<Folder Include="Properties\PublishProfiles\" />
</ItemGroup>
<!--
Post-Publish Target: Push to Gitea Container Registry
Crea automaticamente 3 tag per ogni publish:
1. latest - Sempre ultima versione
2. {Version} - Versione semantica (es. 1.3.0)
3. {Version}-{Date} - Versione + timestamp (es. 1.3.0-20241222)
Condizioni di attivazione:
- Configuration = Release
- Non dentro container Docker
- Profilo Docker in uso
- Docker daemon running (verifica disponibilità)
-->
<Target Name="PushToGiteaRegistry" AfterTargets="Publish" Condition="'$(Configuration)' == 'Release' And '$(DOTNET_RUNNING_IN_CONTAINER)' != 'true' And '$(DockerPublish)' == 'true'">
<PropertyGroup>
<GiteaImage>$(ContainerRegistry)/$(ContainerRepository)</GiteaImage>
<BuildDate>$([System.DateTime]::Now.ToString("yyyyMMdd"))</BuildDate>
<VersionedTag>$(Version)-$(BuildDate)</VersionedTag>
</PropertyGroup>
<!-- Check if Docker is available -->
<Exec Command="docker version" ContinueOnError="true" IgnoreExitCode="true">
<Output TaskParameter="ExitCode" PropertyName="DockerExitCode" />
</Exec>
<!-- Only proceed if Docker is available -->
<PropertyGroup>
<DockerAvailable Condition="'$(DockerExitCode)' == '0'">true</DockerAvailable>
<DockerAvailable Condition="'$(DockerExitCode)' != '0'">false</DockerAvailable>
</PropertyGroup>
<Message Condition="'$(DockerAvailable)' != 'true'" Importance="high" Text="" />
<Message Condition="'$(DockerAvailable)' != 'true'" Importance="high" Text="========================================" />
<Message Condition="'$(DockerAvailable)' != 'true'" Importance="high" Text="⚠️ Docker Not Available" />
<Message Condition="'$(DockerAvailable)' != 'true'" Importance="high" Text="========================================" />
<Message Condition="'$(DockerAvailable)' != 'true'" Importance="high" Text="" />
<Message Condition="'$(DockerAvailable)' != 'true'" Importance="high" Text="Docker daemon is not running or Docker Desktop is not started." />
<Message Condition="'$(DockerAvailable)' != 'true'" Importance="high" Text="" />
<Message Condition="'$(DockerAvailable)' != 'true'" Importance="high" Text="📋 To enable Gitea Registry push:" />
<Message Condition="'$(DockerAvailable)' != 'true'" Importance="high" Text=" 1. Start Docker Desktop" />
<Message Condition="'$(DockerAvailable)' != 'true'" Importance="high" Text=" 2. Wait for Docker to be ready" />
<Message Condition="'$(DockerAvailable)' != 'true'" Importance="high" Text=" 3. Run Publish again" />
<Message Condition="'$(DockerAvailable)' != 'true'" Importance="high" Text="" />
<Message Condition="'$(DockerAvailable)' != 'true'" Importance="high" Text="✅ Compilation successful - application ready to run locally" />
<Message Condition="'$(DockerAvailable)' != 'true'" Importance="high" Text="⏭️ Skipping Gitea Registry push" />
<Message Condition="'$(DockerAvailable)' != 'true'" Importance="high" Text="" />
<!-- Only execute Docker commands if Docker is available -->
<CallTarget Condition="'$(DockerAvailable)' == 'true'" Targets="ExecuteGiteaPush" />
</Target>
<Target Name="ExecuteGiteaPush">
<PropertyGroup>
<GiteaImage>$(ContainerRegistry)/$(ContainerRepository)</GiteaImage>
<BuildDate>$([System.DateTime]::Now.ToString("yyyyMMdd"))</BuildDate>
<VersionedTag>$(Version)-$(BuildDate)</VersionedTag>
</PropertyGroup>
<Message Importance="high" Text="" />
<Message Importance="high" Text="========================================" />
<Message Importance="high" Text="🐳 Gitea Container Registry Push" />
<Message Importance="high" Text="========================================" />
<Message Importance="high" Text="" />
<Message Importance="high" Text="📦 Version: $(Version)" />
<Message Importance="high" Text="📅 Build Date: $(BuildDate)" />
<Message Importance="high" Text="" />
<Message Importance="high" Text="🏷️ Creating tags..." />
<!-- Tag 1: latest -->
<Exec Command="docker tag tradingbot:latest $(GiteaImage):latest" />
<Message Importance="high" Text=" ✅ latest" />
<!-- Tag 2: Version (semantic) -->
<Exec Command="docker tag tradingbot:latest $(GiteaImage):$(Version)" />
<Message Importance="high" Text=" ✅ $(Version)" />
<!-- Tag 3: Version-Date -->
<Exec Command="docker tag tradingbot:latest $(GiteaImage):$(VersionedTag)" />
<Message Importance="high" Text=" ✅ $(VersionedTag)" />
<Message Importance="high" Text="" />
<Message Importance="high" Text="🚀 Pushing to $(ContainerRegistry)..." />
<!-- Push all tags -->
<Exec Command="docker push $(GiteaImage):latest" />
<Message Importance="high" Text=" ✅ Pushed: latest" />
<Exec Command="docker push $(GiteaImage):$(Version)" />
<Message Importance="high" Text=" ✅ Pushed: $(Version)" />
<Exec Command="docker push $(GiteaImage):$(VersionedTag)" />
<Message Importance="high" Text=" ✅ Pushed: $(VersionedTag)" />
<Message Importance="high" Text="" />
<Message Importance="high" Text="========================================" />
<Message Importance="high" Text="✅ Successfully pushed to Gitea Registry!" />
<Message Importance="high" Text="========================================" />
<Message Importance="high" Text="" />
<Message Importance="high" Text="📦 Published images:" />
<Message Importance="high" Text=" - $(GiteaImage):latest" />
<Message Importance="high" Text=" - $(GiteaImage):$(Version)" />
<Message Importance="high" Text=" - $(GiteaImage):$(VersionedTag)" />
<Message Importance="high" Text="" />
<Message Importance="high" Text="🔗 Verify at:" />
<Message Importance="high" Text=" https://$(ContainerRegistry)/Alby96/Encelado/-/packages" />
<Message Importance="high" Text="" />
</Target>
</Project>
-517
View File
@@ -1,517 +0,0 @@
# ?? DEPLOYMENT GUIDE - Unraid + Gitea + Docker
Guida completa per deployare TradingBot su **Unraid** usando **Gitea** come sistema di controllo versione.
---
## ?? PREREQUISITI
### Su Unraid
- ? Docker installato (Community Applications)
- ? Gitea installato e configurato
- ? Accesso SSH abilitato
- ? Portainer installato (opzionale ma consigliato)
### Sul PC di Sviluppo
- ? Git installato
- ? Accesso al server Unraid
- ? Repository Gitea configurato
---
## ?? STEP 1: Configurazione Gitea
### 1.1 Crea Repository su Gitea
```sh
# Accedi a Gitea (esempio)
http://192.168.30.23:3000
# Crea nuovo repository
Nome: TradingBot
Descrizione: Automated Crypto Trading Bot
Privato: ? (consigliato)
```
### 1.2 Configura Git Remote (già fatto)
```sh
cd /path/to/TradingBot
# Verifica remote (dovresti già averlo)
git remote -v
# Output:
# origin https://192.168.30.23/Alby96/Encelado (fetch)
# origin https://192.168.30.23/Alby96/Encelado (push)
# Se non configurato:
git remote add origin https://192.168.30.23/Alby96/Encelado
```
### 1.3 Push del Codice
```sh
# Commit delle modifiche Docker
git add Dockerfile docker-compose.yml .dockerignore
git add build-docker.sh build-docker.bat
git add UNRAID_DEPLOYMENT.md
git commit -m "Add Docker support and Unraid deployment"
# Push su Gitea
git push origin main
```
---
## ?? STEP 2: Deployment su Unraid
### Metodo A: Via Portainer (CONSIGLIATO)
#### 2.1 Accedi a Portainer
```
http://[UNRAID-IP]:9000
```
#### 2.2 Crea Stack
1. **Stacks** ? **Add stack**
2. **Name**: `tradingbot`
3. **Build method**: `Git Repository`
4. **Repository URL**: `https://192.168.30.23/Alby96/Encelado`
5. **Repository reference**: `refs/heads/main`
6. **Compose path**: `TradingBot/docker-compose.yml`
7. **Authentication**:
- Username: `Alby96`
- Personal access token: (crea su Gitea)
#### 2.3 Environment Variables (opzionali)
```
TZ=Europe/Rome
ASPNETCORE_ENVIRONMENT=Production
```
#### 2.4 Deploy
Click **Deploy the stack**
---
### Metodo B: Via SSH + Docker Compose
#### 2.1 Connettiti a Unraid via SSH
```sh
ssh root@[UNRAID-IP]
```
#### 2.2 Crea Directory Progetto
```sh
# Vai nella directory appropriata
cd /mnt/user/appdata/
# Crea directory per TradingBot
mkdir -p tradingbot
cd tradingbot
```
#### 2.3 Clone Repository da Gitea
```sh
# Clone del repository
git clone https://192.168.30.23/Alby96/Encelado.git .
# Entra nella directory del progetto
cd TradingBot
```
#### 2.4 Build e Run
```sh
# Build immagine Docker
docker-compose build
# Avvia container
docker-compose up -d
# Verifica logs
docker-compose logs -f
```
---
### Metodo C: Via Unraid Docker Template
#### 2.1 Crea Template Personalizzato
Crea file: `/boot/config/plugins/dockerMan/templates-user/my-TradingBot.xml`
```xml
<?xml version="1.0"?>
<Container version="2">
<Name>TradingBot</Name>
<Repository>tradingbot:latest</Repository>
<Registry>https://192.168.30.23:5000/</Registry>
<Network>bridge</Network>
<MyIP/>
<Shell>sh</Shell>
<Privileged>false</Privileged>
<Support>https://192.168.30.23/Alby96/Encelado</Support>
<Project>https://192.168.30.23/Alby96/Encelado</Project>
<Overview>Automated Crypto Trading Bot con strategie personalizzabili</Overview>
<Category>Tools:</Category>
<WebUI>http://[IP]:[PORT:8080]</WebUI>
<TemplateURL/>
<Icon>https://raw.githubusercontent.com/docker-library/docs/master/dotnet/logo.png</Icon>
<ExtraParams/>
<PostArgs/>
<CPUset/>
<DateInstalled>1234567890</DateInstalled>
<DonateText/>
<DonateLink/>
<Requires/>
<Config Name="WebUI Port" Target="8080" Default="8080" Mode="tcp" Description="Port per accedere alla WebUI" Type="Port" Display="always" Required="true" Mask="false">8080</Config>
<Config Name="Data Volume" Target="/app/data" Default="/mnt/user/appdata/tradingbot/data" Mode="rw" Description="Volume per dati persistenti" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/tradingbot/data</Config>
<Config Name="Timezone" Target="TZ" Default="Europe/Rome" Mode="" Description="Timezone" Type="Variable" Display="always" Required="false" Mask="false">Europe/Rome</Config>
</Container>
```
#### 2.2 Usa Template da Unraid UI
1. Docker ? Add Container
2. Select: `TradingBot`
3. Configure ports and volumes
4. Apply
---
## ?? STEP 3: Aggiornamenti Automatici
### 3.1 Setup Webhook su Gitea (opzionale)
#### Su Gitea:
```
Settings ? Webhooks ? Add Webhook
Payload URL: http://[UNRAID-IP]:9000/api/webhooks/[webhook-id]
Content type: application/json
Events: Push events
```
#### Su Portainer:
```
Stacks ? tradingbot ? Webhooks ? Create webhook
Copia URL generato
```
### 3.2 Script di Aggiornamento Manuale
```sh
#!/bin/bash
# update-tradingbot.sh
cd /mnt/user/appdata/tradingbot/TradingBot
# Pull latest changes
git pull origin main
# Rebuild
docker-compose down
docker-compose build
docker-compose up -d
echo "? TradingBot aggiornato!"
```
Salva come: `/root/scripts/update-tradingbot.sh`
```sh
chmod +x /root/scripts/update-tradingbot.sh
```
---
## ?? STEP 4: Monitoraggio e Gestione
### 4.1 Verifica Status Container
```sh
# Via SSH
docker ps | grep tradingbot
# Logs
docker logs tradingbot -f
# Stats
docker stats tradingbot
```
### 4.2 Accesso WebUI
```
http://[UNRAID-IP]:8080
```
### 4.3 Health Check
```sh
curl http://[UNRAID-IP]:8080/health
```
---
## ?? STEP 5: Configurazione Avanzata
### 5.1 Reverse Proxy (opzionale)
Se usi **Nginx Proxy Manager** o **Traefik**:
#### docker-compose.yml aggiornato:
```yaml
services:
tradingbot:
# ... altre configurazioni
labels:
- "traefik.enable=true"
- "traefik.http.routers.tradingbot.rule=Host(`trading.tuodominio.com`)"
- "traefik.http.services.tradingbot.loadbalancer.server.port=8080"
networks:
- traefik_proxy
- tradingbot-network
networks:
traefik_proxy:
external: true
```
### 5.2 Backup Automatico
Script backup: `/root/scripts/backup-tradingbot.sh`
```sh
#!/bin/bash
BACKUP_DIR="/mnt/user/backups/tradingbot"
DATE=$(date +%Y%m%d_%H%M%S)
mkdir -p $BACKUP_DIR
# Backup volume dati
docker run --rm \
-v tradingbot_tradingbot-data:/data \
-v $BACKUP_DIR:/backup \
alpine tar czf /backup/tradingbot-data-$DATE.tar.gz -C /data .
echo "? Backup completato: tradingbot-data-$DATE.tar.gz"
# Mantieni solo ultimi 7 backup
find $BACKUP_DIR -name "tradingbot-data-*.tar.gz" -mtime +7 -delete
```
Aggiungi a crontab:
```sh
crontab -e
# Backup giornaliero alle 3 AM
0 3 * * * /root/scripts/backup-tradingbot.sh
```
---
## ?? TROUBLESHOOTING
### Container non si avvia
```sh
# Check logs
docker logs tradingbot
# Check network
docker network ls
docker network inspect tradingbot_tradingbot-network
# Rebuild da zero
docker-compose down -v
docker-compose build --no-cache
docker-compose up -d
```
### Problemi di permessi
```sh
# Fix ownership
docker exec tradingbot chown -R tradingbot:tradingbot /app/data
```
### Porta già in uso
```sh
# Trova processo che usa porta 8080
netstat -tulpn | grep 8080
# Cambia porta in docker-compose.yml
ports:
- "8081:8080" # Usa 8081 invece
```
### Out of Memory
Aumenta limits in docker-compose.yml:
```yaml
deploy:
resources:
limits:
memory: 2G # Da 1G a 2G
```
---
## ?? STEP 6: Registry Privato (opzionale)
### 6.1 Setup Docker Registry su Unraid
```sh
docker run -d \
-p 5000:5000 \
--restart=always \
--name registry \
-v /mnt/user/appdata/registry:/var/lib/registry \
registry:2
```
### 6.2 Build e Push
```sh
# Tag image
docker tag tradingbot:latest 192.168.30.23:5000/tradingbot:latest
# Push to registry
docker push 192.168.30.23:5000/tradingbot:latest
```
### 6.3 Deploy da Registry
```yaml
# docker-compose.yml
services:
tradingbot:
image: 192.168.30.23:5000/tradingbot:latest
# ... resto configurazione
```
---
## ?? SECURITY BEST PRACTICES
### 1. Non esporre porte pubblicamente
```sh
# Usa solo rete interna Unraid
# Accesso via VPN o Wireguard
```
### 2. SSL/TLS
```sh
# Usa reverse proxy con certificati SSL
# Let's Encrypt via Nginx Proxy Manager
```
### 3. Credenziali
```sh
# Non committare secrets in Git
# Usa Docker secrets o environment variables
```
### 4. Firewall
```sh
# Limita accesso solo a IP fidati
# Configura in Unraid Settings ? Network
```
---
## ?? MONITORING
### Grafana + Prometheus (opzionale)
```yaml
# monitoring-stack.yml
version: '3.8'
services:
prometheus:
image: prom/prometheus
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
grafana:
image: grafana/grafana
ports:
- "3001:3000"
volumes:
- grafana-data:/var/lib/grafana
volumes:
grafana-data:
```
---
## ?? CHECKLIST DEPLOYMENT
### Pre-Deployment
- [ ] Codice committed su Gitea
- [ ] Dockerfile testato localmente
- [ ] docker-compose.yml configurato
- [ ] .dockerignore presente
- [ ] Environment variables definite
### Deployment
- [ ] Repository clonato su Unraid
- [ ] Docker image built
- [ ] Container avviato correttamente
- [ ] WebUI accessibile
- [ ] Health check passing
### Post-Deployment
- [ ] Logs verificati (no errori)
- [ ] Dati persistono dopo restart
- [ ] Backup configurato
- [ ] Monitoring attivo
- [ ] Documentazione aggiornata
---
## ?? COMANDI UTILI
```sh
# Build
./build-docker.sh [tag]
# Start
docker-compose up -d
# Stop
docker-compose down
# Logs
docker-compose logs -f
# Restart
docker-compose restart
# Update
git pull && docker-compose up -d --build
# Clean
docker-compose down -v
docker system prune -a
```
---
## ?? RISORSE
- **Unraid Docs**: https://docs.unraid.net/
- **Docker Docs**: https://docs.docker.com/
- **Gitea Docs**: https://docs.gitea.io/
- **Portainer Docs**: https://docs.portainer.io/
---
**?? Deployment completato! Il tuo TradingBot è ora in produzione su Unraid!**
+1 -1
View File
@@ -2,7 +2,7 @@
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"Microsoft.AspNetCore": "Information"
}
}
}
-29
View File
@@ -1,29 +0,0 @@
@echo off
REM Script di build Docker per Windows
echo Building TradingBot Docker Image...
SET IMAGE_NAME=tradingbot
SET TAG=%1
IF "%TAG%"=="" SET TAG=latest
echo Building image: %IMAGE_NAME%:%TAG%
docker build -t %IMAGE_NAME%:%TAG% -f Dockerfile .
IF %ERRORLEVEL% NEQ 0 (
echo Build failed!
exit /b %ERRORLEVEL%
)
echo Build completed successfully!
REM Tag come latest se diverso
IF NOT "%TAG%"=="latest" (
echo Tagging as latest...
docker tag %IMAGE_NAME%:%TAG% %IMAGE_NAME%:latest
)
echo Done! Run with: docker-compose up -d
pause
-49
View File
@@ -1,49 +0,0 @@
#!/bin/bash
# Script di build Docker per TradingBot
set -e
echo "?? Building TradingBot Docker Image..."
# Colori per output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Variabili
IMAGE_NAME="tradingbot"
TAG="${1:-latest}"
REGISTRY="${DOCKER_REGISTRY:-}" # Opzionale: tuo registry privato
echo -e "${BLUE}?? Building image: ${IMAGE_NAME}:${TAG}${NC}"
# Build dell'immagine
docker build \
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--build-arg VCS_REF=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") \
-t ${IMAGE_NAME}:${TAG} \
-f Dockerfile \
.
echo -e "${GREEN}? Build completato con successo!${NC}"
# Tag con latest
if [ "$TAG" != "latest" ]; then
echo -e "${BLUE}??? Tagging as latest...${NC}"
docker tag ${IMAGE_NAME}:${TAG} ${IMAGE_NAME}:latest
fi
# Mostra info immagine
echo -e "${BLUE}?? Image info:${NC}"
docker images | grep ${IMAGE_NAME} | head -n 2
# Se registry è configurato, push
if [ ! -z "$REGISTRY" ]; then
echo -e "${BLUE}?? Pushing to registry: ${REGISTRY}${NC}"
docker tag ${IMAGE_NAME}:${TAG} ${REGISTRY}/${IMAGE_NAME}:${TAG}
docker push ${REGISTRY}/${IMAGE_NAME}:${TAG}
echo -e "${GREEN}? Pushed to registry!${NC}"
fi
echo -e "${GREEN}?? Done! Run with: docker-compose up -d${NC}"
+149
View File
@@ -0,0 +1,149 @@
# Version Bump Script
# Aggiorna automaticamente la versione nel .csproj e crea tag Git
param(
[Parameter(Mandatory=$true)]
[ValidateSet('major', 'minor', 'patch')]
[string]$BumpType,
[Parameter(Mandatory=$false)]
[string]$Message = ""
)
$ErrorActionPreference = "Stop"
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "?? TradingBot Version Bump" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# Path al .csproj
$csprojPath = Join-Path $PSScriptRoot "TradingBot.csproj"
if (-not (Test-Path $csprojPath)) {
Write-Host "? File TradingBot.csproj non trovato!" -ForegroundColor Red
exit 1
}
# Leggi versione corrente
Write-Host "?? Reading current version..." -ForegroundColor Yellow
[xml]$csproj = Get-Content $csprojPath
$currentVersion = $csproj.Project.PropertyGroup.Version
if (-not $currentVersion) {
Write-Host "? Version not found in .csproj!" -ForegroundColor Red
exit 1
}
Write-Host " Current version: $currentVersion" -ForegroundColor Gray
# Parse versione
$versionParts = $currentVersion.Split('.')
$major = [int]$versionParts[0]
$minor = [int]$versionParts[1]
$patch = [int]$versionParts[2]
# Bump versione
switch ($BumpType) {
'major' {
$major++
$minor = 0
$patch = 0
Write-Host " Bumping: MAJOR version (breaking changes)" -ForegroundColor Magenta
}
'minor' {
$minor++
$patch = 0
Write-Host " Bumping: MINOR version (new features)" -ForegroundColor Blue
}
'patch' {
$patch++
Write-Host " Bumping: PATCH version (bug fixes)" -ForegroundColor Green
}
}
$newVersion = "$major.$minor.$patch"
$newAssemblyVersion = "$major.$minor.$patch.0"
Write-Host " New version: $newVersion" -ForegroundColor Green
Write-Host ""
# Aggiorna .csproj
Write-Host "?? Updating TradingBot.csproj..." -ForegroundColor Yellow
# Update Version
$csproj.Project.PropertyGroup.Version = $newVersion
$csproj.Project.PropertyGroup.AssemblyVersion = $newAssemblyVersion
$csproj.Project.PropertyGroup.FileVersion = $newAssemblyVersion
# Save
$csproj.Save($csprojPath)
Write-Host " ? Updated Version: $newVersion" -ForegroundColor Green
Write-Host " ? Updated AssemblyVersion: $newAssemblyVersion" -ForegroundColor Green
Write-Host ""
# Git commit
Write-Host "?? Creating Git commit..." -ForegroundColor Yellow
$commitMessage = if ($Message) {
"chore: Bump version to $newVersion - $Message"
} else {
"chore: Bump version to $newVersion"
}
git add $csprojPath
if ($LASTEXITCODE -ne 0) {
Write-Host "? Git add failed!" -ForegroundColor Red
exit 1
}
git commit -m $commitMessage
if ($LASTEXITCODE -ne 0) {
Write-Host "? Git commit failed!" -ForegroundColor Red
exit 1
}
Write-Host " ? Committed: $commitMessage" -ForegroundColor Green
Write-Host ""
# Git tag
Write-Host "??? Creating Git tag..." -ForegroundColor Yellow
$tagName = "v$newVersion"
$tagMessage = if ($Message) {
"Release $newVersion - $Message"
} else {
"Release $newVersion"
}
git tag -a $tagName -m $tagMessage
if ($LASTEXITCODE -ne 0) {
Write-Host "? Git tag failed!" -ForegroundColor Red
exit 1
}
Write-Host " ? Created tag: $tagName" -ForegroundColor Green
Write-Host ""
# Summary
Write-Host "========================================" -ForegroundColor Green
Write-Host "? Version bumped successfully!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green
Write-Host ""
Write-Host "?? Version: $currentVersion ? $newVersion" -ForegroundColor White
Write-Host "??? Git tag: $tagName" -ForegroundColor White
Write-Host ""
Write-Host "?? Next steps:" -ForegroundColor Cyan
Write-Host " 1. Push commit: git push origin main" -ForegroundColor White
Write-Host " 2. Push tag: git push origin $tagName" -ForegroundColor White
Write-Host " 3. Build & Publish: Visual Studio ? Publish (Docker profile)" -ForegroundColor White
Write-Host " 4. Deploy on Unraid" -ForegroundColor White
Write-Host ""
Write-Host "?? Or push both at once:" -ForegroundColor Cyan
Write-Host " git push origin main --tags" -ForegroundColor White
Write-Host ""
@@ -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
+566
View File
@@ -0,0 +1,566 @@
# ?? TradingBot - Installazione su Unraid (Senza Portainer)
## ? Installazione Diretta da Gitea Registry
Puoi installare TradingBot direttamente dall'Unraid Docker Manager usando il tuo Gitea Registry!
---
## ?? PREREQUISITI
### 1. Login Gitea Registry su Unraid
SSH su Unraid:
```bash
ssh root@192.168.30.23 # O IP Tailscale
# Login al Gitea Registry
docker login gitea.encke-hake.ts.net
# Username: Alby96
# Password: [Personal Access Token Gitea]
```
**Output atteso**:
```
Login Succeeded ?
```
---
## ?? METODO 1: Template XML (Consigliato)
### Step 1: Copia Template su Unraid
```bash
# Su Unraid
mkdir -p /boot/config/plugins/dockerMan/templates-user
# Scarica template
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
```
### Step 2: Installa Container
1. Unraid WebUI ? **Docker** tab
2. Click **Add Container** (in fondo)
3. **Template**: Dropdown ? Seleziona **TradingBot**
4. Configura parametri:
**Parametri Base**:
- **Name**: `TradingBot` (già impostato)
- **Repository**: `gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest` (già impostato)
**Porta WebUI** (Visibile e Configurabile!):
- **WebUI HTTP Port**: `8888` (porta default - cambia se occupata)
- Questa è la porta HOST per accedere all'interfaccia web
- La porta CONTAINER rimane sempre 8080 (non modificare)
- Alternative comuni se 8888 occupata: `8881`, `9999`, `7777`
**Volume Dati** (?? IMPORTANTE per persistenza!):
- **AppData**: `/mnt/user/appdata/tradingbot` (già impostato)
- 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)
- **ASPNETCORE_URLS**: `http://+:8080` (non modificare - porta interna container)
- **TZ**: `Europe/Rome` (cambia per altro timezone)
5. Click **Apply**
Unraid farà:
- ? Pull immagine da Gitea Registry
- ? Crea container con nome "TradingBot"
- ? Configura porta WebUI (default 8888 ? host, 8080 ? container)
- ? **Crea volume persistente per dati**
- ? Start automatico
### Step 3: Accedi WebUI
**Metodo A: Click su WebUI Icon** ??
1. **Docker tab** ? Trova container **TradingBot**
2. Nella riga del container, a destra, vedrai l'**icona globe** ??
3. Click sull'icona ? Si apre automaticamente `http://192.168.30.23:8888`
**Metodo B: URL Manuale**
```
http://192.168.30.23:8888
```
(Sostituisci `8888` con la porta HOST che hai configurato)
?? **IMPORTANTE**: La porta nel browser deve essere quella HOST (8888 default), NON la porta container (8080)
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:
### Step 1: Unraid Docker Tab
1. **Docker** ? **Add Container**
2. **Name**: `TradingBot`
### Step 2: Configurazione Base
**Repository**:
```
gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest
```
**Network Type**: `Bridge`
**Console shell command**: `Shell`
### Step 3: Port Mapping (?? CRITICO!)
Click **Add another Path, Port, Variable, Label or Device**
**Config Type**: `Port`
- **Name**: `WebUI`
- **Container Port**: `8080`
- **Host Port**: `8888` ? **Cambia questa se occupata!**
- **Connection Type**: `TCP`
?? **Se questo mapping non viene configurato, la WebUI non sarà accessibile!**
### Step 4: Volume Mapping (?? IMPORTANTE per persistenza!)
Click **Add another Path, Port, Variable, Label o Device**
**Config Type**: `Path`
- **Name**: `AppData`
- **Container Path**: `/app/data`
- **Host Path**: `/mnt/user/appdata/tradingbot`
- **Access Mode**: `Read/Write`
### Step 5: Environment Variables
**ASPNETCORE_ENVIRONMENT**:
- **Name**: `ASPNETCORE_ENVIRONMENT`
- **Value**: `Production`
**ASPNETCORE_URLS**:
- **Name**: `ASPNETCORE_URLS`
- **Value**: `http://+:8080`
**TZ** (Opzionale):
- **Name**: `TZ`
- **Value**: `Europe/Rome` (o tuo timezone)
### Step 6: Apply
Click **Apply** in fondo alla pagina.
---
## ?? AGGIORNAMENTO CONTAINER
### Via Unraid Docker Tab
1. **Docker** ? Trova **TradingBot**
2. Click **icona Stop** (ferma container)
3. Click **Force Update** (icona update con freccia circolare)
4. Attendi pull dell'immagine aggiornata
5. Click **icona Start** (avvia container)
Unraid farà:
- ? Pull ultima immagine da Gitea
- ? Ricrea container con nuova immagine
- ? **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:
1. **Apps** ? Cerca "User Scripts"
2. Installa
Crea script update:
```bash
#!/bin/bash
# Nome: Update TradingBot
# Schedula: Weekly (ogni domenica alle 3:00 AM)
# Stop container
docker stop TradingBot
# Pull latest image
docker pull gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest
# Start container (Unraid ricrea automaticamente)
docker start TradingBot
# Notifica
/usr/local/emhttp/webGui/scripts/notify -s "TradingBot Update" -d "Container aggiornato con successo!" -i "normal"
echo "Update completato alle $(date)"
```
Schedula: Settimanale o manualmente quando serve.
---
## ??? CONFIGURAZIONE PORTA
### Cambiare Porta WebUI
La porta **default è 8888** (host) ? **8080** (container).
Se la porta 8888 è occupata o vuoi usarne un'altra:
#### **Via Template (Prima Installazione)**
Durante Step 2 dell'installazione:
- **WebUI HTTP Port**: Cambia da `8888` a porta desiderata (es. `8881`, `9999`, `7777`)
- ?? Modifica SOLO la porta HOST (a sinistra)
- NON modificare la porta Container (deve restare 8080)
#### **Via Edit (Container Esistente)**
1. **Docker tab** ? Container **TradingBot**
2. Click **Edit** (icona matita/wrench)
3. Trova sezione **Port Mappings**
4. Vedrai: **Host Port** `8888` ? **Container Port** `8080`
5. Modifica **Host Port** (es. da `8888` a `8881`)
6. **IMPORTANTE**: NON modificare **Container Port** (deve restare `8080`)
7. Click **Apply** in fondo
8. Container si riavvierà automaticamente
#### **Accesso con Nuova Porta**
```
http://192.168.30.23:NUOVA_PORTA_HOST
```
Esempio con porta `8881`:
```
http://192.168.30.23:8881
```
### Porte Comuni Disponibili
Se `8888` è occupata, prova queste alternative:
| Porta | Uso Comune | Probabilità Libera |
|-------|------------|-------------------|
| `8881` | Alternative port | ????? Alta |
| `9999` | Generic services | ???? Alta |
| `7777` | Custom apps | ???? Alta |
| `8889` | Next to 8888 | ??? Media |
| `3000` | Dev servers | ?? Bassa (spesso occupata) |
| `8080` | ? NON usare | Troppo comune, quasi sempre occupata |
**Check porta disponibile**:
```bash
# Su Unraid via SSH
netstat -tulpn | grep :8888
# Se restituisce risultato ? porta occupata
# Se vuoto ? porta libera ?
```
### Differenza HOST vs CONTAINER Port
?? **IMPORTANTE da capire**:
```
HOST Port (8888) ? CONTAINER Port (8080)
?? Porta su Unraid ?? Porta interna Docker
?? Quella nel BROWSER ?? Fissa, NON modificare
?? Configurabile ?? Hardcoded nell'app
?? Esempio: 8888 ?? Sempre 8080
```
**Esempio configurazione corretta**:
```
Browser: http://192.168.30.23:8888
?? Usa porta HOST
Docker: 8888 (host) ? 8080 (container)
?? Mapping ?? App interna
```
**Cosa NON fare**:
- ? Cambiare porta Container da 8080 a altro
- ? Modificare `ASPNETCORE_URLS` (deve restare `http://+:8080`)
- ? Usare porta Host 8080 (conflitto con container)
**Cosa puoi fare**:
- ? Cambiare porta Host da 8888 a qualsiasi altra libera
- ? Usare porta Host diversa per ogni app
- ? Accedere con `http://IP:PORTA_HOST`
---
## ?? QUICK START COMPLETO
**Setup in 3 minuti**:
```bash
# 1. Login (una volta)
docker login gitea.encke-hake.ts.net
# 2. Download template
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
# 3. Install via UI
# Docker tab ? Add Container ? TradingBot template ? Apply
# 4. Access WebUI
# Metodo A: Click icona ?? nella Docker tab
# Metodo B: http://192.168.30.23:8888
```
**?? TradingBot pronto su Unraid!**
---
## ?? Nota sulla Porta
**Default**: Porta HOST `8888` (invece di 8080)
**Perché 8888?**
- Porta 8080 è troppo comune e spesso occupata
- 8888 è quasi sempre libera su Unraid
- Facile da ricordare (quattro 8)
- WebUI icon funziona automaticamente
**Se 8888 è occupata**: Cambia in fase di installazione o dopo via Edit
---
## ?? ACCESSO WEBUI
### Locale (Unraid LAN)
```
http://192.168.30.23:8888
```
Sostituisci:
- `192.168.30.23` con IP del tuo Unraid
- `8888` con porta HOST configurata (se diversa)
### Via Tailscale
Se hai configurato Tailscale su Unraid:
```
http://unraid.encke-hake.ts.net:8888
```
### Via Hostname Unraid
Se hai configurato hostname:
```
http://tower:8888
```
(Sostituisci `tower` con hostname del tuo Unraid e `8888` con porta configurata)
### Reverse Proxy (Accesso HTTPS)
Se usi **Nginx Proxy Manager** o **Swag**:
```nginx
# Nginx Proxy Manager
Upstream: http://192.168.30.23:8888
Domain: tradingbot.tuo-dominio.com
SSL: Let's Encrypt
```
Poi accedi via:
```
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!**
@@ -3,37 +3,23 @@ version: '3.8'
services:
tradingbot:
container_name: tradingbot
image: tradingbot:latest
build:
context: .
dockerfile: Dockerfile
image: gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest
ports:
- "8080:8080"
- "${EXTERNAL_PORT:-8080}:8080"
volumes:
# Persistenza dati applicazione
- tradingbot-data:/app/data
# Opzionale: mount locale per sviluppo
# - ./logs:/app/logs
environment:
# Configurazioni applicazione
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://+:8080
# Fuso orario (importante per trading!)
- TZ=Europe/Rome
# Opzionali - Configurazioni avanzate
# - TRADINGBOT__SimulationMode=true
# - TRADINGBOT__AutoStartBot=true
# - TRADINGBOT__UpdateIntervalSeconds=3
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 10s
networks:
- tradingbot-network
# Resource limits (opzionali ma consigliati per Unraid)
deploy:
resources:
limits:
+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0"?>
<Container version="2">
<Name>TradingBot</Name>
<Repository>gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest</Repository>
<Registry>https://gitea.encke-hake.ts.net</Registry>
<Network>bridge</Network>
<MyIP/>
<Shell>sh</Shell>
<Privileged>false</Privileged>
<Support>https://gitea.encke-hake.ts.net/Alby96/Encelado</Support>
<Project>https://gitea.encke-hake.ts.net/Alby96/Encelado</Project>
<Overview>TradingBot - Automated Crypto Trading Bot con Blazor UI. Trading algoritmico, analisi tecnica e gestione portfolio.</Overview>
<Category>Tools:Productivity Status:Stable</Category>
<WebUI>http://[IP]:[PORT:8888]/</WebUI>
<TemplateURL>https://gitea.encke-hake.ts.net/Alby96/Encelado/raw/branch/main/TradingBot/deployment/unraid-template.xml</TemplateURL>
<Icon>https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/dotnet.png</Icon>
<ExtraParams/>
<PostArgs/>
<CPUset/>
<DateInstalled/>
<DonateText/>
<DonateLink/>
<DonateImg/>
<Requires/>
<!-- CRITICAL: Port Configuration - Must be visible -->
<Config Name="WebUI Port" Target="8080" Default="8888" Mode="tcp" Description="Porta HTTP WebUI (Host:8888 -&gt; Container:8080)" Type="Port" Display="always" Required="true" Mask="false">8888</Config>
<!-- Volume Configuration -->
<Config Name="AppData" Target="/app/data" Default="/mnt/user/appdata/tradingbot" Mode="rw" Description="Directory dati persistenti" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/tradingbot</Config>
<!-- Environment Variables -->
<Config Name="ASPNETCORE_ENVIRONMENT" Target="ASPNETCORE_ENVIRONMENT" Default="Production" Mode="" Description="Runtime environment" Type="Variable" Display="advanced" Required="true" Mask="false">Production</Config>
<Config Name="ASPNETCORE_URLS" Target="ASPNETCORE_URLS" Default="http://+:8080" Mode="" Description="Internal binding (do not change)" Type="Variable" Display="advanced" Required="true" Mask="false">http://+:8080</Config>
<Config Name="TZ" Target="TZ" Default="Europe/Rome" Mode="" Description="Timezone" Type="Variable" Display="advanced" Required="false" Mask="false">Europe/Rome</Config>
</Container>
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env pwsh
<#
.SYNOPSIS
Manual push of Docker image to Gitea Registry
.DESCRIPTION
Pushes the local tradingbot:latest image to Gitea Registry with proper tags
Use this when Visual Studio Publish was done without Docker running
#>
param(
[string]$Version = "1.3.0"
)
$ErrorActionPreference = "Stop"
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "?? Manual Gitea Registry Push" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# Check Docker
Write-Host "?? Checking Docker..." -ForegroundColor Yellow
try {
docker version | Out-Null
Write-Host "? Docker is running" -ForegroundColor Green
} catch {
Write-Host "? Docker is not running!" -ForegroundColor Red
Write-Host " Please start Docker Desktop first" -ForegroundColor Yellow
exit 1
}
Write-Host ""
# Check local image exists
Write-Host "?? Checking local image..." -ForegroundColor Yellow
$localImage = docker images --format "{{.Repository}}:{{.Tag}}" | Select-String "^tradingbot:latest$"
if (-not $localImage) {
Write-Host "? Local image 'tradingbot:latest' not found!" -ForegroundColor Red
Write-Host " Please run Visual Studio Publish first" -ForegroundColor Yellow
exit 1
}
Write-Host "? Local image found: tradingbot:latest" -ForegroundColor Green
Write-Host ""
# Configuration
$registry = "gitea.encke-hake.ts.net"
$repository = "alby96/encelado/tradingbot"
$giteaImage = "$registry/$repository"
$buildDate = Get-Date -Format "yyyyMMdd"
$versionedTag = "$Version-$buildDate"
Write-Host "?? Version: $Version" -ForegroundColor Cyan
Write-Host "?? Build Date: $buildDate" -ForegroundColor Cyan
Write-Host ""
# Create tags
Write-Host "??? Creating tags..." -ForegroundColor Yellow
Write-Host " Tagging: ${giteaImage}:latest" -ForegroundColor Gray
docker tag tradingbot:latest "${giteaImage}:latest"
Write-Host " ? latest" -ForegroundColor Green
Write-Host " Tagging: ${giteaImage}:$Version" -ForegroundColor Gray
docker tag tradingbot:latest "${giteaImage}:$Version"
Write-Host " ? $Version" -ForegroundColor Green
Write-Host " Tagging: ${giteaImage}:$versionedTag" -ForegroundColor Gray
docker tag tradingbot:latest "${giteaImage}:$versionedTag"
Write-Host " ? $versionedTag" -ForegroundColor Green
Write-Host ""
# Push to Gitea
Write-Host "?? Pushing to $registry..." -ForegroundColor Yellow
Write-Host ""
Write-Host " Pushing: latest..." -ForegroundColor Gray
docker push "${giteaImage}:latest"
Write-Host " ? Pushed: latest" -ForegroundColor Green
Write-Host " Pushing: $Version..." -ForegroundColor Gray
docker push "${giteaImage}:$Version"
Write-Host " ? Pushed: $Version" -ForegroundColor Green
Write-Host " Pushing: $versionedTag..." -ForegroundColor Gray
docker push "${giteaImage}:$versionedTag"
Write-Host " ? Pushed: $versionedTag" -ForegroundColor Green
Write-Host ""
Write-Host "========================================" -ForegroundColor Green
Write-Host "? Successfully pushed to Gitea Registry!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green
Write-Host ""
Write-Host "?? Published images:" -ForegroundColor Cyan
Write-Host " - ${giteaImage}:latest" -ForegroundColor White
Write-Host " - ${giteaImage}:$Version" -ForegroundColor White
Write-Host " - ${giteaImage}:$versionedTag" -ForegroundColor White
Write-Host ""
Write-Host "?? Verify at:" -ForegroundColor Cyan
Write-Host " https://$registry/Alby96/Encelado/-/packages" -ForegroundColor White
Write-Host ""
Write-Host "?? Next steps:" -ForegroundColor Cyan
Write-Host " 1. Verify on Gitea Packages (link above)" -ForegroundColor White
Write-Host " 2. SSH to Unraid: ssh root@192.168.30.23" -ForegroundColor White
Write-Host " 3. Force update:" -ForegroundColor White
Write-Host " docker stop TradingBot" -ForegroundColor Gray
Write-Host " docker rmi $giteaImage:latest" -ForegroundColor Gray
Write-Host " docker pull $giteaImage:latest" -ForegroundColor Gray
Write-Host " docker start TradingBot" -ForegroundColor Gray
Write-Host ""
+117
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 */
@media print {
.no-print {