Aggiunta Bootstrap 5.3.3 (CSS, JS, RTL, mappe) al progetto

Sono stati aggiunti tutti i file principali di Bootstrap 5.3.3, inclusi CSS, JavaScript (bundle, ESM, UMD, minificati), versioni RTL, utility, reboot, griglia e relative mappe delle sorgenti. Questi file abilitano un sistema di design moderno, responsive e accessibile, con supporto per layout LTR e RTL, debugging avanzato tramite source map e tutte le funzionalità di Bootstrap per lo sviluppo dell’interfaccia utente. Nessuna modifica ai file esistenti.
This commit is contained in:
2025-12-12 23:27:28 +01:00
parent d50cb1f7b4
commit d25b4443c0
103 changed files with 69677 additions and 0 deletions

View File

@@ -0,0 +1,341 @@
@page "/assets"
@using TradingBot.Models
@using TradingBot.Services
@inject TradingBotService BotService
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Asset - TradingBot</PageTitle>
<div class="assets-page">
<div class="page-header">
<div>
<h1>Gestione Asset</h1>
<p class="subtitle">Visualizza, configura e assegna strategie ai tuoi asset di trading</p>
</div>
<div class="header-controls">
<div class="view-toggle">
<button class="toggle-btn @(viewMode == "grid" ? "active" : "")" @onclick="@(() => viewMode = "grid")">
<span class="bi bi-grid-3x3-gap"></span>
</button>
<button class="toggle-btn @(viewMode == "list" ? "active" : "")" @onclick="@(() => viewMode = "list")">
<span class="bi bi-list-ul"></span>
</button>
</div>
<select class="filter-select" @bind="filterStatus">
<option value="all">Tutti gli Asset</option>
<option value="active">Solo Attivi</option>
<option value="inactive">Solo Inattivi</option>
</select>
</div>
</div>
<!-- Summary Stats -->
<div class="assets-summary">
<div class="summary-stat">
<div class="stat-icon">
<span class="bi bi-coin"></span>
</div>
<div class="stat-content">
<span class="stat-label">Totale Asset</span>
<span class="stat-value">@totalAssets</span>
</div>
</div>
<div class="summary-stat success">
<div class="stat-icon">
<span class="bi bi-check-circle"></span>
</div>
<div class="stat-content">
<span class="stat-label">Asset Attivi</span>
<span class="stat-value">@activeAssets</span>
</div>
</div>
<div class="summary-stat warning">
<div class="stat-icon">
<span class="bi bi-diagram-3"></span>
</div>
<div class="stat-content">
<span class="stat-label">Strategie Assegnate</span>
<span class="stat-value">@assignedStrategies</span>
</div>
</div>
<div class="summary-stat info">
<div class="stat-icon">
<span class="bi bi-currency-dollar"></span>
</div>
<div class="stat-content">
<span class="stat-label">Valore Totale</span>
<span class="stat-value">$@totalValue.ToString("N0")</span>
</div>
</div>
</div>
<!-- Assets Grid/List -->
@if (viewMode == "grid")
{
<div class="assets-grid">
@foreach (var config in GetFilteredAssets())
{
var price = BotService.GetLatestPrice(config.Symbol);
var stats = BotService.AssetStatistics.TryGetValue(config.Symbol, out var s) ? s : null;
<div class="asset-card @(config.IsEnabled ? "enabled" : "disabled")">
<div class="asset-card-header">
<div class="asset-info">
<div class="asset-icon">@config.Symbol.Substring(0, 1)</div>
<div class="asset-title">
<h3>@config.Name</h3>
<span class="asset-symbol">@config.Symbol</span>
</div>
</div>
<label class="toggle-switch">
<input type="checkbox"
checked="@config.IsEnabled"
@onchange="@((e) => ToggleAsset(config.Symbol, (bool)e.Value!))" />
<span class="toggle-slider"></span>
</label>
</div>
<div class="asset-card-body">
@if (price != null)
{
<div class="price-section">
<div class="current-price">$@price.Price.ToString("N2")</div>
<div class="price-change @(price.Change24h >= 0 ? "positive" : "negative")">
<span class="bi @(price.Change24h >= 0 ? "bi-arrow-up" : "bi-arrow-down")"></span>
@Math.Abs(price.Change24h).ToString("F2")% (24h)
</div>
</div>
}
<div class="asset-metrics">
<div class="metric">
<span class="metric-label">Holdings</span>
<span class="metric-value">@config.CurrentHoldings.ToString("F6")</span>
</div>
<div class="metric">
<span class="metric-label">Valore</span>
<span class="metric-value">$@((config.CurrentBalance + config.CurrentHoldings * (price?.Price ?? 0)).ToString("N2"))</span>
</div>
<div class="metric">
<span class="metric-label">Profitto</span>
<span class="metric-value @(config.TotalProfit >= 0 ? "profit" : "loss")">
$@config.TotalProfit.ToString("N2")
</span>
</div>
<div class="metric">
<span class="metric-label">Trades</span>
<span class="metric-value">@(stats?.TotalTrades ?? 0)</span>
</div>
</div>
<div class="strategy-section">
<label class="strategy-label">Strategia Assegnata</label>
<select class="strategy-select"
value="@config.StrategyName"
@onchange="@((e) => AssignStrategy(config.Symbol, e.Value?.ToString() ?? ""))">
<option value="">Nessuna strategia</option>
<option value="RSI + MACD Cross">RSI + MACD Cross</option>
<option value="Media Mobile Semplice">Media Mobile Semplice</option>
<option value="Scalping Veloce">Scalping Veloce</option>
<option value="Trend Following">Trend Following</option>
<option value="Mean Reversion">Mean Reversion</option>
<option value="Conservative">Conservative</option>
</select>
</div>
</div>
<div class="asset-card-footer">
<button class="btn-secondary btn-sm" @onclick="@(() => OpenAssetDetails(config.Symbol))">
<span class="bi bi-gear"></span>
Configura
</button>
<button class="btn-secondary btn-sm" @onclick="@(() => ViewChart(config.Symbol))">
<span class="bi bi-graph-up"></span>
Grafico
</button>
</div>
</div>
}
</div>
}
else
{
<!-- List View -->
<div class="assets-table">
<div class="table-header">
<div class="th">Asset</div>
<div class="th">Prezzo</div>
<div class="th">Var. 24h</div>
<div class="th">Holdings</div>
<div class="th">Valore</div>
<div class="th">Profitto</div>
<div class="th">Strategia</div>
<div class="th">Stato</div>
<div class="th">Azioni</div>
</div>
@foreach (var config in GetFilteredAssets())
{
var price = BotService.GetLatestPrice(config.Symbol);
var stats = BotService.AssetStatistics.TryGetValue(config.Symbol, out var s) ? s : null;
<div class="table-row @(config.IsEnabled ? "enabled" : "disabled")">
<div class="cell-asset">
<div class="asset-icon-small">@config.Symbol.Substring(0, 1)</div>
<div>
<div class="asset-name">@config.Name</div>
<div class="asset-symbol-small">@config.Symbol</div>
</div>
</div>
<div class="cell">
@if (price != null)
{
<span class="price-value">$@price.Price.ToString("N2")</span>
}
else
{
<span class="text-muted">-</span>
}
</div>
<div class="cell">
@if (price != null)
{
<span class="change-badge @(price.Change24h >= 0 ? "positive" : "negative")">
@(price.Change24h >= 0 ? "+" : "")@price.Change24h.ToString("F2")%
</span>
}
else
{
<span class="text-muted">-</span>
}
</div>
<div class="cell">
<span class="mono-value">@config.CurrentHoldings.ToString("F6")</span>
</div>
<div class="cell">
<span class="mono-value">$@((config.CurrentBalance + config.CurrentHoldings * (price?.Price ?? 0)).ToString("N2"))</span>
</div>
<div class="cell">
<span class="mono-value @(config.TotalProfit >= 0 ? "profit" : "loss")">
$@config.TotalProfit.ToString("N2")
</span>
</div>
<div class="cell-strategy">
<select class="strategy-select-small"
value="@config.StrategyName"
@onchange="@((e) => AssignStrategy(config.Symbol, e.Value?.ToString() ?? ""))">
<option value="">Nessuna</option>
<option value="RSI + MACD Cross">RSI + MACD</option>
<option value="Media Mobile Semplice">SMA</option>
<option value="Scalping Veloce">Scalping</option>
<option value="Trend Following">Trend</option>
<option value="Mean Reversion">Mean Rev.</option>
<option value="Conservative">Conserv.</option>
</select>
</div>
<div class="cell">
<label class="toggle-switch-small">
<input type="checkbox"
checked="@config.IsEnabled"
@onchange="@((e) => ToggleAsset(config.Symbol, (bool)e.Value!))" />
<span class="toggle-slider-small"></span>
</label>
</div>
<div class="cell-actions">
<button class="btn-icon-small" title="Configura" @onclick="@(() => OpenAssetDetails(config.Symbol))">
<span class="bi bi-gear"></span>
</button>
<button class="btn-icon-small" title="Grafico" @onclick="@(() => ViewChart(config.Symbol))">
<span class="bi bi-graph-up"></span>
</button>
</div>
</div>
}
</div>
}
</div>
@code {
private string viewMode = "grid";
private string filterStatus = "all";
private int totalAssets = 0;
private int activeAssets = 0;
private int assignedStrategies = 0;
private decimal totalValue = 0;
protected override void OnInitialized()
{
BotService.OnStatusChanged += HandleUpdate;
BotService.OnTradeExecuted += HandleTradeExecuted;
BotService.OnPriceUpdated += HandlePriceUpdate;
RefreshData();
}
private void RefreshData()
{
totalAssets = BotService.AssetConfigurations.Count;
activeAssets = BotService.AssetConfigurations.Values.Count(c => c.IsEnabled);
assignedStrategies = BotService.AssetConfigurations.Values.Count(c => !string.IsNullOrEmpty(c.StrategyName));
totalValue = BotService.AssetConfigurations.Values.Sum(c =>
{
var price = BotService.GetLatestPrice(c.Symbol);
return c.CurrentBalance + (c.CurrentHoldings * (price?.Price ?? 0));
});
StateHasChanged();
}
private IEnumerable<AssetConfiguration> GetFilteredAssets()
{
var assets = BotService.AssetConfigurations.Values.OrderBy(c => c.Symbol);
return filterStatus switch
{
"active" => assets.Where(c => c.IsEnabled),
"inactive" => assets.Where(c => !c.IsEnabled),
_ => assets
};
}
private void ToggleAsset(string symbol, bool enabled)
{
BotService.ToggleAsset(symbol, enabled);
RefreshData();
}
private void AssignStrategy(string symbol, string strategyName)
{
if (BotService.AssetConfigurations.TryGetValue(symbol, out var config))
{
config.StrategyName = strategyName;
RefreshData();
}
}
private void OpenAssetDetails(string symbol)
{
// TODO: Open modal or navigate to asset detail page
}
private void ViewChart(string symbol)
{
var navManager = Navigation;
navManager?.NavigateTo($"/market?symbol={symbol}");
}
private void HandleUpdate() => InvokeAsync(RefreshData);
private void HandleTradeExecuted(Trade trade) => InvokeAsync(RefreshData);
private void HandlePriceUpdate(string symbol, MarketPrice price) => InvokeAsync(RefreshData);
[Inject] private NavigationManager? Navigation { get; set; }
public void Dispose()
{
BotService.OnStatusChanged -= HandleUpdate;
BotService.OnTradeExecuted -= HandleTradeExecuted;
BotService.OnPriceUpdated -= HandlePriceUpdate;
}
}

View File

