Aggiunta Bootstrap 5.3.3 (CSS, JS, RTL, mappe) al progetto
Sono stati aggiunti tutti i file principali di Bootstrap 5.3.3, inclusi CSS, JavaScript (bundle, ESM, UMD, minificati), versioni RTL, utility, reboot, griglia e relative mappe delle sorgenti. Questi file abilitano un sistema di design moderno, responsive e accessibile, con supporto per layout LTR e RTL, debugging avanzato tramite source map e tutte le funzionalità di Bootstrap per lo sviluppo dell’interfaccia utente. Nessuna modifica ai file esistenti.
This commit is contained in:
24
TradingBot/Components/App.razor
Normal file
24
TradingBot/Components/App.razor
Normal file
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="/" />
|
||||
<ResourcePreloader />
|
||||
<link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" />
|
||||
<link rel="stylesheet" href="@Assets["app.css"]" />
|
||||
<link rel="stylesheet" href="@Assets["TradingBot.styles.css"]" />
|
||||
<ImportMap />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<HeadOutlet />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes />
|
||||
<ReconnectModal />
|
||||
<script src="@Assets["_framework/blazor.web.js"]"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
210
TradingBot/Components/Layout/MainLayout.razor
Normal file
210
TradingBot/Components/Layout/MainLayout.razor
Normal file
@@ -0,0 +1,210 @@
|
||||
@inherits LayoutComponentBase
|
||||
@using TradingBot.Services
|
||||
@using TradingBot.Models
|
||||
@inject TradingBotService BotService
|
||||
@inject SettingsService SettingsService
|
||||
@inject NavigationManager Navigation
|
||||
@implements IDisposable
|
||||
|
||||
<div class="trading-bot-layout @(sidebarCollapsed ? "collapsed" : "expanded")">
|
||||
<!-- Modern Vertical Sidebar -->
|
||||
<aside class="modern-sidebar">
|
||||
<!-- Brand Section -->
|
||||
<div class="sidebar-brand">
|
||||
<div class="brand-container @(sidebarCollapsed ? "minimized" : "")">
|
||||
<div class="brand-logo">
|
||||
<span class="logo-icon bi bi-graph-up-arrow"></span>
|
||||
</div>
|
||||
@if (!sidebarCollapsed)
|
||||
{
|
||||
<div class="brand-info">
|
||||
<h1 class="brand-title">Trading<span class="accent">Bot</span></h1>
|
||||
<div class="status-badge @(isRunning ? "online" : "offline")">
|
||||
<span class="status-indicator"></span>
|
||||
<span class="status-text">@(isRunning ? "ATTIVO" : "OFFLINE")</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<button class="collapse-btn" @onclick="ToggleSidebar" title="@(sidebarCollapsed ? "Espandi" : "Minimizza")">
|
||||
<span class="bi bi-chevron-@(sidebarCollapsed ? "right" : "left")"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Menu -->
|
||||
<nav class="sidebar-menu">
|
||||
<NavLink class="menu-item" href="/" Match="NavLinkMatch.All" title="Dashboard">
|
||||
<span class="item-icon bi bi-speedometer2"></span>
|
||||
@if (!sidebarCollapsed)
|
||||
{
|
||||
<span class="item-text">Dashboard</span>
|
||||
}
|
||||
</NavLink>
|
||||
|
||||
<NavLink class="menu-item" href="/strategies" title="Strategie">
|
||||
<span class="item-icon bi bi-diagram-3"></span>
|
||||
@if (!sidebarCollapsed)
|
||||
{
|
||||
<span class="item-text">Strategie</span>
|
||||
}
|
||||
</NavLink>
|
||||
|
||||
<NavLink class="menu-item" href="/assets" title="Asset">
|
||||
<span class="item-icon bi bi-coin"></span>
|
||||
@if (!sidebarCollapsed)
|
||||
{
|
||||
<span class="item-text">Asset</span>
|
||||
}
|
||||
</NavLink>
|
||||
|
||||
<NavLink class="menu-item" href="/trading" title="Trading">
|
||||
<span class="item-icon bi bi-graph-up-arrow"></span>
|
||||
@if (!sidebarCollapsed)
|
||||
{
|
||||
<span class="item-text">Trading</span>
|
||||
}
|
||||
</NavLink>
|
||||
|
||||
<NavLink class="menu-item" href="/market" title="Analisi Mercato">
|
||||
<span class="item-icon bi bi-bar-chart-line"></span>
|
||||
@if (!sidebarCollapsed)
|
||||
{
|
||||
<span class="item-text">Analisi Mercato</span>
|
||||
}
|
||||
</NavLink>
|
||||
|
||||
<NavLink class="menu-item" href="/statistics" title="Statistiche">
|
||||
<span class="item-icon bi bi-graph-up"></span>
|
||||
@if (!sidebarCollapsed)
|
||||
{
|
||||
<span class="item-text">Statistiche</span>
|
||||
}
|
||||
</NavLink>
|
||||
|
||||
<NavLink class="menu-item" href="/settings" title="Impostazioni">
|
||||
<span class="item-icon bi bi-gear"></span>
|
||||
@if (!sidebarCollapsed)
|
||||
{
|
||||
<span class="item-text">Impostazioni</span>
|
||||
}
|
||||
</NavLink>
|
||||
</nav>
|
||||
|
||||
<!-- Portfolio Summary (quando espanso) -->
|
||||
@if (!sidebarCollapsed)
|
||||
{
|
||||
<div class="sidebar-summary">
|
||||
<div class="summary-card">
|
||||
<div class="summary-row">
|
||||
<span class="summary-title">Portfolio</span>
|
||||
<span class="summary-amount">$@portfolioValue.ToString("N0")</span>
|
||||
</div>
|
||||
<div class="summary-row">
|
||||
<span class="summary-title">Profitto</span>
|
||||
<span class="summary-amount @(totalProfit >= 0 ? "profit" : "loss")">
|
||||
$@totalProfit.ToString("N2")
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</aside>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<div class="main-area">
|
||||
<!-- Top Header Bar -->
|
||||
<header class="content-header">
|
||||
<div class="header-left">
|
||||
<!-- Placeholder for page title -->
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button class="header-btn notifications" title="Notifiche">
|
||||
<span class="bi bi-bell"></span>
|
||||
</button>
|
||||
<button class="header-btn bot-control @(isRunning ? "running" : "stopped")" @onclick="ToggleBot">
|
||||
<span class="bi bi-@(isRunning ? "pause" : "play")-circle-fill"></span>
|
||||
<span class="btn-label">@(isRunning ? "Stop" : "Avvia")</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Page Content -->
|
||||
<main class="page-content">
|
||||
@Body
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool sidebarCollapsed = false;
|
||||
private bool isRunning => BotService.Status.IsRunning;
|
||||
private decimal portfolioValue = 0;
|
||||
private decimal totalProfit = 0;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
var settings = SettingsService.GetSettings();
|
||||
sidebarCollapsed = settings.SidebarCollapsed;
|
||||
|
||||
BotService.OnStatusChanged += HandleUpdate;
|
||||
BotService.OnPriceUpdated += HandlePriceUpdate;
|
||||
SettingsService.OnSettingsChanged += HandleSettingsChanged;
|
||||
|
||||
UpdateStats();
|
||||
|
||||
if (settings.AutoStartBot && !BotService.Status.IsRunning)
|
||||
{
|
||||
BotService.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleSidebar()
|
||||
{
|
||||
sidebarCollapsed = !sidebarCollapsed;
|
||||
SettingsService.UpdateSetting(nameof(AppSettings.SidebarCollapsed), sidebarCollapsed);
|
||||
StateHasChanged(); // Force immediate UI update
|
||||
Console.WriteLine($"Sidebar toggled: collapsed={sidebarCollapsed}"); // Debug log
|
||||
}
|
||||
|
||||
private void ToggleBot()
|
||||
{
|
||||
if (isRunning)
|
||||
BotService.Stop();
|
||||
else
|
||||
BotService.Start();
|
||||
}
|
||||
|
||||
private void UpdateStats()
|
||||
{
|
||||
portfolioValue = BotService.AssetConfigurations.Values.Sum(c =>
|
||||
c.CurrentBalance + (c.CurrentHoldings * (BotService.GetLatestPrice(c.Symbol)?.Price ?? 0)));
|
||||
|
||||
totalProfit = BotService.AssetConfigurations.Values.Sum(c => c.TotalProfit);
|
||||
}
|
||||
|
||||
private void HandleUpdate()
|
||||
{
|
||||
UpdateStats();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private void HandlePriceUpdate(string symbol, MarketPrice price)
|
||||
{
|
||||
UpdateStats();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private void HandleSettingsChanged()
|
||||
{
|
||||
var settings = SettingsService.GetSettings();
|
||||
sidebarCollapsed = settings.SidebarCollapsed;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
BotService.OnStatusChanged -= HandleUpdate;
|
||||
BotService.OnPriceUpdated -= HandlePriceUpdate;
|
||||
SettingsService.OnSettingsChanged -= HandleSettingsChanged;
|
||||
}
|
||||
}
|
||||
468
TradingBot/Components/Layout/MainLayout.razor.css
Normal file
468
TradingBot/Components/Layout/MainLayout.razor.css
Normal file
@@ -0,0 +1,468 @@
|
||||
/* ==============================================
|
||||
TRADING BOT LAYOUT - Modern Vertical Sidebar
|
||||
Global Styles (usando ::deep per scoped CSS)
|
||||
============================================== */
|
||||
|
||||
/* Layout Container */
|
||||
::deep .trading-bot-layout {
|
||||
display: flex !important;
|
||||
min-height: 100vh !important;
|
||||
background: #0a0e27 !important;
|
||||
color: #e2e8f0 !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
/* ==============================================
|
||||
MODERN SIDEBAR
|
||||
============================================== */
|
||||
|
||||
::deep .modern-sidebar {
|
||||
width: 280px !important;
|
||||
background: linear-gradient(180deg, #1a1f3a 0%, #0f1629 100%) !important;
|
||||
border-right: 1px solid rgba(99, 102, 241, 0.15) !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
position: fixed !important;
|
||||
left: 0 !important;
|
||||
top: 0 !important;
|
||||
bottom: 0 !important;
|
||||
z-index: 1000 !important;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
box-shadow: 4px 0 20px rgba(0, 0, 0, 0.2) !important;
|
||||
}
|
||||
|
||||
::deep .trading-bot-layout.collapsed .modern-sidebar {
|
||||
width: 80px !important;
|
||||
}
|
||||
|
||||
/* Brand Section */
|
||||
::deep .sidebar-brand {
|
||||
padding: 1.75rem 1.5rem !important;
|
||||
border-bottom: 1px solid rgba(99, 102, 241, 0.1) !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: space-between !important;
|
||||
gap: 1rem !important;
|
||||
}
|
||||
|
||||
::deep .trading-bot-layout.collapsed .sidebar-brand {
|
||||
padding: 1.5rem 0.75rem !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
::deep .brand-container {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 1rem !important;
|
||||
flex: 1 !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
::deep .brand-container.minimized {
|
||||
justify-content: center !important;
|
||||
flex: initial !important;
|
||||
}
|
||||
|
||||
::deep .brand-logo {
|
||||
width: 3.5rem !important;
|
||||
height: 3.5rem !important;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important;
|
||||
border-radius: 1rem !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
flex-shrink: 0 !important;
|
||||
box-shadow: 0 8px 16px rgba(99, 102, 241, 0.3) !important;
|
||||
}
|
||||
|
||||
::deep .trading-bot-layout.collapsed .brand-logo {
|
||||
width: 3rem !important;
|
||||
height: 3rem !important;
|
||||
}
|
||||
|
||||
::deep .logo-icon {
|
||||
font-size: 1.75rem !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
::deep .brand-info {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
gap: 0.5rem !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
::deep .brand-title {
|
||||
font-size: 1.5rem !important;
|
||||
font-weight: 700 !important;
|
||||
color: white !important;
|
||||
margin: 0 !important;
|
||||
line-height: 1 !important;
|
||||
}
|
||||
|
||||
::deep .brand-title .accent {
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important;
|
||||
-webkit-background-clip: text !important;
|
||||
-webkit-text-fill-color: transparent !important;
|
||||
background-clip: text !important;
|
||||
}
|
||||
|
||||
::deep .status-badge {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 0.5rem !important;
|
||||
padding: 0.25rem 0.75rem !important;
|
||||
background: rgba(71, 85, 105, 0.3) !important;
|
||||
border-radius: 1rem !important;
|
||||
width: fit-content !important;
|
||||
}
|
||||
|
||||
::deep .status-badge.online {
|
||||
background: rgba(16, 185, 129, 0.15) !important;
|
||||
}
|
||||
|
||||
::deep .status-indicator {
|
||||
width: 0.5rem !important;
|
||||
height: 0.5rem !important;
|
||||
border-radius: 50% !important;
|
||||
background: #64748b !important;
|
||||
}
|
||||
|
||||
::deep .status-badge.online .status-indicator {
|
||||
background: #10b981 !important;
|
||||
box-shadow: 0 0 8px rgba(16, 185, 129, 0.6) !important;
|
||||
animation: pulse-indicator 2s ease-in-out infinite !important;
|
||||
}
|
||||
|
||||
@keyframes pulse-indicator {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.7; transform: scale(1.1); }
|
||||
}
|
||||
|
||||
::deep .status-text {
|
||||
font-size: 0.625rem !important;
|
||||
font-weight: 700 !important;
|
||||
text-transform: uppercase !important;
|
||||
letter-spacing: 0.05em !important;
|
||||
color: #64748b !important;
|
||||
}
|
||||
|
||||
::deep .status-badge.online .status-text {
|
||||
color: #10b981 !important;
|
||||
}
|
||||
|
||||
::deep .collapse-btn {
|
||||
width: 2.25rem !important;
|
||||
height: 2.25rem !important;
|
||||
border-radius: 0.625rem !important;
|
||||
border: none !important;
|
||||
background: rgba(99, 102, 241, 0.1) !important;
|
||||
color: #6366f1 !important;
|
||||
cursor: pointer !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
transition: all 0.2s ease !important;
|
||||
font-size: 1rem !important;
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
|
||||
::deep .collapse-btn:hover {
|
||||
background: rgba(99, 102, 241, 0.2) !important;
|
||||
transform: scale(1.05) !important;
|
||||
}
|
||||
|
||||
/* Navigation Menu */
|
||||
::deep .sidebar-menu {
|
||||
flex: 1 !important;
|
||||
padding: 1.5rem 0 !important;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
::deep .menu-item {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 1rem !important;
|
||||
padding: 1rem 1.5rem !important;
|
||||
color: #94a3b8 !important;
|
||||
text-decoration: none !important;
|
||||
transition: all 0.2s ease !important;
|
||||
border-left: 3px solid transparent !important;
|
||||
font-weight: 600 !important;
|
||||
font-size: 0.938rem !important;
|
||||
position: relative !important;
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
::deep .trading-bot-layout.collapsed .menu-item {
|
||||
justify-content: center !important;
|
||||
padding: 1rem 0 !important;
|
||||
}
|
||||
|
||||
::deep .menu-item:hover {
|
||||
background: rgba(99, 102, 241, 0.08) !important;
|
||||
color: #cbd5e1 !important;
|
||||
border-left-color: rgba(99, 102, 241, 0.3) !important;
|
||||
}
|
||||
|
||||
::deep .menu-item.active {
|
||||
background: rgba(99, 102, 241, 0.12) !important;
|
||||
border-left-color: #6366f1 !important;
|
||||
color: #6366f1 !important;
|
||||
}
|
||||
|
||||
::deep .menu-item.active::before {
|
||||
content: '' !important;
|
||||
position: absolute !important;
|
||||
right: 0 !important;
|
||||
top: 50% !important;
|
||||
transform: translateY(-50%) !important;
|
||||
width: 3px !important;
|
||||
height: 60% !important;
|
||||
background: #6366f1 !important;
|
||||
border-radius: 3px 0 0 3px !important;
|
||||
}
|
||||
|
||||
::deep .item-icon {
|
||||
font-size: 1.375rem !important;
|
||||
flex-shrink: 0 !important;
|
||||
transition: transform 0.2s ease !important;
|
||||
}
|
||||
|
||||
::deep .menu-item:hover .item-icon {
|
||||
transform: scale(1.1) !important;
|
||||
}
|
||||
|
||||
::deep .item-text {
|
||||
white-space: nowrap !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
}
|
||||
|
||||
/* Portfolio Summary */
|
||||
::deep .sidebar-summary {
|
||||
padding: 1.5rem !important;
|
||||
border-top: 1px solid rgba(99, 102, 241, 0.1) !important;
|
||||
}
|
||||
|
||||
::deep .summary-card {
|
||||
padding: 1.25rem !important;
|
||||
background: rgba(99, 102, 241, 0.08) !important;
|
||||
border-radius: 0.75rem !important;
|
||||
border: 1px solid rgba(99, 102, 241, 0.1) !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
gap: 0.875rem !important;
|
||||
}
|
||||
|
||||
::deep .summary-row {
|
||||
display: flex !important;
|
||||
justify-content: space-between !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
::deep .summary-title {
|
||||
font-size: 0.75rem !important;
|
||||
color: #64748b !important;
|
||||
font-weight: 600 !important;
|
||||
text-transform: uppercase !important;
|
||||
letter-spacing: 0.05em !important;
|
||||
}
|
||||
|
||||
::deep .summary-amount {
|
||||
font-size: 1rem !important;
|
||||
font-weight: 700 !important;
|
||||
color: white !important;
|
||||
font-family: 'Courier New', monospace !important;
|
||||
}
|
||||
|
||||
::deep .summary-amount.profit {
|
||||
color: #10b981 !important;
|
||||
}
|
||||
|
||||
::deep .summary-amount.loss {
|
||||
color: #ef4444 !important;
|
||||
}
|
||||
|
||||
/* ==============================================
|
||||
MAIN CONTENT AREA
|
||||
============================================== */
|
||||
|
||||
::deep .main-area {
|
||||
flex: 1 !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
margin-left: 280px !important;
|
||||
transition: margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
min-height: 100vh !important;
|
||||
}
|
||||
|
||||
::deep .trading-bot-layout.collapsed .main-area {
|
||||
margin-left: 80px !important;
|
||||
}
|
||||
|
||||
/* Content Header */
|
||||
::deep .content-header {
|
||||
background: #0f1629 !important;
|
||||
border-bottom: 1px solid rgba(99, 102, 241, 0.1) !important;
|
||||
padding: 1.25rem 2rem !important;
|
||||
display: flex !important;
|
||||
justify-content: space-between !important;
|
||||
align-items: center !important;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15) !important;
|
||||
position: sticky !important;
|
||||
top: 0 !important;
|
||||
z-index: 100 !important;
|
||||
}
|
||||
|
||||
::deep .header-left {
|
||||
flex: 1 !important;
|
||||
}
|
||||
|
||||
::deep .header-right {
|
||||
display: flex !important;
|
||||
gap: 1rem !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
::deep .header-btn {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 0.625rem !important;
|
||||
padding: 0.75rem 1.25rem !important;
|
||||
border-radius: 0.625rem !important;
|
||||
border: 1px solid #334155 !important;
|
||||
background: #1a1f3a !important;
|
||||
color: #cbd5e1 !important;
|
||||
cursor: pointer !important;
|
||||
transition: all 0.2s ease !important;
|
||||
font-size: 0.875rem !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
::deep .header-btn:hover {
|
||||
background: #1e293b !important;
|
||||
border-color: #475569 !important;
|
||||
transform: translateY(-1px) !important;
|
||||
}
|
||||
|
||||
::deep .header-btn.notifications {
|
||||
padding: 0.75rem !important;
|
||||
}
|
||||
|
||||
::deep .header-btn.notifications .bi {
|
||||
font-size: 1.125rem !important;
|
||||
}
|
||||
|
||||
::deep .header-btn.bot-control {
|
||||
padding: 0.75rem 1.5rem !important;
|
||||
}
|
||||
|
||||
::deep .header-btn.bot-control .bi {
|
||||
font-size: 1.125rem !important;
|
||||
}
|
||||
|
||||
::deep .header-btn.bot-control.running {
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important;
|
||||
border-color: #6366f1 !important;
|
||||
color: white !important;
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3) !important;
|
||||
}
|
||||
|
||||
::deep .header-btn.bot-control.running:hover {
|
||||
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4) !important;
|
||||
}
|
||||
|
||||
::deep .btn-label {
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
/* Page Content */
|
||||
::deep .page-content {
|
||||
flex: 1 !important;
|
||||
padding: 2rem !important;
|
||||
overflow-y: auto !important;
|
||||
background: #0a0e27 !important;
|
||||
}
|
||||
|
||||
/* Scrollbar Styling */
|
||||
::deep .sidebar-menu::-webkit-scrollbar,
|
||||
::deep .page-content::-webkit-scrollbar {
|
||||
width: 0.375rem !important;
|
||||
}
|
||||
|
||||
::deep .sidebar-menu::-webkit-scrollbar-track,
|
||||
::deep .page-content::-webkit-scrollbar-track {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
::deep .sidebar-menu::-webkit-scrollbar-thumb,
|
||||
::deep .page-content::-webkit-scrollbar-thumb {
|
||||
background: #334155 !important;
|
||||
border-radius: 0.25rem !important;
|
||||
}
|
||||
|
||||
::deep .sidebar-menu::-webkit-scrollbar-thumb:hover,
|
||||
::deep .page-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #475569 !important;
|
||||
}
|
||||
|
||||
/* ==============================================
|
||||
RESPONSIVE DESIGN
|
||||
============================================== */
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
::deep .modern-sidebar {
|
||||
width: 260px !important;
|
||||
}
|
||||
|
||||
::deep .main-area {
|
||||
margin-left: 260px !important;
|
||||
}
|
||||
|
||||
::deep .trading-bot-layout.collapsed .modern-sidebar {
|
||||
width: 70px !important;
|
||||
}
|
||||
|
||||
::deep .trading-bot-layout.collapsed .main-area {
|
||||
margin-left: 70px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
::deep .modern-sidebar {
|
||||
transform: translateX(-100%) !important;
|
||||
width: 280px !important;
|
||||
}
|
||||
|
||||
::deep .trading-bot-layout.sidebar-open .modern-sidebar {
|
||||
transform: translateX(0) !important;
|
||||
}
|
||||
|
||||
::deep .main-area {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
::deep .content-header {
|
||||
padding: 1rem 1.5rem !important;
|
||||
}
|
||||
|
||||
::deep .page-content {
|
||||
padding: 1.5rem !important;
|
||||
}
|
||||
|
||||
::deep .header-btn.bot-control .btn-label {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
::deep .page-content {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
|
||||
::deep .sidebar-brand {
|
||||
padding: 1.5rem 1rem !important;
|
||||
}
|
||||
}
|
||||
31
TradingBot/Components/Layout/ReconnectModal.razor
Normal file
31
TradingBot/Components/Layout/ReconnectModal.razor
Normal file
@@ -0,0 +1,31 @@
|
||||
<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]"></script>
|
||||
|
||||
<dialog id="components-reconnect-modal" data-nosnippet>
|
||||
<div class="components-reconnect-container">
|
||||
<div class="components-rejoining-animation" aria-hidden="true">
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<p class="components-reconnect-first-attempt-visible">
|
||||
Rejoining the server...
|
||||
</p>
|
||||
<p class="components-reconnect-repeated-attempt-visible">
|
||||
Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds.
|
||||
</p>
|
||||
<p class="components-reconnect-failed-visible">
|
||||
Failed to rejoin.<br />Please retry or reload the page.
|
||||
</p>
|
||||
<button id="components-reconnect-button" class="components-reconnect-failed-visible">
|
||||
Retry
|
||||
</button>
|
||||
<p class="components-pause-visible">
|
||||
The session has been paused by the server.
|
||||
</p>
|
||||
<button id="components-resume-button" class="components-pause-visible">
|
||||
Resume
|
||||
</button>
|
||||
<p class="components-resume-failed-visible">
|
||||
Failed to resume the session.<br />Please reload the page.
|
||||
</p>
|
||||
</div>
|
||||
</dialog>
|
||||
157
TradingBot/Components/Layout/ReconnectModal.razor.css
Normal file
157
TradingBot/Components/Layout/ReconnectModal.razor.css
Normal file
@@ -0,0 +1,157 @@
|
||||
.components-reconnect-first-attempt-visible,
|
||||
.components-reconnect-repeated-attempt-visible,
|
||||
.components-reconnect-failed-visible,
|
||||
.components-pause-visible,
|
||||
.components-resume-failed-visible,
|
||||
.components-rejoining-animation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
|
||||
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
|
||||
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
|
||||
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
|
||||
#components-reconnect-modal.components-reconnect-retrying,
|
||||
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
|
||||
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
|
||||
#components-reconnect-modal.components-reconnect-failed,
|
||||
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
#components-reconnect-modal {
|
||||
background-color: white;
|
||||
width: 20rem;
|
||||
margin: 20vh auto;
|
||||
padding: 2rem;
|
||||
border: 0;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
|
||||
opacity: 0;
|
||||
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
|
||||
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
|
||||
&[open]
|
||||
|
||||
{
|
||||
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#components-reconnect-modal::backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-slideUp {
|
||||
0% {
|
||||
transform: translateY(30px) scale(0.95);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-fadeInOpacity {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes components-reconnect-modal-fadeOutOpacity {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.components-reconnect-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
#components-reconnect-modal p {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button {
|
||||
border: 0;
|
||||
background-color: #6b9ed2;
|
||||
color: white;
|
||||
padding: 4px 24px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button:hover {
|
||||
background-color: #3b6ea2;
|
||||
}
|
||||
|
||||
#components-reconnect-modal button:active {
|
||||
background-color: #6b9ed2;
|
||||
}
|
||||
|
||||
.components-rejoining-animation {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.components-rejoining-animation div {
|
||||
position: absolute;
|
||||
border: 3px solid #0087ff;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
|
||||
.components-rejoining-animation div:nth-child(2) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
|
||||
@keyframes components-rejoining-animation {
|
||||
0% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
4.9% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
5% {
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
63
TradingBot/Components/Layout/ReconnectModal.razor.js
Normal file
63
TradingBot/Components/Layout/ReconnectModal.razor.js
Normal file
@@ -0,0 +1,63 @@
|
||||
// Set up event handlers
|
||||
const reconnectModal = document.getElementById("components-reconnect-modal");
|
||||
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
|
||||
|
||||
const retryButton = document.getElementById("components-reconnect-button");
|
||||
retryButton.addEventListener("click", retry);
|
||||
|
||||
const resumeButton = document.getElementById("components-resume-button");
|
||||
resumeButton.addEventListener("click", resume);
|
||||
|
||||
function handleReconnectStateChanged(event) {
|
||||
if (event.detail.state === "show") {
|
||||
reconnectModal.showModal();
|
||||
} else if (event.detail.state === "hide") {
|
||||
reconnectModal.close();
|
||||
} else if (event.detail.state === "failed") {
|
||||
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
} else if (event.detail.state === "rejected") {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async function retry() {
|
||||
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
|
||||
try {
|
||||
// Reconnect will asynchronously return:
|
||||
// - true to mean success
|
||||
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
|
||||
// - exception to mean we didn't reach the server (this can be sync or async)
|
||||
const successful = await Blazor.reconnect();
|
||||
if (!successful) {
|
||||
// We have been able to reach the server, but the circuit is no longer available.
|
||||
// We'll reload the page so the user can continue using the app as quickly as possible.
|
||||
const resumeSuccessful = await Blazor.resumeCircuit();
|
||||
if (!resumeSuccessful) {
|
||||
location.reload();
|
||||
} else {
|
||||
reconnectModal.close();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// We got an exception, server is currently unavailable
|
||||
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
|
||||
}
|
||||
}
|
||||
|
||||
async function resume() {
|
||||
try {
|
||||
const successful = await Blazor.resumeCircuit();
|
||||
if (!successful) {
|
||||
location.reload();
|
||||
}
|
||||
} catch {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async function retryWhenDocumentBecomesVisible() {
|
||||
if (document.visibilityState === "visible") {
|
||||
await retry();
|
||||
}
|
||||
}
|
||||
341
TradingBot/Components/Pages/Assets.razor
Normal file
341
TradingBot/Components/Pages/Assets.razor
Normal file
@@ -0,0 +1,341 @@
|
||||
@page "/assets"
|
||||
@using TradingBot.Models
|
||||
@using TradingBot.Services
|
||||
@inject TradingBotService BotService
|
||||
@implements IDisposable
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Asset - TradingBot</PageTitle>
|
||||
|
||||
<div class="assets-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>Gestione Asset</h1>
|
||||
<p class="subtitle">Visualizza, configura e assegna strategie ai tuoi asset di trading</p>
|
||||
</div>
|
||||
<div class="header-controls">
|
||||
<div class="view-toggle">
|
||||
<button class="toggle-btn @(viewMode == "grid" ? "active" : "")" @onclick="@(() => viewMode = "grid")">
|
||||
<span class="bi bi-grid-3x3-gap"></span>
|
||||
</button>
|
||||
<button class="toggle-btn @(viewMode == "list" ? "active" : "")" @onclick="@(() => viewMode = "list")">
|
||||
<span class="bi bi-list-ul"></span>
|
||||
</button>
|
||||
</div>
|
||||
<select class="filter-select" @bind="filterStatus">
|
||||
<option value="all">Tutti gli Asset</option>
|
||||
<option value="active">Solo Attivi</option>
|
||||
<option value="inactive">Solo Inattivi</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary Stats -->
|
||||
<div class="assets-summary">
|
||||
<div class="summary-stat">
|
||||
<div class="stat-icon">
|
||||
<span class="bi bi-coin"></span>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<span class="stat-label">Totale Asset</span>
|
||||
<span class="stat-value">@totalAssets</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-stat success">
|
||||
<div class="stat-icon">
|
||||
<span class="bi bi-check-circle"></span>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<span class="stat-label">Asset Attivi</span>
|
||||
<span class="stat-value">@activeAssets</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-stat warning">
|
||||
<div class="stat-icon">
|
||||
<span class="bi bi-diagram-3"></span>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<span class="stat-label">Strategie Assegnate</span>
|
||||
<span class="stat-value">@assignedStrategies</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-stat info">
|
||||
<div class="stat-icon">
|
||||
<span class="bi bi-currency-dollar"></span>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<span class="stat-label">Valore Totale</span>
|
||||
<span class="stat-value">$@totalValue.ToString("N0")</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assets Grid/List -->
|
||||
@if (viewMode == "grid")
|
||||
{
|
||||
<div class="assets-grid">
|
||||
@foreach (var config in GetFilteredAssets())
|
||||
{
|
||||
var price = BotService.GetLatestPrice(config.Symbol);
|
||||
var stats = BotService.AssetStatistics.TryGetValue(config.Symbol, out var s) ? s : null;
|
||||
|
||||
<div class="asset-card @(config.IsEnabled ? "enabled" : "disabled")">
|
||||
<div class="asset-card-header">
|
||||
<div class="asset-info">
|
||||
<div class="asset-icon">@config.Symbol.Substring(0, 1)</div>
|
||||
<div class="asset-title">
|
||||
<h3>@config.Name</h3>
|
||||
<span class="asset-symbol">@config.Symbol</span>
|
||||
</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox"
|
||||
checked="@config.IsEnabled"
|
||||
@onchange="@((e) => ToggleAsset(config.Symbol, (bool)e.Value!))" />
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="asset-card-body">
|
||||
@if (price != null)
|
||||
{
|
||||
<div class="price-section">
|
||||
<div class="current-price">$@price.Price.ToString("N2")</div>
|
||||
<div class="price-change @(price.Change24h >= 0 ? "positive" : "negative")">
|
||||
<span class="bi @(price.Change24h >= 0 ? "bi-arrow-up" : "bi-arrow-down")"></span>
|
||||
@Math.Abs(price.Change24h).ToString("F2")% (24h)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="asset-metrics">
|
||||
<div class="metric">
|
||||
<span class="metric-label">Holdings</span>
|
||||
<span class="metric-value">@config.CurrentHoldings.ToString("F6")</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Valore</span>
|
||||
<span class="metric-value">$@((config.CurrentBalance + config.CurrentHoldings * (price?.Price ?? 0)).ToString("N2"))</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Profitto</span>
|
||||
<span class="metric-value @(config.TotalProfit >= 0 ? "profit" : "loss")">
|
||||
$@config.TotalProfit.ToString("N2")
|
||||
</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Trades</span>
|
||||
<span class="metric-value">@(stats?.TotalTrades ?? 0)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="strategy-section">
|
||||
<label class="strategy-label">Strategia Assegnata</label>
|
||||
<select class="strategy-select"
|
||||
value="@config.StrategyName"
|
||||
@onchange="@((e) => AssignStrategy(config.Symbol, e.Value?.ToString() ?? ""))">
|
||||
<option value="">Nessuna strategia</option>
|
||||
<option value="RSI + MACD Cross">RSI + MACD Cross</option>
|
||||
<option value="Media Mobile Semplice">Media Mobile Semplice</option>
|
||||
<option value="Scalping Veloce">Scalping Veloce</option>
|
||||
<option value="Trend Following">Trend Following</option>
|
||||
<option value="Mean Reversion">Mean Reversion</option>
|
||||
<option value="Conservative">Conservative</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="asset-card-footer">
|
||||
<button class="btn-secondary btn-sm" @onclick="@(() => OpenAssetDetails(config.Symbol))">
|
||||
<span class="bi bi-gear"></span>
|
||||
Configura
|
||||
</button>
|
||||
<button class="btn-secondary btn-sm" @onclick="@(() => ViewChart(config.Symbol))">
|
||||
<span class="bi bi-graph-up"></span>
|
||||
Grafico
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- List View -->
|
||||
<div class="assets-table">
|
||||
<div class="table-header">
|
||||
<div class="th">Asset</div>
|
||||
<div class="th">Prezzo</div>
|
||||
<div class="th">Var. 24h</div>
|
||||
<div class="th">Holdings</div>
|
||||
<div class="th">Valore</div>
|
||||
<div class="th">Profitto</div>
|
||||
<div class="th">Strategia</div>
|
||||
<div class="th">Stato</div>
|
||||
<div class="th">Azioni</div>
|
||||
</div>
|
||||
|
||||
@foreach (var config in GetFilteredAssets())
|
||||
{
|
||||
var price = BotService.GetLatestPrice(config.Symbol);
|
||||
var stats = BotService.AssetStatistics.TryGetValue(config.Symbol, out var s) ? s : null;
|
||||
|
||||
<div class="table-row @(config.IsEnabled ? "enabled" : "disabled")">
|
||||
<div class="cell-asset">
|
||||
<div class="asset-icon-small">@config.Symbol.Substring(0, 1)</div>
|
||||
<div>
|
||||
<div class="asset-name">@config.Name</div>
|
||||
<div class="asset-symbol-small">@config.Symbol</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
@if (price != null)
|
||||
{
|
||||
<span class="price-value">$@price.Price.ToString("N2")</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">-</span>
|
||||
}
|
||||
</div>
|
||||
<div class="cell">
|
||||
@if (price != null)
|
||||
{
|
||||
<span class="change-badge @(price.Change24h >= 0 ? "positive" : "negative")">
|
||||
@(price.Change24h >= 0 ? "+" : "")@price.Change24h.ToString("F2")%
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">-</span>
|
||||
}
|
||||
</div>
|
||||
<div class="cell">
|
||||
<span class="mono-value">@config.CurrentHoldings.ToString("F6")</span>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<span class="mono-value">$@((config.CurrentBalance + config.CurrentHoldings * (price?.Price ?? 0)).ToString("N2"))</span>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<span class="mono-value @(config.TotalProfit >= 0 ? "profit" : "loss")">
|
||||
$@config.TotalProfit.ToString("N2")
|
||||
</span>
|
||||
</div>
|
||||
<div class="cell-strategy">
|
||||
<select class="strategy-select-small"
|
||||
value="@config.StrategyName"
|
||||
@onchange="@((e) => AssignStrategy(config.Symbol, e.Value?.ToString() ?? ""))">
|
||||
<option value="">Nessuna</option>
|
||||
<option value="RSI + MACD Cross">RSI + MACD</option>
|
||||
<option value="Media Mobile Semplice">SMA</option>
|
||||
<option value="Scalping Veloce">Scalping</option>
|
||||
<option value="Trend Following">Trend</option>
|
||||
<option value="Mean Reversion">Mean Rev.</option>
|
||||
<option value="Conservative">Conserv.</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<label class="toggle-switch-small">
|
||||
<input type="checkbox"
|
||||
checked="@config.IsEnabled"
|
||||
@onchange="@((e) => ToggleAsset(config.Symbol, (bool)e.Value!))" />
|
||||
<span class="toggle-slider-small"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="cell-actions">
|
||||
<button class="btn-icon-small" title="Configura" @onclick="@(() => OpenAssetDetails(config.Symbol))">
|
||||
<span class="bi bi-gear"></span>
|
||||
</button>
|
||||
<button class="btn-icon-small" title="Grafico" @onclick="@(() => ViewChart(config.Symbol))">
|
||||
<span class="bi bi-graph-up"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string viewMode = "grid";
|
||||
private string filterStatus = "all";
|
||||
private int totalAssets = 0;
|
||||
private int activeAssets = 0;
|
||||
private int assignedStrategies = 0;
|
||||
private decimal totalValue = 0;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
BotService.OnStatusChanged += HandleUpdate;
|
||||
BotService.OnTradeExecuted += HandleTradeExecuted;
|
||||
BotService.OnPriceUpdated += HandlePriceUpdate;
|
||||
|
||||
RefreshData();
|
||||
}
|
||||
|
||||
private void RefreshData()
|
||||
{
|
||||
totalAssets = BotService.AssetConfigurations.Count;
|
||||
activeAssets = BotService.AssetConfigurations.Values.Count(c => c.IsEnabled);
|
||||
assignedStrategies = BotService.AssetConfigurations.Values.Count(c => !string.IsNullOrEmpty(c.StrategyName));
|
||||
|
||||
totalValue = BotService.AssetConfigurations.Values.Sum(c =>
|
||||
{
|
||||
var price = BotService.GetLatestPrice(c.Symbol);
|
||||
return c.CurrentBalance + (c.CurrentHoldings * (price?.Price ?? 0));
|
||||
});
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private IEnumerable<AssetConfiguration> GetFilteredAssets()
|
||||
{
|
||||
var assets = BotService.AssetConfigurations.Values.OrderBy(c => c.Symbol);
|
||||
|
||||
return filterStatus switch
|
||||
{
|
||||
"active" => assets.Where(c => c.IsEnabled),
|
||||
"inactive" => assets.Where(c => !c.IsEnabled),
|
||||
_ => assets
|
||||
};
|
||||
}
|
||||
|
||||
private void ToggleAsset(string symbol, bool enabled)
|
||||
{
|
||||
BotService.ToggleAsset(symbol, enabled);
|
||||
RefreshData();
|
||||
}
|
||||
|
||||
private void AssignStrategy(string symbol, string strategyName)
|
||||
{
|
||||
if (BotService.AssetConfigurations.TryGetValue(symbol, out var config))
|
||||
{
|
||||
config.StrategyName = strategyName;
|
||||
RefreshData();
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenAssetDetails(string symbol)
|
||||
{
|
||||
// TODO: Open modal or navigate to asset detail page
|
||||
}
|
||||
|
||||
private void ViewChart(string symbol)
|
||||
{
|
||||
var navManager = Navigation;
|
||||
navManager?.NavigateTo($"/market?symbol={symbol}");
|
||||
}
|
||||
|
||||
private void HandleUpdate() => InvokeAsync(RefreshData);
|
||||
private void HandleTradeExecuted(Trade trade) => InvokeAsync(RefreshData);
|
||||
private void HandlePriceUpdate(string symbol, MarketPrice price) => InvokeAsync(RefreshData);
|
||||
|
||||
[Inject] private NavigationManager? Navigation { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
BotService.OnStatusChanged -= HandleUpdate;
|
||||
BotService.OnTradeExecuted -= HandleTradeExecuted;
|
||||
BotService.OnPriceUpdated -= HandlePriceUpdate;
|
||||
}
|
||||
}
|
||||
597
TradingBot/Components/Pages/Assets.razor.css
Normal file
597
TradingBot/Components/Pages/Assets.razor.css
Normal file
@@ -0,0 +1,597 @@
|
||||
/* Assets Page */
|
||||
.assets-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
/* Page Header */
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0.5rem 0 0 0;
|
||||
color: #94a3b8;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* View Toggle */
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
background: #1a1f3a;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.toggle-btn.active {
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
padding: 0.625rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #334155;
|
||||
background: #1a1f3a;
|
||||
color: white;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Assets Summary */
|
||||
.assets-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.summary-stat {
|
||||
background: #0f1629;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.summary-stat .stat-icon {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 0.625rem;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
color: #6366f1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.summary-stat.success .stat-icon {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.summary-stat.warning .stat-icon {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.summary-stat.info .stat-icon {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* Assets Grid */
|
||||
.assets-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.asset-card {
|
||||
background: #0f1629;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.asset-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.4);
|
||||
border-color: #334155;
|
||||
}
|
||||
|
||||
.asset-card.enabled {
|
||||
border-color: rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.asset-card.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.asset-card-header {
|
||||
padding: 1.25rem;
|
||||
background: #1a1f3a;
|
||||
border-bottom: 1px solid #1e293b;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.asset-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.asset-icon {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 0.5rem;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.asset-title h3 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.asset-symbol {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* Toggle Switch */
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 3rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #334155;
|
||||
transition: 0.3s;
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.toggle-slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 1.125rem;
|
||||
width: 1.125rem;
|
||||
left: 0.1875rem;
|
||||
bottom: 0.1875rem;
|
||||
background-color: white;
|
||||
transition: 0.3s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + .toggle-slider {
|
||||
background-color: #6366f1;
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + .toggle-slider:before {
|
||||
transform: translateX(1.5rem);
|
||||
}
|
||||
|
||||
/* Asset Card Body */
|
||||
.asset-card-body {
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.price-section {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.current-price {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.price-change {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.price-change.positive {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.price-change.negative {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Asset Metrics */
|
||||
.asset-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.625rem;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.metric-value.profit {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.metric-value.loss {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Strategy Section */
|
||||
.strategy-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.strategy-label {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.strategy-select {
|
||||
padding: 0.625rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #334155;
|
||||
background: #1a1f3a;
|
||||
color: white;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.strategy-select:focus {
|
||||
outline: 2px solid #6366f1;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Asset Card Footer */
|
||||
.asset-card-footer {
|
||||
padding: 1rem 1.25rem;
|
||||
background: #0a0e27;
|
||||
border-top: 1px solid #1e293b;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Assets Table (List View) */
|
||||
.assets-table {
|
||||
background: #0f1629;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1.5fr 1fr 1.5fr 1.5fr 1.5fr 2fr 1fr 1.5fr;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.5rem;
|
||||
background: #1a1f3a;
|
||||
border-bottom: 1px solid #1e293b;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1.5fr 1fr 1.5fr 1.5fr 1.5fr 2fr 1fr 1.5fr;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid #1e293b;
|
||||
align-items: center;
|
||||
font-size: 0.875rem;
|
||||
color: #cbd5e1;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
background: #1a1f3a;
|
||||
}
|
||||
|
||||
.table-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table-row.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.cell-asset {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.asset-icon-small {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 0.375rem;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.asset-name {
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.asset-symbol-small {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.price-value,
|
||||
.mono-value {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.change-badge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.change-badge.positive {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.change-badge.negative {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.cell-strategy {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.strategy-select-small {
|
||||
width: 100%;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid #334155;
|
||||
background: #1a1f3a;
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Toggle Switch Small */
|
||||
.toggle-switch-small {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 2.5rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
|
||||
.toggle-switch-small input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toggle-slider-small {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #334155;
|
||||
transition: 0.3s;
|
||||
border-radius: 1.25rem;
|
||||
}
|
||||
|
||||
.toggle-slider-small:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 0.875rem;
|
||||
width: 0.875rem;
|
||||
left: 0.1875rem;
|
||||
bottom: 0.1875rem;
|
||||
background-color: white;
|
||||
transition: 0.3s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.toggle-switch-small input:checked + .toggle-slider-small {
|
||||
background-color: #6366f1;
|
||||
}
|
||||
|
||||
.toggle-switch-small input:checked + .toggle-slider-small:before {
|
||||
transform: translateX(1.25rem);
|
||||
}
|
||||
|
||||
.cell-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-icon-small {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 0.375rem;
|
||||
border: none;
|
||||
background: #1a1f3a;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-icon-small:hover {
|
||||
background: #1e293b;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1200px) {
|
||||
.table-header, .table-row {
|
||||
grid-template-columns: 2fr 1fr 1fr 1fr 1.5fr 1fr 1fr;
|
||||
}
|
||||
|
||||
/* Hide some columns on smaller screens */
|
||||
.table-header div:nth-child(4),
|
||||
.table-row div:nth-child(4),
|
||||
.table-header div:nth-child(6),
|
||||
.table-row div:nth-child(6) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.assets-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.assets-summary {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.assets-table {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
185
TradingBot/Components/Pages/Dashboard.razor
Normal file
185
TradingBot/Components/Pages/Dashboard.razor
Normal file
@@ -0,0 +1,185 @@
|
||||
@page "/"
|
||||
@using TradingBot.Models
|
||||
@using TradingBot.Services
|
||||
@inject TradingBotService BotService
|
||||
@inject NavigationManager Navigation
|
||||
@implements IDisposable
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Dashboard - TradingBot</PageTitle>
|
||||
|
||||
<div class="dashboard-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
<p class="subtitle">Panoramica completa delle performance e attività di trading</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary Cards -->
|
||||
<div class="summary-grid">
|
||||
<div class="summary-card primary">
|
||||
<div class="card-icon">
|
||||
<span class="bi bi-wallet2"></span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="card-label">Valore Portfolio</div>
|
||||
<div class="card-value">$@portfolioStats.TotalBalance.ToString("N2")</div>
|
||||
<div class="card-change @(portfolioStats.TotalProfitPercentage >= 0 ? "positive" : "negative")">
|
||||
<span class="bi @(portfolioStats.TotalProfitPercentage >= 0 ? "bi-arrow-up" : "bi-arrow-down")"></span>
|
||||
@Math.Abs(portfolioStats.TotalProfitPercentage).ToString("F2")%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="summary-card">
|
||||
<div class="card-icon success">
|
||||
<span class="bi bi-graph-up-arrow"></span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="card-label">Profitto Totale</div>
|
||||
<div class="card-value @(portfolioStats.TotalProfit >= 0 ? "profit" : "loss")">
|
||||
$@portfolioStats.TotalProfit.ToString("N2")
|
||||
</div>
|
||||
<div class="card-meta">Da $@portfolioStats.InitialBalance.ToString("N2")</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="summary-card">
|
||||
<div class="card-icon info">
|
||||
<span class="bi bi-arrow-left-right"></span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="card-label">Operazioni Totali</div>
|
||||
<div class="card-value">@portfolioStats.TotalTrades</div>
|
||||
<div class="card-meta">Win Rate: @portfolioStats.WinRate.ToString("F1")%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="summary-card">
|
||||
<div class="card-icon warning">
|
||||
<span class="bi bi-currency-exchange"></span>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="card-label">Asset Attivi</div>
|
||||
<div class="card-value">@portfolioStats.ActiveAssets/@portfolioStats.TotalAssets</div>
|
||||
<div class="card-meta">In trading</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Assets -->
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2>Asset Attivi</h2>
|
||||
<a href="/trading" class="btn-link">Vedi Tutti <span class="bi bi-arrow-right"></span></a>
|
||||
</div>
|
||||
|
||||
<div class="assets-quick-grid">
|
||||
@foreach (var config in BotService.AssetConfigurations.Values.Where(c => c.IsEnabled).Take(6))
|
||||
{
|
||||
var price = BotService.GetLatestPrice(config.Symbol);
|
||||
|
||||
<div class="asset-quick-card">
|
||||
<div class="asset-header">
|
||||
<span class="asset-symbol">@config.Symbol</span>
|
||||
@if (price != null)
|
||||
{
|
||||
<span class="asset-change @(price.Change24h >= 0 ? "positive" : "negative")">
|
||||
@price.Change24h.ToString("F2")%
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="asset-price">$@(price?.Price.ToString("N2") ?? "Loading...")</div>
|
||||
<div class="asset-profit @(config.TotalProfit >= 0 ? "profit" : "loss")">
|
||||
$@config.TotalProfit.ToString("N2")
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2>Attività Recente</h2>
|
||||
<a href="/trading" class="btn-link">Vedi Storico <span class="bi bi-arrow-right"></span></a>
|
||||
</div>
|
||||
|
||||
@if (BotService.Trades.Count == 0)
|
||||
{
|
||||
<div class="empty-state">
|
||||
<span class="bi bi-inbox"></span>
|
||||
<p>Nessuna operazione ancora</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="activity-list">
|
||||
@foreach (var trade in BotService.Trades.Take(8))
|
||||
{
|
||||
<div class="activity-item">
|
||||
<div class="activity-icon @(trade.Type == TradeType.Buy ? "buy" : "sell")">
|
||||
<span class="bi @(trade.Type == TradeType.Buy ? "bi-arrow-down-circle-fill" : "bi-arrow-up-circle-fill")"></span>
|
||||
</div>
|
||||
<div class="activity-content">
|
||||
<div class="activity-main">
|
||||
<span class="activity-type">@(trade.Type == TradeType.Buy ? "ACQUISTO" : "VENDITA")</span>
|
||||
<span class="activity-symbol">@trade.Symbol</span>
|
||||
@if (trade.IsBot)
|
||||
{
|
||||
<span class="bot-badge">
|
||||
<span class="bi bi-robot"></span> BOT
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="activity-details">
|
||||
<span>@trade.Amount.ToString("F6") @ $@trade.Price.ToString("N2")</span>
|
||||
<span class="separator">•</span>
|
||||
<span>@trade.Timestamp.ToLocalTime().ToString("HH:mm:ss")</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="activity-value">
|
||||
$@((trade.Amount * trade.Price).ToString("N2"))
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private PortfolioStatistics portfolioStats = new();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
BotService.OnStatusChanged += HandleUpdate;
|
||||
BotService.OnTradeExecuted += HandleTradeExecuted;
|
||||
BotService.OnPriceUpdated += HandlePriceUpdate;
|
||||
|
||||
if (!BotService.Status.IsRunning)
|
||||
{
|
||||
BotService.Start();
|
||||
}
|
||||
|
||||
RefreshData();
|
||||
}
|
||||
|
||||
private void RefreshData()
|
||||
{
|
||||
portfolioStats = BotService.GetPortfolioStatistics();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void HandleUpdate() => InvokeAsync(RefreshData);
|
||||
private void HandleTradeExecuted(Trade trade) => InvokeAsync(RefreshData);
|
||||
private void HandlePriceUpdate(string symbol, MarketPrice price) => InvokeAsync(RefreshData);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
BotService.OnStatusChanged -= HandleUpdate;
|
||||
BotService.OnTradeExecuted -= HandleTradeExecuted;
|
||||
BotService.OnPriceUpdated -= HandlePriceUpdate;
|
||||
}
|
||||
}
|
||||
362
TradingBot/Components/Pages/Dashboard.razor.css
Normal file
362
TradingBot/Components/Pages/Dashboard.razor.css
Normal file
@@ -0,0 +1,362 @@
|
||||
/* Dashboard Page */
|
||||
.dashboard-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0.5rem 0 0 0;
|
||||
color: #94a3b8;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Summary Grid */
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
background: #0f1629;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.summary-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.summary-card.primary {
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
border-color: #7c3aed;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 0.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-icon.success {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.card-icon.info {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.card-icon.warning {
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.summary-card:not(.primary) .card-label {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 1.875rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
font-family: 'Courier New', monospace;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.card-value.profit {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.card-value.loss {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.card-change {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-change.positive {
|
||||
color: rgba(16, 185, 129, 0.9);
|
||||
}
|
||||
|
||||
.card-change.negative {
|
||||
color: rgba(239, 68, 68, 0.9);
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* Section */
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #6366f1;
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-link:hover {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Assets Quick Grid */
|
||||
.assets-quick-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.asset-quick-card {
|
||||
background: #0f1629;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.25rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.asset-quick-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: #6366f1;
|
||||
}
|
||||
|
||||
.asset-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.asset-symbol {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.asset-change {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.asset-change.positive {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.asset-change.negative {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.asset-price {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
font-family: 'Courier New', monospace;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.asset-profit {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.asset-profit.profit {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.asset-profit.loss {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Activity List */
|
||||
.activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: #0f1629;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.activity-item:hover {
|
||||
background: #1a1f3a;
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.activity-icon.buy {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.activity-icon.sell {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.activity-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.activity-type {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.activity-symbol {
|
||||
font-size: 0.875rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.bot-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.125rem 0.375rem;
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
color: #6366f1;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.activity-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.activity-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
background: #0f1629;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.empty-state .bi {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.assets-quick-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
36
TradingBot/Components/Pages/Error.razor
Normal file
36
TradingBot/Components/Pages/Error.razor
Normal file
@@ -0,0 +1,36 @@
|
||||
@page "/Error"
|
||||
@using System.Diagnostics
|
||||
|
||||
<PageTitle>Error</PageTitle>
|
||||
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
|
||||
@code{
|
||||
[CascadingParameter]
|
||||
private HttpContext? HttpContext { get; set; }
|
||||
|
||||
private string? RequestId { get; set; }
|
||||
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||
|
||||
protected override void OnInitialized() =>
|
||||
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
|
||||
}
|
||||
238
TradingBot/Components/Pages/Market.razor
Normal file
238
TradingBot/Components/Pages/Market.razor
Normal file
@@ -0,0 +1,238 @@
|
||||
@page "/market"
|
||||
@using TradingBot.Models
|
||||
@using TradingBot.Services
|
||||
@using TradingBot.Components.Shared
|
||||
@inject TradingBotService BotService
|
||||
@implements IDisposable
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Analisi Mercato - TradingBot</PageTitle>
|
||||
|
||||
<div class="market-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>Analisi Mercato</h1>
|
||||
<p class="subtitle">Monitora le tendenze di mercato e gli indicatori tecnici in tempo reale</p>
|
||||
</div>
|
||||
<select class="asset-selector" @bind="selectedSymbol" @bind:after="OnAssetChanged">
|
||||
@foreach (var symbol in BotService.AssetConfigurations.Keys.OrderBy(s => s))
|
||||
{
|
||||
<option value="@symbol">@symbol - @BotService.AssetConfigurations[symbol].Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@if (selectedConfig != null && currentPrice != null)
|
||||
{
|
||||
<div class="market-overview">
|
||||
<div class="price-card">
|
||||
<div class="price-header">
|
||||
<div class="asset-info">
|
||||
<span class="asset-icon">@selectedSymbol.Substring(0, 1)</span>
|
||||
<div>
|
||||
<h2>@selectedConfig.Name</h2>
|
||||
<span class="asset-symbol">@selectedSymbol</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="price-main">
|
||||
<div class="current-price">$@currentPrice.Price.ToString("N2")</div>
|
||||
<div class="price-change @(currentPrice.Change24h >= 0 ? "positive" : "negative")">
|
||||
<span class="bi @(currentPrice.Change24h >= 0 ? "bi-arrow-up" : "bi-arrow-down")"></span>
|
||||
@Math.Abs(currentPrice.Change24h).ToString("F2")% (24h)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="price-stats">
|
||||
<div class="stat">
|
||||
<span class="stat-label">Volume 24h</span>
|
||||
<span class="stat-value">$@currentPrice.Volume24h.ToString("N0")</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Holdings</span>
|
||||
<span class="stat-value">@selectedConfig.CurrentHoldings.ToString("F6")</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Valore Posizione</span>
|
||||
<span class="stat-value">$@((selectedConfig.CurrentHoldings * currentPrice.Price).ToString("N2"))</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (currentIndicators != null)
|
||||
{
|
||||
<div class="indicators-grid">
|
||||
<div class="indicator-card">
|
||||
<div class="indicator-header">
|
||||
<span class="indicator-icon">
|
||||
<span class="bi bi-activity"></span>
|
||||
</span>
|
||||
<span class="indicator-name">RSI (14)</span>
|
||||
</div>
|
||||
<div class="indicator-value @GetRSIClass()">
|
||||
@currentIndicators.RSI.ToString("F2")
|
||||
</div>
|
||||
<div class="indicator-status @GetRSIClass()">
|
||||
@GetRSIStatus()
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="indicator-card">
|
||||
<div class="indicator-header">
|
||||
<span class="indicator-icon">
|
||||
<span class="bi bi-graph-up"></span>
|
||||
</span>
|
||||
<span class="indicator-name">MACD</span>
|
||||
</div>
|
||||
<div class="indicator-value">
|
||||
@currentIndicators.MACD.ToString("F2")
|
||||
</div>
|
||||
<div class="indicator-status">
|
||||
Signal: @currentIndicators.Signal.ToString("F2")
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="indicator-card">
|
||||
<div class="indicator-header">
|
||||
<span class="indicator-icon">
|
||||
<span class="bi bi-graph-down"></span>
|
||||
</span>
|
||||
<span class="indicator-name">Histogram</span>
|
||||
</div>
|
||||
<div class="indicator-value @(currentIndicators.Histogram >= 0 ? "positive" : "negative")">
|
||||
@currentIndicators.Histogram.ToString("F4")
|
||||
</div>
|
||||
<div class="indicator-status">
|
||||
@(currentIndicators.Histogram >= 0 ? "Bullish" : "Bearish")
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="indicator-card">
|
||||
<div class="indicator-header">
|
||||
<span class="indicator-icon">
|
||||
<span class="bi bi-bezier2"></span>
|
||||
</span>
|
||||
<span class="indicator-name">EMA</span>
|
||||
</div>
|
||||
<div class="indicator-value">
|
||||
@currentIndicators.EMA12.ToString("F2")
|
||||
</div>
|
||||
<div class="indicator-status">
|
||||
EMA26: @currentIndicators.EMA26.ToString("F2")
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="chart-section">
|
||||
<div class="chart-header">
|
||||
<h3>Andamento Prezzi</h3>
|
||||
<div class="chart-controls">
|
||||
<button class="time-btn active">1H</button>
|
||||
<button class="time-btn">4H</button>
|
||||
<button class="time-btn">1D</button>
|
||||
<button class="time-btn">1W</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<AdvancedChart
|
||||
PriceData="@GetPriceList(selectedSymbol)"
|
||||
Color="#6366f1"
|
||||
Indicators="@currentIndicators" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[SupplyParameterFromQuery(Name = "symbol")]
|
||||
public string? QuerySymbol { get; set; }
|
||||
|
||||
private string selectedSymbol = "BTC";
|
||||
private AssetConfiguration? selectedConfig => BotService.AssetConfigurations.TryGetValue(selectedSymbol, out var c) ? c : null;
|
||||
private MarketPrice? currentPrice => BotService.GetLatestPrice(selectedSymbol);
|
||||
private TechnicalIndicators? currentIndicators;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
// Set initial symbol from query string if available
|
||||
if (!string.IsNullOrEmpty(QuerySymbol) && BotService.AssetConfigurations.ContainsKey(QuerySymbol))
|
||||
{
|
||||
selectedSymbol = QuerySymbol;
|
||||
}
|
||||
|
||||
BotService.OnPriceUpdated += HandlePriceUpdate;
|
||||
BotService.OnIndicatorsUpdated += HandleIndicatorsUpdate;
|
||||
|
||||
UpdateIndicators();
|
||||
}
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
// Update symbol if query parameter changes
|
||||
if (!string.IsNullOrEmpty(QuerySymbol) &&
|
||||
QuerySymbol != selectedSymbol &&
|
||||
BotService.AssetConfigurations.ContainsKey(QuerySymbol))
|
||||
{
|
||||
selectedSymbol = QuerySymbol;
|
||||
UpdateIndicators();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAssetChanged()
|
||||
{
|
||||
UpdateIndicators();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void UpdateIndicators()
|
||||
{
|
||||
currentIndicators = BotService.GetIndicators(selectedSymbol);
|
||||
}
|
||||
|
||||
private List<decimal>? GetPriceList(string symbol)
|
||||
{
|
||||
var history = BotService.GetPriceHistory(symbol);
|
||||
return history?.Select(p => p.Price).ToList();
|
||||
}
|
||||
|
||||
private string GetRSIClass()
|
||||
{
|
||||
if (currentIndicators == null) return "neutral";
|
||||
if (currentIndicators.RSI > 70) return "overbought";
|
||||
if (currentIndicators.RSI < 30) return "oversold";
|
||||
return "neutral";
|
||||
}
|
||||
|
||||
private string GetRSIStatus()
|
||||
{
|
||||
if (currentIndicators == null) return "Neutral";
|
||||
if (currentIndicators.RSI > 70) return "Overbought";
|
||||
if (currentIndicators.RSI < 30) return "Oversold";
|
||||
return "Neutral";
|
||||
}
|
||||
|
||||
private void HandlePriceUpdate(string symbol, MarketPrice price)
|
||||
{
|
||||
if (symbol == selectedSymbol)
|
||||
{
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleIndicatorsUpdate(string symbol, TechnicalIndicators indicators)
|
||||
{
|
||||
if (symbol == selectedSymbol)
|
||||
{
|
||||
currentIndicators = indicators;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
BotService.OnPriceUpdated -= HandlePriceUpdate;
|
||||
BotService.OnIndicatorsUpdated -= HandleIndicatorsUpdate;
|
||||
}
|
||||
}
|
||||
328
TradingBot/Components/Pages/Market.razor.css
Normal file
328
TradingBot/Components/Pages/Market.razor.css
Normal file
@@ -0,0 +1,328 @@
|
||||
/* Market Page */
|
||||
.market-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0.5rem 0 0 0;
|
||||
color: #94a3b8;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.asset-selector {
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #334155;
|
||||
background: #1e293b;
|
||||
color: white;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
/* Market Overview */
|
||||
.market-overview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.price-card {
|
||||
background: #0f1629;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.price-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.asset-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.asset-icon {
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
border-radius: 0.75rem;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.asset-info h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.asset-symbol {
|
||||
font-size: 0.875rem;
|
||||
color: #64748b;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.price-main {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.current-price {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
font-family: 'Courier New', monospace;
|
||||
line-height: 1;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.price-change {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.price-change.positive {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.price-change.negative {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.price-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* Indicators Grid */
|
||||
.indicators-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.indicator-card {
|
||||
background: #0f1629;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.indicator-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: #6366f1;
|
||||
}
|
||||
|
||||
.indicator-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.indicator-icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.indicator-name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.indicator-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
font-family: 'Courier New', monospace;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.indicator-value.positive {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.indicator-value.negative {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.indicator-value.overbought {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.indicator-value.oversold {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.indicator-value.neutral {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.indicator-status {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.indicator-status.overbought {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.indicator-status.oversold {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
/* Chart Section */
|
||||
.chart-section {
|
||||
background: #0f1629;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.chart-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chart-controls {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.time-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #334155;
|
||||
background: transparent;
|
||||
color: #94a3b8;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.time-btn:hover {
|
||||
background: #1a1f3a;
|
||||
border-color: #6366f1;
|
||||
}
|
||||
|
||||
.time-btn.active {
|
||||
background: #6366f1;
|
||||
border-color: #6366f1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1024px) {
|
||||
.indicators-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.asset-selector {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.price-header {
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.price-main {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.price-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.indicators-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
5
TradingBot/Components/Pages/NotFound.razor
Normal file
5
TradingBot/Components/Pages/NotFound.razor
Normal file
@@ -0,0 +1,5 @@
|
||||
@page "/not-found"
|
||||
@layout MainLayout
|
||||
|
||||
<h3>Not Found</h3>
|
||||
<p>Sorry, the content you are looking for does not exist.</p>
|
||||
170
TradingBot/Components/Pages/Settings.razor
Normal file
170
TradingBot/Components/Pages/Settings.razor
Normal file
@@ -0,0 +1,170 @@
|
||||
@page "/settings"
|
||||
@using TradingBot.Services
|
||||
@using TradingBot.Models
|
||||
@inject SettingsService SettingsService
|
||||
@implements IDisposable
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Impostazioni - TradingBot</PageTitle>
|
||||
|
||||
<div class="settings-page">
|
||||
<div class="page-header">
|
||||
<h1>Impostazioni</h1>
|
||||
<p class="subtitle">Configura le impostazioni globali del trading bot</p>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h2>Generale</h2>
|
||||
<div class="settings-group">
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Modalità Simulazione</div>
|
||||
<div class="setting-description">Utilizza dati simulati invece di dati reali di mercato</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" checked="@settings.SimulationMode" @onchange="(e) => UpdateSetting(nameof(AppSettings.SimulationMode), (bool)e.Value!)" disabled />
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Notifiche Desktop</div>
|
||||
<div class="setting-description">Ricevi notifiche per operazioni importanti</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" checked="@settings.DesktopNotifications" @onchange="(e) => UpdateSetting(nameof(AppSettings.DesktopNotifications), (bool)e.Value!)" />
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h2>Trading</h2>
|
||||
<div class="settings-group">
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Auto-Start Bot</div>
|
||||
<div class="setting-description">Avvia automaticamente il bot all'apertura dell'applicazione</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" checked="@settings.AutoStartBot" @onchange="(e) => UpdateSetting(nameof(AppSettings.AutoStartBot), (bool)e.Value!)" />
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Conferma Operazioni Manuali</div>
|
||||
<div class="setting-description">Richiedi conferma prima di eseguire operazioni manuali</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" checked="@settings.ConfirmManualTrades" @onchange="(e) => UpdateSetting(nameof(AppSettings.ConfirmManualTrades), (bool)e.Value!)" />
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h2>Avanzate</h2>
|
||||
<div class="settings-group">
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Intervallo Aggiornamento</div>
|
||||
<div class="setting-description">Frequenza di aggiornamento dei dati di mercato</div>
|
||||
</div>
|
||||
<select class="setting-select" value="@settings.UpdateIntervalSeconds" @onchange="(e) => UpdateSetting(nameof(AppSettings.UpdateIntervalSeconds), int.Parse(e.Value!.ToString()!))">
|
||||
<option value="2">2 secondi</option>
|
||||
<option value="3">3 secondi</option>
|
||||
<option value="5">5 secondi</option>
|
||||
<option value="10">10 secondi</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-info">
|
||||
<div class="setting-label">Log Level</div>
|
||||
<div class="setting-description">Livello di dettaglio dei log di sistema</div>
|
||||
</div>
|
||||
<select class="setting-select" value="@settings.LogLevel" @onchange="(e) => UpdateSetting(nameof(AppSettings.LogLevel), e.Value!.ToString()!)">
|
||||
<option value="Error">Error</option>
|
||||
<option value="Warning">Warning</option>
|
||||
<option value="Info">Info</option>
|
||||
<option value="Debug">Debug</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button class="btn-secondary" @onclick="ResetToDefaults">
|
||||
<span class="bi bi-arrow-counterclockwise"></span>
|
||||
Reset Predefiniti
|
||||
</button>
|
||||
<button class="btn-primary" @onclick="SaveSettings">
|
||||
<span class="bi bi-check-lg"></span>
|
||||
Salva Modifiche
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (showNotification)
|
||||
{
|
||||
<div class="notification success">
|
||||
<span class="bi bi-check-circle-fill"></span>
|
||||
Impostazioni salvate con successo!
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private AppSettings settings = new();
|
||||
private bool showNotification = false;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
settings = SettingsService.GetSettings();
|
||||
SettingsService.OnSettingsChanged += HandleSettingsChanged;
|
||||
}
|
||||
|
||||
private void UpdateSetting<T>(string propertyName, T value)
|
||||
{
|
||||
SettingsService.UpdateSetting(propertyName, value);
|
||||
settings = SettingsService.GetSettings();
|
||||
ShowNotification();
|
||||
}
|
||||
|
||||
private void SaveSettings()
|
||||
{
|
||||
SettingsService.UpdateSettings(settings);
|
||||
ShowNotification();
|
||||
}
|
||||
|
||||
private void ResetToDefaults()
|
||||
{
|
||||
SettingsService.ResetToDefaults();
|
||||
settings = SettingsService.GetSettings();
|
||||
ShowNotification();
|
||||
}
|
||||
|
||||
private async void ShowNotification()
|
||||
{
|
||||
showNotification = true;
|
||||
StateHasChanged();
|
||||
await Task.Delay(3000);
|
||||
showNotification = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void HandleSettingsChanged()
|
||||
{
|
||||
settings = SettingsService.GetSettings();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SettingsService.OnSettingsChanged -= HandleSettingsChanged;
|
||||
}
|
||||
}
|
||||
221
TradingBot/Components/Pages/Settings.razor.css
Normal file
221
TradingBot/Components/Pages/Settings.razor.css
Normal file
@@ -0,0 +1,221 @@
|
||||
/* Settings Page */
|
||||
.settings-page {
|
||||
max-width: 900px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0.5rem 0 0 0;
|
||||
color: #94a3b8;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
background: #0f1629;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.settings-section h2 {
|
||||
margin: 0 0 1.5rem 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.settings-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.setting-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
font-size: 0.938rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.setting-description {
|
||||
font-size: 0.875rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 3rem;
|
||||
height: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #334155;
|
||||
transition: 0.3s;
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.toggle-slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 1.125rem;
|
||||
width: 1.125rem;
|
||||
left: 0.1875rem;
|
||||
bottom: 0.1875rem;
|
||||
background-color: white;
|
||||
transition: 0.3s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + .toggle-slider {
|
||||
background-color: #6366f1;
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + .toggle-slider:before {
|
||||
transform: translateX(1.5rem);
|
||||
}
|
||||
|
||||
.toggle-switch input:disabled + .toggle-slider {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.setting-select {
|
||||
padding: 0.625rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #334155;
|
||||
background: #1a1f3a;
|
||||
color: white;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.settings-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #1e293b;
|
||||
color: #cbd5e1;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #334155;
|
||||
border-color: #475569;
|
||||
}
|
||||
|
||||
/* Notification */
|
||||
.notification {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
|
||||
animation: slide-in 0.3s ease;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
@keyframes slide-in {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.notification.success {
|
||||
background: #064e3b;
|
||||
border: 1px solid #065f46;
|
||||
color: #6ee7b7;
|
||||
}
|
||||
|
||||
.notification .bi {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.setting-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.settings-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
460
TradingBot/Components/Pages/Statistics.razor
Normal file
460
TradingBot/Components/Pages/Statistics.razor
Normal file
@@ -0,0 +1,460 @@
|
||||
@page "/statistics"
|
||||
@using TradingBot.Models
|
||||
@using TradingBot.Services
|
||||
@inject TradingBotService BotService
|
||||
@inject NavigationManager Navigation
|
||||
@implements IDisposable
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Statistiche - TradingBot</PageTitle>
|
||||
|
||||
<div class="statistics-page">
|
||||
|
||||
<!-- Header -->
|
||||
<header class="stats-header">
|
||||
<div class="header-content">
|
||||
<div class="page-title">
|
||||
<h1><span class="bi bi-graph-up"></span> Statistiche Avanzate</h1>
|
||||
<p class="subtitle">Analisi dettagliata delle performance e metriche di trading</p>
|
||||
</div>
|
||||
<div class="header-filters">
|
||||
<select class="filter-select" @bind="selectedSymbol" @bind:after="OnSymbolChanged">
|
||||
<option value="">Tutti gli Asset</option>
|
||||
@foreach (var symbol in BotService.AssetConfigurations.Keys.OrderBy(s => s))
|
||||
{
|
||||
<option value="@symbol">@symbol</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="stats-content">
|
||||
|
||||
@if (string.IsNullOrEmpty(selectedSymbol))
|
||||
{
|
||||
<!-- Portfolio Overview -->
|
||||
<div class="overview-section">
|
||||
<h2 class="section-title">
|
||||
<span class="bi bi-pie-chart-fill"></span> Panoramica Portfolio
|
||||
</h2>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card primary">
|
||||
<div class="stat-header">
|
||||
<span class="stat-icon"><span class="bi bi-wallet2"></span></span>
|
||||
<span class="stat-label">Valore Totale</span>
|
||||
</div>
|
||||
<div class="stat-value">$@portfolioStats.TotalBalance.ToString("N2")</div>
|
||||
<div class="stat-footer">
|
||||
<span class="stat-change @(portfolioStats.TotalProfitPercentage >= 0 ? "positive" : "negative")">
|
||||
<span class="bi @(portfolioStats.TotalProfitPercentage >= 0 ? "bi-arrow-up" : "bi-arrow-down")"></span>
|
||||
@Math.Abs(portfolioStats.TotalProfitPercentage).ToString("F2")%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">
|
||||
<span class="stat-icon success"><span class="bi bi-trophy"></span></span>
|
||||
<span class="stat-label">Profitto Netto</span>
|
||||
</div>
|
||||
<div class="stat-value @(portfolioStats.TotalProfit >= 0 ? "profit" : "loss")">
|
||||
$@portfolioStats.TotalProfit.ToString("N2")
|
||||
</div>
|
||||
<div class="stat-footer">
|
||||
<span class="stat-meta">Da $@portfolioStats.InitialBalance.ToString("N2")</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">
|
||||
<span class="stat-icon info"><span class="bi bi-arrow-left-right"></span></span>
|
||||
<span class="stat-label">Totale Operazioni</span>
|
||||
</div>
|
||||
<div class="stat-value">@portfolioStats.TotalTrades</div>
|
||||
<div class="stat-footer">
|
||||
<span class="stat-meta">@portfolioStats.ActiveAssets asset attivi</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">
|
||||
<span class="stat-icon warning"><span class="bi bi-percent"></span></span>
|
||||
<span class="stat-label">Win Rate</span>
|
||||
</div>
|
||||
<div class="stat-value">@portfolioStats.WinRate.ToString("F1")%</div>
|
||||
<div class="stat-footer">
|
||||
<span class="stat-meta">Tasso di successo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Best/Worst Performers -->
|
||||
<div class="performers-section">
|
||||
<div class="performer-card best">
|
||||
<div class="performer-header">
|
||||
<span class="bi bi-trophy-fill"></span>
|
||||
<span>Miglior Performer</span>
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(portfolioStats.BestPerformingAssetSymbol))
|
||||
{
|
||||
<div class="performer-content">
|
||||
<div class="performer-symbol">@portfolioStats.BestPerformingAssetSymbol</div>
|
||||
<div class="performer-value profit">+$@portfolioStats.BestPerformingAssetProfit.ToString("N2")</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="empty-performer">Nessun dato</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="performer-card worst">
|
||||
<div class="performer-header">
|
||||
<span class="bi bi-graph-down"></span>
|
||||
<span>Peggior Performer</span>
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(portfolioStats.WorstPerformingAssetSymbol))
|
||||
{
|
||||
<div class="performer-content">
|
||||
<div class="performer-symbol">@portfolioStats.WorstPerformingAssetSymbol</div>
|
||||
<div class="performer-value @(portfolioStats.WorstPerformingAssetProfit >= 0 ? "profit" : "loss")">
|
||||
$@portfolioStats.WorstPerformingAssetProfit.ToString("N2")
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="empty-performer">Nessun dato</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Asset Breakdown -->
|
||||
<div class="breakdown-section">
|
||||
<h2 class="section-title">
|
||||
<span class="bi bi-list-columns-reverse"></span> Breakdown per Asset
|
||||
</h2>
|
||||
<div class="breakdown-table">
|
||||
<div class="table-header">
|
||||
<div class="th">Asset</div>
|
||||
<div class="th">Valore</div>
|
||||
<div class="th">Profitto</div>
|
||||
<div class="th">% Profitto</div>
|
||||
<div class="th">Trades</div>
|
||||
<div class="th">Win Rate</div>
|
||||
<div class="th">Azioni</div>
|
||||
</div>
|
||||
@foreach (var assetStat in portfolioStats.AssetStatistics.OrderByDescending(a => a.NetProfit))
|
||||
{
|
||||
var config = BotService.AssetConfigurations.TryGetValue(assetStat.Symbol, out var c) ? c : null;
|
||||
if (config == null) continue;
|
||||
|
||||
var currentValue = config.CurrentBalance + (config.CurrentHoldings * assetStat.CurrentPrice);
|
||||
|
||||
<div class="table-row">
|
||||
<div class="td asset-cell">
|
||||
<span class="asset-symbol">@assetStat.Symbol</span>
|
||||
<span class="asset-name">@assetStat.Name</span>
|
||||
</div>
|
||||
<div class="td">$@currentValue.ToString("N2")</div>
|
||||
<div class="td @(assetStat.NetProfit >= 0 ? "profit" : "loss")">
|
||||
$@assetStat.NetProfit.ToString("N2")
|
||||
</div>
|
||||
<div class="td @(config.ProfitPercentage >= 0 ? "profit" : "loss")">
|
||||
@config.ProfitPercentage.ToString("F2")%
|
||||
</div>
|
||||
<div class="td">@assetStat.TotalTrades</div>
|
||||
<div class="td">@assetStat.WinRate.ToString("F1")%</div>
|
||||
<div class="td">
|
||||
<button class="btn-details" @onclick="() => ViewAssetDetails(assetStat.Symbol)">
|
||||
<span class="bi bi-eye"></span> Dettagli
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Single Asset Statistics -->
|
||||
var assetStats = BotService.AssetStatistics.TryGetValue(selectedSymbol, out var stats) ? stats : null;
|
||||
var assetConfig = BotService.AssetConfigurations.TryGetValue(selectedSymbol, out var config) ? config : null;
|
||||
|
||||
@if (assetStats != null && assetConfig != null)
|
||||
{
|
||||
<div class="asset-details-section">
|
||||
<div class="asset-details-header">
|
||||
<div class="asset-title-section">
|
||||
<h2>@assetStats.Name (@assetStats.Symbol)</h2>
|
||||
<span class="status-badge @(assetConfig.IsEnabled ? "active" : "inactive")">
|
||||
@(assetConfig.IsEnabled ? "Attivo" : "Inattivo")
|
||||
</span>
|
||||
</div>
|
||||
<button class="btn-back" @onclick="ClearSelection">
|
||||
<span class="bi bi-arrow-left"></span> Torna alla panoramica
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Key Metrics -->
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon"><span class="bi bi-cash-stack"></span></div>
|
||||
<div class="metric-content">
|
||||
<div class="metric-label">Prezzo Corrente</div>
|
||||
<div class="metric-value">$@assetStats.CurrentPrice.ToString("N2")</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon success"><span class="bi bi-bar-chart-line"></span></div>
|
||||
<div class="metric-content">
|
||||
<div class="metric-label">Holdings</div>
|
||||
<div class="metric-value">@assetConfig.CurrentHoldings.ToString("F6")</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon @(assetStats.NetProfit >= 0 ? "success" : "danger")">
|
||||
<span class="bi bi-graph-up-arrow"></span>
|
||||
</div>
|
||||
<div class="metric-content">
|
||||
<div class="metric-label">Profitto Netto</div>
|
||||
<div class="metric-value @(assetStats.NetProfit >= 0 ? "profit" : "loss")">
|
||||
$@assetStats.NetProfit.ToString("N2")
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-icon info"><span class="bi bi-percent"></span></div>
|
||||
<div class="metric-content">
|
||||
<div class="metric-label">ROI</div>
|
||||
<div class="metric-value @(assetConfig.ProfitPercentage >= 0 ? "profit" : "loss")">
|
||||
@assetConfig.ProfitPercentage.ToString("F2")%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Trading Performance -->
|
||||
<div class="performance-section">
|
||||
<h3 class="subsection-title">Performance Trading</h3>
|
||||
<div class="performance-grid">
|
||||
<div class="performance-item">
|
||||
<span class="perf-label">Totale Operazioni</span>
|
||||
<span class="perf-value">@assetStats.TotalTrades</span>
|
||||
</div>
|
||||
<div class="performance-item">
|
||||
<span class="perf-label">Operazioni Vincenti</span>
|
||||
<span class="perf-value profit">@assetStats.WinningTrades</span>
|
||||
</div>
|
||||
<div class="performance-item">
|
||||
<span class="perf-label">Operazioni Perdenti</span>
|
||||
<span class="perf-value loss">@assetStats.LosingTrades</span>
|
||||
</div>
|
||||
<div class="performance-item">
|
||||
<span class="perf-label">Win Rate</span>
|
||||
<span class="perf-value">@assetStats.WinRate.ToString("F1")%</span>
|
||||
</div>
|
||||
<div class="performance-item">
|
||||
<span class="perf-label">Profit Factor</span>
|
||||
<span class="perf-value">@(assetStats.ProfitFactor > 1000 ? ">1000" : assetStats.ProfitFactor.ToString("F2"))</span>
|
||||
</div>
|
||||
<div class="performance-item">
|
||||
<span class="perf-label">Vittorie Consecutive</span>
|
||||
<span class="perf-value">@assetStats.MaxConsecutiveWins</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profit/Loss Analysis -->
|
||||
<div class="pnl-section">
|
||||
<h3 class="subsection-title">Analisi Profitti/Perdite</h3>
|
||||
<div class="pnl-grid">
|
||||
<div class="pnl-card profit-card">
|
||||
<div class="pnl-header">
|
||||
<span class="bi bi-arrow-up-circle-fill"></span>
|
||||
<span>Profitti</span>
|
||||
</div>
|
||||
<div class="pnl-amount profit">$@assetStats.TotalProfit.ToString("N2")</div>
|
||||
<div class="pnl-meta">
|
||||
Media per trade: $@assetStats.AverageProfit.ToString("N2")
|
||||
</div>
|
||||
<div class="pnl-meta">
|
||||
Profitto massimo: $@assetStats.LargestWin.ToString("N2")
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pnl-card loss-card">
|
||||
<div class="pnl-header">
|
||||
<span class="bi bi-arrow-down-circle-fill"></span>
|
||||
<span>Perdite</span>
|
||||
</div>
|
||||
<div class="pnl-amount loss">$@assetStats.TotalLoss.ToString("N2")</div>
|
||||
<div class="pnl-meta">
|
||||
Media per trade: $@assetStats.AverageLoss.ToString("N2")
|
||||
</div>
|
||||
<div class="pnl-meta">
|
||||
Perdita massima: $@assetStats.LargestLoss.ToString("N2")
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (assetStats.UnrealizedPnL != 0)
|
||||
{
|
||||
<div class="pnl-card unrealized-card">
|
||||
<div class="pnl-header">
|
||||
<span class="bi bi-hourglass-split"></span>
|
||||
<span>P/L Non Realizzato</span>
|
||||
</div>
|
||||
<div class="pnl-amount @(assetStats.UnrealizedPnL >= 0 ? "profit" : "loss")">
|
||||
$@assetStats.UnrealizedPnL.ToString("N2")
|
||||
</div>
|
||||
<div class="pnl-meta">
|
||||
@assetStats.UnrealizedPnLPercentage.ToString("F2")% sulla posizione corrente
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Trades -->
|
||||
@if (assetStats.RecentTrades.Count > 0)
|
||||
{
|
||||
<div class="trades-section">
|
||||
<h3 class="subsection-title">Operazioni Recenti</h3>
|
||||
<div class="trades-list">
|
||||
@foreach (var trade in assetStats.RecentTrades.Take(20))
|
||||
{
|
||||
<div class="trade-item @(trade.IsBot ? "bot-trade" : "")">
|
||||
<div class="trade-icon @(trade.Type == TradeType.Buy ? "buy" : "sell")">
|
||||
<span class="bi @(trade.Type == TradeType.Buy ? "bi-arrow-down-circle-fill" : "bi-arrow-up-circle-fill")"></span>
|
||||
</div>
|
||||
<div class="trade-details">
|
||||
<div class="trade-type">
|
||||
@(trade.Type == TradeType.Buy ? "ACQUISTO" : "VENDITA")
|
||||
@if (trade.IsBot)
|
||||
{
|
||||
<span class="bot-label">
|
||||
<span class="bi bi-robot"></span> BOT
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="trade-meta">
|
||||
@trade.Timestamp.ToLocalTime().ToString("dd/MM/yyyy HH:mm:ss")
|
||||
</div>
|
||||
</div>
|
||||
<div class="trade-amounts">
|
||||
<div class="trade-quantity">@trade.Amount.ToString("F6") @trade.Symbol</div>
|
||||
<div class="trade-price">@ $@trade.Price.ToString("N2")</div>
|
||||
</div>
|
||||
<div class="trade-value">
|
||||
$@((trade.Amount * trade.Price).ToString("N2"))
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="empty-trades">
|
||||
<span class="bi bi-inbox"></span>
|
||||
<p>Nessuna operazione eseguita per questo asset</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="empty-state">
|
||||
<span class="bi bi-exclamation-circle"></span>
|
||||
<p>Asset non trovato o dati non disponibili</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[SupplyParameterFromQuery(Name = "symbol")]
|
||||
private string? QuerySymbol { get; set; }
|
||||
|
||||
private string selectedSymbol = "";
|
||||
private PortfolioStatistics portfolioStats = new();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
BotService.OnStatusChanged += HandleUpdate;
|
||||
BotService.OnTradeExecuted += HandleTradeExecuted;
|
||||
BotService.OnStatisticsUpdated += HandleUpdate;
|
||||
BotService.OnPriceUpdated += HandlePriceUpdate;
|
||||
|
||||
if (!string.IsNullOrEmpty(QuerySymbol))
|
||||
{
|
||||
selectedSymbol = QuerySymbol;
|
||||
}
|
||||
|
||||
RefreshData();
|
||||
}
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(QuerySymbol) && QuerySymbol != selectedSymbol)
|
||||
{
|
||||
selectedSymbol = QuerySymbol;
|
||||
RefreshData();
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshData()
|
||||
{
|
||||
portfolioStats = BotService.GetPortfolioStatistics();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void OnSymbolChanged()
|
||||
{
|
||||
if (string.IsNullOrEmpty(selectedSymbol))
|
||||
{
|
||||
Navigation.NavigateTo("/statistics");
|
||||
}
|
||||
else
|
||||
{
|
||||
Navigation.NavigateTo($"/statistics?symbol={selectedSymbol}");
|
||||
}
|
||||
RefreshData();
|
||||
}
|
||||
|
||||
private void ViewAssetDetails(string symbol)
|
||||
{
|
||||
selectedSymbol = symbol;
|
||||
Navigation.NavigateTo($"/statistics?symbol={symbol}");
|
||||
RefreshData();
|
||||
}
|
||||
|
||||
private void ClearSelection()
|
||||
{
|
||||
selectedSymbol = "";
|
||||
Navigation.NavigateTo("/statistics");
|
||||
RefreshData();
|
||||
}
|
||||
|
||||
private void HandleUpdate() => InvokeAsync(RefreshData);
|
||||
|
||||
private void HandleTradeExecuted(Trade trade) => InvokeAsync(RefreshData);
|
||||
|
||||
private void HandlePriceUpdate(string symbol, MarketPrice price) => InvokeAsync(RefreshData);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
BotService.OnStatusChanged -= HandleUpdate;
|
||||
BotService.OnTradeExecuted -= HandleTradeExecuted;
|
||||
BotService.OnStatisticsUpdated -= HandleUpdate;
|
||||
BotService.OnPriceUpdated -= HandlePriceUpdate;
|
||||
}
|
||||
}
|
||||
755
TradingBot/Components/Pages/Statistics.razor.css
Normal file
755
TradingBot/Components/Pages/Statistics.razor.css
Normal file
@@ -0,0 +1,755 @@
|
||||
/* Statistics Page */
|
||||
.statistics-page {
|
||||
min-height: 100vh;
|
||||
background: #020617;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.stats-header {
|
||||
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
||||
border-bottom: 1px solid #1e293b;
|
||||
padding: 2rem 1.5rem;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.page-title h1 {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0.5rem 0 0 0;
|
||||
color: #94a3b8;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.header-filters {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #334155;
|
||||
background: #1e293b;
|
||||
color: white;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.filter-select:focus {
|
||||
outline: none;
|
||||
border-color: #6366f1;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.stats-content {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
/* Section Title */
|
||||
.section-title {
|
||||
margin: 0 0 1.5rem 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Stats Grid */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #0f172a;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 24px -4px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.stat-card.primary {
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
border-color: #7c3aed;
|
||||
}
|
||||
|
||||
.stat-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-icon.success {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.stat-icon.info {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.stat-icon.warning {
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stat-card:not(.primary) .stat-label {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
font-family: 'Courier New', monospace;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-change {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.stat-change.positive {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.stat-change.negative {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.stat-meta {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* Performers Section */
|
||||
.performers-section {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.performer-card {
|
||||
background: #0f172a;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.performer-card.best {
|
||||
border-color: rgba(16, 185, 129, 0.3);
|
||||
background: linear-gradient(135deg, rgba(16, 185, 129, 0.05) 0%, #0f172a 100%);
|
||||
}
|
||||
|
||||
.performer-card.worst {
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.05) 0%, #0f172a 100%);
|
||||
}
|
||||
|
||||
.performer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.performer-card.best .performer-header {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.performer-card.worst .performer-header {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.performer-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.performer-symbol {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.performer-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.empty-performer {
|
||||
text-align: center;
|
||||
color: #475569;
|
||||
font-size: 0.875rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Breakdown Table */
|
||||
.breakdown-section {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.breakdown-table {
|
||||
background: #0f172a;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1.5fr 1.5fr 1fr 1fr 1fr 1.5fr;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.5rem;
|
||||
background: #1e293b;
|
||||
border-bottom: 1px solid #334155;
|
||||
}
|
||||
|
||||
.th {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: #94a3b8;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1.5fr 1.5fr 1fr 1fr 1fr 1.5fr;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid #1e293b;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
background: #1e293b;
|
||||
}
|
||||
|
||||
.td {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.875rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.asset-cell {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.asset-symbol {
|
||||
font-weight: 700;
|
||||
font-family: monospace;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.asset-name {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.btn-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
border: 1px solid #334155;
|
||||
background: transparent;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-details:hover {
|
||||
background: #1e293b;
|
||||
border-color: #6366f1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Asset Details */
|
||||
.asset-details-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.asset-details-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.asset-title-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.asset-title-section h2 {
|
||||
margin: 0;
|
||||
font-size: 1.875rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-badge.active {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: #10b981;
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.status-badge.inactive {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #ef4444;
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
border: 1px solid #334155;
|
||||
background: #1e293b;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-back:hover {
|
||||
background: #334155;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Metrics Grid */
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: #0f172a;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.metric-icon {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.metric-icon.success {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.metric-icon.danger {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.metric-icon.info {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.metric-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.75rem;
|
||||
color: #94a3b8;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Performance Section */
|
||||
.performance-section {
|
||||
background: #0f172a;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.subsection-title {
|
||||
margin: 0 0 1.5rem 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.performance-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.performance-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
background: #020617;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.perf-label {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.perf-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* P/L Section */
|
||||
.pnl-section {
|
||||
background: #0f172a;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.pnl-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.pnl-card {
|
||||
background: #020617;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.profit-card {
|
||||
border-color: rgba(16, 185, 129, 0.3);
|
||||
background: linear-gradient(135deg, rgba(16, 185, 129, 0.05) 0%, #020617 100%);
|
||||
}
|
||||
|
||||
.loss-card {
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.05) 0%, #020617 100%);
|
||||
}
|
||||
|
||||
.unrealized-card {
|
||||
border-color: rgba(245, 158, 11, 0.3);
|
||||
background: linear-gradient(135deg, rgba(245, 158, 11, 0.05) 0%, #020617 100%);
|
||||
}
|
||||
|
||||
.pnl-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.profit-card .pnl-header {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.loss-card .pnl-header {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.unrealized-card .pnl-header {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.pnl-amount {
|
||||
font-size: 1.875rem;
|
||||
font-weight: 700;
|
||||
font-family: monospace;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.pnl-meta {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
/* Trades Section */
|
||||
.trades-section {
|
||||
background: #0f172a;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.trades-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.trade-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: #020617;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.trade-item:hover {
|
||||
background: #1e293b;
|
||||
}
|
||||
|
||||
.trade-item.bot-trade {
|
||||
border-color: rgba(99, 102, 241, 0.3);
|
||||
background: rgba(99, 102, 241, 0.05);
|
||||
}
|
||||
|
||||
.trade-icon {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.trade-icon.buy {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.trade-icon.sell {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.trade-details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.trade-type {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.bot-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.125rem 0.375rem;
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
color: #6366f1;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.trade-meta {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.trade-amounts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.trade-quantity {
|
||||
font-size: 0.875rem;
|
||||
color: white;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.trade-price {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.trade-value {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
font-family: monospace;
|
||||
min-width: 100px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Empty States */
|
||||
.empty-state, .empty-trades {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.empty-state .bi, .empty-trades .bi {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state p, .empty-trades p {
|
||||
margin: 1rem 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Common Styles */
|
||||
.profit {
|
||||
color: #10b981 !important;
|
||||
}
|
||||
|
||||
.loss {
|
||||
color: #ef4444 !important;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1024px) {
|
||||
.performers-section {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.table-header, .table-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.th:not(:first-child), .td:not(:first-child) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header-content {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.stats-grid, .metrics-grid, .performance-grid, .pnl-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
318
TradingBot/Components/Pages/Strategies.razor
Normal file
318
TradingBot/Components/Pages/Strategies.razor
Normal file
@@ -0,0 +1,318 @@
|
||||
@page "/strategies"
|
||||
@using TradingBot.Models
|
||||
@using TradingBot.Services
|
||||
@inject TradingBotService BotService
|
||||
@implements IDisposable
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Strategie - TradingBot</PageTitle>
|
||||
|
||||
<div class="strategies-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>Gestione Strategie</h1>
|
||||
<p class="subtitle">Crea e gestisci le tue strategie di trading automatizzate</p>
|
||||
</div>
|
||||
<button class="btn-primary">
|
||||
<span class="bi bi-plus-lg"></span>
|
||||
Nuova Strategia
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="strategies-grid">
|
||||
|
||||
<!-- Active Strategy Card -->
|
||||
<div class="strategy-card active">
|
||||
<div class="card-header">
|
||||
<div class="strategy-info">
|
||||
<h3>RSI + MACD Cross</h3>
|
||||
<span class="badge active">ATTIVA</span>
|
||||
</div>
|
||||
<div class="strategy-actions">
|
||||
<button class="btn-icon" title="Modifica">
|
||||
<span class="bi bi-pencil"></span>
|
||||
</button>
|
||||
<button class="btn-icon" title="Duplica">
|
||||
<span class="bi bi-files"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="strategy-description">
|
||||
Strategia basata su indicatori tecnici RSI e MACD per identificare punti di ingresso e uscita ottimali
|
||||
</div>
|
||||
|
||||
<div class="strategy-stats">
|
||||
<div class="stat">
|
||||
<span class="stat-label">Asset Applicati</span>
|
||||
<span class="stat-value">@activeAssets/@totalAssets</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Win Rate</span>
|
||||
<span class="stat-value profit">@portfolioStats.WinRate.ToString("F1")%</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Trades Totali</span>
|
||||
<span class="stat-value">@portfolioStats.TotalTrades</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Profitto</span>
|
||||
<span class="stat-value @(portfolioStats.TotalProfit >= 0 ? "profit" : "loss")">
|
||||
$@portfolioStats.TotalProfit.ToString("N2")
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="strategy-parameters">
|
||||
<h4>Parametri</h4>
|
||||
<div class="params-grid">
|
||||
<div class="param">
|
||||
<span class="param-label">Condizione BUY</span>
|
||||
<code class="param-value">RSI < 40 AND MACD > 0</code>
|
||||
</div>
|
||||
<div class="param">
|
||||
<span class="param-label">Condizione SELL</span>
|
||||
<code class="param-value">RSI > 60 AND MACD < 0</code>
|
||||
</div>
|
||||
<div class="param">
|
||||
<span class="param-label">Stop Loss</span>
|
||||
<code class="param-value">5%</code>
|
||||
</div>
|
||||
<div class="param">
|
||||
<span class="param-label">Take Profit</span>
|
||||
<code class="param-value">10%</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="strategy-indicators">
|
||||
<h4>Indicatori Utilizzati</h4>
|
||||
<div class="indicators-list">
|
||||
<span class="indicator-tag">
|
||||
<span class="bi bi-graph-up"></span>
|
||||
RSI (14)
|
||||
</span>
|
||||
<span class="indicator-tag">
|
||||
<span class="bi bi-graph-down"></span>
|
||||
MACD (12, 26, 9)
|
||||
</span>
|
||||
<span class="indicator-tag">
|
||||
<span class="bi bi-activity"></span>
|
||||
EMA (12, 26)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<button class="btn-secondary">
|
||||
<span class="bi bi-pause-circle"></span>
|
||||
Disattiva
|
||||
</button>
|
||||
<button class="btn-primary" @onclick="@(() => NavigateToStatistics())">
|
||||
<span class="bi bi-bar-chart-line"></span>
|
||||
Vedi Performance
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Example Inactive Strategy Cards -->
|
||||
<div class="strategy-card">
|
||||
<div class="card-header">
|
||||
<div class="strategy-info">
|
||||
<h3>Media Mobile Semplice</h3>
|
||||
<span class="badge inactive">INATTIVA</span>
|
||||
</div>
|
||||
<div class="strategy-actions">
|
||||
<button class="btn-icon" title="Modifica">
|
||||
<span class="bi bi-pencil"></span>
|
||||
</button>
|
||||
<button class="btn-icon" title="Elimina">
|
||||
<span class="bi bi-trash"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="strategy-description">
|
||||
Strategia classica basata sull'incrocio di medie mobili a breve e lungo termine
|
||||
</div>
|
||||
|
||||
<div class="strategy-stats">
|
||||
<div class="stat">
|
||||
<span class="stat-label">Asset Applicati</span>
|
||||
<span class="stat-value">0/@totalAssets</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Win Rate</span>
|
||||
<span class="stat-value">-</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Trades Totali</span>
|
||||
<span class="stat-value">0</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Profitto</span>
|
||||
<span class="stat-value">$0.00</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="strategy-parameters">
|
||||
<h4>Parametri</h4>
|
||||
<div class="params-grid">
|
||||
<div class="param">
|
||||
<span class="param-label">SMA Breve</span>
|
||||
<code class="param-value">10 periodi</code>
|
||||
</div>
|
||||
<div class="param">
|
||||
<span class="param-label">SMA Lungo</span>
|
||||
<code class="param-value">30 periodi</code>
|
||||
</div>
|
||||
<div class="param">
|
||||
<span class="param-label">Stop Loss</span>
|
||||
<code class="param-value">3%</code>
|
||||
</div>
|
||||
<div class="param">
|
||||
<span class="param-label">Take Profit</span>
|
||||
<code class="param-value">8%</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="strategy-indicators">
|
||||
<h4>Indicatori Utilizzati</h4>
|
||||
<div class="indicators-list">
|
||||
<span class="indicator-tag">
|
||||
<span class="bi bi-graph-up"></span>
|
||||
SMA (10)
|
||||
</span>
|
||||
<span class="indicator-tag">
|
||||
<span class="bi bi-graph-up"></span>
|
||||
SMA (30)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<button class="btn-secondary">
|
||||
<span class="bi bi-play-circle"></span>
|
||||
Attiva
|
||||
</button>
|
||||
<button class="btn-secondary">
|
||||
<span class="bi bi-pencil"></span>
|
||||
Modifica
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Template Strategy Card -->
|
||||
<div class="strategy-card template">
|
||||
<div class="template-content">
|
||||
<div class="template-icon">
|
||||
<span class="bi bi-diagram-3"></span>
|
||||
</div>
|
||||
<h3>Crea Nuova Strategia</h3>
|
||||
<p>Progetta una strategia personalizzata con indicatori tecnici e regole di trading</p>
|
||||
<button class="btn-primary">
|
||||
<span class="bi bi-plus-lg"></span>
|
||||
Inizia Ora
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Strategy Templates Section -->
|
||||
<div class="templates-section">
|
||||
<h2>Template Strategie</h2>
|
||||
<p class="section-subtitle">Inizia da modelli predefiniti e personalizzali secondo le tue esigenze</p>
|
||||
|
||||
<div class="templates-grid">
|
||||
<div class="template-item">
|
||||
<div class="template-header">
|
||||
<span class="bi bi-lightning-charge"></span>
|
||||
<h4>Scalping Veloce</h4>
|
||||
</div>
|
||||
<p>Strategia ad alta frequenza per profitti rapidi su piccoli movimenti di prezzo</p>
|
||||
<button class="btn-outline">
|
||||
<span class="bi bi-download"></span>
|
||||
Usa Template
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="template-item">
|
||||
<div class="template-header">
|
||||
<span class="bi bi-graph-up-arrow"></span>
|
||||
<h4>Trend Following</h4>
|
||||
</div>
|
||||
<p>Segui le tendenze di mercato dominanti per massimizzare i profitti</p>
|
||||
<button class="btn-outline">
|
||||
<span class="bi bi-download"></span>
|
||||
Usa Template
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="template-item">
|
||||
<div class="template-header">
|
||||
<span class="bi bi-arrow-left-right"></span>
|
||||
<h4>Mean Reversion</h4>
|
||||
</div>
|
||||
<p>Sfrutta il ritorno dei prezzi verso la media storica</p>
|
||||
<button class="btn-outline">
|
||||
<span class="bi bi-download"></span>
|
||||
Usa Template
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="template-item">
|
||||
<div class="template-header">
|
||||
<span class="bi bi-shield-check"></span>
|
||||
<h4>Conservative</h4>
|
||||
</div>
|
||||
<p>Strategia a basso rischio con protezione del capitale</p>
|
||||
<button class="btn-outline">
|
||||
<span class="bi bi-download"></span>
|
||||
Usa Template
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private PortfolioStatistics portfolioStats = new();
|
||||
private int activeAssets = 0;
|
||||
private int totalAssets = 0;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
BotService.OnStatusChanged += HandleUpdate;
|
||||
BotService.OnTradeExecuted += HandleTradeExecuted;
|
||||
|
||||
RefreshData();
|
||||
}
|
||||
|
||||
private void RefreshData()
|
||||
{
|
||||
portfolioStats = BotService.GetPortfolioStatistics();
|
||||
activeAssets = BotService.AssetConfigurations.Values.Count(c => c.IsEnabled);
|
||||
totalAssets = BotService.AssetConfigurations.Count;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void NavigateToStatistics()
|
||||
{
|
||||
// Navigate to statistics page
|
||||
}
|
||||
|
||||
private void HandleUpdate() => InvokeAsync(RefreshData);
|
||||
private void HandleTradeExecuted(Trade trade) => InvokeAsync(RefreshData);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
BotService.OnStatusChanged -= HandleUpdate;
|
||||
BotService.OnTradeExecuted -= HandleTradeExecuted;
|
||||
}
|
||||
}
|
||||
407
TradingBot/Components/Pages/Strategies.razor.css
Normal file
407
TradingBot/Components/Pages/Strategies.razor.css
Normal file
@@ -0,0 +1,407 @@
|
||||
/* Strategies Page */
|
||||
.strategies-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
/* Page Header */
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0.5rem 0 0 0;
|
||||
color: #94a3b8;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn-primary, .btn-secondary, .btn-outline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #1e293b;
|
||||
color: #cbd5e1;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #334155;
|
||||
border-color: #475569;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: #6366f1;
|
||||
border: 1px solid #6366f1;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 0.375rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: #1e293b;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
/* Strategies Grid */
|
||||
.strategies-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* Strategy Card */
|
||||
.strategy-card {
|
||||
background: #0f1629;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.strategy-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.4);
|
||||
border-color: #334155;
|
||||
}
|
||||
|
||||
.strategy-card.active {
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 1px #6366f1;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 1.5rem;
|
||||
background: #1a1f3a;
|
||||
border-bottom: 1px solid #1e293b;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.strategy-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.strategy-info h3 {
|
||||
margin: 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.badge.active {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.badge.inactive {
|
||||
background: rgba(100, 116, 139, 0.2);
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.strategy-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.strategy-description {
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Strategy Stats */
|
||||
.strategy-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: #1a1f3a;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.625rem;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.stat-value.profit {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.stat-value.loss {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Strategy Parameters */
|
||||
.strategy-parameters h4,
|
||||
.strategy-indicators h4 {
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #cbd5e1;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.params-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.param {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.param-label {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.param-value {
|
||||
font-size: 0.75rem;
|
||||
color: #10b981;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
padding: 0.375rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* Indicators */
|
||||
.indicators-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.indicator-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
background: #1a1f3a;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
color: #cbd5e1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Card Footer */
|
||||
.card-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
background: #0a0e27;
|
||||
border-top: 1px solid #1e293b;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Template Card */
|
||||
.strategy-card.template {
|
||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%);
|
||||
border: 2px dashed #6366f1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.template-content {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.template-icon {
|
||||
font-size: 3rem;
|
||||
color: #6366f1;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.template-content h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.template-content p {
|
||||
margin: 0 0 1.5rem 0;
|
||||
color: #94a3b8;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Templates Section */
|
||||
.templates-section {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.templates-section h2 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
margin: 0 0 1.5rem 0;
|
||||
color: #94a3b8;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.templates-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.template-item {
|
||||
background: #0f1629;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.template-item:hover {
|
||||
border-color: #6366f1;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.template-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.template-header .bi {
|
||||
font-size: 1.5rem;
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.template-header h4 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.template-item p {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.strategies-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.strategy-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.params-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.templates-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
238
TradingBot/Components/Pages/Trading.razor
Normal file
238
TradingBot/Components/Pages/Trading.razor
Normal file
@@ -0,0 +1,238 @@
|
||||
@page "/trading"
|
||||
@using TradingBot.Models
|
||||
@using TradingBot.Services
|
||||
@inject TradingBotService BotService
|
||||
@implements IDisposable
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Trading - TradingBot</PageTitle>
|
||||
|
||||
<div class="trading-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>Trading Automatico</h1>
|
||||
<p class="subtitle">Applica strategie agli asset e monitora le operazioni in tempo reale</p>
|
||||
</div>
|
||||
<div class="header-controls">
|
||||
<button class="btn-secondary">
|
||||
<span class="bi bi-download"></span>
|
||||
Esporta Report
|
||||
</button>
|
||||
<button class="btn-toggle @(BotService.Status.IsRunning ? "active" : "")" @onclick="ToggleBot">
|
||||
<span class="bi @(BotService.Status.IsRunning ? "bi-pause-circle-fill" : "bi-play-circle-fill")"></span>
|
||||
@(BotService.Status.IsRunning ? "Stop Trading" : "Avvia Trading")
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assets Grid -->
|
||||
<div class="assets-section">
|
||||
<div class="section-header">
|
||||
<h2>Asset Monitorati</h2>
|
||||
<div class="filters">
|
||||
<select class="filter-select">
|
||||
<option>Tutti gli Asset</option>
|
||||
<option>Solo Attivi</option>
|
||||
<option>Solo Inattivi</option>
|
||||
</select>
|
||||
<button class="btn-icon">
|
||||
<span class="bi bi-funnel"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="assets-grid">
|
||||
@foreach (var config in BotService.AssetConfigurations.Values.OrderBy(c => c.Symbol))
|
||||
{
|
||||
var stats = BotService.AssetStatistics.TryGetValue(config.Symbol, out var s) ? s : null;
|
||||
var latestPrice = BotService.GetLatestPrice(config.Symbol);
|
||||
|
||||
<div class="asset-trading-card @(config.IsEnabled ? "enabled" : "disabled")">
|
||||
<div class="asset-header">
|
||||
<div class="asset-title">
|
||||
<span class="asset-icon">@config.Symbol.Substring(0, 1)</span>
|
||||
<div class="asset-name-group">
|
||||
<span class="name">@config.Name</span>
|
||||
<span class="symbol">@config.Symbol</span>
|
||||
</div>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox"
|
||||
checked="@config.IsEnabled"
|
||||
@onchange="(e) => ToggleAsset(config.Symbol, (bool)e.Value!)" />
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@if (latestPrice != null)
|
||||
{
|
||||
<div class="asset-price-info">
|
||||
<div class="current-price">$@latestPrice.Price.ToString("N2")</div>
|
||||
<div class="price-change @(latestPrice.Change24h >= 0 ? "positive" : "negative")">
|
||||
<span class="bi @(latestPrice.Change24h >= 0 ? "bi-arrow-up" : "bi-arrow-down")"></span>
|
||||
@Math.Abs(latestPrice.Change24h).ToString("F2")%
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="asset-price-info">
|
||||
<div class="current-price loading">Loading...</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="asset-strategy">
|
||||
<div class="strategy-label">Strategia Applicata</div>
|
||||
<div class="strategy-name">
|
||||
<span class="bi bi-diagram-3"></span>
|
||||
@config.StrategyName
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="asset-metrics">
|
||||
<div class="metric">
|
||||
<span class="metric-label">Holdings</span>
|
||||
<span class="metric-value">@config.CurrentHoldings.ToString("F4")</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Valore</span>
|
||||
<span class="metric-value">$@((config.CurrentBalance + config.CurrentHoldings * (latestPrice?.Price ?? 0)).ToString("N2"))</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Profitto</span>
|
||||
<span class="metric-value @(config.TotalProfit >= 0 ? "profit" : "loss")">
|
||||
$@config.TotalProfit.ToString("N2")
|
||||
</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">Trades</span>
|
||||
<span class="metric-value">@(stats?.TotalTrades ?? 0)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="asset-actions">
|
||||
<button class="btn-secondary btn-sm" @onclick="() => OpenAssetConfig(config.Symbol)">
|
||||
<span class="bi bi-gear"></span>
|
||||
Configura
|
||||
</button>
|
||||
<button class="btn-secondary btn-sm" @onclick="() => ViewChart(config.Symbol)">
|
||||
<span class="bi bi-graph-up"></span>
|
||||
Grafico
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Trades -->
|
||||
<div class="trades-section">
|
||||
<div class="section-header">
|
||||
<h2>Operazioni Recenti</h2>
|
||||
<button class="btn-secondary btn-sm">
|
||||
<span class="bi bi-clock-history"></span>
|
||||
Vedi Tutto
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (BotService.Trades.Count == 0)
|
||||
{
|
||||
<div class="empty-state">
|
||||
<span class="bi bi-inbox"></span>
|
||||
<p>Nessuna operazione ancora</p>
|
||||
<p class="hint">Avvia il trading per iniziare a eseguire operazioni</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="trades-table">
|
||||
<div class="table-header">
|
||||
<div>Asset</div>
|
||||
<div>Tipo</div>
|
||||
<div>Quantità</div>
|
||||
<div>Prezzo</div>
|
||||
<div>Valore</div>
|
||||
<div>Strategia</div>
|
||||
<div>Data/Ora</div>
|
||||
</div>
|
||||
@foreach (var trade in BotService.Trades.Take(20))
|
||||
{
|
||||
<div class="table-row @(trade.IsBot ? "bot-trade" : "")">
|
||||
<div class="cell-asset">
|
||||
<span class="asset-badge">@trade.Symbol</span>
|
||||
</div>
|
||||
<div class="cell-type @(trade.Type == TradeType.Buy ? "buy" : "sell")">
|
||||
<span class="bi @(trade.Type == TradeType.Buy ? "bi-arrow-down-circle-fill" : "bi-arrow-up-circle-fill")"></span>
|
||||
@(trade.Type == TradeType.Buy ? "BUY" : "SELL")
|
||||
</div>
|
||||
<div>@trade.Amount.ToString("F6")</div>
|
||||
<div>$@trade.Price.ToString("N2")</div>
|
||||
<div class="cell-value">$@((trade.Amount * trade.Price).ToString("N2"))</div>
|
||||
<div class="cell-strategy">
|
||||
@if (trade.IsBot)
|
||||
{
|
||||
<span class="strategy-tag">
|
||||
<span class="bi bi-robot"></span>
|
||||
@trade.Strategy
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="manual-tag">Manuale</span>
|
||||
}
|
||||
</div>
|
||||
<div class="cell-time">@trade.Timestamp.ToLocalTime().ToString("dd/MM HH:mm:ss")</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
BotService.OnStatusChanged += HandleUpdate;
|
||||
BotService.OnTradeExecuted += HandleTradeExecuted;
|
||||
BotService.OnPriceUpdated += HandlePriceUpdate;
|
||||
|
||||
if (!BotService.Status.IsRunning)
|
||||
{
|
||||
BotService.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleBot()
|
||||
{
|
||||
if (BotService.Status.IsRunning)
|
||||
BotService.Stop();
|
||||
else
|
||||
BotService.Start();
|
||||
}
|
||||
|
||||
private void ToggleAsset(string symbol, bool enabled)
|
||||
{
|
||||
BotService.ToggleAsset(symbol, enabled);
|
||||
}
|
||||
|
||||
private void OpenAssetConfig(string symbol)
|
||||
{
|
||||
// TODO: Open asset configuration modal
|
||||
}
|
||||
|
||||
private void ViewChart(string symbol)
|
||||
{
|
||||
// TODO: Navigate to market analysis with selected symbol
|
||||
}
|
||||
|
||||
private void HandleUpdate() => InvokeAsync(StateHasChanged);
|
||||
private void HandleTradeExecuted(Trade trade) => InvokeAsync(StateHasChanged);
|
||||
private void HandlePriceUpdate(string symbol, MarketPrice price) => InvokeAsync(StateHasChanged);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
BotService.OnStatusChanged -= HandleUpdate;
|
||||
BotService.OnTradeExecuted -= HandleTradeExecuted;
|
||||
BotService.OnPriceUpdated -= HandlePriceUpdate;
|
||||
}
|
||||
}
|
||||
478
TradingBot/Components/Pages/Trading.razor.css
Normal file
478
TradingBot/Components/Pages/Trading.razor.css
Normal file
@@ -0,0 +1,478 @@
|
||||
/* Trading Page */
|
||||
.trading-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0.5rem 0 0 0;
|
||||
color: #94a3b8;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
border: 1px solid #334155;
|
||||
background: #1e293b;
|
||||
color: #cbd5e1;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-toggle:hover {
|
||||
background: #334155;
|
||||
}
|
||||
|
||||
.btn-toggle.active {
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
border-color: #6366f1;
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
/* Section Header */
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #334155;
|
||||
background: #1e293b;
|
||||
color: white;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Assets Grid */
|
||||
.assets-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.asset-trading-card {
|
||||
background: #0f1629;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.asset-trading-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.4);
|
||||
border-color: #334155;
|
||||
}
|
||||
|
||||
.asset-trading-card.enabled {
|
||||
border-color: rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.asset-trading-card.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.asset-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.asset-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.asset-icon {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 0.5rem;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.asset-name-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.asset-name-group .name {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.asset-name-group .symbol {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* Toggle Switch */
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 3rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #334155;
|
||||
transition: 0.3s;
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.toggle-slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 1.125rem;
|
||||
width: 1.125rem;
|
||||
left: 0.1875rem;
|
||||
bottom: 0.1875rem;
|
||||
background-color: white;
|
||||
transition: 0.3s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + .toggle-slider {
|
||||
background-color: #6366f1;
|
||||
}
|
||||
|
||||
.toggle-switch input:checked + .toggle-slider:before {
|
||||
transform: translateX(1.5rem);
|
||||
}
|
||||
|
||||
/* Price Info */
|
||||
.asset-price-info {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.current-price {
|
||||
font-size: 1.875rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.current-price.loading {
|
||||
font-size: 1rem;
|
||||
color: #64748b;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.price-change {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.price-change.positive {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.price-change.negative {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Strategy */
|
||||
.asset-strategy {
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: #1a1f3a;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.strategy-label {
|
||||
font-size: 0.625rem;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.strategy-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #cbd5e1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Metrics */
|
||||
.asset-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 0.625rem;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.metric-value.profit {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.metric-value.loss {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
.asset-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Trades Table */
|
||||
.trades-table {
|
||||
background: #0f1629;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1.5fr 1.5fr 1.5fr 2fr 1.5fr;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.5rem;
|
||||
background: #1a1f3a;
|
||||
border-bottom: 1px solid #1e293b;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1.5fr 1.5fr 1.5fr 2fr 1.5fr;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid #1e293b;
|
||||
align-items: center;
|
||||
font-size: 0.875rem;
|
||||
color: #cbd5e1;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
background: #1a1f3a;
|
||||
}
|
||||
|
||||
.table-row.bot-trade {
|
||||
background: rgba(99, 102, 241, 0.05);
|
||||
}
|
||||
|
||||
.table-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.asset-badge {
|
||||
display: inline-flex;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: #1a1f3a;
|
||||
border-radius: 0.375rem;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 700;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.cell-type {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cell-type.buy {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.cell-type.sell {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.cell-value {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.strategy-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
color: #6366f1;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.manual-tag {
|
||||
display: inline-flex;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: rgba(100, 116, 139, 0.2);
|
||||
color: #64748b;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cell-time {
|
||||
color: #64748b;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
background: #0f1629;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.empty-state .bi {
|
||||
font-size: 3rem;
|
||||
color: #334155;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0.5rem 0;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.empty-state .hint {
|
||||
font-size: 0.875rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1024px) {
|
||||
.assets-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
}
|
||||
|
||||
.table-header, .table-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.assets-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
6
TradingBot/Components/Routes.razor
Normal file
6
TradingBot/Components/Routes.razor
Normal file
@@ -0,0 +1,6 @@
|
||||
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
128
TradingBot/Components/Shared/AdvancedChart.razor
Normal file
128
TradingBot/Components/Shared/AdvancedChart.razor
Normal file
@@ -0,0 +1,128 @@
|
||||
@using TradingBot.Models
|
||||
|
||||
<div class="advanced-chart">
|
||||
@if (PriceData == null || PriceData.Count < 2)
|
||||
{
|
||||
<div class="chart-loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<span>In attesa di dati sufficienti...</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<svg viewBox="0 0 @Width @Height" class="chart-svg" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<linearGradient id="gradient-@ColorId" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="@Color" stop-opacity="0.4" />
|
||||
<stop offset="100%" stop-color="@Color" stop-opacity="0.05" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Grid Lines -->
|
||||
@for (int i = 0; i <= 4; i++)
|
||||
{
|
||||
var y = (Height * i / 4.0);
|
||||
<line x1="0" y1="@y" x2="@Width" y2="@y"
|
||||
stroke="rgba(51, 65, 85, 0.3)" stroke-width="1" stroke-dasharray="4 4" />
|
||||
}
|
||||
|
||||
<!-- Area Fill -->
|
||||
@if (!string.IsNullOrEmpty(GetAreaPath()))
|
||||
{
|
||||
<path d="@GetAreaPath()" fill="url(#gradient-@ColorId)" />
|
||||
}
|
||||
|
||||
<!-- Line Chart -->
|
||||
@if (!string.IsNullOrEmpty(GetPolylinePoints()))
|
||||
{
|
||||
<polyline fill="none" stroke="@Color" stroke-width="3"
|
||||
points="@GetPolylinePoints()"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
}
|
||||
</svg>
|
||||
|
||||
@if (Indicators != null)
|
||||
{
|
||||
<div class="indicators-overlay">
|
||||
<div class="indicator-badge">
|
||||
<span class="indicator-label">RSI:</span>
|
||||
<span class="indicator-value @GetRSIClass()">@Indicators.RSI.ToString("F1")</span>
|
||||
</div>
|
||||
<div class="indicator-badge">
|
||||
<span class="indicator-label">MACD:</span>
|
||||
<span class="indicator-value">@Indicators.MACD.ToString("F2")</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public List<decimal>? PriceData { get; set; }
|
||||
[Parameter] public string Color { get; set; } = "#6366f1";
|
||||
[Parameter] public TechnicalIndicators? Indicators { get; set; }
|
||||
|
||||
private int Width = 800;
|
||||
private int Height = 300;
|
||||
private string ColorId => Guid.NewGuid().ToString("N").Substring(0, 8);
|
||||
|
||||
private string GetPolylinePoints()
|
||||
{
|
||||
if (PriceData == null || PriceData.Count < 2) return string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
var max = PriceData.Max();
|
||||
var min = PriceData.Min();
|
||||
var range = max - min;
|
||||
if (range == 0) range = max * 0.01m; // 1% range if all values are same
|
||||
|
||||
var points = new List<string>();
|
||||
var padding = Height * 0.1; // 10% padding
|
||||
var chartHeight = Height - (padding * 2);
|
||||
|
||||
for (int i = 0; i < PriceData.Count; i++)
|
||||
{
|
||||
var x = (i / (double)(PriceData.Count - 1)) * Width;
|
||||
var normalizedValue = (double)((PriceData[i] - min) / range);
|
||||
var y = padding + (chartHeight * (1 - normalizedValue));
|
||||
points.Add($"{x:F2},{y:F2}");
|
||||
}
|
||||
|
||||
return string.Join(" ", points);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetAreaPath()
|
||||
{
|
||||
var polyline = GetPolylinePoints();
|
||||
if (string.IsNullOrEmpty(polyline)) return string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
var points = polyline.Split(' ');
|
||||
if (points.Length < 2) return string.Empty;
|
||||
|
||||
var firstPoint = points[0].Split(',');
|
||||
var lastPoint = points[points.Length - 1].Split(',');
|
||||
|
||||
return $"M {firstPoint[0]},{Height} L {polyline} L {lastPoint[0]},{Height} Z";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetRSIClass()
|
||||
{
|
||||
if (Indicators == null) return "rsi-neutral";
|
||||
if (Indicators.RSI > 70) return "rsi-overbought";
|
||||
if (Indicators.RSI < 30) return "rsi-oversold";
|
||||
return "rsi-neutral";
|
||||
}
|
||||
}
|
||||
82
TradingBot/Components/Shared/AdvancedChart.razor.css
Normal file
82
TradingBot/Components/Shared/AdvancedChart.razor.css
Normal file
@@ -0,0 +1,82 @@
|
||||
.advanced-chart {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
background: #0a0e27;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chart-svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.chart-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 300px;
|
||||
gap: 1rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 3px solid #1e293b;
|
||||
border-top-color: #6366f1;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.indicators-overlay {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.indicator-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: rgba(15, 23, 42, 0.9);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid rgba(99, 102, 241, 0.2);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.indicator-label {
|
||||
color: #94a3b8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.indicator-value {
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.indicator-value.rsi-overbought {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.indicator-value.rsi-oversold {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.indicator-value.rsi-neutral {
|
||||
color: #f59e0b;
|
||||
}
|
||||
344
TradingBot/Components/Shared/AssetSettings.razor
Normal file
344
TradingBot/Components/Shared/AssetSettings.razor
Normal file
@@ -0,0 +1,344 @@
|
||||
@using TradingBot.Models
|
||||
@using TradingBot.Services
|
||||
@inject TradingBotService BotService
|
||||
|
||||
<div class="asset-settings-modal @(IsOpen ? "open" : "")">
|
||||
<div class="modal-overlay" @onclick="Close"></div>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>
|
||||
<span class="bi bi-gear-fill"></span>
|
||||
Impostazioni {{Config?.Symbol}}
|
||||
</h3>
|
||||
<button class="btn-close" @onclick="Close">
|
||||
<span class="bi bi-x-lg"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (Config != null)
|
||||
{
|
||||
<div class="modal-body">
|
||||
|
||||
<!-- Basic Settings -->
|
||||
<div class="settings-section">
|
||||
<h4 class="section-title">Impostazioni Base</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Stato</label>
|
||||
<div class="toggle-wrapper">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox"
|
||||
@bind="Config.IsEnabled" />
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
<span class="toggle-label">
|
||||
{{Config.IsEnabled ? "Attivo" : "Inattivo"}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Bilancio Iniziale ($)</label>
|
||||
<input type="number"
|
||||
class="form-input"
|
||||
@bind="Config.InitialBalance"
|
||||
step="100"
|
||||
min="0" />
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Bilancio Corrente ($)</label>
|
||||
<input type="number"
|
||||
class="form-input"
|
||||
value="@Config.CurrentBalance"
|
||||
readonly />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Holdings</label>
|
||||
<input type="number"
|
||||
class="form-input"
|
||||
value="@Config.CurrentHoldings"
|
||||
readonly />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Risk Management -->
|
||||
<div class="settings-section">
|
||||
<h4 class="section-title">Gestione del Rischio</h4>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Stop Loss (%)</label>
|
||||
<input type="number"
|
||||
class="form-input"
|
||||
@bind="Config.StopLossPercentage"
|
||||
step="0.5"
|
||||
min="0"
|
||||
max="100" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Take Profit (%)</label>
|
||||
<input type="number"
|
||||
class="form-input"
|
||||
@bind="Config.TakeProfitPercentage"
|
||||
step="0.5"
|
||||
min="0"
|
||||
max="100" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Dimensione Massima Posizione ($)</label>
|
||||
<input type="number"
|
||||
class="form-input"
|
||||
@bind="Config.MaxPositionSize"
|
||||
step="10"
|
||||
min="0" />
|
||||
<small class="form-hint">
|
||||
Massimo valore totale della posizione in dollari
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Trading Constraints -->
|
||||
<div class="settings-section">
|
||||
<h4 class="section-title">Limiti di Trading</h4>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Min Trade ($)</label>
|
||||
<input type="number"
|
||||
class="form-input"
|
||||
@bind="Config.MinTradeAmount"
|
||||
step="1"
|
||||
min="1" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Max Trade ($)</label>
|
||||
<input type="number"
|
||||
class="form-input"
|
||||
@bind="Config.MaxTradeAmount"
|
||||
step="10"
|
||||
min="1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Max Operazioni Giornaliere</label>
|
||||
<input type="number"
|
||||
class="form-input"
|
||||
@bind="Config.MaxDailyTrades"
|
||||
step="1"
|
||||
min="1"
|
||||
max="100" />
|
||||
<small class="form-hint">
|
||||
Operazioni oggi: {{Config.DailyTradeCount}} / {{Config.MaxDailyTrades}}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Strategy Parameters -->
|
||||
<div class="settings-section">
|
||||
<h4 class="section-title">Parametri Strategia</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Strategia</label>
|
||||
<input type="text"
|
||||
class="form-input"
|
||||
value="@Config.StrategyName"
|
||||
readonly />
|
||||
</div>
|
||||
|
||||
@if (Config.StrategyParameters.ContainsKey("ShortPeriod"))
|
||||
{
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Periodo Corto</label>
|
||||
<input type="number"
|
||||
class="form-input"
|
||||
value="@Config.StrategyParameters["ShortPeriod"]"
|
||||
@onchange="@((e) => UpdateStrategyParameter("ShortPeriod", e.Value))"
|
||||
step="1"
|
||||
min="5"
|
||||
max="50" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Periodo Lungo</label>
|
||||
<input type="number"
|
||||
class="form-input"
|
||||
value="@Config.StrategyParameters["LongPeriod"]"
|
||||
@onchange="@((e) => UpdateStrategyParameter("LongPeriod", e.Value))"
|
||||
step="1"
|
||||
min="10"
|
||||
max="100" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Config.StrategyParameters.ContainsKey("SignalThreshold"))
|
||||
{
|
||||
<div class="form-group">
|
||||
<label>Soglia Segnale</label>
|
||||
<input type="number"
|
||||
class="form-input"
|
||||
value="@Config.StrategyParameters["SignalThreshold"]"
|
||||
@onchange="@((e) => UpdateStrategyParameter("SignalThreshold", e.Value))"
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="5" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Current Stats -->
|
||||
<div class="settings-section stats-section">
|
||||
<h4 class="section-title">Statistiche Correnti</h4>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Profitto Totale</span>
|
||||
<span class="stat-value @(Config.TotalProfit >= 0 ? "profit" : "loss")">
|
||||
${{Config.TotalProfit:N2}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">% Profitto</span>
|
||||
<span class="stat-value @(Config.ProfitPercentage >= 0 ? "profit" : "loss")">
|
||||
{{Config.ProfitPercentage:F2}}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Prezzo Medio Entrata</span>
|
||||
<span class="stat-value">
|
||||
${{Config.AverageEntryPrice:N2}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">Ultimo Trade</span>
|
||||
<span class="stat-value">
|
||||
{{(Config.LastTradeTime?.ToLocalTime().ToString("HH:mm:ss") ?? "Mai")}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" @onclick="ResetToDefaults">
|
||||
<span class="bi bi-arrow-counterclockwise"></span>
|
||||
Reset
|
||||
</button>
|
||||
<button class="btn-primary" @onclick="SaveSettings">
|
||||
<span class="bi bi-check-lg"></span>
|
||||
Salva Modifiche
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public bool IsOpen { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? Symbol { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback OnClose { get; set; }
|
||||
|
||||
private AssetConfiguration? Config { get; set; }
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (IsOpen && !string.IsNullOrEmpty(Symbol))
|
||||
{
|
||||
LoadConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadConfiguration()
|
||||
{
|
||||
if (BotService.AssetConfigurations.TryGetValue(Symbol!, out var config))
|
||||
{
|
||||
// Create a copy to avoid modifying the original until saved
|
||||
Config = new AssetConfiguration
|
||||
{
|
||||
Symbol = config.Symbol,
|
||||
Name = config.Name,
|
||||
IsEnabled = config.IsEnabled,
|
||||
InitialBalance = config.InitialBalance,
|
||||
CurrentBalance = config.CurrentBalance,
|
||||
CurrentHoldings = config.CurrentHoldings,
|
||||
AverageEntryPrice = config.AverageEntryPrice,
|
||||
StrategyName = config.StrategyName,
|
||||
StrategyParameters = new Dictionary<string, object>(config.StrategyParameters),
|
||||
MaxPositionSize = config.MaxPositionSize,
|
||||
StopLossPercentage = config.StopLossPercentage,
|
||||
TakeProfitPercentage = config.TakeProfitPercentage,
|
||||
MinTradeAmount = config.MinTradeAmount,
|
||||
MaxTradeAmount = config.MaxTradeAmount,
|
||||
MaxDailyTrades = config.MaxDailyTrades,
|
||||
LastTradeTime = config.LastTradeTime,
|
||||
DailyTradeCount = config.DailyTradeCount,
|
||||
DailyTradeCountReset = config.DailyTradeCountReset
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateStrategyParameter(string key, object? value)
|
||||
{
|
||||
if (Config != null && value != null)
|
||||
{
|
||||
if (value is string strValue)
|
||||
{
|
||||
if (decimal.TryParse(strValue, out var decValue))
|
||||
{
|
||||
Config.StrategyParameters[key] = decValue;
|
||||
}
|
||||
else if (int.TryParse(strValue, out var intValue))
|
||||
{
|
||||
Config.StrategyParameters[key] = intValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetToDefaults()
|
||||
{
|
||||
if (Config != null)
|
||||
{
|
||||
Config.InitialBalance = 1000m;
|
||||
Config.StopLossPercentage = 5m;
|
||||
Config.TakeProfitPercentage = 10m;
|
||||
Config.MaxPositionSize = 100m;
|
||||
Config.MinTradeAmount = 10m;
|
||||
Config.MaxTradeAmount = 500m;
|
||||
Config.MaxDailyTrades = 10;
|
||||
Config.StrategyParameters = new Dictionary<string, object>
|
||||
{
|
||||
{ "ShortPeriod", 10 },
|
||||
{ "LongPeriod", 30 },
|
||||
{ "SignalThreshold", 0.5m }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveSettings()
|
||||
{
|
||||
if (Config != null && !string.IsNullOrEmpty(Symbol))
|
||||
{
|
||||
BotService.UpdateAssetConfiguration(Symbol, Config);
|
||||
await Close();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Close()
|
||||
{
|
||||
IsOpen = false;
|
||||
await OnClose.InvokeAsync();
|
||||
}
|
||||
}
|
||||
362
TradingBot/Components/Shared/AssetSettings.razor.css
Normal file
362
TradingBot/Components/Shared/AssetSettings.razor.css
Normal file
@@ -0,0 +1,362 @@
|
||||
/* Asset Settings Modal */
|
||||
.asset-settings-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.asset-settings-modal.open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
width: 90%;
|
||||
max-width: 700px;
|
||||
max-height: 90vh;
|
||||
background: #0f172a;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
animation: modal-in 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes modal-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal Header */
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
background: #1e293b;
|
||||
border-bottom: 1px solid #334155;
|
||||
border-radius: 0.75rem 0.75rem 0 0;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 0.375rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-close:hover {
|
||||
background: #334155;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Modal Body */
|
||||
.modal-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Settings Section */
|
||||
.settings-section {
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 1px solid #1e293b;
|
||||
}
|
||||
|
||||
.settings-section:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0 0 1.5rem 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* Form Elements */
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: #020617;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 0.5rem;
|
||||
color: white;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.form-input:disabled,
|
||||
.form-input:read-only {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
background: #1e293b;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
display: block;
|
||||
margin-top: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Toggle Switch */
|
||||
.toggle-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
/* Stats Section */
|
||||
.stats-section {
|
||||
background: #020617;
|
||||
border: 1px solid #1e293b;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.75rem;
|
||||
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: monospace;
|
||||
}
|
||||
|
||||
.stat-value.profit {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.stat-value.loss {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Modal Footer */
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
background: #020617;
|
||||
border-top: 1px solid #1e293b;
|
||||
border-radius: 0 0 0.75rem 0.75rem;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary {
|
||||
display: 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.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 12px -2px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: #94a3b8;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #1e293b;
|
||||
border-color: #475569;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Scrollbar Styling */
|
||||
.modal-body::-webkit-scrollbar {
|
||||
width: 0.5rem;
|
||||
}
|
||||
|
||||
.modal-body::-webkit-scrollbar-track {
|
||||
background: #0f172a;
|
||||
}
|
||||
|
||||
.modal-body::-webkit-scrollbar-thumb {
|
||||
background: #334155;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.modal-body::-webkit-scrollbar-thumb:hover {
|
||||
background: #475569;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
max-height: 95vh;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.modal-header, .modal-body, .modal-footer {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
0
TradingBot/Components/Shared/TabNavigation.razor
Normal file
0
TradingBot/Components/Shared/TabNavigation.razor
Normal file
13
TradingBot/Components/_Imports.razor
Normal file
13
TradingBot/Components/_Imports.razor
Normal file
@@ -0,0 +1,13 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using TradingBot
|
||||
@using TradingBot.Components
|
||||
@using TradingBot.Components.Layout
|
||||
@using TradingBot.Models
|
||||
@using TradingBot.Services
|
||||
Reference in New Issue
Block a user