Compare commits

...

11 Commits

Author SHA1 Message Date
Alby96 64f3511695 Nuove: multi-strategy, indicatori avanzati, posizioni
- Sidebar portfolio con metriche dettagliate (Totale, Investito, Disponibile, P&L, ROI) e aggiornamento real-time
- Sistema multi-strategia: 8 strategie assegnabili per asset, voting decisionale, pagina Trading Control
- Nuova pagina Posizioni: gestione, chiusura manuale, P&L non realizzato, notifiche
- Sistema indicatori tecnici: 7+ indicatori configurabili, segnali real-time, raccomandazioni, storico segnali
- Refactoring TradingBotService per capitale, P&L, ROI, eventi
- Nuovi modelli e servizi per strategie/indicatori, persistenza configurazioni
- UI/UX: navigazione aggiornata, widget, modali, responsive
- Aggiornamento README e CHANGELOG con tutte le novità
2026-01-06 17:49:07 +01:00
Alby96 c229c50f1d chore: Bump version to 1.5.2 - Add detailed capital metrics in sidebar: Total, Invested, Available, P&L, ROI 2025-12-23 10:51:06 +01:00
Alby96 0809c9af87 chore: Bump version to 1.5.1 - Add Positions management page with manual close functionality 2025-12-22 21:04:16 +01:00
Alby96 ae5f8f9249 chore: Bump version to 1.5.0 - Multi-strategy trading system with 8 famous strategies 2025-12-22 18:16:22 +01:00
Alby96 f21adf3313 chore: Bump version to 1.4.0 - Add comprehensive indicators system with configuration 2025-12-22 15:54:55 +01:00
Alby96 92c8e57a8c Persistenza dati e logging avanzato con UI e Unraid
- Aggiunto TradeHistoryService per persistenza trade/posizioni attive su disco (JSON, auto-save/restore)
- Logging centralizzato (LoggingService) con livelli, categorie, simbolo e buffer circolare (500 log)
- Nuova pagina Logs: monitoraggio real-time, filtri avanzati, cancellazione log, colorazione livelli
- Sezione "Dati Persistenti" in Settings: conteggio trade, dimensione dati, reset con conferma modale
- Background service per salvataggio sicuro su shutdown/stop container
- Aggiornata sidebar, stili modali/bottoni danger, .gitignore e documentazione (README, CHANGELOG, UNRAID_INSTALL, checklist)
- Versione 1.3.0
2025-12-22 11:24:17 +01:00
Alby96 d7ae3e5d44 chore: Bump version to 1.3.0 - Add comprehensive logs page with real-time monitoring 2025-12-22 00:43:45 +01:00
Alby96 54cfe05687 chore: Bump version to 1.2.0 - Add trade history persistence and data management 2025-12-21 18:48:41 +01:00
Alby96 0e64afa1f2 Refactor documentazione, versioning e deployment
- Riorganizzato README.md con badge versione, changelog, guida rapida e istruzioni semplificate per Docker/Unraid
- Creato CHANGELOG.md secondo standard Keep a Changelog/SemVer
- Aggiunto script bump-version.ps1 per gestione automatica versioni e tagging Git
- Aggiornate guide deployment: PUBLISHING_GUIDE.md, UNRAID_INSTALL.md e README.md in /deployment
- Modificato unraid-template.xml: porta WebUI configurabile (default 8888), volumi e variabili ambiente semplificati
- Aggiornata PROJECT_STRUCTURE.md con nuova struttura e best practices
- Migliorata chiarezza, professionalità e automazione del workflow di rilascio
2025-12-21 18:31:00 +01:00
Alby96 121324dfc7 chore: Bump version to 1.1.0 - Add automated deployment system, versioning, and comprehensive documentation 2025-12-17 23:34:36 +01:00
Alby96 cc34d2b57f Riorganizzazione deployment, doc e publish automatico
- Spostata tutta la configurazione di deployment in /deployment (docker-compose, unraid-template, guide)
- Aggiunte e aggiornate guide dettagliate: publishing su Gitea, installazione Unraid, struttura progetto
- Migliorato target MSBuild: publish automatico su Gitea Registry da Visual Studio, log dettagliati, condizioni più robuste
- Aggiornato e ampliato .gitignore per escludere build, dati e file locali
- Rimossi file obsoleti dalla root (ora tutto in /deployment)
- Struttura più chiara, zero script esterni, documentazione completa e workflow di deploy semplificato
2025-12-17 23:15:46 +01:00
34 changed files with 6924 additions and 533 deletions
+105
View File
@@ -0,0 +1,105 @@
# Visual Studio
.vs/
*.user
*.suo
*.userosscache
*.sln.docstates
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Docker
obj/Docker/
# NuGet
*.nupkg
*.snupkg
**/packages/*
!**/packages/build/
# Files generated by publishing
[Pp]ublish/
PublishOutput/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# ReSharper
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JetBrains Rider
.idea/
*.sln.iml
# Visual Studio cache files
*.[Cc]ache
!?*.[Cc]ache/
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# Mono Auto Generated Files
mono_crash.*
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Mac files
.DS_Store
# Application data and persistence
**/data/
trade-history.json
active-positions.json
settings.json
*.db
*.db-shm
*.db-wal
# Logs
*.log
# Temporary files
*.tmp
*.temp
+259
View File
@@ -0,0 +1,259 @@
# Changelog
Tutte le modifiche significative a TradingBot sono documentate qui.
Formato basato su [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), segue [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [1.5.2] - 2024-12-22
### Added
- **Detailed Capital Metrics in Sidebar**: Portfolio Summary espansa con metriche complete
- Capitale Totale: Somma di disponibile + investito
- Capitale Investito: Valore posizioni aperte correnti
- Capitale Disponibile: Cash disponibile per trading
- P&L Corrente: Profitto/perdita non realizzato sulle posizioni aperte
- ROI: Return on Investment percentuale sul capitale iniziale
- **Real-time Updates**: Metriche aggiornate automaticamente
- Su ogni cambio di prezzo
- Su ogni trade eseguito
- Su ogni cambio di status
### Changed
- Portfolio Summary sidebar completamente ridisegnata
- Color-coding per metriche (verde=profit, rosso=loss, arancio=investito, blu=disponibile)
- Calcolo ROI basato su capitale iniziale + P&L realizzato e non realizzato
### Technical
- Event-driven updates su OnPriceUpdated, OnTradeExecuted, OnStatusChanged
- Separazione chiara tra capitale investito e disponibile
- P&L calculation: (Current Value - Entry Value) per posizioni aperte
---
## [1.5.1] - 2024-12-22
### Added
- **Positions Management Page**: Pagina dedicata gestione posizioni aperte
- Visualizzazione completa posizioni attive
- Real-time P&L unrealized calculation
- Manual close position functionality
- Confirmation modal con dettagli completi
- Header statistics (Active positions, Total value, Total P&L)
- Empty state quando nessuna posizione aperta
- Success notifications
- **ClosePositionManuallyAsync**: Metodo pubblico TradingBotService
- Close manual positions via API
- Safety checks (bot running, position exists)
- Automatic logging
### Changed
- MainLayout navigation menu aggiornato con link Positions
- Version display aggiornata a v1.5.1
### Technical
- Event-driven updates per real-time P&L
- Position cards con holding time formattato
- Color-coded P&L (green=profit, red=loss)
- Modal confirmation per sicurezza
- No manual opening - solo chiusura
---
## [1.5.0] - 2024-12-22
### Added
- **Multi-Strategy Trading System**: Sistema completo di gestione strategie multiple per asset
- 8 strategie di trading famose implementate
- Assignment multiplo strategie per asset
- Sistema di voting per decisioni aggregate
- Trading Control page dedicata
- **Trading Strategies**: 8 strategie professionali preimpostate
- RSI Strategy (Oscillator - Medium risk)
- MACD Strategy (Momentum - Medium risk)
- Bollinger Bands (Volatility - Low risk)
- Mean Reversion (Contrarian - High risk)
- Momentum (Trend Following - Medium risk)
- EMA Crossover / Golden Cross (Trend - Low risk)
- Scalping (Short-term - Very High risk)
- Breakout (Volatility - High risk)
- **TradingStrategiesService**: Gestione centralizzata strategie
- Strategy registry con metadata
- Asset-strategy mapping
- Decision aggregation con voting
- Persistence delle configurazioni
- **Trading Control Page**: Interfaccia gestione strategie
- Grid asset con stato real-time
- Strategy selector modal per category
- Visualizzazione decisioni aggregate
- Risk e timeframe indicators
- **Version Display**: Versione applicazione visibile in sidebar footer
- Version number (v1.5.0)
- Build date
### Changed
- TradingBotService integrato con TradingStrategiesService
- MainLayout aggiornato con link Trading Control
- Navigation menu riorganizzato
### Technical
- 8 strategy implementations (ITradingStrategy interface)
- Voting algorithm: 60% consensus threshold
- Strategy metadata: Category, Risk Level, Timeframe
- Persistence: `strategy-mappings.json`
- Confidence-based decision making
---
## [1.4.0] - 2024-12-22
### Added
- **Indicators System**: Sistema completo di indicatori tecnici configurabili
- 7 indicatori predefiniti: RSI, MACD, SMA (20/50), EMA 12, Bollinger Bands, Stochastic
- Configurazione parametri per ogni indicatore
- Abilitazione/disabilitazione selettiva indicatori
- Soglie personalizzabili (ipercomprato/ipervenduto)
- **Indicators Page**: Interfaccia dedicata gestione indicatori
- Configurazione real-time di tutti i parametri
- Status live per ogni asset attivo
- Visualizzazione condizioni mercato
- Raccomandazioni basate su indicatori
- **Indicator Signals**: Sistema di segnali trading
- Segnali BUY/SELL/HOLD con strength rating
- Storia ultimi 100 segnali generati
- Filtro segnali per symbol
- Notifiche real-time nuovi segnali
- **Trading Recommendations**: Raccomandazioni aggregate
- Analisi multi-indicatore per ogni asset
- Livello di confidenza basato su consenso
- Lista indicatori di supporto
- Azioni consigliate (BUY/SELL/HOLD)
- **Indicator Models**: Nuovi modelli dati
- IndicatorConfig - Configurazione indicatore
- IndicatorSignal - Segnale trading
- IndicatorStatus - Status per asset
- TradingRecommendation - Raccomandazione trading
### Changed
- MainLayout aggiornato con link Indicators
- IndicatorsService registrato come singleton
- Configurazione indicatori persistita in `indicators-config.json`
### Technical
- Persistenza configurazione indicatori in `/app/data`
- Event-driven updates per UI real-time
- Supporto 8 tipi di indicatori (RSI, MACD, SMA, EMA, BB, Stochastic, Volume, ATR)
- Market conditions: Overbought, Oversold, Bullish, Bearish, Neutral, Ranging, Trending
---
## [1.3.0] - 2024-12-21
### Added
- **Logs Page**: Comprehensive logging system with real-time monitoring
- Real-time log updates with auto-scroll
- Advanced filtering (Level, Category, Symbol)
- Color-coded log levels (Debug, Info, Warning, Error, Trade)
- Trade-specific logs with detailed information
- 500 log entries buffer with automatic rotation
- Clear logs functionality
- **LoggingService**: Centralized logging management
- Structured log entries with timestamps
- Category and symbol-based filtering
- Event-driven updates for real-time UI
- **Enhanced TradingBotService**: Integrated logging
- Bot lifecycle events (start/stop)
- Trade execution logs (buy/sell)
- Detailed trade information in logs
### Changed
- MainLayout updated with Logs navigation item
- TradingBotService now logs all major operations
---
## [1.2.0] - 2024-12-21
### Added
- **Trade Persistence**: Complete persistence system for trade history and active positions
- TradeHistoryService for JSON-based data storage
- Automatic save every 30 seconds
- Immediate save after each trade execution
- Automatic data restore on application startup
- **Data Management UI**: Settings page section for persistent data management
- View trade count and data size
- View active positions count
- Clear all data functionality with confirmation modal
- **Graceful Shutdown**: TradingBotBackgroundService for data persistence on application exit
- Automatic save on container stop/restart
- No data loss on unexpected shutdowns
### Changed
- TradingBotService now integrates with TradeHistoryService
- Buy/Sell methods are now async to support immediate persistence
- Settings page enhanced with data management section
### Technical
- Data stored in `/app/data` directory
- JSON format for human-readable persistence
- Compatible with Docker volume mapping
- Background service registered as IHostedService
---
## [1.1.0] - 2024-12-17
### Added
- **Automated Deployment**: MSBuild post-build per push automatico su Gitea Registry
- **Multiple Docker Tags**: latest, version, version-date per ogni release
- **Versioning System**: Script PowerShell `bump-version.ps1` per gestione versioni
- **Unraid Support**: Template XML per installazione 1-click
- **Documentation**: Guide complete per deployment e versioning
### Changed
- Riorganizzata struttura progetto (`/deployment`, `/docs`)
- Default WebUI port cambiato da 8080 a 8888
- Health check timing aumentato a 40s per startup Blazor
### Fixed
- WebUI icon non visibile in Unraid Docker tab
- Port mapping non configurabile in template
- Template URL path corretto
---
## [1.0.0] - 2024-12-15
### Added
- **Initial Release** di TradingBot
- Blazor Server UI con dashboard real-time
- Simple Moving Average (SMA) trading strategy
- 15 criptovalute supportate
- Simulazione market data per testing
- Trade history e statistics
- Settings persistenti via JSON
- Indicatori tecnici: SMA, EMA, RSI, MACD, Bollinger Bands
- Docker support con multi-stage build
- Health checks integrati
---
## Version Legend
- **Added**: Nuove features
- **Changed**: Modifiche a funzionalità esistenti
- **Deprecated**: Features da rimuovere
- **Removed**: Features rimosse
- **Fixed**: Bug fixes
- **Security**: Security fixes
- **Technical**: Miglioramenti tecnici e infrastrutturali
---
[1.5.0]: https://gitea.encke-hake.ts.net/Alby96/Encelado/compare/v1.4.0...v1.5.0
[1.4.0]: https://gitea.encke-hake.ts.net/Alby96/Encelado/compare/v1.3.0...v1.4.0
[1.3.0]: https://gitea.encke-hake.ts.net/Alby96/Encelado/compare/v1.2.0...v1.3.0
[1.2.0]: https://gitea.encke-hake.ts.net/Alby96/Encelado/compare/v1.1.0...v1.2.0
[1.1.0]: https://gitea.encke-hake.ts.net/Alby96/Encelado/compare/v1.0.0...v1.1.0
[1.0.0]: https://gitea.encke-hake.ts.net/Alby96/Encelado/releases/tag/v1.0.0
+136 -28
View File
@@ -41,6 +41,22 @@
}
</NavLink>
<NavLink class="menu-item" href="/trading-control" title="Trading Control">
<span class="item-icon bi bi-sliders"></span>
@if (!sidebarCollapsed)
{
<span class="item-text">Trading Control</span>
}
</NavLink>
<NavLink class="menu-item" href="/positions" title="Posizioni">
<span class="item-icon bi bi-wallet2"></span>
@if (!sidebarCollapsed)
{
<span class="item-text">Posizioni</span>
}
</NavLink>
<NavLink class="menu-item" href="/strategies" title="Strategie">
<span class="item-icon bi bi-diagram-3"></span>
@if (!sidebarCollapsed)
@@ -49,6 +65,14 @@
}
</NavLink>
<NavLink class="menu-item" href="/indicators" title="Indicatori">
<span class="item-icon bi bi-graph-down-arrow"></span>
@if (!sidebarCollapsed)
{
<span class="item-text">Indicatori</span>
}
</NavLink>
<NavLink class="menu-item" href="/assets" title="Asset">
<span class="item-icon bi bi-coin"></span>
@if (!sidebarCollapsed)
@@ -81,6 +105,14 @@
}
</NavLink>
<NavLink class="menu-item" href="/logs" title="Logs">
<span class="item-icon bi bi-terminal"></span>
@if (!sidebarCollapsed)
{
<span class="item-text">Logs</span>
}
</NavLink>
<NavLink class="menu-item" href="/settings" title="Impostazioni">
<span class="item-icon bi bi-gear"></span>
@if (!sidebarCollapsed)
@@ -95,16 +127,51 @@
{
<div class="sidebar-summary">
<div class="summary-card">
<div class="summary-row">
<span class="summary-title">Portfolio</span>
<span class="summary-amount">$@portfolioValue.ToString("N0")</span>
<div class="summary-header">
<span class="summary-section-title">Portfolio</span>
</div>
<div class="summary-row">
<span class="summary-title">Profitto</span>
<span class="summary-amount @(totalProfit >= 0 ? "profit" : "loss")">
$@totalProfit.ToString("N2")
<span class="summary-title">Capitale Totale</span>
<span class="summary-amount">$@totalCapital.ToString("N2")</span>
</div>
<div class="summary-row">
<span class="summary-title">Investito</span>
<span class="summary-amount invested">$@investedCapital.ToString("N2")</span>
</div>
<div class="summary-row">
<span class="summary-title">Disponibile</span>
<span class="summary-amount available">$@availableCapital.ToString("N2")</span>
</div>
<div class="summary-divider"></div>
<div class="summary-row highlight">
<span class="summary-title">P&L Corrente</span>
<span class="summary-amount @(currentPL >= 0 ? "profit" : "loss")">
@(currentPL >= 0 ? "+" : "")$@currentPL.ToString("N2")
</span>
</div>
<div class="summary-row">
<span class="summary-title">ROI</span>
<span class="summary-amount @(roiPercentage >= 0 ? "profit" : "loss")">
@(roiPercentage >= 0 ? "+" : "")@roiPercentage.ToString("F2")%
</span>
</div>
</div>
</div>
<!-- Version Footer -->
<div class="sidebar-footer">
<div class="version-info">
<span class="version-label">TradingBot</span>
<span class="version-number">v1.5.2</span>
</div>
<div class="build-info">
<span class="build-date">Build: @DateTime.Now.ToString("yyyy-MM-dd")</span>
</div>
</div>
}
@@ -140,6 +207,11 @@
private bool isRunning => BotService.Status.IsRunning;
private decimal portfolioValue = 0;
private decimal totalProfit = 0;
private decimal totalCapital = 0;
private decimal investedCapital = 0;
private decimal availableCapital = 0;
private decimal currentPL = 0;
private decimal roiPercentage = 0;
protected override void OnInitialized()
{
@@ -147,6 +219,7 @@
sidebarCollapsed = settings.SidebarCollapsed;
BotService.OnStatusChanged += HandleUpdate;
BotService.OnTradeExecuted += HandleTradeExecuted;
BotService.OnPriceUpdated += HandlePriceUpdate;
SettingsService.OnSettingsChanged += HandleSettingsChanged;
@@ -176,34 +249,69 @@
private void UpdateStats()
{
portfolioValue = BotService.AssetConfigurations.Values.Sum(c =>
c.CurrentBalance + (c.CurrentHoldings * (BotService.GetLatestPrice(c.Symbol)?.Price ?? 0)));
totalProfit = BotService.AssetConfigurations.Values.Sum(c => c.TotalProfit);
totalCapital = 0;
investedCapital = 0;
availableCapital = 0;
currentPL = 0;
totalProfit = 0;
foreach (var config in BotService.AssetConfigurations.Values)
{
if (!config.IsEnabled) continue;
// Capitale disponibile (cash)
availableCapital += config.CurrentBalance;
// Capitale investito in posizioni aperte
var currentPrice = BotService.GetLatestPrice(config.Symbol)?.Price ?? 0;
var positionValue = config.CurrentHoldings * currentPrice;
investedCapital += positionValue;
// P&L non realizzato sulle posizioni aperte
if (config.CurrentHoldings > 0 && config.AverageEntryPrice > 0)
{
var entryValue = config.CurrentHoldings * config.AverageEntryPrice;
currentPL += (positionValue - entryValue);
}
// Totale profitti/perdite realizzati
totalProfit += config.TotalProfit;
}
// Capitale totale = Disponibile + Investito
totalCapital = availableCapital + investedCapital;
// Portfolio value include anche i profitti realizzati
portfolioValue = totalCapital + totalProfit;
// ROI basato sul capitale iniziale
var initialCapital = BotService.AssetConfigurations.Values
.Where(c => c.IsEnabled)
.Sum(c => c.InitialBalance);
if (initialCapital > 0)
{
var totalGain = currentPL + totalProfit;
roiPercentage = (totalGain / initialCapital) * 100;
}
else
{
roiPercentage = 0;
}
}
private void HandleUpdate()
{
UpdateStats();
InvokeAsync(StateHasChanged);
}
private void HandlePriceUpdate(string symbol, MarketPrice price)
{
UpdateStats();
InvokeAsync(StateHasChanged);
}
private void HandleSettingsChanged()
{
var settings = SettingsService.GetSettings();
sidebarCollapsed = settings.SidebarCollapsed;
InvokeAsync(StateHasChanged);
}
private void HandleUpdate() => InvokeAsync(() => { UpdateStats(); StateHasChanged(); });
private void HandleTradeExecuted(Trade trade) => InvokeAsync(() => { UpdateStats(); StateHasChanged(); });
private void HandlePriceUpdate(string symbol, MarketPrice price) => InvokeAsync(() => { UpdateStats(); StateHasChanged(); });
private void HandleSettingsChanged() => InvokeAsync(StateHasChanged);
public void Dispose()
{
BotService.OnStatusChanged -= HandleUpdate;
BotService.OnTradeExecuted -= HandleTradeExecuted;
BotService.OnPriceUpdated -= HandlePriceUpdate;
SettingsService.OnSettingsChanged -= HandleSettingsChanged;
}
@@ -256,12 +256,35 @@
gap: 0.875rem !important;
}
::deep .summary-header {
margin-bottom: 0.5rem !important;
}
::deep .summary-section-title {
font-size: 0.875rem !important;
color: #6366f1 !important;
font-weight: 700 !important;
text-transform: uppercase !important;
letter-spacing: 0.05em !important;
}
::deep .summary-row {
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
}
::deep .summary-row.highlight {
padding-top: 0.5rem !important;
margin-top: 0.5rem !important;
}
::deep .summary-divider {
height: 1px !important;
background: rgba(99, 102, 241, 0.2) !important;
margin: 0.25rem 0 !important;
}
::deep .summary-title {
font-size: 0.75rem !important;
color: #64748b !important;
@@ -285,6 +308,14 @@
color: #ef4444 !important;
}
::deep .summary-amount.invested {
color: #f59e0b !important;
}
::deep .summary-amount.available {
color: #3b82f6 !important;
}
/* ==============================================
MAIN CONTENT AREA
============================================== */
@@ -466,3 +497,46 @@
padding: 1.5rem 1rem !important;
}
}
/* ==============================================
VERSION FOOTER
============================================== */
::deep .sidebar-footer {
padding: 1rem 1.5rem !important;
border-top: 1px solid rgba(99, 102, 241, 0.1) !important;
margin-top: auto !important;
}
::deep .version-info {
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
margin-bottom: 0.5rem !important;
}
::deep .version-label {
font-size: 0.75rem !important;
color: #64748b !important;
font-weight: 600 !important;
text-transform: uppercase !important;
letter-spacing: 0.05em !important;
}
::deep .version-number {
font-size: 0.875rem !important;
font-weight: 700 !important;
color: #6366f1 !important;
font-family: 'Courier New', monospace !important;
}
::deep .build-info {
display: flex !important;
justify-content: flex-start !important;
}
::deep .build-date {
font-size: 0.625rem !important;
color: #475569 !important;
font-family: 'Courier New', monospace !important;
}
@@ -0,0 +1,467 @@
@page "/indicators"
@using TradingBot.Services
@using TradingBot.Models
@inject IndicatorsService IndicatorsService
@inject TradingBotService BotService
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Indicatori - TradingBot</PageTitle>
<div class="indicators-page">
<div class="page-header">
<div>
<h1>Indicatori Tecnici</h1>
<p class="subtitle">Configura gli indicatori per analisi avanzata del mercato</p>
</div>
</div>
<!-- Indicators Grid -->
<div class="indicators-grid">
@foreach (var indicator in indicators.Values.OrderBy(i => !i.IsEnabled).ThenBy(i => i.Name))
{
<div class="indicator-card @(indicator.IsEnabled ? "enabled" : "disabled")">
<div class="indicator-header">
<div class="indicator-title">
<h3>@indicator.Name</h3>
<span class="indicator-type">@indicator.Type</span>
</div>
<label class="toggle-switch">
<input type="checkbox" checked="@indicator.IsEnabled"
@onchange="@(e => ToggleIndicator(indicator.Id, (bool)e.Value!))" />
<span class="toggle-slider"></span>
</label>
</div>
<p class="indicator-description">@indicator.Description</p>
@if (indicator.IsEnabled)
{
<div class="indicator-config">
@switch (indicator.Type)
{
case IndicatorType.RSI:
case IndicatorType.Stochastic:
<div class="config-row">
<label>Periodo:</label>
<input type="number" @bind="indicator.Period" min="5" max="50" />
</div>
<div class="config-row">
<label>Ipercomprato:</label>
<input type="number" @bind="indicator.OverboughtThreshold" min="60" max="90" step="5" />
</div>
<div class="config-row">
<label>Ipervenduto:</label>
<input type="number" @bind="indicator.OversoldThreshold" min="10" max="40" step="5" />
</div>
break;
case IndicatorType.MACD:
<div class="config-row">
<label>Fast Period:</label>
<input type="number" @bind="indicator.FastPeriod" min="8" max="20" />
</div>
<div class="config-row">
<label>Slow Period:</label>
<input type="number" @bind="indicator.SlowPeriod" min="20" max="35" />
</div>
<div class="config-row">
<label>Signal Period:</label>
<input type="number" @bind="indicator.SignalPeriod" min="5" max="15" />
</div>
break;
case IndicatorType.SMA:
case IndicatorType.EMA:
case IndicatorType.BollingerBands:
<div class="config-row">
<label>Periodo:</label>
<input type="number" @bind="indicator.Period" min="5" max="200" />
</div>
break;
}
<button class="btn-secondary btn-sm" @onclick="() => SaveIndicator(indicator)">
<span class="bi bi-check-lg"></span>
Salva
</button>
</div>
<!-- Current Status for Active Assets -->
<div class="indicator-status-section">
<h4>Status Asset Attivi</h4>
@foreach (var symbol in BotService.AssetConfigurations.Values.Where(c => c.IsEnabled).Select(c => c.Symbol))
{
var status = IndicatorsService.GetIndicatorStatus(indicator.Id, symbol);
if (status != null)
{
<div class="status-row">
<span class="status-symbol">@symbol</span>
<span class="status-value">@status.CurrentValue.ToString("F2")</span>
<span class="status-condition condition-@status.Condition.ToString().ToLower()">
@status.Condition
</span>
<span class="status-recommendation">@status.Recommendation</span>
</div>
}
}
</div>
}
</div>
}
</div>
<!-- Recent Signals -->
<div class="signals-section">
<div class="section-header">
<h2>Segnali Recenti</h2>
<span class="signals-count">@recentSignals.Count segnali</span>
</div>
@if (recentSignals.Count == 0)
{
<div class="empty-state">
<span class="bi bi-broadcast"></span>
<p>Nessun segnale generato</p>
</div>
}
else
{
<div class="signals-list">
@foreach (var signal in recentSignals.Take(20))
{
<div class="signal-card signal-@signal.Type.ToString().ToLower()">
<div class="signal-header">
<span class="signal-time">@signal.Timestamp.ToLocalTime().ToString("HH:mm:ss")</span>
<span class="signal-indicator">@signal.IndicatorName</span>
<span class="signal-symbol">@signal.Symbol</span>
<span class="signal-type type-@signal.Type.ToString().ToLower()">
@signal.Type
</span>
<span class="signal-strength strength-@signal.Strength.ToString().ToLower()">
@signal.Strength
</span>
</div>
<div class="signal-message">@signal.Message</div>
@if (signal.Value.HasValue)
{
<div class="signal-value">Valore: @signal.Value.Value.ToString("F2")</div>
}
</div>
}
</div>
}
</div>
</div>
<style>
.indicators-page {
display: flex;
flex-direction: column;
gap: 2rem;
}
.indicators-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
gap: 1.5rem;
}
.indicator-card {
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
padding: 1.5rem;
transition: all 0.3s ease;
}
.indicator-card.disabled {
opacity: 0.6;
border-color: rgba(100, 116, 139, 0.2);
}
.indicator-card.enabled {
border-color: rgba(99, 102, 241, 0.4);
}
.indicator-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.indicator-title h3 {
font-size: 1.25rem;
font-weight: 700;
color: #e2e8f0;
margin: 0 0 0.25rem 0;
}
.indicator-type {
display: inline-block;
padding: 0.25rem 0.75rem;
background: rgba(99, 102, 241, 0.2);
border-radius: 0.25rem;
color: #6366f1;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
}
.indicator-description {
color: #94a3b8;
font-size: 0.875rem;
line-height: 1.5;
margin-bottom: 1rem;
}
.indicator-config {
background: rgba(0, 0, 0, 0.2);
border-radius: 0.5rem;
padding: 1rem;
margin-top: 1rem;
}
.config-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.75rem;
}
.config-row:last-of-type {
margin-bottom: 1rem;
}
.config-row label {
font-size: 0.875rem;
color: #cbd5e1;
font-weight: 600;
}
.config-row input[type="number"] {
width: 80px;
padding: 0.5rem;
border-radius: 0.375rem;
background: #0f1629;
border: 1px solid #334155;
color: #e2e8f0;
font-size: 0.875rem;
}
.indicator-status-section {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid rgba(99, 102, 241, 0.1);
}
.indicator-status-section h4 {
font-size: 0.875rem;
color: #94a3b8;
margin-bottom: 0.75rem;
text-transform: uppercase;
font-weight: 600;
}
.status-row {
display: grid;
grid-template-columns: 60px 80px 100px 1fr;
gap: 0.5rem;
align-items: center;
padding: 0.5rem;
background: rgba(0, 0, 0, 0.2);
border-radius: 0.375rem;
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
.status-symbol {
font-weight: 700;
color: #8b5cf6;
}
.status-value {
font-family: 'Courier New', monospace;
color: #e2e8f0;
}
.status-condition {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-weight: 700;
font-size: 0.75rem;
text-align: center;
}
.condition-overbought { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
.condition-oversold { background: rgba(16, 185, 129, 0.2); color: #10b981; }
.condition-bullish { background: rgba(16, 185, 129, 0.2); color: #10b981; }
.condition-bearish { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
.condition-neutral { background: rgba(100, 116, 139, 0.2); color: #94a3b8; }
.status-recommendation {
color: #cbd5e1;
font-size: 0.75rem;
}
.signals-section {
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
padding: 1.5rem;
}
.signals-count {
color: #6366f1;
font-weight: 600;
}
.signals-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-top: 1rem;
}
.signal-card {
background: #0f1629;
border-radius: 0.5rem;
border-left: 3px solid;
padding: 1rem;
}
.signal-card.signal-buy {
border-left-color: #10b981;
}
.signal-card.signal-sell {
border-left-color: #ef4444;
}
.signal-card.signal-hold {
border-left-color: #f59e0b;
}
.signal-header {
display: flex;
gap: 0.75rem;
align-items: center;
margin-bottom: 0.5rem;
flex-wrap: wrap;
}
.signal-time {
color: #64748b;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
}
.signal-indicator {
padding: 0.25rem 0.75rem;
background: rgba(99, 102, 241, 0.2);
border-radius: 0.25rem;
color: #6366f1;
font-weight: 700;
font-size: 0.75rem;
}
.signal-symbol {
padding: 0.25rem 0.75rem;
background: rgba(139, 92, 246, 0.2);
border-radius: 0.25rem;
color: #8b5cf6;
font-weight: 700;
font-size: 0.75rem;
}
.signal-type {
padding: 0.25rem 0.75rem;
border-radius: 0.25rem;
font-weight: 700;
font-size: 0.75rem;
text-transform: uppercase;
}
.type-buy { background: rgba(16, 185, 129, 0.2); color: #10b981; }
.type-sell { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
.type-hold { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
.signal-strength {
padding: 0.25rem 0.75rem;
background: rgba(59, 130, 246, 0.2);
border-radius: 0.25rem;
color: #3b82f6;
font-weight: 600;
font-size: 0.75rem;
}
.signal-message {
color: #e2e8f0;
margin-bottom: 0.25rem;
}
.signal-value {
color: #94a3b8;
font-size: 0.875rem;
font-family: 'Courier New', monospace;
}
.empty-state {
text-align: center;
padding: 3rem;
color: #64748b;
}
.empty-state .bi {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
</style>
@code {
private Dictionary<string, IndicatorConfig> indicators = new();
private List<IndicatorSignal> recentSignals = new();
protected override void OnInitialized()
{
LoadIndicators();
IndicatorsService.OnIndicatorsChanged += HandleIndicatorsChanged;
IndicatorsService.OnSignalGenerated += HandleSignalGenerated;
}
private void LoadIndicators()
{
indicators = IndicatorsService.GetIndicators().ToDictionary(k => k.Key, v => v.Value);
recentSignals = IndicatorsService.GetRecentSignals().ToList();
}
private void ToggleIndicator(string id, bool enabled)
{
IndicatorsService.ToggleIndicator(id, enabled);
}
private void SaveIndicator(IndicatorConfig indicator)
{
IndicatorsService.UpdateIndicator(indicator.Id, indicator);
}
private void HandleIndicatorsChanged()
{
LoadIndicators();
InvokeAsync(StateHasChanged);
}
private void HandleSignalGenerated(IndicatorSignal signal)
{
recentSignals = IndicatorsService.GetRecentSignals().ToList();
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
IndicatorsService.OnIndicatorsChanged -= HandleIndicatorsChanged;
IndicatorsService.OnSignalGenerated -= HandleSignalGenerated;
}
}
+398
View File
@@ -0,0 +1,398 @@
@page "/logs"
@using TradingBot.Services
@using TradingBot.Models
@inject LoggingService LoggingService
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Logs - TradingBot</PageTitle>
<div class="logs-page">
<div class="page-header">
<div>
<h1>Logs Operazioni</h1>
<p class="subtitle">Cronologia eventi e operazioni del bot</p>
</div>
<div class="header-actions">
<button class="btn-secondary" @onclick="ClearLogs">
<span class="bi bi-trash"></span>
Cancella Logs
</button>
</div>
</div>
<div class="logs-filters">
<div class="filter-group">
<label>Livello:</label>
<select @bind="selectedLevel" @bind:after="FilterLogs">
<option value="">Tutti</option>
<option value="Debug">Debug</option>
<option value="Info">Info</option>
<option value="Warning">Warning</option>
<option value="Error">Error</option>
<option value="Trade">Trade</option>
</select>
</div>
<div class="filter-group">
<label>Categoria:</label>
<select @bind="selectedCategory" @bind:after="FilterLogs">
<option value="">Tutte</option>
@foreach (var category in categories)
{
<option value="@category">@category</option>
}
</select>
</div>
<div class="filter-group">
<label>Symbol:</label>
<select @bind="selectedSymbol" @bind:after="FilterLogs">
<option value="">Tutti</option>
@foreach (var symbol in symbols)
{
<option value="@symbol">@symbol</option>
}
</select>
</div>
<div class="filter-group">
<label>
<input type="checkbox" @bind="autoScroll" />
Auto-scroll
</label>
</div>
<div class="logs-count">
@filteredLogs.Count / @allLogs.Count logs
</div>
</div>
<div class="logs-container" @ref="logsContainer">
@if (filteredLogs.Count == 0)
{
<div class="empty-state">
<span class="bi bi-inbox"></span>
<p>Nessun log disponibile</p>
</div>
}
else
{
<div class="logs-list">
@foreach (var log in filteredLogs.OrderByDescending(l => l.Timestamp))
{
<div class="log-entry log-@log.Level.ToString().ToLower()">
<div class="log-header">
<span class="log-timestamp">@log.Timestamp.ToLocalTime().ToString("HH:mm:ss.fff")</span>
<span class="log-level">
<span class="bi bi-@GetLevelIcon(log.Level)"></span>
@log.Level
</span>
<span class="log-category">@log.Category</span>
@if (!string.IsNullOrEmpty(log.Symbol))
{
<span class="log-symbol">@log.Symbol</span>
}
</div>
<div class="log-message">@log.Message</div>
@if (!string.IsNullOrEmpty(log.Details))
{
<div class="log-details">@log.Details</div>
}
</div>
}
</div>
}
</div>
</div>
<style>
.logs-page {
height: 100%;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.logs-filters {
display: flex;
gap: 1rem;
align-items: center;
padding: 1rem;
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
flex-wrap: wrap;
}
.filter-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.filter-group label {
font-size: 0.875rem;
color: #94a3b8;
font-weight: 600;
}
.filter-group select {
padding: 0.5rem 1rem;
border-radius: 0.375rem;
background: #0f1629;
border: 1px solid #334155;
color: #e2e8f0;
font-size: 0.875rem;
}
.logs-count {
margin-left: auto;
padding: 0.5rem 1rem;
background: rgba(99, 102, 241, 0.1);
border-radius: 0.375rem;
color: #6366f1;
font-weight: 600;
font-size: 0.875rem;
}
.logs-container {
flex: 1;
overflow-y: auto;
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
padding: 1rem;
}
.logs-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.log-entry {
padding: 1rem;
border-radius: 0.5rem;
border-left: 3px solid;
background: #0f1629;
}
.log-entry.log-debug {
border-left-color: #64748b;
}
.log-entry.log-info {
border-left-color: #3b82f6;
}
.log-entry.log-warning {
border-left-color: #f59e0b;
}
.log-entry.log-error {
border-left-color: #ef4444;
}
.log-entry.log-trade {
border-left-color: #10b981;
background: rgba(16, 185, 129, 0.05);
}
.log-header {
display: flex;
gap: 1rem;
align-items: center;
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
.log-timestamp {
color: #64748b;
font-family: 'Courier New', monospace;
}
.log-level {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.75rem;
border-radius: 0.25rem;
font-weight: 700;
text-transform: uppercase;
font-size: 0.75rem;
}
.log-entry.log-debug .log-level {
background: rgba(100, 116, 139, 0.2);
color: #94a3b8;
}
.log-entry.log-info .log-level {
background: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.log-entry.log-warning .log-level {
background: rgba(245, 158, 11, 0.2);
color: #f59e0b;
}
.log-entry.log-error .log-level {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.log-entry.log-trade .log-level {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
}
.log-category {
padding: 0.25rem 0.75rem;
background: rgba(99, 102, 241, 0.1);
border-radius: 0.25rem;
color: #6366f1;
font-weight: 600;
}
.log-symbol {
padding: 0.25rem 0.75rem;
background: rgba(139, 92, 246, 0.1);
border-radius: 0.25rem;
color: #8b5cf6;
font-weight: 700;
}
.log-message {
color: #e2e8f0;
line-height: 1.6;
}
.log-details {
margin-top: 0.5rem;
padding: 0.75rem;
background: rgba(0, 0, 0, 0.3);
border-radius: 0.375rem;
color: #94a3b8;
font-size: 0.875rem;
font-family: 'Courier New', monospace;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem 2rem;
color: #64748b;
}
.empty-state .bi {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-state p {
font-size: 1.125rem;
margin: 0;
}
</style>
@code {
private ElementReference logsContainer;
private List<LogEntry> allLogs = new();
private List<LogEntry> filteredLogs = new();
private string selectedLevel = "";
private string selectedCategory = "";
private string selectedSymbol = "";
private bool autoScroll = true;
private List<string> categories = new();
private List<string> symbols = new();
protected override void OnInitialized()
{
LoadLogs();
LoggingService.OnLogAdded += HandleLogAdded;
}
private void LoadLogs()
{
allLogs = LoggingService.GetLogs().ToList();
UpdateFilters();
FilterLogs();
}
private void UpdateFilters()
{
categories = allLogs.Select(l => l.Category).Distinct().OrderBy(c => c).ToList();
symbols = allLogs.Where(l => !string.IsNullOrEmpty(l.Symbol))
.Select(l => l.Symbol!)
.Distinct()
.OrderBy(s => s)
.ToList();
}
private void FilterLogs()
{
var query = allLogs.AsEnumerable();
if (!string.IsNullOrEmpty(selectedLevel))
{
if (Enum.TryParse<TradingBot.Models.LogLevel>(selectedLevel, out var level))
{
query = query.Where(l => l.Level == level);
}
}
if (!string.IsNullOrEmpty(selectedCategory))
{
query = query.Where(l => l.Category == selectedCategory);
}
if (!string.IsNullOrEmpty(selectedSymbol))
{
query = query.Where(l => l.Symbol == selectedSymbol);
}
filteredLogs = query.ToList();
StateHasChanged();
}
private async void HandleLogAdded()
{
LoadLogs();
await InvokeAsync(StateHasChanged);
if (autoScroll)
{
await Task.Delay(100);
// Auto-scroll logic would go here if needed
}
}
private void ClearLogs()
{
LoggingService.ClearLogs();
LoadLogs();
}
private string GetLevelIcon(TradingBot.Models.LogLevel level)
{
return level switch
{
TradingBot.Models.LogLevel.Debug => "bug",
TradingBot.Models.LogLevel.Info => "info-circle",
TradingBot.Models.LogLevel.Warning => "exclamation-triangle",
TradingBot.Models.LogLevel.Error => "x-circle",
TradingBot.Models.LogLevel.Trade => "graph-up-arrow",
_ => "circle"
};
}
public void Dispose()
{
LoggingService.OnLogAdded -= HandleLogAdded;
}
}
+716
View File
@@ -0,0 +1,716 @@
@page "/positions"
@using TradingBot.Services
@using TradingBot.Models
@inject TradingBotService BotService
@inject LoggingService LoggingService
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Posizioni Aperte - TradingBot</PageTitle>
<div class="positions-page">
<div class="page-header">
<div>
<h1>Posizioni Aperte</h1>
<p class="subtitle">Gestisci le tue posizioni attive - Solo chiusura manuale disponibile</p>
</div>
<div class="header-stats">
<div class="stat-card">
<span class="stat-label">Posizioni Attive</span>
<span class="stat-value">@activePositions.Count</span>
</div>
<div class="stat-card">
<span class="stat-label">Valore Totale</span>
<span class="stat-value">$@totalValue.ToString("N2")</span>
</div>
<div class="stat-card">
<span class="stat-label">P&L Non Realizzato</span>
<span class="stat-value @(totalUnrealizedPL >= 0 ? "profit" : "loss")">
@(totalUnrealizedPL >= 0 ? "+" : "")$@totalUnrealizedPL.ToString("N2")
</span>
</div>
</div>
</div>
@if (activePositions.Count == 0)
{
<div class="empty-state">
<div class="empty-icon">
<span class="bi bi-inbox"></span>
</div>
<h3>Nessuna Posizione Aperta</h3>
<p>Non hai posizioni attive al momento. Le posizioni appaiono qui automaticamente quando il bot esegue un acquisto.</p>
<div class="empty-actions">
<a href="/trading-control" class="btn-primary">
<span class="bi bi-sliders"></span>
Configura Trading
</a>
<a href="/dashboard" class="btn-secondary">
<span class="bi bi-speedometer2"></span>
Dashboard
</a>
</div>
</div>
}
else
{
<div class="positions-grid">
@foreach (var position in activePositions.OrderBy(p => p.Symbol))
{
var currentPrice = GetCurrentPrice(position.Symbol);
var unrealizedPL = CalculateUnrealizedPL(position, currentPrice);
var plPercentage = CalculatePLPercentage(position, currentPrice);
var currentValue = position.Amount * currentPrice;
var holdingTime = DateTime.UtcNow - position.Timestamp;
<div class="position-card">
<div class="position-header">
<div class="position-asset">
<h3>@position.Symbol</h3>
<span class="position-date">
Aperta @position.Timestamp.ToLocalTime().ToString("dd/MM/yyyy HH:mm")
</span>
</div>
<div class="position-pl @(unrealizedPL >= 0 ? "profit" : "loss")">
<span class="pl-value">
@(unrealizedPL >= 0 ? "+" : "")$@unrealizedPL.ToString("N2")
</span>
<span class="pl-percentage">
(@(plPercentage >= 0 ? "+" : "")@plPercentage.ToString("F2")%)
</span>
</div>
</div>
<div class="position-details">
<div class="detail-row">
<span class="detail-label">Quantità</span>
<span class="detail-value">@position.Amount.ToString("F8") @position.Symbol</span>
</div>
<div class="detail-row">
<span class="detail-label">Prezzo Entrata</span>
<span class="detail-value">$@position.Price.ToString("N2")</span>
</div>
<div class="detail-row">
<span class="detail-label">Prezzo Corrente</span>
<span class="detail-value">$@currentPrice.ToString("N2")</span>
</div>
<div class="detail-row">
<span class="detail-label">Valore Iniziale</span>
<span class="detail-value">$@(position.Amount * position.Price).ToString("N2")</span>
</div>
<div class="detail-row">
<span class="detail-label">Valore Corrente</span>
<span class="detail-value">$@currentValue.ToString("N2")</span>
</div>
<div class="detail-row">
<span class="detail-label">Tempo Holding</span>
<span class="detail-value">@FormatHoldingTime(holdingTime)</span>
</div>
@if (!string.IsNullOrEmpty(position.Strategy))
{
<div class="detail-row">
<span class="detail-label">Strategia</span>
<span class="detail-value strategy-badge">@position.Strategy</span>
</div>
}
</div>
<div class="position-actions">
<button class="btn-danger" @onclick="() => ShowCloseConfirmation(position)"
disabled="@(!BotService.Status.IsRunning)">
<span class="bi bi-x-circle"></span>
Chiudi Posizione
</button>
@if (!BotService.Status.IsRunning)
{
<span class="action-note">
<span class="bi bi-info-circle"></span>
Avvia il bot per chiudere posizioni
</span>
}
</div>
</div>
}
</div>
}
<!-- Close Confirmation Modal -->
@if (showCloseModal && positionToClose != null)
{
var currentPrice = GetCurrentPrice(positionToClose.Symbol);
var unrealizedPL = CalculateUnrealizedPL(positionToClose, currentPrice);
var plPercentage = CalculatePLPercentage(positionToClose, currentPrice);
<div class="modal-overlay" @onclick="HideCloseConfirmation">
<div class="modal-dialog" @onclick:stopPropagation="true">
<div class="modal-header">
<h3>Conferma Chiusura Posizione</h3>
<button class="btn-close" @onclick="HideCloseConfirmation">×</button>
</div>
<div class="modal-body">
<div class="confirmation-details">
<div class="confirm-asset">
<h4>@positionToClose.Symbol</h4>
<span class="confirm-amount">@positionToClose.Amount.ToString("F8") @positionToClose.Symbol</span>
</div>
<div class="confirm-prices">
<div class="price-item">
<span class="price-label">Prezzo Entrata</span>
<span class="price-value">$@positionToClose.Price.ToString("N2")</span>
</div>
<span class="bi bi-arrow-right"></span>
<div class="price-item">
<span class="price-label">Prezzo Chiusura</span>
<span class="price-value">$@currentPrice.ToString("N2")</span>
</div>
</div>
<div class="confirm-pl @(unrealizedPL >= 0 ? "profit" : "loss")">
<div class="pl-label">Profitto/Perdita Stimato</div>
<div class="pl-amount">
@(unrealizedPL >= 0 ? "+" : "")$@unrealizedPL.ToString("N2")
</div>
<div class="pl-percent">
(@(plPercentage >= 0 ? "+" : "")@plPercentage.ToString("F2")%)
</div>
</div>
<div class="confirm-warning">
<span class="bi bi-exclamation-triangle"></span>
<p>
<strong>Attenzione:</strong> Questa azione chiuderà immediatamente la posizione al prezzo di mercato corrente.
L'operazione non può essere annullata.
</p>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" @onclick="HideCloseConfirmation">Annulla</button>
<button class="btn-danger" @onclick="ConfirmClosePosition">
<span class="bi bi-x-circle"></span>
Conferma Chiusura
</button>
</div>
</div>
</div>
}
<!-- Success Notification -->
@if (showSuccessNotification)
{
<div class="notification success">
<span class="bi bi-check-circle-fill"></span>
Posizione chiusa con successo!
</div>
}
</div>
<style>
.positions-page {
display: flex;
flex-direction: column;
gap: 2rem;
}
.header-stats {
display: flex;
gap: 1.5rem;
}
.stat-card {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem 1.5rem;
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
}
.stat-label {
font-size: 0.875rem;
color: #94a3b8;
font-weight: 600;
text-transform: uppercase;
}
.stat-value {
font-size: 1.75rem;
font-weight: 700;
color: #e2e8f0;
font-family: 'Courier New', monospace;
}
.stat-value.profit {
color: #10b981;
}
.stat-value.loss {
color: #ef4444;
}
.empty-state {
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
padding: 4rem 2rem;
text-align: center;
}
.empty-icon {
font-size: 4rem;
color: #64748b;
margin-bottom: 1.5rem;
}
.empty-state h3 {
color: #e2e8f0;
margin-bottom: 0.75rem;
}
.empty-state p {
color: #94a3b8;
margin-bottom: 2rem;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.empty-actions {
display: flex;
gap: 1rem;
justify-content: center;
}
.positions-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
gap: 1.5rem;
}
.position-card {
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
transition: all 0.3s ease;
}
.position-card:hover {
border-color: rgba(99, 102, 241, 0.4);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.position-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding-bottom: 1rem;
border-bottom: 1px solid rgba(99, 102, 241, 0.1);
}
.position-asset h3 {
font-size: 1.5rem;
font-weight: 700;
color: #e2e8f0;
margin: 0 0 0.25rem 0;
}
.position-date {
font-size: 0.75rem;
color: #64748b;
}
.position-pl {
text-align: right;
}
.pl-value {
display: block;
font-size: 1.25rem;
font-weight: 700;
font-family: 'Courier New', monospace;
}
.position-pl.profit .pl-value {
color: #10b981;
}
.position-pl.loss .pl-value {
color: #ef4444;
}
.pl-percentage {
font-size: 0.875rem;
opacity: 0.8;
}
.position-details {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.detail-label {
font-size: 0.875rem;
color: #94a3b8;
font-weight: 600;
}
.detail-value {
font-size: 0.875rem;
color: #e2e8f0;
font-family: 'Courier New', monospace;
font-weight: 600;
}
.strategy-badge {
padding: 0.25rem 0.75rem;
background: rgba(99, 102, 241, 0.2);
border-radius: 0.25rem;
color: #6366f1;
font-family: inherit;
}
.position-actions {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding-top: 1rem;
border-top: 1px solid rgba(99, 102, 241, 0.1);
}
.action-note {
font-size: 0.75rem;
color: #f59e0b;
display: flex;
align-items: center;
gap: 0.5rem;
}
/* Modal Styles */
.confirmation-details {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.confirm-asset {
text-align: center;
padding: 1rem;
background: rgba(99, 102, 241, 0.1);
border-radius: 0.5rem;
}
.confirm-asset h4 {
font-size: 1.5rem;
color: #e2e8f0;
margin: 0 0 0.5rem 0;
}
.confirm-amount {
color: #94a3b8;
font-family: 'Courier New', monospace;
}
.confirm-prices {
display: flex;
justify-content: space-around;
align-items: center;
padding: 1rem;
background: rgba(0, 0, 0, 0.2);
border-radius: 0.5rem;
}
.price-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
text-align: center;
}
.price-label {
font-size: 0.75rem;
color: #64748b;
text-transform: uppercase;
font-weight: 600;
}
.price-value {
font-size: 1.25rem;
color: #e2e8f0;
font-weight: 700;
font-family: 'Courier New', monospace;
}
.confirm-prices .bi {
font-size: 1.5rem;
color: #6366f1;
}
.confirm-pl {
text-align: center;
padding: 1.5rem;
border-radius: 0.5rem;
}
.confirm-pl.profit {
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.3);
}
.confirm-pl.loss {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
}
.pl-label {
font-size: 0.875rem;
color: #94a3b8;
margin-bottom: 0.5rem;
text-transform: uppercase;
font-weight: 600;
}
.pl-amount {
font-size: 2rem;
font-weight: 700;
font-family: 'Courier New', monospace;
margin-bottom: 0.25rem;
}
.confirm-pl.profit .pl-amount {
color: #10b981;
}
.confirm-pl.loss .pl-amount {
color: #ef4444;
}
.pl-percent {
font-size: 1rem;
opacity: 0.8;
}
.confirm-warning {
display: flex;
gap: 1rem;
padding: 1rem;
background: rgba(245, 158, 11, 0.1);
border: 1px solid rgba(245, 158, 11, 0.3);
border-radius: 0.5rem;
align-items: flex-start;
}
.confirm-warning .bi {
font-size: 1.25rem;
color: #f59e0b;
flex-shrink: 0;
}
.confirm-warning p {
margin: 0;
font-size: 0.875rem;
color: #cbd5e1;
line-height: 1.5;
}
.confirm-warning strong {
color: #f59e0b;
}
.notification {
position: fixed;
bottom: 2rem;
right: 2rem;
padding: 1rem 1.5rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 600;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
z-index: 9999;
animation: slideInRight 0.3s ease;
}
.notification.success {
background: rgba(16, 185, 129, 0.2);
border: 1px solid #10b981;
color: #10b981;
}
@@keyframes slideInRight {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@@media (max-width: 768px) {
.positions-grid {
grid-template-columns: 1fr;
}
.header-stats {
flex-direction: column;
}
.confirm-prices {
flex-direction: column;
gap: 1rem;
}
.confirm-prices .bi {
transform: rotate(90deg);
}
}
</style>
@code {
private List<Trade> activePositions = new();
private decimal totalValue = 0;
private decimal totalUnrealizedPL = 0;
private bool showCloseModal = false;
private Trade? positionToClose = null;
private bool showSuccessNotification = false;
protected override void OnInitialized()
{
LoadPositions();
BotService.OnTradeExecuted += HandleTradeExecuted;
BotService.OnPriceUpdated += HandlePriceUpdated;
BotService.OnStatusChanged += HandleStatusChanged;
}
private void LoadPositions()
{
activePositions = BotService.ActivePositions.Values.ToList();
CalculateTotals();
}
private void CalculateTotals()
{
totalValue = 0;
totalUnrealizedPL = 0;
foreach (var position in activePositions)
{
var currentPrice = GetCurrentPrice(position.Symbol);
var positionValue = position.Amount * currentPrice;
var pl = CalculateUnrealizedPL(position, currentPrice);
totalValue += positionValue;
totalUnrealizedPL += pl;
}
}
private decimal GetCurrentPrice(string symbol)
{
var latestPrice = BotService.GetLatestPrice(symbol);
return latestPrice?.Price ?? 0;
}
private decimal CalculateUnrealizedPL(Trade position, decimal currentPrice)
{
return (currentPrice - position.Price) * position.Amount;
}
private decimal CalculatePLPercentage(Trade position, decimal currentPrice)
{
if (position.Price == 0) return 0;
return ((currentPrice - position.Price) / position.Price) * 100;
}
private string FormatHoldingTime(TimeSpan time)
{
if (time.TotalDays >= 1)
return $"{(int)time.TotalDays}g {time.Hours}h";
else if (time.TotalHours >= 1)
return $"{(int)time.TotalHours}h {time.Minutes}m";
else
return $"{(int)time.TotalMinutes}m {time.Seconds}s";
}
private void ShowCloseConfirmation(Trade position)
{
positionToClose = position;
showCloseModal = true;
}
private void HideCloseConfirmation()
{
showCloseModal = false;
positionToClose = null;
}
private async Task ConfirmClosePosition()
{
if (positionToClose == null) return;
try
{
// Close position using TradingBotService public method
await BotService.ClosePositionManuallyAsync(positionToClose.Symbol);
LoggingService.LogInfo(
"Positions",
$"Posizione chiusa manualmente: {positionToClose.Symbol}",
$"Quantità: {positionToClose.Amount:F8}");
showSuccessNotification = true;
HideCloseConfirmation();
LoadPositions();
// Hide notification after 3 seconds
await Task.Delay(3000);
showSuccessNotification = false;
StateHasChanged();
}
catch (Exception ex)
{
LoggingService.LogError("Positions", $"Errore chiusura posizione: {ex.Message}");
}
}
private void HandleTradeExecuted(Trade trade)
{
LoadPositions();
InvokeAsync(StateHasChanged);
}
private void HandlePriceUpdated(string symbol, MarketPrice price)
{
if (activePositions.Any(p => p.Symbol == symbol))
{
CalculateTotals();
InvokeAsync(StateHasChanged);
}
}
private void HandleStatusChanged()
{
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
BotService.OnTradeExecuted -= HandleTradeExecuted;
BotService.OnPriceUpdated -= HandlePriceUpdated;
BotService.OnStatusChanged -= HandleStatusChanged;
}
}
+118
View File
@@ -2,6 +2,8 @@
@using TradingBot.Services
@using TradingBot.Models
@inject SettingsService SettingsService
@inject TradingBotService TradingBotService
@inject TradeHistoryService HistoryService
@implements IDisposable
@rendermode InteractiveServer
@@ -67,6 +69,39 @@
</div>
</div>
<div class="settings-section">
<h2>Dati Persistenti</h2>
<div class="settings-group">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Trade Salvati</div>
<div class="setting-description">@TradingBotService.Trades.Count trade nella cronologia</div>
</div>
<div class="setting-value">
@FormatBytes(dataSize)
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Posizioni Attive</div>
<div class="setting-description">@TradingBotService.ActivePositions.Count posizioni aperte</div>
</div>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Cancella Tutti i Dati</div>
<div class="setting-description text-danger">Elimina cronologia trade e resetta i saldi</div>
</div>
<button class="btn-danger" @onclick="ShowClearDataConfirmation" disabled="@TradingBotService.Status.IsRunning">
<span class="bi bi-trash"></span>
Cancella Dati
</button>
</div>
</div>
</div>
<div class="settings-section">
<h2>Avanzate</h2>
<div class="settings-group">
@@ -116,16 +151,57 @@
Impostazioni salvate con successo!
</div>
}
@if (showClearConfirmation)
{
<div class="modal-overlay" @onclick="HideClearDataConfirmation">
<div class="modal-dialog" @onclick:stopPropagation="true">
<div class="modal-header">
<h3>Conferma Cancellazione</h3>
<button class="btn-close" @onclick="HideClearDataConfirmation">×</button>
</div>
<div class="modal-body">
<p class="text-danger">
<strong>Attenzione!</strong> Questa azione eliminerà:
</p>
<ul>
<li>Tutta la cronologia dei trade (@TradingBotService.Trades.Count trade)</li>
<li>Tutte le posizioni attive (@TradingBotService.ActivePositions.Count posizioni)</li>
<li>I saldi verranno resettati ai valori iniziali</li>
</ul>
<p class="text-danger">
<strong>Questa operazione è irreversibile!</strong>
</p>
</div>
<div class="modal-footer">
<button class="btn-secondary" @onclick="HideClearDataConfirmation">Annulla</button>
<button class="btn-danger" @onclick="ConfirmClearData">
<span class="bi bi-trash"></span>
Conferma Cancellazione
</button>
</div>
</div>
</div>
}
</div>
@code {
private AppSettings settings = new();
private bool showNotification = false;
private bool showClearConfirmation = false;
private long dataSize = 0;
protected override void OnInitialized()
{
settings = SettingsService.GetSettings();
SettingsService.OnSettingsChanged += HandleSettingsChanged;
TradingBotService.OnStatusChanged += HandleStatusChanged;
UpdateDataSize();
}
private void UpdateDataSize()
{
dataSize = HistoryService.GetDataSize();
}
private void UpdateSetting<T>(string propertyName, T value)
@@ -148,6 +224,28 @@
ShowNotification();
}
private void ShowClearDataConfirmation()
{
showClearConfirmation = true;
}
private void HideClearDataConfirmation()
{
showClearConfirmation = false;
}
private async Task ConfirmClearData()
{
await TradingBotService.ClearAllDataAsync();
UpdateDataSize();
showClearConfirmation = false;
showNotification = true;
StateHasChanged();
await Task.Delay(3000);
showNotification = false;
StateHasChanged();
}
private async void ShowNotification()
{
showNotification = true;
@@ -157,14 +255,34 @@
StateHasChanged();
}
private string FormatBytes(long bytes)
{
string[] sizes = { "B", "KB", "MB", "GB" };
double len = bytes;
int order = 0;
while (len >= 1024 && order < sizes.Length - 1)
{
order++;
len = len / 1024;
}
return $"{len:0.##} {sizes[order]}";
}
private void HandleSettingsChanged()
{
settings = SettingsService.GetSettings();
InvokeAsync(StateHasChanged);
}
private void HandleStatusChanged()
{
UpdateDataSize();
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
SettingsService.OnSettingsChanged -= HandleSettingsChanged;
TradingBotService.OnStatusChanged -= HandleStatusChanged;
}
}
@@ -0,0 +1,555 @@
@page "/trading-control"
@using TradingBot.Services
@using TradingBot.Models
@inject TradingStrategiesService StrategiesService
@inject TradingBotService BotService
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Trading Control - TradingBot</PageTitle>
<div class="trading-control-page">
<div class="page-header">
<div>
<h1>Trading Control</h1>
<p class="subtitle">Gestisci le strategie di trading per ogni asset</p>
</div>
<div class="header-stats">
<div class="stat-item">
<span class="stat-label">Asset Attivi</span>
<span class="stat-value">@activeAssets</span>
</div>
<div class="stat-item">
<span class="stat-label">Strategie in Uso</span>
<span class="stat-value">@totalStrategies</span>
</div>
</div>
</div>
<!-- Assets Grid -->
<div class="assets-control-grid">
@foreach (var asset in BotService.AssetConfigurations.Values.OrderBy(a => a.Symbol))
{
var mapping = StrategiesService.GetAssetMapping(asset.Symbol);
var engineStatus = StrategiesService.GetEngineStatus(asset.Symbol);
var isActive = mapping?.IsActive ?? false;
<div class="asset-control-card @(isActive ? "active" : "")">
<div class="asset-header">
<div class="asset-info">
<h3>@asset.Symbol</h3>
<span class="asset-name">@asset.Name</span>
</div>
<div class="asset-controls">
<label class="toggle-switch">
<input type="checkbox"
checked="@isActive"
@onchange="(e) => ToggleAsset(asset.Symbol, (bool)e.Value!)"
disabled="@((mapping?.StrategyIds.Count ?? 0) == 0)" />
<span class="toggle-slider"></span>
</label>
</div>
</div>
@if (mapping != null && mapping.StrategyIds.Count > 0)
{
<div class="assigned-strategies">
<h4>Strategie Assegnate (@mapping.StrategyIds.Count)</h4>
<div class="strategies-list">
@foreach (var strategyId in mapping.StrategyIds)
{
var strategyInfo = availableStrategies[strategyId];
<div class="strategy-badge">
<span class="strategy-name">@strategyInfo.Name</span>
<span class="strategy-category">@strategyInfo.Category</span>
<button class="btn-remove" @onclick="() => RemoveStrategy(asset.Symbol, strategyId)">
<span class="bi bi-x"></span>
</button>
</div>
}
</div>
</div>
@if (engineStatus?.LastDecision != null && isActive)
{
<div class="last-decision">
<h4>Ultima Decisione</h4>
<div class="decision-info">
<span class="decision-type type-@engineStatus.LastDecision.Decision.ToString().ToLower()">
@engineStatus.LastDecision.Decision
</span>
<span class="decision-confidence">
Confidenza: @engineStatus.LastDecision.Confidence.ToString("F0")%
</span>
</div>
<div class="decision-reason">@engineStatus.LastDecision.Reason</div>
<div class="decision-votes">
<span class="vote buy">Buy: @engineStatus.LastDecision.BuyVotes</span>
<span class="vote sell">Sell: @engineStatus.LastDecision.SellVotes</span>
<span class="vote hold">Hold: @engineStatus.LastDecision.HoldVotes</span>
</div>
</div>
}
}
<div class="asset-actions">
<button class="btn-primary" @onclick="() => OpenStrategySelector(asset.Symbol, asset.Name)">
<span class="bi bi-plus-lg"></span>
@((mapping?.StrategyIds.Count ?? 0) > 0 ? "Gestisci Strategie" : "Assegna Strategie")
</button>
</div>
</div>
}
</div>
<!-- Strategy Selector Modal -->
@if (showStrategySelector)
{
<div class="modal-overlay" @onclick="CloseStrategySelector">
<div class="modal-dialog large" @onclick:stopPropagation="true">
<div class="modal-header">
<h3>Gestisci Strategie - @selectedAssetSymbol</h3>
<button class="btn-close" @onclick="CloseStrategySelector">×</button>
</div>
<div class="modal-body">
<div class="strategies-selector">
@foreach (var category in availableStrategies.Values.Select(s => s.Category).Distinct().OrderBy(c => c))
{
<div class="category-section">
<h4 class="category-title">@category</h4>
<div class="category-strategies">
@foreach (var strategy in availableStrategies.Values.Where(s => s.Category == category))
{
var isSelected = selectedStrategies.Contains(strategy.Id);
<div class="strategy-option @(isSelected ? "selected" : "")"
@onclick="() => ToggleStrategy(strategy.Id)">
<div class="strategy-option-header">
<div class="strategy-checkbox">
<input type="checkbox" checked="@isSelected" />
</div>
<div class="strategy-details">
<h5>@strategy.Name</h5>
<p>@strategy.Description</p>
</div>
</div>
<div class="strategy-meta">
<span class="risk-badge risk-@strategy.RiskLevel.ToString().ToLower()">
@strategy.RiskLevel Risk
</span>
<span class="timeframe-badge">
@strategy.RecommendedTimeFrame
</span>
</div>
</div>
}
</div>
</div>
}
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" @onclick="CloseStrategySelector">Annulla</button>
<button class="btn-primary" @onclick="SaveStrategies">
<span class="bi bi-check-lg"></span>
Salva (@selectedStrategies.Count strategie)
</button>
</div>
</div>
</div>
}
</div>
<style>
.trading-control-page {
display: flex;
flex-direction: column;
gap: 2rem;
}
.header-stats {
display: flex;
gap: 2rem;
}
.stat-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.stat-label {
font-size: 0.875rem;
color: #94a3b8;
font-weight: 600;
}
.stat-value {
font-size: 1.5rem;
font-weight: 700;
color: #6366f1;
}
.assets-control-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 1.5rem;
}
.asset-control-card {
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
padding: 1.5rem;
transition: all 0.3s ease;
}
.asset-control-card.active {
border-color: rgba(16, 185, 129, 0.5);
background: linear-gradient(135deg, #1a1f3a 0%, rgba(16, 185, 129, 0.05) 100%);
}
.asset-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1.5rem;
}
.asset-info h3 {
font-size: 1.5rem;
font-weight: 700;
color: #e2e8f0;
margin: 0 0 0.25rem 0;
}
.asset-name {
color: #94a3b8;
font-size: 0.875rem;
}
.assigned-strategies {
margin-bottom: 1.5rem;
}
.assigned-strategies h4 {
font-size: 0.875rem;
color: #94a3b8;
margin-bottom: 0.75rem;
text-transform: uppercase;
font-weight: 600;
}
.strategies-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.strategy-badge {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem;
background: rgba(0, 0, 0, 0.3);
border-radius: 0.5rem;
border: 1px solid rgba(99, 102, 241, 0.2);
}
.strategy-name {
flex: 1;
font-weight: 600;
color: #e2e8f0;
}
.strategy-category {
padding: 0.25rem 0.75rem;
background: rgba(99, 102, 241, 0.2);
border-radius: 0.25rem;
color: #6366f1;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
}
.btn-remove {
background: none;
border: none;
color: #ef4444;
cursor: pointer;
padding: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 0.25rem;
transition: background 0.2s;
}
.btn-remove:hover {
background: rgba(239, 68, 68, 0.2);
}
.last-decision {
background: rgba(0, 0, 0, 0.3);
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
}
.last-decision h4 {
font-size: 0.875rem;
color: #94a3b8;
margin-bottom: 0.75rem;
text-transform: uppercase;
font-weight: 600;
}
.decision-info {
display: flex;
gap: 1rem;
align-items: center;
margin-bottom: 0.75rem;
}
.decision-type {
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 700;
text-transform: uppercase;
font-size: 0.875rem;
}
.type-buy { background: rgba(16, 185, 129, 0.2); color: #10b981; }
.type-sell { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
.type-hold { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
.decision-confidence {
color: #94a3b8;
font-size: 0.875rem;
}
.decision-reason {
color: #cbd5e1;
font-size: 0.875rem;
margin-bottom: 0.75rem;
}
.decision-votes {
display: flex;
gap: 1rem;
}
.vote {
padding: 0.25rem 0.75rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 700;
}
.vote.buy { background: rgba(16, 185, 129, 0.1); color: #10b981; }
.vote.sell { background: rgba(239, 68, 68, 0.1); color: #ef4444; }
.vote.hold { background: rgba(245, 158, 11, 0.1); color: #f59e0b; }
.asset-actions {
display: flex;
gap: 0.75rem;
}
/* Modal Styles */
.modal-dialog.large {
max-width: 900px;
max-height: 80vh;
}
.modal-body {
max-height: 60vh;
overflow-y: auto;
}
.strategies-selector {
display: flex;
flex-direction: column;
gap: 2rem;
}
.category-section {
border-bottom: 1px solid rgba(99, 102, 241, 0.1);
padding-bottom: 1.5rem;
}
.category-section:last-child {
border-bottom: none;
}
.category-title {
font-size: 1rem;
font-weight: 700;
color: #6366f1;
margin-bottom: 1rem;
text-transform: uppercase;
}
.category-strategies {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.strategy-option {
background: rgba(0, 0, 0, 0.3);
border: 2px solid rgba(99, 102, 241, 0.2);
border-radius: 0.5rem;
padding: 1rem;
cursor: pointer;
transition: all 0.2s ease;
}
.strategy-option:hover {
border-color: rgba(99, 102, 241, 0.5);
background: rgba(99, 102, 241, 0.05);
}
.strategy-option.selected {
border-color: #6366f1;
background: rgba(99, 102, 241, 0.1);
}
.strategy-option-header {
display: flex;
gap: 1rem;
margin-bottom: 0.75rem;
}
.strategy-checkbox input {
width: 20px;
height: 20px;
cursor: pointer;
}
.strategy-details h5 {
font-size: 1rem;
font-weight: 700;
color: #e2e8f0;
margin: 0 0 0.25rem 0;
}
.strategy-details p {
font-size: 0.875rem;
color: #94a3b8;
margin: 0;
line-height: 1.5;
}
.strategy-meta {
display: flex;
gap: 0.75rem;
}
.risk-badge, .timeframe-badge {
padding: 0.25rem 0.75rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
}
.risk-low { background: rgba(16, 185, 129, 0.2); color: #10b981; }
.risk-medium { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
.risk-high { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
.risk-veryhigh { background: rgba(220, 38, 38, 0.3); color: #dc2626; }
.timeframe-badge {
background: rgba(139, 92, 246, 0.2);
color: #8b5cf6;
}
</style>
@code {
private Dictionary<string, StrategyInfo> availableStrategies = new();
private bool showStrategySelector = false;
private string selectedAssetSymbol = "";
private string selectedAssetName = "";
private List<string> selectedStrategies = new();
private int activeAssets = 0;
private int totalStrategies = 0;
protected override void OnInitialized()
{
LoadData();
StrategiesService.OnMappingsChanged += HandleMappingsChanged;
}
private void LoadData()
{
availableStrategies = StrategiesService.GetAvailableStrategies().ToDictionary(k => k.Key, v => v.Value);
var mappings = StrategiesService.GetAllMappings();
activeAssets = mappings.Values.Count(m => m.IsActive);
totalStrategies = mappings.Values.Sum(m => m.StrategyIds.Count);
}
private void ToggleAsset(string symbol, bool active)
{
if (active)
{
StrategiesService.ActivateAsset(symbol);
}
else
{
StrategiesService.DeactivateAsset(symbol);
}
LoadData();
}
private void OpenStrategySelector(string symbol, string name)
{
selectedAssetSymbol = symbol;
selectedAssetName = name;
var mapping = StrategiesService.GetAssetMapping(symbol);
selectedStrategies = mapping?.StrategyIds.ToList() ?? new List<string>();
showStrategySelector = true;
}
private void CloseStrategySelector()
{
showStrategySelector = false;
selectedStrategies.Clear();
}
private void ToggleStrategy(string strategyId)
{
if (selectedStrategies.Contains(strategyId))
{
selectedStrategies.Remove(strategyId);
}
else
{
selectedStrategies.Add(strategyId);
}
}
private void SaveStrategies()
{
StrategiesService.AssignStrategiesToAsset(selectedAssetSymbol, selectedAssetName, selectedStrategies);
CloseStrategySelector();
LoadData();
}
private void RemoveStrategy(string symbol, string strategyId)
{
StrategiesService.RemoveStrategyFromAsset(symbol, strategyId);
LoadData();
}
private void HandleMappingsChanged()
{
LoadData();
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
StrategiesService.OnMappingsChanged -= HandleMappingsChanged;
}
}
@@ -0,0 +1,220 @@
@using TradingBot.Services
@using TradingBot.Models
@inject IndicatorsService IndicatorsService
@inject TradingBotService BotService
@implements IDisposable
<div class="indicators-widget">
<div class="widget-header">
<h3>Indicatori Attivi</h3>
<a href="/indicators" class="btn-link">
Configura <span class="bi bi-arrow-right"></span>
</a>
</div>
<div class="indicators-grid">
@foreach (var indicator in enabledIndicators.Take(6))
{
<div class="indicator-mini-card">
<div class="indicator-mini-header">
<span class="indicator-mini-name">@indicator.Name</span>
<span class="indicator-mini-type">@indicator.Type</span>
</div>
@if (topAssets.Any())
{
var symbol = topAssets.First();
var status = IndicatorsService.GetIndicatorStatus(indicator.Id, symbol);
if (status != null)
{
<div class="indicator-mini-value">
<span class="value-number">@status.CurrentValue.ToString("F2")</span>
<span class="value-condition condition-@status.Condition.ToString().ToLower()">
@status.Condition
</span>
</div>
<div class="indicator-mini-recommendation">
@status.Recommendation
</div>
}
else
{
<div class="indicator-mini-loading">Calcolo...</div>
}
}
else
{
<div class="indicator-mini-empty">Nessun asset attivo</div>
}
</div>
}
</div>
@if (enabledIndicators.Count() > 6)
{
<div class="indicators-more">
<a href="/indicators" class="btn-secondary btn-sm">
Vedi tutti (@enabledIndicators.Count()) <span class="bi bi-arrow-right"></span>
</a>
</div>
}
</div>
<style>
.indicators-widget {
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
padding: 1.5rem;
}
.widget-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.widget-header h3 {
font-size: 1.125rem;
font-weight: 700;
color: #e2e8f0;
margin: 0;
}
.btn-link {
display: inline-flex;
align-items: center;
gap: 0.25rem;
color: #6366f1;
font-size: 0.875rem;
font-weight: 600;
text-decoration: none;
transition: color 0.2s;
}
.btn-link:hover {
color: #8b5cf6;
}
.indicators-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 1rem;
}
.indicator-mini-card {
background: #0f1629;
border-radius: 0.5rem;
padding: 1rem;
border: 1px solid rgba(99, 102, 241, 0.1);
}
.indicator-mini-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.indicator-mini-name {
font-weight: 700;
color: #e2e8f0;
font-size: 0.875rem;
}
.indicator-mini-type {
padding: 0.125rem 0.5rem;
background: rgba(99, 102, 241, 0.2);
border-radius: 0.25rem;
color: #6366f1;
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
}
.indicator-mini-value {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.value-number {
font-size: 1.25rem;
font-weight: 700;
color: #e2e8f0;
font-family: 'Courier New', monospace;
}
.value-condition {
padding: 0.125rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
}
.condition-overbought { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
.condition-oversold { background: rgba(16, 185, 129, 0.2); color: #10b981; }
.condition-bullish { background: rgba(16, 185, 129, 0.2); color: #10b981; }
.condition-bearish { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
.condition-neutral { background: rgba(100, 116, 139, 0.2); color: #94a3b8; }
.condition-ranging { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
.condition-trending { background: rgba(59, 130, 246, 0.2); color: #3b82f6; }
.indicator-mini-recommendation {
color: #94a3b8;
font-size: 0.75rem;
line-height: 1.4;
}
.indicator-mini-loading,
.indicator-mini-empty {
color: #64748b;
font-size: 0.875rem;
text-align: center;
padding: 1rem 0;
}
.indicators-more {
margin-top: 1rem;
text-align: center;
}
</style>
@code {
private List<IndicatorConfig> enabledIndicators = new();
private List<string> topAssets = new();
protected override void OnInitialized()
{
LoadData();
IndicatorsService.OnIndicatorsChanged += HandleUpdate;
BotService.OnStatusChanged += HandleUpdate;
}
private void LoadData()
{
enabledIndicators = IndicatorsService.GetEnabledIndicators().ToList();
topAssets = BotService.AssetConfigurations.Values
.Where(c => c.IsEnabled)
.OrderByDescending(c => c.CurrentBalance + (c.CurrentHoldings * (BotService.GetLatestPrice(c.Symbol)?.Price ?? 0)))
.Select(c => c.Symbol)
.Take(1)
.ToList();
}
private void HandleUpdate()
{
LoadData();
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
IndicatorsService.OnIndicatorsChanged -= HandleUpdate;
BotService.OnStatusChanged -= HandleUpdate;
}
}
+94
View File
@@ -0,0 +1,94 @@
namespace TradingBot.Models;
/// <summary>
/// Configuration for a trading indicator
/// </summary>
public class IndicatorConfig
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public IndicatorType Type { get; set; }
public bool IsEnabled { get; set; } = true;
// Thresholds for signals
public decimal? OverboughtThreshold { get; set; }
public decimal? OversoldThreshold { get; set; }
public decimal? BuyThreshold { get; set; }
public decimal? SellThreshold { get; set; }
// Indicator-specific parameters
public int Period { get; set; } = 14;
public int FastPeriod { get; set; } = 12;
public int SlowPeriod { get; set; } = 26;
public int SignalPeriod { get; set; } = 9;
}
/// <summary>
/// Real-time indicator signal
/// </summary>
public class IndicatorSignal
{
public string IndicatorId { get; set; } = string.Empty;
public string IndicatorName { get; set; } = string.Empty;
public string Symbol { get; set; } = string.Empty;
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public SignalStrength Strength { get; set; }
public SignalType Type { get; set; }
public string Message { get; set; } = string.Empty;
public decimal? Value { get; set; }
}
/// <summary>
/// Indicator status for a specific asset
/// </summary>
public class IndicatorStatus
{
public string IndicatorId { get; set; } = string.Empty;
public string Symbol { get; set; } = string.Empty;
public decimal CurrentValue { get; set; }
public decimal? PreviousValue { get; set; }
public MarketCondition Condition { get; set; }
public string Recommendation { get; set; } = string.Empty;
public DateTime LastUpdate { get; set; } = DateTime.UtcNow;
}
/// <summary>
/// Types of trading indicators
/// </summary>
public enum IndicatorType
{
RSI, // Relative Strength Index
MACD, // Moving Average Convergence Divergence
SMA, // Simple Moving Average
EMA, // Exponential Moving Average
BollingerBands, // Bollinger Bands
Stochastic, // Stochastic Oscillator
Volume, // Volume indicators
ATR // Average True Range (volatility)
}
/// <summary>
/// Signal strength levels
/// </summary>
public enum SignalStrength
{
Weak,
Moderate,
Strong,
VeryStrong
}
/// <summary>
/// Market condition based on indicators
/// </summary>
public enum MarketCondition
{
Neutral,
Overbought,
Oversold,
Bullish,
Bearish,
Ranging,
Trending
}
+27
View File
@@ -0,0 +1,27 @@
namespace TradingBot.Models;
/// <summary>
/// Represents a log entry with timestamp, severity and message
/// </summary>
public class LogEntry
{
public Guid Id { get; set; } = Guid.NewGuid();
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public LogLevel Level { get; set; }
public string Category { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public string? Details { get; set; }
public string? Symbol { get; set; }
}
/// <summary>
/// Log severity levels
/// </summary>
public enum LogLevel
{
Debug,
Info,
Warning,
Error,
Trade
}
+137
View File
@@ -0,0 +1,137 @@
namespace TradingBot.Models;
/// <summary>
/// Represents the mapping between an asset and its assigned trading strategies
/// </summary>
public class AssetStrategyMapping
{
public string Symbol { get; set; } = string.Empty;
public string AssetName { get; set; } = string.Empty;
public List<string> StrategyIds { get; set; } = new();
public bool IsActive { get; set; }
public DateTime ActivatedAt { get; set; }
public DateTime? DeactivatedAt { get; set; }
/// <summary>
/// Strategy-specific parameters override
/// Key: StrategyId, Value: Dictionary of parameter names and values
/// </summary>
public Dictionary<string, Dictionary<string, object>> StrategyParameters { get; set; } = new();
}
/// <summary>
/// Represents a trading strategy instance
/// </summary>
public class StrategyInfo
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty; // Trend, Oscillator, Volatility, etc.
public StrategyRisk RiskLevel { get; set; }
public TimeFrame RecommendedTimeFrame { get; set; }
public List<string> RequiredIndicators { get; set; } = new();
public Dictionary<string, ParameterInfo> Parameters { get; set; } = new();
}
/// <summary>
/// Parameter information for strategy configuration
/// </summary>
public class ParameterInfo
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public ParameterType Type { get; set; }
public object DefaultValue { get; set; } = 0;
public object? MinValue { get; set; }
public object? MaxValue { get; set; }
}
/// <summary>
/// Trading engine status for an asset
/// </summary>
public class TradingEngineStatus
{
public string Symbol { get; set; } = string.Empty;
public bool IsRunning { get; set; }
public int ActiveStrategies { get; set; }
public DateTime? LastSignalTime { get; set; }
public List<StrategySignal> RecentSignals { get; set; } = new();
public TradingDecision? LastDecision { get; set; }
}
/// <summary>
/// Signal from a specific strategy
/// </summary>
public class StrategySignal
{
public string StrategyId { get; set; } = string.Empty;
public string StrategyName { get; set; } = string.Empty;
public TradingSignal Signal { get; set; } = new();
public DateTime GeneratedAt { get; set; } = DateTime.UtcNow;
}
/// <summary>
/// Aggregated trading decision from multiple strategies
/// </summary>
public class TradingDecision
{
public string Symbol { get; set; } = string.Empty;
public SignalType Decision { get; set; }
public decimal Confidence { get; set; }
public string Reason { get; set; } = string.Empty;
public int BuyVotes { get; set; }
public int SellVotes { get; set; }
public int HoldVotes { get; set; }
public List<string> SupportingStrategies { get; set; } = new();
public List<string> OpposingStrategies { get; set; } = new();
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
}
/// <summary>
/// Strategy performance metrics
/// </summary>
public class StrategyPerformance
{
public string StrategyId { get; set; } = string.Empty;
public string Symbol { get; set; } = string.Empty;
public int TotalSignals { get; set; }
public int CorrectSignals { get; set; }
public decimal Accuracy { get; set; }
public decimal TotalProfit { get; set; }
public decimal AverageConfidence { get; set; }
public DateTime FirstSignalTime { get; set; }
public DateTime LastSignalTime { get; set; }
}
/// <summary>
/// Risk level for strategies
/// </summary>
public enum StrategyRisk
{
Low,
Medium,
High,
VeryHigh
}
/// <summary>
/// Recommended time frame for strategy
/// </summary>
public enum TimeFrame
{
ShortTerm, // Minutes to hours
MediumTerm, // Hours to days
LongTerm // Days to weeks
}
/// <summary>
/// Parameter data type
/// </summary>
public enum ParameterType
{
Integer,
Decimal,
Boolean,
String
}
+1
View File
@@ -5,6 +5,7 @@ public class TradingSignal
public string Symbol { get; set; } = string.Empty;
public SignalType Type { get; set; }
public decimal Price { get; set; }
public decimal Confidence { get; set; } // 0-100 confidence level
public string Reason { get; set; } = string.Empty;
public DateTime Timestamp { get; set; }
}
+7
View File
@@ -20,9 +20,16 @@ builder.Services.AddRazorComponents()
// Trading Bot Services - Using Simulated Market Data
builder.Services.AddSingleton<IMarketDataService, SimulatedMarketDataService>();
builder.Services.AddSingleton<ITradingStrategy, SimpleMovingAverageStrategy>();
builder.Services.AddSingleton<TradeHistoryService>();
builder.Services.AddSingleton<LoggingService>();
builder.Services.AddSingleton<IndicatorsService>();
builder.Services.AddSingleton<TradingStrategiesService>();
builder.Services.AddSingleton<TradingBotService>();
builder.Services.AddSingleton<SettingsService>();
// Register background service for graceful shutdown
builder.Services.AddHostedService<TradingBotBackgroundService>();
// Add health checks for Docker
builder.Services.AddHealthChecks();
+163
View File
@@ -0,0 +1,163 @@
# ?? TradingBot
**Automated Crypto Trading Bot** con interfaccia Blazor Server per trading algoritmico simulato.
[![.NET 10](https://img.shields.io/badge/.NET-10.0-512BD4)](https://dotnet.microsoft.com/)
[![Blazor](https://img.shields.io/badge/Blazor-Server-512BD4)](https://blazor.net/)
[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED)](https://www.docker.com/)
[![Version](https://img.shields.io/badge/version-1.5.2-blue)](https://gitea.encke-hake.ts.net/Alby96/Encelado/-/packages)
---
## ? Caratteristiche
- **Multi-Strategy Trading**: 8 strategie professionali assegnabili a ogni asset
- **Positions Management**: Visualizza e chiudi manualmente posizioni aperte
- **Detailed Portfolio Metrics**: Capitale totale, investito, disponibile, P&L, ROI
- **Trading Control**: Gestione visuale strategie con voting system
- **Dashboard Blazor**: Real-time updates ogni 3 secondi
- **15 Criptovalute**: BTC, ETH, BNB, ADA, SOL, XRP, DOT, DOGE, AVAX, MATIC, LINK, LTC, UNI, ATOM, XLM
- **Analisi Tecnica**: SMA, EMA, RSI, MACD, Bollinger Bands, Stochastic
- **Indicators System**: 7+ indicatori tecnici configurabili con segnali real-time
- **Portfolio Management**: Gestione automatizzata posizioni
- **Trade Persistence**: Salvataggio automatico trade e posizioni attive
- **Comprehensive Logs**: Sistema di logging real-time con filtri avanzati
- **Docker Ready**: Container ottimizzato con health checks
---
## ?? Trading Strategies
### **8 Strategie Professionali**
1. **RSI Strategy** - Oscillator (Medium Risk)
2. **MACD Strategy** - Momentum (Medium Risk)
3. **Bollinger Bands** - Volatility (Low Risk)
4. **Mean Reversion** - Contrarian (High Risk)
5. **Momentum** - Trend Following (Medium Risk)
6. **EMA Crossover** - Golden/Death Cross (Low Risk)
7. **Scalping** - Short-term (Very High Risk)
8. **Breakout** - Volatility Breakout (High Risk)
### **Multi-Strategy System**
- Assegna multiple strategie per asset
- Sistema di voting per decisioni aggregate
- Confidence-based trading
- Parametri configurabili per strategia
---
## ?? Quick Start
### Locale (Development)
```bash
git clone https://gitea.encke-hake.ts.net/Alby96/Encelado
cd Encelado/TradingBot
dotnet run
```
Accedi a: `http://localhost:5243`
### Docker
```bash
docker pull gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest
docker run -d -p 8888:8080 \
-v tradingbot-data:/app/data \
gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest
```
Accedi a: `http://localhost:8888`
### Unraid
Guida completa: [deployment/UNRAID_INSTALL.md](deployment/UNRAID_INSTALL.md)
```bash
# 1. Login Gitea Registry
docker login gitea.encke-hake.ts.net
# 2. Download template
wget -O /boot/config/plugins/dockerMan/templates-user/TradingBot.xml \
https://gitea.encke-hake.ts.net/Alby96/Encelado/raw/branch/main/TradingBot/deployment/unraid-template.xml
# 3. Install via Unraid Docker UI
```
---
## ?? Versioning
### Current Version: `1.5.2`
**Latest**: Metriche dettagliate capitale in sidebar (Totale, Investito, Disponibile, P&L, ROI)
```powershell
# Bug fix (1.5.2 ? 1.5.3)
.\bump-version.ps1 patch -Message "Fix calculation bug"
# New feature (1.5.2 ? 1.6.0)
.\bump-version.ps1 minor -Message "Add new strategy"
# Breaking change (1.5.2 ? 2.0.0)
.\bump-version.ps1 major -Message "New API"
```
Vedi [CHANGELOG.md](CHANGELOG.md) per release notes complete.
---
## ?? Publishing
### Da Visual Studio
1. **Build** ? **Configuration Manager** ? **Release**
2. **Build** ? **Publish TradingBot** ? Profilo **Docker**
3. Click **Publish**
Il sistema automaticamente:
- ? Build Docker image
- ? Tag: `latest`, `1.5.1`, `1.5.1-20241222`
- ? Push su Gitea Registry
### Deploy su Unraid
```bash
# Docker tab ? TradingBot ? Stop ? Force Update ? Start
```
---
## ?? Documentazione
- **[UNRAID_INSTALL.md](deployment/UNRAID_INSTALL.md)** - Installazione completa su Unraid
- **[CHANGELOG.md](CHANGELOG.md)** - Release notes e versioni
- **[Dockerfile](Dockerfile)** - Docker multi-stage build
- **[docker-compose.yml](deployment/docker-compose.yml)** - Deploy con Compose
---
## ??? Tecnologie
- **.NET 10** | **Blazor Server** | **C# 14**
- **Bootstrap 5.3** | **Docker** | **Gitea Registry**
---
## ?? License
MIT License - Copyright © 2024 Alby96
---
## ?? Autore
**Alberto** (Alby96)
- Gitea: [@Alby96](https://gitea.encke-hake.ts.net/Alby96)
- Repository: [Encelado/TradingBot](https://gitea.encke-hake.ts.net/Alby96/Encelado)
---
**?? Happy Trading!**
+346
View File
@@ -0,0 +1,346 @@
using TradingBot.Models;
using System.Text.Json;
namespace TradingBot.Services;
/// <summary>
/// Service for managing trading indicators configuration and signals
/// </summary>
public class IndicatorsService
{
private readonly Dictionary<string, IndicatorConfig> _indicators = new();
private readonly Dictionary<string, Dictionary<string, IndicatorStatus>> _indicatorStatus = new();
private readonly List<IndicatorSignal> _recentSignals = new();
private readonly string _configPath;
private const int MaxSignals = 100;
public event Action? OnIndicatorsChanged;
public event Action<IndicatorSignal>? OnSignalGenerated;
public IndicatorsService()
{
_configPath = Path.Combine(Directory.GetCurrentDirectory(), "data", "indicators-config.json");
InitializeDefaultIndicators();
LoadConfiguration();
}
private void InitializeDefaultIndicators()
{
_indicators["rsi"] = new IndicatorConfig
{
Id = "rsi",
Name = "RSI",
Description = "Relative Strength Index - Misura la forza del trend",
Type = IndicatorType.RSI,
IsEnabled = true,
Period = 14,
OverboughtThreshold = 70,
OversoldThreshold = 30
};
_indicators["macd"] = new IndicatorConfig
{
Id = "macd",
Name = "MACD",
Description = "Moving Average Convergence Divergence - Identifica cambi di trend",
Type = IndicatorType.MACD,
IsEnabled = true,
FastPeriod = 12,
SlowPeriod = 26,
SignalPeriod = 9
};
_indicators["sma_20"] = new IndicatorConfig
{
Id = "sma_20",
Name = "SMA 20",
Description = "Simple Moving Average 20 periodi - Trend a breve termine",
Type = IndicatorType.SMA,
IsEnabled = true,
Period = 20
};
_indicators["sma_50"] = new IndicatorConfig
{
Id = "sma_50",
Name = "SMA 50",
Description = "Simple Moving Average 50 periodi - Trend a medio termine",
Type = IndicatorType.SMA,
IsEnabled = true,
Period = 50
};
_indicators["ema_12"] = new IndicatorConfig
{
Id = "ema_12",
Name = "EMA 12",
Description = "Exponential Moving Average 12 periodi - Reattivo ai cambiamenti",
Type = IndicatorType.EMA,
IsEnabled = true,
Period = 12
};
_indicators["bollinger"] = new IndicatorConfig
{
Id = "bollinger",
Name = "Bollinger Bands",
Description = "Bande di Bollinger - Misura volatilità e livelli estremi",
Type = IndicatorType.BollingerBands,
IsEnabled = true,
Period = 20
};
_indicators["stochastic"] = new IndicatorConfig
{
Id = "stochastic",
Name = "Stochastic",
Description = "Oscillatore Stocastico - Identifica momenti di inversione",
Type = IndicatorType.Stochastic,
IsEnabled = false,
Period = 14,
OverboughtThreshold = 80,
OversoldThreshold = 20
};
}
/// <summary>
/// Get all indicator configurations
/// </summary>
public IReadOnlyDictionary<string, IndicatorConfig> GetIndicators()
{
return _indicators;
}
/// <summary>
/// Get enabled indicators only
/// </summary>
public IEnumerable<IndicatorConfig> GetEnabledIndicators()
{
return _indicators.Values.Where(i => i.IsEnabled);
}
/// <summary>
/// Update indicator configuration
/// </summary>
public void UpdateIndicator(string id, IndicatorConfig config)
{
_indicators[id] = config;
SaveConfiguration();
OnIndicatorsChanged?.Invoke();
}
/// <summary>
/// Toggle indicator on/off
/// </summary>
public void ToggleIndicator(string id, bool enabled)
{
if (_indicators.TryGetValue(id, out var indicator))
{
indicator.IsEnabled = enabled;
SaveConfiguration();
OnIndicatorsChanged?.Invoke();
}
}
/// <summary>
/// Update indicator status for a symbol
/// </summary>
public void UpdateIndicatorStatus(string indicatorId, string symbol, IndicatorStatus status)
{
if (!_indicatorStatus.ContainsKey(symbol))
{
_indicatorStatus[symbol] = new Dictionary<string, IndicatorStatus>();
}
_indicatorStatus[symbol][indicatorId] = status;
}
/// <summary>
/// Get indicator status for a symbol
/// </summary>
public IndicatorStatus? GetIndicatorStatus(string indicatorId, string symbol)
{
if (_indicatorStatus.TryGetValue(symbol, out var symbolIndicators))
{
symbolIndicators.TryGetValue(indicatorId, out var status);
return status;
}
return null;
}
/// <summary>
/// Get all indicator statuses for a symbol
/// </summary>
public IEnumerable<IndicatorStatus> GetSymbolIndicators(string symbol)
{
if (_indicatorStatus.TryGetValue(symbol, out var symbolIndicators))
{
return symbolIndicators.Values;
}
return Enumerable.Empty<IndicatorStatus>();
}
/// <summary>
/// Generate and record a signal
/// </summary>
public void GenerateSignal(IndicatorSignal signal)
{
_recentSignals.Insert(0, signal);
// Maintain max size
while (_recentSignals.Count > MaxSignals)
{
_recentSignals.RemoveAt(_recentSignals.Count - 1);
}
OnSignalGenerated?.Invoke(signal);
}
/// <summary>
/// Get recent signals
/// </summary>
public IReadOnlyList<IndicatorSignal> GetRecentSignals(int count = 20)
{
return _recentSignals.Take(count).ToList().AsReadOnly();
}
/// <summary>
/// Get signals for a specific symbol
/// </summary>
public IReadOnlyList<IndicatorSignal> GetSymbolSignals(string symbol, int count = 20)
{
return _recentSignals
.Where(s => s.Symbol.Equals(symbol, StringComparison.OrdinalIgnoreCase))
.Take(count)
.ToList()
.AsReadOnly();
}
/// <summary>
/// Analyze indicators and generate trading recommendation
/// </summary>
public TradingRecommendation AnalyzeIndicators(string symbol)
{
var recommendation = new TradingRecommendation
{
Symbol = symbol,
Timestamp = DateTime.UtcNow
};
var symbolIndicators = GetSymbolIndicators(symbol).ToList();
if (!symbolIndicators.Any())
{
recommendation.Action = "HOLD";
recommendation.Confidence = 0;
recommendation.Reason = "Indicatori non disponibili";
return recommendation;
}
int buySignals = 0;
int sellSignals = 0;
int totalEnabled = GetEnabledIndicators().Count();
foreach (var status in symbolIndicators)
{
if (!_indicators.TryGetValue(status.IndicatorId, out var config) || !config.IsEnabled)
continue;
switch (status.Condition)
{
case MarketCondition.Oversold:
case MarketCondition.Bullish:
buySignals++;
recommendation.SupportingIndicators.Add($"{config.Name}: {status.Recommendation}");
break;
case MarketCondition.Overbought:
case MarketCondition.Bearish:
sellSignals++;
recommendation.SupportingIndicators.Add($"{config.Name}: {status.Recommendation}");
break;
}
}
// Determine action based on signals
if (buySignals > sellSignals && buySignals >= totalEnabled * 0.6m)
{
recommendation.Action = "BUY";
recommendation.Confidence = (decimal)buySignals / totalEnabled * 100;
recommendation.Reason = $"{buySignals}/{totalEnabled} indicatori suggeriscono acquisto";
}
else if (sellSignals > buySignals && sellSignals >= totalEnabled * 0.6m)
{
recommendation.Action = "SELL";
recommendation.Confidence = (decimal)sellSignals / totalEnabled * 100;
recommendation.Reason = $"{sellSignals}/{totalEnabled} indicatori suggeriscono vendita";
}
else
{
recommendation.Action = "HOLD";
recommendation.Confidence = 50;
recommendation.Reason = "Segnali contrastanti - attendere conferma";
}
return recommendation;
}
private void SaveConfiguration()
{
try
{
var directory = Path.GetDirectoryName(_configPath);
if (directory != null && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var json = JsonSerializer.Serialize(_indicators, new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(_configPath, json);
}
catch (Exception ex)
{
Console.WriteLine($"Error saving indicators configuration: {ex.Message}");
}
}
private void LoadConfiguration()
{
try
{
if (File.Exists(_configPath))
{
var json = File.ReadAllText(_configPath);
var loaded = JsonSerializer.Deserialize<Dictionary<string, IndicatorConfig>>(json);
if (loaded != null)
{
foreach (var kvp in loaded)
{
_indicators[kvp.Key] = kvp.Value;
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error loading indicators configuration: {ex.Message}");
}
}
}
/// <summary>
/// Trading recommendation based on multiple indicators
/// </summary>
public class TradingRecommendation
{
public string Symbol { get; set; } = string.Empty;
public DateTime Timestamp { get; set; }
public string Action { get; set; } = "HOLD"; // BUY, SELL, HOLD
public decimal Confidence { get; set; }
public string Reason { get; set; } = string.Empty;
public List<string> SupportingIndicators { get; set; } = new();
}
+122
View File
@@ -0,0 +1,122 @@
using TradingBot.Models;
using System.Collections.Concurrent;
namespace TradingBot.Services;
/// <summary>
/// Centralized logging service for application events
/// </summary>
public class LoggingService
{
private readonly ConcurrentQueue<LogEntry> _logs = new();
private const int MaxLogEntries = 500;
public event Action? OnLogAdded;
/// <summary>
/// Get all log entries
/// </summary>
public IReadOnlyList<LogEntry> GetLogs()
{
return _logs.ToList().AsReadOnly();
}
/// <summary>
/// Add a debug log entry
/// </summary>
public void LogDebug(string category, string message, string? details = null)
{
AddLog(Models.LogLevel.Debug, category, message, details);
}
/// <summary>
/// Add an info log entry
/// </summary>
public void LogInfo(string category, string message, string? details = null, string? symbol = null)
{
AddLog(Models.LogLevel.Info, category, message, details, symbol);
}
/// <summary>
/// Add a warning log entry
/// </summary>
public void LogWarning(string category, string message, string? details = null, string? symbol = null)
{
AddLog(Models.LogLevel.Warning, category, message, details, symbol);
}
/// <summary>
/// Add an error log entry
/// </summary>
public void LogError(string category, string message, string? details = null, string? symbol = null)
{
AddLog(Models.LogLevel.Error, category, message, details, symbol);
}
/// <summary>
/// Add a trade log entry
/// </summary>
public void LogTrade(string symbol, string message, string? details = null)
{
AddLog(Models.LogLevel.Trade, "Trading", message, details, symbol);
}
/// <summary>
/// Clear all logs
/// </summary>
public void ClearLogs()
{
_logs.Clear();
OnLogAdded?.Invoke();
}
/// <summary>
/// Get logs filtered by level
/// </summary>
public IReadOnlyList<LogEntry> GetLogsByLevel(Models.LogLevel level)
{
return _logs.Where(l => l.Level == level).ToList().AsReadOnly();
}
/// <summary>
/// Get logs filtered by category
/// </summary>
public IReadOnlyList<LogEntry> GetLogsByCategory(string category)
{
return _logs.Where(l => l.Category.Equals(category, StringComparison.OrdinalIgnoreCase))
.ToList()
.AsReadOnly();
}
/// <summary>
/// Get logs filtered by symbol
/// </summary>
public IReadOnlyList<LogEntry> GetLogsBySymbol(string symbol)
{
return _logs.Where(l => l.Symbol != null && l.Symbol.Equals(symbol, StringComparison.OrdinalIgnoreCase))
.ToList()
.AsReadOnly();
}
private void AddLog(Models.LogLevel level, string category, string message, string? details = null, string? symbol = null)
{
var logEntry = new LogEntry
{
Level = level,
Category = category,
Message = message,
Details = details,
Symbol = symbol
};
_logs.Enqueue(logEntry);
// Maintain max size
while (_logs.Count > MaxLogEntries)
{
_logs.TryDequeue(out _);
}
OnLogAdded?.Invoke();
}
}
+18
View File
@@ -70,4 +70,22 @@ public static class TechnicalAnalysis
return (macdLine, signalLine, histogram);
}
public static (decimal upper, decimal middle, decimal lower) CalculateBollingerBands(List<decimal> prices, int period = 20, decimal standardDeviations = 2)
{
if (prices.Count < period) return (0, 0, 0);
var recentPrices = prices.TakeLast(period).ToList();
var sma = recentPrices.Average();
// Calculate standard deviation
var squaredDifferences = recentPrices.Select(p => (double)Math.Pow((double)(p - sma), 2));
var variance = squaredDifferences.Average();
var stdDev = (decimal)Math.Sqrt(variance);
var upper = sma + (standardDeviations * stdDev);
var lower = sma - (standardDeviations * stdDev);
return (upper, sma, lower);
}
}
+164
View File
@@ -0,0 +1,164 @@
using System.Text.Json;
using TradingBot.Models;
namespace TradingBot.Services;
/// <summary>
/// Service for persisting trade history and active positions to disk
/// </summary>
public class TradeHistoryService
{
private readonly string _dataDirectory;
private readonly string _tradesFilePath;
private readonly string _activePositionsFilePath;
private readonly ILogger<TradeHistoryService> _logger;
private readonly JsonSerializerOptions _jsonOptions;
public TradeHistoryService(ILogger<TradeHistoryService> logger)
{
_logger = logger;
_dataDirectory = Path.Combine(Directory.GetCurrentDirectory(), "data");
_tradesFilePath = Path.Combine(_dataDirectory, "trade-history.json");
_activePositionsFilePath = Path.Combine(_dataDirectory, "active-positions.json");
_jsonOptions = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
EnsureDataDirectoryExists();
}
private void EnsureDataDirectoryExists()
{
if (!Directory.Exists(_dataDirectory))
{
Directory.CreateDirectory(_dataDirectory);
_logger.LogInformation("Created data directory: {Directory}", _dataDirectory);
}
}
/// <summary>
/// Save complete trade history to disk
/// </summary>
public async Task SaveTradeHistoryAsync(List<Trade> trades)
{
try
{
var json = JsonSerializer.Serialize(trades, _jsonOptions);
await File.WriteAllTextAsync(_tradesFilePath, json);
_logger.LogInformation("Saved {Count} trades to history", trades.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to save trade history");
}
}
/// <summary>
/// Load trade history from disk
/// </summary>
public async Task<List<Trade>> LoadTradeHistoryAsync()
{
try
{
if (!File.Exists(_tradesFilePath))
{
_logger.LogInformation("No trade history file found, starting fresh");
return new List<Trade>();
}
var json = await File.ReadAllTextAsync(_tradesFilePath);
var trades = JsonSerializer.Deserialize<List<Trade>>(json, _jsonOptions);
_logger.LogInformation("Loaded {Count} trades from history", trades?.Count ?? 0);
return trades ?? new List<Trade>();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load trade history, starting fresh");
return new List<Trade>();
}
}
/// <summary>
/// Save active positions (open trades) to disk
/// </summary>
public async Task SaveActivePositionsAsync(Dictionary<string, Trade> activePositions)
{
try
{
var json = JsonSerializer.Serialize(activePositions, _jsonOptions);
await File.WriteAllTextAsync(_activePositionsFilePath, json);
_logger.LogInformation("Saved {Count} active positions", activePositions.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to save active positions");
}
}
/// <summary>
/// Load active positions from disk
/// </summary>
public async Task<Dictionary<string, Trade>> LoadActivePositionsAsync()
{
try
{
if (!File.Exists(_activePositionsFilePath))
{
_logger.LogInformation("No active positions file found");
return new Dictionary<string, Trade>();
}
var json = await File.ReadAllTextAsync(_activePositionsFilePath);
var positions = JsonSerializer.Deserialize<Dictionary<string, Trade>>(json, _jsonOptions);
_logger.LogInformation("Loaded {Count} active positions", positions?.Count ?? 0);
return positions ?? new Dictionary<string, Trade>();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load active positions");
return new Dictionary<string, Trade>();
}
}
/// <summary>
/// Clear all persisted data
/// </summary>
public void ClearAll()
{
try
{
if (File.Exists(_tradesFilePath))
File.Delete(_tradesFilePath);
if (File.Exists(_activePositionsFilePath))
File.Delete(_activePositionsFilePath);
_logger.LogInformation("Cleared all persisted trade data");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to clear persisted data");
}
}
/// <summary>
/// Get total file size of persisted data
/// </summary>
public long GetDataSize()
{
long size = 0;
if (File.Exists(_tradesFilePath))
size += new FileInfo(_tradesFilePath).Length;
if (File.Exists(_activePositionsFilePath))
size += new FileInfo(_activePositionsFilePath).Length;
return size;
}
}
@@ -0,0 +1,58 @@
namespace TradingBot.Services;
/// <summary>
/// Background service for automatic data persistence on application shutdown
/// </summary>
public class TradingBotBackgroundService : BackgroundService
{
private readonly TradingBotService _tradingBotService;
private readonly ILogger<TradingBotBackgroundService> _logger;
private readonly IHostApplicationLifetime _lifetime;
public TradingBotBackgroundService(
TradingBotService tradingBotService,
ILogger<TradingBotBackgroundService> logger,
IHostApplicationLifetime lifetime)
{
_tradingBotService = tradingBotService;
_logger = logger;
_lifetime = lifetime;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("TradingBot Background Service started");
// Register shutdown handler
_lifetime.ApplicationStopping.Register(OnShutdown);
// Keep service running
await Task.Delay(Timeout.Infinite, stoppingToken);
}
private void OnShutdown()
{
_logger.LogInformation("Application shutdown detected, saving trade data...");
try
{
// Stop bot if running
if (_tradingBotService.Status.IsRunning)
{
_tradingBotService.Stop();
}
_logger.LogInformation("Trade data saved successfully on shutdown");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error saving data on shutdown");
}
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("TradingBot Background Service stopping");
await base.StopAsync(cancellationToken);
}
}
+295 -14
View File
@@ -6,17 +6,24 @@ public class TradingBotService
{
private readonly IMarketDataService _marketDataService;
private readonly ITradingStrategy _strategy;
private readonly TradeHistoryService _historyService;
private readonly LoggingService _loggingService;
private readonly IndicatorsService _indicatorsService;
private readonly TradingStrategiesService _strategiesService;
private readonly Dictionary<string, AssetConfiguration> _assetConfigs = new();
private readonly Dictionary<string, AssetStatistics> _assetStats = new();
private readonly List<Trade> _trades = new();
private readonly Dictionary<string, List<MarketPrice>> _priceHistory = new();
private readonly Dictionary<string, TechnicalIndicators> _indicators = new();
private readonly Dictionary<string, Trade> _activePositions = new();
private Timer? _timer;
private Timer? _persistenceTimer;
public BotStatus Status { get; private set; } = new();
public IReadOnlyList<Trade> Trades => _trades.AsReadOnly();
public IReadOnlyDictionary<string, AssetConfiguration> AssetConfigurations => _assetConfigs;
public IReadOnlyDictionary<string, AssetStatistics> AssetStatistics => _assetStats;
public IReadOnlyDictionary<string, Trade> ActivePositions => _activePositions;
public event Action? OnStatusChanged;
public event Action<TradingSignal>? OnSignalGenerated;
@@ -25,10 +32,20 @@ public class TradingBotService
public event Action<string, MarketPrice>? OnPriceUpdated;
public event Action? OnStatisticsUpdated;
public TradingBotService(IMarketDataService marketDataService, ITradingStrategy strategy)
public TradingBotService(
IMarketDataService marketDataService,
ITradingStrategy strategy,
TradeHistoryService historyService,
LoggingService loggingService,
IndicatorsService indicatorsService,
TradingStrategiesService strategiesService)
{
_marketDataService = marketDataService;
_strategy = strategy;
_historyService = historyService;
_loggingService = loggingService;
_indicatorsService = indicatorsService;
_strategiesService = strategiesService;
Status.CurrentStrategy = strategy.Name;
// Subscribe to simulated market updates if available
@@ -38,6 +55,52 @@ public class TradingBotService
}
InitializeDefaultAssets();
// Load persisted data
_ = LoadPersistedDataAsync();
_loggingService.LogInfo("System", "TradingBot Service initialized");
}
private async Task LoadPersistedDataAsync()
{
try
{
// Load trade history
var trades = await _historyService.LoadTradeHistoryAsync();
_trades.AddRange(trades);
// Load active positions
var positions = await _historyService.LoadActivePositionsAsync();
foreach (var kvp in positions)
{
_activePositions[kvp.Key] = kvp.Value;
}
// Restore asset configurations from active positions
RestoreAssetConfigurationsFromTrades();
OnStatusChanged?.Invoke();
}
catch (Exception ex)
{
Console.WriteLine($"Error loading persisted data: {ex.Message}");
}
}
private void RestoreAssetConfigurationsFromTrades()
{
foreach (var position in _activePositions.Values)
{
if (_assetConfigs.TryGetValue(position.Symbol, out var config))
{
if (position.Type == TradeType.Buy)
{
config.CurrentHoldings += position.Amount;
config.AverageEntryPrice = position.Price;
}
}
}
}
private void InitializeDefaultAssets()
@@ -64,7 +127,7 @@ public class TradingBotService
{
Symbol = symbol,
Name = assetNames.TryGetValue(symbol, out var name) ? name : symbol,
IsEnabled = true, // Enable ALL assets by default for full simulation
IsEnabled = true,
InitialBalance = 1000m,
CurrentBalance = 1000m
};
@@ -122,6 +185,8 @@ public class TradingBotService
Status.IsRunning = true;
Status.StartedAt = DateTime.UtcNow;
_loggingService.LogInfo("Bot", "Trading Bot started", $"Strategy: {_strategy.Name}");
// Reset daily trade counts
foreach (var config in _assetConfigs.Values)
{
@@ -135,10 +200,17 @@ public class TradingBotService
// Start update timer (every 3 seconds for simulation)
_timer = new Timer(async _ => await UpdateAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(3));
// Start persistence timer (save every 30 seconds)
_persistenceTimer = new Timer(
async _ => await SaveDataAsync(),
null,
TimeSpan.FromSeconds(30),
TimeSpan.FromSeconds(30));
OnStatusChanged?.Invoke();
}
public void Stop()
public async void Stop()
{
if (!Status.IsRunning) return;
@@ -146,9 +218,30 @@ public class TradingBotService
_timer?.Dispose();
_timer = null;
_persistenceTimer?.Dispose();
_persistenceTimer = null;
_loggingService.LogInfo("Bot", "Trading Bot stopped", $"Total trades: {_trades.Count}");
// Save data on stop
await SaveDataAsync();
OnStatusChanged?.Invoke();
}
private async Task SaveDataAsync()
{
try
{
await _historyService.SaveTradeHistoryAsync(_trades);
await _historyService.SaveActivePositionsAsync(_activePositions);
}
catch (Exception ex)
{
Console.WriteLine($"Error saving data: {ex.Message}");
}
}
private void HandleSimulatedPriceUpdate()
{
if (Status.IsRunning)
@@ -192,7 +285,6 @@ public class TradingBotService
private async Task ProcessAssetUpdate(MarketPrice price)
{
// Add null check for price
if (price == null || price.Price <= 0)
return;
@@ -228,7 +320,6 @@ public class TradingBotService
// Generate trading signal
var signal = await _strategy.AnalyzeAsync(price.Symbol, _priceHistory[price.Symbol]);
// Add null check for signal
if (signal != null)
{
OnSignalGenerated?.Invoke(signal);
@@ -266,7 +357,7 @@ public class TradingBotService
if (tradeAmount >= config.MinTradeAmount)
{
ExecuteBuy(symbol, price.Price, tradeAmount, config);
await ExecuteBuyAsync(symbol, price.Price, tradeAmount, config);
}
}
// Sell logic
@@ -283,14 +374,12 @@ public class TradingBotService
if (profitPercentage >= config.TakeProfitPercentage ||
profitPercentage <= -config.StopLossPercentage)
{
ExecuteSell(symbol, price.Price, config.CurrentHoldings, config);
await ExecuteSellAsync(symbol, price.Price, config.CurrentHoldings, config);
}
}
await Task.CompletedTask;
}
private void ExecuteBuy(string symbol, decimal price, decimal amountUSD, AssetConfiguration config)
private async Task ExecuteBuyAsync(string symbol, decimal price, decimal amountUSD, AssetConfiguration config)
{
var amount = amountUSD / price;
@@ -316,14 +405,24 @@ public class TradingBotService
};
_trades.Add(trade);
_activePositions[symbol] = trade;
UpdateAssetStatistics(symbol, trade);
Status.TradesExecuted++;
_loggingService.LogTrade(
symbol,
$"BUY {amount:F6} {symbol} @ ${price:N2}",
$"Value: ${amountUSD:N2} | Balance: ${config.CurrentBalance:N2}");
OnTradeExecuted?.Invoke(trade);
OnStatusChanged?.Invoke();
// Save immediately after trade
await SaveDataAsync();
}
private void ExecuteSell(string symbol, decimal price, decimal amount, AssetConfiguration config)
private async Task ExecuteSellAsync(string symbol, decimal price, decimal amount, AssetConfiguration config)
{
var amountUSD = amount * price;
var profit = (price - config.AverageEntryPrice) * amount;
@@ -346,11 +445,21 @@ public class TradingBotService
};
_trades.Add(trade);
_activePositions.Remove(symbol);
UpdateAssetStatistics(symbol, trade, profit);
Status.TradesExecuted++;
_loggingService.LogTrade(
symbol,
$"SELL {amount:F6} {symbol} @ ${price:N2}",
$"Value: ${amountUSD:N2} | Profit: ${profit:N2} | Balance: ${config.CurrentBalance:N2}");
OnTradeExecuted?.Invoke(trade);
OnStatusChanged?.Invoke();
// Save immediately after trade
await SaveDataAsync();
}
private void UpdateIndicators(string symbol)
@@ -358,13 +467,11 @@ public class TradingBotService
if (!_priceHistory.TryGetValue(symbol, out var history) || history == null || history.Count < 26)
return;
// Filter out null prices and extract valid price values
var prices = history
.Where(p => p != null && p.Price > 0)
.Select(p => p.Price)
.ToList();
// Ensure we still have enough data after filtering
if (prices.Count < 26)
return;
@@ -383,8 +490,138 @@ public class TradingBotService
_indicators[symbol] = indicators;
OnIndicatorsUpdated?.Invoke(symbol, indicators);
// Update IndicatorsService statuses
UpdateIndicatorStatuses(symbol, indicators, prices);
}
private void UpdateIndicatorStatuses(string symbol, TechnicalIndicators indicators, List<decimal> prices)
{
// Update RSI status
var rsiConfig = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "rsi");
if (rsiConfig?.IsEnabled == true)
{
var rsiStatus = new IndicatorStatus
{
IndicatorId = "rsi",
Symbol = symbol,
CurrentValue = indicators.RSI,
Condition = indicators.RSI > (rsiConfig.OverboughtThreshold ?? 70) ? MarketCondition.Overbought :
indicators.RSI < (rsiConfig.OversoldThreshold ?? 30) ? MarketCondition.Oversold :
MarketCondition.Neutral,
Recommendation = indicators.RSI > (rsiConfig.OverboughtThreshold ?? 70) ? "Possibile vendita" :
indicators.RSI < (rsiConfig.OversoldThreshold ?? 30) ? "Possibile acquisto" :
"Attendi conferma"
};
_indicatorsService.UpdateIndicatorStatus("rsi", symbol, rsiStatus);
// Generate signal if crossing threshold
if (indicators.RSI < 30)
{
_indicatorsService.GenerateSignal(new IndicatorSignal
{
IndicatorId = "rsi",
IndicatorName = "RSI",
Symbol = symbol,
Type = SignalType.Buy,
Strength = indicators.RSI < 20 ? SignalStrength.VeryStrong : SignalStrength.Strong,
Message = $"RSI in zona ipervenduto: {indicators.RSI:F2}",
Value = indicators.RSI
});
}
else if (indicators.RSI > 70)
{
_indicatorsService.GenerateSignal(new IndicatorSignal
{
IndicatorId = "rsi",
IndicatorName = "RSI",
Symbol = symbol,
Type = SignalType.Sell,
Strength = indicators.RSI > 80 ? SignalStrength.VeryStrong : SignalStrength.Strong,
Message = $"RSI in zona ipercomprato: {indicators.RSI:F2}",
Value = indicators.RSI
});
}
}
// Update MACD status
var macdConfig = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "macd");
if (macdConfig?.IsEnabled == true)
{
var macdStatus = new IndicatorStatus
{
IndicatorId = "macd",
Symbol = symbol,
CurrentValue = indicators.MACD,
Condition = indicators.Histogram > 0 ? MarketCondition.Bullish : MarketCondition.Bearish,
Recommendation = indicators.Histogram > 0 ? "Trend rialzista" : "Trend ribassista"
};
_indicatorsService.UpdateIndicatorStatus("macd", symbol, macdStatus);
// Generate signal on crossover
if (Math.Abs(indicators.Histogram) < 0.5m) // Near crossover
{
_indicatorsService.GenerateSignal(new IndicatorSignal
{
IndicatorId = "macd",
IndicatorName = "MACD",
Symbol = symbol,
Type = indicators.Histogram > 0 ? SignalType.Buy : SignalType.Sell,
Strength = SignalStrength.Moderate,
Message = $"MACD {(indicators.Histogram > 0 ? "bullish" : "bearish")} crossover",
Value = indicators.MACD
});
}
}
// Update SMA statuses
var currentPrice = prices.Last();
var sma20Config = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "sma_20");
if (sma20Config?.IsEnabled == true && prices.Count >= 20)
{
var sma20 = prices.TakeLast(20).Average();
var sma20Status = new IndicatorStatus
{
IndicatorId = "sma_20",
Symbol = symbol,
CurrentValue = sma20,
Condition = currentPrice > sma20 ? MarketCondition.Bullish : MarketCondition.Bearish,
Recommendation = currentPrice > sma20 ? "Prezzo sopra media" : "Prezzo sotto media"
};
_indicatorsService.UpdateIndicatorStatus("sma_20", symbol, sma20Status);
}
var sma50Config = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "sma_50");
if (sma50Config?.IsEnabled == true && prices.Count >= 50)
{
var sma50 = prices.TakeLast(50).Average();
var sma50Status = new IndicatorStatus
{
IndicatorId = "sma_50",
Symbol = symbol,
CurrentValue = sma50,
Condition = currentPrice > sma50 ? MarketCondition.Bullish : MarketCondition.Bearish,
Recommendation = currentPrice > sma50 ? "Trend rialzista medio termine" : "Trend ribassista medio termine"
};
_indicatorsService.UpdateIndicatorStatus("sma_50", symbol, sma50Status);
}
// Update EMA status
var ema12Config = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "ema_12");
if (ema12Config?.IsEnabled == true)
{
var ema12Status = new IndicatorStatus
{
IndicatorId = "ema_12",
Symbol = symbol,
CurrentValue = indicators.EMA12,
Condition = currentPrice > indicators.EMA12 ? MarketCondition.Bullish : MarketCondition.Bearish,
Recommendation = currentPrice > indicators.EMA12 ? "Trend positivo" : "Trend negativo"
};
_indicatorsService.UpdateIndicatorStatus("ema_12", symbol, ema12Status);
}
}
private void UpdateAssetStatistics(string symbol, Trade trade, decimal? realizedProfit = null)
{
if (!_assetStats.TryGetValue(symbol, out var stats))
@@ -512,4 +749,48 @@ public class TradingBotService
var history = GetPriceHistory(symbol);
return history?.LastOrDefault();
}
public async Task ClearAllDataAsync()
{
_trades.Clear();
_activePositions.Clear();
_historyService.ClearAll();
foreach (var config in _assetConfigs.Values)
{
config.CurrentBalance = config.InitialBalance;
config.CurrentHoldings = 0;
config.AverageEntryPrice = 0;
config.DailyTradeCount = 0;
}
OnStatusChanged?.Invoke();
await Task.CompletedTask;
}
/// <summary>
/// Manually close a position
/// </summary>
public async Task ClosePositionManuallyAsync(string symbol)
{
if (!_activePositions.TryGetValue(symbol, out var position))
{
throw new InvalidOperationException($"No active position found for {symbol}");
}
if (!_assetConfigs.TryGetValue(symbol, out var config))
{
throw new InvalidOperationException($"Asset configuration not found for {symbol}");
}
// Get current market price
var latestPrice = GetLatestPrice(symbol);
if (latestPrice == null || latestPrice.Price <= 0)
{
throw new InvalidOperationException($"Cannot get current price for {symbol}");
}
// Execute sell
await ExecuteSellAsync(symbol, latestPrice.Price, config.CurrentHoldings, config);
}
}
+564
View File
@@ -0,0 +1,564 @@
using TradingBot.Models;
namespace TradingBot.Services;
/// <summary>
/// RSI-based trading strategy
/// Buy when RSI < oversold threshold, Sell when RSI > overbought threshold
/// </summary>
public class RSIStrategy : ITradingStrategy
{
public string Name => "RSI Strategy";
public string Description => "Strategia basata su Relative Strength Index. Compra in zona ipervenduto, vende in zona ipercomprato.";
private readonly decimal _oversoldThreshold;
private readonly decimal _overboughtThreshold;
private readonly int _period;
public RSIStrategy(decimal oversoldThreshold = 30, decimal overboughtThreshold = 70, int period = 14)
{
_oversoldThreshold = oversoldThreshold;
_overboughtThreshold = overboughtThreshold;
_period = period;
}
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
{
if (priceHistory == null || priceHistory.Count < _period + 1)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 0,
Reason = "Dati insufficienti per RSI"
});
}
var prices = priceHistory.Select(p => p.Price).ToList();
var rsi = TechnicalAnalysis.CalculateRSI(prices, _period);
if (rsi < _oversoldThreshold)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Buy,
Confidence = (decimal)(((_oversoldThreshold - rsi) / _oversoldThreshold) * 100),
Reason = $"RSI in zona ipervenduto: {rsi:F2}"
});
}
else if (rsi > _overboughtThreshold)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Sell,
Confidence = (decimal)(((rsi - _overboughtThreshold) / (100 - _overboughtThreshold)) * 100),
Reason = $"RSI in zona ipercomprato: {rsi:F2}"
});
}
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 50,
Reason = $"RSI neutro: {rsi:F2}"
});
}
}
/// <summary>
/// MACD-based trading strategy
/// Buy on bullish crossover, Sell on bearish crossover
/// </summary>
public class MACDStrategy : ITradingStrategy
{
public string Name => "MACD Strategy";
public string Description => "Strategia basata su MACD crossover. Compra su incrocio rialzista, vende su incrocio ribassista.";
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
{
if (priceHistory == null || priceHistory.Count < 26)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 0,
Reason = "Dati insufficienti per MACD"
});
}
var prices = priceHistory.Select(p => p.Price).ToList();
var (macd, signal, histogram) = TechnicalAnalysis.CalculateMACD(prices);
if (histogram > 0 && Math.Abs(histogram) > 0.1m)
{
var confidence = Math.Min((decimal)(Math.Abs((double)histogram) * 10), 100);
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Buy,
Confidence = confidence,
Reason = $"MACD crossover rialzista, histogram: {histogram:F2}"
});
}
else if (histogram < 0 && Math.Abs(histogram) > 0.1m)
{
var confidence = Math.Min((decimal)(Math.Abs((double)histogram) * 10), 100);
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Sell,
Confidence = confidence,
Reason = $"MACD crossover ribassista, histogram: {histogram:F2}"
});
}
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 30,
Reason = "MACD vicino a equilibrio"
});
}
}
/// <summary>
/// Bollinger Bands strategy
/// Buy when price touches lower band, Sell when price touches upper band
/// </summary>
public class BollingerBandsStrategy : ITradingStrategy
{
public string Name => "Bollinger Bands";
public string Description => "Compra quando il prezzo tocca la banda inferiore, vende alla banda superiore.";
private readonly int _period;
private readonly decimal _standardDeviations;
public BollingerBandsStrategy(int period = 20, decimal standardDeviations = 2)
{
_period = period;
_standardDeviations = standardDeviations;
}
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
{
if (priceHistory == null || priceHistory.Count < _period)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 0,
Reason = "Dati insufficienti per Bollinger Bands"
});
}
var prices = priceHistory.Select(p => p.Price).ToList();
var (upper, middle, lower) = TechnicalAnalysis.CalculateBollingerBands(prices, _period, _standardDeviations);
var currentPrice = prices.Last();
var distanceToLower = ((currentPrice - lower) / lower) * 100;
var distanceToUpper = ((upper - currentPrice) / upper) * 100;
if (distanceToLower < 2) // Within 2% of lower band
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Buy,
Confidence = 80,
Reason = $"Prezzo vicino banda inferiore: ${currentPrice:F2} vs ${lower:F2}"
});
}
else if (distanceToUpper < 2) // Within 2% of upper band
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Sell,
Confidence = 80,
Reason = $"Prezzo vicino banda superiore: ${currentPrice:F2} vs ${upper:F2}"
});
}
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 40,
Reason = "Prezzo tra le bande"
});
}
}
/// <summary>
/// Mean Reversion strategy
/// Assumes price will return to average
/// </summary>
public class MeanReversionStrategy : ITradingStrategy
{
public string Name => "Mean Reversion";
public string Description => "Sfrutta il ritorno del prezzo verso la media. Compra sotto media, vende sopra media.";
private readonly int _period;
private readonly decimal _deviationThreshold;
public MeanReversionStrategy(int period = 20, decimal deviationThreshold = 5)
{
_period = period;
_deviationThreshold = deviationThreshold;
}
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
{
if (priceHistory == null || priceHistory.Count < _period)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 0,
Reason = "Dati insufficienti"
});
}
var prices = priceHistory.Select(p => p.Price).TakeLast(_period).ToList();
var mean = prices.Average();
var currentPrice = prices.Last();
var deviation = ((currentPrice - mean) / mean) * 100;
if (deviation < -_deviationThreshold)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Buy,
Confidence = Math.Min((decimal)Math.Abs((double)deviation) * 10, 100),
Reason = $"Prezzo {deviation:F2}% sotto media, probabile rimbalzo"
});
}
else if (deviation > _deviationThreshold)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Sell,
Confidence = Math.Min((decimal)Math.Abs((double)deviation) * 10, 100),
Reason = $"Prezzo {deviation:F2}% sopra media, probabile correzione"
});
}
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 50,
Reason = "Prezzo vicino alla media"
});
}
}
/// <summary>
/// Momentum strategy
/// Follows strong trends
/// </summary>
public class MomentumStrategy : ITradingStrategy
{
public string Name => "Momentum";
public string Description => "Segue i trend forti. Compra su momentum positivo, vende su momentum negativo.";
private readonly int _period;
public MomentumStrategy(int period = 10)
{
_period = period;
}
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
{
if (priceHistory == null || priceHistory.Count < _period + 5)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 0,
Reason = "Dati insufficienti"
});
}
var prices = priceHistory.Select(p => p.Price).ToList();
var currentPrice = prices.Last();
var pastPrice = prices[^_period];
var momentum = ((currentPrice - pastPrice) / pastPrice) * 100;
// Calculate rate of change
var recentPrices = prices.TakeLast(5).ToList();
var priceChanges = new List<decimal>();
for (int i = 1; i < recentPrices.Count; i++)
{
priceChanges.Add(((recentPrices[i] - recentPrices[i - 1]) / recentPrices[i - 1]) * 100);
}
var avgChange = priceChanges.Average();
if (momentum > 3 && avgChange > 0)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Buy,
Confidence = Math.Min((decimal)Math.Abs((double)momentum) * 15, 100),
Reason = $"Forte momentum positivo: {momentum:F2}%"
});
}
else if (momentum < -3 && avgChange < 0)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Sell,
Confidence = Math.Min((decimal)Math.Abs((double)momentum) * 15, 100),
Reason = $"Forte momentum negativo: {momentum:F2}%"
});
}
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 30,
Reason = "Momentum debole o neutro"
});
}
}
/// <summary>
/// EMA Crossover strategy (Golden Cross / Death Cross)
/// Buy when fast EMA crosses above slow EMA, Sell on opposite
/// </summary>
public class EMACrossoverStrategy : ITradingStrategy
{
public string Name => "EMA Crossover";
public string Description => "Golden Cross/Death Cross. Compra quando EMA veloce supera EMA lenta.";
private readonly int _fastPeriod;
private readonly int _slowPeriod;
public EMACrossoverStrategy(int fastPeriod = 12, int slowPeriod = 26)
{
_fastPeriod = fastPeriod;
_slowPeriod = slowPeriod;
}
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
{
if (priceHistory == null || priceHistory.Count < _slowPeriod + 5)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 0,
Reason = "Dati insufficienti"
});
}
var prices = priceHistory.Select(p => p.Price).ToList();
var fastEMA = TechnicalAnalysis.CalculateEMA(prices, _fastPeriod);
var slowEMA = TechnicalAnalysis.CalculateEMA(prices, _slowPeriod);
// Calculate previous EMAs to detect crossover
var prevPrices = prices.Take(prices.Count - 1).ToList();
var prevFastEMA = TechnicalAnalysis.CalculateEMA(prevPrices, _fastPeriod);
var prevSlowEMA = TechnicalAnalysis.CalculateEMA(prevPrices, _slowPeriod);
var currentDiff = fastEMA - slowEMA;
var prevDiff = prevFastEMA - prevSlowEMA;
// Golden Cross (bullish)
if (currentDiff > 0 && prevDiff <= 0)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Buy,
Confidence = 85,
Reason = $"Golden Cross! EMA{_fastPeriod} crossed above EMA{_slowPeriod}"
});
}
// Death Cross (bearish)
else if (currentDiff < 0 && prevDiff >= 0)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Sell,
Confidence = 85,
Reason = $"Death Cross! EMA{_fastPeriod} crossed below EMA{_slowPeriod}"
});
}
// Trend continuation
else if (currentDiff > 0)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 60,
Reason = "EMA fast sopra slow - trend rialzista confermato"
});
}
else
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 40,
Reason = "EMA fast sotto slow - trend ribassista confermato"
});
}
}
}
/// <summary>
/// Scalping strategy for short-term gains
/// </summary>
public class ScalpingStrategy : ITradingStrategy
{
public string Name => "Scalping";
public string Description => "Strategia per guadagni rapidi a breve termine. Alta frequenza, piccoli profitti.";
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
{
if (priceHistory == null || priceHistory.Count < 10)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 0,
Reason = "Dati insufficienti"
});
}
var recentPrices = priceHistory.Select(p => p.Price).TakeLast(10).ToList();
var currentPrice = recentPrices.Last();
var shortMA = recentPrices.TakeLast(3).Average();
var mediumMA = recentPrices.TakeLast(7).Average();
// Calculate short-term volatility
var priceChanges = new List<decimal>();
for (int i = 1; i < recentPrices.Count; i++)
{
priceChanges.Add(Math.Abs(recentPrices[i] - recentPrices[i - 1]));
}
var avgVolatility = priceChanges.Average();
var recentChange = Math.Abs(currentPrice - recentPrices[^2]);
// Quick reversal detection
if (currentPrice < shortMA && shortMA < mediumMA && recentChange > avgVolatility * 1.5m)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Buy,
Confidence = 70,
Reason = "Possibile rimbalzo rapido"
});
}
else if (currentPrice > shortMA && shortMA > mediumMA && recentChange > avgVolatility * 1.5m)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Sell,
Confidence = 70,
Reason = "Possibile correzione rapida"
});
}
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 30,
Reason = "Attesa opportunità scalping"
});
}
}
/// <summary>
/// Breakout strategy
/// Trades on price breaking resistance/support levels
/// </summary>
public class BreakoutStrategy : ITradingStrategy
{
public string Name => "Breakout";
public string Description => "Compra su rottura resistenza, vende su rottura supporto. Cattura breakout significativi.";
private readonly int _lookbackPeriod;
public BreakoutStrategy(int lookbackPeriod = 20)
{
_lookbackPeriod = lookbackPeriod;
}
public Task<TradingSignal> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
{
if (priceHistory == null || priceHistory.Count < _lookbackPeriod)
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 0,
Reason = "Dati insufficienti"
});
}
var prices = priceHistory.Select(p => p.Price).ToList();
var recentPrices = prices.TakeLast(_lookbackPeriod).ToList();
var currentPrice = prices.Last();
var resistance = recentPrices.Max();
var support = recentPrices.Min();
var range = resistance - support;
// Breakout above resistance
if (currentPrice > resistance * 1.01m) // 1% above previous high
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Buy,
Confidence = 80,
Reason = $"Breakout sopra resistenza: ${resistance:F2}"
});
}
// Breakdown below support
else if (currentPrice < support * 0.99m) // 1% below previous low
{
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Sell,
Confidence = 80,
Reason = $"Breakdown sotto supporto: ${support:F2}"
});
}
return Task.FromResult(new TradingSignal
{
Symbol = symbol,
Type = SignalType.Hold,
Confidence = 40,
Reason = $"Prezzo in range ${support:F2} - ${resistance:F2}"
});
}
}
@@ -0,0 +1,486 @@
using TradingBot.Models;
using System.Text.Json;
namespace TradingBot.Services;
/// <summary>
/// Service for managing trading strategies and their assignments to assets
/// </summary>
public class TradingStrategiesService
{
private readonly Dictionary<string, StrategyInfo> _availableStrategies = new();
private readonly Dictionary<string, ITradingStrategy> _strategyInstances = new();
private readonly Dictionary<string, AssetStrategyMapping> _assetMappings = new();
private readonly Dictionary<string, TradingEngineStatus> _engineStatuses = new();
private readonly string _configPath;
public event Action? OnMappingsChanged;
public event Action<string, TradingDecision>? OnDecisionMade;
public TradingStrategiesService()
{
_configPath = Path.Combine(Directory.GetCurrentDirectory(), "data", "strategy-mappings.json");
InitializeStrategies();
LoadMappings();
}
private void InitializeStrategies()
{
// RSI Strategy
var rsiStrategy = new RSIStrategy();
_strategyInstances["rsi"] = rsiStrategy;
_availableStrategies["rsi"] = new StrategyInfo
{
Id = "rsi",
Name = "RSI Strategy",
Description = "Relative Strength Index - Compra in ipervenduto, vende in ipercomprato",
Category = "Oscillator",
RiskLevel = StrategyRisk.Medium,
RecommendedTimeFrame = TimeFrame.ShortTerm,
RequiredIndicators = new List<string> { "RSI" },
Parameters = new Dictionary<string, ParameterInfo>
{
["oversoldThreshold"] = new() { Name = "Oversold", Description = "Soglia ipervenduto", Type = ParameterType.Decimal, DefaultValue = 30m, MinValue = 10m, MaxValue = 40m },
["overboughtThreshold"] = new() { Name = "Overbought", Description = "Soglia ipercomprato", Type = ParameterType.Decimal, DefaultValue = 70m, MinValue = 60m, MaxValue = 90m },
["period"] = new() { Name = "Period", Description = "Periodo di calcolo", Type = ParameterType.Integer, DefaultValue = 14, MinValue = 5, MaxValue = 30 }
}
};
// MACD Strategy
var macdStrategy = new MACDStrategy();
_strategyInstances["macd"] = macdStrategy;
_availableStrategies["macd"] = new StrategyInfo
{
Id = "macd",
Name = "MACD Strategy",
Description = "Moving Average Convergence Divergence - Crossover rialzista/ribassista",
Category = "Momentum",
RiskLevel = StrategyRisk.Medium,
RecommendedTimeFrame = TimeFrame.MediumTerm,
RequiredIndicators = new List<string> { "MACD", "Signal", "Histogram" }
};
// Bollinger Bands Strategy
var bollingerStrategy = new BollingerBandsStrategy();
_strategyInstances["bollinger"] = bollingerStrategy;
_availableStrategies["bollinger"] = new StrategyInfo
{
Id = "bollinger",
Name = "Bollinger Bands",
Description = "Compra vicino banda inferiore, vende vicino banda superiore",
Category = "Volatility",
RiskLevel = StrategyRisk.Low,
RecommendedTimeFrame = TimeFrame.MediumTerm,
RequiredIndicators = new List<string> { "Bollinger Bands" },
Parameters = new Dictionary<string, ParameterInfo>
{
["period"] = new() { Name = "Period", Description = "Periodo SMA", Type = ParameterType.Integer, DefaultValue = 20, MinValue = 10, MaxValue = 50 },
["standardDeviations"] = new() { Name = "Std Dev", Description = "Deviazioni standard", Type = ParameterType.Decimal, DefaultValue = 2m, MinValue = 1m, MaxValue = 3m }
}
};
// Mean Reversion Strategy
var meanReversionStrategy = new MeanReversionStrategy();
_strategyInstances["mean_reversion"] = meanReversionStrategy;
_availableStrategies["mean_reversion"] = new StrategyInfo
{
Id = "mean_reversion",
Name = "Mean Reversion",
Description = "Sfrutta il ritorno del prezzo verso la media",
Category = "Contrarian",
RiskLevel = StrategyRisk.High,
RecommendedTimeFrame = TimeFrame.ShortTerm,
RequiredIndicators = new List<string> { "SMA" },
Parameters = new Dictionary<string, ParameterInfo>
{
["period"] = new() { Name = "Period", Description = "Periodo media", Type = ParameterType.Integer, DefaultValue = 20, MinValue = 10, MaxValue = 50 },
["deviationThreshold"] = new() { Name = "Deviation %", Description = "Soglia deviazione", Type = ParameterType.Decimal, DefaultValue = 5m, MinValue = 2m, MaxValue = 10m }
}
};
// Momentum Strategy
var momentumStrategy = new MomentumStrategy();
_strategyInstances["momentum"] = momentumStrategy;
_availableStrategies["momentum"] = new StrategyInfo
{
Id = "momentum",
Name = "Momentum",
Description = "Segue i trend forti basati su momentum",
Category = "Trend",
RiskLevel = StrategyRisk.Medium,
RecommendedTimeFrame = TimeFrame.MediumTerm,
RequiredIndicators = new List<string> { "Price Change" },
Parameters = new Dictionary<string, ParameterInfo>
{
["period"] = new() { Name = "Period", Description = "Periodo momentum", Type = ParameterType.Integer, DefaultValue = 10, MinValue = 5, MaxValue = 20 }
}
};
// EMA Crossover Strategy
var emaCrossoverStrategy = new EMACrossoverStrategy();
_strategyInstances["ema_crossover"] = emaCrossoverStrategy;
_availableStrategies["ema_crossover"] = new StrategyInfo
{
Id = "ema_crossover",
Name = "EMA Crossover",
Description = "Golden Cross/Death Cross con EMA",
Category = "Trend",
RiskLevel = StrategyRisk.Low,
RecommendedTimeFrame = TimeFrame.LongTerm,
RequiredIndicators = new List<string> { "EMA12", "EMA26" },
Parameters = new Dictionary<string, ParameterInfo>
{
["fastPeriod"] = new() { Name = "Fast EMA", Description = "Periodo EMA veloce", Type = ParameterType.Integer, DefaultValue = 12, MinValue = 8, MaxValue = 20 },
["slowPeriod"] = new() { Name = "Slow EMA", Description = "Periodo EMA lenta", Type = ParameterType.Integer, DefaultValue = 26, MinValue = 20, MaxValue = 50 }
}
};
// Scalping Strategy
var scalpingStrategy = new ScalpingStrategy();
_strategyInstances["scalping"] = scalpingStrategy;
_availableStrategies["scalping"] = new StrategyInfo
{
Id = "scalping",
Name = "Scalping",
Description = "Guadagni rapidi a breve termine",
Category = "Short-term",
RiskLevel = StrategyRisk.VeryHigh,
RecommendedTimeFrame = TimeFrame.ShortTerm,
RequiredIndicators = new List<string> { "Short MA", "Volatility" }
};
// Breakout Strategy
var breakoutStrategy = new BreakoutStrategy();
_strategyInstances["breakout"] = breakoutStrategy;
_availableStrategies["breakout"] = new StrategyInfo
{
Id = "breakout",
Name = "Breakout",
Description = "Cattura rotture di resistenza/supporto",
Category = "Volatility",
RiskLevel = StrategyRisk.High,
RecommendedTimeFrame = TimeFrame.MediumTerm,
RequiredIndicators = new List<string> { "Resistance", "Support" },
Parameters = new Dictionary<string, ParameterInfo>
{
["lookbackPeriod"] = new() { Name = "Lookback", Description = "Periodo lookback", Type = ParameterType.Integer, DefaultValue = 20, MinValue = 10, MaxValue = 50 }
}
};
}
/// <summary>
/// Get all available strategies
/// </summary>
public IReadOnlyDictionary<string, StrategyInfo> GetAvailableStrategies()
{
return _availableStrategies;
}
/// <summary>
/// Get strategies by category
/// </summary>
public IEnumerable<StrategyInfo> GetStrategiesByCategory(string category)
{
return _availableStrategies.Values.Where(s => s.Category == category);
}
/// <summary>
/// Get asset mapping
/// </summary>
public AssetStrategyMapping? GetAssetMapping(string symbol)
{
_assetMappings.TryGetValue(symbol, out var mapping);
return mapping;
}
/// <summary>
/// Get all asset mappings
/// </summary>
public IReadOnlyDictionary<string, AssetStrategyMapping> GetAllMappings()
{
return _assetMappings;
}
/// <summary>
/// Assign strategies to an asset
/// </summary>
public void AssignStrategiesToAsset(string symbol, string assetName, List<string> strategyIds)
{
var mapping = new AssetStrategyMapping
{
Symbol = symbol,
AssetName = assetName,
StrategyIds = strategyIds,
IsActive = false,
ActivatedAt = DateTime.UtcNow
};
_assetMappings[symbol] = mapping;
// Initialize engine status
if (!_engineStatuses.ContainsKey(symbol))
{
_engineStatuses[symbol] = new TradingEngineStatus
{
Symbol = symbol,
IsRunning = false,
ActiveStrategies = 0
};
}
SaveMappings();
OnMappingsChanged?.Invoke();
}
/// <summary>
/// Remove strategy from asset
/// </summary>
public void RemoveStrategyFromAsset(string symbol, string strategyId)
{
if (_assetMappings.TryGetValue(symbol, out var mapping))
{
mapping.StrategyIds.Remove(strategyId);
if (mapping.StrategyIds.Count == 0)
{
mapping.IsActive = false;
}
SaveMappings();
OnMappingsChanged?.Invoke();
}
}
/// <summary>
/// Activate trading for an asset
/// </summary>
public void ActivateAsset(string symbol)
{
if (_assetMappings.TryGetValue(symbol, out var mapping) && mapping.StrategyIds.Count > 0)
{
mapping.IsActive = true;
mapping.ActivatedAt = DateTime.UtcNow;
mapping.DeactivatedAt = null;
if (_engineStatuses.TryGetValue(symbol, out var status))
{
status.IsRunning = true;
status.ActiveStrategies = mapping.StrategyIds.Count;
}
SaveMappings();
OnMappingsChanged?.Invoke();
}
}
/// <summary>
/// Deactivate trading for an asset
/// </summary>
public void DeactivateAsset(string symbol)
{
if (_assetMappings.TryGetValue(symbol, out var mapping))
{
mapping.IsActive = false;
mapping.DeactivatedAt = DateTime.UtcNow;
if (_engineStatuses.TryGetValue(symbol, out var status))
{
status.IsRunning = false;
}
SaveMappings();
OnMappingsChanged?.Invoke();
}
}
/// <summary>
/// Analyze market with assigned strategies
/// </summary>
public async Task<TradingDecision> AnalyzeAsync(string symbol, List<MarketPrice> priceHistory)
{
if (!_assetMappings.TryGetValue(symbol, out var mapping) || !mapping.IsActive)
{
return new TradingDecision
{
Symbol = symbol,
Decision = SignalType.Hold,
Confidence = 0,
Reason = "Trading non attivo per questo asset"
};
}
var signals = new List<StrategySignal>();
int buyVotes = 0, sellVotes = 0, holdVotes = 0;
decimal totalConfidence = 0;
// Execute all assigned strategies
foreach (var strategyId in mapping.StrategyIds)
{
if (_strategyInstances.TryGetValue(strategyId, out var strategy))
{
var signal = await strategy.AnalyzeAsync(symbol, priceHistory);
var strategySignal = new StrategySignal
{
StrategyId = strategyId,
StrategyName = _availableStrategies[strategyId].Name,
Signal = signal,
GeneratedAt = DateTime.UtcNow
};
signals.Add(strategySignal);
switch (signal.Type)
{
case SignalType.Buy:
buyVotes++;
break;
case SignalType.Sell:
sellVotes++;
break;
case SignalType.Hold:
holdVotes++;
break;
}
totalConfidence += signal.Confidence;
}
}
// Update engine status
if (_engineStatuses.TryGetValue(symbol, out var status))
{
status.RecentSignals = signals;
status.LastSignalTime = DateTime.UtcNow;
}
// Aggregate decision
var decision = MakeDecision(symbol, signals, buyVotes, sellVotes, holdVotes, totalConfidence);
if (status != null)
{
status.LastDecision = decision;
}
OnDecisionMade?.Invoke(symbol, decision);
return decision;
}
private TradingDecision MakeDecision(string symbol, List<StrategySignal> signals, int buyVotes, int sellVotes, int holdVotes, decimal totalConfidence)
{
var totalVotes = buyVotes + sellVotes + holdVotes;
if (totalVotes == 0)
{
return new TradingDecision
{
Symbol = symbol,
Decision = SignalType.Hold,
Confidence = 0,
Reason = "Nessuna strategia attiva"
};
}
var avgConfidence = totalConfidence / totalVotes;
SignalType finalDecision;
string reason;
List<string> supporting = new();
List<string> opposing = new();
// Decision logic: majority voting with confidence threshold
if (buyVotes > sellVotes && buyVotes >= totalVotes * 0.6m)
{
finalDecision = SignalType.Buy;
reason = $"{buyVotes}/{totalVotes} strategie suggeriscono acquisto";
supporting = signals.Where(s => s.Signal.Type == SignalType.Buy).Select(s => s.StrategyName).ToList();
opposing = signals.Where(s => s.Signal.Type != SignalType.Buy).Select(s => s.StrategyName).ToList();
}
else if (sellVotes > buyVotes && sellVotes >= totalVotes * 0.6m)
{
finalDecision = SignalType.Sell;
reason = $"{sellVotes}/{totalVotes} strategie suggeriscono vendita";
supporting = signals.Where(s => s.Signal.Type == SignalType.Sell).Select(s => s.StrategyName).ToList();
opposing = signals.Where(s => s.Signal.Type != SignalType.Sell).Select(s => s.StrategyName).ToList();
}
else
{
finalDecision = SignalType.Hold;
reason = "Segnali contrastanti - attendi conferma";
supporting = signals.Where(s => s.Signal.Type == SignalType.Hold).Select(s => s.StrategyName).ToList();
}
return new TradingDecision
{
Symbol = symbol,
Decision = finalDecision,
Confidence = avgConfidence,
Reason = reason,
BuyVotes = buyVotes,
SellVotes = sellVotes,
HoldVotes = holdVotes,
SupportingStrategies = supporting,
OpposingStrategies = opposing
};
}
/// <summary>
/// Get trading engine status for asset
/// </summary>
public TradingEngineStatus? GetEngineStatus(string symbol)
{
_engineStatuses.TryGetValue(symbol, out var status);
return status;
}
private void SaveMappings()
{
try
{
var directory = Path.GetDirectoryName(_configPath);
if (directory != null && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var json = JsonSerializer.Serialize(_assetMappings, new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(_configPath, json);
}
catch (Exception ex)
{
Console.WriteLine($"Error saving strategy mappings: {ex.Message}");
}
}
private void LoadMappings()
{
try
{
if (File.Exists(_configPath))
{
var json = File.ReadAllText(_configPath);
var loaded = JsonSerializer.Deserialize<Dictionary<string, AssetStrategyMapping>>(json);
if (loaded != null)
{
foreach (var kvp in loaded)
{
_assetMappings[kvp.Key] = kvp.Value;
// Initialize engine status
_engineStatuses[kvp.Key] = new TradingEngineStatus
{
Symbol = kvp.Key,
IsRunning = kvp.Value.IsActive,
ActiveStrategies = kvp.Value.StrategyIds.Count
};
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error loading strategy mappings: {ex.Message}");
}
}
}
+125 -35
View File
@@ -1,48 +1,138 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
<!-- Docker Publishing -->
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>.</DockerfileContext>
<!-- Auto-versioning -->
<Version>1.0.0</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<!--
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>
<!-- Post-Publish: Push to Gitea Registry -->
<Target Name="PushToGitea" AfterTargets="Publish" Condition="'$(Configuration)' == 'Release'">
<Message Importance="high" Text="?? Pushing to Gitea Container Registry..." />
<!-- Folders for organization -->
<ItemGroup>
<Folder Include="Properties\PublishProfiles\" />
<Folder Include="docs\deployment\" />
</ItemGroup>
<!--
Post-Publish Target: Push to Gitea Container Registry
<!-- Tag with 'latest' -->
<Exec Command="docker tag tradingbot:latest gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest"
ContinueOnError="true"
IgnoreExitCode="false" />
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)
<!-- Tag with version -->
<Exec Command="docker tag tradingbot:latest gitea.encke-hake.ts.net/alby96/encelado/tradingbot:$(Version)"
ContinueOnError="true"
IgnoreExitCode="false" />
<!-- Push 'latest' -->
<Exec Command="docker push gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest"
ContinueOnError="true"
IgnoreExitCode="false" />
<!-- Push versioned -->
<Exec Command="docker push gitea.encke-hake.ts.net/alby96/encelado/tradingbot:$(Version)"
ContinueOnError="true"
IgnoreExitCode="false" />
<Message Importance="high" Text="? Image pushed to Gitea Registry!" />
<Message Importance="high" Text=" - gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest" />
<Message Importance="high" Text=" - gitea.encke-hake.ts.net/alby96/encelado/tradingbot:$(Version)" />
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>
</Project>
<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>
+149
View File
@@ -0,0 +1,149 @@
# Version Bump Script
# Aggiorna automaticamente la versione nel .csproj e crea tag Git
param(
[Parameter(Mandatory=$true)]
[ValidateSet('major', 'minor', 'patch')]
[string]$BumpType,
[Parameter(Mandatory=$false)]
[string]$Message = ""
)
$ErrorActionPreference = "Stop"
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "?? TradingBot Version Bump" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# Path al .csproj
$csprojPath = Join-Path $PSScriptRoot "TradingBot.csproj"
if (-not (Test-Path $csprojPath)) {
Write-Host "? File TradingBot.csproj non trovato!" -ForegroundColor Red
exit 1
}
# Leggi versione corrente
Write-Host "?? Reading current version..." -ForegroundColor Yellow
[xml]$csproj = Get-Content $csprojPath
$currentVersion = $csproj.Project.PropertyGroup.Version
if (-not $currentVersion) {
Write-Host "? Version not found in .csproj!" -ForegroundColor Red
exit 1
}
Write-Host " Current version: $currentVersion" -ForegroundColor Gray
# Parse versione
$versionParts = $currentVersion.Split('.')
$major = [int]$versionParts[0]
$minor = [int]$versionParts[1]
$patch = [int]$versionParts[2]
# Bump versione
switch ($BumpType) {
'major' {
$major++
$minor = 0
$patch = 0
Write-Host " Bumping: MAJOR version (breaking changes)" -ForegroundColor Magenta
}
'minor' {
$minor++
$patch = 0
Write-Host " Bumping: MINOR version (new features)" -ForegroundColor Blue
}
'patch' {
$patch++
Write-Host " Bumping: PATCH version (bug fixes)" -ForegroundColor Green
}
}
$newVersion = "$major.$minor.$patch"
$newAssemblyVersion = "$major.$minor.$patch.0"
Write-Host " New version: $newVersion" -ForegroundColor Green
Write-Host ""
# Aggiorna .csproj
Write-Host "?? Updating TradingBot.csproj..." -ForegroundColor Yellow
# Update Version
$csproj.Project.PropertyGroup.Version = $newVersion
$csproj.Project.PropertyGroup.AssemblyVersion = $newAssemblyVersion
$csproj.Project.PropertyGroup.FileVersion = $newAssemblyVersion
# Save
$csproj.Save($csprojPath)
Write-Host " ? Updated Version: $newVersion" -ForegroundColor Green
Write-Host " ? Updated AssemblyVersion: $newAssemblyVersion" -ForegroundColor Green
Write-Host ""
# Git commit
Write-Host "?? Creating Git commit..." -ForegroundColor Yellow
$commitMessage = if ($Message) {
"chore: Bump version to $newVersion - $Message"
} else {
"chore: Bump version to $newVersion"
}
git add $csprojPath
if ($LASTEXITCODE -ne 0) {
Write-Host "? Git add failed!" -ForegroundColor Red
exit 1
}
git commit -m $commitMessage
if ($LASTEXITCODE -ne 0) {
Write-Host "? Git commit failed!" -ForegroundColor Red
exit 1
}
Write-Host " ? Committed: $commitMessage" -ForegroundColor Green
Write-Host ""
# Git tag
Write-Host "??? Creating Git tag..." -ForegroundColor Yellow
$tagName = "v$newVersion"
$tagMessage = if ($Message) {
"Release $newVersion - $Message"
} else {
"Release $newVersion"
}
git tag -a $tagName -m $tagMessage
if ($LASTEXITCODE -ne 0) {
Write-Host "? Git tag failed!" -ForegroundColor Red
exit 1
}
Write-Host " ? Created tag: $tagName" -ForegroundColor Green
Write-Host ""
# Summary
Write-Host "========================================" -ForegroundColor Green
Write-Host "? Version bumped successfully!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green
Write-Host ""
Write-Host "?? Version: $currentVersion ? $newVersion" -ForegroundColor White
Write-Host "??? Git tag: $tagName" -ForegroundColor White
Write-Host ""
Write-Host "?? Next steps:" -ForegroundColor Cyan
Write-Host " 1. Push commit: git push origin main" -ForegroundColor White
Write-Host " 2. Push tag: git push origin $tagName" -ForegroundColor White
Write-Host " 3. Build & Publish: Visual Studio ? Publish (Docker profile)" -ForegroundColor White
Write-Host " 4. Deploy on Unraid" -ForegroundColor White
Write-Host ""
Write-Host "?? Or push both at once:" -ForegroundColor Cyan
Write-Host " git push origin main --tags" -ForegroundColor White
Write-Host ""
@@ -0,0 +1,288 @@
# ?? TradingBot - Deployment Checklist
Checklist completa per deployment sicuro e corretto su Unraid.
---
## ? Pre-Deployment
### Environment
- [ ] Unraid 6.10+ installato e aggiornato
- [ ] Docker service attivo e funzionante
- [ ] Internet connesso e stabile
- [ ] SSH access configurato
- [ ] Backup Unraid recente disponibile
### Network
- [ ] Porta 8888 disponibile (o alternativa scelta)
- [ ] Test porta: `netstat -tulpn | grep :8888`
- [ ] Firewall configurato correttamente
- [ ] IP Unraid noto: `192.168.30.23`
### Gitea Registry
- [ ] Account Gitea attivo
- [ ] Personal Access Token generato
- [ ] Login test: `docker login gitea.encke-hake.ts.net`
- [ ] Immagine disponibile in Packages
---
## ?? Installation
### Template Setup
- [ ] Template XML scaricato
```bash
wget -O /boot/config/plugins/dockerMan/templates-user/TradingBot.xml \
https://gitea.encke-hake.ts.net/Alby96/Encelado/raw/branch/main/TradingBot/deployment/unraid-template.xml
```
- [ ] Template visibile in Unraid UI
- [ ] Dropdown "TradingBot" disponibile
### Container Configuration
- [ ] **Name**: `TradingBot`
- [ ] **Repository**: `gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest`
- [ ] **Network**: Bridge
- [ ] **Port Mapping**: `8888:8080` (o custom)
- Host Port: `8888` (modificabile)
- Container Port: `8080` (FIXED)
- [ ] **Volume**: `/mnt/user/appdata/tradingbot:/app/data`
- Access: Read/Write
- [ ] **Environment Variables**:
- `ASPNETCORE_ENVIRONMENT=Production`
- `ASPNETCORE_URLS=http://+:8080`
- `TZ=Europe/Rome`
### First Start
- [ ] Click **Apply**
- [ ] Container pulls image successfully
- [ ] Container status: **running**
- [ ] No errors in logs: `docker logs TradingBot`
---
## ? Post-Installation Verification
### Container Health
- [ ] Container running: `docker ps | grep TradingBot`
- [ ] Port mapping correct: `docker port TradingBot`
- Expected: `8080/tcp -> 0.0.0.0:8888`
- [ ] Logs healthy: `docker logs TradingBot --tail 50`
- No errors or exceptions
- "Now listening on: http://[::]:8080"
### WebUI Access
- [ ] WebUI icon visible in Unraid Docker tab
- [ ] Click WebUI icon opens browser
- [ ] Manual access works: `http://192.168.30.23:8888`
- [ ] Dashboard loads completely
- [ ] No JavaScript errors in browser console
### Functionality Test
- [ ] Bot can be started from UI
- [ ] Market data updates (check Dashboard)
- [ ] Settings can be modified and saved
- [ ] Assets can be enabled/disabled
- [ ] Trade history visible (if any previous data)
---
## ?? Persistence Verification
### Data Directory
- [ ] Volume created: `ls -la /mnt/user/appdata/tradingbot/`
- [ ] Directory writable: `touch /mnt/user/appdata/tradingbot/test && rm /mnt/user/appdata/tradingbot/test`
### Persistence Test
1. [ ] Start bot and execute some trades
2. [ ] Stop bot
3. [ ] Verify files exist:
```bash
ls -lh /mnt/user/appdata/tradingbot/
# Should show:
# - trade-history.json
# - active-positions.json
# - settings.json
```
4. [ ] Stop container: `docker stop TradingBot`
5. [ ] Start container: `docker start TradingBot`
6. [ ] Verify data restored:
- Trade count same in History page
- Settings preserved
- Active positions restored
### Backup Test
- [ ] Create backup:
```bash
tar -czf tradingbot-backup-$(date +%Y%m%d).tar.gz \
/mnt/user/appdata/tradingbot/
```
- [ ] Backup file created successfully
- [ ] Test restore (optional):
```bash
tar -xzf tradingbot-backup-YYYYMMDD.tar.gz -C /tmp/
# Verify files intact
```
---
## ?? Update Test
### Update Procedure
- [ ] Stop container
- [ ] Force Update in Unraid UI
- [ ] Wait for pull completion
- [ ] Start container
- [ ] Verify data persisted:
- [ ] Trade history intact
- [ ] Settings intact
- [ ] Active positions intact
### Rollback Test (Optional)
- [ ] Tag current image before update
- [ ] Test update to new version
- [ ] If issues, rollback to previous tag
- [ ] Verify data still intact
---
## ?? Security Check
### Access Control
- [ ] Port 8888 not exposed to internet
- [ ] Only LAN/VPN access configured
- [ ] No default passwords used
### Data Protection
- [ ] AppData directory permissions correct
```bash
ls -la /mnt/user/appdata/ | grep tradingbot
# Should be owned by appropriate user
```
- [ ] Backup schedule configured (CA Backup plugin)
- [ ] Backup retention policy set
### Registry Security
- [ ] Gitea login required for pulls
- [ ] Personal Access Token secure
- [ ] No credentials in logs
---
## ?? Monitoring Setup
### Unraid Dashboard
- [ ] Container appears in Docker tab
- [ ] Auto-start enabled (optional)
- [ ] Resource limits configured (optional):
```
--cpus="2.0" --memory="1g"
```
### Logs
- [ ] Know how to access logs:
- Unraid UI: Docker tab ? TradingBot ? Logs icon
- CLI: `docker logs TradingBot -f`
- [ ] No error messages in logs
### Notifications
- [ ] Unraid notifications enabled
- [ ] Email/Telegram configured (optional)
---
## ?? Troubleshooting Checklist
### If WebUI Not Accessible
- [ ] Check container running: `docker ps | grep TradingBot`
- [ ] Check port mapping: `docker port TradingBot`
- [ ] Test localhost: `curl http://localhost:8888/`
- [ ] Check firewall: `iptables -L | grep 8888`
- [ ] Check logs for errors: `docker logs TradingBot`
- [ ] Try different port if 8888 occupied
### If Data Not Persisting
- [ ] Volume mapping correct: `docker inspect TradingBot | grep -A5 Mounts`
- [ ] Directory exists: `ls -la /mnt/user/appdata/tradingbot/`
- [ ] Files being created: Monitor during bot run
- [ ] Permissions correct: `ls -la /mnt/user/appdata/tradingbot/`
### If Container Won't Start
- [ ] Check image pulled: `docker images | grep tradingbot`
- [ ] Check port not in use: `netstat -tulpn | grep :8888`
- [ ] Check disk space: `df -h`
- [ ] Review logs: `docker logs TradingBot`
- [ ] Try manual start: `docker start TradingBot`
---
## ?? Post-Deployment Tasks
### Documentation
- [ ] Note custom port if not 8888
- [ ] Document backup location
- [ ] Save deployment date
- [ ] Note Gitea image tag deployed
### Monitoring
- [ ] Add to monitoring dashboard (if any)
- [ ] Set up health check alerts (optional)
- [ ] Configure update notifications
### User Training
- [ ] Show how to access WebUI
- [ ] Explain Settings page
- [ ] Demonstrate how to view trades
- [ ] Explain data management (clear data)
---
## ?? Success Criteria
All of the following must be true:
? Container running and healthy
? WebUI accessible and functional
? Bot can start/stop from UI
? Market data updates in real-time
? Trades can be executed
? Data persists across restarts
? Backup can be created
? No errors in logs
? Resource usage acceptable
? Update procedure tested
---
## ?? Support
If issues persist after completing this checklist:
1. **Check Documentation**:
- [UNRAID_INSTALL.md](UNRAID_INSTALL.md)
- [CHANGELOG.md](../CHANGELOG.md)
2. **Collect Diagnostic Info**:
```bash
# Container info
docker ps -a | grep TradingBot
docker logs TradingBot --tail 100 > /tmp/tradingbot-logs.txt
docker inspect TradingBot > /tmp/tradingbot-inspect.json
# System info
df -h
free -h
netstat -tulpn | grep 8888
```
3. **Open Issue**:
- Repository: https://gitea.encke-hake.ts.net/Alby96/Encelado/issues
- Include: Docker version, Unraid version, logs
---
**Last Updated**: 2024-12-21
**Version**: 1.2.0
**Status**: ? Production Ready
+566
View File
@@ -0,0 +1,566 @@
# ?? TradingBot - Installazione su Unraid (Senza Portainer)
## ? Installazione Diretta da Gitea Registry
Puoi installare TradingBot direttamente dall'Unraid Docker Manager usando il tuo Gitea Registry!
---
## ?? PREREQUISITI
### 1. Login Gitea Registry su Unraid
SSH su Unraid:
```bash
ssh root@192.168.30.23 # O IP Tailscale
# Login al Gitea Registry
docker login gitea.encke-hake.ts.net
# Username: Alby96
# Password: [Personal Access Token Gitea]
```
**Output atteso**:
```
Login Succeeded ?
```
---
## ?? METODO 1: Template XML (Consigliato)
### Step 1: Copia Template su Unraid
```bash
# Su Unraid
mkdir -p /boot/config/plugins/dockerMan/templates-user
# Scarica template
wget -O /boot/config/plugins/dockerMan/templates-user/TradingBot.xml \
https://gitea.encke-hake.ts.net/Alby96/Encelado/raw/branch/main/TradingBot/deployment/unraid-template.xml
```
### Step 2: Installa Container
1. Unraid WebUI ? **Docker** tab
2. Click **Add Container** (in fondo)
3. **Template**: Dropdown ? Seleziona **TradingBot**
4. Configura parametri:
**Parametri Base**:
- **Name**: `TradingBot` (già impostato)
- **Repository**: `gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest` (già impostato)
**Porta WebUI** (Visibile e Configurabile!):
- **WebUI HTTP Port**: `8888` (porta default - cambia se occupata)
- Questa è la porta HOST per accedere all'interfaccia web
- La porta CONTAINER rimane sempre 8080 (non modificare)
- Alternative comuni se 8888 occupata: `8881`, `9999`, `7777`
**Volume Dati** (?? IMPORTANTE per persistenza!):
- **AppData**: `/mnt/user/appdata/tradingbot` (già impostato)
- Questo volume salva:
- Trade history (`trade-history.json`)
- Posizioni attive (`active-positions.json`)
- Settings applicazione (`settings.json`)
- ? I dati sopravvivono a restart/update del container
**Variabili Ambiente** (Avanzate - espandi se necessario):
- **ASPNETCORE_ENVIRONMENT**: `Production` (non modificare)
- **ASPNETCORE_URLS**: `http://+:8080` (non modificare - porta interna container)
- **TZ**: `Europe/Rome` (cambia per altro timezone)
5. Click **Apply**
Unraid farà:
- ? Pull immagine da Gitea Registry
- ? Crea container con nome "TradingBot"
- ? Configura porta WebUI (default 8888 ? host, 8080 ? container)
- ? **Crea volume persistente per dati**
- ? Start automatico
### Step 3: Accedi WebUI
**Metodo A: Click su WebUI Icon** ??
1. **Docker tab** ? Trova container **TradingBot**
2. Nella riga del container, a destra, vedrai l'**icona globe** ??
3. Click sull'icona ? Si apre automaticamente `http://192.168.30.23:8888`
**Metodo B: URL Manuale**
```
http://192.168.30.23:8888
```
(Sostituisci `8888` con la porta HOST che hai configurato)
?? **IMPORTANTE**: La porta nel browser deve essere quella HOST (8888 default), NON la porta container (8080)
Dovresti vedere la **Dashboard TradingBot**! ??
---
## ?? PERSISTENZA DATI
### Come Funziona
TradingBot salva automaticamente tutti i dati in `/app/data` dentro il container, che viene mappato sul volume host `/mnt/user/appdata/tradingbot`.
**File salvati automaticamente**:
```
/mnt/user/appdata/tradingbot/
??? trade-history.json # Storia completa trade
??? active-positions.json # Posizioni attualmente aperte
??? settings.json # Impostazioni applicazione
```
**Salvataggio automatico**:
- ? Ogni 30 secondi (mentre bot running)
- ?? Immediato dopo ogni trade eseguito
- ?? On-stop quando fermi il bot
- ?? Graceful shutdown su Docker stop/restart
### Benefici
? **Zero perdita dati** - Anche in caso di crash
? **Restore automatico** - Stato ripristinato al riavvio
? **Update sicuri** - Dati preservati durante aggiornamenti
? **Backup facile** - Basta copiare la cartella appdata
### Backup Dati
```bash
# Backup manuale
tar -czf tradingbot-backup-$(date +%Y%m%d).tar.gz \
/mnt/user/appdata/tradingbot
# Restore
tar -xzf tradingbot-backup-20241221.tar.gz \
-C /mnt/user/appdata/
```
### Gestione Dati (via WebUI)
Vai su **Settings** ? **Dati Persistenti**:
- Visualizza numero trade salvati
- Visualizza dimensione dati
- Visualizza posizioni attive
- **Cancella tutti i dati** (con conferma)
?? **Nota**: Puoi cancellare i dati solo se il bot è fermo.
---
## ?? METODO 2: Installazione Manuale
Se preferisci non usare template:
### Step 1: Unraid Docker Tab
1. **Docker** ? **Add Container**
2. **Name**: `TradingBot`
### Step 2: Configurazione Base
**Repository**:
```
gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest
```
**Network Type**: `Bridge`
**Console shell command**: `Shell`
### Step 3: Port Mapping (?? CRITICO!)
Click **Add another Path, Port, Variable, Label or Device**
**Config Type**: `Port`
- **Name**: `WebUI`
- **Container Port**: `8080`
- **Host Port**: `8888` ? **Cambia questa se occupata!**
- **Connection Type**: `TCP`
?? **Se questo mapping non viene configurato, la WebUI non sarà accessibile!**
### Step 4: Volume Mapping (?? IMPORTANTE per persistenza!)
Click **Add another Path, Port, Variable, Label o Device**
**Config Type**: `Path`
- **Name**: `AppData`
- **Container Path**: `/app/data`
- **Host Path**: `/mnt/user/appdata/tradingbot`
- **Access Mode**: `Read/Write`
### Step 5: Environment Variables
**ASPNETCORE_ENVIRONMENT**:
- **Name**: `ASPNETCORE_ENVIRONMENT`
- **Value**: `Production`
**ASPNETCORE_URLS**:
- **Name**: `ASPNETCORE_URLS`
- **Value**: `http://+:8080`
**TZ** (Opzionale):
- **Name**: `TZ`
- **Value**: `Europe/Rome` (o tuo timezone)
### Step 6: Apply
Click **Apply** in fondo alla pagina.
---
## ?? AGGIORNAMENTO CONTAINER
### Via Unraid Docker Tab
1. **Docker** ? Trova **TradingBot**
2. Click **icona Stop** (ferma container)
3. Click **Force Update** (icona update con freccia circolare)
4. Attendi pull dell'immagine aggiornata
5. Click **icona Start** (avvia container)
Unraid farà:
- ? Pull ultima immagine da Gitea
- ? Ricrea container con nuova immagine
- ? **Mantiene dati persistenti** (volume non viene toccato)
- ? Mantiene configurazione (porta, variabili, etc.)
?? **I tuoi trade e impostazioni sono al sicuro durante gli update!**
### Automatico con User Scripts Plugin
Installa **User Scripts** plugin:
1. **Apps** ? Cerca "User Scripts"
2. Installa
Crea script update:
```bash
#!/bin/bash
# Nome: Update TradingBot
# Schedula: Weekly (ogni domenica alle 3:00 AM)
# Stop container
docker stop TradingBot
# Pull latest image
docker pull gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest
# Start container (Unraid ricrea automaticamente)
docker start TradingBot
# Notifica
/usr/local/emhttp/webGui/scripts/notify -s "TradingBot Update" -d "Container aggiornato con successo!" -i "normal"
echo "Update completato alle $(date)"
```
Schedula: Settimanale o manualmente quando serve.
---
## ??? CONFIGURAZIONE PORTA
### Cambiare Porta WebUI
La porta **default è 8888** (host) ? **8080** (container).
Se la porta 8888 è occupata o vuoi usarne un'altra:
#### **Via Template (Prima Installazione)**
Durante Step 2 dell'installazione:
- **WebUI HTTP Port**: Cambia da `8888` a porta desiderata (es. `8881`, `9999`, `7777`)
- ?? Modifica SOLO la porta HOST (a sinistra)
- NON modificare la porta Container (deve restare 8080)
#### **Via Edit (Container Esistente)**
1. **Docker tab** ? Container **TradingBot**
2. Click **Edit** (icona matita/wrench)
3. Trova sezione **Port Mappings**
4. Vedrai: **Host Port** `8888` ? **Container Port** `8080`
5. Modifica **Host Port** (es. da `8888` a `8881`)
6. **IMPORTANTE**: NON modificare **Container Port** (deve restare `8080`)
7. Click **Apply** in fondo
8. Container si riavvierà automaticamente
#### **Accesso con Nuova Porta**
```
http://192.168.30.23:NUOVA_PORTA_HOST
```
Esempio con porta `8881`:
```
http://192.168.30.23:8881
```
### Porte Comuni Disponibili
Se `8888` è occupata, prova queste alternative:
| Porta | Uso Comune | Probabilità Libera |
|-------|------------|-------------------|
| `8881` | Alternative port | ????? Alta |
| `9999` | Generic services | ???? Alta |
| `7777` | Custom apps | ???? Alta |
| `8889` | Next to 8888 | ??? Media |
| `3000` | Dev servers | ?? Bassa (spesso occupata) |
| `8080` | ? NON usare | Troppo comune, quasi sempre occupata |
**Check porta disponibile**:
```bash
# Su Unraid via SSH
netstat -tulpn | grep :8888
# Se restituisce risultato ? porta occupata
# Se vuoto ? porta libera ?
```
### Differenza HOST vs CONTAINER Port
?? **IMPORTANTE da capire**:
```
HOST Port (8888) ? CONTAINER Port (8080)
?? Porta su Unraid ?? Porta interna Docker
?? Quella nel BROWSER ?? Fissa, NON modificare
?? Configurabile ?? Hardcoded nell'app
?? Esempio: 8888 ?? Sempre 8080
```
**Esempio configurazione corretta**:
```
Browser: http://192.168.30.23:8888
?? Usa porta HOST
Docker: 8888 (host) ? 8080 (container)
?? Mapping ?? App interna
```
**Cosa NON fare**:
- ? Cambiare porta Container da 8080 a altro
- ? Modificare `ASPNETCORE_URLS` (deve restare `http://+:8080`)
- ? Usare porta Host 8080 (conflitto con container)
**Cosa puoi fare**:
- ? Cambiare porta Host da 8888 a qualsiasi altra libera
- ? Usare porta Host diversa per ogni app
- ? Accedere con `http://IP:PORTA_HOST`
---
## ?? QUICK START COMPLETO
**Setup in 3 minuti**:
```bash
# 1. Login (una volta)
docker login gitea.encke-hake.ts.net
# 2. Download template
wget -O /boot/config/plugins/dockerMan/templates-user/TradingBot.xml \
https://gitea.encke-hake.ts.net/Alby96/Encelado/raw/branch/main/TradingBot/deployment/unraid-template.xml
# 3. Install via UI
# Docker tab ? Add Container ? TradingBot template ? Apply
# 4. Access WebUI
# Metodo A: Click icona ?? nella Docker tab
# Metodo B: http://192.168.30.23:8888
```
**?? TradingBot pronto su Unraid!**
---
## ?? Nota sulla Porta
**Default**: Porta HOST `8888` (invece di 8080)
**Perché 8888?**
- Porta 8080 è troppo comune e spesso occupata
- 8888 è quasi sempre libera su Unraid
- Facile da ricordare (quattro 8)
- WebUI icon funziona automaticamente
**Se 8888 è occupata**: Cambia in fase di installazione o dopo via Edit
---
## ?? ACCESSO WEBUI
### Locale (Unraid LAN)
```
http://192.168.30.23:8888
```
Sostituisci:
- `192.168.30.23` con IP del tuo Unraid
- `8888` con porta HOST configurata (se diversa)
### Via Tailscale
Se hai configurato Tailscale su Unraid:
```
http://unraid.encke-hake.ts.net:8888
```
### Via Hostname Unraid
Se hai configurato hostname:
```
http://tower:8888
```
(Sostituisci `tower` con hostname del tuo Unraid e `8888` con porta configurata)
### Reverse Proxy (Accesso HTTPS)
Se usi **Nginx Proxy Manager** o **Swag**:
```nginx
# Nginx Proxy Manager
Upstream: http://192.168.30.23:8888
Domain: tradingbot.tuo-dominio.com
SSL: Let's Encrypt
```
Poi accedi via:
```
https://tradingbot.tuo-dominio.com
```
?? **Nota**: Il reverse proxy si connette alla porta HOST (8888), non container (8080)
---
## ?? SICUREZZA
### Best Practices
? **Porta non esposta pubblicamente** (solo LAN o VPN)
? **Volume dati protetto** (`/mnt/user/appdata/tradingbot/`)
? **Registry privato** (Gitea richiede login)
? **Certificati validi** (Tailscale)
? **User non-root** (già configurato nel Dockerfile)
? **Dati persistenti** backup-ready
---
## ?? CHECKLIST INSTALLAZIONE
### Pre-Install
- [ ] Unraid 6.10+ installato
- [ ] Docker service attivo
- [ ] Porta 8888 (o alternativa) disponibile
- [ ] `docker login gitea.encke-hake.ts.net` successful
- [ ] Internet attivo per pull immagine
### Install
- [ ] Template XML scaricato su Unraid
- [ ] Container creato da template
- [ ] Porta WebUI configurata (8888 host ? 8080 container)
- [ ] Volume AppData creato (`/mnt/user/appdata/tradingbot`)
- [ ] Container status: **running**
### Post-Install
- [ ] WebUI accessibile (http://IP:8888)
- [ ] Dashboard carica correttamente
- [ ] Settings modificabili e salvabili
- [ ] Bot avviabile dalla UI
- [ ] Trade vengono salvati automaticamente
- [ ] Dati persistono dopo restart
---
## ?? VANTAGGI UNRAID NATIVO
? **Zero dipendenze** (no Portainer, no docker-compose)
? **WebUI Unraid integrata** (gestione familiare)
? **Auto-start** (container parte con Unraid)
? **Backup integrato** (con plugin CA)
? **Update semplice** (2 click: Stop ? Update ? Start)
? **Template riutilizzabile** (reinstall in 1 minuto)
? **Dati persistenti** (trade e settings sopravvivono)
? **Logs accessibili** dalla UI
---
## ?? WORKFLOW COMPLETO
### Sviluppo (PC)
```
1. ?? Visual Studio ? Codice
2. ?? Build ? Publish (Docker profile)
3. ? Automatico: Push Gitea Registry
?? Tags: latest, 1.2.0, 1.2.0-YYYYMMDD
4. ?? git push origin main --tags
```
### Deploy (Unraid)
```
1. ?? Docker tab ? TradingBot
2. ?? Stop
3. ?? Force Update (pull latest)
4. ?? Start
5. ? Done! (~ 1 minuto)
?? Dati automaticamente ripristinati
```
**Tempo totale**: ~2 minuti dal commit al running!
---
## ?? RISORSE
### Links Utili
| Risorsa | URL |
|---------|-----|
| **Template XML** | `https://gitea.encke-hake.ts.net/Alby96/Encelado/raw/branch/main/TradingBot/deployment/unraid-template.xml` |
| **Repository Git** | `https://gitea.encke-hake.ts.net/Alby96/Encelado` |
| **Docker Image** | `gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest` |
| **Packages** | `https://gitea.encke-hake.ts.net/Alby96/Encelado/-/packages` |
| **Support/Issues** | `https://gitea.encke-hake.ts.net/Alby96/Encelado/issues` |
### Comandi Utili
```bash
# Status container
docker ps -a | grep TradingBot
# Logs real-time
docker logs -f TradingBot
# Statistics
docker stats TradingBot --no-stream
# Restart
docker restart TradingBot
# Update
docker pull gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest
# Remove (mantiene dati in /mnt/user/appdata/tradingbot)
docker rm -f TradingBot
# Inspect persistent data
ls -lh /mnt/user/appdata/tradingbot/
cat /mnt/user/appdata/tradingbot/trade-history.json | jq
```
---
**?? TradingBot v1.2.0 con persistenza completa pronto su Unraid!**
+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0"?>
<Container version="2">
<Name>TradingBot</Name>
<Repository>gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest</Repository>
<Registry>https://gitea.encke-hake.ts.net</Registry>
<Network>bridge</Network>
<MyIP/>
<Shell>sh</Shell>
<Privileged>false</Privileged>
<Support>https://gitea.encke-hake.ts.net/Alby96/Encelado</Support>
<Project>https://gitea.encke-hake.ts.net/Alby96/Encelado</Project>
<Overview>TradingBot - Automated Crypto Trading Bot con Blazor UI. Trading algoritmico, analisi tecnica e gestione portfolio.</Overview>
<Category>Tools:Productivity Status:Stable</Category>
<WebUI>http://[IP]:[PORT:8888]/</WebUI>
<TemplateURL>https://gitea.encke-hake.ts.net/Alby96/Encelado/raw/branch/main/TradingBot/deployment/unraid-template.xml</TemplateURL>
<Icon>https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/dotnet.png</Icon>
<ExtraParams/>
<PostArgs/>
<CPUset/>
<DateInstalled/>
<DonateText/>
<DonateLink/>
<DonateImg/>
<Requires/>
<!-- CRITICAL: Port Configuration - Must be visible -->
<Config Name="WebUI Port" Target="8080" Default="8888" Mode="tcp" Description="Porta HTTP WebUI (Host:8888 -&gt; Container:8080)" Type="Port" Display="always" Required="true" Mask="false">8888</Config>
<!-- Volume Configuration -->
<Config Name="AppData" Target="/app/data" Default="/mnt/user/appdata/tradingbot" Mode="rw" Description="Directory dati persistenti" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/tradingbot</Config>
<!-- Environment Variables -->
<Config Name="ASPNETCORE_ENVIRONMENT" Target="ASPNETCORE_ENVIRONMENT" Default="Production" Mode="" Description="Runtime environment" Type="Variable" Display="advanced" Required="true" Mask="false">Production</Config>
<Config Name="ASPNETCORE_URLS" Target="ASPNETCORE_URLS" Default="http://+:8080" Mode="" Description="Internal binding (do not change)" Type="Variable" Display="advanced" Required="true" Mask="false">http://+:8080</Config>
<Config Name="TZ" Target="TZ" Default="Europe/Rome" Mode="" Description="Timezone" Type="Variable" Display="advanced" Required="false" Mask="false">Europe/Rome</Config>
</Container>
@@ -1,427 +0,0 @@
# ?? 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/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 (opzionale):
- **Port**: `8080` (o cambia se occupata)
- **Data Path**: `/mnt/user/appdata/tradingbot/data`
5. Click **Apply**
Unraid farà:
- ? Pull immagine da Gitea Registry
- ? Crea container
- ? Crea volume per dati
- ? Start automatico
### Step 3: Accedi WebUI
```
http://192.168.30.23:8080
```
Dovresti vedere la Dashboard TradingBot! ??
---
## ?? 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
Click **Add another Path, Port, Variable, Label or Device**
**Config Type**: `Port`
- **Name**: WebUI
- **Container Port**: `8080`
- **Host Port**: `8080` (o altra porta libera)
- **Connection Type**: `TCP`
### Step 4: Volume Mapping
Click **Add another Path, Port, Variable, Label or Device**
**Config Type**: `Path`
- **Name**: AppData
- **Container Path**: `/app/data`
- **Host Path**: `/mnt/user/appdata/tradingbot/data`
- **Access Mode**: `Read/Write`
### Step 5: Environment Variables
**ASPNETCORE_ENVIRONMENT**:
- **Name**: `ASPNETCORE_ENVIRONMENT`
- **Value**: `Production`
**ASPNETCORE_URLS**:
- **Name**: `ASPNETCORE_URLS`
- **Value**: `http://+:8080`
### Step 6: Health Check (Opzionale)
**Extra Parameters**:
```
--health-cmd="wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1" --health-interval=30s --health-timeout=3s --health-retries=3 --health-start-period=10s
```
### Step 7: Apply
Click **Apply** in fondo alla pagina.
---
## ?? AGGIORNAMENTO CONTAINER
### Via Unraid Docker Tab
1. **Docker** ? Trova **TradingBot**
2. Click **icona ferma** (stop container)
3. Click **Force Update** (icona update)
4. Click **icona play** (start container)
Unraid farà:
- ? Pull ultima immagine da Gitea
- ? Ricrea container
- ? Mantiene dati (volume persistente)
### 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
# Stop container
docker stop TradingBot
# Remove container
docker rm TradingBot
# Pull latest image
docker pull gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest
# Recreate container (Unraid fa automaticamente)
# O riavvia manualmente dalla WebUI
echo "Update completato! Riavvia TradingBot dalla Docker tab."
```
Schedula: Ogni settimana o manualmente.
---
## ?? GESTIONE CONTAINER
### Start/Stop/Restart
**Docker tab** ? Container **TradingBot**:
- ?? **Play**: Start
- ?? **Pause**: Pausa (mantiene in memoria)
- ?? **Stop**: Ferma
- ?? **Restart**: Riavvia
### View Logs
**Docker tab** ? Container **TradingBot** ? Click **icona logs**
O via terminal:
```bash
docker logs TradingBot -f
```
### Console Access
**Docker tab** ? Container **TradingBot** ? Click **icona console**
O via terminal:
```bash
docker exec -it TradingBot bash
```
### Statistics
**Docker tab** ? Container **TradingBot** ? Click **icona stats**
Mostra:
- CPU usage
- Memory usage
- Network I/O
- Block I/O
---
## ?? ACCESSO WEBUI
### Locale (Unraid LAN)
```
http://192.168.30.23:8080
```
### Via Tailscale
```
http://unraid.encke-hake.ts.net:8080
```
(Se hai configurato Tailscale su Unraid)
### Reverse Proxy (Opzionale)
Se vuoi accesso HTTPS:
**Nginx Proxy Manager** (tramite Unraid):
```
https://tradingbot.encke-hake.ts.net ? http://192.168.30.23:8080
```
---
## ?? SICUREZZA
### Best Practices
? **Porta non esposta pubblicamente** (solo LAN o Tailscale)
? **Volume dati protetto** (`/mnt/user/appdata/tradingbot/`)
? **Registry privato** (Gitea richiede login)
? **Certificati validi** (Tailscale)
? **User non-root** (già configurato nel Dockerfile)
### Backup Dati
```bash
# Backup manuale
tar -czf tradingbot-backup-$(date +%Y%m%d).tar.gz \
/mnt/user/appdata/tradingbot/data
# Restore
tar -xzf tradingbot-backup-20241212.tar.gz -C /
```
O usa **CA Backup / Restore Appdata** plugin.
---
## ?? TROUBLESHOOTING
### Container Non Si Avvia
**Check logs**:
```bash
docker logs TradingBot
```
**Problemi comuni**:
#### Porta occupata
```
Error: address already in use
```
**Fix**: Cambia porta in configurazione container
#### Pull failed
```
Error: unauthorized: authentication required
```
**Fix**: `docker login gitea.encke-hake.ts.net`
#### Image not found
```
Error: manifest not found
```
**Fix**: Verifica che l'immagine esista su Gitea Packages
### WebUI Non Accessibile
**Checklist**:
- [ ] Container status: **running** (verde)
- [ ] Health check: **healthy**
- [ ] Porta corretta (8080 o custom)
- [ ] Firewall Unraid non blocca
- [ ] Browser su `http://` non `https://`
**Test**:
```bash
curl http://localhost:8080/health
# Deve rispondere: Healthy
```
### Performance Issues
**Check risorse**:
```bash
docker stats TradingBot
```
**Limiti raccomandati**:
- **CPU**: 2 cores max, 0.5 reserved
- **Memory**: 1GB max, 256MB reserved
Configura in **Extra Parameters**:
```
--cpus="2.0" --memory="1g" --memory-reservation="256m"
```
---
## ?? CHECKLIST INSTALLAZIONE
### Pre-Install
- [ ] Unraid aggiornato
- [ ] Docker service attivo
- [ ] Porta 8080 disponibile
- [ ] `docker login gitea.encke-hake.ts.net` successful
### Install
- [ ] Template XML copiato (Metodo 1)
- [ ] O container configurato manualmente (Metodo 2)
- [ ] Container creato
- [ ] Volume dati creato
### Post-Install
- [ ] Container status: running
- [ ] Health check: healthy
- [ ] WebUI accessibile
- [ ] Settings configurati nell'app
---
## ?? 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** (1 click)
? **Template riutilizzabile** (facile reinstall)
---
## ?? WORKFLOW COMPLETO
### Sviluppo (PC)
```
1. Codice in Visual Studio
2. Build ? Publish (Release)
3. ? Automatico: Push Gitea Registry
4. Commit: git push
```
### Deploy (Unraid)
```
1. Docker tab ? TradingBot ? Stop
2. Force Update
3. Start
4. Done! ?
```
**Tempo totale**: ~2 minuti
---
## ?? RISORSE
### Template XML
```
https://gitea.encke-hake.ts.net/Alby96/Encelado/raw/branch/main/TradingBot/unraid-template.xml
```
### Repository
```
https://gitea.encke-hake.ts.net/Alby96/Encelado
```
### Docker Image
```
gitea.encke-hake.ts.net/alby96/encelado/tradingbot:latest
```
### Support
```
https://gitea.encke-hake.ts.net/Alby96/Encelado/issues
```
---
**?? TradingBot pronto per Unraid senza Portainer!**
**Quick Start**:
```bash
# 1. Login
docker login gitea.encke-hake.ts.net
# 2. Install Template
wget -O /boot/config/plugins/dockerMan/templates-user/TradingBot.xml \
https://gitea.encke-hake.ts.net/Alby96/Encelado/raw/branch/main/TradingBot/unraid-template.xml
# 3. Docker tab ? Add Container ? TradingBot ? Apply
```
**Access**: `http://192.168.30.23:8080` ??
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env pwsh
<#
.SYNOPSIS
Manual push of Docker image to Gitea Registry
.DESCRIPTION
Pushes the local tradingbot:latest image to Gitea Registry with proper tags
Use this when Visual Studio Publish was done without Docker running
#>
param(
[string]$Version = "1.3.0"
)
$ErrorActionPreference = "Stop"
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "?? Manual Gitea Registry Push" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# Check Docker
Write-Host "?? Checking Docker..." -ForegroundColor Yellow
try {
docker version | Out-Null
Write-Host "? Docker is running" -ForegroundColor Green
} catch {
Write-Host "? Docker is not running!" -ForegroundColor Red
Write-Host " Please start Docker Desktop first" -ForegroundColor Yellow
exit 1
}
Write-Host ""
# Check local image exists
Write-Host "?? Checking local image..." -ForegroundColor Yellow
$localImage = docker images --format "{{.Repository}}:{{.Tag}}" | Select-String "^tradingbot:latest$"
if (-not $localImage) {
Write-Host "? Local image 'tradingbot:latest' not found!" -ForegroundColor Red
Write-Host " Please run Visual Studio Publish first" -ForegroundColor Yellow
exit 1
}
Write-Host "? Local image found: tradingbot:latest" -ForegroundColor Green
Write-Host ""
# Configuration
$registry = "gitea.encke-hake.ts.net"
$repository = "alby96/encelado/tradingbot"
$giteaImage = "$registry/$repository"
$buildDate = Get-Date -Format "yyyyMMdd"
$versionedTag = "$Version-$buildDate"
Write-Host "?? Version: $Version" -ForegroundColor Cyan
Write-Host "?? Build Date: $buildDate" -ForegroundColor Cyan
Write-Host ""
# Create tags
Write-Host "??? Creating tags..." -ForegroundColor Yellow
Write-Host " Tagging: ${giteaImage}:latest" -ForegroundColor Gray
docker tag tradingbot:latest "${giteaImage}:latest"
Write-Host " ? latest" -ForegroundColor Green
Write-Host " Tagging: ${giteaImage}:$Version" -ForegroundColor Gray
docker tag tradingbot:latest "${giteaImage}:$Version"
Write-Host " ? $Version" -ForegroundColor Green
Write-Host " Tagging: ${giteaImage}:$versionedTag" -ForegroundColor Gray
docker tag tradingbot:latest "${giteaImage}:$versionedTag"
Write-Host " ? $versionedTag" -ForegroundColor Green
Write-Host ""
# Push to Gitea
Write-Host "?? Pushing to $registry..." -ForegroundColor Yellow
Write-Host ""
Write-Host " Pushing: latest..." -ForegroundColor Gray
docker push "${giteaImage}:latest"
Write-Host " ? Pushed: latest" -ForegroundColor Green
Write-Host " Pushing: $Version..." -ForegroundColor Gray
docker push "${giteaImage}:$Version"
Write-Host " ? Pushed: $Version" -ForegroundColor Green
Write-Host " Pushing: $versionedTag..." -ForegroundColor Gray
docker push "${giteaImage}:$versionedTag"
Write-Host " ? Pushed: $versionedTag" -ForegroundColor Green
Write-Host ""
Write-Host "========================================" -ForegroundColor Green
Write-Host "? Successfully pushed to Gitea Registry!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green
Write-Host ""
Write-Host "?? Published images:" -ForegroundColor Cyan
Write-Host " - ${giteaImage}:latest" -ForegroundColor White
Write-Host " - ${giteaImage}:$Version" -ForegroundColor White
Write-Host " - ${giteaImage}:$versionedTag" -ForegroundColor White
Write-Host ""
Write-Host "?? Verify at:" -ForegroundColor Cyan
Write-Host " https://$registry/Alby96/Encelado/-/packages" -ForegroundColor White
Write-Host ""
Write-Host "?? Next steps:" -ForegroundColor Cyan
Write-Host " 1. Verify on Gitea Packages (link above)" -ForegroundColor White
Write-Host " 2. SSH to Unraid: ssh root@192.168.30.23" -ForegroundColor White
Write-Host " 3. Force update:" -ForegroundColor White
Write-Host " docker stop TradingBot" -ForegroundColor Gray
Write-Host " docker rmi $giteaImage:latest" -ForegroundColor Gray
Write-Host " docker pull $giteaImage:latest" -ForegroundColor Gray
Write-Host " docker start TradingBot" -ForegroundColor Gray
Write-Host ""
-29
View File
@@ -1,29 +0,0 @@
<?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>Automated Crypto Trading Bot con Blazor UI. Analisi tecnica, simulazione trading e gestione portfolio automatizzata.</Overview>
<Category>Tools:Productivity Status:Stable</Category>
<WebUI>http://[IP]:[PORT:8080]</WebUI>
<TemplateURL>https://gitea.encke-hake.ts.net/Alby96/Encelado/raw/branch/main/TradingBot/unraid-template.xml</TemplateURL>
<Icon>https://raw.githubusercontent.com/docker-library/docs/master/dotnet/logo.png</Icon>
<ExtraParams>--health-cmd="wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1" --health-interval=30s --health-timeout=3s --health-retries=3 --health-start-period=10s</ExtraParams>
<PostArgs/>
<CPUset/>
<DateInstalled/>
<DonateText/>
<DonateLink/>
<Requires/>
<Config Name="WebUI Port" Target="8080" Default="8080" Mode="tcp" Description="Porta per accesso 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="Persistenza dati applicazione (impostazioni, trade history)" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/tradingbot/data</Config>
<Config Name="ASPNETCORE_ENVIRONMENT" Target="ASPNETCORE_ENVIRONMENT" Default="Production" Mode="" Description="Ambiente ASP.NET Core (non modificare)" Type="Variable" Display="advanced" Required="false" Mask="false">Production</Config>
<Config Name="ASPNETCORE_URLS" Target="ASPNETCORE_URLS" Default="http://+:8080" Mode="" Description="URL binding (non modificare)" Type="Variable" Display="advanced" Required="false" Mask="false">http://+:8080</Config>
<Config Name="DOTNET_RUNNING_IN_CONTAINER" Target="DOTNET_RUNNING_IN_CONTAINER" Default="true" Mode="" Description=".NET container flag (non modificare)" Type="Variable" Display="advanced" Required="false" Mask="false">true</Config>
</Container>
+117
View File
@@ -469,6 +469,123 @@ select:focus {
}
}
/* Modal Styles */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.75);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
animation: fadeIn 0.2s ease-out;
}
.modal-dialog {
background: #1a1f3a;
border-radius: 0.75rem;
width: 90%;
max-width: 500px;
border: 1px solid rgba(99, 102, 241, 0.2);
animation: slideIn 0.3s ease-out;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.modal-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(99, 102, 241, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
}
.modal-header h3 {
font-size: 1.25rem;
font-weight: 700;
color: #e2e8f0;
margin: 0;
}
.btn-close {
width: 2rem;
height: 2rem;
border-radius: 0.375rem;
background: transparent;
border: none;
color: #94a3b8;
font-size: 1.5rem;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.btn-close:hover {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.modal-body {
padding: 1.5rem;
}
.modal-body p {
margin-bottom: 1rem;
line-height: 1.6;
}
.modal-body ul {
margin: 1rem 0;
padding-left: 1.5rem;
}
.modal-body li {
margin: 0.5rem 0;
color: #cbd5e1;
}
.modal-footer {
padding: 1.5rem;
border-top: 1px solid rgba(99, 102, 241, 0.1);
display: flex;
gap: 1rem;
justify-content: flex-end;
}
.text-danger {
color: #ef4444 !important;
}
/* Danger Button */
.btn-danger {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
color: white;
border: none;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.3);
}
.btn-danger:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(220, 38, 38, 0.4);
background: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%);
}
.btn-danger:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
/* Print Styles */
@media print {
.no-print {