@@ -0,0 +1,597 @@
/* Assets Page */
.assets-page {
display: flex;
flex-direction: column;
gap: 2rem;
}
/* Page Header */
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.page-header h1 {
margin: 0;
font-size: 2rem;
font-weight: 700;
color: white;
}
.subtitle {
margin: 0.5rem 0 0 0;
color: #94a3b8;
font-size: 0.875rem;
}
.header-controls {
display: flex;
gap: 1rem;
align-items: center;
}
/* View Toggle */
.view-toggle {
display: flex;
gap: 0.25rem;
background: #1a1f3a;
border-radius: 0.5rem;
padding: 0.25rem;
}
.toggle-btn {
padding: 0.5rem 0.75rem;
border: none;
background: transparent;
color: #64748b;
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.2s ease;
font-size: 1rem;
}
.toggle-btn:hover {
color: #cbd5e1;
}
.toggle-btn.active {
background: #6366f1;
color: white;
}
.filter-select {
padding: 0.625rem 1rem;
border-radius: 0.5rem;
border: 1px solid #334155;
background: #1a1f3a;
color: white;
font-size: 0.875rem;
font-weight: 600;
}
/* Assets Summary */
.assets-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.summary-stat {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.25rem;
display: flex;
align-items: center;
gap: 1rem;
}
.summary-stat .stat-icon {
width: 3rem;
height: 3rem;
border-radius: 0.625rem;
background: rgba(99, 102, 241, 0.1);
color: #6366f1;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
}
.summary-stat.success .stat-icon {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.summary-stat.warning .stat-icon {
background: rgba(245, 158, 11, 0.1);
color: #f59e0b;
}
.summary-stat.info .stat-icon {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
.stat-content {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.stat-label {
font-size: 0.75rem;
color: #64748b;
font-weight: 600;
text-transform: uppercase;
}
.stat-value {
font-size: 1.5rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
}
/* Assets Grid */
.assets-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
}
.asset-card {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
overflow: hidden;
transition: all 0.3s ease;
}
.asset-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.4);
border-color: #334155;
}
.asset-card.enabled {
border-color: rgba(99, 102, 241, 0.3);
}
.asset-card.disabled {
opacity: 0.6;
}
.asset-card-header {
padding: 1.25rem;
background: #1a1f3a;
border-bottom: 1px solid #1e293b;
display: flex;
justify-content: space-between;
align-items: center;
}
.asset-info {
display: flex;
align-items: center;
gap: 0.75rem;
}
.asset-icon {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.5rem;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
font-weight: 700;
color: white;
}
.asset-title h3 {
margin: 0;
font-size: 1rem;
font-weight: 700;
color: white;
}
.asset-symbol {
font-size: 0.75rem;
color: #64748b;
font-family: 'Courier New', monospace;
}
/* Toggle Switch */
.toggle-switch {
position: relative;
display: inline-block;
width: 3rem;
height: 1.5rem;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #334155;
transition: 0.3s;
border-radius: 1.5rem;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 1.125rem;
width: 1.125rem;
left: 0.1875rem;
bottom: 0.1875rem;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
.toggle-switch input:checked + .toggle-slider {
background-color: #6366f1;
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(1.5rem);
}
/* Asset Card Body */
.asset-card-body {
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.price-section {
display: flex;
align-items: baseline;
gap: 0.75rem;
}
.current-price {
font-size: 1.75rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
}
.price-change {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.875rem;
font-weight: 600;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
}
.price-change.positive {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.price-change.negative {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
/* Asset Metrics */
.asset-metrics {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
.metric {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.metric-label {
font-size: 0.625rem;
color: #64748b;
text-transform: uppercase;
font-weight: 600;
}
.metric-value {
font-size: 0.875rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
}
.metric-value.profit {
color: #10b981;
}
.metric-value.loss {
color: #ef4444;
}
/* Strategy Section */
.strategy-section {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.strategy-label {
font-size: 0.75rem;
color: #64748b;
font-weight: 600;
text-transform: uppercase;
}
.strategy-select {
padding: 0.625rem;
border-radius: 0.5rem;
border: 1px solid #334155;
background: #1a1f3a;
color: white;
font-size: 0.875rem;
font-weight: 600;
}
.strategy-select:focus {
outline: 2px solid #6366f1;
outline-offset: 2px;
}
/* Asset Card Footer */
.asset-card-footer {
padding: 1rem 1.25rem;
background: #0a0e27;
border-top: 1px solid #1e293b;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
.btn-sm {
padding: 0.5rem 1rem;
font-size: 0.75rem;
}
/* Assets Table (List View) */
.assets-table {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
overflow: hidden;
}
.table-header {
display: grid;
grid-template-columns: 2fr 1.5fr 1fr 1.5fr 1.5fr 1.5fr 2fr 1fr 1.5fr;
gap: 1rem;
padding: 1rem 1.5rem;
background: #1a1f3a;
border-bottom: 1px solid #1e293b;
font-size: 0.75rem;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
}
.table-row {
display: grid;
grid-template-columns: 2fr 1.5fr 1fr 1.5fr 1.5fr 1.5fr 2fr 1fr 1.5fr;
gap: 1rem;
padding: 1rem 1.5rem;
border-bottom: 1px solid #1e293b;
align-items: center;
font-size: 0.875rem;
color: #cbd5e1;
transition: background 0.2s ease;
}
.table-row:hover {
background: #1a1f3a;
}
.table-row:last-child {
border-bottom: none;
}
.table-row.disabled {
opacity: 0.5;
}
.cell-asset {
display: flex;
align-items: center;
gap: 0.75rem;
}
.asset-icon-small {
width: 2rem;
height: 2rem;
border-radius: 0.375rem;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
font-weight: 700;
color: white;
}
.asset-name {
font-weight: 600;
color: white;
}
.asset-symbol-small {
font-size: 0.75rem;
color: #64748b;
font-family: 'Courier New', monospace;
}
.cell {
display: flex;
align-items: center;
}
.price-value,
.mono-value {
font-family: 'Courier New', monospace;
font-weight: 600;
}
.change-badge {
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 600;
}
.change-badge.positive {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.change-badge.negative {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.cell-strategy {
display: flex;
}
.strategy-select-small {
width: 100%;
padding: 0.375rem 0.5rem;
border-radius: 0.375rem;
border: 1px solid #334155;
background: #1a1f3a;
color: white;
font-size: 0.75rem;
font-weight: 600;
}
/* Toggle Switch Small */
.toggle-switch-small {
position: relative;
display: inline-block;
width: 2.5rem;
height: 1.25rem;
}
.toggle-switch-small input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider-small {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #334155;
transition: 0.3s;
border-radius: 1.25rem;
}
.toggle-slider-small:before {
position: absolute;
content: "";
height: 0.875rem;
width: 0.875rem;
left: 0.1875rem;
bottom: 0.1875rem;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
.toggle-switch-small input:checked + .toggle-slider-small {
background-color: #6366f1;
}
.toggle-switch-small input:checked + .toggle-slider-small:before {
transform: translateX(1.25rem);
}
.cell-actions {
display: flex;
gap: 0.5rem;
}
.btn-icon-small {
width: 2rem;
height: 2rem;
border-radius: 0.375rem;
border: none;
background: #1a1f3a;
color: #94a3b8;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.btn-icon-small:hover {
background: #1e293b;
color: #cbd5e1;
}
/* Responsive */
@media (max-width: 1200px) {
.table-header, .table-row {
grid-template-columns: 2fr 1fr 1fr 1fr 1.5fr 1fr 1fr;
}
/* Hide some columns on smaller screens */
.table-header div:nth-child(4),
.table-row div:nth-child(4),
.table-header div:nth-child(6),
.table-row div:nth-child(6) {
display: none;
}
}
@media (max-width: 768px) {
.assets-grid {
grid-template-columns: 1fr;
}
.header-controls {
flex-direction: column;
width: 100%;
}
.filter-select {
width: 100%;
}
.assets-summary {
grid-template-columns: repeat(2, 1fr);
}
.assets-table {
overflow-x: auto;
}
}

View File

@@ -0,0 +1,185 @@
@page "/"
@using TradingBot.Models
@using TradingBot.Services
@inject TradingBotService BotService
@inject NavigationManager Navigation
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Dashboard - TradingBot</PageTitle>
<div class="dashboard-page">
<div class="page-header">
<div>
<h1>Dashboard</h1>
<p class="subtitle">Panoramica completa delle performance e attività di trading</p>
</div>
</div>
<!-- Summary Cards -->
<div class="summary-grid">
<div class="summary-card primary">
<div class="card-icon">
<span class="bi bi-wallet2"></span>
</div>
<div class="card-content">
<div class="card-label">Valore Portfolio</div>
<div class="card-value">$@portfolioStats.TotalBalance.ToString("N2")</div>
<div class="card-change @(portfolioStats.TotalProfitPercentage >= 0 ? "positive" : "negative")">
<span class="bi @(portfolioStats.TotalProfitPercentage >= 0 ? "bi-arrow-up" : "bi-arrow-down")"></span>
@Math.Abs(portfolioStats.TotalProfitPercentage).ToString("F2")%
</div>
</div>
</div>
<div class="summary-card">
<div class="card-icon success">
<span class="bi bi-graph-up-arrow"></span>
</div>
<div class="card-content">
<div class="card-label">Profitto Totale</div>
<div class="card-value @(portfolioStats.TotalProfit >= 0 ? "profit" : "loss")">
$@portfolioStats.TotalProfit.ToString("N2")
</div>
<div class="card-meta">Da $@portfolioStats.InitialBalance.ToString("N2")</div>
</div>
</div>
<div class="summary-card">
<div class="card-icon info">
<span class="bi bi-arrow-left-right"></span>
</div>
<div class="card-content">
<div class="card-label">Operazioni Totali</div>
<div class="card-value">@portfolioStats.TotalTrades</div>
<div class="card-meta">Win Rate: @portfolioStats.WinRate.ToString("F1")%</div>
</div>
</div>
<div class="summary-card">
<div class="card-icon warning">
<span class="bi bi-currency-exchange"></span>
</div>
<div class="card-content">
<div class="card-label">Asset Attivi</div>
<div class="card-value">@portfolioStats.ActiveAssets/@portfolioStats.TotalAssets</div>
<div class="card-meta">In trading</div>
</div>
</div>
</div>
<!-- Active Assets -->
<div class="section">
<div class="section-header">
<h2>Asset Attivi</h2>
<a href="/trading" class="btn-link">Vedi Tutti <span class="bi bi-arrow-right"></span></a>
</div>
<div class="assets-quick-grid">
@foreach (var config in BotService.AssetConfigurations.Values.Where(c => c.IsEnabled).Take(6))
{
var price = BotService.GetLatestPrice(config.Symbol);
<div class="asset-quick-card">
<div class="asset-header">
<span class="asset-symbol">@config.Symbol</span>
@if (price != null)
{
<span class="asset-change @(price.Change24h >= 0 ? "positive" : "negative")">
@price.Change24h.ToString("F2")%
</span>
}
</div>
<div class="asset-price">$@(price?.Price.ToString("N2") ?? "Loading...")</div>
<div class="asset-profit @(config.TotalProfit >= 0 ? "profit" : "loss")">
$@config.TotalProfit.ToString("N2")
</div>
</div>
}
</div>
</div>
<!-- Recent Activity -->
<div class="section">
<div class="section-header">
<h2>Attività Recente</h2>
<a href="/trading" class="btn-link">Vedi Storico <span class="bi bi-arrow-right"></span></a>
</div>
@if (BotService.Trades.Count == 0)
{
<div class="empty-state">
<span class="bi bi-inbox"></span>
<p>Nessuna operazione ancora</p>
</div>
}
else
{
<div class="activity-list">
@foreach (var trade in BotService.Trades.Take(8))
{
<div class="activity-item">
<div class="activity-icon @(trade.Type == TradeType.Buy ? "buy" : "sell")">
<span class="bi @(trade.Type == TradeType.Buy ? "bi-arrow-down-circle-fill" : "bi-arrow-up-circle-fill")"></span>
</div>
<div class="activity-content">
<div class="activity-main">
<span class="activity-type">@(trade.Type == TradeType.Buy ? "ACQUISTO" : "VENDITA")</span>
<span class="activity-symbol">@trade.Symbol</span>
@if (trade.IsBot)
{
<span class="bot-badge">
<span class="bi bi-robot"></span> BOT
</span>
}
</div>
<div class="activity-details">
<span>@trade.Amount.ToString("F6") &#64; $@trade.Price.ToString("N2")</span>
<span class="separator">•</span>
<span>@trade.Timestamp.ToLocalTime().ToString("HH:mm:ss")</span>
</div>
</div>
<div class="activity-value">
$@((trade.Amount * trade.Price).ToString("N2"))
</div>
</div>
}
</div>
}
</div>
</div>
@code {
private PortfolioStatistics portfolioStats = new();
protected override void OnInitialized()
{
BotService.OnStatusChanged += HandleUpdate;
BotService.OnTradeExecuted += HandleTradeExecuted;
BotService.OnPriceUpdated += HandlePriceUpdate;
if (!BotService.Status.IsRunning)
{
BotService.Start();
}
RefreshData();
}
private void RefreshData()
{
portfolioStats = BotService.GetPortfolioStatistics();
StateHasChanged();
}
private void HandleUpdate() => InvokeAsync(RefreshData);
private void HandleTradeExecuted(Trade trade) => InvokeAsync(RefreshData);
private void HandlePriceUpdate(string symbol, MarketPrice price) => InvokeAsync(RefreshData);
public void Dispose()
{
BotService.OnStatusChanged -= HandleUpdate;
BotService.OnTradeExecuted -= HandleTradeExecuted;
BotService.OnPriceUpdated -= HandlePriceUpdate;
}
}

View File

@@ -0,0 +1,362 @@
/* Dashboard Page */
.dashboard-page {
display: flex;
flex-direction: column;
gap: 2rem;
}
.page-header h1 {
margin: 0;
font-size: 2rem;
font-weight: 700;
color: white;
}
.subtitle {
margin: 0.5rem 0 0 0;
color: #94a3b8;
font-size: 0.875rem;
}
/* Summary Grid */
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.summary-card {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
display: flex;
gap: 1rem;
transition: all 0.3s ease;
}
.summary-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.4);
}
.summary-card.primary {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
border-color: #7c3aed;
}
.card-icon {
width: 3rem;
height: 3rem;
border-radius: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
background: rgba(255, 255, 255, 0.1);
color: white;
}
.card-icon.success {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
}
.card-icon.info {
background: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.card-icon.warning {
background: rgba(245, 158, 11, 0.2);
color: #f59e0b;
}
.card-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.card-label {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.7);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.summary-card:not(.primary) .card-label {
color: #94a3b8;
}
.card-value {
font-size: 1.875rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
line-height: 1;
}
.card-value.profit {
color: #10b981;
}
.card-value.loss {
color: #ef4444;
}
.card-change {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.875rem;
font-weight: 600;
}
.card-change.positive {
color: rgba(16, 185, 129, 0.9);
}
.card-change.negative {
color: rgba(239, 68, 68, 0.9);
}
.card-meta {
font-size: 0.75rem;
color: #64748b;
}
/* Section */
.section {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.section-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
color: white;
}
.btn-link {
display: flex;
align-items: center;
gap: 0.5rem;
color: #6366f1;
text-decoration: none;
font-size: 0.875rem;
font-weight: 600;
transition: all 0.2s ease;
}
.btn-link:hover {
gap: 0.75rem;
}
/* Assets Quick Grid */
.assets-quick-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
.asset-quick-card {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.25rem;
transition: all 0.3s ease;
}
.asset-quick-card:hover {
transform: translateY(-2px);
border-color: #6366f1;
}
.asset-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.asset-symbol {
font-size: 0.875rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
}
.asset-change {
font-size: 0.75rem;
font-weight: 600;
}
.asset-change.positive {
color: #10b981;
}
.asset-change.negative {
color: #ef4444;
}
.asset-price {
font-size: 1.5rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
margin-bottom: 0.5rem;
}
.asset-profit {
font-size: 0.875rem;
font-weight: 600;
font-family: 'Courier New', monospace;
}
.asset-profit.profit {
color: #10b981;
}
.asset-profit.loss {
color: #ef4444;
}
/* Activity List */
.activity-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.activity-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
transition: all 0.2s ease;
}
.activity-item:hover {
background: #1a1f3a;
}
.activity-icon {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
}
.activity-icon.buy {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.activity-icon.sell {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.activity-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.activity-main {
display: flex;
align-items: center;
gap: 0.5rem;
}
.activity-type {
font-size: 0.875rem;
font-weight: 600;
color: white;
}
.activity-symbol {
font-size: 0.875rem;
font-family: 'Courier New', monospace;
color: #94a3b8;
}
.bot-badge {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.375rem;
background: rgba(99, 102, 241, 0.2);
color: #6366f1;
border-radius: 0.25rem;
font-size: 0.625rem;
font-weight: 700;
}
.activity-details {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
color: #64748b;
}
.separator {
color: #334155;
}
.activity-value {
font-size: 1rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 3rem 1rem;
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
color: #64748b;
}
.empty-state .bi {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-state p {
margin: 0.5rem 0;
}
/* Responsive */
@media (max-width: 768px) {
.summary-grid {
grid-template-columns: 1fr;
}
.assets-quick-grid {
grid-template-columns: repeat(2, 1fr);
}
}

View File

@@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}

View File

@@ -0,0 +1,238 @@
@page "/market"
@using TradingBot.Models
@using TradingBot.Services
@using TradingBot.Components.Shared
@inject TradingBotService BotService
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Analisi Mercato - TradingBot</PageTitle>
<div class="market-page">
<div class="page-header">
<div>
<h1>Analisi Mercato</h1>
<p class="subtitle">Monitora le tendenze di mercato e gli indicatori tecnici in tempo reale</p>
</div>
<select class="asset-selector" @bind="selectedSymbol" @bind:after="OnAssetChanged">
@foreach (var symbol in BotService.AssetConfigurations.Keys.OrderBy(s => s))
{
<option value="@symbol">@symbol - @BotService.AssetConfigurations[symbol].Name</option>
}
</select>
</div>
@if (selectedConfig != null && currentPrice != null)
{
<div class="market-overview">
<div class="price-card">
<div class="price-header">
<div class="asset-info">
<span class="asset-icon">@selectedSymbol.Substring(0, 1)</span>
<div>
<h2>@selectedConfig.Name</h2>
<span class="asset-symbol">@selectedSymbol</span>
</div>
</div>
<div class="price-main">
<div class="current-price">$@currentPrice.Price.ToString("N2")</div>
<div class="price-change @(currentPrice.Change24h >= 0 ? "positive" : "negative")">
<span class="bi @(currentPrice.Change24h >= 0 ? "bi-arrow-up" : "bi-arrow-down")"></span>
@Math.Abs(currentPrice.Change24h).ToString("F2")% (24h)
</div>
</div>
</div>
<div class="price-stats">
<div class="stat">
<span class="stat-label">Volume 24h</span>
<span class="stat-value">$@currentPrice.Volume24h.ToString("N0")</span>
</div>
<div class="stat">
<span class="stat-label">Holdings</span>
<span class="stat-value">@selectedConfig.CurrentHoldings.ToString("F6")</span>
</div>
<div class="stat">
<span class="stat-label">Valore Posizione</span>
<span class="stat-value">$@((selectedConfig.CurrentHoldings * currentPrice.Price).ToString("N2"))</span>
</div>
</div>
</div>
@if (currentIndicators != null)
{
<div class="indicators-grid">
<div class="indicator-card">
<div class="indicator-header">
<span class="indicator-icon">
<span class="bi bi-activity"></span>
</span>
<span class="indicator-name">RSI (14)</span>
</div>
<div class="indicator-value @GetRSIClass()">
@currentIndicators.RSI.ToString("F2")
</div>
<div class="indicator-status @GetRSIClass()">
@GetRSIStatus()
</div>
</div>
<div class="indicator-card">
<div class="indicator-header">
<span class="indicator-icon">
<span class="bi bi-graph-up"></span>
</span>
<span class="indicator-name">MACD</span>
</div>
<div class="indicator-value">
@currentIndicators.MACD.ToString("F2")
</div>
<div class="indicator-status">
Signal: @currentIndicators.Signal.ToString("F2")
</div>
</div>
<div class="indicator-card">
<div class="indicator-header">
<span class="indicator-icon">
<span class="bi bi-graph-down"></span>
</span>
<span class="indicator-name">Histogram</span>
</div>
<div class="indicator-value @(currentIndicators.Histogram >= 0 ? "positive" : "negative")">
@currentIndicators.Histogram.ToString("F4")
</div>
<div class="indicator-status">
@(currentIndicators.Histogram >= 0 ? "Bullish" : "Bearish")
</div>
</div>
<div class="indicator-card">
<div class="indicator-header">
<span class="indicator-icon">
<span class="bi bi-bezier2"></span>
</span>
<span class="indicator-name">EMA</span>
</div>
<div class="indicator-value">
@currentIndicators.EMA12.ToString("F2")
</div>
<div class="indicator-status">
EMA26: @currentIndicators.EMA26.ToString("F2")
</div>
</div>
</div>
}
</div>
<div class="chart-section">
<div class="chart-header">
<h3>Andamento Prezzi</h3>
<div class="chart-controls">
<button class="time-btn active">1H</button>
<button class="time-btn">4H</button>
<button class="time-btn">1D</button>
<button class="time-btn">1W</button>
</div>
</div>
<div class="chart-container">
<AdvancedChart
PriceData="@GetPriceList(selectedSymbol)"
Color="#6366f1"
Indicators="@currentIndicators" />
</div>
</div>
}
</div>
@code {
[SupplyParameterFromQuery(Name = "symbol")]
public string? QuerySymbol { get; set; }
private string selectedSymbol = "BTC";
private AssetConfiguration? selectedConfig => BotService.AssetConfigurations.TryGetValue(selectedSymbol, out var c) ? c : null;
private MarketPrice? currentPrice => BotService.GetLatestPrice(selectedSymbol);
private TechnicalIndicators? currentIndicators;
protected override void OnInitialized()
{
// Set initial symbol from query string if available
if (!string.IsNullOrEmpty(QuerySymbol) && BotService.AssetConfigurations.ContainsKey(QuerySymbol))
{
selectedSymbol = QuerySymbol;
}
BotService.OnPriceUpdated += HandlePriceUpdate;
BotService.OnIndicatorsUpdated += HandleIndicatorsUpdate;
UpdateIndicators();
}
protected override void OnParametersSet()
{
// Update symbol if query parameter changes
if (!string.IsNullOrEmpty(QuerySymbol) &&
QuerySymbol != selectedSymbol &&
BotService.AssetConfigurations.ContainsKey(QuerySymbol))
{
selectedSymbol = QuerySymbol;
UpdateIndicators();
}
}
private void OnAssetChanged()
{
UpdateIndicators();
StateHasChanged();
}
private void UpdateIndicators()
{
currentIndicators = BotService.GetIndicators(selectedSymbol);
}
private List<decimal>? GetPriceList(string symbol)
{
var history = BotService.GetPriceHistory(symbol);
return history?.Select(p => p.Price).ToList();
}
private string GetRSIClass()
{
if (currentIndicators == null) return "neutral";
if (currentIndicators.RSI > 70) return "overbought";
if (currentIndicators.RSI < 30) return "oversold";
return "neutral";
}
private string GetRSIStatus()
{
if (currentIndicators == null) return "Neutral";
if (currentIndicators.RSI > 70) return "Overbought";
if (currentIndicators.RSI < 30) return "Oversold";
return "Neutral";
}
private void HandlePriceUpdate(string symbol, MarketPrice price)
{
if (symbol == selectedSymbol)
{
InvokeAsync(StateHasChanged);
}
}
private void HandleIndicatorsUpdate(string symbol, TechnicalIndicators indicators)
{
if (symbol == selectedSymbol)
{
currentIndicators = indicators;
InvokeAsync(StateHasChanged);
}
}
public void Dispose()
{
BotService.OnPriceUpdated -= HandlePriceUpdate;
BotService.OnIndicatorsUpdated -= HandleIndicatorsUpdate;
}
}

View File

@@ -0,0 +1,328 @@
/* Market Page */
.market-page {
display: flex;
flex-direction: column;
gap: 2rem;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.page-header h1 {
margin: 0;
font-size: 2rem;
font-weight: 700;
color: white;
}
.subtitle {
margin: 0.5rem 0 0 0;
color: #94a3b8;
font-size: 0.875rem;
}
.asset-selector {
padding: 0.75rem 1rem;
border-radius: 0.5rem;
border: 1px solid #334155;
background: #1e293b;
color: white;
font-size: 0.875rem;
font-weight: 600;
min-width: 250px;
}
/* Market Overview */
.market-overview {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.price-card {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 2rem;
}
.price-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.asset-info {
display: flex;
align-items: center;
gap: 1rem;
}
.asset-icon {
width: 3.5rem;
height: 3.5rem;
border-radius: 0.75rem;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: 700;
color: white;
}
.asset-info h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
color: white;
}
.asset-symbol {
font-size: 0.875rem;
color: #64748b;
font-family: 'Courier New', monospace;
}
.price-main {
text-align: right;
}
.current-price {
font-size: 2.5rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
line-height: 1;
margin-bottom: 0.5rem;
}
.price-change {
display: inline-flex;
align-items: center;
gap: 0.375rem;
font-size: 1rem;
font-weight: 600;
padding: 0.375rem 0.75rem;
border-radius: 0.5rem;
}
.price-change.positive {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.price-change.negative {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.price-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
}
.stat {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.stat-label {
font-size: 0.75rem;
color: #64748b;
text-transform: uppercase;
font-weight: 600;
}
.stat-value {
font-size: 1.25rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
}
/* Indicators Grid */
.indicators-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.indicator-card {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
transition: all 0.3s ease;
}
.indicator-card:hover {
transform: translateY(-2px);
border-color: #6366f1;
}
.indicator-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.indicator-icon {
width: 2rem;
height: 2rem;
border-radius: 0.5rem;
background: rgba(99, 102, 241, 0.1);
display: flex;
align-items: center;
justify-content: center;
color: #6366f1;
}
.indicator-name {
font-size: 0.875rem;
font-weight: 600;
color: #94a3b8;
}
.indicator-value {
font-size: 2rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
margin-bottom: 0.5rem;
}
.indicator-value.positive {
color: #10b981;
}
.indicator-value.negative {
color: #ef4444;
}
.indicator-value.overbought {
color: #ef4444;
}
.indicator-value.oversold {
color: #10b981;
}
.indicator-value.neutral {
color: #f59e0b;
}
.indicator-status {
font-size: 0.75rem;
color: #64748b;
font-weight: 600;
}
.indicator-status.overbought {
color: #ef4444;
}
.indicator-status.oversold {
color: #10b981;
}
/* Chart Section */
.chart-section {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.chart-header h3 {
margin: 0;
font-size: 1.25rem;
font-weight: 700;
color: white;
}
.chart-controls {
display: flex;
gap: 0.5rem;
}
.time-btn {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
border: 1px solid #334155;
background: transparent;
color: #94a3b8;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.time-btn:hover {
background: #1a1f3a;
border-color: #6366f1;
}
.time-btn.active {
background: #6366f1;
border-color: #6366f1;
color: white;
}
.chart-container {
height: 400px;
width: 100%;
}
/* Responsive */
@media (max-width: 1024px) {
.indicators-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.page-header {
flex-direction: column;
gap: 1rem;
}
.asset-selector {
width: 100%;
}
.price-header {
flex-direction: column;
gap: 1.5rem;
}
.price-main {
text-align: left;
}
.price-stats {
grid-template-columns: 1fr;
}
.indicators-grid {
grid-template-columns: 1fr;
}
.chart-container {
height: 300px;
}
}

View File

@@ -0,0 +1,5 @@
@page "/not-found"
@layout MainLayout
<h3>Not Found</h3>
<p>Sorry, the content you are looking for does not exist.</p>

View File

@@ -0,0 +1,170 @@
@page "/settings"
@using TradingBot.Services
@using TradingBot.Models
@inject SettingsService SettingsService
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Impostazioni - TradingBot</PageTitle>
<div class="settings-page">
<div class="page-header">
<h1>Impostazioni</h1>
<p class="subtitle">Configura le impostazioni globali del trading bot</p>
</div>
<div class="settings-section">
<h2>Generale</h2>
<div class="settings-group">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Modalità Simulazione</div>
<div class="setting-description">Utilizza dati simulati invece di dati reali di mercato</div>
</div>
<label class="toggle-switch">
<input type="checkbox" checked="@settings.SimulationMode" @onchange="(e) => UpdateSetting(nameof(AppSettings.SimulationMode), (bool)e.Value!)" disabled />
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Notifiche Desktop</div>
<div class="setting-description">Ricevi notifiche per operazioni importanti</div>
</div>
<label class="toggle-switch">
<input type="checkbox" checked="@settings.DesktopNotifications" @onchange="(e) => UpdateSetting(nameof(AppSettings.DesktopNotifications), (bool)e.Value!)" />
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
<div class="settings-section">
<h2>Trading</h2>
<div class="settings-group">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Auto-Start Bot</div>
<div class="setting-description">Avvia automaticamente il bot all'apertura dell'applicazione</div>
</div>
<label class="toggle-switch">
<input type="checkbox" checked="@settings.AutoStartBot" @onchange="(e) => UpdateSetting(nameof(AppSettings.AutoStartBot), (bool)e.Value!)" />
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Conferma Operazioni Manuali</div>
<div class="setting-description">Richiedi conferma prima di eseguire operazioni manuali</div>
</div>
<label class="toggle-switch">
<input type="checkbox" checked="@settings.ConfirmManualTrades" @onchange="(e) => UpdateSetting(nameof(AppSettings.ConfirmManualTrades), (bool)e.Value!)" />
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
<div class="settings-section">
<h2>Avanzate</h2>
<div class="settings-group">
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Intervallo Aggiornamento</div>
<div class="setting-description">Frequenza di aggiornamento dei dati di mercato</div>
</div>
<select class="setting-select" value="@settings.UpdateIntervalSeconds" @onchange="(e) => UpdateSetting(nameof(AppSettings.UpdateIntervalSeconds), int.Parse(e.Value!.ToString()!))">
<option value="2">2 secondi</option>
<option value="3">3 secondi</option>
<option value="5">5 secondi</option>
<option value="10">10 secondi</option>
</select>
</div>
<div class="setting-item">
<div class="setting-info">
<div class="setting-label">Log Level</div>
<div class="setting-description">Livello di dettaglio dei log di sistema</div>
</div>
<select class="setting-select" value="@settings.LogLevel" @onchange="(e) => UpdateSetting(nameof(AppSettings.LogLevel), e.Value!.ToString()!)">
<option value="Error">Error</option>
<option value="Warning">Warning</option>
<option value="Info">Info</option>
<option value="Debug">Debug</option>
</select>
</div>
</div>
</div>
<div class="settings-actions">
<button class="btn-secondary" @onclick="ResetToDefaults">
<span class="bi bi-arrow-counterclockwise"></span>
Reset Predefiniti
</button>
<button class="btn-primary" @onclick="SaveSettings">
<span class="bi bi-check-lg"></span>
Salva Modifiche
</button>
</div>
@if (showNotification)
{
<div class="notification success">
<span class="bi bi-check-circle-fill"></span>
Impostazioni salvate con successo!
</div>
}
</div>
@code {
private AppSettings settings = new();
private bool showNotification = false;
protected override void OnInitialized()
{
settings = SettingsService.GetSettings();
SettingsService.OnSettingsChanged += HandleSettingsChanged;
}
private void UpdateSetting<T>(string propertyName, T value)
{
SettingsService.UpdateSetting(propertyName, value);
settings = SettingsService.GetSettings();
ShowNotification();
}
private void SaveSettings()
{
SettingsService.UpdateSettings(settings);
ShowNotification();
}
private void ResetToDefaults()
{
SettingsService.ResetToDefaults();
settings = SettingsService.GetSettings();
ShowNotification();
}
private async void ShowNotification()
{
showNotification = true;
StateHasChanged();
await Task.Delay(3000);
showNotification = false;
StateHasChanged();
}
private void HandleSettingsChanged()
{
settings = SettingsService.GetSettings();
InvokeAsync(StateHasChanged);
}
public void Dispose()
{
SettingsService.OnSettingsChanged -= HandleSettingsChanged;
}
}

View File

@@ -0,0 +1,221 @@
/* Settings Page */
.settings-page {
max-width: 900px;
display: flex;
flex-direction: column;
gap: 2rem;
}
.page-header h1 {
margin: 0;
font-size: 2rem;
font-weight: 700;
color: white;
}
.subtitle {
margin: 0.5rem 0 0 0;
color: #94a3b8;
font-size: 0.875rem;
}
.settings-section {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 2rem;
}
.settings-section h2 {
margin: 0 0 1.5rem 0;
font-size: 1.25rem;
font-weight: 700;
color: white;
}
.settings-group {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 2rem;
}
.setting-info {
flex: 1;
}
.setting-label {
font-size: 0.938rem;
font-weight: 600;
color: white;
margin-bottom: 0.25rem;
}
.setting-description {
font-size: 0.875rem;
color: #64748b;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 3rem;
height: 1.5rem;
flex-shrink: 0;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #334155;
transition: 0.3s;
border-radius: 1.5rem;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 1.125rem;
width: 1.125rem;
left: 0.1875rem;
bottom: 0.1875rem;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
.toggle-switch input:checked + .toggle-slider {
background-color: #6366f1;
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(1.5rem);
}
.toggle-switch input:disabled + .toggle-slider {
opacity: 0.5;
cursor: not-allowed;
}
.setting-select {
padding: 0.625rem 1rem;
border-radius: 0.5rem;
border: 1px solid #334155;
background: #1a1f3a;
color: white;
font-size: 0.875rem;
font-weight: 600;
min-width: 150px;
}
.settings-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
}
.btn-primary, .btn-secondary {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-primary {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: white;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4);
}
.btn-secondary {
background: #1e293b;
color: #cbd5e1;
border: 1px solid #334155;
}
.btn-secondary:hover {
background: #334155;
border-color: #475569;
}
/* Notification */
.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 8px 16px rgba(0, 0, 0, 0.3);
animation: slide-in 0.3s ease;
z-index: 1000;
}
@keyframes slide-in {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.notification.success {
background: #064e3b;
border: 1px solid #065f46;
color: #6ee7b7;
}
.notification .bi {
font-size: 1.25rem;
}
/* Responsive */
@media (max-width: 768px) {
.setting-item {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.settings-actions {
flex-direction: column;
}
.btn-primary, .btn-secondary {
width: 100%;
justify-content: center;
}
}

View File

@@ -0,0 +1,460 @@
@page "/statistics"
@using TradingBot.Models
@using TradingBot.Services
@inject TradingBotService BotService
@inject NavigationManager Navigation
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Statistiche - TradingBot</PageTitle>
<div class="statistics-page">
<!-- Header -->
<header class="stats-header">
<div class="header-content">
<div class="page-title">
<h1><span class="bi bi-graph-up"></span> Statistiche Avanzate</h1>
<p class="subtitle">Analisi dettagliata delle performance e metriche di trading</p>
</div>
<div class="header-filters">
<select class="filter-select" @bind="selectedSymbol" @bind:after="OnSymbolChanged">
<option value="">Tutti gli Asset</option>
@foreach (var symbol in BotService.AssetConfigurations.Keys.OrderBy(s => s))
{
<option value="@symbol">@symbol</option>
}
</select>
</div>
</div>
</header>
<div class="stats-content">
@if (string.IsNullOrEmpty(selectedSymbol))
{
<!-- Portfolio Overview -->
<div class="overview-section">
<h2 class="section-title">
<span class="bi bi-pie-chart-fill"></span> Panoramica Portfolio
</h2>
<div class="stats-grid">
<div class="stat-card primary">
<div class="stat-header">
<span class="stat-icon"><span class="bi bi-wallet2"></span></span>
<span class="stat-label">Valore Totale</span>
</div>
<div class="stat-value">$@portfolioStats.TotalBalance.ToString("N2")</div>
<div class="stat-footer">
<span class="stat-change @(portfolioStats.TotalProfitPercentage >= 0 ? "positive" : "negative")">
<span class="bi @(portfolioStats.TotalProfitPercentage >= 0 ? "bi-arrow-up" : "bi-arrow-down")"></span>
@Math.Abs(portfolioStats.TotalProfitPercentage).ToString("F2")%
</span>
</div>
</div>
<div class="stat-card">
<div class="stat-header">
<span class="stat-icon success"><span class="bi bi-trophy"></span></span>
<span class="stat-label">Profitto Netto</span>
</div>
<div class="stat-value @(portfolioStats.TotalProfit >= 0 ? "profit" : "loss")">
$@portfolioStats.TotalProfit.ToString("N2")
</div>
<div class="stat-footer">
<span class="stat-meta">Da $@portfolioStats.InitialBalance.ToString("N2")</span>
</div>
</div>
<div class="stat-card">
<div class="stat-header">
<span class="stat-icon info"><span class="bi bi-arrow-left-right"></span></span>
<span class="stat-label">Totale Operazioni</span>
</div>
<div class="stat-value">@portfolioStats.TotalTrades</div>
<div class="stat-footer">
<span class="stat-meta">@portfolioStats.ActiveAssets asset attivi</span>
</div>
</div>
<div class="stat-card">
<div class="stat-header">
<span class="stat-icon warning"><span class="bi bi-percent"></span></span>
<span class="stat-label">Win Rate</span>
</div>
<div class="stat-value">@portfolioStats.WinRate.ToString("F1")%</div>
<div class="stat-footer">
<span class="stat-meta">Tasso di successo</span>
</div>
</div>
</div>
<!-- Best/Worst Performers -->
<div class="performers-section">
<div class="performer-card best">
<div class="performer-header">
<span class="bi bi-trophy-fill"></span>
<span>Miglior Performer</span>
</div>
@if (!string.IsNullOrEmpty(portfolioStats.BestPerformingAssetSymbol))
{
<div class="performer-content">
<div class="performer-symbol">@portfolioStats.BestPerformingAssetSymbol</div>
<div class="performer-value profit">+$@portfolioStats.BestPerformingAssetProfit.ToString("N2")</div>
</div>
}
else
{
<div class="empty-performer">Nessun dato</div>
}
</div>
<div class="performer-card worst">
<div class="performer-header">
<span class="bi bi-graph-down"></span>
<span>Peggior Performer</span>
</div>
@if (!string.IsNullOrEmpty(portfolioStats.WorstPerformingAssetSymbol))
{
<div class="performer-content">
<div class="performer-symbol">@portfolioStats.WorstPerformingAssetSymbol</div>
<div class="performer-value @(portfolioStats.WorstPerformingAssetProfit >= 0 ? "profit" : "loss")">
$@portfolioStats.WorstPerformingAssetProfit.ToString("N2")
</div>
</div>
}
else
{
<div class="empty-performer">Nessun dato</div>
}
</div>
</div>
</div>
<!-- Asset Breakdown -->
<div class="breakdown-section">
<h2 class="section-title">
<span class="bi bi-list-columns-reverse"></span> Breakdown per Asset
</h2>
<div class="breakdown-table">
<div class="table-header">
<div class="th">Asset</div>
<div class="th">Valore</div>
<div class="th">Profitto</div>
<div class="th">% Profitto</div>
<div class="th">Trades</div>
<div class="th">Win Rate</div>
<div class="th">Azioni</div>
</div>
@foreach (var assetStat in portfolioStats.AssetStatistics.OrderByDescending(a => a.NetProfit))
{
var config = BotService.AssetConfigurations.TryGetValue(assetStat.Symbol, out var c) ? c : null;
if (config == null) continue;
var currentValue = config.CurrentBalance + (config.CurrentHoldings * assetStat.CurrentPrice);
<div class="table-row">
<div class="td asset-cell">
<span class="asset-symbol">@assetStat.Symbol</span>
<span class="asset-name">@assetStat.Name</span>
</div>
<div class="td">$@currentValue.ToString("N2")</div>
<div class="td @(assetStat.NetProfit >= 0 ? "profit" : "loss")">
$@assetStat.NetProfit.ToString("N2")
</div>
<div class="td @(config.ProfitPercentage >= 0 ? "profit" : "loss")">
@config.ProfitPercentage.ToString("F2")%
</div>
<div class="td">@assetStat.TotalTrades</div>
<div class="td">@assetStat.WinRate.ToString("F1")%</div>
<div class="td">
<button class="btn-details" @onclick="() => ViewAssetDetails(assetStat.Symbol)">
<span class="bi bi-eye"></span> Dettagli
</button>
</div>
</div>
}
</div>
</div>
}
else
{
<!-- Single Asset Statistics -->
var assetStats = BotService.AssetStatistics.TryGetValue(selectedSymbol, out var stats) ? stats : null;
var assetConfig = BotService.AssetConfigurations.TryGetValue(selectedSymbol, out var config) ? config : null;
@if (assetStats != null && assetConfig != null)
{
<div class="asset-details-section">
<div class="asset-details-header">
<div class="asset-title-section">
<h2>@assetStats.Name (@assetStats.Symbol)</h2>
<span class="status-badge @(assetConfig.IsEnabled ? "active" : "inactive")">
@(assetConfig.IsEnabled ? "Attivo" : "Inattivo")
</span>
</div>
<button class="btn-back" @onclick="ClearSelection">
<span class="bi bi-arrow-left"></span> Torna alla panoramica
</button>
</div>
<!-- Key Metrics -->
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-icon"><span class="bi bi-cash-stack"></span></div>
<div class="metric-content">
<div class="metric-label">Prezzo Corrente</div>
<div class="metric-value">$@assetStats.CurrentPrice.ToString("N2")</div>
</div>
</div>
<div class="metric-card">
<div class="metric-icon success"><span class="bi bi-bar-chart-line"></span></div>
<div class="metric-content">
<div class="metric-label">Holdings</div>
<div class="metric-value">@assetConfig.CurrentHoldings.ToString("F6")</div>
</div>
</div>
<div class="metric-card">
<div class="metric-icon @(assetStats.NetProfit >= 0 ? "success" : "danger")">
<span class="bi bi-graph-up-arrow"></span>
</div>
<div class="metric-content">
<div class="metric-label">Profitto Netto</div>
<div class="metric-value @(assetStats.NetProfit >= 0 ? "profit" : "loss")">
$@assetStats.NetProfit.ToString("N2")
</div>
</div>
</div>
<div class="metric-card">
<div class="metric-icon info"><span class="bi bi-percent"></span></div>
<div class="metric-content">
<div class="metric-label">ROI</div>
<div class="metric-value @(assetConfig.ProfitPercentage >= 0 ? "profit" : "loss")">
@assetConfig.ProfitPercentage.ToString("F2")%
</div>
</div>
</div>
</div>
<!-- Trading Performance -->
<div class="performance-section">
<h3 class="subsection-title">Performance Trading</h3>
<div class="performance-grid">
<div class="performance-item">
<span class="perf-label">Totale Operazioni</span>
<span class="perf-value">@assetStats.TotalTrades</span>
</div>
<div class="performance-item">
<span class="perf-label">Operazioni Vincenti</span>
<span class="perf-value profit">@assetStats.WinningTrades</span>
</div>
<div class="performance-item">
<span class="perf-label">Operazioni Perdenti</span>
<span class="perf-value loss">@assetStats.LosingTrades</span>
</div>
<div class="performance-item">
<span class="perf-label">Win Rate</span>
<span class="perf-value">@assetStats.WinRate.ToString("F1")%</span>
</div>
<div class="performance-item">
<span class="perf-label">Profit Factor</span>
<span class="perf-value">@(assetStats.ProfitFactor > 1000 ? ">1000" : assetStats.ProfitFactor.ToString("F2"))</span>
</div>
<div class="performance-item">
<span class="perf-label">Vittorie Consecutive</span>
<span class="perf-value">@assetStats.MaxConsecutiveWins</span>
</div>
</div>
</div>
<!-- Profit/Loss Analysis -->
<div class="pnl-section">
<h3 class="subsection-title">Analisi Profitti/Perdite</h3>
<div class="pnl-grid">
<div class="pnl-card profit-card">
<div class="pnl-header">
<span class="bi bi-arrow-up-circle-fill"></span>
<span>Profitti</span>
</div>
<div class="pnl-amount profit">$@assetStats.TotalProfit.ToString("N2")</div>
<div class="pnl-meta">
Media per trade: $@assetStats.AverageProfit.ToString("N2")
</div>
<div class="pnl-meta">
Profitto massimo: $@assetStats.LargestWin.ToString("N2")
</div>
</div>
<div class="pnl-card loss-card">
<div class="pnl-header">
<span class="bi bi-arrow-down-circle-fill"></span>
<span>Perdite</span>
</div>
<div class="pnl-amount loss">$@assetStats.TotalLoss.ToString("N2")</div>
<div class="pnl-meta">
Media per trade: $@assetStats.AverageLoss.ToString("N2")
</div>
<div class="pnl-meta">
Perdita massima: $@assetStats.LargestLoss.ToString("N2")
</div>
</div>
@if (assetStats.UnrealizedPnL != 0)
{
<div class="pnl-card unrealized-card">
<div class="pnl-header">
<span class="bi bi-hourglass-split"></span>
<span>P/L Non Realizzato</span>
</div>
<div class="pnl-amount @(assetStats.UnrealizedPnL >= 0 ? "profit" : "loss")">
$@assetStats.UnrealizedPnL.ToString("N2")
</div>
<div class="pnl-meta">
@assetStats.UnrealizedPnLPercentage.ToString("F2")% sulla posizione corrente
</div>
</div>
}
</div>
</div>
<!-- Recent Trades -->
@if (assetStats.RecentTrades.Count > 0)
{
<div class="trades-section">
<h3 class="subsection-title">Operazioni Recenti</h3>
<div class="trades-list">
@foreach (var trade in assetStats.RecentTrades.Take(20))
{
<div class="trade-item @(trade.IsBot ? "bot-trade" : "")">
<div class="trade-icon @(trade.Type == TradeType.Buy ? "buy" : "sell")">
<span class="bi @(trade.Type == TradeType.Buy ? "bi-arrow-down-circle-fill" : "bi-arrow-up-circle-fill")"></span>
</div>
<div class="trade-details">
<div class="trade-type">
@(trade.Type == TradeType.Buy ? "ACQUISTO" : "VENDITA")
@if (trade.IsBot)
{
<span class="bot-label">
<span class="bi bi-robot"></span> BOT
</span>
}
</div>
<div class="trade-meta">
@trade.Timestamp.ToLocalTime().ToString("dd/MM/yyyy HH:mm:ss")
</div>
</div>
<div class="trade-amounts">
<div class="trade-quantity">@trade.Amount.ToString("F6") @trade.Symbol</div>
<div class="trade-price">&#64; $@trade.Price.ToString("N2")</div>
</div>
<div class="trade-value">
$@((trade.Amount * trade.Price).ToString("N2"))
</div>
</div>
}
</div>
</div>
}
else
{
<div class="empty-trades">
<span class="bi bi-inbox"></span>
<p>Nessuna operazione eseguita per questo asset</p>
</div>
}
</div>
}
else
{
<div class="empty-state">
<span class="bi bi-exclamation-circle"></span>
<p>Asset non trovato o dati non disponibili</p>
</div>
}
}
</div>
</div>
@code {
[SupplyParameterFromQuery(Name = "symbol")]
private string? QuerySymbol { get; set; }
private string selectedSymbol = "";
private PortfolioStatistics portfolioStats = new();
protected override void OnInitialized()
{
BotService.OnStatusChanged += HandleUpdate;
BotService.OnTradeExecuted += HandleTradeExecuted;
BotService.OnStatisticsUpdated += HandleUpdate;
BotService.OnPriceUpdated += HandlePriceUpdate;
if (!string.IsNullOrEmpty(QuerySymbol))
{
selectedSymbol = QuerySymbol;
}
RefreshData();
}
protected override void OnParametersSet()
{
if (!string.IsNullOrEmpty(QuerySymbol) && QuerySymbol != selectedSymbol)
{
selectedSymbol = QuerySymbol;
RefreshData();
}
}
private void RefreshData()
{
portfolioStats = BotService.GetPortfolioStatistics();
StateHasChanged();
}
private void OnSymbolChanged()
{
if (string.IsNullOrEmpty(selectedSymbol))
{
Navigation.NavigateTo("/statistics");
}
else
{
Navigation.NavigateTo($"/statistics?symbol={selectedSymbol}");
}
RefreshData();
}
private void ViewAssetDetails(string symbol)
{
selectedSymbol = symbol;
Navigation.NavigateTo($"/statistics?symbol={symbol}");
RefreshData();
}
private void ClearSelection()
{
selectedSymbol = "";
Navigation.NavigateTo("/statistics");
RefreshData();
}
private void HandleUpdate() => InvokeAsync(RefreshData);
private void HandleTradeExecuted(Trade trade) => InvokeAsync(RefreshData);
private void HandlePriceUpdate(string symbol, MarketPrice price) => InvokeAsync(RefreshData);
public void Dispose()
{
BotService.OnStatusChanged -= HandleUpdate;
BotService.OnTradeExecuted -= HandleTradeExecuted;
BotService.OnStatisticsUpdated -= HandleUpdate;
BotService.OnPriceUpdated -= HandlePriceUpdate;
}
}

View File

@@ -0,0 +1,755 @@
/* Statistics Page */
.statistics-page {
min-height: 100vh;
background: #020617;
color: #f1f5f9;
}
/* Header */
.stats-header {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
border-bottom: 1px solid #1e293b;
padding: 2rem 1.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
}
.header-content {
max-width: 1400px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
gap: 2rem;
}
.page-title h1 {
margin: 0;
font-size: 2rem;
font-weight: 700;
color: white;
display: flex;
align-items: center;
gap: 0.75rem;
}
.subtitle {
margin: 0.5rem 0 0 0;
color: #94a3b8;
font-size: 0.875rem;
}
.header-filters {
display: flex;
gap: 0.75rem;
}
.filter-select {
padding: 0.75rem 1rem;
border-radius: 0.5rem;
border: 1px solid #334155;
background: #1e293b;
color: white;
font-size: 0.875rem;
cursor: pointer;
min-width: 200px;
}
.filter-select:focus {
outline: none;
border-color: #6366f1;
}
/* Content */
.stats-content {
max-width: 1400px;
margin: 0 auto;
padding: 2rem 1.5rem;
}
/* Section Title */
.section-title {
margin: 0 0 1.5rem 0;
font-size: 1.5rem;
font-weight: 700;
color: white;
display: flex;
align-items: center;
gap: 0.75rem;
}
/* Stats Grid */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin-bottom: 2.5rem;
}
.stat-card {
background: #0f172a;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px -4px rgba(0, 0, 0, 0.4);
}
.stat-card.primary {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
border-color: #7c3aed;
}
.stat-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
.stat-icon {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
background: rgba(255, 255, 255, 0.1);
color: white;
}
.stat-icon.success {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
}
.stat-icon.info {
background: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.stat-icon.warning {
background: rgba(245, 158, 11, 0.2);
color: #f59e0b;
}
.stat-label {
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.7);
font-weight: 600;
}
.stat-card:not(.primary) .stat-label {
color: #94a3b8;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
margin-bottom: 0.5rem;
}
.stat-footer {
display: flex;
align-items: center;
gap: 0.5rem;
}
.stat-change {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.875rem;
font-weight: 600;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
}
.stat-change.positive {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.stat-change.negative {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.stat-meta {
font-size: 0.75rem;
color: #64748b;
}
/* Performers Section */
.performers-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
margin-bottom: 2.5rem;
}
.performer-card {
background: #0f172a;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
}
.performer-card.best {
border-color: rgba(16, 185, 129, 0.3);
background: linear-gradient(135deg, rgba(16, 185, 129, 0.05) 0%, #0f172a 100%);
}
.performer-card.worst {
border-color: rgba(239, 68, 68, 0.3);
background: linear-gradient(135deg, rgba(239, 68, 68, 0.05) 0%, #0f172a 100%);
}
.performer-header {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
color: #94a3b8;
margin-bottom: 1rem;
}
.performer-card.best .performer-header {
color: #10b981;
}
.performer-card.worst .performer-header {
color: #ef4444;
}
.performer-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.performer-symbol {
font-size: 1.5rem;
font-weight: 700;
color: white;
font-family: monospace;
}
.performer-value {
font-size: 1.5rem;
font-weight: 700;
font-family: monospace;
}
.empty-performer {
text-align: center;
color: #475569;
font-size: 0.875rem;
padding: 1rem;
}
/* Breakdown Table */
.breakdown-section {
margin-bottom: 2.5rem;
}
.breakdown-table {
background: #0f172a;
border: 1px solid #1e293b;
border-radius: 0.75rem;
overflow: hidden;
}
.table-header {
display: grid;
grid-template-columns: 2fr 1.5fr 1.5fr 1fr 1fr 1fr 1.5fr;
gap: 1rem;
padding: 1rem 1.5rem;
background: #1e293b;
border-bottom: 1px solid #334155;
}
.th {
font-size: 0.75rem;
font-weight: 700;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.table-row {
display: grid;
grid-template-columns: 2fr 1.5fr 1.5fr 1fr 1fr 1fr 1.5fr;
gap: 1rem;
padding: 1rem 1.5rem;
border-bottom: 1px solid #1e293b;
transition: background 0.2s ease;
}
.table-row:hover {
background: #1e293b;
}
.td {
display: flex;
align-items: center;
font-size: 0.875rem;
color: white;
}
.asset-cell {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
}
.asset-symbol {
font-weight: 700;
font-family: monospace;
color: white;
}
.asset-name {
font-size: 0.75rem;
color: #64748b;
}
.btn-details {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 600;
border: 1px solid #334155;
background: transparent;
color: #94a3b8;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-details:hover {
background: #1e293b;
border-color: #6366f1;
color: white;
}
/* Asset Details */
.asset-details-section {
display: flex;
flex-direction: column;
gap: 2rem;
}
.asset-details-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.asset-title-section {
display: flex;
align-items: center;
gap: 1rem;
}
.asset-title-section h2 {
margin: 0;
font-size: 1.875rem;
font-weight: 700;
color: white;
}
.status-badge {
padding: 0.375rem 0.75rem;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
}
.status-badge.active {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
border: 1px solid rgba(16, 185, 129, 0.3);
}
.status-badge.inactive {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.3);
}
.btn-back {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
border: 1px solid #334155;
background: #1e293b;
color: #94a3b8;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-back:hover {
background: #334155;
color: white;
}
/* Metrics Grid */
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.metric-card {
background: #0f172a;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
display: flex;
align-items: center;
gap: 1rem;
}
.metric-icon {
width: 3rem;
height: 3rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
background: rgba(255, 255, 255, 0.1);
color: white;
}
.metric-icon.success {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
}
.metric-icon.danger {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.metric-icon.info {
background: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.metric-content {
flex: 1;
}
.metric-label {
font-size: 0.75rem;
color: #94a3b8;
text-transform: uppercase;
font-weight: 600;
margin-bottom: 0.5rem;
}
.metric-value {
font-size: 1.5rem;
font-weight: 700;
color: white;
font-family: monospace;
}
/* Performance Section */
.performance-section {
background: #0f172a;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
}
.subsection-title {
margin: 0 0 1.5rem 0;
font-size: 1.125rem;
font-weight: 600;
color: white;
}
.performance-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.performance-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem;
background: #020617;
border: 1px solid #1e293b;
border-radius: 0.5rem;
}
.perf-label {
font-size: 0.75rem;
color: #64748b;
font-weight: 600;
}
.perf-value {
font-size: 1.25rem;
font-weight: 700;
color: white;
font-family: monospace;
}
/* P/L Section */
.pnl-section {
background: #0f172a;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
}
.pnl-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.pnl-card {
background: #020617;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
}
.profit-card {
border-color: rgba(16, 185, 129, 0.3);
background: linear-gradient(135deg, rgba(16, 185, 129, 0.05) 0%, #020617 100%);
}
.loss-card {
border-color: rgba(239, 68, 68, 0.3);
background: linear-gradient(135deg, rgba(239, 68, 68, 0.05) 0%, #020617 100%);
}
.unrealized-card {
border-color: rgba(245, 158, 11, 0.3);
background: linear-gradient(135deg, rgba(245, 158, 11, 0.05) 0%, #020617 100%);
}
.pnl-header {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
color: #94a3b8;
margin-bottom: 1rem;
}
.profit-card .pnl-header {
color: #10b981;
}
.loss-card .pnl-header {
color: #ef4444;
}
.unrealized-card .pnl-header {
color: #f59e0b;
}
.pnl-amount {
font-size: 1.875rem;
font-weight: 700;
font-family: monospace;
margin-bottom: 0.75rem;
}
.pnl-meta {
font-size: 0.75rem;
color: #64748b;
margin-bottom: 0.375rem;
}
/* Trades Section */
.trades-section {
background: #0f172a;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
}
.trades-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.trade-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: #020617;
border: 1px solid #1e293b;
border-radius: 0.5rem;
transition: all 0.2s ease;
}
.trade-item:hover {
background: #1e293b;
}
.trade-item.bot-trade {
border-color: rgba(99, 102, 241, 0.3);
background: rgba(99, 102, 241, 0.05);
}
.trade-icon {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
}
.trade-icon.buy {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.trade-icon.sell {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
.trade-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.trade-type {
font-size: 0.875rem;
font-weight: 600;
color: white;
display: flex;
align-items: center;
gap: 0.5rem;
}
.bot-label {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.375rem;
background: rgba(99, 102, 241, 0.2);
color: #6366f1;
border-radius: 0.25rem;
font-size: 0.625rem;
font-weight: 700;
}
.trade-meta {
font-size: 0.75rem;
color: #64748b;
}
.trade-amounts {
display: flex;
flex-direction: column;
gap: 0.25rem;
align-items: flex-end;
}
.trade-quantity {
font-size: 0.875rem;
color: white;
font-family: monospace;
}
.trade-price {
font-size: 0.75rem;
color: #64748b;
}
.trade-value {
font-size: 1rem;
font-weight: 700;
color: white;
font-family: monospace;
min-width: 100px;
text-align: right;
}
/* Empty States */
.empty-state, .empty-trades {
text-align: center;
padding: 3rem 1rem;
color: #64748b;
}
.empty-state .bi, .empty-trades .bi {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-state p, .empty-trades p {
margin: 1rem 0;
font-size: 1rem;
}
/* Common Styles */
.profit {
color: #10b981 !important;
}
.loss {
color: #ef4444 !important;
}
/* Responsive */
@media (max-width: 1024px) {
.performers-section {
grid-template-columns: 1fr;
}
.table-header, .table-row {
grid-template-columns: 1fr;
}
.th:not(:first-child), .td:not(:first-child) {
display: none;
}
}
@media (max-width: 768px) {
.header-content {
flex-direction: column;
align-items: flex-start;
}
.stats-grid, .metrics-grid, .performance-grid, .pnl-grid {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,318 @@
@page "/strategies"
@using TradingBot.Models
@using TradingBot.Services
@inject TradingBotService BotService
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Strategie - TradingBot</PageTitle>
<div class="strategies-page">
<div class="page-header">
<div>
<h1>Gestione Strategie</h1>
<p class="subtitle">Crea e gestisci le tue strategie di trading automatizzate</p>
</div>
<button class="btn-primary">
<span class="bi bi-plus-lg"></span>
Nuova Strategia
</button>
</div>
<div class="strategies-grid">
<!-- Active Strategy Card -->
<div class="strategy-card active">
<div class="card-header">
<div class="strategy-info">
<h3>RSI + MACD Cross</h3>
<span class="badge active">ATTIVA</span>
</div>
<div class="strategy-actions">
<button class="btn-icon" title="Modifica">
<span class="bi bi-pencil"></span>
</button>
<button class="btn-icon" title="Duplica">
<span class="bi bi-files"></span>
</button>
</div>
</div>
<div class="card-body">
<div class="strategy-description">
Strategia basata su indicatori tecnici RSI e MACD per identificare punti di ingresso e uscita ottimali
</div>
<div class="strategy-stats">
<div class="stat">
<span class="stat-label">Asset Applicati</span>
<span class="stat-value">@activeAssets/@totalAssets</span>
</div>
<div class="stat">
<span class="stat-label">Win Rate</span>
<span class="stat-value profit">@portfolioStats.WinRate.ToString("F1")%</span>
</div>
<div class="stat">
<span class="stat-label">Trades Totali</span>
<span class="stat-value">@portfolioStats.TotalTrades</span>
</div>
<div class="stat">
<span class="stat-label">Profitto</span>
<span class="stat-value @(portfolioStats.TotalProfit >= 0 ? "profit" : "loss")">
$@portfolioStats.TotalProfit.ToString("N2")
</span>
</div>
</div>
<div class="strategy-parameters">
<h4>Parametri</h4>
<div class="params-grid">
<div class="param">
<span class="param-label">Condizione BUY</span>
<code class="param-value">RSI &lt; 40 AND MACD &gt; 0</code>
</div>
<div class="param">
<span class="param-label">Condizione SELL</span>
<code class="param-value">RSI &gt; 60 AND MACD &lt; 0</code>
</div>
<div class="param">
<span class="param-label">Stop Loss</span>
<code class="param-value">5%</code>
</div>
<div class="param">
<span class="param-label">Take Profit</span>
<code class="param-value">10%</code>
</div>
</div>
</div>
<div class="strategy-indicators">
<h4>Indicatori Utilizzati</h4>
<div class="indicators-list">
<span class="indicator-tag">
<span class="bi bi-graph-up"></span>
RSI (14)
</span>
<span class="indicator-tag">
<span class="bi bi-graph-down"></span>
MACD (12, 26, 9)
</span>
<span class="indicator-tag">
<span class="bi bi-activity"></span>
EMA (12, 26)
</span>
</div>
</div>
</div>
<div class="card-footer">
<button class="btn-secondary">
<span class="bi bi-pause-circle"></span>
Disattiva
</button>
<button class="btn-primary" @onclick="@(() => NavigateToStatistics())">
<span class="bi bi-bar-chart-line"></span>
Vedi Performance
</button>
</div>
</div>
<!-- Example Inactive Strategy Cards -->
<div class="strategy-card">
<div class="card-header">
<div class="strategy-info">
<h3>Media Mobile Semplice</h3>
<span class="badge inactive">INATTIVA</span>
</div>
<div class="strategy-actions">
<button class="btn-icon" title="Modifica">
<span class="bi bi-pencil"></span>
</button>
<button class="btn-icon" title="Elimina">
<span class="bi bi-trash"></span>
</button>
</div>
</div>
<div class="card-body">
<div class="strategy-description">
Strategia classica basata sull'incrocio di medie mobili a breve e lungo termine
</div>
<div class="strategy-stats">
<div class="stat">
<span class="stat-label">Asset Applicati</span>
<span class="stat-value">0/@totalAssets</span>
</div>
<div class="stat">
<span class="stat-label">Win Rate</span>
<span class="stat-value">-</span>
</div>
<div class="stat">
<span class="stat-label">Trades Totali</span>
<span class="stat-value">0</span>
</div>
<div class="stat">
<span class="stat-label">Profitto</span>
<span class="stat-value">$0.00</span>
</div>
</div>
<div class="strategy-parameters">
<h4>Parametri</h4>
<div class="params-grid">
<div class="param">
<span class="param-label">SMA Breve</span>
<code class="param-value">10 periodi</code>
</div>
<div class="param">
<span class="param-label">SMA Lungo</span>
<code class="param-value">30 periodi</code>
</div>
<div class="param">
<span class="param-label">Stop Loss</span>
<code class="param-value">3%</code>
</div>
<div class="param">
<span class="param-label">Take Profit</span>
<code class="param-value">8%</code>
</div>
</div>
</div>
<div class="strategy-indicators">
<h4>Indicatori Utilizzati</h4>
<div class="indicators-list">
<span class="indicator-tag">
<span class="bi bi-graph-up"></span>
SMA (10)
</span>
<span class="indicator-tag">
<span class="bi bi-graph-up"></span>
SMA (30)
</span>
</div>
</div>
</div>
<div class="card-footer">
<button class="btn-secondary">
<span class="bi bi-play-circle"></span>
Attiva
</button>
<button class="btn-secondary">
<span class="bi bi-pencil"></span>
Modifica
</button>
</div>
</div>
<!-- Template Strategy Card -->
<div class="strategy-card template">
<div class="template-content">
<div class="template-icon">
<span class="bi bi-diagram-3"></span>
</div>
<h3>Crea Nuova Strategia</h3>
<p>Progetta una strategia personalizzata con indicatori tecnici e regole di trading</p>
<button class="btn-primary">
<span class="bi bi-plus-lg"></span>
Inizia Ora
</button>
</div>
</div>
</div>
<!-- Strategy Templates Section -->
<div class="templates-section">
<h2>Template Strategie</h2>
<p class="section-subtitle">Inizia da modelli predefiniti e personalizzali secondo le tue esigenze</p>
<div class="templates-grid">
<div class="template-item">
<div class="template-header">
<span class="bi bi-lightning-charge"></span>
<h4>Scalping Veloce</h4>
</div>
<p>Strategia ad alta frequenza per profitti rapidi su piccoli movimenti di prezzo</p>
<button class="btn-outline">
<span class="bi bi-download"></span>
Usa Template
</button>
</div>
<div class="template-item">
<div class="template-header">
<span class="bi bi-graph-up-arrow"></span>
<h4>Trend Following</h4>
</div>
<p>Segui le tendenze di mercato dominanti per massimizzare i profitti</p>
<button class="btn-outline">
<span class="bi bi-download"></span>
Usa Template
</button>
</div>
<div class="template-item">
<div class="template-header">
<span class="bi bi-arrow-left-right"></span>
<h4>Mean Reversion</h4>
</div>
<p>Sfrutta il ritorno dei prezzi verso la media storica</p>
<button class="btn-outline">
<span class="bi bi-download"></span>
Usa Template
</button>
</div>
<div class="template-item">
<div class="template-header">
<span class="bi bi-shield-check"></span>
<h4>Conservative</h4>
</div>
<p>Strategia a basso rischio con protezione del capitale</p>
<button class="btn-outline">
<span class="bi bi-download"></span>
Usa Template
</button>
</div>
</div>
</div>
</div>
@code {
private PortfolioStatistics portfolioStats = new();
private int activeAssets = 0;
private int totalAssets = 0;
protected override void OnInitialized()
{
BotService.OnStatusChanged += HandleUpdate;
BotService.OnTradeExecuted += HandleTradeExecuted;
RefreshData();
}
private void RefreshData()
{
portfolioStats = BotService.GetPortfolioStatistics();
activeAssets = BotService.AssetConfigurations.Values.Count(c => c.IsEnabled);
totalAssets = BotService.AssetConfigurations.Count;
StateHasChanged();
}
private void NavigateToStatistics()
{
// Navigate to statistics page
}
private void HandleUpdate() => InvokeAsync(RefreshData);
private void HandleTradeExecuted(Trade trade) => InvokeAsync(RefreshData);
public void Dispose()
{
BotService.OnStatusChanged -= HandleUpdate;
BotService.OnTradeExecuted -= HandleTradeExecuted;
}
}

View File

@@ -0,0 +1,407 @@
/* Strategies Page */
.strategies-page {
display: flex;
flex-direction: column;
gap: 2rem;
}
/* Page Header */
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.page-header h1 {
margin: 0;
font-size: 2rem;
font-weight: 700;
color: white;
}
.subtitle {
margin: 0.5rem 0 0 0;
color: #94a3b8;
font-size: 0.875rem;
}
/* Buttons */
.btn-primary, .btn-secondary, .btn-outline {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-primary {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
color: white;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4);
}
.btn-secondary {
background: #1e293b;
color: #cbd5e1;
border: 1px solid #334155;
}
.btn-secondary:hover {
background: #334155;
border-color: #475569;
}
.btn-outline {
background: transparent;
color: #6366f1;
border: 1px solid #6366f1;
}
.btn-outline:hover {
background: rgba(99, 102, 241, 0.1);
}
.btn-icon {
width: 2rem;
height: 2rem;
border-radius: 0.375rem;
border: none;
background: transparent;
color: #64748b;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.btn-icon:hover {
background: #1e293b;
color: #cbd5e1;
}
/* Strategies Grid */
.strategies-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 1.5rem;
}
/* Strategy Card */
.strategy-card {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
overflow: hidden;
transition: all 0.3s ease;
}
.strategy-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.4);
border-color: #334155;
}
.strategy-card.active {
border-color: #6366f1;
box-shadow: 0 0 0 1px #6366f1;
}
.card-header {
padding: 1.5rem;
background: #1a1f3a;
border-bottom: 1px solid #1e293b;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.strategy-info {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.strategy-info h3 {
margin: 0;
font-size: 1.125rem;
font-weight: 700;
color: white;
}
.badge {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.badge.active {
background: rgba(16, 185, 129, 0.2);
color: #10b981;
}
.badge.inactive {
background: rgba(100, 116, 139, 0.2);
color: #64748b;
}
.strategy-actions {
display: flex;
gap: 0.5rem;
}
.card-body {
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.strategy-description {
font-size: 0.875rem;
color: #94a3b8;
line-height: 1.6;
}
/* Strategy Stats */
.strategy-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
padding: 1rem;
background: #1a1f3a;
border-radius: 0.5rem;
}
.stat {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.stat-label {
font-size: 0.625rem;
color: #64748b;
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.05em;
}
.stat-value {
font-size: 1.125rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
}
.stat-value.profit {
color: #10b981;
}
.stat-value.loss {
color: #ef4444;
}
/* Strategy Parameters */
.strategy-parameters h4,
.strategy-indicators h4 {
margin: 0 0 0.75rem 0;
font-size: 0.875rem;
font-weight: 600;
color: #cbd5e1;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.params-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
.param {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.param-label {
font-size: 0.75rem;
color: #64748b;
font-weight: 600;
}
.param-value {
font-size: 0.75rem;
color: #10b981;
background: rgba(16, 185, 129, 0.1);
padding: 0.375rem 0.5rem;
border-radius: 0.25rem;
font-family: 'Courier New', monospace;
}
/* Indicators */
.indicators-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.indicator-tag {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
background: #1a1f3a;
border: 1px solid #334155;
border-radius: 0.375rem;
font-size: 0.75rem;
color: #cbd5e1;
font-weight: 600;
}
/* Card Footer */
.card-footer {
padding: 1rem 1.5rem;
background: #0a0e27;
border-top: 1px solid #1e293b;
display: flex;
gap: 0.75rem;
}
/* Template Card */
.strategy-card.template {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%);
border: 2px dashed #6366f1;
display: flex;
align-items: center;
justify-content: center;
min-height: 300px;
}
.template-content {
text-align: center;
padding: 2rem;
}
.template-icon {
font-size: 3rem;
color: #6366f1;
margin-bottom: 1rem;
}
.template-content h3 {
margin: 0 0 0.5rem 0;
font-size: 1.25rem;
font-weight: 700;
color: white;
}
.template-content p {
margin: 0 0 1.5rem 0;
color: #94a3b8;
font-size: 0.875rem;
}
/* Templates Section */
.templates-section {
margin-top: 2rem;
}
.templates-section h2 {
margin: 0 0 0.5rem 0;
font-size: 1.5rem;
font-weight: 700;
color: white;
}
.section-subtitle {
margin: 0 0 1.5rem 0;
color: #94a3b8;
font-size: 0.875rem;
}
.templates-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
}
.template-item {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
transition: all 0.3s ease;
}
.template-item:hover {
border-color: #6366f1;
transform: translateY(-2px);
}
.template-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.template-header .bi {
font-size: 1.5rem;
color: #6366f1;
}
.template-header h4 {
margin: 0;
font-size: 1rem;
font-weight: 700;
color: white;
}
.template-item p {
margin: 0 0 1rem 0;
font-size: 0.875rem;
color: #94a3b8;
line-height: 1.5;
}
/* Responsive */
@media (max-width: 768px) {
.strategies-grid {
grid-template-columns: 1fr;
}
.strategy-stats {
grid-template-columns: repeat(2, 1fr);
}
.params-grid {
grid-template-columns: 1fr;
}
.templates-grid {
grid-template-columns: 1fr;
}
.page-header {
flex-direction: column;
gap: 1rem;
}
}

View File

@@ -0,0 +1,238 @@
@page "/trading"
@using TradingBot.Models
@using TradingBot.Services
@inject TradingBotService BotService
@implements IDisposable
@rendermode InteractiveServer
<PageTitle>Trading - TradingBot</PageTitle>
<div class="trading-page">
<div class="page-header">
<div>
<h1>Trading Automatico</h1>
<p class="subtitle">Applica strategie agli asset e monitora le operazioni in tempo reale</p>
</div>
<div class="header-controls">
<button class="btn-secondary">
<span class="bi bi-download"></span>
Esporta Report
</button>
<button class="btn-toggle @(BotService.Status.IsRunning ? "active" : "")" @onclick="ToggleBot">
<span class="bi @(BotService.Status.IsRunning ? "bi-pause-circle-fill" : "bi-play-circle-fill")"></span>
@(BotService.Status.IsRunning ? "Stop Trading" : "Avvia Trading")
</button>
</div>
</div>
<!-- Assets Grid -->
<div class="assets-section">
<div class="section-header">
<h2>Asset Monitorati</h2>
<div class="filters">
<select class="filter-select">
<option>Tutti gli Asset</option>
<option>Solo Attivi</option>
<option>Solo Inattivi</option>
</select>
<button class="btn-icon">
<span class="bi bi-funnel"></span>
</button>
</div>
</div>
<div class="assets-grid">
@foreach (var config in BotService.AssetConfigurations.Values.OrderBy(c => c.Symbol))
{
var stats = BotService.AssetStatistics.TryGetValue(config.Symbol, out var s) ? s : null;
var latestPrice = BotService.GetLatestPrice(config.Symbol);
<div class="asset-trading-card @(config.IsEnabled ? "enabled" : "disabled")">
<div class="asset-header">
<div class="asset-title">
<span class="asset-icon">@config.Symbol.Substring(0, 1)</span>
<div class="asset-name-group">
<span class="name">@config.Name</span>
<span class="symbol">@config.Symbol</span>
</div>
</div>
<label class="toggle-switch">
<input type="checkbox"
checked="@config.IsEnabled"
@onchange="(e) => ToggleAsset(config.Symbol, (bool)e.Value!)" />
<span class="toggle-slider"></span>
</label>
</div>
@if (latestPrice != null)
{
<div class="asset-price-info">
<div class="current-price">$@latestPrice.Price.ToString("N2")</div>
<div class="price-change @(latestPrice.Change24h >= 0 ? "positive" : "negative")">
<span class="bi @(latestPrice.Change24h >= 0 ? "bi-arrow-up" : "bi-arrow-down")"></span>
@Math.Abs(latestPrice.Change24h).ToString("F2")%
</div>
</div>
}
else
{
<div class="asset-price-info">
<div class="current-price loading">Loading...</div>
</div>
}
<div class="asset-strategy">
<div class="strategy-label">Strategia Applicata</div>
<div class="strategy-name">
<span class="bi bi-diagram-3"></span>
@config.StrategyName
</div>
</div>
<div class="asset-metrics">
<div class="metric">
<span class="metric-label">Holdings</span>
<span class="metric-value">@config.CurrentHoldings.ToString("F4")</span>
</div>
<div class="metric">
<span class="metric-label">Valore</span>
<span class="metric-value">$@((config.CurrentBalance + config.CurrentHoldings * (latestPrice?.Price ?? 0)).ToString("N2"))</span>
</div>
<div class="metric">
<span class="metric-label">Profitto</span>
<span class="metric-value @(config.TotalProfit >= 0 ? "profit" : "loss")">
$@config.TotalProfit.ToString("N2")
</span>
</div>
<div class="metric">
<span class="metric-label">Trades</span>
<span class="metric-value">@(stats?.TotalTrades ?? 0)</span>
</div>
</div>
<div class="asset-actions">
<button class="btn-secondary btn-sm" @onclick="() => OpenAssetConfig(config.Symbol)">
<span class="bi bi-gear"></span>
Configura
</button>
<button class="btn-secondary btn-sm" @onclick="() => ViewChart(config.Symbol)">
<span class="bi bi-graph-up"></span>
Grafico
</button>
</div>
</div>
}
</div>
</div>
<!-- Recent Trades -->
<div class="trades-section">
<div class="section-header">
<h2>Operazioni Recenti</h2>
<button class="btn-secondary btn-sm">
<span class="bi bi-clock-history"></span>
Vedi Tutto
</button>
</div>
@if (BotService.Trades.Count == 0)
{
<div class="empty-state">
<span class="bi bi-inbox"></span>
<p>Nessuna operazione ancora</p>
<p class="hint">Avvia il trading per iniziare a eseguire operazioni</p>
</div>
}
else
{
<div class="trades-table">
<div class="table-header">
<div>Asset</div>
<div>Tipo</div>
<div>Quantità</div>
<div>Prezzo</div>
<div>Valore</div>
<div>Strategia</div>
<div>Data/Ora</div>
</div>
@foreach (var trade in BotService.Trades.Take(20))
{
<div class="table-row @(trade.IsBot ? "bot-trade" : "")">
<div class="cell-asset">
<span class="asset-badge">@trade.Symbol</span>
</div>
<div class="cell-type @(trade.Type == TradeType.Buy ? "buy" : "sell")">
<span class="bi @(trade.Type == TradeType.Buy ? "bi-arrow-down-circle-fill" : "bi-arrow-up-circle-fill")"></span>
@(trade.Type == TradeType.Buy ? "BUY" : "SELL")
</div>
<div>@trade.Amount.ToString("F6")</div>
<div>$@trade.Price.ToString("N2")</div>
<div class="cell-value">$@((trade.Amount * trade.Price).ToString("N2"))</div>
<div class="cell-strategy">
@if (trade.IsBot)
{
<span class="strategy-tag">
<span class="bi bi-robot"></span>
@trade.Strategy
</span>
}
else
{
<span class="manual-tag">Manuale</span>
}
</div>
<div class="cell-time">@trade.Timestamp.ToLocalTime().ToString("dd/MM HH:mm:ss")</div>
</div>
}
</div>
}
</div>
</div>
@code {
protected override void OnInitialized()
{
BotService.OnStatusChanged += HandleUpdate;
BotService.OnTradeExecuted += HandleTradeExecuted;
BotService.OnPriceUpdated += HandlePriceUpdate;
if (!BotService.Status.IsRunning)
{
BotService.Start();
}
}
private void ToggleBot()
{
if (BotService.Status.IsRunning)
BotService.Stop();
else
BotService.Start();
}
private void ToggleAsset(string symbol, bool enabled)
{
BotService.ToggleAsset(symbol, enabled);
}
private void OpenAssetConfig(string symbol)
{
// TODO: Open asset configuration modal
}
private void ViewChart(string symbol)
{
// TODO: Navigate to market analysis with selected symbol
}
private void HandleUpdate() => InvokeAsync(StateHasChanged);
private void HandleTradeExecuted(Trade trade) => InvokeAsync(StateHasChanged);
private void HandlePriceUpdate(string symbol, MarketPrice price) => InvokeAsync(StateHasChanged);
public void Dispose()
{
BotService.OnStatusChanged -= HandleUpdate;
BotService.OnTradeExecuted -= HandleTradeExecuted;
BotService.OnPriceUpdated -= HandlePriceUpdate;
}
}

View File

@@ -0,0 +1,478 @@
/* Trading Page */
.trading-page {
display: flex;
flex-direction: column;
gap: 2rem;
}
/* Header */
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.page-header h1 {
margin: 0;
font-size: 2rem;
font-weight: 700;
color: white;
}
.subtitle {
margin: 0.5rem 0 0 0;
color: #94a3b8;
font-size: 0.875rem;
}
.header-controls {
display: flex;
gap: 0.75rem;
}
.btn-toggle {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
border: 1px solid #334155;
background: #1e293b;
color: #cbd5e1;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-toggle:hover {
background: #334155;
}
.btn-toggle.active {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
border-color: #6366f1;
color: white;
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
}
/* Section Header */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.section-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
color: white;
}
.filters {
display: flex;
gap: 0.5rem;
}
.filter-select {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
border: 1px solid #334155;
background: #1e293b;
color: white;
font-size: 0.875rem;
}
/* Assets Grid */
.assets-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.asset-trading-card {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
padding: 1.5rem;
transition: all 0.3s ease;
}
.asset-trading-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.4);
border-color: #334155;
}
.asset-trading-card.enabled {
border-color: rgba(99, 102, 241, 0.3);
}
.asset-trading-card.disabled {
opacity: 0.6;
}
.asset-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.asset-title {
display: flex;
align-items: center;
gap: 0.75rem;
}
.asset-icon {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.5rem;
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
font-weight: 700;
color: white;
}
.asset-name-group {
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.asset-name-group .name {
font-size: 1rem;
font-weight: 700;
color: white;
}
.asset-name-group .symbol {
font-size: 0.75rem;
color: #64748b;
font-family: 'Courier New', monospace;
}
/* Toggle Switch */
.toggle-switch {
position: relative;
display: inline-block;
width: 3rem;
height: 1.5rem;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #334155;
transition: 0.3s;
border-radius: 1.5rem;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 1.125rem;
width: 1.125rem;
left: 0.1875rem;
bottom: 0.1875rem;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
.toggle-switch input:checked + .toggle-slider {
background-color: #6366f1;
}
.toggle-switch input:checked + .toggle-slider:before {
transform: translateX(1.5rem);
}
/* Price Info */
.asset-price-info {
display: flex;
align-items: baseline;
gap: 0.75rem;
margin-bottom: 1rem;
}
.current-price {
font-size: 1.875rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
}
.current-price.loading {
font-size: 1rem;
color: #64748b;
font-family: inherit;
}
.price-change {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.875rem;
font-weight: 600;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
}
.price-change.positive {
background: rgba(16, 185, 129, 0.1);
color: #10b981;
}
.price-change.negative {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
/* Strategy */
.asset-strategy {
margin-bottom: 1rem;
padding: 0.75rem;
background: #1a1f3a;
border-radius: 0.5rem;
}
.strategy-label {
font-size: 0.625rem;
color: #64748b;
text-transform: uppercase;
font-weight: 600;
margin-bottom: 0.375rem;
}
.strategy-name {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: #cbd5e1;
font-weight: 600;
}
/* Metrics */
.asset-metrics {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
margin-bottom: 1rem;
}
.metric {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.metric-label {
font-size: 0.625rem;
color: #64748b;
text-transform: uppercase;
font-weight: 600;
}
.metric-value {
font-size: 0.875rem;
font-weight: 700;
color: white;
font-family: 'Courier New', monospace;
}
.metric-value.profit {
color: #10b981;
}
.metric-value.loss {
color: #ef4444;
}
/* Actions */
.asset-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
}
.btn-sm {
padding: 0.5rem 1rem;
font-size: 0.75rem;
}
/* Trades Table */
.trades-table {
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
overflow: hidden;
}
.table-header {
display: grid;
grid-template-columns: 1fr 1fr 1.5fr 1.5fr 1.5fr 2fr 1.5fr;
gap: 1rem;
padding: 1rem 1.5rem;
background: #1a1f3a;
border-bottom: 1px solid #1e293b;
font-size: 0.75rem;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
}
.table-row {
display: grid;
grid-template-columns: 1fr 1fr 1.5fr 1.5fr 1.5fr 2fr 1.5fr;
gap: 1rem;
padding: 1rem 1.5rem;
border-bottom: 1px solid #1e293b;
align-items: center;
font-size: 0.875rem;
color: #cbd5e1;
transition: background 0.2s ease;
}
.table-row:hover {
background: #1a1f3a;
}
.table-row.bot-trade {
background: rgba(99, 102, 241, 0.05);
}
.table-row:last-child {
border-bottom: none;
}
.asset-badge {
display: inline-flex;
padding: 0.25rem 0.5rem;
background: #1a1f3a;
border-radius: 0.375rem;
font-family: 'Courier New', monospace;
font-weight: 700;
font-size: 0.75rem;
}
.cell-type {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 700;
}
.cell-type.buy {
color: #10b981;
}
.cell-type.sell {
color: #ef4444;
}
.cell-value {
font-family: 'Courier New', monospace;
font-weight: 600;
}
.strategy-tag {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.25rem 0.5rem;
background: rgba(99, 102, 241, 0.2);
color: #6366f1;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 600;
}
.manual-tag {
display: inline-flex;
padding: 0.25rem 0.5rem;
background: rgba(100, 116, 139, 0.2);
color: #64748b;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 600;
}
.cell-time {
color: #64748b;
font-size: 0.75rem;
}
/* Empty State */
.empty-state {
text-align: center;
padding: 4rem 2rem;
background: #0f1629;
border: 1px solid #1e293b;
border-radius: 0.75rem;
}
.empty-state .bi {
font-size: 3rem;
color: #334155;
margin-bottom: 1rem;
}
.empty-state p {
margin: 0.5rem 0;
color: #94a3b8;
}
.empty-state .hint {
font-size: 0.875rem;
color: #64748b;
}
/* Responsive */
@media (max-width: 1024px) {
.assets-grid {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
.table-header, .table-row {
grid-template-columns: 1fr;
gap: 0.5rem;
}
.table-header {
display: none;
}
}
@media (max-width: 768px) {
.page-header {
flex-direction: column;
gap: 1rem;
}
.header-controls {
width: 100%;
flex-direction: column;
}
.assets-grid {
grid-template-columns: 1fr;
}
}