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:
GifariKemal
2026-02-08 10:33:24 +07:00
co-authored by Claude Opus 4.6
parent 53d8cd26a2
commit 214b64945d
64 changed files with 11073 additions and 929 deletions
+48
View File
@@ -0,0 +1,48 @@
# Dependencies
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# Next.js
.next/
out/
build
dist
# Testing
coverage
# Misc
.DS_Store
*.pem
# Debug
*.log
# Local env files
.env*.local
.env
# Vercel
.vercel
# TypeScript
*.tsbuildinfo
next-env.d.ts
# IDE
.vscode
.idea
# Git
.git
.gitignore
README.md
# Docs
*.md
# Exclude v3 tailwind config (use @theme in CSS for v4)
tailwind.config.ts
+48
View File
@@ -0,0 +1,48 @@
# Multi-stage build for Next.js Dashboard
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Copy package files
COPY package.json package-lock.json* ./
RUN npm ci
# Build the source code
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# NEXT_PUBLIC_API_URL defaults to http://localhost:8000 in use-trading-data.ts
# The browser fetches from the host machine, not Docker internal network
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# Production image
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy built files
COPY --from=builder /app/public ./public
# standalone output includes server.js + required node_modules
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
+217
View File
@@ -0,0 +1,217 @@
# Web Dashboard Styling Migration - Summary
## ✅ Completed Changes
### 1. **Created Tailwind Configuration** (`tailwind.config.ts`)
- Custom dark theme colors based on SURGE-AI-Trading design
- Extended color palette with semantic colors (success, warning, danger, info)
- Custom animations (fade-in, slide-up, shimmer)
- Custom font families (Inter for sans, JetBrains Mono for mono)
- Responsive design utilities
### 2. **Updated Global Styles** (`src/app/globals.css`)
- Dark theme color variables using HSL
- Custom scrollbar styling
- Utility classes for:
- Text gradient effects
- Card variations (glass, hover)
- Badge variants (success, warning, danger, info)
- Button utilities
- Number formatting (font-number)
- Price colors (price-up, price-down, price-neutral)
- Live pulse indicator
- Loading skeleton with shimmer
- Input styling
### 3. **Enhanced Utility Functions** (`src/lib/utils.ts`)
Added comprehensive utility functions:
- **Formatting:** formatUSD, formatGoldPrice, formatPercent, formatCompact
- **Date/Time:** formatTime, formatDate, formatDateTime, formatDateTimeWIB, getRelativeTime
- **Colors:** getValueColor, getValueBgColor, getSignalColor, getSignalBadgeColor
- **Confidence:** getConfidenceLevel, getConfidenceColor
- **Helpers:** calcProgress, debounce, generateId, sleep
### 4. **Updated shadcn/ui Components**
#### Badge Component (`src/components/ui/badge.tsx`)
- Added semantic variants: success, warning, danger, info
- Improved styling consistency
- Better hover effects
#### Card Component (`src/components/ui/card.tsx`)
- Simplified implementation
- Better border and shadow styling
- Consistent with shadcn/ui patterns
### 5. **Updated Dashboard Components**
#### PriceCard (`src/components/dashboard/price-card.tsx`)
- ✅ Uses `glass` effect
- ✅ Uses `formatGoldPrice` and `getValueColor`
- ✅ Uses `font-number` for numeric displays
- ✅ Uppercase + tracking-wider for title
- ✅ Proper semantic colors
#### AccountCard (`src/components/dashboard/account-card.tsx`)
- ✅ Uses `glass` effect
- ✅ Uses `formatUSD` for currency display
- ✅ Uses `getValueColor` for profit/loss
- ✅ Uses `font-number` for numeric displays
- ✅ Proper border styling with `border-border`
#### SignalCard (`src/components/dashboard/signal-card.tsx`)
- ✅ Uses `glass` effect
- ✅ Uses `getSignalColor` for signal colors
- ✅ Uses `getConfidenceColor` for confidence display
- ✅ Improved progress bar colors
- ✅ Better probability display formatting
- ✅ Uses `font-number` for numeric displays
#### SessionCard (`src/components/dashboard/session-card.tsx`)
- ✅ Uses `glass` effect
- ✅ Uses semantic badge variants (success/danger)
- ✅ Improved golden time indicator with proper colors
- ✅ Better visual hierarchy
- ✅ Uppercase + tracking-wider for title
#### RiskCard (`src/components/dashboard/risk-card.tsx`)
- ✅ Uses `glass` effect
- ✅ Uses `formatUSD` for currency display
- ✅ Dynamic risk level colors (success/warning/danger)
- ✅ Better progress bar with semantic colors
- ✅ Improved risk status indicator
- ✅ Uses `font-number` for numeric displays
#### RegimeCard (`src/components/dashboard/regime-card.tsx`)
- ✅ Uses `glass` effect
- ✅ Uses Badge component for regime display
- ✅ Uses `getConfidenceColor` for confidence display
- ✅ Better regime color mapping (danger/success/info/warning)
- ✅ Uses `font-number` for numeric displays
#### Header (`src/components/dashboard/header.tsx`)
- ✅ Improved branding with gradient text effect
- ✅ Better badge styling with semantic variants
- ✅ Added primary color accent box for logo
- ✅ Improved time display with proper formatting
- ✅ Responsive design (hide time on small screens)
- ✅ Uses `font-number` for time display
### 6. **Updated Configuration** (`components.json`)
- Changed style from "new-york" to "default"
- Added `tailwind.config.ts` reference
- Changed baseColor from "neutral" to "slate"
- Added shadcn registry configuration
### 7. **Created Documentation**
#### STYLING-GUIDE.md
Comprehensive guide covering:
- Color system with hex and HSL values
- Component styling examples
- Utility classes documentation
- Utility functions API reference
- Typography guidelines
- Responsive design patterns
- Best practices
- Example implementations
- Migration checklist
## 📝 Migration Notes
### Color Changes
- `text-green-500``text-success`
- `text-red-500``text-danger`
- `text-amber-500``text-warning`
- `text-blue-500``text-info`
- `bg-card/50 backdrop-blur``glass`
### Formatting Changes
- Manual `.toLocaleString()``formatUSD()`, `formatGoldPrice()`
- Manual percentage formatting → `formatPercent()`
- Manual color logic → `getValueColor()`, `getSignalColor()`
### Component Improvements
- All cards now use consistent `glass` effect
- All numeric displays use `font-number` class
- All titles use `uppercase tracking-wider`
- Consistent spacing with `space-y-*` utilities
- Better badge variants with semantic colors
## 🎨 Design System
### Primary Colors
- **Primary:** #6366f1 (Indigo) - Main brand color
- **Accent:** #8b5cf6 (Purple) - Highlights and accents
### Semantic Colors
- **Success:** #22c55e (Green) - Positive values, buy signals
- **Warning:** #f59e0b (Orange) - Caution, hold signals
- **Danger:** #ef4444 (Red) - Negative values, sell signals
- **Info:** #3b82f6 (Blue) - Informational content
### Background Hierarchy
1. `background` (#0a0a0f) - Page background
2. `surface` (#121218) - Card background
3. `surface-light` (#1a1a24) - Nested elements
4. `surface-hover` (#22222e) - Hover states
## 🔄 Remaining Components to Migrate
The following components still need to be updated:
- [ ] `positions-card.tsx`
- [ ] `log-card.tsx`
- [ ] `price-chart.tsx`
- [ ] `equity-chart.tsx`
These should follow the same pattern:
1. Add `glass` effect to cards
2. Use utility formatting functions
3. Apply `font-number` to numbers
4. Use semantic colors
5. Apply uppercase + tracking-wider to titles
## 🚀 Next Steps
1. **Test the dashboard:**
```bash
cd web-dashboard
npm run dev
```
2. **Add more shadcn/ui components as needed:**
```bash
npx shadcn@latest add tooltip
npx shadcn@latest add dialog
npx shadcn@latest add dropdown-menu
```
3. **Migrate remaining components** using the patterns in STYLING-GUIDE.md
4. **Consider adding:**
- Toast notifications (sonner)
- Loading states (spinner)
- Error boundaries
- Tooltips for detailed info
## 📚 Resources
- **STYLING-GUIDE.md** - Complete styling reference
- **tailwind.config.ts** - Theme configuration
- **src/lib/utils.ts** - Utility functions
- **shadcn/ui docs:** https://ui.shadcn.com
## 🎯 Benefits
1. **Consistent Design** - All components follow the same design system
2. **Better Maintainability** - Centralized theme and utilities
3. **Improved Readability** - Semantic colors and proper formatting
4. **Type Safety** - TypeScript utility functions
5. **Performance** - Optimized Tailwind CSS with PurgeCSS
6. **Accessibility** - Better color contrast and semantic HTML
7. **Developer Experience** - Clear utility functions and documentation
---
**Migration completed:** Feb 6, 2026
**By:** Claude Sonnet 4.5
+314
View File
@@ -0,0 +1,314 @@
# XAUBot AI Dashboard - Styling Guide
## Overview
The dashboard uses **shadcn/ui** components with **Tailwind CSS** and a custom dark theme inspired by nof1.ai and SURGE-AI-Trading.
## Color System
### Theme Colors
```typescript
// Background & Surface
background: #0a0a0f (HSL: 222 47% 6%)
surface: #121218 (HSL: 222 25% 7%)
surface-light: #1a1a24 (HSL: 222 20% 10%)
surface-hover: #22222e (HSL: 222 18% 14%)
// Primary & Accent
primary: #6366f1 (Indigo)
primary-dark: #4f46e5
accent: #8b5cf6 (Purple)
// Semantic Colors
success: #22c55e (Green)
warning: #f59e0b (Orange)
danger: #ef4444 (Red)
info: #3b82f6 (Blue)
// Each semantic color has a background variant with 12.5% opacity
success-bg: #22c55e20
warning-bg: #f59e0b20
danger-bg: #ef444420
info-bg: #3b82f620
```
### Border & Text
```typescript
border: #2a2a3a
border-light: #3a3a4a
foreground: #ffffff
muted-foreground: #a1a1aa
```
## Component Styling
### Cards
```tsx
// Glass effect card (recommended for dashboard)
<Card className="glass">
<CardHeader>...</CardHeader>
<CardContent>...</CardContent>
</Card>
// Custom card utilities
.glass bg-surface/80 + backdrop-blur
.card-custom bg-surface + rounded-xl + border
.card-hover card-custom + hover effect
```
### Badges
```tsx
// Available badge variants
<Badge variant="default">Primary</Badge>
<Badge variant="success">Success</Badge>
<Badge variant="warning">Warning</Badge>
<Badge variant="danger">Danger</Badge>
<Badge variant="info">Info</Badge>
<Badge variant="outline">Outline</Badge>
```
### Buttons
```tsx
// Utility classes for buttons
className="btn-primary" Primary button
className="btn-success" Success button
className="btn-danger" Danger button
className="btn-outline" Outline button
```
## Utility Classes
### Text & Numbers
```css
.font-number font-mono + tabular-nums (for prices, numbers)
.text-gradient gradient from primary to accent
.price-up text-success
.price-down text-danger
.price-neutral text-muted-foreground
```
### Animations
```css
.animate-pulse-slow 3s pulse
.animate-fade-in fade in effect
.animate-slide-up slide up effect
.animate-shimmer shimmer loading effect
.skeleton loading skeleton with shimmer
```
### Live Indicators
```tsx
// Adds a pulsing dot indicator
<div className="pulse-live">LIVE</div>
```
## Utility Functions
### Formatting
```typescript
import {
formatUSD, // → $1,234.56
formatGoldPrice, // → 2345.67
formatPercent, // → +2.45%
formatCompact, // → 1.2M, 3.4K
formatTime, // → 14:23:45
formatDate, // → Jan 17, 2026
formatDateTime, // → Jan 17, 2026 14:23:45
formatDateTimeWIB // → 17 Jan 2026 14:23:45 WIB
} from '@/lib/utils';
```
### Color Helpers
```typescript
import {
getValueColor, // → Returns color class based on +/-
getValueBgColor, // → Returns bg color class based on +/-
getSignalColor, // → Returns color for BUY/SELL/HOLD
getSignalBadgeColor, // → Returns badge variant for signals
getConfidenceColor, // → Returns color based on confidence %
getConfidenceLevel // → Returns "Very High", "High", etc.
} from '@/lib/utils';
```
### Other Utilities
```typescript
import {
cn, // Merge Tailwind classes
calcProgress, // Calculate progress % (capped at 100)
debounce, // Debounce function
generateId, // Generate unique ID
sleep // Async sleep
} from '@/lib/utils';
```
## Usage Examples
### Price Display
```tsx
import { formatGoldPrice, getValueColor } from '@/lib/utils';
<span className={cn(
"text-3xl font-bold font-number",
getValueColor(priceChange)
)}>
${formatGoldPrice(price)}
</span>
```
### Signal Badge
```tsx
import { getSignalBadgeColor } from '@/lib/utils';
<Badge variant={getSignalBadgeColor(signal)}>
{signal}
</Badge>
```
### Confidence Display
```tsx
import { getConfidenceColor, getConfidenceLevel } from '@/lib/utils';
const confidencePercent = confidence * 100;
<span className={cn(
"font-semibold",
getConfidenceColor(confidencePercent)
)}>
{confidencePercent.toFixed(0)}% - {getConfidenceLevel(confidencePercent)}
</span>
```
### Profit/Loss Display
```tsx
import { formatUSD, getValueColor } from '@/lib/utils';
<span className={cn(
"font-bold font-number",
getValueColor(profit)
)}>
{profit >= 0 ? '+' : ''}{formatUSD(profit)}
</span>
```
## Typography
### Fonts
- **Sans:** Inter, system-ui, sans-serif
- **Mono:** JetBrains Mono, Fira Code, monospace
### Font Classes
```tsx
<span className="font-sans">Regular text</span>
<span className="font-mono">Code or numbers</span>
<span className="font-number">Numbers (tabular-nums)</span>
```
## Responsive Design
The dashboard is optimized for desktop but responsive:
```tsx
<div className="hidden sm:flex">Desktop only</div>
<div className="sm:hidden">Mobile only</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
Responsive grid
</div>
```
## Adding New shadcn/ui Components
1. Check available components:
```bash
npx shadcn@latest view @shadcn
```
2. Add a component:
```bash
npx shadcn@latest add button
npx shadcn@latest add tooltip
npx shadcn@latest add dialog
```
3. Components will be added to `src/components/ui/`
## Best Practices
1. **Always use utility functions** for formatting numbers, dates, and colors
2. **Use the `cn()` helper** to merge Tailwind classes
3. **Apply `font-number`** to all numeric displays for consistent monospace formatting
4. **Use semantic colors** (success, warning, danger, info) instead of raw colors
5. **Apply `glass` effect** to cards for depth and consistency
6. **Use uppercase + tracking-wider** for card titles: `className="uppercase tracking-wider"`
7. **Add proper spacing** with `space-y-*` or `gap-*` utilities
8. **Keep contrast in mind** - use `text-muted-foreground` for secondary text
## Example Card Component
```tsx
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { TrendingUp } from "lucide-react";
import { cn, formatUSD, getValueColor } from "@/lib/utils";
interface ExampleCardProps {
title: string;
value: number;
change: number;
status: "active" | "inactive";
}
export function ExampleCard({ title, value, change, status }: ExampleCardProps) {
return (
<Card className="glass">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2 uppercase tracking-wider">
<TrendingUp className="h-4 w-4" />
{title}
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="text-2xl font-bold font-number">
{formatUSD(value)}
</div>
<div className="flex items-center justify-between">
<span className={cn(
"text-sm font-medium font-number",
getValueColor(change)
)}>
{change >= 0 ? '+' : ''}{change.toFixed(2)}%
</span>
<Badge variant={status === 'active' ? 'success' : 'danger'}>
{status}
</Badge>
</div>
</CardContent>
</Card>
);
}
```
## Migration Checklist
When updating existing components to the new styling:
- [ ] Replace hardcoded colors with theme colors (text-green-500 → text-success)
- [ ] Add `glass` class to cards
- [ ] Use utility formatting functions instead of manual formatting
- [ ] Apply `font-number` to numeric displays
- [ ] Use uppercase + tracking-wider for titles
- [ ] Replace manual color logic with utility functions (getValueColor, etc.)
- [ ] Update Badge variants to semantic ones (success, warning, danger, info)
- [ ] Add proper spacing with space-y or gap utilities
- [ ] Ensure proper use of `cn()` for class merging
## Resources
- shadcn/ui docs: https://ui.shadcn.com
- Tailwind CSS docs: https://tailwindcss.com
- Lucide Icons: https://lucide.dev
---
Last updated: Feb 6, 2026
+59 -258
View File
@@ -1,42 +1,21 @@
"""
FastAPI Backend for Web Dashboard
=================================
FastAPI Backend for Web Dashboard (Docker-compatible)
=====================================================
Serves trading bot status data to the web frontend.
Reads from data/bot_status.json which is written by main_live.py.
This allows the API to run in Docker without needing MT5 (Windows-only).
"""
import sys
import json
from pathlib import Path
from datetime import datetime
from zoneinfo import ZoneInfo
from collections import deque
import asyncio
from typing import Optional
import json
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from dotenv import load_dotenv
load_dotenv()
# Import bot components
try:
from src.mt5_connector import MT5Connector
from src.smc_polars import SMCAnalyzer
from src.ml_model import TradingModel
from src.regime_detector import MarketRegimeDetector
from src.session_filter import SessionFilter
from src.feature_eng import FeatureEngineer
from src.config import TradingConfig
except ImportError as e:
print(f"Import error: {e}")
print("Make sure you're running from the correct directory")
app = FastAPI(title="Trading Bot API", version="1.0.0")
app = FastAPI(title="Trading Bot API", version="2.0.0")
# CORS for frontend
app.add_middleware(
@@ -47,246 +26,68 @@ app.add_middleware(
allow_headers=["*"],
)
# Global state
class BotState:
def __init__(self):
self.mt5: Optional[MT5Connector] = None
self.smc: Optional[SMCAnalyzer] = None
self.ml: Optional[TradingModel] = None
self.hmm: Optional[MarketRegimeDetector] = None
self.session: Optional[SessionFilter] = None
self.feature_eng: Optional[FeatureEngineer] = None
self.config: Optional[TradingConfig] = None
self.connected = False
# Status file path (mounted as volume in Docker)
STATUS_FILE = Path("/app/data/bot_status.json")
# History buffers
self.price_history = deque(maxlen=120)
self.equity_history = deque(maxlen=120)
self.balance_history = deque(maxlen=120)
self.logs = deque(maxlen=50)
# Last known values
self.last_price = 0.0
self.last_update = None
state = BotState()
def add_log(level: str, message: str):
"""Add log entry to buffer"""
now = datetime.now(ZoneInfo("Asia/Jakarta"))
state.logs.append({
"time": now.strftime("%H:%M:%S"),
"level": level,
"message": message
})
@app.on_event("startup")
async def startup():
"""Initialize bot components on startup"""
add_log("info", "Starting API server...")
try:
state.config = TradingConfig()
state.mt5 = MT5Connector(
login=state.config.mt5_login,
password=state.config.mt5_password,
server=state.config.mt5_server,
path=state.config.mt5_path,
)
if state.mt5.connect():
state.connected = True
add_log("info", "MT5 connected successfully")
# Initialize components
state.smc = SMCAnalyzer()
state.ml = TradingModel(model_path="models/xgboost_model")
state.ml.load()
state.hmm = MarketRegimeDetector(model_path="models/hmm_regime")
state.hmm.load()
state.session = SessionFilter()
state.feature_eng = FeatureEngineer()
add_log("info", f"ML Model loaded ({len(state.ml.feature_names)} features)")
else:
add_log("error", "Failed to connect to MT5")
except Exception as e:
add_log("error", f"Startup error: {e}")
@app.on_event("shutdown")
async def shutdown():
"""Cleanup on shutdown"""
if state.mt5:
state.mt5.disconnect()
add_log("info", "API server stopped")
# Default empty response
DEFAULT_STATUS = {
"timestamp": "00:00:00",
"connected": False,
"price": 0.0,
"spread": 0.0,
"priceChange": 0.0,
"priceHistory": [],
"balance": 0.0,
"equity": 0.0,
"profit": 0.0,
"equityHistory": [],
"balanceHistory": [],
"session": "Unknown",
"isGoldenTime": False,
"canTrade": False,
"dailyLoss": 0.0,
"dailyProfit": 0.0,
"consecutiveLosses": 0,
"riskPercent": 0.0,
"smc": {"signal": "", "confidence": 0.0, "reason": ""},
"ml": {"signal": "", "confidence": 0.0, "buyProb": 0.0, "sellProb": 0.0},
"regime": {"name": "", "volatility": 0.0, "confidence": 0.0},
"positions": [],
"logs": [],
}
@app.get("/api/status")
async def get_status():
"""Get current trading status"""
wib = ZoneInfo("Asia/Jakarta")
now = datetime.now(wib)
result = {
"timestamp": now.strftime("%H:%M:%S"),
"connected": state.connected,
"price": 0.0,
"spread": 0.0,
"priceChange": 0.0,
"priceHistory": list(state.price_history),
"balance": 0.0,
"equity": 0.0,
"profit": 0.0,
"equityHistory": list(state.equity_history),
"balanceHistory": list(state.balance_history),
"session": "Unknown",
"isGoldenTime": 19 <= now.hour < 23,
"canTrade": False,
"dailyLoss": 0.0,
"dailyProfit": 0.0,
"consecutiveLosses": 0,
"riskPercent": 0.0,
"smc": {"signal": "", "confidence": 0.0, "reason": ""},
"ml": {"signal": "", "confidence": 0.0, "buyProb": 0.0, "sellProb": 0.0},
"regime": {"name": "", "volatility": 0.0, "confidence": 0.0},
"positions": [],
"logs": list(state.logs),
}
if not state.connected or not state.mt5:
return result
try:
# Price
tick = state.mt5.get_tick(state.config.symbol)
if tick:
price = (tick.bid + tick.ask) / 2
spread = (tick.ask - tick.bid) * 100
# Calculate change
price_change = price - state.last_price if state.last_price > 0 else 0
state.last_price = price
# Update history
state.price_history.append(price)
result["price"] = price
result["spread"] = spread
result["priceChange"] = price_change
result["priceHistory"] = list(state.price_history)
# Account
balance = state.mt5.account_balance or 0
equity = state.mt5.account_equity or 0
profit = equity - balance
state.equity_history.append(equity)
state.balance_history.append(balance)
result["balance"] = balance
result["equity"] = equity
result["profit"] = profit
result["equityHistory"] = list(state.equity_history)
result["balanceHistory"] = list(state.balance_history)
# Session
if state.session:
session_info = state.session.get_status_report()
if session_info:
result["session"] = session_info.get('current_session', 'Unknown')
can_trade, _, _ = state.session.can_trade()
result["canTrade"] = can_trade
# Risk state from file
risk_file = Path("data/risk_state.txt")
if risk_file.exists():
content = risk_file.read_text()
for line in content.strip().split('\n'):
if ':' in line:
key, value = line.split(':', 1)
key = key.strip()
value = value.strip()
if key == 'daily_loss':
result["dailyLoss"] = float(value)
elif key == 'daily_profit':
result["dailyProfit"] = float(value)
elif key == 'consecutive_losses':
result["consecutiveLosses"] = int(value)
# Calculate risk percent
max_loss = state.config.capital * (state.config.risk.max_daily_loss / 100)
if max_loss > 0:
result["riskPercent"] = (result["dailyLoss"] / max_loss) * 100
# Signals
df = state.mt5.get_market_data(state.config.symbol, state.config.execution_timeframe, 200)
if df is not None and len(df) > 50:
# Feature engineering
df = state.feature_eng.calculate_all(df, include_ml_features=True)
df = state.smc.calculate_all(df)
# Regime
if state.hmm:
df = state.hmm.predict(df)
regime = state.hmm.get_current_state(df)
if regime:
result["regime"] = {
"name": regime.regime.value.replace('_', ' ').title(),
"volatility": regime.volatility,
"confidence": regime.confidence,
}
# SMC Signal
smc_signal = state.smc.generate_signal(df)
if smc_signal:
result["smc"] = {
"signal": smc_signal.signal_type,
"confidence": smc_signal.confidence,
"reason": smc_signal.reason or "",
}
# ML Prediction
if state.ml and state.ml.fitted:
available_features = [f for f in state.ml.feature_names if f in df.columns]
ml_pred = state.ml.predict(df, available_features)
if ml_pred:
result["ml"] = {
"signal": ml_pred.signal,
"confidence": ml_pred.confidence,
"buyProb": ml_pred.probability,
"sellProb": 1.0 - ml_pred.probability,
}
# Positions
positions = state.mt5.get_open_positions(state.config.symbol)
if positions is not None and not positions.is_empty():
pos_list = []
for row in positions.iter_rows(named=True):
pos_list.append({
"ticket": row.get('ticket', 0),
"type": "BUY" if row.get('type', 0) == 0 else "SELL",
"volume": row.get('volume', 0),
"priceOpen": row.get('price_open', 0),
"profit": row.get('profit', 0),
})
result["positions"] = pos_list
state.last_update = now
except Exception as e:
add_log("error", f"Status error: {str(e)[:50]}")
"""Get current trading status from bot's status file."""
# Try local path first (non-Docker), then Docker path
for path in [STATUS_FILE, Path("data/bot_status.json")]:
if path.exists():
try:
data = json.loads(path.read_text())
return data
except (json.JSONDecodeError, OSError):
continue
# No status file — bot not running
now = datetime.now(ZoneInfo("Asia/Jakarta"))
result = DEFAULT_STATUS.copy()
result["timestamp"] = now.strftime("%H:%M:%S")
result["logs"] = [
{
"time": now.strftime("%H:%M:%S"),
"level": "warning",
"message": "Bot is not running — waiting for bot_status.json",
}
]
return result
@app.get("/api/health")
async def health():
"""Health check endpoint"""
return {"status": "ok", "connected": state.connected}
"""Health check endpoint."""
bot_running = STATUS_FILE.exists() or Path("data/bot_status.json").exists()
return {"status": "ok", "bot_running": bot_running}
if __name__ == "__main__":
+1 -3
View File
@@ -1,4 +1,2 @@
fastapi>=0.109.0
uvicorn>=0.27.0
python-dotenv>=1.0.0
pydantic>=2.5.0
uvicorn[standard]>=0.27.0
+6 -4
View File
@@ -1,12 +1,12 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "neutral",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
@@ -19,5 +19,7 @@
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
"registries": {
"@shadcn": "https://ui.shadcn.com/r"
}
}
+7 -1
View File
@@ -1,7 +1,13 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
// Enable standalone output for Docker
output: 'standalone',
// Disable static optimization for dynamic data
experimental: {
// Enable if needed for better performance
}
};
export default nextConfig;
+236 -115
View File
@@ -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; }
}
+10 -8
View File
@@ -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
View File
@@ -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>
+13 -11
View File
@@ -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>
);
}
+19 -24
View File
@@ -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} />
)
}
+69 -83
View File
@@ -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 }
+28
View File
@@ -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 {
+117
View File
@@ -0,0 +1,117 @@
import type { Config } from 'tailwindcss'
const config: Config = {
darkMode: ['class', '.dark'],
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
colors: {
// Dark theme colors (nof1.ai / SURGE-AI inspired)
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
surface: 'hsl(var(--surface))',
'surface-light': 'hsl(var(--surface-light))',
'surface-hover': 'hsl(var(--surface-hover))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
dark: 'hsl(var(--primary-dark))',
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
border: {
DEFAULT: 'hsl(var(--border))',
light: 'hsl(var(--border-light))',
},
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
// Semantic colors
success: {
DEFAULT: 'hsl(var(--success))',
bg: 'hsl(var(--success-bg))',
},
warning: {
DEFAULT: 'hsl(var(--warning))',
bg: 'hsl(var(--warning-bg))',
},
danger: {
DEFAULT: 'hsl(var(--danger))',
bg: 'hsl(var(--danger-bg))',
},
info: {
DEFAULT: 'hsl(var(--info))',
bg: 'hsl(var(--info-bg))',
},
// Chart colors
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))',
},
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
},
animation: {
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
'fade-in': 'fadeIn 0.3s ease-in-out',
'slide-up': 'slideUp 0.3s ease-out',
'shimmer': 'shimmer 1.5s infinite',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
slideUp: {
'0%': { transform: 'translateY(10px)', opacity: '0' },
'100%': { transform: 'translateY(0)', opacity: '1' },
},
shimmer: {
'0%': { backgroundPosition: '-200% 0' },
'100%': { backgroundPosition: '200% 0' },
},
},
},
},
plugins: [],
}
export default config