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:
341
TradingBot/Components/Pages/Assets.razor
Normal file
341
TradingBot/Components/Pages/Assets.razor
Normal 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;
|
||||
}
|
||||
}
|
||||
597
TradingBot/Components/Pages/Assets.razor.css
Normal file
597
TradingBot/Components/Pages/Assets.razor.css
Normal 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;
|
||||
}
|
||||
}
|
||||
185
TradingBot/Components/Pages/Dashboard.razor
Normal file
185
TradingBot/Components/Pages/Dashboard.razor
Normal 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") @ $@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;
|
||||
}
|
||||
}
|
||||
362
TradingBot/Components/Pages/Dashboard.razor.css
Normal file
362
TradingBot/Components/Pages/Dashboard.razor.css
Normal 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);
|
||||
}
|
||||
}
|
||||
36
TradingBot/Components/Pages/Error.razor
Normal file
36
TradingBot/Components/Pages/Error.razor
Normal 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;
|
||||
}
|
||||
238
TradingBot/Components/Pages/Market.razor
Normal file
238
TradingBot/Components/Pages/Market.razor
Normal 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;
|
||||
}
|
||||
}
|
||||
328
TradingBot/Components/Pages/Market.razor.css
Normal file
328
TradingBot/Components/Pages/Market.razor.css
Normal 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;
|
||||
}
|
||||
}
|
||||
5
TradingBot/Components/Pages/NotFound.razor
Normal file
5
TradingBot/Components/Pages/NotFound.razor
Normal 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>
|
||||
170
TradingBot/Components/Pages/Settings.razor
Normal file
170
TradingBot/Components/Pages/Settings.razor
Normal 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;
|
||||
}
|
||||
}
|
||||
221
TradingBot/Components/Pages/Settings.razor.css
Normal file
221
TradingBot/Components/Pages/Settings.razor.css
Normal 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;
|
||||
}
|
||||
}
|
||||
460
TradingBot/Components/Pages/Statistics.razor
Normal file
460
TradingBot/Components/Pages/Statistics.razor
Normal 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">@ $@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;
|
||||
}
|
||||
}
|
||||
755
TradingBot/Components/Pages/Statistics.razor.css
Normal file
755
TradingBot/Components/Pages/Statistics.razor.css
Normal 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;
|
||||
}
|
||||
}
|
||||
318
TradingBot/Components/Pages/Strategies.razor
Normal file
318
TradingBot/Components/Pages/Strategies.razor
Normal 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 < 40 AND MACD > 0</code>
|
||||
</div>
|
||||
<div class="param">
|
||||
<span class="param-label">Condizione SELL</span>
|
||||
<code class="param-value">RSI > 60 AND MACD < 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;
|
||||
}
|
||||
}
|
||||
407
TradingBot/Components/Pages/Strategies.razor.css
Normal file
407
TradingBot/Components/Pages/Strategies.razor.css
Normal 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;
|
||||
}
|
||||
}
|
||||
238
TradingBot/Components/Pages/Trading.razor
Normal file
238
TradingBot/Components/Pages/Trading.razor
Normal 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;
|
||||
}
|
||||
}
|
||||
478
TradingBot/Components/Pages/Trading.razor.css
Normal file
478
TradingBot/Components/Pages/Trading.razor.css
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user