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

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

View File

@@ -0,0 +1,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>

View 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;
}
}

View 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;
}
}

View 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>

View 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;
}
}

View 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();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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>

View 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";
}
}

View 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;
}

View 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();
}
}

View 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;
}
}

View 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