7af9183af3
- XGBoost ML model with 37 features for market direction prediction - Smart Money Concepts (SMC): Order Blocks, FVG, BOS, CHoCH - HMM market regime detection (trending/ranging/volatile) - ATR-based stop loss with 1.5 ATR minimum distance - Broker-level SL protection with fallback - Time-based exit (max 6 hours per trade) - Session-aware trading optimized for London/NY overlap - Auto-retraining based on market conditions - Telegram notifications and web dashboard - Backtest results: 63.9% win rate, 2.64 profit factor, 4.83 Sharpe Backtest period: Jan 2025 - Feb 2026, 654 trades, $4,189 net P/L Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
908 lines
34 KiB
Python
908 lines
34 KiB
Python
"""
|
|
Trading Bot Dashboard - Modern UI with CustomTkinter
|
|
=====================================================
|
|
Beautiful responsive dashboard with dark/light mode toggle.
|
|
"""
|
|
|
|
import customtkinter as ctk
|
|
import threading
|
|
import time
|
|
from datetime import datetime
|
|
from zoneinfo import ZoneInfo
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
# Add project to path
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
|
|
# Load environment variables
|
|
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}")
|
|
sys.exit(1)
|
|
|
|
# Set default appearance
|
|
ctk.set_default_color_theme("blue")
|
|
|
|
# Theme colors
|
|
THEMES = {
|
|
"dark": {
|
|
"bg": "#0d0d1a",
|
|
"card": "#1a1a2e",
|
|
"card_inner": "#0d0d1a",
|
|
"text": "#ffffff",
|
|
"text_secondary": "#888888",
|
|
"text_muted": "#666666",
|
|
"accent": "#00d4ff",
|
|
"green": "#00ff88",
|
|
"red": "#ff4757",
|
|
"orange": "#ffa500",
|
|
"highlight": "#2d4a2d",
|
|
"highlight_off": "#2d2d44",
|
|
},
|
|
"light": {
|
|
"bg": "#f0f2f5",
|
|
"card": "#ffffff",
|
|
"card_inner": "#f8f9fa",
|
|
"text": "#1a1a2e",
|
|
"text_secondary": "#555555",
|
|
"text_muted": "#888888",
|
|
"accent": "#0066cc",
|
|
"green": "#00aa55",
|
|
"red": "#dd3344",
|
|
"orange": "#ee8800",
|
|
"highlight": "#d4edda",
|
|
"highlight_off": "#e9ecef",
|
|
}
|
|
}
|
|
|
|
|
|
class ModernCard(ctk.CTkFrame):
|
|
"""Reusable card component with title - theme aware"""
|
|
|
|
def __init__(self, master, title, theme="dark", **kwargs):
|
|
self.theme = theme
|
|
colors = THEMES[theme]
|
|
super().__init__(master, corner_radius=12, fg_color=colors["card"], **kwargs)
|
|
|
|
# Title
|
|
self.title_label = ctk.CTkLabel(
|
|
self,
|
|
text=title,
|
|
font=ctk.CTkFont(size=12, weight="bold"),
|
|
text_color=colors["accent"]
|
|
)
|
|
self.title_label.pack(anchor="w", padx=12, pady=(10, 6))
|
|
|
|
# Content frame
|
|
self.content = ctk.CTkFrame(self, fg_color="transparent")
|
|
self.content.pack(fill="both", expand=True, padx=12, pady=(0, 10))
|
|
|
|
def update_theme(self, theme):
|
|
"""Update card colors for theme"""
|
|
self.theme = theme
|
|
colors = THEMES[theme]
|
|
self.configure(fg_color=colors["card"])
|
|
self.title_label.configure(text_color=colors["accent"])
|
|
|
|
|
|
class TradingDashboard(ctk.CTk):
|
|
"""Modern Trading Dashboard with CustomTkinter - Responsive & Theme Toggle"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
# Theme state
|
|
self.current_theme = "dark"
|
|
ctk.set_appearance_mode("dark")
|
|
|
|
# Window setup - responsive minimum size for split screen
|
|
self.title("AI Trading Bot")
|
|
self.geometry("700x800")
|
|
self.minsize(600, 700)
|
|
self.configure(fg_color=THEMES[self.current_theme]["bg"])
|
|
|
|
# Store all themed widgets for updates
|
|
self.themed_widgets = []
|
|
self.cards = []
|
|
|
|
# Initialize components
|
|
self.mt5 = None
|
|
self.smc = None
|
|
self.ml = None
|
|
self.hmm = None
|
|
self.session = None
|
|
self.feature_eng = None
|
|
self.config = TradingConfig()
|
|
|
|
# State
|
|
self.running = False
|
|
self.last_update = None
|
|
|
|
# Create UI
|
|
self.create_header()
|
|
self.create_main_layout()
|
|
self.create_status_bar()
|
|
|
|
# Start connection
|
|
self.after(100, self.connect_systems)
|
|
|
|
def toggle_theme(self):
|
|
"""Toggle between dark and light mode"""
|
|
self.current_theme = "light" if self.current_theme == "dark" else "dark"
|
|
ctk.set_appearance_mode(self.current_theme)
|
|
self.apply_theme()
|
|
|
|
def apply_theme(self):
|
|
"""Apply current theme to all widgets"""
|
|
colors = THEMES[self.current_theme]
|
|
|
|
# Update main window
|
|
self.configure(fg_color=colors["bg"])
|
|
|
|
# Update theme button
|
|
if self.current_theme == "dark":
|
|
self.theme_btn.configure(text="☀️ Light")
|
|
else:
|
|
self.theme_btn.configure(text="🌙 Dark")
|
|
|
|
# Update all cards
|
|
for card in self.cards:
|
|
card.update_theme(self.current_theme)
|
|
|
|
# Update header
|
|
self.title_label.configure(text_color=colors["text"])
|
|
self.subtitle_label.configure(text_color=colors["text_muted"])
|
|
self.time_label.configure(text_color=colors["text_secondary"])
|
|
|
|
# Update status bar
|
|
self.status_frame.configure(fg_color=colors["card"])
|
|
self.status_label.configure(text_color=colors["text_secondary"])
|
|
self.update_label.configure(text_color=colors["text_muted"])
|
|
|
|
# Update text boxes
|
|
self.log_text.configure(fg_color=colors["card_inner"], text_color=colors["green"])
|
|
self.positions_text.configure(fg_color=colors["card_inner"], text_color=colors["text"])
|
|
|
|
# Update settings labels
|
|
if hasattr(self, 'settings_labels'):
|
|
for lbl, val_lbl in self.settings_labels:
|
|
lbl.configure(text_color=colors["text_secondary"])
|
|
val_lbl.configure(text_color=colors["text"])
|
|
|
|
# Update golden time frame
|
|
is_golden = hasattr(self, 'golden_frame')
|
|
if is_golden:
|
|
# Reapply golden time based on current state
|
|
current_text = self.golden_label.cget("text")
|
|
if "YES" in current_text:
|
|
self.golden_frame.configure(fg_color=colors["highlight"])
|
|
self.golden_label.configure(text_color=colors["green"])
|
|
else:
|
|
self.golden_frame.configure(fg_color=colors["highlight_off"])
|
|
self.golden_label.configure(text_color=colors["text_secondary"])
|
|
|
|
def create_header(self):
|
|
"""Create header section"""
|
|
colors = THEMES[self.current_theme]
|
|
|
|
header_frame = ctk.CTkFrame(self, fg_color="transparent", height=50)
|
|
header_frame.pack(fill="x", padx=15, pady=(10, 5))
|
|
header_frame.pack_propagate(False)
|
|
|
|
# Logo/Title
|
|
self.title_label = ctk.CTkLabel(
|
|
header_frame,
|
|
text="AI TRADING BOT",
|
|
font=ctk.CTkFont(size=20, weight="bold"),
|
|
text_color=colors["text"]
|
|
)
|
|
self.title_label.pack(side="left")
|
|
|
|
self.subtitle_label = ctk.CTkLabel(
|
|
header_frame,
|
|
text=" Live",
|
|
font=ctk.CTkFont(size=12),
|
|
text_color=colors["text_muted"]
|
|
)
|
|
self.subtitle_label.pack(side="left", pady=(4, 0))
|
|
|
|
# Theme toggle button
|
|
self.theme_btn = ctk.CTkButton(
|
|
header_frame,
|
|
text="☀️ Light",
|
|
width=80,
|
|
height=28,
|
|
corner_radius=14,
|
|
font=ctk.CTkFont(size=11),
|
|
command=self.toggle_theme
|
|
)
|
|
self.theme_btn.pack(side="right", padx=5)
|
|
|
|
# Connection status
|
|
self.connection_label = ctk.CTkLabel(
|
|
header_frame,
|
|
text="● Connecting...",
|
|
font=ctk.CTkFont(size=12),
|
|
text_color=colors["orange"]
|
|
)
|
|
self.connection_label.pack(side="right", padx=10)
|
|
|
|
# Time display
|
|
self.time_label = ctk.CTkLabel(
|
|
header_frame,
|
|
text="--:--:-- WIB",
|
|
font=ctk.CTkFont(size=12, weight="bold"),
|
|
text_color=colors["text_secondary"]
|
|
)
|
|
self.time_label.pack(side="right", padx=10)
|
|
|
|
def create_main_layout(self):
|
|
"""Create main dashboard layout - responsive 2-column for split screen"""
|
|
# Scrollable main container for small windows
|
|
self.main_scroll = ctk.CTkScrollableFrame(self, fg_color="transparent")
|
|
self.main_scroll.pack(fill="both", expand=True, padx=10, pady=5)
|
|
|
|
# Configure 2-column grid (responsive for split screen)
|
|
self.main_scroll.grid_columnconfigure(0, weight=1, minsize=280)
|
|
self.main_scroll.grid_columnconfigure(1, weight=1, minsize=280)
|
|
|
|
# Row 0: Price & Account
|
|
self.create_price_card(self.main_scroll, 0, 0)
|
|
self.create_account_card(self.main_scroll, 0, 1)
|
|
|
|
# Row 1: Session & Risk
|
|
self.create_session_card(self.main_scroll, 1, 0)
|
|
self.create_risk_card(self.main_scroll, 1, 1)
|
|
|
|
# Row 2: SMC & ML
|
|
self.create_smc_card(self.main_scroll, 2, 0)
|
|
self.create_ml_card(self.main_scroll, 2, 1)
|
|
|
|
# Row 3: Regime & Positions
|
|
self.create_regime_card(self.main_scroll, 3, 0)
|
|
self.create_positions_card(self.main_scroll, 3, 1)
|
|
|
|
# Row 4: Settings (full width)
|
|
self.create_settings_card(self.main_scroll, 4, 0)
|
|
|
|
# Row 5: Log (full width)
|
|
self.create_log_card(self.main_scroll, 5, 0)
|
|
|
|
def create_price_card(self, parent, row, col):
|
|
"""Price information card"""
|
|
colors = THEMES[self.current_theme]
|
|
card = ModernCard(parent, "PRICE", theme=self.current_theme)
|
|
card.grid(row=row, column=col, padx=4, pady=4, sticky="nsew")
|
|
self.cards.append(card)
|
|
|
|
# Price value
|
|
self.price_label = ctk.CTkLabel(
|
|
card.content,
|
|
text="-----.--",
|
|
font=ctk.CTkFont(size=28, weight="bold"),
|
|
text_color=colors["green"]
|
|
)
|
|
self.price_label.pack(pady=(5, 0))
|
|
|
|
# Symbol
|
|
self.symbol_label = ctk.CTkLabel(
|
|
card.content,
|
|
text="XAUUSD",
|
|
font=ctk.CTkFont(size=11),
|
|
text_color=colors["text_muted"]
|
|
)
|
|
self.symbol_label.pack()
|
|
|
|
# Spread
|
|
self.spread_label = ctk.CTkLabel(
|
|
card.content,
|
|
text="Spread: -- pips",
|
|
font=ctk.CTkFont(size=10),
|
|
text_color=colors["text_secondary"]
|
|
)
|
|
self.spread_label.pack(pady=(3, 0))
|
|
|
|
def create_account_card(self, parent, row, col):
|
|
"""Account information card"""
|
|
colors = THEMES[self.current_theme]
|
|
card = ModernCard(parent, "ACCOUNT", theme=self.current_theme)
|
|
card.grid(row=row, column=col, padx=4, pady=4, sticky="nsew")
|
|
self.cards.append(card)
|
|
|
|
# Balance
|
|
balance_frame = ctk.CTkFrame(card.content, fg_color="transparent")
|
|
balance_frame.pack(fill="x", pady=2)
|
|
ctk.CTkLabel(balance_frame, text="Balance:", font=ctk.CTkFont(size=11), text_color=colors["text_secondary"]).pack(side="left")
|
|
self.balance_label = ctk.CTkLabel(balance_frame, text="$-----.--", font=ctk.CTkFont(size=11, weight="bold"), text_color=colors["text"])
|
|
self.balance_label.pack(side="right")
|
|
|
|
# Equity
|
|
equity_frame = ctk.CTkFrame(card.content, fg_color="transparent")
|
|
equity_frame.pack(fill="x", pady=2)
|
|
ctk.CTkLabel(equity_frame, text="Equity:", font=ctk.CTkFont(size=11), text_color=colors["text_secondary"]).pack(side="left")
|
|
self.equity_label = ctk.CTkLabel(equity_frame, text="$-----.--", font=ctk.CTkFont(size=11, weight="bold"), text_color=colors["text"])
|
|
self.equity_label.pack(side="right")
|
|
|
|
# P/L
|
|
pl_frame = ctk.CTkFrame(card.content, fg_color="transparent")
|
|
pl_frame.pack(fill="x", pady=2)
|
|
ctk.CTkLabel(pl_frame, text="P/L:", font=ctk.CTkFont(size=11), text_color=colors["text_secondary"]).pack(side="left")
|
|
self.pl_label = ctk.CTkLabel(pl_frame, text="$0.00", font=ctk.CTkFont(size=12, weight="bold"), text_color=colors["green"])
|
|
self.pl_label.pack(side="right")
|
|
|
|
def create_session_card(self, parent, row, col):
|
|
"""Session information card"""
|
|
colors = THEMES[self.current_theme]
|
|
card = ModernCard(parent, "SESSION", theme=self.current_theme)
|
|
card.grid(row=row, column=col, padx=4, pady=4, sticky="nsew")
|
|
self.cards.append(card)
|
|
|
|
# Current session
|
|
self.session_label = ctk.CTkLabel(
|
|
card.content,
|
|
text="Loading...",
|
|
font=ctk.CTkFont(size=12, weight="bold"),
|
|
text_color=colors["orange"]
|
|
)
|
|
self.session_label.pack(pady=(3, 0))
|
|
|
|
# Golden time indicator
|
|
self.golden_frame = ctk.CTkFrame(card.content, fg_color=colors["highlight_off"], corner_radius=6)
|
|
self.golden_frame.pack(pady=6, padx=3, fill="x")
|
|
self.golden_label = ctk.CTkLabel(
|
|
self.golden_frame,
|
|
text="GOLDEN: --",
|
|
font=ctk.CTkFont(size=10, weight="bold"),
|
|
text_color=colors["text_secondary"]
|
|
)
|
|
self.golden_label.pack(pady=4)
|
|
|
|
# Can trade
|
|
self.can_trade_label = ctk.CTkLabel(
|
|
card.content,
|
|
text="Can Trade: --",
|
|
font=ctk.CTkFont(size=10),
|
|
text_color=colors["text_secondary"]
|
|
)
|
|
self.can_trade_label.pack()
|
|
|
|
def create_risk_card(self, parent, row, col):
|
|
"""Risk status card"""
|
|
colors = THEMES[self.current_theme]
|
|
card = ModernCard(parent, "RISK STATUS", theme=self.current_theme)
|
|
card.grid(row=row, column=col, padx=4, pady=4, sticky="nsew")
|
|
self.cards.append(card)
|
|
|
|
# Daily loss
|
|
dl_frame = ctk.CTkFrame(card.content, fg_color="transparent")
|
|
dl_frame.pack(fill="x", pady=2)
|
|
ctk.CTkLabel(dl_frame, text="Daily Loss:", font=ctk.CTkFont(size=10), text_color=colors["text_secondary"]).pack(side="left")
|
|
self.daily_loss_label = ctk.CTkLabel(dl_frame, text="$0.00", font=ctk.CTkFont(size=10, weight="bold"), text_color=colors["red"])
|
|
self.daily_loss_label.pack(side="right")
|
|
|
|
# Daily profit
|
|
dp_frame = ctk.CTkFrame(card.content, fg_color="transparent")
|
|
dp_frame.pack(fill="x", pady=2)
|
|
ctk.CTkLabel(dp_frame, text="Daily Profit:", font=ctk.CTkFont(size=10), text_color=colors["text_secondary"]).pack(side="left")
|
|
self.daily_profit_label = ctk.CTkLabel(dp_frame, text="$0.00", font=ctk.CTkFont(size=10, weight="bold"), text_color=colors["green"])
|
|
self.daily_profit_label.pack(side="right")
|
|
|
|
# Consecutive losses
|
|
cl_frame = ctk.CTkFrame(card.content, fg_color="transparent")
|
|
cl_frame.pack(fill="x", pady=2)
|
|
ctk.CTkLabel(cl_frame, text="Consec. Losses:", font=ctk.CTkFont(size=10), text_color=colors["text_secondary"]).pack(side="left")
|
|
self.consec_loss_label = ctk.CTkLabel(cl_frame, text="0", font=ctk.CTkFont(size=10, weight="bold"), text_color=colors["text"])
|
|
self.consec_loss_label.pack(side="right")
|
|
|
|
def create_smc_card(self, parent, row, col):
|
|
"""SMC Signal card"""
|
|
colors = THEMES[self.current_theme]
|
|
card = ModernCard(parent, "SMC SIGNAL", theme=self.current_theme)
|
|
card.grid(row=row, column=col, padx=4, pady=4, sticky="nsew")
|
|
self.cards.append(card)
|
|
|
|
# Signal
|
|
self.smc_signal_label = ctk.CTkLabel(
|
|
card.content,
|
|
text="---",
|
|
font=ctk.CTkFont(size=24, weight="bold"),
|
|
text_color=colors["text_muted"]
|
|
)
|
|
self.smc_signal_label.pack(pady=(5, 3))
|
|
|
|
# Confidence bar
|
|
self.smc_confidence_bar = ctk.CTkProgressBar(card.content, height=6, corner_radius=3)
|
|
self.smc_confidence_bar.pack(pady=4, fill="x", padx=10)
|
|
self.smc_confidence_bar.set(0)
|
|
|
|
# Confidence text
|
|
self.smc_conf_label = ctk.CTkLabel(
|
|
card.content,
|
|
text="Confidence: --%",
|
|
font=ctk.CTkFont(size=10),
|
|
text_color=colors["text_secondary"]
|
|
)
|
|
self.smc_conf_label.pack()
|
|
|
|
# Reason
|
|
self.smc_reason_label = ctk.CTkLabel(
|
|
card.content,
|
|
text="Waiting...",
|
|
font=ctk.CTkFont(size=9),
|
|
text_color=colors["text_muted"],
|
|
wraplength=250
|
|
)
|
|
self.smc_reason_label.pack(pady=(3, 0))
|
|
|
|
def create_ml_card(self, parent, row, col):
|
|
"""ML Prediction card"""
|
|
colors = THEMES[self.current_theme]
|
|
card = ModernCard(parent, "ML PREDICTION", theme=self.current_theme)
|
|
card.grid(row=row, column=col, padx=4, pady=4, sticky="nsew")
|
|
self.cards.append(card)
|
|
|
|
# Signal
|
|
self.ml_signal_label = ctk.CTkLabel(
|
|
card.content,
|
|
text="---",
|
|
font=ctk.CTkFont(size=24, weight="bold"),
|
|
text_color=colors["text_muted"]
|
|
)
|
|
self.ml_signal_label.pack(pady=(5, 3))
|
|
|
|
# Confidence bar
|
|
self.ml_confidence_bar = ctk.CTkProgressBar(card.content, height=6, corner_radius=3)
|
|
self.ml_confidence_bar.pack(pady=4, fill="x", padx=10)
|
|
self.ml_confidence_bar.set(0)
|
|
|
|
# Confidence text
|
|
self.ml_conf_label = ctk.CTkLabel(
|
|
card.content,
|
|
text="Confidence: --%",
|
|
font=ctk.CTkFont(size=10),
|
|
text_color=colors["text_secondary"]
|
|
)
|
|
self.ml_conf_label.pack()
|
|
|
|
# Probabilities
|
|
self.ml_prob_label = ctk.CTkLabel(
|
|
card.content,
|
|
text="Buy: --% | Sell: --%",
|
|
font=ctk.CTkFont(size=9),
|
|
text_color=colors["text_muted"]
|
|
)
|
|
self.ml_prob_label.pack(pady=(3, 0))
|
|
|
|
def create_regime_card(self, parent, row, col):
|
|
"""Market Regime card"""
|
|
colors = THEMES[self.current_theme]
|
|
card = ModernCard(parent, "MARKET REGIME", theme=self.current_theme)
|
|
card.grid(row=row, column=col, padx=4, pady=4, sticky="nsew")
|
|
self.cards.append(card)
|
|
|
|
# Regime
|
|
self.regime_label = ctk.CTkLabel(
|
|
card.content,
|
|
text="---",
|
|
font=ctk.CTkFont(size=16, weight="bold"),
|
|
text_color=colors["text_muted"]
|
|
)
|
|
self.regime_label.pack(pady=(8, 5))
|
|
|
|
# Volatility
|
|
vol_frame = ctk.CTkFrame(card.content, fg_color="transparent")
|
|
vol_frame.pack(fill="x", pady=2)
|
|
ctk.CTkLabel(vol_frame, text="Volatility:", font=ctk.CTkFont(size=10), text_color=colors["text_secondary"]).pack(side="left")
|
|
self.volatility_label = ctk.CTkLabel(vol_frame, text="--", font=ctk.CTkFont(size=10, weight="bold"), text_color=colors["text"])
|
|
self.volatility_label.pack(side="right")
|
|
|
|
# Confidence
|
|
conf_frame = ctk.CTkFrame(card.content, fg_color="transparent")
|
|
conf_frame.pack(fill="x", pady=2)
|
|
ctk.CTkLabel(conf_frame, text="Confidence:", font=ctk.CTkFont(size=10), text_color=colors["text_secondary"]).pack(side="left")
|
|
self.regime_conf_label = ctk.CTkLabel(conf_frame, text="--%", font=ctk.CTkFont(size=10, weight="bold"), text_color=colors["text"])
|
|
self.regime_conf_label.pack(side="right")
|
|
|
|
def create_positions_card(self, parent, row, col):
|
|
"""Open Positions card"""
|
|
colors = THEMES[self.current_theme]
|
|
card = ModernCard(parent, "OPEN POSITIONS", theme=self.current_theme)
|
|
card.grid(row=row, column=col, padx=4, pady=4, sticky="nsew")
|
|
self.cards.append(card)
|
|
|
|
# Positions text
|
|
self.positions_text = ctk.CTkTextbox(
|
|
card.content,
|
|
font=ctk.CTkFont(family="Consolas", size=10),
|
|
fg_color=colors["card_inner"],
|
|
text_color=colors["text"],
|
|
height=90,
|
|
corner_radius=6
|
|
)
|
|
self.positions_text.pack(fill="both", expand=True)
|
|
self.positions_text.insert("1.0", "No open positions")
|
|
self.positions_text.configure(state="disabled")
|
|
|
|
def create_settings_card(self, parent, row, col):
|
|
"""Settings display card"""
|
|
colors = THEMES[self.current_theme]
|
|
card = ModernCard(parent, "SETTINGS", theme=self.current_theme)
|
|
card.grid(row=row, column=col, columnspan=2, padx=4, pady=4, sticky="nsew")
|
|
self.cards.append(card)
|
|
|
|
# Settings grid - use 2 columns for compact layout
|
|
settings_frame = ctk.CTkFrame(card.content, fg_color="transparent")
|
|
settings_frame.pack(fill="both", expand=True)
|
|
settings_frame.grid_columnconfigure(0, weight=1)
|
|
settings_frame.grid_columnconfigure(1, weight=1)
|
|
|
|
settings = [
|
|
("Symbol", self.config.symbol),
|
|
("Capital", f"${self.config.capital:,.2f}"),
|
|
("Max Daily Loss", f"{self.config.risk.max_daily_loss}%"),
|
|
("Risk Per Trade", f"{self.config.risk.risk_per_trade}%"),
|
|
("Min Lot", f"{self.config.risk.min_lot_size}"),
|
|
("Max Lot", f"{self.config.risk.max_lot_size}"),
|
|
("Timeframe", self.config.execution_timeframe),
|
|
("Golden Time", "19:00-23:00 WIB"),
|
|
]
|
|
|
|
self.settings_labels = [] # Store for theme updates
|
|
|
|
for i, (label, value) in enumerate(settings):
|
|
row_idx = i // 2
|
|
col_idx = i % 2
|
|
|
|
row_frame = ctk.CTkFrame(settings_frame, fg_color="transparent")
|
|
row_frame.grid(row=row_idx, column=col_idx, sticky="w", padx=5, pady=1)
|
|
|
|
lbl = ctk.CTkLabel(
|
|
row_frame,
|
|
text=f"{label}:",
|
|
font=ctk.CTkFont(size=10),
|
|
text_color=colors["text_secondary"],
|
|
anchor="w"
|
|
)
|
|
lbl.pack(side="left")
|
|
|
|
val_lbl = ctk.CTkLabel(
|
|
row_frame,
|
|
text=f" {value}",
|
|
font=ctk.CTkFont(size=10, weight="bold"),
|
|
text_color=colors["text"],
|
|
anchor="w"
|
|
)
|
|
val_lbl.pack(side="left")
|
|
|
|
self.settings_labels.append((lbl, val_lbl))
|
|
|
|
def create_log_card(self, parent, row, col):
|
|
"""Activity log card"""
|
|
colors = THEMES[self.current_theme]
|
|
card = ModernCard(parent, "ACTIVITY LOG", theme=self.current_theme)
|
|
card.grid(row=row, column=col, columnspan=2, padx=4, pady=4, sticky="nsew")
|
|
self.cards.append(card)
|
|
|
|
self.log_text = ctk.CTkTextbox(
|
|
card.content,
|
|
font=ctk.CTkFont(family="Consolas", size=10),
|
|
fg_color=colors["card_inner"],
|
|
text_color=colors["green"],
|
|
height=100,
|
|
corner_radius=6
|
|
)
|
|
self.log_text.pack(fill="both", expand=True)
|
|
|
|
def create_status_bar(self):
|
|
"""Create status bar"""
|
|
colors = THEMES[self.current_theme]
|
|
|
|
self.status_frame = ctk.CTkFrame(self, fg_color=colors["card"], height=28, corner_radius=0)
|
|
self.status_frame.pack(fill="x", side="bottom")
|
|
self.status_frame.pack_propagate(False)
|
|
|
|
self.status_label = ctk.CTkLabel(
|
|
self.status_frame,
|
|
text="Initializing...",
|
|
font=ctk.CTkFont(size=10),
|
|
text_color=colors["text_secondary"]
|
|
)
|
|
self.status_label.pack(side="left", padx=12, pady=4)
|
|
|
|
self.update_label = ctk.CTkLabel(
|
|
self.status_frame,
|
|
text="Last update: --:--:--",
|
|
font=ctk.CTkFont(size=10),
|
|
text_color=colors["text_muted"]
|
|
)
|
|
self.update_label.pack(side="right", padx=12, pady=4)
|
|
|
|
def log(self, message):
|
|
"""Add message to activity log"""
|
|
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
self.log_text.insert("end", f"[{timestamp}] {message}\n")
|
|
self.log_text.see("end")
|
|
|
|
def connect_systems(self):
|
|
"""Connect to MT5 and initialize components"""
|
|
colors = THEMES[self.current_theme]
|
|
self.log("Connecting to MT5...")
|
|
|
|
try:
|
|
self.mt5 = MT5Connector(
|
|
login=self.config.mt5_login,
|
|
password=self.config.mt5_password,
|
|
server=self.config.mt5_server,
|
|
path=self.config.mt5_path,
|
|
)
|
|
if not self.mt5.connect():
|
|
self.log("ERROR: Failed to connect to MT5!")
|
|
self.connection_label.configure(text="● Disconnected", text_color=colors["red"])
|
|
return
|
|
|
|
self.log("MT5 connected successfully!")
|
|
self.connection_label.configure(text="● Connected", text_color=colors["green"])
|
|
|
|
# Initialize components
|
|
self.smc = SMCAnalyzer()
|
|
self.ml = TradingModel(model_path="models/xgboost_model")
|
|
self.ml.load()
|
|
self.hmm = MarketRegimeDetector(model_path="models/hmm_regime")
|
|
self.hmm.load()
|
|
self.session = SessionFilter()
|
|
self.feature_eng = FeatureEngineer()
|
|
|
|
self.log("All components initialized")
|
|
if self.ml.fitted:
|
|
self.log(f"ML Model loaded ({len(self.ml.feature_names)} features)")
|
|
|
|
self.running = True
|
|
|
|
# Start update thread
|
|
update_thread = threading.Thread(target=self.update_loop, daemon=True)
|
|
update_thread.start()
|
|
|
|
except Exception as e:
|
|
self.log(f"ERROR: {e}")
|
|
self.connection_label.configure(text="● Error", text_color=colors["red"])
|
|
|
|
def update_loop(self):
|
|
"""Main update loop"""
|
|
while self.running:
|
|
try:
|
|
self.after(0, self.update_data)
|
|
time.sleep(1)
|
|
except Exception as e:
|
|
self.after(0, lambda: self.log(f"Update error: {e}"))
|
|
time.sleep(5)
|
|
|
|
def update_data(self):
|
|
"""Fetch and update all data"""
|
|
if not self.mt5:
|
|
return
|
|
|
|
# Update time
|
|
wib = ZoneInfo("Asia/Jakarta")
|
|
now = datetime.now(wib)
|
|
self.time_label.configure(text=now.strftime("%H:%M:%S WIB"))
|
|
|
|
# Check golden time (use theme colors)
|
|
colors = THEMES[self.current_theme]
|
|
is_golden = 19 <= now.hour < 23
|
|
if is_golden:
|
|
self.golden_frame.configure(fg_color=colors["highlight"])
|
|
self.golden_label.configure(text="GOLDEN TIME: YES", text_color=colors["green"])
|
|
else:
|
|
self.golden_frame.configure(fg_color=colors["highlight_off"])
|
|
self.golden_label.configure(text="GOLDEN TIME: NO", text_color=colors["text_secondary"])
|
|
|
|
# Update price
|
|
try:
|
|
tick = self.mt5.get_tick(self.config.symbol)
|
|
if tick:
|
|
price = (tick.bid + tick.ask) / 2
|
|
spread = (tick.ask - tick.bid) * 100
|
|
self.price_label.configure(text=f"{price:.2f}")
|
|
self.spread_label.configure(text=f"Spread: {spread:.1f} pips")
|
|
except:
|
|
pass
|
|
|
|
# Update account
|
|
try:
|
|
balance = self.mt5.account_balance
|
|
equity = self.mt5.account_equity
|
|
profit = equity - balance
|
|
self.balance_label.configure(text=f"${balance:,.2f}")
|
|
self.equity_label.configure(text=f"${equity:,.2f}")
|
|
|
|
if profit >= 0:
|
|
self.pl_label.configure(text=f"+${profit:.2f}", text_color=colors["green"])
|
|
else:
|
|
self.pl_label.configure(text=f"-${abs(profit):.2f}", text_color=colors["red"])
|
|
except:
|
|
pass
|
|
|
|
# Update session
|
|
try:
|
|
session_info = self.session.get_status_report()
|
|
if session_info:
|
|
self.session_label.configure(text=session_info.get('current_session', 'Unknown'))
|
|
can_trade, reason, _ = self.session.can_trade()
|
|
if can_trade:
|
|
self.can_trade_label.configure(text="Can Trade: YES", text_color=colors["green"])
|
|
else:
|
|
self.can_trade_label.configure(text="Can Trade: NO", text_color=colors["red"])
|
|
except:
|
|
pass
|
|
|
|
# Get market data and update signals
|
|
try:
|
|
df = self.mt5.get_market_data(self.config.symbol, self.config.execution_timeframe, 500)
|
|
if df is not None and len(df) > 100:
|
|
self.update_signals(df)
|
|
except:
|
|
pass
|
|
|
|
# Update positions
|
|
try:
|
|
positions = self.mt5.get_open_positions(self.config.symbol)
|
|
self.update_positions(positions)
|
|
except:
|
|
pass
|
|
|
|
# Update risk state
|
|
self.update_risk_state()
|
|
|
|
# Update status
|
|
self.last_update = datetime.now()
|
|
self.update_label.configure(text=f"Last update: {self.last_update.strftime('%H:%M:%S')}")
|
|
self.status_label.configure(text="Running...")
|
|
|
|
def update_signals(self, df):
|
|
"""Update SMC, ML, and Regime signals"""
|
|
colors = THEMES[self.current_theme]
|
|
|
|
# Build complete features
|
|
df = self.feature_eng.calculate_all(df, include_ml_features=True)
|
|
df = self.smc.calculate_all(df)
|
|
|
|
# Regime
|
|
try:
|
|
df = self.hmm.predict(df)
|
|
regime = self.hmm.get_current_state(df)
|
|
if regime:
|
|
regime_name = regime.regime.value.replace('_', ' ').title()
|
|
self.regime_label.configure(text=regime_name)
|
|
self.volatility_label.configure(text=f"{regime.volatility:.2f}")
|
|
self.regime_conf_label.configure(text=f"{regime.confidence:.0%}")
|
|
|
|
if "HIGH" in regime.regime.value:
|
|
self.regime_label.configure(text_color=colors["red"])
|
|
elif "LOW" in regime.regime.value:
|
|
self.regime_label.configure(text_color=colors["green"])
|
|
else:
|
|
self.regime_label.configure(text_color=colors["orange"])
|
|
except:
|
|
pass
|
|
|
|
# SMC Signal
|
|
try:
|
|
smc_signal = self.smc.generate_signal(df)
|
|
if smc_signal:
|
|
self.smc_signal_label.configure(text=smc_signal.signal_type)
|
|
self.smc_confidence_bar.set(smc_signal.confidence)
|
|
self.smc_conf_label.configure(text=f"Confidence: {smc_signal.confidence:.0%}")
|
|
self.smc_reason_label.configure(text=smc_signal.reason[:60] + "..." if len(smc_signal.reason) > 60 else smc_signal.reason)
|
|
|
|
if smc_signal.signal_type == "BUY":
|
|
self.smc_signal_label.configure(text_color=colors["green"])
|
|
self.smc_confidence_bar.configure(progress_color=colors["green"])
|
|
else:
|
|
self.smc_signal_label.configure(text_color=colors["red"])
|
|
self.smc_confidence_bar.configure(progress_color=colors["red"])
|
|
else:
|
|
self.smc_signal_label.configure(text="NO SIGNAL", text_color=colors["text_muted"])
|
|
self.smc_confidence_bar.set(0)
|
|
self.smc_conf_label.configure(text="Confidence: --%")
|
|
self.smc_reason_label.configure(text="Waiting for setup...")
|
|
except:
|
|
pass
|
|
|
|
# ML Prediction
|
|
try:
|
|
if self.ml.fitted:
|
|
available_features = [f for f in self.ml.feature_names if f in df.columns]
|
|
ml_pred = self.ml.predict(df, available_features)
|
|
if ml_pred:
|
|
self.ml_signal_label.configure(text=ml_pred.signal)
|
|
self.ml_confidence_bar.set(ml_pred.confidence)
|
|
self.ml_conf_label.configure(text=f"Confidence: {ml_pred.confidence:.0%}")
|
|
|
|
buy_prob = ml_pred.probability
|
|
sell_prob = 1.0 - buy_prob
|
|
self.ml_prob_label.configure(text=f"Buy: {buy_prob:.0%} | Sell: {sell_prob:.0%}")
|
|
|
|
if ml_pred.signal == "BUY":
|
|
self.ml_signal_label.configure(text_color=colors["green"])
|
|
self.ml_confidence_bar.configure(progress_color=colors["green"])
|
|
elif ml_pred.signal == "SELL":
|
|
self.ml_signal_label.configure(text_color=colors["red"])
|
|
self.ml_confidence_bar.configure(progress_color=colors["red"])
|
|
else:
|
|
self.ml_signal_label.configure(text_color=colors["orange"])
|
|
self.ml_confidence_bar.configure(progress_color=colors["orange"])
|
|
except:
|
|
pass
|
|
|
|
def update_positions(self, positions):
|
|
"""Update positions display"""
|
|
self.positions_text.configure(state="normal")
|
|
self.positions_text.delete("1.0", "end")
|
|
|
|
if positions is None or positions.is_empty():
|
|
self.positions_text.insert("1.0", "No open positions")
|
|
else:
|
|
for row in positions.iter_rows(named=True):
|
|
ticket = row.get('ticket', 'N/A')
|
|
pos_type = "BUY" if row.get('type', 0) == 0 else "SELL"
|
|
volume = row.get('volume', 0)
|
|
profit = row.get('profit', 0)
|
|
price_open = row.get('price_open', 0)
|
|
|
|
line = f"#{ticket} | {pos_type} {volume} @ {price_open:.2f} | P/L: ${profit:+.2f}\n"
|
|
self.positions_text.insert("end", line)
|
|
|
|
self.positions_text.configure(state="disabled")
|
|
|
|
def update_risk_state(self):
|
|
"""Update risk state from file"""
|
|
try:
|
|
risk_file = Path("data/risk_state.txt")
|
|
if risk_file.exists():
|
|
content = risk_file.read_text()
|
|
lines = content.strip().split('\n')
|
|
|
|
for line in lines:
|
|
if ':' in line:
|
|
key, value = line.split(':', 1)
|
|
key = key.strip()
|
|
value = value.strip()
|
|
|
|
if key == 'daily_loss':
|
|
self.daily_loss_label.configure(text=f"${float(value):,.2f}")
|
|
elif key == 'daily_profit':
|
|
self.daily_profit_label.configure(text=f"${float(value):,.2f}")
|
|
elif key == 'consecutive_losses':
|
|
self.consec_loss_label.configure(text=value)
|
|
except:
|
|
pass
|
|
|
|
def on_closing(self):
|
|
"""Handle window close"""
|
|
self.running = False
|
|
if self.mt5:
|
|
self.mt5.disconnect()
|
|
self.destroy()
|
|
|
|
|
|
def main():
|
|
app = TradingDashboard()
|
|
app.protocol("WM_DELETE_WINDOW", app.on_closing)
|
|
app.mainloop()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|