feat: Smart AI Trading Bot for XAUUSD with ML and SMC

- XGBoost ML model with 37 features for market direction prediction
- Smart Money Concepts (SMC): Order Blocks, FVG, BOS, CHoCH
- HMM market regime detection (trending/ranging/volatile)
- ATR-based stop loss with 1.5 ATR minimum distance
- Broker-level SL protection with fallback
- Time-based exit (max 6 hours per trade)
- Session-aware trading optimized for London/NY overlap
- Auto-retraining based on market conditions
- Telegram notifications and web dashboard
- Backtest results: 63.9% win rate, 2.64 profit factor, 4.83 Sharpe

Backtest period: Jan 2025 - Feb 2026, 654 trades, $4,189 net P/L

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
GifariKemal
2026-02-06 09:01:35 +07:00
co-authored by Claude Opus 4.5
commit 7af9183af3
121 changed files with 43387 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
+36
View File
@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+1
View File
@@ -0,0 +1 @@
Access is denied.
+294
View File
@@ -0,0 +1,294 @@
"""
FastAPI Backend for Web Dashboard
=================================
Serves trading bot status data to the web frontend.
"""
import sys
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.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")
# CORS for frontend
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
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
# 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")
@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]}")
return result
@app.get("/api/health")
async def health():
"""Health check endpoint"""
return {"status": "ok", "connected": state.connected}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
+4
View File
@@ -0,0 +1,4 @@
fastapi>=0.109.0
uvicorn>=0.27.0
python-dotenv>=1.0.0
pydantic>=2.5.0
+23
View File
@@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;
+7
View File
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
+8686
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
{
"name": "web-dashboard",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.563.0",
"next": "16.1.6",
"radix-ui": "^1.4.3",
"react": "19.2.3",
"react-dom": "19.2.3",
"recharts": "^2.15.4",
"tailwind-merge": "^3.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.6",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
"typescript": "^5"
}
}
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
+1
View File
@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+125
View File
@@ -0,0 +1,125 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@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);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
+34
View File
@@ -0,0 +1,34 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "AI Trading Bot - Monitor",
description: "Real-time monitoring dashboard for AI Trading Bot",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className="dark">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-background text-foreground`}
>
{children}
</body>
</html>
);
}
+159
View File
@@ -0,0 +1,159 @@
"use client";
import { useTradingData } from "@/hooks/use-trading-data";
import {
Header,
PriceCard,
AccountCard,
SessionCard,
RiskCard,
SignalCard,
RegimeCard,
PositionsCard,
LogCard,
PriceChart,
EquityChart,
} 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>
);
}
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">
Make sure the API server is running on port 8000
</p>
</div>
</div>
);
}
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',
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
if (loading && !data) {
return (
<div className="min-h-screen bg-background">
<Header connected={false} lastUpdate={wibTime} dataAge={999} />
<LoadingSkeleton />
</div>
);
}
if (error && !data) {
return (
<div className="min-h-screen bg-background">
<Header connected={false} lastUpdate={wibTime} dataAge={999} />
<ErrorDisplay message={error} />
</div>
);
}
if (!data) return null;
return (
<div className="min-h-screen bg-background">
<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} />
{/* 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 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} />
</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>
);
}
@@ -0,0 +1,41 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Wallet } from "lucide-react";
interface AccountCardProps {
balance: number;
equity: number;
profit: number;
}
export function AccountCard({ balance, equity, profit }: 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
</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<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>
</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>
</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)}
</span>
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,78 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { AreaChart, Area, XAxis, YAxis, ResponsiveContainer, Tooltip } from "recharts";
import { Wallet } from "lucide-react";
interface EquityChartProps {
equityData: number[];
balanceData: number[];
}
export function EquityChart({ equityData, balanceData }: EquityChartProps) {
const chartData = equityData.map((equity, i) => ({
index: i,
equity,
balance: balanceData[i] || equity,
}));
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)
</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[120px] w-full">
{equityData.length > 1 ? (
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData}>
<XAxis dataKey="index" hide />
<YAxis domain={['auto', 'auto']} hide />
<Tooltip
contentStyle={{
backgroundColor: 'hsl(var(--card))',
border: '1px solid hsl(var(--border))',
borderRadius: '8px',
}}
labelStyle={{ display: 'none' }}
formatter={(value: number, name: string) => [
`$${value.toFixed(2)}`,
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="95%" stopColor="#22c55e" stopOpacity={0} />
</linearGradient>
</defs>
<Area
type="monotone"
dataKey="balance"
stroke="#666"
strokeWidth={1}
strokeDasharray="3 3"
fill="none"
/>
<Area
type="monotone"
dataKey="equity"
stroke="#22c55e"
strokeWidth={2}
fill="url(#equityGradient)"
/>
</AreaChart>
</ResponsiveContainer>
) : (
<div className="h-full flex items-center justify-center text-muted-foreground">
Waiting for data...
</div>
)}
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,47 @@
"use client";
import { Badge } from "@/components/ui/badge";
import { Bot, Wifi, WifiOff, Clock } from "lucide-react";
interface HeaderProps {
connected: boolean;
lastUpdate: string;
dataAge: number;
}
export function Header({ connected, lastUpdate, dataAge }: HeaderProps) {
const isStale = dataAge > 5;
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>
</div>
</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)`}
</Badge>
{/* Connection Status */}
<Badge variant={connected ? "default" : "destructive"} className="gap-1">
{connected ? <Wifi className="h-3 w-3" /> : <WifiOff className="h-3 w-3" />}
{connected ? 'Connected' : 'Disconnected'}
</Badge>
{/* Time */}
<span className="text-sm font-medium text-muted-foreground">
{lastUpdate || '--:--:--'} WIB
</span>
</div>
</div>
</header>
);
}
@@ -0,0 +1,11 @@
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';
@@ -0,0 +1,60 @@
"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";
interface LogCardProps {
logs: LogEntry[];
}
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';
}
};
const getLevelBadge = (level: string) => {
switch (level) {
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
</CardTitle>
</CardHeader>
<CardContent>
<ScrollArea className="h-[150px] rounded-md bg-black/50 p-3 font-mono text-xs">
{logs.length === 0 ? (
<p className="text-muted-foreground">Waiting for activity...</p>
) : (
<div className="space-y-1">
{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)}]
</span>
<span className="text-foreground/80">{log.message}</span>
</div>
))}
</div>
)}
</ScrollArea>
</CardContent>
</Card>
);
}
@@ -0,0 +1,55 @@
"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 type { Position } from "@/types/trading";
interface PositionsCardProps {
positions: Position[];
}
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
{positions.length > 0 && (
<Badge variant="secondary" className="ml-auto">{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)}
</span>
</div>
))}
</div>
)}
</ScrollArea>
</CardContent>
</Card>
);
}
@@ -0,0 +1,45 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { TrendingUp, TrendingDown } from "lucide-react";
interface PriceCardProps {
price: number;
spread: number;
priceChange: number;
}
export function PriceCard({ price, spread, priceChange }: 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
</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)}
</span>
</div>
<p className="text-xs text-muted-foreground mt-1">
Spread: {spread.toFixed(1)} pips
</p>
</CardContent>
</Card>
);
}
@@ -0,0 +1,63 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { LineChart, Line, XAxis, YAxis, ResponsiveContainer, Tooltip } from "recharts";
import { TrendingUp } from "lucide-react";
interface PriceChartProps {
data: number[];
}
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)
</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[120px] w-full">
{data.length > 1 ? (
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData}>
<XAxis dataKey="index" hide />
<YAxis domain={['auto', 'auto']} hide />
<Tooltip
contentStyle={{
backgroundColor: 'hsl(var(--card))',
border: '1px solid hsl(var(--border))',
borderRadius: '8px',
}}
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} />
</linearGradient>
</defs>
<Line
type="monotone"
dataKey="price"
stroke="hsl(var(--primary))"
strokeWidth={2}
dot={false}
fill="url(#priceGradient)"
/>
</LineChart>
</ResponsiveContainer>
) : (
<div className="h-full flex items-center justify-center text-muted-foreground">
Waiting for data...
</div>
)}
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,45 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Activity } from "lucide-react";
interface RegimeCardProps {
name: string;
volatility: number;
confidence: number;
}
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';
};
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
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="text-center">
<span className={`text-lg font-bold ${getRegimeColor(name)}`}>
{name || '---'}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Volatility</span>
<span className="font-semibold">{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>
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,55 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { ShieldAlert } from "lucide-react";
interface RiskCardProps {
dailyLoss: number;
dailyProfit: number;
consecutiveLosses: number;
riskPercent: number;
}
export function RiskCard({ dailyLoss, dailyProfit, consecutiveLosses, riskPercent }: RiskCardProps) {
const isHighRisk = riskPercent >= 80;
const isMediumRisk = riskPercent >= 50;
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
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<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>
</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>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Consec. Losses</span>
<span className="font-semibold">{consecutiveLosses}</span>
</div>
<div className="pt-2 border-t">
<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'}`}>
{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>
</CardContent>
</Card>
);
}
@@ -0,0 +1,44 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Clock, Sparkles } from "lucide-react";
interface SessionCardProps {
session: string;
isGoldenTime: boolean;
canTrade: boolean;
}
export function SessionCard({ session, isGoldenTime, canTrade }: SessionCardProps) {
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
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="text-center">
<span className="text-lg font-bold text-amber-500">{session}</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'}
</Badge>
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,68 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { Brain, BarChart3 } from "lucide-react";
interface SignalCardProps {
title: string;
icon: "smc" | "ml";
signal: string;
confidence: number;
detail?: string;
buyProb?: number;
sellProb?: number;
}
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';
};
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 '';
};
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" />}
{title}
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="text-center">
<span className={`text-2xl font-bold ${getSignalColor(signal)}`}>
{signal || 'NO SIGNAL'}
</span>
</div>
<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>
</div>
<Progress value={confidence * 100} className={`h-1.5 ${getProgressColor(signal)}`} />
</div>
{detail && (
<p className="text-xs text-muted-foreground line-clamp-2">{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>
)}
</CardContent>
</Card>
);
}
+48
View File
@@ -0,0 +1,48 @@
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",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
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",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant = "default",
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot.Root : "span"
return (
<Comp
data-slot="badge"
data-variant={variant}
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }
+92
View File
@@ -0,0 +1,92 @@
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}
/>
)
}
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}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
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}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
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,
}
+357
View File
@@ -0,0 +1,357 @@
"use client"
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/lib/utils"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
function ChartContainer({
id,
className,
children,
config,
...props
}: React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}) {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-slot="chart"
data-chart={chartId}
className={cn(
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
}
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([, config]) => config.theme || config.color
)
if (!colorConfig.length) {
return null
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
function ChartTooltipContent({
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}) {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
className={cn(
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload
.filter((item) => item.type !== "none")
.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
const ChartLegend = RechartsPrimitive.Legend
function ChartLegendContent({
className,
hideIcon = false,
payload,
verticalAlign = "bottom",
nameKey,
}: React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}) {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload
.filter((item) => item.type !== "none")
.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}
@@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import { Progress as ProgressPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
)
}
export { Progress }
@@ -0,0 +1,58 @@
"use client"
import * as React from "react"
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
)
}
function ScrollBar({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={cn(
"flex touch-none p-px transition-colors select-none",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
)
}
export { ScrollArea, ScrollBar }
@@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import { Separator as SeparatorPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
)
}
export { Separator }
@@ -0,0 +1,13 @@
import { cn } from "@/lib/utils"
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)}
{...props}
/>
)
}
export { Skeleton }
+116
View File
@@ -0,0 +1,116 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}
@@ -0,0 +1,50 @@
"use client";
import { useState, useEffect, useCallback } from 'react';
import type { TradingStatus } from '@/types/trading';
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
export function useTradingData() {
const [data, setData] = useState<TradingStatus | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [lastFetch, setLastFetch] = useState<Date | null>(null);
const fetchData = useCallback(async () => {
try {
const res = await fetch(`${API_URL}/api/status`, {
cache: 'no-store',
});
if (!res.ok) {
throw new Error(`HTTP error: ${res.status}`);
}
const json = await res.json();
setData(json);
setError(null);
setLastFetch(new Date());
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
// Initial fetch
fetchData();
// Poll every second
const interval = setInterval(fetchData, 1000);
return () => clearInterval(interval);
}, [fetchData]);
const dataAge = lastFetch
? (Date.now() - lastFetch.getTime()) / 1000
: 999;
return { data, loading, error, dataAge, refetch: fetchData };
}
+68
View File
@@ -0,0 +1,68 @@
// Trading data types
export interface TradingStatus {
timestamp: string;
connected: boolean;
// Price
price: number;
spread: number;
priceChange: number;
priceHistory: number[];
// Account
balance: number;
equity: number;
profit: number;
equityHistory: number[];
balanceHistory: number[];
// Session
session: string;
isGoldenTime: boolean;
canTrade: boolean;
// Risk
dailyLoss: number;
dailyProfit: number;
consecutiveLosses: number;
riskPercent: number;
// Signals
smc: {
signal: string;
confidence: number;
reason: string;
};
ml: {
signal: string;
confidence: number;
buyProb: number;
sellProb: number;
};
regime: {
name: string;
volatility: number;
confidence: number;
};
// Positions
positions: Position[];
// Log
logs: LogEntry[];
}
export interface Position {
ticket: number;
type: 'BUY' | 'SELL';
volume: number;
priceOpen: number;
profit: number;
}
export interface LogEntry {
time: string;
level: 'info' | 'warn' | 'error' | 'trade';
message: string;
}
+34
View File
@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}