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:
2026-01-06 17:49:07 +01:00
parent c229c50f1d
commit 64f3511695
18 changed files with 4266 additions and 41 deletions

View 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;
}
}

View File

@@ -0,0 +1,716 @@
@page "/positions"
@using TradingBot.Services
@using TradingBot.Models
@inject TradingBotService BotService
@inject LoggingService LoggingService
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Posizioni Aperte - TradingBot</PageTitle>
<div class="positions-page">
<div class="page-header">
<div>
<h1>Posizioni Aperte</h1>
<p class="subtitle">Gestisci le tue posizioni attive - Solo chiusura manuale disponibile</p>
</div>
<div class="header-stats">
<div class="stat-card">
<span class="stat-label">Posizioni Attive</span>
<span class="stat-value">@activePositions.Count</span>
</div>
<div class="stat-card">
<span class="stat-label">Valore Totale</span>
<span class="stat-value">$@totalValue.ToString("N2")</span>
</div>
<div class="stat-card">
<span class="stat-label">P&L Non Realizzato</span>
<span class="stat-value @(totalUnrealizedPL >= 0 ? "profit" : "loss")">
@(totalUnrealizedPL >= 0 ? "+" : "")$@totalUnrealizedPL.ToString("N2")
</span>
</div>
</div>
</div>
@if (activePositions.Count == 0)
{
<div class="empty-state">
<div class="empty-icon">
<span class="bi bi-inbox"></span>
</div>
<h3>Nessuna Posizione Aperta</h3>
<p>Non hai posizioni attive al momento. Le posizioni appaiono qui automaticamente quando il bot esegue un acquisto.</p>
<div class="empty-actions">
<a href="/trading-control" class="btn-primary">
<span class="bi bi-sliders"></span>
Configura Trading
</a>
<a href="/dashboard" class="btn-secondary">
<span class="bi bi-speedometer2"></span>
Dashboard
</a>
</div>
</div>
}
else
{
<div class="positions-grid">
@foreach (var position in activePositions.OrderBy(p => p.Symbol))
{
var currentPrice = GetCurrentPrice(position.Symbol);
var unrealizedPL = CalculateUnrealizedPL(position, currentPrice);
var plPercentage = CalculatePLPercentage(position, currentPrice);
var currentValue = position.Amount * currentPrice;
var holdingTime = DateTime.UtcNow - position.Timestamp;
<div class="position-card">
<div class="position-header">
<div class="position-asset">
<h3>@position.Symbol</h3>
<span class="position-date">
Aperta @position.Timestamp.ToLocalTime().ToString("dd/MM/yyyy HH:mm")
</span>
</div>
<div class="position-pl @(unrealizedPL >= 0 ? "profit" : "loss")">
<span class="pl-value">
@(unrealizedPL >= 0 ? "+" : "")$@unrealizedPL.ToString("N2")
</span>
<span class="pl-percentage">
(@(plPercentage >= 0 ? "+" : "")@plPercentage.ToString("F2")%)
</span>
</div>
</div>
<div class="position-details">
<div class="detail-row">
<span class="detail-label">Quantità</span>
<span class="detail-value">@position.Amount.ToString("F8") @position.Symbol</span>
</div>
<div class="detail-row">
<span class="detail-label">Prezzo Entrata</span>
<span class="detail-value">$@position.Price.ToString("N2")</span>
</div>
<div class="detail-row">
<span class="detail-label">Prezzo Corrente</span>
<span class="detail-value">$@currentPrice.ToString("N2")</span>
</div>
<div class="detail-row">
<span class="detail-label">Valore Iniziale</span>
<span class="detail-value">$@(position.Amount * position.Price).ToString("N2")</span>
</div>
<div class="detail-row">
<span class="detail-label">Valore Corrente</span>
<span class="detail-value">$@currentValue.ToString("N2")</span>
</div>
<div class="detail-row">
<span class="detail-label">Tempo Holding</span>
<span class="detail-value">@FormatHoldingTime(holdingTime)</span>
</div>
@if (!string.IsNullOrEmpty(position.Strategy))
{
<div class="detail-row">
<span class="detail-label">Strategia</span>
<span class="detail-value strategy-badge">@position.Strategy</span>
</div>
}
</div>
<div class="position-actions">
<button class="btn-danger" @onclick="() => ShowCloseConfirmation(position)"
disabled="@(!BotService.Status.IsRunning)">
<span class="bi bi-x-circle"></span>
Chiudi Posizione
</button>
@if (!BotService.Status.IsRunning)
{
<span class="action-note">
<span class="bi bi-info-circle"></span>
Avvia il bot per chiudere posizioni
</span>
}
</div>
</div>
}
</div>
}
<!-- Close Confirmation Modal -->
@if (showCloseModal && positionToClose != null)
{
var currentPrice = GetCurrentPrice(positionToClose.Symbol);
var unrealizedPL = CalculateUnrealizedPL(positionToClose, currentPrice);
var plPercentage = CalculatePLPercentage(positionToClose, currentPrice);
<div class="modal-overlay" @onclick="HideCloseConfirmation">
<div class="modal-dialog" @onclick:stopPropagation="true">
<div class="modal-header">
<h3>Conferma Chiusura Posizione</h3>
<button class="btn-close" @onclick="HideCloseConfirmation">×</button>
</div>
<div class="modal-body">
<div class="confirmation-details">
<div class="confirm-asset">
<h4>@positionToClose.Symbol</h4>
<span class="confirm-amount">@positionToClose.Amount.ToString("F8") @positionToClose.Symbol</span>
</div>
<div class="confirm-prices">
<div class="price-item">
<span class="price-label">Prezzo Entrata</span>
<span class="price-value">$@positionToClose.Price.ToString("N2")</span>
</div>
<span class="bi bi-arrow-right"></span>
<div class="price-item">
<span class="price-label">Prezzo Chiusura</span>
<span class="price-value">$@currentPrice.ToString("N2")</span>
</div>
</div>
<div class="confirm-pl @(unrealizedPL >= 0 ? "profit" : "loss")">
<div class="pl-label">Profitto/Perdita Stimato</div>
<div class="pl-amount">
@(unrealizedPL >= 0 ? "+" : "")$@unrealizedPL.ToString("N2")
</div>
<div class="pl-percent">
(@(plPercentage >= 0 ? "+" : "")@plPercentage.ToString("F2")%)
</div>
</div>
<div class="confirm-warning">
<span class="bi bi-exclamation-triangle"></span>
<p>
<strong>Attenzione:</strong> Questa azione chiuderà immediatamente la posizione al prezzo di mercato corrente.
L'operazione non può essere annullata.
</p>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" @onclick="HideCloseConfirmation">Annulla</button>
<button class="btn-danger" @onclick="ConfirmClosePosition">
<span class="bi bi-x-circle"></span>
Conferma Chiusura
</button>
</div>
</div>
</div>
}
<!-- Success Notification -->
@if (showSuccessNotification)
{
<div class="notification success">
<span class="bi bi-check-circle-fill"></span>
Posizione chiusa con successo!
</div>
}
</div>
<style>
.positions-page {
display: flex;
flex-direction: column;
gap: 2rem;
}
.header-stats {
display: flex;
gap: 1.5rem;
}
.stat-card {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem 1.5rem;
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
}
.stat-label {
font-size: 0.875rem;
color: #94a3b8;
font-weight: 600;
text-transform: uppercase;
}
.stat-value {
font-size: 1.75rem;
font-weight: 700;
color: #e2e8f0;
font-family: 'Courier New', monospace;
}
.stat-value.profit {
color: #10b981;
}
.stat-value.loss {
color: #ef4444;
}
.empty-state {
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
padding: 4rem 2rem;
text-align: center;
}
.empty-icon {
font-size: 4rem;
color: #64748b;
margin-bottom: 1.5rem;
}
.empty-state h3 {
color: #e2e8f0;
margin-bottom: 0.75rem;
}
.empty-state p {
color: #94a3b8;
margin-bottom: 2rem;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.empty-actions {
display: flex;
gap: 1rem;
justify-content: center;
}
.positions-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
gap: 1.5rem;
}
.position-card {
background: #1a1f3a;
border-radius: 0.75rem;
border: 1px solid rgba(99, 102, 241, 0.2);
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
transition: all 0.3s ease;
}
.position-card:hover {
border-color: rgba(99, 102, 241, 0.4);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.position-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding-bottom: 1rem;
border-bottom: 1px solid rgba(99, 102, 241, 0.1);
}
.position-asset h3 {
font-size: 1.5rem;
font-weight: 700;
color: #e2e8f0;
margin: 0 0 0.25rem 0;
}
.position-date {
font-size: 0.75rem;
color: #64748b;
}
.position-pl {
text-align: right;
}
.pl-value {
display: block;
font-size: 1.25rem;
font-weight: 700;
font-family: 'Courier New', monospace;
}
.position-pl.profit .pl-value {
color: #10b981;
}
.position-pl.loss .pl-value {
color: #ef4444;
}
.pl-percentage {
font-size: 0.875rem;
opacity: 0.8;
}
.position-details {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.detail-label {
font-size: 0.875rem;
color: #94a3b8;
font-weight: 600;
}
.detail-value {
font-size: 0.875rem;
color: #e2e8f0;
font-family: 'Courier New', monospace;
font-weight: 600;
}
.strategy-badge {
padding: 0.25rem 0.75rem;
background: rgba(99, 102, 241, 0.2);
border-radius: 0.25rem;
color: #6366f1;
font-family: inherit;
}
.position-actions {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding-top: 1rem;
border-top: 1px solid rgba(99, 102, 241, 0.1);
}
.action-note {
font-size: 0.75rem;
color: #f59e0b;
display: flex;
align-items: center;
gap: 0.5rem;
}
/* Modal Styles */
.confirmation-details {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.confirm-asset {
text-align: center;
padding: 1rem;
background: rgba(99, 102, 241, 0.1);
border-radius: 0.5rem;
}
.confirm-asset h4 {
font-size: 1.5rem;
color: #e2e8f0;
margin: 0 0 0.5rem 0;
}
.confirm-amount {
color: #94a3b8;
font-family: 'Courier New', monospace;
}
.confirm-prices {
display: flex;
justify-content: space-around;
align-items: center;
padding: 1rem;
background: rgba(0, 0, 0, 0.2);
border-radius: 0.5rem;
}
.price-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
text-align: center;
}
.price-label {
font-size: 0.75rem;
color: #64748b;
text-transform: uppercase;
font-weight: 600;
}
.price-value {
font-size: 1.25rem;
color: #e2e8f0;
font-weight: 700;
font-family: 'Courier New', monospace;
}
.confirm-prices .bi {
font-size: 1.5rem;
color: #6366f1;
}
.confirm-pl {
text-align: center;
padding: 1.5rem;
border-radius: 0.5rem;
}
.confirm-pl.profit {
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.3);
}
.confirm-pl.loss {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
}
.pl-label {
font-size: 0.875rem;
color: #94a3b8;
margin-bottom: 0.5rem;
text-transform: uppercase;
font-weight: 600;
}
.pl-amount {
font-size: 2rem;
font-weight: 700;
font-family: 'Courier New', monospace;
margin-bottom: 0.25rem;
}
.confirm-pl.profit .pl-amount {
color: #10b981;
}
.confirm-pl.loss .pl-amount {
color: #ef4444;
}
.pl-percent {
font-size: 1rem;
opacity: 0.8;
}
.confirm-warning {
display: flex;
gap: 1rem;
padding: 1rem;
background: rgba(245, 158, 11, 0.1);
border: 1px solid rgba(245, 158, 11, 0.3);
border-radius: 0.5rem;
align-items: flex-start;
}
.confirm-warning .bi {
font-size: 1.25rem;
color: #f59e0b;
flex-shrink: 0;
}
.confirm-warning p {
margin: 0;
font-size: 0.875rem;
color: #cbd5e1;
line-height: 1.5;
}
.confirm-warning strong {
color: #f59e0b;
}
.notification {
position: fixed;
bottom: 2rem;
right: 2rem;
padding: 1rem 1.5rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 600;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
z-index: 9999;
animation: slideInRight 0.3s ease;
}
.notification.success {
background: rgba(16, 185, 129, 0.2);
border: 1px solid #10b981;
color: #10b981;
}
@@keyframes slideInRight {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@@media (max-width: 768px) {
.positions-grid {
grid-template-columns: 1fr;
}
.header-stats {
flex-direction: column;
}
.confirm-prices {
flex-direction: column;
gap: 1rem;
}
.confirm-prices .bi {
transform: rotate(90deg);
}
}
</style>
@code {
private List<Trade> activePositions = new();
private decimal totalValue = 0;
private decimal totalUnrealizedPL = 0;
private bool showCloseModal = false;
private Trade? positionToClose = null;
private bool showSuccessNotification = false;
protected override void OnInitialized()
{
LoadPositions();
BotService.OnTradeExecuted += HandleTradeExecuted;
BotService.OnPriceUpdated += HandlePriceUpdated;
BotService.OnStatusChanged += HandleStatusChanged;
}
private void LoadPositions()
{
activePositions = BotService.ActivePositions.Values.ToList();
CalculateTotals();
}
private void CalculateTotals()
{
totalValue = 0;
totalUnrealizedPL = 0;
foreach (var position in activePositions)
{
var currentPrice = GetCurrentPrice(position.Symbol);
var positionValue = position.Amount * currentPrice;
var pl = CalculateUnrealizedPL(position, currentPrice);
totalValue += positionValue;
totalUnrealizedPL += pl;
}
}
private decimal GetCurrentPrice(string symbol)
{
var latestPrice = BotService.GetLatestPrice(symbol);
return latestPrice?.Price ?? 0;
}
private decimal CalculateUnrealizedPL(Trade position, decimal currentPrice)
{
return (currentPrice - position.Price) * position.Amount;
}
private decimal CalculatePLPercentage(Trade position, decimal currentPrice)
{
if (position.Price == 0) return 0;
return ((currentPrice - position.Price) / position.Price) * 100;
}
private string FormatHoldingTime(TimeSpan time)
{
if (time.TotalDays >= 1)
return $"{(int)time.TotalDays}g {time.Hours}h";
else if (time.TotalHours >= 1)
return $"{(int)time.TotalHours}h {time.Minutes}m";
else
return $"{(int)time.TotalMinutes}m {time.Seconds}s";
}
private void ShowCloseConfirmation(Trade position)
{
positionToClose = position;
showCloseModal = true;
}
private void HideCloseConfirmation()
{
showCloseModal = false;
positionToClose = null;
}
private async Task ConfirmClosePosition()
{
if (positionToClose == null) return;
try
{
// Close position using TradingBotService public method
await BotService.ClosePositionManuallyAsync(positionToClose.Symbol);
LoggingService.LogInfo(
"Positions",
$"Posizione chiusa manualmente: {positionToClose.Symbol}",
$"Quantità: {positionToClose.Amount:F8}");
showSuccessNotification = true;
HideCloseConfirmation();
LoadPositions();
// Hide notification after 3 seconds
await Task.Delay(3000);
showSuccessNotification = false;
StateHasChanged();
}
catch (Exception ex)
{
LoggingService.LogError("Positions", $"Errore chiusura posizione: {ex.Message}");
}
}
private void HandleTradeExecuted(Trade trade)
{
LoadPositions();
InvokeAsync(StateHasChanged);
}
private void HandlePriceUpdated(string symbol, MarketPrice price)
{
if (activePositions.Any(p => p.Symbol == symbol))
{
CalculateTotals();
InvokeAsync(StateHasChanged);
}
}
private void HandleStatusChanged()
{
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
BotService.OnTradeExecuted -= HandleTradeExecuted;
BotService.OnPriceUpdated -= HandlePriceUpdated;
BotService.OnStatusChanged -= HandleStatusChanged;
}
}

View 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;
}
}