feat: apply #28B smart breakeven + #31B H1 EMA20 filter, add backtests #26-#32
Live trading optimizations (cumulative: $2,807 net, 81.8% WR, Sharpe 3.97): - #28B: Smart breakeven locks profit at entry + 0.5x ATR instead of fixed $2 - #31B: H1 Price vs EMA20 filter — BUY only when H1 bullish, SELL only when bearish Backtests #26-#32 (7 scripts testing sell improvement, regime-aware entry, confluence scoring, dynamic RR, multi-TF H1, and ML exit optimizer). Winners: #28B (+$229), #31B (+$343). Failed: #26, #27, #29, #30, #32. Also includes: web dashboard redesign, Docker setup, startup scripts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
53d8cd26a2
commit
214b64945d
+236
-115
@@ -1,125 +1,246 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
@theme {
|
||||
/* Background layers — soft dark, GitHub Dark Dimmed inspired */
|
||||
--color-background: oklch(0.21 0.01 250);
|
||||
--color-foreground: oklch(0.85 0.01 250);
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--radius-2xl: calc(var(--radius) + 8px);
|
||||
--radius-3xl: calc(var(--radius) + 12px);
|
||||
--radius-4xl: calc(var(--radius) + 16px);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
--color-surface: oklch(0.25 0.01 250);
|
||||
--color-surface-light: oklch(0.30 0.008 250);
|
||||
--color-surface-hover: oklch(0.34 0.008 250);
|
||||
|
||||
--color-card: oklch(0.25 0.01 250);
|
||||
--color-card-foreground: oklch(0.85 0.01 250);
|
||||
|
||||
--color-popover: oklch(0.25 0.01 250);
|
||||
--color-popover-foreground: oklch(0.85 0.01 250);
|
||||
|
||||
/* Primary — calm blue */
|
||||
--color-primary: oklch(0.62 0.18 255);
|
||||
--color-primary-foreground: oklch(0.98 0 0);
|
||||
--color-primary-dark: oklch(0.56 0.18 255);
|
||||
|
||||
--color-secondary: oklch(0.30 0.008 250);
|
||||
--color-secondary-foreground: oklch(0.85 0.01 250);
|
||||
|
||||
--color-muted: oklch(0.30 0.008 250);
|
||||
--color-muted-foreground: oklch(0.58 0.01 250);
|
||||
|
||||
--color-accent: oklch(0.62 0.17 290);
|
||||
--color-accent-foreground: oklch(0.98 0 0);
|
||||
|
||||
--color-destructive: oklch(0.62 0.19 25);
|
||||
--color-destructive-foreground: oklch(0.98 0 0);
|
||||
|
||||
/* Borders — gentle, not harsh */
|
||||
--color-border: oklch(0.34 0.008 250);
|
||||
--color-border-light: oklch(0.40 0.006 250);
|
||||
|
||||
--color-input: oklch(0.34 0.008 250);
|
||||
--color-ring: oklch(0.62 0.18 255);
|
||||
|
||||
/* Semantic colors — softer, less saturated */
|
||||
--color-success: oklch(0.68 0.15 155);
|
||||
--color-success-bg: oklch(0.68 0.15 155 / 0.12);
|
||||
|
||||
--color-warning: oklch(0.76 0.14 75);
|
||||
--color-warning-bg: oklch(0.76 0.14 75 / 0.12);
|
||||
|
||||
--color-danger: oklch(0.62 0.19 25);
|
||||
--color-danger-bg: oklch(0.62 0.19 25 / 0.12);
|
||||
|
||||
--color-info: oklch(0.65 0.15 250);
|
||||
--color-info-bg: oklch(0.65 0.15 250 / 0.12);
|
||||
|
||||
/* Charts */
|
||||
--color-chart-1: oklch(0.62 0.18 255);
|
||||
--color-chart-2: oklch(0.68 0.15 155);
|
||||
--color-chart-3: oklch(0.76 0.14 75);
|
||||
--color-chart-4: oklch(0.62 0.17 290);
|
||||
--color-chart-5: oklch(0.62 0.19 25);
|
||||
|
||||
/* Radius */
|
||||
--radius-sm: calc(0.625rem - 4px);
|
||||
--radius-md: calc(0.625rem - 2px);
|
||||
--radius-lg: 0.625rem;
|
||||
--radius-xl: 0.875rem;
|
||||
|
||||
/* Fonts */
|
||||
--font-sans: var(--font-inter), 'Inter', system-ui, sans-serif;
|
||||
--font-mono: var(--font-jetbrains), 'JetBrains Mono', 'Fira Code', monospace;
|
||||
|
||||
/* Animations */
|
||||
--animate-pulse-slow: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
--animate-fade-in: fadeIn 0.4s ease-out;
|
||||
--animate-slide-up: slideUp 0.4s ease-out;
|
||||
--animate-shimmer: shimmer 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* ─── Base ─── */
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
border-color: var(--color-border);
|
||||
outline-color: color-mix(in oklch, var(--color-ring) 50%, transparent);
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
|
||||
html {
|
||||
color-scheme: dark;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
html, body {
|
||||
@apply bg-background text-foreground font-sans;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-feature-settings: "cv02", "cv03", "cv04", "cv11";
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Scrollbar ─── */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-border-light);
|
||||
}
|
||||
|
||||
/* ─── Utilities ─── */
|
||||
@layer utilities {
|
||||
/* Glass — soft frosted effect */
|
||||
.glass {
|
||||
background: color-mix(in oklch, var(--color-surface) 90%, transparent);
|
||||
backdrop-filter: blur(10px) saturate(120%);
|
||||
-webkit-backdrop-filter: blur(10px) saturate(120%);
|
||||
border: 1px solid color-mix(in oklch, var(--color-border) 50%, transparent);
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0, 0, 0, 0.12),
|
||||
0 0 1px rgba(0, 0, 0, 0.08);
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.glass:hover {
|
||||
border-color: var(--color-border-light);
|
||||
}
|
||||
|
||||
/* Monospace numbers with tabular figures */
|
||||
.font-number {
|
||||
font-family: var(--font-mono);
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* Section label */
|
||||
.section-label {
|
||||
@apply text-[11px] font-medium text-muted-foreground uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
/* Signal border accents */
|
||||
.signal-buy {
|
||||
border-left: 3px solid var(--color-success);
|
||||
}
|
||||
|
||||
.signal-sell {
|
||||
border-left: 3px solid var(--color-danger);
|
||||
}
|
||||
|
||||
.signal-hold {
|
||||
border-left: 3px solid var(--color-warning);
|
||||
}
|
||||
|
||||
.signal-none {
|
||||
border-left: 3px solid var(--color-muted);
|
||||
}
|
||||
|
||||
/* Badge variants */
|
||||
.badge-success {
|
||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-success-bg text-success;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-warning-bg text-warning;
|
||||
}
|
||||
|
||||
.badge-danger {
|
||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-danger-bg text-danger;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-info-bg text-info;
|
||||
}
|
||||
|
||||
/* Text gradient */
|
||||
.text-gradient {
|
||||
@apply bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent;
|
||||
}
|
||||
|
||||
/* Skeleton */
|
||||
.skeleton {
|
||||
@apply bg-surface-light rounded;
|
||||
animation: shimmer 2s ease-in-out infinite;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--color-surface) 0%,
|
||||
var(--color-surface-light) 50%,
|
||||
var(--color-surface) 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
}
|
||||
|
||||
/* Live pulse dot */
|
||||
.pulse-live::before {
|
||||
content: '';
|
||||
@apply absolute -left-2 top-1/2 -translate-y-1/2 w-1.5 h-1.5 bg-success rounded-full;
|
||||
animation: pulse-dot 2s infinite;
|
||||
}
|
||||
|
||||
.pulse-stale::before {
|
||||
content: '';
|
||||
@apply absolute -left-2 top-1/2 -translate-y-1/2 w-1.5 h-1.5 bg-warning rounded-full;
|
||||
animation: pulse-dot 1.5s infinite;
|
||||
}
|
||||
|
||||
.pulse-dead::before {
|
||||
content: '';
|
||||
@apply absolute -left-2 top-1/2 -translate-y-1/2 w-1.5 h-1.5 bg-danger rounded-full;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Keyframes ─── */
|
||||
@keyframes pulse-dot {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: translateY(-50%) scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
transform: translateY(-50%) scale(1.8);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% { opacity: 0; transform: translateY(8px); }
|
||||
100% { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
0% { transform: translateY(12px); opacity: 0; }
|
||||
100% { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { Inter, JetBrains_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
const inter = Inter({
|
||||
variable: "--font-inter",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
variable: "--font-jetbrains",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "AI Trading Bot - Monitor",
|
||||
description: "Real-time monitoring dashboard for AI Trading Bot",
|
||||
title: "XAUBOT AI — Trading Monitor",
|
||||
description: "Real-time monitoring dashboard for XAUBOT AI Trading Bot",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -25,7 +27,7 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en" className="dark">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-background text-foreground`}
|
||||
className={`${inter.variable} ${jetbrainsMono.variable} antialiased bg-background text-foreground`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
|
||||
+131
-88
@@ -12,27 +12,41 @@ import {
|
||||
PositionsCard,
|
||||
LogCard,
|
||||
PriceChart,
|
||||
EquityChart,
|
||||
SettingsCard,
|
||||
} from "@/components/dashboard";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
function LoadingSkeleton() {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4 p-4">
|
||||
{[...Array(8)].map((_, i) => (
|
||||
<Skeleton key={i} className="h-[150px] rounded-xl" />
|
||||
))}
|
||||
<div className="flex-1 min-h-0 flex flex-col gap-1.5 p-1.5">
|
||||
<div className="flex gap-1.5">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<Skeleton key={`r1-${i}`} className="flex-1 h-[80px] rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-1.5">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<Skeleton key={`r2-${i}`} className="flex-1 h-[90px] rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-1 min-h-0 flex gap-1.5">
|
||||
<Skeleton className="flex-[3] rounded-lg" />
|
||||
<Skeleton className="flex-1 rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorDisplay({ message }: { message: string }) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-[80vh]">
|
||||
<div className="text-center">
|
||||
<p className="text-destructive text-lg font-semibold">Connection Error</p>
|
||||
<p className="text-muted-foreground">{message}</p>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<div className="text-center space-y-3">
|
||||
<div className="w-12 h-12 rounded-full bg-danger-bg mx-auto flex items-center justify-center">
|
||||
<span className="text-danger text-xl">!</span>
|
||||
</div>
|
||||
<p className="text-danger text-base font-semibold">Connection Error</p>
|
||||
<p className="text-muted-foreground text-sm">{message}</p>
|
||||
<p className="text-muted-foreground/60 text-xs">
|
||||
Make sure the API server is running on port 8000
|
||||
</p>
|
||||
</div>
|
||||
@@ -43,19 +57,18 @@ function ErrorDisplay({ message }: { message: string }) {
|
||||
export default function Dashboard() {
|
||||
const { data, loading, error, dataAge } = useTradingData();
|
||||
|
||||
// Format current time for header
|
||||
const now = new Date();
|
||||
const wibTime = now.toLocaleTimeString('en-US', {
|
||||
timeZone: 'Asia/Jakarta',
|
||||
const wibTime = now.toLocaleTimeString("en-US", {
|
||||
timeZone: "Asia/Jakarta",
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
|
||||
if (loading && !data) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="fixed inset-0 overflow-hidden flex flex-col bg-background">
|
||||
<Header connected={false} lastUpdate={wibTime} dataAge={999} />
|
||||
<LoadingSkeleton />
|
||||
</div>
|
||||
@@ -64,7 +77,7 @@ export default function Dashboard() {
|
||||
|
||||
if (error && !data) {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="fixed inset-0 overflow-hidden flex flex-col bg-background">
|
||||
<Header connected={false} lastUpdate={wibTime} dataAge={999} />
|
||||
<ErrorDisplay message={error} />
|
||||
</div>
|
||||
@@ -74,86 +87,116 @@ export default function Dashboard() {
|
||||
if (!data) return null;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="fixed inset-0 overflow-hidden flex flex-col bg-background max-w-full">
|
||||
<Header
|
||||
connected={data.connected}
|
||||
lastUpdate={wibTime}
|
||||
dataAge={dataAge}
|
||||
/>
|
||||
|
||||
<main className="container py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Row 1: Price Chart (full width) */}
|
||||
<PriceChart data={data.priceHistory} />
|
||||
<main className="flex-1 min-h-0 flex flex-col gap-1.5 p-1.5 overflow-hidden">
|
||||
{/* ── Row 1: Status ── */}
|
||||
<div
|
||||
className="grid gap-1.5 overflow-hidden"
|
||||
style={{ gridTemplateColumns: 'repeat(4, minmax(0, 1fr))' }}
|
||||
>
|
||||
<div className="min-w-0 overflow-hidden">
|
||||
<PriceCard
|
||||
price={data.price}
|
||||
spread={data.spread}
|
||||
priceChange={data.priceChange}
|
||||
priceHistory={data.priceHistory}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 overflow-hidden">
|
||||
<AccountCard
|
||||
balance={data.balance}
|
||||
equity={data.equity}
|
||||
profit={data.profit}
|
||||
equityHistory={data.equityHistory}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 overflow-hidden">
|
||||
<SessionCard
|
||||
session={data.session}
|
||||
isGoldenTime={data.isGoldenTime}
|
||||
canTrade={data.canTrade}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 overflow-hidden">
|
||||
<RiskCard
|
||||
dailyLoss={data.dailyLoss}
|
||||
dailyProfit={data.dailyProfit}
|
||||
consecutiveLosses={data.consecutiveLosses}
|
||||
riskPercent={data.riskPercent}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Price & Account */}
|
||||
<PriceCard
|
||||
price={data.price}
|
||||
spread={data.spread}
|
||||
priceChange={data.priceChange}
|
||||
/>
|
||||
<AccountCard
|
||||
balance={data.balance}
|
||||
equity={data.equity}
|
||||
profit={data.profit}
|
||||
/>
|
||||
{/* ── Row 2: Signals ── */}
|
||||
<div
|
||||
className="grid gap-1.5 overflow-hidden"
|
||||
style={{ gridTemplateColumns: 'repeat(4, minmax(0, 1fr))' }}
|
||||
>
|
||||
<div className="min-w-0 overflow-hidden">
|
||||
<SignalCard
|
||||
title="SMC Signal"
|
||||
icon="smc"
|
||||
signal={data.smc.signal}
|
||||
confidence={data.smc.confidence}
|
||||
detail={`${data.smc.reason || ""}${data.h1Bias ? ` | H1: ${data.h1Bias}` : ""}`}
|
||||
updatedAt={data.smc.updatedAt}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 overflow-hidden">
|
||||
<SignalCard
|
||||
title="ML Prediction"
|
||||
icon="ml"
|
||||
signal={data.ml.signal}
|
||||
confidence={data.ml.confidence}
|
||||
buyProb={data.ml.buyProb}
|
||||
sellProb={data.ml.sellProb}
|
||||
updatedAt={data.ml.updatedAt}
|
||||
threshold={data.dynamicThreshold}
|
||||
marketQuality={data.marketQuality}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 overflow-hidden">
|
||||
<RegimeCard
|
||||
name={data.regime.name}
|
||||
volatility={data.regime.volatility}
|
||||
confidence={data.regime.confidence}
|
||||
updatedAt={data.regime.updatedAt}
|
||||
h1Bias={data.h1Bias}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 overflow-hidden">
|
||||
{data.settings ? (
|
||||
<SettingsCard settings={data.settings} />
|
||||
) : (
|
||||
<div className="glass rounded-lg h-full" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 3: Session & Risk */}
|
||||
<SessionCard
|
||||
session={data.session}
|
||||
isGoldenTime={data.isGoldenTime}
|
||||
canTrade={data.canTrade}
|
||||
/>
|
||||
<RiskCard
|
||||
dailyLoss={data.dailyLoss}
|
||||
dailyProfit={data.dailyProfit}
|
||||
consecutiveLosses={data.consecutiveLosses}
|
||||
riskPercent={data.riskPercent}
|
||||
/>
|
||||
|
||||
{/* Row 4: SMC & ML */}
|
||||
<SignalCard
|
||||
title="SMC SIGNAL"
|
||||
icon="smc"
|
||||
signal={data.smc.signal}
|
||||
confidence={data.smc.confidence}
|
||||
detail={data.smc.reason}
|
||||
/>
|
||||
<SignalCard
|
||||
title="ML PREDICTION"
|
||||
icon="ml"
|
||||
signal={data.ml.signal}
|
||||
confidence={data.ml.confidence}
|
||||
buyProb={data.ml.buyProb}
|
||||
sellProb={data.ml.sellProb}
|
||||
/>
|
||||
|
||||
{/* Row 5: Regime & Positions */}
|
||||
<RegimeCard
|
||||
name={data.regime.name}
|
||||
volatility={data.regime.volatility}
|
||||
confidence={data.regime.confidence}
|
||||
/>
|
||||
<PositionsCard positions={data.positions} />
|
||||
|
||||
{/* Row 6: Equity Chart (full width) */}
|
||||
<EquityChart
|
||||
equityData={data.equityHistory}
|
||||
balanceData={data.balanceHistory}
|
||||
/>
|
||||
|
||||
{/* Row 7: Log (full width) */}
|
||||
<LogCard logs={data.logs} />
|
||||
{/* ── Row 3: Chart + Sidebar (fills remaining) ── */}
|
||||
<div
|
||||
className="flex-1 min-h-0 grid gap-1.5 overflow-hidden"
|
||||
style={{ gridTemplateColumns: '3fr 1fr' }}
|
||||
>
|
||||
<div className="min-w-0 min-h-0 overflow-hidden">
|
||||
<PriceChart data={data.priceHistory} />
|
||||
</div>
|
||||
<div className="min-w-0 min-h-0 overflow-hidden flex flex-col gap-1.5">
|
||||
<div className="flex-1 min-h-0">
|
||||
<PositionsCard positions={data.positions} />
|
||||
</div>
|
||||
<div className="flex-1 min-h-0">
|
||||
<LogCard logs={data.logs} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Footer Status */}
|
||||
<footer className="fixed bottom-0 w-full border-t bg-background/95 backdrop-blur py-2">
|
||||
<div className="container flex justify-between text-xs text-muted-foreground">
|
||||
<span>Last update: {data.timestamp}</span>
|
||||
<span>AI Trading Bot Monitor v1.0</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,39 +2,52 @@
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Wallet } from "lucide-react";
|
||||
import { Sparkline } from "./sparkline";
|
||||
import { cn, formatUSD, getValueColor } from "@/lib/utils";
|
||||
|
||||
interface AccountCardProps {
|
||||
balance: number;
|
||||
equity: number;
|
||||
profit: number;
|
||||
equityHistory?: number[];
|
||||
}
|
||||
|
||||
export function AccountCard({ balance, equity, profit }: AccountCardProps) {
|
||||
export function AccountCard({ balance, equity, profit, equityHistory = [] }: AccountCardProps) {
|
||||
const isProfit = profit >= 0;
|
||||
|
||||
return (
|
||||
<Card className="bg-card/50 backdrop-blur">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
||||
<Wallet className="h-4 w-4" />
|
||||
ACCOUNT
|
||||
<Card className="glass">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-[11px] font-medium text-muted-foreground flex items-center gap-1.5 uppercase tracking-wider">
|
||||
<Wallet className="h-3.5 w-3.5" />
|
||||
Account
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<CardContent className="space-y-1">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted-foreground">Balance</span>
|
||||
<span className="font-semibold">${balance.toLocaleString(undefined, { minimumFractionDigits: 2 })}</span>
|
||||
<span className="text-[11px] text-muted-foreground">Balance</span>
|
||||
<span className="text-sm font-semibold font-number">{formatUSD(balance)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted-foreground">Equity</span>
|
||||
<span className="font-semibold">${equity.toLocaleString(undefined, { minimumFractionDigits: 2 })}</span>
|
||||
<span className="text-[11px] text-muted-foreground">Equity</span>
|
||||
<span className="text-sm font-semibold font-number">{formatUSD(equity)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center pt-2 border-t">
|
||||
<span className="text-sm text-muted-foreground">P/L</span>
|
||||
<span className={`font-bold ${isProfit ? 'text-green-500' : 'text-red-500'}`}>
|
||||
{isProfit ? '+' : ''}${profit.toFixed(2)}
|
||||
<div className="flex justify-between items-center pt-1 border-t border-border">
|
||||
<span className="text-[11px] text-muted-foreground">P/L</span>
|
||||
<span className={cn("text-base font-bold font-number", getValueColor(profit))}>
|
||||
{isProfit ? "+" : ""}{formatUSD(profit)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{equityHistory.length > 2 && (
|
||||
<div className="-mx-1">
|
||||
<Sparkline
|
||||
data={equityHistory.slice(-30)}
|
||||
color={isProfit ? "#22c55e" : "#ef4444"}
|
||||
height={20}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -17,58 +17,68 @@ export function EquityChart({ equityData, balanceData }: EquityChartProps) {
|
||||
}));
|
||||
|
||||
return (
|
||||
<Card className="bg-card/50 backdrop-blur col-span-2">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
||||
<Wallet className="h-4 w-4" />
|
||||
EQUITY vs BALANCE (2H)
|
||||
<Card className="glass">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-[11px] font-medium text-muted-foreground flex items-center gap-1.5 uppercase tracking-wider">
|
||||
<Wallet className="h-3.5 w-3.5" />
|
||||
Equity vs Balance (2H)
|
||||
{equityData.length > 0 && (
|
||||
<span className="ml-auto text-xs font-number text-success">
|
||||
${equityData[equityData.length - 1]?.toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-[120px] w-full">
|
||||
<div className="h-[100px] w-full">
|
||||
{equityData.length > 1 ? (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={chartData}>
|
||||
<XAxis dataKey="index" hide />
|
||||
<YAxis domain={['auto', 'auto']} hide />
|
||||
<YAxis domain={["auto", "auto"]} hide />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'hsl(var(--card))',
|
||||
border: '1px solid hsl(var(--border))',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: "var(--color-card)",
|
||||
border: "1px solid var(--color-border)",
|
||||
borderRadius: "6px",
|
||||
fontSize: "11px",
|
||||
fontFamily: "var(--font-mono)",
|
||||
}}
|
||||
labelStyle={{ display: 'none' }}
|
||||
labelStyle={{ display: "none" }}
|
||||
formatter={(value: number, name: string) => [
|
||||
`$${value.toFixed(2)}`,
|
||||
name === 'equity' ? 'Equity' : 'Balance'
|
||||
name === "equity" ? "Equity" : "Balance",
|
||||
]}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient id="equityGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#22c55e" stopOpacity={0.3} />
|
||||
<stop offset="5%" stopColor="#22c55e" stopOpacity={0.2} />
|
||||
<stop offset="95%" stopColor="#22c55e" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="balance"
|
||||
stroke="#666"
|
||||
stroke="#555"
|
||||
strokeWidth={1}
|
||||
strokeDasharray="3 3"
|
||||
strokeDasharray="4 4"
|
||||
fill="none"
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="equity"
|
||||
stroke="#22c55e"
|
||||
strokeWidth={2}
|
||||
strokeWidth={1.5}
|
||||
fill="url(#equityGradient)"
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center text-muted-foreground">
|
||||
Waiting for data...
|
||||
<div className="h-full flex items-center justify-center text-muted-foreground/50">
|
||||
<div className="text-center space-y-1">
|
||||
<Wallet className="h-5 w-5 mx-auto opacity-30" />
|
||||
<p className="text-xs">Collecting data...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Bot, Wifi, WifiOff, Clock } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface HeaderProps {
|
||||
connected: boolean;
|
||||
@@ -10,36 +11,47 @@ interface HeaderProps {
|
||||
}
|
||||
|
||||
export function Header({ connected, lastUpdate, dataAge }: HeaderProps) {
|
||||
const isStale = dataAge > 5;
|
||||
const getDataStatus = () => {
|
||||
if (dataAge > 45) return { label: "OFFLINE", variant: "danger" as const, dot: "bg-danger" };
|
||||
if (dataAge > 15) return { label: `STALE ${dataAge.toFixed(0)}s`, variant: "warning" as const, dot: "bg-warning animate-pulse" };
|
||||
return { label: `LIVE ${dataAge.toFixed(1)}s`, variant: "success" as const, dot: "bg-success" };
|
||||
};
|
||||
|
||||
const status = getDataStatus();
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container flex h-14 items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Bot className="h-6 w-6 text-primary" />
|
||||
<div className="flex items-baseline gap-2">
|
||||
<h1 className="text-lg font-bold">AI TRADING BOT</h1>
|
||||
<span className="text-xs text-primary font-semibold">MONITOR</span>
|
||||
<header className="sticky top-0 z-50 w-full border-b border-border bg-background/80 backdrop-blur-xl">
|
||||
<div className="flex h-10 items-center justify-between px-3">
|
||||
{/* Brand */}
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="flex items-center justify-center w-7 h-7 rounded-lg bg-primary/10">
|
||||
<Bot className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<h1 className="text-base font-bold text-gradient">XAUBOT AI</h1>
|
||||
<span className="text-[10px] text-muted-foreground font-medium uppercase tracking-widest hidden sm:block">
|
||||
Monitor
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Data Freshness */}
|
||||
<Badge variant={isStale ? "destructive" : "secondary"} className="gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
{isStale ? `STALE (${dataAge.toFixed(0)}s)` : `LIVE (${dataAge.toFixed(1)}s)`}
|
||||
{/* Status */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={status.variant} className="gap-1.5 font-number text-[11px]">
|
||||
<span className={cn("w-1.5 h-1.5 rounded-full", status.dot)} />
|
||||
{status.label}
|
||||
</Badge>
|
||||
|
||||
{/* Connection Status */}
|
||||
<Badge variant={connected ? "default" : "destructive"} className="gap-1">
|
||||
<Badge variant={connected ? "success" : "danger"} className="gap-1.5 text-[11px] hidden sm:inline-flex">
|
||||
{connected ? <Wifi className="h-3 w-3" /> : <WifiOff className="h-3 w-3" />}
|
||||
{connected ? 'Connected' : 'Disconnected'}
|
||||
{connected ? "Connected" : "Disconnected"}
|
||||
</Badge>
|
||||
|
||||
{/* Time */}
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
{lastUpdate || '--:--:--'} WIB
|
||||
</span>
|
||||
<div className="hidden md:flex items-center gap-1.5 px-2.5 py-1 rounded-md bg-surface border border-border text-[11px]">
|
||||
<Clock className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="font-number font-medium">
|
||||
{lastUpdate || "--:--:--"}
|
||||
</span>
|
||||
<span className="text-muted-foreground">WIB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
export { PriceCard } from './price-card';
|
||||
export { AccountCard } from './account-card';
|
||||
export { SessionCard } from './session-card';
|
||||
export { RiskCard } from './risk-card';
|
||||
export { SignalCard } from './signal-card';
|
||||
export { RegimeCard } from './regime-card';
|
||||
export { PositionsCard } from './positions-card';
|
||||
export { LogCard } from './log-card';
|
||||
export { PriceChart } from './price-chart';
|
||||
export { EquityChart } from './equity-chart';
|
||||
export { Header } from './header';
|
||||
export { PriceCard } from "./price-card";
|
||||
export { AccountCard } from "./account-card";
|
||||
export { SessionCard } from "./session-card";
|
||||
export { RiskCard } from "./risk-card";
|
||||
export { SignalCard } from "./signal-card";
|
||||
export { RegimeCard } from "./regime-card";
|
||||
export { PositionsCard } from "./positions-card";
|
||||
export { LogCard } from "./log-card";
|
||||
export { PriceChart } from "./price-chart";
|
||||
export { EquityChart } from "./equity-chart";
|
||||
export { Header } from "./header";
|
||||
export { Sparkline } from "./sparkline";
|
||||
export { SettingsCard } from "./settings-card";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Terminal } from "lucide-react";
|
||||
import type { LogEntry } from "@/types/trading";
|
||||
|
||||
@@ -12,48 +11,48 @@ interface LogCardProps {
|
||||
export function LogCard({ logs }: LogCardProps) {
|
||||
const getLevelColor = (level: string) => {
|
||||
switch (level) {
|
||||
case 'error': return 'text-red-500';
|
||||
case 'warn': return 'text-amber-500';
|
||||
case 'trade': return 'text-cyan-400';
|
||||
default: return 'text-green-400';
|
||||
case "error": return "text-danger";
|
||||
case "warn": return "text-warning";
|
||||
case "trade": return "text-info";
|
||||
default: return "text-success";
|
||||
}
|
||||
};
|
||||
|
||||
const getLevelBadge = (level: string) => {
|
||||
switch (level) {
|
||||
case 'error': return 'ERR';
|
||||
case 'warn': return 'WRN';
|
||||
case 'trade': return 'TRD';
|
||||
default: return 'INF';
|
||||
case "error": return "ERR";
|
||||
case "warn": return "WRN";
|
||||
case "trade": return "TRD";
|
||||
default: return "INF";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-card/50 backdrop-blur col-span-2">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
||||
<Terminal className="h-4 w-4" />
|
||||
AI ACTIVITY LOG
|
||||
<Card className="glass h-full flex flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-[11px] font-medium text-muted-foreground flex items-center gap-1.5 uppercase tracking-wider">
|
||||
<Terminal className="h-3.5 w-3.5" />
|
||||
Activity
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-[150px] rounded-md bg-black/50 p-3 font-mono text-xs">
|
||||
<CardContent className="flex-1 min-h-0">
|
||||
<div className="h-full overflow-auto rounded-md bg-background/60 p-2 font-mono text-[10px] leading-relaxed">
|
||||
{logs.length === 0 ? (
|
||||
<p className="text-muted-foreground">Waiting for activity...</p>
|
||||
<p className="text-muted-foreground/60">Waiting for activity...</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-0.5">
|
||||
{logs.map((log, i) => (
|
||||
<div key={i} className="flex gap-2">
|
||||
<span className="text-muted-foreground">[{log.time}]</span>
|
||||
<span className={`font-semibold ${getLevelColor(log.level)}`}>
|
||||
[{getLevelBadge(log.level)}]
|
||||
<div key={i} className="flex gap-1.5">
|
||||
<span className="text-muted-foreground/60 shrink-0">{log.time}</span>
|
||||
<span className={`font-semibold shrink-0 ${getLevelColor(log.level)}`}>
|
||||
{getLevelBadge(log.level)}
|
||||
</span>
|
||||
<span className="text-foreground/80">{log.message}</span>
|
||||
<span className="text-foreground/70 truncate">{log.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Layers } from "lucide-react";
|
||||
import { Layers, Inbox } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Position } from "@/types/trading";
|
||||
|
||||
interface PositionsCardProps {
|
||||
@@ -12,43 +12,55 @@ interface PositionsCardProps {
|
||||
|
||||
export function PositionsCard({ positions }: PositionsCardProps) {
|
||||
return (
|
||||
<Card className="bg-card/50 backdrop-blur">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
||||
<Layers className="h-4 w-4" />
|
||||
OPEN POSITIONS
|
||||
<Card className="glass h-full flex flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-[11px] font-medium text-muted-foreground flex items-center gap-1.5 uppercase tracking-wider">
|
||||
<Layers className="h-3.5 w-3.5" />
|
||||
Positions
|
||||
{positions.length > 0 && (
|
||||
<Badge variant="secondary" className="ml-auto">{positions.length}</Badge>
|
||||
<Badge variant="secondary" className="ml-auto text-[10px] h-4 px-1.5">
|
||||
{positions.length}
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-[100px]">
|
||||
{positions.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">
|
||||
No open positions
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{positions.map((pos) => (
|
||||
<div
|
||||
key={pos.ticket}
|
||||
className="flex items-center justify-between p-2 rounded-md bg-muted/50"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={pos.type === 'BUY' ? 'default' : 'destructive'} className="text-xs">
|
||||
{pos.type}
|
||||
</Badge>
|
||||
<span className="text-sm">{pos.volume} @ {pos.priceOpen.toFixed(2)}</span>
|
||||
</div>
|
||||
<span className={`font-semibold ${pos.profit >= 0 ? 'text-green-500' : 'text-red-500'}`}>
|
||||
{pos.profit >= 0 ? '+' : ''}${pos.profit.toFixed(2)}
|
||||
<CardContent className="flex-1 min-h-0 overflow-auto">
|
||||
{positions.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center">
|
||||
<Inbox className="h-5 w-5 text-muted-foreground/30 mb-1" />
|
||||
<p className="text-[11px] text-muted-foreground/60">No open positions</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{positions.map((pos) => (
|
||||
<div
|
||||
key={pos.ticket}
|
||||
className={cn(
|
||||
"flex items-center justify-between p-1.5 rounded-md bg-surface-light/50",
|
||||
pos.type === "BUY" ? "border-l-2 border-l-success" : "border-l-2 border-l-danger"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Badge
|
||||
variant={pos.type === "BUY" ? "success" : "danger"}
|
||||
className="text-[10px] h-4 px-1"
|
||||
>
|
||||
{pos.type}
|
||||
</Badge>
|
||||
<span className="text-[11px] font-number">
|
||||
{pos.volume} @ {pos.priceOpen.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
<span className={cn(
|
||||
"text-[11px] font-bold font-number",
|
||||
pos.profit >= 0 ? "text-success" : "text-danger"
|
||||
)}>
|
||||
{pos.profit >= 0 ? "+" : ""}${pos.profit.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -2,43 +2,58 @@
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { TrendingUp, TrendingDown } from "lucide-react";
|
||||
import { Sparkline } from "./sparkline";
|
||||
import { cn, formatGoldPrice, getValueColor } from "@/lib/utils";
|
||||
|
||||
interface PriceCardProps {
|
||||
price: number;
|
||||
spread: number;
|
||||
priceChange: number;
|
||||
priceHistory?: number[];
|
||||
}
|
||||
|
||||
export function PriceCard({ price, spread, priceChange }: PriceCardProps) {
|
||||
export function PriceCard({ price, spread, priceChange, priceHistory = [] }: PriceCardProps) {
|
||||
const isUp = priceChange >= 0;
|
||||
|
||||
return (
|
||||
<Card className="bg-card/50 backdrop-blur">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
PRICE
|
||||
<Card className="glass">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
|
||||
XAUUSD
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className={`text-3xl font-bold ${isUp ? 'text-green-500' : 'text-red-500'}`}>
|
||||
{price.toFixed(2)}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">XAUUSD</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
{isUp ? (
|
||||
<TrendingUp className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<TrendingDown className="h-4 w-4 text-red-500" />
|
||||
)}
|
||||
<span className={`text-sm ${isUp ? 'text-green-500' : 'text-red-500'}`}>
|
||||
{isUp ? '+' : ''}{priceChange.toFixed(2)}
|
||||
<div className="flex items-baseline gap-1.5">
|
||||
<span className={cn("text-2xl font-bold font-number", getValueColor(priceChange))}>
|
||||
${formatGoldPrice(price)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Spread: {spread.toFixed(1)} pips
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between mt-1">
|
||||
<div className="flex items-center gap-1">
|
||||
{isUp ? (
|
||||
<TrendingUp className="h-3 w-3 text-success" />
|
||||
) : (
|
||||
<TrendingDown className="h-3 w-3 text-danger" />
|
||||
)}
|
||||
<span className={cn("text-xs font-medium font-number", getValueColor(priceChange))}>
|
||||
{isUp ? "+" : ""}{priceChange.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[11px] text-muted-foreground font-number">
|
||||
{spread.toFixed(1)}p
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{priceHistory.length > 2 && (
|
||||
<div className="mt-1.5 -mx-1">
|
||||
<Sparkline
|
||||
data={priceHistory.slice(-30)}
|
||||
color={isUp ? "#22c55e" : "#ef4444"}
|
||||
height={24}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { LineChart, Line, XAxis, YAxis, ResponsiveContainer, Tooltip } from "recharts";
|
||||
import { AreaChart, Area, XAxis, YAxis, ResponsiveContainer, Tooltip } from "recharts";
|
||||
import { TrendingUp } from "lucide-react";
|
||||
|
||||
interface PriceChartProps {
|
||||
@@ -12,48 +12,58 @@ export function PriceChart({ data }: PriceChartProps) {
|
||||
const chartData = data.map((price, i) => ({ index: i, price }));
|
||||
|
||||
return (
|
||||
<Card className="bg-card/50 backdrop-blur col-span-2">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
||||
<TrendingUp className="h-4 w-4" />
|
||||
PRICE CHART (2H)
|
||||
<Card className="glass h-full flex flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-[11px] font-medium text-muted-foreground flex items-center gap-1.5 uppercase tracking-wider">
|
||||
<TrendingUp className="h-3.5 w-3.5" />
|
||||
Price Chart (2H)
|
||||
{data.length > 0 && (
|
||||
<span className="ml-auto text-xs font-number text-foreground">
|
||||
${data[data.length - 1]?.toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-[120px] w-full">
|
||||
<CardContent className="flex-1 min-h-0">
|
||||
<div className="h-full w-full">
|
||||
{data.length > 1 ? (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={chartData}>
|
||||
<AreaChart data={chartData}>
|
||||
<XAxis dataKey="index" hide />
|
||||
<YAxis domain={['auto', 'auto']} hide />
|
||||
<YAxis domain={["auto", "auto"]} hide />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'hsl(var(--card))',
|
||||
border: '1px solid hsl(var(--border))',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: "var(--color-card)",
|
||||
border: "1px solid var(--color-border)",
|
||||
borderRadius: "6px",
|
||||
fontSize: "11px",
|
||||
fontFamily: "var(--font-mono)",
|
||||
}}
|
||||
labelStyle={{ display: 'none' }}
|
||||
formatter={(value: number) => [`$${value.toFixed(2)}`, 'Price']}
|
||||
labelStyle={{ display: "none" }}
|
||||
formatter={(value: number) => [`$${value.toFixed(2)}`, "Price"]}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient id="priceGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="hsl(var(--primary))" stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor="hsl(var(--primary))" stopOpacity={0} />
|
||||
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.2} />
|
||||
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Line
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="price"
|
||||
stroke="hsl(var(--primary))"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
stroke="#3b82f6"
|
||||
strokeWidth={1.5}
|
||||
fill="url(#priceGradient)"
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center text-muted-foreground">
|
||||
Waiting for data...
|
||||
<div className="h-full flex items-center justify-center text-muted-foreground/50">
|
||||
<div className="text-center space-y-1">
|
||||
<TrendingUp className="h-5 w-5 mx-auto opacity-30" />
|
||||
<p className="text-xs">Collecting data...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,44 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Activity } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Activity, Clock } from "lucide-react";
|
||||
import { cn, getConfidenceColor } from "@/lib/utils";
|
||||
|
||||
interface RegimeCardProps {
|
||||
name: string;
|
||||
volatility: number;
|
||||
confidence: number;
|
||||
updatedAt?: string;
|
||||
h1Bias?: string;
|
||||
}
|
||||
|
||||
export function RegimeCard({ name, volatility, confidence }: RegimeCardProps) {
|
||||
const getRegimeColor = (regime: string) => {
|
||||
if (regime.toLowerCase().includes('high')) return 'text-red-500';
|
||||
if (regime.toLowerCase().includes('low')) return 'text-green-500';
|
||||
return 'text-amber-500';
|
||||
export function RegimeCard({ name, volatility, confidence, updatedAt, h1Bias }: RegimeCardProps) {
|
||||
const getRegimeBadgeVariant = (regime: string) => {
|
||||
const lower = regime.toLowerCase();
|
||||
if (lower.includes("high") || lower.includes("volatile") || lower.includes("crisis")) return "danger";
|
||||
if (lower.includes("low") || lower.includes("ranging")) return "success";
|
||||
if (lower.includes("trend")) return "info";
|
||||
return "warning";
|
||||
};
|
||||
|
||||
const confidencePercent = confidence * 100;
|
||||
|
||||
return (
|
||||
<Card className="bg-card/50 backdrop-blur">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
||||
<Activity className="h-4 w-4" />
|
||||
MARKET REGIME
|
||||
<Card className="glass">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-[11px] font-medium text-muted-foreground flex items-center gap-1.5 uppercase tracking-wider">
|
||||
<Activity className="h-3.5 w-3.5" />
|
||||
Market Regime
|
||||
{updatedAt && (
|
||||
<span className="ml-auto flex items-center gap-1 text-[10px] text-muted-foreground/60 font-number normal-case tracking-normal">
|
||||
<Clock className="h-2.5 w-2.5" />
|
||||
{updatedAt}
|
||||
</span>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="text-center">
|
||||
<span className={`text-lg font-bold ${getRegimeColor(name)}`}>
|
||||
{name || '---'}
|
||||
</span>
|
||||
</div>
|
||||
<CardContent className="space-y-2">
|
||||
<Badge variant={getRegimeBadgeVariant(name) as any} className="text-xs font-bold">
|
||||
{name || "Unknown"}
|
||||
</Badge>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted-foreground">Volatility</span>
|
||||
<span className="font-semibold">{volatility.toFixed(2)}</span>
|
||||
<span className="text-[11px] text-muted-foreground">Volatility</span>
|
||||
<span className="text-sm font-semibold font-number">{volatility.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted-foreground">Confidence</span>
|
||||
<span className="font-semibold">{(confidence * 100).toFixed(0)}%</span>
|
||||
<span className="text-[11px] text-muted-foreground">Confidence</span>
|
||||
<span className={cn(
|
||||
"text-sm font-semibold font-number",
|
||||
getConfidenceColor(confidencePercent)
|
||||
)}>
|
||||
{confidencePercent.toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
{h1Bias && (
|
||||
<div className="flex justify-between items-center pt-1 border-t border-border">
|
||||
<span className="text-[11px] text-muted-foreground">H1 Bias</span>
|
||||
<span className={cn(
|
||||
"text-xs font-bold",
|
||||
h1Bias === "BULLISH" ? "text-success" :
|
||||
h1Bias === "BEARISH" ? "text-danger" :
|
||||
"text-muted-foreground"
|
||||
)}>
|
||||
{h1Bias === "BULLISH" ? "↑ " : h1Bias === "BEARISH" ? "↓ " : ""}{h1Bias}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { ShieldAlert } from "lucide-react";
|
||||
import { ShieldAlert, AlertTriangle } from "lucide-react";
|
||||
import { cn, formatUSD } from "@/lib/utils";
|
||||
|
||||
interface RiskCardProps {
|
||||
dailyLoss: number;
|
||||
@@ -12,42 +12,75 @@ interface RiskCardProps {
|
||||
}
|
||||
|
||||
export function RiskCard({ dailyLoss, dailyProfit, consecutiveLosses, riskPercent }: RiskCardProps) {
|
||||
const isHighRisk = riskPercent >= 80;
|
||||
const isMediumRisk = riskPercent >= 50;
|
||||
const isCritical = riskPercent >= 100;
|
||||
const isHigh = riskPercent >= 80;
|
||||
const isMedium = riskPercent >= 50;
|
||||
|
||||
const getRiskColor = () => {
|
||||
if (isHigh) return "text-danger";
|
||||
if (isMedium) return "text-warning";
|
||||
return "text-success";
|
||||
};
|
||||
|
||||
const getSegmentFill = () => {
|
||||
if (isHigh) return "bg-danger";
|
||||
if (isMedium) return "bg-warning";
|
||||
return "bg-success";
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={`bg-card/50 backdrop-blur ${isHighRisk ? 'border-red-500 border-2 animate-pulse' : ''}`}>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
||||
<ShieldAlert className={`h-4 w-4 ${isHighRisk ? 'text-red-500' : ''}`} />
|
||||
RISK STATUS
|
||||
<Card className={cn(
|
||||
"glass",
|
||||
isCritical && "border-danger/50 ring-1 ring-danger/20",
|
||||
isHigh && !isCritical && "border-danger/30"
|
||||
)}>
|
||||
<CardHeader>
|
||||
<CardTitle className={cn(
|
||||
"text-[11px] font-medium flex items-center gap-1.5 uppercase tracking-wider",
|
||||
isHigh ? "text-danger" : "text-muted-foreground"
|
||||
)}>
|
||||
<ShieldAlert className="h-3.5 w-3.5" />
|
||||
Risk
|
||||
{isCritical && (
|
||||
<span className="ml-auto flex items-center gap-1 text-[10px] bg-danger text-white px-1.5 py-0.5 rounded-full animate-pulse">
|
||||
<AlertTriangle className="h-2.5 w-2.5" />
|
||||
BREACHED
|
||||
</span>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<CardContent className="space-y-1">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted-foreground">Daily Loss</span>
|
||||
<span className="font-semibold text-red-500">${dailyLoss.toFixed(2)}</span>
|
||||
<span className="text-[11px] text-muted-foreground">Daily Loss</span>
|
||||
<span className="text-xs font-semibold font-number text-danger">{formatUSD(dailyLoss)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted-foreground">Daily Profit</span>
|
||||
<span className="font-semibold text-green-500">${dailyProfit.toFixed(2)}</span>
|
||||
<span className="text-[11px] text-muted-foreground">Daily Profit</span>
|
||||
<span className="text-xs font-semibold font-number text-success">{formatUSD(dailyProfit)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-muted-foreground">Consec. Losses</span>
|
||||
<span className="font-semibold">{consecutiveLosses}</span>
|
||||
<span className="text-[11px] text-muted-foreground">Consec. Losses</span>
|
||||
<span className={cn(
|
||||
"text-xs font-semibold font-number",
|
||||
consecutiveLosses >= 3 ? "text-warning" : "text-foreground"
|
||||
)}>
|
||||
{consecutiveLosses}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t">
|
||||
<div className="pt-1 border-t border-border">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-sm text-muted-foreground">Risk Used</span>
|
||||
<span className={`font-bold ${isHighRisk ? 'text-red-500' : isMediumRisk ? 'text-amber-500' : 'text-green-500'}`}>
|
||||
<span className="text-[11px] text-muted-foreground">Risk Used</span>
|
||||
<span className={cn("text-sm font-bold font-number", getRiskColor())}>
|
||||
{riskPercent.toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={riskPercent}
|
||||
className={`h-2 ${isHighRisk ? '[&>div]:bg-red-500' : isMediumRisk ? '[&>div]:bg-amber-500' : '[&>div]:bg-green-500'}`}
|
||||
/>
|
||||
<div className="h-1.5 w-full bg-surface-light rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn("h-full rounded-full transition-all duration-500", getSegmentFill())}
|
||||
style={{ width: `${Math.min(riskPercent, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Clock, Sparkles } from "lucide-react";
|
||||
import { Clock, Sparkles, CheckCircle2, XCircle } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SessionCardProps {
|
||||
session: string;
|
||||
@@ -11,31 +12,48 @@ interface SessionCardProps {
|
||||
}
|
||||
|
||||
export function SessionCard({ session, isGoldenTime, canTrade }: SessionCardProps) {
|
||||
const getSessionColor = (s: string) => {
|
||||
const lower = s.toLowerCase();
|
||||
if (lower.includes("london")) return "text-info";
|
||||
if (lower.includes("new york") || lower.includes("ny")) return "text-success";
|
||||
if (lower.includes("sydney") || lower.includes("asian")) return "text-accent";
|
||||
return "text-warning";
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-card/50 backdrop-blur">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
||||
<Clock className="h-4 w-4" />
|
||||
SESSION
|
||||
<Card className="glass">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-[11px] font-medium text-muted-foreground flex items-center gap-1.5 uppercase tracking-wider">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
Session
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="text-center">
|
||||
<span className="text-lg font-bold text-amber-500">{session}</span>
|
||||
<CardContent className="space-y-1.5">
|
||||
<span className={cn("text-lg font-bold block", getSessionColor(session))}>
|
||||
{session || "Closed"}
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Sparkles className={cn(
|
||||
"h-3 w-3",
|
||||
isGoldenTime ? "text-warning" : "text-muted-foreground/40"
|
||||
)} />
|
||||
<span className={cn(
|
||||
"text-[11px]",
|
||||
isGoldenTime ? "text-warning font-semibold" : "text-muted-foreground"
|
||||
)}>
|
||||
{isGoldenTime ? "Golden Hour" : "Standard Hours"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={`rounded-md p-2 text-center ${isGoldenTime ? 'bg-green-500/20' : 'bg-muted'}`}>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Sparkles className={`h-4 w-4 ${isGoldenTime ? 'text-yellow-400' : 'text-muted-foreground'}`} />
|
||||
<span className={`text-sm font-semibold ${isGoldenTime ? 'text-green-400' : 'text-muted-foreground'}`}>
|
||||
GOLDEN: {isGoldenTime ? 'YES' : 'NO'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Badge variant={canTrade ? "default" : "destructive"}>
|
||||
{canTrade ? 'CAN TRADE' : 'NO TRADE'}
|
||||
<div className="flex items-center gap-1.5">
|
||||
{canTrade ? (
|
||||
<CheckCircle2 className="h-3 w-3 text-success" />
|
||||
) : (
|
||||
<XCircle className="h-3 w-3 text-danger" />
|
||||
)}
|
||||
<Badge variant={canTrade ? "success" : "danger"} className="text-[10px] h-5">
|
||||
{canTrade ? "CAN TRADE" : "NO TRADE"}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Settings2 } from "lucide-react";
|
||||
import type { BotSettings } from "@/types/trading";
|
||||
|
||||
interface SettingsCardProps {
|
||||
settings: BotSettings;
|
||||
}
|
||||
|
||||
export function SettingsCard({ settings }: SettingsCardProps) {
|
||||
const rows: { label: string; value: string }[] = [
|
||||
{ label: "Mode", value: settings.capitalMode.toUpperCase() },
|
||||
{ label: "Capital", value: `$${settings.capital.toLocaleString()}` },
|
||||
{ label: "TF", value: `${settings.executionTF}/${settings.trendTF}` },
|
||||
{ label: "Risk", value: `${settings.riskPerTrade}%` },
|
||||
{ label: "Max Loss", value: `${settings.maxDailyLoss}%` },
|
||||
{ label: "Leverage", value: `1:${settings.leverage}` },
|
||||
{ label: "Max Lot", value: `${settings.maxLotSize}` },
|
||||
{ label: "Max Pos", value: `${settings.maxPositions}` },
|
||||
{ label: "R:R", value: `1:${settings.minRR}` },
|
||||
{ label: "ML Conf", value: `${(settings.mlConfidence * 100).toFixed(0)}%` },
|
||||
{ label: "Cooldown", value: `${settings.cooldownSeconds}s` },
|
||||
{ label: "Symbol", value: settings.symbol },
|
||||
];
|
||||
|
||||
return (
|
||||
<Card className="glass">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-[11px] font-medium text-muted-foreground flex items-center gap-1.5 uppercase tracking-wider">
|
||||
<Settings2 className="h-3.5 w-3.5" />
|
||||
Bot Settings
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-3 gap-x-3 gap-y-1">
|
||||
{rows.map((row) => (
|
||||
<div key={row.label} className="flex justify-between items-center gap-1">
|
||||
<span className="text-[10px] text-muted-foreground truncate">{row.label}</span>
|
||||
<span className="text-[10px] font-semibold font-number text-foreground shrink-0">{row.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Brain, BarChart3 } from "lucide-react";
|
||||
import { Brain, BarChart3, Clock } from "lucide-react";
|
||||
import { cn, getSignalColor, getConfidenceColor } from "@/lib/utils";
|
||||
|
||||
interface SignalCardProps {
|
||||
title: string;
|
||||
@@ -12,54 +12,119 @@ interface SignalCardProps {
|
||||
detail?: string;
|
||||
buyProb?: number;
|
||||
sellProb?: number;
|
||||
updatedAt?: string;
|
||||
threshold?: number;
|
||||
marketQuality?: string;
|
||||
}
|
||||
|
||||
export function SignalCard({ title, icon, signal, confidence, detail, buyProb, sellProb }: SignalCardProps) {
|
||||
const getSignalColor = (sig: string) => {
|
||||
if (sig === 'BUY') return 'text-green-500';
|
||||
if (sig === 'SELL') return 'text-red-500';
|
||||
if (sig === 'HOLD') return 'text-amber-500';
|
||||
return 'text-muted-foreground';
|
||||
export function SignalCard({
|
||||
title,
|
||||
icon,
|
||||
signal,
|
||||
confidence,
|
||||
detail,
|
||||
buyProb,
|
||||
sellProb,
|
||||
updatedAt,
|
||||
threshold,
|
||||
marketQuality,
|
||||
}: SignalCardProps) {
|
||||
const confidencePercent = confidence * 100;
|
||||
const hasSignal = signal && signal.toUpperCase() !== "NO SIGNAL" && signal !== "";
|
||||
const normalized = (signal || "").toUpperCase();
|
||||
|
||||
const getBorderClass = () => {
|
||||
if (normalized === "BUY") return "signal-buy";
|
||||
if (normalized === "SELL") return "signal-sell";
|
||||
if (normalized === "HOLD") return "signal-hold";
|
||||
return "signal-none";
|
||||
};
|
||||
|
||||
const getProgressColor = (sig: string) => {
|
||||
if (sig === 'BUY') return '[&>div]:bg-green-500';
|
||||
if (sig === 'SELL') return '[&>div]:bg-red-500';
|
||||
if (sig === 'HOLD') return '[&>div]:bg-amber-500';
|
||||
return '';
|
||||
const getBarColor = () => {
|
||||
if (normalized === "BUY") return "bg-success";
|
||||
if (normalized === "SELL") return "bg-danger";
|
||||
if (normalized === "HOLD") return "bg-warning";
|
||||
return "bg-muted";
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-card/50 backdrop-blur">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
|
||||
{icon === 'smc' ? <BarChart3 className="h-4 w-4" /> : <Brain className="h-4 w-4" />}
|
||||
<Card className={cn("glass", getBorderClass())}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-[11px] font-medium text-muted-foreground flex items-center gap-1.5 uppercase tracking-wider">
|
||||
{icon === "smc" ? <BarChart3 className="h-3.5 w-3.5" /> : <Brain className="h-3.5 w-3.5" />}
|
||||
{title}
|
||||
{updatedAt && (
|
||||
<span className="ml-auto flex items-center gap-1 text-[10px] text-muted-foreground/60 font-number normal-case tracking-normal">
|
||||
<Clock className="h-2.5 w-2.5" />
|
||||
{updatedAt}
|
||||
</span>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="text-center">
|
||||
<span className={`text-2xl font-bold ${getSignalColor(signal)}`}>
|
||||
{signal || 'NO SIGNAL'}
|
||||
</span>
|
||||
</div>
|
||||
<CardContent className="space-y-2">
|
||||
<span className={cn(
|
||||
"text-xl font-bold block",
|
||||
hasSignal ? getSignalColor(signal) : "text-muted-foreground/60"
|
||||
)}>
|
||||
{signal || "NO SIGNAL"}
|
||||
</span>
|
||||
|
||||
{/* Confidence bar */}
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-xs text-muted-foreground">Confidence</span>
|
||||
<span className="text-xs font-semibold">{(confidence * 100).toFixed(0)}%</span>
|
||||
<span className="text-[11px] text-muted-foreground">Confidence</span>
|
||||
<span className={cn(
|
||||
"text-[11px] font-semibold font-number",
|
||||
getConfidenceColor(confidencePercent)
|
||||
)}>
|
||||
{confidencePercent.toFixed(0)}%
|
||||
{threshold !== undefined && (
|
||||
<span className="text-muted-foreground font-normal">
|
||||
/{(threshold * 100).toFixed(0)}%
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<Progress value={confidence * 100} className={`h-1.5 ${getProgressColor(signal)}`} />
|
||||
<div className="relative h-1.5 w-full bg-surface-light rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn("h-full rounded-full transition-all duration-300", getBarColor())}
|
||||
style={{ width: `${confidencePercent}%` }}
|
||||
/>
|
||||
{threshold !== undefined && (
|
||||
<div
|
||||
className="absolute top-0 h-full w-[2px] bg-foreground/50"
|
||||
style={{ left: `${threshold * 100}%` }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{threshold !== undefined && (
|
||||
<div className="flex justify-between items-center mt-0.5">
|
||||
<span className="text-[10px] text-muted-foreground/60">
|
||||
{confidencePercent >= threshold * 100 ? "✓ Above" : "✗ Below"} threshold
|
||||
</span>
|
||||
{marketQuality && (
|
||||
<span className="text-[10px] text-muted-foreground/60 font-number">
|
||||
Mkt: {marketQuality}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{detail && (
|
||||
<p className="text-xs text-muted-foreground line-clamp-2">{detail}</p>
|
||||
<p className="text-[11px] text-muted-foreground line-clamp-1">{detail}</p>
|
||||
)}
|
||||
|
||||
{buyProb !== undefined && sellProb !== undefined && (
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-green-500">Buy: {(buyProb * 100).toFixed(0)}%</span>
|
||||
<span className="text-red-500">Sell: {(sellProb * 100).toFixed(0)}%</span>
|
||||
<div className="flex justify-between gap-2 text-[11px] font-number">
|
||||
<span>
|
||||
<span className="text-muted-foreground">Buy </span>
|
||||
<span className="text-success font-semibold">{(buyProb * 100).toFixed(0)}%</span>
|
||||
</span>
|
||||
<span>
|
||||
<span className="text-muted-foreground">Sell </span>
|
||||
<span className="text-danger font-semibold">{(sellProb * 100).toFixed(0)}%</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import { LineChart, Line, ResponsiveContainer, YAxis } from "recharts";
|
||||
|
||||
interface SparklineProps {
|
||||
data: number[];
|
||||
color?: string;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export function Sparkline({ data, color = "#22c55e", height = 28 }: SparklineProps) {
|
||||
if (data.length < 2) return null;
|
||||
|
||||
const chartData = data.map((v, i) => ({ i, v }));
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", height }}>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={chartData}>
|
||||
<YAxis domain={["auto", "auto"]} hide />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="v"
|
||||
stroke={color}
|
||||
strokeWidth={1.5}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +1,27 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
"inline-flex items-center justify-center rounded-full border border-transparent px-2.5 py-0.5 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 [a&]:hover:underline",
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground border-border",
|
||||
success:
|
||||
"border-transparent bg-success-bg text-success hover:bg-success-bg/80",
|
||||
warning:
|
||||
"border-transparent bg-warning-bg text-warning hover:bg-warning-bg/80",
|
||||
danger:
|
||||
"border-transparent bg-danger-bg text-danger hover:bg-danger-bg/80",
|
||||
info:
|
||||
"border-transparent bg-info-bg text-info hover:bg-info-bg/80",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -26,22 +30,13 @@ const badgeVariants = cva(
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot.Root : "span"
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
data-variant={variant}
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,92 +1,78 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border border-border bg-card text-card-foreground shadow-sm transition-colors",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1 px-3 pt-2 pb-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-sm font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("px-3 pb-2 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center px-4 pb-3 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
|
||||
@@ -33,17 +33,20 @@ export interface TradingStatus {
|
||||
signal: string;
|
||||
confidence: number;
|
||||
reason: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
ml: {
|
||||
signal: string;
|
||||
confidence: number;
|
||||
buyProb: number;
|
||||
sellProb: number;
|
||||
updatedAt?: string;
|
||||
};
|
||||
regime: {
|
||||
name: string;
|
||||
volatility: number;
|
||||
confidence: number;
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
// Positions
|
||||
@@ -51,6 +54,31 @@ export interface TradingStatus {
|
||||
|
||||
// Log
|
||||
logs: LogEntry[];
|
||||
|
||||
// Bot Settings
|
||||
settings?: BotSettings;
|
||||
|
||||
// Entry Conditions
|
||||
h1Bias?: string;
|
||||
dynamicThreshold?: number;
|
||||
marketQuality?: string;
|
||||
marketScore?: number;
|
||||
}
|
||||
|
||||
export interface BotSettings {
|
||||
capitalMode: string;
|
||||
capital: number;
|
||||
riskPerTrade: number;
|
||||
maxDailyLoss: number;
|
||||
maxPositions: number;
|
||||
maxLotSize: number;
|
||||
leverage: number;
|
||||
executionTF: string;
|
||||
trendTF: string;
|
||||
minRR: number;
|
||||
mlConfidence: number;
|
||||
cooldownSeconds: number;
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
|
||||
Reference in New Issue
Block a user