Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
@page "/indicators"
|
||||
@using TradingBot.Services
|
||||
@using TradingBot.Models
|
||||
@inject IndicatorsService IndicatorsService
|
||||
@inject TradingBotService BotService
|
||||
@implements IDisposable
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Indicatori - TradingBot</PageTitle>
|
||||
|
||||
<div class="indicators-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>Indicatori Tecnici</h1>
|
||||
<p class="subtitle">Configura gli indicatori per analisi avanzata del mercato</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Indicators Grid -->
|
||||
<div class="indicators-grid">
|
||||
@foreach (var indicator in indicators.Values.OrderBy(i => !i.IsEnabled).ThenBy(i => i.Name))
|
||||
{
|
||||
<div class="indicator-card @(indicator.IsEnabled ? "enabled" : "disabled")">
|
||||
<div class="indicator-header">
|
||||
<div class="indicator-title">
|
||||
<h3>@indicator.Name</h3>
|
||||
<span class="indicator-type">@indicator.Type</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" checked="@indicator.IsEnabled"
|
||||
@onchange="@(e => ToggleIndicator(indicator.Id, (bool)e.Value!))" />
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p class="indicator-description">@indicator.Description</p>
|
||||
|
||||
@if (indicator.IsEnabled)
|
||||
{
|
||||
<div class="indicator-config">
|
||||
@switch (indicator.Type)
|
||||
{
|
||||
case IndicatorType.RSI:
|
||||
case IndicatorType.Stochastic:
|
||||
<div class="config-row">
|
||||
<label>Periodo:</label>
|
||||
<input type="number" @bind="indicator.Period" min="5" max="50" />
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label>Ipercomprato:</label>
|
||||
<input type="number" @bind="indicator.OverboughtThreshold" min="60" max="90" step="5" />
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label>Ipervenduto:</label>
|
||||
<input type="number" @bind="indicator.OversoldThreshold" min="10" max="40" step="5" />
|
||||
</div>
|
||||
break;
|
||||
|
||||
case IndicatorType.MACD:
|
||||
<div class="config-row">
|
||||
<label>Fast Period:</label>
|
||||
<input type="number" @bind="indicator.FastPeriod" min="8" max="20" />
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label>Slow Period:</label>
|
||||
<input type="number" @bind="indicator.SlowPeriod" min="20" max="35" />
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label>Signal Period:</label>
|
||||
<input type="number" @bind="indicator.SignalPeriod" min="5" max="15" />
|
||||
</div>
|
||||
break;
|
||||
|
||||
case IndicatorType.SMA:
|
||||
case IndicatorType.EMA:
|
||||
case IndicatorType.BollingerBands:
|
||||
<div class="config-row">
|
||||
<label>Periodo:</label>
|
||||
<input type="number" @bind="indicator.Period" min="5" max="200" />
|
||||
</div>
|
||||
break;
|
||||
}
|
||||
|
||||
<button class="btn-secondary btn-sm" @onclick="() => SaveIndicator(indicator)">
|
||||
<span class="bi bi-check-lg"></span>
|
||||
Salva
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Current Status for Active Assets -->
|
||||
<div class="indicator-status-section">
|
||||
<h4>Status Asset Attivi</h4>
|
||||
@foreach (var symbol in BotService.AssetConfigurations.Values.Where(c => c.IsEnabled).Select(c => c.Symbol))
|
||||
{
|
||||
var status = IndicatorsService.GetIndicatorStatus(indicator.Id, symbol);
|
||||
if (status != null)
|
||||
{
|
||||
<div class="status-row">
|
||||
<span class="status-symbol">@symbol</span>
|
||||
<span class="status-value">@status.CurrentValue.ToString("F2")</span>
|
||||
<span class="status-condition condition-@status.Condition.ToString().ToLower()">
|
||||
@status.Condition
|
||||
</span>
|
||||
<span class="status-recommendation">@status.Recommendation</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Recent Signals -->
|
||||
<div class="signals-section">
|
||||
<div class="section-header">
|
||||
<h2>Segnali Recenti</h2>
|
||||
<span class="signals-count">@recentSignals.Count segnali</span>
|
||||
</div>
|
||||
|
||||
@if (recentSignals.Count == 0)
|
||||
{
|
||||
<div class="empty-state">
|
||||
<span class="bi bi-broadcast"></span>
|
||||
<p>Nessun segnale generato</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="signals-list">
|
||||
@foreach (var signal in recentSignals.Take(20))
|
||||
{
|
||||
<div class="signal-card signal-@signal.Type.ToString().ToLower()">
|
||||
<div class="signal-header">
|
||||
<span class="signal-time">@signal.Timestamp.ToLocalTime().ToString("HH:mm:ss")</span>
|
||||
<span class="signal-indicator">@signal.IndicatorName</span>
|
||||
<span class="signal-symbol">@signal.Symbol</span>
|
||||
<span class="signal-type type-@signal.Type.ToString().ToLower()">
|
||||
@signal.Type
|
||||
</span>
|
||||
<span class="signal-strength strength-@signal.Strength.ToString().ToLower()">
|
||||
@signal.Strength
|
||||
</span>
|
||||
</div>
|
||||
<div class="signal-message">@signal.Message</div>
|
||||
@if (signal.Value.HasValue)
|
||||
{
|
||||
<div class="signal-value">Valore: @signal.Value.Value.ToString("F2")</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.indicators-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.indicators-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.indicator-card {
|
||||
background: #1a1f3a;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.indicator-card.disabled {
|
||||
opacity: 0.6;
|
||||
border-color: rgba(100, 116, 139, 0.2);
|
||||
}
|
||||
|
||||
.indicator-card.enabled {
|
||||
border-color: rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
|
||||
.indicator-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.indicator-title h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #e2e8f0;
|
||||
margin: 0 0 0.25rem 0;
|
||||
}
|
||||
|
||||
.indicator-type {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
border-radius: 0.25rem;
|
||||
color: #6366f1;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.indicator-description {
|
||||
color: #94a3b8;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.indicator-config {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.config-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.config-row:last-of-type {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.config-row label {
|
||||
font-size: 0.875rem;
|
||||
color: #cbd5e1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.config-row input[type="number"] {
|
||||
width: 80px;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
background: #0f1629;
|
||||
border: 1px solid #334155;
|
||||
color: #e2e8f0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.indicator-status-section {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.indicator-status-section h4 {
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-row {
|
||||
display: grid;
|
||||
grid-template-columns: 60px 80px 100px 1fr;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.status-symbol {
|
||||
font-weight: 700;
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.status-condition {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-weight: 700;
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.condition-overbought { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
|
||||
.condition-oversold { background: rgba(16, 185, 129, 0.2); color: #10b981; }
|
||||
.condition-bullish { background: rgba(16, 185, 129, 0.2); color: #10b981; }
|
||||
.condition-bearish { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
|
||||
.condition-neutral { background: rgba(100, 116, 139, 0.2); color: #94a3b8; }
|
||||
|
||||
.status-recommendation {
|
||||
color: #cbd5e1;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.signals-section {
|
||||
background: #1a1f3a;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.signals-count {
|
||||
color: #6366f1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.signals-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.signal-card {
|
||||
background: #0f1629;
|
||||
border-radius: 0.5rem;
|
||||
border-left: 3px solid;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.signal-card.signal-buy {
|
||||
border-left-color: #10b981;
|
||||
}
|
||||
|
||||
.signal-card.signal-sell {
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
|
||||
.signal-card.signal-hold {
|
||||
border-left-color: #f59e0b;
|
||||
}
|
||||
|
||||
.signal-header {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.signal-time {
|
||||
color: #64748b;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.signal-indicator {
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
border-radius: 0.25rem;
|
||||
color: #6366f1;
|
||||
font-weight: 700;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.signal-symbol {
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: rgba(139, 92, 246, 0.2);
|
||||
border-radius: 0.25rem;
|
||||
color: #8b5cf6;
|
||||
font-weight: 700;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.signal-type {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
font-weight: 700;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.type-buy { background: rgba(16, 185, 129, 0.2); color: #10b981; }
|
||||
.type-sell { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
|
||||
.type-hold { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
|
||||
|
||||
.signal-strength {
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
border-radius: 0.25rem;
|
||||
color: #3b82f6;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.signal-message {
|
||||
color: #e2e8f0;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.signal-value {
|
||||
color: #94a3b8;
|
||||
font-size: 0.875rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.empty-state .bi {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
private Dictionary<string, IndicatorConfig> indicators = new();
|
||||
private List<IndicatorSignal> recentSignals = new();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
LoadIndicators();
|
||||
IndicatorsService.OnIndicatorsChanged += HandleIndicatorsChanged;
|
||||
IndicatorsService.OnSignalGenerated += HandleSignalGenerated;
|
||||
}
|
||||
|
||||
private void LoadIndicators()
|
||||
{
|
||||
indicators = IndicatorsService.GetIndicators().ToDictionary(k => k.Key, v => v.Value);
|
||||
recentSignals = IndicatorsService.GetRecentSignals().ToList();
|
||||
}
|
||||
|
||||
private void ToggleIndicator(string id, bool enabled)
|
||||
{
|
||||
IndicatorsService.ToggleIndicator(id, enabled);
|
||||
}
|
||||
|
||||
private void SaveIndicator(IndicatorConfig indicator)
|
||||
{
|
||||
IndicatorsService.UpdateIndicator(indicator.Id, indicator);
|
||||
}
|
||||
|
||||
private void HandleIndicatorsChanged()
|
||||
{
|
||||
LoadIndicators();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private void HandleSignalGenerated(IndicatorSignal signal)
|
||||
{
|
||||
recentSignals = IndicatorsService.GetRecentSignals().ToList();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
IndicatorsService.OnIndicatorsChanged -= HandleIndicatorsChanged;
|
||||
IndicatorsService.OnSignalGenerated -= HandleSignalGenerated;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
@page "/logs"
|
||||
@using TradingBot.Services
|
||||
@using TradingBot.Models
|
||||
@inject LoggingService LoggingService
|
||||
@implements IDisposable
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Logs - TradingBot</PageTitle>
|
||||
|
||||
<div class="logs-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>Logs Operazioni</h1>
|
||||
<p class="subtitle">Cronologia eventi e operazioni del bot</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="btn-secondary" @onclick="ClearLogs">
|
||||
<span class="bi bi-trash"></span>
|
||||
Cancella Logs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="logs-filters">
|
||||
<div class="filter-group">
|
||||
<label>Livello:</label>
|
||||
<select @bind="selectedLevel" @bind:after="FilterLogs">
|
||||
<option value="">Tutti</option>
|
||||
<option value="Debug">Debug</option>
|
||||
<option value="Info">Info</option>
|
||||
<option value="Warning">Warning</option>
|
||||
<option value="Error">Error</option>
|
||||
<option value="Trade">Trade</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>Categoria:</label>
|
||||
<select @bind="selectedCategory" @bind:after="FilterLogs">
|
||||
<option value="">Tutte</option>
|
||||
@foreach (var category in categories)
|
||||
{
|
||||
<option value="@category">@category</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>Symbol:</label>
|
||||
<select @bind="selectedSymbol" @bind:after="FilterLogs">
|
||||
<option value="">Tutti</option>
|
||||
@foreach (var symbol in symbols)
|
||||
{
|
||||
<option value="@symbol">@symbol</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label>
|
||||
<input type="checkbox" @bind="autoScroll" />
|
||||
Auto-scroll
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="logs-count">
|
||||
@filteredLogs.Count / @allLogs.Count logs
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="logs-container" @ref="logsContainer">
|
||||
@if (filteredLogs.Count == 0)
|
||||
{
|
||||
<div class="empty-state">
|
||||
<span class="bi bi-inbox"></span>
|
||||
<p>Nessun log disponibile</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="logs-list">
|
||||
@foreach (var log in filteredLogs.OrderByDescending(l => l.Timestamp))
|
||||
{
|
||||
<div class="log-entry log-@log.Level.ToString().ToLower()">
|
||||
<div class="log-header">
|
||||
<span class="log-timestamp">@log.Timestamp.ToLocalTime().ToString("HH:mm:ss.fff")</span>
|
||||
<span class="log-level">
|
||||
<span class="bi bi-@GetLevelIcon(log.Level)"></span>
|
||||
@log.Level
|
||||
</span>
|
||||
<span class="log-category">@log.Category</span>
|
||||
@if (!string.IsNullOrEmpty(log.Symbol))
|
||||
{
|
||||
<span class="log-symbol">@log.Symbol</span>
|
||||
}
|
||||
</div>
|
||||
<div class="log-message">@log.Message</div>
|
||||
@if (!string.IsNullOrEmpty(log.Details))
|
||||
{
|
||||
<div class="log-details">@log.Details</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.logs-page {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.logs-filters {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: #1a1f3a;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.filter-group select {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
background: #0f1629;
|
||||
border: 1px solid #334155;
|
||||
color: #e2e8f0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.logs-count {
|
||||
margin-left: auto;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border-radius: 0.375rem;
|
||||
color: #6366f1;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: #1a1f3a;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.logs-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border-left: 3px solid;
|
||||
background: #0f1629;
|
||||
}
|
||||
|
||||
.log-entry.log-debug {
|
||||
border-left-color: #64748b;
|
||||
}
|
||||
|
||||
.log-entry.log-info {
|
||||
border-left-color: #3b82f6;
|
||||
}
|
||||
|
||||
.log-entry.log-warning {
|
||||
border-left-color: #f59e0b;
|
||||
}
|
||||
|
||||
.log-entry.log-error {
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
|
||||
.log-entry.log-trade {
|
||||
border-left-color: #10b981;
|
||||
background: rgba(16, 185, 129, 0.05);
|
||||
}
|
||||
|
||||
.log-header {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.log-timestamp {
|
||||
color: #64748b;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.log-level {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.log-entry.log-debug .log-level {
|
||||
background: rgba(100, 116, 139, 0.2);
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.log-entry.log-info .log-level {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.log-entry.log-warning .log-level {
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.log-entry.log-error .log-level {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.log-entry.log-trade .log-level {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.log-category {
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border-radius: 0.25rem;
|
||||
color: #6366f1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.log-symbol {
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
border-radius: 0.25rem;
|
||||
color: #8b5cf6;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
color: #e2e8f0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.log-details {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 0.375rem;
|
||||
color: #94a3b8;
|
||||
font-size: 0.875rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 2rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.empty-state .bi {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
font-size: 1.125rem;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
private ElementReference logsContainer;
|
||||
private List<LogEntry> allLogs = new();
|
||||
private List<LogEntry> filteredLogs = new();
|
||||
private string selectedLevel = "";
|
||||
private string selectedCategory = "";
|
||||
private string selectedSymbol = "";
|
||||
private bool autoScroll = true;
|
||||
private List<string> categories = new();
|
||||
private List<string> symbols = new();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
LoadLogs();
|
||||
LoggingService.OnLogAdded += HandleLogAdded;
|
||||
}
|
||||
|
||||
private void LoadLogs()
|
||||
{
|
||||
allLogs = LoggingService.GetLogs().ToList();
|
||||
UpdateFilters();
|
||||
FilterLogs();
|
||||
}
|
||||
|
||||
private void UpdateFilters()
|
||||
{
|
||||
categories = allLogs.Select(l => l.Category).Distinct().OrderBy(c => c).ToList();
|
||||
symbols = allLogs.Where(l => !string.IsNullOrEmpty(l.Symbol))
|
||||
.Select(l => l.Symbol!)
|
||||
.Distinct()
|
||||
.OrderBy(s => s)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private void FilterLogs()
|
||||
{
|
||||
var query = allLogs.AsEnumerable();
|
||||
|
||||
if (!string.IsNullOrEmpty(selectedLevel))
|
||||
{
|
||||
if (Enum.TryParse<TradingBot.Models.LogLevel>(selectedLevel, out var level))
|
||||
{
|
||||
query = query.Where(l => l.Level == level);
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(selectedCategory))
|
||||
{
|
||||
query = query.Where(l => l.Category == selectedCategory);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(selectedSymbol))
|
||||
{
|
||||
query = query.Where(l => l.Symbol == selectedSymbol);
|
||||
}
|
||||
|
||||
filteredLogs = query.ToList();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async void HandleLogAdded()
|
||||
{
|
||||
LoadLogs();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
if (autoScroll)
|
||||
{
|
||||
await Task.Delay(100);
|
||||
// Auto-scroll logic would go here if needed
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearLogs()
|
||||
{
|
||||
LoggingService.ClearLogs();
|
||||
LoadLogs();
|
||||
}
|
||||
|
||||
private string GetLevelIcon(TradingBot.Models.LogLevel level)
|
||||
{
|
||||
return level switch
|
||||
{
|
||||
TradingBot.Models.LogLevel.Debug => "bug",
|
||||
TradingBot.Models.LogLevel.Info => "info-circle",
|
||||
TradingBot.Models.LogLevel.Warning => "exclamation-triangle",
|
||||
TradingBot.Models.LogLevel.Error => "x-circle",
|
||||
TradingBot.Models.LogLevel.Trade => "graph-up-arrow",
|
||||
_ => "circle"
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
LoggingService.OnLogAdded -= HandleLogAdded;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,716 @@
|
||||
@page "/positions"
|
||||
@using TradingBot.Services
|
||||
@using TradingBot.Models
|
||||
@inject TradingBotService BotService
|
||||
@inject LoggingService LoggingService
|
||||
@implements IDisposable
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Posizioni Aperte - TradingBot</PageTitle>
|
||||
|
||||
<div class="positions-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>Posizioni Aperte</h1>
|
||||
<p class="subtitle">Gestisci le tue posizioni attive - Solo chiusura manuale disponibile</p>
|
||||
</div>
|
||||
<div class="header-stats">
|
||||
<div class="stat-card">
|
||||
<span class="stat-label">Posizioni Attive</span>
|
||||
<span class="stat-value">@activePositions.Count</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-label">Valore Totale</span>
|
||||
<span class="stat-value">$@totalValue.ToString("N2")</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-label">P&L Non Realizzato</span>
|
||||
<span class="stat-value @(totalUnrealizedPL >= 0 ? "profit" : "loss")">
|
||||
@(totalUnrealizedPL >= 0 ? "+" : "")$@totalUnrealizedPL.ToString("N2")
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (activePositions.Count == 0)
|
||||
{
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<span class="bi bi-inbox"></span>
|
||||
</div>
|
||||
<h3>Nessuna Posizione Aperta</h3>
|
||||
<p>Non hai posizioni attive al momento. Le posizioni appaiono qui automaticamente quando il bot esegue un acquisto.</p>
|
||||
<div class="empty-actions">
|
||||
<a href="/trading-control" class="btn-primary">
|
||||
<span class="bi bi-sliders"></span>
|
||||
Configura Trading
|
||||
</a>
|
||||
<a href="/dashboard" class="btn-secondary">
|
||||
<span class="bi bi-speedometer2"></span>
|
||||
Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="positions-grid">
|
||||
@foreach (var position in activePositions.OrderBy(p => p.Symbol))
|
||||
{
|
||||
var currentPrice = GetCurrentPrice(position.Symbol);
|
||||
var unrealizedPL = CalculateUnrealizedPL(position, currentPrice);
|
||||
var plPercentage = CalculatePLPercentage(position, currentPrice);
|
||||
var currentValue = position.Amount * currentPrice;
|
||||
var holdingTime = DateTime.UtcNow - position.Timestamp;
|
||||
|
||||
<div class="position-card">
|
||||
<div class="position-header">
|
||||
<div class="position-asset">
|
||||
<h3>@position.Symbol</h3>
|
||||
<span class="position-date">
|
||||
Aperta @position.Timestamp.ToLocalTime().ToString("dd/MM/yyyy HH:mm")
|
||||
</span>
|
||||
</div>
|
||||
<div class="position-pl @(unrealizedPL >= 0 ? "profit" : "loss")">
|
||||
<span class="pl-value">
|
||||
@(unrealizedPL >= 0 ? "+" : "")$@unrealizedPL.ToString("N2")
|
||||
</span>
|
||||
<span class="pl-percentage">
|
||||
(@(plPercentage >= 0 ? "+" : "")@plPercentage.ToString("F2")%)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="position-details">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Quantità</span>
|
||||
<span class="detail-value">@position.Amount.ToString("F8") @position.Symbol</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Prezzo Entrata</span>
|
||||
<span class="detail-value">$@position.Price.ToString("N2")</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Prezzo Corrente</span>
|
||||
<span class="detail-value">$@currentPrice.ToString("N2")</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Valore Iniziale</span>
|
||||
<span class="detail-value">$@(position.Amount * position.Price).ToString("N2")</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Valore Corrente</span>
|
||||
<span class="detail-value">$@currentValue.ToString("N2")</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Tempo Holding</span>
|
||||
<span class="detail-value">@FormatHoldingTime(holdingTime)</span>
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(position.Strategy))
|
||||
{
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Strategia</span>
|
||||
<span class="detail-value strategy-badge">@position.Strategy</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="position-actions">
|
||||
<button class="btn-danger" @onclick="() => ShowCloseConfirmation(position)"
|
||||
disabled="@(!BotService.Status.IsRunning)">
|
||||
<span class="bi bi-x-circle"></span>
|
||||
Chiudi Posizione
|
||||
</button>
|
||||
@if (!BotService.Status.IsRunning)
|
||||
{
|
||||
<span class="action-note">
|
||||
<span class="bi bi-info-circle"></span>
|
||||
Avvia il bot per chiudere posizioni
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Close Confirmation Modal -->
|
||||
@if (showCloseModal && positionToClose != null)
|
||||
{
|
||||
var currentPrice = GetCurrentPrice(positionToClose.Symbol);
|
||||
var unrealizedPL = CalculateUnrealizedPL(positionToClose, currentPrice);
|
||||
var plPercentage = CalculatePLPercentage(positionToClose, currentPrice);
|
||||
|
||||
<div class="modal-overlay" @onclick="HideCloseConfirmation">
|
||||
<div class="modal-dialog" @onclick:stopPropagation="true">
|
||||
<div class="modal-header">
|
||||
<h3>Conferma Chiusura Posizione</h3>
|
||||
<button class="btn-close" @onclick="HideCloseConfirmation">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="confirmation-details">
|
||||
<div class="confirm-asset">
|
||||
<h4>@positionToClose.Symbol</h4>
|
||||
<span class="confirm-amount">@positionToClose.Amount.ToString("F8") @positionToClose.Symbol</span>
|
||||
</div>
|
||||
|
||||
<div class="confirm-prices">
|
||||
<div class="price-item">
|
||||
<span class="price-label">Prezzo Entrata</span>
|
||||
<span class="price-value">$@positionToClose.Price.ToString("N2")</span>
|
||||
</div>
|
||||
<span class="bi bi-arrow-right"></span>
|
||||
<div class="price-item">
|
||||
<span class="price-label">Prezzo Chiusura</span>
|
||||
<span class="price-value">$@currentPrice.ToString("N2")</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="confirm-pl @(unrealizedPL >= 0 ? "profit" : "loss")">
|
||||
<div class="pl-label">Profitto/Perdita Stimato</div>
|
||||
<div class="pl-amount">
|
||||
@(unrealizedPL >= 0 ? "+" : "")$@unrealizedPL.ToString("N2")
|
||||
</div>
|
||||
<div class="pl-percent">
|
||||
(@(plPercentage >= 0 ? "+" : "")@plPercentage.ToString("F2")%)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="confirm-warning">
|
||||
<span class="bi bi-exclamation-triangle"></span>
|
||||
<p>
|
||||
<strong>Attenzione:</strong> Questa azione chiuderà immediatamente la posizione al prezzo di mercato corrente.
|
||||
L'operazione non può essere annullata.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" @onclick="HideCloseConfirmation">Annulla</button>
|
||||
<button class="btn-danger" @onclick="ConfirmClosePosition">
|
||||
<span class="bi bi-x-circle"></span>
|
||||
Conferma Chiusura
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Success Notification -->
|
||||
@if (showSuccessNotification)
|
||||
{
|
||||
<div class="notification success">
|
||||
<span class="bi bi-check-circle-fill"></span>
|
||||
Posizione chiusa con successo!
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.positions-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.header-stats {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem 1.5rem;
|
||||
background: #1a1f3a;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: #e2e8f0;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.stat-value.profit {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.stat-value.loss {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
background: #1a1f3a;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
padding: 4rem 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 4rem;
|
||||
color: #64748b;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
color: #e2e8f0;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
color: #94a3b8;
|
||||
margin-bottom: 2rem;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.empty-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.positions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.position-card {
|
||||
background: #1a1f3a;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.position-card:hover {
|
||||
border-color: rgba(99, 102, 241, 0.4);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.position-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.position-asset h3 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #e2e8f0;
|
||||
margin: 0 0 0.25rem 0;
|
||||
}
|
||||
|
||||
.position-date {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.position-pl {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.pl-value {
|
||||
display: block;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.position-pl.profit .pl-value {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.position-pl.loss .pl-value {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.pl-percentage {
|
||||
font-size: 0.875rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.position-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 0.875rem;
|
||||
color: #e2e8f0;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.strategy-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
border-radius: 0.25rem;
|
||||
color: #6366f1;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.position-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.action-note {
|
||||
font-size: 0.75rem;
|
||||
color: #f59e0b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.confirmation-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.confirm-asset {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.confirm-asset h4 {
|
||||
font-size: 1.5rem;
|
||||
color: #e2e8f0;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.confirm-amount {
|
||||
color: #94a3b8;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.confirm-prices {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.price-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.price-label {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.price-value {
|
||||
font-size: 1.25rem;
|
||||
color: #e2e8f0;
|
||||
font-weight: 700;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.confirm-prices .bi {
|
||||
font-size: 1.5rem;
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.confirm-pl {
|
||||
text-align: center;
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.confirm-pl.profit {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.confirm-pl.loss {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.pl-label {
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pl-amount {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
font-family: 'Courier New', monospace;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.confirm-pl.profit .pl-amount {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.confirm-pl.loss .pl-amount {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.pl-percent {
|
||||
font-size: 1rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.confirm-warning {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
border-radius: 0.5rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.confirm-warning .bi {
|
||||
font-size: 1.25rem;
|
||||
color: #f59e0b;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.confirm-warning p {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: #cbd5e1;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.confirm-warning strong {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.notification {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
||||
z-index: 9999;
|
||||
animation: slideInRight 0.3s ease;
|
||||
}
|
||||
|
||||
.notification.success {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
border: 1px solid #10b981;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
@@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@media (max-width: 768px) {
|
||||
.positions-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.header-stats {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.confirm-prices {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.confirm-prices .bi {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
private List<Trade> activePositions = new();
|
||||
private decimal totalValue = 0;
|
||||
private decimal totalUnrealizedPL = 0;
|
||||
private bool showCloseModal = false;
|
||||
private Trade? positionToClose = null;
|
||||
private bool showSuccessNotification = false;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
LoadPositions();
|
||||
BotService.OnTradeExecuted += HandleTradeExecuted;
|
||||
BotService.OnPriceUpdated += HandlePriceUpdated;
|
||||
BotService.OnStatusChanged += HandleStatusChanged;
|
||||
}
|
||||
|
||||
private void LoadPositions()
|
||||
{
|
||||
activePositions = BotService.ActivePositions.Values.ToList();
|
||||
CalculateTotals();
|
||||
}
|
||||
|
||||
private void CalculateTotals()
|
||||
{
|
||||
totalValue = 0;
|
||||
totalUnrealizedPL = 0;
|
||||
|
||||
foreach (var position in activePositions)
|
||||
{
|
||||
var currentPrice = GetCurrentPrice(position.Symbol);
|
||||
var positionValue = position.Amount * currentPrice;
|
||||
var pl = CalculateUnrealizedPL(position, currentPrice);
|
||||
|
||||
totalValue += positionValue;
|
||||
totalUnrealizedPL += pl;
|
||||
}
|
||||
}
|
||||
|
||||
private decimal GetCurrentPrice(string symbol)
|
||||
{
|
||||
var latestPrice = BotService.GetLatestPrice(symbol);
|
||||
return latestPrice?.Price ?? 0;
|
||||
}
|
||||
|
||||
private decimal CalculateUnrealizedPL(Trade position, decimal currentPrice)
|
||||
{
|
||||
return (currentPrice - position.Price) * position.Amount;
|
||||
}
|
||||
|
||||
private decimal CalculatePLPercentage(Trade position, decimal currentPrice)
|
||||
{
|
||||
if (position.Price == 0) return 0;
|
||||
return ((currentPrice - position.Price) / position.Price) * 100;
|
||||
}
|
||||
|
||||
private string FormatHoldingTime(TimeSpan time)
|
||||
{
|
||||
if (time.TotalDays >= 1)
|
||||
return $"{(int)time.TotalDays}g {time.Hours}h";
|
||||
else if (time.TotalHours >= 1)
|
||||
return $"{(int)time.TotalHours}h {time.Minutes}m";
|
||||
else
|
||||
return $"{(int)time.TotalMinutes}m {time.Seconds}s";
|
||||
}
|
||||
|
||||
private void ShowCloseConfirmation(Trade position)
|
||||
{
|
||||
positionToClose = position;
|
||||
showCloseModal = true;
|
||||
}
|
||||
|
||||
private void HideCloseConfirmation()
|
||||
{
|
||||
showCloseModal = false;
|
||||
positionToClose = null;
|
||||
}
|
||||
|
||||
private async Task ConfirmClosePosition()
|
||||
{
|
||||
if (positionToClose == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
// Close position using TradingBotService public method
|
||||
await BotService.ClosePositionManuallyAsync(positionToClose.Symbol);
|
||||
|
||||
LoggingService.LogInfo(
|
||||
"Positions",
|
||||
$"Posizione chiusa manualmente: {positionToClose.Symbol}",
|
||||
$"Quantità: {positionToClose.Amount:F8}");
|
||||
|
||||
showSuccessNotification = true;
|
||||
HideCloseConfirmation();
|
||||
LoadPositions();
|
||||
|
||||
// Hide notification after 3 seconds
|
||||
await Task.Delay(3000);
|
||||
showSuccessNotification = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggingService.LogError("Positions", $"Errore chiusura posizione: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleTradeExecuted(Trade trade)
|
||||
{
|
||||
LoadPositions();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private void HandlePriceUpdated(string symbol, MarketPrice price)
|
||||
{
|
||||
if (activePositions.Any(p => p.Symbol == symbol))
|
||||
{
|
||||
CalculateTotals();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleStatusChanged()
|
||||
{
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
BotService.OnTradeExecuted -= HandleTradeExecuted;
|
||||
BotService.OnPriceUpdated -= HandlePriceUpdated;
|
||||
BotService.OnStatusChanged -= HandleStatusChanged;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
@page "/settings"
|
||||
@using TradingBot.Services
|
||||
@using TradingBot.Models
|
||||
@inject SettingsService SettingsService
|
||||
@inject TradingBotService TradingBotService
|
||||
@inject TradeHistoryService HistoryService
|
||||
@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>Dati Persistenti</h2>
|
||||
<div class="settings-group">
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Trade Salvati</div>
|
||||
<div class="setting-description">@TradingBotService.Trades.Count trade nella cronologia</div>
|
||||
</div>
|
||||
<div class="setting-value">
|
||||
@FormatBytes(dataSize)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Posizioni Attive</div>
|
||||
<div class="setting-description">@TradingBotService.ActivePositions.Count posizioni aperte</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Cancella Tutti i Dati</div>
|
||||
<div class="setting-description text-danger">Elimina cronologia trade e resetta i saldi</div>
|
||||
</div>
|
||||
<button class="btn-danger" @onclick="ShowClearDataConfirmation" disabled="@TradingBotService.Status.IsRunning">
|
||||
<span class="bi bi-trash"></span>
|
||||
Cancella Dati
|
||||
</button>
|
||||
</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>
|
||||
}
|
||||
|
||||
@if (showClearConfirmation)
|
||||
{
|
||||
<div class="modal-overlay" @onclick="HideClearDataConfirmation">
|
||||
<div class="modal-dialog" @onclick:stopPropagation="true">
|
||||
<div class="modal-header">
|
||||
<h3>Conferma Cancellazione</h3>
|
||||
<button class="btn-close" @onclick="HideClearDataConfirmation">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-danger">
|
||||
<strong>Attenzione!</strong> Questa azione eliminerà:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Tutta la cronologia dei trade (@TradingBotService.Trades.Count trade)</li>
|
||||
<li>Tutte le posizioni attive (@TradingBotService.ActivePositions.Count posizioni)</li>
|
||||
<li>I saldi verranno resettati ai valori iniziali</li>
|
||||
</ul>
|
||||
<p class="text-danger">
|
||||
<strong>Questa operazione è irreversibile!</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" @onclick="HideClearDataConfirmation">Annulla</button>
|
||||
<button class="btn-danger" @onclick="ConfirmClearData">
|
||||
<span class="bi bi-trash"></span>
|
||||
Conferma Cancellazione
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private AppSettings settings = new();
|
||||
private bool showNotification = false;
|
||||
private bool showClearConfirmation = false;
|
||||
private long dataSize = 0;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
settings = SettingsService.GetSettings();
|
||||
SettingsService.OnSettingsChanged += HandleSettingsChanged;
|
||||
TradingBotService.OnStatusChanged += HandleStatusChanged;
|
||||
UpdateDataSize();
|
||||
}
|
||||
|
||||
private void UpdateDataSize()
|
||||
{
|
||||
dataSize = HistoryService.GetDataSize();
|
||||
}
|
||||
|
||||
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 void ShowClearDataConfirmation()
|
||||
{
|
||||
showClearConfirmation = true;
|
||||
}
|
||||
|
||||
private void HideClearDataConfirmation()
|
||||
{
|
||||
showClearConfirmation = false;
|
||||
}
|
||||
|
||||
private async Task ConfirmClearData()
|
||||
{
|
||||
await TradingBotService.ClearAllDataAsync();
|
||||
UpdateDataSize();
|
||||
showClearConfirmation = false;
|
||||
showNotification = true;
|
||||
StateHasChanged();
|
||||
await Task.Delay(3000);
|
||||
showNotification = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async void ShowNotification()
|
||||
{
|
||||
showNotification = true;
|
||||
StateHasChanged();
|
||||
await Task.Delay(3000);
|
||||
showNotification = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private string FormatBytes(long bytes)
|
||||
{
|
||||
string[] sizes = { "B", "KB", "MB", "GB" };
|
||||
double len = bytes;
|
||||
int order = 0;
|
||||
while (len >= 1024 && order < sizes.Length - 1)
|
||||
{
|
||||
order++;
|
||||
len = len / 1024;
|
||||
}
|
||||
return $"{len:0.##} {sizes[order]}";
|
||||
}
|
||||
|
||||
private void HandleSettingsChanged()
|
||||
{
|
||||
settings = SettingsService.GetSettings();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private void HandleStatusChanged()
|
||||
{
|
||||
UpdateDataSize();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SettingsService.OnSettingsChanged -= HandleSettingsChanged;
|
||||
TradingBotService.OnStatusChanged -= HandleStatusChanged;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,555 @@
|
||||
@page "/trading-control"
|
||||
@using TradingBot.Services
|
||||
@using TradingBot.Models
|
||||
@inject TradingStrategiesService StrategiesService
|
||||
@inject TradingBotService BotService
|
||||
@implements IDisposable
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Trading Control - TradingBot</PageTitle>
|
||||
|
||||
<div class="trading-control-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>Trading Control</h1>
|
||||
<p class="subtitle">Gestisci le strategie di trading per ogni asset</p>
|
||||
</div>
|
||||
<div class="header-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Asset Attivi</span>
|
||||
<span class="stat-value">@activeAssets</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Strategie in Uso</span>
|
||||
<span class="stat-value">@totalStrategies</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assets Grid -->
|
||||
<div class="assets-control-grid">
|
||||
@foreach (var asset in BotService.AssetConfigurations.Values.OrderBy(a => a.Symbol))
|
||||
{
|
||||
var mapping = StrategiesService.GetAssetMapping(asset.Symbol);
|
||||
var engineStatus = StrategiesService.GetEngineStatus(asset.Symbol);
|
||||
var isActive = mapping?.IsActive ?? false;
|
||||
|
||||
<div class="asset-control-card @(isActive ? "active" : "")">
|
||||
<div class="asset-header">
|
||||
<div class="asset-info">
|
||||
<h3>@asset.Symbol</h3>
|
||||
<span class="asset-name">@asset.Name</span>
|
||||
</div>
|
||||
<div class="asset-controls">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox"
|
||||
checked="@isActive"
|
||||
@onchange="(e) => ToggleAsset(asset.Symbol, (bool)e.Value!)"
|
||||
disabled="@((mapping?.StrategyIds.Count ?? 0) == 0)" />
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (mapping != null && mapping.StrategyIds.Count > 0)
|
||||
{
|
||||
<div class="assigned-strategies">
|
||||
<h4>Strategie Assegnate (@mapping.StrategyIds.Count)</h4>
|
||||
<div class="strategies-list">
|
||||
@foreach (var strategyId in mapping.StrategyIds)
|
||||
{
|
||||
var strategyInfo = availableStrategies[strategyId];
|
||||
<div class="strategy-badge">
|
||||
<span class="strategy-name">@strategyInfo.Name</span>
|
||||
<span class="strategy-category">@strategyInfo.Category</span>
|
||||
<button class="btn-remove" @onclick="() => RemoveStrategy(asset.Symbol, strategyId)">
|
||||
<span class="bi bi-x"></span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (engineStatus?.LastDecision != null && isActive)
|
||||
{
|
||||
<div class="last-decision">
|
||||
<h4>Ultima Decisione</h4>
|
||||
<div class="decision-info">
|
||||
<span class="decision-type type-@engineStatus.LastDecision.Decision.ToString().ToLower()">
|
||||
@engineStatus.LastDecision.Decision
|
||||
</span>
|
||||
<span class="decision-confidence">
|
||||
Confidenza: @engineStatus.LastDecision.Confidence.ToString("F0")%
|
||||
</span>
|
||||
</div>
|
||||
<div class="decision-reason">@engineStatus.LastDecision.Reason</div>
|
||||
<div class="decision-votes">
|
||||
<span class="vote buy">Buy: @engineStatus.LastDecision.BuyVotes</span>
|
||||
<span class="vote sell">Sell: @engineStatus.LastDecision.SellVotes</span>
|
||||
<span class="vote hold">Hold: @engineStatus.LastDecision.HoldVotes</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<div class="asset-actions">
|
||||
<button class="btn-primary" @onclick="() => OpenStrategySelector(asset.Symbol, asset.Name)">
|
||||
<span class="bi bi-plus-lg"></span>
|
||||
@((mapping?.StrategyIds.Count ?? 0) > 0 ? "Gestisci Strategie" : "Assegna Strategie")
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Strategy Selector Modal -->
|
||||
@if (showStrategySelector)
|
||||
{
|
||||
<div class="modal-overlay" @onclick="CloseStrategySelector">
|
||||
<div class="modal-dialog large" @onclick:stopPropagation="true">
|
||||
<div class="modal-header">
|
||||
<h3>Gestisci Strategie - @selectedAssetSymbol</h3>
|
||||
<button class="btn-close" @onclick="CloseStrategySelector">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="strategies-selector">
|
||||
@foreach (var category in availableStrategies.Values.Select(s => s.Category).Distinct().OrderBy(c => c))
|
||||
{
|
||||
<div class="category-section">
|
||||
<h4 class="category-title">@category</h4>
|
||||
<div class="category-strategies">
|
||||
@foreach (var strategy in availableStrategies.Values.Where(s => s.Category == category))
|
||||
{
|
||||
var isSelected = selectedStrategies.Contains(strategy.Id);
|
||||
<div class="strategy-option @(isSelected ? "selected" : "")"
|
||||
@onclick="() => ToggleStrategy(strategy.Id)">
|
||||
<div class="strategy-option-header">
|
||||
<div class="strategy-checkbox">
|
||||
<input type="checkbox" checked="@isSelected" />
|
||||
</div>
|
||||
<div class="strategy-details">
|
||||
<h5>@strategy.Name</h5>
|
||||
<p>@strategy.Description</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="strategy-meta">
|
||||
<span class="risk-badge risk-@strategy.RiskLevel.ToString().ToLower()">
|
||||
@strategy.RiskLevel Risk
|
||||
</span>
|
||||
<span class="timeframe-badge">
|
||||
@strategy.RecommendedTimeFrame
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" @onclick="CloseStrategySelector">Annulla</button>
|
||||
<button class="btn-primary" @onclick="SaveStrategies">
|
||||
<span class="bi bi-check-lg"></span>
|
||||
Salva (@selectedStrategies.Count strategie)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.trading-control-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.header-stats {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.assets-control-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.asset-control-card {
|
||||
background: #1a1f3a;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.asset-control-card.active {
|
||||
border-color: rgba(16, 185, 129, 0.5);
|
||||
background: linear-gradient(135deg, #1a1f3a 0%, rgba(16, 185, 129, 0.05) 100%);
|
||||
}
|
||||
|
||||
.asset-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.asset-info h3 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #e2e8f0;
|
||||
margin: 0 0 0.25rem 0;
|
||||
}
|
||||
|
||||
.asset-name {
|
||||
color: #94a3b8;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.assigned-strategies {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.assigned-strategies h4 {
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.strategies-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.strategy-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.strategy-name {
|
||||
flex: 1;
|
||||
font-weight: 600;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.strategy-category {
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
border-radius: 0.25rem;
|
||||
color: #6366f1;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.btn-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #ef4444;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.25rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-remove:hover {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.last-decision {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.last-decision h4 {
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.decision-info {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.decision-type {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.type-buy { background: rgba(16, 185, 129, 0.2); color: #10b981; }
|
||||
.type-sell { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
|
||||
.type-hold { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
|
||||
|
||||
.decision-confidence {
|
||||
color: #94a3b8;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.decision-reason {
|
||||
color: #cbd5e1;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.decision-votes {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.vote {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.vote.buy { background: rgba(16, 185, 129, 0.1); color: #10b981; }
|
||||
.vote.sell { background: rgba(239, 68, 68, 0.1); color: #ef4444; }
|
||||
.vote.hold { background: rgba(245, 158, 11, 0.1); color: #f59e0b; }
|
||||
|
||||
.asset-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-dialog.large {
|
||||
max-width: 900px;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.strategies-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.category-section {
|
||||
border-bottom: 1px solid rgba(99, 102, 241, 0.1);
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.category-section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: #6366f1;
|
||||
margin-bottom: 1rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.category-strategies {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.strategy-option {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 2px solid rgba(99, 102, 241, 0.2);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.strategy-option:hover {
|
||||
border-color: rgba(99, 102, 241, 0.5);
|
||||
background: rgba(99, 102, 241, 0.05);
|
||||
}
|
||||
|
||||
.strategy-option.selected {
|
||||
border-color: #6366f1;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.strategy-option-header {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.strategy-checkbox input {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.strategy-details h5 {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: #e2e8f0;
|
||||
margin: 0 0 0.25rem 0;
|
||||
}
|
||||
|
||||
.strategy-details p {
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.strategy-meta {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.risk-badge, .timeframe-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.risk-low { background: rgba(16, 185, 129, 0.2); color: #10b981; }
|
||||
.risk-medium { background: rgba(245, 158, 11, 0.2); color: #f59e0b; }
|
||||
.risk-high { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
|
||||
.risk-veryhigh { background: rgba(220, 38, 38, 0.3); color: #dc2626; }
|
||||
|
||||
.timeframe-badge {
|
||||
background: rgba(139, 92, 246, 0.2);
|
||||
color: #8b5cf6;
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
private Dictionary<string, StrategyInfo> availableStrategies = new();
|
||||
private bool showStrategySelector = false;
|
||||
private string selectedAssetSymbol = "";
|
||||
private string selectedAssetName = "";
|
||||
private List<string> selectedStrategies = new();
|
||||
private int activeAssets = 0;
|
||||
private int totalStrategies = 0;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
LoadData();
|
||||
StrategiesService.OnMappingsChanged += HandleMappingsChanged;
|
||||
}
|
||||
|
||||
private void LoadData()
|
||||
{
|
||||
availableStrategies = StrategiesService.GetAvailableStrategies().ToDictionary(k => k.Key, v => v.Value);
|
||||
|
||||
var mappings = StrategiesService.GetAllMappings();
|
||||
activeAssets = mappings.Values.Count(m => m.IsActive);
|
||||
totalStrategies = mappings.Values.Sum(m => m.StrategyIds.Count);
|
||||
}
|
||||
|
||||
private void ToggleAsset(string symbol, bool active)
|
||||
{
|
||||
if (active)
|
||||
{
|
||||
StrategiesService.ActivateAsset(symbol);
|
||||
}
|
||||
else
|
||||
{
|
||||
StrategiesService.DeactivateAsset(symbol);
|
||||
}
|
||||
LoadData();
|
||||
}
|
||||
|
||||
private void OpenStrategySelector(string symbol, string name)
|
||||
{
|
||||
selectedAssetSymbol = symbol;
|
||||
selectedAssetName = name;
|
||||
|
||||
var mapping = StrategiesService.GetAssetMapping(symbol);
|
||||
selectedStrategies = mapping?.StrategyIds.ToList() ?? new List<string>();
|
||||
|
||||
showStrategySelector = true;
|
||||
}
|
||||
|
||||
private void CloseStrategySelector()
|
||||
{
|
||||
showStrategySelector = false;
|
||||
selectedStrategies.Clear();
|
||||
}
|
||||
|
||||
private void ToggleStrategy(string strategyId)
|
||||
{
|
||||
if (selectedStrategies.Contains(strategyId))
|
||||
{
|
||||
selectedStrategies.Remove(strategyId);
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedStrategies.Add(strategyId);
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveStrategies()
|
||||
{
|
||||
StrategiesService.AssignStrategiesToAsset(selectedAssetSymbol, selectedAssetName, selectedStrategies);
|
||||
CloseStrategySelector();
|
||||
LoadData();
|
||||
}
|
||||
|
||||
private void RemoveStrategy(string symbol, string strategyId)
|
||||
{
|
||||
StrategiesService.RemoveStrategyFromAsset(symbol, strategyId);
|
||||
LoadData();
|
||||
}
|
||||
|
||||
private void HandleMappingsChanged()
|
||||
{
|
||||
LoadData();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
StrategiesService.OnMappingsChanged -= HandleMappingsChanged;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user