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