Compare commits

..

19 Commits

Author SHA1 Message Date
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
Alby96 b2f04b6600 Supporto Docker/Unraid: build, healthcheck, docs
Aggiunti Dockerfile multi-stage, .dockerignore e docker-compose.yml per deployment containerizzato (con healthcheck, volumi persistenti, limiti risorse). Script di build per Linux/Mac e Windows. In Program.cs aggiunto endpoint /health e health checks per orchestrazione. Documentazione estesa: guide Unraid, quickstart Docker, workflow Git/DevOps, best practices su sicurezza, backup, monitoring. Progetto ora pronto per deploy e gestione professionale in ambienti Docker/Unraid.
2025-12-12 23:40:34 +01:00
Alby96 d25b4443c0 Aggiunta Bootstrap 5.3.3 (CSS, JS, RTL, mappe) al progetto
Sono stati aggiunti tutti i file principali di Bootstrap 5.3.3, inclusi CSS, JavaScript (bundle, ESM, UMD, minificati), versioni RTL, utility, reboot, griglia e relative mappe delle sorgenti. Questi file abilitano un sistema di design moderno, responsive e accessibile, con supporto per layout LTR e RTL, debugging avanzato tramite source map e tutte le funzionalità di Bootstrap per lo sviluppo dell’interfaccia utente. Nessuna modifica ai file esistenti.
2025-12-12 23:27:28 +01:00
126 changed files with 76459 additions and 2 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
+73
View File
@@ -0,0 +1,73 @@
# Build artifacts
**/bin/
**/obj/
**/out/
# Visual Studio
.vs/
.vscode/
*.suo
*.user
*.userosscache
*.sln.docstates
# Files
*.log
*.cache
*.swp
*~
# NuGet
*.nupkg
**/packages/*
!**/packages/build/
*.nuget.props
*.nuget.targets
project.lock.json
project.fragment.lock.json
artifacts/
# Test results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
*.trx
*.coverage
*.coveragexml
# Node (se usato per build frontend)
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# OS
.DS_Store
Thumbs.db
# Documentation (opzionale - include se vuoi in produzione)
*.md
!README.md
# Git
.git/
.gitignore
.gitattributes
# Docker
Dockerfile
docker-compose*.yml
.dockerignore
# Temp files
*.tmp
*.temp
*.bak
# Local settings
appsettings.Development.json
**/appsettings.local.json
# IDE
.idea/
*.swp
*.swo
+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
+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
+24
View File
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<ResourcePreloader />
<link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" />
<link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["TradingBot.styles.css"]" />
<ImportMap />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes />
<ReconnectModal />
<script src="@Assets["_framework/blazor.web.js"]"></script>
</body>
</html>
@@ -0,0 +1,318 @@
@inherits LayoutComponentBase
@using TradingBot.Services
@using TradingBot.Models
@inject TradingBotService BotService
@inject SettingsService SettingsService
@inject NavigationManager Navigation
@implements IDisposable
<div class="trading-bot-layout @(sidebarCollapsed ? "collapsed" : "expanded")">
<!-- Modern Vertical Sidebar -->
<aside class="modern-sidebar">
<!-- Brand Section -->
<div class="sidebar-brand">
<div class="brand-container @(sidebarCollapsed ? "minimized" : "")">
<div class="brand-logo">
<span class="logo-icon bi bi-graph-up-arrow"></span>
</div>
@if (!sidebarCollapsed)
{
<div class="brand-info">
<h1 class="brand-title">Trading<span class="accent">Bot</span></h1>
<div class="status-badge @(isRunning ? "online" : "offline")">
<span class="status-indicator"></span>
<span class="status-text">@(isRunning ? "ATTIVO" : "OFFLINE")</span>
</div>
</div>
}
</div>
<button class="collapse-btn" @onclick="ToggleSidebar" title="@(sidebarCollapsed ? "Espandi" : "Minimizza")">
<span class="bi bi-chevron-@(sidebarCollapsed ? "right" : "left")"></span>
</button>
</div>
<!-- Navigation Menu -->
<nav class="sidebar-menu">
<NavLink class="menu-item" href="/" Match="NavLinkMatch.All" title="Dashboard">
<span class="item-icon bi bi-speedometer2"></span>
@if (!sidebarCollapsed)
{
<span class="item-text">Dashboard</span>
}
</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)
{
<span class="item-text">Strategie</span>
}
</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)
{
<span class="item-text">Asset</span>
}
</NavLink>
<NavLink class="menu-item" href="/trading" title="Trading">
<span class="item-icon bi bi-graph-up-arrow"></span>
@if (!sidebarCollapsed)
{
<span class="item-text">Trading</span>
}
</NavLink>
<NavLink class="menu-item" href="/market" title="Analisi Mercato">
<span class="item-icon bi bi-bar-chart-line"></span>
@if (!sidebarCollapsed)
{
<span class="item-text">Analisi Mercato</span>
}
</NavLink>
<NavLink class="menu-item" href="/statistics" title="Statistiche">
<span class="item-icon bi bi-graph-up"></span>
@if (!sidebarCollapsed)
{
<span class="item-text">Statistiche</span>
}
</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)
{
<span class="item-text">Impostazioni</span>
}
</NavLink>
</nav>
<!-- Portfolio Summary (quando espanso) -->
@if (!sidebarCollapsed)
{
<div class="sidebar-summary">
<div class="summary-card">
<div class="summary-header">
<span class="summary-section-title">Portfolio</span>
</div>
<div class="summary-row">
<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>
}
</aside>
<!-- Main Content Area -->
<div class="main-area">
<!-- Top Header Bar -->
<header class="content-header">
<div class="header-left">
<!-- Placeholder for page title -->
</div>
<div class="header-right">
<button class="header-btn notifications" title="Notifiche">
<span class="bi bi-bell"></span>
</button>
<button class="header-btn bot-control @(isRunning ? "running" : "stopped")" @onclick="ToggleBot">
<span class="bi bi-@(isRunning ? "pause" : "play")-circle-fill"></span>
<span class="btn-label">@(isRunning ? "Stop" : "Avvia")</span>
</button>
</div>
</header>
<!-- Page Content -->
<main class="page-content">
@Body
</main>
</div>
</div>
@code {
private bool sidebarCollapsed = false;
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()
{
var settings = SettingsService.GetSettings();
sidebarCollapsed = settings.SidebarCollapsed;
BotService.OnStatusChanged += HandleUpdate;
BotService.OnTradeExecuted += HandleTradeExecuted;
BotService.OnPriceUpdated += HandlePriceUpdate;
SettingsService.OnSettingsChanged += HandleSettingsChanged;
UpdateStats();
if (settings.AutoStartBot && !BotService.Status.IsRunning)
{
BotService.Start();
}
}
private void ToggleSidebar()
{
sidebarCollapsed = !sidebarCollapsed;
SettingsService.UpdateSetting(nameof(AppSettings.SidebarCollapsed), sidebarCollapsed);
StateHasChanged(); // Force immediate UI update
Console.WriteLine($"Sidebar toggled: collapsed={sidebarCollapsed}"); // Debug log
}
private void ToggleBot()
{
if (isRunning)
BotService.Stop();
else
BotService.Start();
}
private void UpdateStats()
{
totalCapital = 0;
investedCapital = 0;
availableCapital = 0;
currentPL = 0;
totalProfit = 0;
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() => InvokeAsync(() => { UpdateStats(); StateHasChanged(); });
private void HandleTradeExecuted(Trade trade) => InvokeAsync(() => { UpdateStats(); 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;
}
}
@@ -0,0 +1,542 @@
/* ==============================================
TRADING BOT LAYOUT - Modern Vertical Sidebar
Global Styles (usando ::deep per scoped CSS)
============================================== */
/* Layout Container */
::deep .trading-bot-layout {
display: flex !important;
min-height: 100vh !important;
background: #0a0e27 !important;
color: #e2e8f0 !important;
position: relative !important;
}
/* ==============================================
MODERN SIDEBAR
============================================== */
::deep .modern-sidebar {
width: 280px !important;
background: linear-gradient(180deg, #1a1f3a 0%, #0f1629 100%) !important;
border-right: 1px solid rgba(99, 102, 241, 0.15) !important;
display: flex !important;
flex-direction: column !important;
position: fixed !important;
left: 0 !important;
top: 0 !important;
bottom: 0 !important;
z-index: 1000 !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
box-shadow: 4px 0 20px rgba(0, 0, 0, 0.2) !important;
}
::deep .trading-bot-layout.collapsed .modern-sidebar {
width: 80px !important;
}
/* Brand Section */
::deep .sidebar-brand {
padding: 1.75rem 1.5rem !important;
border-bottom: 1px solid rgba(99, 102, 241, 0.1) !important;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
gap: 1rem !important;
}
::deep .trading-bot-layout.collapsed .sidebar-brand {
padding: 1.5rem 0.75rem !important;
justify-content: center !important;
}
::deep .brand-container {
display: flex !important;
align-items: center !important;
gap: 1rem !important;
flex: 1 !important;
min-width: 0 !important;
}
::deep .brand-container.minimized {
justify-content: center !important;
flex: initial !important;
}
::deep .brand-logo {
width: 3.5rem !important;
height: 3.5rem !important;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important;
border-radius: 1rem !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
flex-shrink: 0 !important;
box-shadow: 0 8px 16px rgba(99, 102, 241, 0.3) !important;
}
::deep .trading-bot-layout.collapsed .brand-logo {
width: 3rem !important;
height: 3rem !important;
}
::deep .logo-icon {
font-size: 1.75rem !important;
color: white !important;
}
::deep .brand-info {
display: flex !important;
flex-direction: column !important;
gap: 0.5rem !important;
min-width: 0 !important;
}
::deep .brand-title {
font-size: 1.5rem !important;
font-weight: 700 !important;
color: white !important;
margin: 0 !important;
line-height: 1 !important;
}
::deep .brand-title .accent {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important;
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
background-clip: text !important;
}
::deep .status-badge {
display: flex !important;
align-items: center !important;
gap: 0.5rem !important;
padding: 0.25rem 0.75rem !important;
background: rgba(71, 85, 105, 0.3) !important;
border-radius: 1rem !important;
width: fit-content !important;
}
::deep .status-badge.online {
background: rgba(16, 185, 129, 0.15) !important;
}
::deep .status-indicator {
width: 0.5rem !important;
height: 0.5rem !important;
border-radius: 50% !important;
background: #64748b !important;
}
::deep .status-badge.online .status-indicator {
background: #10b981 !important;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.6) !important;
animation: pulse-indicator 2s ease-in-out infinite !important;
}
@keyframes pulse-indicator {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.7; transform: scale(1.1); }
}
::deep .status-text {
font-size: 0.625rem !important;
font-weight: 700 !important;
text-transform: uppercase !important;
letter-spacing: 0.05em !important;
color: #64748b !important;
}
::deep .status-badge.online .status-text {
color: #10b981 !important;
}
::deep .collapse-btn {
width: 2.25rem !important;
height: 2.25rem !important;
border-radius: 0.625rem !important;
border: none !important;
background: rgba(99, 102, 241, 0.1) !important;
color: #6366f1 !important;
cursor: pointer !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
transition: all 0.2s ease !important;
font-size: 1rem !important;
flex-shrink: 0 !important;
}
::deep .collapse-btn:hover {
background: rgba(99, 102, 241, 0.2) !important;
transform: scale(1.05) !important;
}
/* Navigation Menu */
::deep .sidebar-menu {
flex: 1 !important;
padding: 1.5rem 0 !important;
overflow-y: auto !important;
overflow-x: hidden !important;
}
::deep .menu-item {
display: flex !important;
align-items: center !important;
gap: 1rem !important;
padding: 1rem 1.5rem !important;
color: #94a3b8 !important;
text-decoration: none !important;
transition: all 0.2s ease !important;
border-left: 3px solid transparent !important;
font-weight: 600 !important;
font-size: 0.938rem !important;
position: relative !important;
cursor: pointer !important;
}
::deep .trading-bot-layout.collapsed .menu-item {
justify-content: center !important;
padding: 1rem 0 !important;
}
::deep .menu-item:hover {
background: rgba(99, 102, 241, 0.08) !important;
color: #cbd5e1 !important;
border-left-color: rgba(99, 102, 241, 0.3) !important;
}
::deep .menu-item.active {
background: rgba(99, 102, 241, 0.12) !important;
border-left-color: #6366f1 !important;
color: #6366f1 !important;
}
::deep .menu-item.active::before {
content: '' !important;
position: absolute !important;
right: 0 !important;
top: 50% !important;
transform: translateY(-50%) !important;
width: 3px !important;
height: 60% !important;
background: #6366f1 !important;
border-radius: 3px 0 0 3px !important;
}
::deep .item-icon {
font-size: 1.375rem !important;
flex-shrink: 0 !important;
transition: transform 0.2s ease !important;
}
::deep .menu-item:hover .item-icon {
transform: scale(1.1) !important;
}
::deep .item-text {
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
/* Portfolio Summary */
::deep .sidebar-summary {
padding: 1.5rem !important;
border-top: 1px solid rgba(99, 102, 241, 0.1) !important;
}
::deep .summary-card {
padding: 1.25rem !important;
background: rgba(99, 102, 241, 0.08) !important;
border-radius: 0.75rem !important;
border: 1px solid rgba(99, 102, 241, 0.1) !important;
display: flex !important;
flex-direction: column !important;
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;
font-weight: 600 !important;
text-transform: uppercase !important;
letter-spacing: 0.05em !important;
}
::deep .summary-amount {
font-size: 1rem !important;
font-weight: 700 !important;
color: white !important;
font-family: 'Courier New', monospace !important;
}
::deep .summary-amount.profit {
color: #10b981 !important;
}
::deep .summary-amount.loss {
color: #ef4444 !important;
}
::deep .summary-amount.invested {
color: #f59e0b !important;
}
::deep .summary-amount.available {
color: #3b82f6 !important;
}
/* ==============================================
MAIN CONTENT AREA
============================================== */
::deep .main-area {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
margin-left: 280px !important;
transition: margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
min-height: 100vh !important;
}
::deep .trading-bot-layout.collapsed .main-area {
margin-left: 80px !important;
}
/* Content Header */
::deep .content-header {
background: #0f1629 !important;
border-bottom: 1px solid rgba(99, 102, 241, 0.1) !important;
padding: 1.25rem 2rem !important;
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15) !important;
position: sticky !important;
top: 0 !important;
z-index: 100 !important;
}
::deep .header-left {
flex: 1 !important;
}
::deep .header-right {
display: flex !important;
gap: 1rem !important;
align-items: center !important;
}
::deep .header-btn {
display: flex !important;
align-items: center !important;
gap: 0.625rem !important;
padding: 0.75rem 1.25rem !important;
border-radius: 0.625rem !important;
border: 1px solid #334155 !important;
background: #1a1f3a !important;
color: #cbd5e1 !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
font-size: 0.875rem !important;
font-weight: 600 !important;
}
::deep .header-btn:hover {
background: #1e293b !important;
border-color: #475569 !important;
transform: translateY(-1px) !important;
}
::deep .header-btn.notifications {
padding: 0.75rem !important;
}
::deep .header-btn.notifications .bi {
font-size: 1.125rem !important;
}
::deep .header-btn.bot-control {
padding: 0.75rem 1.5rem !important;
}
::deep .header-btn.bot-control .bi {
font-size: 1.125rem !important;
}
::deep .header-btn.bot-control.running {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important;
border-color: #6366f1 !important;
color: white !important;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3) !important;
}
::deep .header-btn.bot-control.running:hover {
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4) !important;
}
::deep .btn-label {
font-weight: 600 !important;
}
/* Page Content */
::deep .page-content {
flex: 1 !important;
padding: 2rem !important;
overflow-y: auto !important;
background: #0a0e27 !important;
}
/* Scrollbar Styling */
::deep .sidebar-menu::-webkit-scrollbar,
::deep .page-content::-webkit-scrollbar {
width: 0.375rem !important;
}
::deep .sidebar-menu::-webkit-scrollbar-track,
::deep .page-content::-webkit-scrollbar-track {
background: transparent !important;
}
::deep .sidebar-menu::-webkit-scrollbar-thumb,
::deep .page-content::-webkit-scrollbar-thumb {
background: #334155 !important;
border-radius: 0.25rem !important;
}
::deep .sidebar-menu::-webkit-scrollbar-thumb:hover,
::deep .page-content::-webkit-scrollbar-thumb:hover {
background: #475569 !important;
}
/* ==============================================
RESPONSIVE DESIGN
============================================== */
@media (max-width: 1024px) {
::deep .modern-sidebar {
width: 260px !important;
}
::deep .main-area {
margin-left: 260px !important;
}
::deep .trading-bot-layout.collapsed .modern-sidebar {
width: 70px !important;
}
::deep .trading-bot-layout.collapsed .main-area {
margin-left: 70px !important;
}
}
@media (max-width: 768px) {
::deep .modern-sidebar {
transform: translateX(-100%) !important;
width: 280px !important;
}
::deep .trading-bot-layout.sidebar-open .modern-sidebar {
transform: translateX(0) !important;
}
::deep .main-area {
margin-left: 0 !important;
}
::deep .content-header {
padding: 1rem 1.5rem !important;
}
::deep .page-content {
padding: 1.5rem !important;
}
::deep .header-btn.bot-control .btn-label {
display: none !important;
}
}
@media (max-width: 480px) {
::deep .page-content {
padding: 1rem !important;
}
::deep .sidebar-brand {
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,31 @@
<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]"></script>
<dialog id="components-reconnect-modal" data-nosnippet>
<div class="components-reconnect-container">
<div class="components-rejoining-animation" aria-hidden="true">
<div></div>
<div></div>
</div>
<p class="components-reconnect-first-attempt-visible">
Rejoining the server...
</p>
<p class="components-reconnect-repeated-attempt-visible">
Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds.
</p>
<p class="components-reconnect-failed-visible">
Failed to rejoin.<br />Please retry or reload the page.
</p>
<button id="components-reconnect-button" class="components-reconnect-failed-visible">
Retry
</button>
<p class="components-pause-visible">
The session has been paused by the server.
</p>
<button id="components-resume-button" class="components-pause-visible">
Resume
</button>
<p class="components-resume-failed-visible">
Failed to resume the session.<br />Please reload the page.
</p>
</div>
</dialog>
@@ -0,0 +1,157 @@
.components-reconnect-first-attempt-visible,
.components-reconnect-repeated-attempt-visible,
.components-reconnect-failed-visible,
.components-pause-visible,
.components-resume-failed-visible,
.components-rejoining-animation {
display: none;
}
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
#components-reconnect-modal.components-reconnect-retrying,
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
display: block;
}
#components-reconnect-modal {
background-color: white;
width: 20rem;
margin: 20vh auto;
padding: 2rem;
border: 0;
border-radius: 0.5rem;
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
opacity: 0;
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
&[open]
{
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
animation-fill-mode: both;
}
}
#components-reconnect-modal::backdrop {
background-color: rgba(0, 0, 0, 0.4);
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
opacity: 1;
}
@keyframes components-reconnect-modal-slideUp {
0% {
transform: translateY(30px) scale(0.95);
}
100% {
transform: translateY(0);
}
}
@keyframes components-reconnect-modal-fadeInOpacity {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes components-reconnect-modal-fadeOutOpacity {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.components-reconnect-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
#components-reconnect-modal p {
margin: 0;
text-align: center;
}
#components-reconnect-modal button {
border: 0;
background-color: #6b9ed2;
color: white;
padding: 4px 24px;
border-radius: 4px;
}
#components-reconnect-modal button:hover {
background-color: #3b6ea2;
}
#components-reconnect-modal button:active {
background-color: #6b9ed2;
}
.components-rejoining-animation {
position: relative;
width: 80px;
height: 80px;
}
.components-rejoining-animation div {
position: absolute;
border: 3px solid #0087ff;
opacity: 1;
border-radius: 50%;
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.components-rejoining-animation div:nth-child(2) {
animation-delay: -0.5s;
}
@keyframes components-rejoining-animation {
0% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 0;
}
4.9% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 0;
}
5% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 1;
}
100% {
top: 0px;
left: 0px;
width: 80px;
height: 80px;
opacity: 0;
}
}
@@ -0,0 +1,63 @@
// Set up event handlers
const reconnectModal = document.getElementById("components-reconnect-modal");
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
const retryButton = document.getElementById("components-reconnect-button");
retryButton.addEventListener("click", retry);
const resumeButton = document.getElementById("components-resume-button");
resumeButton.addEventListener("click", resume);
function handleReconnectStateChanged(event) {
if (event.detail.state === "show") {
reconnectModal.showModal();
} else if (event.detail.state === "hide") {
reconnectModal.close();
} else if (event.detail.state === "failed") {
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
} else if (event.detail.state === "rejected") {
location.reload();
}
}
async function retry() {
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
try {
// Reconnect will asynchronously return:
// - true to mean success
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
// - exception to mean we didn't reach the server (this can be sync or async)
const successful = await Blazor.reconnect();
if (!successful) {
// We have been able to reach the server, but the circuit is no longer available.
// We'll reload the page so the user can continue using the app as quickly as possible.
const resumeSuccessful = await Blazor.resumeCircuit();
if (!resumeSuccessful) {
location.reload();
} else {
reconnectModal.close();
}
}
} catch (err) {
// We got an exception, server is currently unavailable
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
}
}
async function resume() {
try {
const successful = await Blazor.resumeCircuit();
if (!successful) {
location.reload();
}
} catch {
location.reload();
}
}
async function retryWhenDocumentBecomesVisible() {
if (document.visibilityState === "visible") {
await retry();
}
}
+341
View File
@@ -0,0 +1,341 @@
@page "/assets"
@using TradingBot.Models
@using TradingBot.Services
@inject TradingBotService BotService
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Asset - TradingBot</PageTitle>
<div class="assets-page">
<div class="page-header">
<div>
<h1>Gestione Asset</h1>
<p class="subtitle">Visualizza, configura e assegna strategie ai tuoi asset di trading</p>
</div>
<div class="header-controls">
<div class="view-toggle">
<button class="toggle-btn @(viewMode == "grid" ? "active" : "")" @onclick="@(() => viewMode = "grid")">
<span class="bi bi-grid-3x3-gap"></span>
</button>
<button class="toggle-btn @(viewMode == "list" ? "active" : "")" @onclick="@(() => viewMode = "list")">
<span class="bi bi-list-ul"></span>
</button>
</div>
<select class="filter-select" @bind="filterStatus">
<option value="all">Tutti gli Asset</option>
<option value="active">Solo Attivi</option>
<option value="inactive">Solo Inattivi</option>
</select>
</div>
</div>
<!-- Summary Stats -->
<div class="assets-summary">
<div class="summary-stat">
<div class="stat-icon">
<span class="bi bi-coin"></span>
</div>
<div class="stat-content">
<span class="stat-label">Totale Asset</span>
<span class="stat-value">@totalAssets</span>
</div>
</div>
<div class="summary-stat success">
<div class="stat-icon">
<span class="bi bi-check-circle"></span>
</div>
<div class="stat-content">
<span class="stat-label">Asset Attivi</span>
<span class="stat-value">@activeAssets</span>
</div>
</div>
<div class="summary-stat warning">
<div class="stat-icon">
<span class="bi bi-diagram-3"></span>
</div>
<div class="stat-content">
<span class="stat-label">Strategie Assegnate</span>
<span class="stat-value">@assignedStrategies</span>
</div>
</div>
<div class="summary-stat info">
<div class="stat-icon">
<span class="bi bi-currency-dollar"></span>
</div>
<div class="stat-content">
<span class="stat-label">Valore Totale</span>
<span class="stat-value">$@totalValue.ToString("N0")</span>
</div>
</div>
</div>
<!-- Assets Grid/List -->
@if (viewMode == "grid")
{
<div class="assets-grid">
@foreach (var config in GetFilteredAssets())
{
var price = BotService.GetLatestPrice(config.Symbol);
var stats = BotService.AssetStatistics.TryGetValue(config.Symbol, out var s) ? s : null;
<div class="asset-card @(config.IsEnabled ? "enabled" : "disabled")">
<div class="asset-card-header">
<div class="asset-info">
<div class="asset-icon">@config.Symbol.Substring(0, 1)</div>
<div class="asset-title">
<h3>@config.Name</h3>
<span class="asset-symbol">@config.Symbol</span>
</div>
</div>
<label class="toggle-switch">
<input type="checkbox"
checked="@config.IsEnabled"
@onchange="@((e) => ToggleAsset(config.Symbol, (bool)e.Value!))" />
<span class="toggle-slider"></span>
</label>
</div>
<div class="asset-card-body">
@if (price != null)
{
<div class="price-section">
<div class="current-price">$@price.Price.ToString("N2")</div>
<div class="price-change @(price.Change24h >= 0 ? "positive" : "negative")">
<span class="bi @(price.Change24h >= 0 ? "bi-arrow-up" : "bi-arrow-down")"></span>
@Math.Abs(price.Change24h).ToString("F2")% (24h)
</div>
</div>
}
<div class="asset-metrics">
<div class="metric">
<span class="metric-label">Holdings</span>
<span class="metric-value">@config.CurrentHoldings.ToString("F6")</span>
</div>
<div class="metric">
<span class="metric-label">Valore</span>
<span class="metric-value">$@((config.CurrentBalance + config.CurrentHoldings * (price?.Price ?? 0)).ToString("N2"))</span>
</div>
<div class="metric">
<span class="metric-label">Profitto</span>
<span class="metric-value @(config.TotalProfit >= 0 ? "profit" : "loss")">
$@config.TotalProfit.ToString("N2")
</span>
</div>
<div class="metric">
<span class="metric-label">Trades</span>
<span class="metric-value">@(stats?.TotalTrades ?? 0)</span>
</div>
</div>
<div class="strategy-section">
<label class="strategy-label">Strategia Assegnata</label>
<select class="strategy-select"
value="@config.StrategyName"
@onchange="@((e) => AssignStrategy(config.Symbol, e.Value?.ToString() ?? ""))">
<option value="">Nessuna strategia</option>
<option value="RSI + MACD Cross">RSI + MACD Cross</option>
<option value="Media Mobile Semplice">Media Mobile Semplice</option>
<option value="Scalping Veloce">Scalping Veloce</option>
<option value="Trend Following">Trend Following</option>
<option value="Mean Reversion">Mean Reversion</option>
<option value="Conservative">Conservative</option>
</select>
</div>
</div>
<div class="asset-card-footer">
<button class="btn-secondary btn-sm" @onclick="@(() => OpenAssetDetails(config.Symbol))">
<span class="bi bi-gear"></span>
Configura
</button>
<button class="btn-secondary btn-sm" @onclick="@(() => ViewChart(config.Symbol))">
<span class="bi bi-graph-up"></span>
Grafico
</button>
</div>
</div>
}
</div>
}
else
{
<!-- List View -->
<div class="assets-table">
<div class="table-header">
<div class="th">Asset</div>
<div class="th">Prezzo</div>
<div class="th">Var. 24h</div>
<div class="th">Holdings</div>
<div class="th">Valore</div>
<div class="th">Profitto</div>
<div class="th">Strategia</div>
<div class="th">Stato</div>
<div class="th">Azioni</div>
</div>
@foreach (var config in GetFilteredAssets())
{
var price = BotService.GetLatestPrice(config.Symbol);
var stats = BotService.AssetStatistics.TryGetValue(config.Symbol, out var s) ? s : null;
<div class="table-row @(config.IsEnabled ? "enabled" : "disabled")">
<div class="cell-asset">
<div class="asset-icon-small">@config.Symbol.Substring(0, 1)</div>
<div>
<div class="asset-name">@config.Name</div>
<div class="asset-symbol-small">@config.Symbol</div>
</div>
</div>
<div class="cell">
@if (price != null)
{
<span class="price-value">$@price.Price.ToString("N2")</span>
}
else
{
<span class="text-muted">-</span>
}
</div>
<div class="cell">
@if (price != null)
{
<span class="change-badge @(price.Change24h >= 0 ? "positive" : "negative")">
@(price.Change24h >= 0 ? "+" : "")@price.Change24h.ToString("F2")%
</span>
}
else
{
<span class="text-muted">-</span>
}
</div>
<div class="cell">
<span class="mono-value">@config.CurrentHoldings.ToString("F6")</span>
</div>
<div class="cell">
<span class="mono-value">$@((config.CurrentBalance + config.CurrentHoldings * (price?.Price ?? 0)).ToString("N2"))</span>
</div>
<div class="cell">
<span class="mono-value @(config.TotalProfit >= 0 ? "profit" : "loss")">
$@config.TotalProfit.ToString("N2")
</span>
</div>
<div class="cell-strategy">
<select class="strategy-select-small"
value="@config.StrategyName"
@onchange="@((e) => AssignStrategy(config.Symbol, e.Value?.ToString() ?? ""))">
<option value="">Nessuna</option>
<option value="RSI + MACD Cross">RSI + MACD</option>
<option value="Media Mobile Semplice">SMA</option>
<option value="Scalping Veloce">Scalping</option>
<option value="Trend Following">Trend</option>
<option value="Mean Reversion">Mean Rev.</option>
<option value="Conservative">Conserv.</option>
</select>
</div>
<div class="cell">
<label class="toggle-switch-small">
<input type="checkbox"
checked="@config.IsEnabled"
@onchange="@((e) => ToggleAsset(config.Symbol, (bool)e.Value!))" />
<span class="toggle-slider-small"></span>
</label>
</div>
<div class="cell-actions">
<button class="btn-icon-small" title="Configura" @onclick="@(() => OpenAssetDetails(config.Symbol))">
<span class="bi bi-gear"></span>
</button>
<button class="btn-icon-small" title="Grafico" @onclick="@(() => ViewChart(config.Symbol))">
<span class="bi bi-graph-up"></span>
</button>
</div>
</div>
}
</div>
}
</div>
@code {
private string viewMode = "grid";
private string filterStatus = "all";
private int totalAssets = 0;
private int activeAssets = 0;
private int assignedStrategies = 0;
private decimal totalValue = 0;
protected override void OnInitialized()
{
BotService.OnStatusChanged += HandleUpdate;
BotService.OnTradeExecuted += HandleTradeExecuted;
BotService.OnPriceUpdated += HandlePriceUpdate;
RefreshData();
}
private void RefreshData()
{
totalAssets = BotService.AssetConfigurations.Count;
activeAssets = BotService.AssetConfigurations.Values.Count(c => c.IsEnabled);
assignedStrategies = BotService.AssetConfigurations.Values.Count(c => !string.IsNullOrEmpty(c.StrategyName));
totalValue = BotService.AssetConfigurations.Values.Sum(c =>
{
var price = BotService.GetLatestPrice(c.Symbol);
return c.CurrentBalance + (c.CurrentHoldings * (price?.Price ?? 0));
});
StateHasChanged();
}
private IEnumerable<AssetConfiguration> GetFilteredAssets()
{
var assets = BotService.AssetConfigurations.Values.OrderBy(c => c.Symbol);
return filterStatus switch
{
"active" => assets.Where(c => c.IsEnabled),
"inactive" => assets.Where(c => !c.IsEnabled),
_ => assets
};
}
private void ToggleAsset(string symbol, bool enabled)
{
BotService.ToggleAsset(symbol, enabled);
RefreshData();
}
private void AssignStrategy(string symbol, string strategyName)
{
if (BotService.AssetConfigurations.TryGetValue(symbol, out var config))
{
config.StrategyName = strategyName;
RefreshData();
}
}
private void OpenAssetDetails(string symbol)
{
// TODO: Open modal or navigate to asset detail page
}
private void ViewChart(string symbol)
{
var navManager = Navigation;
navManager?.NavigateTo($"/market?symbol={symbol}");
}
private void HandleUpdate() => InvokeAsync(RefreshData);
private void HandleTradeExecuted(Trade trade) => InvokeAsync(RefreshData);
private void HandlePriceUpdate(string symbol, MarketPrice price) => InvokeAsync(RefreshData);
[Inject] private NavigationManager? Navigation { get; set; }
public void Dispose()
{
BotService.OnStatusChanged -= HandleUpdate;
BotService.OnTradeExecuted -= HandleTradeExecuted;
BotService.OnPriceUpdated -= HandlePriceUpdate;
}
}
@@ -0,0 +1,597 @@
/* Assets Page */
.assets-page {
display: flex;
flex-direction: column;
gap: 2rem;
}
/* Page Header */
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.page-header h1 {
margin: 0;
font-size: 2rem;
font-weight: 700;
color: white;
}
.subtitle {
margin: 0.5rem 0 0 0;
color: #94a3b8;
font-size: 0.875rem;
}
.header-controls {
display: flex;
gap: 1rem;
align-items: center;
}
/* View Toggle */
.view-toggle {
display: flex;
gap: 0.25rem;
background: #1a1f3a;
border-radius: 0.5rem;
padding: 0.25rem;
}
.toggle-btn {
padding: 0.5rem 0.75rem;
border: none;
background: transparent;
color: #64748b;
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.2s ease;
font-size: 1rem;
}
.toggle-btn:hover {
color: #cbd5e1;
}
.toggle-btn.active {
background: #6366f1;
color: white;
}
.filter-select {
padding: 0.625rem 1rem;
border-radius: 0.5rem;
border: 1px solid #334155;
background: #1a1f3a;
color: white;
font-size: 0.875rem;
font-weight: 600;
}
/* Assets Summary */
.assets-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.summary-stat {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.25rem;
display: flex;
align-items: center;
gap: 1rem;
}
.summary-stat .stat-icon {
width: 3rem;
height: 3rem;
border-radius: 0.625rem;
background: rgba(99, 102, 241, 0.1);
color: #6366f1;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
}
.summary-stat.success .stat-icon {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.summary-stat.warning .stat-icon {
background: rgba(245, 158, 11, 0.1);
color: #f59e0b;
}
.summary-stat.info .stat-icon {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
.stat-content {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.stat-label {
font-size: 0.75rem;
color: #64748b;
font-weight: 600;
text-transform: uppercase;
}
.stat-value {
font-size: 1.5rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
}
/* Assets Grid */
.assets-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
}
.asset-card {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
overflow: hidden;
transition: all 0.3s ease;
}
.asset-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.4);
border-color: #334155;
}
.asset-card.enabled {
border-color: rgba(99, 102, 241, 0.3);
}
.asset-card.disabled {
opacity: 0.6;
}
.asset-card-header {
padding: 1.25rem;
background: #1a1f3a;
border-bottom: 1px solid #1e293b;
display: flex;
justify-content: space-between;
align-items: center;
}
.asset-info {
display: flex;
align-items: center;
gap: 0.75rem;
}
.asset-icon {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.5rem;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
font-weight: 700;
color: white;
}
.asset-title h3 {
margin: 0;
font-size: 1rem;
font-weight: 700;
color: white;
}
.asset-symbol {
font-size: 0.75rem;
color: #64748b;
font-family: 'Courier New', monospace;
}
/* Toggle Switch */
.toggle-switch {
position: relative;
display: inline-block;
width: 3rem;
height: 1.5rem;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #334155;
transition: 0.3s;
border-radius: 1.5rem;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 1.125rem;
width: 1.125rem;
left: 0.1875rem;
bottom: 0.1875rem;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
.toggle-switch input:checked + .toggle-slider {
background-color: #6366f1;
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(1.5rem);
}
/* Asset Card Body */
.asset-card-body {
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.price-section {
display: flex;
align-items: baseline;
gap: 0.75rem;
}
.current-price {
font-size: 1.75rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
}
.price-change {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.875rem;
font-weight: 600;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
}
.price-change.positive {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.price-change.negative {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
/* Asset Metrics */
.asset-metrics {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
.metric {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.metric-label {
font-size: 0.625rem;
color: #64748b;
text-transform: uppercase;
font-weight: 600;
}
.metric-value {
font-size: 0.875rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
}
.metric-value.profit {
color: #10b981;
}
.metric-value.loss {
color: #ef4444;
}
/* Strategy Section */
.strategy-section {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.strategy-label {
font-size: 0.75rem;
color: #64748b;
font-weight: 600;
text-transform: uppercase;
}
.strategy-select {
padding: 0.625rem;
border-radius: 0.5rem;
border: 1px solid #334155;
background: #1a1f3a;
color: white;
font-size: 0.875rem;
font-weight: 600;
}
.strategy-select:focus {
outline: 2px solid #6366f1;
outline-offset: 2px;
}
/* Asset Card Footer */
.asset-card-footer {
padding: 1rem 1.25rem;
background: #0a0e27;
border-top: 1px solid #1e293b;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
.btn-sm {
padding: 0.5rem 1rem;
font-size: 0.75rem;
}
/* Assets Table (List View) */
.assets-table {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
overflow: hidden;
}
.table-header {
display: grid;
grid-template-columns: 2fr 1.5fr 1fr 1.5fr 1.5fr 1.5fr 2fr 1fr 1.5fr;
gap: 1rem;
padding: 1rem 1.5rem;
background: #1a1f3a;
border-bottom: 1px solid #1e293b;
font-size: 0.75rem;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
}
.table-row {
display: grid;
grid-template-columns: 2fr 1.5fr 1fr 1.5fr 1.5fr 1.5fr 2fr 1fr 1.5fr;
gap: 1rem;
padding: 1rem 1.5rem;
border-bottom: 1px solid #1e293b;
align-items: center;
font-size: 0.875rem;
color: #cbd5e1;
transition: background 0.2s ease;
}
.table-row:hover {
background: #1a1f3a;
}
.table-row:last-child {
border-bottom: none;
}
.table-row.disabled {
opacity: 0.5;
}
.cell-asset {
display: flex;
align-items: center;
gap: 0.75rem;
}
.asset-icon-small {
width: 2rem;
height: 2rem;
border-radius: 0.375rem;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
font-weight: 700;
color: white;
}
.asset-name {
font-weight: 600;
color: white;
}
.asset-symbol-small {
font-size: 0.75rem;
color: #64748b;
font-family: 'Courier New', monospace;
}
.cell {
display: flex;
align-items: center;
}
.price-value,
.mono-value {
font-family: 'Courier New', monospace;
font-weight: 600;
}
.change-badge {
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 600;
}
.change-badge.positive {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.change-badge.negative {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.cell-strategy {
display: flex;
}
.strategy-select-small {
width: 100%;
padding: 0.375rem 0.5rem;
border-radius: 0.375rem;
border: 1px solid #334155;
background: #1a1f3a;
color: white;
font-size: 0.75rem;
font-weight: 600;
}
/* Toggle Switch Small */
.toggle-switch-small {
position: relative;
display: inline-block;
width: 2.5rem;
height: 1.25rem;
}
.toggle-switch-small input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider-small {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #334155;
transition: 0.3s;
border-radius: 1.25rem;
}
.toggle-slider-small:before {
position: absolute;
content: "";
height: 0.875rem;
width: 0.875rem;
left: 0.1875rem;
bottom: 0.1875rem;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
.toggle-switch-small input:checked + .toggle-slider-small {
background-color: #6366f1;
}
.toggle-switch-small input:checked + .toggle-slider-small:before {
transform: translateX(1.25rem);
}
.cell-actions {
display: flex;
gap: 0.5rem;
}
.btn-icon-small {
width: 2rem;
height: 2rem;
border-radius: 0.375rem;
border: none;
background: #1a1f3a;
color: #94a3b8;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.btn-icon-small:hover {
background: #1e293b;
color: #cbd5e1;
}
/* Responsive */
@media (max-width: 1200px) {
.table-header, .table-row {
grid-template-columns: 2fr 1fr 1fr 1fr 1.5fr 1fr 1fr;
}
/* Hide some columns on smaller screens */
.table-header div:nth-child(4),
.table-row div:nth-child(4),
.table-header div:nth-child(6),
.table-row div:nth-child(6) {
display: none;
}
}
@media (max-width: 768px) {
.assets-grid {
grid-template-columns: 1fr;
}
.header-controls {
flex-direction: column;
width: 100%;
}
.filter-select {
width: 100%;
}
.assets-summary {
grid-template-columns: repeat(2, 1fr);
}
.assets-table {
overflow-x: auto;
}
}
+185
View File
@@ -0,0 +1,185 @@
@page "/"
@using TradingBot.Models
@using TradingBot.Services
@inject TradingBotService BotService
@inject NavigationManager Navigation
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Dashboard - TradingBot</PageTitle>
<div class="dashboard-page">
<div class="page-header">
<div>
<h1>Dashboard</h1>
<p class="subtitle">Panoramica completa delle performance e attività di trading</p>
</div>
</div>
<!-- Summary Cards -->
<div class="summary-grid">
<div class="summary-card primary">
<div class="card-icon">
<span class="bi bi-wallet2"></span>
</div>
<div class="card-content">
<div class="card-label">Valore Portfolio</div>
<div class="card-value">$@portfolioStats.TotalBalance.ToString("N2")</div>
<div class="card-change @(portfolioStats.TotalProfitPercentage >= 0 ? "positive" : "negative")">
<span class="bi @(portfolioStats.TotalProfitPercentage >= 0 ? "bi-arrow-up" : "bi-arrow-down")"></span>
@Math.Abs(portfolioStats.TotalProfitPercentage).ToString("F2")%
</div>
</div>
</div>
<div class="summary-card">
<div class="card-icon success">
<span class="bi bi-graph-up-arrow"></span>
</div>
<div class="card-content">
<div class="card-label">Profitto Totale</div>
<div class="card-value @(portfolioStats.TotalProfit >= 0 ? "profit" : "loss")">
$@portfolioStats.TotalProfit.ToString("N2")
</div>
<div class="card-meta">Da $@portfolioStats.InitialBalance.ToString("N2")</div>
</div>
</div>
<div class="summary-card">
<div class="card-icon info">
<span class="bi bi-arrow-left-right"></span>
</div>
<div class="card-content">
<div class="card-label">Operazioni Totali</div>
<div class="card-value">@portfolioStats.TotalTrades</div>
<div class="card-meta">Win Rate: @portfolioStats.WinRate.ToString("F1")%</div>
</div>
</div>
<div class="summary-card">
<div class="card-icon warning">
<span class="bi bi-currency-exchange"></span>
</div>
<div class="card-content">
<div class="card-label">Asset Attivi</div>
<div class="card-value">@portfolioStats.ActiveAssets/@portfolioStats.TotalAssets</div>
<div class="card-meta">In trading</div>
</div>
</div>
</div>
<!-- Active Assets -->
<div class="section">
<div class="section-header">
<h2>Asset Attivi</h2>
<a href="/trading" class="btn-link">Vedi Tutti <span class="bi bi-arrow-right"></span></a>
</div>
<div class="assets-quick-grid">
@foreach (var config in BotService.AssetConfigurations.Values.Where(c => c.IsEnabled).Take(6))
{
var price = BotService.GetLatestPrice(config.Symbol);
<div class="asset-quick-card">
<div class="asset-header">
<span class="asset-symbol">@config.Symbol</span>
@if (price != null)
{
<span class="asset-change @(price.Change24h >= 0 ? "positive" : "negative")">
@price.Change24h.ToString("F2")%
</span>
}
</div>
<div class="asset-price">$@(price?.Price.ToString("N2") ?? "Loading...")</div>
<div class="asset-profit @(config.TotalProfit >= 0 ? "profit" : "loss")">
$@config.TotalProfit.ToString("N2")
</div>
</div>
}
</div>
</div>
<!-- Recent Activity -->
<div class="section">
<div class="section-header">
<h2>Attività Recente</h2>
<a href="/trading" class="btn-link">Vedi Storico <span class="bi bi-arrow-right"></span></a>
</div>
@if (BotService.Trades.Count == 0)
{
<div class="empty-state">
<span class="bi bi-inbox"></span>
<p>Nessuna operazione ancora</p>
</div>
}
else
{
<div class="activity-list">
@foreach (var trade in BotService.Trades.Take(8))
{
<div class="activity-item">
<div class="activity-icon @(trade.Type == TradeType.Buy ? "buy" : "sell")">
<span class="bi @(trade.Type == TradeType.Buy ? "bi-arrow-down-circle-fill" : "bi-arrow-up-circle-fill")"></span>
</div>
<div class="activity-content">
<div class="activity-main">
<span class="activity-type">@(trade.Type == TradeType.Buy ? "ACQUISTO" : "VENDITA")</span>
<span class="activity-symbol">@trade.Symbol</span>
@if (trade.IsBot)
{
<span class="bot-badge">
<span class="bi bi-robot"></span> BOT
</span>
}
</div>
<div class="activity-details">
<span>@trade.Amount.ToString("F6") &#64; $@trade.Price.ToString("N2")</span>
<span class="separator">•</span>
<span>@trade.Timestamp.ToLocalTime().ToString("HH:mm:ss")</span>
</div>
</div>
<div class="activity-value">
$@((trade.Amount * trade.Price).ToString("N2"))
</div>
</div>
}
</div>
}
</div>
</div>
@code {
private PortfolioStatistics portfolioStats = new();
protected override void OnInitialized()
{
BotService.OnStatusChanged += HandleUpdate;
BotService.OnTradeExecuted += HandleTradeExecuted;
BotService.OnPriceUpdated += HandlePriceUpdate;
if (!BotService.Status.IsRunning)
{
BotService.Start();
}
RefreshData();
}
private void RefreshData()
{
portfolioStats = BotService.GetPortfolioStatistics();
StateHasChanged();
}
private void HandleUpdate() => InvokeAsync(RefreshData);
private void HandleTradeExecuted(Trade trade) => InvokeAsync(RefreshData);
private void HandlePriceUpdate(string symbol, MarketPrice price) => InvokeAsync(RefreshData);
public void Dispose()
{
BotService.OnStatusChanged -= HandleUpdate;
BotService.OnTradeExecuted -= HandleTradeExecuted;
BotService.OnPriceUpdated -= HandlePriceUpdate;
}
}
@@ -0,0 +1,362 @@
/* Dashboard Page */
.dashboard-page {
display: flex;
flex-direction: column;
gap: 2rem;
}
.page-header h1 {
margin: 0;
font-size: 2rem;
font-weight: 700;
color: white;
}
.subtitle {
margin: 0.5rem 0 0 0;
color: #94a3b8;
font-size: 0.875rem;
}
/* Summary Grid */
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.summary-card {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
display: flex;
gap: 1rem;
transition: all 0.3s ease;
}
.summary-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.4);
}
.summary-card.primary {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
border-color: #7c3aed;
}
.card-icon {
width: 3rem;
height: 3rem;
border-radius: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
background: rgba(255, 255, 255, 0.1);
color: white;
}
.card-icon.success {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
}
.card-icon.info {
background: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.card-icon.warning {
background: rgba(245, 158, 11, 0.2);
color: #f59e0b;
}
.card-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.card-label {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.7);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.summary-card:not(.primary) .card-label {
color: #94a3b8;
}
.card-value {
font-size: 1.875rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
line-height: 1;
}
.card-value.profit {
color: #10b981;
}
.card-value.loss {
color: #ef4444;
}
.card-change {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.875rem;
font-weight: 600;
}
.card-change.positive {
color: rgba(16, 185, 129, 0.9);
}
.card-change.negative {
color: rgba(239, 68, 68, 0.9);
}
.card-meta {
font-size: 0.75rem;
color: #64748b;
}
/* Section */
.section {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.section-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
color: white;
}
.btn-link {
display: flex;
align-items: center;
gap: 0.5rem;
color: #6366f1;
text-decoration: none;
font-size: 0.875rem;
font-weight: 600;
transition: all 0.2s ease;
}
.btn-link:hover {
gap: 0.75rem;
}
/* Assets Quick Grid */
.assets-quick-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
.asset-quick-card {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.25rem;
transition: all 0.3s ease;
}
.asset-quick-card:hover {
transform: translateY(-2px);
border-color: #6366f1;
}
.asset-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.asset-symbol {
font-size: 0.875rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
}
.asset-change {
font-size: 0.75rem;
font-weight: 600;
}
.asset-change.positive {
color: #10b981;
}
.asset-change.negative {
color: #ef4444;
}
.asset-price {
font-size: 1.5rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
margin-bottom: 0.5rem;
}
.asset-profit {
font-size: 0.875rem;
font-weight: 600;
font-family: 'Courier New', monospace;
}
.asset-profit.profit {
color: #10b981;
}
.asset-profit.loss {
color: #ef4444;
}
/* Activity List */
.activity-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.activity-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
transition: all 0.2s ease;
}
.activity-item:hover {
background: #1a1f3a;
}
.activity-icon {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
}
.activity-icon.buy {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.activity-icon.sell {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.activity-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.activity-main {
display: flex;
align-items: center;
gap: 0.5rem;
}
.activity-type {
font-size: 0.875rem;
font-weight: 600;
color: white;
}
.activity-symbol {
font-size: 0.875rem;
font-family: 'Courier New', monospace;
color: #94a3b8;
}
.bot-badge {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.375rem;
background: rgba(99, 102, 241, 0.2);
color: #6366f1;
border-radius: 0.25rem;
font-size: 0.625rem;
font-weight: 700;
}
.activity-details {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
color: #64748b;
}
.separator {
color: #334155;
}
.activity-value {
font-size: 1rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 3rem 1rem;
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
color: #64748b;
}
.empty-state .bi {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-state p {
margin: 0.5rem 0;
}
/* Responsive */
@media (max-width: 768px) {
.summary-grid {
grid-template-columns: 1fr;
}
.assets-quick-grid {
grid-template-columns: repeat(2, 1fr);
}
}
+36
View File
@@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}
@@ -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;
}
}
+238
View File
@@ -0,0 +1,238 @@
@page "/market"
@using TradingBot.Models
@using TradingBot.Services
@using TradingBot.Components.Shared
@inject TradingBotService BotService
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Analisi Mercato - TradingBot</PageTitle>
<div class="market-page">
<div class="page-header">
<div>
<h1>Analisi Mercato</h1>
<p class="subtitle">Monitora le tendenze di mercato e gli indicatori tecnici in tempo reale</p>
</div>
<select class="asset-selector" @bind="selectedSymbol" @bind:after="OnAssetChanged">
@foreach (var symbol in BotService.AssetConfigurations.Keys.OrderBy(s => s))
{
<option value="@symbol">@symbol - @BotService.AssetConfigurations[symbol].Name</option>
}
</select>
</div>
@if (selectedConfig != null && currentPrice != null)
{
<div class="market-overview">
<div class="price-card">
<div class="price-header">
<div class="asset-info">
<span class="asset-icon">@selectedSymbol.Substring(0, 1)</span>
<div>
<h2>@selectedConfig.Name</h2>
<span class="asset-symbol">@selectedSymbol</span>
</div>
</div>
<div class="price-main">
<div class="current-price">$@currentPrice.Price.ToString("N2")</div>
<div class="price-change @(currentPrice.Change24h >= 0 ? "positive" : "negative")">
<span class="bi @(currentPrice.Change24h >= 0 ? "bi-arrow-up" : "bi-arrow-down")"></span>
@Math.Abs(currentPrice.Change24h).ToString("F2")% (24h)
</div>
</div>
</div>
<div class="price-stats">
<div class="stat">
<span class="stat-label">Volume 24h</span>
<span class="stat-value">$@currentPrice.Volume24h.ToString("N0")</span>
</div>
<div class="stat">
<span class="stat-label">Holdings</span>
<span class="stat-value">@selectedConfig.CurrentHoldings.ToString("F6")</span>
</div>
<div class="stat">
<span class="stat-label">Valore Posizione</span>
<span class="stat-value">$@((selectedConfig.CurrentHoldings * currentPrice.Price).ToString("N2"))</span>
</div>
</div>
</div>
@if (currentIndicators != null)
{
<div class="indicators-grid">
<div class="indicator-card">
<div class="indicator-header">
<span class="indicator-icon">
<span class="bi bi-activity"></span>
</span>
<span class="indicator-name">RSI (14)</span>
</div>
<div class="indicator-value @GetRSIClass()">
@currentIndicators.RSI.ToString("F2")
</div>
<div class="indicator-status @GetRSIClass()">
@GetRSIStatus()
</div>
</div>
<div class="indicator-card">
<div class="indicator-header">
<span class="indicator-icon">
<span class="bi bi-graph-up"></span>
</span>
<span class="indicator-name">MACD</span>
</div>
<div class="indicator-value">
@currentIndicators.MACD.ToString("F2")
</div>
<div class="indicator-status">
Signal: @currentIndicators.Signal.ToString("F2")
</div>
</div>
<div class="indicator-card">
<div class="indicator-header">
<span class="indicator-icon">
<span class="bi bi-graph-down"></span>
</span>
<span class="indicator-name">Histogram</span>
</div>
<div class="indicator-value @(currentIndicators.Histogram >= 0 ? "positive" : "negative")">
@currentIndicators.Histogram.ToString("F4")
</div>
<div class="indicator-status">
@(currentIndicators.Histogram >= 0 ? "Bullish" : "Bearish")
</div>
</div>
<div class="indicator-card">
<div class="indicator-header">
<span class="indicator-icon">
<span class="bi bi-bezier2"></span>
</span>
<span class="indicator-name">EMA</span>
</div>
<div class="indicator-value">
@currentIndicators.EMA12.ToString("F2")
</div>
<div class="indicator-status">
EMA26: @currentIndicators.EMA26.ToString("F2")
</div>
</div>
</div>
}
</div>
<div class="chart-section">
<div class="chart-header">
<h3>Andamento Prezzi</h3>
<div class="chart-controls">
<button class="time-btn active">1H</button>
<button class="time-btn">4H</button>
<button class="time-btn">1D</button>
<button class="time-btn">1W</button>
</div>
</div>
<div class="chart-container">
<AdvancedChart
PriceData="@GetPriceList(selectedSymbol)"
Color="#6366f1"
Indicators="@currentIndicators" />
</div>
</div>
}
</div>
@code {
[SupplyParameterFromQuery(Name = "symbol")]
public string? QuerySymbol { get; set; }
private string selectedSymbol = "BTC";
private AssetConfiguration? selectedConfig => BotService.AssetConfigurations.TryGetValue(selectedSymbol, out var c) ? c : null;
private MarketPrice? currentPrice => BotService.GetLatestPrice(selectedSymbol);
private TechnicalIndicators? currentIndicators;
protected override void OnInitialized()
{
// Set initial symbol from query string if available
if (!string.IsNullOrEmpty(QuerySymbol) && BotService.AssetConfigurations.ContainsKey(QuerySymbol))
{
selectedSymbol = QuerySymbol;
}
BotService.OnPriceUpdated += HandlePriceUpdate;
BotService.OnIndicatorsUpdated += HandleIndicatorsUpdate;
UpdateIndicators();
}
protected override void OnParametersSet()
{
// Update symbol if query parameter changes
if (!string.IsNullOrEmpty(QuerySymbol) &&
QuerySymbol != selectedSymbol &&
BotService.AssetConfigurations.ContainsKey(QuerySymbol))
{
selectedSymbol = QuerySymbol;
UpdateIndicators();
}
}
private void OnAssetChanged()
{
UpdateIndicators();
StateHasChanged();
}
private void UpdateIndicators()
{
currentIndicators = BotService.GetIndicators(selectedSymbol);
}
private List<decimal>? GetPriceList(string symbol)
{
var history = BotService.GetPriceHistory(symbol);
return history?.Select(p => p.Price).ToList();
}
private string GetRSIClass()
{
if (currentIndicators == null) return "neutral";
if (currentIndicators.RSI > 70) return "overbought";
if (currentIndicators.RSI < 30) return "oversold";
return "neutral";
}
private string GetRSIStatus()
{
if (currentIndicators == null) return "Neutral";
if (currentIndicators.RSI > 70) return "Overbought";
if (currentIndicators.RSI < 30) return "Oversold";
return "Neutral";
}
private void HandlePriceUpdate(string symbol, MarketPrice price)
{
if (symbol == selectedSymbol)
{
InvokeAsync(StateHasChanged);
}
}
private void HandleIndicatorsUpdate(string symbol, TechnicalIndicators indicators)
{
if (symbol == selectedSymbol)
{
currentIndicators = indicators;
InvokeAsync(StateHasChanged);
}
}
public void Dispose()
{
BotService.OnPriceUpdated -= HandlePriceUpdate;
BotService.OnIndicatorsUpdated -= HandleIndicatorsUpdate;
}
}
@@ -0,0 +1,328 @@
/* Market Page */
.market-page {
display: flex;
flex-direction: column;
gap: 2rem;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.page-header h1 {
margin: 0;
font-size: 2rem;
font-weight: 700;
color: white;
}
.subtitle {
margin: 0.5rem 0 0 0;
color: #94a3b8;
font-size: 0.875rem;
}
.asset-selector {
padding: 0.75rem 1rem;
border-radius: 0.5rem;
border: 1px solid #334155;
background: #1e293b;
color: white;
font-size: 0.875rem;
font-weight: 600;
min-width: 250px;
}
/* Market Overview */
.market-overview {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.price-card {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 2rem;
}
.price-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.asset-info {
display: flex;
align-items: center;
gap: 1rem;
}
.asset-icon {
width: 3.5rem;
height: 3.5rem;
border-radius: 0.75rem;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: 700;
color: white;
}
.asset-info h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
color: white;
}
.asset-symbol {
font-size: 0.875rem;
color: #64748b;
font-family: 'Courier New', monospace;
}
.price-main {
text-align: right;
}
.current-price {
font-size: 2.5rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
line-height: 1;
margin-bottom: 0.5rem;
}
.price-change {
display: inline-flex;
align-items: center;
gap: 0.375rem;
font-size: 1rem;
font-weight: 600;
padding: 0.375rem 0.75rem;
border-radius: 0.5rem;
}
.price-change.positive {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.price-change.negative {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.price-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
}
.stat {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.stat-label {
font-size: 0.75rem;
color: #64748b;
text-transform: uppercase;
font-weight: 600;
}
.stat-value {
font-size: 1.25rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
}
/* Indicators Grid */
.indicators-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.indicator-card {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
transition: all 0.3s ease;
}
.indicator-card:hover {
transform: translateY(-2px);
border-color: #6366f1;
}
.indicator-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.indicator-icon {
width: 2rem;
height: 2rem;
border-radius: 0.5rem;
background: rgba(99, 102, 241, 0.1);
display: flex;
align-items: center;
justify-content: center;
color: #6366f1;
}
.indicator-name {
font-size: 0.875rem;
font-weight: 600;
color: #94a3b8;
}
.indicator-value {
font-size: 2rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
margin-bottom: 0.5rem;
}
.indicator-value.positive {
color: #10b981;
}
.indicator-value.negative {
color: #ef4444;
}
.indicator-value.overbought {
color: #ef4444;
}
.indicator-value.oversold {
color: #10b981;
}
.indicator-value.neutral {
color: #f59e0b;
}
.indicator-status {
font-size: 0.75rem;
color: #64748b;
font-weight: 600;
}
.indicator-status.overbought {
color: #ef4444;
}
.indicator-status.oversold {
color: #10b981;
}
/* Chart Section */
.chart-section {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.chart-header h3 {
margin: 0;
font-size: 1.25rem;
font-weight: 700;
color: white;
}
.chart-controls {
display: flex;
gap: 0.5rem;
}
.time-btn {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
border: 1px solid #334155;
background: transparent;
color: #94a3b8;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.time-btn:hover {
background: #1a1f3a;
border-color: #6366f1;
}
.time-btn.active {
background: #6366f1;
border-color: #6366f1;
color: white;
}
.chart-container {
height: 400px;
width: 100%;
}
/* Responsive */
@media (max-width: 1024px) {
.indicators-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.page-header {
flex-direction: column;
gap: 1rem;
}
.asset-selector {
width: 100%;
}
.price-header {
flex-direction: column;
gap: 1.5rem;
}
.price-main {
text-align: left;
}
.price-stats {
grid-template-columns: 1fr;
}
.indicators-grid {
grid-template-columns: 1fr;
}
.chart-container {
height: 300px;
}
}
@@ -0,0 +1,5 @@
@page "/not-found"
@layout MainLayout
<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>
+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;
}
}
+288
View File
@@ -0,0 +1,288 @@
@page "/settings"
@using TradingBot.Services
@using TradingBot.Models
@inject SettingsService SettingsService
@inject TradingBotService TradingBotService
@inject TradeHistoryService HistoryService
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Impostazioni - TradingBot</PageTitle>
<div class="settings-page">
<div class="page-header">
<h1>Impostazioni</h1>
<p class="subtitle">Configura le impostazioni globali del trading bot</p>
</div>
<div class="settings-section">
<h2>Generale</h2>
<div class="settings-group">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Modalità Simulazione</div>
<div class="setting-description">Utilizza dati simulati invece di dati reali di mercato</div>
</div>
<label class="toggle-switch">
<input type="checkbox" checked="@settings.SimulationMode" @onchange="(e) => UpdateSetting(nameof(AppSettings.SimulationMode), (bool)e.Value!)" disabled />
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Notifiche Desktop</div>
<div class="setting-description">Ricevi notifiche per operazioni importanti</div>
</div>
<label class="toggle-switch">
<input type="checkbox" checked="@settings.DesktopNotifications" @onchange="(e) => UpdateSetting(nameof(AppSettings.DesktopNotifications), (bool)e.Value!)" />
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
<div class="settings-section">
<h2>Trading</h2>
<div class="settings-group">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Auto-Start Bot</div>
<div class="setting-description">Avvia automaticamente il bot all'apertura dell'applicazione</div>
</div>
<label class="toggle-switch">
<input type="checkbox" checked="@settings.AutoStartBot" @onchange="(e) => UpdateSetting(nameof(AppSettings.AutoStartBot), (bool)e.Value!)" />
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Conferma Operazioni Manuali</div>
<div class="setting-description">Richiedi conferma prima di eseguire operazioni manuali</div>
</div>
<label class="toggle-switch">
<input type="checkbox" checked="@settings.ConfirmManualTrades" @onchange="(e) => UpdateSetting(nameof(AppSettings.ConfirmManualTrades), (bool)e.Value!)" />
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
<div class="settings-section">
<h2>Dati Persistenti</h2>
<div class="settings-group">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Trade Salvati</div>
<div class="setting-description">@TradingBotService.Trades.Count trade nella cronologia</div>
</div>
<div class="setting-value">
@FormatBytes(dataSize)
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Posizioni Attive</div>
<div class="setting-description">@TradingBotService.ActivePositions.Count posizioni aperte</div>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Cancella Tutti i Dati</div>
<div class="setting-description text-danger">Elimina cronologia trade e resetta i saldi</div>
</div>
<button class="btn-danger" @onclick="ShowClearDataConfirmation" disabled="@TradingBotService.Status.IsRunning">
<span class="bi bi-trash"></span>
Cancella Dati
</button>
</div>
</div>
</div>
<div class="settings-section">
<h2>Avanzate</h2>
<div class="settings-group">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Intervallo Aggiornamento</div>
<div class="setting-description">Frequenza di aggiornamento dei dati di mercato</div>
</div>
<select class="setting-select" value="@settings.UpdateIntervalSeconds" @onchange="(e) => UpdateSetting(nameof(AppSettings.UpdateIntervalSeconds), int.Parse(e.Value!.ToString()!))">
<option value="2">2 secondi</option>
<option value="3">3 secondi</option>
<option value="5">5 secondi</option>
<option value="10">10 secondi</option>
</select>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Log Level</div>
<div class="setting-description">Livello di dettaglio dei log di sistema</div>
</div>
<select class="setting-select" value="@settings.LogLevel" @onchange="(e) => UpdateSetting(nameof(AppSettings.LogLevel), e.Value!.ToString()!)">
<option value="Error">Error</option>
<option value="Warning">Warning</option>
<option value="Info">Info</option>
<option value="Debug">Debug</option>
</select>
</div>
</div>
</div>
<div class="settings-actions">
<button class="btn-secondary" @onclick="ResetToDefaults">
<span class="bi bi-arrow-counterclockwise"></span>
Reset Predefiniti
</button>
<button class="btn-primary" @onclick="SaveSettings">
<span class="bi bi-check-lg"></span>
Salva Modifiche
</button>
</div>
@if (showNotification)
{
<div class="notification success">
<span class="bi bi-check-circle-fill"></span>
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)
{
SettingsService.UpdateSetting(propertyName, value);
settings = SettingsService.GetSettings();
ShowNotification();
}
private void SaveSettings()
{
SettingsService.UpdateSettings(settings);
ShowNotification();
}
private void ResetToDefaults()
{
SettingsService.ResetToDefaults();
settings = SettingsService.GetSettings();
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;
StateHasChanged();
await Task.Delay(3000);
showNotification = false;
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,221 @@
/* Settings Page */
.settings-page {
max-width: 900px;
display: flex;
flex-direction: column;
gap: 2rem;
}
.page-header h1 {
margin: 0;
font-size: 2rem;
font-weight: 700;
color: white;
}
.subtitle {
margin: 0.5rem 0 0 0;
color: #94a3b8;
font-size: 0.875rem;
}
.settings-section {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 2rem;
}
.settings-section h2 {
margin: 0 0 1.5rem 0;
font-size: 1.25rem;
font-weight: 700;
color: white;
}
.settings-group {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 2rem;
}
.setting-info {
flex: 1;
}
.setting-label {
font-size: 0.938rem;
font-weight: 600;
color: white;
margin-bottom: 0.25rem;
}
.setting-description {
font-size: 0.875rem;
color: #64748b;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 3rem;
height: 1.5rem;
flex-shrink: 0;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #334155;
transition: 0.3s;
border-radius: 1.5rem;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 1.125rem;
width: 1.125rem;
left: 0.1875rem;
bottom: 0.1875rem;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
.toggle-switch input:checked + .toggle-slider {
background-color: #6366f1;
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(1.5rem);
}
.toggle-switch input:disabled + .toggle-slider {
opacity: 0.5;
cursor: not-allowed;
}
.setting-select {
padding: 0.625rem 1rem;
border-radius: 0.5rem;
border: 1px solid #334155;
background: #1a1f3a;
color: white;
font-size: 0.875rem;
font-weight: 600;
min-width: 150px;
}
.settings-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
}
.btn-primary, .btn-secondary {
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;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-primary {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: white;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4);
}
.btn-secondary {
background: #1e293b;
color: #cbd5e1;
border: 1px solid #334155;
}
.btn-secondary:hover {
background: #334155;
border-color: #475569;
}
/* Notification */
.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 8px 16px rgba(0, 0, 0, 0.3);
animation: slide-in 0.3s ease;
z-index: 1000;
}
@keyframes slide-in {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.notification.success {
background: #064e3b;
border: 1px solid #065f46;
color: #6ee7b7;
}
.notification .bi {
font-size: 1.25rem;
}
/* Responsive */
@media (max-width: 768px) {
.setting-item {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.settings-actions {
flex-direction: column;
}
.btn-primary, .btn-secondary {
width: 100%;
justify-content: center;
}
}
@@ -0,0 +1,460 @@
@page "/statistics"
@using TradingBot.Models
@using TradingBot.Services
@inject TradingBotService BotService
@inject NavigationManager Navigation
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Statistiche - TradingBot</PageTitle>
<div class="statistics-page">
<!-- Header -->
<header class="stats-header">
<div class="header-content">
<div class="page-title">
<h1><span class="bi bi-graph-up"></span> Statistiche Avanzate</h1>
<p class="subtitle">Analisi dettagliata delle performance e metriche di trading</p>
</div>
<div class="header-filters">
<select class="filter-select" @bind="selectedSymbol" @bind:after="OnSymbolChanged">
<option value="">Tutti gli Asset</option>
@foreach (var symbol in BotService.AssetConfigurations.Keys.OrderBy(s => s))
{
<option value="@symbol">@symbol</option>
}
</select>
</div>
</div>
</header>
<div class="stats-content">
@if (string.IsNullOrEmpty(selectedSymbol))
{
<!-- Portfolio Overview -->
<div class="overview-section">
<h2 class="section-title">
<span class="bi bi-pie-chart-fill"></span> Panoramica Portfolio
</h2>
<div class="stats-grid">
<div class="stat-card primary">
<div class="stat-header">
<span class="stat-icon"><span class="bi bi-wallet2"></span></span>
<span class="stat-label">Valore Totale</span>
</div>
<div class="stat-value">$@portfolioStats.TotalBalance.ToString("N2")</div>
<div class="stat-footer">
<span class="stat-change @(portfolioStats.TotalProfitPercentage >= 0 ? "positive" : "negative")">
<span class="bi @(portfolioStats.TotalProfitPercentage >= 0 ? "bi-arrow-up" : "bi-arrow-down")"></span>
@Math.Abs(portfolioStats.TotalProfitPercentage).ToString("F2")%
</span>
</div>
</div>
<div class="stat-card">
<div class="stat-header">
<span class="stat-icon success"><span class="bi bi-trophy"></span></span>
<span class="stat-label">Profitto Netto</span>
</div>
<div class="stat-value @(portfolioStats.TotalProfit >= 0 ? "profit" : "loss")">
$@portfolioStats.TotalProfit.ToString("N2")
</div>
<div class="stat-footer">
<span class="stat-meta">Da $@portfolioStats.InitialBalance.ToString("N2")</span>
</div>
</div>
<div class="stat-card">
<div class="stat-header">
<span class="stat-icon info"><span class="bi bi-arrow-left-right"></span></span>
<span class="stat-label">Totale Operazioni</span>
</div>
<div class="stat-value">@portfolioStats.TotalTrades</div>
<div class="stat-footer">
<span class="stat-meta">@portfolioStats.ActiveAssets asset attivi</span>
</div>
</div>
<div class="stat-card">
<div class="stat-header">
<span class="stat-icon warning"><span class="bi bi-percent"></span></span>
<span class="stat-label">Win Rate</span>
</div>
<div class="stat-value">@portfolioStats.WinRate.ToString("F1")%</div>
<div class="stat-footer">
<span class="stat-meta">Tasso di successo</span>
</div>
</div>
</div>
<!-- Best/Worst Performers -->
<div class="performers-section">
<div class="performer-card best">
<div class="performer-header">
<span class="bi bi-trophy-fill"></span>
<span>Miglior Performer</span>
</div>
@if (!string.IsNullOrEmpty(portfolioStats.BestPerformingAssetSymbol))
{
<div class="performer-content">
<div class="performer-symbol">@portfolioStats.BestPerformingAssetSymbol</div>
<div class="performer-value profit">+$@portfolioStats.BestPerformingAssetProfit.ToString("N2")</div>
</div>
}
else
{
<div class="empty-performer">Nessun dato</div>
}
</div>
<div class="performer-card worst">
<div class="performer-header">
<span class="bi bi-graph-down"></span>
<span>Peggior Performer</span>
</div>
@if (!string.IsNullOrEmpty(portfolioStats.WorstPerformingAssetSymbol))
{
<div class="performer-content">
<div class="performer-symbol">@portfolioStats.WorstPerformingAssetSymbol</div>
<div class="performer-value @(portfolioStats.WorstPerformingAssetProfit >= 0 ? "profit" : "loss")">
$@portfolioStats.WorstPerformingAssetProfit.ToString("N2")
</div>
</div>
}
else
{
<div class="empty-performer">Nessun dato</div>
}
</div>
</div>
</div>
<!-- Asset Breakdown -->
<div class="breakdown-section">
<h2 class="section-title">
<span class="bi bi-list-columns-reverse"></span> Breakdown per Asset
</h2>
<div class="breakdown-table">
<div class="table-header">
<div class="th">Asset</div>
<div class="th">Valore</div>
<div class="th">Profitto</div>
<div class="th">% Profitto</div>
<div class="th">Trades</div>
<div class="th">Win Rate</div>
<div class="th">Azioni</div>
</div>
@foreach (var assetStat in portfolioStats.AssetStatistics.OrderByDescending(a => a.NetProfit))
{
var config = BotService.AssetConfigurations.TryGetValue(assetStat.Symbol, out var c) ? c : null;
if (config == null) continue;
var currentValue = config.CurrentBalance + (config.CurrentHoldings * assetStat.CurrentPrice);
<div class="table-row">
<div class="td asset-cell">
<span class="asset-symbol">@assetStat.Symbol</span>
<span class="asset-name">@assetStat.Name</span>
</div>
<div class="td">$@currentValue.ToString("N2")</div>
<div class="td @(assetStat.NetProfit >= 0 ? "profit" : "loss")">
$@assetStat.NetProfit.ToString("N2")
</div>
<div class="td @(config.ProfitPercentage >= 0 ? "profit" : "loss")">
@config.ProfitPercentage.ToString("F2")%
</div>
<div class="td">@assetStat.TotalTrades</div>
<div class="td">@assetStat.WinRate.ToString("F1")%</div>
<div class="td">
<button class="btn-details" @onclick="() => ViewAssetDetails(assetStat.Symbol)">
<span class="bi bi-eye"></span> Dettagli
</button>
</div>
</div>
}
</div>
</div>
}
else
{
<!-- Single Asset Statistics -->
var assetStats = BotService.AssetStatistics.TryGetValue(selectedSymbol, out var stats) ? stats : null;
var assetConfig = BotService.AssetConfigurations.TryGetValue(selectedSymbol, out var config) ? config : null;
@if (assetStats != null && assetConfig != null)
{
<div class="asset-details-section">
<div class="asset-details-header">
<div class="asset-title-section">
<h2>@assetStats.Name (@assetStats.Symbol)</h2>
<span class="status-badge @(assetConfig.IsEnabled ? "active" : "inactive")">
@(assetConfig.IsEnabled ? "Attivo" : "Inattivo")
</span>
</div>
<button class="btn-back" @onclick="ClearSelection">
<span class="bi bi-arrow-left"></span> Torna alla panoramica
</button>
</div>
<!-- Key Metrics -->
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-icon"><span class="bi bi-cash-stack"></span></div>
<div class="metric-content">
<div class="metric-label">Prezzo Corrente</div>
<div class="metric-value">$@assetStats.CurrentPrice.ToString("N2")</div>
</div>
</div>
<div class="metric-card">
<div class="metric-icon success"><span class="bi bi-bar-chart-line"></span></div>
<div class="metric-content">
<div class="metric-label">Holdings</div>
<div class="metric-value">@assetConfig.CurrentHoldings.ToString("F6")</div>
</div>
</div>
<div class="metric-card">
<div class="metric-icon @(assetStats.NetProfit >= 0 ? "success" : "danger")">
<span class="bi bi-graph-up-arrow"></span>
</div>
<div class="metric-content">
<div class="metric-label">Profitto Netto</div>
<div class="metric-value @(assetStats.NetProfit >= 0 ? "profit" : "loss")">
$@assetStats.NetProfit.ToString("N2")
</div>
</div>
</div>
<div class="metric-card">
<div class="metric-icon info"><span class="bi bi-percent"></span></div>
<div class="metric-content">
<div class="metric-label">ROI</div>
<div class="metric-value @(assetConfig.ProfitPercentage >= 0 ? "profit" : "loss")">
@assetConfig.ProfitPercentage.ToString("F2")%
</div>
</div>
</div>
</div>
<!-- Trading Performance -->
<div class="performance-section">
<h3 class="subsection-title">Performance Trading</h3>
<div class="performance-grid">
<div class="performance-item">
<span class="perf-label">Totale Operazioni</span>
<span class="perf-value">@assetStats.TotalTrades</span>
</div>
<div class="performance-item">
<span class="perf-label">Operazioni Vincenti</span>
<span class="perf-value profit">@assetStats.WinningTrades</span>
</div>
<div class="performance-item">
<span class="perf-label">Operazioni Perdenti</span>
<span class="perf-value loss">@assetStats.LosingTrades</span>
</div>
<div class="performance-item">
<span class="perf-label">Win Rate</span>
<span class="perf-value">@assetStats.WinRate.ToString("F1")%</span>
</div>
<div class="performance-item">
<span class="perf-label">Profit Factor</span>
<span class="perf-value">@(assetStats.ProfitFactor > 1000 ? ">1000" : assetStats.ProfitFactor.ToString("F2"))</span>
</div>
<div class="performance-item">
<span class="perf-label">Vittorie Consecutive</span>
<span class="perf-value">@assetStats.MaxConsecutiveWins</span>
</div>
</div>
</div>
<!-- Profit/Loss Analysis -->
<div class="pnl-section">
<h3 class="subsection-title">Analisi Profitti/Perdite</h3>
<div class="pnl-grid">
<div class="pnl-card profit-card">
<div class="pnl-header">
<span class="bi bi-arrow-up-circle-fill"></span>
<span>Profitti</span>
</div>
<div class="pnl-amount profit">$@assetStats.TotalProfit.ToString("N2")</div>
<div class="pnl-meta">
Media per trade: $@assetStats.AverageProfit.ToString("N2")
</div>
<div class="pnl-meta">
Profitto massimo: $@assetStats.LargestWin.ToString("N2")
</div>
</div>
<div class="pnl-card loss-card">
<div class="pnl-header">
<span class="bi bi-arrow-down-circle-fill"></span>
<span>Perdite</span>
</div>
<div class="pnl-amount loss">$@assetStats.TotalLoss.ToString("N2")</div>
<div class="pnl-meta">
Media per trade: $@assetStats.AverageLoss.ToString("N2")
</div>
<div class="pnl-meta">
Perdita massima: $@assetStats.LargestLoss.ToString("N2")
</div>
</div>
@if (assetStats.UnrealizedPnL != 0)
{
<div class="pnl-card unrealized-card">
<div class="pnl-header">
<span class="bi bi-hourglass-split"></span>
<span>P/L Non Realizzato</span>
</div>
<div class="pnl-amount @(assetStats.UnrealizedPnL >= 0 ? "profit" : "loss")">
$@assetStats.UnrealizedPnL.ToString("N2")
</div>
<div class="pnl-meta">
@assetStats.UnrealizedPnLPercentage.ToString("F2")% sulla posizione corrente
</div>
</div>
}
</div>
</div>
<!-- Recent Trades -->
@if (assetStats.RecentTrades.Count > 0)
{
<div class="trades-section">
<h3 class="subsection-title">Operazioni Recenti</h3>
<div class="trades-list">
@foreach (var trade in assetStats.RecentTrades.Take(20))
{
<div class="trade-item @(trade.IsBot ? "bot-trade" : "")">
<div class="trade-icon @(trade.Type == TradeType.Buy ? "buy" : "sell")">
<span class="bi @(trade.Type == TradeType.Buy ? "bi-arrow-down-circle-fill" : "bi-arrow-up-circle-fill")"></span>
</div>
<div class="trade-details">
<div class="trade-type">
@(trade.Type == TradeType.Buy ? "ACQUISTO" : "VENDITA")
@if (trade.IsBot)
{
<span class="bot-label">
<span class="bi bi-robot"></span> BOT
</span>
}
</div>
<div class="trade-meta">
@trade.Timestamp.ToLocalTime().ToString("dd/MM/yyyy HH:mm:ss")
</div>
</div>
<div class="trade-amounts">
<div class="trade-quantity">@trade.Amount.ToString("F6") @trade.Symbol</div>
<div class="trade-price">&#64; $@trade.Price.ToString("N2")</div>
</div>
<div class="trade-value">
$@((trade.Amount * trade.Price).ToString("N2"))
</div>
</div>
}
</div>
</div>
}
else
{
<div class="empty-trades">
<span class="bi bi-inbox"></span>
<p>Nessuna operazione eseguita per questo asset</p>
</div>
}
</div>
}
else
{
<div class="empty-state">
<span class="bi bi-exclamation-circle"></span>
<p>Asset non trovato o dati non disponibili</p>
</div>
}
}
</div>
</div>
@code {
[SupplyParameterFromQuery(Name = "symbol")]
private string? QuerySymbol { get; set; }
private string selectedSymbol = "";
private PortfolioStatistics portfolioStats = new();
protected override void OnInitialized()
{
BotService.OnStatusChanged += HandleUpdate;
BotService.OnTradeExecuted += HandleTradeExecuted;
BotService.OnStatisticsUpdated += HandleUpdate;
BotService.OnPriceUpdated += HandlePriceUpdate;
if (!string.IsNullOrEmpty(QuerySymbol))
{
selectedSymbol = QuerySymbol;
}
RefreshData();
}
protected override void OnParametersSet()
{
if (!string.IsNullOrEmpty(QuerySymbol) && QuerySymbol != selectedSymbol)
{
selectedSymbol = QuerySymbol;
RefreshData();
}
}
private void RefreshData()
{
portfolioStats = BotService.GetPortfolioStatistics();
StateHasChanged();
}
private void OnSymbolChanged()
{
if (string.IsNullOrEmpty(selectedSymbol))
{
Navigation.NavigateTo("/statistics");
}
else
{
Navigation.NavigateTo($"/statistics?symbol={selectedSymbol}");
}
RefreshData();
}
private void ViewAssetDetails(string symbol)
{
selectedSymbol = symbol;
Navigation.NavigateTo($"/statistics?symbol={symbol}");
RefreshData();
}
private void ClearSelection()
{
selectedSymbol = "";
Navigation.NavigateTo("/statistics");
RefreshData();
}
private void HandleUpdate() => InvokeAsync(RefreshData);
private void HandleTradeExecuted(Trade trade) => InvokeAsync(RefreshData);
private void HandlePriceUpdate(string symbol, MarketPrice price) => InvokeAsync(RefreshData);
public void Dispose()
{
BotService.OnStatusChanged -= HandleUpdate;
BotService.OnTradeExecuted -= HandleTradeExecuted;
BotService.OnStatisticsUpdated -= HandleUpdate;
BotService.OnPriceUpdated -= HandlePriceUpdate;
}
}
@@ -0,0 +1,755 @@
/* Statistics Page */
.statistics-page {
min-height: 100vh;
background: #020617;
color: #f1f5f9;
}
/* Header */
.stats-header {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
border-bottom: 1px solid #1e293b;
padding: 2rem 1.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
}
.header-content {
max-width: 1400px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
gap: 2rem;
}
.page-title h1 {
margin: 0;
font-size: 2rem;
font-weight: 700;
color: white;
display: flex;
align-items: center;
gap: 0.75rem;
}
.subtitle {
margin: 0.5rem 0 0 0;
color: #94a3b8;
font-size: 0.875rem;
}
.header-filters {
display: flex;
gap: 0.75rem;
}
.filter-select {
padding: 0.75rem 1rem;
border-radius: 0.5rem;
border: 1px solid #334155;
background: #1e293b;
color: white;
font-size: 0.875rem;
cursor: pointer;
min-width: 200px;
}
.filter-select:focus {
outline: none;
border-color: #6366f1;
}
/* Content */
.stats-content {
max-width: 1400px;
margin: 0 auto;
padding: 2rem 1.5rem;
}
/* Section Title */
.section-title {
margin: 0 0 1.5rem 0;
font-size: 1.5rem;
font-weight: 700;
color: white;
display: flex;
align-items: center;
gap: 0.75rem;
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2.5rem;
}
.stat-card {
background: #0f172a;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px -4px rgba(0, 0, 0, 0.4);
}
.stat-card.primary {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
border-color: #7c3aed;
}
.stat-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
.stat-icon {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
background: rgba(255, 255, 255, 0.1);
color: white;
}
.stat-icon.success {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
}
.stat-icon.info {
background: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.stat-icon.warning {
background: rgba(245, 158, 11, 0.2);
color: #f59e0b;
}
.stat-label {
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.7);
font-weight: 600;
}
.stat-card:not(.primary) .stat-label {
color: #94a3b8;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
margin-bottom: 0.5rem;
}
.stat-footer {
display: flex;
align-items: center;
gap: 0.5rem;
}
.stat-change {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.875rem;
font-weight: 600;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
}
.stat-change.positive {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.stat-change.negative {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.stat-meta {
font-size: 0.75rem;
color: #64748b;
}
/* Performers Section */
.performers-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
margin-bottom: 2.5rem;
}
.performer-card {
background: #0f172a;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
}
.performer-card.best {
border-color: rgba(16, 185, 129, 0.3);
background: linear-gradient(135deg, rgba(16, 185, 129, 0.05) 0%, #0f172a 100%);
}
.performer-card.worst {
border-color: rgba(239, 68, 68, 0.3);
background: linear-gradient(135deg, rgba(239, 68, 68, 0.05) 0%, #0f172a 100%);
}
.performer-header {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
color: #94a3b8;
margin-bottom: 1rem;
}
.performer-card.best .performer-header {
color: #10b981;
}
.performer-card.worst .performer-header {
color: #ef4444;
}
.performer-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.performer-symbol {
font-size: 1.5rem;
font-weight: 700;
color: white;
font-family: monospace;
}
.performer-value {
font-size: 1.5rem;
font-weight: 700;
font-family: monospace;
}
.empty-performer {
text-align: center;
color: #475569;
font-size: 0.875rem;
padding: 1rem;
}
/* Breakdown Table */
.breakdown-section {
margin-bottom: 2.5rem;
}
.breakdown-table {
background: #0f172a;
border: 1px solid #1e293b;
border-radius: 0.75rem;
overflow: hidden;
}
.table-header {
display: grid;
grid-template-columns: 2fr 1.5fr 1.5fr 1fr 1fr 1fr 1.5fr;
gap: 1rem;
padding: 1rem 1.5rem;
background: #1e293b;
border-bottom: 1px solid #334155;
}
.th {
font-size: 0.75rem;
font-weight: 700;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.table-row {
display: grid;
grid-template-columns: 2fr 1.5fr 1.5fr 1fr 1fr 1fr 1.5fr;
gap: 1rem;
padding: 1rem 1.5rem;
border-bottom: 1px solid #1e293b;
transition: background 0.2s ease;
}
.table-row:hover {
background: #1e293b;
}
.td {
display: flex;
align-items: center;
font-size: 0.875rem;
color: white;
}
.asset-cell {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
}
.asset-symbol {
font-weight: 700;
font-family: monospace;
color: white;
}
.asset-name {
font-size: 0.75rem;
color: #64748b;
}
.btn-details {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 600;
border: 1px solid #334155;
background: transparent;
color: #94a3b8;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-details:hover {
background: #1e293b;
border-color: #6366f1;
color: white;
}
/* Asset Details */
.asset-details-section {
display: flex;
flex-direction: column;
gap: 2rem;
}
.asset-details-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.asset-title-section {
display: flex;
align-items: center;
gap: 1rem;
}
.asset-title-section h2 {
margin: 0;
font-size: 1.875rem;
font-weight: 700;
color: white;
}
.status-badge {
padding: 0.375rem 0.75rem;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
}
.status-badge.active {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
border: 1px solid rgba(16, 185, 129, 0.3);
}
.status-badge.inactive {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.3);
}
.btn-back {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
border: 1px solid #334155;
background: #1e293b;
color: #94a3b8;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-back:hover {
background: #334155;
color: white;
}
/* Metrics Grid */
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.metric-card {
background: #0f172a;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
display: flex;
align-items: center;
gap: 1rem;
}
.metric-icon {
width: 3rem;
height: 3rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
background: rgba(255, 255, 255, 0.1);
color: white;
}
.metric-icon.success {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
}
.metric-icon.danger {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.metric-icon.info {
background: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.metric-content {
flex: 1;
}
.metric-label {
font-size: 0.75rem;
color: #94a3b8;
text-transform: uppercase;
font-weight: 600;
margin-bottom: 0.5rem;
}
.metric-value {
font-size: 1.5rem;
font-weight: 700;
color: white;
font-family: monospace;
}
/* Performance Section */
.performance-section {
background: #0f172a;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
}
.subsection-title {
margin: 0 0 1.5rem 0;
font-size: 1.125rem;
font-weight: 600;
color: white;
}
.performance-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.performance-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem;
background: #020617;
border: 1px solid #1e293b;
border-radius: 0.5rem;
}
.perf-label {
font-size: 0.75rem;
color: #64748b;
font-weight: 600;
}
.perf-value {
font-size: 1.25rem;
font-weight: 700;
color: white;
font-family: monospace;
}
/* P/L Section */
.pnl-section {
background: #0f172a;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
}
.pnl-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.pnl-card {
background: #020617;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
}
.profit-card {
border-color: rgba(16, 185, 129, 0.3);
background: linear-gradient(135deg, rgba(16, 185, 129, 0.05) 0%, #020617 100%);
}
.loss-card {
border-color: rgba(239, 68, 68, 0.3);
background: linear-gradient(135deg, rgba(239, 68, 68, 0.05) 0%, #020617 100%);
}
.unrealized-card {
border-color: rgba(245, 158, 11, 0.3);
background: linear-gradient(135deg, rgba(245, 158, 11, 0.05) 0%, #020617 100%);
}
.pnl-header {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
color: #94a3b8;
margin-bottom: 1rem;
}
.profit-card .pnl-header {
color: #10b981;
}
.loss-card .pnl-header {
color: #ef4444;
}
.unrealized-card .pnl-header {
color: #f59e0b;
}
.pnl-amount {
font-size: 1.875rem;
font-weight: 700;
font-family: monospace;
margin-bottom: 0.75rem;
}
.pnl-meta {
font-size: 0.75rem;
color: #64748b;
margin-bottom: 0.375rem;
}
/* Trades Section */
.trades-section {
background: #0f172a;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
}
.trades-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.trade-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: #020617;
border: 1px solid #1e293b;
border-radius: 0.5rem;
transition: all 0.2s ease;
}
.trade-item:hover {
background: #1e293b;
}
.trade-item.bot-trade {
border-color: rgba(99, 102, 241, 0.3);
background: rgba(99, 102, 241, 0.05);
}
.trade-icon {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
}
.trade-icon.buy {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.trade-icon.sell {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.trade-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.trade-type {
font-size: 0.875rem;
font-weight: 600;
color: white;
display: flex;
align-items: center;
gap: 0.5rem;
}
.bot-label {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.375rem;
background: rgba(99, 102, 241, 0.2);
color: #6366f1;
border-radius: 0.25rem;
font-size: 0.625rem;
font-weight: 700;
}
.trade-meta {
font-size: 0.75rem;
color: #64748b;
}
.trade-amounts {
display: flex;
flex-direction: column;
gap: 0.25rem;
align-items: flex-end;
}
.trade-quantity {
font-size: 0.875rem;
color: white;
font-family: monospace;
}
.trade-price {
font-size: 0.75rem;
color: #64748b;
}
.trade-value {
font-size: 1rem;
font-weight: 700;
color: white;
font-family: monospace;
min-width: 100px;
text-align: right;
}
/* Empty States */
.empty-state, .empty-trades {
text-align: center;
padding: 3rem 1rem;
color: #64748b;
}
.empty-state .bi, .empty-trades .bi {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-state p, .empty-trades p {
margin: 1rem 0;
font-size: 1rem;
}
/* Common Styles */
.profit {
color: #10b981 !important;
}
.loss {
color: #ef4444 !important;
}
/* Responsive */
@media (max-width: 1024px) {
.performers-section {
grid-template-columns: 1fr;
}
.table-header, .table-row {
grid-template-columns: 1fr;
}
.th:not(:first-child), .td:not(:first-child) {
display: none;
}
}
@media (max-width: 768px) {
.header-content {
flex-direction: column;
align-items: flex-start;
}
.stats-grid, .metrics-grid, .performance-grid, .pnl-grid {
grid-template-columns: 1fr;
}
}
@@ -0,0 +1,318 @@
@page "/strategies"
@using TradingBot.Models
@using TradingBot.Services
@inject TradingBotService BotService
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Strategie - TradingBot</PageTitle>
<div class="strategies-page">
<div class="page-header">
<div>
<h1>Gestione Strategie</h1>
<p class="subtitle">Crea e gestisci le tue strategie di trading automatizzate</p>
</div>
<button class="btn-primary">
<span class="bi bi-plus-lg"></span>
Nuova Strategia
</button>
</div>
<div class="strategies-grid">
<!-- Active Strategy Card -->
<div class="strategy-card active">
<div class="card-header">
<div class="strategy-info">
<h3>RSI + MACD Cross</h3>
<span class="badge active">ATTIVA</span>
</div>
<div class="strategy-actions">
<button class="btn-icon" title="Modifica">
<span class="bi bi-pencil"></span>
</button>
<button class="btn-icon" title="Duplica">
<span class="bi bi-files"></span>
</button>
</div>
</div>
<div class="card-body">
<div class="strategy-description">
Strategia basata su indicatori tecnici RSI e MACD per identificare punti di ingresso e uscita ottimali
</div>
<div class="strategy-stats">
<div class="stat">
<span class="stat-label">Asset Applicati</span>
<span class="stat-value">@activeAssets/@totalAssets</span>
</div>
<div class="stat">
<span class="stat-label">Win Rate</span>
<span class="stat-value profit">@portfolioStats.WinRate.ToString("F1")%</span>
</div>
<div class="stat">
<span class="stat-label">Trades Totali</span>
<span class="stat-value">@portfolioStats.TotalTrades</span>
</div>
<div class="stat">
<span class="stat-label">Profitto</span>
<span class="stat-value @(portfolioStats.TotalProfit >= 0 ? "profit" : "loss")">
$@portfolioStats.TotalProfit.ToString("N2")
</span>
</div>
</div>
<div class="strategy-parameters">
<h4>Parametri</h4>
<div class="params-grid">
<div class="param">
<span class="param-label">Condizione BUY</span>
<code class="param-value">RSI &lt; 40 AND MACD &gt; 0</code>
</div>
<div class="param">
<span class="param-label">Condizione SELL</span>
<code class="param-value">RSI &gt; 60 AND MACD &lt; 0</code>
</div>
<div class="param">
<span class="param-label">Stop Loss</span>
<code class="param-value">5%</code>
</div>
<div class="param">
<span class="param-label">Take Profit</span>
<code class="param-value">10%</code>
</div>
</div>
</div>
<div class="strategy-indicators">
<h4>Indicatori Utilizzati</h4>
<div class="indicators-list">
<span class="indicator-tag">
<span class="bi bi-graph-up"></span>
RSI (14)
</span>
<span class="indicator-tag">
<span class="bi bi-graph-down"></span>
MACD (12, 26, 9)
</span>
<span class="indicator-tag">
<span class="bi bi-activity"></span>
EMA (12, 26)
</span>
</div>
</div>
</div>
<div class="card-footer">
<button class="btn-secondary">
<span class="bi bi-pause-circle"></span>
Disattiva
</button>
<button class="btn-primary" @onclick="@(() => NavigateToStatistics())">
<span class="bi bi-bar-chart-line"></span>
Vedi Performance
</button>
</div>
</div>
<!-- Example Inactive Strategy Cards -->
<div class="strategy-card">
<div class="card-header">
<div class="strategy-info">
<h3>Media Mobile Semplice</h3>
<span class="badge inactive">INATTIVA</span>
</div>
<div class="strategy-actions">
<button class="btn-icon" title="Modifica">
<span class="bi bi-pencil"></span>
</button>
<button class="btn-icon" title="Elimina">
<span class="bi bi-trash"></span>
</button>
</div>
</div>
<div class="card-body">
<div class="strategy-description">
Strategia classica basata sull'incrocio di medie mobili a breve e lungo termine
</div>
<div class="strategy-stats">
<div class="stat">
<span class="stat-label">Asset Applicati</span>
<span class="stat-value">0/@totalAssets</span>
</div>
<div class="stat">
<span class="stat-label">Win Rate</span>
<span class="stat-value">-</span>
</div>
<div class="stat">
<span class="stat-label">Trades Totali</span>
<span class="stat-value">0</span>
</div>
<div class="stat">
<span class="stat-label">Profitto</span>
<span class="stat-value">$0.00</span>
</div>
</div>
<div class="strategy-parameters">
<h4>Parametri</h4>
<div class="params-grid">
<div class="param">
<span class="param-label">SMA Breve</span>
<code class="param-value">10 periodi</code>
</div>
<div class="param">
<span class="param-label">SMA Lungo</span>
<code class="param-value">30 periodi</code>
</div>
<div class="param">
<span class="param-label">Stop Loss</span>
<code class="param-value">3%</code>
</div>
<div class="param">
<span class="param-label">Take Profit</span>
<code class="param-value">8%</code>
</div>
</div>
</div>
<div class="strategy-indicators">
<h4>Indicatori Utilizzati</h4>
<div class="indicators-list">
<span class="indicator-tag">
<span class="bi bi-graph-up"></span>
SMA (10)
</span>
<span class="indicator-tag">
<span class="bi bi-graph-up"></span>
SMA (30)
</span>
</div>
</div>
</div>
<div class="card-footer">
<button class="btn-secondary">
<span class="bi bi-play-circle"></span>
Attiva
</button>
<button class="btn-secondary">
<span class="bi bi-pencil"></span>
Modifica
</button>
</div>
</div>
<!-- Template Strategy Card -->
<div class="strategy-card template">
<div class="template-content">
<div class="template-icon">
<span class="bi bi-diagram-3"></span>
</div>
<h3>Crea Nuova Strategia</h3>
<p>Progetta una strategia personalizzata con indicatori tecnici e regole di trading</p>
<button class="btn-primary">
<span class="bi bi-plus-lg"></span>
Inizia Ora
</button>
</div>
</div>
</div>
<!-- Strategy Templates Section -->
<div class="templates-section">
<h2>Template Strategie</h2>
<p class="section-subtitle">Inizia da modelli predefiniti e personalizzali secondo le tue esigenze</p>
<div class="templates-grid">
<div class="template-item">
<div class="template-header">
<span class="bi bi-lightning-charge"></span>
<h4>Scalping Veloce</h4>
</div>
<p>Strategia ad alta frequenza per profitti rapidi su piccoli movimenti di prezzo</p>
<button class="btn-outline">
<span class="bi bi-download"></span>
Usa Template
</button>
</div>
<div class="template-item">
<div class="template-header">
<span class="bi bi-graph-up-arrow"></span>
<h4>Trend Following</h4>
</div>
<p>Segui le tendenze di mercato dominanti per massimizzare i profitti</p>
<button class="btn-outline">
<span class="bi bi-download"></span>
Usa Template
</button>
</div>
<div class="template-item">
<div class="template-header">
<span class="bi bi-arrow-left-right"></span>
<h4>Mean Reversion</h4>
</div>
<p>Sfrutta il ritorno dei prezzi verso la media storica</p>
<button class="btn-outline">
<span class="bi bi-download"></span>
Usa Template
</button>
</div>
<div class="template-item">
<div class="template-header">
<span class="bi bi-shield-check"></span>
<h4>Conservative</h4>
</div>
<p>Strategia a basso rischio con protezione del capitale</p>
<button class="btn-outline">
<span class="bi bi-download"></span>
Usa Template
</button>
</div>
</div>
</div>
</div>
@code {
private PortfolioStatistics portfolioStats = new();
private int activeAssets = 0;
private int totalAssets = 0;
protected override void OnInitialized()
{
BotService.OnStatusChanged += HandleUpdate;
BotService.OnTradeExecuted += HandleTradeExecuted;
RefreshData();
}
private void RefreshData()
{
portfolioStats = BotService.GetPortfolioStatistics();
activeAssets = BotService.AssetConfigurations.Values.Count(c => c.IsEnabled);
totalAssets = BotService.AssetConfigurations.Count;
StateHasChanged();
}
private void NavigateToStatistics()
{
// Navigate to statistics page
}
private void HandleUpdate() => InvokeAsync(RefreshData);
private void HandleTradeExecuted(Trade trade) => InvokeAsync(RefreshData);
public void Dispose()
{
BotService.OnStatusChanged -= HandleUpdate;
BotService.OnTradeExecuted -= HandleTradeExecuted;
}
}
@@ -0,0 +1,407 @@
/* Strategies Page */
.strategies-page {
display: flex;
flex-direction: column;
gap: 2rem;
}
/* Page Header */
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.page-header h1 {
margin: 0;
font-size: 2rem;
font-weight: 700;
color: white;
}
.subtitle {
margin: 0.5rem 0 0 0;
color: #94a3b8;
font-size: 0.875rem;
}
/* Buttons */
.btn-primary, .btn-secondary, .btn-outline {
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;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-primary {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: white;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4);
}
.btn-secondary {
background: #1e293b;
color: #cbd5e1;
border: 1px solid #334155;
}
.btn-secondary:hover {
background: #334155;
border-color: #475569;
}
.btn-outline {
background: transparent;
color: #6366f1;
border: 1px solid #6366f1;
}
.btn-outline:hover {
background: rgba(99, 102, 241, 0.1);
}
.btn-icon {
width: 2rem;
height: 2rem;
border-radius: 0.375rem;
border: none;
background: transparent;
color: #64748b;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.btn-icon:hover {
background: #1e293b;
color: #cbd5e1;
}
/* Strategies Grid */
.strategies-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 1.5rem;
}
/* Strategy Card */
.strategy-card {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
overflow: hidden;
transition: all 0.3s ease;
}
.strategy-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.4);
border-color: #334155;
}
.strategy-card.active {
border-color: #6366f1;
box-shadow: 0 0 0 1px #6366f1;
}
.card-header {
padding: 1.5rem;
background: #1a1f3a;
border-bottom: 1px solid #1e293b;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.strategy-info {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.strategy-info h3 {
margin: 0;
font-size: 1.125rem;
font-weight: 700;
color: white;
}
.badge {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.badge.active {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
}
.badge.inactive {
background: rgba(100, 116, 139, 0.2);
color: #64748b;
}
.strategy-actions {
display: flex;
gap: 0.5rem;
}
.card-body {
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.strategy-description {
font-size: 0.875rem;
color: #94a3b8;
line-height: 1.6;
}
/* Strategy Stats */
.strategy-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
padding: 1rem;
background: #1a1f3a;
border-radius: 0.5rem;
}
.stat {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.stat-label {
font-size: 0.625rem;
color: #64748b;
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.05em;
}
.stat-value {
font-size: 1.125rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
}
.stat-value.profit {
color: #10b981;
}
.stat-value.loss {
color: #ef4444;
}
/* Strategy Parameters */
.strategy-parameters h4,
.strategy-indicators h4 {
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
color: #cbd5e1;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.params-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
.param {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.param-label {
font-size: 0.75rem;
color: #64748b;
font-weight: 600;
}
.param-value {
font-size: 0.75rem;
color: #10b981;
background: rgba(16, 185, 129, 0.1);
padding: 0.375rem 0.5rem;
border-radius: 0.25rem;
font-family: 'Courier New', monospace;
}
/* Indicators */
.indicators-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.indicator-tag {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
background: #1a1f3a;
border: 1px solid #334155;
border-radius: 0.375rem;
font-size: 0.75rem;
color: #cbd5e1;
font-weight: 600;
}
/* Card Footer */
.card-footer {
padding: 1rem 1.5rem;
background: #0a0e27;
border-top: 1px solid #1e293b;
display: flex;
gap: 0.75rem;
}
/* Template Card */
.strategy-card.template {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%);
border: 2px dashed #6366f1;
display: flex;
align-items: center;
justify-content: center;
min-height: 300px;
}
.template-content {
text-align: center;
padding: 2rem;
}
.template-icon {
font-size: 3rem;
color: #6366f1;
margin-bottom: 1rem;
}
.template-content h3 {
margin: 0 0 0.5rem 0;
font-size: 1.25rem;
font-weight: 700;
color: white;
}
.template-content p {
margin: 0 0 1.5rem 0;
color: #94a3b8;
font-size: 0.875rem;
}
/* Templates Section */
.templates-section {
margin-top: 2rem;
}
.templates-section h2 {
margin: 0 0 0.5rem 0;
font-size: 1.5rem;
font-weight: 700;
color: white;
}
.section-subtitle {
margin: 0 0 1.5rem 0;
color: #94a3b8;
font-size: 0.875rem;
}
.templates-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
}
.template-item {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
transition: all 0.3s ease;
}
.template-item:hover {
border-color: #6366f1;
transform: translateY(-2px);
}
.template-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.template-header .bi {
font-size: 1.5rem;
color: #6366f1;
}
.template-header h4 {
margin: 0;
font-size: 1rem;
font-weight: 700;
color: white;
}
.template-item p {
margin: 0 0 1rem 0;
font-size: 0.875rem;
color: #94a3b8;
line-height: 1.5;
}
/* Responsive */
@media (max-width: 768px) {
.strategies-grid {
grid-template-columns: 1fr;
}
.strategy-stats {
grid-template-columns: repeat(2, 1fr);
}
.params-grid {
grid-template-columns: 1fr;
}
.templates-grid {
grid-template-columns: 1fr;
}
.page-header {
flex-direction: column;
gap: 1rem;
}
}
+238
View File
@@ -0,0 +1,238 @@
@page "/trading"
@using TradingBot.Models
@using TradingBot.Services
@inject TradingBotService BotService
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Trading - TradingBot</PageTitle>
<div class="trading-page">
<div class="page-header">
<div>
<h1>Trading Automatico</h1>
<p class="subtitle">Applica strategie agli asset e monitora le operazioni in tempo reale</p>
</div>
<div class="header-controls">
<button class="btn-secondary">
<span class="bi bi-download"></span>
Esporta Report
</button>
<button class="btn-toggle @(BotService.Status.IsRunning ? "active" : "")" @onclick="ToggleBot">
<span class="bi @(BotService.Status.IsRunning ? "bi-pause-circle-fill" : "bi-play-circle-fill")"></span>
@(BotService.Status.IsRunning ? "Stop Trading" : "Avvia Trading")
</button>
</div>
</div>
<!-- Assets Grid -->
<div class="assets-section">
<div class="section-header">
<h2>Asset Monitorati</h2>
<div class="filters">
<select class="filter-select">
<option>Tutti gli Asset</option>
<option>Solo Attivi</option>
<option>Solo Inattivi</option>
</select>
<button class="btn-icon">
<span class="bi bi-funnel"></span>
</button>
</div>
</div>
<div class="assets-grid">
@foreach (var config in BotService.AssetConfigurations.Values.OrderBy(c => c.Symbol))
{
var stats = BotService.AssetStatistics.TryGetValue(config.Symbol, out var s) ? s : null;
var latestPrice = BotService.GetLatestPrice(config.Symbol);
<div class="asset-trading-card @(config.IsEnabled ? "enabled" : "disabled")">
<div class="asset-header">
<div class="asset-title">
<span class="asset-icon">@config.Symbol.Substring(0, 1)</span>
<div class="asset-name-group">
<span class="name">@config.Name</span>
<span class="symbol">@config.Symbol</span>
</div>
</div>
<label class="toggle-switch">
<input type="checkbox"
checked="@config.IsEnabled"
@onchange="(e) => ToggleAsset(config.Symbol, (bool)e.Value!)" />
<span class="toggle-slider"></span>
</label>
</div>
@if (latestPrice != null)
{
<div class="asset-price-info">
<div class="current-price">$@latestPrice.Price.ToString("N2")</div>
<div class="price-change @(latestPrice.Change24h >= 0 ? "positive" : "negative")">
<span class="bi @(latestPrice.Change24h >= 0 ? "bi-arrow-up" : "bi-arrow-down")"></span>
@Math.Abs(latestPrice.Change24h).ToString("F2")%
</div>
</div>
}
else
{
<div class="asset-price-info">
<div class="current-price loading">Loading...</div>
</div>
}
<div class="asset-strategy">
<div class="strategy-label">Strategia Applicata</div>
<div class="strategy-name">
<span class="bi bi-diagram-3"></span>
@config.StrategyName
</div>
</div>
<div class="asset-metrics">
<div class="metric">
<span class="metric-label">Holdings</span>
<span class="metric-value">@config.CurrentHoldings.ToString("F4")</span>
</div>
<div class="metric">
<span class="metric-label">Valore</span>
<span class="metric-value">$@((config.CurrentBalance + config.CurrentHoldings * (latestPrice?.Price ?? 0)).ToString("N2"))</span>
</div>
<div class="metric">
<span class="metric-label">Profitto</span>
<span class="metric-value @(config.TotalProfit >= 0 ? "profit" : "loss")">
$@config.TotalProfit.ToString("N2")
</span>
</div>
<div class="metric">
<span class="metric-label">Trades</span>
<span class="metric-value">@(stats?.TotalTrades ?? 0)</span>
</div>
</div>
<div class="asset-actions">
<button class="btn-secondary btn-sm" @onclick="() => OpenAssetConfig(config.Symbol)">
<span class="bi bi-gear"></span>
Configura
</button>
<button class="btn-secondary btn-sm" @onclick="() => ViewChart(config.Symbol)">
<span class="bi bi-graph-up"></span>
Grafico
</button>
</div>
</div>
}
</div>
</div>
<!-- Recent Trades -->
<div class="trades-section">
<div class="section-header">
<h2>Operazioni Recenti</h2>
<button class="btn-secondary btn-sm">
<span class="bi bi-clock-history"></span>
Vedi Tutto
</button>
</div>
@if (BotService.Trades.Count == 0)
{
<div class="empty-state">
<span class="bi bi-inbox"></span>
<p>Nessuna operazione ancora</p>
<p class="hint">Avvia il trading per iniziare a eseguire operazioni</p>
</div>
}
else
{
<div class="trades-table">
<div class="table-header">
<div>Asset</div>
<div>Tipo</div>
<div>Quantità</div>
<div>Prezzo</div>
<div>Valore</div>
<div>Strategia</div>
<div>Data/Ora</div>
</div>
@foreach (var trade in BotService.Trades.Take(20))
{
<div class="table-row @(trade.IsBot ? "bot-trade" : "")">
<div class="cell-asset">
<span class="asset-badge">@trade.Symbol</span>
</div>
<div class="cell-type @(trade.Type == TradeType.Buy ? "buy" : "sell")">
<span class="bi @(trade.Type == TradeType.Buy ? "bi-arrow-down-circle-fill" : "bi-arrow-up-circle-fill")"></span>
@(trade.Type == TradeType.Buy ? "BUY" : "SELL")
</div>
<div>@trade.Amount.ToString("F6")</div>
<div>$@trade.Price.ToString("N2")</div>
<div class="cell-value">$@((trade.Amount * trade.Price).ToString("N2"))</div>
<div class="cell-strategy">
@if (trade.IsBot)
{
<span class="strategy-tag">
<span class="bi bi-robot"></span>
@trade.Strategy
</span>
}
else
{
<span class="manual-tag">Manuale</span>
}
</div>
<div class="cell-time">@trade.Timestamp.ToLocalTime().ToString("dd/MM HH:mm:ss")</div>
</div>
}
</div>
}
</div>
</div>
@code {
protected override void OnInitialized()
{
BotService.OnStatusChanged += HandleUpdate;
BotService.OnTradeExecuted += HandleTradeExecuted;
BotService.OnPriceUpdated += HandlePriceUpdate;
if (!BotService.Status.IsRunning)
{
BotService.Start();
}
}
private void ToggleBot()
{
if (BotService.Status.IsRunning)
BotService.Stop();
else
BotService.Start();
}
private void ToggleAsset(string symbol, bool enabled)
{
BotService.ToggleAsset(symbol, enabled);
}
private void OpenAssetConfig(string symbol)
{
// TODO: Open asset configuration modal
}
private void ViewChart(string symbol)
{
// TODO: Navigate to market analysis with selected symbol
}
private void HandleUpdate() => InvokeAsync(StateHasChanged);
private void HandleTradeExecuted(Trade trade) => InvokeAsync(StateHasChanged);
private void HandlePriceUpdate(string symbol, MarketPrice price) => InvokeAsync(StateHasChanged);
public void Dispose()
{
BotService.OnStatusChanged -= HandleUpdate;
BotService.OnTradeExecuted -= HandleTradeExecuted;
BotService.OnPriceUpdated -= HandlePriceUpdate;
}
}
@@ -0,0 +1,478 @@
/* Trading Page */
.trading-page {
display: flex;
flex-direction: column;
gap: 2rem;
}
/* Header */
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.page-header h1 {
margin: 0;
font-size: 2rem;
font-weight: 700;
color: white;
}
.subtitle {
margin: 0.5rem 0 0 0;
color: #94a3b8;
font-size: 0.875rem;
}
.header-controls {
display: flex;
gap: 0.75rem;
}
.btn-toggle {
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;
border: 1px solid #334155;
background: #1e293b;
color: #cbd5e1;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-toggle:hover {
background: #334155;
}
.btn-toggle.active {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
border-color: #6366f1;
color: white;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
}
/* Section Header */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.section-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
color: white;
}
.filters {
display: flex;
gap: 0.5rem;
}
.filter-select {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
border: 1px solid #334155;
background: #1e293b;
color: white;
font-size: 0.875rem;
}
/* Assets Grid */
.assets-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.asset-trading-card {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
transition: all 0.3s ease;
}
.asset-trading-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.4);
border-color: #334155;
}
.asset-trading-card.enabled {
border-color: rgba(99, 102, 241, 0.3);
}
.asset-trading-card.disabled {
opacity: 0.6;
}
.asset-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.asset-title {
display: flex;
align-items: center;
gap: 0.75rem;
}
.asset-icon {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.5rem;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
font-weight: 700;
color: white;
}
.asset-name-group {
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.asset-name-group .name {
font-size: 1rem;
font-weight: 700;
color: white;
}
.asset-name-group .symbol {
font-size: 0.75rem;
color: #64748b;
font-family: 'Courier New', monospace;
}
/* Toggle Switch */
.toggle-switch {
position: relative;
display: inline-block;
width: 3rem;
height: 1.5rem;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #334155;
transition: 0.3s;
border-radius: 1.5rem;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 1.125rem;
width: 1.125rem;
left: 0.1875rem;
bottom: 0.1875rem;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
.toggle-switch input:checked + .toggle-slider {
background-color: #6366f1;
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(1.5rem);
}
/* Price Info */
.asset-price-info {
display: flex;
align-items: baseline;
gap: 0.75rem;
margin-bottom: 1rem;
}
.current-price {
font-size: 1.875rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
}
.current-price.loading {
font-size: 1rem;
color: #64748b;
font-family: inherit;
}
.price-change {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.875rem;
font-weight: 600;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
}
.price-change.positive {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.price-change.negative {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
/* Strategy */
.asset-strategy {
margin-bottom: 1rem;
padding: 0.75rem;
background: #1a1f3a;
border-radius: 0.5rem;
}
.strategy-label {
font-size: 0.625rem;
color: #64748b;
text-transform: uppercase;
font-weight: 600;
margin-bottom: 0.375rem;
}
.strategy-name {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: #cbd5e1;
font-weight: 600;
}
/* Metrics */
.asset-metrics {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
margin-bottom: 1rem;
}
.metric {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.metric-label {
font-size: 0.625rem;
color: #64748b;
text-transform: uppercase;
font-weight: 600;
}
.metric-value {
font-size: 0.875rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
}
.metric-value.profit {
color: #10b981;
}
.metric-value.loss {
color: #ef4444;
}
/* Actions */
.asset-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
}
.btn-sm {
padding: 0.5rem 1rem;
font-size: 0.75rem;
}
/* Trades Table */
.trades-table {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
overflow: hidden;
}
.table-header {
display: grid;
grid-template-columns: 1fr 1fr 1.5fr 1.5fr 1.5fr 2fr 1.5fr;
gap: 1rem;
padding: 1rem 1.5rem;
background: #1a1f3a;
border-bottom: 1px solid #1e293b;
font-size: 0.75rem;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
}
.table-row {
display: grid;
grid-template-columns: 1fr 1fr 1.5fr 1.5fr 1.5fr 2fr 1.5fr;
gap: 1rem;
padding: 1rem 1.5rem;
border-bottom: 1px solid #1e293b;
align-items: center;
font-size: 0.875rem;
color: #cbd5e1;
transition: background 0.2s ease;
}
.table-row:hover {
background: #1a1f3a;
}
.table-row.bot-trade {
background: rgba(99, 102, 241, 0.05);
}
.table-row:last-child {
border-bottom: none;
}
.asset-badge {
display: inline-flex;
padding: 0.25rem 0.5rem;
background: #1a1f3a;
border-radius: 0.375rem;
font-family: 'Courier New', monospace;
font-weight: 700;
font-size: 0.75rem;
}
.cell-type {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 700;
}
.cell-type.buy {
color: #10b981;
}
.cell-type.sell {
color: #ef4444;
}
.cell-value {
font-family: 'Courier New', monospace;
font-weight: 600;
}
.strategy-tag {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.5rem;
background: rgba(99, 102, 241, 0.2);
color: #6366f1;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 600;
}
.manual-tag {
display: inline-flex;
padding: 0.25rem 0.5rem;
background: rgba(100, 116, 139, 0.2);
color: #64748b;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 600;
}
.cell-time {
color: #64748b;
font-size: 0.75rem;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 4rem 2rem;
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
}
.empty-state .bi {
font-size: 3rem;
color: #334155;
margin-bottom: 1rem;
}
.empty-state p {
margin: 0.5rem 0;
color: #94a3b8;
}
.empty-state .hint {
font-size: 0.875rem;
color: #64748b;
}
/* Responsive */
@media (max-width: 1024px) {
.assets-grid {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
.table-header, .table-row {
grid-template-columns: 1fr;
gap: 0.5rem;
}
.table-header {
display: none;
}
}
@media (max-width: 768px) {
.page-header {
flex-direction: column;
gap: 1rem;
}
.header-controls {
width: 100%;
flex-direction: column;
}
.assets-grid {
grid-template-columns: 1fr;
}
}
@@ -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;
}
}
+6
View File
@@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
@@ -0,0 +1,128 @@
@using TradingBot.Models
<div class="advanced-chart">
@if (PriceData == null || PriceData.Count < 2)
{
<div class="chart-loading">
<div class="loading-spinner"></div>
<span>In attesa di dati sufficienti...</span>
</div>
}
else
{
<svg viewBox="0 0 @Width @Height" class="chart-svg" preserveAspectRatio="none">
<defs>
<linearGradient id="gradient-@ColorId" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="@Color" stop-opacity="0.4" />
<stop offset="100%" stop-color="@Color" stop-opacity="0.05" />
</linearGradient>
</defs>
<!-- Grid Lines -->
@for (int i = 0; i <= 4; i++)
{
var y = (Height * i / 4.0);
<line x1="0" y1="@y" x2="@Width" y2="@y"
stroke="rgba(51, 65, 85, 0.3)" stroke-width="1" stroke-dasharray="4 4" />
}
<!-- Area Fill -->
@if (!string.IsNullOrEmpty(GetAreaPath()))
{
<path d="@GetAreaPath()" fill="url(#gradient-@ColorId)" />
}
<!-- Line Chart -->
@if (!string.IsNullOrEmpty(GetPolylinePoints()))
{
<polyline fill="none" stroke="@Color" stroke-width="3"
points="@GetPolylinePoints()"
stroke-linecap="round" stroke-linejoin="round" />
}
</svg>
@if (Indicators != null)
{
<div class="indicators-overlay">
<div class="indicator-badge">
<span class="indicator-label">RSI:</span>
<span class="indicator-value @GetRSIClass()">@Indicators.RSI.ToString("F1")</span>
</div>
<div class="indicator-badge">
<span class="indicator-label">MACD:</span>
<span class="indicator-value">@Indicators.MACD.ToString("F2")</span>
</div>
</div>
}
}
</div>
@code {
[Parameter] public List<decimal>? PriceData { get; set; }
[Parameter] public string Color { get; set; } = "#6366f1";
[Parameter] public TechnicalIndicators? Indicators { get; set; }
private int Width = 800;
private int Height = 300;
private string ColorId => Guid.NewGuid().ToString("N").Substring(0, 8);
private string GetPolylinePoints()
{
if (PriceData == null || PriceData.Count < 2) return string.Empty;
try
{
var max = PriceData.Max();
var min = PriceData.Min();
var range = max - min;
if (range == 0) range = max * 0.01m; // 1% range if all values are same
var points = new List<string>();
var padding = Height * 0.1; // 10% padding
var chartHeight = Height - (padding * 2);
for (int i = 0; i < PriceData.Count; i++)
{
var x = (i / (double)(PriceData.Count - 1)) * Width;
var normalizedValue = (double)((PriceData[i] - min) / range);
var y = padding + (chartHeight * (1 - normalizedValue));
points.Add($"{x:F2},{y:F2}");
}
return string.Join(" ", points);
}
catch
{
return string.Empty;
}
}
private string GetAreaPath()
{
var polyline = GetPolylinePoints();
if (string.IsNullOrEmpty(polyline)) return string.Empty;
try
{
var points = polyline.Split(' ');
if (points.Length < 2) return string.Empty;
var firstPoint = points[0].Split(',');
var lastPoint = points[points.Length - 1].Split(',');
return $"M {firstPoint[0]},{Height} L {polyline} L {lastPoint[0]},{Height} Z";
}
catch
{
return string.Empty;
}
}
private string GetRSIClass()
{
if (Indicators == null) return "rsi-neutral";
if (Indicators.RSI > 70) return "rsi-overbought";
if (Indicators.RSI < 30) return "rsi-oversold";
return "rsi-neutral";
}
}
@@ -0,0 +1,82 @@
.advanced-chart {
position: relative;
width: 100%;
height: 100%;
min-height: 300px;
background: #0a0e27;
border-radius: 0.5rem;
overflow: hidden;
}
.chart-svg {
width: 100%;
height: 100%;
display: block;
}
.chart-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
gap: 1rem;
color: #64748b;
}
.loading-spinner {
width: 2rem;
height: 2rem;
border: 3px solid #1e293b;
border-top-color: #6366f1;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.indicators-overlay {
position: absolute;
top: 1rem;
left: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.indicator-badge {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: rgba(15, 23, 42, 0.9);
backdrop-filter: blur(8px);
border: 1px solid rgba(99, 102, 241, 0.2);
border-radius: 0.375rem;
font-size: 0.75rem;
}
.indicator-label {
color: #94a3b8;
font-weight: 600;
}
.indicator-value {
color: white;
font-weight: 700;
font-family: 'Courier New', monospace;
}
.indicator-value.rsi-overbought {
color: #ef4444;
}
.indicator-value.rsi-oversold {
color: #10b981;
}
.indicator-value.rsi-neutral {
color: #f59e0b;
}
@@ -0,0 +1,344 @@
@using TradingBot.Models
@using TradingBot.Services
@inject TradingBotService BotService
<div class="asset-settings-modal @(IsOpen ? "open" : "")">
<div class="modal-overlay" @onclick="Close"></div>
<div class="modal-content">
<div class="modal-header">
<h3>
<span class="bi bi-gear-fill"></span>
Impostazioni {{Config?.Symbol}}
</h3>
<button class="btn-close" @onclick="Close">
<span class="bi bi-x-lg"></span>
</button>
</div>
@if (Config != null)
{
<div class="modal-body">
<!-- Basic Settings -->
<div class="settings-section">
<h4 class="section-title">Impostazioni Base</h4>
<div class="form-group">
<label>Stato</label>
<div class="toggle-wrapper">
<label class="toggle-switch">
<input type="checkbox"
@bind="Config.IsEnabled" />
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">
{{Config.IsEnabled ? "Attivo" : "Inattivo"}}
</span>
</div>
</div>
<div class="form-group">
<label>Bilancio Iniziale ($)</label>
<input type="number"
class="form-input"
@bind="Config.InitialBalance"
step="100"
min="0" />
</div>
<div class="form-row">
<div class="form-group">
<label>Bilancio Corrente ($)</label>
<input type="number"
class="form-input"
value="@Config.CurrentBalance"
readonly />
</div>
<div class="form-group">
<label>Holdings</label>
<input type="number"
class="form-input"
value="@Config.CurrentHoldings"
readonly />
</div>
</div>
</div>
<!-- Risk Management -->
<div class="settings-section">
<h4 class="section-title">Gestione del Rischio</h4>
<div class="form-row">
<div class="form-group">
<label>Stop Loss (%)</label>
<input type="number"
class="form-input"
@bind="Config.StopLossPercentage"
step="0.5"
min="0"
max="100" />
</div>
<div class="form-group">
<label>Take Profit (%)</label>
<input type="number"
class="form-input"
@bind="Config.TakeProfitPercentage"
step="0.5"
min="0"
max="100" />
</div>
</div>
<div class="form-group">
<label>Dimensione Massima Posizione ($)</label>
<input type="number"
class="form-input"
@bind="Config.MaxPositionSize"
step="10"
min="0" />
<small class="form-hint">
Massimo valore totale della posizione in dollari
</small>
</div>
</div>
<!-- Trading Constraints -->
<div class="settings-section">
<h4 class="section-title">Limiti di Trading</h4>
<div class="form-row">
<div class="form-group">
<label>Min Trade ($)</label>
<input type="number"
class="form-input"
@bind="Config.MinTradeAmount"
step="1"
min="1" />
</div>
<div class="form-group">
<label>Max Trade ($)</label>
<input type="number"
class="form-input"
@bind="Config.MaxTradeAmount"
step="10"
min="1" />
</div>
</div>
<div class="form-group">
<label>Max Operazioni Giornaliere</label>
<input type="number"
class="form-input"
@bind="Config.MaxDailyTrades"
step="1"
min="1"
max="100" />
<small class="form-hint">
Operazioni oggi: {{Config.DailyTradeCount}} / {{Config.MaxDailyTrades}}
</small>
</div>
</div>
<!-- Strategy Parameters -->
<div class="settings-section">
<h4 class="section-title">Parametri Strategia</h4>
<div class="form-group">
<label>Strategia</label>
<input type="text"
class="form-input"
value="@Config.StrategyName"
readonly />
</div>
@if (Config.StrategyParameters.ContainsKey("ShortPeriod"))
{
<div class="form-row">
<div class="form-group">
<label>Periodo Corto</label>
<input type="number"
class="form-input"
value="@Config.StrategyParameters["ShortPeriod"]"
@onchange="@((e) => UpdateStrategyParameter("ShortPeriod", e.Value))"
step="1"
min="5"
max="50" />
</div>
<div class="form-group">
<label>Periodo Lungo</label>
<input type="number"
class="form-input"
value="@Config.StrategyParameters["LongPeriod"]"
@onchange="@((e) => UpdateStrategyParameter("LongPeriod", e.Value))"
step="1"
min="10"
max="100" />
</div>
</div>
}
@if (Config.StrategyParameters.ContainsKey("SignalThreshold"))
{
<div class="form-group">
<label>Soglia Segnale</label>
<input type="number"
class="form-input"
value="@Config.StrategyParameters["SignalThreshold"]"
@onchange="@((e) => UpdateStrategyParameter("SignalThreshold", e.Value))"
step="0.1"
min="0"
max="5" />
</div>
}
</div>
<!-- Current Stats -->
<div class="settings-section stats-section">
<h4 class="section-title">Statistiche Correnti</h4>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Profitto Totale</span>
<span class="stat-value @(Config.TotalProfit >= 0 ? "profit" : "loss")">
${{Config.TotalProfit:N2}}
</span>
</div>
<div class="stat-item">
<span class="stat-label">% Profitto</span>
<span class="stat-value @(Config.ProfitPercentage >= 0 ? "profit" : "loss")">
{{Config.ProfitPercentage:F2}}%
</span>
</div>
<div class="stat-item">
<span class="stat-label">Prezzo Medio Entrata</span>
<span class="stat-value">
${{Config.AverageEntryPrice:N2}}
</span>
</div>
<div class="stat-item">
<span class="stat-label">Ultimo Trade</span>
<span class="stat-value">
{{(Config.LastTradeTime?.ToLocalTime().ToString("HH:mm:ss") ?? "Mai")}}
</span>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" @onclick="ResetToDefaults">
<span class="bi bi-arrow-counterclockwise"></span>
Reset
</button>
<button class="btn-primary" @onclick="SaveSettings">
<span class="bi bi-check-lg"></span>
Salva Modifiche
</button>
</div>
}
</div>
</div>
@code {
[Parameter]
public bool IsOpen { get; set; }
[Parameter]
public string? Symbol { get; set; }
[Parameter]
public EventCallback OnClose { get; set; }
private AssetConfiguration? Config { get; set; }
protected override void OnParametersSet()
{
if (IsOpen && !string.IsNullOrEmpty(Symbol))
{
LoadConfiguration();
}
}
private void LoadConfiguration()
{
if (BotService.AssetConfigurations.TryGetValue(Symbol!, out var config))
{
// Create a copy to avoid modifying the original until saved
Config = new AssetConfiguration
{
Symbol = config.Symbol,
Name = config.Name,
IsEnabled = config.IsEnabled,
InitialBalance = config.InitialBalance,
CurrentBalance = config.CurrentBalance,
CurrentHoldings = config.CurrentHoldings,
AverageEntryPrice = config.AverageEntryPrice,
StrategyName = config.StrategyName,
StrategyParameters = new Dictionary<string, object>(config.StrategyParameters),
MaxPositionSize = config.MaxPositionSize,
StopLossPercentage = config.StopLossPercentage,
TakeProfitPercentage = config.TakeProfitPercentage,
MinTradeAmount = config.MinTradeAmount,
MaxTradeAmount = config.MaxTradeAmount,
MaxDailyTrades = config.MaxDailyTrades,
LastTradeTime = config.LastTradeTime,
DailyTradeCount = config.DailyTradeCount,
DailyTradeCountReset = config.DailyTradeCountReset
};
}
}
private void UpdateStrategyParameter(string key, object? value)
{
if (Config != null && value != null)
{
if (value is string strValue)
{
if (decimal.TryParse(strValue, out var decValue))
{
Config.StrategyParameters[key] = decValue;
}
else if (int.TryParse(strValue, out var intValue))
{
Config.StrategyParameters[key] = intValue;
}
}
}
}
private void ResetToDefaults()
{
if (Config != null)
{
Config.InitialBalance = 1000m;
Config.StopLossPercentage = 5m;
Config.TakeProfitPercentage = 10m;
Config.MaxPositionSize = 100m;
Config.MinTradeAmount = 10m;
Config.MaxTradeAmount = 500m;
Config.MaxDailyTrades = 10;
Config.StrategyParameters = new Dictionary<string, object>
{
{ "ShortPeriod", 10 },
{ "LongPeriod", 30 },
{ "SignalThreshold", 0.5m }
};
}
}
private async Task SaveSettings()
{
if (Config != null && !string.IsNullOrEmpty(Symbol))
{
BotService.UpdateAssetConfiguration(Symbol, Config);
await Close();
}
}
private async Task Close()
{
IsOpen = false;
await OnClose.InvokeAsync();
}
}
@@ -0,0 +1,362 @@
/* Asset Settings Modal */
.asset-settings-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: none;
align-items: center;
justify-content: center;
}
.asset-settings-modal.open {
display: flex;
}
.modal-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(4px);
}
.modal-content {
position: relative;
width: 90%;
max-width: 700px;
max-height: 90vh;
background: #0f172a;
border: 1px solid #1e293b;
border-radius: 0.75rem;
display: flex;
flex-direction: column;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
animation: modal-in 0.3s ease;
}
@keyframes modal-in {
from {
opacity: 0;
transform: scale(0.9) translateY(20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
/* Modal Header */
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
background: #1e293b;
border-bottom: 1px solid #334155;
border-radius: 0.75rem 0.75rem 0 0;
}
.modal-header h3 {
margin: 0;
font-size: 1.25rem;
font-weight: 700;
color: white;
display: flex;
align-items: center;
gap: 0.75rem;
}
.btn-close {
width: 2rem;
height: 2rem;
border-radius: 0.375rem;
border: none;
background: transparent;
color: #94a3b8;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.btn-close:hover {
background: #334155;
color: white;
}
/* Modal Body */
.modal-body {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
}
/* Settings Section */
.settings-section {
margin-bottom: 2rem;
padding-bottom: 2rem;
border-bottom: 1px solid #1e293b;
}
.settings-section:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.section-title {
margin: 0 0 1.5rem 0;
font-size: 1rem;
font-weight: 600;
color: white;
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 0.875rem;
color: #94a3b8;
}
/* Form Elements */
.form-group {
margin-bottom: 1.5rem;
}
.form-group:last-child {
margin-bottom: 0;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
color: #cbd5e1;
}
.form-input {
width: 100%;
padding: 0.75rem 1rem;
background: #020617;
border: 1px solid #334155;
border-radius: 0.5rem;
color: white;
font-size: 0.875rem;
transition: all 0.2s ease;
}
.form-input:focus {
outline: none;
border-color: #6366f1;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.form-input:disabled,
.form-input:read-only {
opacity: 0.6;
cursor: not-allowed;
background: #1e293b;
}
.form-hint {
display: block;
margin-top: 0.375rem;
font-size: 0.75rem;
color: #64748b;
font-style: italic;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
/* Toggle Switch */
.toggle-wrapper {
display: flex;
align-items: center;
gap: 1rem;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 3rem;
height: 1.5rem;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #334155;
transition: 0.3s;
border-radius: 1.5rem;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 1.125rem;
width: 1.125rem;
left: 0.1875rem;
bottom: 0.1875rem;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
.toggle-switch input:checked + .toggle-slider {
background-color: #6366f1;
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(1.5rem);
}
.toggle-label {
font-size: 0.875rem;
font-weight: 600;
color: #cbd5e1;
}
/* Stats Section */
.stats-section {
background: #020617;
border: 1px solid #1e293b;
border-radius: 0.5rem;
padding: 1.5rem;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.stat-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.stat-label {
font-size: 0.75rem;
color: #64748b;
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.05em;
}
.stat-value {
font-size: 1.125rem;
font-weight: 700;
color: white;
font-family: monospace;
}
.stat-value.profit {
color: #10b981;
}
.stat-value.loss {
color: #ef4444;
}
/* Modal Footer */
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding: 1.5rem;
background: #020617;
border-top: 1px solid #1e293b;
border-radius: 0 0 0.75rem 0.75rem;
}
.btn-primary, .btn-secondary {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-primary {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px -2px rgba(99, 102, 241, 0.3);
}
.btn-secondary {
background: transparent;
color: #94a3b8;
border: 1px solid #334155;
}
.btn-secondary:hover {
background: #1e293b;
border-color: #475569;
color: white;
}
/* Scrollbar Styling */
.modal-body::-webkit-scrollbar {
width: 0.5rem;
}
.modal-body::-webkit-scrollbar-track {
background: #0f172a;
}
.modal-body::-webkit-scrollbar-thumb {
background: #334155;
border-radius: 0.25rem;
}
.modal-body::-webkit-scrollbar-thumb:hover {
background: #475569;
}
/* Responsive */
@media (max-width: 768px) {
.modal-content {
width: 95%;
max-height: 95vh;
}
.form-row {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: 1fr;
}
.modal-header, .modal-body, .modal-footer {
padding: 1rem;
}
}
@@ -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;
}
}
+13
View File
@@ -0,0 +1,13 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using TradingBot
@using TradingBot.Components
@using TradingBot.Components.Layout
@using TradingBot.Models
@using TradingBot.Services
+30
View File
@@ -0,0 +1,30 @@
# 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.
# 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
EXPOSE 8080
EXPOSE 8081
# 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 .
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"]
+12
View File
@@ -0,0 +1,12 @@
namespace TradingBot.Models;
public class AppSettings
{
public bool SimulationMode { get; set; } = true;
public bool DesktopNotifications { get; set; } = false;
public bool AutoStartBot { get; set; } = true;
public bool ConfirmManualTrades { get; set; } = false;
public int UpdateIntervalSeconds { get; set; } = 3;
public string LogLevel { get; set; } = "Info";
public bool SidebarCollapsed { get; set; } = false;
}
+45
View File
@@ -0,0 +1,45 @@
namespace TradingBot.Models;
public class AssetConfiguration
{
public string Symbol { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public bool IsEnabled { get; set; }
public decimal InitialBalance { get; set; } = 1000m;
public decimal CurrentBalance { get; set; } = 1000m;
public decimal CurrentHoldings { get; set; }
public decimal AverageEntryPrice { get; set; }
// Strategy Settings
public string StrategyName { get; set; } = "Simple Moving Average";
public Dictionary<string, object> StrategyParameters { get; set; } = new();
// Risk Management
public decimal MaxPositionSize { get; set; } = 100m;
public decimal StopLossPercentage { get; set; } = 5m;
public decimal TakeProfitPercentage { get; set; } = 10m;
// Trading Constraints
public decimal MinTradeAmount { get; set; } = 10m;
public decimal MaxTradeAmount { get; set; } = 500m;
public int MaxDailyTrades { get; set; } = 10;
// Current State
public DateTime? LastTradeTime { get; set; }
public int DailyTradeCount { get; set; }
public DateTime DailyTradeCountReset { get; set; } = DateTime.UtcNow.Date;
// Statistics Quick Access
public decimal TotalProfit => CurrentBalance + (CurrentHoldings * AverageEntryPrice) - InitialBalance;
public decimal ProfitPercentage => InitialBalance > 0 ? (TotalProfit / InitialBalance) * 100 : 0;
public AssetConfiguration()
{
StrategyParameters = new Dictionary<string, object>
{
{ "ShortPeriod", 10 },
{ "LongPeriod", 30 },
{ "SignalThreshold", 0.5m }
};
}
}
+94
View File
@@ -0,0 +1,94 @@
namespace TradingBot.Models;
public class AssetStatistics
{
public string Symbol { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
// Trading Performance
public int TotalTrades { get; set; }
public int WinningTrades { get; set; }
public int LosingTrades { get; set; }
public decimal WinRate => TotalTrades > 0 ? (decimal)WinningTrades / TotalTrades * 100 : 0;
// Financial Metrics
public decimal TotalProfit { get; set; }
public decimal TotalLoss { get; set; }
public decimal NetProfit => TotalProfit - TotalLoss;
public decimal ProfitPercentage { get; set; }
public decimal AverageProfit => WinningTrades > 0 ? TotalProfit / WinningTrades : 0;
public decimal AverageLoss => LosingTrades > 0 ? TotalLoss / LosingTrades : 0;
public decimal ProfitFactor => TotalLoss > 0 ? TotalProfit / TotalLoss : TotalProfit > 0 ? decimal.MaxValue : 0;
// Position Information
public decimal CurrentPosition { get; set; }
public decimal AverageEntryPrice { get; set; }
public decimal CurrentPrice { get; set; }
public decimal UnrealizedPnL => CurrentPosition > 0 && AverageEntryPrice > 0
? (CurrentPrice - AverageEntryPrice) * CurrentPosition
: 0;
public decimal UnrealizedPnLPercentage => AverageEntryPrice > 0
? (CurrentPrice - AverageEntryPrice) / AverageEntryPrice * 100
: 0;
// Risk Metrics
public decimal MaxDrawdown { get; set; }
public decimal CurrentDrawdown { get; set; }
public decimal LargestWin { get; set; }
public decimal LargestLoss { get; set; }
public decimal SharpeRatio { get; set; }
// Time-based Metrics
public DateTime? FirstTradeTime { get; set; }
public DateTime? LastTradeTime { get; set; }
public TimeSpan TradingDuration => FirstTradeTime.HasValue && LastTradeTime.HasValue
? LastTradeTime.Value - FirstTradeTime.Value
: TimeSpan.Zero;
// Daily Statistics
public int TradesToday { get; set; }
public decimal ProfitToday { get; set; }
public decimal ProfitTodayPercentage { get; set; }
// Trade Details
public List<Trade> RecentTrades { get; set; } = new();
public List<decimal> EquityCurve { get; set; } = new();
// Strategy Performance
public Dictionary<string, int> TradesByStrategy { get; set; } = new();
public Dictionary<string, decimal> ProfitByStrategy { get; set; } = new();
// Additional Metrics
public decimal AverageTradeSize { get; set; }
public decimal AverageHoldingTime { get; set; } // in hours
public int ConsecutiveWins { get; set; }
public int ConsecutiveLosses { get; set; }
public int MaxConsecutiveWins { get; set; }
public int MaxConsecutiveLosses { get; set; }
}
public class PortfolioStatistics
{
public decimal TotalBalance { get; set; }
public decimal InitialBalance { get; set; }
public decimal TotalProfit => TotalBalance - InitialBalance;
public decimal TotalProfitPercentage => InitialBalance > 0 ? (TotalProfit / InitialBalance) * 100 : 0;
public int TotalAssets { get; set; }
public int ActiveAssets { get; set; }
public int TotalTrades { get; set; }
public decimal WinRate { get; set; }
public decimal BestPerformingAssetProfit { get; set; }
public string BestPerformingAssetSymbol { get; set; } = string.Empty;
public decimal WorstPerformingAssetProfit { get; set; }
public string WorstPerformingAssetSymbol { get; set; } = string.Empty;
public List<AssetStatistics> AssetStatistics { get; set; } = new();
public Dictionary<string, decimal> DailyProfits { get; set; } = new();
public Dictionary<string, int> DailyTrades { get; set; } = new();
public DateTime? StartDate { get; set; }
public DateTime LastUpdateTime { get; set; } = DateTime.UtcNow;
}
+10
View File
@@ -0,0 +1,10 @@
namespace TradingBot.Models;
public class BotStatus
{
public bool IsRunning { get; set; }
public DateTime? StartedAt { get; set; }
public decimal TotalProfit { get; set; }
public int TradesExecuted { get; set; }
public string CurrentStrategy { get; set; } = "Simple Moving Average";
}
+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
}
+10
View File
@@ -0,0 +1,10 @@
namespace TradingBot.Models;
public class MarketPrice
{
public string Symbol { get; set; } = string.Empty;
public decimal Price { get; set; }
public decimal Change24h { get; set; }
public decimal Volume24h { get; set; }
public DateTime Timestamp { get; set; }
}
+7
View File
@@ -0,0 +1,7 @@
namespace TradingBot.Models;
public class Notification
{
public string Message { get; set; } = string.Empty;
public string Type { get; set; } = "info";
}
+11
View File
@@ -0,0 +1,11 @@
namespace TradingBot.Models;
public class TechnicalIndicators
{
public decimal RSI { get; set; }
public decimal MACD { get; set; }
public decimal Signal { get; set; }
public decimal Histogram { get; set; }
public decimal EMA12 { get; set; }
public decimal EMA26 { get; set; }
}
+19
View File
@@ -0,0 +1,19 @@
namespace TradingBot.Models;
public class Trade
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Symbol { get; set; } = string.Empty;
public TradeType Type { get; set; }
public decimal Price { get; set; }
public decimal Amount { get; set; }
public DateTime Timestamp { get; set; }
public string Strategy { get; set; } = string.Empty;
public bool IsBot { get; set; }
}
public enum TradeType
{
Buy,
Sell
}
+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
}
+18
View File
@@ -0,0 +1,18 @@
namespace TradingBot.Models;
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; }
}
public enum SignalType
{
Buy,
Sell,
Hold
}
+60
View File
@@ -0,0 +1,60 @@
using TradingBot.Components;
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();
// 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();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
app.UseHsts();
}
// HTTPS redirect solo in Development
if (app.Environment.IsDevelopment())
{
app.UseHttpsRedirection();
}
app.UseStaticFiles();
app.UseAntiforgery();
// Health check endpoint
app.MapHealthChecks("/health");
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();
+34
View File
@@ -0,0 +1,34 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"TradingBot": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5243",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"TradingBot (HTTPS)": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7241;http://localhost:5243",
"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
}
}
}
+163
View File
@@ -0,0 +1,163 @@
# ?? TradingBot
**Automated Crypto Trading Bot** con interfaccia Blazor Server per trading algoritmico simulato.
[![.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)
---
## ? 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!**
@@ -0,0 +1,101 @@
using System.Text.Json;
using TradingBot.Models;
namespace TradingBot.Services;
public class CoinGeckoMarketDataService : IMarketDataService
{
private readonly HttpClient _httpClient;
private readonly Dictionary<string, string> _symbolToId = new()
{
{ "BTC", "bitcoin" },
{ "ETH", "ethereum" },
{ "BNB", "binancecoin" },
{ "XRP", "ripple" },
{ "ADA", "cardano" },
{ "SOL", "solana" },
{ "DOT", "polkadot" }
};
public CoinGeckoMarketDataService(HttpClient httpClient)
{
_httpClient = httpClient;
_httpClient.BaseAddress = new Uri("https://api.coingecko.com/api/v3/");
_httpClient.DefaultRequestHeaders.Add("User-Agent", "NovaTrader-Bot");
}
public async Task<List<MarketPrice>> GetMarketPricesAsync(List<string> symbols)
{
var prices = new List<MarketPrice>();
// Convert symbols to CoinGecko IDs
var ids = string.Join(",", symbols.Select(s => _symbolToId.GetValueOrDefault(s.ToUpper(), s.ToLower())));
try
{
// CoinGecko API: /simple/price endpoint
var response = await _httpClient.GetAsync(
$"simple/price?ids={ids}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true&include_last_updated_at=true");
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync();
var data = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json);
if (data != null)
{
foreach (var symbol in symbols)
{
var coinId = _symbolToId.GetValueOrDefault(symbol.ToUpper(), symbol.ToLower());
if (data.TryGetValue(coinId, out var coinData))
{
var price = new MarketPrice
{
Symbol = symbol.ToUpper(),
Price = coinData.GetProperty("usd").GetDecimal(),
Timestamp = DateTime.UtcNow
};
// Safely get optional properties
if (coinData.TryGetProperty("usd_24h_change", out var changeElement))
{
price.Change24h = changeElement.GetDecimal();
}
if (coinData.TryGetProperty("usd_24h_vol", out var volumeElement))
{
price.Volume24h = volumeElement.GetDecimal();
}
prices.Add(price);
}
}
}
}
else
{
Console.WriteLine($"CoinGecko API error: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}");
}
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Network error fetching market data: {ex.Message}");
}
catch (JsonException ex)
{
Console.WriteLine($"JSON parsing error: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Unexpected error fetching market data: {ex.Message}");
}
return prices;
}
public async Task<MarketPrice?> GetPriceAsync(string symbol)
{
var prices = await GetMarketPricesAsync(new List<string> { symbol });
return prices.FirstOrDefault();
}
}
@@ -0,0 +1,9 @@
using TradingBot.Models;
namespace TradingBot.Services;
public interface IMarketDataService
{
Task<List<MarketPrice>> GetMarketPricesAsync(List<string> symbols);
Task<MarketPrice?> GetPriceAsync(string symbol);
}
+9
View File
@@ -0,0 +1,9 @@
using TradingBot.Models;
namespace TradingBot.Services;
public interface ITradingStrategy
{
string Name { get; }
Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> historicalPrices);
}
+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();
}
}
+96
View File
@@ -0,0 +1,96 @@
using System.Text.Json;
using TradingBot.Models;
namespace TradingBot.Services;
public class SettingsService
{
private const string SettingsFileName = "appsettings.json";
private AppSettings _settings;
private readonly string _settingsPath;
public event Action? OnSettingsChanged;
public SettingsService()
{
_settingsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"TradingBot",
SettingsFileName
);
_settings = LoadSettings();
}
public AppSettings GetSettings()
{
return _settings;
}
public void UpdateSettings(AppSettings settings)
{
_settings = settings;
SaveSettings();
OnSettingsChanged?.Invoke();
}
public void UpdateSetting<T>(string propertyName, T value)
{
var property = typeof(AppSettings).GetProperty(propertyName);
if (property != null && property.CanWrite)
{
property.SetValue(_settings, value);
SaveSettings();
OnSettingsChanged?.Invoke();
}
}
private AppSettings LoadSettings()
{
try
{
if (File.Exists(_settingsPath))
{
var json = File.ReadAllText(_settingsPath);
var settings = JsonSerializer.Deserialize<AppSettings>(json);
return settings ?? new AppSettings();
}
}
catch (Exception ex)
{
Console.WriteLine($"Error loading settings: {ex.Message}");
}
return new AppSettings();
}
private void SaveSettings()
{
try
{
var directory = Path.GetDirectoryName(_settingsPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var json = JsonSerializer.Serialize(_settings, new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(_settingsPath, json);
}
catch (Exception ex)
{
Console.WriteLine($"Error saving settings: {ex.Message}");
}
}
public void ResetToDefaults()
{
_settings = new AppSettings();
SaveSettings();
OnSettingsChanged?.Invoke();
}
}
@@ -0,0 +1,87 @@
using TradingBot.Models;
namespace TradingBot.Services;
public class SimpleMovingAverageStrategy : ITradingStrategy
{
private readonly int _shortPeriod = 5;
private readonly int _longPeriod = 10;
public string Name => "Simple Moving Average (SMA)";
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> historicalPrices)
{
// 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,
Reason = "Dati insufficienti per l'analisi",
Timestamp = DateTime.UtcNow
});
}
// 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);
var currentPrice = recentPrices.First().Price;
// Strategia: Compra quando la SMA breve incrocia sopra la SMA lunga
// Vendi quando la SMA breve incrocia sotto la SMA lunga
if (shortSMA > longSMA * 1.02m) // 2% sopra
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Buy,
Price = currentPrice,
Reason = $"SMA breve ({shortSMA:F2}) > SMA lunga ({longSMA:F2}) - Trend rialzista",
Timestamp = DateTime.UtcNow
});
}
else if (shortSMA < longSMA * 0.98m) // 2% sotto
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Sell,
Price = currentPrice,
Reason = $"SMA breve ({shortSMA:F2}) < SMA lunga ({longSMA:F2}) - Trend ribassista",
Timestamp = DateTime.UtcNow
});
}
else
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Price = currentPrice,
Reason = $"SMA breve ({shortSMA:F2}) ? SMA lunga ({longSMA:F2}) - Nessun segnale chiaro",
Timestamp = DateTime.UtcNow
});
}
}
}
@@ -0,0 +1,209 @@
using TradingBot.Models;
namespace TradingBot.Services;
public class SimulatedMarketDataService : IMarketDataService
{
private readonly Dictionary<string, SimulatedAsset> _assets = new();
private readonly Random _random = new();
private readonly Timer _updateTimer;
private readonly object _lock = new();
public event Action? OnPriceUpdated;
public SimulatedMarketDataService()
{
InitializeAssets();
_updateTimer = new Timer(UpdatePrices, null, TimeSpan.Zero, TimeSpan.FromSeconds(2));
}
private void InitializeAssets()
{
var assets = new[]
{
new { Symbol = "BTC", Name = "Bitcoin", BasePrice = 45000m, Volatility = 0.02m, TrendBias = 0.0002m },
new { Symbol = "ETH", Name = "Ethereum", BasePrice = 2500m, Volatility = 0.025m, TrendBias = 0.0003m },
new { Symbol = "BNB", Name = "Binance Coin", BasePrice = 350m, Volatility = 0.03m, TrendBias = 0.0001m },
new { Symbol = "SOL", Name = "Solana", BasePrice = 100m, Volatility = 0.035m, TrendBias = 0.0004m },
new { Symbol = "ADA", Name = "Cardano", BasePrice = 0.45m, Volatility = 0.028m, TrendBias = 0.0002m },
new { Symbol = "XRP", Name = "Ripple", BasePrice = 0.65m, Volatility = 0.032m, TrendBias = 0.0001m },
new { Symbol = "DOT", Name = "Polkadot", BasePrice = 6.5m, Volatility = 0.03m, TrendBias = 0.0003m },
new { Symbol = "AVAX", Name = "Avalanche", BasePrice = 35m, Volatility = 0.038m, TrendBias = 0.0005m },
new { Symbol = "MATIC", Name = "Polygon", BasePrice = 0.85m, Volatility = 0.033m, TrendBias = 0.0002m },
new { Symbol = "LINK", Name = "Chainlink", BasePrice = 15m, Volatility = 0.029m, TrendBias = 0.0003m },
new { Symbol = "UNI", Name = "Uniswap", BasePrice = 6.5m, Volatility = 0.031m, TrendBias = 0.0001m },
new { Symbol = "ATOM", Name = "Cosmos", BasePrice = 10m, Volatility = 0.03m, TrendBias = 0.0004m },
new { Symbol = "LTC", Name = "Litecoin", BasePrice = 75m, Volatility = 0.025m, TrendBias = 0.0001m },
new { Symbol = "ALGO", Name = "Algorand", BasePrice = 0.25m, Volatility = 0.032m, TrendBias = 0.0003m },
new { Symbol = "VET", Name = "VeChain", BasePrice = 0.03m, Volatility = 0.035m, TrendBias = 0.0002m }
};
foreach (var asset in assets)
{
_assets[asset.Symbol] = new SimulatedAsset
{
Symbol = asset.Symbol,
Name = asset.Name,
CurrentPrice = asset.BasePrice,
BasePrice = asset.BasePrice,
Volatility = asset.Volatility,
TrendBias = asset.TrendBias,
LastUpdate = DateTime.UtcNow
};
}
}
private void UpdatePrices(object? state)
{
lock (_lock)
{
var now = DateTime.UtcNow;
foreach (var asset in _assets.Values)
{
// Calculate time-based factors
var timeSinceStart = (now - asset.LastUpdate).TotalSeconds;
// Generate random walk with trend
var randomChange = (_random.NextDouble() - 0.5) * 2 * (double)asset.Volatility;
var trendComponent = (double)asset.TrendBias;
// Add market cycles (sine wave for realistic market behavior)
var cycleComponent = Math.Sin((double)asset.PriceUpdateCount / 100.0) * 0.001;
// Combine all factors
var totalChange = randomChange + trendComponent + cycleComponent;
// Update price
var newPrice = asset.CurrentPrice * (1 + (decimal)totalChange);
// Keep price within reasonable bounds (50% to 200% of base price)
newPrice = Math.Max(asset.BasePrice * 0.5m, Math.Min(asset.BasePrice * 2.0m, newPrice));
// Calculate change and volume
var priceChange = newPrice - asset.CurrentPrice;
var changePercentage = asset.CurrentPrice > 0 ? (priceChange / asset.CurrentPrice) * 100 : 0;
// Simulate volume based on volatility and price change
var baseVolume = asset.BasePrice * 1000000m;
var volumeVariation = (decimal)(_random.NextDouble() * 0.5 + 0.75); // 75% to 125%
var volumeFromVolatility = Math.Abs(changePercentage) * 100000m;
asset.CurrentPrice = newPrice;
asset.Change24h = changePercentage;
asset.Volume24h = (baseVolume + volumeFromVolatility) * volumeVariation;
asset.LastUpdate = now;
asset.PriceUpdateCount++;
// Add to history
asset.PriceHistory.Add(new MarketPrice
{
Symbol = asset.Symbol,
Price = newPrice,
Change24h = changePercentage,
Volume24h = asset.Volume24h,
Timestamp = now
});
// Keep history limited to last 500 points
if (asset.PriceHistory.Count > 500)
{
asset.PriceHistory.RemoveAt(0);
}
}
OnPriceUpdated?.Invoke();
}
}
public Task<List<MarketPrice>> GetMarketPricesAsync(List<string> symbols)
{
lock (_lock)
{
var prices = new List<MarketPrice>();
foreach (var symbol in symbols)
{
if (_assets.TryGetValue(symbol, out var asset))
{
prices.Add(new MarketPrice
{
Symbol = asset.Symbol,
Price = asset.CurrentPrice,
Change24h = asset.Change24h,
Volume24h = asset.Volume24h,
Timestamp = asset.LastUpdate
});
}
}
return Task.FromResult(prices);
}
}
public Task<MarketPrice?> GetPriceAsync(string symbol)
{
lock (_lock)
{
if (_assets.TryGetValue(symbol, out var asset))
{
return Task.FromResult<MarketPrice?>(new MarketPrice
{
Symbol = asset.Symbol,
Price = asset.CurrentPrice,
Change24h = asset.Change24h,
Volume24h = asset.Volume24h,
Timestamp = asset.LastUpdate
});
}
return Task.FromResult<MarketPrice?>(null);
}
}
public List<MarketPrice> GetPriceHistory(string symbol, int count = 100)
{
lock (_lock)
{
if (_assets.TryGetValue(symbol, out var asset))
{
return asset.PriceHistory
.Skip(Math.Max(0, asset.PriceHistory.Count - count))
.ToList();
}
return new List<MarketPrice>();
}
}
public List<string> GetAvailableSymbols()
{
lock (_lock)
{
return _assets.Keys.OrderBy(s => s).ToList();
}
}
public Dictionary<string, string> GetAssetNames()
{
lock (_lock)
{
return _assets.ToDictionary(a => a.Key, a => a.Value.Name);
}
}
private class SimulatedAsset
{
public string Symbol { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public decimal CurrentPrice { get; set; }
public decimal BasePrice { get; set; }
public decimal Change24h { get; set; }
public decimal Volume24h { get; set; }
public decimal Volatility { get; set; }
public decimal TrendBias { get; set; }
public DateTime LastUpdate { get; set; }
public int PriceUpdateCount { get; set; }
public List<MarketPrice> PriceHistory { get; set; } = new();
}
}
+91
View File
@@ -0,0 +1,91 @@
namespace TradingBot.Services;
public static class TechnicalAnalysis
{
public static decimal CalculateEMA(List<decimal> prices, int period)
{
if (prices.Count == 0) return 0;
decimal k = 2m / (period + 1);
decimal ema = prices[0];
for (int i = 1; i < prices.Count; i++)
{
ema = prices[i] * k + ema * (1 - k);
}
return ema;
}
public static List<decimal> CalculateEMAArray(List<decimal> prices, int period)
{
if (prices.Count == 0) return new List<decimal>();
decimal k = 2m / (period + 1);
var emaArray = new List<decimal> { prices[0] };
for (int i = 1; i < prices.Count; i++)
{
emaArray.Add(prices[i] * k + emaArray[i - 1] * (1 - k));
}
return emaArray;
}
public static decimal CalculateRSI(List<decimal> prices, int period = 14)
{
if (prices.Count < period + 1) return 50;
decimal gains = 0;
decimal losses = 0;
for (int i = prices.Count - period; i < prices.Count; i++)
{
decimal diff = prices[i] - prices[i - 1];
if (diff >= 0)
gains += diff;
else
losses -= diff;
}
decimal avgGain = gains / period;
decimal avgLoss = losses / period;
if (avgLoss == 0) return 100;
decimal rs = avgGain / avgLoss;
return 100 - (100 / (1 + rs));
}
public static (decimal macd, decimal signal, decimal histogram) CalculateMACD(List<decimal> prices)
{
if (prices.Count < 26) return (0, 0, 0);
var ema12Array = CalculateEMAArray(prices, 12);
var ema26Array = CalculateEMAArray(prices, 26);
var macdLine = ema12Array[^1] - ema26Array[^1];
var signalLine = macdLine * 0.9m; // Simplified signal
var histogram = macdLine - signalLine;
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);
}
}
+796
View File
@@ -0,0 +1,796 @@
using TradingBot.Models;
namespace TradingBot.Services;
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;
public event Action<Trade>? OnTradeExecuted;
public event Action<string, TechnicalIndicators>? OnIndicatorsUpdated;
public event Action<string, MarketPrice>? OnPriceUpdated;
public event Action? OnStatisticsUpdated;
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
if (_marketDataService is SimulatedMarketDataService simService)
{
simService.OnPriceUpdated += HandleSimulatedPriceUpdate;
}
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()
{
// Get available symbols from SimulatedMarketDataService
var availableSymbols = _marketDataService is SimulatedMarketDataService simService
? simService.GetAvailableSymbols()
: new List<string> { "BTC", "ETH", "SOL", "ADA", "MATIC" };
var assetNames = _marketDataService is SimulatedMarketDataService simService2
? simService2.GetAssetNames()
: new Dictionary<string, string>
{
{ "BTC", "Bitcoin" },
{ "ETH", "Ethereum" },
{ "SOL", "Solana" },
{ "ADA", "Cardano" },
{ "MATIC", "Polygon" }
};
foreach (var symbol in availableSymbols)
{
_assetConfigs[symbol] = new AssetConfiguration
{
Symbol = symbol,
Name = assetNames.TryGetValue(symbol, out var name) ? name : symbol,
IsEnabled = true,
InitialBalance = 1000m,
CurrentBalance = 1000m
};
_assetStats[symbol] = new AssetStatistics
{
Symbol = symbol,
Name = assetNames.TryGetValue(symbol, out var name2) ? name2 : symbol
};
}
}
public void UpdateAssetConfiguration(string symbol, AssetConfiguration config)
{
_assetConfigs[symbol] = config;
OnStatusChanged?.Invoke();
}
public void ToggleAsset(string symbol, bool enabled)
{
if (_assetConfigs.TryGetValue(symbol, out var config))
{
config.IsEnabled = enabled;
OnStatusChanged?.Invoke();
}
}
public void AddAsset(string symbol, string name)
{
if (!_assetConfigs.ContainsKey(symbol))
{
_assetConfigs[symbol] = new AssetConfiguration
{
Symbol = symbol,
Name = name,
IsEnabled = false,
InitialBalance = 1000m,
CurrentBalance = 1000m
};
_assetStats[symbol] = new AssetStatistics
{
Symbol = symbol,
Name = name
};
OnStatusChanged?.Invoke();
}
}
public void Start()
{
if (Status.IsRunning) return;
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)
{
if (config.DailyTradeCountReset.Date < DateTime.UtcNow.Date)
{
config.DailyTradeCount = 0;
config.DailyTradeCountReset = DateTime.UtcNow.Date;
}
}
// 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 async void Stop()
{
if (!Status.IsRunning) return;
Status.IsRunning = false;
_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)
{
_ = UpdateAsync();
}
}
private async Task UpdateAsync()
{
try
{
var enabledSymbols = _assetConfigs.Values
.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)
{
if (price != null)
{
await ProcessAssetUpdate(price);
}
}
UpdateGlobalStatistics();
}
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;
// Update price history
if (!_priceHistory.ContainsKey(price.Symbol))
{
_priceHistory[price.Symbol] = new List<MarketPrice>();
}
_priceHistory[price.Symbol].Add(price);
if (_priceHistory[price.Symbol].Count > 200)
{
_priceHistory[price.Symbol].RemoveAt(0);
}
// Update statistics current price
if (_assetStats.TryGetValue(price.Symbol, out var stats))
{
stats.CurrentPrice = price.Price;
}
OnPriceUpdated?.Invoke(price.Symbol, price);
// Calculate indicators if enough data
if (_priceHistory[price.Symbol].Count >= 26)
{
UpdateIndicators(price.Symbol);
// Generate trading signal
var signal = await _strategy.AnalyzeAsync(price.Symbol, _priceHistory[price.Symbol]);
if (signal != null)
{
OnSignalGenerated?.Invoke(signal);
// Execute trades based on strategy and configuration
await EvaluateAndExecuteTrade(price.Symbol, signal, price, config);
}
}
}
private async Task EvaluateAndExecuteTrade(string symbol, TradingSignal signal, MarketPrice price, AssetConfiguration config)
{
if (!_indicators.TryGetValue(symbol, out var indicators))
return;
// Check daily trade limit
if (config.DailyTradeCount >= config.MaxDailyTrades)
return;
// Check if enough time has passed since last trade (min 10 seconds)
if (config.LastTradeTime.HasValue &&
(DateTime.UtcNow - config.LastTradeTime.Value).TotalSeconds < 10)
return;
// Buy logic
if (signal.Type == SignalType.Buy &&
indicators.RSI < 40 &&
indicators.Histogram > 0 &&
config.CurrentBalance >= config.MinTradeAmount)
{
var tradeAmount = Math.Min(
Math.Min(config.CurrentBalance * 0.3m, config.MaxTradeAmount),
config.MaxPositionSize - (config.CurrentHoldings * price.Price)
);
if (tradeAmount >= config.MinTradeAmount)
{
await ExecuteBuyAsync(symbol, price.Price, tradeAmount, config);
}
}
// Sell logic
else if (signal.Type == SignalType.Sell &&
indicators.RSI > 60 &&
indicators.Histogram < 0 &&
config.CurrentHoldings > 0)
{
var profitPercentage = config.AverageEntryPrice > 0
? ((price.Price - config.AverageEntryPrice) / config.AverageEntryPrice) * 100
: 0;
// Sell if profit target reached or stop loss triggered
if (profitPercentage >= config.TakeProfitPercentage ||
profitPercentage <= -config.StopLossPercentage)
{
await ExecuteSellAsync(symbol, price.Price, config.CurrentHoldings, config);
}
}
}
private async Task ExecuteBuyAsync(string symbol, decimal price, decimal amountUSD, AssetConfiguration config)
{
var amount = amountUSD / price;
// Update config
var previousHoldings = config.CurrentHoldings;
config.CurrentHoldings += amount;
config.CurrentBalance -= amountUSD;
config.AverageEntryPrice = previousHoldings > 0
? ((config.AverageEntryPrice * previousHoldings) + (price * amount)) / config.CurrentHoldings
: price;
config.LastTradeTime = DateTime.UtcNow;
config.DailyTradeCount++;
var trade = new Trade
{
Symbol = symbol,
Type = TradeType.Buy,
Price = price,
Amount = amount,
Timestamp = DateTime.UtcNow,
Strategy = _strategy.Name,
IsBot = true
};
_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 async Task ExecuteSellAsync(string symbol, decimal price, decimal amount, AssetConfiguration config)
{
var amountUSD = amount * price;
var profit = (price - config.AverageEntryPrice) * amount;
// Update config
config.CurrentHoldings = 0;
config.CurrentBalance += amountUSD;
config.LastTradeTime = DateTime.UtcNow;
config.DailyTradeCount++;
var trade = new Trade
{
Symbol = symbol,
Type = TradeType.Sell,
Price = price,
Amount = amount,
Timestamp = DateTime.UtcNow,
Strategy = _strategy.Name,
IsBot = true
};
_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)
{
if (!_priceHistory.TryGetValue(symbol, out var history) || history == null || history.Count < 26)
return;
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);
var indicators = new TechnicalIndicators
{
RSI = rsi,
MACD = macd,
Signal = signal,
Histogram = histogram,
EMA12 = TechnicalAnalysis.CalculateEMA(prices, 12),
EMA26 = TechnicalAnalysis.CalculateEMA(prices, 26)
};
_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)
{
if (!_assetStats.TryGetValue(symbol, out var stats))
return;
stats.TotalTrades++;
stats.RecentTrades.Insert(0, trade);
if (stats.RecentTrades.Count > 50)
stats.RecentTrades.RemoveAt(stats.RecentTrades.Count - 1);
if (!stats.FirstTradeTime.HasValue)
stats.FirstTradeTime = trade.Timestamp;
stats.LastTradeTime = trade.Timestamp;
if (realizedProfit.HasValue)
{
if (realizedProfit.Value > 0)
{
stats.WinningTrades++;
stats.TotalProfit += realizedProfit.Value;
stats.ConsecutiveWins++;
stats.ConsecutiveLosses = 0;
stats.MaxConsecutiveWins = Math.Max(stats.MaxConsecutiveWins, stats.ConsecutiveWins);
if (realizedProfit.Value > stats.LargestWin)
stats.LargestWin = realizedProfit.Value;
}
else if (realizedProfit.Value < 0)
{
stats.LosingTrades++;
stats.TotalLoss += Math.Abs(realizedProfit.Value);
stats.ConsecutiveLosses++;
stats.ConsecutiveWins = 0;
stats.MaxConsecutiveLosses = Math.Max(stats.MaxConsecutiveLosses, stats.ConsecutiveLosses);
if (Math.Abs(realizedProfit.Value) > stats.LargestLoss)
stats.LargestLoss = Math.Abs(realizedProfit.Value);
}
}
if (_assetConfigs.TryGetValue(symbol, out var config))
{
stats.TotalProfit = config.TotalProfit;
stats.ProfitPercentage = config.ProfitPercentage;
stats.CurrentPosition = config.CurrentHoldings;
stats.AverageEntryPrice = config.AverageEntryPrice;
}
OnStatisticsUpdated?.Invoke();
}
private void UpdateGlobalStatistics()
{
decimal totalProfit = 0;
int totalTrades = 0;
foreach (var config in _assetConfigs.Values.Where(c => c.IsEnabled))
{
totalProfit += config.TotalProfit;
}
totalTrades = _trades.Count;
Status.TotalProfit = totalProfit;
Status.TradesExecuted = totalTrades;
}
public PortfolioStatistics GetPortfolioStatistics()
{
var portfolio = new PortfolioStatistics
{
TotalAssets = _assetConfigs.Count,
ActiveAssets = _assetConfigs.Values.Count(c => c.IsEnabled),
TotalTrades = _trades.Count,
AssetStatistics = _assetStats.Values.ToList(),
StartDate = Status.StartedAt
};
portfolio.TotalBalance = _assetConfigs.Values.Sum(c =>
c.CurrentBalance + (c.CurrentHoldings * (_assetStats.TryGetValue(c.Symbol, out var s) ? s.CurrentPrice : 0)));
portfolio.InitialBalance = _assetConfigs.Values.Sum(c => c.InitialBalance);
if (_assetStats.Values.Any())
{
var winningTrades = _assetStats.Values.Sum(s => s.WinningTrades);
var totalTrades = _assetStats.Values.Sum(s => s.TotalTrades);
portfolio.WinRate = totalTrades > 0 ? (decimal)winningTrades / totalTrades * 100 : 0;
var bestAsset = _assetStats.Values.OrderByDescending(s => s.NetProfit).FirstOrDefault();
if (bestAsset != null)
{
portfolio.BestPerformingAssetSymbol = bestAsset.Symbol;
portfolio.BestPerformingAssetProfit = bestAsset.NetProfit;
}
var worstAsset = _assetStats.Values.OrderBy(s => s.NetProfit).FirstOrDefault();
if (worstAsset != null)
{
portfolio.WorstPerformingAssetSymbol = worstAsset.Symbol;
portfolio.WorstPerformingAssetProfit = worstAsset.NetProfit;
}
}
return portfolio;
}
public List<MarketPrice>? GetPriceHistory(string symbol)
{
return _priceHistory.TryGetValue(symbol, out var history) ? history : null;
}
public TechnicalIndicators? GetIndicators(string symbol)
{
return _indicators.TryGetValue(symbol, out var indicators) ? indicators : null;
}
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}");
}
}
}
+138
View File
@@ -0,0 +1,138 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
<!-- 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\" />
<Folder Include="docs\deployment\" />
</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>
+3
View File
@@ -0,0 +1,3 @@
<Solution>
<Project Path="TradingBot.csproj" />
</Solution>
+8
View File
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Information"
}
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
+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!**
+38
View File
@@ -0,0 +1,38 @@
version: '3.8'
services:
tradingbot:
container_name: tradingbot
image: gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest
ports:
- "${EXTERNAL_PORT:-8080}:8080"
volumes:
- tradingbot-data:/app/data
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://+:8080
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 10s
networks:
- tradingbot-network
deploy:
resources:
limits:
cpus: '2.0'
memory: 1G
reservations:
cpus: '0.5'
memory: 256M
volumes:
tradingbot-data:
driver: local
networks:
tradingbot-network:
driver: bridge
+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 ""
+598
View File
@@ -0,0 +1,598 @@
/* CRITICAL: Force Modern Layout Styles */
.trading-bot-layout {
display: flex !important;
min-height: 100vh !important;
background: #0a0e27 !important;
}
.trading-bot-layout.collapsed .modern-sidebar {
width: 80px !important;
}
.trading-bot-layout.collapsed .main-area {
margin-left: 80px !important;
}
.trading-bot-layout.collapsed .sidebar-brand {
padding: 1.5rem 0.75rem !important;
justify-content: center !important;
}
.trading-bot-layout.collapsed .brand-container {
justify-content: center !important;
}
.trading-bot-layout.collapsed .menu-item {
justify-content: center !important;
padding: 1rem 0 !important;
}
.trading-bot-layout.collapsed .item-text {
display: none !important;
}
.modern-sidebar {
width: 280px !important;
background: linear-gradient(180deg, #1a1f3a 0%, #0f1629 100%) !important;
border-right: 1px solid rgba(99, 102, 241, 0.15) !important;
display: flex !important;
flex-direction: column !important;
position: fixed !important;
left: 0 !important;
top: 0 !important;
bottom: 0 !important;
z-index: 1000 !important;
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
.sidebar-brand {
padding: 1.75rem 1.5rem !important;
border-bottom: 1px solid rgba(99, 102, 241, 0.1) !important;
display: flex !important;
}
.brand-logo {
width: 3.5rem !important;
height: 3.5rem !important;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important;
border-radius: 1rem !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.sidebar-menu {
flex: 1 !important;
padding: 1.5rem 0 !important;
}
.menu-item {
display: flex !important;
align-items: center !important;
gap: 1rem !important;
padding: 1rem 1.5rem !important;
color: #94a3b8 !important;
text-decoration: none !important;
border-left: 3px solid transparent !important;
font-weight: 600 !important;
}
.menu-item:hover {
background: rgba(99, 102, 241, 0.08) !important;
color: #cbd5e1 !important;
}
.menu-item.active {
background: rgba(99, 102, 241, 0.12) !important;
border-left-color: #6366f1 !important;
color: #6366f1 !important;
}
.item-icon {
font-size: 1.375rem !important;
}
.main-area {
flex: 1 !important;
display: flex !important;
flex-direction: column !important;
margin-left: 280px !important;
}
.content-header {
background: #0f1629 !important;
border-bottom: 1px solid rgba(99, 102, 241, 0.1) !important;
padding: 1.25rem 2rem !important;
display: flex !important;
}
.page-content {
flex: 1 !important;
padding: 2rem !important;
background: #0a0e27 !important;
}
/* Global Reset & Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
background: #0a0e27;
color: #e2e8f0;
overflow-x: hidden;
}
body {
min-height: 100vh;
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #0f1629;
}
::-webkit-scrollbar-thumb {
background: #334155;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #475569;
}
/* Focus States */
button:focus,
input:focus,
select:focus {
outline: 2px solid #6366f1;
outline-offset: 2px;
}
/* Common Button Styles */
.btn-primary, .btn-secondary, .btn-outline, .btn-icon {
cursor: pointer;
transition: all 0.3s ease;
border: none;
}
.btn-primary {
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, #6366f1 0%, #8b5cf6 100%);
color: white;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4);
}
.btn-secondary {
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: #1e293b;
color: #cbd5e1;
border: 1px solid #334155;
}
.btn-secondary:hover {
background: #334155;
border-color: #475569;
}
.btn-outline {
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: transparent;
color: #6366f1;
border: 1px solid #6366f1;
}
.btn-outline:hover {
background: rgba(99, 102, 241, 0.1);
}
.btn-icon {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.5rem;
background: #1a1f3a;
color: #94a3b8;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.btn-icon:hover {
background: #1e293b;
color: #cbd5e1;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideIn {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Utility Classes */
.animate-fade-in {
animation: fadeIn 0.5s ease-out;
}
.animate-slide-in {
animation: slideIn 0.3s ease-out;
}
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.animate-spin {
animation: spin 1s linear infinite;
}
/* Loading States */
.skeleton {
background: linear-gradient(90deg, #1e293b 25%, #334155 50%, #1e293b 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 0.375rem;
}
.loading-spinner {
width: 2rem;
height: 2rem;
border: 3px solid #1e293b;
border-top-color: #6366f1;
border-radius: 50%;
animation: spin 1s linear infinite;
}
/* Blazor Error Boundary */
.blazor-error-boundary {
background: linear-gradient(135deg, #7f1d1d 0%, #991b1b 100%);
padding: 1rem 1rem 1rem 3.5rem;
color: #fecaca;
border-left: 4px solid #ef4444;
position: relative;
}
.blazor-error-boundary::before {
content: "?";
position: absolute;
left: 1rem;
font-size: 1.5rem;
}
.blazor-error-boundary::after {
content: "Si è verificato un errore nell'applicazione.";
font-weight: 600;
}
/* Card Effects */
.card-glow {
box-shadow: 0 0 20px rgba(99, 102, 241, 0.3);
}
.card-hover {
transition: all 0.3s ease;
}
.card-hover:hover {
transform: translateY(-2px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
}
/* Gradient Text */
.gradient-text {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Status Indicators */
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.status-dot.green {
background: #10b981;
box-shadow: 0 0 8px rgba(16, 185, 129, 0.5);
}
.status-dot.red {
background: #ef4444;
box-shadow: 0 0 8px rgba(239, 68, 68, 0.5);
}
.status-dot.yellow {
background: #f59e0b;
box-shadow: 0 0 8px rgba(245, 158, 11, 0.5);
}
/* Color Classes */
.text-profit {
color: #10b981 !important;
}
.text-loss {
color: #ef4444 !important;
}
.text-warning {
color: #f59e0b !important;
}
.text-info {
color: #3b82f6 !important;
}
.text-muted {
color: #64748b !important;
}
/* Background Classes */
.bg-primary {
background: #6366f1 !important;
}
.bg-success {
background: #10b981 !important;
}
.bg-danger {
background: #ef4444 !important;
}
.bg-dark {
background: #0f1629 !important;
}
/* Badge Styles */
.badge {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
}
.badge.success {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
}
.badge.danger {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.badge.warning {
background: rgba(245, 158, 11, 0.2);
color: #f59e0b;
}
.badge.info {
background: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
/* Responsive Utilities */
@media (max-width: 1024px) {
html {
font-size: 14px;
}
}
@media (max-width: 768px) {
html {
font-size: 13px;
}
.modern-sidebar {
transform: translateX(-100%) !important;
}
.main-area {
margin-left: 0 !important;
}
}
/* 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 {
display: none !important;
}
.sidebar {
display: none !important;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,597 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,594 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More