Compare commits
17 Commits
b2f04b6600
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 64f3511695 | |||
| c229c50f1d | |||
| 0809c9af87 | |||
| ae5f8f9249 | |||
| f21adf3313 | |||
| 92c8e57a8c | |||
| d7ae3e5d44 | |||
| 54cfe05687 | |||
| 0e64afa1f2 | |||
| 121324dfc7 | |||
| cc34d2b57f | |||
| 5532ad2473 | |||
| 8ee8dc7e71 | |||
| d933c7e812 | |||
| f69d5dd567 | |||
| c93ccd5e4a | |||
| e414123cd0 |
@@ -1,3 +1,838 @@
|
|||||||
# Encelado
|
# ?? Encelado - TradingBot
|
||||||
|
|
||||||
Semplice e piccolo robot di trading automatizzato
|
**Sistema automatizzato di trading su criptovalute con interfaccia web moderna**
|
||||||
|
|
||||||
|
[](https://dotnet.microsoft.com/)
|
||||||
|
[](https://blazor.net/)
|
||||||
|
[](https://www.docker.com/)
|
||||||
|
[](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
|
||||||
@@ -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
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
# ?? ISTRUZIONI PER FORZARE IL REFRESH DEL BROWSER
|
|
||||||
|
|
||||||
## ?? Il Problema
|
|
||||||
L'applicazione è stata completamente aggiornata con una nuova sidebar verticale moderna, ma il browser potrebbe mostrare ancora la versione vecchia a causa della **cache**.
|
|
||||||
|
|
||||||
## ? Build Status
|
|
||||||
- ? **Compilazione riuscita**
|
|
||||||
- ? **0 errori**
|
|
||||||
- ? **0 warning**
|
|
||||||
- ? **Tutti i CSS aggiornati**
|
|
||||||
- ? **Bootstrap Icons caricato**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? METODO 1: Hard Refresh (CONSIGLIATO)
|
|
||||||
|
|
||||||
### Windows - Chrome/Edge
|
|
||||||
```
|
|
||||||
1. Apri DevTools: F12
|
|
||||||
2. Click DESTRO sul pulsante Refresh (?)
|
|
||||||
3. Seleziona "Svuota cache e ricaricamento forzato"
|
|
||||||
```
|
|
||||||
|
|
||||||
**OPPURE**
|
|
||||||
```
|
|
||||||
Premi: Ctrl + Shift + R
|
|
||||||
```
|
|
||||||
|
|
||||||
### Windows - Firefox
|
|
||||||
```
|
|
||||||
Premi: Ctrl + Shift + R
|
|
||||||
```
|
|
||||||
|
|
||||||
### Windows - Tutti i Browser
|
|
||||||
```
|
|
||||||
Premi: Ctrl + F5
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? METODO 2: Cancella Cache Manualmente
|
|
||||||
|
|
||||||
### Chrome/Edge
|
|
||||||
```
|
|
||||||
1. Premi Ctrl + Shift + Delete
|
|
||||||
2. Seleziona "Immagini e file memorizzati nella cache"
|
|
||||||
3. Intervallo: "Tutto"
|
|
||||||
4. Click "Cancella dati"
|
|
||||||
5. Ricarica la pagina (F5)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Firefox
|
|
||||||
```
|
|
||||||
1. Premi Ctrl + Shift + Delete
|
|
||||||
2. Seleziona "Cache"
|
|
||||||
3. Click "Cancella adesso"
|
|
||||||
4. Ricarica la pagina (F5)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? METODO 3: Modalità Incognito (TEST VELOCE)
|
|
||||||
|
|
||||||
### Chrome/Edge
|
|
||||||
```
|
|
||||||
Premi: Ctrl + Shift + N
|
|
||||||
```
|
|
||||||
|
|
||||||
### Firefox
|
|
||||||
```
|
|
||||||
Premi: Ctrl + Shift + P
|
|
||||||
```
|
|
||||||
|
|
||||||
Poi naviga su `https://localhost:[PORT]` nella finestra incognito.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ??? METODO 4: Disabilita Cache (Durante Sviluppo)
|
|
||||||
|
|
||||||
### Per Tutti i Browser
|
|
||||||
```
|
|
||||||
1. Apri DevTools: F12
|
|
||||||
2. Vai su tab "Network"
|
|
||||||
3. Spunta "Disable cache"
|
|
||||||
4. MANTIENI DevTools APERTO
|
|
||||||
5. Ricarica (F5)
|
|
||||||
```
|
|
||||||
|
|
||||||
Questo è perfetto durante lo sviluppo!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? METODO 5: Restart Server + Clean Build
|
|
||||||
|
|
||||||
Se proprio non funziona, fai un clean restart:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Stop server
|
|
||||||
Ctrl + C
|
|
||||||
|
|
||||||
# Clean
|
|
||||||
dotnet clean
|
|
||||||
|
|
||||||
# Remove bin/obj
|
|
||||||
Remove-Item -Recurse -Force bin,obj
|
|
||||||
|
|
||||||
# Restore
|
|
||||||
dotnet restore
|
|
||||||
|
|
||||||
# Rebuild
|
|
||||||
dotnet build
|
|
||||||
|
|
||||||
# Run
|
|
||||||
dotnet run
|
|
||||||
```
|
|
||||||
|
|
||||||
Poi fai Hard Refresh nel browser.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? COSA DOVRESTI VEDERE
|
|
||||||
|
|
||||||
Dopo il refresh corretto, dovresti vedere:
|
|
||||||
|
|
||||||
```
|
|
||||||
??????????????????????????????????????
|
|
||||||
? Sidebar Verticale Sinistra ?
|
|
||||||
? ?
|
|
||||||
? [??] TradingBot [?] ? ? Brand + Toggle
|
|
||||||
? ? ATTIVO ?
|
|
||||||
? ?????????????????????????????????? ?
|
|
||||||
? ?? Dashboard ? ? Menu Items
|
|
||||||
? ?? Strategie ? Verticali
|
|
||||||
? ?? Asset ?
|
|
||||||
? ?? Trading ?
|
|
||||||
? ?? Analisi Mercato ?
|
|
||||||
? ?? Statistiche ?
|
|
||||||
? ?? Impostazioni ?
|
|
||||||
? ?????????????????????????????????? ?
|
|
||||||
? Portfolio $15,000 ? ? Summary
|
|
||||||
? Profitto $0.00 ?
|
|
||||||
??????????????????????????????????????
|
|
||||||
```
|
|
||||||
|
|
||||||
**NON** dovresti vedere più i link testuali sotto il logo!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? TROUBLESHOOTING
|
|
||||||
|
|
||||||
### Problema: "Vedo ancora i link sotto il logo"
|
|
||||||
**Soluzione**: Cache non pulita correttamente
|
|
||||||
```
|
|
||||||
1. Chiudi TUTTE le tab del browser
|
|
||||||
2. Chiudi il browser completamente
|
|
||||||
3. Riapri e vai direttamente a localhost
|
|
||||||
4. Premi Ctrl + Shift + R
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problema: "Le icone non si vedono"
|
|
||||||
**Soluzione**: Bootstrap Icons non caricato
|
|
||||||
```
|
|
||||||
1. Apri DevTools (F12)
|
|
||||||
2. Tab Console
|
|
||||||
3. Cerca errori di caricamento CSS
|
|
||||||
4. Se vedi errori, il server potrebbe non essere avviato correttamente
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problema: "Tutto bianco/rotto"
|
|
||||||
**Soluzione**: CSS non caricato
|
|
||||||
```
|
|
||||||
1. DevTools ? Network tab
|
|
||||||
2. Ricarica (F5)
|
|
||||||
3. Verifica che app.css e MainLayout.razor.css siano caricati (200 OK)
|
|
||||||
4. Se vedi 404, restart del server
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ? CHECKLIST FINALE
|
|
||||||
|
|
||||||
Prima di contattare per supporto, verifica:
|
|
||||||
|
|
||||||
- [ ] Ho fatto Hard Refresh (Ctrl + Shift + R)?
|
|
||||||
- [ ] Ho provato in modalità Incognito?
|
|
||||||
- [ ] Ho pulito la cache manualmente?
|
|
||||||
- [ ] Il server è in esecuzione correttamente?
|
|
||||||
- [ ] Ho fatto `dotnet clean` e `dotnet build`?
|
|
||||||
- [ ] Ho verificato la Console (F12) per errori?
|
|
||||||
- [ ] Ho provato con un browser diverso?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? FUNZIONA?
|
|
||||||
|
|
||||||
Se dopo questi passaggi vedi la sidebar moderna verticale:
|
|
||||||
- ? Tutto è corretto!
|
|
||||||
- ? Puoi iniziare a usare l'applicazione
|
|
||||||
- ? Il problema era solo la cache
|
|
||||||
|
|
||||||
Se NON funziona ancora:
|
|
||||||
- ?? Apri DevTools (F12)
|
|
||||||
- ?? Fai uno screenshot della Console
|
|
||||||
- ?? Condividi gli errori che vedi
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? NOTE TECNICHE
|
|
||||||
|
|
||||||
### File CSS Modificati
|
|
||||||
1. `wwwroot/app.css` - Stili globali con priorità
|
|
||||||
2. `Components/Layout/MainLayout.razor.css` - Stili scoped con ::deep
|
|
||||||
|
|
||||||
### Modifiche Applicate
|
|
||||||
- ? Bootstrap Icons CDN aggiunto
|
|
||||||
- ? Namespace globali in _Imports.razor
|
|
||||||
- ? CSS con !important per override
|
|
||||||
- ? ::deep selectors per scoped CSS
|
|
||||||
- ? Layout completamente riscritto
|
|
||||||
|
|
||||||
### Port di Default
|
|
||||||
L'applicazione di solito gira su:
|
|
||||||
- `https://localhost:5001` (HTTPS)
|
|
||||||
- `http://localhost:5000` (HTTP)
|
|
||||||
|
|
||||||
Verifica nel terminal quale porta sta usando!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Buon trading! ??**
|
|
||||||
@@ -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
|
||||||
@@ -41,6 +41,22 @@
|
|||||||
}
|
}
|
||||||
</NavLink>
|
</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">
|
<NavLink class="menu-item" href="/strategies" title="Strategie">
|
||||||
<span class="item-icon bi bi-diagram-3"></span>
|
<span class="item-icon bi bi-diagram-3"></span>
|
||||||
@if (!sidebarCollapsed)
|
@if (!sidebarCollapsed)
|
||||||
@@ -49,6 +65,14 @@
|
|||||||
}
|
}
|
||||||
</NavLink>
|
</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">
|
<NavLink class="menu-item" href="/assets" title="Asset">
|
||||||
<span class="item-icon bi bi-coin"></span>
|
<span class="item-icon bi bi-coin"></span>
|
||||||
@if (!sidebarCollapsed)
|
@if (!sidebarCollapsed)
|
||||||
@@ -81,6 +105,14 @@
|
|||||||
}
|
}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
|
<NavLink class="menu-item" href="/logs" title="Logs">
|
||||||
|
<span class="item-icon bi bi-terminal"></span>
|
||||||
|
@if (!sidebarCollapsed)
|
||||||
|
{
|
||||||
|
<span class="item-text">Logs</span>
|
||||||
|
}
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
<NavLink class="menu-item" href="/settings" title="Impostazioni">
|
<NavLink class="menu-item" href="/settings" title="Impostazioni">
|
||||||
<span class="item-icon bi bi-gear"></span>
|
<span class="item-icon bi bi-gear"></span>
|
||||||
@if (!sidebarCollapsed)
|
@if (!sidebarCollapsed)
|
||||||
@@ -95,16 +127,51 @@
|
|||||||
{
|
{
|
||||||
<div class="sidebar-summary">
|
<div class="sidebar-summary">
|
||||||
<div class="summary-card">
|
<div class="summary-card">
|
||||||
<div class="summary-row">
|
<div class="summary-header">
|
||||||
<span class="summary-title">Portfolio</span>
|
<span class="summary-section-title">Portfolio</span>
|
||||||
<span class="summary-amount">$@portfolioValue.ToString("N0")</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="summary-row">
|
<div class="summary-row">
|
||||||
<span class="summary-title">Profitto</span>
|
<span class="summary-title">Capitale Totale</span>
|
||||||
<span class="summary-amount @(totalProfit >= 0 ? "profit" : "loss")">
|
<span class="summary-amount">$@totalCapital.ToString("N2")</span>
|
||||||
$@totalProfit.ToString("N2")
|
</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>
|
</span>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -140,6 +207,11 @@
|
|||||||
private bool isRunning => BotService.Status.IsRunning;
|
private bool isRunning => BotService.Status.IsRunning;
|
||||||
private decimal portfolioValue = 0;
|
private decimal portfolioValue = 0;
|
||||||
private decimal totalProfit = 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()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
@@ -147,6 +219,7 @@
|
|||||||
sidebarCollapsed = settings.SidebarCollapsed;
|
sidebarCollapsed = settings.SidebarCollapsed;
|
||||||
|
|
||||||
BotService.OnStatusChanged += HandleUpdate;
|
BotService.OnStatusChanged += HandleUpdate;
|
||||||
|
BotService.OnTradeExecuted += HandleTradeExecuted;
|
||||||
BotService.OnPriceUpdated += HandlePriceUpdate;
|
BotService.OnPriceUpdated += HandlePriceUpdate;
|
||||||
SettingsService.OnSettingsChanged += HandleSettingsChanged;
|
SettingsService.OnSettingsChanged += HandleSettingsChanged;
|
||||||
|
|
||||||
@@ -176,34 +249,69 @@
|
|||||||
|
|
||||||
private void UpdateStats()
|
private void UpdateStats()
|
||||||
{
|
{
|
||||||
portfolioValue = BotService.AssetConfigurations.Values.Sum(c =>
|
totalCapital = 0;
|
||||||
c.CurrentBalance + (c.CurrentHoldings * (BotService.GetLatestPrice(c.Symbol)?.Price ?? 0)));
|
investedCapital = 0;
|
||||||
|
availableCapital = 0;
|
||||||
|
currentPL = 0;
|
||||||
|
totalProfit = 0;
|
||||||
|
|
||||||
totalProfit = BotService.AssetConfigurations.Values.Sum(c => c.TotalProfit);
|
foreach (var config in BotService.AssetConfigurations.Values)
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleUpdate()
|
|
||||||
{
|
{
|
||||||
UpdateStats();
|
if (!config.IsEnabled) continue;
|
||||||
InvokeAsync(StateHasChanged);
|
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandlePriceUpdate(string symbol, MarketPrice price)
|
// Totale profitti/perdite realizzati
|
||||||
{
|
totalProfit += config.TotalProfit;
|
||||||
UpdateStats();
|
|
||||||
InvokeAsync(StateHasChanged);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleSettingsChanged()
|
// 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 settings = SettingsService.GetSettings();
|
var totalGain = currentPL + totalProfit;
|
||||||
sidebarCollapsed = settings.SidebarCollapsed;
|
roiPercentage = (totalGain / initialCapital) * 100;
|
||||||
InvokeAsync(StateHasChanged);
|
|
||||||
}
|
}
|
||||||
|
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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
BotService.OnStatusChanged -= HandleUpdate;
|
BotService.OnStatusChanged -= HandleUpdate;
|
||||||
|
BotService.OnTradeExecuted -= HandleTradeExecuted;
|
||||||
BotService.OnPriceUpdated -= HandlePriceUpdate;
|
BotService.OnPriceUpdated -= HandlePriceUpdate;
|
||||||
SettingsService.OnSettingsChanged -= HandleSettingsChanged;
|
SettingsService.OnSettingsChanged -= HandleSettingsChanged;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -256,12 +256,35 @@
|
|||||||
gap: 0.875rem !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 {
|
::deep .summary-row {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
justify-content: space-between !important;
|
justify-content: space-between !important;
|
||||||
align-items: center !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 {
|
::deep .summary-title {
|
||||||
font-size: 0.75rem !important;
|
font-size: 0.75rem !important;
|
||||||
color: #64748b !important;
|
color: #64748b !important;
|
||||||
@@ -285,6 +308,14 @@
|
|||||||
color: #ef4444 !important;
|
color: #ef4444 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::deep .summary-amount.invested {
|
||||||
|
color: #f59e0b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::deep .summary-amount.available {
|
||||||
|
color: #3b82f6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* ==============================================
|
/* ==============================================
|
||||||
MAIN CONTENT AREA
|
MAIN CONTENT AREA
|
||||||
============================================== */
|
============================================== */
|
||||||
@@ -466,3 +497,46 @@
|
|||||||
padding: 1.5rem 1rem !important;
|
padding: 1.5rem 1rem !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==============================================
|
||||||
|
VERSION FOOTER
|
||||||
|
============================================== */
|
||||||
|
|
||||||
|
::deep .sidebar-footer {
|
||||||
|
padding: 1rem 1.5rem !important;
|
||||||
|
border-top: 1px solid rgba(99, 102, 241, 0.1) !important;
|
||||||
|
margin-top: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::deep .version-info {
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: space-between !important;
|
||||||
|
align-items: center !important;
|
||||||
|
margin-bottom: 0.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::deep .version-label {
|
||||||
|
font-size: 0.75rem !important;
|
||||||
|
color: #64748b !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
text-transform: uppercase !important;
|
||||||
|
letter-spacing: 0.05em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::deep .version-number {
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
color: #6366f1 !important;
|
||||||
|
font-family: 'Courier New', monospace !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::deep .build-info {
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: flex-start !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::deep .build-date {
|
||||||
|
font-size: 0.625rem !important;
|
||||||
|
color: #475569 !important;
|
||||||
|
font-family: 'Courier New', monospace !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,467 @@
|
|||||||
|
@page "/indicators"
|
||||||
|
@using TradingBot.Services
|
||||||
|
@using TradingBot.Models
|
||||||
|
@inject IndicatorsService IndicatorsService
|
||||||
|
@inject TradingBotService BotService
|
||||||
|
@implements IDisposable
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
|
<PageTitle>Indicatori - TradingBot</PageTitle>
|
||||||
|
|
||||||
|
<div class="indicators-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<div>
|
||||||
|
<h1>Indicatori Tecnici</h1>
|
||||||
|
<p class="subtitle">Configura gli indicatori per analisi avanzata del mercato</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Indicators Grid -->
|
||||||
|
<div class="indicators-grid">
|
||||||
|
@foreach (var indicator in indicators.Values.OrderBy(i => !i.IsEnabled).ThenBy(i => i.Name))
|
||||||
|
{
|
||||||
|
<div class="indicator-card @(indicator.IsEnabled ? "enabled" : "disabled")">
|
||||||
|
<div class="indicator-header">
|
||||||
|
<div class="indicator-title">
|
||||||
|
<h3>@indicator.Name</h3>
|
||||||
|
<span class="indicator-type">@indicator.Type</span>
|
||||||
|
</div>
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" checked="@indicator.IsEnabled"
|
||||||
|
@onchange="@(e => ToggleIndicator(indicator.Id, (bool)e.Value!))" />
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="indicator-description">@indicator.Description</p>
|
||||||
|
|
||||||
|
@if (indicator.IsEnabled)
|
||||||
|
{
|
||||||
|
<div class="indicator-config">
|
||||||
|
@switch (indicator.Type)
|
||||||
|
{
|
||||||
|
case IndicatorType.RSI:
|
||||||
|
case IndicatorType.Stochastic:
|
||||||
|
<div class="config-row">
|
||||||
|
<label>Periodo:</label>
|
||||||
|
<input type="number" @bind="indicator.Period" min="5" max="50" />
|
||||||
|
</div>
|
||||||
|
<div class="config-row">
|
||||||
|
<label>Ipercomprato:</label>
|
||||||
|
<input type="number" @bind="indicator.OverboughtThreshold" min="60" max="90" step="5" />
|
||||||
|
</div>
|
||||||
|
<div class="config-row">
|
||||||
|
<label>Ipervenduto:</label>
|
||||||
|
<input type="number" @bind="indicator.OversoldThreshold" min="10" max="40" step="5" />
|
||||||
|
</div>
|
||||||
|
break;
|
||||||
|
|
||||||
|
case IndicatorType.MACD:
|
||||||
|
<div class="config-row">
|
||||||
|
<label>Fast Period:</label>
|
||||||
|
<input type="number" @bind="indicator.FastPeriod" min="8" max="20" />
|
||||||
|
</div>
|
||||||
|
<div class="config-row">
|
||||||
|
<label>Slow Period:</label>
|
||||||
|
<input type="number" @bind="indicator.SlowPeriod" min="20" max="35" />
|
||||||
|
</div>
|
||||||
|
<div class="config-row">
|
||||||
|
<label>Signal Period:</label>
|
||||||
|
<input type="number" @bind="indicator.SignalPeriod" min="5" max="15" />
|
||||||
|
</div>
|
||||||
|
break;
|
||||||
|
|
||||||
|
case IndicatorType.SMA:
|
||||||
|
case IndicatorType.EMA:
|
||||||
|
case IndicatorType.BollingerBands:
|
||||||
|
<div class="config-row">
|
||||||
|
<label>Periodo:</label>
|
||||||
|
<input type="number" @bind="indicator.Period" min="5" max="200" />
|
||||||
|
</div>
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
<button class="btn-secondary btn-sm" @onclick="() => SaveIndicator(indicator)">
|
||||||
|
<span class="bi bi-check-lg"></span>
|
||||||
|
Salva
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Current Status for Active Assets -->
|
||||||
|
<div class="indicator-status-section">
|
||||||
|
<h4>Status Asset Attivi</h4>
|
||||||
|
@foreach (var symbol in BotService.AssetConfigurations.Values.Where(c => c.IsEnabled).Select(c => c.Symbol))
|
||||||
|
{
|
||||||
|
var status = IndicatorsService.GetIndicatorStatus(indicator.Id, symbol);
|
||||||
|
if (status != null)
|
||||||
|
{
|
||||||
|
<div class="status-row">
|
||||||
|
<span class="status-symbol">@symbol</span>
|
||||||
|
<span class="status-value">@status.CurrentValue.ToString("F2")</span>
|
||||||
|
<span class="status-condition condition-@status.Condition.ToString().ToLower()">
|
||||||
|
@status.Condition
|
||||||
|
</span>
|
||||||
|
<span class="status-recommendation">@status.Recommendation</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Signals -->
|
||||||
|
<div class="signals-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h2>Segnali Recenti</h2>
|
||||||
|
<span class="signals-count">@recentSignals.Count segnali</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (recentSignals.Count == 0)
|
||||||
|
{
|
||||||
|
<div class="empty-state">
|
||||||
|
<span class="bi bi-broadcast"></span>
|
||||||
|
<p>Nessun segnale generato</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="signals-list">
|
||||||
|
@foreach (var signal in recentSignals.Take(20))
|
||||||
|
{
|
||||||
|
<div class="signal-card signal-@signal.Type.ToString().ToLower()">
|
||||||
|
<div class="signal-header">
|
||||||
|
<span class="signal-time">@signal.Timestamp.ToLocalTime().ToString("HH:mm:ss")</span>
|
||||||
|
<span class="signal-indicator">@signal.IndicatorName</span>
|
||||||
|
<span class="signal-symbol">@signal.Symbol</span>
|
||||||
|
<span class="signal-type type-@signal.Type.ToString().ToLower()">
|
||||||
|
@signal.Type
|
||||||
|
</span>
|
||||||
|
<span class="signal-strength strength-@signal.Strength.ToString().ToLower()">
|
||||||
|
@signal.Strength
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="signal-message">@signal.Message</div>
|
||||||
|
@if (signal.Value.HasValue)
|
||||||
|
{
|
||||||
|
<div class="signal-value">Valore: @signal.Value.Value.ToString("F2")</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.indicators-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicators-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-card {
|
||||||
|
background: #1a1f3a;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||||
|
padding: 1.5rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-card.disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
border-color: rgba(100, 116, 139, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-card.enabled {
|
||||||
|
border-color: rgba(99, 102, 241, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-title h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e2e8f0;
|
||||||
|
margin: 0 0 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-type {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
background: rgba(99, 102, 241, 0.2);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
color: #6366f1;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-description {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-config {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-row:last-of-type {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-row label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-row input[type="number"] {
|
||||||
|
width: 80px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: #0f1629;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-status-section {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid rgba(99, 102, 241, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-status-section h4 {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 60px 80px 100px 1fr;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-symbol {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #8b5cf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-condition {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.condition-overbought { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
|
||||||
|
.condition-oversold { background: rgba(16, 185, 129, 0.2); color: #10b981; }
|
||||||
|
.condition-bullish { background: rgba(16, 185, 129, 0.2); color: #10b981; }
|
||||||
|
.condition-bearish { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
|
||||||
|
.condition-neutral { background: rgba(100, 116, 139, 0.2); color: #94a3b8; }
|
||||||
|
|
||||||
|
.status-recommendation {
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signals-section {
|
||||||
|
background: #1a1f3a;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signals-count {
|
||||||
|
color: #6366f1;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signals-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-card {
|
||||||
|
background: #0f1629;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border-left: 3px solid;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-card.signal-buy {
|
||||||
|
border-left-color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-card.signal-sell {
|
||||||
|
border-left-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-card.signal-hold {
|
||||||
|
border-left-color: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-header {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-time {
|
||||||
|
color: #64748b;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-indicator {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
background: rgba(99, 102, 241, 0.2);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
color: #6366f1;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-symbol {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
background: rgba(139, 92, 246, 0.2);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
color: #8b5cf6;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-type {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-buy { background: rgba(16, 185, 129, 0.2); color: #10b981; }
|
||||||
|
.type-sell { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
|
||||||
|
.type-hold { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
|
||||||
|
|
||||||
|
.signal-strength {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
background: rgba(59, 130, 246, 0.2);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
color: #3b82f6;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-message {
|
||||||
|
color: #e2e8f0;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-value {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state .bi {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private Dictionary<string, IndicatorConfig> indicators = new();
|
||||||
|
private List<IndicatorSignal> recentSignals = new();
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
LoadIndicators();
|
||||||
|
IndicatorsService.OnIndicatorsChanged += HandleIndicatorsChanged;
|
||||||
|
IndicatorsService.OnSignalGenerated += HandleSignalGenerated;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadIndicators()
|
||||||
|
{
|
||||||
|
indicators = IndicatorsService.GetIndicators().ToDictionary(k => k.Key, v => v.Value);
|
||||||
|
recentSignals = IndicatorsService.GetRecentSignals().ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleIndicator(string id, bool enabled)
|
||||||
|
{
|
||||||
|
IndicatorsService.ToggleIndicator(id, enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveIndicator(IndicatorConfig indicator)
|
||||||
|
{
|
||||||
|
IndicatorsService.UpdateIndicator(indicator.Id, indicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleIndicatorsChanged()
|
||||||
|
{
|
||||||
|
LoadIndicators();
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleSignalGenerated(IndicatorSignal signal)
|
||||||
|
{
|
||||||
|
recentSignals = IndicatorsService.GetRecentSignals().ToList();
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
IndicatorsService.OnIndicatorsChanged -= HandleIndicatorsChanged;
|
||||||
|
IndicatorsService.OnSignalGenerated -= HandleSignalGenerated;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
@using TradingBot.Services
|
@using TradingBot.Services
|
||||||
@using TradingBot.Models
|
@using TradingBot.Models
|
||||||
@inject SettingsService SettingsService
|
@inject SettingsService SettingsService
|
||||||
|
@inject TradingBotService TradingBotService
|
||||||
|
@inject TradeHistoryService HistoryService
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
@@ -67,6 +69,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-section">
|
||||||
|
<h2>Dati Persistenti</h2>
|
||||||
|
<div class="settings-group">
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-info">
|
||||||
|
<div class="setting-label">Trade Salvati</div>
|
||||||
|
<div class="setting-description">@TradingBotService.Trades.Count trade nella cronologia</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-value">
|
||||||
|
@FormatBytes(dataSize)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-info">
|
||||||
|
<div class="setting-label">Posizioni Attive</div>
|
||||||
|
<div class="setting-description">@TradingBotService.ActivePositions.Count posizioni aperte</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-info">
|
||||||
|
<div class="setting-label">Cancella Tutti i Dati</div>
|
||||||
|
<div class="setting-description text-danger">Elimina cronologia trade e resetta i saldi</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn-danger" @onclick="ShowClearDataConfirmation" disabled="@TradingBotService.Status.IsRunning">
|
||||||
|
<span class="bi bi-trash"></span>
|
||||||
|
Cancella Dati
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h2>Avanzate</h2>
|
<h2>Avanzate</h2>
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
@@ -116,16 +151,57 @@
|
|||||||
Impostazioni salvate con successo!
|
Impostazioni salvate con successo!
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (showClearConfirmation)
|
||||||
|
{
|
||||||
|
<div class="modal-overlay" @onclick="HideClearDataConfirmation">
|
||||||
|
<div class="modal-dialog" @onclick:stopPropagation="true">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Conferma Cancellazione</h3>
|
||||||
|
<button class="btn-close" @onclick="HideClearDataConfirmation">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="text-danger">
|
||||||
|
<strong>Attenzione!</strong> Questa azione eliminerà:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Tutta la cronologia dei trade (@TradingBotService.Trades.Count trade)</li>
|
||||||
|
<li>Tutte le posizioni attive (@TradingBotService.ActivePositions.Count posizioni)</li>
|
||||||
|
<li>I saldi verranno resettati ai valori iniziali</li>
|
||||||
|
</ul>
|
||||||
|
<p class="text-danger">
|
||||||
|
<strong>Questa operazione è irreversibile!</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn-secondary" @onclick="HideClearDataConfirmation">Annulla</button>
|
||||||
|
<button class="btn-danger" @onclick="ConfirmClearData">
|
||||||
|
<span class="bi bi-trash"></span>
|
||||||
|
Conferma Cancellazione
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private AppSettings settings = new();
|
private AppSettings settings = new();
|
||||||
private bool showNotification = false;
|
private bool showNotification = false;
|
||||||
|
private bool showClearConfirmation = false;
|
||||||
|
private long dataSize = 0;
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
settings = SettingsService.GetSettings();
|
settings = SettingsService.GetSettings();
|
||||||
SettingsService.OnSettingsChanged += HandleSettingsChanged;
|
SettingsService.OnSettingsChanged += HandleSettingsChanged;
|
||||||
|
TradingBotService.OnStatusChanged += HandleStatusChanged;
|
||||||
|
UpdateDataSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateDataSize()
|
||||||
|
{
|
||||||
|
dataSize = HistoryService.GetDataSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateSetting<T>(string propertyName, T value)
|
private void UpdateSetting<T>(string propertyName, T value)
|
||||||
@@ -148,6 +224,28 @@
|
|||||||
ShowNotification();
|
ShowNotification();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ShowClearDataConfirmation()
|
||||||
|
{
|
||||||
|
showClearConfirmation = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HideClearDataConfirmation()
|
||||||
|
{
|
||||||
|
showClearConfirmation = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ConfirmClearData()
|
||||||
|
{
|
||||||
|
await TradingBotService.ClearAllDataAsync();
|
||||||
|
UpdateDataSize();
|
||||||
|
showClearConfirmation = false;
|
||||||
|
showNotification = true;
|
||||||
|
StateHasChanged();
|
||||||
|
await Task.Delay(3000);
|
||||||
|
showNotification = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
private async void ShowNotification()
|
private async void ShowNotification()
|
||||||
{
|
{
|
||||||
showNotification = true;
|
showNotification = true;
|
||||||
@@ -157,14 +255,34 @@
|
|||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string FormatBytes(long bytes)
|
||||||
|
{
|
||||||
|
string[] sizes = { "B", "KB", "MB", "GB" };
|
||||||
|
double len = bytes;
|
||||||
|
int order = 0;
|
||||||
|
while (len >= 1024 && order < sizes.Length - 1)
|
||||||
|
{
|
||||||
|
order++;
|
||||||
|
len = len / 1024;
|
||||||
|
}
|
||||||
|
return $"{len:0.##} {sizes[order]}";
|
||||||
|
}
|
||||||
|
|
||||||
private void HandleSettingsChanged()
|
private void HandleSettingsChanged()
|
||||||
{
|
{
|
||||||
settings = SettingsService.GetSettings();
|
settings = SettingsService.GetSettings();
|
||||||
InvokeAsync(StateHasChanged);
|
InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleStatusChanged()
|
||||||
|
{
|
||||||
|
UpdateDataSize();
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
SettingsService.OnSettingsChanged -= HandleSettingsChanged;
|
SettingsService.OnSettingsChanged -= HandleSettingsChanged;
|
||||||
|
TradingBotService.OnStatusChanged -= HandleStatusChanged;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,555 @@
|
|||||||
|
@page "/trading-control"
|
||||||
|
@using TradingBot.Services
|
||||||
|
@using TradingBot.Models
|
||||||
|
@inject TradingStrategiesService StrategiesService
|
||||||
|
@inject TradingBotService BotService
|
||||||
|
@implements IDisposable
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
|
<PageTitle>Trading Control - TradingBot</PageTitle>
|
||||||
|
|
||||||
|
<div class="trading-control-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<div>
|
||||||
|
<h1>Trading Control</h1>
|
||||||
|
<p class="subtitle">Gestisci le strategie di trading per ogni asset</p>
|
||||||
|
</div>
|
||||||
|
<div class="header-stats">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Asset Attivi</span>
|
||||||
|
<span class="stat-value">@activeAssets</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Strategie in Uso</span>
|
||||||
|
<span class="stat-value">@totalStrategies</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Assets Grid -->
|
||||||
|
<div class="assets-control-grid">
|
||||||
|
@foreach (var asset in BotService.AssetConfigurations.Values.OrderBy(a => a.Symbol))
|
||||||
|
{
|
||||||
|
var mapping = StrategiesService.GetAssetMapping(asset.Symbol);
|
||||||
|
var engineStatus = StrategiesService.GetEngineStatus(asset.Symbol);
|
||||||
|
var isActive = mapping?.IsActive ?? false;
|
||||||
|
|
||||||
|
<div class="asset-control-card @(isActive ? "active" : "")">
|
||||||
|
<div class="asset-header">
|
||||||
|
<div class="asset-info">
|
||||||
|
<h3>@asset.Symbol</h3>
|
||||||
|
<span class="asset-name">@asset.Name</span>
|
||||||
|
</div>
|
||||||
|
<div class="asset-controls">
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox"
|
||||||
|
checked="@isActive"
|
||||||
|
@onchange="(e) => ToggleAsset(asset.Symbol, (bool)e.Value!)"
|
||||||
|
disabled="@((mapping?.StrategyIds.Count ?? 0) == 0)" />
|
||||||
|
<span class="toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (mapping != null && mapping.StrategyIds.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="assigned-strategies">
|
||||||
|
<h4>Strategie Assegnate (@mapping.StrategyIds.Count)</h4>
|
||||||
|
<div class="strategies-list">
|
||||||
|
@foreach (var strategyId in mapping.StrategyIds)
|
||||||
|
{
|
||||||
|
var strategyInfo = availableStrategies[strategyId];
|
||||||
|
<div class="strategy-badge">
|
||||||
|
<span class="strategy-name">@strategyInfo.Name</span>
|
||||||
|
<span class="strategy-category">@strategyInfo.Category</span>
|
||||||
|
<button class="btn-remove" @onclick="() => RemoveStrategy(asset.Symbol, strategyId)">
|
||||||
|
<span class="bi bi-x"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (engineStatus?.LastDecision != null && isActive)
|
||||||
|
{
|
||||||
|
<div class="last-decision">
|
||||||
|
<h4>Ultima Decisione</h4>
|
||||||
|
<div class="decision-info">
|
||||||
|
<span class="decision-type type-@engineStatus.LastDecision.Decision.ToString().ToLower()">
|
||||||
|
@engineStatus.LastDecision.Decision
|
||||||
|
</span>
|
||||||
|
<span class="decision-confidence">
|
||||||
|
Confidenza: @engineStatus.LastDecision.Confidence.ToString("F0")%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="decision-reason">@engineStatus.LastDecision.Reason</div>
|
||||||
|
<div class="decision-votes">
|
||||||
|
<span class="vote buy">Buy: @engineStatus.LastDecision.BuyVotes</span>
|
||||||
|
<span class="vote sell">Sell: @engineStatus.LastDecision.SellVotes</span>
|
||||||
|
<span class="vote hold">Hold: @engineStatus.LastDecision.HoldVotes</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="asset-actions">
|
||||||
|
<button class="btn-primary" @onclick="() => OpenStrategySelector(asset.Symbol, asset.Name)">
|
||||||
|
<span class="bi bi-plus-lg"></span>
|
||||||
|
@((mapping?.StrategyIds.Count ?? 0) > 0 ? "Gestisci Strategie" : "Assegna Strategie")
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Strategy Selector Modal -->
|
||||||
|
@if (showStrategySelector)
|
||||||
|
{
|
||||||
|
<div class="modal-overlay" @onclick="CloseStrategySelector">
|
||||||
|
<div class="modal-dialog large" @onclick:stopPropagation="true">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Gestisci Strategie - @selectedAssetSymbol</h3>
|
||||||
|
<button class="btn-close" @onclick="CloseStrategySelector">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="strategies-selector">
|
||||||
|
@foreach (var category in availableStrategies.Values.Select(s => s.Category).Distinct().OrderBy(c => c))
|
||||||
|
{
|
||||||
|
<div class="category-section">
|
||||||
|
<h4 class="category-title">@category</h4>
|
||||||
|
<div class="category-strategies">
|
||||||
|
@foreach (var strategy in availableStrategies.Values.Where(s => s.Category == category))
|
||||||
|
{
|
||||||
|
var isSelected = selectedStrategies.Contains(strategy.Id);
|
||||||
|
<div class="strategy-option @(isSelected ? "selected" : "")"
|
||||||
|
@onclick="() => ToggleStrategy(strategy.Id)">
|
||||||
|
<div class="strategy-option-header">
|
||||||
|
<div class="strategy-checkbox">
|
||||||
|
<input type="checkbox" checked="@isSelected" />
|
||||||
|
</div>
|
||||||
|
<div class="strategy-details">
|
||||||
|
<h5>@strategy.Name</h5>
|
||||||
|
<p>@strategy.Description</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="strategy-meta">
|
||||||
|
<span class="risk-badge risk-@strategy.RiskLevel.ToString().ToLower()">
|
||||||
|
@strategy.RiskLevel Risk
|
||||||
|
</span>
|
||||||
|
<span class="timeframe-badge">
|
||||||
|
@strategy.RecommendedTimeFrame
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn-secondary" @onclick="CloseStrategySelector">Annulla</button>
|
||||||
|
<button class="btn-primary" @onclick="SaveStrategies">
|
||||||
|
<span class="bi bi-check-lg"></span>
|
||||||
|
Salva (@selectedStrategies.Count strategie)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.trading-control-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assets-control-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-control-card {
|
||||||
|
background: #1a1f3a;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||||
|
padding: 1.5rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-control-card.active {
|
||||||
|
border-color: rgba(16, 185, 129, 0.5);
|
||||||
|
background: linear-gradient(135deg, #1a1f3a 0%, rgba(16, 185, 129, 0.05) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-info h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e2e8f0;
|
||||||
|
margin: 0 0 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-name {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assigned-strategies {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assigned-strategies h4 {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategies-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-badge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-name {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-category {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
background: rgba(99, 102, 241, 0.2);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
color: #6366f1;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #ef4444;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-decision {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-decision h4 {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decision-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decision-type {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-buy { background: rgba(16, 185, 129, 0.2); color: #10b981; }
|
||||||
|
.type-sell { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
|
||||||
|
.type-hold { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
|
||||||
|
|
||||||
|
.decision-confidence {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decision-reason {
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.decision-votes {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vote.buy { background: rgba(16, 185, 129, 0.1); color: #10b981; }
|
||||||
|
.vote.sell { background: rgba(239, 68, 68, 0.1); color: #ef4444; }
|
||||||
|
.vote.hold { background: rgba(245, 158, 11, 0.1); color: #f59e0b; }
|
||||||
|
|
||||||
|
.asset-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Styles */
|
||||||
|
.modal-dialog.large {
|
||||||
|
max-width: 900px;
|
||||||
|
max-height: 80vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategies-selector {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-section {
|
||||||
|
border-bottom: 1px solid rgba(99, 102, 241, 0.1);
|
||||||
|
padding-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-section:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #6366f1;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-strategies {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-option {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border: 2px solid rgba(99, 102, 241, 0.2);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-option:hover {
|
||||||
|
border-color: rgba(99, 102, 241, 0.5);
|
||||||
|
background: rgba(99, 102, 241, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-option.selected {
|
||||||
|
border-color: #6366f1;
|
||||||
|
background: rgba(99, 102, 241, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-option-header {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-checkbox input {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-details h5 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e2e8f0;
|
||||||
|
margin: 0 0 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-details p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-badge, .timeframe-badge {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-low { background: rgba(16, 185, 129, 0.2); color: #10b981; }
|
||||||
|
.risk-medium { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
|
||||||
|
.risk-high { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
|
||||||
|
.risk-veryhigh { background: rgba(220, 38, 38, 0.3); color: #dc2626; }
|
||||||
|
|
||||||
|
.timeframe-badge {
|
||||||
|
background: rgba(139, 92, 246, 0.2);
|
||||||
|
color: #8b5cf6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private Dictionary<string, StrategyInfo> availableStrategies = new();
|
||||||
|
private bool showStrategySelector = false;
|
||||||
|
private string selectedAssetSymbol = "";
|
||||||
|
private string selectedAssetName = "";
|
||||||
|
private List<string> selectedStrategies = new();
|
||||||
|
private int activeAssets = 0;
|
||||||
|
private int totalStrategies = 0;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
LoadData();
|
||||||
|
StrategiesService.OnMappingsChanged += HandleMappingsChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadData()
|
||||||
|
{
|
||||||
|
availableStrategies = StrategiesService.GetAvailableStrategies().ToDictionary(k => k.Key, v => v.Value);
|
||||||
|
|
||||||
|
var mappings = StrategiesService.GetAllMappings();
|
||||||
|
activeAssets = mappings.Values.Count(m => m.IsActive);
|
||||||
|
totalStrategies = mappings.Values.Sum(m => m.StrategyIds.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleAsset(string symbol, bool active)
|
||||||
|
{
|
||||||
|
if (active)
|
||||||
|
{
|
||||||
|
StrategiesService.ActivateAsset(symbol);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StrategiesService.DeactivateAsset(symbol);
|
||||||
|
}
|
||||||
|
LoadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenStrategySelector(string symbol, string name)
|
||||||
|
{
|
||||||
|
selectedAssetSymbol = symbol;
|
||||||
|
selectedAssetName = name;
|
||||||
|
|
||||||
|
var mapping = StrategiesService.GetAssetMapping(symbol);
|
||||||
|
selectedStrategies = mapping?.StrategyIds.ToList() ?? new List<string>();
|
||||||
|
|
||||||
|
showStrategySelector = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CloseStrategySelector()
|
||||||
|
{
|
||||||
|
showStrategySelector = false;
|
||||||
|
selectedStrategies.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleStrategy(string strategyId)
|
||||||
|
{
|
||||||
|
if (selectedStrategies.Contains(strategyId))
|
||||||
|
{
|
||||||
|
selectedStrategies.Remove(strategyId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
selectedStrategies.Add(strategyId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveStrategies()
|
||||||
|
{
|
||||||
|
StrategiesService.AssignStrategiesToAsset(selectedAssetSymbol, selectedAssetName, selectedStrategies);
|
||||||
|
CloseStrategySelector();
|
||||||
|
LoadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveStrategy(string symbol, string strategyId)
|
||||||
|
{
|
||||||
|
StrategiesService.RemoveStrategyFromAsset(symbol, strategyId);
|
||||||
|
LoadData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleMappingsChanged()
|
||||||
|
{
|
||||||
|
LoadData();
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
StrategiesService.OnMappingsChanged -= HandleMappingsChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
@using TradingBot.Services
|
||||||
|
@using TradingBot.Models
|
||||||
|
@inject IndicatorsService IndicatorsService
|
||||||
|
@inject TradingBotService BotService
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
|
<div class="indicators-widget">
|
||||||
|
<div class="widget-header">
|
||||||
|
<h3>Indicatori Attivi</h3>
|
||||||
|
<a href="/indicators" class="btn-link">
|
||||||
|
Configura <span class="bi bi-arrow-right"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="indicators-grid">
|
||||||
|
@foreach (var indicator in enabledIndicators.Take(6))
|
||||||
|
{
|
||||||
|
<div class="indicator-mini-card">
|
||||||
|
<div class="indicator-mini-header">
|
||||||
|
<span class="indicator-mini-name">@indicator.Name</span>
|
||||||
|
<span class="indicator-mini-type">@indicator.Type</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (topAssets.Any())
|
||||||
|
{
|
||||||
|
var symbol = topAssets.First();
|
||||||
|
var status = IndicatorsService.GetIndicatorStatus(indicator.Id, symbol);
|
||||||
|
|
||||||
|
if (status != null)
|
||||||
|
{
|
||||||
|
<div class="indicator-mini-value">
|
||||||
|
<span class="value-number">@status.CurrentValue.ToString("F2")</span>
|
||||||
|
<span class="value-condition condition-@status.Condition.ToString().ToLower()">
|
||||||
|
@status.Condition
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="indicator-mini-recommendation">
|
||||||
|
@status.Recommendation
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="indicator-mini-loading">Calcolo...</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="indicator-mini-empty">Nessun asset attivo</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (enabledIndicators.Count() > 6)
|
||||||
|
{
|
||||||
|
<div class="indicators-more">
|
||||||
|
<a href="/indicators" class="btn-secondary btn-sm">
|
||||||
|
Vedi tutti (@enabledIndicators.Count()) <span class="bi bi-arrow-right"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.indicators-widget {
|
||||||
|
background: #1a1f3a;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-header h3 {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e2e8f0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
color: #6366f1;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-link:hover {
|
||||||
|
color: #8b5cf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicators-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-mini-card {
|
||||||
|
background: #0f1629;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid rgba(99, 102, 241, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-mini-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-mini-name {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-mini-type {
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
background: rgba(99, 102, 241, 0.2);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
color: #6366f1;
|
||||||
|
font-size: 0.625rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-mini-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-number {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-condition {
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.625rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.condition-overbought { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
|
||||||
|
.condition-oversold { background: rgba(16, 185, 129, 0.2); color: #10b981; }
|
||||||
|
.condition-bullish { background: rgba(16, 185, 129, 0.2); color: #10b981; }
|
||||||
|
.condition-bearish { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
|
||||||
|
.condition-neutral { background: rgba(100, 116, 139, 0.2); color: #94a3b8; }
|
||||||
|
.condition-ranging { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
|
||||||
|
.condition-trending { background: rgba(59, 130, 246, 0.2); color: #3b82f6; }
|
||||||
|
|
||||||
|
.indicator-mini-recommendation {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-mini-loading,
|
||||||
|
.indicator-mini-empty {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicators-more {
|
||||||
|
margin-top: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private List<IndicatorConfig> enabledIndicators = new();
|
||||||
|
private List<string> topAssets = new();
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
LoadData();
|
||||||
|
IndicatorsService.OnIndicatorsChanged += HandleUpdate;
|
||||||
|
BotService.OnStatusChanged += HandleUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadData()
|
||||||
|
{
|
||||||
|
enabledIndicators = IndicatorsService.GetEnabledIndicators().ToList();
|
||||||
|
topAssets = BotService.AssetConfigurations.Values
|
||||||
|
.Where(c => c.IsEnabled)
|
||||||
|
.OrderByDescending(c => c.CurrentBalance + (c.CurrentHoldings * (BotService.GetLatestPrice(c.Symbol)?.Price ?? 0)))
|
||||||
|
.Select(c => c.Symbol)
|
||||||
|
.Take(1)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleUpdate()
|
||||||
|
{
|
||||||
|
LoadData();
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
IndicatorsService.OnIndicatorsChanged -= HandleUpdate;
|
||||||
|
BotService.OnStatusChanged -= HandleUpdate;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
# ?? QUICK START - Docker Deployment
|
|
||||||
|
|
||||||
## Per Sviluppo Locale
|
|
||||||
|
|
||||||
### Windows
|
|
||||||
```powershell
|
|
||||||
# Build
|
|
||||||
.\build-docker.bat
|
|
||||||
|
|
||||||
# Run
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# Stop
|
|
||||||
docker-compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linux/Mac
|
|
||||||
```sh
|
|
||||||
# Build
|
|
||||||
chmod +x build-docker.sh
|
|
||||||
./build-docker.sh
|
|
||||||
|
|
||||||
# Run
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# Stop
|
|
||||||
docker-compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
### Accesso
|
|
||||||
```
|
|
||||||
http://localhost:8080
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Per Unraid (via Portainer)
|
|
||||||
|
|
||||||
### 1. Setup Git Repository
|
|
||||||
```sh
|
|
||||||
git add .
|
|
||||||
git commit -m "Docker ready"
|
|
||||||
git push origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Deploy su Portainer
|
|
||||||
1. Stacks ? Add stack
|
|
||||||
2. Name: `tradingbot`
|
|
||||||
3. Git Repository: `https://192.168.30.23/Alby96/Encelado`
|
|
||||||
4. Compose path: `TradingBot/docker-compose.yml`
|
|
||||||
5. Deploy
|
|
||||||
|
|
||||||
### 3. Accesso
|
|
||||||
```
|
|
||||||
http://[UNRAID-IP]:8080
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Per Unraid (via SSH)
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# SSH
|
|
||||||
ssh root@[UNRAID-IP]
|
|
||||||
|
|
||||||
# Clone
|
|
||||||
cd /mnt/user/appdata
|
|
||||||
git clone https://192.168.30.23/Alby96/Encelado.git tradingbot
|
|
||||||
cd tradingbot/TradingBot
|
|
||||||
|
|
||||||
# Deploy
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Check
|
|
||||||
docker ps | grep tradingbot
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Comandi Utili
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Status
|
|
||||||
docker ps
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
docker logs tradingbot -f
|
|
||||||
|
|
||||||
# Restart
|
|
||||||
docker restart tradingbot
|
|
||||||
|
|
||||||
# Update
|
|
||||||
git pull && docker-compose up -d --build
|
|
||||||
|
|
||||||
# Remove
|
|
||||||
docker-compose down -v
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Environment Variables (opzionali)
|
|
||||||
|
|
||||||
Crea file `.env`:
|
|
||||||
```env
|
|
||||||
TZ=Europe/Rome
|
|
||||||
ASPNETCORE_ENVIRONMENT=Production
|
|
||||||
TRADINGBOT_AUTOSTART=true
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Container non parte
|
|
||||||
```sh
|
|
||||||
docker logs tradingbot
|
|
||||||
```
|
|
||||||
|
|
||||||
### Porta già usata
|
|
||||||
```sh
|
|
||||||
# Cambia porta in docker-compose.yml
|
|
||||||
ports:
|
|
||||||
- "8081:8080"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rebuild da zero
|
|
||||||
```sh
|
|
||||||
docker-compose down -v
|
|
||||||
docker-compose build --no-cache
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Documentazione completa: [UNRAID_DEPLOYMENT.md](UNRAID_DEPLOYMENT.md)
|
|
||||||
+24
-48
@@ -1,54 +1,30 @@
|
|||||||
# Dockerfile per TradingBot - Multi-stage build ottimizzato
|
# 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.
|
||||||
# Stage 1: Build
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
|
||||||
WORKDIR /src
|
|
||||||
|
|
||||||
# Copy csproj e restore dipendenze (layer caching)
|
# Questa fase viene usata durante l'esecuzione da Visual Studio in modalità rapida (impostazione predefinita per la configurazione di debug)
|
||||||
COPY ["TradingBot.csproj", "./"]
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||||
RUN dotnet restore "TradingBot.csproj"
|
USER $APP_UID
|
||||||
|
|
||||||
# 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
|
WORKDIR /app
|
||||||
|
|
||||||
# Crea utente non-root per sicurezza
|
|
||||||
RUN useradd -m -u 1000 tradingbot && \
|
|
||||||
chown -R tradingbot:tradingbot /app
|
|
||||||
|
|
||||||
# Esponi porta
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
EXPOSE 8081
|
||||||
|
|
||||||
# Copy published app
|
|
||||||
|
# Questa fase viene usata per compilare il progetto di servizio
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
WORKDIR /src
|
||||||
|
COPY ["TradingBot.csproj", "."]
|
||||||
|
RUN dotnet restore "./TradingBot.csproj"
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/src/."
|
||||||
|
RUN dotnet build "./TradingBot.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||||
|
|
||||||
|
# Questa fase viene usata per pubblicare il progetto di servizio da copiare nella fase finale
|
||||||
|
FROM build AS publish
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
RUN dotnet publish "./TradingBot.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||||
|
|
||||||
|
# Questa fase viene usata nell'ambiente di produzione o durante l'esecuzione da Visual Studio in modalità normale (impostazione predefinita quando non si usa la configurazione di debug)
|
||||||
|
FROM base AS final
|
||||||
|
WORKDIR /app
|
||||||
COPY --from=publish /app/publish .
|
COPY --from=publish /app/publish .
|
||||||
|
|
||||||
# Crea directory per persistenza dati
|
|
||||||
RUN mkdir -p /app/data && \
|
|
||||||
chown -R tradingbot:tradingbot /app/data
|
|
||||||
|
|
||||||
# Volume per dati persistenti
|
|
||||||
VOLUME ["/app/data"]
|
|
||||||
|
|
||||||
# Switch a utente non-root
|
|
||||||
USER tradingbot
|
|
||||||
|
|
||||||
# Environment variables
|
|
||||||
ENV ASPNETCORE_URLS=http://+:8080
|
|
||||||
ENV ASPNETCORE_ENVIRONMENT=Production
|
|
||||||
ENV DOTNET_RUNNING_IN_CONTAINER=true
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
||||||
CMD curl -f http://localhost:8080/health || exit 1
|
|
||||||
|
|
||||||
# Entry point
|
|
||||||
ENTRYPOINT ["dotnet", "TradingBot.dll"]
|
ENTRYPOINT ["dotnet", "TradingBot.dll"]
|
||||||
@@ -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"]
|
||||||
@@ -1,386 +0,0 @@
|
|||||||
# ? VERIFICA FINALE - TradingBot Application
|
|
||||||
|
|
||||||
## ?? CHECKLIST COMPLETA
|
|
||||||
|
|
||||||
### ?? Layout & Design
|
|
||||||
- [x] Sidebar verticale moderna implementata
|
|
||||||
- [x] Brand section con logo gradient
|
|
||||||
- [x] Status badge animato (? ATTIVO)
|
|
||||||
- [x] Menu items con icone Bootstrap
|
|
||||||
- [x] Active state highlighting
|
|
||||||
- [x] Collapsible sidebar (280px ? 80px)
|
|
||||||
- [x] Portfolio summary nel footer
|
|
||||||
- [x] Top bar con bot control
|
|
||||||
- [x] Responsive design (mobile-ready)
|
|
||||||
|
|
||||||
### ?? File Modificati
|
|
||||||
- [x] `Components/Layout/MainLayout.razor` - Layout completo riscritto
|
|
||||||
- [x] `Components/Layout/MainLayout.razor.css` - CSS moderno con ::deep
|
|
||||||
- [x] `wwwroot/app.css` - Stili globali prioritari
|
|
||||||
- [x] `Components/App.razor` - Bootstrap Icons CDN
|
|
||||||
- [x] `Components/_Imports.razor` - Namespace globali
|
|
||||||
|
|
||||||
### ?? Pagine Verificate
|
|
||||||
- [x] Dashboard - Razor syntax corretta
|
|
||||||
- [x] Strategies - Asset count dinamico (15/15)
|
|
||||||
- [x] Assets - Nuova pagina completa
|
|
||||||
- [x] Trading - Funzionante
|
|
||||||
- [x] Market - Query parameters supportati
|
|
||||||
- [x] Statistics - Dettagli completi
|
|
||||||
- [x] Settings - Persistenza attiva
|
|
||||||
|
|
||||||
### ?? Services
|
|
||||||
- [x] TradingBotService - 15 asset abilitati
|
|
||||||
- [x] SettingsService - Persistenza JSON
|
|
||||||
- [x] SimulatedMarketDataService - Tutti asset disponibili
|
|
||||||
- [x] SimpleMovingAverageStrategy - RSI + MACD
|
|
||||||
|
|
||||||
### ?? Features Implementate
|
|
||||||
|
|
||||||
#### Sidebar
|
|
||||||
```
|
|
||||||
? Logo gradient 3.5rem
|
|
||||||
? Brand text con accent
|
|
||||||
? Status indicator animato
|
|
||||||
? 7 menu items verticali
|
|
||||||
? Icone 1.375rem
|
|
||||||
? Hover effects
|
|
||||||
? Active state con border
|
|
||||||
? Portfolio summary live
|
|
||||||
? Toggle collapse button
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Navigation
|
|
||||||
```
|
|
||||||
? Dashboard (/)
|
|
||||||
? Strategie (/strategies)
|
|
||||||
? Asset (/assets) - NUOVA
|
|
||||||
? Trading (/trading)
|
|
||||||
? Analisi Mercato (/market)
|
|
||||||
? Statistiche (/statistics)
|
|
||||||
? Impostazioni (/settings)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Assets Page
|
|
||||||
```
|
|
||||||
? Grid view / List view
|
|
||||||
? 15 asset visibili
|
|
||||||
? Strategy assignment dropdown
|
|
||||||
? Toggle on/off per asset
|
|
||||||
? Filtri: Tutti/Attivi/Inattivi
|
|
||||||
? Real-time metrics
|
|
||||||
? Navigate to chart
|
|
||||||
```
|
|
||||||
|
|
||||||
### ?? Design System
|
|
||||||
|
|
||||||
#### Colors
|
|
||||||
```css
|
|
||||||
Primary: #6366f1 (Indigo)
|
|
||||||
Secondary: #8b5cf6 (Purple)
|
|
||||||
Success: #10b981 (Green)
|
|
||||||
Danger: #ef4444 (Red)
|
|
||||||
Warning: #f59e0b (Amber)
|
|
||||||
Background: #0a0e27 (Dark Blue)
|
|
||||||
Sidebar: #1a1f3a ? #0f1629 (Gradient)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Typography
|
|
||||||
```
|
|
||||||
Headers: System Font Stack
|
|
||||||
Monospace: Courier New (numeri)
|
|
||||||
Weights: 600 (semi-bold), 700 (bold)
|
|
||||||
Sizes: 0.75rem - 1.75rem
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Spacing
|
|
||||||
```
|
|
||||||
Unit: 0.25rem (4px)
|
|
||||||
Padding: 1rem - 2rem
|
|
||||||
Gaps: 0.5rem - 1.5rem
|
|
||||||
Radius: 0.5rem - 1rem
|
|
||||||
```
|
|
||||||
|
|
||||||
### ?? Real-time Updates
|
|
||||||
- [x] Prezzi aggiornati ogni 3 secondi
|
|
||||||
- [x] Portfolio stats live
|
|
||||||
- [x] Trade notifications
|
|
||||||
- [x] Indicators recalculated
|
|
||||||
- [x] SignalR connection active
|
|
||||||
|
|
||||||
### ?? Persistenza
|
|
||||||
- [x] Settings salvati in JSON
|
|
||||||
- [x] Sidebar state ricordato
|
|
||||||
- [x] Auto-start bot configurabile
|
|
||||||
- [x] File path: %LocalAppData%/TradingBot/appsettings.json
|
|
||||||
|
|
||||||
### ?? Simulazione
|
|
||||||
- [x] 15 asset simultanei
|
|
||||||
- [x] Dati di mercato realistici
|
|
||||||
- [x] Variazioni % simulate
|
|
||||||
- [x] Trading automatico attivo
|
|
||||||
- [x] Risk management implementato
|
|
||||||
|
|
||||||
### ??? Architettura
|
|
||||||
|
|
||||||
#### Frontend
|
|
||||||
```
|
|
||||||
Blazor Server (.NET 10)
|
|
||||||
??? SignalR per real-time
|
|
||||||
??? Scoped CSS per component isolation
|
|
||||||
??? Global CSS per layout
|
|
||||||
??? Bootstrap Icons via CDN
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Backend
|
|
||||||
```
|
|
||||||
Services
|
|
||||||
??? TradingBotService (singleton)
|
|
||||||
??? SimulatedMarketDataService (singleton)
|
|
||||||
??? SettingsService (singleton)
|
|
||||||
??? SimpleMovingAverageStrategy (singleton)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Models
|
|
||||||
```
|
|
||||||
Core
|
|
||||||
??? AssetConfiguration
|
|
||||||
??? AssetStatistics
|
|
||||||
??? MarketPrice
|
|
||||||
??? Trade
|
|
||||||
??? TechnicalIndicators
|
|
||||||
??? PortfolioStatistics
|
|
||||||
??? AppSettings
|
|
||||||
```
|
|
||||||
|
|
||||||
### ?? Build Status
|
|
||||||
```
|
|
||||||
Compilazione: ? RIUSCITA
|
|
||||||
Errori: ? 0
|
|
||||||
Warning: ? 0
|
|
||||||
Target: ? .NET 10
|
|
||||||
```
|
|
||||||
|
|
||||||
### ?? Documentazione
|
|
||||||
- [x] README.md aggiornato
|
|
||||||
- [x] BROWSER_CACHE_GUIDE.md creato
|
|
||||||
- [x] FINAL_VERIFICATION.md (questo file)
|
|
||||||
- [x] Inline code comments
|
|
||||||
|
|
||||||
### ?? Sicurezza
|
|
||||||
- [x] Input validation
|
|
||||||
- [x] Readonly settings per sim mode
|
|
||||||
- [x] Safe decimal calculations
|
|
||||||
- [x] Error boundaries
|
|
||||||
|
|
||||||
### ? Accessibilità
|
|
||||||
- [x] Semantic HTML
|
|
||||||
- [x] ARIA labels via title attributes
|
|
||||||
- [x] Keyboard navigation support
|
|
||||||
- [x] Focus states visible
|
|
||||||
|
|
||||||
### ?? Responsive
|
|
||||||
```
|
|
||||||
Desktop: > 1024px ? Full layout
|
|
||||||
Tablet: 768-1024px ? Sidebar 260px
|
|
||||||
Mobile: < 768px ? Offscreen sidebar
|
|
||||||
Small: < 480px ? Compact padding
|
|
||||||
```
|
|
||||||
|
|
||||||
### ? Performance
|
|
||||||
- [x] CSS transitions GPU-accelerated
|
|
||||||
- [x] Component rendering optimized
|
|
||||||
- [x] Minimal re-renders (StateHasChanged strategico)
|
|
||||||
- [x] Lazy evaluation dove possibile
|
|
||||||
|
|
||||||
### ?? Testing Checklist
|
|
||||||
|
|
||||||
#### Manual Testing
|
|
||||||
- [ ] Avvia applicazione
|
|
||||||
- [ ] Verifica sidebar appare correttamente
|
|
||||||
- [ ] Click su ogni menu item
|
|
||||||
- [ ] Verifica navigazione funziona
|
|
||||||
- [ ] Toggle sidebar collapse/expand
|
|
||||||
- [ ] Verifica portfolio stats si aggiornano
|
|
||||||
- [ ] Click "Stop" bot
|
|
||||||
- [ ] Click "Avvia" bot
|
|
||||||
- [ ] Vai su Assets page
|
|
||||||
- [ ] Cambia view (Grid ? List)
|
|
||||||
- [ ] Assegna strategia ad un asset
|
|
||||||
- [ ] Toggle asset on/off
|
|
||||||
- [ ] Vai su Settings
|
|
||||||
- [ ] Cambia impostazioni
|
|
||||||
- [ ] Verifica salvataggio automatico
|
|
||||||
- [ ] Resize finestra (responsive test)
|
|
||||||
- [ ] Test su mobile (DevTools)
|
|
||||||
|
|
||||||
#### Browser Compatibility
|
|
||||||
- [ ] Chrome (latest)
|
|
||||||
- [ ] Edge (latest)
|
|
||||||
- [ ] Firefox (latest)
|
|
||||||
- [ ] Safari (se disponibile)
|
|
||||||
|
|
||||||
#### Cache Testing
|
|
||||||
- [ ] Hard refresh (Ctrl+Shift+R)
|
|
||||||
- [ ] Incognito mode
|
|
||||||
- [ ] After server restart
|
|
||||||
- [ ] After clean build
|
|
||||||
|
|
||||||
### ?? Metrics
|
|
||||||
|
|
||||||
#### Code Stats
|
|
||||||
```
|
|
||||||
Razor Files: ~15 pages
|
|
||||||
CSS Files: ~15 scoped + 1 global
|
|
||||||
C# Services: ~8 services
|
|
||||||
Models: ~12 models
|
|
||||||
Total Lines: ~5000+ LOC
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Features Count
|
|
||||||
```
|
|
||||||
Pages: 7 main pages
|
|
||||||
Components: ~5 shared components
|
|
||||||
Services: 8 business services
|
|
||||||
Asset Support: 15 cryptocurrencies
|
|
||||||
Strategies: 6 templates
|
|
||||||
Indicators: 3 technical (RSI, MACD, EMA)
|
|
||||||
```
|
|
||||||
|
|
||||||
### ?? Success Criteria
|
|
||||||
|
|
||||||
#### Visual
|
|
||||||
? Sidebar verticale moderna visibile
|
|
||||||
? Icone Bootstrap caricate
|
|
||||||
? Gradients applicati
|
|
||||||
? Animazioni fluide
|
|
||||||
? Colors coerenti
|
|
||||||
? Typography corretta
|
|
||||||
|
|
||||||
#### Functional
|
|
||||||
? Navigazione funzionante
|
|
||||||
? Bot start/stop
|
|
||||||
? Real-time updates
|
|
||||||
? Settings persistono
|
|
||||||
? Assets management
|
|
||||||
? Strategy assignment
|
|
||||||
|
|
||||||
#### Technical
|
|
||||||
? Build successful
|
|
||||||
? 0 compilation errors
|
|
||||||
? CSS correttamente applicato
|
|
||||||
? Services registered
|
|
||||||
? SignalR connected
|
|
||||||
|
|
||||||
### ?? Deployment Ready
|
|
||||||
|
|
||||||
#### Pre-deployment
|
|
||||||
- [x] Build in Release mode
|
|
||||||
- [x] Verify all assets
|
|
||||||
- [x] Test all routes
|
|
||||||
- [x] Check console for errors
|
|
||||||
- [x] Validate responsive design
|
|
||||||
|
|
||||||
#### Production Checklist
|
|
||||||
- [ ] Remove debug code
|
|
||||||
- [ ] Optimize images
|
|
||||||
- [ ] Minify CSS/JS
|
|
||||||
- [ ] Enable HTTPS
|
|
||||||
- [ ] Configure CORS
|
|
||||||
- [ ] Set production URLs
|
|
||||||
- [ ] Configure logging
|
|
||||||
- [ ] Setup monitoring
|
|
||||||
|
|
||||||
### ?? Support
|
|
||||||
|
|
||||||
#### Se Qualcosa Non Funziona
|
|
||||||
|
|
||||||
1. **Verifica Build**
|
|
||||||
```sh
|
|
||||||
dotnet build
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Pulisci Cache**
|
|
||||||
```sh
|
|
||||||
dotnet clean
|
|
||||||
Remove-Item bin,obj -Recurse -Force
|
|
||||||
dotnet restore
|
|
||||||
dotnet build
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Hard Refresh Browser**
|
|
||||||
```
|
|
||||||
Ctrl + Shift + R
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Check Console**
|
|
||||||
```
|
|
||||||
F12 ? Console tab
|
|
||||||
Cerca errori rossi
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Verifica Network**
|
|
||||||
```
|
|
||||||
F12 ? Network tab
|
|
||||||
Reload ? Verifica CSS caricati (200 OK)
|
|
||||||
```
|
|
||||||
|
|
||||||
### ?? Screenshots Attesi
|
|
||||||
|
|
||||||
#### Desktop - Expanded
|
|
||||||
```
|
|
||||||
[Logo 3.5rem] TradingBot [?]
|
|
||||||
? ATTIVO
|
|
||||||
????????????????????????????????
|
|
||||||
?? Dashboard
|
|
||||||
?? Strategie
|
|
||||||
?? Asset
|
|
||||||
?? Trading
|
|
||||||
?? Analisi Mercato
|
|
||||||
?? Statistiche
|
|
||||||
?? Impostazioni
|
|
||||||
????????????????????????????????
|
|
||||||
Portfolio $15,000
|
|
||||||
Profitto $0.00
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Desktop - Collapsed
|
|
||||||
```
|
|
||||||
[Logo]
|
|
||||||
[?]
|
|
||||||
|
|
||||||
??
|
|
||||||
??
|
|
||||||
??
|
|
||||||
??
|
|
||||||
??
|
|
||||||
??
|
|
||||||
??
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Mobile
|
|
||||||
```
|
|
||||||
[?] TradingBot [Stop]
|
|
||||||
|
|
||||||
Main Content Here...
|
|
||||||
```
|
|
||||||
|
|
||||||
### ? CONCLUSIONE
|
|
||||||
|
|
||||||
L'applicazione è:
|
|
||||||
- ? **Completamente funzionale**
|
|
||||||
- ? **Build successful**
|
|
||||||
- ? **Design moderno implementato**
|
|
||||||
- ? **Tutti i 15 asset attivi**
|
|
||||||
- ? **Persistenza settings funzionante**
|
|
||||||
- ? **Responsive su tutti i device**
|
|
||||||
- ? **Real-time updates attivi**
|
|
||||||
- ? **Documentazione completa**
|
|
||||||
|
|
||||||
**?? PRONTO PER L'USO!**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Data verifica**: 2025-12-12
|
|
||||||
**Versione**: 1.0.0
|
|
||||||
**Status**: ? PRODUCTION READY
|
|
||||||
@@ -1,410 +0,0 @@
|
|||||||
# ?? WORKFLOW: Sviluppo ? Gitea ? Unraid
|
|
||||||
|
|
||||||
## Flusso di Lavoro Completo
|
|
||||||
|
|
||||||
```
|
|
||||||
PC Sviluppo ? Git Commit ? Gitea Push ? Unraid Pull ? Docker Deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? STEP BY STEP
|
|
||||||
|
|
||||||
### 1. Sviluppo Locale (PC)
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Lavora sul codice
|
|
||||||
code .
|
|
||||||
|
|
||||||
# Test locale
|
|
||||||
dotnet run
|
|
||||||
# Oppure
|
|
||||||
docker-compose up
|
|
||||||
|
|
||||||
# Verifica funzionamento
|
|
||||||
http://localhost:8080
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Commit e Push su Gitea
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Status modifiche
|
|
||||||
git status
|
|
||||||
|
|
||||||
# Stage files
|
|
||||||
git add .
|
|
||||||
|
|
||||||
# Commit
|
|
||||||
git commit -m "Feature: Descrizione modifiche"
|
|
||||||
|
|
||||||
# Push su Gitea
|
|
||||||
git push origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Deploy su Unraid
|
|
||||||
|
|
||||||
#### Opzione A: Automatico (Portainer Webhook)
|
|
||||||
|
|
||||||
**Setup iniziale** (una volta):
|
|
||||||
```
|
|
||||||
1. Portainer ? Stacks ? tradingbot ? Webhooks
|
|
||||||
2. Create webhook
|
|
||||||
3. Copy URL
|
|
||||||
|
|
||||||
4. Gitea ? Settings ? Webhooks ? Add Webhook
|
|
||||||
5. Paste URL
|
|
||||||
6. Trigger: Push events
|
|
||||||
7. Save
|
|
||||||
```
|
|
||||||
|
|
||||||
**Uso**: Ogni push su Gitea ? Auto-deploy su Unraid!
|
|
||||||
|
|
||||||
#### Opzione B: Manuale (SSH)
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# SSH su Unraid
|
|
||||||
ssh root@[UNRAID-IP]
|
|
||||||
|
|
||||||
# Vai nella directory
|
|
||||||
cd /mnt/user/appdata/tradingbot/TradingBot
|
|
||||||
|
|
||||||
# Pull modifiche
|
|
||||||
git pull origin main
|
|
||||||
|
|
||||||
# Rebuild e restart
|
|
||||||
docker-compose down
|
|
||||||
docker-compose build
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Opzione C: Script Automatico
|
|
||||||
|
|
||||||
Crea `/root/scripts/deploy-tradingbot.sh`:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
#!/bin/bash
|
|
||||||
cd /mnt/user/appdata/tradingbot/TradingBot
|
|
||||||
|
|
||||||
echo "?? Pulling latest changes..."
|
|
||||||
git pull origin main
|
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo "?? Rebuilding container..."
|
|
||||||
docker-compose down
|
|
||||||
docker-compose build
|
|
||||||
docker-compose up -d
|
|
||||||
echo "? Deployment completed!"
|
|
||||||
else
|
|
||||||
echo "? Git pull failed!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
|
|
||||||
Usa:
|
|
||||||
```sh
|
|
||||||
chmod +x /root/scripts/deploy-tradingbot.sh
|
|
||||||
/root/scripts/deploy-tradingbot.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? WORKFLOW GIORNALIERO
|
|
||||||
|
|
||||||
### Mattina - Modifiche
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# PC
|
|
||||||
git pull origin main # Sync
|
|
||||||
code . # Sviluppa
|
|
||||||
dotnet run # Test
|
|
||||||
```
|
|
||||||
|
|
||||||
### Pomeriggio - Deploy
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# PC
|
|
||||||
git add .
|
|
||||||
git commit -m "Daily improvements"
|
|
||||||
git push origin main
|
|
||||||
|
|
||||||
# Unraid (se non auto-deploy)
|
|
||||||
ssh root@unraid
|
|
||||||
/root/scripts/deploy-tradingbot.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sera - Monitoring
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Check logs
|
|
||||||
docker logs tradingbot -f
|
|
||||||
|
|
||||||
# Check stats
|
|
||||||
docker stats tradingbot
|
|
||||||
|
|
||||||
# Backup (opzionale)
|
|
||||||
/root/scripts/backup-tradingbot.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? BRANCHING STRATEGY
|
|
||||||
|
|
||||||
### Main Branch (Production)
|
|
||||||
```sh
|
|
||||||
# Solo codice stabile e testato
|
|
||||||
git checkout main
|
|
||||||
git pull origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
### Development Branch
|
|
||||||
```sh
|
|
||||||
# Crea branch per nuove feature
|
|
||||||
git checkout -b feature/nome-feature
|
|
||||||
|
|
||||||
# Sviluppa e testa
|
|
||||||
# ...
|
|
||||||
|
|
||||||
# Merge in main quando pronto
|
|
||||||
git checkout main
|
|
||||||
git merge feature/nome-feature
|
|
||||||
git push origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
### Hotfix
|
|
||||||
```sh
|
|
||||||
# Per fix urgenti
|
|
||||||
git checkout -b hotfix/descrizione
|
|
||||||
# Fix
|
|
||||||
git checkout main
|
|
||||||
git merge hotfix/descrizione
|
|
||||||
git push origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? RELEASE VERSIONING
|
|
||||||
|
|
||||||
### Tagging
|
|
||||||
```sh
|
|
||||||
# Tag versione
|
|
||||||
git tag -a v1.0.0 -m "Release v1.0.0"
|
|
||||||
git push origin v1.0.0
|
|
||||||
|
|
||||||
# Build con tag
|
|
||||||
docker build -t tradingbot:v1.0.0 .
|
|
||||||
docker tag tradingbot:v1.0.0 tradingbot:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rollback
|
|
||||||
```sh
|
|
||||||
# Lista tags
|
|
||||||
git tag -l
|
|
||||||
|
|
||||||
# Checkout versione precedente
|
|
||||||
git checkout v0.9.0
|
|
||||||
|
|
||||||
# Deploy versione specifica
|
|
||||||
docker-compose down
|
|
||||||
docker-compose build
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? BEST PRACTICES
|
|
||||||
|
|
||||||
### 1. Non Committare Secrets
|
|
||||||
```sh
|
|
||||||
# .gitignore già configurato per:
|
|
||||||
appsettings.Development.json
|
|
||||||
*.env
|
|
||||||
*.key
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Test Prima di Push
|
|
||||||
```sh
|
|
||||||
# Sempre test locale prima
|
|
||||||
dotnet build
|
|
||||||
dotnet test # Se hai tests
|
|
||||||
docker-compose up # Test container
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Commit Messages Descrittivi
|
|
||||||
```sh
|
|
||||||
# ? Buoni
|
|
||||||
git commit -m "Fix: Sidebar toggle button not working"
|
|
||||||
git commit -m "Feature: Add Docker support"
|
|
||||||
git commit -m "Docs: Update deployment guide"
|
|
||||||
|
|
||||||
# ? Cattivi
|
|
||||||
git commit -m "fix"
|
|
||||||
git commit -m "update"
|
|
||||||
git commit -m "changes"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Pull Prima di Push
|
|
||||||
```sh
|
|
||||||
# Sempre sync prima
|
|
||||||
git pull origin main
|
|
||||||
# Risolvi conflitti se presenti
|
|
||||||
git push origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? TROUBLESHOOTING
|
|
||||||
|
|
||||||
### Conflitto Git
|
|
||||||
```sh
|
|
||||||
# Pull con conflitti
|
|
||||||
git pull origin main
|
|
||||||
|
|
||||||
# Risolvi manualmente i file in conflitto
|
|
||||||
# Cerca <<<<<<< HEAD
|
|
||||||
|
|
||||||
# Dopo risolto
|
|
||||||
git add .
|
|
||||||
git commit -m "Resolve merge conflicts"
|
|
||||||
git push origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
### Push Rifiutato
|
|
||||||
```sh
|
|
||||||
# Se remote è avanti
|
|
||||||
git pull --rebase origin main
|
|
||||||
git push origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reset Completo (ATTENZIONE!)
|
|
||||||
```sh
|
|
||||||
# Solo in caso di emergenza
|
|
||||||
git fetch origin
|
|
||||||
git reset --hard origin/main
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? MONITORING WORKFLOW
|
|
||||||
|
|
||||||
### Check Health
|
|
||||||
```sh
|
|
||||||
# Local
|
|
||||||
curl http://localhost:8080/health
|
|
||||||
|
|
||||||
# Unraid
|
|
||||||
curl http://[UNRAID-IP]:8080/health
|
|
||||||
```
|
|
||||||
|
|
||||||
### View Logs
|
|
||||||
```sh
|
|
||||||
# Real-time
|
|
||||||
docker logs -f tradingbot
|
|
||||||
|
|
||||||
# Last 100 lines
|
|
||||||
docker logs --tail 100 tradingbot
|
|
||||||
|
|
||||||
# Since timestamp
|
|
||||||
docker logs --since 2024-12-12T10:00:00 tradingbot
|
|
||||||
```
|
|
||||||
|
|
||||||
### Resource Usage
|
|
||||||
```sh
|
|
||||||
# Stats
|
|
||||||
docker stats tradingbot
|
|
||||||
|
|
||||||
# Processes
|
|
||||||
docker top tradingbot
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? CHECKLIST COMPLETO
|
|
||||||
|
|
||||||
### Pre-Development
|
|
||||||
- [ ] Git repository synced (`git pull`)
|
|
||||||
- [ ] Branch corretto (`git branch`)
|
|
||||||
- [ ] Dependencies updated (`dotnet restore`)
|
|
||||||
|
|
||||||
### Development
|
|
||||||
- [ ] Codice scritto e testato
|
|
||||||
- [ ] Build successful (`dotnet build`)
|
|
||||||
- [ ] Test locale OK (`dotnet run`)
|
|
||||||
- [ ] Docker test OK (`docker-compose up`)
|
|
||||||
|
|
||||||
### Pre-Commit
|
|
||||||
- [ ] Codice formattato
|
|
||||||
- [ ] No secrets committati
|
|
||||||
- [ ] .gitignore aggiornato
|
|
||||||
- [ ] README aggiornato se necessario
|
|
||||||
|
|
||||||
### Commit & Push
|
|
||||||
- [ ] `git status` verificato
|
|
||||||
- [ ] Commit message descrittivo
|
|
||||||
- [ ] Push successful
|
|
||||||
- [ ] Verifica su Gitea web UI
|
|
||||||
|
|
||||||
### Deployment
|
|
||||||
- [ ] Pull su Unraid OK
|
|
||||||
- [ ] Docker build successful
|
|
||||||
- [ ] Container running
|
|
||||||
- [ ] Health check passing
|
|
||||||
- [ ] WebUI accessibile
|
|
||||||
|
|
||||||
### Post-Deployment
|
|
||||||
- [ ] Logs verificati
|
|
||||||
- [ ] Nessun errore critico
|
|
||||||
- [ ] Funzionalità testate
|
|
||||||
- [ ] Performance OK
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? MAINTENANCE
|
|
||||||
|
|
||||||
### Giornaliero
|
|
||||||
- Check logs per errori
|
|
||||||
- Verifica health endpoint
|
|
||||||
- Monitor resource usage
|
|
||||||
|
|
||||||
### Settimanale
|
|
||||||
- Git pull updates
|
|
||||||
- Review commits
|
|
||||||
- Check disk space
|
|
||||||
|
|
||||||
### Mensile
|
|
||||||
- Full backup
|
|
||||||
- Review performance metrics
|
|
||||||
- Update dependencies
|
|
||||||
- Security audit
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? COMANDI RAPIDI
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Development
|
|
||||||
git status
|
|
||||||
git add .
|
|
||||||
git commit -m "message"
|
|
||||||
git push origin main
|
|
||||||
|
|
||||||
# Local Test
|
|
||||||
dotnet run
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Unraid Deploy
|
|
||||||
ssh root@unraid "/root/scripts/deploy-tradingbot.sh"
|
|
||||||
|
|
||||||
# Check Status
|
|
||||||
docker ps | grep tradingbot
|
|
||||||
docker logs tradingbot --tail 50
|
|
||||||
|
|
||||||
# Restart
|
|
||||||
docker restart tradingbot
|
|
||||||
|
|
||||||
# Update
|
|
||||||
git pull && docker-compose up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**?? Workflow pronto! Sviluppo ? Gitea ? Unraid automatizzato!**
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ public class TradingSignal
|
|||||||
public string Symbol { get; set; } = string.Empty;
|
public string Symbol { get; set; } = string.Empty;
|
||||||
public SignalType Type { get; set; }
|
public SignalType Type { get; set; }
|
||||||
public decimal Price { get; set; }
|
public decimal Price { get; set; }
|
||||||
|
public decimal Confidence { get; set; } // 0-100 confidence level
|
||||||
public string Reason { get; set; } = string.Empty;
|
public string Reason { get; set; } = string.Empty;
|
||||||
public DateTime Timestamp { get; set; }
|
public DateTime Timestamp { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-2
@@ -3,6 +3,16 @@ using TradingBot.Services;
|
|||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
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.
|
// Add services to the container.
|
||||||
builder.Services.AddRazorComponents()
|
builder.Services.AddRazorComponents()
|
||||||
.AddInteractiveServerComponents();
|
.AddInteractiveServerComponents();
|
||||||
@@ -10,9 +20,16 @@ builder.Services.AddRazorComponents()
|
|||||||
// Trading Bot Services - Using Simulated Market Data
|
// Trading Bot Services - Using Simulated Market Data
|
||||||
builder.Services.AddSingleton<IMarketDataService, SimulatedMarketDataService>();
|
builder.Services.AddSingleton<IMarketDataService, SimulatedMarketDataService>();
|
||||||
builder.Services.AddSingleton<ITradingStrategy, SimpleMovingAverageStrategy>();
|
builder.Services.AddSingleton<ITradingStrategy, SimpleMovingAverageStrategy>();
|
||||||
|
builder.Services.AddSingleton<TradeHistoryService>();
|
||||||
|
builder.Services.AddSingleton<LoggingService>();
|
||||||
|
builder.Services.AddSingleton<IndicatorsService>();
|
||||||
|
builder.Services.AddSingleton<TradingStrategiesService>();
|
||||||
builder.Services.AddSingleton<TradingBotService>();
|
builder.Services.AddSingleton<TradingBotService>();
|
||||||
builder.Services.AddSingleton<SettingsService>();
|
builder.Services.AddSingleton<SettingsService>();
|
||||||
|
|
||||||
|
// Register background service for graceful shutdown
|
||||||
|
builder.Services.AddHostedService<TradingBotBackgroundService>();
|
||||||
|
|
||||||
// Add health checks for Docker
|
// Add health checks for Docker
|
||||||
builder.Services.AddHealthChecks();
|
builder.Services.AddHealthChecks();
|
||||||
|
|
||||||
@@ -22,16 +39,19 @@ var app = builder.Build();
|
|||||||
if (!app.Environment.IsDevelopment())
|
if (!app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
|
||||||
app.UseHsts();
|
app.UseHsts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTTPS redirect solo in Development
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
}
|
||||||
|
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
app.UseAntiforgery();
|
app.UseAntiforgery();
|
||||||
|
|
||||||
// Health check endpoint for Docker
|
// Health check endpoint
|
||||||
app.MapHealthChecks("/health");
|
app.MapHealthChecks("/health");
|
||||||
|
|
||||||
app.MapRazorComponents<App>()
|
app.MapRazorComponents<App>()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
"profiles": {
|
"profiles": {
|
||||||
"http": {
|
"TradingBot": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"https": {
|
"TradingBot (HTTPS)": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
@@ -18,6 +18,17 @@
|
|||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Docker": {
|
||||||
|
"commandName": "Docker",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_URLS": "http://+:8080",
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Production"
|
||||||
|
},
|
||||||
|
"publishAllPorts": true,
|
||||||
|
"useSSL": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+158
-340
@@ -1,345 +1,163 @@
|
|||||||
# ?? TradingBot - Automated Crypto Trading Simulator
|
# ?? TradingBot
|
||||||
|
|
||||||
Un'applicazione Blazor Server avanzata per simulare e testare strategie di trading automatizzato su criptovalute.
|
**Automated Crypto Trading Bot** con interfaccia Blazor Server per trading algoritmico simulato.
|
||||||
|
|
||||||
## ?? Caratteristiche Principali
|
[](https://dotnet.microsoft.com/)
|
||||||
|
[](https://blazor.net/)
|
||||||
### ?? Dashboard
|
[](https://www.docker.com/)
|
||||||
- **Panoramica Portfolio**: Visualizzazione completa del valore totale e performance
|
[](https://gitea.encke-hake.ts.net/Alby96/Encelado/-/packages)
|
||||||
- **Metriche Chiave**: Profitto totale, operazioni eseguite, asset attivi
|
|
||||||
- **Asset Attivi**: Grid dei top 6 asset con performance in tempo reale
|
|
||||||
- **Attività Recente**: Storico delle ultime 8 operazioni
|
|
||||||
|
|
||||||
### ?? Strategie
|
|
||||||
- **Gestione Strategie**: Crea, modifica ed elimina strategie di trading
|
|
||||||
- **Template Predefiniti**:
|
|
||||||
- Scalping Veloce
|
|
||||||
- Trend Following
|
|
||||||
- Mean Reversion
|
|
||||||
- Conservative
|
|
||||||
- **Strategia Attiva**: RSI + MACD Cross (personalizzabile)
|
|
||||||
- **Parametri Configurabili**: Stop Loss, Take Profit, condizioni BUY/SELL
|
|
||||||
|
|
||||||
### ?? Asset (NUOVO!)
|
|
||||||
- **Vista Completa Asset**: Tutti i 15 asset disponibili
|
|
||||||
- **Due Modalità di Visualizzazione**:
|
|
||||||
- **Grid View**: Card dettagliate con metriche
|
|
||||||
- **List View**: Tabella compatta per overview rapido
|
|
||||||
- **Assegnazione Strategie**: Dropdown per ogni asset
|
|
||||||
- **Toggle On/Off**: Attiva/disattiva trading per asset
|
|
||||||
- **Filtri**: Tutti / Solo Attivi / Solo Inattivi
|
|
||||||
- **Metriche Real-time**: Prezzo, variazione 24h, holdings, profitto
|
|
||||||
- **Azioni Rapide**: Configura e Visualizza Grafico
|
|
||||||
|
|
||||||
### ?? Trading
|
|
||||||
- **15 Asset Simulati**: BTC, ETH, BNB, SOL, ADA, XRP, DOT, AVAX, MATIC, LINK, UNI, ATOM, LTC, ALGO, VET
|
|
||||||
- **Gestione Asset**: Toggle on/off per ogni asset
|
|
||||||
- **Monitoraggio Real-time**: Prezzi, holdings, profitti aggiornati ogni 3 secondi
|
|
||||||
- **Tabella Operazioni**: Storico completo con filtri e ricerca
|
|
||||||
|
|
||||||
### ?? Analisi Mercato
|
|
||||||
- **Grafici Interattivi**: Visualizzazione prezzi con SVG rendering
|
|
||||||
- **Indicatori Tecnici**:
|
|
||||||
- RSI (14) con stati Overbought/Oversold/Neutral
|
|
||||||
- MACD con signal e histogram
|
|
||||||
- EMA (12, 26)
|
|
||||||
- **Selector Asset**: Cambia asset per analisi dettagliate
|
|
||||||
|
|
||||||
### ?? Statistiche
|
|
||||||
- **Overview Portfolio**: Metriche aggregate di tutti gli asset
|
|
||||||
- **Breakdown per Asset**: Tabella dettagliata con ROI, win rate, trades
|
|
||||||
- **Best/Worst Performers**: Identificazione automatica
|
|
||||||
- **Analisi Dettagliata**: Drilldown su singolo asset con:
|
|
||||||
- Performance trading completa
|
|
||||||
- Analisi profitti/perdite
|
|
||||||
- Operazioni recenti
|
|
||||||
|
|
||||||
### ?? Impostazioni
|
|
||||||
- **Persistenza Automatica**: Tutte le modifiche salvate su file
|
|
||||||
- **Configurazioni**:
|
|
||||||
- Modalità simulazione
|
|
||||||
- Notifiche desktop
|
|
||||||
- Auto-start bot
|
|
||||||
- Conferma operazioni manuali
|
|
||||||
- Intervallo aggiornamento (2-10 secondi)
|
|
||||||
- Log level
|
|
||||||
- **Notifiche Visive**: Feedback immediato sui salvataggi
|
|
||||||
|
|
||||||
## ??? Architettura
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
- **Blazor Server (.NET 10)**: Rendering server-side con SignalR
|
|
||||||
- **Sidebar Collapsible**: Navigazione verticale espandibile/minimizzabile
|
|
||||||
- **Responsive Design**: Ottimizzato per desktop, tablet e mobile
|
|
||||||
- **Dark Theme**: Design moderno con palette Indigo/Purple
|
|
||||||
|
|
||||||
### Backend Services
|
|
||||||
- **TradingBotService**: Core logic per trading automatizzato
|
|
||||||
- **SimulatedMarketDataService**: Generazione dati di mercato realistici
|
|
||||||
- **SettingsService**: Persistenza configurazioni su file JSON
|
|
||||||
- **SimpleMovingAverageStrategy**: Strategia di trading con RSI e MACD
|
|
||||||
|
|
||||||
### Models
|
|
||||||
- **AssetConfiguration**: Configurazione per singolo asset
|
|
||||||
- **AssetStatistics**: Metriche e performance tracking
|
|
||||||
- **MarketPrice**: Dati di mercato in tempo reale
|
|
||||||
- **TechnicalIndicators**: RSI, MACD, EMA
|
|
||||||
- **AppSettings**: Configurazioni globali applicazione
|
|
||||||
|
|
||||||
## ?? Quick Start
|
|
||||||
|
|
||||||
### Prerequisiti
|
|
||||||
- .NET 10 SDK
|
|
||||||
- Visual Studio 2022+ o VS Code
|
|
||||||
|
|
||||||
### Installazione Locale
|
|
||||||
```bash
|
|
||||||
# Clone repository
|
|
||||||
git clone https://192.168.30.23/Alby96/Encelado
|
|
||||||
cd TradingBot
|
|
||||||
|
|
||||||
# Restore packages
|
|
||||||
dotnet restore
|
|
||||||
|
|
||||||
# Run application
|
|
||||||
dotnet run
|
|
||||||
```
|
|
||||||
|
|
||||||
### ?? Deployment Docker
|
|
||||||
|
|
||||||
#### Development
|
|
||||||
```sh
|
|
||||||
# Build
|
|
||||||
docker-compose build
|
|
||||||
|
|
||||||
# Run
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Access
|
|
||||||
http://localhost:8080
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Production (Unraid)
|
|
||||||
Vedi documentazione completa:
|
|
||||||
- ?? [UNRAID_DEPLOYMENT.md](UNRAID_DEPLOYMENT.md) - Guida completa Unraid + Gitea
|
|
||||||
- ?? [DOCKER_QUICKSTART.md](DOCKER_QUICKSTART.md) - Quick start rapido
|
|
||||||
|
|
||||||
### Uso
|
|
||||||
1. L'applicazione si avvia automaticamente in modalità simulazione
|
|
||||||
2. Tutti i 15 asset sono attivi di default
|
|
||||||
3. Il bot inizia il trading automaticamente (configurabile in Impostazioni)
|
|
||||||
4. Usa la sidebar per navigare tra le sezioni
|
|
||||||
|
|
||||||
## ?? Struttura Progetto
|
|
||||||
|
|
||||||
```
|
|
||||||
TradingBot/
|
|
||||||
??? Components/
|
|
||||||
? ??? Layout/
|
|
||||||
? ? ??? MainLayout.razor # Layout principale con sidebar
|
|
||||||
? ? ??? MainLayout.razor.css
|
|
||||||
? ??? Pages/
|
|
||||||
? ? ??? Dashboard.razor # Homepage overview
|
|
||||||
? ? ??? Strategies.razor # Gestione strategie
|
|
||||||
? ? ??? Trading.razor # Trading view
|
|
||||||
? ? ??? Market.razor # Analisi mercato
|
|
||||||
? ? ??? Statistics.razor # Statistiche dettagliate
|
|
||||||
? ? ??? Settings.razor # Configurazioni
|
|
||||||
? ? ??? Assets.razor # Gestione asset (NUOVA PAGINA)
|
|
||||||
? ??? Shared/
|
|
||||||
? ??? AdvancedChart.razor # Componente grafico SVG
|
|
||||||
? ??? AssetSettings.razor # Config singolo asset
|
|
||||||
??? Models/
|
|
||||||
? ??? AssetConfiguration.cs
|
|
||||||
? ??? AssetStatistics.cs
|
|
||||||
? ??? AppSettings.cs
|
|
||||||
? ??? MarketPrice.cs
|
|
||||||
? ??? TechnicalIndicators.cs
|
|
||||||
? ??? ...
|
|
||||||
??? Services/
|
|
||||||
? ??? TradingBotService.cs # Core trading logic
|
|
||||||
? ??? SimulatedMarketDataService.cs # Simulazione mercato
|
|
||||||
? ??? SettingsService.cs # Persistenza settings
|
|
||||||
? ??? ITradingStrategy.cs # Interface strategia
|
|
||||||
? ??? SimpleMovingAverageStrategy.cs
|
|
||||||
? ??? TechnicalAnalysis.cs # Calcolo indicatori
|
|
||||||
??? wwwroot/
|
|
||||||
? ??? app.css # Stili globali
|
|
||||||
??? Program.cs # Entry point + DI
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## ?? **STRUTTURA FINALE APPLICAZIONE**
|
|
||||||
|
|
||||||
### **7 Sezioni Principali**
|
|
||||||
|
|
||||||
1. **?? Dashboard** (`/`)
|
|
||||||
- Overview portfolio completo
|
|
||||||
- 4 summary cards con metriche
|
|
||||||
- Top 6 asset attivi
|
|
||||||
- Ultimi 8 trades
|
|
||||||
|
|
||||||
2. **?? Strategie** (`/strategies`)
|
|
||||||
- Gestione strategie di trading
|
|
||||||
- Strategia attiva: RSI + MACD Cross
|
|
||||||
- Template predefiniti
|
|
||||||
- Performance tracking
|
|
||||||
|
|
||||||
3. **?? Asset** (`/assets`) **? NUOVA PAGINA!**
|
|
||||||
- Vista completa tutti i 15 asset
|
|
||||||
- Grid view / List view
|
|
||||||
- Assegnazione strategia per asset
|
|
||||||
- Toggle attivazione
|
|
||||||
- Filtri e ricerca
|
|
||||||
- Metriche real-time
|
|
||||||
|
|
||||||
4. **?? Trading** (`/trading`
|
|
||||||
- Tutti 15 asset in grid
|
|
||||||
- Toggle on/off per ogni asset
|
|
||||||
- Metriche real-time
|
|
||||||
- Tabella operazioni complete
|
|
||||||
|
|
||||||
5. **?? Analisi Mercato** (`/market`)
|
|
||||||
- Grafici interattivi SVG
|
|
||||||
- Indicatori tecnici (RSI, MACD, EMA)
|
|
||||||
- Selector asset
|
|
||||||
- Dati aggiornati ogni 3 secondi
|
|
||||||
|
|
||||||
6. **?? Statistiche** (`/statistics`)
|
|
||||||
- Overview portfolio dettagliato
|
|
||||||
- Breakdown per asset
|
|
||||||
- Best/Worst performers
|
|
||||||
- Analisi P&L completa
|
|
||||||
- Drilldown su singolo asset
|
|
||||||
|
|
||||||
7. **?? Impostazioni** (`/settings`)
|
|
||||||
- Tutte le configurazioni globali
|
|
||||||
- Salvataggio automatico
|
|
||||||
- Notifiche di conferma
|
|
||||||
- Reset a defaults
|
|
||||||
|
|
||||||
|
|
||||||
## ?? Design System
|
|
||||||
|
|
||||||
### Colori
|
|
||||||
- **Primary**: `#6366f1` (Indigo)
|
|
||||||
- **Secondary**: `#8b5cf6` (Purple)
|
|
||||||
- **Success**: `#10b981` (Green)
|
|
||||||
- **Danger**: `#ef4444` (Red)
|
|
||||||
- **Warning**: `#f59e0b` (Amber)
|
|
||||||
- **Background**: `#0a0e27` (Dark Blue)
|
|
||||||
|
|
||||||
### Typography
|
|
||||||
- **Headers**: System Font Stack
|
|
||||||
- **Monospace**: Courier New (per valori numerici)
|
|
||||||
|
|
||||||
## ?? Configurazione
|
|
||||||
|
|
||||||
Le impostazioni vengono salvate automaticamente in:
|
|
||||||
```
|
|
||||||
%LocalAppData%/TradingBot/appsettings.json
|
|
||||||
```
|
|
||||||
|
|
||||||
### Esempio appsettings.json
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"SimulationMode": true,
|
|
||||||
"DesktopNotifications": false,
|
|
||||||
"AutoStartBot": true,
|
|
||||||
"ConfirmManualTrades": false,
|
|
||||||
"UpdateIntervalSeconds": 3,
|
|
||||||
"LogLevel": "Info",
|
|
||||||
"SidebarCollapsed": false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## ?? Indicatori Tecnici Implementati
|
|
||||||
|
|
||||||
### RSI (Relative Strength Index)
|
|
||||||
- **Periodo**: 14
|
|
||||||
- **Overbought**: > 70
|
|
||||||
- **Oversold**: < 30
|
|
||||||
- **Neutro**: 30-70
|
|
||||||
|
|
||||||
### MACD (Moving Average Convergence Divergence)
|
|
||||||
- **Fast EMA**: 12 periodi
|
|
||||||
- **Slow EMA**: 26 periodi
|
|
||||||
- **Signal**: 9 periodi
|
|
||||||
- **Histogram**: MACD - Signal
|
|
||||||
|
|
||||||
### EMA (Exponential Moving Average)
|
|
||||||
- **EMA 12**: Media breve termine
|
|
||||||
- **EMA 26**: Media lungo termine
|
|
||||||
|
|
||||||
## ?? Strategia di Trading
|
|
||||||
|
|
||||||
### Condizioni BUY
|
|
||||||
- RSI < 40 (asset ipervenduto)
|
|
||||||
- MACD Histogram > 0 (momentum positivo)
|
|
||||||
- Budget disponibile >= MinTradeAmount
|
|
||||||
|
|
||||||
### Condizioni SELL
|
|
||||||
- RSI > 60 (asset ipercomprato)
|
|
||||||
- MACD Histogram < 0 (momentum negativo)
|
|
||||||
- Holdings > 0
|
|
||||||
- **Oppure**:
|
|
||||||
- Profitto >= Take Profit (10%)
|
|
||||||
- Perdita >= Stop Loss (5%)
|
|
||||||
|
|
||||||
### Risk Management
|
|
||||||
- **Max Daily Trades**: 50 per asset
|
|
||||||
- **Max Position Size**: $5000 per asset
|
|
||||||
- **Min Trade Amount**: $10
|
|
||||||
- **Trade Size**: 30% del balance disponibile (max)
|
|
||||||
- **Min Interval**: 10 secondi tra trades
|
|
||||||
|
|
||||||
## ?? Aggiornamenti Real-time
|
|
||||||
|
|
||||||
- **Prezzi**: Ogni 3 secondi (configurabile)
|
|
||||||
- **Indicatori**: Calcolati ad ogni aggiornamento prezzo
|
|
||||||
- **Stats Portfolio**: Aggiornate ad ogni trade
|
|
||||||
- **UI**: SignalR per aggiornamenti istantanei
|
|
||||||
|
|
||||||
## ?? Responsive Breakpoints
|
|
||||||
|
|
||||||
- **Desktop**: > 1024px (full features)
|
|
||||||
- **Tablet**: 768px - 1024px (layout adattato)
|
|
||||||
- **Mobile**: < 768px (sidebar collapsible automatico)
|
|
||||||
|
|
||||||
## ?? Debug & Logging
|
|
||||||
|
|
||||||
I log vengono stampati nella console del browser e nel terminal di Visual Studio.
|
|
||||||
|
|
||||||
Livelli disponibili:
|
|
||||||
- **Error**: Solo errori critici
|
|
||||||
- **Warning**: Warning e errori
|
|
||||||
- **Info**: Informazioni generali (default)
|
|
||||||
- **Debug**: Dettagli completi per debugging
|
|
||||||
|
|
||||||
## ?? Note Importanti
|
|
||||||
|
|
||||||
1. **Modalità Simulazione**: Sempre attiva, dati non reali
|
|
||||||
2. **Dati Persistenti**: Solo impostazioni, non trades storici
|
|
||||||
3. **Reset Dati**: Riavvio applicazione = reset portfolio
|
|
||||||
4. **Performance**: Ottimizzata per 15 asset simultanei
|
|
||||||
|
|
||||||
## ?? Future Enhancements
|
|
||||||
|
|
||||||
- [ ] Backtesting su dati storici
|
|
||||||
- [ ] Multi-strategy support
|
|
||||||
- [ ] Export/import configurazioni
|
|
||||||
- [ ] Alert system con notifiche
|
|
||||||
- [ ] Paper trading con dati reali
|
|
||||||
- [ ] Machine learning per ottimizzazione strategie
|
|
||||||
|
|
||||||
## ????? Sviluppatore
|
|
||||||
|
|
||||||
**Alberto** - Encelado Project
|
|
||||||
|
|
||||||
## ?? Licenza
|
|
||||||
|
|
||||||
Progetto privato - Tutti i diritti riservati
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Note**: Questa è un'applicazione di simulazione a scopo educativo. Non utilizzare con denaro reale senza test approfonditi e comprensione completa dei rischi del trading.
|
## ? Caratteristiche
|
||||||
|
|
||||||
|
- **Multi-Strategy Trading**: 8 strategie professionali assegnabili a ogni asset
|
||||||
|
- **Positions Management**: Visualizza e chiudi manualmente posizioni aperte
|
||||||
|
- **Detailed Portfolio Metrics**: Capitale totale, investito, disponibile, P&L, ROI
|
||||||
|
- **Trading Control**: Gestione visuale strategie con voting system
|
||||||
|
- **Dashboard Blazor**: Real-time updates ogni 3 secondi
|
||||||
|
- **15 Criptovalute**: BTC, ETH, BNB, ADA, SOL, XRP, DOT, DOGE, AVAX, MATIC, LINK, LTC, UNI, ATOM, XLM
|
||||||
|
- **Analisi Tecnica**: SMA, EMA, RSI, MACD, Bollinger Bands, Stochastic
|
||||||
|
- **Indicators System**: 7+ indicatori tecnici configurabili con segnali real-time
|
||||||
|
- **Portfolio Management**: Gestione automatizzata posizioni
|
||||||
|
- **Trade Persistence**: Salvataggio automatico trade e posizioni attive
|
||||||
|
- **Comprehensive Logs**: Sistema di logging real-time con filtri avanzati
|
||||||
|
- **Docker Ready**: Container ottimizzato con health checks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ?? Trading Strategies
|
||||||
|
|
||||||
|
### **8 Strategie Professionali**
|
||||||
|
|
||||||
|
1. **RSI Strategy** - Oscillator (Medium Risk)
|
||||||
|
2. **MACD Strategy** - Momentum (Medium Risk)
|
||||||
|
3. **Bollinger Bands** - Volatility (Low Risk)
|
||||||
|
4. **Mean Reversion** - Contrarian (High Risk)
|
||||||
|
5. **Momentum** - Trend Following (Medium Risk)
|
||||||
|
6. **EMA Crossover** - Golden/Death Cross (Low Risk)
|
||||||
|
7. **Scalping** - Short-term (Very High Risk)
|
||||||
|
8. **Breakout** - Volatility Breakout (High Risk)
|
||||||
|
|
||||||
|
### **Multi-Strategy System**
|
||||||
|
|
||||||
|
- Assegna multiple strategie per asset
|
||||||
|
- Sistema di voting per decisioni aggregate
|
||||||
|
- Confidence-based trading
|
||||||
|
- Parametri configurabili per strategia
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ?? Quick Start
|
||||||
|
|
||||||
|
### Locale (Development)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.encke-hake.ts.net/Alby96/Encelado
|
||||||
|
cd Encelado/TradingBot
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
Accedi a: `http://localhost:5243`
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest
|
||||||
|
docker run -d -p 8888:8080 \
|
||||||
|
-v tradingbot-data:/app/data \
|
||||||
|
gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Accedi a: `http://localhost:8888`
|
||||||
|
|
||||||
|
### Unraid
|
||||||
|
|
||||||
|
Guida completa: [deployment/UNRAID_INSTALL.md](deployment/UNRAID_INSTALL.md)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Login Gitea Registry
|
||||||
|
docker login gitea.encke-hake.ts.net
|
||||||
|
|
||||||
|
# 2. Download template
|
||||||
|
wget -O /boot/config/plugins/dockerMan/templates-user/TradingBot.xml \
|
||||||
|
https://gitea.encke-hake.ts.net/Alby96/Encelado/raw/branch/main/TradingBot/deployment/unraid-template.xml
|
||||||
|
|
||||||
|
# 3. Install via Unraid Docker UI
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ?? Versioning
|
||||||
|
|
||||||
|
### Current Version: `1.5.2`
|
||||||
|
|
||||||
|
**Latest**: Metriche dettagliate capitale in sidebar (Totale, Investito, Disponibile, P&L, ROI)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Bug fix (1.5.2 ? 1.5.3)
|
||||||
|
.\bump-version.ps1 patch -Message "Fix calculation bug"
|
||||||
|
|
||||||
|
# New feature (1.5.2 ? 1.6.0)
|
||||||
|
.\bump-version.ps1 minor -Message "Add new strategy"
|
||||||
|
|
||||||
|
# Breaking change (1.5.2 ? 2.0.0)
|
||||||
|
.\bump-version.ps1 major -Message "New API"
|
||||||
|
```
|
||||||
|
|
||||||
|
Vedi [CHANGELOG.md](CHANGELOG.md) per release notes complete.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ?? Publishing
|
||||||
|
|
||||||
|
### Da Visual Studio
|
||||||
|
|
||||||
|
1. **Build** ? **Configuration Manager** ? **Release**
|
||||||
|
2. **Build** ? **Publish TradingBot** ? Profilo **Docker**
|
||||||
|
3. Click **Publish**
|
||||||
|
|
||||||
|
Il sistema automaticamente:
|
||||||
|
- ? Build Docker image
|
||||||
|
- ? Tag: `latest`, `1.5.1`, `1.5.1-20241222`
|
||||||
|
- ? Push su Gitea Registry
|
||||||
|
|
||||||
|
### Deploy su Unraid
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Docker tab ? TradingBot ? Stop ? Force Update ? Start
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ?? Documentazione
|
||||||
|
|
||||||
|
- **[UNRAID_INSTALL.md](deployment/UNRAID_INSTALL.md)** - Installazione completa su Unraid
|
||||||
|
- **[CHANGELOG.md](CHANGELOG.md)** - Release notes e versioni
|
||||||
|
- **[Dockerfile](Dockerfile)** - Docker multi-stage build
|
||||||
|
- **[docker-compose.yml](deployment/docker-compose.yml)** - Deploy con Compose
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ??? Tecnologie
|
||||||
|
|
||||||
|
- **.NET 10** | **Blazor Server** | **C# 14**
|
||||||
|
- **Bootstrap 5.3** | **Docker** | **Gitea Registry**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ?? License
|
||||||
|
|
||||||
|
MIT License - Copyright © 2024 Alby96
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ?? Autore
|
||||||
|
|
||||||
|
**Alberto** (Alby96)
|
||||||
|
- Gitea: [@Alby96](https://gitea.encke-hake.ts.net/Alby96)
|
||||||
|
- Repository: [Encelado/TradingBot](https://gitea.encke-hake.ts.net/Alby96/Encelado)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**?? Happy Trading!**
|
||||||
|
|||||||
@@ -1,271 +0,0 @@
|
|||||||
# ?? DEBUG - Sidebar Collapse Toggle
|
|
||||||
|
|
||||||
## Problema Riportato
|
|
||||||
Il pulsante per ridurre la sidebar a sole icone non funziona.
|
|
||||||
|
|
||||||
## Modifiche Applicate
|
|
||||||
|
|
||||||
### 1. **MainLayout.razor** - Migliorato Toggle
|
|
||||||
```csharp
|
|
||||||
private void ToggleSidebar()
|
|
||||||
{
|
|
||||||
sidebarCollapsed = !sidebarCollapsed;
|
|
||||||
SettingsService.UpdateSetting(nameof(AppSettings.SidebarCollapsed), sidebarCollapsed);
|
|
||||||
StateHasChanged(); // ? AGGIUNTO: Force immediate UI update
|
|
||||||
Console.WriteLine($"Sidebar toggled: collapsed={sidebarCollapsed}"); // ? AGGIUNTO: Debug log
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cosa fa**:
|
|
||||||
- ? Forza il re-render immediato con `StateHasChanged()`
|
|
||||||
- ? Log nella console per debug
|
|
||||||
- ? Salva lo stato nelle impostazioni
|
|
||||||
|
|
||||||
### 2. **MainLayout.razor.css** - CSS Collapsed State
|
|
||||||
```css
|
|
||||||
::deep .trading-bot-layout.collapsed .sidebar-brand {
|
|
||||||
padding: 1.5rem 0.75rem !important;
|
|
||||||
justify-content: center !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
::deep .trading-bot-layout.collapsed .brand-logo {
|
|
||||||
width: 3rem !important;
|
|
||||||
height: 3rem !important;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cosa fa**:
|
|
||||||
- ? Riduce padding quando collapsed
|
|
||||||
- ? Centra il logo
|
|
||||||
- ? Riduce dimensione logo
|
|
||||||
|
|
||||||
## Come Testare
|
|
||||||
|
|
||||||
### 1. **Riavvia l'Applicazione**
|
|
||||||
```sh
|
|
||||||
# Stop server
|
|
||||||
Ctrl + C
|
|
||||||
|
|
||||||
# Clean build
|
|
||||||
dotnet clean
|
|
||||||
dotnet build
|
|
||||||
|
|
||||||
# Run
|
|
||||||
dotnet run
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. **Forza Cache Refresh**
|
|
||||||
```
|
|
||||||
Ctrl + Shift + R (o Ctrl + F5)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. **Test del Button**
|
|
||||||
1. Apri l'applicazione
|
|
||||||
2. Click sul pulsante `[?]` in alto a destra nella sidebar
|
|
||||||
3. Verifica che:
|
|
||||||
- La sidebar si riduca a ~80px
|
|
||||||
- Rimangano solo le icone
|
|
||||||
- Il logo si ridimensioni
|
|
||||||
- L'area contenuto si espanda
|
|
||||||
|
|
||||||
### 4. **Verifica Console**
|
|
||||||
Apri DevTools (F12) ? Console
|
|
||||||
|
|
||||||
Dovresti vedere:
|
|
||||||
```
|
|
||||||
Sidebar toggled: collapsed=true (quando minimizzi)
|
|
||||||
Sidebar toggled: collapsed=false (quando espandi)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Comportamento Atteso
|
|
||||||
|
|
||||||
### Expanded (280px)
|
|
||||||
```
|
|
||||||
????????????????????????????
|
|
||||||
? [??] TradingBot [?] ? ? Button qui
|
|
||||||
? ? ATTIVO ?
|
|
||||||
????????????????????????????
|
|
||||||
? ?? Dashboard ?
|
|
||||||
? ?? Strategie ?
|
|
||||||
? ?? Asset ?
|
|
||||||
? ... ?
|
|
||||||
????????????????????????????
|
|
||||||
```
|
|
||||||
|
|
||||||
### Collapsed (80px)
|
|
||||||
```
|
|
||||||
???????
|
|
||||||
? [??]? ? Logo centrato
|
|
||||||
? ?
|
|
||||||
???????
|
|
||||||
? ?? ? ? Solo icone
|
|
||||||
? ?? ? centrate
|
|
||||||
? ?? ?
|
|
||||||
? ... ?
|
|
||||||
???????
|
|
||||||
```
|
|
||||||
|
|
||||||
## Debug Checklist
|
|
||||||
|
|
||||||
Se il button ancora non funziona:
|
|
||||||
|
|
||||||
- [ ] Build riuscito senza errori?
|
|
||||||
- [ ] Cache browser pulita (Ctrl+Shift+R)?
|
|
||||||
- [ ] Console mostra i log "Sidebar toggled"?
|
|
||||||
- [ ] Ispeziona elemento: classe "collapsed" viene applicata al container?
|
|
||||||
- [ ] CSS caricato correttamente (verifica in Network tab)?
|
|
||||||
|
|
||||||
## Verifica con DevTools
|
|
||||||
|
|
||||||
### 1. Ispeziona il Container
|
|
||||||
```
|
|
||||||
F12 ? Elements tab
|
|
||||||
Cerca: <div class="trading-bot-layout ...">
|
|
||||||
```
|
|
||||||
|
|
||||||
**Quando Expanded**:
|
|
||||||
```html
|
|
||||||
<div class="trading-bot-layout expanded">
|
|
||||||
```
|
|
||||||
|
|
||||||
**Quando Collapsed**:
|
|
||||||
```html
|
|
||||||
<div class="trading-bot-layout collapsed">
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Verifica CSS Applicato
|
|
||||||
```
|
|
||||||
Click su .modern-sidebar
|
|
||||||
Guarda tab "Computed" ? width
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expanded**: `width: 280px`
|
|
||||||
**Collapsed**: `width: 80px`
|
|
||||||
|
|
||||||
### 3. Verifica Button Click
|
|
||||||
```
|
|
||||||
Console tab
|
|
||||||
Click sul button [?]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Output atteso**:
|
|
||||||
```
|
|
||||||
Sidebar toggled: collapsed=true
|
|
||||||
```
|
|
||||||
|
|
||||||
## Possibili Cause se Non Funziona
|
|
||||||
|
|
||||||
### 1. CSS Non Caricato
|
|
||||||
**Sintomo**: Button visibile ma sidebar non cambia dimensione
|
|
||||||
|
|
||||||
**Soluzione**:
|
|
||||||
```sh
|
|
||||||
dotnet clean
|
|
||||||
dotnet build
|
|
||||||
Ctrl + Shift + R nel browser
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. JavaScript/SignalR Bloccato
|
|
||||||
**Sintomo**: Click non produce effetto, nessun log
|
|
||||||
|
|
||||||
**Soluzione**:
|
|
||||||
```
|
|
||||||
F12 ? Console ? Cerca errori
|
|
||||||
Riavvia server Blazor
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Settings Service Non Salva
|
|
||||||
**Sintomo**: Toggle funziona ma non persiste al reload
|
|
||||||
|
|
||||||
**Soluzione**:
|
|
||||||
Verifica file:
|
|
||||||
```
|
|
||||||
%LocalAppData%/TradingBot/appsettings.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Cerca proprietà:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"SidebarCollapsed": true/false
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Binding Non Aggiornato
|
|
||||||
**Sintomo**: Classe non cambia nel DOM
|
|
||||||
|
|
||||||
**Soluzione**:
|
|
||||||
Aggiungi nel code block:
|
|
||||||
```csharp
|
|
||||||
protected override void OnAfterRender(bool firstRender)
|
|
||||||
{
|
|
||||||
if (firstRender)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Initial collapsed state: {sidebarCollapsed}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## File Modificati
|
|
||||||
|
|
||||||
1. ? `Components/Layout/MainLayout.razor`
|
|
||||||
- Aggiunto `StateHasChanged()`
|
|
||||||
- Aggiunto debug log
|
|
||||||
|
|
||||||
2. ? `Components/Layout/MainLayout.razor.css`
|
|
||||||
- CSS specifico per collapsed state
|
|
||||||
- Riduzione dimensioni logo
|
|
||||||
|
|
||||||
## Test Manuale Step-by-Step
|
|
||||||
|
|
||||||
1. ? Avvia app: `dotnet run`
|
|
||||||
2. ? Apri browser: `https://localhost:[PORT]`
|
|
||||||
3. ? Hard refresh: `Ctrl + Shift + R`
|
|
||||||
4. ? Apri DevTools: `F12`
|
|
||||||
5. ? Vai su Console tab
|
|
||||||
6. ? Click sul button `[?]`
|
|
||||||
7. ? Verifica log: "Sidebar toggled: collapsed=true"
|
|
||||||
8. ? Verifica visuale: Sidebar si riduce
|
|
||||||
9. ? Click di nuovo: "Sidebar toggled: collapsed=false"
|
|
||||||
10. ? Verifica visuale: Sidebar si espande
|
|
||||||
|
|
||||||
## Expected Log Output
|
|
||||||
|
|
||||||
```
|
|
||||||
// Al caricamento
|
|
||||||
Initial collapsed state: false
|
|
||||||
|
|
||||||
// Click 1 (Minimize)
|
|
||||||
Sidebar toggled: collapsed=true
|
|
||||||
|
|
||||||
// Click 2 (Expand)
|
|
||||||
Sidebar toggled: collapsed=false
|
|
||||||
|
|
||||||
// Click 3 (Minimize)
|
|
||||||
Sidebar toggled: collapsed=true
|
|
||||||
```
|
|
||||||
|
|
||||||
## CSS Transitions
|
|
||||||
|
|
||||||
Con le modifiche applicate, le transizioni dovrebbero essere smooth:
|
|
||||||
|
|
||||||
```css
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
|
||||||
```
|
|
||||||
|
|
||||||
**Durata**: 300ms
|
|
||||||
**Easing**: Smooth cubic-bezier
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
Se dopo questi passaggi il button ancora non funziona:
|
|
||||||
|
|
||||||
1. ?? Screenshot della sidebar
|
|
||||||
2. ?? Log della Console (F12)
|
|
||||||
3. ?? Ispeziona elemento HTML del container
|
|
||||||
4. ?? Contenuto di appsettings.json
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Status**: ? Fix Applicato
|
|
||||||
**Build**: ? Successful
|
|
||||||
**Test**: ? Pending User Verification
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
using TradingBot.Models;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace TradingBot.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Centralized logging service for application events
|
||||||
|
/// </summary>
|
||||||
|
public class LoggingService
|
||||||
|
{
|
||||||
|
private readonly ConcurrentQueue<LogEntry> _logs = new();
|
||||||
|
private const int MaxLogEntries = 500;
|
||||||
|
|
||||||
|
public event Action? OnLogAdded;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all log entries
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<LogEntry> GetLogs()
|
||||||
|
{
|
||||||
|
return _logs.ToList().AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a debug log entry
|
||||||
|
/// </summary>
|
||||||
|
public void LogDebug(string category, string message, string? details = null)
|
||||||
|
{
|
||||||
|
AddLog(Models.LogLevel.Debug, category, message, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add an info log entry
|
||||||
|
/// </summary>
|
||||||
|
public void LogInfo(string category, string message, string? details = null, string? symbol = null)
|
||||||
|
{
|
||||||
|
AddLog(Models.LogLevel.Info, category, message, details, symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a warning log entry
|
||||||
|
/// </summary>
|
||||||
|
public void LogWarning(string category, string message, string? details = null, string? symbol = null)
|
||||||
|
{
|
||||||
|
AddLog(Models.LogLevel.Warning, category, message, details, symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add an error log entry
|
||||||
|
/// </summary>
|
||||||
|
public void LogError(string category, string message, string? details = null, string? symbol = null)
|
||||||
|
{
|
||||||
|
AddLog(Models.LogLevel.Error, category, message, details, symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a trade log entry
|
||||||
|
/// </summary>
|
||||||
|
public void LogTrade(string symbol, string message, string? details = null)
|
||||||
|
{
|
||||||
|
AddLog(Models.LogLevel.Trade, "Trading", message, details, symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clear all logs
|
||||||
|
/// </summary>
|
||||||
|
public void ClearLogs()
|
||||||
|
{
|
||||||
|
_logs.Clear();
|
||||||
|
OnLogAdded?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get logs filtered by level
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<LogEntry> GetLogsByLevel(Models.LogLevel level)
|
||||||
|
{
|
||||||
|
return _logs.Where(l => l.Level == level).ToList().AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get logs filtered by category
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<LogEntry> GetLogsByCategory(string category)
|
||||||
|
{
|
||||||
|
return _logs.Where(l => l.Category.Equals(category, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList()
|
||||||
|
.AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get logs filtered by symbol
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<LogEntry> GetLogsBySymbol(string symbol)
|
||||||
|
{
|
||||||
|
return _logs.Where(l => l.Symbol != null && l.Symbol.Equals(symbol, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList()
|
||||||
|
.AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddLog(Models.LogLevel level, string category, string message, string? details = null, string? symbol = null)
|
||||||
|
{
|
||||||
|
var logEntry = new LogEntry
|
||||||
|
{
|
||||||
|
Level = level,
|
||||||
|
Category = category,
|
||||||
|
Message = message,
|
||||||
|
Details = details,
|
||||||
|
Symbol = symbol
|
||||||
|
};
|
||||||
|
|
||||||
|
_logs.Enqueue(logEntry);
|
||||||
|
|
||||||
|
// Maintain max size
|
||||||
|
while (_logs.Count > MaxLogEntries)
|
||||||
|
{
|
||||||
|
_logs.TryDequeue(out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
OnLogAdded?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,19 +11,38 @@ public class SimpleMovingAverageStrategy : ITradingStrategy
|
|||||||
|
|
||||||
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> historicalPrices)
|
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> historicalPrices)
|
||||||
{
|
{
|
||||||
if (historicalPrices.Count < _longPeriod)
|
// Filtra null e valori invalidi prima di usare la lista
|
||||||
|
if (historicalPrices == null || historicalPrices.Count < _longPeriod)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new TradingSignal
|
return Task.FromResult(new TradingSignal
|
||||||
{
|
{
|
||||||
Symbol = symbol,
|
Symbol = symbol,
|
||||||
Type = SignalType.Hold,
|
Type = SignalType.Hold,
|
||||||
Price = historicalPrices.LastOrDefault()?.Price ?? 0,
|
Price = historicalPrices?.LastOrDefault()?.Price ?? 0,
|
||||||
Reason = "Dati insufficienti per l'analisi",
|
Reason = "Dati insufficienti per l'analisi",
|
||||||
Timestamp = DateTime.UtcNow
|
Timestamp = DateTime.UtcNow
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var recentPrices = historicalPrices.OrderByDescending(p => p.Timestamp).Take(_longPeriod).ToList();
|
// Filtra oggetti null e ordina
|
||||||
|
var recentPrices = historicalPrices
|
||||||
|
.Where(p => p != null && p.Price > 0)
|
||||||
|
.OrderByDescending(p => p.Timestamp)
|
||||||
|
.Take(_longPeriod)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Verifica ancora la count dopo il filtro
|
||||||
|
if (recentPrices.Count < _longPeriod)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new TradingSignal
|
||||||
|
{
|
||||||
|
Symbol = symbol,
|
||||||
|
Type = SignalType.Hold,
|
||||||
|
Price = recentPrices.LastOrDefault()?.Price ?? 0,
|
||||||
|
Reason = "Dati insufficienti per l'analisi dopo il filtro",
|
||||||
|
Timestamp = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var shortSMA = recentPrices.Take(_shortPeriod).Average(p => p.Price);
|
var shortSMA = recentPrices.Take(_shortPeriod).Average(p => p.Price);
|
||||||
var longSMA = recentPrices.Average(p => p.Price);
|
var longSMA = recentPrices.Average(p => p.Price);
|
||||||
|
|||||||
@@ -70,4 +70,22 @@ public static class TechnicalAnalysis
|
|||||||
|
|
||||||
return (macdLine, signalLine, histogram);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,17 +6,24 @@ public class TradingBotService
|
|||||||
{
|
{
|
||||||
private readonly IMarketDataService _marketDataService;
|
private readonly IMarketDataService _marketDataService;
|
||||||
private readonly ITradingStrategy _strategy;
|
private readonly ITradingStrategy _strategy;
|
||||||
|
private readonly TradeHistoryService _historyService;
|
||||||
|
private readonly LoggingService _loggingService;
|
||||||
|
private readonly IndicatorsService _indicatorsService;
|
||||||
|
private readonly TradingStrategiesService _strategiesService;
|
||||||
private readonly Dictionary<string, AssetConfiguration> _assetConfigs = new();
|
private readonly Dictionary<string, AssetConfiguration> _assetConfigs = new();
|
||||||
private readonly Dictionary<string, AssetStatistics> _assetStats = new();
|
private readonly Dictionary<string, AssetStatistics> _assetStats = new();
|
||||||
private readonly List<Trade> _trades = new();
|
private readonly List<Trade> _trades = new();
|
||||||
private readonly Dictionary<string, List<MarketPrice>> _priceHistory = new();
|
private readonly Dictionary<string, List<MarketPrice>> _priceHistory = new();
|
||||||
private readonly Dictionary<string, TechnicalIndicators> _indicators = new();
|
private readonly Dictionary<string, TechnicalIndicators> _indicators = new();
|
||||||
|
private readonly Dictionary<string, Trade> _activePositions = new();
|
||||||
private Timer? _timer;
|
private Timer? _timer;
|
||||||
|
private Timer? _persistenceTimer;
|
||||||
|
|
||||||
public BotStatus Status { get; private set; } = new();
|
public BotStatus Status { get; private set; } = new();
|
||||||
public IReadOnlyList<Trade> Trades => _trades.AsReadOnly();
|
public IReadOnlyList<Trade> Trades => _trades.AsReadOnly();
|
||||||
public IReadOnlyDictionary<string, AssetConfiguration> AssetConfigurations => _assetConfigs;
|
public IReadOnlyDictionary<string, AssetConfiguration> AssetConfigurations => _assetConfigs;
|
||||||
public IReadOnlyDictionary<string, AssetStatistics> AssetStatistics => _assetStats;
|
public IReadOnlyDictionary<string, AssetStatistics> AssetStatistics => _assetStats;
|
||||||
|
public IReadOnlyDictionary<string, Trade> ActivePositions => _activePositions;
|
||||||
|
|
||||||
public event Action? OnStatusChanged;
|
public event Action? OnStatusChanged;
|
||||||
public event Action<TradingSignal>? OnSignalGenerated;
|
public event Action<TradingSignal>? OnSignalGenerated;
|
||||||
@@ -25,10 +32,20 @@ public class TradingBotService
|
|||||||
public event Action<string, MarketPrice>? OnPriceUpdated;
|
public event Action<string, MarketPrice>? OnPriceUpdated;
|
||||||
public event Action? OnStatisticsUpdated;
|
public event Action? OnStatisticsUpdated;
|
||||||
|
|
||||||
public TradingBotService(IMarketDataService marketDataService, ITradingStrategy strategy)
|
public TradingBotService(
|
||||||
|
IMarketDataService marketDataService,
|
||||||
|
ITradingStrategy strategy,
|
||||||
|
TradeHistoryService historyService,
|
||||||
|
LoggingService loggingService,
|
||||||
|
IndicatorsService indicatorsService,
|
||||||
|
TradingStrategiesService strategiesService)
|
||||||
{
|
{
|
||||||
_marketDataService = marketDataService;
|
_marketDataService = marketDataService;
|
||||||
_strategy = strategy;
|
_strategy = strategy;
|
||||||
|
_historyService = historyService;
|
||||||
|
_loggingService = loggingService;
|
||||||
|
_indicatorsService = indicatorsService;
|
||||||
|
_strategiesService = strategiesService;
|
||||||
Status.CurrentStrategy = strategy.Name;
|
Status.CurrentStrategy = strategy.Name;
|
||||||
|
|
||||||
// Subscribe to simulated market updates if available
|
// Subscribe to simulated market updates if available
|
||||||
@@ -38,6 +55,52 @@ public class TradingBotService
|
|||||||
}
|
}
|
||||||
|
|
||||||
InitializeDefaultAssets();
|
InitializeDefaultAssets();
|
||||||
|
|
||||||
|
// Load persisted data
|
||||||
|
_ = LoadPersistedDataAsync();
|
||||||
|
|
||||||
|
_loggingService.LogInfo("System", "TradingBot Service initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadPersistedDataAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Load trade history
|
||||||
|
var trades = await _historyService.LoadTradeHistoryAsync();
|
||||||
|
_trades.AddRange(trades);
|
||||||
|
|
||||||
|
// Load active positions
|
||||||
|
var positions = await _historyService.LoadActivePositionsAsync();
|
||||||
|
foreach (var kvp in positions)
|
||||||
|
{
|
||||||
|
_activePositions[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore asset configurations from active positions
|
||||||
|
RestoreAssetConfigurationsFromTrades();
|
||||||
|
|
||||||
|
OnStatusChanged?.Invoke();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error loading persisted data: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RestoreAssetConfigurationsFromTrades()
|
||||||
|
{
|
||||||
|
foreach (var position in _activePositions.Values)
|
||||||
|
{
|
||||||
|
if (_assetConfigs.TryGetValue(position.Symbol, out var config))
|
||||||
|
{
|
||||||
|
if (position.Type == TradeType.Buy)
|
||||||
|
{
|
||||||
|
config.CurrentHoldings += position.Amount;
|
||||||
|
config.AverageEntryPrice = position.Price;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeDefaultAssets()
|
private void InitializeDefaultAssets()
|
||||||
@@ -64,7 +127,7 @@ public class TradingBotService
|
|||||||
{
|
{
|
||||||
Symbol = symbol,
|
Symbol = symbol,
|
||||||
Name = assetNames.TryGetValue(symbol, out var name) ? name : symbol,
|
Name = assetNames.TryGetValue(symbol, out var name) ? name : symbol,
|
||||||
IsEnabled = true, // Enable ALL assets by default for full simulation
|
IsEnabled = true,
|
||||||
InitialBalance = 1000m,
|
InitialBalance = 1000m,
|
||||||
CurrentBalance = 1000m
|
CurrentBalance = 1000m
|
||||||
};
|
};
|
||||||
@@ -122,6 +185,8 @@ public class TradingBotService
|
|||||||
Status.IsRunning = true;
|
Status.IsRunning = true;
|
||||||
Status.StartedAt = DateTime.UtcNow;
|
Status.StartedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
_loggingService.LogInfo("Bot", "Trading Bot started", $"Strategy: {_strategy.Name}");
|
||||||
|
|
||||||
// Reset daily trade counts
|
// Reset daily trade counts
|
||||||
foreach (var config in _assetConfigs.Values)
|
foreach (var config in _assetConfigs.Values)
|
||||||
{
|
{
|
||||||
@@ -135,10 +200,17 @@ public class TradingBotService
|
|||||||
// Start update timer (every 3 seconds for simulation)
|
// Start update timer (every 3 seconds for simulation)
|
||||||
_timer = new Timer(async _ => await UpdateAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(3));
|
_timer = new Timer(async _ => await UpdateAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(3));
|
||||||
|
|
||||||
|
// Start persistence timer (save every 30 seconds)
|
||||||
|
_persistenceTimer = new Timer(
|
||||||
|
async _ => await SaveDataAsync(),
|
||||||
|
null,
|
||||||
|
TimeSpan.FromSeconds(30),
|
||||||
|
TimeSpan.FromSeconds(30));
|
||||||
|
|
||||||
OnStatusChanged?.Invoke();
|
OnStatusChanged?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Stop()
|
public async void Stop()
|
||||||
{
|
{
|
||||||
if (!Status.IsRunning) return;
|
if (!Status.IsRunning) return;
|
||||||
|
|
||||||
@@ -146,9 +218,30 @@ public class TradingBotService
|
|||||||
_timer?.Dispose();
|
_timer?.Dispose();
|
||||||
_timer = null;
|
_timer = null;
|
||||||
|
|
||||||
|
_persistenceTimer?.Dispose();
|
||||||
|
_persistenceTimer = null;
|
||||||
|
|
||||||
|
_loggingService.LogInfo("Bot", "Trading Bot stopped", $"Total trades: {_trades.Count}");
|
||||||
|
|
||||||
|
// Save data on stop
|
||||||
|
await SaveDataAsync();
|
||||||
|
|
||||||
OnStatusChanged?.Invoke();
|
OnStatusChanged?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SaveDataAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _historyService.SaveTradeHistoryAsync(_trades);
|
||||||
|
await _historyService.SaveActivePositionsAsync(_activePositions);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error saving data: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void HandleSimulatedPriceUpdate()
|
private void HandleSimulatedPriceUpdate()
|
||||||
{
|
{
|
||||||
if (Status.IsRunning)
|
if (Status.IsRunning)
|
||||||
@@ -162,29 +255,39 @@ public class TradingBotService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var enabledSymbols = _assetConfigs.Values
|
var enabledSymbols = _assetConfigs.Values
|
||||||
.Where(c => c.IsEnabled)
|
.Where(c => c != null && c.IsEnabled)
|
||||||
.Select(c => c.Symbol)
|
.Select(c => c.Symbol)
|
||||||
|
.Where(s => !string.IsNullOrWhiteSpace(s))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if (enabledSymbols.Count == 0) return;
|
if (enabledSymbols.Count == 0) return;
|
||||||
|
|
||||||
var prices = await _marketDataService.GetMarketPricesAsync(enabledSymbols);
|
var prices = await _marketDataService.GetMarketPricesAsync(enabledSymbols);
|
||||||
|
|
||||||
|
if (prices == null) return;
|
||||||
|
|
||||||
foreach (var price in prices)
|
foreach (var price in prices)
|
||||||
|
{
|
||||||
|
if (price != null)
|
||||||
{
|
{
|
||||||
await ProcessAssetUpdate(price);
|
await ProcessAssetUpdate(price);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
UpdateGlobalStatistics();
|
UpdateGlobalStatistics();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Error in UpdateAsync: {ex.Message}");
|
Console.WriteLine($"Error in UpdateAsync: {ex.Message}");
|
||||||
|
Console.WriteLine($"Stack trace: {ex.StackTrace}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProcessAssetUpdate(MarketPrice price)
|
private async Task ProcessAssetUpdate(MarketPrice price)
|
||||||
{
|
{
|
||||||
|
if (price == null || price.Price <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
if (!_assetConfigs.TryGetValue(price.Symbol, out var config) || !config.IsEnabled)
|
if (!_assetConfigs.TryGetValue(price.Symbol, out var config) || !config.IsEnabled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -216,12 +319,16 @@ public class TradingBotService
|
|||||||
|
|
||||||
// Generate trading signal
|
// Generate trading signal
|
||||||
var signal = await _strategy.AnalyzeAsync(price.Symbol, _priceHistory[price.Symbol]);
|
var signal = await _strategy.AnalyzeAsync(price.Symbol, _priceHistory[price.Symbol]);
|
||||||
|
|
||||||
|
if (signal != null)
|
||||||
|
{
|
||||||
OnSignalGenerated?.Invoke(signal);
|
OnSignalGenerated?.Invoke(signal);
|
||||||
|
|
||||||
// Execute trades based on strategy and configuration
|
// Execute trades based on strategy and configuration
|
||||||
await EvaluateAndExecuteTrade(price.Symbol, signal, price, config);
|
await EvaluateAndExecuteTrade(price.Symbol, signal, price, config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task EvaluateAndExecuteTrade(string symbol, TradingSignal signal, MarketPrice price, AssetConfiguration config)
|
private async Task EvaluateAndExecuteTrade(string symbol, TradingSignal signal, MarketPrice price, AssetConfiguration config)
|
||||||
{
|
{
|
||||||
@@ -250,7 +357,7 @@ public class TradingBotService
|
|||||||
|
|
||||||
if (tradeAmount >= config.MinTradeAmount)
|
if (tradeAmount >= config.MinTradeAmount)
|
||||||
{
|
{
|
||||||
ExecuteBuy(symbol, price.Price, tradeAmount, config);
|
await ExecuteBuyAsync(symbol, price.Price, tradeAmount, config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Sell logic
|
// Sell logic
|
||||||
@@ -267,14 +374,12 @@ public class TradingBotService
|
|||||||
if (profitPercentage >= config.TakeProfitPercentage ||
|
if (profitPercentage >= config.TakeProfitPercentage ||
|
||||||
profitPercentage <= -config.StopLossPercentage)
|
profitPercentage <= -config.StopLossPercentage)
|
||||||
{
|
{
|
||||||
ExecuteSell(symbol, price.Price, config.CurrentHoldings, config);
|
await ExecuteSellAsync(symbol, price.Price, config.CurrentHoldings, config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.CompletedTask;
|
private async Task ExecuteBuyAsync(string symbol, decimal price, decimal amountUSD, AssetConfiguration config)
|
||||||
}
|
|
||||||
|
|
||||||
private void ExecuteBuy(string symbol, decimal price, decimal amountUSD, AssetConfiguration config)
|
|
||||||
{
|
{
|
||||||
var amount = amountUSD / price;
|
var amount = amountUSD / price;
|
||||||
|
|
||||||
@@ -300,14 +405,24 @@ public class TradingBotService
|
|||||||
};
|
};
|
||||||
|
|
||||||
_trades.Add(trade);
|
_trades.Add(trade);
|
||||||
|
_activePositions[symbol] = trade;
|
||||||
UpdateAssetStatistics(symbol, trade);
|
UpdateAssetStatistics(symbol, trade);
|
||||||
|
|
||||||
Status.TradesExecuted++;
|
Status.TradesExecuted++;
|
||||||
|
|
||||||
|
_loggingService.LogTrade(
|
||||||
|
symbol,
|
||||||
|
$"BUY {amount:F6} {symbol} @ ${price:N2}",
|
||||||
|
$"Value: ${amountUSD:N2} | Balance: ${config.CurrentBalance:N2}");
|
||||||
|
|
||||||
OnTradeExecuted?.Invoke(trade);
|
OnTradeExecuted?.Invoke(trade);
|
||||||
OnStatusChanged?.Invoke();
|
OnStatusChanged?.Invoke();
|
||||||
|
|
||||||
|
// Save immediately after trade
|
||||||
|
await SaveDataAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteSell(string symbol, decimal price, decimal amount, AssetConfiguration config)
|
private async Task ExecuteSellAsync(string symbol, decimal price, decimal amount, AssetConfiguration config)
|
||||||
{
|
{
|
||||||
var amountUSD = amount * price;
|
var amountUSD = amount * price;
|
||||||
var profit = (price - config.AverageEntryPrice) * amount;
|
var profit = (price - config.AverageEntryPrice) * amount;
|
||||||
@@ -330,19 +445,35 @@ public class TradingBotService
|
|||||||
};
|
};
|
||||||
|
|
||||||
_trades.Add(trade);
|
_trades.Add(trade);
|
||||||
|
_activePositions.Remove(symbol);
|
||||||
UpdateAssetStatistics(symbol, trade, profit);
|
UpdateAssetStatistics(symbol, trade, profit);
|
||||||
|
|
||||||
Status.TradesExecuted++;
|
Status.TradesExecuted++;
|
||||||
|
|
||||||
|
_loggingService.LogTrade(
|
||||||
|
symbol,
|
||||||
|
$"SELL {amount:F6} {symbol} @ ${price:N2}",
|
||||||
|
$"Value: ${amountUSD:N2} | Profit: ${profit:N2} | Balance: ${config.CurrentBalance:N2}");
|
||||||
|
|
||||||
OnTradeExecuted?.Invoke(trade);
|
OnTradeExecuted?.Invoke(trade);
|
||||||
OnStatusChanged?.Invoke();
|
OnStatusChanged?.Invoke();
|
||||||
|
|
||||||
|
// Save immediately after trade
|
||||||
|
await SaveDataAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateIndicators(string symbol)
|
private void UpdateIndicators(string symbol)
|
||||||
{
|
{
|
||||||
var history = _priceHistory[symbol];
|
if (!_priceHistory.TryGetValue(symbol, out var history) || history == null || history.Count < 26)
|
||||||
if (history.Count < 26) return;
|
return;
|
||||||
|
|
||||||
var prices = history.Select(p => p.Price).ToList();
|
var prices = history
|
||||||
|
.Where(p => p != null && p.Price > 0)
|
||||||
|
.Select(p => p.Price)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (prices.Count < 26)
|
||||||
|
return;
|
||||||
|
|
||||||
var rsi = TechnicalAnalysis.CalculateRSI(prices);
|
var rsi = TechnicalAnalysis.CalculateRSI(prices);
|
||||||
var (macd, signal, histogram) = TechnicalAnalysis.CalculateMACD(prices);
|
var (macd, signal, histogram) = TechnicalAnalysis.CalculateMACD(prices);
|
||||||
@@ -359,6 +490,136 @@ public class TradingBotService
|
|||||||
|
|
||||||
_indicators[symbol] = indicators;
|
_indicators[symbol] = indicators;
|
||||||
OnIndicatorsUpdated?.Invoke(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)
|
private void UpdateAssetStatistics(string symbol, Trade trade, decimal? realizedProfit = null)
|
||||||
@@ -482,7 +743,54 @@ public class TradingBotService
|
|||||||
|
|
||||||
public MarketPrice? GetLatestPrice(string symbol)
|
public MarketPrice? GetLatestPrice(string symbol)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(symbol))
|
||||||
|
return null;
|
||||||
|
|
||||||
var history = GetPriceHistory(symbol);
|
var history = GetPriceHistory(symbol);
|
||||||
return history?.LastOrDefault();
|
return history?.LastOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ClearAllDataAsync()
|
||||||
|
{
|
||||||
|
_trades.Clear();
|
||||||
|
_activePositions.Clear();
|
||||||
|
_historyService.ClearAll();
|
||||||
|
|
||||||
|
foreach (var config in _assetConfigs.Values)
|
||||||
|
{
|
||||||
|
config.CurrentBalance = config.InitialBalance;
|
||||||
|
config.CurrentHoldings = 0;
|
||||||
|
config.AverageEntryPrice = 0;
|
||||||
|
config.DailyTradeCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
OnStatusChanged?.Invoke();
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,138 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
|
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
|
||||||
</PropertyGroup>
|
<!-- Docker Publishing -->
|
||||||
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
<DockerfileContext>.</DockerfileContext>
|
||||||
|
<!--
|
||||||
|
Versioning - Semantic Versioning (https://semver.org/)
|
||||||
|
Format: MAJOR.MINOR.PATCH
|
||||||
|
- MAJOR: Breaking changes (incompatible API changes)
|
||||||
|
- MINOR: New features (backward-compatible)
|
||||||
|
- PATCH: Bug fixes (backward-compatible)
|
||||||
|
|
||||||
|
Esempio workflow:
|
||||||
|
- 1.0.0 -> 1.0.1 (bug fix)
|
||||||
|
- 1.0.1 -> 1.1.0 (new feature)
|
||||||
|
- 1.1.0 -> 2.0.0 (breaking change)
|
||||||
|
-->
|
||||||
|
<Version>1.5.2</Version>
|
||||||
|
<AssemblyVersion>1.5.2.0</AssemblyVersion>
|
||||||
|
<FileVersion>1.5.2.0</FileVersion>
|
||||||
|
<!-- Assembly Information -->
|
||||||
|
<Product>TradingBot</Product>
|
||||||
|
<Description>Automated Crypto Trading Bot with Blazor UI</Description>
|
||||||
|
<Copyright>Copyright © 2024 Alby96</Copyright>
|
||||||
|
<Company>Alby96</Company>
|
||||||
|
<Authors>Alby96</Authors>
|
||||||
|
<!-- Build Metadata -->
|
||||||
|
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
|
||||||
|
<Deterministic>true</Deterministic>
|
||||||
|
<InformationalVersion>$(Version)+$(GITHUB_SHA)</InformationalVersion>
|
||||||
|
<!-- Gitea Registry -->
|
||||||
|
<ContainerRegistry>gitea.encke-hake.ts.net</ContainerRegistry>
|
||||||
|
<ContainerRepository>alby96/encelado/tradingbot</ContainerRepository>
|
||||||
|
</PropertyGroup>
|
||||||
|
<!-- Folders for organization -->
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Properties\PublishProfiles\" />
|
||||||
|
<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>
|
</Project>
|
||||||
@@ -1,517 +0,0 @@
|
|||||||
# ?? DEPLOYMENT GUIDE - Unraid + Gitea + Docker
|
|
||||||
|
|
||||||
Guida completa per deployare TradingBot su **Unraid** usando **Gitea** come sistema di controllo versione.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? PREREQUISITI
|
|
||||||
|
|
||||||
### Su Unraid
|
|
||||||
- ? Docker installato (Community Applications)
|
|
||||||
- ? Gitea installato e configurato
|
|
||||||
- ? Accesso SSH abilitato
|
|
||||||
- ? Portainer installato (opzionale ma consigliato)
|
|
||||||
|
|
||||||
### Sul PC di Sviluppo
|
|
||||||
- ? Git installato
|
|
||||||
- ? Accesso al server Unraid
|
|
||||||
- ? Repository Gitea configurato
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? STEP 1: Configurazione Gitea
|
|
||||||
|
|
||||||
### 1.1 Crea Repository su Gitea
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Accedi a Gitea (esempio)
|
|
||||||
http://192.168.30.23:3000
|
|
||||||
|
|
||||||
# Crea nuovo repository
|
|
||||||
Nome: TradingBot
|
|
||||||
Descrizione: Automated Crypto Trading Bot
|
|
||||||
Privato: ? (consigliato)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.2 Configura Git Remote (già fatto)
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cd /path/to/TradingBot
|
|
||||||
|
|
||||||
# Verifica remote (dovresti già averlo)
|
|
||||||
git remote -v
|
|
||||||
# Output:
|
|
||||||
# origin https://192.168.30.23/Alby96/Encelado (fetch)
|
|
||||||
# origin https://192.168.30.23/Alby96/Encelado (push)
|
|
||||||
|
|
||||||
# Se non configurato:
|
|
||||||
git remote add origin https://192.168.30.23/Alby96/Encelado
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.3 Push del Codice
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Commit delle modifiche Docker
|
|
||||||
git add Dockerfile docker-compose.yml .dockerignore
|
|
||||||
git add build-docker.sh build-docker.bat
|
|
||||||
git add UNRAID_DEPLOYMENT.md
|
|
||||||
|
|
||||||
git commit -m "Add Docker support and Unraid deployment"
|
|
||||||
|
|
||||||
# Push su Gitea
|
|
||||||
git push origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? STEP 2: Deployment su Unraid
|
|
||||||
|
|
||||||
### Metodo A: Via Portainer (CONSIGLIATO)
|
|
||||||
|
|
||||||
#### 2.1 Accedi a Portainer
|
|
||||||
```
|
|
||||||
http://[UNRAID-IP]:9000
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.2 Crea Stack
|
|
||||||
1. **Stacks** ? **Add stack**
|
|
||||||
2. **Name**: `tradingbot`
|
|
||||||
3. **Build method**: `Git Repository`
|
|
||||||
4. **Repository URL**: `https://192.168.30.23/Alby96/Encelado`
|
|
||||||
5. **Repository reference**: `refs/heads/main`
|
|
||||||
6. **Compose path**: `TradingBot/docker-compose.yml`
|
|
||||||
7. **Authentication**:
|
|
||||||
- Username: `Alby96`
|
|
||||||
- Personal access token: (crea su Gitea)
|
|
||||||
|
|
||||||
#### 2.3 Environment Variables (opzionali)
|
|
||||||
```
|
|
||||||
TZ=Europe/Rome
|
|
||||||
ASPNETCORE_ENVIRONMENT=Production
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.4 Deploy
|
|
||||||
Click **Deploy the stack**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Metodo B: Via SSH + Docker Compose
|
|
||||||
|
|
||||||
#### 2.1 Connettiti a Unraid via SSH
|
|
||||||
|
|
||||||
```sh
|
|
||||||
ssh root@[UNRAID-IP]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.2 Crea Directory Progetto
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Vai nella directory appropriata
|
|
||||||
cd /mnt/user/appdata/
|
|
||||||
|
|
||||||
# Crea directory per TradingBot
|
|
||||||
mkdir -p tradingbot
|
|
||||||
cd tradingbot
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.3 Clone Repository da Gitea
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Clone del repository
|
|
||||||
git clone https://192.168.30.23/Alby96/Encelado.git .
|
|
||||||
|
|
||||||
# Entra nella directory del progetto
|
|
||||||
cd TradingBot
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.4 Build e Run
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Build immagine Docker
|
|
||||||
docker-compose build
|
|
||||||
|
|
||||||
# Avvia container
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Verifica logs
|
|
||||||
docker-compose logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Metodo C: Via Unraid Docker Template
|
|
||||||
|
|
||||||
#### 2.1 Crea Template Personalizzato
|
|
||||||
|
|
||||||
Crea file: `/boot/config/plugins/dockerMan/templates-user/my-TradingBot.xml`
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<?xml version="1.0"?>
|
|
||||||
<Container version="2">
|
|
||||||
<Name>TradingBot</Name>
|
|
||||||
<Repository>tradingbot:latest</Repository>
|
|
||||||
<Registry>https://192.168.30.23:5000/</Registry>
|
|
||||||
<Network>bridge</Network>
|
|
||||||
<MyIP/>
|
|
||||||
<Shell>sh</Shell>
|
|
||||||
<Privileged>false</Privileged>
|
|
||||||
<Support>https://192.168.30.23/Alby96/Encelado</Support>
|
|
||||||
<Project>https://192.168.30.23/Alby96/Encelado</Project>
|
|
||||||
<Overview>Automated Crypto Trading Bot con strategie personalizzabili</Overview>
|
|
||||||
<Category>Tools:</Category>
|
|
||||||
<WebUI>http://[IP]:[PORT:8080]</WebUI>
|
|
||||||
<TemplateURL/>
|
|
||||||
<Icon>https://raw.githubusercontent.com/docker-library/docs/master/dotnet/logo.png</Icon>
|
|
||||||
<ExtraParams/>
|
|
||||||
<PostArgs/>
|
|
||||||
<CPUset/>
|
|
||||||
<DateInstalled>1234567890</DateInstalled>
|
|
||||||
<DonateText/>
|
|
||||||
<DonateLink/>
|
|
||||||
<Requires/>
|
|
||||||
<Config Name="WebUI Port" Target="8080" Default="8080" Mode="tcp" Description="Port per accedere alla WebUI" Type="Port" Display="always" Required="true" Mask="false">8080</Config>
|
|
||||||
<Config Name="Data Volume" Target="/app/data" Default="/mnt/user/appdata/tradingbot/data" Mode="rw" Description="Volume per dati persistenti" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/tradingbot/data</Config>
|
|
||||||
<Config Name="Timezone" Target="TZ" Default="Europe/Rome" Mode="" Description="Timezone" Type="Variable" Display="always" Required="false" Mask="false">Europe/Rome</Config>
|
|
||||||
</Container>
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2.2 Usa Template da Unraid UI
|
|
||||||
1. Docker ? Add Container
|
|
||||||
2. Select: `TradingBot`
|
|
||||||
3. Configure ports and volumes
|
|
||||||
4. Apply
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? STEP 3: Aggiornamenti Automatici
|
|
||||||
|
|
||||||
### 3.1 Setup Webhook su Gitea (opzionale)
|
|
||||||
|
|
||||||
#### Su Gitea:
|
|
||||||
```
|
|
||||||
Settings ? Webhooks ? Add Webhook
|
|
||||||
Payload URL: http://[UNRAID-IP]:9000/api/webhooks/[webhook-id]
|
|
||||||
Content type: application/json
|
|
||||||
Events: Push events
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Su Portainer:
|
|
||||||
```
|
|
||||||
Stacks ? tradingbot ? Webhooks ? Create webhook
|
|
||||||
Copia URL generato
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 Script di Aggiornamento Manuale
|
|
||||||
|
|
||||||
```sh
|
|
||||||
#!/bin/bash
|
|
||||||
# update-tradingbot.sh
|
|
||||||
|
|
||||||
cd /mnt/user/appdata/tradingbot/TradingBot
|
|
||||||
|
|
||||||
# Pull latest changes
|
|
||||||
git pull origin main
|
|
||||||
|
|
||||||
# Rebuild
|
|
||||||
docker-compose down
|
|
||||||
docker-compose build
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
echo "? TradingBot aggiornato!"
|
|
||||||
```
|
|
||||||
|
|
||||||
Salva come: `/root/scripts/update-tradingbot.sh`
|
|
||||||
|
|
||||||
```sh
|
|
||||||
chmod +x /root/scripts/update-tradingbot.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? STEP 4: Monitoraggio e Gestione
|
|
||||||
|
|
||||||
### 4.1 Verifica Status Container
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Via SSH
|
|
||||||
docker ps | grep tradingbot
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
docker logs tradingbot -f
|
|
||||||
|
|
||||||
# Stats
|
|
||||||
docker stats tradingbot
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 Accesso WebUI
|
|
||||||
|
|
||||||
```
|
|
||||||
http://[UNRAID-IP]:8080
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.3 Health Check
|
|
||||||
|
|
||||||
```sh
|
|
||||||
curl http://[UNRAID-IP]:8080/health
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? STEP 5: Configurazione Avanzata
|
|
||||||
|
|
||||||
### 5.1 Reverse Proxy (opzionale)
|
|
||||||
|
|
||||||
Se usi **Nginx Proxy Manager** o **Traefik**:
|
|
||||||
|
|
||||||
#### docker-compose.yml aggiornato:
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
tradingbot:
|
|
||||||
# ... altre configurazioni
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.http.routers.tradingbot.rule=Host(`trading.tuodominio.com`)"
|
|
||||||
- "traefik.http.services.tradingbot.loadbalancer.server.port=8080"
|
|
||||||
networks:
|
|
||||||
- traefik_proxy
|
|
||||||
- tradingbot-network
|
|
||||||
|
|
||||||
networks:
|
|
||||||
traefik_proxy:
|
|
||||||
external: true
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 Backup Automatico
|
|
||||||
|
|
||||||
Script backup: `/root/scripts/backup-tradingbot.sh`
|
|
||||||
|
|
||||||
```sh
|
|
||||||
#!/bin/bash
|
|
||||||
BACKUP_DIR="/mnt/user/backups/tradingbot"
|
|
||||||
DATE=$(date +%Y%m%d_%H%M%S)
|
|
||||||
|
|
||||||
mkdir -p $BACKUP_DIR
|
|
||||||
|
|
||||||
# Backup volume dati
|
|
||||||
docker run --rm \
|
|
||||||
-v tradingbot_tradingbot-data:/data \
|
|
||||||
-v $BACKUP_DIR:/backup \
|
|
||||||
alpine tar czf /backup/tradingbot-data-$DATE.tar.gz -C /data .
|
|
||||||
|
|
||||||
echo "? Backup completato: tradingbot-data-$DATE.tar.gz"
|
|
||||||
|
|
||||||
# Mantieni solo ultimi 7 backup
|
|
||||||
find $BACKUP_DIR -name "tradingbot-data-*.tar.gz" -mtime +7 -delete
|
|
||||||
```
|
|
||||||
|
|
||||||
Aggiungi a crontab:
|
|
||||||
```sh
|
|
||||||
crontab -e
|
|
||||||
# Backup giornaliero alle 3 AM
|
|
||||||
0 3 * * * /root/scripts/backup-tradingbot.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? TROUBLESHOOTING
|
|
||||||
|
|
||||||
### Container non si avvia
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Check logs
|
|
||||||
docker logs tradingbot
|
|
||||||
|
|
||||||
# Check network
|
|
||||||
docker network ls
|
|
||||||
docker network inspect tradingbot_tradingbot-network
|
|
||||||
|
|
||||||
# Rebuild da zero
|
|
||||||
docker-compose down -v
|
|
||||||
docker-compose build --no-cache
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problemi di permessi
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Fix ownership
|
|
||||||
docker exec tradingbot chown -R tradingbot:tradingbot /app/data
|
|
||||||
```
|
|
||||||
|
|
||||||
### Porta già in uso
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Trova processo che usa porta 8080
|
|
||||||
netstat -tulpn | grep 8080
|
|
||||||
|
|
||||||
# Cambia porta in docker-compose.yml
|
|
||||||
ports:
|
|
||||||
- "8081:8080" # Usa 8081 invece
|
|
||||||
```
|
|
||||||
|
|
||||||
### Out of Memory
|
|
||||||
|
|
||||||
Aumenta limits in docker-compose.yml:
|
|
||||||
```yaml
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 2G # Da 1G a 2G
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? STEP 6: Registry Privato (opzionale)
|
|
||||||
|
|
||||||
### 6.1 Setup Docker Registry su Unraid
|
|
||||||
|
|
||||||
```sh
|
|
||||||
docker run -d \
|
|
||||||
-p 5000:5000 \
|
|
||||||
--restart=always \
|
|
||||||
--name registry \
|
|
||||||
-v /mnt/user/appdata/registry:/var/lib/registry \
|
|
||||||
registry:2
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.2 Build e Push
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Tag image
|
|
||||||
docker tag tradingbot:latest 192.168.30.23:5000/tradingbot:latest
|
|
||||||
|
|
||||||
# Push to registry
|
|
||||||
docker push 192.168.30.23:5000/tradingbot:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.3 Deploy da Registry
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# docker-compose.yml
|
|
||||||
services:
|
|
||||||
tradingbot:
|
|
||||||
image: 192.168.30.23:5000/tradingbot:latest
|
|
||||||
# ... resto configurazione
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? SECURITY BEST PRACTICES
|
|
||||||
|
|
||||||
### 1. Non esporre porte pubblicamente
|
|
||||||
```sh
|
|
||||||
# Usa solo rete interna Unraid
|
|
||||||
# Accesso via VPN o Wireguard
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. SSL/TLS
|
|
||||||
```sh
|
|
||||||
# Usa reverse proxy con certificati SSL
|
|
||||||
# Let's Encrypt via Nginx Proxy Manager
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Credenziali
|
|
||||||
```sh
|
|
||||||
# Non committare secrets in Git
|
|
||||||
# Usa Docker secrets o environment variables
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Firewall
|
|
||||||
```sh
|
|
||||||
# Limita accesso solo a IP fidati
|
|
||||||
# Configura in Unraid Settings ? Network
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? MONITORING
|
|
||||||
|
|
||||||
### Grafana + Prometheus (opzionale)
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# monitoring-stack.yml
|
|
||||||
version: '3.8'
|
|
||||||
services:
|
|
||||||
prometheus:
|
|
||||||
image: prom/prometheus
|
|
||||||
volumes:
|
|
||||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
|
||||||
ports:
|
|
||||||
- "9090:9090"
|
|
||||||
|
|
||||||
grafana:
|
|
||||||
image: grafana/grafana
|
|
||||||
ports:
|
|
||||||
- "3001:3000"
|
|
||||||
volumes:
|
|
||||||
- grafana-data:/var/lib/grafana
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
grafana-data:
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? CHECKLIST DEPLOYMENT
|
|
||||||
|
|
||||||
### Pre-Deployment
|
|
||||||
- [ ] Codice committed su Gitea
|
|
||||||
- [ ] Dockerfile testato localmente
|
|
||||||
- [ ] docker-compose.yml configurato
|
|
||||||
- [ ] .dockerignore presente
|
|
||||||
- [ ] Environment variables definite
|
|
||||||
|
|
||||||
### Deployment
|
|
||||||
- [ ] Repository clonato su Unraid
|
|
||||||
- [ ] Docker image built
|
|
||||||
- [ ] Container avviato correttamente
|
|
||||||
- [ ] WebUI accessibile
|
|
||||||
- [ ] Health check passing
|
|
||||||
|
|
||||||
### Post-Deployment
|
|
||||||
- [ ] Logs verificati (no errori)
|
|
||||||
- [ ] Dati persistono dopo restart
|
|
||||||
- [ ] Backup configurato
|
|
||||||
- [ ] Monitoring attivo
|
|
||||||
- [ ] Documentazione aggiornata
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? COMANDI UTILI
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# Build
|
|
||||||
./build-docker.sh [tag]
|
|
||||||
|
|
||||||
# Start
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Stop
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# Restart
|
|
||||||
docker-compose restart
|
|
||||||
|
|
||||||
# Update
|
|
||||||
git pull && docker-compose up -d --build
|
|
||||||
|
|
||||||
# Clean
|
|
||||||
docker-compose down -v
|
|
||||||
docker system prune -a
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ?? RISORSE
|
|
||||||
|
|
||||||
- **Unraid Docs**: https://docs.unraid.net/
|
|
||||||
- **Docker Docs**: https://docs.docker.com/
|
|
||||||
- **Gitea Docs**: https://docs.gitea.io/
|
|
||||||
- **Portainer Docs**: https://docs.portainer.io/
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**?? Deployment completato! Il tuo TradingBot è ora in produzione su Unraid!**
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Information"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
@echo off
|
|
||||||
REM Script di build Docker per Windows
|
|
||||||
|
|
||||||
echo Building TradingBot Docker Image...
|
|
||||||
|
|
||||||
SET IMAGE_NAME=tradingbot
|
|
||||||
SET TAG=%1
|
|
||||||
IF "%TAG%"=="" SET TAG=latest
|
|
||||||
|
|
||||||
echo Building image: %IMAGE_NAME%:%TAG%
|
|
||||||
|
|
||||||
docker build -t %IMAGE_NAME%:%TAG% -f Dockerfile .
|
|
||||||
|
|
||||||
IF %ERRORLEVEL% NEQ 0 (
|
|
||||||
echo Build failed!
|
|
||||||
exit /b %ERRORLEVEL%
|
|
||||||
)
|
|
||||||
|
|
||||||
echo Build completed successfully!
|
|
||||||
|
|
||||||
REM Tag come latest se diverso
|
|
||||||
IF NOT "%TAG%"=="latest" (
|
|
||||||
echo Tagging as latest...
|
|
||||||
docker tag %IMAGE_NAME%:%TAG% %IMAGE_NAME%:latest
|
|
||||||
)
|
|
||||||
|
|
||||||
echo Done! Run with: docker-compose up -d
|
|
||||||
|
|
||||||
pause
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Script di build Docker per TradingBot
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "?? Building TradingBot Docker Image..."
|
|
||||||
|
|
||||||
# Colori per output
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
RED='\033[0;31m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Variabili
|
|
||||||
IMAGE_NAME="tradingbot"
|
|
||||||
TAG="${1:-latest}"
|
|
||||||
REGISTRY="${DOCKER_REGISTRY:-}" # Opzionale: tuo registry privato
|
|
||||||
|
|
||||||
echo -e "${BLUE}?? Building image: ${IMAGE_NAME}:${TAG}${NC}"
|
|
||||||
|
|
||||||
# Build dell'immagine
|
|
||||||
docker build \
|
|
||||||
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
|
|
||||||
--build-arg VCS_REF=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") \
|
|
||||||
-t ${IMAGE_NAME}:${TAG} \
|
|
||||||
-f Dockerfile \
|
|
||||||
.
|
|
||||||
|
|
||||||
echo -e "${GREEN}? Build completato con successo!${NC}"
|
|
||||||
|
|
||||||
# Tag con latest
|
|
||||||
if [ "$TAG" != "latest" ]; then
|
|
||||||
echo -e "${BLUE}??? Tagging as latest...${NC}"
|
|
||||||
docker tag ${IMAGE_NAME}:${TAG} ${IMAGE_NAME}:latest
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Mostra info immagine
|
|
||||||
echo -e "${BLUE}?? Image info:${NC}"
|
|
||||||
docker images | grep ${IMAGE_NAME} | head -n 2
|
|
||||||
|
|
||||||
# Se registry è configurato, push
|
|
||||||
if [ ! -z "$REGISTRY" ]; then
|
|
||||||
echo -e "${BLUE}?? Pushing to registry: ${REGISTRY}${NC}"
|
|
||||||
docker tag ${IMAGE_NAME}:${TAG} ${REGISTRY}/${IMAGE_NAME}:${TAG}
|
|
||||||
docker push ${REGISTRY}/${IMAGE_NAME}:${TAG}
|
|
||||||
echo -e "${GREEN}? Pushed to registry!${NC}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -e "${GREEN}?? Done! Run with: docker-compose up -d${NC}"
|
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,566 @@
|
|||||||
|
# ?? TradingBot - Installazione su Unraid (Senza Portainer)
|
||||||
|
|
||||||
|
## ? Installazione Diretta da Gitea Registry
|
||||||
|
|
||||||
|
Puoi installare TradingBot direttamente dall'Unraid Docker Manager usando il tuo Gitea Registry!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ?? PREREQUISITI
|
||||||
|
|
||||||
|
### 1. Login Gitea Registry su Unraid
|
||||||
|
|
||||||
|
SSH su Unraid:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@192.168.30.23 # O IP Tailscale
|
||||||
|
|
||||||
|
# Login al Gitea Registry
|
||||||
|
docker login gitea.encke-hake.ts.net
|
||||||
|
|
||||||
|
# Username: Alby96
|
||||||
|
# Password: [Personal Access Token Gitea]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output atteso**:
|
||||||
|
```
|
||||||
|
Login Succeeded ?
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ?? METODO 1: Template XML (Consigliato)
|
||||||
|
|
||||||
|
### Step 1: Copia Template su Unraid
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Su Unraid
|
||||||
|
mkdir -p /boot/config/plugins/dockerMan/templates-user
|
||||||
|
|
||||||
|
# Scarica template
|
||||||
|
wget -O /boot/config/plugins/dockerMan/templates-user/TradingBot.xml \
|
||||||
|
https://gitea.encke-hake.ts.net/Alby96/Encelado/raw/branch/main/TradingBot/deployment/unraid-template.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Installa Container
|
||||||
|
|
||||||
|
1. Unraid WebUI ? **Docker** tab
|
||||||
|
2. Click **Add Container** (in fondo)
|
||||||
|
3. **Template**: Dropdown ? Seleziona **TradingBot**
|
||||||
|
4. Configura parametri:
|
||||||
|
|
||||||
|
**Parametri Base**:
|
||||||
|
- **Name**: `TradingBot` (già impostato)
|
||||||
|
- **Repository**: `gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest` (già impostato)
|
||||||
|
|
||||||
|
**Porta WebUI** (Visibile e Configurabile!):
|
||||||
|
- **WebUI HTTP Port**: `8888` (porta default - cambia se occupata)
|
||||||
|
- Questa è la porta HOST per accedere all'interfaccia web
|
||||||
|
- La porta CONTAINER rimane sempre 8080 (non modificare)
|
||||||
|
- Alternative comuni se 8888 occupata: `8881`, `9999`, `7777`
|
||||||
|
|
||||||
|
**Volume Dati** (?? IMPORTANTE per persistenza!):
|
||||||
|
- **AppData**: `/mnt/user/appdata/tradingbot` (già impostato)
|
||||||
|
- Questo volume salva:
|
||||||
|
- Trade history (`trade-history.json`)
|
||||||
|
- Posizioni attive (`active-positions.json`)
|
||||||
|
- Settings applicazione (`settings.json`)
|
||||||
|
- ? I dati sopravvivono a restart/update del container
|
||||||
|
|
||||||
|
**Variabili Ambiente** (Avanzate - espandi se necessario):
|
||||||
|
- **ASPNETCORE_ENVIRONMENT**: `Production` (non modificare)
|
||||||
|
- **ASPNETCORE_URLS**: `http://+:8080` (non modificare - porta interna container)
|
||||||
|
- **TZ**: `Europe/Rome` (cambia per altro timezone)
|
||||||
|
|
||||||
|
5. Click **Apply**
|
||||||
|
|
||||||
|
Unraid farà:
|
||||||
|
- ? Pull immagine da Gitea Registry
|
||||||
|
- ? Crea container con nome "TradingBot"
|
||||||
|
- ? Configura porta WebUI (default 8888 ? host, 8080 ? container)
|
||||||
|
- ? **Crea volume persistente per dati**
|
||||||
|
- ? Start automatico
|
||||||
|
|
||||||
|
### Step 3: Accedi WebUI
|
||||||
|
|
||||||
|
**Metodo A: Click su WebUI Icon** ??
|
||||||
|
|
||||||
|
1. **Docker tab** ? Trova container **TradingBot**
|
||||||
|
2. Nella riga del container, a destra, vedrai l'**icona globe** ??
|
||||||
|
3. Click sull'icona ? Si apre automaticamente `http://192.168.30.23:8888`
|
||||||
|
|
||||||
|
**Metodo B: URL Manuale**
|
||||||
|
|
||||||
|
```
|
||||||
|
http://192.168.30.23:8888
|
||||||
|
```
|
||||||
|
|
||||||
|
(Sostituisci `8888` con la porta HOST che hai configurato)
|
||||||
|
|
||||||
|
?? **IMPORTANTE**: La porta nel browser deve essere quella HOST (8888 default), NON la porta container (8080)
|
||||||
|
|
||||||
|
Dovresti vedere la **Dashboard TradingBot**! ??
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ?? PERSISTENZA DATI
|
||||||
|
|
||||||
|
### Come Funziona
|
||||||
|
|
||||||
|
TradingBot salva automaticamente tutti i dati in `/app/data` dentro il container, che viene mappato sul volume host `/mnt/user/appdata/tradingbot`.
|
||||||
|
|
||||||
|
**File salvati automaticamente**:
|
||||||
|
```
|
||||||
|
/mnt/user/appdata/tradingbot/
|
||||||
|
??? trade-history.json # Storia completa trade
|
||||||
|
??? active-positions.json # Posizioni attualmente aperte
|
||||||
|
??? settings.json # Impostazioni applicazione
|
||||||
|
```
|
||||||
|
|
||||||
|
**Salvataggio automatico**:
|
||||||
|
- ? Ogni 30 secondi (mentre bot running)
|
||||||
|
- ?? Immediato dopo ogni trade eseguito
|
||||||
|
- ?? On-stop quando fermi il bot
|
||||||
|
- ?? Graceful shutdown su Docker stop/restart
|
||||||
|
|
||||||
|
### Benefici
|
||||||
|
|
||||||
|
? **Zero perdita dati** - Anche in caso di crash
|
||||||
|
? **Restore automatico** - Stato ripristinato al riavvio
|
||||||
|
? **Update sicuri** - Dati preservati durante aggiornamenti
|
||||||
|
? **Backup facile** - Basta copiare la cartella appdata
|
||||||
|
|
||||||
|
### Backup Dati
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup manuale
|
||||||
|
tar -czf tradingbot-backup-$(date +%Y%m%d).tar.gz \
|
||||||
|
/mnt/user/appdata/tradingbot
|
||||||
|
|
||||||
|
# Restore
|
||||||
|
tar -xzf tradingbot-backup-20241221.tar.gz \
|
||||||
|
-C /mnt/user/appdata/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Gestione Dati (via WebUI)
|
||||||
|
|
||||||
|
Vai su **Settings** ? **Dati Persistenti**:
|
||||||
|
- Visualizza numero trade salvati
|
||||||
|
- Visualizza dimensione dati
|
||||||
|
- Visualizza posizioni attive
|
||||||
|
- **Cancella tutti i dati** (con conferma)
|
||||||
|
|
||||||
|
?? **Nota**: Puoi cancellare i dati solo se il bot è fermo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ?? METODO 2: Installazione Manuale
|
||||||
|
|
||||||
|
Se preferisci non usare template:
|
||||||
|
|
||||||
|
### Step 1: Unraid Docker Tab
|
||||||
|
|
||||||
|
1. **Docker** ? **Add Container**
|
||||||
|
2. **Name**: `TradingBot`
|
||||||
|
|
||||||
|
### Step 2: Configurazione Base
|
||||||
|
|
||||||
|
**Repository**:
|
||||||
|
```
|
||||||
|
gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**Network Type**: `Bridge`
|
||||||
|
|
||||||
|
**Console shell command**: `Shell`
|
||||||
|
|
||||||
|
### Step 3: Port Mapping (?? CRITICO!)
|
||||||
|
|
||||||
|
Click **Add another Path, Port, Variable, Label or Device**
|
||||||
|
|
||||||
|
**Config Type**: `Port`
|
||||||
|
- **Name**: `WebUI`
|
||||||
|
- **Container Port**: `8080`
|
||||||
|
- **Host Port**: `8888` ? **Cambia questa se occupata!**
|
||||||
|
- **Connection Type**: `TCP`
|
||||||
|
|
||||||
|
?? **Se questo mapping non viene configurato, la WebUI non sarà accessibile!**
|
||||||
|
|
||||||
|
### Step 4: Volume Mapping (?? IMPORTANTE per persistenza!)
|
||||||
|
|
||||||
|
Click **Add another Path, Port, Variable, Label o Device**
|
||||||
|
|
||||||
|
**Config Type**: `Path`
|
||||||
|
- **Name**: `AppData`
|
||||||
|
- **Container Path**: `/app/data`
|
||||||
|
- **Host Path**: `/mnt/user/appdata/tradingbot`
|
||||||
|
- **Access Mode**: `Read/Write`
|
||||||
|
|
||||||
|
### Step 5: Environment Variables
|
||||||
|
|
||||||
|
**ASPNETCORE_ENVIRONMENT**:
|
||||||
|
- **Name**: `ASPNETCORE_ENVIRONMENT`
|
||||||
|
- **Value**: `Production`
|
||||||
|
|
||||||
|
**ASPNETCORE_URLS**:
|
||||||
|
- **Name**: `ASPNETCORE_URLS`
|
||||||
|
- **Value**: `http://+:8080`
|
||||||
|
|
||||||
|
**TZ** (Opzionale):
|
||||||
|
- **Name**: `TZ`
|
||||||
|
- **Value**: `Europe/Rome` (o tuo timezone)
|
||||||
|
|
||||||
|
### Step 6: Apply
|
||||||
|
|
||||||
|
Click **Apply** in fondo alla pagina.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ?? AGGIORNAMENTO CONTAINER
|
||||||
|
|
||||||
|
### Via Unraid Docker Tab
|
||||||
|
|
||||||
|
1. **Docker** ? Trova **TradingBot**
|
||||||
|
2. Click **icona Stop** (ferma container)
|
||||||
|
3. Click **Force Update** (icona update con freccia circolare)
|
||||||
|
4. Attendi pull dell'immagine aggiornata
|
||||||
|
5. Click **icona Start** (avvia container)
|
||||||
|
|
||||||
|
Unraid farà:
|
||||||
|
- ? Pull ultima immagine da Gitea
|
||||||
|
- ? Ricrea container con nuova immagine
|
||||||
|
- ? **Mantiene dati persistenti** (volume non viene toccato)
|
||||||
|
- ? Mantiene configurazione (porta, variabili, etc.)
|
||||||
|
|
||||||
|
?? **I tuoi trade e impostazioni sono al sicuro durante gli update!**
|
||||||
|
|
||||||
|
### Automatico con User Scripts Plugin
|
||||||
|
|
||||||
|
Installa **User Scripts** plugin:
|
||||||
|
|
||||||
|
1. **Apps** ? Cerca "User Scripts"
|
||||||
|
2. Installa
|
||||||
|
|
||||||
|
Crea script update:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# Nome: Update TradingBot
|
||||||
|
# Schedula: Weekly (ogni domenica alle 3:00 AM)
|
||||||
|
|
||||||
|
# Stop container
|
||||||
|
docker stop TradingBot
|
||||||
|
|
||||||
|
# Pull latest image
|
||||||
|
docker pull gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest
|
||||||
|
|
||||||
|
# Start container (Unraid ricrea automaticamente)
|
||||||
|
docker start TradingBot
|
||||||
|
|
||||||
|
# Notifica
|
||||||
|
/usr/local/emhttp/webGui/scripts/notify -s "TradingBot Update" -d "Container aggiornato con successo!" -i "normal"
|
||||||
|
|
||||||
|
echo "Update completato alle $(date)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Schedula: Settimanale o manualmente quando serve.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ??? CONFIGURAZIONE PORTA
|
||||||
|
|
||||||
|
### Cambiare Porta WebUI
|
||||||
|
|
||||||
|
La porta **default è 8888** (host) ? **8080** (container).
|
||||||
|
|
||||||
|
Se la porta 8888 è occupata o vuoi usarne un'altra:
|
||||||
|
|
||||||
|
#### **Via Template (Prima Installazione)**
|
||||||
|
|
||||||
|
Durante Step 2 dell'installazione:
|
||||||
|
- **WebUI HTTP Port**: Cambia da `8888` a porta desiderata (es. `8881`, `9999`, `7777`)
|
||||||
|
- ?? Modifica SOLO la porta HOST (a sinistra)
|
||||||
|
- NON modificare la porta Container (deve restare 8080)
|
||||||
|
|
||||||
|
#### **Via Edit (Container Esistente)**
|
||||||
|
|
||||||
|
1. **Docker tab** ? Container **TradingBot**
|
||||||
|
2. Click **Edit** (icona matita/wrench)
|
||||||
|
3. Trova sezione **Port Mappings**
|
||||||
|
4. Vedrai: **Host Port** `8888` ? **Container Port** `8080`
|
||||||
|
5. Modifica **Host Port** (es. da `8888` a `8881`)
|
||||||
|
6. **IMPORTANTE**: NON modificare **Container Port** (deve restare `8080`)
|
||||||
|
7. Click **Apply** in fondo
|
||||||
|
8. Container si riavvierà automaticamente
|
||||||
|
|
||||||
|
#### **Accesso con Nuova Porta**
|
||||||
|
|
||||||
|
```
|
||||||
|
http://192.168.30.23:NUOVA_PORTA_HOST
|
||||||
|
```
|
||||||
|
|
||||||
|
Esempio con porta `8881`:
|
||||||
|
```
|
||||||
|
http://192.168.30.23:8881
|
||||||
|
```
|
||||||
|
|
||||||
|
### Porte Comuni Disponibili
|
||||||
|
|
||||||
|
Se `8888` è occupata, prova queste alternative:
|
||||||
|
|
||||||
|
| Porta | Uso Comune | Probabilità Libera |
|
||||||
|
|-------|------------|-------------------|
|
||||||
|
| `8881` | Alternative port | ????? Alta |
|
||||||
|
| `9999` | Generic services | ???? Alta |
|
||||||
|
| `7777` | Custom apps | ???? Alta |
|
||||||
|
| `8889` | Next to 8888 | ??? Media |
|
||||||
|
| `3000` | Dev servers | ?? Bassa (spesso occupata) |
|
||||||
|
| `8080` | ? NON usare | Troppo comune, quasi sempre occupata |
|
||||||
|
|
||||||
|
**Check porta disponibile**:
|
||||||
|
```bash
|
||||||
|
# Su Unraid via SSH
|
||||||
|
netstat -tulpn | grep :8888
|
||||||
|
# Se restituisce risultato ? porta occupata
|
||||||
|
# Se vuoto ? porta libera ?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Differenza HOST vs CONTAINER Port
|
||||||
|
|
||||||
|
?? **IMPORTANTE da capire**:
|
||||||
|
|
||||||
|
```
|
||||||
|
HOST Port (8888) ? CONTAINER Port (8080)
|
||||||
|
?? Porta su Unraid ?? Porta interna Docker
|
||||||
|
?? Quella nel BROWSER ?? Fissa, NON modificare
|
||||||
|
?? Configurabile ?? Hardcoded nell'app
|
||||||
|
?? Esempio: 8888 ?? Sempre 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
**Esempio configurazione corretta**:
|
||||||
|
```
|
||||||
|
Browser: http://192.168.30.23:8888
|
||||||
|
?? Usa porta HOST
|
||||||
|
|
||||||
|
Docker: 8888 (host) ? 8080 (container)
|
||||||
|
?? Mapping ?? App interna
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cosa NON fare**:
|
||||||
|
- ? Cambiare porta Container da 8080 a altro
|
||||||
|
- ? Modificare `ASPNETCORE_URLS` (deve restare `http://+:8080`)
|
||||||
|
- ? Usare porta Host 8080 (conflitto con container)
|
||||||
|
|
||||||
|
**Cosa puoi fare**:
|
||||||
|
- ? Cambiare porta Host da 8888 a qualsiasi altra libera
|
||||||
|
- ? Usare porta Host diversa per ogni app
|
||||||
|
- ? Accedere con `http://IP:PORTA_HOST`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ?? QUICK START COMPLETO
|
||||||
|
|
||||||
|
**Setup in 3 minuti**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Login (una volta)
|
||||||
|
docker login gitea.encke-hake.ts.net
|
||||||
|
|
||||||
|
# 2. Download template
|
||||||
|
wget -O /boot/config/plugins/dockerMan/templates-user/TradingBot.xml \
|
||||||
|
https://gitea.encke-hake.ts.net/Alby96/Encelado/raw/branch/main/TradingBot/deployment/unraid-template.xml
|
||||||
|
|
||||||
|
# 3. Install via UI
|
||||||
|
# Docker tab ? Add Container ? TradingBot template ? Apply
|
||||||
|
|
||||||
|
# 4. Access WebUI
|
||||||
|
# Metodo A: Click icona ?? nella Docker tab
|
||||||
|
# Metodo B: http://192.168.30.23:8888
|
||||||
|
```
|
||||||
|
|
||||||
|
**?? TradingBot pronto su Unraid!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ?? Nota sulla Porta
|
||||||
|
|
||||||
|
**Default**: Porta HOST `8888` (invece di 8080)
|
||||||
|
|
||||||
|
**Perché 8888?**
|
||||||
|
- Porta 8080 è troppo comune e spesso occupata
|
||||||
|
- 8888 è quasi sempre libera su Unraid
|
||||||
|
- Facile da ricordare (quattro 8)
|
||||||
|
- WebUI icon funziona automaticamente
|
||||||
|
|
||||||
|
**Se 8888 è occupata**: Cambia in fase di installazione o dopo via Edit
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ?? ACCESSO WEBUI
|
||||||
|
|
||||||
|
### Locale (Unraid LAN)
|
||||||
|
|
||||||
|
```
|
||||||
|
http://192.168.30.23:8888
|
||||||
|
```
|
||||||
|
|
||||||
|
Sostituisci:
|
||||||
|
- `192.168.30.23` con IP del tuo Unraid
|
||||||
|
- `8888` con porta HOST configurata (se diversa)
|
||||||
|
|
||||||
|
### Via Tailscale
|
||||||
|
|
||||||
|
Se hai configurato Tailscale su Unraid:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://unraid.encke-hake.ts.net:8888
|
||||||
|
```
|
||||||
|
|
||||||
|
### Via Hostname Unraid
|
||||||
|
|
||||||
|
Se hai configurato hostname:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://tower:8888
|
||||||
|
```
|
||||||
|
|
||||||
|
(Sostituisci `tower` con hostname del tuo Unraid e `8888` con porta configurata)
|
||||||
|
|
||||||
|
### Reverse Proxy (Accesso HTTPS)
|
||||||
|
|
||||||
|
Se usi **Nginx Proxy Manager** o **Swag**:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
# Nginx Proxy Manager
|
||||||
|
Upstream: http://192.168.30.23:8888
|
||||||
|
Domain: tradingbot.tuo-dominio.com
|
||||||
|
SSL: Let's Encrypt
|
||||||
|
```
|
||||||
|
|
||||||
|
Poi accedi via:
|
||||||
|
```
|
||||||
|
https://tradingbot.tuo-dominio.com
|
||||||
|
```
|
||||||
|
|
||||||
|
?? **Nota**: Il reverse proxy si connette alla porta HOST (8888), non container (8080)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ?? SICUREZZA
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
|
||||||
|
? **Porta non esposta pubblicamente** (solo LAN o VPN)
|
||||||
|
? **Volume dati protetto** (`/mnt/user/appdata/tradingbot/`)
|
||||||
|
? **Registry privato** (Gitea richiede login)
|
||||||
|
? **Certificati validi** (Tailscale)
|
||||||
|
? **User non-root** (già configurato nel Dockerfile)
|
||||||
|
? **Dati persistenti** backup-ready
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ?? CHECKLIST INSTALLAZIONE
|
||||||
|
|
||||||
|
### Pre-Install
|
||||||
|
- [ ] Unraid 6.10+ installato
|
||||||
|
- [ ] Docker service attivo
|
||||||
|
- [ ] Porta 8888 (o alternativa) disponibile
|
||||||
|
- [ ] `docker login gitea.encke-hake.ts.net` successful
|
||||||
|
- [ ] Internet attivo per pull immagine
|
||||||
|
|
||||||
|
### Install
|
||||||
|
- [ ] Template XML scaricato su Unraid
|
||||||
|
- [ ] Container creato da template
|
||||||
|
- [ ] Porta WebUI configurata (8888 host ? 8080 container)
|
||||||
|
- [ ] Volume AppData creato (`/mnt/user/appdata/tradingbot`)
|
||||||
|
- [ ] Container status: **running**
|
||||||
|
|
||||||
|
### Post-Install
|
||||||
|
- [ ] WebUI accessibile (http://IP:8888)
|
||||||
|
- [ ] Dashboard carica correttamente
|
||||||
|
- [ ] Settings modificabili e salvabili
|
||||||
|
- [ ] Bot avviabile dalla UI
|
||||||
|
- [ ] Trade vengono salvati automaticamente
|
||||||
|
- [ ] Dati persistono dopo restart
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ?? VANTAGGI UNRAID NATIVO
|
||||||
|
|
||||||
|
? **Zero dipendenze** (no Portainer, no docker-compose)
|
||||||
|
? **WebUI Unraid integrata** (gestione familiare)
|
||||||
|
? **Auto-start** (container parte con Unraid)
|
||||||
|
? **Backup integrato** (con plugin CA)
|
||||||
|
? **Update semplice** (2 click: Stop ? Update ? Start)
|
||||||
|
? **Template riutilizzabile** (reinstall in 1 minuto)
|
||||||
|
? **Dati persistenti** (trade e settings sopravvivono)
|
||||||
|
? **Logs accessibili** dalla UI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ?? WORKFLOW COMPLETO
|
||||||
|
|
||||||
|
### Sviluppo (PC)
|
||||||
|
```
|
||||||
|
1. ?? Visual Studio ? Codice
|
||||||
|
2. ?? Build ? Publish (Docker profile)
|
||||||
|
3. ? Automatico: Push Gitea Registry
|
||||||
|
?? Tags: latest, 1.2.0, 1.2.0-YYYYMMDD
|
||||||
|
4. ?? git push origin main --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy (Unraid)
|
||||||
|
```
|
||||||
|
1. ?? Docker tab ? TradingBot
|
||||||
|
2. ?? Stop
|
||||||
|
3. ?? Force Update (pull latest)
|
||||||
|
4. ?? Start
|
||||||
|
5. ? Done! (~ 1 minuto)
|
||||||
|
?? Dati automaticamente ripristinati
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tempo totale**: ~2 minuti dal commit al running!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ?? RISORSE
|
||||||
|
|
||||||
|
### Links Utili
|
||||||
|
|
||||||
|
| Risorsa | URL |
|
||||||
|
|---------|-----|
|
||||||
|
| **Template XML** | `https://gitea.encke-hake.ts.net/Alby96/Encelado/raw/branch/main/TradingBot/deployment/unraid-template.xml` |
|
||||||
|
| **Repository Git** | `https://gitea.encke-hake.ts.net/Alby96/Encelado` |
|
||||||
|
| **Docker Image** | `gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest` |
|
||||||
|
| **Packages** | `https://gitea.encke-hake.ts.net/Alby96/Encelado/-/packages` |
|
||||||
|
| **Support/Issues** | `https://gitea.encke-hake.ts.net/Alby96/Encelado/issues` |
|
||||||
|
|
||||||
|
### Comandi Utili
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Status container
|
||||||
|
docker ps -a | grep TradingBot
|
||||||
|
|
||||||
|
# Logs real-time
|
||||||
|
docker logs -f TradingBot
|
||||||
|
|
||||||
|
# Statistics
|
||||||
|
docker stats TradingBot --no-stream
|
||||||
|
|
||||||
|
# Restart
|
||||||
|
docker restart TradingBot
|
||||||
|
|
||||||
|
# Update
|
||||||
|
docker pull gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest
|
||||||
|
|
||||||
|
# Remove (mantiene dati in /mnt/user/appdata/tradingbot)
|
||||||
|
docker rm -f TradingBot
|
||||||
|
|
||||||
|
# Inspect persistent data
|
||||||
|
ls -lh /mnt/user/appdata/tradingbot/
|
||||||
|
cat /mnt/user/appdata/tradingbot/trade-history.json | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**?? TradingBot v1.2.0 con persistenza completa pronto su Unraid!**
|
||||||
@@ -3,37 +3,23 @@ version: '3.8'
|
|||||||
services:
|
services:
|
||||||
tradingbot:
|
tradingbot:
|
||||||
container_name: tradingbot
|
container_name: tradingbot
|
||||||
image: tradingbot:latest
|
image: gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "${EXTERNAL_PORT:-8080}:8080"
|
||||||
volumes:
|
volumes:
|
||||||
# Persistenza dati applicazione
|
|
||||||
- tradingbot-data:/app/data
|
- tradingbot-data:/app/data
|
||||||
# Opzionale: mount locale per sviluppo
|
|
||||||
# - ./logs:/app/logs
|
|
||||||
environment:
|
environment:
|
||||||
# Configurazioni applicazione
|
|
||||||
- ASPNETCORE_ENVIRONMENT=Production
|
- ASPNETCORE_ENVIRONMENT=Production
|
||||||
- ASPNETCORE_URLS=http://+:8080
|
- ASPNETCORE_URLS=http://+:8080
|
||||||
# Fuso orario (importante per trading!)
|
|
||||||
- TZ=Europe/Rome
|
|
||||||
# Opzionali - Configurazioni avanzate
|
|
||||||
# - TRADINGBOT__SimulationMode=true
|
|
||||||
# - TRADINGBOT__AutoStartBot=true
|
|
||||||
# - TRADINGBOT__UpdateIntervalSeconds=3
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 10s
|
start_period: 10s
|
||||||
networks:
|
networks:
|
||||||
- tradingbot-network
|
- tradingbot-network
|
||||||
# Resource limits (opzionali ma consigliati per Unraid)
|
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
@@ -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 -> 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>
|
||||||
@@ -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 ""
|
||||||
@@ -469,6 +469,123 @@ select:focus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modal Styles */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.75);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
animation: fadeIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog {
|
||||||
|
background: #1a1f3a;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||||
|
animation: slideIn 0.3s ease-out;
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid rgba(99, 102, 241, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e2e8f0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close:hover {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body ul {
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body li {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-top: 1px solid rgba(99, 102, 241, 0.1);
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-danger {
|
||||||
|
color: #ef4444 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Danger Button */
|
||||||
|
.btn-danger {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(220, 38, 38, 0.4);
|
||||||
|
background: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Print Styles */
|
/* Print Styles */
|
||||||
@media print {
|
@media print {
|
||||||
.no-print {
|
.no-print {
|
||||||
|
|||||||
Reference in New Issue
Block a user