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:
buckybonez
2026-02-08 10:33:24 +07:00
parent a8d01995ab
commit 82eca84230
64 changed files with 11073 additions and 929 deletions
+22
View File
@@ -0,0 +1,22 @@
# Only the API code is needed — keep context minimal
.git/
.venv/
venv/
__pycache__/
*.pyc
node_modules/
.next/
*.log
*.pkl
*.joblib
docs/
archive/
backtests/
tests/
scripts/
models/
logs/
data/
.env
.env.local
*.md
+39
View File
@@ -0,0 +1,39 @@
# ===========================================
# XAUBot AI - Docker Environment Configuration
# ===========================================
# ============== MT5 CONNECTION ==============
# Your MetaTrader 5 account credentials
MT5_LOGIN=your_mt5_login
MT5_PASSWORD=your_mt5_password
MT5_SERVER=your_mt5_server
MT5_PATH=/path/to/mt5/terminal
# ============== TRADING CONFIG ==============
SYMBOL=XAUUSD
CAPITAL=10000
# ============== DATABASE ==============
DB_HOST=postgres
DB_PORT=5432
DB_USER=trading_bot
DB_PASSWORD=trading_bot_2026
DB_NAME=trading_db
# ============== TELEGRAM (Optional) ==============
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
# ============== PORTS ==============
# Ports accessible from host machine
API_PORT=8000 # Trading API (FastAPI)
DASHBOARD_PORT=3000 # Web Dashboard (Next.js)
DB_PORT=5432 # PostgreSQL
PGADMIN_PORT=5050 # pgAdmin (optional)
# ============== PGADMIN (Optional) ==============
PGADMIN_EMAIL=admin@trading.local
PGADMIN_PASSWORD=admin123
# ============== TIMEZONE ==============
TZ=Asia/Jakarta
+255
View File
@@ -0,0 +1,255 @@
# Dashboard Integration with Existing Docker Setup
## 🎯 Overview
Dashboard dan API telah diintegrasikan ke dalam Docker setup yang **sudah ada**. Database PostgreSQL yang sudah running **TIDAK AKAN DIGANGGU**.
## ✅ Existing Setup (Tidak Berubah)
Yang sudah jalan dan **tetap aman**:
-`trading_bot_db` - PostgreSQL database
-`trading_bot_network` - Docker network
- ✅ Database schema dengan 7 tables (trades, signals, dll)
- ✅ Volume `postgres_data` untuk persistence
## 🆕 New Services Added
Layanan baru yang ditambahkan:
1. **trading-api** - FastAPI backend untuk dashboard
2. **dashboard** - Next.js web interface
3. **pgadmin** - Database management (optional)
## 🚀 Quick Start
### Option 1: Gunakan Helper Script (Recommended)
```cmd
# Tambahkan dashboard ke setup yang sudah ada
docker-add-dashboard.bat
```
Script ini akan:
1. Check database yang sudah running
2. Build API & Dashboard services
3. Start kedua services baru
4. Connect ke database & network yang sudah ada
### Option 2: Manual Docker Compose
```cmd
# Build hanya services baru
docker-compose build trading-api dashboard
# Start hanya services baru
docker-compose up -d trading-api dashboard
```
## 📊 Access Points
Setelah services running:
- **Dashboard:** http://localhost:3000
- **API:** http://localhost:8000
- **API Docs:** http://localhost:8000/docs
- **Database:** localhost:5432 (sudah running)
## 🔧 Service Management
### Check Status
```cmd
# Lihat status semua services
docker-status.bat
# Atau manual
docker-compose ps
```
### View Logs
```cmd
# Logs dashboard
docker-compose logs -f dashboard
# Logs API
docker-compose logs -f trading-api
# Logs database
docker-compose logs -f postgres
```
### Restart Services
```cmd
# Restart hanya dashboard
docker-compose restart dashboard
# Restart hanya API
docker-compose restart trading-api
# Restart semua (termasuk database)
docker-compose restart
```
### Remove Dashboard (Keep Database)
```cmd
# Hapus dashboard tapi tetap keep database
docker-remove-dashboard.bat
# Atau manual
docker-compose stop trading-api dashboard
docker-compose rm -f trading-api dashboard
```
## 🔗 Service Architecture
```
┌─────────────────────────────────────────────┐
│ trading_bot_network │
├─────────────────────────────────────────────┤
│ │
│ 📊 Dashboard (NEW) │
│ Port: 3000 │
│ └─> http://trading-api:8000 │
│ │
│ 🔌 Trading API (NEW) │
│ Port: 8000 │
│ └─> postgres:5432 │
│ │
│ 🗄️ PostgreSQL (EXISTING - NO CHANGE) │
│ Port: 5432 │
│ Status: Already Running │
│ Volume: postgres_data │
│ │
└─────────────────────────────────────────────┘
```
## 📝 Environment Variables
Edit `.env` untuk konfigurasi:
```env
# MT5 (Required for API)
MT5_LOGIN=your_login
MT5_PASSWORD=your_password
MT5_SERVER=your_server
MT5_PATH=C:/Program Files/MetaTrader 5/terminal64.exe
# Trading
SYMBOL=XAUUSD
CAPITAL=10000
# Database (Already configured)
DB_USER=trading_bot
DB_PASSWORD=trading_bot_2026
DB_NAME=trading_db
# Ports
API_PORT=8000
DASHBOARD_PORT=3000
DB_PORT=5432
```
## 🐛 Troubleshooting
### Dashboard tidak bisa connect ke API
**Check API health:**
```cmd
curl http://localhost:8000/api/health
```
**View API logs:**
```cmd
docker-compose logs -f trading-api
```
### API tidak bisa connect ke database
**Check database:**
```cmd
docker exec trading_bot_db pg_isready -U trading_bot
```
**Check network:**
```cmd
docker network inspect trading_bot_network
```
### Port conflict
Edit `.env` untuk ganti port:
```env
API_PORT=8001
DASHBOARD_PORT=3001
```
Then restart:
```cmd
docker-compose down trading-api dashboard
docker-compose up -d trading-api dashboard
```
## 💾 Data Persistence
**Database data tetap aman:**
- Volume `postgres_data` tetap ada
- Hapus container tidak hapus data
- Data tersimpan di Docker volume
**Check volume:**
```cmd
docker volume ls | findstr postgres
docker volume inspect trading_bot_postgres_data
```
## 🔄 Updates
**Update code dan rebuild:**
```cmd
# Pull latest code
git pull
# Rebuild services baru
docker-compose build trading-api dashboard
# Restart
docker-compose up -d trading-api dashboard
```
**Database tidak perlu rebuild** karena schema sudah ada.
## ⚠️ Important Notes
1. **Database tidak boleh dihapus** - Data trades ada di sini
2. **Jangan run `docker-compose down -v`** - Ini akan hapus volumes
3. **Untuk stop semua:** `docker-compose stop` (data aman)
4. **Untuk restart:** `docker-compose restart` atau `docker-compose up -d`
## 📚 Files Structure
```
xaubot-ai/
├── docker-compose.yml # Main orchestration (UPDATED)
├── Dockerfile # API image (NEW)
├── .env # Environment config
├── .dockerignore # Build exclusions
├── docker-add-dashboard.bat # Add dashboard script (NEW)
├── docker-remove-dashboard.bat # Remove dashboard script (NEW)
├── docker-status.bat # Status check script (NEW)
├── docker/
│ └── init-db/
│ └── 01-schema.sql # Database schema (EXISTING)
└── web-dashboard/
├── Dockerfile # Dashboard image (NEW)
└── .dockerignore # Build exclusions
```
## 🎯 Summary
**Database tetap jalan** - Tidak ada perubahan
**Services baru ditambahkan** - API & Dashboard
**Data aman** - Volume persistence
**Easy management** - Helper scripts
**Independent** - Bisa start/stop tanpa ganggu database
---
**Integration completed:** Feb 6, 2026
**Status:** Dashboard integrated with existing Docker setup ✨
+401
View File
@@ -0,0 +1,401 @@
# XAUBot AI - Docker Integration Summary
## ✅ Completed Tasks
### 1. **Created Dockerfile for Next.js Dashboard**
- Multi-stage build for optimization
- Standalone output for minimal image size
- Production-ready configuration
- Non-root user for security
**Location:** `web-dashboard/Dockerfile`
### 2. **Created Dockerfile for Python Trading API**
- Python 3.11-slim base image
- FastAPI server with health checks
- Proper dependency management
- Volume mounts for data/logs/models
**Location:** `Dockerfile` (root directory)
### 3. **Updated Docker Compose Configuration**
- 4 services: postgres, trading-api, dashboard, pgadmin
- Proper service dependencies and health checks
- Custom bridge network for inter-service communication
- Environment variable support via .env file
- Volume persistence for database and pgadmin
**Location:** `docker-compose.yml`
### 4. **Created Environment Configuration**
- Template with all required variables
- Clear documentation for each setting
- Default values for non-sensitive configs
**Location:** `.env.docker.example`
### 5. **Created Docker Ignore Files**
- Excludes unnecessary files from images
- Reduces build context size
- Improves build performance
**Locations:**
- `web-dashboard/.dockerignore`
- `.dockerignore` (root)
### 6. **Created Helper Scripts**
#### Windows Batch Scripts:
- `docker-start.bat` - Start all services
- `docker-stop.bat` - Stop services with options
- `docker-logs.bat` - View service logs
#### Linux/Mac Shell Scripts:
- `docker-start.sh` - Start all services
- `docker-stop.sh` - Stop services with options
- `docker-logs.sh` - View service logs
### 7. **Updated Next.js Configuration**
- Enabled standalone output for Docker
- Optimized for production builds
**Location:** `web-dashboard/next.config.ts`
### 8. **Created Comprehensive Documentation**
- Complete Docker setup guide
- Architecture diagram
- Service management commands
- Troubleshooting section
- Security best practices
- Performance tuning tips
**Location:** `DOCKER.md`
### 9. **Updated Main README**
- Added Docker deployment section as recommended method
- Clear quick start instructions
- Links to full documentation
**Location:** `README.md`
## 🏗️ Architecture
```
┌──────────────────────────────────────────────────────┐
│ Docker Network │
│ (trading_bot_network) │
├──────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌──────────────────┐ │
│ │ Dashboard │────────▶│ Trading API │ │
│ │ (Next.js) │ HTTP │ (FastAPI) │ │
│ │ Port: 3000 │ │ Port: 8000 │ │
│ └─────────────────┘ └────────┬─────────┘ │
│ │ │
│ │ PostgreSQL │
│ │ Protocol │
│ │ │
│ ┌────────▼─────────┐ │
│ │ PostgreSQL │ │
│ │ Database │ │
│ │ Port: 5432 │ │
│ └──────────────────┘ │
│ │
│ ┌─────────────────┐ (Optional - Admin Profile) │
│ │ pgAdmin │ │
│ │ Port: 5050 │ │
│ └─────────────────┘ │
└──────────────────────────────────────────────────────┘
↕ Exposed Ports
localhost:3000 (Dashboard)
localhost:8000 (API)
localhost:5432 (Database)
localhost:5050 (pgAdmin)
```
## 🚀 Quick Start Guide
### 1. Initial Setup (One-time)
```bash
# Navigate to project
cd "Smart Automatic Trading BOT + AI"
# Create environment file
copy .env.docker.example .env
# Edit .env with your MT5 credentials
notepad .env
```
**Required credentials in .env:**
```env
MT5_LOGIN=your_login
MT5_PASSWORD=your_password
MT5_SERVER=your_server
MT5_PATH=/path/to/mt5/terminal
```
### 2. Start Services (Windows)
**Option A: Using helper script (Recommended)**
```cmd
REM Start core services
docker-start.bat
REM Or start with pgAdmin
docker-start.bat --admin
```
**Option B: Manual docker-compose**
```cmd
REM Build and start
docker-compose up -d
REM With pgAdmin
docker-compose --profile admin up -d
```
### 3. Access the Dashboard
Open your browser and go to:
- **Dashboard:** http://localhost:3000
You'll see:
- Real-time price updates
- Account balance and equity
- Trading signals (SMC + ML)
- Market regime
- Open positions
- Risk status
- Activity logs
### 4. Check Other Services
- **API Docs:** http://localhost:8000/docs
- **API Health:** http://localhost:8000/api/health
- **API Status:** http://localhost:8000/api/status
- **pgAdmin:** http://localhost:5050 (if started with --admin)
## 📋 Common Commands
### View Logs
```cmd
REM All services
docker-logs.bat
REM Specific service
docker-logs.bat trading-api
docker-logs.bat dashboard
docker-logs.bat postgres
```
### Check Status
```cmd
docker-compose ps
```
### Restart Services
```cmd
REM Restart all
docker-compose restart
REM Restart specific
docker-compose restart trading-api
docker-compose restart dashboard
```
### Stop Services
```cmd
REM Stop (keeps data)
docker-stop.bat
REM Stop and remove containers (keeps data)
docker-stop.bat --remove
REM Stop and remove everything including data (⚠️ DANGER!)
docker-stop.bat --clean
```
### Update Code and Rebuild
```cmd
REM Pull latest code
git pull
REM Rebuild and restart
docker-compose build
docker-compose up -d
```
## 🔧 Configuration
### Port Configuration
Default ports can be changed in `.env`:
```env
API_PORT=8000 # Trading API
DASHBOARD_PORT=3000 # Web Dashboard
DB_PORT=5432 # PostgreSQL
PGADMIN_PORT=5050 # pgAdmin
```
### Environment Variables
All configuration is in `.env`:
| Category | Variables |
|----------|-----------|
| **MT5** | MT5_LOGIN, MT5_PASSWORD, MT5_SERVER, MT5_PATH |
| **Trading** | SYMBOL, CAPITAL |
| **Database** | DB_USER, DB_PASSWORD, DB_NAME |
| **Telegram** | TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID |
| **Ports** | API_PORT, DASHBOARD_PORT, DB_PORT, PGADMIN_PORT |
## 🐛 Troubleshooting
### Dashboard Shows "Connection Error"
**Check if API is running:**
```cmd
curl http://localhost:8000/api/health
```
**View API logs:**
```cmd
docker-logs.bat trading-api
```
### Port Already in Use
**Find what's using the port:**
```cmd
netstat -ano | findstr :3000
netstat -ano | findstr :8000
```
**Change port in .env:**
```env
DASHBOARD_PORT=3001
API_PORT=8001
```
**Restart services:**
```cmd
docker-compose down
docker-compose up -d
```
### Can't Connect to MT5
1. Check credentials in `.env`
2. Ensure MT5 terminal is accessible
3. View API logs for connection errors:
```cmd
docker-logs.bat trading-api
```
### Database Connection Issues
**Check database health:**
```cmd
docker-compose ps postgres
```
**Test connection:**
```cmd
docker exec -it trading_bot_db pg_isready -U trading_bot
```
**View database logs:**
```cmd
docker-logs.bat postgres
```
## 📊 Monitoring
### View Real-time Logs
```cmd
REM Follow all logs
docker-compose logs -f
REM Follow specific service
docker-compose logs -f trading-api
```
### Check Resource Usage
```cmd
docker stats
```
### Service Health
```cmd
REM All services
docker-compose ps
REM Detailed info
docker inspect trading_bot_api
docker inspect trading_bot_dashboard
```
## 🔐 Security Notes
1. **Never commit .env file** - It contains sensitive credentials
2. **Change default passwords** - Especially for database and pgAdmin
3. **Use strong passwords** - For all services
4. **Limit port exposure** - Only expose ports you need
5. **Keep Docker updated** - Regular security updates
## 📁 File Structure
```
xaubot-ai/
├── Dockerfile # Python API Docker image
├── docker-compose.yml # Service orchestration
├── .env # Environment variables (DO NOT COMMIT)
├── .env.docker.example # Environment template
├── .dockerignore # Files to exclude from build
├── docker-start.bat # Windows start script
├── docker-stop.bat # Windows stop script
├── docker-logs.bat # Windows logs script
├── docker-start.sh # Linux/Mac start script
├── docker-stop.sh # Linux/Mac stop script
├── docker-logs.sh # Linux/Mac logs script
├── DOCKER.md # Full Docker documentation
└── web-dashboard/
├── Dockerfile # Next.js dashboard image
├── .dockerignore # Dashboard build exclusions
└── next.config.ts # Next.js config (standalone output)
```
## 🎯 Benefits of Docker Setup
✅ **Easy Setup** - One command to start everything
✅ **Consistent Environment** - Same setup on any machine
✅ **Isolated Services** - No conflicts with other software
✅ **Easy Updates** - Rebuild and restart to update
✅ **Production Ready** - Same setup for dev and production
✅ **Automatic Restarts** - Services auto-restart on crash
✅ **Health Monitoring** - Built-in health checks
✅ **Volume Persistence** - Data survives container restarts
## 📚 Additional Resources
- **Full Documentation:** [DOCKER.md](DOCKER.md)
- **Styling Guide:** [web-dashboard/STYLING-GUIDE.md](web-dashboard/STYLING-GUIDE.md)
- **Docker Docs:** https://docs.docker.com
- **Docker Compose:** https://docs.docker.com/compose
## 🆘 Support
If you encounter issues:
1. Check the logs: `docker-logs.bat`
2. Verify services: `docker-compose ps`
3. Review troubleshooting section in [DOCKER.md](DOCKER.md)
4. Check service health: `curl http://localhost:8000/api/health`
---
**Setup completed:** Feb 6, 2026
**Ready to deploy!** 🚀
+437
View File
@@ -0,0 +1,437 @@
# XAUBot AI - Docker Setup Guide
Complete guide to running the XAUBot AI trading system with Docker.
## 📋 Prerequisites
- Docker Engine 20.10+
- Docker Compose 2.0+
- 4GB+ RAM available
- MetaTrader 5 account credentials
## 🏗️ Architecture
The Docker setup includes 4 services:
```
┌─────────────────────────────────────────────────┐
│ Host Machine │
├─────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Dashboard │─────▶│ Trading API │ │
│ │ Next.js │ │ FastAPI │ │
│ │ Port: 3000 │ │ Port: 8000 │ │
│ └──────────────┘ └──────┬───────┘ │
│ │ │
│ ┌──────▼───────┐ │
│ │ PostgreSQL │ │
│ │ Port: 5432 │ │
│ └──────────────┘ │
│ │
│ ┌──────────────┐ (Optional - Profile: admin) │
│ │ pgAdmin │ │
│ │ Port: 5050 │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────┘
```
### Services
1. **postgres** - PostgreSQL 16 database for trade logging
2. **trading-api** - Python FastAPI backend serving trading data
3. **dashboard** - Next.js web interface for monitoring
4. **pgadmin** - Database management UI (optional, admin profile)
## 🚀 Quick Start
### 1. Clone & Setup
```bash
cd "Smart Automatic Trading BOT + AI"
# Copy environment template
cp .env.docker.example .env
```
### 2. Configure Environment
Edit `.env` file with your credentials:
```bash
# Required
MT5_LOGIN=your_login
MT5_PASSWORD=your_password
MT5_SERVER=your_server
MT5_PATH=/path/to/mt5
# Optional - adjust ports if needed
API_PORT=8000
DASHBOARD_PORT=3000
DB_PORT=5432
```
### 3. Start Services
**Option A: All services (without pgAdmin)**
```bash
docker-compose up -d
```
**Option B: All services including pgAdmin**
```bash
docker-compose --profile admin up -d
```
**Option C: Specific services only**
```bash
# Just database and API
docker-compose up -d postgres trading-api
# Add dashboard
docker-compose up -d dashboard
```
### 4. Access Services
- **Dashboard**: http://localhost:3000
- **Trading API**: http://localhost:8000
- **API Docs**: http://localhost:8000/docs
- **pgAdmin**: http://localhost:5050 (if using admin profile)
- **PostgreSQL**: localhost:5432
## 📊 Service Management
### View Logs
```bash
# All services
docker-compose logs -f
# Specific service
docker-compose logs -f dashboard
docker-compose logs -f trading-api
docker-compose logs -f postgres
# Last 50 lines
docker-compose logs --tail=50 trading-api
```
### Check Status
```bash
# List running containers
docker-compose ps
# Check health
docker-compose ps --format json | jq '.[].Health'
# Detailed status
docker inspect trading_bot_api
```
### Restart Services
```bash
# Restart all
docker-compose restart
# Restart specific service
docker-compose restart trading-api
docker-compose restart dashboard
```
### Stop Services
```bash
# Stop all (keeps data)
docker-compose stop
# Stop and remove containers (keeps data)
docker-compose down
# Stop and remove everything including volumes (⚠️ deletes data!)
docker-compose down -v
```
## 🔧 Development & Debugging
### Access Container Shell
```bash
# Trading API container
docker exec -it trading_bot_api bash
# Dashboard container
docker exec -it trading_bot_dashboard sh
# Database
docker exec -it trading_bot_db psql -U trading_bot -d trading_db
```
### Rebuild After Code Changes
```bash
# Rebuild all
docker-compose build
# Rebuild specific service
docker-compose build trading-api
docker-compose build dashboard
# Rebuild and restart
docker-compose up -d --build
```
### View Resource Usage
```bash
# CPU, Memory, Network
docker stats
# Specific container
docker stats trading_bot_api
```
## 🗄️ Database Management
### Connect to PostgreSQL
```bash
# Via Docker
docker exec -it trading_bot_db psql -U trading_bot -d trading_db
# Via host (if port exposed)
psql -h localhost -p 5432 -U trading_bot -d trading_db
```
### Backup Database
```bash
# Create backup
docker exec trading_bot_db pg_dump -U trading_bot trading_db > backup_$(date +%Y%m%d).sql
# Restore backup
docker exec -i trading_bot_db psql -U trading_bot -d trading_db < backup_20260206.sql
```
### Using pgAdmin
1. Start with admin profile:
```bash
docker-compose --profile admin up -d
```
2. Open http://localhost:5050
3. Login:
- Email: admin@trading.local
- Password: admin123
4. Add Server:
- Host: postgres
- Port: 5432
- Database: trading_db
- Username: trading_bot
- Password: trading_bot_2026
## 🔍 Troubleshooting
### Container Won't Start
```bash
# Check logs
docker-compose logs trading-api
# Check events
docker events --filter container=trading_bot_api
# Inspect container
docker inspect trading_bot_api
```
### Port Already in Use
```bash
# Find what's using the port
netstat -ano | findstr :3000
netstat -ano | findstr :8000
# Change port in .env
DASHBOARD_PORT=3001
API_PORT=8001
# Restart
docker-compose down
docker-compose up -d
```
### API Can't Connect to MT5
1. Check MT5 credentials in `.env`
2. Ensure MT5 terminal is running (if running on host)
3. Check container logs:
```bash
docker-compose logs trading-api | grep MT5
```
### Dashboard Shows Connection Error
1. Check if API is healthy:
```bash
curl http://localhost:8000/api/health
```
2. Check API logs:
```bash
docker-compose logs trading-api
```
3. Verify API_URL in dashboard:
```bash
docker exec -it trading_bot_dashboard env | grep API
```
### Database Connection Issues
```bash
# Check if postgres is healthy
docker-compose ps postgres
# Test connection
docker exec -it trading_bot_db pg_isready -U trading_bot
# Check logs
docker-compose logs postgres
```
## 🔐 Security Best Practices
1. **Change Default Passwords**
```bash
# In .env
DB_PASSWORD=strong_password_here
PGADMIN_PASSWORD=another_strong_password
```
2. **Don't Expose Unnecessary Ports**
```yaml
# In docker-compose.yml, comment out if not needed:
# ports:
# - "5432:5432" # Only if you need external DB access
```
3. **Use Secrets for Production**
```bash
# Use Docker secrets instead of .env
docker secret create mt5_password password.txt
```
4. **Restrict Network Access**
```bash
# Only expose dashboard port
docker-compose up -d postgres trading-api
# Then separately: docker-compose up -d dashboard
```
## 📈 Performance Tuning
### Allocate More Resources
```yaml
# In docker-compose.yml
services:
trading-api:
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '1.0'
memory: 1G
```
### Optimize Database
```bash
# Connect to DB
docker exec -it trading_bot_db psql -U trading_bot -d trading_db
# Run vacuum
VACUUM ANALYZE;
# Check table sizes
SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
```
## 🔄 Updates & Maintenance
### Update Images
```bash
# Pull latest base images
docker-compose pull
# Rebuild
docker-compose build --no-cache
# Restart
docker-compose up -d
```
### Clean Up
```bash
# Remove unused images
docker image prune -a
# Remove unused volumes (⚠️ careful!)
docker volume prune
# Remove everything unused
docker system prune -a --volumes
```
## 📝 Environment Variables Reference
| Variable | Default | Description |
|----------|---------|-------------|
| `MT5_LOGIN` | - | MT5 account login |
| `MT5_PASSWORD` | - | MT5 account password |
| `MT5_SERVER` | - | MT5 server name |
| `MT5_PATH` | - | Path to MT5 terminal |
| `SYMBOL` | XAUUSD | Trading symbol |
| `CAPITAL` | 10000 | Trading capital |
| `API_PORT` | 8000 | API port on host |
| `DASHBOARD_PORT` | 3000 | Dashboard port on host |
| `DB_PORT` | 5432 | Database port on host |
| `DB_USER` | trading_bot | Database username |
| `DB_PASSWORD` | trading_bot_2026 | Database password |
| `DB_NAME` | trading_db | Database name |
| `TELEGRAM_BOT_TOKEN` | - | Telegram bot token (optional) |
| `TELEGRAM_CHAT_ID` | - | Telegram chat ID (optional) |
## 📚 Additional Resources
- **Docker Docs**: https://docs.docker.com
- **Docker Compose**: https://docs.docker.com/compose
- **FastAPI**: https://fastapi.tiangolo.com
- **Next.js**: https://nextjs.org
## 🆘 Getting Help
If you encounter issues:
1. Check logs: `docker-compose logs -f`
2. Verify services: `docker-compose ps`
3. Check health: `curl http://localhost:8000/api/health`
4. Review this guide's troubleshooting section
5. Open an issue on GitHub
---
**Last Updated:** Feb 6, 2026
+24
View File
@@ -0,0 +1,24 @@
# Lightweight API server — reads bot_status.json from mounted volume
FROM python:3.11-slim
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
# Install only API dependencies
COPY web-dashboard/api/requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Copy only the API code
COPY web-dashboard/api/main.py main.py
# Create data directory (will be overridden by volume mount)
RUN mkdir -p data
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')" || exit 1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
+161
View File
@@ -0,0 +1,161 @@
# Quick Start - Tambah Dashboard ke Docker Existing
## Status Saat Ini
**Docker Compose sudah ada**
**Service `postgres` sudah running** (container: `trading_bot_db`)
**Service `trading-api` dan `dashboard` sudah didefinisikan** tapi belum di-build
## 🚀 Cara Menjalankan
### 1. Setup Environment (Kalau Belum)
```cmd
cd "C:\Users\Administrator\Videos\Smart Automatic Trading BOT + AI"
REM Copy environment template kalau belum ada
copy .env.docker.example .env
REM Edit dengan MT5 credentials Anda
notepad .env
```
Pastikan isi `.env`:
```env
MT5_LOGIN=your_login
MT5_PASSWORD=your_password
MT5_SERVER=your_server
MT5_PATH=C:/Program Files/MetaTrader 5/terminal64.exe
SYMBOL=XAUUSD
CAPITAL=10000
```
### 2. Build Services Baru
```cmd
REM Build trading-api dan dashboard
docker-compose build trading-api dashboard
```
Ini akan:
- Build Dockerfile untuk Python API
- Build Dockerfile untuk Next.js Dashboard
- Tidak ganggu database yang sudah running
### 3. Start Services Baru
```cmd
REM Start trading-api dan dashboard
docker-compose up -d trading-api dashboard
```
### 4. Check Status
```cmd
docker-compose ps
```
Output akan menunjukkan:
```
NAME STATUS PORTS
trading_bot_db Up (healthy) 0.0.0.0:5432->5432/tcp
trading_bot_api Up (healthy) 0.0.0.0:8000->8000/tcp
trading_bot_dashboard Up (healthy) 0.0.0.0:3000->3000/tcp
```
### 5. Akses Dashboard
Buka browser:
- **Dashboard:** http://localhost:3000
- **API:** http://localhost:8000
- **API Docs:** http://localhost:8000/docs
## 📋 Commands Penting
```cmd
# Lihat logs
docker-compose logs -f dashboard
docker-compose logs -f trading-api
# Restart service
docker-compose restart trading-api
docker-compose restart dashboard
# Stop service
docker-compose stop trading-api dashboard
# Start lagi
docker-compose up -d trading-api dashboard
# Rebuild setelah update code
docker-compose build trading-api dashboard
docker-compose up -d trading-api dashboard
```
## 🔍 Troubleshooting
### Build Error
```cmd
# Clean build
docker-compose build --no-cache trading-api dashboard
```
### Service Tidak Start
```cmd
# Check logs
docker-compose logs trading-api
docker-compose logs dashboard
# Check health
curl http://localhost:8000/api/health
curl http://localhost:3000
```
### Port Conflict
Edit `.env`:
```env
API_PORT=8001
DASHBOARD_PORT=3001
```
Lalu restart:
```cmd
docker-compose down trading-api dashboard
docker-compose up -d trading-api dashboard
```
## ⚡ One-Liner (All in One)
```cmd
cd "C:\Users\Administrator\Videos\Smart Automatic Trading BOT + AI" && docker-compose build trading-api dashboard && docker-compose up -d trading-api dashboard && docker-compose ps
```
## 📊 Arsitektur
```
Docker Compose Project: "smart-automatic-trading-bot-ai"
├── postgres (RUNNING) ✅
│ └── trading_bot_db
├── trading-api (BUILD & START) ⚡
│ └── trading_bot_api
└── dashboard (BUILD & START) ⚡
└── trading_bot_dashboard
```
## ✅ Checklist
- [ ] Copy `.env.docker.example` ke `.env`
- [ ] Edit `.env` dengan MT5 credentials
- [ ] Run: `docker-compose build trading-api dashboard`
- [ ] Run: `docker-compose up -d trading-api dashboard`
- [ ] Check: `docker-compose ps`
- [ ] Open: http://localhost:3000
- [ ] Test API: http://localhost:8000/api/health
---
**That's it!** Simple kan? 🎉
+33 -2
View File
@@ -119,13 +119,44 @@ xaubot-ai/
## Installation
### Prerequisites
### 🐳 Docker Deployment (Recommended)
**Quick Start:**
```bash
# 1. Clone the repository
git clone https://github.com/GifariKemal/xaubot-ai.git
cd xaubot-ai
# 2. Configure environment
cp .env.docker.example .env
# Edit .env with your MT5 credentials
# 3. Start all services (Windows)
docker-start.bat
# 3. Start all services (Linux/Mac)
./docker-start.sh
```
**Services will be available at:**
- 📊 Dashboard: http://localhost:3000
- 🔌 API: http://localhost:8000
- 📚 API Docs: http://localhost:8000/docs
- 🗄️ Database: localhost:5432
**Full Docker documentation:** See [DOCKER.md](DOCKER.md)
---
### 🐍 Manual Installation
**Prerequisites:**
- Python 3.11+
- MetaTrader 5 terminal (Windows)
- PostgreSQL (optional, for trade logging)
### Setup
**Setup:**
```bash
# Clone the repository
+170
View File
@@ -0,0 +1,170 @@
# Simple Start Guide - XAUBot AI Dashboard
## 🎯 Cara Tercepat (1 Command)
```cmd
start-all.bat
```
Script ini akan:
1. ✅ Check database Docker container
2. 🚀 Start Trading API di http://localhost:8000
3. 🚀 Start Dashboard di http://localhost:3000
Dua window akan terbuka otomatis!
## 📋 Manual Start (Jika Perlu)
### Option 1: Start Semua Sekaligus
```cmd
start-all.bat
```
### Option 2: Start Satu-satu
**Terminal 1: API**
```cmd
start-api.bat
```
**Terminal 2: Dashboard**
```cmd
start-dashboard.bat
```
## ✅ Pre-requisites
### 1. Database (Docker)
Database harus sudah running:
```cmd
# Check status
docker ps | findstr trading_bot_db
# Start jika belum running
docker-compose up -d postgres
```
### 2. Python Environment
- Python 3.11+ installed
- Virtual environment akan dibuat otomatis
### 3. Node.js
- Node.js 18+ installed
- npm dependencies akan diinstall otomatis
## 🌐 Access Points
Setelah start:
- **Dashboard:** http://localhost:3000
- **API:** http://localhost:8000
- **API Docs:** http://localhost:8000/docs
- **Health Check:** http://localhost:8000/api/health
- **Status:** http://localhost:8000/api/status
## 🛑 Stop Services
Close kedua command windows atau tekan `Ctrl+C` di masing-masing window.
## 🔍 Troubleshooting
### API Error: "Module not found"
Install dependencies:
```cmd
pip install -r requirements.txt
```
### Dashboard Error: "Module not found"
Install dependencies:
```cmd
cd web-dashboard
npm install
```
### Port Already in Use
**Change API Port:**
Edit `web-dashboard/api/main.py` line terakhir:
```python
uvicorn.run(app, host="0.0.0.0", port=8001) # Change 8000 to 8001
```
**Change Dashboard Port:**
Edit `web-dashboard/.env.local`:
```
NEXT_PUBLIC_API_URL=http://localhost:8001
```
Then start dashboard on different port:
```cmd
cd web-dashboard
set PORT=3001 && npm run dev
```
### Database Not Running
Start database:
```cmd
docker-compose up -d postgres
# Check status
docker ps
```
## 📊 Architecture
```
┌─────────────────────────────────────┐
│ Windows Host Machine │
├─────────────────────────────────────┤
│ │
│ 📊 Dashboard (Port 3000) │
│ npm run dev │
│ ↓ HTTP │
│ 🔌 API (Port 8000) │
│ uvicorn main:app │
│ ↓ PostgreSQL │
│ 🗄️ Database (Docker) │
│ trading_bot_db │
│ │
└─────────────────────────────────────┘
```
## 🎨 Features
Dashboard akan menampilkan:
- ⏰ Real-time XAUUSD price
- 💰 Account balance & equity
- 📈 Price history chart
- 🎯 Trading signals (SMC + ML)
- 🌊 Market regime
- ⚠️ Risk status
- 📋 Open positions
- 📝 Activity logs
## 💡 Tips
1. **Auto-start Database:**
Tambahkan Docker Desktop ke Windows startup
2. **Keep API Running:**
Minimize command windows, jangan close
3. **Monitor Logs:**
Lihat output di command windows untuk debug
4. **Quick Restart:**
Close windows dan run `start-all.bat` lagi
## 📝 Files
```
start-all.bat # Start API + Dashboard
start-api.bat # Start API only
start-dashboard.bat # Start Dashboard only
```
---
**Super Simple!** Tinggal double-click `start-all.bat` 🎉
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+919
View File
@@ -0,0 +1,919 @@
"""
Backtest #28 — Smart Breakeven + Loss Reduction
=================================================
Base: #24B (19B+20B+22D) — 739 trades, 80.4% WR, $2,235, Sharpe 2.87
Problem: 31.9% of exits are breakeven trades that went profitable then
reversed back to entry. These are dead weight (~$0 profit each).
Also: 10.4% early_cut + 2.7% max_loss = preventable losses.
Configs:
A: Smart BE lock small profit at entry + 0.3x ATR (not exact entry + $2)
B: Smart BE lock at entry + 0.5x ATR (more aggressive profit lock)
C: First-candle adverse exit if bar 1 goes against >0.5x ATR, cut immediately
D: A+C combined (smart BE + first-candle exit)
E: B+C combined (aggressive BE + first-candle exit)
Usage:
python backtests/backtest_28_smart_breakeven.py
"""
import polars as pl
import pandas as pd
import numpy as np
from datetime import datetime, timedelta, date
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass, field
from enum import Enum
import sys
import os
from zoneinfo import ZoneInfo
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.mt5_connector import MT5Connector
from src.smc_polars import SMCAnalyzer, SMCSignal
from src.feature_eng import FeatureEngineer
from src.regime_detector import MarketRegimeDetector, MarketRegime
from src.ml_model import TradingModel
from src.config import get_config
from src.dynamic_confidence import DynamicConfidenceManager, create_dynamic_confidence, MarketQuality
from loguru import logger
logger.remove()
logger.add(sys.stderr, level="WARNING")
WIB = ZoneInfo("Asia/Jakarta")
# ─── Enums & Dataclasses ──────────────────────────────────────
class TradeResult(Enum):
WIN = "WIN"
LOSS = "LOSS"
BREAKEVEN = "BREAKEVEN"
class ExitReason(Enum):
TAKE_PROFIT = "take_profit"
SMART_TP = "smart_tp"
PEAK_PROTECT = "peak_protect"
EARLY_EXIT = "early_exit"
EARLY_CUT = "early_cut"
MAX_LOSS = "max_loss"
STALL = "stall"
TREND_REVERSAL = "trend_reversal"
TIMEOUT = "timeout"
WEEKEND_CLOSE = "weekend_close"
TRAILING_SL = "trailing_sl"
BREAKEVEN_EXIT = "breakeven_exit"
DAILY_LIMIT = "daily_limit"
REGIME_DANGER = "regime_danger"
MARKET_SIGNAL = "market_signal"
FIRST_CANDLE_CUT = "first_candle_cut" # NEW
class TradingMode(Enum):
NORMAL = "normal"
RECOVERY = "recovery"
PROTECTED = "protected"
STOPPED = "stopped"
@dataclass
class SimulatedTrade:
ticket: int
entry_time: datetime
exit_time: datetime
direction: str
entry_price: float
exit_price: float
stop_loss: float
take_profit: float
lot_size: float
profit_usd: float
profit_pips: float
result: TradeResult
exit_reason: ExitReason
smc_confidence: float
regime: str
session: str
signal_reason: str
has_bos: bool = False
has_choch: bool = False
has_fvg: bool = False
has_ob: bool = False
atr_at_entry: float = 0.0
rr_ratio: float = 0.0
trading_mode: str = "normal"
@dataclass
class BacktestStats:
total_trades: int = 0
wins: int = 0
losses: int = 0
total_profit: float = 0.0
total_loss: float = 0.0
max_drawdown: float = 0.0
max_drawdown_usd: float = 0.0
win_rate: float = 0.0
profit_factor: float = 0.0
avg_win: float = 0.0
avg_loss: float = 0.0
avg_trade: float = 0.0
expectancy: float = 0.0
sharpe_ratio: float = 0.0
trades: List[SimulatedTrade] = field(default_factory=list)
equity_curve: List[float] = field(default_factory=list)
avoided_signals: int = 0
daily_limit_stops: int = 0
recovery_mode_trades: int = 0
session_blocked: int = 0
first_candle_cuts: int = 0
# ─── Smart Breakeven Backtest ─────────────────────────────────
class SmartBreakevenBacktest:
"""#24B base + smart breakeven and first-candle adverse exit."""
def __init__(
self,
capital: float = 5000.0,
max_daily_loss_percent: float = 5.0,
max_loss_per_trade_percent: float = 1.0,
base_lot_size: float = 0.01,
max_lot_size: float = 0.02,
recovery_lot_size: float = 0.01,
trend_reversal_threshold: float = 0.75,
max_concurrent_positions: int = 2,
min_profit_to_protect: float = 5.0,
max_drawdown_from_peak: float = 50.0,
trade_cooldown_bars: int = 10,
trend_reversal_mult: float = 0.6,
# #24B base
skip_tokyo_london: bool = True,
early_cut_momentum: float = -50.0,
early_cut_loss_pct: float = 30.0,
be_mult: float = 2.0,
trail_start_mult: float = 4.0,
trail_step_mult: float = 3.0,
# ═══ #28 SMART BREAKEVEN PARAMS ═══
be_profit_lock_atr_mult: float = 0.0, # Lock profit at entry + X * ATR (0 = use $2 like #24B)
first_candle_cut_atr_mult: float = 0.0, # Cut if bar 1 adverse > X * ATR (0 = disabled)
):
self.capital = capital
self.max_daily_loss_usd = capital * (max_daily_loss_percent / 100)
self.max_loss_per_trade = capital * (max_loss_per_trade_percent / 100)
self.base_lot_size = base_lot_size
self.max_lot_size = max_lot_size
self.recovery_lot_size = recovery_lot_size
self.trend_reversal_threshold = trend_reversal_threshold
self.max_concurrent_positions = max_concurrent_positions
self.min_profit_to_protect = min_profit_to_protect
self.max_drawdown_from_peak = max_drawdown_from_peak
self.trade_cooldown_bars = trade_cooldown_bars
self.trend_reversal_mult = trend_reversal_mult
self.skip_tokyo_london = skip_tokyo_london
self.early_cut_momentum = early_cut_momentum
self.early_cut_loss_pct = early_cut_loss_pct
self.be_mult = be_mult
self.trail_start_mult = trail_start_mult
self.trail_step_mult = trail_step_mult
# #28 params
self.be_profit_lock_atr_mult = be_profit_lock_atr_mult
self.first_candle_cut_atr_mult = first_candle_cut_atr_mult
config = get_config()
self.smc = SMCAnalyzer(swing_length=config.smc.swing_length, ob_lookback=config.smc.ob_lookback)
self.features = FeatureEngineer()
self.dynamic_confidence = create_dynamic_confidence()
self.ml_model = TradingModel(model_path="models/xgboost_model.pkl")
try:
self.ml_model.load()
print(" ML model loaded (for exit evaluation)")
except Exception:
print(" [WARN] ML model not loaded")
self.regime_detector = MarketRegimeDetector(model_path="models/hmm_regime.pkl")
try:
self.regime_detector.load()
except Exception:
print(" [WARN] HMM model not loaded")
self._ticket_counter = 2280000
def _get_session_from_time(self, dt):
if dt.tzinfo is None:
dt = dt.replace(tzinfo=ZoneInfo("UTC"))
wib_time = dt.astimezone(WIB)
hour = wib_time.hour
if 6 <= hour < 15:
return "Sydney-Tokyo", True, 0.5
elif 15 <= hour < 16:
if self.skip_tokyo_london:
return "Tokyo-London Overlap", False, 0.0
return "Tokyo-London Overlap", True, 0.75
elif 16 <= hour < 19:
return "London Early", True, 0.8
elif 19 <= hour < 24:
return "London-NY Overlap (Golden)", True, 1.0
elif 0 <= hour < 4:
return "NY Session", True, 0.9
else:
return "Off Hours", False, 0.0
def _hours_to_golden(self, dt):
if dt.tzinfo is None:
dt = dt.replace(tzinfo=ZoneInfo("UTC"))
wib = dt.astimezone(WIB)
if 19 <= wib.hour < 24:
return 0
target = wib.replace(hour=19, minute=0, second=0, microsecond=0)
if wib.hour >= 19:
target += timedelta(days=1)
return max(0, (target - wib).total_seconds() / 3600)
def _is_near_weekend_close(self, dt):
if dt.tzinfo is None:
dt = dt.replace(tzinfo=ZoneInfo("UTC"))
wib = dt.astimezone(WIB)
return wib.weekday() == 5 and wib.hour >= 4 and wib.minute >= 30
def _calculate_lot_size(self, confidence, regime, trading_mode, session_mult):
if trading_mode == TradingMode.STOPPED:
return 0
lot = self.base_lot_size
if trading_mode in (TradingMode.RECOVERY, TradingMode.PROTECTED):
lot = self.recovery_lot_size
else:
if confidence >= 0.65:
lot = self.max_lot_size
elif confidence >= 0.55:
lot = self.base_lot_size
else:
lot = self.recovery_lot_size
if regime.lower() in ["high_volatility", "crisis"]:
lot = self.recovery_lot_size
lot = max(0.01, lot * session_mult)
return round(lot, 2)
def _simulate_trade_exit(
self, df, entry_idx, direction, entry_price, take_profit, stop_loss,
lot_size, daily_loss_so_far, feature_cols, max_bars=100,
):
pip_value = 10
highs = df["high"].to_list()
lows = df["low"].to_list()
closes = df["close"].to_list()
times = df["time"].to_list()
atr = 12.0
if "atr" in df.columns:
atr_list = df["atr"].to_list()
if entry_idx < len(atr_list) and atr_list[entry_idx] is not None:
atr = atr_list[entry_idx]
adaptive_breakeven_pips = atr * self.be_mult
adaptive_trail_start_pips = atr * self.trail_start_mult
adaptive_trail_step_pips = atr * self.trail_step_mult
reversal_momentum_threshold = atr * self.trend_reversal_mult
min_loss_for_reversal_exit = atr * 0.8
# ═══ #28: Smart breakeven profit lock ═══
# Instead of entry + $2, lock at entry + (ATR * be_profit_lock_atr_mult)
if self.be_profit_lock_atr_mult > 0:
be_lock_distance = atr * self.be_profit_lock_atr_mult # in price terms
else:
be_lock_distance = 2.0 # Original $2 buffer
# ═══ #28: First-candle adverse threshold ═══
first_candle_adverse_threshold = 0.0
first_candle_cut_triggered = False
if self.first_candle_cut_atr_mult > 0:
first_candle_adverse_threshold = atr * self.first_candle_cut_atr_mult
profit_history = []
peak_profit = 0.0
stall_count = 0
reversal_warnings = 0
current_sl = stop_loss
breakeven_moved = False
if direction == "BUY":
target_tp_profit = (take_profit - entry_price) / 0.1 * pip_value * lot_size
else:
target_tp_profit = (entry_price - take_profit) / 0.1 * pip_value * lot_size
cached_ml_signal = ""
cached_ml_confidence = 0.5
for i in range(entry_idx + 1, min(entry_idx + max_bars, len(df))):
high = highs[i]
low = lows[i]
close = closes[i]
current_time = times[i]
if direction == "BUY":
current_pips = (close - entry_price) / 0.1
pip_profit_from_entry = current_pips
else:
current_pips = (entry_price - close) / 0.1
pip_profit_from_entry = current_pips
current_profit = current_pips * pip_value * lot_size
profit_history.append(current_profit)
if current_profit > peak_profit:
peak_profit = current_profit
bars_since_entry = i - entry_idx
# ═══ #28: FIRST-CANDLE ADVERSE EXIT ═══
if bars_since_entry == 1 and first_candle_adverse_threshold > 0:
if direction == "BUY":
adverse_move = entry_price - low # How far price went against us
else:
adverse_move = high - entry_price
if adverse_move > first_candle_adverse_threshold:
first_candle_cut_triggered = True
pips = current_pips
return current_profit, pips, ExitReason.FIRST_CANDLE_CUT, i, close
if bars_since_entry % 4 == 0 and self.ml_model.fitted:
try:
df_slice = df.head(i + 1)
ml_pred = self.ml_model.predict(df_slice, feature_cols)
cached_ml_signal = ml_pred.signal
cached_ml_confidence = ml_pred.confidence
except Exception:
pass
momentum = 0.0
if len(profit_history) >= 3:
recent = profit_history[-5:] if len(profit_history) >= 5 else profit_history
profit_change = recent[-1] - recent[0]
momentum = max(-100, min(100, (profit_change / 10) * 50))
profit_growing = momentum > 0
# A.0 TP hit
if direction == "BUY" and high >= take_profit:
pips = (take_profit - entry_price) / 0.1
return pips * pip_value * lot_size, pips, ExitReason.TAKE_PROFIT, i, take_profit
elif direction == "SELL" and low <= take_profit:
pips = (entry_price - take_profit) / 0.1
return pips * pip_value * lot_size, pips, ExitReason.TAKE_PROFIT, i, take_profit
# A.0b Trailing SL hit
if breakeven_moved and current_sl > 0:
if direction == "BUY" and low <= current_sl:
pips = (current_sl - entry_price) / 0.1
reason = ExitReason.TRAILING_SL if pip_profit_from_entry >= adaptive_trail_start_pips else ExitReason.BREAKEVEN_EXIT
return pips * pip_value * lot_size, pips, reason, i, current_sl
elif direction == "SELL" and high >= current_sl:
pips = (entry_price - current_sl) / 0.1
reason = ExitReason.TRAILING_SL if pip_profit_from_entry >= adaptive_trail_start_pips else ExitReason.BREAKEVEN_EXIT
return pips * pip_value * lot_size, pips, reason, i, current_sl
# A.1 Breakeven (#28: SMART — lock profit at entry + ATR*mult)
if pip_profit_from_entry >= adaptive_breakeven_pips and not breakeven_moved:
if direction == "BUY":
current_sl = entry_price + be_lock_distance
else:
current_sl = entry_price - be_lock_distance
breakeven_moved = True
# A.2 Trailing SL
if pip_profit_from_entry >= adaptive_trail_start_pips:
trail_distance = adaptive_trail_step_pips * 0.1
if direction == "BUY":
new_trail_sl = close - trail_distance
if new_trail_sl > current_sl:
current_sl = new_trail_sl
else:
new_trail_sl = close + trail_distance
if current_sl == 0 or new_trail_sl < current_sl:
current_sl = new_trail_sl
# A.3 Peak protect
if peak_profit > self.min_profit_to_protect:
drawdown_pct = ((peak_profit - current_profit) / peak_profit) * 100 if peak_profit > 0 else 0
if drawdown_pct > self.max_drawdown_from_peak:
return current_profit, current_pips, ExitReason.PEAK_PROTECT, i, close
# A.4 Market analysis
if bars_since_entry % 5 == 0 and bars_since_entry >= 5 and i >= 20:
ma_fast = np.mean(closes[i-4:i+1])
ma_slow = np.mean(closes[i-19:i+1])
trend = "BULLISH" if ma_fast > ma_slow * 1.001 else ("BEARISH" if ma_fast < ma_slow * 0.999 else "NEUTRAL")
roc = (closes[i] / closes[max(0,i-4)] - 1) * 100
mom_dir = "BULLISH" if roc > 0.3 else ("BEARISH" if roc < -0.3 else "NEUTRAL")
rsi_val = None
if "rsi" in df.columns:
rsi_list = df["rsi"].to_list()
if i < len(rsi_list):
rsi_val = rsi_list[i]
urgency = 0
should_exit = False
if cached_ml_confidence > 0.75:
if (direction == "BUY" and cached_ml_signal == "SELL") or (direction == "SELL" and cached_ml_signal == "BUY"):
should_exit = True; urgency += 2
if rsi_val:
if (rsi_val > 75 and direction == "BUY") or (rsi_val < 25 and direction == "SELL"):
should_exit = True; urgency += 2
if (direction == "BUY" and trend == "BEARISH" and mom_dir == "BEARISH") or \
(direction == "SELL" and trend == "BULLISH" and mom_dir == "BULLISH"):
should_exit = True; urgency += 3
if should_exit and current_profit > self.min_profit_to_protect / 2:
return current_profit, current_pips, ExitReason.MARKET_SIGNAL, i, close
if urgency >= 7 and current_profit > 0:
return current_profit, current_pips, ExitReason.MARKET_SIGNAL, i, close
# A.5 Weekend close
if self._is_near_weekend_close(current_time):
if current_profit > 0 or current_profit > -10:
return current_profit, current_pips, ExitReason.WEEKEND_CLOSE, i, close
# B.1 Smart TP
if current_profit >= 15:
if current_profit >= 40:
return current_profit, current_pips, ExitReason.SMART_TP, i, close
if current_profit >= 25 and momentum < -30:
return current_profit, current_pips, ExitReason.SMART_TP, i, close
if peak_profit > 30 and current_profit < peak_profit * 0.6:
return current_profit, current_pips, ExitReason.PEAK_PROTECT, i, close
if current_profit >= 20:
progress = (current_profit / target_tp_profit) * 100 if target_tp_profit > 0 else 0
progress_score = min(40, max(0, progress * 0.4))
momentum_score = ((momentum + 100) / 200) * 30
time_penalty = min(10, bars_since_entry / 4 * 2)
tp_probability = progress_score + momentum_score + 10 - time_penalty
if tp_probability < 25:
return current_profit, current_pips, ExitReason.SMART_TP, i, close
# B.2 Smart Early Exit
if 5 <= current_profit < 15:
if momentum < -50 and cached_ml_confidence >= 0.65:
is_reversal = (direction == "BUY" and cached_ml_signal == "SELL") or (direction == "SELL" and cached_ml_signal == "BUY")
if is_reversal:
return current_profit, current_pips, ExitReason.EARLY_EXIT, i, close
# B.3 Early cut
if current_profit < 0:
loss_percent_of_max = abs(current_profit) / self.max_loss_per_trade * 100
if momentum < self.early_cut_momentum and loss_percent_of_max >= self.early_cut_loss_pct:
return current_profit, current_pips, ExitReason.EARLY_CUT, i, close
# B.4 Trend Reversal
is_ml_reversal = False
if (direction == "BUY" and cached_ml_signal == "SELL" and cached_ml_confidence >= self.trend_reversal_threshold) or \
(direction == "SELL" and cached_ml_signal == "BUY" and cached_ml_confidence >= self.trend_reversal_threshold):
is_ml_reversal = True
reversal_warnings += 1
loss_moderate = abs(current_profit) > (self.max_loss_per_trade * 0.4)
if is_ml_reversal and current_profit < -8 and loss_moderate:
return current_profit, current_pips, ExitReason.TREND_REVERSAL, i, close
if reversal_warnings >= 3 and current_profit < -10:
return current_profit, current_pips, ExitReason.TREND_REVERSAL, i, close
# B.5 Max loss
if current_profit <= -(self.max_loss_per_trade * 0.50):
htg = self._hours_to_golden(current_time)
if htg <= 1 and htg > 0 and momentum > -40:
pass
else:
return current_profit, current_pips, ExitReason.MAX_LOSS, i, close
# B.6 Stall
if len(profit_history) >= 10:
recent_range = max(profit_history[-10:]) - min(profit_history[-10:])
if recent_range < 3 and current_profit < -15:
stall_count += 1
if stall_count >= 5:
return current_profit, current_pips, ExitReason.STALL, i, close
# B.7 Daily loss limit
potential_daily_loss = daily_loss_so_far + abs(min(0, current_profit))
if potential_daily_loss >= self.max_daily_loss_usd:
return current_profit, current_pips, ExitReason.DAILY_LIMIT, i, close
# C) Time-based
if bars_since_entry >= 16 and current_profit < 5 and not profit_growing:
if current_profit >= 0 or current_profit > -15:
return current_profit, current_pips, ExitReason.TIMEOUT, i, close
if bars_since_entry >= 24 and (current_profit < 10 or not profit_growing):
return current_profit, current_pips, ExitReason.TIMEOUT, i, close
if bars_since_entry >= 32:
return current_profit, current_pips, ExitReason.TIMEOUT, i, close
# C.2 ATR trend reversal
if bars_since_entry > 10:
recent_closes = closes[i-5:i+1]
mom = recent_closes[-1] - recent_closes[0]
if (direction == "BUY" and mom < -reversal_momentum_threshold) or \
(direction == "SELL" and mom > reversal_momentum_threshold):
if current_profit < -min_loss_for_reversal_exit:
return current_profit, current_pips, ExitReason.TREND_REVERSAL, i, close
final_idx = min(entry_idx + max_bars - 1, len(df) - 1)
final_price = closes[final_idx]
pips = ((final_price - entry_price) if direction == "BUY" else (entry_price - final_price)) / 0.1
return pips * pip_value * lot_size, pips, ExitReason.TIMEOUT, final_idx, final_price
# ── Main run (identical to #24B except passes first_candle_cuts) ──
def run(self, df, start_date=None, end_date=None, initial_capital=5000.0):
stats = BacktestStats()
capital = initial_capital
peak_capital = initial_capital
stats.equity_curve.append(capital)
daily_loss = 0.0
daily_profit = 0.0
daily_trades = 0
consecutive_losses = 0
trading_mode = TradingMode.NORMAL
current_date = None
feature_cols = []
if self.ml_model.fitted and self.ml_model.feature_names:
feature_cols = [f for f in self.ml_model.feature_names if f in df.columns]
times = df["time"].to_list()
start_idx = next((i for i, t in enumerate(times) if t >= start_date), 100) if start_date else 100
end_idx = next((i for i, t in enumerate(times) if t > end_date), len(df) - 100) if end_date else len(df) - 100
last_trade_idx = -self.trade_cooldown_bars * 2
print(f" #28 BE lock ATR mult: {self.be_profit_lock_atr_mult}, First-candle cut ATR mult: {self.first_candle_cut_atr_mult}")
print(f" Date range: {times[start_idx]} to {times[end_idx - 1]}")
print(f" Total bars: {end_idx - start_idx}")
for i in range(start_idx, end_idx):
if i - last_trade_idx < self.trade_cooldown_bars:
continue
current_time = times[i]
trade_date = current_time.date() if hasattr(current_time, 'date') else current_time
if current_date is None or trade_date != current_date:
daily_loss = 0.0
daily_profit = 0.0
daily_trades = 0
current_date = trade_date
if consecutive_losses < 2:
trading_mode = TradingMode.NORMAL
if trading_mode == TradingMode.STOPPED:
continue
session_name, can_trade, lot_mult = self._get_session_from_time(current_time)
if not can_trade:
if session_name == "Tokyo-London Overlap":
stats.session_blocked += 1
continue
if hasattr(current_time, 'weekday') and current_time.weekday() >= 5:
continue
df_slice = df.head(i + 1)
regime = "normal"
try:
if self.regime_detector.fitted:
regime_state = self.regime_detector.get_current_state(df_slice)
if regime_state:
regime = regime_state.regime.value
if regime_state.regime == MarketRegime.CRISIS:
continue
if regime_state.recommendation == "SLEEP":
continue
except Exception:
pass
try:
ml_signal = ""
ml_confidence = 0.5
if self.ml_model.fitted and feature_cols:
ml_pred = self.ml_model.predict(df_slice, feature_cols)
ml_signal = ml_pred.signal
ml_confidence = ml_pred.confidence
market_analysis = self.dynamic_confidence.analyze_market(
session=session_name, regime=regime, volatility="medium",
trend_direction=regime, has_smc_signal=True,
ml_signal=ml_signal, ml_confidence=ml_confidence,
)
if market_analysis.quality == MarketQuality.AVOID:
stats.avoided_signals += 1
continue
except Exception:
pass
try:
smc_signal = self.smc.generate_signal(df_slice)
except Exception:
continue
if smc_signal is None:
continue
recent_df = df_slice.tail(10)
recent_bos = recent_df["bos"].to_list() if "bos" in df_slice.columns else []
recent_choch = recent_df["choch"].to_list() if "choch" in df_slice.columns else []
recent_fvg_bull = recent_df["is_fvg_bull"].to_list() if "is_fvg_bull" in df_slice.columns else []
recent_fvg_bear = recent_df["is_fvg_bear"].to_list() if "is_fvg_bear" in df_slice.columns else []
recent_obs = recent_df["ob"].to_list() if "ob" in df_slice.columns else []
has_bos = 1 in recent_bos or -1 in recent_bos
has_choch = 1 in recent_choch or -1 in recent_choch
has_fvg = any(recent_fvg_bull) or any(recent_fvg_bear)
has_ob = 1 in recent_obs or -1 in recent_obs
atr_at_entry = 12.0
if "atr" in df_slice.columns:
atr_val = df_slice.tail(1)["atr"].item()
if atr_val is not None and atr_val > 0:
atr_at_entry = atr_val
confidence = smc_signal.confidence
ml_agrees = (smc_signal.signal_type == "BUY" and ml_signal == "BUY") or \
(smc_signal.signal_type == "SELL" and ml_signal == "SELL")
if ml_agrees:
confidence = (smc_signal.confidence + ml_confidence) / 2
if regime == "high_volatility":
confidence *= 0.9
lot_size = self._calculate_lot_size(confidence, regime, trading_mode, lot_mult)
if lot_size <= 0:
continue
if trading_mode == TradingMode.RECOVERY:
stats.recovery_mode_trades += 1
entry_price = smc_signal.entry_price
take_profit_price = smc_signal.take_profit
stop_loss_price = smc_signal.stop_loss
risk = abs(entry_price - stop_loss_price)
rr = abs(take_profit_price - entry_price) / risk if risk > 0 else 0
profit, pips, exit_reason, exit_idx, exit_price = self._simulate_trade_exit(
df=df, entry_idx=i, direction=smc_signal.signal_type,
entry_price=entry_price, take_profit=take_profit_price,
stop_loss=stop_loss_price, lot_size=lot_size,
daily_loss_so_far=daily_loss, feature_cols=feature_cols,
)
if exit_reason == ExitReason.FIRST_CANDLE_CUT:
stats.first_candle_cuts += 1
self._ticket_counter += 1
result = TradeResult.WIN if profit > 0 else (TradeResult.LOSS if profit < 0 else TradeResult.BREAKEVEN)
trade = SimulatedTrade(
ticket=self._ticket_counter,
entry_time=current_time,
exit_time=times[exit_idx] if exit_idx < len(times) else times[-1],
direction=smc_signal.signal_type,
entry_price=entry_price, exit_price=exit_price,
stop_loss=stop_loss_price, take_profit=take_profit_price,
lot_size=lot_size, profit_usd=profit, profit_pips=pips,
result=result, exit_reason=exit_reason,
smc_confidence=confidence, regime=regime,
session=session_name, signal_reason=smc_signal.reason,
has_bos=has_bos, has_choch=has_choch,
has_fvg=has_fvg, has_ob=has_ob,
atr_at_entry=atr_at_entry, rr_ratio=rr,
trading_mode=trading_mode.value,
)
stats.trades.append(trade)
stats.total_trades += 1
daily_trades += 1
capital += profit
if profit > 0:
stats.wins += 1
stats.total_profit += profit
daily_profit += profit
consecutive_losses = 0
if trading_mode == TradingMode.RECOVERY:
trading_mode = TradingMode.NORMAL
else:
stats.losses += 1
stats.total_loss += abs(profit)
daily_loss += abs(profit)
consecutive_losses += 1
if daily_loss >= self.max_daily_loss_usd:
trading_mode = TradingMode.STOPPED
stats.daily_limit_stops += 1
elif consecutive_losses >= 3 or daily_loss >= self.max_daily_loss_usd * 0.6:
trading_mode = TradingMode.PROTECTED
elif consecutive_losses >= 2:
trading_mode = TradingMode.RECOVERY
if capital > peak_capital:
peak_capital = capital
drawdown_pct = (peak_capital - capital) / peak_capital * 100
drawdown_usd = peak_capital - capital
if drawdown_pct > stats.max_drawdown:
stats.max_drawdown = drawdown_pct
stats.max_drawdown_usd = drawdown_usd
stats.equity_curve.append(capital)
last_trade_idx = exit_idx
if stats.total_trades % 100 == 0:
print(f" {stats.total_trades} trades processed...")
if stats.total_trades > 0:
stats.win_rate = stats.wins / stats.total_trades * 100
stats.avg_win = stats.total_profit / stats.wins if stats.wins > 0 else 0
stats.avg_loss = stats.total_loss / stats.losses if stats.losses > 0 else 0
stats.avg_trade = (stats.total_profit - stats.total_loss) / stats.total_trades
stats.profit_factor = stats.total_profit / stats.total_loss if stats.total_loss > 0 else float("inf")
win_prob = stats.wins / stats.total_trades
loss_prob = stats.losses / stats.total_trades
stats.expectancy = (win_prob * stats.avg_win) - (loss_prob * stats.avg_loss)
returns = [t.profit_usd for t in stats.trades]
if len(returns) > 1:
avg_return = np.mean(returns)
std_return = np.std(returns)
stats.sharpe_ratio = (avg_return / std_return) * np.sqrt(252) if std_return > 0 else 0
return stats
# ─── Main ──────────────────────────────────────────────────────
def main():
print("=" * 70)
print("XAUBOT AI — #28 Smart Breakeven + Loss Reduction")
print("Base: #24B | Modified: Smart BE profit lock + first-candle exit")
print("=" * 70)
config = get_config()
mt5 = MT5Connector(
login=config.mt5_login, password=config.mt5_password,
server=config.mt5_server, path=config.mt5_path,
)
mt5.connect()
print(f"\nConnected to MT5")
print("Fetching XAUUSD M15 historical data...")
df = mt5.get_market_data(symbol="XAUUSD", timeframe="M15", count=50000)
if len(df) == 0:
print("ERROR: No data")
mt5.disconnect()
return
print(f" Received {len(df)} bars")
times = df["time"].to_list()
print(f" Data range: {times[0]} to {times[-1]}")
end_date = datetime.now()
start_date = datetime(2025, 8, 1)
data_start = times[0]
if hasattr(data_start, 'replace') and data_start.tzinfo:
start_date = start_date.replace(tzinfo=data_start.tzinfo)
end_date = end_date.replace(tzinfo=data_start.tzinfo)
if data_start > start_date:
start_date = data_start + timedelta(days=5)
print(f"\n Backtest period: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")
print("\nCalculating indicators...")
features = FeatureEngineer()
smc = SMCAnalyzer(swing_length=config.smc.swing_length, ob_lookback=config.smc.ob_lookback)
df = features.calculate_all(df, include_ml_features=True)
df = smc.calculate_all(df)
regime_detector = MarketRegimeDetector(model_path="models/hmm_regime.pkl")
try:
regime_detector.load()
df = regime_detector.predict(df)
print(" HMM regime loaded")
except Exception:
print(" [WARN] HMM not available")
print(" Indicators calculated")
baseline_24b_pnl = 2235.0
# ═══ CONFIGS ═══
configs = [
# (name, be_profit_lock_atr_mult, first_candle_cut_atr_mult)
("A: Smart BE 0.3x ATR", 0.3, 0.0), # Lock small profit
("B: Smart BE 0.5x ATR", 0.5, 0.0), # Lock more profit
("C: First-candle 0.5x ATR", 0.0, 0.5), # Cut if bar 1 adverse
("D: A+C (0.3x BE + FC)", 0.3, 0.5), # Combined
("E: B+C (0.5x BE + FC)", 0.5, 0.5), # Combined aggressive
]
all_results = []
for cfg_name, be_lock, fc_cut in configs:
print(f"\n{'=' * 60}")
print(f" Config: {cfg_name}")
bt = SmartBreakevenBacktest(
be_profit_lock_atr_mult=be_lock,
first_candle_cut_atr_mult=fc_cut,
)
stats = bt.run(df=df, start_date=start_date, end_date=end_date, initial_capital=5000.0)
net_pnl = stats.total_profit - stats.total_loss
diff = net_pnl - baseline_24b_pnl
# Count BE exits and their avg profit
be_exits = [t for t in stats.trades if t.exit_reason == ExitReason.BREAKEVEN_EXIT]
be_avg_profit = np.mean([t.profit_usd for t in be_exits]) if be_exits else 0
be_wins = sum(1 for t in be_exits if t.profit_usd > 0)
buy_trades = [t for t in stats.trades if t.direction == "BUY"]
sell_trades = [t for t in stats.trades if t.direction == "SELL"]
buy_wins = sum(1 for t in buy_trades if t.result == TradeResult.WIN)
sell_wins = sum(1 for t in sell_trades if t.result == TradeResult.WIN)
buy_wr = buy_wins / len(buy_trades) * 100 if buy_trades else 0
sell_wr = sell_wins / len(sell_trades) * 100 if sell_trades else 0
print(f"\n [{cfg_name}] Results:")
print(f" Trades: {stats.total_trades} | WR: {stats.win_rate:.1f}%")
print(f" Net PnL: ${net_pnl:,.2f} | PF: {stats.profit_factor:.2f}")
print(f" Max DD: {stats.max_drawdown:.1f}% | Sharpe: {stats.sharpe_ratio:.2f}")
print(f" BE exits: {len(be_exits)} (avg ${be_avg_profit:.2f}, {be_wins} profitable)")
print(f" First-candle cuts: {stats.first_candle_cuts}")
print(f" BUY: {len(buy_trades)}, {buy_wr:.1f}% WR | SELL: {len(sell_trades)}, {sell_wr:.1f}% WR")
print(f" vs #24B: ${diff:+,.2f}")
all_results.append((cfg_name, stats, net_pnl, diff, len(be_exits), be_avg_profit, be_wins, stats.first_candle_cuts))
# ═══ FINAL SUMMARY ═══
print(f"\n{'=' * 70}")
print("#28 SMART BREAKEVEN — ALL CONFIGURATIONS")
print("=" * 70)
print(f"\n {'Config':<25} {'Trades':>6} {'WR':>6} {'Net PnL':>10} {'DD':>6} {'Sharpe':>7} {'PF':>5} {'BE#':>4} {'BE$':>6} {'FC#':>4} {'vs #24B':>10}")
print(f" {'-' * 100}")
print(f" {'#24B (base)':<25} {'739':>6} {'80.4%':>6} {'$2,235':>10} {'3.4%':>6} {'2.87':>7} {'1.77':>5} {'236':>4} {'$0.0':>6} {'':>4} {'':>10}")
for cfg_name, stats, net_pnl, diff, be_n, be_avg, be_w, fc_n in all_results:
print(f" {cfg_name:<25} {stats.total_trades:>6} {stats.win_rate:>5.1f}% ${net_pnl:>9,.2f} {stats.max_drawdown:>5.1f}% {stats.sharpe_ratio:>7.2f} {stats.profit_factor:>5.2f} {be_n:>4} ${be_avg:>5.1f} {fc_n:>4} ${diff:>+9,.2f}")
best_pnl = -999999
best_name = ""
best_stats = None
for entry in all_results:
if entry[2] > best_pnl:
best_pnl = entry[2]
best_name = entry[0]
best_stats = entry[1]
print(f"\n Best config: {best_name}")
# Exit reasons for best
print(f"\n Exit Reasons (best config):")
exit_counts = {}
for t in best_stats.trades:
r = t.exit_reason.value
exit_counts[r] = exit_counts.get(r, 0) + 1
for reason, count in sorted(exit_counts.items(), key=lambda x: -x[1]):
pct = count / best_stats.total_trades * 100 if best_stats.total_trades > 0 else 0
print(f" {reason:20s}: {count} ({pct:.1f}%)")
# Save
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "28_smart_breakeven_results")
os.makedirs(output_dir, exist_ok=True)
log_path = os.path.join(output_dir, f"smart_be_{timestamp}.log")
with open(log_path, "w") as f:
f.write(f"#28 Smart Breakeven Results\n")
f.write(f"Generated: {datetime.now()}\n")
f.write(f"Base: #24B (739 trades, 80.4% WR, $2,235)\n\n")
for cfg_name, stats, net_pnl, diff, be_n, be_avg, be_w, fc_n in all_results:
f.write(f" {cfg_name}: {stats.total_trades} trades, {stats.win_rate:.1f}% WR, "
f"${net_pnl:,.2f}, DD: {stats.max_drawdown:.1f}%, "
f"Sharpe: {stats.sharpe_ratio:.2f}, PF: {stats.profit_factor:.2f}, "
f"BE: {be_n} (avg ${be_avg:.2f}, {be_w} wins), FC cuts: {fc_n}, "
f"vs #24B: ${diff:+,.2f}\n")
f.write(f"\nBest: {best_name}\n")
print(f" Log saved: {log_path}")
try:
from backtests.backtest_01_smc_only import generate_xlsx_report as gen_xlsx
xlsx_path = os.path.join(output_dir, f"smart_be_{timestamp}.xlsx")
gen_xlsx(best_stats, xlsx_path, start_date, end_date)
print(f"\n Report saved: {xlsx_path}")
except Exception as e:
print(f" [WARN] XLSX: {e}")
mt5.disconnect()
print(f"\n{'=' * 70}")
print(f"Output: {output_dir}")
print(f" Log: {os.path.basename(log_path)}")
print("=" * 70)
print("Backtest complete!")
if __name__ == "__main__":
main()
+953
View File
@@ -0,0 +1,953 @@
"""
Backtest #29 — Confluence Scoring
==================================
Base: #28B (Smart BE 0.5x ATR) — 741 trades, 79.8% WR, $2,464, Sharpe 3.23
Idea: Require minimum number of SMC confirmations (BOS, CHoCH, FVG, OB)
before entering. Currently any single SMC signal triggers entry. By requiring
more confirmations, we filter weak signals and keep only high-quality setups.
Configs:
A: Min 2 SMC elements (any 2 of BOS/CHoCH/FVG/OB)
B: Min confidence >= 0.55 (threshold filter)
C: Min confidence >= 0.60
D: A+B combined (2 elements + conf >= 0.55)
E: Min 3 SMC elements (BOS/CHoCH + FVG or OB)
Usage:
python backtests/backtest_29_confluence_scoring.py
"""
import polars as pl
import pandas as pd
import numpy as np
from datetime import datetime, timedelta, date
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass, field
from enum import Enum
import sys
import os
from zoneinfo import ZoneInfo
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.mt5_connector import MT5Connector
from src.smc_polars import SMCAnalyzer, SMCSignal
from src.feature_eng import FeatureEngineer
from src.regime_detector import MarketRegimeDetector, MarketRegime
from src.ml_model import TradingModel
from src.config import get_config
from src.dynamic_confidence import DynamicConfidenceManager, create_dynamic_confidence, MarketQuality
from loguru import logger
logger.remove()
logger.add(sys.stderr, level="WARNING")
WIB = ZoneInfo("Asia/Jakarta")
# ─── Enums & Dataclasses ──────────────────────────────────────
class TradeResult(Enum):
WIN = "WIN"
LOSS = "LOSS"
BREAKEVEN = "BREAKEVEN"
class ExitReason(Enum):
TAKE_PROFIT = "take_profit"
SMART_TP = "smart_tp"
PEAK_PROTECT = "peak_protect"
EARLY_EXIT = "early_exit"
EARLY_CUT = "early_cut"
MAX_LOSS = "max_loss"
STALL = "stall"
TREND_REVERSAL = "trend_reversal"
TIMEOUT = "timeout"
WEEKEND_CLOSE = "weekend_close"
TRAILING_SL = "trailing_sl"
BREAKEVEN_EXIT = "breakeven_exit"
DAILY_LIMIT = "daily_limit"
REGIME_DANGER = "regime_danger"
MARKET_SIGNAL = "market_signal"
class TradingMode(Enum):
NORMAL = "normal"
RECOVERY = "recovery"
PROTECTED = "protected"
STOPPED = "stopped"
@dataclass
class SimulatedTrade:
ticket: int
entry_time: datetime
exit_time: datetime
direction: str
entry_price: float
exit_price: float
stop_loss: float
take_profit: float
lot_size: float
profit_usd: float
profit_pips: float
result: TradeResult
exit_reason: ExitReason
smc_confidence: float
regime: str
session: str
signal_reason: str
has_bos: bool = False
has_choch: bool = False
has_fvg: bool = False
has_ob: bool = False
atr_at_entry: float = 0.0
rr_ratio: float = 0.0
trading_mode: str = "normal"
smc_element_count: int = 0 # NEW: count of SMC elements
@dataclass
class BacktestStats:
total_trades: int = 0
wins: int = 0
losses: int = 0
total_profit: float = 0.0
total_loss: float = 0.0
max_drawdown: float = 0.0
max_drawdown_usd: float = 0.0
win_rate: float = 0.0
profit_factor: float = 0.0
avg_win: float = 0.0
avg_loss: float = 0.0
avg_trade: float = 0.0
expectancy: float = 0.0
sharpe_ratio: float = 0.0
trades: List[SimulatedTrade] = field(default_factory=list)
equity_curve: List[float] = field(default_factory=list)
avoided_signals: int = 0
daily_limit_stops: int = 0
recovery_mode_trades: int = 0
session_blocked: int = 0
confluence_filtered: int = 0 # NEW
# ─── Confluence Scoring Backtest ─────────────────────────────
class ConfluenceScoringBacktest:
"""#28B base + confluence scoring entry filter."""
def __init__(
self,
capital: float = 5000.0,
max_daily_loss_percent: float = 5.0,
max_loss_per_trade_percent: float = 1.0,
base_lot_size: float = 0.01,
max_lot_size: float = 0.02,
recovery_lot_size: float = 0.01,
trend_reversal_threshold: float = 0.75,
max_concurrent_positions: int = 2,
min_profit_to_protect: float = 5.0,
max_drawdown_from_peak: float = 50.0,
trade_cooldown_bars: int = 10,
trend_reversal_mult: float = 0.6,
# #24B base
skip_tokyo_london: bool = True,
early_cut_momentum: float = -50.0,
early_cut_loss_pct: float = 30.0,
be_mult: float = 2.0,
trail_start_mult: float = 4.0,
trail_step_mult: float = 3.0,
# #28B: Smart breakeven
be_profit_lock_atr_mult: float = 0.5,
# ═══ #29 CONFLUENCE SCORING PARAMS ═══
min_smc_elements: int = 0, # Minimum number of SMC elements (BOS, CHoCH, FVG, OB)
min_confidence: float = 0.0, # Minimum confidence threshold
):
self.capital = capital
self.max_daily_loss_usd = capital * (max_daily_loss_percent / 100)
self.max_loss_per_trade = capital * (max_loss_per_trade_percent / 100)
self.base_lot_size = base_lot_size
self.max_lot_size = max_lot_size
self.recovery_lot_size = recovery_lot_size
self.trend_reversal_threshold = trend_reversal_threshold
self.max_concurrent_positions = max_concurrent_positions
self.min_profit_to_protect = min_profit_to_protect
self.max_drawdown_from_peak = max_drawdown_from_peak
self.trade_cooldown_bars = trade_cooldown_bars
self.trend_reversal_mult = trend_reversal_mult
self.skip_tokyo_london = skip_tokyo_london
self.early_cut_momentum = early_cut_momentum
self.early_cut_loss_pct = early_cut_loss_pct
self.be_mult = be_mult
self.trail_start_mult = trail_start_mult
self.trail_step_mult = trail_step_mult
self.be_profit_lock_atr_mult = be_profit_lock_atr_mult
# #29 params
self.min_smc_elements = min_smc_elements
self.min_confidence = min_confidence
config = get_config()
self.smc = SMCAnalyzer(swing_length=config.smc.swing_length, ob_lookback=config.smc.ob_lookback)
self.features = FeatureEngineer()
self.dynamic_confidence = create_dynamic_confidence()
self.ml_model = TradingModel(model_path="models/xgboost_model.pkl")
try:
self.ml_model.load()
print(" ML model loaded (for exit evaluation)")
except Exception:
print(" [WARN] ML model not loaded")
self.regime_detector = MarketRegimeDetector(model_path="models/hmm_regime.pkl")
try:
self.regime_detector.load()
except Exception:
print(" [WARN] HMM model not loaded")
self._ticket_counter = 2290000
def _get_session_from_time(self, dt):
if dt.tzinfo is None:
dt = dt.replace(tzinfo=ZoneInfo("UTC"))
wib_time = dt.astimezone(WIB)
hour = wib_time.hour
if 6 <= hour < 15:
return "Sydney-Tokyo", True, 0.5
elif 15 <= hour < 16:
if self.skip_tokyo_london:
return "Tokyo-London Overlap", False, 0.0
return "Tokyo-London Overlap", True, 0.75
elif 16 <= hour < 19:
return "London Early", True, 0.8
elif 19 <= hour < 24:
return "London-NY Overlap (Golden)", True, 1.0
elif 0 <= hour < 4:
return "NY Session", True, 0.9
else:
return "Off Hours", False, 0.0
def _hours_to_golden(self, dt):
if dt.tzinfo is None:
dt = dt.replace(tzinfo=ZoneInfo("UTC"))
wib = dt.astimezone(WIB)
if 19 <= wib.hour < 24:
return 0
target = wib.replace(hour=19, minute=0, second=0, microsecond=0)
if wib.hour >= 19:
target += timedelta(days=1)
return max(0, (target - wib).total_seconds() / 3600)
def _is_near_weekend_close(self, dt):
if dt.tzinfo is None:
dt = dt.replace(tzinfo=ZoneInfo("UTC"))
wib = dt.astimezone(WIB)
return wib.weekday() == 5 and wib.hour >= 4 and wib.minute >= 30
def _calculate_lot_size(self, confidence, regime, trading_mode, session_mult):
if trading_mode == TradingMode.STOPPED:
return 0
lot = self.base_lot_size
if trading_mode in (TradingMode.RECOVERY, TradingMode.PROTECTED):
lot = self.recovery_lot_size
else:
if confidence >= 0.65:
lot = self.max_lot_size
elif confidence >= 0.55:
lot = self.base_lot_size
else:
lot = self.recovery_lot_size
if regime.lower() in ["high_volatility", "crisis"]:
lot = self.recovery_lot_size
lot = max(0.01, lot * session_mult)
return round(lot, 2)
def _simulate_trade_exit(
self, df, entry_idx, direction, entry_price, take_profit, stop_loss,
lot_size, daily_loss_so_far, feature_cols, max_bars=100,
):
pip_value = 10
highs = df["high"].to_list()
lows = df["low"].to_list()
closes = df["close"].to_list()
times = df["time"].to_list()
atr = 12.0
if "atr" in df.columns:
atr_list = df["atr"].to_list()
if entry_idx < len(atr_list) and atr_list[entry_idx] is not None:
atr = atr_list[entry_idx]
adaptive_breakeven_pips = atr * self.be_mult
adaptive_trail_start_pips = atr * self.trail_start_mult
adaptive_trail_step_pips = atr * self.trail_step_mult
reversal_momentum_threshold = atr * self.trend_reversal_mult
min_loss_for_reversal_exit = atr * 0.8
# #28B: Smart breakeven profit lock
if self.be_profit_lock_atr_mult > 0:
be_lock_distance = atr * self.be_profit_lock_atr_mult
else:
be_lock_distance = 2.0
profit_history = []
peak_profit = 0.0
stall_count = 0
reversal_warnings = 0
current_sl = stop_loss
breakeven_moved = False
if direction == "BUY":
target_tp_profit = (take_profit - entry_price) / 0.1 * pip_value * lot_size
else:
target_tp_profit = (entry_price - take_profit) / 0.1 * pip_value * lot_size
cached_ml_signal = ""
cached_ml_confidence = 0.5
for i in range(entry_idx + 1, min(entry_idx + max_bars, len(df))):
high = highs[i]
low = lows[i]
close = closes[i]
current_time = times[i]
if direction == "BUY":
current_pips = (close - entry_price) / 0.1
pip_profit_from_entry = current_pips
else:
current_pips = (entry_price - close) / 0.1
pip_profit_from_entry = current_pips
current_profit = current_pips * pip_value * lot_size
profit_history.append(current_profit)
if current_profit > peak_profit:
peak_profit = current_profit
bars_since_entry = i - entry_idx
if bars_since_entry % 4 == 0 and self.ml_model.fitted:
try:
df_slice = df.head(i + 1)
ml_pred = self.ml_model.predict(df_slice, feature_cols)
cached_ml_signal = ml_pred.signal
cached_ml_confidence = ml_pred.confidence
except Exception:
pass
momentum = 0.0
if len(profit_history) >= 3:
recent = profit_history[-5:] if len(profit_history) >= 5 else profit_history
profit_change = recent[-1] - recent[0]
momentum = max(-100, min(100, (profit_change / 10) * 50))
profit_growing = momentum > 0
# A.0 TP hit
if direction == "BUY" and high >= take_profit:
pips = (take_profit - entry_price) / 0.1
return pips * pip_value * lot_size, pips, ExitReason.TAKE_PROFIT, i, take_profit
elif direction == "SELL" and low <= take_profit:
pips = (entry_price - take_profit) / 0.1
return pips * pip_value * lot_size, pips, ExitReason.TAKE_PROFIT, i, take_profit
# A.0b Trailing SL hit
if breakeven_moved and current_sl > 0:
if direction == "BUY" and low <= current_sl:
pips = (current_sl - entry_price) / 0.1
reason = ExitReason.TRAILING_SL if pip_profit_from_entry >= adaptive_trail_start_pips else ExitReason.BREAKEVEN_EXIT
return pips * pip_value * lot_size, pips, reason, i, current_sl
elif direction == "SELL" and high >= current_sl:
pips = (entry_price - current_sl) / 0.1
reason = ExitReason.TRAILING_SL if pip_profit_from_entry >= adaptive_trail_start_pips else ExitReason.BREAKEVEN_EXIT
return pips * pip_value * lot_size, pips, reason, i, current_sl
# A.1 Breakeven (#28B: Smart — lock profit at entry + ATR*0.5)
if pip_profit_from_entry >= adaptive_breakeven_pips and not breakeven_moved:
if direction == "BUY":
current_sl = entry_price + be_lock_distance
else:
current_sl = entry_price - be_lock_distance
breakeven_moved = True
# A.2 Trailing SL
if pip_profit_from_entry >= adaptive_trail_start_pips:
trail_distance = adaptive_trail_step_pips * 0.1
if direction == "BUY":
new_trail_sl = close - trail_distance
if new_trail_sl > current_sl:
current_sl = new_trail_sl
else:
new_trail_sl = close + trail_distance
if current_sl == 0 or new_trail_sl < current_sl:
current_sl = new_trail_sl
# A.3 Peak protect
if peak_profit > self.min_profit_to_protect:
drawdown_pct = ((peak_profit - current_profit) / peak_profit) * 100 if peak_profit > 0 else 0
if drawdown_pct > self.max_drawdown_from_peak:
return current_profit, current_pips, ExitReason.PEAK_PROTECT, i, close
# A.4 Market analysis
if bars_since_entry % 5 == 0 and bars_since_entry >= 5 and i >= 20:
ma_fast = np.mean(closes[i-4:i+1])
ma_slow = np.mean(closes[i-19:i+1])
trend = "BULLISH" if ma_fast > ma_slow * 1.001 else ("BEARISH" if ma_fast < ma_slow * 0.999 else "NEUTRAL")
roc = (closes[i] / closes[max(0,i-4)] - 1) * 100
mom_dir = "BULLISH" if roc > 0.3 else ("BEARISH" if roc < -0.3 else "NEUTRAL")
rsi_val = None
if "rsi" in df.columns:
rsi_list = df["rsi"].to_list()
if i < len(rsi_list):
rsi_val = rsi_list[i]
urgency = 0
should_exit = False
if cached_ml_confidence > 0.75:
if (direction == "BUY" and cached_ml_signal == "SELL") or (direction == "SELL" and cached_ml_signal == "BUY"):
should_exit = True; urgency += 2
if rsi_val:
if (rsi_val > 75 and direction == "BUY") or (rsi_val < 25 and direction == "SELL"):
should_exit = True; urgency += 2
if (direction == "BUY" and trend == "BEARISH" and mom_dir == "BEARISH") or \
(direction == "SELL" and trend == "BULLISH" and mom_dir == "BULLISH"):
should_exit = True; urgency += 3
if should_exit and current_profit > self.min_profit_to_protect / 2:
return current_profit, current_pips, ExitReason.MARKET_SIGNAL, i, close
if urgency >= 7 and current_profit > 0:
return current_profit, current_pips, ExitReason.MARKET_SIGNAL, i, close
# A.5 Weekend close
if self._is_near_weekend_close(current_time):
if current_profit > 0 or current_profit > -10:
return current_profit, current_pips, ExitReason.WEEKEND_CLOSE, i, close
# B.1 Smart TP
if current_profit >= 15:
if current_profit >= 40:
return current_profit, current_pips, ExitReason.SMART_TP, i, close
if current_profit >= 25 and momentum < -30:
return current_profit, current_pips, ExitReason.SMART_TP, i, close
if peak_profit > 30 and current_profit < peak_profit * 0.6:
return current_profit, current_pips, ExitReason.PEAK_PROTECT, i, close
if current_profit >= 20:
progress = (current_profit / target_tp_profit) * 100 if target_tp_profit > 0 else 0
progress_score = min(40, max(0, progress * 0.4))
momentum_score = ((momentum + 100) / 200) * 30
time_penalty = min(10, bars_since_entry / 4 * 2)
tp_probability = progress_score + momentum_score + 10 - time_penalty
if tp_probability < 25:
return current_profit, current_pips, ExitReason.SMART_TP, i, close
# B.2 Smart Early Exit
if 5 <= current_profit < 15:
if momentum < -50 and cached_ml_confidence >= 0.65:
is_reversal = (direction == "BUY" and cached_ml_signal == "SELL") or (direction == "SELL" and cached_ml_signal == "BUY")
if is_reversal:
return current_profit, current_pips, ExitReason.EARLY_EXIT, i, close
# B.3 Early cut
if current_profit < 0:
loss_percent_of_max = abs(current_profit) / self.max_loss_per_trade * 100
if momentum < self.early_cut_momentum and loss_percent_of_max >= self.early_cut_loss_pct:
return current_profit, current_pips, ExitReason.EARLY_CUT, i, close
# B.4 Trend Reversal
is_ml_reversal = False
if (direction == "BUY" and cached_ml_signal == "SELL" and cached_ml_confidence >= self.trend_reversal_threshold) or \
(direction == "SELL" and cached_ml_signal == "BUY" and cached_ml_confidence >= self.trend_reversal_threshold):
is_ml_reversal = True
reversal_warnings += 1
loss_moderate = abs(current_profit) > (self.max_loss_per_trade * 0.4)
if is_ml_reversal and current_profit < -8 and loss_moderate:
return current_profit, current_pips, ExitReason.TREND_REVERSAL, i, close
if reversal_warnings >= 3 and current_profit < -10:
return current_profit, current_pips, ExitReason.TREND_REVERSAL, i, close
# B.5 Max loss
if current_profit <= -(self.max_loss_per_trade * 0.50):
htg = self._hours_to_golden(current_time)
if htg <= 1 and htg > 0 and momentum > -40:
pass
else:
return current_profit, current_pips, ExitReason.MAX_LOSS, i, close
# B.6 Stall
if len(profit_history) >= 10:
recent_range = max(profit_history[-10:]) - min(profit_history[-10:])
if recent_range < 3 and current_profit < -15:
stall_count += 1
if stall_count >= 5:
return current_profit, current_pips, ExitReason.STALL, i, close
# B.7 Daily loss limit
potential_daily_loss = daily_loss_so_far + abs(min(0, current_profit))
if potential_daily_loss >= self.max_daily_loss_usd:
return current_profit, current_pips, ExitReason.DAILY_LIMIT, i, close
# C) Time-based
if bars_since_entry >= 16 and current_profit < 5 and not profit_growing:
if current_profit >= 0 or current_profit > -15:
return current_profit, current_pips, ExitReason.TIMEOUT, i, close
if bars_since_entry >= 24 and (current_profit < 10 or not profit_growing):
return current_profit, current_pips, ExitReason.TIMEOUT, i, close
if bars_since_entry >= 32:
return current_profit, current_pips, ExitReason.TIMEOUT, i, close
# C.2 ATR trend reversal
if bars_since_entry > 10:
recent_closes = closes[i-5:i+1]
mom = recent_closes[-1] - recent_closes[0]
if (direction == "BUY" and mom < -reversal_momentum_threshold) or \
(direction == "SELL" and mom > reversal_momentum_threshold):
if current_profit < -min_loss_for_reversal_exit:
return current_profit, current_pips, ExitReason.TREND_REVERSAL, i, close
final_idx = min(entry_idx + max_bars - 1, len(df) - 1)
final_price = closes[final_idx]
pips = ((final_price - entry_price) if direction == "BUY" else (entry_price - final_price)) / 0.1
return pips * pip_value * lot_size, pips, ExitReason.TIMEOUT, final_idx, final_price
# ── Main run ──
def run(self, df, start_date=None, end_date=None, initial_capital=5000.0):
stats = BacktestStats()
capital = initial_capital
peak_capital = initial_capital
stats.equity_curve.append(capital)
daily_loss = 0.0
daily_profit = 0.0
daily_trades = 0
consecutive_losses = 0
trading_mode = TradingMode.NORMAL
current_date = None
feature_cols = []
if self.ml_model.fitted and self.ml_model.feature_names:
feature_cols = [f for f in self.ml_model.feature_names if f in df.columns]
times = df["time"].to_list()
start_idx = next((i for i, t in enumerate(times) if t >= start_date), 100) if start_date else 100
end_idx = next((i for i, t in enumerate(times) if t > end_date), len(df) - 100) if end_date else len(df) - 100
last_trade_idx = -self.trade_cooldown_bars * 2
print(f" #29 Min SMC elements: {self.min_smc_elements}, Min confidence: {self.min_confidence}")
print(f" Date range: {times[start_idx]} to {times[end_idx - 1]}")
print(f" Total bars: {end_idx - start_idx}")
for i in range(start_idx, end_idx):
if i - last_trade_idx < self.trade_cooldown_bars:
continue
current_time = times[i]
trade_date = current_time.date() if hasattr(current_time, 'date') else current_time
if current_date is None or trade_date != current_date:
daily_loss = 0.0
daily_profit = 0.0
daily_trades = 0
current_date = trade_date
if consecutive_losses < 2:
trading_mode = TradingMode.NORMAL
if trading_mode == TradingMode.STOPPED:
continue
session_name, can_trade, lot_mult = self._get_session_from_time(current_time)
if not can_trade:
if session_name == "Tokyo-London Overlap":
stats.session_blocked += 1
continue
if hasattr(current_time, 'weekday') and current_time.weekday() >= 5:
continue
df_slice = df.head(i + 1)
regime = "normal"
try:
if self.regime_detector.fitted:
regime_state = self.regime_detector.get_current_state(df_slice)
if regime_state:
regime = regime_state.regime.value
if regime_state.regime == MarketRegime.CRISIS:
continue
if regime_state.recommendation == "SLEEP":
continue
except Exception:
pass
try:
ml_signal = ""
ml_confidence = 0.5
if self.ml_model.fitted and feature_cols:
ml_pred = self.ml_model.predict(df_slice, feature_cols)
ml_signal = ml_pred.signal
ml_confidence = ml_pred.confidence
market_analysis = self.dynamic_confidence.analyze_market(
session=session_name, regime=regime, volatility="medium",
trend_direction=regime, has_smc_signal=True,
ml_signal=ml_signal, ml_confidence=ml_confidence,
)
if market_analysis.quality == MarketQuality.AVOID:
stats.avoided_signals += 1
continue
except Exception:
pass
try:
smc_signal = self.smc.generate_signal(df_slice)
except Exception:
continue
if smc_signal is None:
continue
# ═══ SMC ELEMENT DETECTION ═══
recent_df = df_slice.tail(10)
recent_bos = recent_df["bos"].to_list() if "bos" in df_slice.columns else []
recent_choch = recent_df["choch"].to_list() if "choch" in df_slice.columns else []
recent_fvg_bull = recent_df["is_fvg_bull"].to_list() if "is_fvg_bull" in df_slice.columns else []
recent_fvg_bear = recent_df["is_fvg_bear"].to_list() if "is_fvg_bear" in df_slice.columns else []
recent_obs = recent_df["ob"].to_list() if "ob" in df_slice.columns else []
has_bos = 1 in recent_bos or -1 in recent_bos
has_choch = 1 in recent_choch or -1 in recent_choch
has_fvg = any(recent_fvg_bull) or any(recent_fvg_bear)
has_ob = 1 in recent_obs or -1 in recent_obs
# ═══ #29: COUNT SMC ELEMENTS ═══
smc_element_count = sum([has_bos, has_choch, has_fvg, has_ob])
# ═══ #29: CONFLUENCE FILTER ═══
if self.min_smc_elements > 0 and smc_element_count < self.min_smc_elements:
stats.confluence_filtered += 1
continue
atr_at_entry = 12.0
if "atr" in df_slice.columns:
atr_val = df_slice.tail(1)["atr"].item()
if atr_val is not None and atr_val > 0:
atr_at_entry = atr_val
confidence = smc_signal.confidence
ml_agrees = (smc_signal.signal_type == "BUY" and ml_signal == "BUY") or \
(smc_signal.signal_type == "SELL" and ml_signal == "SELL")
if ml_agrees:
confidence = (smc_signal.confidence + ml_confidence) / 2
if regime == "high_volatility":
confidence *= 0.9
# ═══ #29: CONFIDENCE FILTER ═══
if self.min_confidence > 0 and confidence < self.min_confidence:
stats.confluence_filtered += 1
continue
lot_size = self._calculate_lot_size(confidence, regime, trading_mode, lot_mult)
if lot_size <= 0:
continue
if trading_mode == TradingMode.RECOVERY:
stats.recovery_mode_trades += 1
entry_price = smc_signal.entry_price
take_profit_price = smc_signal.take_profit
stop_loss_price = smc_signal.stop_loss
risk = abs(entry_price - stop_loss_price)
rr = abs(take_profit_price - entry_price) / risk if risk > 0 else 0
profit, pips, exit_reason, exit_idx, exit_price = self._simulate_trade_exit(
df=df, entry_idx=i, direction=smc_signal.signal_type,
entry_price=entry_price, take_profit=take_profit_price,
stop_loss=stop_loss_price, lot_size=lot_size,
daily_loss_so_far=daily_loss, feature_cols=feature_cols,
)
self._ticket_counter += 1
result = TradeResult.WIN if profit > 0 else (TradeResult.LOSS if profit < 0 else TradeResult.BREAKEVEN)
trade = SimulatedTrade(
ticket=self._ticket_counter,
entry_time=current_time,
exit_time=times[exit_idx] if exit_idx < len(times) else times[-1],
direction=smc_signal.signal_type,
entry_price=entry_price, exit_price=exit_price,
stop_loss=stop_loss_price, take_profit=take_profit_price,
lot_size=lot_size, profit_usd=profit, profit_pips=pips,
result=result, exit_reason=exit_reason,
smc_confidence=confidence, regime=regime,
session=session_name, signal_reason=smc_signal.reason,
has_bos=has_bos, has_choch=has_choch,
has_fvg=has_fvg, has_ob=has_ob,
atr_at_entry=atr_at_entry, rr_ratio=rr,
trading_mode=trading_mode.value,
smc_element_count=smc_element_count,
)
stats.trades.append(trade)
stats.total_trades += 1
daily_trades += 1
capital += profit
if profit > 0:
stats.wins += 1
stats.total_profit += profit
daily_profit += profit
consecutive_losses = 0
if trading_mode == TradingMode.RECOVERY:
trading_mode = TradingMode.NORMAL
else:
stats.losses += 1
stats.total_loss += abs(profit)
daily_loss += abs(profit)
consecutive_losses += 1
if daily_loss >= self.max_daily_loss_usd:
trading_mode = TradingMode.STOPPED
stats.daily_limit_stops += 1
elif consecutive_losses >= 3 or daily_loss >= self.max_daily_loss_usd * 0.6:
trading_mode = TradingMode.PROTECTED
elif consecutive_losses >= 2:
trading_mode = TradingMode.RECOVERY
if capital > peak_capital:
peak_capital = capital
drawdown_pct = (peak_capital - capital) / peak_capital * 100
drawdown_usd = peak_capital - capital
if drawdown_pct > stats.max_drawdown:
stats.max_drawdown = drawdown_pct
stats.max_drawdown_usd = drawdown_usd
stats.equity_curve.append(capital)
last_trade_idx = exit_idx
if stats.total_trades % 100 == 0:
print(f" {stats.total_trades} trades processed...")
if stats.total_trades > 0:
stats.win_rate = stats.wins / stats.total_trades * 100
stats.avg_win = stats.total_profit / stats.wins if stats.wins > 0 else 0
stats.avg_loss = stats.total_loss / stats.losses if stats.losses > 0 else 0
stats.avg_trade = (stats.total_profit - stats.total_loss) / stats.total_trades
stats.profit_factor = stats.total_profit / stats.total_loss if stats.total_loss > 0 else float("inf")
win_prob = stats.wins / stats.total_trades
loss_prob = stats.losses / stats.total_trades
stats.expectancy = (win_prob * stats.avg_win) - (loss_prob * stats.avg_loss)
returns = [t.profit_usd for t in stats.trades]
if len(returns) > 1:
avg_return = np.mean(returns)
std_return = np.std(returns)
stats.sharpe_ratio = (avg_return / std_return) * np.sqrt(252) if std_return > 0 else 0
return stats
# ─── Main ──────────────────────────────────────────────────────
def main():
print("=" * 70)
print("XAUBOT AI — #29 Confluence Scoring")
print("Base: #28B (Smart BE 0.5x ATR) | Modified: Confluence entry filters")
print("=" * 70)
config = get_config()
mt5 = MT5Connector(
login=config.mt5_login, password=config.mt5_password,
server=config.mt5_server, path=config.mt5_path,
)
mt5.connect()
print(f"\nConnected to MT5")
print("Fetching XAUUSD M15 historical data...")
df = mt5.get_market_data(symbol="XAUUSD", timeframe="M15", count=50000)
if len(df) == 0:
print("ERROR: No data")
mt5.disconnect()
return
print(f" Received {len(df)} bars")
times = df["time"].to_list()
print(f" Data range: {times[0]} to {times[-1]}")
end_date = datetime.now()
start_date = datetime(2025, 8, 1)
data_start = times[0]
if hasattr(data_start, 'replace') and data_start.tzinfo:
start_date = start_date.replace(tzinfo=data_start.tzinfo)
end_date = end_date.replace(tzinfo=data_start.tzinfo)
if data_start > start_date:
start_date = data_start + timedelta(days=5)
print(f"\n Backtest period: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")
print("\nCalculating indicators...")
features = FeatureEngineer()
smc = SMCAnalyzer(swing_length=config.smc.swing_length, ob_lookback=config.smc.ob_lookback)
df = features.calculate_all(df, include_ml_features=True)
df = smc.calculate_all(df)
regime_detector = MarketRegimeDetector(model_path="models/hmm_regime.pkl")
try:
regime_detector.load()
df = regime_detector.predict(df)
print(" HMM regime loaded")
except Exception:
print(" [WARN] HMM not available")
print(" Indicators calculated")
# ═══ ANALYZE ELEMENT DISTRIBUTION BEFORE BACKTEST ═══
print("\n SMC Element Distribution (pre-analysis)...")
if "bos" in df.columns:
bos_count = df.filter(pl.col("bos") != 0).height
choch_count = df.filter(pl.col("choch") != 0).height if "choch" in df.columns else 0
fvg_bull = df.filter(pl.col("is_fvg_bull") == True).height if "is_fvg_bull" in df.columns else 0
fvg_bear = df.filter(pl.col("is_fvg_bear") == True).height if "is_fvg_bear" in df.columns else 0
ob_count = df.filter(pl.col("ob") != 0).height if "ob" in df.columns else 0
print(f" BOS: {bos_count} bars | CHoCH: {choch_count} bars | FVG: {fvg_bull + fvg_bear} bars | OB: {ob_count} bars")
baseline_28b_pnl = 2463.80
# ═══ CONFIGS ═══
configs = [
# (name, min_smc_elements, min_confidence)
("A: Min 2 SMC elements", 2, 0.0), # Require 2 of BOS/CHoCH/FVG/OB
("B: Min conf >= 0.55", 0, 0.55), # Confidence threshold
("C: Min conf >= 0.60", 0, 0.60), # Higher confidence
("D: 2 elem + conf>=0.55", 2, 0.55), # Combined
("E: Min 3 SMC elements", 3, 0.0), # Strict: 3 out of 4 elements
]
all_results = []
for cfg_name, min_elem, min_conf in configs:
print(f"\n{'=' * 60}")
print(f" Config: {cfg_name}")
bt = ConfluenceScoringBacktest(
min_smc_elements=min_elem,
min_confidence=min_conf,
)
stats = bt.run(df=df, start_date=start_date, end_date=end_date, initial_capital=5000.0)
net_pnl = stats.total_profit - stats.total_loss
diff = net_pnl - baseline_28b_pnl
buy_trades = [t for t in stats.trades if t.direction == "BUY"]
sell_trades = [t for t in stats.trades if t.direction == "SELL"]
buy_wins = sum(1 for t in buy_trades if t.result == TradeResult.WIN)
sell_wins = sum(1 for t in sell_trades if t.result == TradeResult.WIN)
buy_wr = buy_wins / len(buy_trades) * 100 if buy_trades else 0
sell_wr = sell_wins / len(sell_trades) * 100 if sell_trades else 0
buy_pnl = sum(t.profit_usd for t in buy_trades)
sell_pnl = sum(t.profit_usd for t in sell_trades)
# Element distribution for trades taken
elem_dist = {}
for t in stats.trades:
c = t.smc_element_count
elem_dist[c] = elem_dist.get(c, 0) + 1
print(f"\n [{cfg_name}] Results:")
print(f" Trades: {stats.total_trades} | WR: {stats.win_rate:.1f}%")
print(f" Net PnL: ${net_pnl:,.2f} | PF: {stats.profit_factor:.2f}")
print(f" Max DD: {stats.max_drawdown:.1f}% | Sharpe: {stats.sharpe_ratio:.2f}")
print(f" Confluence filtered: {stats.confluence_filtered}")
print(f" BUY: {len(buy_trades)}, {buy_wr:.1f}% WR, ${buy_pnl:,.2f}")
print(f" SELL: {len(sell_trades)}, {sell_wr:.1f}% WR, ${sell_pnl:,.2f}")
print(f" Element dist: {dict(sorted(elem_dist.items()))}")
print(f" vs #28B: ${diff:+,.2f}")
all_results.append((cfg_name, stats, net_pnl, diff, stats.confluence_filtered, elem_dist))
# ═══ FINAL SUMMARY ═══
print(f"\n{'=' * 70}")
print("#29 CONFLUENCE SCORING — ALL CONFIGURATIONS")
print("=" * 70)
print(f"\n {'Config':<25} {'Trades':>6} {'WR':>6} {'Net PnL':>10} {'DD':>6} {'Sharpe':>7} {'PF':>5} {'Filt':>5} {'vs #28B':>10}")
print(f" {'-' * 90}")
print(f" {'#24B (base) ':<25} {'739':>6} {'80.4%':>6} {'$2,235':>10} {'3.4%':>6} {'2.87':>7} {'1.77':>5} {'':>5} {'':>10}")
print(f" {'#28B (smart BE)':<25} {'741':>6} {'79.8%':>6} {'$2,464':>10} {'3.5%':>6} {'3.23':>7} {'1.83':>5} {'':>5} {'':>10}")
for cfg_name, stats, net_pnl, diff, filt, elem_dist in all_results:
print(f" {cfg_name:<25} {stats.total_trades:>6} {stats.win_rate:>5.1f}% ${net_pnl:>9,.2f} {stats.max_drawdown:>5.1f}% {stats.sharpe_ratio:>7.2f} {stats.profit_factor:>5.2f} {filt:>5} ${diff:>+9,.2f}")
best_pnl = -999999
best_name = ""
best_stats = None
for entry in all_results:
if entry[2] > best_pnl:
best_pnl = entry[2]
best_name = entry[0]
best_stats = entry[1]
print(f"\n Best config: {best_name}")
# Element analysis for best config
print(f"\n Element Analysis (best config):")
for elem_count in sorted(set(t.smc_element_count for t in best_stats.trades)):
elem_trades = [t for t in best_stats.trades if t.smc_element_count == elem_count]
elem_wins = sum(1 for t in elem_trades if t.result == TradeResult.WIN)
elem_wr = elem_wins / len(elem_trades) * 100 if elem_trades else 0
elem_pnl = sum(t.profit_usd for t in elem_trades)
print(f" {elem_count} elements: {len(elem_trades)} trades, {elem_wr:.1f}% WR, ${elem_pnl:,.2f}")
# Direction analysis
print(f"\n Direction (best config):")
buy_trades = [t for t in best_stats.trades if t.direction == "BUY"]
sell_trades = [t for t in best_stats.trades if t.direction == "SELL"]
buy_wins = sum(1 for t in buy_trades if t.result == TradeResult.WIN)
sell_wins = sum(1 for t in sell_trades if t.result == TradeResult.WIN)
buy_wr = buy_wins / len(buy_trades) * 100 if buy_trades else 0
sell_wr = sell_wins / len(sell_trades) * 100 if sell_trades else 0
buy_pnl = sum(t.profit_usd for t in buy_trades)
sell_pnl = sum(t.profit_usd for t in sell_trades)
print(f" BUY: {len(buy_trades)} trades, {buy_wr:.1f}% WR, ${buy_pnl:,.2f}")
print(f" SELL: {len(sell_trades)} trades, {sell_wr:.1f}% WR, ${sell_pnl:,.2f}")
# Exit reasons
print(f"\n Exit Reasons (best config):")
exit_counts = {}
for t in best_stats.trades:
r = t.exit_reason.value
exit_counts[r] = exit_counts.get(r, 0) + 1
for reason, count in sorted(exit_counts.items(), key=lambda x: -x[1]):
pct = count / best_stats.total_trades * 100 if best_stats.total_trades > 0 else 0
print(f" {reason:20s}: {count} ({pct:.1f}%)")
# Save
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "29_confluence_scoring_results")
os.makedirs(output_dir, exist_ok=True)
log_path = os.path.join(output_dir, f"confluence_{timestamp}.log")
with open(log_path, "w") as f:
f.write(f"#29 Confluence Scoring Results\n")
f.write(f"Generated: {datetime.now()}\n")
f.write(f"Base: #28B (741 trades, 79.8% WR, $2,464)\n\n")
for cfg_name, stats, net_pnl, diff, filt, elem_dist in all_results:
f.write(f" {cfg_name}: {stats.total_trades} trades, {stats.win_rate:.1f}% WR, "
f"${net_pnl:,.2f}, DD: {stats.max_drawdown:.1f}%, "
f"Sharpe: {stats.sharpe_ratio:.2f}, PF: {stats.profit_factor:.2f}, "
f"Filtered: {filt}, Elem dist: {dict(sorted(elem_dist.items()))}, "
f"vs #28B: ${diff:+,.2f}\n")
f.write(f"\nBest: {best_name}\n")
print(f" Log saved: {log_path}")
try:
from backtests.backtest_01_smc_only import generate_xlsx_report as gen_xlsx
xlsx_path = os.path.join(output_dir, f"confluence_{timestamp}.xlsx")
gen_xlsx(best_stats, xlsx_path, start_date, end_date)
print(f"\n Report saved: {xlsx_path}")
except Exception as e:
print(f" [WARN] XLSX: {e}")
mt5.disconnect()
print(f"\n{'=' * 70}")
print(f"Output: {output_dir}")
print(f" Log: {os.path.basename(log_path)}")
print("=" * 70)
print("Backtest complete!")
if __name__ == "__main__":
main()
+999
View File
@@ -0,0 +1,999 @@
"""
Backtest #30 — Dynamic Risk-Reward
====================================
Base: #28B (Smart BE 0.5x ATR) — 741 trades, 79.8% WR, $2,464, Sharpe 3.23
Idea: Adjust TP distance based on session, ATR, and conditions.
Currently, SMC uses a fixed RR of 1.5-2.0. What if we:
- Use tighter TP in Asian session (smaller moves)
- Use wider TP in Golden session (bigger moves)
- Scale TP with ATR (high vol = wider TP)
Configs:
A: Session-based RR (Golden=2.0x, London=1.5x, Asian=1.0x of SMC TP)
B: ATR-scaled TP (TP = entry + direction * ATR * 3.0)
C: ATR-scaled TP wider (TP = entry + direction * ATR * 4.0)
D: A + ATR floor (session RR but min TP = ATR * 2.5)
E: Tighter TP across board (RR mult 0.8 = closer TP for higher hit rate)
Usage:
python backtests/backtest_30_dynamic_rr.py
"""
import polars as pl
import pandas as pd
import numpy as np
from datetime import datetime, timedelta, date
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass, field
from enum import Enum
import sys
import os
from zoneinfo import ZoneInfo
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.mt5_connector import MT5Connector
from src.smc_polars import SMCAnalyzer, SMCSignal
from src.feature_eng import FeatureEngineer
from src.regime_detector import MarketRegimeDetector, MarketRegime
from src.ml_model import TradingModel
from src.config import get_config
from src.dynamic_confidence import DynamicConfidenceManager, create_dynamic_confidence, MarketQuality
from loguru import logger
logger.remove()
logger.add(sys.stderr, level="WARNING")
WIB = ZoneInfo("Asia/Jakarta")
# ─── Enums & Dataclasses ──────────────────────────────────────
class TradeResult(Enum):
WIN = "WIN"
LOSS = "LOSS"
BREAKEVEN = "BREAKEVEN"
class ExitReason(Enum):
TAKE_PROFIT = "take_profit"
SMART_TP = "smart_tp"
PEAK_PROTECT = "peak_protect"
EARLY_EXIT = "early_exit"
EARLY_CUT = "early_cut"
MAX_LOSS = "max_loss"
STALL = "stall"
TREND_REVERSAL = "trend_reversal"
TIMEOUT = "timeout"
WEEKEND_CLOSE = "weekend_close"
TRAILING_SL = "trailing_sl"
BREAKEVEN_EXIT = "breakeven_exit"
DAILY_LIMIT = "daily_limit"
REGIME_DANGER = "regime_danger"
MARKET_SIGNAL = "market_signal"
class TradingMode(Enum):
NORMAL = "normal"
RECOVERY = "recovery"
PROTECTED = "protected"
STOPPED = "stopped"
@dataclass
class SimulatedTrade:
ticket: int
entry_time: datetime
exit_time: datetime
direction: str
entry_price: float
exit_price: float
stop_loss: float
take_profit: float
lot_size: float
profit_usd: float
profit_pips: float
result: TradeResult
exit_reason: ExitReason
smc_confidence: float
regime: str
session: str
signal_reason: str
has_bos: bool = False
has_choch: bool = False
has_fvg: bool = False
has_ob: bool = False
atr_at_entry: float = 0.0
rr_ratio: float = 0.0
trading_mode: str = "normal"
original_tp: float = 0.0 # NEW: track original TP for comparison
@dataclass
class BacktestStats:
total_trades: int = 0
wins: int = 0
losses: int = 0
total_profit: float = 0.0
total_loss: float = 0.0
max_drawdown: float = 0.0
max_drawdown_usd: float = 0.0
win_rate: float = 0.0
profit_factor: float = 0.0
avg_win: float = 0.0
avg_loss: float = 0.0
avg_trade: float = 0.0
expectancy: float = 0.0
sharpe_ratio: float = 0.0
trades: List[SimulatedTrade] = field(default_factory=list)
equity_curve: List[float] = field(default_factory=list)
avoided_signals: int = 0
daily_limit_stops: int = 0
recovery_mode_trades: int = 0
session_blocked: int = 0
tp_modified: int = 0 # NEW
# ─── Dynamic RR Backtest ─────────────────────────────────────
class DynamicRRBacktest:
"""#28B base + dynamic risk-reward TP adjustment."""
def __init__(
self,
capital: float = 5000.0,
max_daily_loss_percent: float = 5.0,
max_loss_per_trade_percent: float = 1.0,
base_lot_size: float = 0.01,
max_lot_size: float = 0.02,
recovery_lot_size: float = 0.01,
trend_reversal_threshold: float = 0.75,
max_concurrent_positions: int = 2,
min_profit_to_protect: float = 5.0,
max_drawdown_from_peak: float = 50.0,
trade_cooldown_bars: int = 10,
trend_reversal_mult: float = 0.6,
# #24B base
skip_tokyo_london: bool = True,
early_cut_momentum: float = -50.0,
early_cut_loss_pct: float = 30.0,
be_mult: float = 2.0,
trail_start_mult: float = 4.0,
trail_step_mult: float = 3.0,
# #28B: Smart breakeven
be_profit_lock_atr_mult: float = 0.5,
# ═══ #30 DYNAMIC RR PARAMS ═══
session_rr_multipliers: Optional[Dict[str, float]] = None, # session -> TP multiplier
atr_tp_mult: float = 0.0, # If > 0, override TP with entry +/- ATR * mult
tp_rr_multiplier: float = 1.0, # Global TP distance multiplier
atr_tp_floor_mult: float = 0.0, # Minimum TP distance = ATR * this
):
self.capital = capital
self.max_daily_loss_usd = capital * (max_daily_loss_percent / 100)
self.max_loss_per_trade = capital * (max_loss_per_trade_percent / 100)
self.base_lot_size = base_lot_size
self.max_lot_size = max_lot_size
self.recovery_lot_size = recovery_lot_size
self.trend_reversal_threshold = trend_reversal_threshold
self.max_concurrent_positions = max_concurrent_positions
self.min_profit_to_protect = min_profit_to_protect
self.max_drawdown_from_peak = max_drawdown_from_peak
self.trade_cooldown_bars = trade_cooldown_bars
self.trend_reversal_mult = trend_reversal_mult
self.skip_tokyo_london = skip_tokyo_london
self.early_cut_momentum = early_cut_momentum
self.early_cut_loss_pct = early_cut_loss_pct
self.be_mult = be_mult
self.trail_start_mult = trail_start_mult
self.trail_step_mult = trail_step_mult
self.be_profit_lock_atr_mult = be_profit_lock_atr_mult
# #30 params
self.session_rr_multipliers = session_rr_multipliers or {}
self.atr_tp_mult = atr_tp_mult
self.tp_rr_multiplier = tp_rr_multiplier
self.atr_tp_floor_mult = atr_tp_floor_mult
config = get_config()
self.smc = SMCAnalyzer(swing_length=config.smc.swing_length, ob_lookback=config.smc.ob_lookback)
self.features = FeatureEngineer()
self.dynamic_confidence = create_dynamic_confidence()
self.ml_model = TradingModel(model_path="models/xgboost_model.pkl")
try:
self.ml_model.load()
print(" ML model loaded (for exit evaluation)")
except Exception:
print(" [WARN] ML model not loaded")
self.regime_detector = MarketRegimeDetector(model_path="models/hmm_regime.pkl")
try:
self.regime_detector.load()
except Exception:
print(" [WARN] HMM model not loaded")
self._ticket_counter = 2300000
def _get_session_from_time(self, dt):
if dt.tzinfo is None:
dt = dt.replace(tzinfo=ZoneInfo("UTC"))
wib_time = dt.astimezone(WIB)
hour = wib_time.hour
if 6 <= hour < 15:
return "Sydney-Tokyo", True, 0.5
elif 15 <= hour < 16:
if self.skip_tokyo_london:
return "Tokyo-London Overlap", False, 0.0
return "Tokyo-London Overlap", True, 0.75
elif 16 <= hour < 19:
return "London Early", True, 0.8
elif 19 <= hour < 24:
return "London-NY Overlap (Golden)", True, 1.0
elif 0 <= hour < 4:
return "NY Session", True, 0.9
else:
return "Off Hours", False, 0.0
def _hours_to_golden(self, dt):
if dt.tzinfo is None:
dt = dt.replace(tzinfo=ZoneInfo("UTC"))
wib = dt.astimezone(WIB)
if 19 <= wib.hour < 24:
return 0
target = wib.replace(hour=19, minute=0, second=0, microsecond=0)
if wib.hour >= 19:
target += timedelta(days=1)
return max(0, (target - wib).total_seconds() / 3600)
def _is_near_weekend_close(self, dt):
if dt.tzinfo is None:
dt = dt.replace(tzinfo=ZoneInfo("UTC"))
wib = dt.astimezone(WIB)
return wib.weekday() == 5 and wib.hour >= 4 and wib.minute >= 30
def _calculate_lot_size(self, confidence, regime, trading_mode, session_mult):
if trading_mode == TradingMode.STOPPED:
return 0
lot = self.base_lot_size
if trading_mode in (TradingMode.RECOVERY, TradingMode.PROTECTED):
lot = self.recovery_lot_size
else:
if confidence >= 0.65:
lot = self.max_lot_size
elif confidence >= 0.55:
lot = self.base_lot_size
else:
lot = self.recovery_lot_size
if regime.lower() in ["high_volatility", "crisis"]:
lot = self.recovery_lot_size
lot = max(0.01, lot * session_mult)
return round(lot, 2)
def _adjust_tp(self, direction, entry_price, original_tp, stop_loss, session_name, atr):
"""#30: Adjust take profit based on session/ATR/multiplier."""
tp = original_tp
modified = False
# Method 1: ATR-based TP override
if self.atr_tp_mult > 0:
if direction == "BUY":
tp = entry_price + atr * self.atr_tp_mult
else:
tp = entry_price - atr * self.atr_tp_mult
modified = True
# Method 2: Session-based multiplier
elif self.session_rr_multipliers:
mult = self.session_rr_multipliers.get(session_name, 1.0)
tp_distance = abs(original_tp - entry_price)
new_tp_distance = tp_distance * mult
if direction == "BUY":
tp = entry_price + new_tp_distance
else:
tp = entry_price - new_tp_distance
if mult != 1.0:
modified = True
# Method 3: Global TP multiplier
if self.tp_rr_multiplier != 1.0 and self.atr_tp_mult == 0 and not self.session_rr_multipliers:
tp_distance = abs(original_tp - entry_price)
new_tp_distance = tp_distance * self.tp_rr_multiplier
if direction == "BUY":
tp = entry_price + new_tp_distance
else:
tp = entry_price - new_tp_distance
modified = True
# Floor: ensure minimum TP distance
if self.atr_tp_floor_mult > 0:
min_tp_distance = atr * self.atr_tp_floor_mult
current_tp_distance = abs(tp - entry_price)
if current_tp_distance < min_tp_distance:
if direction == "BUY":
tp = entry_price + min_tp_distance
else:
tp = entry_price - min_tp_distance
modified = True
return tp, modified
def _simulate_trade_exit(
self, df, entry_idx, direction, entry_price, take_profit, stop_loss,
lot_size, daily_loss_so_far, feature_cols, max_bars=100,
):
pip_value = 10
highs = df["high"].to_list()
lows = df["low"].to_list()
closes = df["close"].to_list()
times = df["time"].to_list()
atr = 12.0
if "atr" in df.columns:
atr_list = df["atr"].to_list()
if entry_idx < len(atr_list) and atr_list[entry_idx] is not None:
atr = atr_list[entry_idx]
adaptive_breakeven_pips = atr * self.be_mult
adaptive_trail_start_pips = atr * self.trail_start_mult
adaptive_trail_step_pips = atr * self.trail_step_mult
reversal_momentum_threshold = atr * self.trend_reversal_mult
min_loss_for_reversal_exit = atr * 0.8
if self.be_profit_lock_atr_mult > 0:
be_lock_distance = atr * self.be_profit_lock_atr_mult
else:
be_lock_distance = 2.0
profit_history = []
peak_profit = 0.0
stall_count = 0
reversal_warnings = 0
current_sl = stop_loss
breakeven_moved = False
if direction == "BUY":
target_tp_profit = (take_profit - entry_price) / 0.1 * pip_value * lot_size
else:
target_tp_profit = (entry_price - take_profit) / 0.1 * pip_value * lot_size
cached_ml_signal = ""
cached_ml_confidence = 0.5
for i in range(entry_idx + 1, min(entry_idx + max_bars, len(df))):
high = highs[i]
low = lows[i]
close = closes[i]
current_time = times[i]
if direction == "BUY":
current_pips = (close - entry_price) / 0.1
pip_profit_from_entry = current_pips
else:
current_pips = (entry_price - close) / 0.1
pip_profit_from_entry = current_pips
current_profit = current_pips * pip_value * lot_size
profit_history.append(current_profit)
if current_profit > peak_profit:
peak_profit = current_profit
bars_since_entry = i - entry_idx
if bars_since_entry % 4 == 0 and self.ml_model.fitted:
try:
df_slice = df.head(i + 1)
ml_pred = self.ml_model.predict(df_slice, feature_cols)
cached_ml_signal = ml_pred.signal
cached_ml_confidence = ml_pred.confidence
except Exception:
pass
momentum = 0.0
if len(profit_history) >= 3:
recent = profit_history[-5:] if len(profit_history) >= 5 else profit_history
profit_change = recent[-1] - recent[0]
momentum = max(-100, min(100, (profit_change / 10) * 50))
profit_growing = momentum > 0
# A.0 TP hit
if direction == "BUY" and high >= take_profit:
pips = (take_profit - entry_price) / 0.1
return pips * pip_value * lot_size, pips, ExitReason.TAKE_PROFIT, i, take_profit
elif direction == "SELL" and low <= take_profit:
pips = (entry_price - take_profit) / 0.1
return pips * pip_value * lot_size, pips, ExitReason.TAKE_PROFIT, i, take_profit
# A.0b Trailing SL hit
if breakeven_moved and current_sl > 0:
if direction == "BUY" and low <= current_sl:
pips = (current_sl - entry_price) / 0.1
reason = ExitReason.TRAILING_SL if pip_profit_from_entry >= adaptive_trail_start_pips else ExitReason.BREAKEVEN_EXIT
return pips * pip_value * lot_size, pips, reason, i, current_sl
elif direction == "SELL" and high >= current_sl:
pips = (entry_price - current_sl) / 0.1
reason = ExitReason.TRAILING_SL if pip_profit_from_entry >= adaptive_trail_start_pips else ExitReason.BREAKEVEN_EXIT
return pips * pip_value * lot_size, pips, reason, i, current_sl
# A.1 Breakeven (#28B: Smart)
if pip_profit_from_entry >= adaptive_breakeven_pips and not breakeven_moved:
if direction == "BUY":
current_sl = entry_price + be_lock_distance
else:
current_sl = entry_price - be_lock_distance
breakeven_moved = True
# A.2 Trailing SL
if pip_profit_from_entry >= adaptive_trail_start_pips:
trail_distance = adaptive_trail_step_pips * 0.1
if direction == "BUY":
new_trail_sl = close - trail_distance
if new_trail_sl > current_sl:
current_sl = new_trail_sl
else:
new_trail_sl = close + trail_distance
if current_sl == 0 or new_trail_sl < current_sl:
current_sl = new_trail_sl
# A.3 Peak protect
if peak_profit > self.min_profit_to_protect:
drawdown_pct = ((peak_profit - current_profit) / peak_profit) * 100 if peak_profit > 0 else 0
if drawdown_pct > self.max_drawdown_from_peak:
return current_profit, current_pips, ExitReason.PEAK_PROTECT, i, close
# A.4 Market analysis
if bars_since_entry % 5 == 0 and bars_since_entry >= 5 and i >= 20:
ma_fast = np.mean(closes[i-4:i+1])
ma_slow = np.mean(closes[i-19:i+1])
trend = "BULLISH" if ma_fast > ma_slow * 1.001 else ("BEARISH" if ma_fast < ma_slow * 0.999 else "NEUTRAL")
roc = (closes[i] / closes[max(0,i-4)] - 1) * 100
mom_dir = "BULLISH" if roc > 0.3 else ("BEARISH" if roc < -0.3 else "NEUTRAL")
rsi_val = None
if "rsi" in df.columns:
rsi_list = df["rsi"].to_list()
if i < len(rsi_list):
rsi_val = rsi_list[i]
urgency = 0
should_exit = False
if cached_ml_confidence > 0.75:
if (direction == "BUY" and cached_ml_signal == "SELL") or (direction == "SELL" and cached_ml_signal == "BUY"):
should_exit = True; urgency += 2
if rsi_val:
if (rsi_val > 75 and direction == "BUY") or (rsi_val < 25 and direction == "SELL"):
should_exit = True; urgency += 2
if (direction == "BUY" and trend == "BEARISH" and mom_dir == "BEARISH") or \
(direction == "SELL" and trend == "BULLISH" and mom_dir == "BULLISH"):
should_exit = True; urgency += 3
if should_exit and current_profit > self.min_profit_to_protect / 2:
return current_profit, current_pips, ExitReason.MARKET_SIGNAL, i, close
if urgency >= 7 and current_profit > 0:
return current_profit, current_pips, ExitReason.MARKET_SIGNAL, i, close
# A.5 Weekend close
if self._is_near_weekend_close(current_time):
if current_profit > 0 or current_profit > -10:
return current_profit, current_pips, ExitReason.WEEKEND_CLOSE, i, close
# B.1 Smart TP
if current_profit >= 15:
if current_profit >= 40:
return current_profit, current_pips, ExitReason.SMART_TP, i, close
if current_profit >= 25 and momentum < -30:
return current_profit, current_pips, ExitReason.SMART_TP, i, close
if peak_profit > 30 and current_profit < peak_profit * 0.6:
return current_profit, current_pips, ExitReason.PEAK_PROTECT, i, close
if current_profit >= 20:
progress = (current_profit / target_tp_profit) * 100 if target_tp_profit > 0 else 0
progress_score = min(40, max(0, progress * 0.4))
momentum_score = ((momentum + 100) / 200) * 30
time_penalty = min(10, bars_since_entry / 4 * 2)
tp_probability = progress_score + momentum_score + 10 - time_penalty
if tp_probability < 25:
return current_profit, current_pips, ExitReason.SMART_TP, i, close
# B.2 Smart Early Exit
if 5 <= current_profit < 15:
if momentum < -50 and cached_ml_confidence >= 0.65:
is_reversal = (direction == "BUY" and cached_ml_signal == "SELL") or (direction == "SELL" and cached_ml_signal == "BUY")
if is_reversal:
return current_profit, current_pips, ExitReason.EARLY_EXIT, i, close
# B.3 Early cut
if current_profit < 0:
loss_percent_of_max = abs(current_profit) / self.max_loss_per_trade * 100
if momentum < self.early_cut_momentum and loss_percent_of_max >= self.early_cut_loss_pct:
return current_profit, current_pips, ExitReason.EARLY_CUT, i, close
# B.4 Trend Reversal
is_ml_reversal = False
if (direction == "BUY" and cached_ml_signal == "SELL" and cached_ml_confidence >= self.trend_reversal_threshold) or \
(direction == "SELL" and cached_ml_signal == "BUY" and cached_ml_confidence >= self.trend_reversal_threshold):
is_ml_reversal = True
reversal_warnings += 1
loss_moderate = abs(current_profit) > (self.max_loss_per_trade * 0.4)
if is_ml_reversal and current_profit < -8 and loss_moderate:
return current_profit, current_pips, ExitReason.TREND_REVERSAL, i, close
if reversal_warnings >= 3 and current_profit < -10:
return current_profit, current_pips, ExitReason.TREND_REVERSAL, i, close
# B.5 Max loss
if current_profit <= -(self.max_loss_per_trade * 0.50):
htg = self._hours_to_golden(current_time)
if htg <= 1 and htg > 0 and momentum > -40:
pass
else:
return current_profit, current_pips, ExitReason.MAX_LOSS, i, close
# B.6 Stall
if len(profit_history) >= 10:
recent_range = max(profit_history[-10:]) - min(profit_history[-10:])
if recent_range < 3 and current_profit < -15:
stall_count += 1
if stall_count >= 5:
return current_profit, current_pips, ExitReason.STALL, i, close
# B.7 Daily loss limit
potential_daily_loss = daily_loss_so_far + abs(min(0, current_profit))
if potential_daily_loss >= self.max_daily_loss_usd:
return current_profit, current_pips, ExitReason.DAILY_LIMIT, i, close
# C) Time-based
if bars_since_entry >= 16 and current_profit < 5 and not profit_growing:
if current_profit >= 0 or current_profit > -15:
return current_profit, current_pips, ExitReason.TIMEOUT, i, close
if bars_since_entry >= 24 and (current_profit < 10 or not profit_growing):
return current_profit, current_pips, ExitReason.TIMEOUT, i, close
if bars_since_entry >= 32:
return current_profit, current_pips, ExitReason.TIMEOUT, i, close
# C.2 ATR trend reversal
if bars_since_entry > 10:
recent_closes = closes[i-5:i+1]
mom = recent_closes[-1] - recent_closes[0]
if (direction == "BUY" and mom < -reversal_momentum_threshold) or \
(direction == "SELL" and mom > reversal_momentum_threshold):
if current_profit < -min_loss_for_reversal_exit:
return current_profit, current_pips, ExitReason.TREND_REVERSAL, i, close
final_idx = min(entry_idx + max_bars - 1, len(df) - 1)
final_price = closes[final_idx]
pips = ((final_price - entry_price) if direction == "BUY" else (entry_price - final_price)) / 0.1
return pips * pip_value * lot_size, pips, ExitReason.TIMEOUT, final_idx, final_price
def run(self, df, start_date=None, end_date=None, initial_capital=5000.0):
stats = BacktestStats()
capital = initial_capital
peak_capital = initial_capital
stats.equity_curve.append(capital)
daily_loss = 0.0
daily_profit = 0.0
daily_trades = 0
consecutive_losses = 0
trading_mode = TradingMode.NORMAL
current_date = None
feature_cols = []
if self.ml_model.fitted and self.ml_model.feature_names:
feature_cols = [f for f in self.ml_model.feature_names if f in df.columns]
times = df["time"].to_list()
start_idx = next((i for i, t in enumerate(times) if t >= start_date), 100) if start_date else 100
end_idx = next((i for i, t in enumerate(times) if t > end_date), len(df) - 100) if end_date else len(df) - 100
last_trade_idx = -self.trade_cooldown_bars * 2
sess_rr_str = str(self.session_rr_multipliers) if self.session_rr_multipliers else "none"
print(f" #30 Session RR: {sess_rr_str}, ATR TP: {self.atr_tp_mult}, TP mult: {self.tp_rr_multiplier}, ATR floor: {self.atr_tp_floor_mult}")
print(f" Date range: {times[start_idx]} to {times[end_idx - 1]}")
print(f" Total bars: {end_idx - start_idx}")
for i in range(start_idx, end_idx):
if i - last_trade_idx < self.trade_cooldown_bars:
continue
current_time = times[i]
trade_date = current_time.date() if hasattr(current_time, 'date') else current_time
if current_date is None or trade_date != current_date:
daily_loss = 0.0
daily_profit = 0.0
daily_trades = 0
current_date = trade_date
if consecutive_losses < 2:
trading_mode = TradingMode.NORMAL
if trading_mode == TradingMode.STOPPED:
continue
session_name, can_trade, lot_mult = self._get_session_from_time(current_time)
if not can_trade:
if session_name == "Tokyo-London Overlap":
stats.session_blocked += 1
continue
if hasattr(current_time, 'weekday') and current_time.weekday() >= 5:
continue
df_slice = df.head(i + 1)
regime = "normal"
try:
if self.regime_detector.fitted:
regime_state = self.regime_detector.get_current_state(df_slice)
if regime_state:
regime = regime_state.regime.value
if regime_state.regime == MarketRegime.CRISIS:
continue
if regime_state.recommendation == "SLEEP":
continue
except Exception:
pass
try:
ml_signal = ""
ml_confidence = 0.5
if self.ml_model.fitted and feature_cols:
ml_pred = self.ml_model.predict(df_slice, feature_cols)
ml_signal = ml_pred.signal
ml_confidence = ml_pred.confidence
market_analysis = self.dynamic_confidence.analyze_market(
session=session_name, regime=regime, volatility="medium",
trend_direction=regime, has_smc_signal=True,
ml_signal=ml_signal, ml_confidence=ml_confidence,
)
if market_analysis.quality == MarketQuality.AVOID:
stats.avoided_signals += 1
continue
except Exception:
pass
try:
smc_signal = self.smc.generate_signal(df_slice)
except Exception:
continue
if smc_signal is None:
continue
recent_df = df_slice.tail(10)
recent_bos = recent_df["bos"].to_list() if "bos" in df_slice.columns else []
recent_choch = recent_df["choch"].to_list() if "choch" in df_slice.columns else []
recent_fvg_bull = recent_df["is_fvg_bull"].to_list() if "is_fvg_bull" in df_slice.columns else []
recent_fvg_bear = recent_df["is_fvg_bear"].to_list() if "is_fvg_bear" in df_slice.columns else []
recent_obs = recent_df["ob"].to_list() if "ob" in df_slice.columns else []
has_bos = 1 in recent_bos or -1 in recent_bos
has_choch = 1 in recent_choch or -1 in recent_choch
has_fvg = any(recent_fvg_bull) or any(recent_fvg_bear)
has_ob = 1 in recent_obs or -1 in recent_obs
atr_at_entry = 12.0
if "atr" in df_slice.columns:
atr_val = df_slice.tail(1)["atr"].item()
if atr_val is not None and atr_val > 0:
atr_at_entry = atr_val
confidence = smc_signal.confidence
ml_agrees = (smc_signal.signal_type == "BUY" and ml_signal == "BUY") or \
(smc_signal.signal_type == "SELL" and ml_signal == "SELL")
if ml_agrees:
confidence = (smc_signal.confidence + ml_confidence) / 2
if regime == "high_volatility":
confidence *= 0.9
lot_size = self._calculate_lot_size(confidence, regime, trading_mode, lot_mult)
if lot_size <= 0:
continue
if trading_mode == TradingMode.RECOVERY:
stats.recovery_mode_trades += 1
entry_price = smc_signal.entry_price
original_tp = smc_signal.take_profit
stop_loss_price = smc_signal.stop_loss
# ═══ #30: ADJUST TP ═══
take_profit_price, tp_was_modified = self._adjust_tp(
direction=smc_signal.signal_type,
entry_price=entry_price,
original_tp=original_tp,
stop_loss=stop_loss_price,
session_name=session_name,
atr=atr_at_entry,
)
if tp_was_modified:
stats.tp_modified += 1
risk = abs(entry_price - stop_loss_price)
rr = abs(take_profit_price - entry_price) / risk if risk > 0 else 0
profit, pips, exit_reason, exit_idx, exit_price = self._simulate_trade_exit(
df=df, entry_idx=i, direction=smc_signal.signal_type,
entry_price=entry_price, take_profit=take_profit_price,
stop_loss=stop_loss_price, lot_size=lot_size,
daily_loss_so_far=daily_loss, feature_cols=feature_cols,
)
self._ticket_counter += 1
result = TradeResult.WIN if profit > 0 else (TradeResult.LOSS if profit < 0 else TradeResult.BREAKEVEN)
trade = SimulatedTrade(
ticket=self._ticket_counter,
entry_time=current_time,
exit_time=times[exit_idx] if exit_idx < len(times) else times[-1],
direction=smc_signal.signal_type,
entry_price=entry_price, exit_price=exit_price,
stop_loss=stop_loss_price, take_profit=take_profit_price,
lot_size=lot_size, profit_usd=profit, profit_pips=pips,
result=result, exit_reason=exit_reason,
smc_confidence=confidence, regime=regime,
session=session_name, signal_reason=smc_signal.reason,
has_bos=has_bos, has_choch=has_choch,
has_fvg=has_fvg, has_ob=has_ob,
atr_at_entry=atr_at_entry, rr_ratio=rr,
trading_mode=trading_mode.value,
original_tp=original_tp,
)
stats.trades.append(trade)
stats.total_trades += 1
daily_trades += 1
capital += profit
if profit > 0:
stats.wins += 1
stats.total_profit += profit
daily_profit += profit
consecutive_losses = 0
if trading_mode == TradingMode.RECOVERY:
trading_mode = TradingMode.NORMAL
else:
stats.losses += 1
stats.total_loss += abs(profit)
daily_loss += abs(profit)
consecutive_losses += 1
if daily_loss >= self.max_daily_loss_usd:
trading_mode = TradingMode.STOPPED
stats.daily_limit_stops += 1
elif consecutive_losses >= 3 or daily_loss >= self.max_daily_loss_usd * 0.6:
trading_mode = TradingMode.PROTECTED
elif consecutive_losses >= 2:
trading_mode = TradingMode.RECOVERY
if capital > peak_capital:
peak_capital = capital
drawdown_pct = (peak_capital - capital) / peak_capital * 100
drawdown_usd = peak_capital - capital
if drawdown_pct > stats.max_drawdown:
stats.max_drawdown = drawdown_pct
stats.max_drawdown_usd = drawdown_usd
stats.equity_curve.append(capital)
last_trade_idx = exit_idx
if stats.total_trades % 100 == 0:
print(f" {stats.total_trades} trades processed...")
if stats.total_trades > 0:
stats.win_rate = stats.wins / stats.total_trades * 100
stats.avg_win = stats.total_profit / stats.wins if stats.wins > 0 else 0
stats.avg_loss = stats.total_loss / stats.losses if stats.losses > 0 else 0
stats.avg_trade = (stats.total_profit - stats.total_loss) / stats.total_trades
stats.profit_factor = stats.total_profit / stats.total_loss if stats.total_loss > 0 else float("inf")
win_prob = stats.wins / stats.total_trades
loss_prob = stats.losses / stats.total_trades
stats.expectancy = (win_prob * stats.avg_win) - (loss_prob * stats.avg_loss)
returns = [t.profit_usd for t in stats.trades]
if len(returns) > 1:
avg_return = np.mean(returns)
std_return = np.std(returns)
stats.sharpe_ratio = (avg_return / std_return) * np.sqrt(252) if std_return > 0 else 0
return stats
# ─── Main ──────────────────────────────────────────────────────
def main():
print("=" * 70)
print("XAUBOT AI — #30 Dynamic Risk-Reward")
print("Base: #28B (Smart BE 0.5x ATR) | Modified: Dynamic TP adjustment")
print("=" * 70)
config = get_config()
mt5 = MT5Connector(
login=config.mt5_login, password=config.mt5_password,
server=config.mt5_server, path=config.mt5_path,
)
mt5.connect()
print(f"\nConnected to MT5")
print("Fetching XAUUSD M15 historical data...")
df = mt5.get_market_data(symbol="XAUUSD", timeframe="M15", count=50000)
if len(df) == 0:
print("ERROR: No data")
mt5.disconnect()
return
print(f" Received {len(df)} bars")
times = df["time"].to_list()
print(f" Data range: {times[0]} to {times[-1]}")
end_date = datetime.now()
start_date = datetime(2025, 8, 1)
data_start = times[0]
if hasattr(data_start, 'replace') and data_start.tzinfo:
start_date = start_date.replace(tzinfo=data_start.tzinfo)
end_date = end_date.replace(tzinfo=data_start.tzinfo)
if data_start > start_date:
start_date = data_start + timedelta(days=5)
print(f"\n Backtest period: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")
print("\nCalculating indicators...")
features = FeatureEngineer()
smc = SMCAnalyzer(swing_length=config.smc.swing_length, ob_lookback=config.smc.ob_lookback)
df = features.calculate_all(df, include_ml_features=True)
df = smc.calculate_all(df)
regime_detector = MarketRegimeDetector(model_path="models/hmm_regime.pkl")
try:
regime_detector.load()
df = regime_detector.predict(df)
print(" HMM regime loaded")
except Exception:
print(" [WARN] HMM not available")
print(" Indicators calculated")
baseline_28b_pnl = 2463.80
# ═══ CONFIGS ═══
configs = [
("A: Session RR", {
"session_rr_multipliers": {
"Sydney-Tokyo": 0.8,
"London Early": 1.0,
"London-NY Overlap (Golden)": 1.3,
"NY Session": 1.0,
},
}),
("B: ATR TP 3.0x", {
"atr_tp_mult": 3.0,
}),
("C: ATR TP 4.0x", {
"atr_tp_mult": 4.0,
}),
("D: Session + ATR floor", {
"session_rr_multipliers": {
"Sydney-Tokyo": 0.8,
"London Early": 1.0,
"London-NY Overlap (Golden)": 1.3,
"NY Session": 1.0,
},
"atr_tp_floor_mult": 2.5,
}),
("E: Tighter TP 0.8x", {
"tp_rr_multiplier": 0.8,
}),
]
all_results = []
for cfg_name, cfg_params in configs:
print(f"\n{'=' * 60}")
print(f" Config: {cfg_name}")
bt = DynamicRRBacktest(**cfg_params)
stats = bt.run(df=df, start_date=start_date, end_date=end_date, initial_capital=5000.0)
net_pnl = stats.total_profit - stats.total_loss
diff = net_pnl - baseline_28b_pnl
buy_trades = [t for t in stats.trades if t.direction == "BUY"]
sell_trades = [t for t in stats.trades if t.direction == "SELL"]
buy_wins = sum(1 for t in buy_trades if t.result == TradeResult.WIN)
sell_wins = sum(1 for t in sell_trades if t.result == TradeResult.WIN)
buy_wr = buy_wins / len(buy_trades) * 100 if buy_trades else 0
sell_wr = sell_wins / len(sell_trades) * 100 if sell_trades else 0
buy_pnl = sum(t.profit_usd for t in buy_trades)
sell_pnl = sum(t.profit_usd for t in sell_trades)
# TP hit rate
tp_hits = sum(1 for t in stats.trades if t.exit_reason == ExitReason.TAKE_PROFIT)
tp_rate = tp_hits / stats.total_trades * 100 if stats.total_trades > 0 else 0
# Avg RR
avg_rr = np.mean([t.rr_ratio for t in stats.trades]) if stats.trades else 0
print(f"\n [{cfg_name}] Results:")
print(f" Trades: {stats.total_trades} | WR: {stats.win_rate:.1f}%")
print(f" Net PnL: ${net_pnl:,.2f} | PF: {stats.profit_factor:.2f}")
print(f" Max DD: {stats.max_drawdown:.1f}% | Sharpe: {stats.sharpe_ratio:.2f}")
print(f" TP modified: {stats.tp_modified} | TP hit rate: {tp_rate:.1f}% | Avg RR: {avg_rr:.2f}")
print(f" BUY: {len(buy_trades)}, {buy_wr:.1f}% WR, ${buy_pnl:,.2f}")
print(f" SELL: {len(sell_trades)}, {sell_wr:.1f}% WR, ${sell_pnl:,.2f}")
print(f" vs #28B: ${diff:+,.2f}")
all_results.append((cfg_name, stats, net_pnl, diff, stats.tp_modified, tp_rate, avg_rr))
# ═══ FINAL SUMMARY ═══
print(f"\n{'=' * 70}")
print("#30 DYNAMIC RISK-REWARD — ALL CONFIGURATIONS")
print("=" * 70)
print(f"\n {'Config':<25} {'Trades':>6} {'WR':>6} {'Net PnL':>10} {'DD':>6} {'Sharpe':>7} {'PF':>5} {'TP%':>5} {'RR':>5} {'vs #28B':>10}")
print(f" {'-' * 95}")
print(f" {'#24B (base) ':<25} {'739':>6} {'80.4%':>6} {'$2,235':>10} {'3.4%':>6} {'2.87':>7} {'1.77':>5} {'3.5%':>5} {'1.6':>5} {'':>10}")
print(f" {'#28B (smart BE)':<25} {'741':>6} {'79.8%':>6} {'$2,464':>10} {'3.5%':>6} {'3.23':>7} {'1.83':>5} {'3.0%':>5} {'1.6':>5} {'':>10}")
for cfg_name, stats, net_pnl, diff, tp_mod, tp_rate, avg_rr in all_results:
print(f" {cfg_name:<25} {stats.total_trades:>6} {stats.win_rate:>5.1f}% ${net_pnl:>9,.2f} {stats.max_drawdown:>5.1f}% {stats.sharpe_ratio:>7.2f} {stats.profit_factor:>5.2f} {tp_rate:>4.1f}% {avg_rr:>5.2f} ${diff:>+9,.2f}")
best_pnl = -999999
best_name = ""
best_stats = None
for entry in all_results:
if entry[2] > best_pnl:
best_pnl = entry[2]
best_name = entry[0]
best_stats = entry[1]
print(f"\n Best config: {best_name}")
# Per-session analysis for best
print(f"\n Per-Session (best config):")
sessions = set(t.session for t in best_stats.trades)
for sess in sorted(sessions):
sess_trades = [t for t in best_stats.trades if t.session == sess]
sess_wins = sum(1 for t in sess_trades if t.result == TradeResult.WIN)
sess_wr = sess_wins / len(sess_trades) * 100 if sess_trades else 0
sess_pnl = sum(t.profit_usd for t in sess_trades)
print(f" {sess:30s}: {len(sess_trades):>4} trades, {sess_wr:>5.1f}% WR, ${sess_pnl:>8,.2f}")
# Exit reasons
print(f"\n Exit Reasons (best config):")
exit_counts = {}
for t in best_stats.trades:
r = t.exit_reason.value
exit_counts[r] = exit_counts.get(r, 0) + 1
for reason, count in sorted(exit_counts.items(), key=lambda x: -x[1]):
pct = count / best_stats.total_trades * 100 if best_stats.total_trades > 0 else 0
print(f" {reason:20s}: {count} ({pct:.1f}%)")
# Save
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "30_dynamic_rr_results")
os.makedirs(output_dir, exist_ok=True)
log_path = os.path.join(output_dir, f"dynamic_rr_{timestamp}.log")
with open(log_path, "w") as f:
f.write(f"#30 Dynamic Risk-Reward Results\n")
f.write(f"Generated: {datetime.now()}\n")
f.write(f"Base: #28B (741 trades, 79.8% WR, $2,464)\n\n")
for cfg_name, stats, net_pnl, diff, tp_mod, tp_rate, avg_rr in all_results:
f.write(f" {cfg_name}: {stats.total_trades} trades, {stats.win_rate:.1f}% WR, "
f"${net_pnl:,.2f}, DD: {stats.max_drawdown:.1f}%, "
f"Sharpe: {stats.sharpe_ratio:.2f}, PF: {stats.profit_factor:.2f}, "
f"TP modified: {tp_mod}, TP rate: {tp_rate:.1f}%, Avg RR: {avg_rr:.2f}, "
f"vs #28B: ${diff:+,.2f}\n")
f.write(f"\nBest: {best_name}\n")
print(f" Log saved: {log_path}")
try:
from backtests.backtest_01_smc_only import generate_xlsx_report as gen_xlsx
xlsx_path = os.path.join(output_dir, f"dynamic_rr_{timestamp}.xlsx")
gen_xlsx(best_stats, xlsx_path, start_date, end_date)
print(f"\n Report saved: {xlsx_path}")
except Exception as e:
print(f" [WARN] XLSX: {e}")
mt5.disconnect()
print(f"\n{'=' * 70}")
print(f"Output: {output_dir}")
print(f" Log: {os.path.basename(log_path)}")
print("=" * 70)
print("Backtest complete!")
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
+983
View File
@@ -0,0 +1,983 @@
"""
Backtest #32 — ML Exit Optimizer
==================================
Base: #31B (H1 Price vs EMA20) — 625 trades, 81.8% WR, $2,807, Sharpe 3.97
Idea: Use XGBoost ML predictions more aggressively for exit decisions.
Currently ML is only used for trend reversal detection in exits.
What if we:
- Lower the ML reversal confidence threshold (catch reversals earlier)
- Use ML to tighten trailing SL when ML opposes
- Use ML agreement to hold winners longer
Configs:
A: Lower ML reversal threshold (0.75 0.65) catch reversals earlier
B: ML-tightened trail (if ML opposes, use 2x ATR trail instead of 3x)
C: ML hold boost (if ML agrees, extend timeout from 1624 bars)
D: A+B combined (earlier reversal + tighter trail when opposed)
E: A+B+C combined (full ML exit optimization)
Usage:
python backtests/backtest_32_ml_exit_optimizer.py
"""
import polars as pl
import pandas as pd
import numpy as np
from datetime import datetime, timedelta, date
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass, field
from enum import Enum
import sys
import os
from zoneinfo import ZoneInfo
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.mt5_connector import MT5Connector
from src.smc_polars import SMCAnalyzer, SMCSignal
from src.feature_eng import FeatureEngineer
from src.regime_detector import MarketRegimeDetector, MarketRegime
from src.ml_model import TradingModel
from src.config import get_config
from src.dynamic_confidence import DynamicConfidenceManager, create_dynamic_confidence, MarketQuality
from loguru import logger
logger.remove()
logger.add(sys.stderr, level="WARNING")
WIB = ZoneInfo("Asia/Jakarta")
# ─── Enums & Dataclasses ──────────────────────────────────────
class TradeResult(Enum):
WIN = "WIN"
LOSS = "LOSS"
BREAKEVEN = "BREAKEVEN"
class ExitReason(Enum):
TAKE_PROFIT = "take_profit"
SMART_TP = "smart_tp"
PEAK_PROTECT = "peak_protect"
EARLY_EXIT = "early_exit"
EARLY_CUT = "early_cut"
MAX_LOSS = "max_loss"
STALL = "stall"
TREND_REVERSAL = "trend_reversal"
TIMEOUT = "timeout"
WEEKEND_CLOSE = "weekend_close"
TRAILING_SL = "trailing_sl"
BREAKEVEN_EXIT = "breakeven_exit"
DAILY_LIMIT = "daily_limit"
REGIME_DANGER = "regime_danger"
MARKET_SIGNAL = "market_signal"
class TradingMode(Enum):
NORMAL = "normal"
RECOVERY = "recovery"
PROTECTED = "protected"
STOPPED = "stopped"
@dataclass
class SimulatedTrade:
ticket: int
entry_time: datetime
exit_time: datetime
direction: str
entry_price: float
exit_price: float
stop_loss: float
take_profit: float
lot_size: float
profit_usd: float
profit_pips: float
result: TradeResult
exit_reason: ExitReason
smc_confidence: float
regime: str
session: str
signal_reason: str
has_bos: bool = False
has_choch: bool = False
has_fvg: bool = False
has_ob: bool = False
atr_at_entry: float = 0.0
rr_ratio: float = 0.0
trading_mode: str = "normal"
h1_trend: str = "NEUTRAL"
@dataclass
class BacktestStats:
total_trades: int = 0
wins: int = 0
losses: int = 0
total_profit: float = 0.0
total_loss: float = 0.0
max_drawdown: float = 0.0
max_drawdown_usd: float = 0.0
win_rate: float = 0.0
profit_factor: float = 0.0
avg_win: float = 0.0
avg_loss: float = 0.0
avg_trade: float = 0.0
expectancy: float = 0.0
sharpe_ratio: float = 0.0
trades: List[SimulatedTrade] = field(default_factory=list)
equity_curve: List[float] = field(default_factory=list)
avoided_signals: int = 0
daily_limit_stops: int = 0
recovery_mode_trades: int = 0
session_blocked: int = 0
h1_filtered: int = 0
# ─── ML Exit Optimizer Backtest ──────────────────────────────
class MLExitBacktest:
"""#31B base + ML exit optimization."""
def __init__(
self,
capital: float = 5000.0,
max_daily_loss_percent: float = 5.0,
max_loss_per_trade_percent: float = 1.0,
base_lot_size: float = 0.01,
max_lot_size: float = 0.02,
recovery_lot_size: float = 0.01,
max_concurrent_positions: int = 2,
min_profit_to_protect: float = 5.0,
max_drawdown_from_peak: float = 50.0,
trade_cooldown_bars: int = 10,
# #24B base
skip_tokyo_london: bool = True,
early_cut_momentum: float = -50.0,
early_cut_loss_pct: float = 30.0,
be_mult: float = 2.0,
trail_start_mult: float = 4.0,
trail_step_mult: float = 3.0,
# #28B: Smart breakeven
be_profit_lock_atr_mult: float = 0.5,
# ═══ #32 ML EXIT OPTIMIZER PARAMS ═══
ml_reversal_threshold: float = 0.75, # ML confidence to trigger reversal exit
ml_tighten_trail: bool = False, # If ML opposes, tighten trail step
ml_tighten_trail_mult: float = 2.0, # Tightened trail step multiplier (vs 3.0 default)
ml_hold_boost: bool = False, # If ML agrees, extend timeout
ml_hold_timeout_bars: int = 24, # Extended timeout when ML agrees
trend_reversal_mult: float = 0.6,
):
self.capital = capital
self.max_daily_loss_usd = capital * (max_daily_loss_percent / 100)
self.max_loss_per_trade = capital * (max_loss_per_trade_percent / 100)
self.base_lot_size = base_lot_size
self.max_lot_size = max_lot_size
self.recovery_lot_size = recovery_lot_size
self.max_concurrent_positions = max_concurrent_positions
self.min_profit_to_protect = min_profit_to_protect
self.max_drawdown_from_peak = max_drawdown_from_peak
self.trade_cooldown_bars = trade_cooldown_bars
self.trend_reversal_mult = trend_reversal_mult
self.skip_tokyo_london = skip_tokyo_london
self.early_cut_momentum = early_cut_momentum
self.early_cut_loss_pct = early_cut_loss_pct
self.be_mult = be_mult
self.trail_start_mult = trail_start_mult
self.trail_step_mult = trail_step_mult
self.be_profit_lock_atr_mult = be_profit_lock_atr_mult
# #32 params
self.ml_reversal_threshold = ml_reversal_threshold
self.ml_tighten_trail = ml_tighten_trail
self.ml_tighten_trail_mult = ml_tighten_trail_mult
self.ml_hold_boost = ml_hold_boost
self.ml_hold_timeout_bars = ml_hold_timeout_bars
config = get_config()
self.smc = SMCAnalyzer(swing_length=config.smc.swing_length, ob_lookback=config.smc.ob_lookback)
self.features = FeatureEngineer()
self.dynamic_confidence = create_dynamic_confidence()
self.ml_model = TradingModel(model_path="models/xgboost_model.pkl")
try:
self.ml_model.load()
print(" ML model loaded (for exit evaluation)")
except Exception:
print(" [WARN] ML model not loaded")
self.regime_detector = MarketRegimeDetector(model_path="models/hmm_regime.pkl")
try:
self.regime_detector.load()
except Exception:
print(" [WARN] HMM model not loaded")
self._ticket_counter = 2320000
def _get_session_from_time(self, dt):
if dt.tzinfo is None:
dt = dt.replace(tzinfo=ZoneInfo("UTC"))
wib_time = dt.astimezone(WIB)
hour = wib_time.hour
if 6 <= hour < 15:
return "Sydney-Tokyo", True, 0.5
elif 15 <= hour < 16:
if self.skip_tokyo_london:
return "Tokyo-London Overlap", False, 0.0
return "Tokyo-London Overlap", True, 0.75
elif 16 <= hour < 19:
return "London Early", True, 0.8
elif 19 <= hour < 24:
return "London-NY Overlap (Golden)", True, 1.0
elif 0 <= hour < 4:
return "NY Session", True, 0.9
else:
return "Off Hours", False, 0.0
def _hours_to_golden(self, dt):
if dt.tzinfo is None:
dt = dt.replace(tzinfo=ZoneInfo("UTC"))
wib = dt.astimezone(WIB)
if 19 <= wib.hour < 24:
return 0
target = wib.replace(hour=19, minute=0, second=0, microsecond=0)
if wib.hour >= 19:
target += timedelta(days=1)
return max(0, (target - wib).total_seconds() / 3600)
def _is_near_weekend_close(self, dt):
if dt.tzinfo is None:
dt = dt.replace(tzinfo=ZoneInfo("UTC"))
wib = dt.astimezone(WIB)
return wib.weekday() == 5 and wib.hour >= 4 and wib.minute >= 30
def _calculate_lot_size(self, confidence, regime, trading_mode, session_mult):
if trading_mode == TradingMode.STOPPED:
return 0
lot = self.base_lot_size
if trading_mode in (TradingMode.RECOVERY, TradingMode.PROTECTED):
lot = self.recovery_lot_size
else:
if confidence >= 0.65:
lot = self.max_lot_size
elif confidence >= 0.55:
lot = self.base_lot_size
else:
lot = self.recovery_lot_size
if regime.lower() in ["high_volatility", "crisis"]:
lot = self.recovery_lot_size
lot = max(0.01, lot * session_mult)
return round(lot, 2)
def _calc_ema(self, data, period):
if len(data) < period:
return data[-1] if data else 0
multiplier = 2 / (period + 1)
ema = np.mean(data[:period])
for val in data[period:]:
ema = (val - ema) * multiplier + ema
return ema
def _get_h1_trend(self, df_h1_slice):
"""#31B: H1 Price vs EMA20."""
if df_h1_slice is None or len(df_h1_slice) < 20:
return "NEUTRAL"
closes = df_h1_slice["close"].to_list()
ema20 = self._calc_ema(closes, 20)
current_price = closes[-1]
if current_price > ema20 * 1.001:
return "BULLISH"
elif current_price < ema20 * 0.999:
return "BEARISH"
return "NEUTRAL"
def _simulate_trade_exit(
self, df, entry_idx, direction, entry_price, take_profit, stop_loss,
lot_size, daily_loss_so_far, feature_cols, max_bars=100,
):
pip_value = 10
highs = df["high"].to_list()
lows = df["low"].to_list()
closes = df["close"].to_list()
times = df["time"].to_list()
atr = 12.0
if "atr" in df.columns:
atr_list = df["atr"].to_list()
if entry_idx < len(atr_list) and atr_list[entry_idx] is not None:
atr = atr_list[entry_idx]
adaptive_breakeven_pips = atr * self.be_mult
adaptive_trail_start_pips = atr * self.trail_start_mult
adaptive_trail_step_pips = atr * self.trail_step_mult
# #32: ML-tightened trail step
tightened_trail_step_pips = atr * self.ml_tighten_trail_mult
reversal_momentum_threshold = atr * self.trend_reversal_mult
min_loss_for_reversal_exit = atr * 0.8
if self.be_profit_lock_atr_mult > 0:
be_lock_distance = atr * self.be_profit_lock_atr_mult
else:
be_lock_distance = 2.0
profit_history = []
peak_profit = 0.0
stall_count = 0
reversal_warnings = 0
current_sl = stop_loss
breakeven_moved = False
if direction == "BUY":
target_tp_profit = (take_profit - entry_price) / 0.1 * pip_value * lot_size
else:
target_tp_profit = (entry_price - take_profit) / 0.1 * pip_value * lot_size
cached_ml_signal = ""
cached_ml_confidence = 0.5
ml_agrees_with_trade = False
for i in range(entry_idx + 1, min(entry_idx + max_bars, len(df))):
high = highs[i]
low = lows[i]
close = closes[i]
current_time = times[i]
if direction == "BUY":
current_pips = (close - entry_price) / 0.1
pip_profit_from_entry = current_pips
else:
current_pips = (entry_price - close) / 0.1
pip_profit_from_entry = current_pips
current_profit = current_pips * pip_value * lot_size
profit_history.append(current_profit)
if current_profit > peak_profit:
peak_profit = current_profit
bars_since_entry = i - entry_idx
if bars_since_entry % 4 == 0 and self.ml_model.fitted:
try:
df_slice = df.head(i + 1)
ml_pred = self.ml_model.predict(df_slice, feature_cols)
cached_ml_signal = ml_pred.signal
cached_ml_confidence = ml_pred.confidence
# #32: Track ML agreement
ml_agrees_with_trade = (
(direction == "BUY" and cached_ml_signal == "BUY") or
(direction == "SELL" and cached_ml_signal == "SELL")
)
except Exception:
pass
momentum = 0.0
if len(profit_history) >= 3:
recent = profit_history[-5:] if len(profit_history) >= 5 else profit_history
profit_change = recent[-1] - recent[0]
momentum = max(-100, min(100, (profit_change / 10) * 50))
profit_growing = momentum > 0
# A.0 TP hit
if direction == "BUY" and high >= take_profit:
pips = (take_profit - entry_price) / 0.1
return pips * pip_value * lot_size, pips, ExitReason.TAKE_PROFIT, i, take_profit
elif direction == "SELL" and low <= take_profit:
pips = (entry_price - take_profit) / 0.1
return pips * pip_value * lot_size, pips, ExitReason.TAKE_PROFIT, i, take_profit
# A.0b Trailing SL hit
if breakeven_moved and current_sl > 0:
if direction == "BUY" and low <= current_sl:
pips = (current_sl - entry_price) / 0.1
reason = ExitReason.TRAILING_SL if pip_profit_from_entry >= adaptive_trail_start_pips else ExitReason.BREAKEVEN_EXIT
return pips * pip_value * lot_size, pips, reason, i, current_sl
elif direction == "SELL" and high >= current_sl:
pips = (entry_price - current_sl) / 0.1
reason = ExitReason.TRAILING_SL if pip_profit_from_entry >= adaptive_trail_start_pips else ExitReason.BREAKEVEN_EXIT
return pips * pip_value * lot_size, pips, reason, i, current_sl
# A.1 Breakeven (#28B: Smart)
if pip_profit_from_entry >= adaptive_breakeven_pips and not breakeven_moved:
if direction == "BUY":
current_sl = entry_price + be_lock_distance
else:
current_sl = entry_price - be_lock_distance
breakeven_moved = True
# A.2 Trailing SL (#32: tighter when ML opposes)
if pip_profit_from_entry >= adaptive_trail_start_pips:
# #32B: Use tighter trail if ML opposes the trade direction
if self.ml_tighten_trail and not ml_agrees_with_trade and cached_ml_confidence >= 0.6:
active_trail_step = tightened_trail_step_pips
else:
active_trail_step = adaptive_trail_step_pips
trail_distance = active_trail_step * 0.1
if direction == "BUY":
new_trail_sl = close - trail_distance
if new_trail_sl > current_sl:
current_sl = new_trail_sl
else:
new_trail_sl = close + trail_distance
if current_sl == 0 or new_trail_sl < current_sl:
current_sl = new_trail_sl
# A.3 Peak protect
if peak_profit > self.min_profit_to_protect:
drawdown_pct = ((peak_profit - current_profit) / peak_profit) * 100 if peak_profit > 0 else 0
if drawdown_pct > self.max_drawdown_from_peak:
return current_profit, current_pips, ExitReason.PEAK_PROTECT, i, close
# A.4 Market analysis
if bars_since_entry % 5 == 0 and bars_since_entry >= 5 and i >= 20:
ma_fast = np.mean(closes[i-4:i+1])
ma_slow = np.mean(closes[i-19:i+1])
trend = "BULLISH" if ma_fast > ma_slow * 1.001 else ("BEARISH" if ma_fast < ma_slow * 0.999 else "NEUTRAL")
roc = (closes[i] / closes[max(0,i-4)] - 1) * 100
mom_dir = "BULLISH" if roc > 0.3 else ("BEARISH" if roc < -0.3 else "NEUTRAL")
rsi_val = None
if "rsi" in df.columns:
rsi_list = df["rsi"].to_list()
if i < len(rsi_list):
rsi_val = rsi_list[i]
urgency = 0
should_exit = False
if cached_ml_confidence > 0.75:
if (direction == "BUY" and cached_ml_signal == "SELL") or (direction == "SELL" and cached_ml_signal == "BUY"):
should_exit = True; urgency += 2
if rsi_val:
if (rsi_val > 75 and direction == "BUY") or (rsi_val < 25 and direction == "SELL"):
should_exit = True; urgency += 2
if (direction == "BUY" and trend == "BEARISH" and mom_dir == "BEARISH") or \
(direction == "SELL" and trend == "BULLISH" and mom_dir == "BULLISH"):
should_exit = True; urgency += 3
if should_exit and current_profit > self.min_profit_to_protect / 2:
return current_profit, current_pips, ExitReason.MARKET_SIGNAL, i, close
if urgency >= 7 and current_profit > 0:
return current_profit, current_pips, ExitReason.MARKET_SIGNAL, i, close
# A.5 Weekend close
if self._is_near_weekend_close(current_time):
if current_profit > 0 or current_profit > -10:
return current_profit, current_pips, ExitReason.WEEKEND_CLOSE, i, close
# B.1 Smart TP
if current_profit >= 15:
if current_profit >= 40:
return current_profit, current_pips, ExitReason.SMART_TP, i, close
if current_profit >= 25 and momentum < -30:
return current_profit, current_pips, ExitReason.SMART_TP, i, close
if peak_profit > 30 and current_profit < peak_profit * 0.6:
return current_profit, current_pips, ExitReason.PEAK_PROTECT, i, close
if current_profit >= 20:
progress = (current_profit / target_tp_profit) * 100 if target_tp_profit > 0 else 0
progress_score = min(40, max(0, progress * 0.4))
momentum_score = ((momentum + 100) / 200) * 30
time_penalty = min(10, bars_since_entry / 4 * 2)
tp_probability = progress_score + momentum_score + 10 - time_penalty
if tp_probability < 25:
return current_profit, current_pips, ExitReason.SMART_TP, i, close
# B.2 Smart Early Exit
if 5 <= current_profit < 15:
if momentum < -50 and cached_ml_confidence >= 0.65:
is_reversal = (direction == "BUY" and cached_ml_signal == "SELL") or (direction == "SELL" and cached_ml_signal == "BUY")
if is_reversal:
return current_profit, current_pips, ExitReason.EARLY_EXIT, i, close
# B.3 Early cut
if current_profit < 0:
loss_percent_of_max = abs(current_profit) / self.max_loss_per_trade * 100
if momentum < self.early_cut_momentum and loss_percent_of_max >= self.early_cut_loss_pct:
return current_profit, current_pips, ExitReason.EARLY_CUT, i, close
# B.4 Trend Reversal (#32A: configurable ML threshold)
is_ml_reversal = False
if (direction == "BUY" and cached_ml_signal == "SELL" and cached_ml_confidence >= self.ml_reversal_threshold) or \
(direction == "SELL" and cached_ml_signal == "BUY" and cached_ml_confidence >= self.ml_reversal_threshold):
is_ml_reversal = True
reversal_warnings += 1
loss_moderate = abs(current_profit) > (self.max_loss_per_trade * 0.4)
if is_ml_reversal and current_profit < -8 and loss_moderate:
return current_profit, current_pips, ExitReason.TREND_REVERSAL, i, close
if reversal_warnings >= 3 and current_profit < -10:
return current_profit, current_pips, ExitReason.TREND_REVERSAL, i, close
# B.5 Max loss
if current_profit <= -(self.max_loss_per_trade * 0.50):
htg = self._hours_to_golden(current_time)
if htg <= 1 and htg > 0 and momentum > -40:
pass
else:
return current_profit, current_pips, ExitReason.MAX_LOSS, i, close
# B.6 Stall
if len(profit_history) >= 10:
recent_range = max(profit_history[-10:]) - min(profit_history[-10:])
if recent_range < 3 and current_profit < -15:
stall_count += 1
if stall_count >= 5:
return current_profit, current_pips, ExitReason.STALL, i, close
# B.7 Daily loss limit
potential_daily_loss = daily_loss_so_far + abs(min(0, current_profit))
if potential_daily_loss >= self.max_daily_loss_usd:
return current_profit, current_pips, ExitReason.DAILY_LIMIT, i, close
# C) Time-based (#32C: ML hold boost extends timeout)
base_timeout = 16
if self.ml_hold_boost and ml_agrees_with_trade and cached_ml_confidence >= 0.6:
base_timeout = self.ml_hold_timeout_bars
if bars_since_entry >= base_timeout and current_profit < 5 and not profit_growing:
if current_profit >= 0 or current_profit > -15:
return current_profit, current_pips, ExitReason.TIMEOUT, i, close
if bars_since_entry >= 24 and (current_profit < 10 or not profit_growing):
return current_profit, current_pips, ExitReason.TIMEOUT, i, close
if bars_since_entry >= 32:
return current_profit, current_pips, ExitReason.TIMEOUT, i, close
# C.2 ATR trend reversal
if bars_since_entry > 10:
recent_closes = closes[i-5:i+1]
mom = recent_closes[-1] - recent_closes[0]
if (direction == "BUY" and mom < -reversal_momentum_threshold) or \
(direction == "SELL" and mom > reversal_momentum_threshold):
if current_profit < -min_loss_for_reversal_exit:
return current_profit, current_pips, ExitReason.TREND_REVERSAL, i, close
final_idx = min(entry_idx + max_bars - 1, len(df) - 1)
final_price = closes[final_idx]
pips = ((final_price - entry_price) if direction == "BUY" else (entry_price - final_price)) / 0.1
return pips * pip_value * lot_size, pips, ExitReason.TIMEOUT, final_idx, final_price
def run(self, df_m15, df_h1, start_date=None, end_date=None, initial_capital=5000.0):
stats = BacktestStats()
capital = initial_capital
peak_capital = initial_capital
stats.equity_curve.append(capital)
daily_loss = 0.0
daily_profit = 0.0
daily_trades = 0
consecutive_losses = 0
trading_mode = TradingMode.NORMAL
current_date = None
feature_cols = []
if self.ml_model.fitted and self.ml_model.feature_names:
feature_cols = [f for f in self.ml_model.feature_names if f in df_m15.columns]
times_m15 = df_m15["time"].to_list()
times_h1 = df_h1["time"].to_list() if df_h1 is not None else []
start_idx = next((i for i, t in enumerate(times_m15) if t >= start_date), 100) if start_date else 100
end_idx = next((i for i, t in enumerate(times_m15) if t > end_date), len(df_m15) - 100) if end_date else len(df_m15) - 100
last_trade_idx = -self.trade_cooldown_bars * 2
print(f" #32 ML reversal threshold: {self.ml_reversal_threshold}, tighten trail: {self.ml_tighten_trail}, hold boost: {self.ml_hold_boost}")
print(f" Date range: {times_m15[start_idx]} to {times_m15[end_idx - 1]}")
print(f" Total bars: {end_idx - start_idx}")
for i in range(start_idx, end_idx):
if i - last_trade_idx < self.trade_cooldown_bars:
continue
current_time = times_m15[i]
trade_date = current_time.date() if hasattr(current_time, 'date') else current_time
if current_date is None or trade_date != current_date:
daily_loss = 0.0
daily_profit = 0.0
daily_trades = 0
current_date = trade_date
if consecutive_losses < 2:
trading_mode = TradingMode.NORMAL
if trading_mode == TradingMode.STOPPED:
continue
session_name, can_trade, lot_mult = self._get_session_from_time(current_time)
if not can_trade:
if session_name == "Tokyo-London Overlap":
stats.session_blocked += 1
continue
if hasattr(current_time, 'weekday') and current_time.weekday() >= 5:
continue
df_slice = df_m15.head(i + 1)
regime = "normal"
try:
if self.regime_detector.fitted:
regime_state = self.regime_detector.get_current_state(df_slice)
if regime_state:
regime = regime_state.regime.value
if regime_state.regime == MarketRegime.CRISIS:
continue
if regime_state.recommendation == "SLEEP":
continue
except Exception:
pass
try:
ml_signal = ""
ml_confidence = 0.5
if self.ml_model.fitted and feature_cols:
ml_pred = self.ml_model.predict(df_slice, feature_cols)
ml_signal = ml_pred.signal
ml_confidence = ml_pred.confidence
market_analysis = self.dynamic_confidence.analyze_market(
session=session_name, regime=regime, volatility="medium",
trend_direction=regime, has_smc_signal=True,
ml_signal=ml_signal, ml_confidence=ml_confidence,
)
if market_analysis.quality == MarketQuality.AVOID:
stats.avoided_signals += 1
continue
except Exception:
pass
try:
smc_signal = self.smc.generate_signal(df_slice)
except Exception:
continue
if smc_signal is None:
continue
# #31B: H1 Price vs EMA20 filter
h1_trend = "NEUTRAL"
if df_h1 is not None and len(times_h1) > 0:
h1_idx = 0
for j, t in enumerate(times_h1):
if t <= current_time:
h1_idx = j
else:
break
if h1_idx > 20:
df_h1_slice = df_h1.head(h1_idx + 1)
h1_trend = self._get_h1_trend(df_h1_slice)
# Strict H1 filter: signal must match H1 trend direction
if smc_signal.signal_type == "BUY" and h1_trend != "BULLISH":
stats.h1_filtered += 1
continue
if smc_signal.signal_type == "SELL" and h1_trend != "BEARISH":
stats.h1_filtered += 1
continue
recent_df = df_slice.tail(10)
recent_bos = recent_df["bos"].to_list() if "bos" in df_slice.columns else []
recent_choch = recent_df["choch"].to_list() if "choch" in df_slice.columns else []
recent_fvg_bull = recent_df["is_fvg_bull"].to_list() if "is_fvg_bull" in df_slice.columns else []
recent_fvg_bear = recent_df["is_fvg_bear"].to_list() if "is_fvg_bear" in df_slice.columns else []
recent_obs = recent_df["ob"].to_list() if "ob" in df_slice.columns else []
has_bos = 1 in recent_bos or -1 in recent_bos
has_choch = 1 in recent_choch or -1 in recent_choch
has_fvg = any(recent_fvg_bull) or any(recent_fvg_bear)
has_ob = 1 in recent_obs or -1 in recent_obs
atr_at_entry = 12.0
if "atr" in df_slice.columns:
atr_val = df_slice.tail(1)["atr"].item()
if atr_val is not None and atr_val > 0:
atr_at_entry = atr_val
confidence = smc_signal.confidence
ml_agrees = (smc_signal.signal_type == "BUY" and ml_signal == "BUY") or \
(smc_signal.signal_type == "SELL" and ml_signal == "SELL")
if ml_agrees:
confidence = (smc_signal.confidence + ml_confidence) / 2
if regime == "high_volatility":
confidence *= 0.9
lot_size = self._calculate_lot_size(confidence, regime, trading_mode, lot_mult)
if lot_size <= 0:
continue
if trading_mode == TradingMode.RECOVERY:
stats.recovery_mode_trades += 1
entry_price = smc_signal.entry_price
take_profit_price = smc_signal.take_profit
stop_loss_price = smc_signal.stop_loss
risk = abs(entry_price - stop_loss_price)
rr = abs(take_profit_price - entry_price) / risk if risk > 0 else 0
profit, pips, exit_reason, exit_idx, exit_price = self._simulate_trade_exit(
df=df_m15, entry_idx=i, direction=smc_signal.signal_type,
entry_price=entry_price, take_profit=take_profit_price,
stop_loss=stop_loss_price, lot_size=lot_size,
daily_loss_so_far=daily_loss, feature_cols=feature_cols,
)
self._ticket_counter += 1
result = TradeResult.WIN if profit > 0 else (TradeResult.LOSS if profit < 0 else TradeResult.BREAKEVEN)
trade = SimulatedTrade(
ticket=self._ticket_counter,
entry_time=current_time,
exit_time=times_m15[exit_idx] if exit_idx < len(times_m15) else times_m15[-1],
direction=smc_signal.signal_type,
entry_price=entry_price, exit_price=exit_price,
stop_loss=stop_loss_price, take_profit=take_profit_price,
lot_size=lot_size, profit_usd=profit, profit_pips=pips,
result=result, exit_reason=exit_reason,
smc_confidence=confidence, regime=regime,
session=session_name, signal_reason=smc_signal.reason,
has_bos=has_bos, has_choch=has_choch,
has_fvg=has_fvg, has_ob=has_ob,
atr_at_entry=atr_at_entry, rr_ratio=rr,
trading_mode=trading_mode.value,
h1_trend=h1_trend,
)
stats.trades.append(trade)
stats.total_trades += 1
daily_trades += 1
capital += profit
if profit > 0:
stats.wins += 1
stats.total_profit += profit
daily_profit += profit
consecutive_losses = 0
if trading_mode == TradingMode.RECOVERY:
trading_mode = TradingMode.NORMAL
else:
stats.losses += 1
stats.total_loss += abs(profit)
daily_loss += abs(profit)
consecutive_losses += 1
if daily_loss >= self.max_daily_loss_usd:
trading_mode = TradingMode.STOPPED
stats.daily_limit_stops += 1
elif consecutive_losses >= 3 or daily_loss >= self.max_daily_loss_usd * 0.6:
trading_mode = TradingMode.PROTECTED
elif consecutive_losses >= 2:
trading_mode = TradingMode.RECOVERY
if capital > peak_capital:
peak_capital = capital
drawdown_pct = (peak_capital - capital) / peak_capital * 100
drawdown_usd = peak_capital - capital
if drawdown_pct > stats.max_drawdown:
stats.max_drawdown = drawdown_pct
stats.max_drawdown_usd = drawdown_usd
stats.equity_curve.append(capital)
last_trade_idx = exit_idx
if stats.total_trades % 100 == 0:
print(f" {stats.total_trades} trades processed...")
if stats.total_trades > 0:
stats.win_rate = stats.wins / stats.total_trades * 100
stats.avg_win = stats.total_profit / stats.wins if stats.wins > 0 else 0
stats.avg_loss = stats.total_loss / stats.losses if stats.losses > 0 else 0
stats.avg_trade = (stats.total_profit - stats.total_loss) / stats.total_trades
stats.profit_factor = stats.total_profit / stats.total_loss if stats.total_loss > 0 else float("inf")
win_prob = stats.wins / stats.total_trades
loss_prob = stats.losses / stats.total_trades
stats.expectancy = (win_prob * stats.avg_win) - (loss_prob * stats.avg_loss)
returns = [t.profit_usd for t in stats.trades]
if len(returns) > 1:
avg_return = np.mean(returns)
std_return = np.std(returns)
stats.sharpe_ratio = (avg_return / std_return) * np.sqrt(252) if std_return > 0 else 0
return stats
# ─── Main ──────────────────────────────────────────────────────
def main():
print("=" * 70)
print("XAUBOT AI — #32 ML Exit Optimizer")
print("Base: #31B (H1 Price vs EMA20) | Modified: ML-enhanced exits")
print("=" * 70)
config = get_config()
mt5_conn = MT5Connector(
login=config.mt5_login, password=config.mt5_password,
server=config.mt5_server, path=config.mt5_path,
)
mt5_conn.connect()
print(f"\nConnected to MT5")
print("Fetching XAUUSD M15 historical data...")
df_m15 = mt5_conn.get_market_data(symbol="XAUUSD", timeframe="M15", count=50000)
print(f" M15: {len(df_m15)} bars")
print("Fetching XAUUSD H1 historical data...")
df_h1 = mt5_conn.get_market_data(symbol="XAUUSD", timeframe="H1", count=15000)
print(f" H1: {len(df_h1)} bars")
times = df_m15["time"].to_list()
print(f" M15 range: {times[0]} to {times[-1]}")
end_date = datetime.now()
start_date = datetime(2025, 8, 1)
data_start = times[0]
if hasattr(data_start, 'replace') and data_start.tzinfo:
start_date = start_date.replace(tzinfo=data_start.tzinfo)
end_date = end_date.replace(tzinfo=data_start.tzinfo)
if data_start > start_date:
start_date = data_start + timedelta(days=5)
print(f"\n Backtest period: {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}")
print("\nCalculating M15 indicators...")
features = FeatureEngineer()
smc = SMCAnalyzer(swing_length=config.smc.swing_length, ob_lookback=config.smc.ob_lookback)
df_m15 = features.calculate_all(df_m15, include_ml_features=True)
df_m15 = smc.calculate_all(df_m15)
regime_detector = MarketRegimeDetector(model_path="models/hmm_regime.pkl")
try:
regime_detector.load()
df_m15 = regime_detector.predict(df_m15)
print(" HMM regime loaded")
except Exception:
print(" [WARN] HMM not available")
print("Calculating H1 indicators...")
df_h1 = features.calculate_all(df_h1, include_ml_features=False)
print(" All indicators calculated")
baseline_31b_pnl = 2806.56
# ═══ CONFIGS ═══
configs = [
("A: ML reversal 0.65", {
"ml_reversal_threshold": 0.65,
}),
("B: ML tighten trail", {
"ml_tighten_trail": True,
"ml_tighten_trail_mult": 2.0,
}),
("C: ML hold boost", {
"ml_hold_boost": True,
"ml_hold_timeout_bars": 24,
}),
("D: A+B combined", {
"ml_reversal_threshold": 0.65,
"ml_tighten_trail": True,
"ml_tighten_trail_mult": 2.0,
}),
("E: A+B+C all", {
"ml_reversal_threshold": 0.65,
"ml_tighten_trail": True,
"ml_tighten_trail_mult": 2.0,
"ml_hold_boost": True,
"ml_hold_timeout_bars": 24,
}),
]
all_results = []
for cfg_name, cfg_params in configs:
print(f"\n{'=' * 60}")
print(f" Config: {cfg_name}")
bt = MLExitBacktest(**cfg_params)
stats = bt.run(df_m15=df_m15, df_h1=df_h1, start_date=start_date, end_date=end_date, initial_capital=5000.0)
net_pnl = stats.total_profit - stats.total_loss
diff = net_pnl - baseline_31b_pnl
buy_trades = [t for t in stats.trades if t.direction == "BUY"]
sell_trades = [t for t in stats.trades if t.direction == "SELL"]
buy_wins = sum(1 for t in buy_trades if t.result == TradeResult.WIN)
sell_wins = sum(1 for t in sell_trades if t.result == TradeResult.WIN)
buy_wr = buy_wins / len(buy_trades) * 100 if buy_trades else 0
sell_wr = sell_wins / len(sell_trades) * 100 if sell_trades else 0
buy_pnl = sum(t.profit_usd for t in buy_trades)
sell_pnl = sum(t.profit_usd for t in sell_trades)
print(f"\n [{cfg_name}] Results:")
print(f" Trades: {stats.total_trades} | WR: {stats.win_rate:.1f}%")
print(f" Net PnL: ${net_pnl:,.2f} | PF: {stats.profit_factor:.2f}")
print(f" Max DD: {stats.max_drawdown:.1f}% | Sharpe: {stats.sharpe_ratio:.2f}")
print(f" BUY: {len(buy_trades)}, {buy_wr:.1f}% WR, ${buy_pnl:,.2f}")
print(f" SELL: {len(sell_trades)}, {sell_wr:.1f}% WR, ${sell_pnl:,.2f}")
print(f" vs #31B: ${diff:+,.2f}")
all_results.append((cfg_name, stats, net_pnl, diff))
# ═══ FINAL SUMMARY ═══
print(f"\n{'=' * 70}")
print("#32 ML EXIT OPTIMIZER — ALL CONFIGURATIONS")
print("=" * 70)
print(f"\n {'Config':<25} {'Trades':>6} {'WR':>6} {'Net PnL':>10} {'DD':>6} {'Sharpe':>7} {'PF':>5} {'vs #31B':>10}")
print(f" {'-' * 85}")
print(f" {'#24B (base) ':<25} {'739':>6} {'80.4%':>6} {'$2,235':>10} {'3.4%':>6} {'2.87':>7} {'1.77':>5} {'':>10}")
print(f" {'#28B (smart BE)':<25} {'741':>6} {'79.8%':>6} {'$2,464':>10} {'3.5%':>6} {'3.23':>7} {'1.83':>5} {'':>10}")
print(f" {'#31B (H1 filter)':<25} {'625':>6} {'81.8%':>6} {'$2,807':>10} {'2.5%':>6} {'3.97':>7} {'2.19':>5} {'':>10}")
for cfg_name, stats, net_pnl, diff in all_results:
print(f" {cfg_name:<25} {stats.total_trades:>6} {stats.win_rate:>5.1f}% ${net_pnl:>9,.2f} {stats.max_drawdown:>5.1f}% {stats.sharpe_ratio:>7.2f} {stats.profit_factor:>5.2f} ${diff:>+9,.2f}")
best_pnl = -999999
best_name = ""
best_stats = None
for entry in all_results:
if entry[2] > best_pnl:
best_pnl = entry[2]
best_name = entry[0]
best_stats = entry[1]
print(f"\n Best config: {best_name}")
# Exit reasons
print(f"\n Exit Reasons (best config):")
exit_counts = {}
for t in best_stats.trades:
r = t.exit_reason.value
exit_counts[r] = exit_counts.get(r, 0) + 1
for reason, count in sorted(exit_counts.items(), key=lambda x: -x[1]):
pct = count / best_stats.total_trades * 100 if best_stats.total_trades > 0 else 0
print(f" {reason:20s}: {count} ({pct:.1f}%)")
# Save
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "32_ml_exit_results")
os.makedirs(output_dir, exist_ok=True)
log_path = os.path.join(output_dir, f"ml_exit_{timestamp}.log")
with open(log_path, "w") as f:
f.write(f"#32 ML Exit Optimizer Results\n")
f.write(f"Generated: {datetime.now()}\n")
f.write(f"Base: #31B (625 trades, 81.8% WR, $2,807)\n\n")
for cfg_name, stats, net_pnl, diff in all_results:
f.write(f" {cfg_name}: {stats.total_trades} trades, {stats.win_rate:.1f}% WR, "
f"${net_pnl:,.2f}, DD: {stats.max_drawdown:.1f}%, "
f"Sharpe: {stats.sharpe_ratio:.2f}, PF: {stats.profit_factor:.2f}, "
f"vs #31B: ${diff:+,.2f}\n")
f.write(f"\nBest: {best_name}\n")
print(f" Log saved: {log_path}")
try:
from backtests.backtest_01_smc_only import generate_xlsx_report as gen_xlsx
xlsx_path = os.path.join(output_dir, f"ml_exit_{timestamp}.xlsx")
gen_xlsx(best_stats, xlsx_path, start_date, end_date)
print(f"\n Report saved: {xlsx_path}")
except Exception as e:
print(f" [WARN] XLSX: {e}")
mt5_conn.disconnect()
print(f"\n{'=' * 70}")
print(f"Output: {output_dir}")
print(f" Log: {os.path.basename(log_path)}")
print("=" * 70)
print("Backtest complete!")
if __name__ == "__main__":
main()
+6 -4
View File
@@ -4,12 +4,14 @@ Backtest Live Sync - 100% Identical to main_live.py
This backtest MUST be identical to live trading logic.
SYNCED with Critical & Major Fixes (Feb 2025):
1. SMC Signal: No lookahead bias, current_close entry, min RR 2.0
1. SMC Signal: No lookahead bias, current_close entry, Fixed RR 1:1.5
2. Pullback Filter: ATR-based thresholds (not hardcoded $2, $1.5)
3. Time-Based Exit: Checks profit_growing + ML agreement before exit
4. Trend Reversal: ATR-based momentum thresholds
4. Trend Reversal: ATR-based momentum thresholds (0.6x multiplier)
5. Signal Persistence: Index-based cleanup (prevents memory leak)
6. Calibrated Confidence: Uses SMC's weighted confidence calculation
7. Dynamic RR: 1.5 (ranging) to 2.0 (strong trend) based on market conditions
8. SELL Filter: Requires ML agreement + 55% confidence
Synchronized elements:
1. ML Model: XGBoost with same features, 50-bar train/test gap
@@ -25,9 +27,9 @@ Synchronized elements:
6. Position Sizing: Based on ML confidence tiers (0.01-0.02 lot)
7. Trade Cooldown: 20 bars (~5 hours on M15)
8. Exit Logic:
- TP hit (RR 1:2 enforced)
- TP hit (Dynamic RR 1.5-2.0)
- ML reversal (>65% opposite signal)
- Trend reversal (ATR-based momentum shift)
- Trend reversal (ATR * 0.6 momentum shift)
- Smart timeout (checks profit_growing before exit)
- Max loss per trade ($50 default)
+64
View File
@@ -0,0 +1,64 @@
@echo off
REM Add Dashboard & API to existing Docker setup
echo.
echo ========================================
echo Adding Dashboard to Existing Setup
echo ========================================
echo.
REM Check if .env exists
if not exist .env (
echo WARNING: .env file not found!
echo Creating .env from template...
copy .env.docker.example .env
echo.
echo Please edit .env with your MT5 credentials:
echo - MT5_LOGIN
echo - MT5_PASSWORD
echo - MT5_SERVER
echo - MT5_PATH
echo.
pause
)
REM Check if Docker is running
docker info >nul 2>&1
if errorlevel 1 (
echo ERROR: Docker is not running!
echo Please start Docker Desktop and try again.
pause
exit /b 1
)
echo [1/4] Checking existing services...
docker ps --filter "name=trading_bot_db" --format "table {{.Names}}\t{{.Status}}"
echo.
echo [2/4] Building new services API and Dashboard...
docker-compose build trading-api dashboard
echo.
echo [3/4] Starting new services...
docker-compose up -d trading-api dashboard
echo.
echo [4/4] Checking all services...
docker-compose ps
echo.
echo ========================================
echo Dashboard Added Successfully!
echo ========================================
echo.
echo Access Points:
echo Dashboard: http://localhost:3000
echo API: http://localhost:8000
echo API Docs: http://localhost:8000/docs
echo Database: localhost:5432 (already running)
echo.
echo View logs:
echo docker-compose logs -f dashboard
echo docker-compose logs -f trading-api
echo.
pause
+54 -4
View File
@@ -1,5 +1,3 @@
version: '3.8'
services:
# PostgreSQL Database
postgres:
@@ -21,8 +19,57 @@ services:
interval: 10s
timeout: 5s
retries: 5
networks:
- trading_bot_network
# pgAdmin (Optional - for database management)
# Trading Bot API (reads bot_status.json from shared volume)
trading-api:
build:
context: .
dockerfile: Dockerfile
container_name: trading_bot_api
restart: unless-stopped
environment:
TZ: Asia/Jakarta
ports:
- "${API_PORT:-8000}:8000"
volumes:
# Mount data/ so API can read bot_status.json written by the bot on host
- ./data:/app/data:ro
depends_on:
postgres:
condition: service_healthy
networks:
- trading_bot_network
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
# Web Dashboard (Next.js Frontend)
dashboard:
build:
context: ./web-dashboard
dockerfile: Dockerfile
container_name: trading_bot_dashboard
restart: unless-stopped
ports:
- "${DASHBOARD_PORT:-3000}:3000"
depends_on:
trading-api:
condition: service_healthy
networks:
- trading_bot_network
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
# pgAdmin (OPTIONAL - for database management)
pgadmin:
image: dpage/pgadmin4:latest
container_name: trading_bot_pgadmin
@@ -38,6 +85,8 @@ services:
depends_on:
postgres:
condition: service_healthy
networks:
- trading_bot_network
profiles:
- admin # Only start with: docker-compose --profile admin up
@@ -48,5 +97,6 @@ volumes:
name: trading_bot_pgadmin_data
networks:
default:
trading_bot_network:
name: trading_bot_network
driver: bridge
+18
View File
@@ -0,0 +1,18 @@
@echo off
REM XAUBot AI - Docker Logs Viewer (Windows)
set SERVICE=%1
set LINES=%2
if "%LINES%"=="" set LINES=100
if "%SERVICE%"=="" (
echo Viewing logs for all services...
echo Tip: Use 'docker-logs.bat SERVICE [LINES]' to view specific service
echo Available: trading-api, dashboard, postgres, pgadmin
echo.
docker-compose logs -f --tail=%LINES%
) else (
echo Viewing logs for: %SERVICE% last %LINES% lines
echo.
docker-compose logs -f --tail=%LINES% %SERVICE%
)
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
# XAUBot AI - Docker Logs Viewer
set -e
SERVICE="$1"
LINES="${2:-100}"
if [ -z "$SERVICE" ]; then
echo "📋 Viewing logs for all services..."
echo "💡 Tip: Use './docker-logs.sh SERVICE [LINES]' to view specific service"
echo " Available: trading-api, dashboard, postgres, pgadmin"
echo ""
docker-compose logs -f --tail=$LINES
else
echo "📋 Viewing logs for: $SERVICE (last $LINES lines)"
echo ""
docker-compose logs -f --tail=$LINES $SERVICE
fi
+34
View File
@@ -0,0 +1,34 @@
@echo off
REM Remove Dashboard & API while keeping database
echo.
echo ========================================
echo Removing Dashboard & API Services
echo ========================================
echo.
echo This will stop and remove:
echo - trading_bot_dashboard
echo - trading_bot_api
echo.
echo Database (trading_bot_db) will remain running.
echo.
pause
echo Stopping services...
docker-compose stop trading-api dashboard
echo.
echo Removing containers...
docker-compose rm -f trading-api dashboard
echo.
echo ========================================
echo Dashboard Removed!
echo ========================================
echo.
echo Database is still running:
docker ps --filter "name=trading_bot_db" --format "table {{.Names}}\t{{.Status}}"
echo.
echo To add dashboard back: docker-add-dashboard.bat
echo.
pause
+91
View File
@@ -0,0 +1,91 @@
@echo off
REM XAUBot AI - Docker Start Script (Windows)
echo.
echo Starting XAUBot AI Docker Services...
echo.
REM Check if .env exists
if not exist .env (
echo WARNING: .env file not found!
echo Creating .env from template...
copy .env.docker.example .env
echo.
echo .env created. Please edit it with your MT5 credentials:
echo - MT5_LOGIN
echo - MT5_PASSWORD
echo - MT5_SERVER
echo - MT5_PATH
echo.
pause
)
REM Check if Docker is running
docker info >nul 2>&1
if errorlevel 1 (
echo ERROR: Docker is not running!
echo Please start Docker Desktop and try again.
pause
exit /b 1
)
echo Docker is running
echo.
REM Parse arguments
set PROFILE_FLAG=
if "%1"=="--admin" set PROFILE_FLAG=--profile admin
if "%1"=="-a" set PROFILE_FLAG=--profile admin
if defined PROFILE_FLAG (
echo Starting with pgAdmin admin profile...
) else (
echo Starting core services postgres, api, dashboard...
echo Tip: Use 'docker-start.bat --admin' to include pgAdmin
)
echo.
echo Pulling latest base images...
docker-compose pull
echo.
echo Building services...
docker-compose build
echo.
echo Starting services...
docker-compose %PROFILE_FLAG% up -d
echo.
echo Waiting for services to be healthy...
timeout /t 10 /nobreak >nul
echo.
echo Checking service health...
docker-compose ps
echo.
echo ========================================
echo Services started successfully!
echo ========================================
echo.
echo Access Points:
echo Dashboard: http://localhost:3000
echo API: http://localhost:8000
echo API Docs: http://localhost:8000/docs
echo Database: localhost:5432
if defined PROFILE_FLAG (
echo pgAdmin: http://localhost:5050
)
echo.
echo Useful Commands:
echo View logs: docker-compose logs -f
echo View API logs: docker-compose logs -f trading-api
echo Stop services: docker-compose down
echo Restart: docker-compose restart
echo.
echo Full documentation: DOCKER.md
echo.
pause
+87
View File
@@ -0,0 +1,87 @@
#!/bin/bash
# XAUBot AI - Docker Start Script
set -e
echo "🚀 Starting XAUBot AI Docker Services..."
echo ""
# Check if .env exists
if [ ! -f .env ]; then
echo "⚠️ .env file not found!"
echo "📝 Creating .env from template..."
cp .env.docker.example .env
echo "✅ .env created. Please edit it with your MT5 credentials:"
echo " - MT5_LOGIN"
echo " - MT5_PASSWORD"
echo " - MT5_SERVER"
echo " - MT5_PATH"
echo ""
read -p "Press Enter after editing .env to continue..."
fi
# Check if Docker is running
if ! docker info > /dev/null 2>&1; then
echo "❌ Docker is not running!"
echo "Please start Docker Desktop and try again."
exit 1
fi
echo "✅ Docker is running"
echo ""
# Parse command line arguments
PROFILE_FLAG=""
if [ "$1" == "--admin" ] || [ "$1" == "-a" ]; then
PROFILE_FLAG="--profile admin"
echo "📊 Starting with pgAdmin (admin profile)..."
else
echo "📊 Starting core services (postgres, api, dashboard)..."
echo "💡 Use './docker-start.sh --admin' to include pgAdmin"
fi
echo ""
# Pull latest images
echo "📥 Pulling latest base images..."
docker-compose pull
echo ""
echo "🔨 Building services..."
docker-compose build
echo ""
echo "🎯 Starting services..."
docker-compose $PROFILE_FLAG up -d
echo ""
echo "⏳ Waiting for services to be healthy..."
sleep 10
# Check health
echo ""
echo "🏥 Checking service health..."
docker-compose ps
echo ""
echo "✅ Services started successfully!"
echo ""
echo "📍 Access Points:"
echo " Dashboard: http://localhost:3000"
echo " API: http://localhost:8000"
echo " API Docs: http://localhost:8000/docs"
echo " Database: localhost:5432"
if [ "$1" == "--admin" ] || [ "$1" == "-a" ]; then
echo " pgAdmin: http://localhost:5050"
fi
echo ""
echo "📋 Useful Commands:"
echo " View logs: docker-compose logs -f"
echo " View API logs: docker-compose logs -f trading-api"
echo " Stop services: docker-compose down"
echo " Restart: docker-compose restart"
echo ""
echo "📖 Full documentation: DOCKER.md"
echo ""
+55
View File
@@ -0,0 +1,55 @@
@echo off
REM Check status of all trading bot services
echo.
echo ========================================
echo Trading Bot Docker Services Status
echo ========================================
echo.
docker-compose ps
echo.
echo ========================================
echo Service Health Checks
echo ========================================
echo.
echo [Database]
docker exec trading_bot_db pg_isready -U trading_bot 2>nul
if %errorlevel%==0 (
echo Status: HEALTHY
) else (
echo Status: NOT RUNNING
)
echo.
echo [API]
curl -s http://localhost:8000/api/health >nul 2>&1
if %errorlevel%==0 (
echo Status: HEALTHY
echo URL: http://localhost:8000
) else (
echo Status: NOT RUNNING or UNHEALTHY
)
echo.
echo [Dashboard]
curl -s http://localhost:3000 >nul 2>&1
if %errorlevel%==0 (
echo Status: HEALTHY
echo URL: http://localhost:3000
) else (
echo Status: NOT RUNNING or UNHEALTHY
)
echo.
echo ========================================
echo Quick Commands
echo ========================================
echo View logs: docker-compose logs -f
echo Restart: docker-compose restart
echo Stop all: docker-compose stop
echo Start all: docker-compose up -d
echo.
pause
+44
View File
@@ -0,0 +1,44 @@
@echo off
REM XAUBot AI - Docker Stop Script (Windows)
echo.
echo Stopping XAUBot AI Docker Services...
echo.
if "%1"=="--remove" goto remove
if "%1"=="-r" goto remove
if "%1"=="--clean" goto clean
if "%1"=="-c" goto clean
goto stop
:remove
echo Stopping and removing containers...
docker-compose down
echo.
echo Containers stopped and removed
goto end
:clean
echo WARNING: This will remove all data including database!
set /p confirm="Are you sure? (yes/no): "
if /i "%confirm%"=="yes" (
docker-compose down -v
echo.
echo Containers, networks, and volumes removed
) else (
echo.
echo Cancelled
)
goto end
:stop
echo Stopping containers data will be preserved...
docker-compose stop
echo.
echo Containers stopped
:end
echo.
echo To restart: docker-start.bat
echo.
pause
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
# XAUBot AI - Docker Stop Script
set -e
echo "🛑 Stopping XAUBot AI Docker Services..."
echo ""
# Parse arguments
if [ "$1" == "--remove" ] || [ "$1" == "-r" ]; then
echo "⚠️ Stopping and removing containers..."
docker-compose down
echo "✅ Containers stopped and removed"
elif [ "$1" == "--clean" ] || [ "$1" == "-c" ]; then
echo "⚠️ WARNING: This will remove all data including database!"
read -p "Are you sure? (yes/no): " confirm
if [ "$confirm" == "yes" ]; then
docker-compose down -v
echo "✅ Containers, networks, and volumes removed"
else
echo "❌ Cancelled"
fi
else
echo "🔄 Stopping containers (data will be preserved)..."
docker-compose stop
echo "✅ Containers stopped"
fi
echo ""
echo "📋 To restart: ./docker-start.sh"
echo ""
+31 -66
View File
@@ -525,15 +525,14 @@ class TradingBot:
# --- H1 Multi-Timeframe Bias (Fix 5) ---
def _get_h1_bias(self) -> str:
"""
Determine H1 higher-timeframe bias using SMC structure.
Determine H1 higher-timeframe bias using Price vs EMA20 (#31B).
Returns: "BULLISH", "BEARISH", or "NEUTRAL"
Logic:
Logic (#31B: backtest +$343, WR 81.8%, Sharpe 3.97, DD 2.5%):
- Fetch H1 data (100 bars)
- Run SMC analysis (BOS, CHoCH, OB, FVG)
- Last BOS/CHoCH direction = H1 bias
- If H1 has bullish OB near price BULLISH zone
- If H1 has bearish OB near price BEARISH zone
- Calculate EMA20 on H1 closes
- If price > EMA20 * 1.001 BULLISH (allow BUY only)
- If price < EMA20 * 0.999 BEARISH (allow SELL only)
"""
try:
# Cache H1 bias — only update every 4 candles (1 hour) since H1 changes slowly
@@ -550,73 +549,31 @@ class TradingBot:
if len(df_h1) < 20:
return "NEUTRAL"
# Run SMC on H1 data
from src.smc_polars import SMCAnalyzer
h1_smc = SMCAnalyzer(swing_length=5, fvg_min_gap_pips=5.0, ob_lookback=10)
df_h1 = h1_smc.calculate_all(df_h1)
# #31B: Price vs EMA20 method (backtested winner)
import numpy as np
closes = df_h1["close"].to_list()
current_price = closes[-1]
current_price = df_h1["close"].tail(1).item()
# Calculate EMA20
period = 20
multiplier = 2 / (period + 1)
ema = np.mean(closes[:period])
for val in closes[period:]:
ema = (val - ema) * multiplier + ema
# Determine bias with small buffer (0.1% threshold)
bias = "NEUTRAL"
# 1. Check last BOS direction on H1
bos_col = df_h1["bos"].to_list()
last_bos = 0
for v in reversed(bos_col[-20:]):
if v != 0:
last_bos = v
break
# 2. Check last CHoCH direction on H1
choch_col = df_h1["choch"].to_list()
last_choch = 0
for v in reversed(choch_col[-20:]):
if v != 0:
last_choch = v
break
# 3. Check if price is near H1 Order Block
ob_col = df_h1["ob"].to_list()
highs = df_h1["high"].to_list()
lows = df_h1["low"].to_list()
near_bullish_ob = False
near_bearish_ob = False
for i in range(-10, 0): # Last 10 H1 candles
idx = len(ob_col) + i
if idx < 0:
continue
ob_val = ob_col[idx]
if ob_val == 1: # Bullish OB
# Price within OB zone (low to high of that candle)
if lows[idx] <= current_price <= highs[idx] * 1.002:
near_bullish_ob = True
elif ob_val == -1: # Bearish OB
if lows[idx] * 0.998 <= current_price <= highs[idx]:
near_bearish_ob = True
# Determine bias: BOS > CHoCH > OB proximity
if last_bos == 1:
if current_price > ema * 1.001:
bias = "BULLISH"
elif last_bos == -1:
elif current_price < ema * 0.999:
bias = "BEARISH"
elif last_choch == 1:
bias = "BULLISH"
elif last_choch == -1:
bias = "BEARISH"
# OB proximity can override if no clear structure
if bias == "NEUTRAL":
if near_bullish_ob:
bias = "BULLISH"
elif near_bearish_ob:
bias = "BEARISH"
# Cache result
self._h1_bias_cache = bias
self._h1_bias_loop = self._loop_count
if self._loop_count % 4 == 0:
logger.info(f"H1 Bias: {bias} (BOS={last_bos}, CHoCH={last_choch}, near_bull_OB={near_bullish_ob}, near_bear_OB={near_bearish_ob})")
logger.info(f"H1 Bias: {bias} (price={current_price:.2f}, EMA20={ema:.2f})")
return bias
@@ -962,10 +919,18 @@ class TradingBot:
if final_signal is None:
return
# 10.1 H1 Multi-Timeframe Filter - DISABLED (SMC-only mode)
# H1 bias still logged for dashboard but does NOT block trades
# 10.1 H1 Multi-Timeframe Filter (#31B: Price vs EMA20 — backtest +$343)
# BUY only when H1 is BULLISH, SELL only when H1 is BEARISH
if h1_bias != "NEUTRAL":
logger.info(f"H1 Bias: {h1_bias} (monitoring only, not blocking)")
if (final_signal.signal_type == "BUY" and h1_bias != "BULLISH") or \
(final_signal.signal_type == "SELL" and h1_bias != "BEARISH"):
logger.info(f"H1 Filter: {final_signal.signal_type} blocked (H1={h1_bias})")
return
logger.info(f"H1 Filter: {final_signal.signal_type} aligned with H1={h1_bias}")
else:
# H1 NEUTRAL = block both directions (strict mode from backtest)
logger.info(f"H1 Filter: {final_signal.signal_type} blocked (H1=NEUTRAL)")
return
# 10.5 Check trade cooldown
if self._last_trade_time:
+38
View File
@@ -0,0 +1,38 @@
# Docker-compatible requirements (Linux)
# Note: MetaTrader5 is Windows-only, excluded from Docker build
# Core Data Engine (Rust-based, NOT Pandas)
polars>=1.37.0
pyarrow>=15.0.0
# Machine Learning
xgboost>=2.1.0
scikit-learn>=1.4.0
hmmlearn>=0.3.3
joblib>=1.4.0
# Asynchronous Processing
asyncio-throttle>=1.0.2
# Logging and Monitoring
loguru>=0.7.2
# Environment Variables
python-dotenv>=1.0.1
# Numerical Computing
numpy>=1.26.0
# HTTP Client
aiohttp>=3.9.0
# PostgreSQL Database
psycopg2-binary>=2.9.9
# FastAPI for Web Dashboard API
fastapi>=0.109.0
uvicorn[standard]>=0.27.0
pydantic>=2.6.0
# CORS middleware
python-multipart>=0.0.6
+5
View File
@@ -33,6 +33,11 @@ aiohttp>=3.9.0
# PostgreSQL Database
psycopg2-binary>=2.9.9
# FastAPI for Web Dashboard API
fastapi>=0.109.0
uvicorn[standard]>=0.27.0
pydantic>=2.6.0
# Optional: For backtesting
# vectorbt>=0.26.2
+3 -2
View File
@@ -554,9 +554,10 @@ class SmartPositionManager:
trail_start = self.trail_start_pips
trail_step = self.trail_step_pips
# 5. Breakeven protection
# 5. Breakeven protection (#28B: smart BE locks profit at 0.5*ATR instead of fixed $2)
if pip_profit >= be_pips and current_sl != 0:
breakeven_sl = entry_price + (1 if is_buy else -1) * 2 # 2 points buffer
be_lock_distance = current_atr * 0.5 if (current_atr is not None and current_atr > 0) else 2.0
breakeven_sl = entry_price + (1 if is_buy else -1) * be_lock_distance
if is_buy and current_sl < breakeven_sl:
return PositionAction(
+81 -5
View File
@@ -135,6 +135,80 @@ class SMCAnalyzer:
# Cap confidence at 0.85 (never 100% certain)
return min(conf, 0.85)
def _calculate_dynamic_rr(
self,
market_structure: int,
has_bullish_break: bool,
has_bearish_break: bool,
has_fvg: bool,
has_ob: bool,
df: Optional[pl.DataFrame] = None,
) -> float:
"""
Calculate dynamic Risk:Reward ratio based on market conditions.
Returns RR between 1.5 and 2.0:
- 2.0: Strong trend, high confidence -> let profits run
- 1.5: Ranging/uncertain -> take profit earlier (higher hit rate)
Factors considered:
1. Market structure strength (trending vs ranging)
2. Number of confirmations (BOS, FVG, OB)
3. Trend strength (multiple BOS in same direction)
4. Volatility (high vol = lower RR for faster exit)
"""
# Start with base RR
rr = 1.5 # Conservative base
# === Factor 1: Market Structure ===
# Strong trend = higher RR
if market_structure != 0: # Trending (bullish or bearish)
rr += 0.15
# === Factor 2: Structure Break Confirmation ===
if has_bullish_break or has_bearish_break:
rr += 0.10 # BOS/CHoCH adds confidence
# === Factor 3: Entry Zone Confirmation ===
if has_fvg:
rr += 0.05 # FVG present
if has_ob:
rr += 0.05 # Order Block present
# === Factor 4: Trend Strength (multiple BOS) ===
if df is not None and "bos" in df.columns:
recent_bos = df.tail(20)["bos"].to_list()
bos_count = sum(1 for b in recent_bos if b != 0)
if bos_count >= 3: # Strong trend with multiple breaks
rr += 0.10
elif bos_count >= 2:
rr += 0.05
# === Factor 5: Volatility Adjustment ===
# High volatility = reduce RR (take profit faster)
if df is not None and "atr" in df.columns:
atr = df.tail(1)["atr"].item()
if atr is not None:
# Typical XAUUSD ATR is ~$10-15
if atr > 18: # High volatility
rr -= 0.15 # Take profit faster
elif atr > 15: # Above average volatility
rr -= 0.05
# === Factor 6: Check for ranging market (low BOS count) ===
if df is not None and "bos" in df.columns:
recent_bos = df.tail(30)["bos"].to_list()
bos_count = sum(1 for b in recent_bos if b != 0)
if bos_count == 0: # No structure breaks = ranging
rr = 1.5 # Use minimum RR in ranging market
# Clamp RR between 1.5 and 2.0
rr = max(1.5, min(2.0, rr))
logger.debug(f"Dynamic RR: {rr:.2f} (struct={market_structure}, break={has_bullish_break or has_bearish_break}, fvg={has_fvg}, ob={has_ob})")
return rr
def calculate_all(self, df: pl.DataFrame) -> pl.DataFrame:
"""
Calculate all SMC indicators.
@@ -710,9 +784,11 @@ class SMCAnalyzer:
# SL: 1.5-2 ATR distance (protects against noise)
min_sl_distance = 1.5 * atr
# TP: Must be at least 2x risk (RR 1:2 minimum)
# With 1.5 ATR SL, TP should be at least 3 ATR
min_rr_ratio = 2.0 # ENFORCED: Minimum Risk:Reward 1:2
# === FIXED RR RATIO 1:1.5 ===
# Based on backtest analysis: RR 1:2 only hits TP 14% of the time
# RR 1:1.5 is more realistic for higher hit rate
min_rr_ratio = 1.5
# BULLISH SIGNAL CONDITIONS
# Need: bullish structure OR recent bullish break, AND (FVG OR OB)
@@ -738,7 +814,7 @@ class SMCAnalyzer:
if entry - sl < min_sl_distance:
sl = entry - min_sl_distance
# FIX: TP at EXACTLY min_rr_ratio (1:2) - ENFORCED
# FIXED TP at RR 1:1.5
risk = entry - sl
tp = entry + (risk * min_rr_ratio)
@@ -797,7 +873,7 @@ class SMCAnalyzer:
if sl - entry < min_sl_distance:
sl = entry + min_sl_distance
# FIX: TP at EXACTLY min_rr_ratio (1:2) - ENFORCED
# FIXED TP at RR 1:1.5
risk = sl - entry
tp = entry - (risk * min_rr_ratio)
+52
View File
@@ -0,0 +1,52 @@
@echo off
REM Start API and Dashboard in separate windows
echo.
echo ========================================
echo XAUBot AI - Starting All Services
echo ========================================
echo.
cd "%~dp0"
REM Check database
echo [1/3] Checking database...
docker ps --filter "name=trading_bot_db" --format "{{.Names}}: {{.Status}}" 2>nul
if errorlevel 1 (
echo.
echo WARNING: Database not running!
echo Please start with: docker-compose up -d postgres
echo.
pause
exit /b 1
)
echo.
echo [2/3] Starting API...
start "Trading API" cmd /k start-api.bat
echo Waiting for API to start...
timeout /t 5 /nobreak >nul
echo.
echo [3/3] Starting Dashboard...
start "Web Dashboard" cmd /k start-dashboard.bat
echo.
echo ========================================
echo All Services Started!
echo ========================================
echo.
echo Access Points:
echo - Dashboard: http://localhost:3000
echo - API: http://localhost:8000
echo - API Docs: http://localhost:8000/docs
echo - Database: localhost:5432
echo.
echo Two windows will open:
echo 1. Trading API (FastAPI)
echo 2. Web Dashboard (Next.js)
echo.
echo Close this window when done.
echo.
pause
+35
View File
@@ -0,0 +1,35 @@
@echo off
REM Start Trading API (FastAPI)
echo.
echo ========================================
echo Starting Trading API
echo ========================================
echo.
cd "%~dp0"
REM Check if virtual environment exists
if not exist "venv" (
echo Creating virtual environment...
python -m venv venv
echo.
)
REM Activate virtual environment
call venv\Scripts\activate.bat
REM Install/update dependencies
echo Installing dependencies...
pip install -q fastapi uvicorn pydantic python-dotenv aiohttp
echo.
echo ========================================
echo API Starting on http://localhost:8000
echo ========================================
echo.
echo Press Ctrl+C to stop
echo.
REM Start the API
python web-dashboard\api\main.py
+28
View File
@@ -0,0 +1,28 @@
@echo off
REM Start Next.js Dashboard
echo.
echo ========================================
echo Starting Web Dashboard
echo ========================================
echo.
cd "%~dp0web-dashboard"
REM Check if node_modules exists
if not exist "node_modules" (
echo Installing dependencies...
npm install
echo.
)
echo.
echo ========================================
echo Dashboard Starting on http://localhost:3000
echo ========================================
echo.
echo Press Ctrl+C to stop
echo.
REM Start dashboard
npm run dev
+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