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à
This commit is contained in:
@@ -6,6 +6,148 @@ Formato basato su [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), segu
|
||||
|
||||
---
|
||||
|
||||
## [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
|
||||
@@ -109,6 +251,8 @@ Formato basato su [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), segu
|
||||
|
||||
---
|
||||
|
||||
[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
|
||||
|
||||
@@ -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)
|
||||
@@ -103,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>
|
||||
}
|
||||
@@ -148,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()
|
||||
{
|
||||
@@ -155,6 +219,7 @@
|
||||
sidebarCollapsed = settings.SidebarCollapsed;
|
||||
|
||||
BotService.OnStatusChanged += HandleUpdate;
|
||||
BotService.OnTradeExecuted += HandleTradeExecuted;
|
||||
BotService.OnPriceUpdated += HandlePriceUpdate;
|
||||
SettingsService.OnSettingsChanged += HandleSettingsChanged;
|
||||
|
||||
@@ -184,34 +249,69 @@
|
||||
|
||||
private void UpdateStats()
|
||||
{
|
||||
portfolioValue = BotService.AssetConfigurations.Values.Sum(c =>
|
||||
c.CurrentBalance + (c.CurrentHoldings * (BotService.GetLatestPrice(c.Symbol)?.Price ?? 0)));
|
||||
totalCapital = 0;
|
||||
investedCapital = 0;
|
||||
availableCapital = 0;
|
||||
currentPL = 0;
|
||||
totalProfit = 0;
|
||||
|
||||
totalProfit = BotService.AssetConfigurations.Values.Sum(c => c.TotalProfit);
|
||||
foreach (var config in BotService.AssetConfigurations.Values)
|
||||
{
|
||||
if (!config.IsEnabled) continue;
|
||||
|
||||
// Capitale disponibile (cash)
|
||||
availableCapital += config.CurrentBalance;
|
||||
|
||||
// Capitale investito in posizioni aperte
|
||||
var currentPrice = BotService.GetLatestPrice(config.Symbol)?.Price ?? 0;
|
||||
var positionValue = config.CurrentHoldings * currentPrice;
|
||||
investedCapital += positionValue;
|
||||
|
||||
// P&L non realizzato sulle posizioni aperte
|
||||
if (config.CurrentHoldings > 0 && config.AverageEntryPrice > 0)
|
||||
{
|
||||
var entryValue = config.CurrentHoldings * config.AverageEntryPrice;
|
||||
currentPL += (positionValue - entryValue);
|
||||
}
|
||||
|
||||
// Totale profitti/perdite realizzati
|
||||
totalProfit += config.TotalProfit;
|
||||
}
|
||||
|
||||
// Capitale totale = Disponibile + Investito
|
||||
totalCapital = availableCapital + investedCapital;
|
||||
|
||||
// Portfolio value include anche i profitti realizzati
|
||||
portfolioValue = totalCapital + totalProfit;
|
||||
|
||||
// ROI basato sul capitale iniziale
|
||||
var initialCapital = BotService.AssetConfigurations.Values
|
||||
.Where(c => c.IsEnabled)
|
||||
.Sum(c => c.InitialBalance);
|
||||
|
||||
if (initialCapital > 0)
|
||||
{
|
||||
var totalGain = currentPL + totalProfit;
|
||||
roiPercentage = (totalGain / initialCapital) * 100;
|
||||
}
|
||||
else
|
||||
{
|
||||
roiPercentage = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleUpdate()
|
||||
{
|
||||
UpdateStats();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
private void HandleUpdate() => InvokeAsync(() => { UpdateStats(); StateHasChanged(); });
|
||||
|
||||
private void HandlePriceUpdate(string symbol, MarketPrice price)
|
||||
{
|
||||
UpdateStats();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
private void HandleTradeExecuted(Trade trade) => InvokeAsync(() => { UpdateStats(); StateHasChanged(); });
|
||||
|
||||
private void HandleSettingsChanged()
|
||||
{
|
||||
var settings = SettingsService.GetSettings();
|
||||
sidebarCollapsed = settings.SidebarCollapsed;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
private void HandlePriceUpdate(string symbol, MarketPrice price) => InvokeAsync(() => { UpdateStats(); StateHasChanged(); });
|
||||
|
||||
private void HandleSettingsChanged() => InvokeAsync(StateHasChanged);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
BotService.OnStatusChanged -= HandleUpdate;
|
||||
BotService.OnTradeExecuted -= HandleTradeExecuted;
|
||||
BotService.OnPriceUpdated -= HandlePriceUpdate;
|
||||
SettingsService.OnSettingsChanged -= HandleSettingsChanged;
|
||||
}
|
||||
|
||||
@@ -256,12 +256,35 @@
|
||||
gap: 0.875rem !important;
|
||||
}
|
||||
|
||||
::deep .summary-header {
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
::deep .summary-section-title {
|
||||
font-size: 0.875rem !important;
|
||||
color: #6366f1 !important;
|
||||
font-weight: 700 !important;
|
||||
text-transform: uppercase !important;
|
||||
letter-spacing: 0.05em !important;
|
||||
}
|
||||
|
||||
::deep .summary-row {
|
||||
display: flex !important;
|
||||
justify-content: space-between !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
::deep .summary-row.highlight {
|
||||
padding-top: 0.5rem !important;
|
||||
margin-top: 0.5rem !important;
|
||||
}
|
||||
|
||||
::deep .summary-divider {
|
||||
height: 1px !important;
|
||||
background: rgba(99, 102, 241, 0.2) !important;
|
||||
margin: 0.25rem 0 !important;
|
||||
}
|
||||
|
||||
::deep .summary-title {
|
||||
font-size: 0.75rem !important;
|
||||
color: #64748b !important;
|
||||
@@ -285,6 +308,14 @@
|
||||
color: #ef4444 !important;
|
||||
}
|
||||
|
||||
::deep .summary-amount.invested {
|
||||
color: #f59e0b !important;
|
||||
}
|
||||
|
||||
::deep .summary-amount.available {
|
||||
color: #3b82f6 !important;
|
||||
}
|
||||
|
||||
/* ==============================================
|
||||
MAIN CONTENT AREA
|
||||
============================================== */
|
||||
@@ -466,3 +497,46 @@
|
||||
padding: 1.5rem 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==============================================
|
||||
VERSION FOOTER
|
||||
============================================== */
|
||||
|
||||
::deep .sidebar-footer {
|
||||
padding: 1rem 1.5rem !important;
|
||||
border-top: 1px solid rgba(99, 102, 241, 0.1) !important;
|
||||
margin-top: auto !important;
|
||||
}
|
||||
|
||||
::deep .version-info {
|
||||
display: flex !important;
|
||||
justify-content: space-between !important;
|
||||
align-items: center !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
::deep .version-label {
|
||||
font-size: 0.75rem !important;
|
||||
color: #64748b !important;
|
||||
font-weight: 600 !important;
|
||||
text-transform: uppercase !important;
|
||||
letter-spacing: 0.05em !important;
|
||||
}
|
||||
|
||||
::deep .version-number {
|
||||
font-size: 0.875rem !important;
|
||||
font-weight: 700 !important;
|
||||
color: #6366f1 !important;
|
||||
font-family: 'Courier New', monospace !important;
|
||||
}
|
||||
|
||||
::deep .build-info {
|
||||
display: flex !important;
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
||||
::deep .build-date {
|
||||
font-size: 0.625rem !important;
|
||||
color: #475569 !important;
|
||||
font-family: 'Courier New', monospace !important;
|
||||
}
|
||||
|
||||
467
TradingBot/Components/Pages/Indicators.razor
Normal file
467
TradingBot/Components/Pages/Indicators.razor
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
716
TradingBot/Components/Pages/Positions.razor
Normal file
716
TradingBot/Components/Pages/Positions.razor
Normal 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;
|
||||
}
|
||||
}
|
||||
555
TradingBot/Components/Pages/TradingControl.razor
Normal file
555
TradingBot/Components/Pages/TradingControl.razor
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
220
TradingBot/Components/Shared/IndicatorsWidget.razor
Normal file
220
TradingBot/Components/Shared/IndicatorsWidget.razor
Normal file
@@ -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
TradingBot/Models/IndicatorModels.cs
Normal file
94
TradingBot/Models/IndicatorModels.cs
Normal 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
|
||||
}
|
||||
137
TradingBot/Models/TradingEngine.cs
Normal file
137
TradingBot/Models/TradingEngine.cs
Normal 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
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ 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>();
|
||||
|
||||
|
||||
@@ -5,16 +5,20 @@
|
||||
[](https://dotnet.microsoft.com/)
|
||||
[](https://blazor.net/)
|
||||
[](https://www.docker.com/)
|
||||
[](https://gitea.encke-hake.ts.net/Alby96/Encelado/-/packages)
|
||||
[](https://gitea.encke-hake.ts.net/Alby96/Encelado/-/packages)
|
||||
|
||||
---
|
||||
|
||||
## ? Caratteristiche
|
||||
|
||||
- **Trading Algoritmico**: Simple Moving Average (SMA) strategy
|
||||
- **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
|
||||
- **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
|
||||
@@ -22,6 +26,28 @@
|
||||
|
||||
---
|
||||
|
||||
## ?? 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)
|
||||
@@ -64,18 +90,18 @@ wget -O /boot/config/plugins/dockerMan/templates-user/TradingBot.xml \
|
||||
|
||||
## ?? Versioning
|
||||
|
||||
### Current Version: `1.3.0`
|
||||
### Current Version: `1.5.2`
|
||||
|
||||
**Latest**: Comprehensive logs page con monitoring real-time
|
||||
**Latest**: Metriche dettagliate capitale in sidebar (Totale, Investito, Disponibile, P&L, ROI)
|
||||
|
||||
```powershell
|
||||
# Bug fix (1.3.0 ? 1.3.1)
|
||||
.\bump-version.ps1 patch -Message "Fix memory leak"
|
||||
# Bug fix (1.5.2 ? 1.5.3)
|
||||
.\bump-version.ps1 patch -Message "Fix calculation bug"
|
||||
|
||||
# New feature (1.3.0 ? 1.4.0)
|
||||
.\bump-version.ps1 minor -Message "Add RSI strategy"
|
||||
# New feature (1.5.2 ? 1.6.0)
|
||||
.\bump-version.ps1 minor -Message "Add new strategy"
|
||||
|
||||
# Breaking change (1.3.0 ? 2.0.0)
|
||||
# Breaking change (1.5.2 ? 2.0.0)
|
||||
.\bump-version.ps1 major -Message "New API"
|
||||
```
|
||||
|
||||
@@ -93,7 +119,7 @@ Vedi [CHANGELOG.md](CHANGELOG.md) per release notes complete.
|
||||
|
||||
Il sistema automaticamente:
|
||||
- ? Build Docker image
|
||||
- ? Tag: `latest`, `1.3.0`, `1.3.0-20241221`
|
||||
- ? Tag: `latest`, `1.5.1`, `1.5.1-20241222`
|
||||
- ? Push su Gitea Registry
|
||||
|
||||
### Deploy su Unraid
|
||||
|
||||
346
TradingBot/Services/IndicatorsService.cs
Normal file
346
TradingBot/Services/IndicatorsService.cs
Normal 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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ public class TradingBotService
|
||||
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();
|
||||
@@ -34,12 +36,16 @@ public class TradingBotService
|
||||
IMarketDataService marketDataService,
|
||||
ITradingStrategy strategy,
|
||||
TradeHistoryService historyService,
|
||||
LoggingService loggingService)
|
||||
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
|
||||
@@ -484,6 +490,136 @@ public class TradingBotService
|
||||
|
||||
_indicators[symbol] = indicators;
|
||||
OnIndicatorsUpdated?.Invoke(symbol, indicators);
|
||||
|
||||
// Update IndicatorsService statuses
|
||||
UpdateIndicatorStatuses(symbol, indicators, prices);
|
||||
}
|
||||
|
||||
private void UpdateIndicatorStatuses(string symbol, TechnicalIndicators indicators, List<decimal> prices)
|
||||
{
|
||||
// Update RSI status
|
||||
var rsiConfig = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "rsi");
|
||||
if (rsiConfig?.IsEnabled == true)
|
||||
{
|
||||
var rsiStatus = new IndicatorStatus
|
||||
{
|
||||
IndicatorId = "rsi",
|
||||
Symbol = symbol,
|
||||
CurrentValue = indicators.RSI,
|
||||
Condition = indicators.RSI > (rsiConfig.OverboughtThreshold ?? 70) ? MarketCondition.Overbought :
|
||||
indicators.RSI < (rsiConfig.OversoldThreshold ?? 30) ? MarketCondition.Oversold :
|
||||
MarketCondition.Neutral,
|
||||
Recommendation = indicators.RSI > (rsiConfig.OverboughtThreshold ?? 70) ? "Possibile vendita" :
|
||||
indicators.RSI < (rsiConfig.OversoldThreshold ?? 30) ? "Possibile acquisto" :
|
||||
"Attendi conferma"
|
||||
};
|
||||
_indicatorsService.UpdateIndicatorStatus("rsi", symbol, rsiStatus);
|
||||
|
||||
// Generate signal if crossing threshold
|
||||
if (indicators.RSI < 30)
|
||||
{
|
||||
_indicatorsService.GenerateSignal(new IndicatorSignal
|
||||
{
|
||||
IndicatorId = "rsi",
|
||||
IndicatorName = "RSI",
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Buy,
|
||||
Strength = indicators.RSI < 20 ? SignalStrength.VeryStrong : SignalStrength.Strong,
|
||||
Message = $"RSI in zona ipervenduto: {indicators.RSI:F2}",
|
||||
Value = indicators.RSI
|
||||
});
|
||||
}
|
||||
else if (indicators.RSI > 70)
|
||||
{
|
||||
_indicatorsService.GenerateSignal(new IndicatorSignal
|
||||
{
|
||||
IndicatorId = "rsi",
|
||||
IndicatorName = "RSI",
|
||||
Symbol = symbol,
|
||||
Type = SignalType.Sell,
|
||||
Strength = indicators.RSI > 80 ? SignalStrength.VeryStrong : SignalStrength.Strong,
|
||||
Message = $"RSI in zona ipercomprato: {indicators.RSI:F2}",
|
||||
Value = indicators.RSI
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update MACD status
|
||||
var macdConfig = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "macd");
|
||||
if (macdConfig?.IsEnabled == true)
|
||||
{
|
||||
var macdStatus = new IndicatorStatus
|
||||
{
|
||||
IndicatorId = "macd",
|
||||
Symbol = symbol,
|
||||
CurrentValue = indicators.MACD,
|
||||
Condition = indicators.Histogram > 0 ? MarketCondition.Bullish : MarketCondition.Bearish,
|
||||
Recommendation = indicators.Histogram > 0 ? "Trend rialzista" : "Trend ribassista"
|
||||
};
|
||||
_indicatorsService.UpdateIndicatorStatus("macd", symbol, macdStatus);
|
||||
|
||||
// Generate signal on crossover
|
||||
if (Math.Abs(indicators.Histogram) < 0.5m) // Near crossover
|
||||
{
|
||||
_indicatorsService.GenerateSignal(new IndicatorSignal
|
||||
{
|
||||
IndicatorId = "macd",
|
||||
IndicatorName = "MACD",
|
||||
Symbol = symbol,
|
||||
Type = indicators.Histogram > 0 ? SignalType.Buy : SignalType.Sell,
|
||||
Strength = SignalStrength.Moderate,
|
||||
Message = $"MACD {(indicators.Histogram > 0 ? "bullish" : "bearish")} crossover",
|
||||
Value = indicators.MACD
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update SMA statuses
|
||||
var currentPrice = prices.Last();
|
||||
var sma20Config = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "sma_20");
|
||||
if (sma20Config?.IsEnabled == true && prices.Count >= 20)
|
||||
{
|
||||
var sma20 = prices.TakeLast(20).Average();
|
||||
var sma20Status = new IndicatorStatus
|
||||
{
|
||||
IndicatorId = "sma_20",
|
||||
Symbol = symbol,
|
||||
CurrentValue = sma20,
|
||||
Condition = currentPrice > sma20 ? MarketCondition.Bullish : MarketCondition.Bearish,
|
||||
Recommendation = currentPrice > sma20 ? "Prezzo sopra media" : "Prezzo sotto media"
|
||||
};
|
||||
_indicatorsService.UpdateIndicatorStatus("sma_20", symbol, sma20Status);
|
||||
}
|
||||
|
||||
var sma50Config = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "sma_50");
|
||||
if (sma50Config?.IsEnabled == true && prices.Count >= 50)
|
||||
{
|
||||
var sma50 = prices.TakeLast(50).Average();
|
||||
var sma50Status = new IndicatorStatus
|
||||
{
|
||||
IndicatorId = "sma_50",
|
||||
Symbol = symbol,
|
||||
CurrentValue = sma50,
|
||||
Condition = currentPrice > sma50 ? MarketCondition.Bullish : MarketCondition.Bearish,
|
||||
Recommendation = currentPrice > sma50 ? "Trend rialzista medio termine" : "Trend ribassista medio termine"
|
||||
};
|
||||
_indicatorsService.UpdateIndicatorStatus("sma_50", symbol, sma50Status);
|
||||
}
|
||||
|
||||
// Update EMA status
|
||||
var ema12Config = _indicatorsService.GetIndicators().Values.FirstOrDefault(i => i.Id == "ema_12");
|
||||
if (ema12Config?.IsEnabled == true)
|
||||
{
|
||||
var ema12Status = new IndicatorStatus
|
||||
{
|
||||
IndicatorId = "ema_12",
|
||||
Symbol = symbol,
|
||||
CurrentValue = indicators.EMA12,
|
||||
Condition = currentPrice > indicators.EMA12 ? MarketCondition.Bullish : MarketCondition.Bearish,
|
||||
Recommendation = currentPrice > indicators.EMA12 ? "Trend positivo" : "Trend negativo"
|
||||
};
|
||||
_indicatorsService.UpdateIndicatorStatus("ema_12", symbol, ema12Status);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAssetStatistics(string symbol, Trade trade, decimal? realizedProfit = null)
|
||||
@@ -631,4 +767,30 @@ public class TradingBotService
|
||||
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
TradingBot/Services/TradingStrategies.cs
Normal file
564
TradingBot/Services/TradingStrategies.cs
Normal 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}"
|
||||
});
|
||||
}
|
||||
}
|
||||
486
TradingBot/Services/TradingStrategiesService.cs
Normal file
486
TradingBot/Services/TradingStrategiesService.cs
Normal file
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
113
TradingBot/manual-push.ps1
Normal file
113
TradingBot/manual-push.ps1
Normal 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 ""
|
||||
Reference in New Issue
Block a user