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:
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user